Skip to content

Commit 8e5e71b

Browse files
committed
add header parameters to FastAPIInstrumentor().instrument_app
1 parent 46a8c59 commit 8e5e71b

File tree

2 files changed

+95
-9
lines changed

2 files changed

+95
-9
lines changed

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def client_response_hook(span: Span, message: dict):
7676
if span and span.is_recording():
7777
span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
7878
79-
FastAPIInstrumentor().instrument(server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook)
79+
FastAPIInstrumentor().instrument_app(server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook)
8080
8181
Capture HTTP request and response headers
8282
*****************************************
@@ -86,9 +86,10 @@ def client_response_hook(span: Span, message: dict):
8686
Request headers
8787
***************
8888
To capture HTTP request headers as span attributes, set the environment variable
89-
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names.
89+
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names,
90+
or pass the ``http_capture_headers_server_request`` keyword argument to the ``instrument_app`` method.
9091
91-
For example,
92+
For example using the environment variable,
9293
::
9394
9495
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
@@ -120,9 +121,10 @@ def client_response_hook(span: Span, message: dict):
120121
Response headers
121122
****************
122123
To capture HTTP response headers as span attributes, set the environment variable
123-
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names.
124+
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names,
125+
or pass the ``http_capture_headers_server_response`` keyword argument to the ``instrument_app`` method.
124126
125-
For example,
127+
For example using the environment variable,
126128
::
127129
128130
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
@@ -155,10 +157,12 @@ def client_response_hook(span: Span, message: dict):
155157
******************
156158
In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords,
157159
etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS``
158-
to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be
159-
matched in a case-insensitive manner.
160+
to a comma delimited list of HTTP header names to be sanitized, or pass the ``http_capture_headers_sanitize_fields``
161+
keyword argument to the ``instrument_app`` method.
160162
161-
For example,
163+
Regexes may be used, and all header names will be matched in a case-insensitive manner.
164+
165+
For example using the environment variable,
162166
::
163167
164168
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie"
@@ -212,6 +216,9 @@ def instrument_app(
212216
tracer_provider=None,
213217
meter_provider=None,
214218
excluded_urls=None,
219+
http_capture_headers_server_request: list[str] | None = None,
220+
http_capture_headers_server_response: list[str] | None = None,
221+
http_capture_headers_sanitize_fields: list[str] | None = None,
215222
):
216223
"""Instrument an uninstrumented FastAPI application."""
217224
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
@@ -238,6 +245,9 @@ def instrument_app(
238245
client_response_hook=client_response_hook,
239246
tracer_provider=tracer_provider,
240247
meter=meter,
248+
http_capture_headers_server_request=http_capture_headers_server_request,
249+
http_capture_headers_server_response=http_capture_headers_server_response,
250+
http_capture_headers_sanitize_fields=http_capture_headers_sanitize_fields,
241251
)
242252
app._is_instrumented_by_opentelemetry = True
243253
if app not in _InstrumentedFastAPI._instrumented_fastapi_apps:

instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def test_basic_post_request_metric_success(self):
279279
if isinstance(point, NumberDataPoint):
280280
self.assertEqual(point.value, 0)
281281

282-
def test_metric_uninstruemnt_app(self):
282+
def test_metric_uninstrument_app(self):
283283
self._client.get("/foobar")
284284
self._instrumentor.uninstrument_app(self._app)
285285
self._client.get("/foobar")
@@ -693,6 +693,82 @@ def test_http_custom_response_headers_not_in_span_attributes(self):
693693
self.assertNotIn(key, server_span.attributes)
694694

695695

696+
class TestHTTPAppWithCustomHeadersParameters(TestBase):
697+
"""Minimal tests here since the behavior of this logic is tested above and in the ASGI tests."""
698+
def setUp(self):
699+
super().setUp()
700+
self.app = self._create_app()
701+
otel_fastapi.FastAPIInstrumentor().instrument_app(
702+
self.app,
703+
http_capture_headers_server_request=["a.*", "b.*"],
704+
http_capture_headers_server_response=["c.*", "d.*"],
705+
http_capture_headers_sanitize_fields=[".*secret.*"]
706+
)
707+
self.client = TestClient(self.app)
708+
709+
def tearDown(self) -> None:
710+
super().tearDown()
711+
with self.disable_logging():
712+
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
713+
714+
@staticmethod
715+
def _create_app():
716+
app = fastapi.FastAPI()
717+
718+
@app.get("/foobar")
719+
async def _():
720+
headers = {
721+
"carrot": "bar",
722+
"date-secret": "yellow",
723+
"egg": "ham",
724+
}
725+
content = {"message": "hello world"}
726+
return JSONResponse(content=content, headers=headers)
727+
728+
return app
729+
730+
def test_http_custom_request_headers_in_span_attributes(self):
731+
resp = self.client.get("/foobar", headers={
732+
"apple": "red", "banana-secret": "yellow", "fig": "green"
733+
})
734+
self.assertEqual(200, resp.status_code)
735+
span_list = self.memory_exporter.get_finished_spans()
736+
self.assertEqual(len(span_list), 3)
737+
738+
server_span = [
739+
span for span in span_list if span.kind == trace.SpanKind.SERVER
740+
][0]
741+
742+
from pprint import pprint
743+
pprint(server_span)
744+
expected = {
745+
# apple should be included because it starts with a
746+
"http.request.header.apple": ("red",),
747+
# same with banana because it starts with b,
748+
# redacted because it contains "secret"
749+
"http.request.header.banana_secret": ("[REDACTED]",),
750+
}
751+
self.assertSpanHasAttributes(server_span, expected)
752+
self.assertNotIn("http.request.header.fig", server_span.attributes)
753+
754+
def test_http_custom_response_headers_in_span_attributes(self):
755+
resp = self.client.get("/foobar")
756+
self.assertEqual(200, resp.status_code)
757+
span_list = self.memory_exporter.get_finished_spans()
758+
self.assertEqual(len(span_list), 3)
759+
760+
server_span = [
761+
span for span in span_list if span.kind == trace.SpanKind.SERVER
762+
][0]
763+
764+
expected = {
765+
"http.response.header.carrot": ("bar",),
766+
"http.response.header.date_secret": ("[REDACTED]",),
767+
}
768+
self.assertSpanHasAttributes(server_span, expected)
769+
self.assertNotIn("http.response.header.egg", server_span.attributes)
770+
771+
696772
@patch.dict(
697773
"os.environ",
698774
{

0 commit comments

Comments
 (0)