LCOV - code coverage report
Current view: top level - lib/src/export - otlp_json.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 96.0 % 25 24
Test Date: 2026-08-27 23:42:02 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.0-1