LCOV - code coverage report
Current view: top level - src/trace - w3c_trace_context_propagator.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 85.6 % 97 83
Test Date: 2025-11-15 13:23:01 Functions: - 0 0

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

Generated by: LCOV version 2.0-1