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 : /// OTLP/JSON encoding of an OTLP request message.
11 : ///
12 : /// OTLP/JSON is proto3-JSON **with explicit deviations** — the one that bites
13 : /// is ID encoding: the OTLP specification requires `traceId`, `spanId`, and
14 : /// `parentSpanId` to be **hex-encoded** strings (case-insensitive), NOT the
15 : /// proto3-JSON default base64 for `bytes` fields. See
16 : /// opentelemetry-proto's JSON Protobuf Encoding notes.
17 : ///
18 : /// `toProto3Json()` alone therefore produces payloads that strict OTLP
19 : /// receivers reject — the OpenTelemetry Collector enforces hex from
20 : /// contrib ~0.15x ("ID.UnmarshalJSONIter: length mismatch", HTTP 400); older
21 : /// collectors happened to tolerate base64, which is how this survived in the
22 : /// wild until the first strict endpoint (Dartastic Cloud, 2026-07-10).
23 : ///
24 : /// This helper converts the proto3-JSON tree, hex-encoding every ID field
25 : /// wherever it appears (spans, span links, log records, metric exemplars).
26 4 : Object? otlpProto3JsonWithHexIds(GeneratedMessage request) =>
27 8 : _hexifyIds(request.toProto3Json());
28 :
29 : const _idKeys = {'traceId', 'spanId', 'parentSpanId'};
30 :
31 4 : Object? _hexifyIds(Object? node) {
32 4 : if (node is Map) {
33 4 : return <String, Object?>{
34 4 : for (final entry in node.entries)
35 8 : entry.key as String:
36 12 : _idKeys.contains(entry.key) && entry.value is String
37 4 : ? _base64ToHex(entry.value as String)
38 8 : : _hexifyIds(entry.value),
39 : };
40 : }
41 4 : if (node is List) {
42 12 : return <Object?>[for (final item in node) _hexifyIds(item)];
43 : }
44 : return node;
45 : }
46 :
47 : /// Base64 → lowercase hex, via the same codec that formats
48 : /// `TraceId.hexString`/`SpanId.hexString`. Defensive: a value that doesn't
49 : /// parse as base64 is returned unchanged (never corrupt a payload we don't
50 : /// understand).
51 2 : String _base64ToHex(String b64) {
52 : try {
53 4 : return IdGenerator.bytesToHex(base64.decode(b64));
54 0 : } on FormatException {
55 : return b64;
56 : }
57 : }
|