Skip to content

Commit 10ea879

Browse files
committed
Added opt-in support to return traceresponse headers for server instrumentations.
This allows users to configure their web apps to inject trace context as headers in HTTP responses. This is useful when client side apps need to connect their spans with the server side spans e.g, in RUM products. Today the most practical way to do this is to use the Server-Timing header but in near future we might use the traceresponse header as described here: https://w3c.github.io/trace-context/#trace-context-http-response-headers-format Added trace response propagation support for: Django Falcon Flask Pyramid Tornado
1 parent 370952f commit 10ea879

File tree

14 files changed

+258
-4
lines changed

14 files changed

+258
-4
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
- 'release/*'
77
pull_request:
88
env:
9-
CORE_REPO_SHA: cad261e5dae1fe986c87e6965664b45cc9ab73c3
9+
CORE_REPO_SHA: f6b04c483f6c416e1927f010c07e71a17a5d79d0
1010

1111
jobs:
1212
build:

CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation
2121
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
2222
- `opentelemetry-instrumenation-django` now supports request and response hooks.
23-
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
23+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
24+
- `opentelemetry-instrumenation-tornado` now supports trace response headers.
25+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
26+
- `opentelemetry-instrumenation-pyramid` now supports trace response headers.
27+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
28+
- `opentelemetry-instrumenation-falcon` added trace response headers support to Falcon.
29+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
30+
- `opentelemetry-instrumenation-flask` added support for trace response headers.
31+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
2432

2533
### Removed
2634
- Remove `http.status_text` from span attributes

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121
from opentelemetry.context import attach, detach
2222
from opentelemetry.instrumentation.django.version import __version__
23+
from opentelemetry.instrumentation.propagators import (
24+
get_global_back_propagator,
25+
)
2326
from opentelemetry.instrumentation.utils import extract_attributes_from_object
2427
from opentelemetry.instrumentation.wsgi import (
2528
add_response_attributes,
@@ -179,6 +182,11 @@ def process_response(self, request, response):
179182
response,
180183
)
181184

185+
propagator = get_global_back_propagator()
186+
if propagator:
187+
propagator.inject(response)
188+
189+
# record any exceptions raised while processing the request
182190
exception = request.META.pop(self._environ_exception_key, None)
183191
if _DjangoMiddleware._otel_response_hook:
184192
_DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable

instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py

+64-1
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,19 @@
2626
DjangoInstrumentor,
2727
_DjangoMiddleware,
2828
)
29+
from opentelemetry.instrumentation.propagators import (
30+
TraceResponsePropagator,
31+
set_global_back_propagator,
32+
)
2933
from opentelemetry.sdk.trace import Span
3034
from opentelemetry.test.test_base import TestBase
3135
from opentelemetry.test.wsgitestutil import WsgiTestBase
32-
from opentelemetry.trace import SpanKind, StatusCode
36+
from opentelemetry.trace import (
37+
SpanKind,
38+
StatusCode,
39+
format_span_id,
40+
format_trace_id,
41+
)
3342
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
3443

3544
# pylint: disable=import-error
@@ -41,6 +50,7 @@
4150
route_span_name,
4251
traced,
4352
traced_template,
53+
with_response_header,
4454
)
4555

