LCOV - code coverage report
Current view: top level - lib/src/context/propagation - w3c_baggage_propagator.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 96.3 % 81 78
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' show utf8;
       5              : 
       6              : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
       7              : 
       8              : import '../../otel.dart';
       9              : 
      10              : /// Implementation of the W3C Baggage specification for context propagation.
      11              : ///
      12              : /// This propagator handles the extraction and injection of baggage information
      13              : /// following the W3C Baggage specification as defined at:
      14              : /// https://www.w3.org/TR/baggage/
      15              : ///
      16              : /// Baggage allows for propagating key-value pairs alongside the trace context
      17              : /// across service boundaries. This enables the correlation of related telemetry
      18              : /// using application-specific or domain-specific properties.
      19              : class W3CBaggagePropagator
      20              :     implements TextMapPropagator<Map<String, String>, String> {
      21              :   /// The standard header name for W3C baggage as defined in the specification
      22              :   static const _baggageHeader = 'baggage';
      23              : 
      24              :   /// Extracts baggage information from the carrier and updates the context.
      25              :   ///
      26              :   /// This method parses the W3C baggage header and creates a new baggage
      27              :   /// context to return as part of the updated Context.
      28              :   ///
      29              :   /// @param context The current context
      30              :   /// @param carrier The carrier containing the baggage header
      31              :   /// @param getter The getter used to extract values from the carrier
      32              :   /// @return A new Context with the extracted baggage
      33            3 :   @override
      34              :   Context extract(
      35              :     Context context,
      36              :     Map<String, String> carrier,
      37              :     TextMapGetter<String> getter,
      38              :   ) {
      39            3 :     final value = getter.get(_baggageHeader);
      40            3 :     if (OTelLog.isDebug()) {
      41            6 :       OTelLog.debug('Extracting baggage: $value');
      42              :     }
      43            3 :     if (value == null || value.isEmpty) {
      44              :       // Propagators API spec: extract returns the passed context, updated
      45              :       // with extracted values — and unchanged when there is nothing to
      46              :       // extract. Returning a fresh context here would discard whatever an
      47              :       // earlier propagator in a composite (e.g. tracecontext) extracted.
      48              :       return context;
      49              :     }
      50              : 
      51            3 :     final entries = <String, BaggageEntry>{};
      52            3 :     final pairs = value.split(',');
      53            6 :     for (final pair in pairs) {
      54            3 :       final trimmedPair = pair.trim();
      55            3 :       if (trimmedPair.isEmpty) continue;
      56              : 
      57              :       // Split on the first '=' only — the W3C Baggage spec allows '='
      58              :       // inside values (e.g. base64 padding like "token=abc123==").
      59            3 :       final eqIndex = trimmedPair.indexOf('=');
      60            3 :       if (eqIndex <= 0) continue;
      61              : 
      62              :       // Keys are tokens per RFC 7230 — they are NOT percent-encoded or
      63              :       // decoded on the wire. Apply the same token rule inject applies so a
      64              :       // pass-through hop cannot lose entries (extract→inject symmetry).
      65            6 :       final key = trimmedPair.substring(0, eqIndex).trim();
      66            3 :       if (!_isValidToken(key)) continue;
      67              : 
      68            9 :       final valueAndMetadata = trimmedPair.substring(eqIndex + 1).split(';');
      69              : 
      70              :       // W3C Baggage: an unparsable list member is ignored, not fatal — one
      71              :       // malformed entry must neither break the rest of the header nor, in
      72              :       // composite propagation, prevent traceparent from being parsed.
      73              :       String value;
      74              :       try {
      75            9 :         value = _decodeValue(valueAndMetadata[0].trim());
      76              :       }
      77              :       // ignore: avoid_catching_errors
      78            1 :       on ArgumentError {
      79              :         // Uri.decodeComponent signals truncated/invalid escapes as
      80              :         // ArgumentError; W3C says skip the list member.
      81              :         continue;
      82            1 :       } on FormatException {
      83              :         continue;
      84              :       }
      85              : 
      86              :       String? metadata;
      87            6 :       if (valueAndMetadata.length > 1) {
      88           12 :         metadata = _safeDecode(valueAndMetadata.sublist(1).join(';').trim());
      89              :       }
      90              : 
      91            6 :       entries[key] = OTel.baggageEntry(value, metadata);
      92              :     }
      93              : 
      94              :     // Propagators API spec: "If a value can not be parsed from the carrier
      95              :     // for a cross-cutting concern, the implementation MUST NOT store a new
      96              :     // value in the Context, in order to preserve any previously existing
      97              :     // valid value."  An empty entry map means nothing was parsed, so we
      98              :     // return the original context untouched.
      99            3 :     if (entries.isEmpty) return context;
     100            3 :     final baggage = OTel.baggage(entries);
     101            3 :     return context.withBaggage(baggage);
     102              :   }
     103              : 
     104              :   /// Injects baggage from the context into the carrier.
     105              :   ///
     106              :   /// This method serializes the baggage from the context into the
     107              :   /// W3C baggage header format and adds it to the carrier.
     108              :   ///
     109              :   /// @param context The context containing baggage to be injected
     110              :   /// @param carrier The carrier to inject the baggage header into
     111              :   /// @param setter The setter used to add values to the carrier
     112            2 :   @override
     113              :   void inject(
     114              :     Context context,
     115              :     Map<String, String> carrier,
     116              :     TextMapSetter<String> setter,
     117              :   ) {
     118            2 :     if (OTelLog.isDebug()) {
     119            4 :       OTelLog.debug('Injecting baggage. Context: $context');
     120              :     }
     121            2 :     final contextBaggage = context.baggage;
     122              :     if (contextBaggage != null) {
     123            2 :       if (OTelLog.isDebug()) {
     124            2 :         OTelLog.debug(
     125            4 :           'Context baggage: $contextBaggage (${contextBaggage.runtimeType})',
     126              :         );
     127              :       }
     128              : 
     129              :       final baggage = contextBaggage;
     130            2 :       final entries = baggage.getAllEntries();
     131            6 :       if (OTelLog.isDebug()) OTelLog.debug('Baggage entries: $entries');
     132              : 
     133            2 :       if (entries.isEmpty) {
     134            2 :         if (OTelLog.isDebug()) OTelLog.debug('Empty baggage entries');
     135              :         return;
     136              :       }
     137              : 
     138            6 :       final serializedEntries = entries.entries.where((entry) {
     139            4 :         if (!_isValidToken(entry.key)) {
     140            1 :           if (OTelLog.isDebug()) {
     141            1 :             OTelLog.debug(
     142            2 :               'Dropping baggage entry with invalid key: ${entry.key}',
     143              :             );
     144              :           }
     145              :           return false;
     146              :         }
     147              :         return true;
     148            4 :       }).map((entry) {
     149            2 :         final key = entry.key;
     150            6 :         final value = _encodeValue(entry.value.value);
     151            4 :         final metadata = entry.value.metadata;
     152            2 :         if (OTelLog.isDebug()) {
     153            2 :           OTelLog.debug(
     154            2 :             'Processing entry - Key: $key, Value: $value, Metadata: $metadata',
     155              :           );
     156              :         }
     157              :         // Metadata is `property = token "=" *baggage-octet` on the wire and
     158              :         // free text in the API, so it must go through the same encoding as
     159              :         // values or a raw `,`/`;`/CRLF in it would forge extra list members.
     160            2 :         if (metadata != null && metadata.isNotEmpty) {
     161            4 :           return '$key=$value;${_encodeValue(metadata)}';
     162              :         }
     163            2 :         return '$key=$value';
     164            2 :       }).join(',');
     165              : 
     166            2 :       if (OTelLog.isDebug()) {
     167            4 :         OTelLog.debug('Setting baggage header to: $serializedEntries');
     168              :       }
     169            2 :       if (serializedEntries.isNotEmpty) {
     170            2 :         setter.set(_baggageHeader, serializedEntries);
     171              :       }
     172              :     }
     173              :   }
     174              : 
     175              :   /// Returns the list of propagation fields used by this propagator.
     176              :   ///
     177              :   /// @return A list containing the baggage header name
     178            0 :   @override
     179              :   List<String> fields() => const [_baggageHeader];
     180              : 
     181              :   /// Matches an RFC 7230 `token`, the grammar W3C Baggage requires for
     182              :   /// keys (and property names): `!`, `#`, `$`, `%`, `&`, `'`, `*`, `+`,
     183              :   /// `-`, `.`, `^`, `_`, `` ` ``, `|`, `~`, DIGIT, ALPHA.
     184            9 :   static final RegExp _token = RegExp(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$");
     185              : 
     186              :   /// Checks whether [key] is a valid W3C Baggage key.
     187            9 :   bool _isValidToken(String key) => _token.hasMatch(key);
     188              : 
     189              :   /// Whether [octet] is in the W3C Baggage `baggage-octet` set
     190              :   /// (`%x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E`) — i.e. printable
     191              :   /// ASCII excluding space, `"`, `,`, `;`, `\`, controls, and non-ASCII.
     192            2 :   static bool _isBaggageOctet(int octet) =>
     193            2 :       octet >= 0x21 &&
     194            2 :       octet <= 0x7E &&
     195            2 :       octet != 0x22 && // "
     196            2 :       octet != 0x2C && // ,
     197            2 :       octet != 0x3B && // ;
     198            2 :       octet != 0x5C; //   \
     199              : 
     200              :   /// Encodes a baggage value per the W3C Baggage specification.
     201              :   ///
     202              :   /// Allowlist encoding: every UTF-8 byte outside `baggage-octet`
     203              :   /// (non-ASCII included) is percent-encoded with uppercase hex. `%` itself
     204              :   /// is always emitted as `%25` even though it is a valid octet — otherwise
     205              :   /// the decoder could misread pre-existing escape sequences and the codec
     206              :   /// would not be injective (e.g. `a%2Cb` must not decode to `a,b`).
     207            2 :   String _encodeValue(String value) {
     208            2 :     final buffer = StringBuffer();
     209            4 :     for (final octet in utf8.encode(value)) {
     210            2 :       if (octet == 0x25) {
     211            1 :         buffer.write('%25');
     212            2 :       } else if (_isBaggageOctet(octet)) {
     213            2 :         buffer.writeCharCode(octet);
     214              :       } else {
     215            2 :         buffer.write(
     216            8 :           '%${octet.toRadixString(16).toUpperCase().padLeft(2, '0')}',
     217              :         );
     218              :       }
     219              :     }
     220            2 :     return buffer.toString();
     221              :   }
     222              : 
     223              :   /// Decodes a baggage value (plain percent-decoding, no form-style `+`).
     224              :   ///
     225              :   /// Throws [ArgumentError] or [FormatException] on malformed input;
     226              :   /// callers guard per entry per the W3C "ignore unparsable member" rule.
     227            3 :   String _decodeValue(String value) {
     228            3 :     return Uri.decodeComponent(value);
     229              :   }
     230              : 
     231              :   /// Best-effort decode used for metadata: returns the input unchanged when
     232              :   /// it is not valid percent-encoding, since metadata is auxiliary and must
     233              :   /// never be able to fail the surrounding entry.
     234            3 :   static String _safeDecode(String value) {
     235              :     try {
     236            3 :       return Uri.decodeComponent(value);
     237              :     }
     238              :     // ignore: avoid_catching_errors
     239            0 :     on ArgumentError {
     240              :       return value;
     241            0 :     } on FormatException {
     242              :       return value;
     243              :     }
     244              :   }
     245              : }
        

Generated by: LCOV version 2.0-1