Metadata-Version: 2.4
Name: raven-logger
Version: 1.2.0
Summary: Application logging SDK for raven-log contract compliance — structured, validated JSON logs
Author-email: Giggso <m.abbasi@giggso.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/giggso/raven-logger
Project-URL: Issues, https://github.com/giggso/raven-logger/issues
Keywords: logging,observability,structured-logging,raven,tracing
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
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
Classifier: Topic :: System :: Logging
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: kafka
Requires-Dist: confluent-kafka>=2.0.0; extra == "kafka"
Provides-Extra: s3
Requires-Dist: boto3>=1.26.0; extra == "s3"
Dynamic: license-file

# raven-logger

Application logging SDK for **raven-log contract compliance** — emit validated, structured JSON records from any Python service.

## Install

```bash
# Core (stdout emitter only)
pip install raven-logger

# With Kafka emitter support
pip install "raven-logger[kafka]"

# With S3 emitter support
pip install "raven-logger[s3]"

# All emitters
pip install "raven-logger[kafka,s3]"
```

## Quick start

```python
from raven_logger import raven_log, new_trace_id, new_span_id

trace = new_trace_id()

raven_log(
    level="INFO",
    criticality="P3",
    message="User signed in",
    service="auth-service",
    env="prod",
    principal="user:42",
    project="my-app",
    trace_id=trace,
)
```

Output (one JSON line to stdout):

```json
{
  "schema_version": "1.0",
  "timestamp": "2026-06-22T12:00:00.000000+00:00",
  "level": "INFO",
  "criticality": "P3",
  "service": "auth-service",
  "env": "prod",
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "span_id": "a1b2c3d4",
  "principal": "user:42",
  "error_code": "",
  "message": "User signed in",
  "context": {},
  "sample_payload": "",
  "project": "my-app"
}
```

## What's new in 1.2.0

