Skip to content

Commit 4bde7f1

Browse files
Add capture_headers to logfire.instrument_aiohttp_client (#1405)
Co-authored-by: Alex Hall <[email protected]>
1 parent 92e1cd7 commit 4bde7f1

File tree

4 files changed

+463
-10
lines changed

4 files changed

+463
-10
lines changed
Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
from typing import Any
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any, Callable, Literal
4+
5+
import attr
6+
from aiohttp.client_reqrep import ClientResponse
7+
from aiohttp.tracing import TraceRequestEndParams, TraceRequestExceptionParams, TraceRequestStartParams
8+
from opentelemetry.trace import Span
9+
from yarl import URL
210

311
try:
412
from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor
@@ -8,18 +16,164 @@
816
'You can install this with:\n'
917
" pip install 'logfire[aiohttp-client]'"
1018
)
19+
1120
from logfire import Logfire
21+
from logfire._internal.utils import handle_internal_errors
22+
from logfire.integrations.aiohttp_client import AioHttpRequestHeaders, AioHttpResponseHeaders, RequestHook, ResponseHook
23+
24+
if TYPE_CHECKING:
25+
from typing import ParamSpec
26+
27+
P = ParamSpec('P')
1228

1329

14-
def instrument_aiohttp_client(logfire_instance: Logfire, **kwargs: Any):
30+
def instrument_aiohttp_client(
31+
logfire_instance: Logfire,
32+
capture_headers: bool,
33+
request_hook: RequestHook | None,
34+
response_hook: ResponseHook | None,
35+
**kwargs: Any,
36+
) -> None:
1537
"""Instrument the `aiohttp` module so that spans are automatically created for each client request.
1638
1739
See the `Logfire.instrument_aiohttp_client` method for details.
1840
"""
41+
logfire_instance = logfire_instance.with_settings(custom_scope_suffix='aiohttp_client')
42+
1943
AioHttpClientInstrumentor().instrument(
2044
**{
2145
'tracer_provider': logfire_instance.config.get_tracer_provider(),
46+
'request_hook': make_request_hook(request_hook, capture_headers),
47+
'response_hook': make_response_hook(
48+
response_hook,
49+
logfire_instance,
50+
capture_headers,
51+
),
2252
'meter_provider': logfire_instance.config.get_meter_provider(),
2353
**kwargs,
2454
},
2555
)
56+
57+
58+
class LogfireClientInfoMixin:
59+
headers: AioHttpRequestHeaders
60+
61+
62+
@attr.s(auto_attribs=True, frozen=True, slots=True)
63+
class LogfireAioHttpRequestInfo(TraceRequestStartParams, LogfireClientInfoMixin):
64+
span: Span
65+
66+
def capture_headers(self):
67+
capture_request_or_response_headers(self.span, self.headers, 'request')
68+
69+
70+
@attr.s(auto_attribs=True, frozen=True, slots=True)
71+
class LogfireAioHttpResponseInfo(LogfireClientInfoMixin):
72+
span: Span
73+
method: str
74+
url: URL
75+
headers: AioHttpRequestHeaders
76+
response: ClientResponse | None
77+
exception: BaseException | None
78+
logfire_instance: Logfire
79+
80+
def capture_headers(self):
81+
if self.response:
82+
capture_request_or_response_headers(self.span, self.response.headers, 'response')
83+
84+
@classmethod
85+
def create_from_trace_params(
86+
cls,
87+
span: Span,
88+
params: TraceRequestEndParams | TraceRequestExceptionParams,
89+
logfire_instance: Logfire,
90+
) -> LogfireAioHttpResponseInfo:
91+
return cls(
92+
span=span,
93+
method=params.method,
94+
url=params.url,
95+
headers=params.headers,
96+
response=getattr(params, 'response', None),
97+
exception=getattr(params, 'exception', None),
98+
logfire_instance=logfire_instance,
99+
)
100+
101+
102+
def make_request_hook(hook: RequestHook | None, capture_headers: bool) -> RequestHook | None:
103+
if not (capture_headers or hook):
104+
return None
105+
106+
def new_hook(span: Span, request: TraceRequestStartParams) -> None:
107+
with handle_internal_errors:
108+
capture_request(span, request, capture_headers)
109+
run_hook(hook, span, request)
110+
111+
return new_hook
112+
113+
114+
def make_response_hook(
115+
hook: ResponseHook | None,
116+
logfire_instance: Logfire,
117+
capture_headers: bool,
118+
) -> ResponseHook | None:
119+
if not (capture_headers or hook):
120+
return None
121+
122+
def new_hook(span: Span, response: TraceRequestEndParams | TraceRequestExceptionParams) -> None:
123+
with handle_internal_errors:
124+
capture_response(
125+
span,
126+
response,
127+
logfire_instance,
128+
capture_headers,
129+
)
130+
run_hook(hook, span, response)
131+
132+
return new_hook
133+
134+
135+
def capture_request(
136+
span: Span,
137+
request: TraceRequestStartParams,
138+
capture_headers: bool,
139+
) -> LogfireAioHttpRequestInfo:
140+
request_info = LogfireAioHttpRequestInfo(method=request.method, url=request.url, headers=request.headers, span=span)
141+
142+
if capture_headers:
143+
request_info.capture_headers()
144+
145+
return request_info
146+
147+
148+
def capture_response(
149+
span: Span,
150+
response: TraceRequestEndParams | TraceRequestExceptionParams,
151+
logfire_instance: Logfire,
152+
capture_headers: bool,
153+
) -> LogfireAioHttpResponseInfo:
154+
response_info = LogfireAioHttpResponseInfo.create_from_trace_params(
155+
span=span, params=response, logfire_instance=logfire_instance
156+
)
157+
158+
if capture_headers:
159+
response_info.capture_headers()
160+
161+
return response_info
162+
163+
164+
def run_hook(hook: Callable[P, Any] | None, *args: P.args, **kwargs: P.kwargs) -> None:
165+
if hook:
166+
hook(*args, **kwargs)
167+
168+
169+
def capture_request_or_response_headers(
170+
span: Span,
171+
headers: AioHttpRequestHeaders | AioHttpResponseHeaders,
172+
request_or_response: Literal['request', 'response'],
173+
) -> None:
174+
span.set_attributes(
175+
{
176+
f'http.{request_or_response}.header.{header_name}': headers.getall(header_name)
177+
for header_name in headers.keys()
178+
}
179+
)

