Skip to content

Commit 6bd163f

Browse files
authored
Added experimental HTTP backpropagators (#1762)
1 parent e2a5b0b commit 6bd163f

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-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,128 @@
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 context
17+
into response carriers. 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+
_RESPONSE_PROPAGATOR = None
36+
37+
38+
def get_global_response_propagator():
39+
return _RESPONSE_PROPAGATOR
40+
41+
42+
def set_global_response_propagator(propagator):
43+
global _RESPONSE_PROPAGATOR # pylint:disable=global-statement
44+
_RESPONSE_PROPAGATOR = propagator
45+
46+
47+
class Setter(ABC):
48+
@abstractmethod
49+
def set(self, carrier, key, value):
50+
"""Inject the provided key value pair in carrier."""
51+
52+
53+
class DictHeaderSetter(Setter):
54+
def set(self, carrier, key, value): # pylint: disable=no-self-use
55+
old_value = carrier.get(key, "")
56+
if old_value:
57+
value = "{0}, {1}".format(old_value, value)
58+
carrier[key] = value
59+
60+
61+
class FuncSetter(Setter):
62+
"""FuncSetter coverts a function into a valid Setter. Any function that can
63+
set values in a carrier can be converted into a Setter by using FuncSetter.
64+
This is useful when injecting trace context into non-dict objects such
65+
HTTP Response objects for different framework.
66+
67+
For example, it can be used to create a setter for Falcon response object as:
68+
69+
setter = FuncSetter(falcon.api.Response.append_header)
70+
71+
and then used with the propagator as:
72+
73+
propagator.inject(falcon_response, setter=setter)
74+
75+
This would essentially make the propagator call `falcon_response.append_header(key, value)`
76+
"""
77+
78+
def __init__(self, func):
79+
self._func = func
80+
81+
def set(self, carrier, key, value):
82+
self._func(carrier, key, value)
83+
84+
85+
default_setter = DictHeaderSetter()
86+
87+
88+
class ResponsePropagator(ABC):
89+
@abstractmethod
90+
def inject(
91+
self,
92+
carrier: textmap.CarrierT,
93+
context: typing.Optional[Context] = None,
94+
setter: textmap.Setter = default_setter,
95+
) -> None:
96+
"""Injects SpanContext into the HTTP response carrier."""
97+
98+
99+
class TraceResponsePropagator(ResponsePropagator):
100+
"""Experimental propagator that injects tracecontext into HTTP responses."""
101+
102+
def inject(
103+
self,
104+
carrier: textmap.CarrierT,
105+
context: typing.Optional[Context] = None,
106+
setter: textmap.Setter = default_setter,
107+
) -> None:
108+
"""Injects SpanContext into the HTTP response carrier."""
109+
span = trace.get_current_span(context)
110+
span_context = span.get_span_context()
111+
if span_context == trace.INVALID_SPAN_CONTEXT:
112+
return
113+
114+
header_name = "traceresponse"
115+
setter.set(
116+
carrier,
117+
header_name,
118+
"00-{trace_id}-{span_id}-{:02x}".format(
119+
span_context.trace_flags,
120+
trace_id=format_trace_id(span_context.trace_id),
121+
span_id=format_span_id(span_context.span_id),
122+
),
123+
)
124+
setter.set(
125+
carrier,
126+
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
127+
header_name,
128+
)
+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
TraceResponsePropagator,
22+
get_global_response_propagator,
23+
set_global_response_propagator,
24+
)
25+
from opentelemetry.test.test_base import TestBase
26+
27+
28+
class TestGlobals(TestBase):
29+
def test_get_set(self):
30+
original = propagators._RESPONSE_PROPAGATOR
31+
32+
propagators._RESPONSE_PROPAGATOR = None
33+
self.assertIsNone(get_global_response_propagator())
34+
35+
prop = TraceResponsePropagator()
36+
set_global_response_propagator(prop)
37+
self.assertIs(prop, get_global_response_propagator())
38+
39+
propagators._RESPONSE_PROPAGATOR = original
40+
41+
42+
class TestDictHeaderSetter(TestBase):
43+
def test_simple(self):
44+
setter = DictHeaderSetter()
45+
carrier = {}
46+
setter.set(carrier, "kk", "vv")
47+
self.assertIn("kk", carrier)
48+
self.assertEqual(carrier["kk"], "vv")
49+
50+
def test_append(self):
51+
setter = DictHeaderSetter()
52+
carrier = {"kk": "old"}
53+
setter.set(carrier, "kk", "vv")
54+
self.assertIn("kk", carrier)
55+
self.assertEqual(carrier["kk"], "old, vv")
56+
57+
58+
class TestTraceResponsePropagator(TestBase):
59+
def test_inject(self):
60+
span = trace.NonRecordingSpan(
61+
trace.SpanContext(
62+
trace_id=1,
63+
span_id=2,
64+
is_remote=False,
65+
trace_flags=trace.DEFAULT_TRACE_OPTIONS,
66+
trace_state=trace.DEFAULT_TRACE_STATE,
67+
),
68+
)
69+
70+
ctx = trace.set_span_in_context(span)
71+
prop = TraceResponsePropagator()
72+
carrier = {}
73+
prop.inject(carrier, ctx)
74+
self.assertEqual(
75+
carrier["Access-Control-Expose-Headers"], "traceresponse"
76+
)
77+
self.assertEqual(
78+
carrier["traceresponse"],
79+
"00-00000000000000000000000000000001-0000000000000002-00",
80+
)

0 commit comments

Comments
 (0)