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
// 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) => recordhook 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 chainvoltro logs --trace <id>shows locally. - Deliberately not shipped (yet) — no gzip compression (batches ship uncompressed;
maxBatchBytesbounds the uncompressed payload), no built-in sampling (shed by severity withminLevelor by predicate withredact), 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.