4656
DJANGO_2_2 = VERSION >= (2, 2)
@@ -52,6 +62,7 @@
5262
url(r"^excluded_arg/", excluded),
5363
url(r"^excluded_noarg/", excluded_noarg),
5464
url(r"^excluded_noarg2/", excluded_noarg2),
65+
url(r"^response_header/", with_response_header),
5566
url(r"^span_name/([0-9]{4})/$", route_span_name),
5667
]
5768
_django_instrumentor = DjangoInstrumentor()
@@ -309,3 +320,55 @@ def response_hook(span, request, response):
309320
self.assertIsInstance(response_hook_args[1], HttpRequest)
310321
self.assertIsInstance(response_hook_args[2], HttpResponse)
311322
self.assertEqual(response_hook_args[2], response)
323+
324+
def test_trace_response_headers(self):
325+
response = Client().get("/span_name/1234/")
326+
self.assertNotIn("Server-Timing", response._headers)
327+
self.memory_exporter.clear()
328+
329+
set_global_back_propagator(TraceResponsePropagator())
330+
331+
response = Client().get("/span_name/1234/")
332+
span = self.memory_exporter.get_finished_spans()[0]
333+
334+
self.assertIn("traceresponse", response._headers)
335+
self.assertEqual(
336+
response._headers["access-control-expose-headers"][0],
337+
"Access-Control-Expose-Headers",
338+
)
339+
self.assertEqual(
340+
response._headers["access-control-expose-headers"][1],
341+
"traceresponse",
342+
)
343+
self.assertEqual(
344+
response._headers["traceresponse"][0], "traceresponse"
345+
)
346+
self.assertEqual(
347+
response._headers["traceresponse"][1],
348+
"00-{0}-{1}-01".format(
349+
format_trace_id(span.get_span_context().trace_id),
350+
format_span_id(span.get_span_context().span_id),
351+
),
352+
)
353+
self.memory_exporter.clear()
354+
355+
def test_trace_response_header_pre_existing_header(self):
356+
set_global_back_propagator(TraceResponsePropagator())
357+
358+
response = Client().get("/response_header/")
359+
span = self.memory_exporter.get_finished_spans()[0]
360+
self.assertIn("traceresponse", response._headers)
361+
self.assertEqual(
362+
response._headers["access-control-expose-headers"][1],
363+
"X-Test-Header, traceresponse",
364+
)
365+
self.assertEqual(
366+
response._headers["traceresponse"][1],
367+
"abc; val=1, "
368+
+ "00-{0}-{1}-01".format(
369+
format_trace_id(span.get_span_context().trace_id),
370+
format_span_id(span.get_span_context().span_id),
371+
),
372+
)
373+
374+
self.memory_exporter.clear()

instrumentation/opentelemetry-instrumentation-django/tests/views.py

+11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from django.http import HttpResponse
22

3+
from opentelemetry.instrumentation.propagators import (
4+
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
5+
)
6+
37

48
def traced(request): # pylint: disable=unused-argument
59
return HttpResponse()
@@ -29,3 +33,10 @@ def route_span_name(
2933
request, *args, **kwargs
3034
): # pylint: disable=unused-argument
3135
return HttpResponse()
36+
37+
38+
def with_response_header(request): # pylint: disable=unused-argument
39+
response = HttpResponse()
40+
response["traceresponse"] = "abc; val=1"
41+
response[_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS] = "X-Test-Header"
42+
return response

instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def on_get(self, req, resp):
5252
from opentelemetry import context, trace
5353
from opentelemetry.instrumentation.falcon.version import __version__
5454
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
55+
from opentelemetry.instrumentation.propagators import (
56+
FuncSetter,
57+
get_global_back_propagator,
58+
)
5559
from opentelemetry.instrumentation.utils import (
5660
extract_attributes_from_object,
5761
http_status_to_status_code,
@@ -148,6 +152,8 @@ def _start_response(status, response_headers, *args, **kwargs):
148152
class _TraceMiddleware:
149153
# pylint:disable=R0201,W0613
150154

155+
back_propagation_setter = FuncSetter(falcon.api.Response.append_header)
156+
151157
def __init__(self, tracer=None, traced_request_attrs=None):
152158
self.tracer = tracer
153159
self._traced_request_attrs = _traced_request_attrs
@@ -209,3 +215,7 @@ def process_response(
209215
description=reason,
210216
)
211217
)
218+
219+
propagator = get_global_back_propagator()
220+
if propagator:
221+
propagator.inject(resp, setter=self.back_propagation_setter)

instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717
from falcon import testing
1818

