LCOV - code coverage report
Current view: top level - lib/src/trace - w3c_trace_context_propagator.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 90.8 % 98 89
Test Date: 2026-07-11 13:35:19 Functions: - 0 0

            Line data    Source code
       1              : // Copyright The OpenTelemetry Authors
       2              : // SPDX-License-Identifier: Apache-2.0
       3              : 
       4              : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
       5              : 
       6              : import '../otel.dart';
       7              : 
       8              : /// Implementation of the W3C Trace Context specification for context propagation.
       9              : ///
      10              : /// This propagator handles the extraction and injection of trace context information
      11              : /// following the W3C Trace Context specification as defined at:
      12              : /// https://www.w3.org/TR/trace-context/
      13              : ///
      14              : /// The traceparent header contains:
      15              : /// - version (2 hex digits)
      16              : /// - trace-id (32 hex digits)
      17              : /// - parent-id/span-id (16 hex digits)
      18              : /// - trace-flags (2 hex digits)
      19              : ///
      20              : /// Example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
      21              : ///
      22              : /// The tracestate header is optional and contains vendor-specific trace information
      23              : /// as a comma-separated list of key=value pairs.
      24              : class W3CTraceContextPropagator
      25              :     implements TextMapPropagator<Map<String, String>, String> {
      26              :   /// The standard header name for W3C trace parent
      27              :   static const _traceparentHeader = 'traceparent';
      28              : 
      29              :   /// The standard header name for W3C trace state
      30              :   static const _tracestateHeader = 'tracestate';
      31              : 
      32              :   /// The current version of the W3C Trace Context specification
      33              :   static const _version = '00';
      34              : 
      35              :   /// The length of a valid traceparent header value
      36              :   static const _traceparentLength = 55; // 00-{32}-{16}-{2}
      37              : 
      38            5 :   @override
      39              :   Context extract(
      40              :     Context context,
      41              :     Map<String, String> carrier,
      42              :     TextMapGetter<String> getter,
      43              :   ) {
      44            5 :     final traceparent = getter.get(_traceparentHeader);
      45              : 
      46            5 :     if (OTelLog.isDebug()) {
      47           10 :       OTelLog.debug('Extracting traceparent: $traceparent');
      48              :     }
      49              : 
      50            5 :     if (traceparent == null || traceparent.isEmpty) {
      51              :       return context;
      52              :     }
      53              : 
      54              :     // Parse the traceparent header
      55            5 :     final spanContext = _parseTraceparent(traceparent);
      56              :     if (spanContext == null) {
      57            5 :       if (OTelLog.isDebug()) {
      58            5 :         OTelLog.debug('Invalid traceparent format, skipping extraction');
      59              :       }
      60              :       return context;
      61              :     }
      62              : 
      63              :     // Extract tracestate if present
      64            4 :     final tracestate = getter.get(_tracestateHeader);
      65              :     var finalSpanContext = spanContext;
      66              : 
      67            4 :     if (tracestate != null && tracestate.isNotEmpty) {
      68            3 :       final tracestateMap = _parseTracestate(tracestate);
      69            3 :       if (tracestateMap.isNotEmpty) {
      70            3 :         finalSpanContext = spanContext.withTraceState(
      71            3 :           OTel.traceState(tracestateMap),
      72              :         );
      73              :       }
      74              :     }
      75              : 
      76            4 :     if (OTelLog.isDebug()) {
      77            8 :       OTelLog.debug('Extracted span context: $finalSpanContext');
      78              :     }
      79              : 
      80            4 :     return context.withSpanContext(finalSpanContext);
      81              :   }
      82              : 
      83            3 :   @override
      84              :   void inject(
      85              :     Context context,
      86              :     Map<String, String> carrier,
      87              :     TextMapSetter<String> setter,
      88              :   ) {
      89            3 :     final spanContext = context.spanContext;
      90              : 
      91            3 :     if (OTelLog.isDebug()) {
      92            6 :       OTelLog.debug('Injecting span context: $spanContext');
      93              :     }
      94              : 
      95            3 :     if (spanContext == null || !spanContext.isValid) {
      96            2 :       if (OTelLog.isDebug()) {
      97            2 :         OTelLog.debug('No valid span context to inject');
      98              :       }
      99              :       return;
     100              :     }
     101              : 
     102              :     // Build traceparent header: version-traceId-spanId-traceFlags
     103            3 :     final traceparent = '$_version-'
     104            6 :         '${spanContext.traceId.hexString}-'
     105            6 :         '${spanContext.spanId.hexString}-'
     106            3 :         '${spanContext.traceFlags}';
     107              : 
     108            3 :     setter.set(_traceparentHeader, traceparent);
     109              : 
     110            3 :     if (OTelLog.isDebug()) {
     111            6 :       OTelLog.debug('Injected traceparent: $traceparent');
     112              :     }
     113              : 
     114              :     // Inject tracestate if present
     115            3 :     final traceState = spanContext.traceState;
     116            6 :     if (traceState != null && traceState.entries.isNotEmpty) {
     117            3 :       final tracestateValue = _serializeTracestate(traceState);
     118            3 :       setter.set(_tracestateHeader, tracestateValue);
     119              : 
     120            3 :       if (OTelLog.isDebug()) {
     121            6 :         OTelLog.debug('Injected tracestate: $tracestateValue');
     122              :       }
     123              :     }
     124              :   }
     125              : 
     126            2 :   @override
     127              :   List<String> fields() => const [_traceparentHeader, _tracestateHeader];
     128              : 
     129              :   /// Parses a traceparent header value into a SpanContext.
     130              :   ///
     131              :   /// The traceparent format is: version-traceId-spanId-traceFlags
     132              :   /// Example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
     133              :   ///
     134              :   /// Returns null if the format is invalid.
     135            5 :   SpanContext? _parseTraceparent(String traceparent) {
     136              :     // Basic validation
     137           10 :     if (traceparent.length != _traceparentLength) {
     138            4 :       if (OTelLog.isDebug()) {
     139            4 :         OTelLog.debug(
     140            8 :           'Invalid traceparent length: ${traceparent.length}, expected $_traceparentLength',
     141              :         );
     142              :       }
     143              :       return null;
     144              :     }
     145              : 
     146            4 :     final parts = traceparent.split('-');
     147            8 :     if (parts.length != 4) {
     148            1 :       if (OTelLog.isDebug()) {
     149            1 :         OTelLog.debug(
     150            2 :           'Invalid traceparent format: expected 4 parts, got ${parts.length}',
     151              :         );
     152              :       }
     153              :       return null;
     154              :     }
     155              : 
     156            4 :     final version = parts[0];
     157            4 :     final traceIdHex = parts[1];
     158            4 :     final spanIdHex = parts[2];
     159            4 :     final traceFlagsHex = parts[3];
     160              : 
     161              :     // Validate version (currently only 00 is supported)
     162            4 :     if (version != _version) {
     163            2 :       if (OTelLog.isDebug()) {
     164            4 :         OTelLog.debug('Unsupported traceparent version: $version');
     165              :       }
     166              :       // Per spec, we should still try to parse if version is unknown
     167              :       // but for now we'll reject it
     168              :       return null;
     169              :     }
     170              : 
     171              :     // Validate trace ID length (32 hex chars = 16 bytes)
     172            8 :     if (traceIdHex.length != 32) {
     173            0 :       if (OTelLog.isDebug()) {
     174            0 :         OTelLog.debug(
     175            0 :           'Invalid trace ID length: ${traceIdHex.length}, expected 32',
     176              :         );
     177              :       }
     178              :       return null;
     179              :     }
     180              : 
     181              :     // Validate span ID length (16 hex chars = 8 bytes)
     182            8 :     if (spanIdHex.length != 16) {
     183            0 :       if (OTelLog.isDebug()) {
     184            0 :         OTelLog.debug(
     185            0 :           'Invalid span ID length: ${spanIdHex.length}, expected 16',
     186              :         );
     187              :       }
     188              :       return null;
     189              :     }
     190              : 
     191              :     // Validate trace flags length (2 hex chars = 1 byte)
     192            8 :     if (traceFlagsHex.length != 2) {
     193            0 :       if (OTelLog.isDebug()) {
     194            0 :         OTelLog.debug(
     195            0 :           'Invalid trace flags length: ${traceFlagsHex.length}, expected 2',
     196              :         );
     197              :       }
     198              :       return null;
     199              :     }
     200              : 
     201              :     try {
     202              :       // Parse the components
     203            4 :       final traceId = OTel.traceIdFrom(traceIdHex);
     204            4 :       final spanId = OTel.spanIdFrom(spanIdHex);
     205            4 :       final traceFlags = TraceFlags.fromString(traceFlagsHex);
     206              : 
     207              :       // Validate that trace ID and span ID are not all zeros
     208            4 :       if (!traceId.isValid) {
     209            2 :         if (OTelLog.isDebug()) {
     210            2 :           OTelLog.debug('Invalid trace ID: all zeros');
     211              :         }
     212              :         return null;
     213              :       }
     214              : 
     215            4 :       if (!spanId.isValid) {
     216            2 :         if (OTelLog.isDebug()) {
     217            2 :           OTelLog.debug('Invalid span ID: all zeros');
     218              :         }
     219              :         return null;
     220              :       }
     221              : 
     222              :       // Create the span context with isRemote=true since it came from a carrier
     223            4 :       return OTel.spanContext(
     224              :         traceId: traceId,
     225              :         spanId: spanId,
     226              :         traceFlags: traceFlags,
     227              :         isRemote: true,
     228              :       );
     229              :     } catch (e) {
     230            1 :       if (OTelLog.isDebug()) {
     231            2 :         OTelLog.debug('Error parsing traceparent: $e');
     232              :       }
     233              :       return null;
     234              :     }
     235              :   }
     236              : 
     237              :   /// Parses a tracestate header value into a map.
     238              :   ///
     239              :   /// The tracestate format is: key1=value1,key2=value2,...
     240              :   /// Example: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE
     241            3 :   Map<String, String> _parseTracestate(String tracestate) {
     242            3 :     final result = <String, String>{};
     243              : 
     244            3 :     if (tracestate.isEmpty) {
     245              :       return result;
     246              :     }
     247              : 
     248              :     // Split by comma and process each entry
     249            3 :     final entries = tracestate.split(',');
     250            6 :     for (final entry in entries) {
     251            3 :       final trimmedEntry = entry.trim();
     252            3 :       if (trimmedEntry.isEmpty) continue;
     253              : 
     254            3 :       final separatorIndex = trimmedEntry.indexOf('=');
     255           12 :       if (separatorIndex <= 0 || separatorIndex >= trimmedEntry.length - 1) {
     256              :         // Invalid format, skip this entry
     257            2 :         if (OTelLog.isDebug()) {
     258            4 :           OTelLog.debug('Invalid tracestate entry format: $trimmedEntry');
     259              :         }
     260              :         continue;
     261              :       }
     262              : 
     263            6 :       final key = trimmedEntry.substring(0, separatorIndex).trim();
     264            9 :       final value = trimmedEntry.substring(separatorIndex + 1).trim();
     265              : 
     266            6 :       if (key.isNotEmpty && value.isNotEmpty) {
     267            3 :         result[key] = value;
     268              :       }
     269              :     }
     270              : 
     271              :     return result;
     272              :   }
     273              : 
     274              :   /// Serializes a TraceState into a tracestate header value.
     275              :   ///
     276              :   /// The format is: key1=value1,key2=value2,...
     277            3 :   String _serializeTracestate(TraceState traceState) {
     278            3 :     final entries = traceState.entries;
     279            3 :     if (entries.isEmpty) {
     280              :       return '';
     281              :     }
     282              : 
     283           21 :     return entries.entries.map((e) => '${e.key}=${e.value}').join(',');
     284              :   }
     285              : }
        

Generated by: LCOV version 2.0-1