Building blocks

Toolkit Reference

The evlog/toolkit public API — every primitive used to build adapters, enrichers, plugins, and framework integrations.

evlog/toolkit is the public surface that every built-in adapter, enricher, and framework integration is built on. If you're publishing a community package on top of evlog, this is your entry point.

The toolkit is marked as beta. The shape is stable (used in production by every built-in) and follows semver — but breaking changes are still possible while community usage validates the surface.

Quick Reference

import {
  // Plugins — the unified extension contract
  definePlugin,
  drainPlugin,
  enricherPlugin,
  composePlugins,

  // Drains
  defineDrain,
  defineHttpDrain,
  composeDrains,

  // Enrichers
  defineEnricher,
  composeEnrichers,

  // Tail sampling
  composeKeep,

  // Configuration
  defineEvlog,
  toLoggerConfig,
  toMiddlewareOptions,
  resolveAdapterConfig,
  type ConfigField,

  // Framework integrations
  defineFrameworkIntegration,
  createMiddlewareLogger,
  createLoggerStorage,
  type BaseEvlogOptions,

  // HTTP transport
  httpPost,

  // Helpers
  getHeader,
  normalizeNumber,
  extractSafeHeaders,
  extractSafeNodeHeaders,
  mergeEventField,
  toTypedAttributeValue,
  toOtlpAttributeValue,
  OTEL_SEVERITY_NUMBER,
  OTEL_SEVERITY_TEXT,
} from 'evlog/toolkit'

The plugin contract

definePlugin is the canonical extension contract. Drains and enrichers are sugar over it.

import { definePlugin } from 'evlog/toolkit'

const requestMetricsPlugin = definePlugin({
  name: 'request-metrics',

  setup({ env }) {
    statsd.init({ service: env.service })
  },

  enrich({ event }) {
    event.tier = event.duration && event.duration > 1000 ? 'slow' : 'fast'
  },

  drain({ event }) {
    statsd.timing('http.request', event.duration as number, { path: event.path as string })
  },

  onRequestStart({ logger, request }) {
    logger.set({ trace: { startedAt: Date.now() } })
  },

  onRequestFinish({ event, durationMs }) {
    if (event && (event.level === 'error' || durationMs > 5000)) {
      // alert / forward / etc.
    }
  },

  onClientLog({ event }) {
    // Hook into client-side logs received via /api/_evlog/ingest
  },

  extendLogger(logger) {
    // Add custom typed methods to RequestLogger here
  },
})

Register it once via defineEvlog({ plugins: [requestMetricsPlugin] }) or scoped per-middleware via evlog({ plugins: [requestMetricsPlugin] }). Plugins run in registration order; errors in any hook are isolated and logged with the [evlog/<name>] prefix.

Sugar plugins

import { drainPlugin, enricherPlugin } from 'evlog/toolkit'

const drainOnly = drainPlugin('axiom', createAxiomDrain())
const enricherOnly = enricherPlugin('user-agent', createUserAgentEnricher())

defineHttpDrain — the adapter recipe

import {
  defineHttpDrain,
  resolveAdapterConfig,
  type ConfigField,
} from 'evlog/toolkit'

interface AcmeConfig {
  apiKey: string
  endpoint?: string
  timeout?: number
}

const FIELDS: ConfigField<AcmeConfig>[] = [
  { key: 'apiKey', env: ['ACME_API_KEY'] },
  { key: 'endpoint', env: ['ACME_ENDPOINT'] },
  { key: 'timeout' },
]

export function createAcmeDrain(overrides?: Partial<AcmeConfig>) {
  return defineHttpDrain<AcmeConfig>({
    name: 'acme',
    resolve: async () => {
      const cfg = await resolveAdapterConfig<AcmeConfig>('acme', FIELDS, overrides)
      return cfg.apiKey ? cfg as AcmeConfig : null
    },
    encode: (events, cfg) => ({
      url: `${cfg.endpoint ?? 'https://api.acme.com'}/v1/ingest`,
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.apiKey}` },
      body: JSON.stringify(events),
    }),
  })
}

defineHttpDrain handles batching, retries (default 2), timeouts (default 5000ms), and error isolation.

defineEnricher — the enricher recipe

import { defineEnricher, getHeader } from 'evlog/toolkit'

export const tenantEnricher = defineEnricher<{ id: string }>({
  name: 'tenant',
  field: 'tenant',
  compute: ({ headers }) => {
    const id = getHeader(headers, 'x-tenant-id')
    return id ? { id } : undefined
  },
})

defineEvlog — canonical config

A single config object that works everywhere — initLogger, framework middlewares, the Nuxt module, Workers.

import { defineEvlog, toLoggerConfig, toMiddlewareOptions } from 'evlog/toolkit'

export const evlogConfig = defineEvlog({
  service: 'shop',
  environment: process.env.NODE_ENV,
  drain: createAxiomDrain(),
  enrich: createDefaultEnrichers(),
  plugins: [requestMetricsPlugin],
})

// Standalone
initLogger(toLoggerConfig(evlogConfig))

// Framework
app.use(evlog(toMiddlewareOptions(evlogConfig)))

defineFrameworkIntegration — the framework recipe

For any framework with a (ctx, next) middleware shape (Hono, Express, Elysia, Fastify, …) — see Custom Integration for the full guide.

Composition

import { composeDrains, composeEnrichers, composeKeep } from 'evlog/toolkit'

const drain = composeDrains([createAxiomDrain(), createSentryDrain()])
const enrich = composeEnrichers([createUserAgentEnricher(), createGeoEnricher()])
const keep = composeKeep([
  ({ duration, shouldKeep }) => duration && duration > 2000 ? true : shouldKeep,
  ({ event }) => event.level === 'error',
])

All composers isolate errors in individual functions and run drains concurrently with Promise.allSettled semantics.

Helpers

ExportPurpose
httpPost(opts)POST helper used by every built-in HTTP adapter — handles timeout, retries, redacted error messages
resolveAdapterConfig(ns, fields, overrides)Standard config priority: overrides → runtimeConfig.evlog.<ns>runtimeConfig.<ns>NUXT_<NS>_*<NS>_*
getHeader(headers, name)Case-insensitive HTTP header lookup
normalizeNumber(value)Parse a string to number, return undefined if non-finite
extractSafeHeaders(headers)Filter sensitive headers from a Web Headers
extractSafeNodeHeaders(headers)Filter sensitive headers from Node IncomingHttpHeaders
mergeEventField(existing, computed, overwrite?)Merge a sub-object into an event field, respecting overwrite
toTypedAttributeValue(value)Convert any value to the typed attribute shape used by Axiom / Sentry
toOtlpAttributeValue(value)Convert any value to the OTLP AnyValue shape (used by OTLP / HyperDX / PostHog logs)
OTEL_SEVERITY_NUMBER, OTEL_SEVERITY_TEXTOTEL log severity tables

Building a community package

The recommended structure for a community package on top of evlog:

my-evlog-pkg/
├─ src/
│  ├─ drain.ts        # createMyDrain via defineHttpDrain
│  ├─ enricher.ts     # createMyEnricher via defineEnricher
│  └─ index.ts        # re-exports
├─ test/              # vitest, mock fetch
├─ package.json       # peerDependency: "evlog"
└─ README.md

Add evlog as a peerDependency (not a dependency) — your package shouldn't pull in a copy of evlog at install time.

Built something great? Open a PR to add a row to the Adapters / Enrichers tables — the community will thank you.

See Also