Line data Source code
1 : // Copyright The OpenTelemetry Authors
2 : // SPDX-License-Identifier: Apache-2.0
3 :
4 : import 'dart:convert';
5 :
6 : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'
7 : show IdGenerator;
8 : import 'package:protobuf/protobuf.dart';
9 :
10 : import '../../proto/logs/v1/logs.pb.dart' show SeverityNumber;
11 : import '../../proto/metrics/v1/metrics.pb.dart' show AggregationTemporality;
12 : import '../../proto/trace/v1/trace.pb.dart'
13 : show Span_SpanKind, Status_StatusCode;
14 :
15 : /// OTLP/JSON encoding of an OTLP request message.
16 : ///
17 : /// OTLP/JSON is proto3-JSON **with explicit deviations** — the one that bites
18 : /// is ID encoding: the OTLP specification requires `traceId`, `spanId`, and
19 : /// `parentSpanId` to be **hex-encoded** strings (case-insensitive), NOT the
20 : /// proto3-JSON default base64 for `bytes` fields. See
21 : /// opentelemetry-proto's JSON Protobuf Encoding notes.
22 : ///
23 : /// `toProto3Json()` alone therefore produces payloads that strict OTLP
24 : /// receivers reject — the OpenTelemetry Collector enforces hex from
25 : /// contrib ~0.15x ("ID.UnmarshalJSONIter: length mismatch", HTTP 400); older
26 : /// collectors happened to tolerate base64, which is how this survived in the
27 : /// wild until the first strict endpoint (Dartastic Cloud, 2026-07-10).
28 : ///
29 : /// The second deviation, same origin story: the OTLP spec requires enum
30 : /// fields to be encoded as their **integer** values, while `toProto3Json()`
31 : /// emits proto3-JSON's default enum **names** (`"SPAN_KIND_SERVER"`).
32 : /// Lenient receivers accept both; the spec (and the engine wire-parity
33 : /// harness in dartastic-pro#140, which found this) says integers.
34 : ///
35 : /// This helper converts the proto3-JSON tree, hex-encoding every ID field
36 : /// wherever it appears (spans, span links, log records, metric exemplars)
37 : /// and int-encoding every enum field (span `kind`, status `code`, log
38 : /// `severityNumber`, metric `aggregationTemporality`).
39 4 : Object? otlpProto3JsonWithHexIds(GeneratedMessage request) =>
40 8 : _fixupOtlpJson(request.toProto3Json());
41 :
42 : const _idKeys = {'traceId', 'spanId', 'parentSpanId'};
43 :
44 : /// Enum fields keyed by their JSON field name, each with its own
45 : /// name→int table built from the generated protos (drift-proof) and
46 : /// guarded by the enum's name prefix so an attribute value that merely
47 : /// resembles an enum name can never be corrupted (attribute values live
48 : /// under `stringValue` keys, never these).
49 12 : final Map<String, Map<String, int>> _enumFields = {
50 20 : 'kind': {for (final v in Span_SpanKind.values) v.name: v.value},
51 20 : 'code': {for (final v in Status_StatusCode.values) v.name: v.value},
52 20 : 'severityNumber': {for (final v in SeverityNumber.values) v.name: v.value},
53 4 : 'aggregationTemporality': {
54 16 : for (final v in AggregationTemporality.values) v.name: v.value,
55 : },
56 : };
57 :
58 4 : Object? _fixupOtlpJson(Object? node) {
59 4 : if (node is Map) {
60 4 : return <String, Object?>{
61 4 : for (final entry in node.entries)
62 20 : entry.key as String: _fixupValue(entry.key as String, entry.value),
63 : };
64 : }
65 4 : if (node is List) {
66 12 : return <Object?>[for (final item in node) _fixupOtlpJson(item)];
67 : }
68 : return node;
69 : }
70 :
71 4 : Object? _fixupValue(String key, Object? value) {
72 6 : if (_idKeys.contains(key) && value is String) {
73 2 : return _base64ToHex(value);
74 : }
75 8 : final enumTable = _enumFields[key];
76 4 : if (enumTable != null && value is String) {
77 : // Defensive like _base64ToHex: an unknown name passes through
78 : // unchanged (never corrupt a payload we don't understand).
79 4 : return enumTable[value] ?? value;
80 : }
81 4 : return _fixupOtlpJson(value);
82 : }
83 :
84 : /// Base64 → lowercase hex, via the same codec that formats
85 : /// `TraceId.hexString`/`SpanId.hexString`. Defensive: a value that doesn't
86 : /// parse as base64 is returned unchanged (never corrupt a payload we don't
87 : /// understand).
88 2 : String _base64ToHex(String b64) {
89 : try {
90 4 : return IdGenerator.bytesToHex(base64.decode(b64));
91 0 : } on FormatException {
92 : return b64;
93 : }
94 : }
|