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) => 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.