1919
from opentelemetry.instrumentation.falcon import FalconInstrumentor
20+
from opentelemetry.instrumentation.propagators import (
21+
TraceResponsePropagator,
22+
get_global_back_propagator,
23+
set_global_back_propagator,
24+
)
2025
from opentelemetry.test.test_base import TestBase
21-
from opentelemetry.trace import StatusCode
26+
from opentelemetry.trace import StatusCode, format_span_id, format_trace_id
2227
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
2328

2429
from .app import make_app
@@ -192,6 +197,28 @@ def test_traced_request_attributes(self):
192197
self.assertEqual(span.attributes["query_string"], "q=abc")
193198
self.assertNotIn("not_available_attr", span.attributes)
194199

200+
def test_trace_response(self):
201+
orig = get_global_back_propagator()
202+
set_global_back_propagator(TraceResponsePropagator())
203+
204+
response = self.client().simulate_get(path="/hello?q=abc")
205+
headers = response.headers
206+
span = self.memory_exporter.get_finished_spans()[0]
207+
208+
self.assertIn("traceresponse", headers)
209+
self.assertEqual(
210+
headers["access-control-expose-headers"], "traceresponse",
211+
)
212+
self.assertEqual(
213+
headers["traceresponse"],
214+
"00-{0}-{1}-01".format(
215+
format_trace_id(span.get_span_context().trace_id),
216+
format_span_id(span.get_span_context().span_id),
217+
),
218+
)
219+
220+
set_global_back_propagator(orig)
221+
195222
def test_traced_not_recording(self):
196223
mock_tracer = Mock()
197224
mock_span = Mock()

instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ def hello():
5555
from opentelemetry import context, trace
5656
from opentelemetry.instrumentation.flask.version import __version__
5757
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
58+
from opentelemetry.instrumentation.propagators import (
59+
get_global_back_propagator,
60+
)
5861
from opentelemetry.propagate import extract
5962
from opentelemetry.util._time import _time_ns
6063
from opentelemetry.util.http import get_excluded_urls
@@ -91,6 +94,13 @@ def _start_response(status, response_headers, *args, **kwargs):
9194
if not _excluded_urls.url_disabled(flask.request.url):
9295
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
9396

97+
propagator = get_global_back_propagator()
98+
if propagator:
99+
propagator.inject(
100+
response_headers,
101+
setter=otel_wsgi.default_back_propagation_setter,
102+
)
103+
94104
if span:
95105
otel_wsgi.add_response_attributes(
96106
span, status, response_headers

instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py

+30
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818

1919
from opentelemetry import trace
2020
from opentelemetry.instrumentation.flask import FlaskInstrumentor
21+
from opentelemetry.instrumentation.propagators import (
22+
TraceResponsePropagator,
23+
get_global_back_propagator,
24+
set_global_back_propagator,
25+
)
2126
from opentelemetry.test.test_base import TestBase
2227
from opentelemetry.test.wsgitestutil import WsgiTestBase
2328
from opentelemetry.util.http import get_excluded_urls
@@ -119,6 +124,31 @@ def test_simple(self):
119124
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
120125
self.assertEqual(span_list[0].attributes, expected_attrs)
121126

127+
def test_trace_response(self):
128+
orig = get_global_back_propagator()
129+
130+
set_global_back_propagator(TraceResponsePropagator())
131+
response = self.client.get("/hello/123")
132+
headers = response.headers
133+
134+
span_list = self.memory_exporter.get_finished_spans()
135+
self.assertEqual(len(span_list), 1)
136+
span = span_list[0]
137+
138+
self.assertIn("traceresponse", headers)
139+
self.assertEqual(
140+
headers["access-control-expose-headers"], "traceresponse",
141+
)
142+
self.assertEqual(
143+
headers["traceresponse"],
144+
"00-{0}-{1}-01".format(
145+
trace.format_trace_id(span.get_span_context().trace_id),
146+
trace.format_span_id(span.get_span_context().span_id),
147+
),
148+
)
149+
150+
set_global_back_propagator(orig)
151+
122152
def test_not_recording(self):
123153
mock_tracer = Mock()
124154
mock_span = Mock()

instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
import opentelemetry.instrumentation.wsgi as otel_wsgi
2323
from opentelemetry import context, trace
24+
from opentelemetry.instrumentation.propagators import (
25+
get_global_back_propagator,
26+
)
2427
from opentelemetry.instrumentation.pyramid.version import __version__
2528
from opentelemetry.propagate import extract
2629
from opentelemetry.util._time import _time_ns
@@ -157,6 +160,10 @@ def trace_tween(request):
157160
response_or_exception.headers,
158161
)
159162

