Skip to content

Commit 425b441

Browse files
authored
Add capture_all to instrument_httpx (#780)
1 parent 724fd14 commit 425b441

File tree

3 files changed

+111
-4
lines changed

3 files changed

+111
-4
lines changed

logfire/_internal/integrations/httpx.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
def instrument_httpx(
4545
logfire_instance: Logfire,
4646
client: httpx.Client | httpx.AsyncClient | None,
47+
capture_all: bool,
4748
capture_headers: bool,
4849
capture_request_body: bool,
4950
capture_response_body: bool,
@@ -57,6 +58,11 @@ def instrument_httpx(
5758
5859
See the `Logfire.instrument_httpx` method for details.
5960
"""
61+
if capture_all and (capture_headers or capture_request_body or capture_response_body):
62+
warn_at_user_stacklevel(
63+
'You should use either `capture_all` or the specific capture parameters, not both.', UserWarning
64+
)
65+
6066
capture_request_headers = kwargs.get('capture_request_headers')
6167
capture_response_headers = kwargs.get('capture_response_headers')
6268

@@ -69,10 +75,10 @@ def instrument_httpx(
6975
'The `capture_response_headers` parameter is deprecated. Use `capture_headers` instead.', DeprecationWarning
7076
)
7177

72-
should_capture_request_headers = capture_request_headers or capture_headers
73-
should_capture_response_headers = capture_response_headers or capture_headers
74-
should_capture_request_body = capture_request_body
75-
should_capture_response_body = capture_response_body
78+
should_capture_request_headers = capture_request_headers or capture_headers or capture_all
79+
should_capture_response_headers = capture_response_headers or capture_headers or capture_all
80+
should_capture_request_body = capture_request_body or capture_all
81+
should_capture_response_body = capture_response_body or capture_all
7682

7783
final_kwargs: dict[str, Any] = {
7884
'tracer_provider': logfire_instance.config.get_tracer_provider(),

logfire/_internal/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,7 @@ def instrument_httpx(
11831183
self,
11841184
client: httpx.Client,
11851185
*,
1186+
capture_all: bool = False,
11861187
capture_headers: bool = False,
11871188
capture_request_body: bool = False,
11881189
capture_response_body: bool = False,
@@ -1196,6 +1197,7 @@ def instrument_httpx(
11961197
self,
11971198
client: httpx.AsyncClient,
11981199
*,
1200+
capture_all: bool = False,
11991201
capture_headers: bool = False,
12001202
capture_request_body: bool = False,
12011203
capture_response_body: bool = False,
@@ -1209,6 +1211,7 @@ def instrument_httpx(
12091211
self,
12101212
client: None = None,
12111213
*,
1214+
capture_all: bool = False,
12121215
capture_headers: bool = False,
12131216
capture_request_body: bool = False,
12141217
capture_response_body: bool = False,
@@ -1223,6 +1226,7 @@ def instrument_httpx(
12231226
self,
12241227
client: httpx.Client | httpx.AsyncClient | None = None,
12251228
*,
1229+
capture_all: bool = False,
12261230
capture_headers: bool = False,
12271231
capture_request_body: bool = False,
12281232
capture_response_body: bool = False,
@@ -1243,6 +1247,7 @@ def instrument_httpx(
12431247
Args:
12441248
client: The `httpx.Client` or `httpx.AsyncClient` instance to instrument.
12451249
If `None`, the default, all clients will be instrumented.
1250+
capture_all: Set to `True` to capture all HTTP headers, request and response bodies.
12461251
capture_headers: Set to `True` to capture all HTTP headers.
12471252
12481253
If you don't want to capture all headers, you can customize the headers captured. See the
@@ -1261,6 +1266,7 @@ def instrument_httpx(
12611266
return instrument_httpx(
12621267
self,
12631268
client,
1269+
capture_all=capture_all,
12641270
capture_headers=capture_headers,
12651271
capture_request_body=capture_request_body,
12661272
capture_response_body=capture_response_body,

tests/otel_integrations/test_httpx.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,101 @@ def test_is_json_type():
673673
assert not is_json_type('application//json')
674674

675675

676+
async def test_httpx_client_capture_all(exporter: TestExporter):
677+
with check_traceparent_header() as checker:
678+
async with httpx.AsyncClient(transport=create_transport()) as client:
679+
logfire.instrument_httpx(client, capture_all=True)
680+
response = await client.post('https://example.org/', json={'hello': 'world'})
681+
checker(response)
682+
assert response.json() == {'good': 'response'}
683+
assert await response.aread() == b'{"good": "response"}'
684+
685+
assert exporter.exported_spans_as_dict() == snapshot(
686+
[
687+
{
688+
'name': 'POST',
689+
'context': {'trace_id': 1, 'span_id': 3, 'is_remote': False},
690+
'parent': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
691+
'start_time': 2000000000,
692+
'end_time': 3000000000,
693+
'attributes': {
694+
'http.method': 'POST',
695+
'http.request.method': 'POST',
696+
'http.url': 'https://example.org/',
697+
'url.full': 'https://example.org/',
698+
'http.host': 'example.org',
699+
'server.address': 'example.org',
700+
'network.peer.address': 'example.org',
701+
'logfire.span_type': 'span',
702+
'logfire.msg': 'POST /',
703+
'http.request.header.host': ('example.org',),
704+
'http.request.header.accept': ('*/*',),
705+
'http.request.header.accept-encoding': ('gzip, deflate',),
706+
'http.request.header.connection': ('keep-alive',),
707+
'http.request.header.user-agent': ('python-httpx/0.28.1',),
708+
'http.request.header.content-length': ('17',),
709+
'http.request.header.content-type': ('application/json',),
710+
'logfire.json_schema': '{"type":"object","properties":{"http.request.body.text":{"type":"object"}}}',
711+
'http.request.body.text': '{"hello":"world"}',
712+
'http.status_code': 200,
713+
'http.response.status_code': 200,
714+
'http.flavor': '1.1',
715+
'network.protocol.version': '1.1',
716+
'http.response.header.host': ('example.org',),
717+
'http.response.header.accept': ('*/*',),
718+
'http.response.header.accept-encoding': ('gzip, deflate',),
719+
'http.response.header.connection': ('keep-alive',),
720+
'http.response.header.user-agent': ('python-httpx/0.28.1',),
721+
'http.response.header.content-length': ('17',),
722+
'http.response.header.content-type': ('application/json',),
723+
'http.response.header.traceparent': ('00-00000000000000000000000000000001-0000000000000003-01',),
724+
'http.target': '/',
725+
},
726+
},
727+
{
728+
'name': 'Reading response body',
729+
'context': {'trace_id': 1, 'span_id': 5, 'is_remote': False},
730+
'parent': {'trace_id': 1, 'span_id': 3, 'is_remote': False},
731+
'start_time': 4000000000,
732+
'end_time': 5000000000,
733+
'attributes': {
734+
'code.filepath': 'test_httpx.py',
735+
'code.function': 'test_httpx_client_capture_all',
736+
'code.lineno': 123,
737+
'logfire.msg_template': 'Reading response body',
738+
'logfire.msg': 'Reading response body',
739+
'logfire.span_type': 'span',
740+
'http.response.body.text': '{"good": "response"}',
741+
'logfire.json_schema': '{"type":"object","properties":{"http.response.body.text":{}}}',
742+
},
743+
},
744+
{
745+
'name': 'test span',
746+
'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
747+
'parent': None,
748+
'start_time': 1000000000,
749+
'end_time': 6000000000,
750+
'attributes': {
751+
'code.filepath': 'test_httpx.py',
752+
'code.function': 'check_traceparent_header',
753+
'code.lineno': 123,
754+
'logfire.msg_template': 'test span',
755+
'logfire.msg': 'test span',
756+
'logfire.span_type': 'span',
757+
},
758+
},
759+
]
760+
)
761+
762+
763+
def test_httpx_capture_all_and_other_flags_should_warn(exporter: TestExporter):
764+
with httpx.Client(transport=create_transport()) as client:
765+
with pytest.warns(
766+
UserWarning, match='You should use either `capture_all` or the specific capture parameters, not both.'
767+
):
768+
logfire.instrument_httpx(client, capture_all=True, capture_request_body=True)
769+
770+
676771
def test_missing_opentelemetry_dependency() -> None:
677772
with mock.patch.dict('sys.modules', {'opentelemetry.instrumentation.httpx': None}):
678773
with pytest.raises(RuntimeError) as exc_info:

0 commit comments

Comments
 (0)