Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anytrace-0.9.0.tar.gz (112.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

anytrace-0.9.0-py3-none-any.whl (55.0 kB view details)

Uploaded Python 3

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

Hashes for anytrace-0.9.0.tar.gz
Algorithm Hash digest
SHA256 e2e3d97047c5d7cd3472e30af6d0233652c40aad901a4999d1fb6c46a89194c8
MD5 df703c61b955a69d9d0b58eb450b14de
BLAKE2b-256 d0579a6157705fce7d9cf031a31b9c88dc997d66b5ba7b27746a98ef4958cb10

See more details on using hashes here.

Provenance

The following attestation bundles were made for anytrace-0.9.0.tar.gz:

Publisher: release.yml on hitesh-balapanuru/anytrace

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for anytrace-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bf2bef3f0f288f525a085da1fc64b953c2c18ea327636761ec4f4791b9b2f5ea
MD5 47ea662ecf1e9b2765a82b534b13a516
BLAKE2b-256 85a218b64ab5bd3a05ffb73e96619da3e07df356c0204d08075d3ead0e346e17

See more details on using hashes here.

Provenance

The following attestation bundles were made for anytrace-0.9.0-py3-none-any.whl:

Publisher: release.yml on hitesh-balapanuru/anytrace

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page