Skip to content

Commit 47b1275

Browse files
committed
[Django] Added support for traceresponse headers
Added opt-in support to return traceresponse headers from Django. This allows users to configure their Django 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 As a result the implementation does not use a hard-coded header and instead let's the users pick one. This can be done by setting the `OTEL_PYTHON_TRACE_RESPONSE_HEADER` to the header name that users want to inject in HTTP responses. The option does not have a default value and the feature is disbaled when a env var is not set.
1 parent 370952f commit 47b1275

File tree

5 files changed

+86
-2
lines changed

5 files changed

+86
-2
lines changed

Diff for: .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: 7b11971c504387341df0c38f5a34d7d1293c7e4f
1010

1111
jobs:
1212
build:

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
2222
- `opentelemetry-instrumenation-django` now supports request and response hooks.
2323
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
24+
- Add trace response header support for Django.
25+
([#395](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/395))
2426

2527
### Removed
2628
- Remove `http.status_text` from span attributes

Diff for: 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

Diff for: 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()

Diff for: 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

0 commit comments

Comments
 (0)