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