Metadata-Version: 2.4
Name: anytrace
Version: 0.9.0
Summary: Vendor-neutral agent tracing library: a thin wrapper over the OpenTelemetry SDK with pluggable OTLP backend adapters
Project-URL: Homepage, https://github.com/hitesh-balapanuru/anytrace
Project-URL: Repository, https://github.com/hitesh-balapanuru/anytrace
Project-URL: Changelog, https://github.com/hitesh-balapanuru/anytrace/blob/main/CHANGELOG.md
Author: Hitesh Balapanuru
License: Apache-2.0
License-File: LICENSE
Keywords: genai,langfuse,langsmith,llm,observability,opentelemetry,phoenix,tracing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27
Requires-Dist: opentelemetry-sdk>=1.27
Provides-Extra: dev
Requires-Dist: fastapi>=0.115; extra == 'dev'
Requires-Dist: flask>=3.0; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: langchain-core>=0.3; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: pyyaml>=6.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
Provides-Extra: grpc
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.27; extra == 'grpc'
Description-Content-Type: text/markdown

# 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

```bash
pip install anytrace
```

```python
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:

```bash
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:

```bash
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](docs/backend-verification.md),
including a LangSmith thread-view screenshot.

## Tutorials

New to the library? Work through [docs/tutorials/](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):

```yaml
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

```python
# 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

```python
# 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](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):

```python
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:

```python
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:

```python
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:

```python
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:

```python
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/](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:

```python
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](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":

```bash
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](docs/backend-verification.md).

## Install

```bash
pip install anytrace            # core (HTTP OTLP export)
pip install "anytrace[grpc]"    # + gRPC OTLP export
```

## Releasing (maintainers)

```bash
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

```bash
pip install -e ".[dev]"
make test      # pytest
make lint      # ruff check
make typecheck # mypy
```
