1# Copyright The OpenTelemetry Authors
2# SPDX-License-Identifier: Apache-2.0
3#
4import logging
5import re
6
7from opentelemetry import trace
8from opentelemetry.context.context import Context
9from opentelemetry.propagators import textmap
10from opentelemetry.trace import format_span_id, format_trace_id
11from opentelemetry.trace.span import (
12 _TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH,
13 TraceState,
14)
15
16_logger = logging.getLogger(__name__)
17
18
19class TraceContextTextMapPropagator(textmap.TextMapPropagator):
20 """Extracts and injects using w3c TraceContext's headers."""
21
22 _TRACEPARENT_HEADER_NAME = "traceparent"
23 _TRACESTATE_HEADER_NAME = "tracestate"
24 _TRACEPARENT_HEADER_FORMAT = "^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})" + "(-.*)?[ \t]*$"
25 _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT)
26
27 def extract(
28 self,
29 carrier: textmap.CarrierT,
30 context: Context | None = None,
31 getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
32 ) -> Context:
33 """Extracts SpanContext from the carrier.
34
35 See `opentelemetry.propagators.textmap.TextMapPropagator.extract`
36 """
37 if context is None:
38 context = Context()
39
40 header = getter.get(carrier, self._TRACEPARENT_HEADER_NAME)
41
42 if not header:
43 return context
44
45 match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0])
46 if not match:
47 return context
48
49 version: str = match.group(1)
50 trace_id: str = match.group(2)
51 span_id: str = match.group(3)
52 trace_flags: str = match.group(4)
53
54 if trace_id == "0" * 32 or span_id == "0" * 16:
55 return context
56
57 if version == "00":
58 if match.group(5): # type: ignore
59 return context
60 if version == "ff":
61 return context
62
63 tracestate_headers = getter.get(carrier, self._TRACESTATE_HEADER_NAME)
64 if tracestate_headers is None:
65 tracestate = None
66 elif (
67 # multiple header fields are combined with "," (RFC 9110, section 5.3)
68 sum(len(header) for header in tracestate_headers) + len(tracestate_headers) - 1
69 > _TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH
70 ):
71 _logger.warning(
72 "tracestate header exceeded the maximum length of %d characters",
73 _TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH,
74 )
75 tracestate = None
76 else:
77 tracestate = TraceState.from_header(tracestate_headers)
78
79 span_context = trace.SpanContext(
80 trace_id=int(trace_id, 16),
81 span_id=int(span_id, 16),
82 is_remote=True,
83 trace_flags=trace.TraceFlags(int(trace_flags, 16)),
84 trace_state=tracestate,
85 )
86 return trace.set_span_in_context(trace.NonRecordingSpan(span_context), context)
87
88 def inject(
89 self,
90 carrier: textmap.CarrierT,
91 context: Context | None = None,
92 setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
93 ) -> None:
94 """Injects SpanContext into the carrier.
95
96 See `opentelemetry.propagators.textmap.TextMapPropagator.inject`
97 """
98 span = trace.get_current_span(context)
99 span_context = span.get_span_context()
100 if span_context == trace.INVALID_SPAN_CONTEXT:
101 return
102 traceparent_string = f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}"
103 setter.set(carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string)
104 if span_context.trace_state:
105 tracestate_string = span_context.trace_state.to_header()
106 setter.set(carrier, self._TRACESTATE_HEADER_NAME, tracestate_string)
107
108 @property
109 def fields(self) -> set[str]:
110 """Returns a set with the fields set in `inject`.
111
112 See
113 `opentelemetry.propagators.textmap.TextMapPropagator.fields`
114 """
115 return {self._TRACEPARENT_HEADER_NAME, self._TRACESTATE_HEADER_NAME}