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 Baggage specification for context propagation.
8 : ///
9 : /// This propagator handles the extraction and injection of baggage information
10 : /// following the W3C Baggage specification as defined at:
11 : /// https://www.w3.org/TR/baggage/
12 : ///
13 : /// Baggage allows for propagating key-value pairs alongside the trace context
14 : /// across service boundaries. This enables the correlation of related telemetry
15 : /// using application-specific or domain-specific properties.
16 : class W3CBaggagePropagator
17 : implements TextMapPropagator<Map<String, String>, String> {
18 : /// The standard header name for W3C baggage as defined in the specification
19 : static const _baggageHeader = 'baggage';
20 :
21 : /// Extracts baggage information from the carrier and updates the context.
22 : ///
23 : /// This method parses the W3C baggage header and creates a new baggage
24 : /// context to return as part of the updated Context.
25 : ///
26 : /// @param context The current context
27 : /// @param carrier The carrier containing the baggage header
28 : /// @param getter The getter used to extract values from the carrier
29 : /// @return A new Context with the extracted baggage
30 1 : @override
31 : Context extract(Context context, Map<String, String> carrier,
32 : TextMapGetter<String> getter) {
33 1 : final value = getter.get(_baggageHeader);
34 2 : OTelLog.debug('Extracting baggage: $value');
35 1 : if (value == null || value.isEmpty) {
36 : // Return context with empty baggage instead of original context
37 0 : return OTel.context();
38 : }
39 :
40 1 : final entries = <String, BaggageEntry>{};
41 1 : final pairs = value.split(',');
42 2 : for (final pair in pairs) {
43 1 : final trimmedPair = pair.trim();
44 1 : if (trimmedPair.isEmpty) continue;
45 :
46 1 : final keyValue = trimmedPair.split('=');
47 2 : if (keyValue.length != 2) continue;
48 :
49 3 : final key = _decodeComponent(keyValue[0].trim());
50 1 : if (key.isEmpty) continue;
51 :
52 2 : final valueAndMetadata = keyValue[1].split(';');
53 3 : final value = _decodeComponent(valueAndMetadata[0].trim());
54 : String? metadata;
55 2 : if (valueAndMetadata.length > 1) {
56 3 : metadata = valueAndMetadata.sublist(1).join(';').trim();
57 : }
58 :
59 2 : entries[key] = OTel.baggageEntry(value, metadata);
60 : }
61 :
62 1 : final baggage = OTel.baggage(entries);
63 1 : return context.withBaggage(baggage);
64 : }
65 :
66 : /// Injects baggage from the context into the carrier.
67 : ///
68 : /// This method serializes the baggage from the context into the
69 : /// W3C baggage header format and adds it to the carrier.
70 : ///
71 : /// @param context The context containing baggage to be injected
72 : /// @param carrier The carrier to inject the baggage header into
73 : /// @param setter The setter used to add values to the carrier
74 1 : @override
75 : void inject(Context context, Map<String, String> carrier,
76 : TextMapSetter<String> setter) {
77 1 : if (OTelLog.isDebug()) {
78 2 : OTelLog.debug('Injecting baggage. Context: $context');
79 : }
80 1 : final contextBaggage = context.baggage;
81 : if (contextBaggage != null) {
82 1 : if (OTelLog.isDebug()) {
83 1 : OTelLog.debug(
84 2 : 'Context baggage: $contextBaggage (${contextBaggage.runtimeType})');
85 : }
86 :
87 : final baggage = contextBaggage;
88 1 : final entries = baggage.getAllEntries();
89 3 : if (OTelLog.isDebug()) OTelLog.debug('Baggage entries: $entries');
90 :
91 1 : if (entries.isEmpty) {
92 2 : if (OTelLog.isDebug()) OTelLog.debug('Empty baggage entries');
93 : return;
94 : }
95 :
96 3 : final serializedEntries = entries.entries.map((entry) {
97 2 : final key = _encodeComponent(entry.key);
98 3 : final value = _encodeComponent(entry.value.value);
99 2 : final metadata = entry.value.metadata;
100 1 : if (OTelLog.isDebug()) {
101 1 : OTelLog.debug(
102 1 : 'Processing entry - Key: $key, Value: $value, Metadata: $metadata');
103 : }
104 1 : if (metadata != null && metadata.isNotEmpty) {
105 1 : return '$key=$value;$metadata';
106 : }
107 1 : return '$key=$value';
108 1 : }).join(',');
109 :
110 1 : if (OTelLog.isDebug()) {
111 2 : OTelLog.debug('Setting baggage header to: $serializedEntries');
112 : }
113 1 : if (serializedEntries.isNotEmpty) {
114 1 : setter.set(_baggageHeader, serializedEntries);
115 : }
116 : }
117 : }
118 :
119 : /// Returns the list of propagation fields used by this propagator.
120 : ///
121 : /// @return A list containing the baggage header name
122 0 : @override
123 : List<String> fields() => const [_baggageHeader];
124 :
125 : /// Encodes a component for use in the baggage header.
126 : ///
127 : /// @param value The value to encode
128 : /// @return The encoded value
129 1 : String _encodeComponent(String value) {
130 1 : return Uri.encodeComponent(value)
131 1 : .replaceAll('%20', '+')
132 1 : .replaceAll('*', '%2A');
133 : }
134 :
135 : /// Decodes a component from the baggage header.
136 : ///
137 : /// @param value The value to decode
138 : /// @return The decoded value
139 1 : String _decodeComponent(String value) {
140 2 : return Uri.decodeComponent(value.replaceAll('+', '%20'));
141 : }
142 : }
|