Skip to content

Commit

Permalink
[Django] Added support for traceresponse headers
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
owais committed Apr 12, 2021
1 parent 370952f commit 47b1275
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- 'release/*'
pull_request:
env:
CORE_REPO_SHA: cad261e5dae1fe986c87e6965664b45cc9ab73c3
CORE_REPO_SHA: 7b11971c504387341df0c38f5a34d7d1293c7e4f

jobs:
build:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
- `opentelemetry-instrumenation-django` now supports request and response hooks.
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
- Add trace response header support for Django.
([#395](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/395))

### Removed
- Remove `http.status_text` from span attributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

from opentelemetry.context import attach, detach
from opentelemetry.instrumentation.django.version import __version__
from opentelemetry.instrumentation.propagators import (
get_global_back_propagator,
)
from opentelemetry.instrumentation.utils import extract_attributes_from_object
from opentelemetry.instrumentation.wsgi import (
add_response_attributes,
Expand Down Expand Up @@ -179,6 +182,11 @@ def process_response(self, request, response):
response,
)

propagator = get_global_back_propagator()
if propagator:
propagator.inject(response)

# record any exceptions raised while processing the request
exception = request.META.pop(self._environ_exception_key, None)
if _DjangoMiddleware._otel_response_hook:
_DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,19 @@
DjangoInstrumentor,
_DjangoMiddleware,
)
from opentelemetry.instrumentation.propagators import (
TraceResponsePropagator,
set_global_back_propagator,
)
from opentelemetry.sdk.trace import Span
from opentelemetry.test.test_base import TestBase
from opentelemetry.test.wsgitestutil import WsgiTestBase
from opentelemetry.trace import SpanKind, StatusCode
from opentelemetry.trace import (
SpanKind,
StatusCode,
format_span_id,
format_trace_id,
)
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs

# pylint: disable=import-error
Expand All @@ -41,6 +50,7 @@
route_span_name,
traced,
traced_template,
with_response_header,
)

DJANGO_2_2 = VERSION >= (2, 2)
Expand All @@ -52,6 +62,7 @@
url(r"^excluded_arg/", excluded),
url(r"^excluded_noarg/", excluded_noarg),
url(r"^excluded_noarg2/", excluded_noarg2),
url(r"^response_header/", with_response_header),
url(r"^span_name/([0-9]{4})/$", route_span_name),
]
_django_instrumentor = DjangoInstrumentor()
Expand Down Expand Up @@ -309,3 +320,55 @@ def response_hook(span, request, response):
self.assertIsInstance(response_hook_args[1], HttpRequest)
self.assertIsInstance(response_hook_args[2], HttpResponse)
self.assertEqual(response_hook_args[2], response)

def test_trace_response_headers(self):
response = Client().get("/span_name/1234/")
self.assertNotIn("Server-Timing", response._headers)
self.memory_exporter.clear()

set_global_back_propagator(TraceResponsePropagator())

response = Client().get("/span_name/1234/")
span = self.memory_exporter.get_finished_spans()[0]

self.assertIn("traceresponse", response._headers)
self.assertEqual(
response._headers["access-control-expose-headers"][0],
"Access-Control-Expose-Headers",
)
self.assertEqual(
response._headers["access-control-expose-headers"][1],
"traceresponse",
)
self.assertEqual(
response._headers["traceresponse"][0], "traceresponse"
)
self.assertEqual(
response._headers["traceresponse"][1],
"00-{0}-{1}-01".format(
format_trace_id(span.get_span_context().trace_id),
format_span_id(span.get_span_context().span_id),
),
)
self.memory_exporter.clear()

def test_trace_response_header_pre_existing_header(self):
set_global_back_propagator(TraceResponsePropagator())

response = Client().get("/response_header/")
span = self.memory_exporter.get_finished_spans()[0]
self.assertIn("traceresponse", response._headers)
self.assertEqual(
response._headers["access-control-expose-headers"][1],
"X-Test-Header, traceresponse",
)
self.assertEqual(
response._headers["traceresponse"][1],
"abc; val=1, "
+ "00-{0}-{1}-01".format(
format_trace_id(span.get_span_context().trace_id),
format_span_id(span.get_span_context().span_id),
),
)

self.memory_exporter.clear()
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from django.http import HttpResponse

from opentelemetry.instrumentation.propagators import (
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
)


def traced(request): # pylint: disable=unused-argument
return HttpResponse()
Expand Down Expand Up @@ -29,3 +33,10 @@ def route_span_name(
request, *args, **kwargs
): # pylint: disable=unused-argument
return HttpResponse()


def with_response_header(request): # pylint: disable=unused-argument
response = HttpResponse()
response["traceresponse"] = "abc; val=1"
response[_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS] = "X-Test-Header"
return response

0 comments on commit 47b1275

Please sign in to comment.