Skip to content

Commit af7aa98

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 af7aa98

File tree

6 files changed

+211
-6
lines changed

6 files changed

+211
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
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

opentelemetry-instrumentation/setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@
2424
with open(VERSION_FILENAME) as f:
2525
exec(f.read(), PACKAGE_INFO)
2626

27-
setuptools.setup(version=PACKAGE_INFO["__version__"],)
27+
setuptools.setup(
28+
version=PACKAGE_INFO["__version__"],
29+
)
Lines changed: 106 additions & 0 deletions
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+
"""
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+
_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 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+
@abstractmethod
68+
def inject(
69+
self,
70+
carrier: textmap.CarrierT,
71+
context: typing.Optional[Context] = None,
72+
setter: textmap.Setter = default_setter,
73+
) -> None:
74+
"""Injects SpanContext into the HTTP response carrier."""
75+
76+
77+
class TraceResponsePropagator(ResponsePropagator):
78+
"""Experimental propagator that injects tracecontext into HTTP responses."""
79+
80+
def inject(
81+
self,
82+
carrier: textmap.CarrierT,
83+
context: typing.Optional[Context] = None,
84+
setter: textmap.Setter = default_setter,
85+
) -> None:
86+
"""Injects SpanContext into the HTTP response carrier."""
87+
span = trace.get_current_span(context)
88+
span_context = span.get_span_context()
89+
if span_context == trace.INVALID_SPAN_CONTEXT:
90+
return
91+
92+
header_name = "traceresponse"
93+
setter.set(
94+
carrier,
95+
header_name,
96+
"00-{trace_id}-{span_id}-{:02x}".format(
97+
span_context.trace_flags,
98+
trace_id=format_trace_id(span_context.trace_id),
99+
span_id=format_span_id(span_context.span_id),
100+
),
101+
)
102+
setter.set(
103+
carrier,
104+
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
105+
header_name,
106+
)

opentelemetry-instrumentation/tests/test_bootstrap.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323

2424

2525
def sample_packages(packages, rate):
26-
sampled = sample(list(packages), int(len(packages) * rate),)
26+
sampled = sample(
27+
list(packages),
28+
int(len(packages) * rate),
29+
)
2730
return {k: v for k, v in packages.items() if k in sampled}
2831

2932

Lines changed: 80 additions & 0 deletions
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+
)

opentelemetry-instrumentation/tests/test_utils.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,24 @@ def test_http_status_to_status_code(self):
3131
(HTTPStatus.UNAUTHORIZED, StatusCode.ERROR),
3232
(HTTPStatus.FORBIDDEN, StatusCode.ERROR),
3333
(HTTPStatus.NOT_FOUND, StatusCode.ERROR),
34-
(HTTPStatus.UNPROCESSABLE_ENTITY, StatusCode.ERROR,),
35-
(HTTPStatus.TOO_MANY_REQUESTS, StatusCode.ERROR,),
34+
(
35+
HTTPStatus.UNPROCESSABLE_ENTITY,
36+
StatusCode.ERROR,
37+
),
38+
(
39+
HTTPStatus.TOO_MANY_REQUESTS,
40+
StatusCode.ERROR,
41+
),
3642
(HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR),
3743
(HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR),
38-
(HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR,),
39-
(HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, StatusCode.ERROR,),
44+
(
45+
HTTPStatus.GATEWAY_TIMEOUT,
46+
StatusCode.ERROR,
47+
),
48+
(
49+
HTTPStatus.HTTP_VERSION_NOT_SUPPORTED,
50+
StatusCode.ERROR,
51+
),
4052
(600, StatusCode.ERROR),
4153
(99, StatusCode.ERROR),
4254
):

0 commit comments

Comments
 (0)