Skip to content

Commit 7b11971

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 6f8c077 commit 7b11971

File tree

3 files changed

+198
-0
lines changed

3 files changed

+198
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Added `py.typed` file to every package. This should resolve a bunch of mypy
1111
errors for users.
1212
([#1720](https://github.com/open-telemetry/opentelemetry-python/pull/1720))
13+
- Added experimental HTTP back propagators.
14+
([#1762](https://github.com/open-telemetry/opentelemetry-python/pull/1762))
1315

1416
### Changed
1517
- Adjust `B3Format` propagator to be spec compliant by not modifying context
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
import typing
16+
17+
import opentelemetry.trace as trace
18+
from opentelemetry.context.context import Context
19+
from opentelemetry.propagators import textmap
20+
from opentelemetry.trace import format_span_id, format_trace_id
21+
22+
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"
23+
_BACK_PROPAGATOR = None
24+
25+
26+
def get_global_back_propagator():
27+
return _BACK_PROPAGATOR
28+
29+
30+
def set_global_back_propagator(propagator):
31+
global _BACK_PROPAGATOR # pylint:disable=global-statement
32+
_BACK_PROPAGATOR = propagator
33+
34+
35+
class DictHeaderSetter:
36+
def set(self, carrier, key, value): # pylint: disable=no-self-use
37+
old_value = carrier.get(key, "")
38+
if old_value:
39+
value = "{0}, {1}".format(old_value, value)
40+
carrier[key] = value
41+
42+
43+
default_setter = DictHeaderSetter()
44+
45+
46+
class TraceResponsePropagator:
47+
"""Experimental propagator that injects tracecontext into HTTP responses."""
48+
49+
_HEADER_NAME = "traceresponse"
50+
51+
def _format_header(self, span_context): # pylint: disable=no-self-use
52+
return "00-{trace_id}-{span_id}-{:02x}".format(
53+
span_context.trace_flags,
54+
trace_id=format_trace_id(span_context.trace_id),
55+
span_id=format_span_id(span_context.span_id),
56+
)
57+
58+
def inject(
59+
self,
60+
carrier: textmap.CarrierT,
61+
context: typing.Optional[Context] = None,
62+
setter: textmap.Setter = default_setter,
63+
) -> None:
64+
"""Injects SpanContext into the HTTP response carrier."""
65+
span = trace.get_current_span(context)
66+
span_context = span.get_span_context()
67+
if span_context == trace.INVALID_SPAN_CONTEXT:
68+
return
69+
70+
setter.set(
71+
carrier, self._HEADER_NAME, self._format_header(span_context)
72+
)
73+
setter.set(
74+
carrier,
75+
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
76+
self._HEADER_NAME,
77+
)
78+
79+
80+
class ServerTimingPropagator(TraceResponsePropagator):
81+
82+
_HEADER_NAME = "Server-Timing"
83+
84+
def _format_header(self, span_context):
85+
return 'traceparent;desc="00-{trace_id}-{span_id}-{:02x}"'.format(
86+
span_context.trace_flags,
87+
trace_id=format_trace_id(span_context.trace_id),
88+
span_id=format_span_id(span_context.span_id),
89+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
31+
def test_get_set(self):
32+
original = propagators._BACK_PROPAGATOR
33+
34+
propagators._BACK_PROPAGATOR = None
35+
self.assertIsNone(get_global_back_propagator())
36+
37+
prop = TraceResponsePropagator()
38+
set_global_back_propagator(prop)
39+
self.assertIs(prop, get_global_back_propagator())
40+
41+
propagators._BACK_PROPAGATOR = original
42+
43+
44+
class TestDictHeaderSetter(TestBase):
45+
def test_simple(self):
46+
setter = DictHeaderSetter()
47+
carrier = {}
48+
setter.set(carrier, "kk", "vv")
49+
self.assertIn("kk", carrier)
50+
self.assertEqual(carrier["kk"], "vv")
51+
52+
def test_append(self):
53+
setter = DictHeaderSetter()
54+
carrier = {"kk": "old"}
55+
setter.set(carrier, "kk", "vv")
56+
self.assertIn("kk", carrier)
57+
self.assertEqual(carrier["kk"], "old, vv")
58+
59+
60+
class TestTraceResponsePropagator(TestBase):
61+
def test_inject(self):
62+
span = trace.NonRecordingSpan(
63+
trace.SpanContext(
64+
trace_id=1,
65+
span_id=2,
66+
is_remote=False,
67+
trace_flags=trace.DEFAULT_TRACE_OPTIONS,
68+
trace_state=trace.DEFAULT_TRACE_STATE,
69+
),
70+
)
71+
72+
ctx = trace.set_span_in_context(span)
73+
prop = TraceResponsePropagator()
74+
carrier = {}
75+
prop.inject(carrier, ctx)
76+
self.assertEqual(
77+
carrier["Access-Control-Expose-Headers"], "traceresponse"
78+
)
79+
self.assertEqual(
80+
carrier["traceresponse"],
81+
"00-00000000000000000000000000000001-0000000000000002-00",
82+
)
83+
84+
85+
class TestServerTimingPropagator(TestBase):
86+
def test_inject(self):
87+
span = trace.NonRecordingSpan(
88+
trace.SpanContext(
89+
trace_id=1,
90+
span_id=2,
91+
is_remote=False,
92+
trace_flags=trace.DEFAULT_TRACE_OPTIONS,
93+
trace_state=trace.DEFAULT_TRACE_STATE,
94+
),
95+
)
96+
97+
ctx = trace.set_span_in_context(span)
98+
prop = ServerTimingPropagator()
99+
carrier = {}
100+
prop.inject(carrier, ctx)
101+
self.assertEqual(
102+
carrier["Access-Control-Expose-Headers"], "Server-Timing"
103+
)
104+
self.assertEqual(
105+
carrier["Server-Timing"],
106+
'traceparent;desc="00-00000000000000000000000000000001-0000000000000002-00"',
107+
)

0 commit comments

Comments
 (0)