Metadata-Version: 2.4
Name: devstream-client
Version: 0.4.0
Summary: Python client library for DevStream logging service
Home-page: https://github.com/paul-wolf/devstream
Author: Paul
Author-email: paul.wolf@yew.io
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.25.0
Provides-Extra: asgi
Requires-Dist: asgi-correlation-id>=4.0; extra == "asgi"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# DevStream Python Client

Python client library for sending logs to DevStream logging service.

> **For AI coding agents:** a machine-readable integration guide is hosted at
> your DevStream server's `/llms.txt` (e.g. https://devstream.fly.dev/llms.txt),
> covering this client, the raw HTTP API, and the OpenAPI spec.

## Installation

```bash
pip install devstream-client
```

Or install from source:

```bash
cd client
pip install -e .
```

## Quick Start

### Basic Usage

```python
from devstream_client import DevStreamClient

# Initialize client
client = DevStreamClient(
    api_key="your-api-key",
    app_key="my-app",
    deployment_key="production",
    base_url="http://localhost:8787"  # or your production URL
)

# Send a simple log
client.log("Application started successfully")

# Send a log with tags
client.log_with_tags(
    "User logged in",
    user_id="12345",
    level="info",
    ip_address="192.168.1.1"
)

# Attach structured JSON data to a log
client.log(
    "Order placed",
    tags=[{"name": "level", "value": "info"}],
    data={"order_id": 4823, "total": 19.99, "items": ["sku-1", "sku-2"]},
)

# data also works with the keyword-tag helper
client.log_with_tags(
    "Order placed",
    data={"order_id": 4823, "total": 19.99},
    level="info",
)
```

### Batch Sending

Send many messages in a single HTTP request (one bulk insert server-side):

```python
client.log_batch([
    {"message": "step 1", "correlation_id": "req-1"},
    {"message": "step 2", "tags": [{"name": "level", "value": "info"}]},
    {"message": "step 3", "data": {"rows": 42}},
])
```

Each entry accepts `message` (required), `tags`, `data`, and `correlation_id`.

### Python Logging Integration

```python
import logging
from devstream_client import DevStreamHandler

# Create logger
logger = logging.getLogger("myapp")
logger.setLevel(logging.INFO)

# Add DevStream handler
handler = DevStreamHandler(
    api_key="your-api-key",
    app_key="my-app",
    deployment_key="production",
    base_url="http://localhost:8787"  # or your production URL
)
logger.addHandler(handler)

# Use standard Python logging
logger.info("Application started")
logger.error("Something went wrong", exc_info=True)
```

The handler is **non-blocking**: each record is queued and sent on a background
thread, so logging never blocks the calling thread on the network (important
under async servers like Daphne and in Celery tasks). If the queue fills up
(default 1000 pending records, configurable via `queue_size`), new records are
dropped rather than blocking. Pending records are flushed on interpreter exit.

For high-volume logging, pass `batch=True` to coalesce queued records into a
single batch request per drain (uses the server's batch endpoint):

```python
handler = DevStreamHandler(..., batch=True, batch_max=100)
```

### Context tags (correlation id, etc.)

Attach request-scoped context to every log line as a tag, without custom handler
code, using a filter that reads a `contextvars.ContextVar`:

```python
import contextvars
from devstream_client import ContextVarTagFilter

slack_thread = contextvars.ContextVar("slack_thread", default=None)
handler.addFilter(ContextVarTagFilter(slack_thread, "slack_thread"))

slack_thread.set("T123.456")  # set per request/task; appears as a tag on logs
```

If you use [`asgi-correlation-id`](https://github.com/snok/asgi-correlation-id),
there's a ready-made filter (install with `pip install devstream-client[asgi]`):

```python
from devstream_client import CorrelationIdFilter

handler.addFilter(CorrelationIdFilter())  # adds a "correlation_id" tag
```

### Django Integration

Add to your Django settings:

```python
# settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'devstream': {
            'class': 'devstream_client.client.DevStreamHandler',
            'api_key': 'your-api-key',
            'app_key': 'my-django-app',
            'deployment_key': os.environ.get('DEPLOYMENT', 'local'),
            'base_url': 'http://localhost:8787',  # or your production URL
        },
    },
    'loggers': {
        'django': {
            'handlers': ['devstream'],
            'level': 'INFO',
        },
        'myapp': {
            'handlers': ['devstream'],
            'level': 'DEBUG',
        },
    },
}
```

## Configuration

### Client Parameters

- `api_key` (required): Your DevStream API key
- `app_key` (required): Application identifier
- `deployment_key` (required): Deployment environment (e.g., 'local', 'dev', 'staging', 'production')
- `base_url` (optional): DevStream service URL (default: http://localhost:8787)
- `timeout` (optional): Per-request timeout in seconds (default: 5.0)
- `max_retries` (optional): Automatic retries on connection errors and transient 5xx responses (default: 2)
- `retry_after_cap` (optional): On a `429 Too Many Requests` response, the maximum seconds to honour the server's `Retry-After` header before giving up (default: 10.0; set 0 to never wait). This bounds how long a send can block.

### Rate limiting

If the server rate-limits ingestion it responds with `429` and a `Retry-After`
header. The client waits for that interval and retries once, but only up to
`retry_after_cap` seconds — beyond that it drops the log (returns `False`) rather
than blocking your application. With the non-blocking `DevStreamHandler` this
backpressure happens entirely on the background thread.

### Scrubbing sensitive data (client-side)

Enable scrubbing to redact or mask sensitive data — emails, passwords, tokens,
API keys, Bearer/JWT — from the message, `data` and tags **before it leaves your
application**:

```python
client = DevStreamClient(
    api_key="...", app_key="...", deployment_key="prod",
    scrub=True,            # off by default
    scrub_mode="redact",   # "redact" -> [EMAIL]/[REDACTED], or "mask" -> j***@x.com / ***1234
)
client.log("login user@example.com password=hunter2")
# sent as: "login [EMAIL] password=[REDACTED]"
```

The same options work on `DevStreamHandler(..., scrub=True, scrub_mode="mask")`.

This is best-effort (regex-based) and complements the server-side scrubbing
that can be enabled per application — use either or both. Client-side keeps the
data off the network entirely; server-side covers every sender.

### Structured Data

Pass a `data` dictionary to attach arbitrary JSON to a log message. It is stored
alongside the message and returned by the API and log viewer:

```python
client.log("Payment failed", data={"amount": 50, "currency": "USD", "code": "card_declined"})
```

### Tags

Tags are key-value pairs that help you filter and search logs. You can add tags in two ways:

1. As a list of dictionaries:
```python
client.log("User action", tags=[
    {"name": "user_id", "value": "12345"},
    {"name": "action", "value": "login"}
])
```

2. As keyword arguments:
```python
client.log_with_tags(
    "User action",
    user_id="12345",
    action="login"
)
```

## Features

- Simple API for sending logs
- Integration with Python's standard logging module
- Support for custom tags
- Automatic retry and error handling
- Django-ready configuration

## License

MIT License
