Recount
Python

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

ParameterTypeDescription
namestrLabel for the span in the dashboard
urlstrRequest URL
methodstrHTTP method. Default: "GET"
headersdict[str, str]Request headers
bodyAnyRequest 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
redactlist[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, so redact paths 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, PATCH and DELETE are sent as given but recorded as GET.

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,
})

On this page