Drains & Adapters

Control output format and ship logs to external backends with evlog

The Vercube logger is backed by evlog, so output formatting and shipping logs to external systems is handled by evlog's output modes, drains and adapters rather than by custom provider classes.

Output modes

evlog has two built-in output modes, selected automatically by environment and controllable via configure().

Pretty (development)

Human-readable, colorized output. Enabled automatically outside production.

container.get(Logger).configure({ logLevel: 'debug', pretty: true });
12:34:56.789 [UserService::createUser] creating user
12:34:56.801 INFO  [app]
  ├─ userId: abc-123
  └─ message: UserService::createUser

Disable colors with the standard NO_COLOR environment variable.

JSON (production)

Structured JSON, ideal for log aggregation (ELK, Splunk, CloudWatch, …). Used automatically in production, or force it by disabling pretty:

container.get(Logger).configure({ logLevel: 'info', pretty: false });
{"level":"info","userId":"abc-123","message":"UserService::createUser","timestamp":"2026-01-01T12:34:56.801Z"}

When pretty is false, stringify: true (the default) emits JSON strings; set stringify: false for raw objects (e.g. Cloudflare Workers).

Drains

A drain is a callback invoked with every emitted event - use it to forward logs anywhere. Configure it globally via configure():

container.get(Logger).configure({
  logLevel: 'info',
  drain: async (ctx) => {
    await fetch('https://logs.example.com/ingest', {
      method: 'POST',
      body: JSON.stringify(ctx.event),
    });
  },
});

For request-scoped drains/enrichment, pass options to the EvlogMiddleware (see the core middleware).

Adding a drain without replacing the configuration

configure() sets the whole configuration, which makes it the wrong tool for a package that only wants to observe. Use addDrain instead:

container.get(Logger).addDrain('metrics', ({ event }) => {
  if (event.level === 'error') {
    errorCounter.add(1);
  }
});

Registered drains survive a later configure() call, and registering the same name twice replaces the previous registration rather than stacking. addEnricher(name, fn) and addPlugin(plugin) work the same way for evlog's other extension points.

Context providers

evlog runs its enrich hook only for request wide events, so it cannot decorate a plain logger.info(). A context provider covers exactly the calls it misses: it is consulted for every event, and its fields are merged in first, so anything the call site passes wins.

container.get(Logger).addContextProvider(() => ({ region: process.env.FLY_REGION }));

This is how Telemetry puts traceId and spanId on every log line.

Adapters

evlog ships first-class adapters for popular backends. They are plain drains, so they plug straight into configure({ drain }):

import { createAxiomDrain } from 'evlog/axiom';

container.get(Logger).configure({
  drain: createAxiomDrain({ dataset: 'logs', token: process.env.AXIOM_TOKEN! }),
});

Available adapters include Axiom, OTLP, Sentry, Datadog, PostHog, Better Stack, HyperDX, filesystem and memory - see the evlog adapters overview.

Batching & retries

Wrap any drain with evlog's pipeline for batching and retry:

import { createDrainPipeline } from 'evlog/pipeline';
import { createAxiomDrain } from 'evlog/axiom';

const pipeline = createDrainPipeline({ batch: { size: 25 } });
const drain = pipeline(createAxiomDrain({ dataset: 'logs', token: '...' }));

container.get(Logger).configure({ drain });

Sampling & redaction

Reduce volume and protect PII with evlog's built-ins:

container.get(Logger).configure({
  // probabilistic sampling on top of `minLevel`
  sampling: { rates: { info: 0.25, debug: 0.05 } },
  // redaction: `true` enables built-in patterns (email, JWT, card, …)
  redact: { paths: ['user.password', 'headers.authorization'] },
});

Per-request wide events

@vercube/core registers EvlogMiddleware by default, emitting one wide event per request (method, path, status, duration). Configure or disable it:

vercube.config.ts
import { defineConfig } from '@vercube/core';

export default defineConfig({
  // set to false to turn off per-request wide events
  requestLogging: true,
});

For advanced integrations (custom request loggers, route filtering, enrich/keep callbacks) the evlog toolkit is re-exported from @vercube/logger/toolkit:

import { createMiddlewareLogger, extractSafeHeaders } from '@vercube/logger/toolkit';
Previous

API

Complete API reference for the evlog-backed Logger module

Next