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 an anytrace dependency (the wrappers are duck-typed). Content follows the capture/masking rules; streaming calls get a span without usage.

The same pattern covers the cloud model APIs:

from anytrace.integrations.bedrock import instrument
client = instrument(boto3.client("bedrock-runtime"))   # converse() + converse_stream()
                                                         # invoke_model() best-effort

from anytrace.integrations.google import instrument
client = instrument(genai.Client())                     # google-genai: models + aio.models
model = instrument(GenerativeModel("gemini-1.5-pro"))   # classic vertexai SDK, same wrapper

gen_ai.system is aws.bedrock for Bedrock; for Google it's gcp.vertex_ai (classic SDK, or the new client configured with vertexai=True) or gcp.gemini otherwise. invoke_model()'s request/response bodies are opaque per model family — usage/content are extracted for Anthropic, Meta, and Amazon Titan bodies; other families still get a span, without token counts. Neither boto3 nor google-genai/google-cloud-aiplatform becomes an anytrace dependency. Unit-tested against stubbed clients; live verification against real AWS/GCP is still pending (tracked in tasks/epic-10-cloud-providers.md).

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 an 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.10.0.tar.gz (117.6 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.10.0-py3-none-any.whl (59.1 kB view details)

Uploaded Python 3

File details

Details for the file anytrace-0.10.0.tar.gz.

File metadata

  • Download URL: anytrace-0.10.0.tar.gz
  • Upload date:
  • Size: 117.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for anytrace-0.10.0.tar.gz
Algorithm Hash digest
SHA256 ea7acb55981b522f0b2f77fbfbbf58a9f36b209cc9bae2037395157f976016d7
MD5 3e8c6c846974bfb10710ab2ac6da192d
BLAKE2b-256 e52df8c4e638da9c90823de51360814da35cb5c60b6730c4abf5dc8911a86f52

See more details on using hashes here.

Provenance

The following attestation bundles were made for anytrace-0.10.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.10.0-py3-none-any.whl.

File metadata

  • Download URL: anytrace-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 59.1 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.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cac1d1bc034ca0add3e99badb85b2ca211ece1a88abd55d9acffad413217b186
MD5 d509181566057bd3be2ce53d99174333
BLAKE2b-256 29cbbd9aaafd51b89b1014229aea7902797d854fc920324cb925f3a11be6ed78

See more details on using hashes here.

Provenance

The following attestation bundles were made for anytrace-0.10.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