Toolkit Reference
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.
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
| Export | Purpose |
|---|---|
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_TEXT | OTEL 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.
See Also
- Custom Adapters — the
defineHttpDrainrecipe in detail - Custom Enrichers — the
defineEnricherrecipe in detail - Custom Integration — the
defineFrameworkIntegrationrecipe in detail - Pipeline — batching and retry primitives for high-throughput drains
- HTTP drain — browser→server log transport
Custom Adapters
Build your own adapter to send logs to any destination using defineHttpDrain — config resolution, retries, timeouts, and error handling are handled for you.
Overview
Enrich your wide events with derived context like user agent, geo data, request size, and trace context. Built-in enrichers and custom enricher support.