163+
propagator = get_global_back_propagator()
164+
if propagator:
165+
propagator.inject(response.headers)
166+
160167
activation = request.environ.get(_ENVIRON_ACTIVATION_KEY)
161168

162169
if isinstance(response_or_exception, HTTPException):

instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py

+27
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
from pyramid.config import Configurator
1818

1919
from opentelemetry import trace
20+
from opentelemetry.instrumentation.propagators import (
21+
TraceResponsePropagator,
22+
get_global_back_propagator,
23+
set_global_back_propagator,
24+
)
2025
from opentelemetry.instrumentation.pyramid import PyramidInstrumentor
2126
from opentelemetry.test.test_base import TestBase
2227
from opentelemetry.test.wsgitestutil import WsgiTestBase
@@ -98,6 +103,28 @@ def test_simple(self):
98103
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
99104
self.assertEqual(span_list[0].attributes, expected_attrs)
100105

106+
def test_response_headers(self):
107+
orig = get_global_back_propagator()
108+
set_global_back_propagator(TraceResponsePropagator())
109+
110+
response = self.client.get("/hello/500")
111+
headers = response.headers
112+
span = self.memory_exporter.get_finished_spans()[0]
113+
114+
self.assertIn("traceresponse", headers)
115+
self.assertEqual(
116+
headers["access-control-expose-headers"], "traceresponse",
117+
)
118+
self.assertEqual(
119+
headers["traceresponse"],
120+
"00-{0}-{1}-01".format(
121+
trace.format_trace_id(span.get_span_context().trace_id),
122+
trace.format_span_id(span.get_span_context().span_id),
123+
),
124+
)
125+
126+
set_global_back_propagator(orig)
127+
101128
def test_not_recording(self):
102129
mock_tracer = Mock()
103130
mock_span = Mock()

instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py

+14
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def get(self):
4646

4747
from opentelemetry import context, trace
4848
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
49+
from opentelemetry.instrumentation.propagators import (
50+
FuncSetter,
51+
get_global_back_propagator,
52+
)
4953
from opentelemetry.instrumentation.tornado.version import __version__
5054
from opentelemetry.instrumentation.utils import (
5155
extract_attributes_from_object,
@@ -68,6 +72,8 @@ def get(self):
6872
_excluded_urls = get_excluded_urls("TORNADO")
6973
_traced_request_attrs = get_traced_request_attrs("TORNADO")
7074

75+
back_propagation_setter = FuncSetter(tornado.web.RequestHandler.add_header)
76+
7177

7278
class TornadoInstrumentor(BaseInstrumentor):
7379
patched_handlers = []
@@ -211,6 +217,14 @@ def _start_span(tracer, handler, start_time) -> _TraceContext:
211217
activation.__enter__() # pylint: disable=E1101
212218
ctx = _TraceContext(activation, span, token)
213219
setattr(handler, _HANDLER_CONTEXT_KEY, ctx)
220+
221+
# finish handler is called after the response is sent back to
222+
# the client so it is too late to inject trace response headers
223+
# there.
224+
propagator = get_global_back_propagator()
225+
if propagator:
226+
propagator.inject(handler, setter=back_propagation_setter)
227+
214228
return ctx
215229

216230

0 commit comments

Comments
 (0)