Metadata-Version: 2.5
Name: e-volv-logs
Version: 0.2.1
Summary: e-volv SDK for Python — feature flags evaluated locally (e-volv Launch) plus logs, traces and error capture (e-volv Observer).
Project-URL: Homepage, https://e-volv.io/docs/flags/sdk/python
Project-URL: Documentation, https://e-volv.io/docs/flags/sdk/python
Project-URL: Support, https://e-volv.io/support
License-Expression: MIT
License-File: LICENSE
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

e-volv Log Manager SDK for Python — batched log shipping, trace context and
error capture against the e-volv 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 (e-volv 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
e-volv 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.

## Feature flags (e-volv Launch)

Evaluate e-volv Launch flags locally, in-process, on the same key as telemetry —
through the [OpenFeature](https://openfeature.dev) provider or directly:

```bash
pip install e-volv-openfeature   # OpenFeature provider
# or just e-volv-logs — flags are built in
```

```python
from openfeature import api
from openfeature.evaluation_context import EvaluationContext

from e_volv_logs import init
from evolve_openfeature import EvolveProvider

client = init(key="evk_…", url="https://your-host/api/public/v1/logs")
api.set_provider(EvolveProvider(client.flags))

details = api.get_client().get_boolean_details(
    "checkout.new",
    False,
    EvaluationContext(targeting_key="u_1", attributes={"plan": "pro"}),
)
```

Directly on the client:

```python
from e_volv_logs import FlagsOptions, init

client = init(key="evk_…", url="…", flags=FlagsOptions())

client.flags.bool("checkout.new", False, {"targetingKey": "u_1", "plan": "pro"})
client.flags.string("banner.copy", "Hello")
client.flags.number("limits.maxItems", 10)
client.flags.json("theme.config", {})
client.flags.detail("checkout.new", False, ctx)  # Evaluation(value, variant, reason)

client.flags.ready(timeout=5.0)  # the only call that waits
client.flags.on_change(lambda keys: ...)
client.flags.last_updated_at  # epoch seconds, None before the first payload
client.flags.verify()  # install check against GET /ping
```

Flag options (Python spellings of the Launch contract, §4):

| `FlagsOptions` field              | Default    | Meaning                                         |
| --------------------------------- | ---------- | ----------------------------------------------- |
| `enabled`                         | `True`     | start the flags client                          |
| `mode`                            | `"stream"` | `stream` \| `poll` \| `offline`                 |
| `poll_interval_seconds`           | `30`       | poll period; minimum 15                         |
| `cache`                           | temp dir   | last-known-payload dir; `False` disables        |
| `bootstrap`                       | none       | a bundled ruleset served before the first fetch |
| `exposures_enabled`               | `True`     | record exposures                                |
| `exposures_sample_rate`           | `1.0`      | probability an evaluation is recorded           |
| `exposures_dedupe_window_seconds` | `60`       | suppress repeats of the same subject/variant    |
| `exposures_send_attributes`       | `False`    | send context attributes with exposures          |
| `private_attributes`              | `[]`       | attribute names never sent anywhere             |

Guarantees (the e-volv Launch SDK contract):

- Evaluation is **synchronous, never raises, never performs I/O** — every
  call answers from the last-held ruleset or your default.
- Wrong type → your default with reason `TYPE_MISMATCH`.
- Absent flag → your default with reason `FLAG_NOT_FOUND`.
- The control plane unreachable → last held value; a cold start serves a valid
  cache file instantly while fetching in the background.
- Server keys never send `Origin` or `x-evolve-app-id`; requests carry
  `User-Agent: e-volv-logs-py/<version>`.
- Flag evaluation is byte-for-byte identical to every other e-volv SDK: the
  Python kernel is a port held to `packages/flags-kernel/fixture.json`.

## Development

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