- **The documented call works:** `raven_log("ERROR", "DB timed out", criticality="P2",
  service="payments-svc", error_code="DB_CONN_TIMEOUT")`. `env`, `project` and
  `principal` default from `RAVEN_ENV` (unset means `prod`), `RAVEN_PROJECT` (or the
  manifest's `project`) and `RAVEN_PRINCIPAL` (or `service:<service>`). The 1.1.0
  positional call (`level, criticality, message, service, env, principal, project`)
  still works.
- **Never raises into your app.** A record that fails validation is written to the
  dead-letter queue (`.raven/dlq/YYYYMMDD.ndjson`, or `RAVEN_LOG_DLQ_DIR`) with the
  reason, and the call returns `None`. Set `RAVEN_LOG_STRICT=1` to raise instead.
  Strict is the default when `RAVEN_ENV` is dev and under pytest.
- **Redacted before any emitter.** Secrets and PII in `message`, `context` and
  `sample_payload` are scrubbed with the same rules as Raven's pipeline, whether or not
  you pass `_emit`.
- **A real default sink.** Without `_emit`, records are queued, batched and written to:
  `RAVEN_LOG_SINK` (`stdout` | `file` | `s3`), else the manifest's `logging.sink`, else
  stdout in a container and an NDJSON file on a host (`RAVEN_LOG_DIR`, default
  `~/.raven/logs`).
  - `s3` needs `RAVEN_LOG_S3_PATH=s3://bucket/prefix`. Failed uploads are spooled
    (`RAVEN_LOG_SPOOL_DIR`) and re-sent when S3 is back, with one
    `logging.remote_degraded` P2 per minute meanwhile.
  - The first record is `logging.sink_selected`.
  - Stdout in prod with no `RAVEN_LOG_COLLECTOR` set adds a WARN/P2
    `LOG_SINK_NOT_DURABLE`.
- `raven_logger.flush()` ships what is queued; the queue also flushes every 2 s
  (`RAVEN_LOG_FLUSH_SECONDS`) and at exit.

## Kafka emitter

`KafkaEmitter` is a production-ready emitter that sends records directly to a Kafka topic. Pass it as `_emit` to `raven_log`.

### Local / Docker broker (no authentication)

```python
from raven_logger import raven_log, KafkaEmitter

emitter = KafkaEmitter(
    bootstrap_servers="localhost:9092",
    topic="raven-logs",
    cluster_name="local-dev",   # optional label, not sent to broker
)

raven_log(
    level="INFO",
    criticality="P3",
    message="User signed in",
    service="auth-service",
    env="dev",
    principal="user:42",
    project="my-app",
    _emit=emitter,
)
```

### Authenticated cluster (SASL_SSL)

```python
emitter = KafkaEmitter(
    bootstrap_servers="broker.example.com:9092",
    topic="raven-logs",
    sasl_username="my-user",
    sasl_password="my-secret",
)
```

When `sasl_username` and `sasl_password` are provided the security protocol defaults to `SASL_SSL`. Omit both for `PLAINTEXT` (local/Docker). Providing only one raises `ValueError` at construction time.

### `KafkaEmitter` parameters

| Parameter | Required | Default | Description |
|---|---|---|---|
| `bootstrap_servers` | ✓ | — | Broker address(es), e.g. `"localhost:9092"` |
| `topic` | ✓ | — | Target Kafka topic |
| `cluster_name` | — | `""` | Human label stored on the instance; not sent to broker |
| `sasl_username` | — | `None` | SASL username; must be paired with `sasl_password` |
| `sasl_password` | — | `None` | SASL password; must be paired with `sasl_username` |
| `security_protocol` | — | auto | Overrides the auto-detected protocol (`PLAINTEXT` / `SASL_SSL`) |
| `sasl_mechanism` | — | `"PLAIN"` | SASL mechanism |

`KafkaEmitter` requires `confluent-kafka`. Install it with `pip install "raven-logger[kafka]"`.

## S3 emitter

`S3Emitter` writes each log record as an individual JSON file to an Amazon S3 bucket. Pass it as `_emit` to `raven_log`.

Each file is stored at:
```
s3://<bucket>/<prefix><timestamp>_<trace_id>.json
```

### Basic usage

```python
from raven_logger import raven_log, S3Emitter

emitter = S3Emitter(
    s3_path="s3://my-bucket/raven-logs/",
    access_id="AKIAIOSFODNN7EXAMPLE",
    access_token="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
)

raven_log(
    level="ERROR",
    criticality="P1",
    message="failed to create charge",
    service="payments-svc",
    env="prod",
    principal="m.abbasi@giggso.com",
    project="shayv1",
    error_code="CHARGE_CREATE_FAILED",
    context={"amount": 99.0, "error": "card declined"},
    _emit=emitter,
)
```

### With a custom region

```python
emitter = S3Emitter(
    s3_path="s3://my-bucket/raven-logs/",
    access_id="AKIAIOSFODNN7EXAMPLE",
    access_token="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    region_name="eu-west-1",
)
```

### `S3Emitter` parameters

| Parameter | Required | Default | Description |
|---|---|---|---|
| `s3_path` | ✓ | — | S3 destination, e.g. `"s3://bucket/prefix/"` |
| `access_id` | ✓ | — | AWS access key ID |
| `access_token` | ✓ | — | AWS secret access key |
| `region_name` | — | `"us-east-1"` | AWS region of the target bucket |

`S3Emitter` requires `boto3`. Install it with `pip install "raven-logger[s3]"`.

---

## API

### `raven_log(...) -> dict`

| Parameter | Type | Required | Description |
|---|---|---|---|
| `level` | `"ERROR"` \| `"WARN"` \| `"INFO"` \| `"DEBUG"` \| `"API_INFO"` | ✓ | Log level — see **API_INFO** below |
| `criticality` | `"P1"` \| `"P2"` \| `"P3"` \| `"P4"` | ✓ | Business criticality |
| `message` | `str` | ✓ | Human-readable description |
| `service` | `str` | ✓ | Emitting service name |
| `env` | `"prod"` \| `"staging"` \| `"dev"` | ✓ | Deployment environment |
| `principal` | `str` | ✓ | Acting identity (e.g. `"user:42"`) |
| `project` | `str` | ✓ | Project/team identifier |
| `error_code` | `str` | Required for `ERROR`/`WARN` | Machine-readable error code |
| `trace_id` | `str` | — | Distributed trace ID (auto-generated if omitted) |
| `span_id` | `str` | — | Span ID (auto-generated if omitted) |
| `context` | `dict` | — | Arbitrary structured context |
| `sample_payload` | `str` | — | Redacted payload snippet |
| `_emit` | `callable` | — | Custom emitter; defaults to stdout JSON writer |

Returns the emitted record as a `dict`.

#### `API_INFO` (new in 1.1.0)

`API_INFO` marks a record emitted at the **HTTP request/response boundary** —
one per request, normally by an ASGI middleware rather than by hand. It is a
*channel* marker, not a severity: there is no ordering relationship between it
and `DEBUG`/`INFO`/`WARN`/`ERROR`. Severity for these records lives in
`criticality`, by convention `5xx -> P2`, `4xx -> P3`, everything else `P4`.

`error_code` is **not** required for `API_INFO` — a 404 is a normal outcome of
a healthy API and should not have to invent one. Consumers that want an error
code on 5xx responses enforce that themselves, since only the consumer sees
`context["status_code"]`.

```python
raven_log(
    level="API_INFO", criticality="P4", message="GET /users/{id} 200",
    service="hub", env="prod", principal="anonymous", project="aryx",
    context={
        "http_method": "GET", "http_route": "/users/{id}",
        "status_code": 200, "duration_ms": 3,
        "client_addr": "127.0.0.1:59112", "http_version": "1.1",
    },
)
```

Record `http_route` as the route **template** (`/users/{id}`), never the raw
path — raw paths have unbounded cardinality and make the field useless to
aggregate on.

### `new_trace_id() -> str`

Generates a UUID v4 trace ID for use at service boundaries.

### `new_span_id() -> str`

Generates an 8-character span ID for operations within a trace.

## License

MIT © Giggso
