Falcon: Capture request/response headers as span attributes (#1003)

This commit is contained in:
Ashutosh Goel
2022-03-21 22:53:06 +05:30
committed by GitHub
parent e861b93362
commit 59ca95d12f
4 changed files with 129 additions and 0 deletions

View File

@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#999])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/999) ([#999])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/999)
- `opentelemetry-instrumentation-tornado` Fix non-recording span bug - `opentelemetry-instrumentation-tornado` Fix non-recording span bug
([#999])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/999) ([#999])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/999)
- `opentelemetry-instrumentation-falcon` Falcon: Capture custom request/response headers in span attributes
([#1003])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1003)
## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10

View File

@ -190,6 +190,8 @@ class _InstrumentedFalconAPI(getattr(falcon, _instrument_app)):
attributes = otel_wsgi.collect_request_attributes(env) attributes = otel_wsgi.collect_request_attributes(env)
for key, value in attributes.items(): for key, value in attributes.items():
span.set_attribute(key, value) span.set_attribute(key, value)
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
otel_wsgi.add_custom_request_headers(span, env)
activation = trace.use_span(span, end_on_exit=True) activation = trace.use_span(span, end_on_exit=True)
activation.__enter__() activation.__enter__()
@ -295,6 +297,10 @@ class _TraceMiddleware:
description=reason, description=reason,
) )
) )
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
otel_wsgi.add_custom_response_headers(
span, resp.headers.items()
)
except ValueError: except ValueError:
pass pass

View File

@ -33,6 +33,18 @@ class ErrorResource:
print(non_existent_var) # noqa print(non_existent_var) # noqa
class CustomResponseHeaderResource:
def on_get(self, _, resp):
# pylint: disable=no-member
resp.status = falcon.HTTP_201
resp.set_header("content-type", "text/plain; charset=utf-8")
resp.set_header("content-length", "0")
resp.set_header(
"my-custom-header", "my-custom-value-1,my-custom-header-2"
)
resp.set_header("dont-capture-me", "test-value")
def make_app(): def make_app():
if hasattr(falcon, "App"): if hasattr(falcon, "App"):
# Falcon 3 # Falcon 3
@ -43,4 +55,7 @@ def make_app():
app.add_route("/hello", HelloWorldResource()) app.add_route("/hello", HelloWorldResource())
app.add_route("/ping", HelloWorldResource()) app.add_route("/ping", HelloWorldResource())
app.add_route("/error", ErrorResource()) app.add_route("/error", ErrorResource())
app.add_route(
"/test_custom_response_headers", CustomResponseHeaderResource()
)
return app return app

View File

@ -28,6 +28,10 @@ from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.test.test_base import TestBase from opentelemetry.test.test_base import TestBase
from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.test.wsgitestutil import WsgiTestBase
from opentelemetry.trace import StatusCode from opentelemetry.trace import StatusCode
from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
)
from .app import make_app from .app import make_app
@ -280,3 +284,105 @@ class TestFalconInstrumentationWrappedWithOtherFramework(TestFalconBase):
self.assertEqual( self.assertEqual(
span.parent.span_id, parent_span.get_span_context().span_id span.parent.span_id, parent_span.get_span_context().span_id
) )
@patch.dict(
"os.environ",
{
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header",
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header",
},
)
class TestCustomRequestResponseHeaders(TestFalconBase):
def test_custom_request_header_added_in_server_span(self):
headers = {
"Custom-Test-Header-1": "Test Value 1",
"Custom-Test-Header-2": "TestValue2,TestValue3",
"Custom-Test-Header-3": "TestValue4",
}
self.client().simulate_request(
method="GET", path="/hello", headers=headers
)
span = self.memory_exporter.get_finished_spans()[0]
assert span.status.is_ok
expected = {
"http.request.header.custom_test_header_1": ("Test Value 1",),
"http.request.header.custom_test_header_2": (
"TestValue2,TestValue3",
),
}
not_expected = {
"http.request.header.custom_test_header_3": ("TestValue4",),
}
self.assertEqual(span.kind, trace.SpanKind.SERVER)
self.assertSpanHasAttributes(span, expected)
for key, _ in not_expected.items():
self.assertNotIn(key, span.attributes)
def test_custom_request_header_not_added_in_internal_span(self):
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):
headers = {
"Custom-Test-Header-1": "Test Value 1",
"Custom-Test-Header-2": "TestValue2,TestValue3",
}
self.client().simulate_request(
method="GET", path="/hello", headers=headers
)
span = self.memory_exporter.get_finished_spans()[0]
assert span.status.is_ok
not_expected = {
"http.request.header.custom_test_header_1": ("Test Value 1",),
"http.request.header.custom_test_header_2": (
"TestValue2,TestValue3",
),
}
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
for key, _ in not_expected.items():
self.assertNotIn(key, span.attributes)
def test_custom_response_header_added_in_server_span(self):
self.client().simulate_request(
method="GET", path="/test_custom_response_headers"
)
span = self.memory_exporter.get_finished_spans()[0]
assert span.status.is_ok
expected = {
"http.response.header.content_type": (
"text/plain; charset=utf-8",
),
"http.response.header.content_length": ("0",),
"http.response.header.my_custom_header": (
"my-custom-value-1,my-custom-header-2",
),
}
not_expected = {
"http.response.header.dont_capture_me": ("test-value",)
}
self.assertEqual(span.kind, trace.SpanKind.SERVER)
self.assertSpanHasAttributes(span, expected)
for key, _ in not_expected.items():
self.assertNotIn(key, span.attributes)
def test_custom_response_header_not_added_in_internal_span(self):
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):
self.client().simulate_request(
method="GET", path="/test_custom_response_headers"
)
span = self.memory_exporter.get_finished_spans()[0]
assert span.status.is_ok
not_expected = {
"http.response.header.content_type": (
"text/plain; charset=utf-8",
),
"http.response.header.content_length": ("0",),
"http.response.header.my_custom_header": (
"my-custom-value-1,my-custom-header-2",
),
}
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
for key, _ in not_expected.items():
self.assertNotIn(key, span.attributes)