Metadata-Version: 2.4
Name: wardex-sdk
Version: 0.1.0b5
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
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: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: System :: Monitoring
Summary: Open-source observability SDK for AI agents — zero-instrumentation capture, OpenTelemetry-native
Keywords: observability,ai-agents,opentelemetry,llm,tracing
Author: Wardex Labs
Maintainer: Wardex Labs
License: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/wardex-labs/wardex-sdk
Project-URL: Issues, https://github.com/wardex-labs/wardex-sdk/issues
Project-URL: Repository, https://github.com/wardex-labs/wardex-sdk

# wardex-sdk

[![PyPI](https://img.shields.io/pypi/v/wardex-sdk)](https://pypi.org/project/wardex-sdk/)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/wardex-labs/wardex-sdk/blob/main/LICENSE)

Open-source observability SDK for AI agents — zero-instrumentation capture,
OpenTelemetry-native.

> ⚠️ **Beta.** PII masking is on by default (see below), but the
> SDK is still early: review the caveats below before sending sensitive data
> through it.

## Install

```bash
pip install wardex-sdk
```

## Quickstart

```python
import wardex_sdk as wardex
from wardex_sdk import OtlpHttpTransport

wardex.init(
    transport=OtlpHttpTransport(endpoint="https://<your-collector>/v1/traces"),
    intercept=True,  # zero-instrumentation capture of LLM calls
)

# your app code — OpenAI/Anthropic calls are captured automatically

wardex.close()   # optional — spans auto-flush every 5s, on buffer threshold, and at exit
```

## Status

**Works today**
- Zero-instrumentation capture of LLM HTTP calls (OpenAI, Anthropic) over
  `https`, cleartext `http`, and h2c
- `gen_ai` semantics: model, tokens, parameters, finish reasons, input/output messages
- Transport metrics (TCP/TLS timing, TTFT), gRPC (grpclib), WebSocket (`wss`), MCP stdio
- Export to any OpenTelemetry backend via `OtlpHttpTransport`
- Manual span decorators: `@workflow` / `@agent` / `@task` / `@tool` / `@span`
- PII masking on by default: emails, phone numbers, credit cards (Luhn-verified),
  US SSNs, IP addresses, bank routing numbers, IBANs, and API-key/token secrets
  are masked before anything leaves the process (`pii_mode=PIIMode.OFF` to disable,
  `pii_disabled_categories={PIICategory.IP_ADDRESS}` for per-category opt-out)
- Background batching: automatic flush every 5s / on buffer threshold /
  at exit and on SIGINT/SIGTERM (chained; opt out with `flush_on_signals=False`)

**Not yet (see Roadmap)**
- Framework adapters (LangGraph, Anthropic/OpenAI Agent SDKs)
- Node/TS and Java SDKs

**Notes**
- After `os.fork()` the worker respawns lazily in the child on first capture;
  spans buffered before the fork may be sent by both processes (duplicates
  are possible; a fork landing mid-export can also strand the child's
  pre-fork buffer — re-init in the child for a clean slate). Under uWSGI
  enable threads (`--enable-threads`).

## Distributed tracing

Trace context propagation is **opt-in** — a plain `wardex.init(...)` never
touches your outbound requests or headers. Turn it on with:

```python
wardex.init(
    transport=OtlpHttpTransport(endpoint="https://<your-collector>/v1/traces"),
    intercept=True,
    propagate_trace=True,                       # inject W3C headers on outbound calls
    propagate_targets=["api.internal.example.com", "*.svc.cluster.local"],  # optional glob allowlist; default None = all hosts
)
```

With `propagate_trace=True`, outbound calls made through httpx (sync + async),
requests, or aiohttp get a `traceparent` (and `tracestate`, if one was
received) header attached automatically, as long as an active trace context
exists and the request doesn't already carry a `traceparent`. **If
`propagate_targets` is left unset, the trace ID is sent to every host you
call — including third-party LLM providers.** Set it to an allowlist of glob
patterns to scope injection to your own services.

### Joining an inbound trace

Drop the middleware in front of your app to join whatever trace the caller
started:

```python
# ASGI (FastAPI, Starlette, Django ASGI)
app.add_middleware(wardex.WardexMiddleware)

# WSGI (Flask, Django WSGI)
app.wsgi_app = wardex.WardexWSGIMiddleware(app.wsgi_app)
```

Both extract the incoming `traceparent`/`tracestate` and continue the trace
for the lifetime of the request; a missing or malformed header just starts a
fresh trace (never raises). One WSGI caveat: the joined context covers the
app callable only, so streaming responses (work done while iterating the
returned iterable) run outside it.

### Manual propagation (the universal escape hatch)

The baton is just a string, so it travels over any channel that can carry
one — not just HTTP. `get_traceparent()` and `get_trace_headers()` are plain
functions that return the current trace headers; `continue_trace(headers)`
is a **context manager** — the remote parent is only installed inside the
`with` block, so it must be entered, not merely called. Use them directly
wherever the automatic client patches or ASGI/WSGI middleware don't reach:

```python
# gRPC metadata
stub.Check(req, metadata=[("traceparent", wardex.get_traceparent())])

# WebSocket handshake
websockets.connect(uri, extra_headers=wardex.get_trace_headers())

# Celery: put get_trace_headers() on the task's headers when sending it,
# then inside the worker:
with wardex.continue_trace(task.request.headers):
    ...  # task body

# Kafka: put get_trace_headers() on the message headers when producing,
# then inside the consumer:
with wardex.continue_trace(dict(msg.headers())):
    ...  # process the message
```

`with wardex.continue_from_otel():` is a one-line alternative to
`continue_trace()` for code that already runs under an active OpenTelemetry
span — it adopts that span as the remote parent for the duration of the
`with` block (no-op if `opentelemetry` isn't installed or there's no active
span). Like `continue_trace()`, it is a context manager and must be entered
with `with`.

### Propagating into threads

`asyncio` tasks inherit the current trace context automatically; threads do
not. Wrap the target with `wardex.run_in_context()` at the point where you
still have the right context:

```python
thread = threading.Thread(target=wardex.run_in_context(worker_fn), args=(...,))
thread.start()
```

### `capture_mode`: what gets captured without an active span

`capture_mode` defaults to `"agent"`: LLM-semantic traffic (recognized
`gen_ai` calls, MCP stdio) is always captured, but generic HTTP/gRPC/WS
traffic is only captured while it happens inside an active *local* wardex
span (a `traceparent` received from an upstream caller doesn't count on its
own — this keeps a service mesh stamping every request with a traceparent
from reviving the pre-Phase-4 "capture everything" noise).

This means a bare, unwrapped call to an LLM provider wardex doesn't
recognize (or a WS-based provider such as OpenAI Realtime, which carries no
parseable semantics) can be silently dropped if it isn't inside a local
span. Wrap it with `@wardex.workflow` (or any of the span decorators), or set
`capture_mode=wardex.CaptureMode.ALL` to restore the previous
capture-everything behavior:

```python
wardex.init(..., capture_mode=wardex.CaptureMode.ALL)
```

Plaintext hosts you've explicitly named via `intercept_hosts` are always
captured regardless of `capture_mode` — a targeted allowlist entry is a
stronger opt-in than the default policy.

## Roadmap

1. ~~PII masking (pre-send safety)~~ — shipped
2. ~~Batching & lifecycle (background worker, at-exit/periodic flush, concurrency)~~ — shipped
3. ~~Distributed propagation (W3C)~~ — shipped
4. Framework adapters
5. Node/TS and Java SDKs

> PII masking caveats: `before_send` sees pre-masking data (masking runs inside
> the encoder), the Console transport prints raw (local debugging only), and
> non-UTF-8 binary payloads pass through unmasked.

## License

Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).

"Wardex" is a trademark of Wardex Labs.

