Skip to content
This repository was archived by the owner on Oct 13, 2021. It is now read-only.

Commit 1c66a1a

Browse files
implement w3c and b3 context propagators
This commit implements new http propagators in other to be compatible with OpenTelemetry instrumented apps: - w3c: default context propagator in opentelemetry - b3: another choice tracer.configure() was extended to accept a new parameter, "http_propagator", this is a factory function that will be used to get new instances of the propagator to be used. The following is an example of how to set b3 as the propagator: ... from oteltrace.propagation.b3 import B3HTTPPropagator oteltrace.tracer.configure( http_propagator=B3HTTPPropagator ) ... Users of oteltrace-run can choose the propagator to be used by setting the OTEL_TRACER_PROPAGATOR env variable to one of the current supported values: w3c, b3 and datadog.
1 parent dcf7fb3 commit 1c66a1a

File tree

15 files changed

+657
-223
lines changed

15 files changed

+657
-223
lines changed

docs/advanced_usage.rst

+10-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ To trace requests across hosts, the spans on the secondary hosts must be linked
2626
- On the server side, it means to read propagated attributes and set them to the active tracing context.
2727
- On the client side, it means to propagate the attributes, commonly as a header/metadata.
2828

29-
`oteltrace` already provides default propagators but you can also implement your own.
29+
`oteltrace` already provides default propagators (``w3c``, ``b3`` and ``datadog``) but you can also implement your own.
3030

3131
Web Frameworks
3232
^^^^^^^^^^^^^^
@@ -69,8 +69,8 @@ on the other side, the metadata is retrieved and the trace can continue.
6969
To propagate the tracing information, HTTP headers are used to transmit the
7070
required metadata to piece together the trace.
7171

72-
.. autoclass:: oteltrace.propagation.http.HTTPPropagator
73-
:members:
72+
:func:`oteltrace.propagation.http.HTTPPropagator` returns an instance of the configured
73+
propagator.
7474

7575
Custom
7676
^^^^^^
@@ -477,6 +477,13 @@ The specific configuration for each type of exporter is defined by using the
477477
The text after ``OTEL_EXPORTER_OPTIONS_`` will be passed to
478478
``OTEL_EXPORTER_FACTORY`` as kwargs.
479479

480+
Propagator Configuration
481+
^^^^^^^^^^^^^^^^^^^^^^^^
482+
483+
``oteltrace-run`` supports different formats to distribute the trace context.
484+
The propagator used is defined by the ``OTEL_TRACER_PROPAGATOR`` env variable.
485+
Currently ``w3c`` (default), ``b3`` and ``datadog`` are supported.
486+
480487
``oteltrace-run`` respects a variety of common entrypoints for web applications:
481488

482489
- ``oteltrace-run python my_app.py``

oteltrace/bootstrap/sitecustomize.py

+25
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import logging
1010
import importlib
1111

12+
from oteltrace.propagation.datadog import DatadogHTTPPropagator
13+
from oteltrace.propagation.w3c import W3CHTTPPropagator
14+
from oteltrace.propagation.b3 import B3HTTPPropagator
15+
1216
from oteltrace import api_otel_exporter
1317

1418
from oteltrace.utils.formats import asbool, get_env
@@ -116,6 +120,25 @@ def load_otel_exporter():
116120
return None
117121

118122

123+
OTEL_TRACER_PROPAGATOR = 'OTEL_TRACER_PROPAGATOR'
124+
OTEL_TRACER_PROPAGATOR_W3C = 'w3c'
125+
OTEL_TRACER_PROPAGATOR_B3 = 'b3'
126+
OTEL_TRACER_PROPAGATOR_DATADOG = 'datadog'
127+
OTEL_TRACER_PROPAGATOR_DEFAULT = OTEL_TRACER_PROPAGATOR_W3C
128+
129+
OTEL_TRACER_PROPAGATOR_MAP = {
130+
OTEL_TRACER_PROPAGATOR_W3C: W3CHTTPPropagator,
131+
OTEL_TRACER_PROPAGATOR_B3: B3HTTPPropagator,
132+
OTEL_TRACER_PROPAGATOR_DATADOG: DatadogHTTPPropagator,
133+
}
134+
135+
136+
def get_http_propagator_factory():
137+
"""Returns an http propagator factory based on set env variables"""
138+
prop = os.getenv(OTEL_TRACER_PROPAGATOR, OTEL_TRACER_PROPAGATOR_DEFAULT)
139+
return OTEL_TRACER_PROPAGATOR_MAP[prop.lower()]
140+
141+
119142
try:
120143
from oteltrace import tracer
121144
patch = True
@@ -137,6 +160,8 @@ def load_otel_exporter():
137160

138161
opts['api'] = api_otel_exporter.APIOtel(exporter=load_otel_exporter())
139162

163+
opts['http_propagator'] = get_http_propagator_factory()
164+
140165
if opts:
141166
tracer.configure(**opts)
142167

