Metadata-Version: 2.5
Name: e-volv-logs
Version: 0.1.0
Summary: Evolve Log Manager SDK — logs, traces and error capture for Python.
Project-URL: Homepage, https://github.com/Pactify-Pty-Ltd/theevolve/tree/main/packages/logs-py
Project-URL: Source, https://github.com/Pactify-Pty-Ltd/theevolve
License: MIT
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.0
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
Description-Content-Type: text/markdown

# e-volv-logs

Evolve Log Manager SDK for Python — batched log shipping, trace context and
error capture against the Evolve ingest endpoint
(`POST /api/public/v1/logs`). Companion: the Node.js SDK `@e-volv/logs`.

Python 3.10+.

```bash
pip install e-volv-logs
```

## Quick start

```python
import e_volv_logs as evolve_logs
from e_volv_logs import init, log, span, trace

init(
    key="evk_…",  # project ingest key
    url="https://your-host/api/public/v1/logs",
    service="api",
    environment="production",
    release="1.4.2",
)

log.info("order created", {"orderId": "o_1", "total": 42.5})
log.error("payment failed", {"orderId": "o_1"})

try:
    await charge()
except Exception as err:
    log.exception(err, {"orderId": "o_1"})  # exception.type/message/stack

# Traces live in contextvars — they flow across await. httpx/requests
# requests made inside a trace get a W3C traceparent header automatically.
with trace():
    with span("db.query", {"table": "orders"}):
        await db.query("SELECT …")
    await httpx.get("https://internal/svc")  # traceparent injected
```

If `key` or `url` is missing, `init()` returns a no-op client and warns once.
The SDK never raises into user code.

## Batching, retries, drops

- Flushes at **200 events**, **2 s** (age of the oldest buffered event), or a
  **512 KB** payload — whichever comes first. Bodies are gzipped
  (`Content-Encoding: gzip`).
- **429** retries with exponential backoff (0.5 s doubling, capped at 10 s),
  up to 3 attempts. **413** halves the batch and retries.
- When the pending buffer exceeds **2× batch size**, the **oldest** events are
  dropped; losses are visible on `client.dropped`.
- `atexit` flushes whatever is pending.

## Options

`init(key=, url=, service=, environment=, release=, redact_keys=[...],
sample_rate=1.0, capture_excepthook=True)` — `redact_keys` are merged into the
backstop regex `password|secret|token|authorization|cookie|set-cookie|api[-_]?key`
(case-insensitive), applied to attrs before queueing.

## stdlib logging

```python
import logging
from e_volv_logs import init, LogHandler

init(key="evk_…", url="…", service="api")
logging.getLogger("myapp").addHandler(LogHandler())
```

Levels map to OTel severities (DEBUG→5, INFO→9, WARNING→13, ERROR→17,
CRITICAL→21). `extra={...}` on the log call becomes event attrs; `exc_info`
becomes `exception.*` fields. `sys.excepthook` capture is on by default.

## FastAPI / Starlette

```python
from e_volv_logs.integrations.fastapi import EvolveLogsMiddleware

app.add_middleware(EvolveLogsMiddleware)  # one root span per request
```

The middleware continues the trace when the inbound request carries an
`x-evolve-traceparent` header (Evolve backend → evolve-ai propagation),
otherwise starts a fresh root trace.

## LangChain and LangGraph

```bash
pip install "e-volv-logs[langchain]"
```

```python
from e_volv_logs.integrations.langchain import EvolveLogsCallbackHandler

result = graph.invoke(
    {"question": "…"},
    config={"callbacks": [EvolveLogsCallbackHandler()]},
)
```

One span per LangChain run, nested by `parent_run_id`, all sharing one trace —
so a LangGraph node's tool calls and model calls appear inside the node on the
Evolve trace graph. Span names:

| Callback                               | Span name      |
| -------------------------------------- | -------------- |
| `on_chain_start` with `langgraph_node` | `graph.<node>` |
| `on_chain_start` otherwise             | `chain.<name>` |
| `on_tool_start`                        | `tool.<name>`  |
| `on_llm_start` / `on_chat_model_start` | `llm.<model>`  |

Errors end the span through `_log_span_error` (severity error, `exception.*`
attrs), exactly like `span()` raised inside a body.

## Queue hops

A span on the consumer side joins the producer's trace only if the traceparent
travels with the job:

```python
from e_volv_logs import run_with_traceparent, span, traceparent

# producer
queue.enqueue(work, payload, trace=traceparent())

# consumer: the body runs as a new hop of the producer's trace.
with run_with_traceparent(job.trace), span("queue.work", {"queue": "work"}):
    handle(job)
```

`run_with_traceparent(header)` parses a W3C traceparent; an absent or malformed
header starts a fresh trace, so a producer that sends nothing still yields a
trace of its own.

## Development

```bash
cd packages/logs-py
uv sync
uv run pytest tests/ -q
```
