Vendor-neutral agent tracing library: a thin wrapper over the OpenTelemetry SDK with pluggable OTLP backend adapters
Project description
anytrace
Vendor-neutral agent tracing: a thin wrapper over the OpenTelemetry SDK, standardized on OTel GenAI semantic conventions, with pluggable OTLP backend adapters (LangSmith, Langfuse — self-hosted or cloud). Switching or adding a backend requires zero code changes — configuration only. No vendor SDKs, no framework dependencies.
Quickstart
pip install anytrace
import anytrace
anytrace.init() # reads TRACING_* env vars
@anytrace.traced()
def plan_step(question: str) -> str:
anytrace.add_metadata(strategy="react")
return "search the docs"
def answer(question: str) -> str:
with anytrace.trace_span("answer-question", kind="server"):
anytrace.set_user("user-42")
anytrace.set_session("sess-7")
plan = plan_step(question)
with anytrace.record_generation("claude-sonnet-5", system="anthropic",
temperature=0.2) as gen:
completion = call_your_llm(question, plan)
gen.set_usage(input_tokens=1200, output_tokens=250, cost=0.004)
gen.set_content(prompt=question, completion=completion) # only if enabled
return completion
# ... at process exit
anytrace.shutdown()
Run it against Langfuse:
export TRACING_BACKEND=langfuse TRACING_SERVICE_NAME=my-agent
export LANGFUSE_ENDPOINT=https://langfuse.internal \
LANGFUSE_PUBLIC_KEY=pk-... LANGFUSE_SECRET_KEY=sk-...
python app.py
Flip to LangSmith — same code, different env:
export TRACING_BACKEND=langsmith
export LANGSMITH_ENDPOINT=https://langsmith.internal LANGSMITH_API_KEY=lsv2_pt_...
python app.py
Or fan out to both during a migration: TRACING_BACKEND=langsmith,langfuse.
Both renderings were verified live (hierarchy, generation typing, tokens, cost,
user/session) — see docs/backend-verification.md,
including a LangSmith thread-view screenshot.
Tutorials
New to the library? Work through docs/tutorials/ — eight hands-on, runnable tutorials from first trace to multi-backend migration.
Configuration reference
Env vars first; a YAML file pointed to by TRACING_CONFIG_FILE overrides them.
| Env var | Required | Default | Meaning |
|---|---|---|---|
TRACING_BACKEND |
yes | — | Comma-separated: langsmith, langfuse |
TRACING_SERVICE_NAME |
yes | — | service.name resource attribute |
TRACING_ENVIRONMENT |
no | development |
deployment.environment resource attribute |
TRACING_CAPTURE_CONTENT |
no | false |
Record prompts/completions (gen_ai.prompt/gen_ai.completion) |
TRACING_ENABLED |
no | true |
Kill switch: false makes init() and all tracing a no-op |
TRACING_STRICT |
no | false |
Fail init() fast if a backend rejects an export probe (default: fail open) |
TRACING_SAMPLE_RATE |
no | 1.0 |
0.0–1.0 trace sampling ratio (parent-based, distributed-safe) |
TRACING_MASK_PATTERNS |
no | — | Comma-separated regexes; matches become *** in captured content AND custom attribute values (or pass mask=callable to init()). Identifiers (user/session/tags) are not masked |
TRACING_CONFIG_FILE |
no | — | Path to YAML override file |
LANGSMITH_PROJECT |
no | default |
Routes traces to a LangSmith project via the Langsmith-Project header |
LANGSMITH_ENDPOINT |
with langsmith | — | Base URL (cloud or self-hosted; OTLP path appended automatically) |
LANGSMITH_API_KEY |
with langsmith | — | Sent as x-api-key (LangSmith cloud: use a PAT lsv2_pt_...) |
LANGFUSE_ENDPOINT |
with langfuse | — | Base URL; /api/public/otel/v1/traces appended |
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY |
with langfuse | — | HTTP Basic auth pair |
PHOENIX_ENDPOINT / PHOENIX_API_KEY |
with phoenix | — | Arize Phoenix base URL; api key only for Phoenix Cloud |
OTLP_ENDPOINT / OTLP_HEADERS |
with otlp | — | Any collector/OTLP receiver; optional k=v,k2=v2 headers |
TRACING_RELEASE / TRACING_VERSION |
no | — | Deploy identifiers on every span (langfuse.release, LangSmith metadata, service.version) |
TRACING_PROTOCOL |
no | http |
grpc needs pip install 'anytrace[grpc]' (collector use) |
YAML override (values win over env; backends replaces the env list wholesale):
service_name: my-agent
environment: staging
capture_content: false
backends:
- kind: langsmith
endpoint: https://langsmith.internal
auth: { api_key: lsv2_pt_... }
- kind: langfuse
endpoint: https://langfuse.internal
auth: { public_key: pk-..., secret_key: sk-... }
Secrets never appear in repr()/logs (redacted as ***).
Migrating from vendor SDKs
From the langsmith SDK
# before # after
from langsmith import traceable import anytrace
@traceable @anytrace.traced()
def plan(q): ... def plan(q): ...
@traceable(run_type="llm") with anytrace.record_generation(
def call_llm(prompt): ... "claude-sonnet-5") as gen:
out = client.messages.create(...)
gen.set_usage(input_tokens=...,
output_tokens=...)
# metadata={"user_id": ...} on traceable anytrace.set_user("user-42")
Delete LANGSMITH_TRACING/LANGCHAIN_TRACING_V2; set TRACING_BACKEND=langsmith.
Your data still lands in LangSmith — but the code no longer knows that.
From the langfuse SDK
# before # after
from langfuse.decorators import observe, import anytrace
langfuse_context
@observe() @anytrace.traced()
def rag(q): ... def rag(q): ...
@observe(as_type="generation") with anytrace.record_generation(
def call_llm(prompt): ... "claude-sonnet-5") as gen:
...
langfuse_context.update_current_trace( anytrace.set_user("user-42")
user_id="user-42", anytrace.set_session("sess-7")
session_id="sess-7")
langfuse.flush() anytrace.shutdown()
Distributed tracing
W3C traceparent propagation across services and languages — extract/inject
helpers, FastAPI/Flask middleware, and how a browser starts the root trace:
see docs/distributed-tracing.md (including a
LangSmith-specific caveat about UI-minted roots).
Typed spans, IO, and tags
Beyond trace_span/record_generation, spans can be typed so backends render
them correctly (LangSmith run types, Langfuse observation types — verified live):
with anytrace.trace_tool("order_lookup", order_id=42): ... # tool
with anytrace.trace_retriever("docs-index", top_k=8): ... # retriever
with anytrace.trace_embedding("embed-model-v2"): ... # embedding
anytrace.set_io(input=question, output=answer) # I/O on ANY span (capture-gated, masked)
anytrace.add_tags("beta", "checkout") # tags, inherited by child spans
Reliability & operations
Tracing must never break the app — anytrace fails open: using it before
init() (or with TRACING_ENABLED=false) is a silent no-op, and a crashing
mask hook redacts content instead of raising. For the opposite posture,
TRACING_STRICT=true makes init() fail fast when a backend rejects an
export probe.
The tracer reports its own health: anytrace.stats() returns per-backend
exported/failed span counts, and shutdown() logs a warning if any exports
failed (so a bad key can't fail silently).
anytrace.instrument_logging() stamps trace_id / span_id / user_id /
session_id onto every log record for log↔trace correlation:
logging.basicConfig(format="%(asctime)s %(levelname)s [trace=%(trace_id)s] %(message)s")
anytrace.instrument_logging()
User, session, and tags also cross service boundaries: inject_context
carries them in the W3C baggage header and use_context (or the middleware)
rehydrates them, so remote spans keep the caller's context.
LLM client auto-instrumentation
Wrap an Anthropic or OpenAI client once; every call becomes a generation span with model, temperature, and token usage — no manual recording:
from anytrace.integrations.anthropic import instrument
client = instrument(anthropic.Anthropic()) # sync or async
from anytrace.integrations.openai import instrument
client = instrument(openai.OpenAI()) # chat.completions + responses
Neither SDK becomes a anytrace dependency (the wrappers are duck-typed). Content follows the capture/masking rules; streaming calls get a span without usage.
CrewAI auto-instrumentation
One line before kickoff; every crew/task/agent/tool/LLM step becomes a typed, nested span — with per-call token usage from CrewAI's LLM events:
from anytrace.integrations.crewai import AnytraceCrewListener
AnytraceCrewListener() # instantiate once, before kickoff
crew.kickoff()
Spans are parented via CrewAI's event ids (thread-safe — CrewAI fires events
from worker threads). crewai never becomes a anytrace dependency.
LangChain / LangGraph auto-instrumentation
Zero manual spans — attach the callback handler and every chain, LLM, tool, and retriever run becomes a correctly-typed, correctly-nested span with token usage:
from anytrace.integrations.langchain import AnytraceCallbackHandler
chain.invoke(inputs, config={"callbacks": [AnytraceCallbackHandler()]})
graph.invoke(state, config={"callbacks": [AnytraceCallbackHandler()]}) # LangGraph
Requires langchain-core (never a dependency of anytrace itself). Teams using
other frameworks can pair anytrace with OpenInference or OpenLLMetry
auto-instrumentation libraries — LangSmith ingests both dialects natively, and
the spans flow through the same anytrace-configured OTLP pipeline.
Examples
Runnable examples live in examples/ — plain Python, LangChain
(auto-instrumented, zero manual spans), LangGraph, and CrewAI. Every example
runs offline (in-memory exporter, fake LLMs) with no keys needed; set
TRACING_* env vars to send to a real backend instead.
Backends & collectors
Four backend kinds ship built in: langsmith, langfuse, phoenix (Arize
Phoenix, OpenInference dialect), and otlp — a passthrough for any OTel
Collector or OTLP-native receiver. Fan out to any combination:
TRACING_BACKEND=langsmith,phoenix. When routing through a collector, do
vendor attribute mapping in the collector (or use the vendor's adapter
directly); batching/tail-sampling also belong at the collector layer.
Testing your instrumentation
App teams can assert on their own spans without any backend:
from anytrace.testing import capture
def test_my_agent():
with capture() as spans:
run_my_agent()
names = [s.name for s in spans.get_finished_spans()]
assert "chat claude-sonnet-5" in names
Migrating historical traces
anytrace-migrate --from langsmith --to langfuse --project X --verify 5
backfills existing traces between backends with original timestamps, resume
checkpointing, and read-API verification — see
docs/migration.md (spoiler: for live traffic, fan-out is
usually the better migration tool).
Backend verification
Live integration tests are gated behind env vars and assert delivery
(SpanExportResult.SUCCESS), not just "no exception":
ITEST_LANGFUSE=1 LANGFUSE_ENDPOINT=... LANGFUSE_PUBLIC_KEY=... LANGFUSE_SECRET_KEY=... \
pytest tests/integration -k langfuse
ITEST_LANGSMITH=1 LANGSMITH_ENDPOINT=... LANGSMITH_API_KEY=... \
pytest tests/integration -k langsmith
Checklist and findings: docs/backend-verification.md.
Install
pip install anytrace # core (HTTP OTLP export)
pip install "anytrace[grpc]" # + gRPC OTLP export
Releasing (maintainers)
python -m build # dist/anytrace-X.Y.Z*.{whl,tar.gz}
twine upload dist/*
git tag -a vX.Y.Z -m "anytrace X.Y.Z" && git push origin vX.Y.Z
Bump the version in pyproject.toml and add a CHANGELOG.md entry first.
Development
pip install -e ".[dev]"
make test # pytest
make lint # ruff check
make typecheck # mypy
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file anytrace-0.9.0.tar.gz.
File metadata
- Download URL: anytrace-0.9.0.tar.gz
- Upload date:
- Size: 112.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2e3d97047c5d7cd3472e30af6d0233652c40aad901a4999d1fb6c46a89194c8
|
|
| MD5 |
df703c61b955a69d9d0b58eb450b14de
|
|
| BLAKE2b-256 |
d0579a6157705fce7d9cf031a31b9c88dc997d66b5ba7b27746a98ef4958cb10
|
Provenance
The following attestation bundles were made for anytrace-0.9.0.tar.gz:
Publisher:
release.yml on hitesh-balapanuru/anytrace
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anytrace-0.9.0.tar.gz -
Subject digest:
e2e3d97047c5d7cd3472e30af6d0233652c40aad901a4999d1fb6c46a89194c8 - Sigstore transparency entry: 2204813280
- Sigstore integration time:
-
Permalink:
hitesh-balapanuru/anytrace@ad92c020486e8449103f56de15095081719adb5a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/hitesh-balapanuru
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ad92c020486e8449103f56de15095081719adb5a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file anytrace-0.9.0-py3-none-any.whl.
File metadata
- Download URL: anytrace-0.9.0-py3-none-any.whl
- Upload date:
- Size: 55.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf2bef3f0f288f525a085da1fc64b953c2c18ea327636761ec4f4791b9b2f5ea
|
|
| MD5 |
47ea662ecf1e9b2765a82b534b13a516
|
|
| BLAKE2b-256 |
85a218b64ab5bd3a05ffb73e96619da3e07df356c0204d08075d3ead0e346e17
|
Provenance
The following attestation bundles were made for anytrace-0.9.0-py3-none-any.whl:
Publisher:
release.yml on hitesh-balapanuru/anytrace
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anytrace-0.9.0-py3-none-any.whl -
Subject digest:
bf2bef3f0f288f525a085da1fc64b953c2c18ea327636761ec4f4791b9b2f5ea - Sigstore transparency entry: 2204813313
- Sigstore integration time:
-
Permalink:
hitesh-balapanuru/anytrace@ad92c020486e8449103f56de15095081719adb5a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/hitesh-balapanuru
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ad92c020486e8449103f56de15095081719adb5a -
Trigger Event:
workflow_dispatch
-
Statement type: