Skip to content

Commit 59ca95d

Browse files
authored
Falcon: Capture request/response headers as span attributes (#1003)
1 parent e861b93 commit 59ca95d

File tree

4 files changed

+129
-0
lines changed

4 files changed

+129
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
([#999])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/999)
1616
- `opentelemetry-instrumentation-tornado` Fix non-recording span bug
1717
([#999])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/999)
18+
- `opentelemetry-instrumentation-falcon` Falcon: Capture custom request/response headers in span attributes
19+
([#1003])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1003)
1820

1921
## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10
2022

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

+6
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ def __call__(self, env, start_response):
190190
attributes = otel_wsgi.collect_request_attributes(env)
191191
for key, value in attributes.items():
192192
span.set_attribute(key, value)
193+
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
194+
otel_wsgi.add_custom_request_headers(span, env)
193195

194196
activation = trace.use_span(span, end_on_exit=True)
195197
activation.__enter__()
@@ -295,6 +297,10 @@ def process_response(
295297
description=reason,
296298
)
297299
)
300+
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
301+
otel_wsgi.add_custom_response_headers(
302+
span, resp.headers.items()
303+
)
298304
except ValueError:
299305
pass
300306

instrumentation/opentelemetry-instrumentation-falcon/tests/app.py

+15
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ def on_get(self, req, resp):
3333
print(non_existent_var) # noqa
3434

3535

36+
class CustomResponseHeaderResource:
37+
def on_get(self, _, resp):
38+
# pylint: disable=no-member
39+
resp.status = falcon.HTTP_201
40+
resp.set_header("content-type", "text/plain; charset=utf-8")
41+
resp.set_header("content-length", "0")
42+
resp.set_header(
43+
"my-custom-header", "my-custom-value-1,my-custom-header-2"
44+
)
45+
resp.set_header("dont-capture-me", "test-value")
46+
47+
3648
def make_app():
3749
if hasattr(falcon, "App"):
3850
# Falcon 3
@@ -43,4 +55,7 @@ def make_app():
4355
app.add_route("/hello", HelloWorldResource())
4456
app.add_route("/ping", HelloWorldResource())
4557
app.add_route("/error", ErrorResource())
58+
app.add_route(
59+
"/test_custom_response_headers", CustomResponseHeaderResource()
60+
)
4661
return app

instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py

+106
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
from opentelemetry.test.test_base import TestBase
2929
from opentelemetry.test.wsgitestutil import WsgiTestBase
3030
from opentelemetry.trace import StatusCode
31+
from opentelemetry.util.http import (
32+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
33+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
34+
)
3135

3236
from .app import make_app
3337

@@ -280,3 +284,105 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
280284
self.assertEqual(
281285
span.parent.span_id, parent_span.get_span_context().span_id
282286
)
287+
288+
289+
@patch.dict(
290+
"os.environ",
291+
{
292+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header",
293+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header",
294+
},
295+
)
296+
class TestCustomRequestResponseHeaders(TestFalconBase):
297+
def test_custom_request_header_added_in_server_span(self):
298+
headers = {
299+
"Custom-Test-Header-1": "Test Value 1",
300+
"Custom-Test-Header-2": "TestValue2,TestValue3",
301+
"Custom-Test-Header-3": "TestValue4",
302+
}
303+
self.client().simulate_request(
304+
method="GET", path="/hello", headers=headers
305+
)
306+
span = self.memory_exporter.get_finished_spans()[0]
307+
assert span.status.is_ok
308+
309+
expected = {
310+
"http.request.header.custom_test_header_1": ("Test Value 1",),
311+
"http.request.header.custom_test_header_2": (
312+
"TestValue2,TestValue3",
313+
),
314+
}
315+
not_expected = {
316+
"http.request.header.custom_test_header_3": ("TestValue4",),
317+
}
318+
319+
self.assertEqual(span.kind, trace.SpanKind.SERVER)
320+
self.assertSpanHasAttributes(span, expected)
321+
for key, _ in not_expected.items():
322+
self.assertNotIn(key, span.attributes)
323+
324+
def test_custom_request_header_not_added_in_internal_span(self):
325+
tracer = trace.get_tracer(__name__)
326+
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):
327+
headers = {
328+
"Custom-Test-Header-1": "Test Value 1",
329+
"Custom-Test-Header-2": "TestValue2,TestValue3",
330+
}
331+
self.client().simulate_request(
332+
method="GET", path="/hello", headers=headers
333+
)
334+
span = self.memory_exporter.get_finished_spans()[0]
335+
assert span.status.is_ok
336+
not_expected = {
337+
"http.request.header.custom_test_header_1": ("Test Value 1",),
338+
"http.request.header.custom_test_header_2": (
339+
"TestValue2,TestValue3",
340+
),
341+
}
342+
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
343+
for key, _ in not_expected.items():
344+
self.assertNotIn(key, span.attributes)
345+
346+
def test_custom_response_header_added_in_server_span(self):
347+
self.client().simulate_request(
348+
method="GET", path="/test_custom_response_headers"
349+
)
350+
span = self.memory_exporter.get_finished_spans()[0]
351+
assert span.status.is_ok
352+
expected = {
353+
"http.response.header.content_type": (
354+
"text/plain; charset=utf-8",
355+
),
356+
"http.response.header.content_length": ("0",),
357+
"http.response.header.my_custom_header": (
358+
"my-custom-value-1,my-custom-header-2",
359+
),
360+
}
361+
not_expected = {
362+
"http.response.header.dont_capture_me": ("test-value",)
363+
}
364+
self.assertEqual(span.kind, trace.SpanKind.SERVER)
365+
self.assertSpanHasAttributes(span, expected)
366+
for key, _ in not_expected.items():
367+
self.assertNotIn(key, span.attributes)
368+
369+
def test_custom_response_header_not_added_in_internal_span(self):
370+
tracer = trace.get_tracer(__name__)
371+
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):
372+
self.client().simulate_request(
373+
method="GET", path="/test_custom_response_headers"
374+
)
375+
span = self.memory_exporter.get_finished_spans()[0]
376+
assert span.status.is_ok
377+
not_expected = {
378+
"http.response.header.content_type": (
379+
"text/plain; charset=utf-8",
380+
),
381+
"http.response.header.content_length": ("0",),
382+
"http.response.header.my_custom_header": (
383+
"my-custom-value-1,my-custom-header-2",
384+
),
385+
}
386+
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
387+
for key, _ in not_expected.items():
388+
self.assertNotIn(key, span.attributes)

0 commit comments

Comments
 (0)