HTTP tracing
Trace outbound HTTP requests in Python with recount.fetch, which records the request and response as a span.
recount.fetch(name, url, method="GET", headers=None, body=None, redact=None)
A traced HTTP call that returns the httpx.Response and records the request and response as an http span on the active trace. Check the status and read the body as you would with httpx:
async def load_policy():
res = await recount.fetch(
"Fetch policy", "https://api.example.com/policies/POL-8821-AU"
)
res.raise_for_status()
return res.json()
await recount.start_trace("Load policy", load_policy)The span captures:
- Request method, URL, headers, and body
- Response status, headers, and body
- Duration in milliseconds
Bodies are captured as JSON when they parse as JSON (whatever the content-type says), and as text otherwise. Binary responses (images, PDFs, archives, application/octet-stream) aren't captured, and captured bodies, URLs and headers are trimmed to fit the ingest limits.
If the request fails without a response (DNS failure, refused connection, timeout), the span is still recorded with the request and the error as its description, and the exception is re-raised.
Parameters
| Parameter | Type | Description |
|---|---|---|
name | str | Label for the span in the dashboard |
url | str | Request URL |
method | str | HTTP method. Default: "GET" |
headers | dict[str, str] | Request headers |
body | Any | Request body - strings and bytes are sent as-is, a dict is form-encoded if the content-type is a form type, anything else is sent as JSON |
redact | list[str] | Fields to redact from request/response body. See Redaction |
Returns the httpx.Response.
POST example
res = await recount.fetch(
"create-record",
"https://api.example.com/records",
method="POST",
body={"name": "Acme Corp"},
)
created = res.json()Redacting body fields
Pass redact to strip sensitive fields from the captured request or response body. Redaction only affects the span; the response you get back is unchanged:
res = await recount.fetch(
"get-user",
"https://api.example.com/users/1",
redact=["user.ssn", "user.dateOfBirth"],
)Authorization headers are always redacted automatically - see Redaction.
Limits
To keep a trace within what ingest accepts, the captured span (never the request or response itself) is trimmed:
- Text bodies longer than 65,536 characters are truncated.
- JSON bodies longer than 65,536 characters are replaced with a short
[JSON body omitted: ...]note. Truncated JSON can't be parsed, soredactpaths couldn't be applied to it. - URLs are truncated to 2,048 characters.
- At most 50 headers are kept, each value truncated to 1,024 characters.
- Methods other than
GET,POST,PUT,PATCHandDELETEare sent as given but recorded asGET.
Migrating
recount.fetch used to return the parsed body (or None). It now returns the httpx.Response:
# before
policy = await recount.fetch("Fetch policy", url)
# after
res = await recount.fetch("Fetch policy", url)
policy = res.json()Manual HTTP spans
If you need to capture HTTP details from a request you made yourself, use add_span with type "http":
recount.add_span("http", "Fraud check", {
"input": {
"method": "POST",
"url": "https://api.fraudservice.com/v1/check",
"content_type": "json",
"body": {"claimId": "CLM-2024-03941", "amount": 6840},
},
"output": {
"status": 200,
"content_type": "json",
"body": {"score": 0.03, "verdict": "low_risk"},
},
"durationMs": 340,
})