Skip to content

Commit 3083690

Browse files
authored
Added opt-in support to return traceresponse headers for server instrumentations. (#436)
1 parent 4aec1e4 commit 3083690

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)