Skip to content

Commit 67ff3c6

Browse files
Add traceresponse headers for asgi apps (FastAPI, Starlette)
This asgi version is modeled after the original wsgi version in open-telemetry#436.
1 parent 444e0a1 commit 67ff3c6

File tree

3 files changed

+130
-1
lines changed

3 files changed

+130
-1
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.7.0-0.26b0...HEAD)
99

10+
### Added
11+
12+
`opentelemetry-instrumenation-asgi` now returns a `traceresponse` response header.
13+
([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436))
14+
15+
1016
### Fixed
1117

1218
- `opentelemetry-exporter-richconsole` Fixed attribute error on parentless spans.
@@ -17,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1723

1824
## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11
1925

26+
### Added
27+
2028
- `opentelemetry-instrumentation-aws-lambda` Add instrumentation for AWS Lambda Service - pkg metadata files (Part 1/2)
2129
([#739](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/739))
2230
- Add support for Python 3.10

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

+33-1
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,12 @@ def client_response_hook(span: Span, message: dict):
103103

104104
from opentelemetry import context, trace
105105
from opentelemetry.instrumentation.asgi.version import __version__ # noqa
106+
from opentelemetry.instrumentation.propagators import (
107+
get_global_response_propagator,
108+
)
106109
from opentelemetry.instrumentation.utils import http_status_to_status_code
107110
from opentelemetry.propagate import extract
108-
from opentelemetry.propagators.textmap import Getter
111+
from opentelemetry.propagators.textmap import Getter, Setter
109112
from opentelemetry.semconv.trace import SpanAttributes
110113
from opentelemetry.trace import Span
111114
from opentelemetry.trace.status import Status, StatusCode
@@ -152,6 +155,30 @@ def keys(self, carrier: dict) -> typing.List[str]:
152155
asgi_getter = ASGIGetter()
153156

154157

158+
class ASGISetter(Setter):
159+
def set(
160+
self, carrier: dict, key: str, value: str
161+
) -> None: # pylint: disable=no-self-use
162+
"""Sets response header values on an ASGI scope according to `the spec <https://asgi.readthedocs.io/en/latest/specs/www.html#response-start-send-event>`_.
163+
164+
Args:
165+
carrier: ASGI scope object
166+
key: response header name to set
167+
value: response header value
168+
Returns:
169+
None
170+
"""
171+
headers = carrier.get("headers")
172+
if not headers:
173+
headers = []
174+
carrier["headers"] = headers
175+
176+
headers.append([key.lower().encode(), value.encode()])
177+
178+
179+
asgi_setter = ASGISetter()
180+
181+
155182
def collect_request_attributes(scope):
156183
"""Collects HTTP request attributes from the ASGI scope and returns a
157184
dictionary to be used as span creation attributes."""
@@ -341,6 +368,11 @@ async def wrapped_send(message):
341368
set_status_code(span, 200)
342369
set_status_code(send_span, 200)
343370
send_span.set_attribute("type", message["type"])
371+
372+
propagator = get_global_response_propagator()
373+
if propagator:
374+
propagator.inject(message, setter=asgi_setter)
375+
344376
await send(message)
345377

346378
await self.app(scope, wrapped_receive, wrapped_send)

instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py

+89
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@
1818

1919
import opentelemetry.instrumentation.asgi as otel_asgi
2020
from opentelemetry import trace as trace_api
21+
from opentelemetry.instrumentation.propagators import (
22+
TraceResponsePropagator,
23+
get_global_response_propagator,
24+
set_global_response_propagator,
25+
)
2126
from opentelemetry.sdk import resources
2227
from opentelemetry.semconv.trace import SpanAttributes
2328
from opentelemetry.test.asgitestutil import (
2429
AsgiTestBase,
2530
setup_testing_defaults,
2631
)
2732
from opentelemetry.test.test_base import TestBase
33+
from opentelemetry.trace import format_span_id, format_trace_id
2834

2935

3036
async def http_app(scope, receive, send):
@@ -287,6 +293,45 @@ def update_expected_user_agent(expected):
287293
outputs = self.get_all_output()
288294
self.validate_outputs(outputs, modifiers=[update_expected_user_agent])
289295

296+
def test_traceresponse_header(self):
297+
"""Test a traceresponse header is sent when a global propagator is set."""
298+
299+
orig = get_global_response_propagator()
300+
set_global_response_propagator(TraceResponsePropagator())
301+
302+
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
303+
self.seed_app(app)
304+
self.send_default_request()
305+
306+
# traceresponse header corresponds to http.response.start span
307+
span = self.memory_exporter.get_finished_spans()[1]
308+
self.assertDictEqual(
309+
dict(span.attributes),
310+
{
311+
SpanAttributes.HTTP_STATUS_CODE: 200,
312+
"type": "http.response.start",
313+
},
314+
)
315+
316+
response_start, response_body, *_ = self.get_all_output()
317+
self.assertEqual(response_body["body"], b"*")
318+
self.assertEqual(response_start["status"], 200)
319+
320+
traceresponse = "00-{0}-{1}-01".format(
321+
format_trace_id(span.get_span_context().trace_id),
322+
format_span_id(span.get_span_context().span_id),
323+
)
324+
self.assertListEqual(
325+
response_start["headers"],
326+
[
327+
[b"Content-Type", b"text/plain"],
328+
[b"traceresponse", f"{traceresponse}".encode()],
329+
[b"access-control-expose-headers", b"traceresponse"],
330+
],
331+
)
332+
333+
set_global_response_propagator(orig)
334+
290335
def test_websocket(self):
291336
self.scope = {
292337
"type": "websocket",
@@ -359,6 +404,50 @@ def test_websocket(self):
359404
self.assertEqual(span.kind, expected["kind"])
360405
self.assertDictEqual(dict(span.attributes), expected["attributes"])
361406

407+
def test_websocket_traceresponse_header(self):
408+
"""Test a traceresponse header is set for websocket messages"""
409+
410+
orig = get_global_response_propagator()
411+
set_global_response_propagator(TraceResponsePropagator())
412+
413+
self.scope = {
414+
"type": "websocket",
415+
"http_version": "1.1",
416+
"scheme": "ws",
417+
"path": "/",
418+
"query_string": b"",
419+
"headers": [],
420+
"client": ("127.0.0.1", 32767),
421+
"server": ("127.0.0.1", 80),
422+
}
423+
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
424+
self.seed_app(app)
425+
self.send_input({"type": "websocket.connect"})
426+
self.send_input({"type": "websocket.receive", "text": "ping"})
427+
self.send_input({"type": "websocket.disconnect"})
428+
_, socket_send, *_ = self.get_all_output()
429+
430+
# traceresponse header corresponds to the 2nd websocket.send span
431+
span = self.memory_exporter.get_finished_spans()[3]
432+
self.assertDictEqual(
433+
dict(span.attributes),
434+
{SpanAttributes.HTTP_STATUS_CODE: 200, "type": "websocket.send"},
435+
)
436+
437+
traceresponse = "00-{0}-{1}-01".format(
438+
format_trace_id(span.get_span_context().trace_id),
439+
format_span_id(span.get_span_context().span_id),
440+
)
441+
self.assertListEqual(
442+
socket_send["headers"],
443+
[
444+
[b"traceresponse", f"{traceresponse}".encode()],
445+
[b"access-control-expose-headers", b"traceresponse"],
446+
],
447+
)
448+
449+
set_global_response_propagator(orig)
450+
362451
def test_lifespan(self):
363452
self.scope["type"] = "lifespan"
364453
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)

0 commit comments

Comments
 (0)