# Log shipping

> Ship structured logs to Better Stack / Axiom / Loki / any HTTP sink — batched, redacted, fail-soft. Rides the framework log-sink hook.



---

<!-- source: en/plugins/logship.md -->
## Log shipping

_Ship structured logs to Better Stack / Axiom / Loki / any HTTP sink — batched, redacted, fail-soft. Rides the framework log-sink hook._

`@voltro/plugin-logship` forwards the framework's structured `@voltro/logger` stream to a log backend. It rides the log-sink hook (`addSink`), batches records, redacts secrets, and fails soft — a backend outage never disturbs the app.

## Wiring

```ts
// app.config.ts
import { logshipPlugin } from '@voltro/plugin-logship'

export default {
  type: 'api' as const,
  name: 'api',
  plugins: [
    // Pick ONE backend (discriminated union):
    logshipPlugin({ backend: 'betterstack', token: process.env.BETTERSTACK_TOKEN! }),
    // logshipPlugin({ backend: 'axiom', token: process.env.AXIOM_TOKEN!, dataset: 'voltro' }),
    // logshipPlugin({ backend: 'loki', endpoint: process.env.LOKI_URL!, tags: { app: 'api' } }),
    // logshipPlugin({ backend: 'http', endpoint: 'https://logs.example.com/ingest', headers: { ... } }),
  ],
}
```

## Behaviour

- **Batching** — records buffer and flush on a size/interval threshold (one HTTP POST per batch, not per line). The batch is bounded on BOTH axes: record count (`maxBatch`, default 200) and approximate payload bytes (`maxBatchBytes`, default 900 000 — headroom under the ~1 MB tiers Better Stack / Axiom / Loki cap at). A burst of large records flushes early so a batch can't exceed the intake's payload limit; a single record larger than the cap still ships alone (the cap bounds the batch, not the record).
- **Redaction** — a `redact: (record) => record` hook rewrites each record before shipping (drop or mask fields), e.g. `redact: (r) => ({ ...r, fields: { ...r.fields, token: undefined } })`.
- **Fail-soft** — a backend error logs a local warning and drops the batch; the app keeps running. Logs are observability, not a hard dependency.
- **Trace correlation** — every record carries `fields.traceId`, so the shipped logs join the same end-to-end chain `voltro logs --trace <id>` shows locally.
- **Deliberately not shipped (yet)** — no gzip compression (batches ship uncompressed; `maxBatchBytes` bounds the uncompressed payload), no built-in sampling (shed by severity with `minLevel` or by predicate with `redact`), and no bounded retry (a failed batch is dropped, not requeued — the shed-don't-requeue fail-soft posture). Each is additive and roadmap, not a phantom advertised feature.

## Backends

| `backend` | Required | Endpoint |
|---|---|---|
| `betterstack` | `token` (opt. `endpoint`, `tags`) | Better Stack Logs ingest |
| `axiom` | `token`, `dataset` (opt. `endpoint`, `tags`) | Axiom `/v1/datasets/<dataset>/ingest` |
| `loki` | `endpoint` (opt. `tags`) | Grafana Loki `/loki/api/v1/push` |
| `http` | `endpoint` (opt. `headers`, `tags`) | any JSON-array ingest |

The request shaping per backend is pure (`betterStackRequest` / `axiomRequest` / `lokiRequest` / `httpRequest` → a `ShipRequest`), so the mapping is unit-tested without a network.

## Permissions

`network:outbound:<host>` (declared from the backend host) — the plugin only ships to the configured host.
