Python SDK
sdk/python (the conduit-observability distribution, imported as conduit_observability)
sends spans from a Python agent to the Conduit gateway over mTLS and gRPC.
It requires Python 3.10 or newer.
For runnable Python agents (hello-trace, tool calling, Claude, OpenAI) that
work against make quickstart with no setup, see Examples.
Quick start (2 minutes)
With a local stack from make quickstart (needs only Docker):
export CONDUIT_CERT_DIR=$PWD/deployments/quickstart/.conduit/certs CONDUIT_AGENT=my-agent
pip install conduit-observability # 0.2.0 or newer
import conduit_observability as conduit
conduit.configure() # reads CONDUIT_CERT_DIR, CONDUIT_AGENT, CONDUIT_GATEWAY_ADDR
conduit.get_client().check() # optional: fail fast if the gateway won't accept us
@conduit.traced() # one span per call, named after the function
def search(query):
...
with conduit.span("agent.run") as run:
search("conduit") # its span is a child of agent.run
run.set_ok()
conduit.shutdown()
Open http://localhost:8080: the trace appears under agent my-agent.
The examples build on this: tool calls, asyncio, Claude, and OpenAI.
Record real LLM usage
llm_call_from_anthropic and llm_call_from_openai turn a provider
response into an LLMCall. Each one captures the model, the text and tool
calls, the real token counts and an estimated cost. They read the response
by attribute, so they never import the provider SDKs. The anthropic and
openai extras install those SDKs for you if you want them.
with conduit.span("llm.call") as llm:
msg = anthropic_client.messages.create(model=..., max_tokens=1024, messages=messages)
llm.record_llm_call(conduit.llm_call_from_anthropic(msg, prompt=user_text))
llm.set_ok()
# OpenAI: conduit.llm_call_from_openai(resp) for Chat Completions or Responses API results
Configuration
conduit.configure() and Client.from_env() read:
| Variable | Meaning |
|---|---|
CONDUIT_CERT_DIR |
Directory with ca.crt and <agent>.crt/<agent>.key (the layout make quickstart and scripts/gen-certs.sh produce). |
CONDUIT_AGENT |
Which agent’s certificate to use from that directory. Optional when it holds exactly one agent (ca and server files are skipped). |
CONDUIT_TLS_CERT, CONDUIT_TLS_KEY, CONDUIT_TLS_CA |
Explicit file paths; they override CONDUIT_CERT_DIR. The 0.1.0 names TLS_CERT, TLS_KEY, TLS_CA still work. |
CONDUIT_GATEWAY_ADDR |
Gateway address, default localhost:4317 (OTEL_GATEWAY_ADDR is accepted too). The host must be in the gateway certificate. |
To be explicit, call conduit.configure(gateway_addr=..., tls_cert=..., tls_key=..., tls_ca=...)
or pass a Client.
Module API vs Client: the module-level span() and @traced never
break your agent. If nothing can be configured, they become no-ops and log
one warning. Use a Client directly for explicit control, for example
raise_errors=True.
When it doesn’t connect
The SDK raises ConfigError (a ConduitError) with what to do when the
client is created:
- Nothing configured.
- Several agents in the directory: it lists them.
- A missing file: points at
make quickstart/scripts/gen-certs.sh. - A key that belongs to another certificate.
- A CA file that isn’t PEM.
check() and every send add a hint to gRPC errors:
- the gateway isn’t running or reachable;
- the host isn’t in the gateway’s certificate;
- a certificate verification or TLS handshake failure, which usually means the agent’s certificate and the gateway’s come from different CAs;
- rate limiting, timeouts.
Production: agent certificates
The process is the same as for the Go SDK. The certificate’s CommonName is
the agent’s identity: the gateway rate-limits by it and records it on every
span as agent_name. See sdk-go.md.
./scripts/gen-certs.sh agent research-bot
Install
pip install conduit-observability
Import it as conduit_observability.
Instrument with a Client
from conduit_observability import Client, LLMCall
client = Client(
"localhost:4317", # host must match a SAN on the gateway cert
tls_cert="certs/research-bot.crt",
tls_key="certs/research-bot.key",
tls_ca="certs/ca.crt", # the only CA trusted for the gateway
)
# or: Client.from_env(); see Configuration
with client.span("agent.run") as run:
run.set_attr("task", "summarize")
with client.span("llm.call") as llm: # child of agent.run
resp = call_model(prompt) # an exception here marks llm as an error
llm.record_llm_call(LLMCall(
model=resp.model, prompt=prompt, completion=resp.text,
input_tokens=resp.input_tokens, output_tokens=resp.output_tokens,
cost_usd=resp.cost_usd,
))
llm.set_ok()
client.close() # or use `with Client(...) as client:`
Spans opened with client.span(...) nest automatically. The current span
is tracked with contextvars, so nesting is correct per thread and per
asyncio task, and a single Client can be shared by the whole process.
API
| Call | What it does |
|---|---|
conduit.configure(client=None, **kwargs) |
Sets the module-level client (from the environment by default). get_client() returns it; shutdown() closes it. |
conduit.span(name) / @conduit.traced(name=None, **attrs) |
A span on the module-level client: a context manager, or a decorator for sync and async functions (OK on return, error if it raises). current_span() is the innermost open span. |
llm_call_from_anthropic(msg, prompt="") / llm_call_from_openai(resp, prompt="") |
An LLMCall from a provider response, with real usage and an estimated cost. |
Client.from_env(timeout=10.0) / client.check() |
Builds a client from the environment; checks the gateway accepts it (raises a diagnosed ConduitError if not). |
Client(addr, tls_cert, tls_key, tls_ca, timeout=10.0) |
Opens a TLS channel to the gateway. The gateway’s certificate is verified against tls_ca only. |
client.span(name, *, raise_errors=False) |
A context manager. It opens a child of the current span, makes it current, and ends and sends it on exit. If the block raises, the span is marked as an error with error.message, and the exception propagates. |
client.start_span(name) |
Starts a child of the current span without making it current. You call end() yourself. |
span.set_attr(k, v) |
Adds a string attribute. The gateway allows up to 128 attributes, keys up to 128 bytes and values up to 4096 bytes. A span over those limits is rejected. |
span.set_ok() / span.set_error(err) |
Sets the span status. set_error stores the message in error.message, cut to 4096 bytes. set_error(None) does nothing. |
span.record_llm_call(LLMCall(...)) |
Attaches model, prompt, completion, token counts and cost. Prompts and completions are capped at 128 KiB each. |
span.end() |
Sends the span once; calling it again does nothing. Raises ConduitError if delivery fails or the gateway rejects the span. |
client.log(severity, body, attributes=None) |
Sends one log record (SEVERITY_DEBUG, SEVERITY_INFO, SEVERITY_WARN or SEVERITY_ERROR). Inside with client.span(...) it’s linked to that span and trace. Raises ConduitError if it’s rejected. Bodies are limited to 32 KiB. |
client.gauge(name, value, labels=None) / client.counter(...) |
Sends one metric sample. The same naming rules apply as in the Go SDK: Prometheus names, conduit_ and __ reserved, and the agent label set from your certificate. |
span.trace_id, span.span_id |
32-hex and 16-hex IDs, for log correlation. |
Delivery errors
span.end()raisesConduitErrorwhen a span isn’t accepted, for example if the gateway is unreachable, the agent is over its rate limit, or validation fails.with client.span(...)logs delivery failures to theconduit_observabilitylogger by default, so instrumentation can never crash your agent. Passraise_errors=Trueto getConduitErrorinstead. Even then, an exception raised by your own code inside the block always wins and is never masked.
Things to know
- Synchronous delivery: each span is one blocking RPC, sent when the
span ends. Set
timeoutif the gateway might be slow. - No retries: the SDK doesn’t retry failed sends.
- Identity comes from the certificate: setting an
agent_nameattribute, or passing the legacyagent_nameargument, doesn’t change the agent’s identity. - Price table:
estimate_cost_usd(model, in, out)uses a small built-in table that will go out of date. Prefer the cost your provider reports.
Development
cd sdk/python
uv venv && uv pip install -e '.[dev]' # or python -m venv / pip
.venv/bin/python -m pytest
The tests run against an in-process gRPC server that requires client
certificates, using certificates generated on the fly. The generated stubs
in src/conduit_observability/_proto are committed. After editing proto/*.proto,
regenerate them with make proto, which also regenerates the Go code.