Skip to content

Commit 201994e

Browse files
authored
capture_headers (#318)
1 parent 80c7f49 commit 201994e

File tree

8 files changed

+57
-10
lines changed

8 files changed

+57
-10
lines changed

docs/integrations/use_cases/web_frameworks.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ Otherwise, check if your server uses [WSGI](../wsgi.md) or [ASGI](../asgi.md) an
1515

1616
## Capturing HTTP server request and response headers
1717

18-
There are three environment variables to tell the OpenTelemetry instrumentation libraries to capture request and response headers:
18+
Some methods (e.g. `logfire.instrument_fastapi()`) allow you to pass `capture_headers=True` to record all request and response headers in the spans,
19+
and that's all you usually need.
20+
21+
If you want more control, there are three environment variables to tell the OpenTelemetry instrumentation libraries to capture request and response headers:
1922

2023
- `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`
2124
- `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`
@@ -30,6 +33,8 @@ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*"
3033
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*"
3134
```
3235

36+
(this is what `capture_headers=True` does)
37+
3338
To specifically capture the `content-type` request header and request headers starting with `X-`:
3439

3540
```
@@ -42,6 +47,8 @@ To replace the `Authorization` header value with `[REDACTED]` to avoid leaking u
4247
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS="Authorization"
4348
```
4449

50+
(although usually it's better to rely on **Logfire**'s [scrubbing](../../guides/advanced/scrubbing.md) feature)
51+
4552
## Query HTTP requests duration per percentile
4653

4754
It's usually interesting to visualize HTTP requests duration per percentile. Instead of having an average, which may be influenced by extreme values, percentiles allow us know the maximum duration for 50%, 90%, 95% or 99% of the requests.

logfire/_internal/integrations/django.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import Any
22

3+
from logfire._internal.utils import maybe_capture_server_headers
4+
35
try:
46
from opentelemetry.instrumentation.django import DjangoInstrumentor
57
except ModuleNotFoundError:
@@ -10,9 +12,10 @@
1012
)
1113

1214

13-
def instrument_django(**kwargs: Any):
15+
def instrument_django(*, capture_headers: bool = False, **kwargs: Any):
1416
"""Instrument the `django` module so that spans are automatically created for each web request.
1517
1618
See the `Logfire.instrument_django` method for details.
1719
"""
20+
maybe_capture_server_headers(capture_headers)
1821
DjangoInstrumentor().instrument(**kwargs) # type: ignore[reportUnknownMemberType]

logfire/_internal/integrations/fastapi.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from ..main import Logfire
1818
from ..stack_info import StackInfo, get_code_object_info
19+
from ..utils import maybe_capture_server_headers
1920

2021
try:
2122
from opentelemetry.instrumentation.asgi import get_host_port_url_tuple # type: ignore
@@ -46,6 +47,7 @@ def instrument_fastapi(
4647
logfire_instance: Logfire,
4748
app: FastAPI,
4849
*,
50+
capture_headers: bool = False,
4951
request_attributes_mapper: Callable[
5052
[
5153
Request | WebSocket,
@@ -68,6 +70,7 @@ def instrument_fastapi(
6870
excluded_urls = ','.join(excluded_urls)
6971

7072
if use_opentelemetry_instrumentation: # pragma: no branch
73+
maybe_capture_server_headers(capture_headers)
7174
FastAPIInstrumentor.instrument_app(app, excluded_urls=excluded_urls, **opentelemetry_kwargs) # type: ignore
7275

7376
registry = patch_fastapi()

logfire/_internal/integrations/flask.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from flask.app import Flask
66
from opentelemetry.instrumentation.flask import FlaskInstrumentor
77

8+
from logfire._internal.utils import maybe_capture_server_headers
9+
810
if TYPE_CHECKING:
911
from wsgiref.types import WSGIEnvironment
1012

@@ -25,9 +27,10 @@ class FlaskInstrumentKwargs(TypedDict, total=False):
2527
commenter_options: dict[str, str] | None
2628

2729

28-
def instrument_flask(app: Flask, **kwargs: Unpack[FlaskInstrumentKwargs]):
30+
def instrument_flask(app: Flask, capture_headers: bool = False, **kwargs: Unpack[FlaskInstrumentKwargs]):
2931
"""Instrument `app` so that spans are automatically created for each request.
3032
3133
See the `Logfire.instrument_flask` method for details.
3234
"""
35+
maybe_capture_server_headers(capture_headers)
3336
FlaskInstrumentor().instrument_app(app, **kwargs) # type: ignore[reportUnknownMemberType]

logfire/_internal/integrations/starlette.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from opentelemetry.instrumentation.starlette import StarletteInstrumentor
66
from starlette.applications import Starlette
77

8+
from logfire._internal.utils import maybe_capture_server_headers
9+
810
if TYPE_CHECKING:
911
from opentelemetry.trace import Span
1012
from typing_extensions import Protocol, TypedDict, Unpack
@@ -24,9 +26,10 @@ class StarletteInstrumentKwargs(TypedDict, total=False):
2426
client_response_hook: ClientResponseHook | None
2527

2628

27-
def instrument_starlette(app: Starlette, **kwargs: Unpack[StarletteInstrumentKwargs]):
29+
def instrument_starlette(app: Starlette, *, capture_headers: bool = False, **kwargs: Unpack[StarletteInstrumentKwargs]):
2830
"""Instrument `app` so that spans are automatically created for each request.
2931
3032
See the `Logfire.instrument_starlette` method for details.
3133
"""
34+
maybe_capture_server_headers(capture_headers)
3235
StarletteInstrumentor().instrument_app(app, **kwargs) # type: ignore[reportUnknownMemberType]

logfire/_internal/main.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@
8888
except ImportError: # pragma: no cover
8989
ValidationError = None
9090

91-
9291
# This is the type of the exc_info/_exc_info parameter of the log methods.
9392
# sys.exc_info() returns a tuple of (type, value, traceback) or (None, None, None).
9493
# We just need the exception, but we allow the user to pass the tuple because:
@@ -822,6 +821,7 @@ def instrument_fastapi(
822821
self,
823822
app: FastAPI,
824823
*,
824+
capture_headers: bool = False,
825825
request_attributes_mapper: Callable[
826826
[
827827
Request | WebSocket,
@@ -838,6 +838,7 @@ def instrument_fastapi(
838838
839839
Args:
840840
app: The FastAPI app to instrument.
841+
capture_headers: Set to `True` to capture all request and response headers.
841842
request_attributes_mapper: A function that takes a [`Request`][fastapi.Request] or [`WebSocket`][fastapi.WebSocket]
842843
and a dictionary of attributes and returns a new dictionary of attributes.
843844
The input dictionary will contain:
@@ -877,6 +878,7 @@ def instrument_fastapi(
877878
return instrument_fastapi(
878879
self,
879880
app,
881+
capture_headers=capture_headers,
880882
request_attributes_mapper=request_attributes_mapper,
881883
excluded_urls=excluded_urls,
882884
use_opentelemetry_instrumentation=use_opentelemetry_instrumentation,
@@ -1064,6 +1066,7 @@ def instrument_celery(self, **kwargs: Unpack[CeleryInstrumentKwargs]) -> None:
10641066

10651067
def instrument_django(
10661068
self,
1069+
capture_headers: bool = False,
10671070
is_sql_commentor_enabled: bool | None = None,
10681071
request_hook: Callable[[Span, HttpRequest], None] | None = None,
10691072
response_hook: Callable[[Span, HttpRequest, HttpResponse], None] | None = None,
@@ -1077,6 +1080,7 @@ def instrument_django(
10771080
library.
10781081
10791082
Args:
1083+
capture_headers: Set to `True` to capture all request and response headers.
10801084
is_sql_commentor_enabled: Adds comments to SQL queries performed by Django,
10811085
so that database logs have additional context.
10821086
@@ -1103,6 +1107,7 @@ def instrument_django(
11031107

11041108
self._warn_if_not_initialized_for_instrumentation()
11051109
return instrument_django(
1110+
capture_headers=capture_headers,
11061111
is_sql_commentor_enabled=is_sql_commentor_enabled,
11071112
request_hook=request_hook,
11081113
response_hook=response_hook,
@@ -1147,29 +1152,37 @@ def instrument_psycopg(self, conn_or_module: Any = None, **kwargs: Unpack[Psycop
11471152
self._warn_if_not_initialized_for_instrumentation()
11481153
return instrument_psycopg(conn_or_module, **kwargs)
11491154

1150-
def instrument_flask(self, app: Flask, **kwargs: Unpack[FlaskInstrumentKwargs]) -> None:
1155+
def instrument_flask(
1156+
self, app: Flask, *, capture_headers: bool = False, **kwargs: Unpack[FlaskInstrumentKwargs]
1157+
) -> None:
11511158
"""Instrument `app` so that spans are automatically created for each request.
11521159
1160+
Set `capture_headers` to `True` to capture all request and response headers.
1161+
11531162
Uses the
11541163
[OpenTelemetry Flask Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/flask/flask.html)
11551164
library, specifically `FlaskInstrumentor().instrument_app()`, to which it passes `**kwargs`.
11561165
"""
11571166
from .integrations.flask import instrument_flask
11581167

11591168
self._warn_if_not_initialized_for_instrumentation()
1160-
return instrument_flask(app, **kwargs)
1169+
return instrument_flask(app, capture_headers=capture_headers, **kwargs)
11611170

1162-
def instrument_starlette(self, app: Starlette, **kwargs: Unpack[StarletteInstrumentKwargs]) -> None:
1171+
def instrument_starlette(
1172+
self, app: Starlette, *, capture_headers: bool = False, **kwargs: Unpack[StarletteInstrumentKwargs]
1173+
) -> None:
11631174
"""Instrument `app` so that spans are automatically created for each request.
11641175
1176+
Set `capture_headers` to `True` to capture all request and response headers.
1177+
11651178
Uses the
11661179
[OpenTelemetry Starlette Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/starlette/starlette.html)
11671180
library, specifically `StarletteInstrumentor.instrument_app()`, to which it passes `**kwargs`.
11681181
"""
11691182
from .integrations.starlette import instrument_starlette
11701183

11711184
self._warn_if_not_initialized_for_instrumentation()
1172-
return instrument_starlette(app, **kwargs)
1185+
return instrument_starlette(app, capture_headers=capture_headers, **kwargs)
11731186

11741187
def instrument_aiohttp_client(self, **kwargs: Any):
11751188
"""Instrument the `aiohttp` module so that spans are automatically created for each client request.

logfire/_internal/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,9 @@ def handle_internal_errors():
247247
yield
248248
except Exception:
249249
log_internal_error()
250+
251+
252+
def maybe_capture_server_headers(capture: bool):
253+
if capture:
254+
os.environ['OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST'] = '.*'
255+
os.environ['OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE'] = '.*'

tests/otel_integrations/test_starlette.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ def app():
3333

3434
app = Starlette(routes=routes)
3535
try:
36-
logfire.instrument_starlette(app)
36+
logfire.instrument_starlette(app, capture_headers=True)
3737
yield app
3838
finally:
3939
StarletteInstrumentor.uninstrument_app(app)
40+
del os.environ['OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST']
41+
del os.environ['OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE']
4042

4143

4244
@pytest.fixture()
@@ -138,6 +140,13 @@ def test_websocket(client: TestClient, exporter: TestExporter) -> None:
138140
'net.peer.ip': 'testclient',
139141
'net.peer.port': 50000,
140142
'http.route': '/ws',
143+
'http.request.header.host': ('testserver',),
144+
'http.request.header.accept': ('*/*',),
145+
'http.request.header.accept_encoding': ('gzip, deflate',),
146+
'http.request.header.user_agent': ('testclient',),
147+
'http.request.header.connection': ('upgrade',),
148+
'http.request.header.sec_websocket_key': ('testserver==',),
149+
'http.request.header.sec_websocket_version': ('13',),
141150
'http.status_code': 200,
142151
},
143152
},

0 commit comments

Comments
 (0)