Python
evolve-logs is the SDK for Python 3.10 or later: FastAPI and Starlette services, Django, Celery or RQ workers, and LangChain or LangGraph agents. One dependency (httpx). Trace context lives in contextvars, so it follows await and spawned tasks.
Install and initialise
pip install evolve-logs # or: uv add evolve-logs pip install "evolve-logs[langchain]" # adds the LangChain / LangGraph handler
import os
from evolve_logs import init, log, span, trace, patch_httpx
init(
key=os.environ["EVOLVE_LOGS_KEY"], # the project's ingest key
url="https://api.e-volv.io/api/public/v1/logs",
service="orders-worker",
environment=os.environ.get("ENV"),
release=os.environ.get("GIT_SHA"),
)
patch_httpx() # inject traceparent into httpx calls made inside a traceAll init() arguments are keyword-only: key, url, service, environment, release, redact_keys, sample_rate (default 1.0), capture_excepthook (default True) and client to supply your own instance. Without a key and URL the client is a no-op and warns once; it never raises into your code.
Logs
log.info("order created", {"orderId": "o_1", "total": 42.5})
log.warn("retrying payment", {"orderId": "o_1", "attempt": 2})
log.error("payment failed", {"orderId": "o_1"})Errors
Pass the exception object. The SDK sends exception.type, exception.message and the formatted traceback as exception.stack, which makes the event an error occurrence with a stack on the group page. sys.excepthook capture is on by default, so an unhandled crash is reported before the process exits.
try:
charge(order)
except Exception as err:
log.exception(err, {"orderId": order.id})
raiseTraces and spans
import httpx
with trace(): # one trace_id for everything inside
with span("db.query", {"table": "orders"}):
db.query("...") # an exception ends the span as failed
httpx.get("https://internal/inventory") # traceparent injectedtrace() starts a root trace; span(name, attrs) is a context manager that records the span end, failed if the body raised. patch_httpx() and patch_requests() inject traceparent into outbound calls inside a trace without overwriting an existing header. traceparent() returns the current value.
Inbound requests and queue hops
from evolve_logs import run_with_traceparent, span, traceparent
# producer
queue.enqueue(work, payload, trace=traceparent())
# consumer: a new hop of the producer's trace
with run_with_traceparent(job.trace), span("queue.work", {"queue": "work"}):
handle(job.payload)An absent or malformed header starts a fresh trace, so a producer that sends nothing still yields a trace of its own.
Frameworks
Standard logging
import logging
from evolve_logs import LogHandler
logging.getLogger("app").addHandler(LogHandler())
logging.getLogger("app").error("payment failed", extra={"orderId": "o_1"}, exc_info=True)Levels map DEBUG 5, INFO 9, WARNING 13, ERROR 17, CRITICAL 21; extra becomes attributes and exc_info becomes exception.*.
FastAPI and Starlette
from evolve_logs.integrations.fastapi import EvolveLogsMiddleware app.add_middleware(EvolveLogsMiddleware) # one root span per request
Pure ASGI, so streaming responses are safe. The middleware continues an inbound trace when the request carries a traceparent (Evolve’s own backend sends x-evolve-traceparent to its Python service) and starts a fresh one otherwise.
LangChain and LangGraph
from evolve_logs.integrations.langchain import EvolveLogsCallbackHandler
result = graph.invoke(
{"question": "..."},
config={"callbacks": [EvolveLogsCallbackHandler()]},
)One span per run, nested by parent_run_id, all in one trace, so a graph node’s tool and model calls appear inside the node on the trace graph. Errors end the span as failed with exception.* attributes. The Node SDK's LangChain.js handler and Vercel AI SDK middleware emit the same span names, so a JavaScript agent and a Python agent read alike.
| Callback | Span name |
|---|---|
| on_chain_start with a langgraph_node | graph.<node> |
| on_chain_start otherwise | chain.<name> |
| on_tool_start | tool.<name> |
| on_llm_start, on_chat_model_start | llm.<model> |
Django
Add a middleware that reads the request’s traceparent header into run_with_traceparent and wraps get_response in a span; attach LogHandler to the django logger for request errors.
Delivery
Batches flush at 200 events, 2 seconds or 512 KB, gzipped. A 429 is retried with exponential backoff up to three times; a 413 halves the batch. When the pending buffer exceeds twice the batch size the oldest events are dropped and counted on client.dropped. atexit flushes what is pending; call get_client().flush() before os._exit.
Package reference: packages/logs-py. Back to SDK overview.