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

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

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

Lines changed: 1 addition & 2 deletions
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

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

Lines changed: 34 additions & 1 deletion
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

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

Lines changed: 28 additions & 1 deletion
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

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

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

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

0 commit comments

Comments
 (0)