oteltrace/propagation/b3.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from ..context import Context
2+
from ..ext import priority
3+
4+
5+
class B3HTTPPropagator:
6+
"""b3 compatible propagator"""
7+
8+
SINGLE_HEADER_KEY = 'b3'
9+
TRACE_ID_KEY = 'x-b3-traceid'
10+
SPAN_ID_KEY = 'x-b3-spanid'
11+
SAMPLED_KEY = 'x-b3-sampled'
12+
FLAGS_KEY = 'x-b3-flags'
13+
_SAMPLE_PROPAGATE_VALUES = set(['1', 'True', 'true', 'd'])
14+
15+
_SAMPLING_PRIORITY_MAP = {
16+
priority.USER_REJECT: '0',
17+
priority.AUTO_REJECT: '0',
18+
priority.AUTO_KEEP: '1',
19+
priority.USER_KEEP: '1',
20+
}
21+
22+
def inject(self, span_context, headers):
23+
# TODO: what should be a default value?
24+
sampled = '0'
25+
if span_context.sampling_priority is not None:
26+
sampled = self._SAMPLING_PRIORITY_MAP[span_context.sampling_priority]
27+
28+
headers[self.TRACE_ID_KEY] = format_trace_id(span_context.trace_id)
29+
headers[self.SPAN_ID_KEY] = format_span_id(span_context.span_id)
30+
headers[self.SAMPLED_KEY] = sampled
31+
32+
def extract(self, headers):
33+
trace_id = '0'
34+
span_id = '0'
35+
sampled = '0'
36+
flags = None
37+
38+
single_header = headers.get(self.SINGLE_HEADER_KEY)
39+
40+
if single_header:
41+
# The b3 spec calls for the sampling state to be
42+
# "deferred", which is unspecified. This concept does not
43+
# translate to SpanContext, so we set it as recorded.
44+
sampled = '1'
45+
fields = single_header.split('-', 4)
46+
47+
if len(fields) == 1:
48+
sampled = fields[0]
49+
elif len(fields) == 2:
50+
trace_id, span_id = fields
51+
elif len(fields) == 3:
52+
trace_id, span_id, sampled = fields
53+
elif len(fields) == 4:
54+
trace_id, span_id, sampled, _parent_span_id = fields
55+
else:
56+
return Context()
57+
else:
58+
trace_id = headers.get(self.TRACE_ID_KEY) or trace_id
59+
span_id = headers.get(self.SPAN_ID_KEY) or span_id
60+
sampled = headers.get(self.SAMPLED_KEY) or sampled
61+
flags = headers.get(self.FLAGS_KEY) or flags
62+
63+
if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == '1':
64+
sampling_priority = priority.AUTO_KEEP
65+
else:
66+
sampling_priority = priority.AUTO_REJECT
67+
68+
return Context(
69+
trace_id=int(trace_id, 16),
70+
span_id=int(span_id, 16),
71+
sampling_priority=sampling_priority,
72+
)
73+
74+
75+
def format_trace_id(trace_id: int) -> str:
76+
"""Format the trace id according to b3 specification."""
77+
return format(trace_id, '032x')
78+
79+
80+
def format_span_id(span_id: int) -> str:
81+
"""Format the span id according to b3 specification."""
82+
return format(span_id, '016x')