logfire/_internal/main.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@
8585
from starlette.websockets import WebSocket
8686
from typing_extensions import Unpack
8787

88+
from ..integrations.aiohttp_client import (
89+
RequestHook as AiohttpClientRequestHook,
90+
ResponseHook as AiohttpClientResponseHook,
91+
)
8892
from ..integrations.flask import (
8993
CommenterOptions as FlaskCommenterOptions,
9094
RequestHook as FlaskRequestHook,
@@ -1746,7 +1750,14 @@ def instrument_wsgi(
17461750
},
17471751
)
17481752

1749-
def instrument_aiohttp_client(self, **kwargs: Any) -> None:
1753+
def instrument_aiohttp_client(
1754+
self,
1755+
*,
1756+
capture_headers: bool = False,
1757+
request_hook: AiohttpClientRequestHook | None = None,
1758+
response_hook: AiohttpClientResponseHook | None = None,
1759+
**kwargs: Any,
1760+
) -> None:
17501761
"""Instrument the `aiohttp` module so that spans are automatically created for each client request.
17511762
17521763
Uses the
@@ -1756,7 +1767,13 @@ def instrument_aiohttp_client(self, **kwargs: Any) -> None:
17561767
from .integrations.aiohttp_client import instrument_aiohttp_client
17571768

17581769
self._warn_if_not_initialized_for_instrumentation()
1759-
return instrument_aiohttp_client(self, **kwargs)
1770+
return instrument_aiohttp_client(
1771+
self,
1772+
capture_headers=capture_headers,
1773+
request_hook=request_hook,
1774+
response_hook=response_hook,
1775+
**kwargs,
1776+
)
17601777

17611778
def instrument_aiohttp_server(self, **kwargs: Any) -> None:
17621779
"""Instrument the `aiohttp` module so that spans are automatically created for each server request.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Callable
4+
from typing import Union
5+
6+
from aiohttp.tracing import TraceRequestEndParams, TraceRequestExceptionParams, TraceRequestStartParams
7+
from multidict import CIMultiDict, CIMultiDictProxy
8+
from opentelemetry.trace import Span
9+
10+
AioHttpRequestHeaders = CIMultiDict[str]
11+
AioHttpResponseHeaders = CIMultiDictProxy[str]
12+
RequestHook = Callable[[Span, TraceRequestStartParams], None]
13+
ResponseHook = Callable[[Span, Union[TraceRequestEndParams, TraceRequestExceptionParams]], None]

0 commit comments

Comments
 (0)