Automatic instrumentation
The default path. Add one line — tracely.init() — and your existing OpenAI / Anthropic /
LangChain / LiteLLM code is traced into Tracely with no span code: model, messages, token usage
(including streaming), latency, tool calls, and errors are captured for you. Manual spans
(custom spans) remain available for anything bespoke.
Install with a provider extra
pip install "tracely-ai[openai]" # or [anthropic], [langchain], [litellm], [all]Each extra pulls the matching auto-instrumentor. Tracely adopts the OpenTelemetry ecosystem rather
than reinventing it: the extras install OpenInference
(Arize) packages by default; OpenLLMetry (Traceloop) is
an equivalent alternative that init("auto") also detects (pip install "tracely-ai[openllmetry]").
The backend ingests both conventions (gen_ai.* and llm.*) independently, so either works.
One-call setup
Initialize once at startup
import tracely_sdk as tracely
tracely.init(
endpoint="http://localhost:8000", # your Tracely API
api_key="tracely_dev_key", # an ingest key
service_name="weather-agent",
env="prod", # prod | staging | ci | dev — the gating axis
instrument="auto", # activate every importable provider instrumentor
)instrument accepts "auto" (detect installed SDKs), an explicit list like ["openai", "anthropic"], or False (export only — use the manual API). init() is idempotent and safe to
call once at startup.
Call your provider normally — no span code
from openai import OpenAI
client = OpenAI()
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Why is the sky blue?"}],
)That call appears in Tracely as a GENERATION span with the model, input/output messages, token
counts, latency, tool calls, and (on error) level=ERROR. Sync, async, and streamed calls all work.
Streaming token usage. OpenAI omits usage on streamed responses unless you ask for it — pass
stream_options={"include_usage": True} so token (and therefore cost) data isn’t lost on streams.
Attach run context — tracely.trace(...)
Provider spans are created by the instrumentor, which knows nothing about Tracely. Wrap a run in
tracely.trace(...) and Tracely stamps the run’s agent / conversation / turn / user / env
(plus arbitrary metadata) onto every span inside it — auto-captured or manual — via a custom
span processor. No per-call plumbing.
with tracely.trace(agent="weather-agent", conversation="conv-1", user="u_7", env="prod", tenant="acme"):
client.chat.completions.create(model="gpt-4o", messages=[...]) # inherits all of the abovetrace() also works as a decorator (sync or async):
@tracely.trace(agent="weather-agent", conversation="conv-1")
async def handle(request): ...It sets context only — it doesn’t open a span. Nested trace()s merge over the enclosing one.
Function-level spans — @observe
@observe turns any function into a span: arguments → input, return value → output, latency, and
exceptions (→ level=ERROR) captured automatically, auto-nested via OpenTelemetry context with
no manual parent wiring. as_type sets the observation type.
@tracely.observe(as_type="tool")
def get_weather(city: str) -> dict:
return {"city": city, "tempF": 64}
@tracely.observe(as_type="agent")
def weather_agent(question: str) -> str:
client.chat.completions.create(model="gpt-4o", messages=[...]) # GENERATION (auto)
get_weather("SF") # TOOL (@observe)
return client.chat.completions.create(model="gpt-4o", messages=[...]).choices[0].message.content
with tracely.trace(agent="weather-agent", conversation="conv-1"):
weather_agent("What's the weather in SF?")This yields a 4-span tree — weather-agent (AGENT) → decide (GENERATION) · get_weather (TOOL) ·
answer (GENERATION) — all carrying the agent/conversation from the enclosing trace().
as_type ∈ span · generation · agent · tool · chain · retriever · embedding ·
guardrail · … (capture_input / capture_output default True). See
sdk/examples/auto_agent.py.
The layers compose
L1 auto-instrumentors OpenAI · Anthropic · LangChain · LiteLLM ← default, zero span code
L2 @observe(as_type=…) arbitrary fns / agents / tools ← one decorator
L3 tracely.trace(…) run context (agent/conversation/turn/user) ← tags flow onto every span
L4 with tracely.llm(…) manual spans ← escape hatch (custom spans)All four nest into one trace via OpenTelemetry context. Use as much or as little as you need.
LangChain & LangGraph
pip install "tracely-ai[langchain]"Chains, agents, and LangGraph graphs trace end-to-end with no manual spans — init()
auto-registers the LangChain callback handler. Build agents with the current LangChain 1.0+ API,
from langchain.agents import create_agent (it replaces the deprecated create_react_agent and
create_tool_calling_agent + AgentExecutor):
from langchain.agents import create_agent
agent = create_agent("openai:gpt-5.4-mini", tools=[get_order_status], system_prompt="…")
agent.invoke({"messages": [{"role": "user", "content": "…"}]})A LangGraph run nests correctly: the graph is a CHAIN span, each node a child CHAIN (its name +
step number become the span’s step_name / step_id), and the LLM calls inside are GENERATION
spans — all carrying the enclosing tracely.trace(...) context.
LangChain + provider de-dup. LangChain calls providers through their SDKs, so running both
the LangChain instrumentor and a provider instrumentor would double-trace those calls (two spans).
Under instrument="auto", when the LangChain instrumentor is installed it owns the LLM spans and
Tracely skips the OpenAI/Anthropic instrumentors. Want both (e.g. you also make direct OpenAI
calls)? Pass an explicit list: instrument=["openai", "langchain"].
Non-patching drop-in (wrap_openai)
Prefer not to monkey-patch globally? Wrap a client instance instead — only that client is traced, nothing global changes:
from tracely_sdk.openai import OpenAI # a pre-wrapped client
client = OpenAI()
client.chat.completions.create(model="gpt-4o", messages=[...]) # GENERATION span, no global patch
# or wrap one you already built / use the module import:
from tracely_sdk.openai import wrap_openai, openai
client = wrap_openai(OpenAI())
openai.OpenAI().chat.completions.create(...)Anthropic has the same drop-in — from tracely_sdk.anthropic import Anthropic, wrap_anthropic. Both
emit the same attributes as the manual llm() helper, so they inherit tracely.trace(...) and map
identically. Non-streaming sync + async calls capture model · messages · output · usage · tool calls;
for full streaming capture, prefer the instrumentor path above.
OpenRouter & other OpenAI-compatible gateways
OpenRouter routes one API to 100+ models, and needs no special instrumentor. The recommended path is
LangChain’s first-party ChatOpenRouter (langchain-openrouter) inside create_agent, traced by
the LangChain instrumentor:
from langchain.agents import create_agent
from langchain_openrouter import ChatOpenRouter # pip install langchain-openrouter
agent = create_agent(ChatOpenRouter(model="anthropic/claude-3.5-sonnet"), tools=[...], system_prompt="…")OpenRouter is also OpenAI-wire-compatible, so pointing the OpenAI SDK at its base_url works too —
traced by the OpenAI instrumentor:
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])
client.chat.completions.create(model="anthropic/claude-3.5-sonnet", messages=[...]) # tracedEither way the routed model id (vendor/model) flows into model_id, and cost is derived from it
(the rate table matches on substring, so openai/gpt-4o → gpt-4o pricing).
Agent frameworks — first-party SDKs
The big labs ship their own agent harnesses, each with an OpenInference instrumentor that
init(instrument=[...]) activates (emitting AGENT/TOOL/LLM spans into Tracely):
| Framework | instrument= | Extra (+ SDK) |
|---|---|---|
OpenAI Agents SDK (agents) | ["openai-agents"] | [openai-agents] + openai-agents |
| Anthropic Claude Agent SDK | ["claude-agent-sdk"] | [claude-agent-sdk] + claude-agent-sdk |
Google ADK (google.adk) | ["google-adk"] | [google-adk] + google-adk |
tracely.init(instrument=["openai-agents"]) # then use the SDK normally — runs are traced
from agents import Agent, Runner, function_toolGoogle ADK patches at import time, so init(instrument=["google-adk"]) must run before you
import google.adk. The Claude Agent SDK needs the Claude Code CLI installed and is async-only.
LiteLLM — 100+ providers through one path
LiteLLM is opt-in (it’s a router, not a provider SDK):
tracely.init(instrument=["litellm"]) # wires litellm.callbacks = ["otel"]Avoid double-instrumentation. A call traced by both a provider instrumentor and LiteLLM’s
OTel callback would appear twice. init() activates one path per provider, and "auto" excludes
LiteLLM for this reason. If you run both deliberately, disable the overlap with
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS.
Threads
Auto-nesting is in-process (contextvar-based). To trace work on another thread, copy the context so its spans nest correctly:
th = tracely.run_in_thread(do_work, arg) # inherits the current span + trace() context
th.join()
result = th.resultWhat’s next
- Custom spans — the manual API: retrievers, embeddings, guardrails, thinking, handoffs, multimodal content. The escape hatch for anything the auto path doesn’t cover.
- Core concepts — observation types and how runs thread into conversations.