oteltrace/propagation/datadog.py

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from ..context import Context
2+
from ..internal.logger import get_logger
3+
4+
from .utils import get_wsgi_header
5+
6+
log = get_logger(__name__)
7+
8+
# HTTP headers one should set for distributed tracing.
9+
# These are cross-language (eg: Python, Go and other implementations should honor these)
10+
HTTP_HEADER_TRACE_ID = 'x-datadog-trace-id'
11+
HTTP_HEADER_PARENT_ID = 'x-datadog-parent-id'
12+
HTTP_HEADER_SAMPLING_PRIORITY = 'x-datadog-sampling-priority'
13+
HTTP_HEADER_ORIGIN = 'x-datadog-origin'
14+
15+
16+
# Note that due to WSGI spec we have to also check for uppercased and prefixed
17+
# versions of these headers
18+
POSSIBLE_HTTP_HEADER_TRACE_IDS = frozenset(
19+
[HTTP_HEADER_TRACE_ID, get_wsgi_header(HTTP_HEADER_TRACE_ID)]
20+
)
21+
POSSIBLE_HTTP_HEADER_PARENT_IDS = frozenset(
22+
[HTTP_HEADER_PARENT_ID, get_wsgi_header(HTTP_HEADER_PARENT_ID)]
23+
)
24+
POSSIBLE_HTTP_HEADER_SAMPLING_PRIORITIES = frozenset(
25+
[HTTP_HEADER_SAMPLING_PRIORITY, get_wsgi_header(HTTP_HEADER_SAMPLING_PRIORITY)]
26+
)
27+
POSSIBLE_HTTP_HEADER_ORIGIN = frozenset(
28+
[HTTP_HEADER_ORIGIN, get_wsgi_header(HTTP_HEADER_ORIGIN)]
29+
)
30+
31+
32+
class DatadogHTTPPropagator(object):
33+
"""A HTTP Propagator using HTTP headers as carrier."""
34+
35+
def inject(self, span_context, headers):
36+
"""Inject Context attributes that have to be propagated as HTTP headers.
37+
38+
Here is an example using `requests`::
39+
40+
import requests
41+
from oteltrace.propagation.http import DatadogHTTPPropagator
42+
43+
def parent_call():
44+
with tracer.trace('parent_span') as span:
45+
headers = {}
46+
propagator = DatadogHTTPPropagator()
47+
propagator.inject(span.context, headers)
48+
url = '<some RPC endpoint>'
49+
r = requests.get(url, headers=headers)
50+
51+
:param Context span_context: Span context to propagate.
52+
:param dict headers: HTTP headers to extend with tracing attributes.
53+
"""
54+
headers[HTTP_HEADER_TRACE_ID] = str(span_context.trace_id)
55+
headers[HTTP_HEADER_PARENT_ID] = str(span_context.span_id)
56+
sampling_priority = span_context.sampling_priority
57+
# Propagate priority only if defined
58+
if sampling_priority is not None:
59+
headers[HTTP_HEADER_SAMPLING_PRIORITY] = str(span_context.sampling_priority)
60+
# Propagate origin only if defined
61+
if span_context._otel_origin is not None:
62+
headers[HTTP_HEADER_ORIGIN] = str(span_context._otel_origin)
63+
64+
@staticmethod
65+
def extract_header_value(possible_header_names, headers, default=None):
66+
for header, value in headers.items():
67+
for header_name in possible_header_names:
68+
if header.lower() == header_name.lower():
69+
return value
70+
71+
return default
72+
73+
@staticmethod
74+
def extract_trace_id(headers):
75+
return int(
76+
DatadogHTTPPropagator.extract_header_value(
77+
POSSIBLE_HTTP_HEADER_TRACE_IDS, headers, default=0,
78+
)
79+
)
80+
81+
@staticmethod
82+
def extract_parent_span_id(headers):
83+
return int(
84+
DatadogHTTPPropagator.extract_header_value(
85+
POSSIBLE_HTTP_HEADER_PARENT_IDS, headers, default=0,
86+
)
87+
)
88+
89+
@staticmethod
90+
def extract_sampling_priority(headers):
91+
return DatadogHTTPPropagator.extract_header_value(
92+
POSSIBLE_HTTP_HEADER_SAMPLING_PRIORITIES, headers,
93+
)
94+
95+
@staticmethod
96+
def extract_origin(headers):
97+
return DatadogHTTPPropagator.extract_header_value(
98+
POSSIBLE_HTTP_HEADER_ORIGIN, headers,
99+
)
100+
101+
def extract(self, headers):
102+
"""Extract a Context from HTTP headers into a new Context.
103+
104+
Here is an example from a web endpoint::
105+
106+
from oteltrace.propagation.http import DatadogHTTPPropagator
107+
108+
def my_controller(url, headers):
109+
propagator = DatadogHTTPPropagator()
110+
context = propagator.extract(headers)
111+
tracer.context_provider.activate(context)
112+
113+
with tracer.trace('my_controller') as span:
114+
span.set_meta('http.url', url)
115+
116+
:param dict headers: HTTP headers to extract tracing attributes.
117+
:return: New `Context` with propagated attributes.
118+
"""
119+
if not headers:
120+
return Context()
121+
122+
try:
123+
trace_id = DatadogHTTPPropagator.extract_trace_id(headers)
124+
parent_span_id = DatadogHTTPPropagator.extract_parent_span_id(headers)
125+
sampling_priority = DatadogHTTPPropagator.extract_sampling_priority(headers)
126+
origin = DatadogHTTPPropagator.extract_origin(headers)
127+
128+
if sampling_priority is not None:
129+
sampling_priority = int(sampling_priority)
130+
131+
return Context(
132+
trace_id=trace_id,
133+
span_id=parent_span_id,
134+
sampling_priority=sampling_priority,
135+
_otel_origin=origin,
136+
)
137+
# If headers are invalid and cannot be parsed, return a new context and log the issue.
138+
except Exception as error:
139+
try:
140+
log.debug(
141+
'invalid x-datadog-* headers, trace-id: %s, parent-id: %s, priority: %s, origin: %s, error: %s',
142+
headers.get(HTTP_HEADER_TRACE_ID, 0),
143+
headers.get(HTTP_HEADER_PARENT_ID, 0),
144+
headers.get(HTTP_HEADER_SAMPLING_PRIORITY),
145+
headers.get(HTTP_HEADER_ORIGIN, ''),
146+
error,
147+
)
148+
# We might fail on string formatting errors ; in that case only format the first error
149+
except Exception:
150+
log.debug(error)
151+
return Context()

0 commit comments

Comments
 (0)