Skip to content

Commit f453f21

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 0fcb60d commit f453f21

File tree

13 files changed

+218
-3
lines changed

13 files changed

+218
-3
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: bf0c6812936bf7ffb22fbe65a2a20a18494755ea
1010

1111
jobs:
1212
build:

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
3333
- `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks.
3434
([#415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/415))
35+
- `opentelemetry-instrumenation-django` now supports trace response headers.
36+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
37+
- `opentelemetry-instrumenation-tornado` now supports trace response headers.
38+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
39+
- `opentelemetry-instrumenation-pyramid` now supports trace response headers.
40+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
41+
- `opentelemetry-instrumenation-falcon` now supports trace response headers.
42+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
43+
- `opentelemetry-instrumenation-flask` now supports trace response headers.
44+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
3545

3646
### Removed
3747
- 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_response_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_response_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

+34-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_response_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
@@ -309,3 +318,27 @@ def response_hook(span, request, response):
309318
self.assertIsInstance(response_hook_args[1], HttpRequest)
310319
self.assertIsInstance(response_hook_args[2], HttpResponse)
311320
self.assertEqual(response_hook_args[2], response)
321+
322+
def test_trace_response_headers(self):
323+
response = Client().get("/span_name/1234/")
324+
325+
self.assertNotIn("Server-Timing", response.headers)
326+
self.memory_exporter.clear()
327+
328+
set_global_response_propagator(TraceResponsePropagator())
329+
330+
response = Client().get("/span_name/1234/")
331+
span = self.memory_exporter.get_finished_spans()[0]
332+
333+
self.assertIn("traceresponse", response.headers)
334+
self.assertEqual(
335+
response.headers["Access-Control-Expose-Headers"], "traceresponse",
336+
)
337+
self.assertEqual(
338+
response.headers["traceresponse"],
339+
"00-{0}-{1}-01".format(
340+
format_trace_id(span.get_span_context().trace_id),
341+
format_span_id(span.get_span_context().span_id),
342+
),
343+
)
344+
self.memory_exporter.clear()

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

+10
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ def response_hook(span, req, resp):
9999
from opentelemetry import context, trace
100100
from opentelemetry.instrumentation.falcon.version import __version__
101101
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
102+
from opentelemetry.instrumentation.propagators import (
103+
FuncSetter,
104+
get_global_response_propagator,
105+
)
102106
from opentelemetry.instrumentation.utils import (
103107
extract_attributes_from_object,
104108
http_status_to_status_code,
@@ -199,6 +203,8 @@ def _start_response(status, response_headers, *args, **kwargs):
199203
class _TraceMiddleware:
200204
# pylint:disable=R0201,W0613
201205

206+
back_propagation_setter = FuncSetter(falcon.api.Response.append_header)
207+
202208
def __init__(
203209
self,
204210
tracer=None,
@@ -273,5 +279,9 @@ def process_response(
273279
)
274280
)
275281

282+
propagator = get_global_response_propagator()
283+
if propagator:
284+
propagator.inject(resp, setter=self.back_propagation_setter)
285+
276286
if self._response_hook:
277287
self._response_hook(span, req, resp)

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_response_propagator,
23+
set_global_response_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
@@ -197,6 +202,28 @@ def test_traced_request_attributes(self):
197202
self.assertEqual(span.attributes["query_string"], "q=abc")
198203
self.assertNotIn("not_available_attr", span.attributes)
199204

205+
def test_trace_response(self):
206+
orig = get_global_response_propagator()
207+
set_global_response_propagator(TraceResponsePropagator())
208+
209+
response = self.client().simulate_get(path="/hello?q=abc")
210+
headers = response.headers
211+
span = self.memory_exporter.get_finished_spans()[0]
212+
213+
self.assertIn("traceresponse", headers)
214+
self.assertEqual(
215+
headers["access-control-expose-headers"], "traceresponse",
216+
)
217+
self.assertEqual(
218+
headers["traceresponse"],
219+
"00-{0}-{1}-01".format(
220+
format_trace_id(span.get_span_context().trace_id),
221+
format_span_id(span.get_span_context().span_id),
222+
),
223+
)
224+
225+
set_global_response_propagator(orig)
226+
200227
def test_traced_not_recording(self):
201228
mock_tracer = Mock()
202229
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_response_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_response_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_response_propagator,
24+
set_global_response_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_response_propagator()
129+
130+
set_global_response_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_response_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_response_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_response_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_response_propagator,
23+
set_global_response_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_response_propagator()
108+
set_global_response_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_response_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_response_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_response_propagator()
225+
if propagator:
226+
propagator.inject(handler, setter=back_propagation_setter)
227+
214228
return ctx
215229

216230

instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py

+31
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
from tornado.testing import AsyncHTTPTestCase
1919

2020
from opentelemetry import trace
21+
from opentelemetry.instrumentation.propagators import (
22+
TraceResponsePropagator,
23+
get_global_response_propagator,
24+
set_global_response_propagator,
25+
)
2126
from opentelemetry.instrumentation.tornado import (
2227
TornadoInstrumentor,
2328
patch_handler_class,
@@ -366,6 +371,32 @@ def test_traced_attrs(self):
366371
)
367372
self.memory_exporter.clear()
368373

374+
def test_response_headers(self):
375+
orig = get_global_response_propagator()
376+
set_global_response_propagator(TraceResponsePropagator())
377+
378+
response = self.fetch("/")
379+
headers = response.headers
380+
381+
spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
382+
self.assertEqual(len(spans), 3)
383+
server_span = spans[1]
384+
385+
self.assertIn("traceresponse", headers)
386+
self.assertEqual(
387+
headers["access-control-expose-headers"], "traceresponse",
388+
)
389+
self.assertEqual(
390+
headers["traceresponse"],
391+
"00-{0}-{1}-01".format(
392+
trace.format_trace_id(server_span.get_span_context().trace_id),
393+
trace.format_span_id(server_span.get_span_context().span_id),
394+
),
395+
)
396+
397+
self.memory_exporter.clear()
398+
set_global_response_propagator(orig)
399+
369400

370401
class TestTornadoUninstrument(TornadoTest):
371402
def test_uninstrument(self):

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

+8
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,11 @@ def _end_span_after_iterating(iterable, span, tracer, token):
264264
close()
265265
span.end()
266266
context.detach(token)
267+
268+
269+
class BackPropagationSetter:
270+
def set(self, carrier, key, value): # pylint: disable=no-self-use
271+
carrier.append((key, value))
272+
273+
274+
default_back_propagation_setter = BackPropagationSetter()

0 commit comments

Comments
 (0)