Skip to content

Commit 092dcf3

Browse files
committed
Added experimental HTTP backpropagators
The experimental back propagators inject trace response headers into HTTP responses. These are meant to be used by instrumentations and are not considered stable.
1 parent 71e3a7a commit 092dcf3

File tree

3 files changed

+234
-0
lines changed

3 files changed

+234
-0
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Added `SpanKind` to `should_sample` parameters, suggest using parent span context's tracestate
1414
instead of manually passed in tracestate in `should_sample`
1515
([#1764](https://github.com/open-telemetry/opentelemetry-python/pull/1764))
16+
- Added experimental HTTP back propagators.
17+
([#1762](https://github.com/open-telemetry/opentelemetry-python/pull/1762))
1618

1719
### Changed
1820
- Adjust `B3Format` propagator to be spec compliant by not modifying context
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
This module implements experimental propagators to inject trace response context
17+
into HTTP responses. This is useful for server side frameworks that start traces
18+
when server requests and want to share the trace context with the client so the
19+
client can add it's spans to the same trace.
20+
21+
This is part of an upcoming W3C spec and will eventually make it to the Otel spec.
22+
23+
https://w3c.github.io/trace-context/#trace-context-http-response-headers-format
24+
"""
25+
26+
import typing
27+
from abc import ABC, abstractmethod
28+
29+
import opentelemetry.trace as trace
30+
from opentelemetry.context.context import Context
31+
from opentelemetry.propagators import textmap
32+
from opentelemetry.trace import format_span_id, format_trace_id
33+
34+
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"
35+
_BACK_PROPAGATOR = None
36+
37+
38+
def get_global_back_propagator():
39+
return _BACK_PROPAGATOR
40+
41+
42+
def set_global_back_propagator(propagator):
43+
global _BACK_PROPAGATOR # pylint:disable=global-statement
44+
_BACK_PROPAGATOR = propagator
45+
46+
47+
class DictHeaderSetter:
48+
def set(self, carrier, key, value): # pylint: disable=no-self-use
49+
old_value = carrier.get(key, "")
50+
if old_value:
51+
value = "{0}, {1}".format(old_value, value)
52+
carrier[key] = value
53+
54+
55+
default_setter = DictHeaderSetter()
56+
57+
58+
class FuncSetter:
59+
def __init__(self, func):
60+
self._func = func
61+
62+
def set(self, carrier, key, value):
63+
self._func(carrier, key, value)
64+
65+
66+
class ResponsePropagator(ABC):
67+
68+
@abstractmethod
69+
def _header_name(self) -> str:
70+
pass
71+
72+
@abstractmethod
73+
def _format_header(self, span_context) -> str:
74+
pass
75+
76+
def inject(
77+
self,
78+
carrier: textmap.CarrierT,
79+
context: typing.Optional[Context] = None,
80+
setter: textmap.Setter = default_setter,
81+
) -> None:
82+
"""Injects SpanContext into the HTTP response carrier."""
83+
span = trace.get_current_span(context)
84+
span_context = span.get_span_context()
85+
if span_context == trace.INVALID_SPAN_CONTEXT:
86+
return
87+
88+
header_name = self._header_name()
89+
setter.set(
90+
carrier, header_name, self._format_header(span_context)
91+
)
92+
setter.set(
93+
carrier,
94+
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
95+
header_name,
96+
)
97+
98+
99+
class TraceResponsePropagator(ResponsePropagator):
100+
"""Experimental propagator that injects tracecontext into HTTP responses."""
101+
102+
def _header_name(self) -> str: # pylint: disable=no-self-use
103+
return "traceresponse"
104+
105+
def _format_header(
106+
self, span_context
107+
) -> str: # pylint: disable=no-self-use
108+
return "00-{trace_id}-{span_id}-{:02x}".format(
109+
span_context.trace_flags,
110+
trace_id=format_trace_id(span_context.trace_id),
111+
span_id=format_span_id(span_context.span_id),
112+
)
113+
114+
115+
class ServerTimingPropagator(ResponsePropagator):
116+
117+
118+
def _header_name(self) -> str: # pylint: disable=no-self-use
119+
return "Server-Timing"
120+
121+
def _format_header(self, span_context) -> str: # pylint: disable=no-self-use
122+
return 'traceparent;desc="00-{trace_id}-{span_id}-{:02x}"'.format(
123+
span_context.trace_flags,
124+
trace_id=format_trace_id(span_context.trace_id),
125+
span_id=format_span_id(span_context.span_id),
126+
)
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# pylint: disable=protected-access
16+
17+
from opentelemetry import trace
18+
from opentelemetry.instrumentation import propagators
19+
from opentelemetry.instrumentation.propagators import (
20+
DictHeaderSetter,
21+
ServerTimingPropagator,
22+
TraceResponsePropagator,
23+
get_global_back_propagator,
24+
set_global_back_propagator,
25+
)
26+
from opentelemetry.test.test_base import TestBase
27+
28+
29+
class TestGlobals(TestBase):
30+
def test_get_set(self):
31+
original = propagators._BACK_PROPAGATOR
32+
33+
propagators._BACK_PROPAGATOR = None
34+
self.assertIsNone(get_global_back_propagator())
35+
36+
prop = TraceResponsePropagator()
37+
set_global_back_propagator(prop)
38+
self.assertIs(prop, get_global_back_propagator())
39+
40+
propagators._BACK_PROPAGATOR = original
41+
42+
43+
class TestDictHeaderSetter(TestBase):
44+
def test_simple(self):
45+
setter = DictHeaderSetter()
46+
carrier = {}
47+
setter.set(carrier, "kk", "vv")
48+
self.assertIn("kk", carrier)
49+
self.assertEqual(carrier["kk"], "vv")
50+
51+
def test_append(self):
52+
setter = DictHeaderSetter()
53+
carrier = {"kk": "old"}
54+
setter.set(carrier, "kk", "vv")
55+
self.assertIn("kk", carrier)
56+
self.assertEqual(carrier["kk"], "old, vv")
57+
58+
59+
class TestTraceResponsePropagator(TestBase):
60+
def test_inject(self):
61+
span = trace.NonRecordingSpan(
62+
trace.SpanContext(
63+
trace_id=1,
64+
span_id=2,
65+
is_remote=False,
66+
trace_flags=trace.DEFAULT_TRACE_OPTIONS,
67+
trace_state=trace.DEFAULT_TRACE_STATE,
68+
),
69+
)
70+
71+
ctx = trace.set_span_in_context(span)
72+
prop = TraceResponsePropagator()
73+
carrier = {}
74+
prop.inject(carrier, ctx)
75+
self.assertEqual(
76+
carrier["Access-Control-Expose-Headers"], "traceresponse"
77+
)
78+
self.assertEqual(
79+
carrier["traceresponse"],
80+
"00-00000000000000000000000000000001-0000000000000002-00",
81+
)
82+
83+
84+
class TestServerTimingPropagator(TestBase):
85+
def test_inject(self):
86+
span = trace.NonRecordingSpan(
87+
trace.SpanContext(
88+
trace_id=1,
89+
span_id=2,
90+
is_remote=False,
91+
trace_flags=trace.DEFAULT_TRACE_OPTIONS,
92+
trace_state=trace.DEFAULT_TRACE_STATE,
93+
),
94+
)
95+
96+
ctx = trace.set_span_in_context(span)
97+
prop = ServerTimingPropagator()
98+
carrier = {}
99+
prop.inject(carrier, ctx)
100+
self.assertEqual(
101+
carrier["Access-Control-Expose-Headers"], "Server-Timing"
102+
)
103+
self.assertEqual(
104+
carrier["Server-Timing"],
105+
'traceparent;desc="00-00000000000000000000000000000001-0000000000000002-00"',
106+
)

0 commit comments

Comments
 (0)