Skip to content

Commit 9a710fb

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 4aec1e4 commit 9a710fb

File tree

14 files changed

+230
-14
lines changed

14 files changed

+230
-14
lines changed

.github/workflows/test.yml

+2-2
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: 6bd163f6d670319eba6693b8465a068a1828f484
1010

1111
jobs:
1212
build:
@@ -98,6 +98,6 @@ jobs:
9898
uses: actions/cache@v2
9999
with:
100100
path: .tox
101-
key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt', 'docs-requirements.txt') }}
101+
key: v2-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt', 'docs-requirements.txt') }}
102102
- name: run tox
103103
run: tox -e ${{ matrix.tox-environment }}

CHANGELOG.md

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

3848
### Removed
3949
- Remove `http.status_text` from span attributes

docs-requirements.txt

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ sphinx-autodoc-typehints
66
# doesn't work for pkg_resources.
77
-e "git+https://github.com/open-telemetry/opentelemetry-python.git#egg=opentelemetry-api&subdirectory=opentelemetry-api"
88
-e "git+https://github.com/open-telemetry/opentelemetry-python.git#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk"
9+
-e "git+https://github.com/open-telemetry/opentelemetry-python.git#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation"
910

1011
# Required by opentelemetry-instrumentation
1112
fastapi~=0.58.1
1213
psutil~=5.7.0
1314
pymemcache~=1.3
1415

15-
-e "git+https://github.com/open-telemetry/opentelemetry-python.git#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation"
16-
1716
# Required by conf
1817
django>=2.2
1918

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

+9
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,
@@ -119,6 +123,7 @@ def response_hook(span, req, resp):
119123

120124
_excluded_urls = get_excluded_urls("FALCON")
121125
_traced_request_attrs = get_traced_request_attrs("FALCON")
126+
_response_propagation_setter = FuncSetter(falcon.api.Response.append_header)
122127

123128

124129
class FalconInstrumentor(BaseInstrumentor):
@@ -273,5 +278,9 @@ def process_response(
273278
)
274279
)
275280

281+
propagator = get_global_response_propagator()
282+
if propagator:
283+
propagator.inject(resp, setter=_response_propagation_setter)
284+
276285
if self._response_hook:
277286
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_response_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
@@ -83,6 +83,10 @@ def client_resposne_hook(span, future):
8383

8484
from opentelemetry import context, trace
8585
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
86+
from opentelemetry.instrumentation.propagators import (
87+
FuncSetter,
88+
get_global_response_propagator,
89+
)
8690
from opentelemetry.instrumentation.tornado.version import __version__
8791
from opentelemetry.instrumentation.utils import (
8892
extract_attributes_from_object,
@@ -105,6 +109,8 @@ def client_resposne_hook(span, future):
105109
_excluded_urls = get_excluded_urls("TORNADO")
106110
_traced_request_attrs = get_traced_request_attrs("TORNADO")
107111

112+
response_propagation_setter = FuncSetter(tornado.web.RequestHandler.add_header)
113+
108114

109115
class TornadoInstrumentor(BaseInstrumentor):
110116
patched_handlers = []
@@ -256,6 +262,14 @@ def _start_span(tracer, handler, start_time) -> _TraceContext:
256262
activation.__enter__() # pylint: disable=E1101
257263
ctx = _TraceContext(activation, span, token)
258264
setattr(handler, _HANDLER_CONTEXT_KEY, ctx)
265+
266+
# finish handler is called after the response is sent back to
267+
# the client so it is too late to inject trace response headers
268+
# there.
269+
propagator = get_global_response_propagator()
270+
if propagator:
271+
propagator.inject(handler, setter=response_propagation_setter)
272+
259273
return ctx
260274

261275

0 commit comments

Comments
 (0)