Enrichers as packages
Custom enrichers (Custom enrichers) are pure functions over the event context — perfect material for an npm package. Publish once, install everywhere.
Why package this?
- Same context across services — every app pulls
tenant_idfrom the same header, formatted the same way - Versioned semantics — bumping the package is the audit trail of context-shape changes
- Single bug fix point — fix the enricher once, every consumer gets it
Scaffold
import { defineEnricher } from 'evlog/toolkit'
export interface TenantEnricherOptions {
/** Header name carrying the tenant id. Default: `x-tenant-id`. */
header?: string
/** Whether to fail-soft when the header is absent. Default: true. */
optional?: boolean
}
export function createTenantEnricher(options: TenantEnricherOptions = {}) {
const header = options.header ?? 'x-tenant-id'
const optional = options.optional ?? true
return defineEnricher({
name: 'tenant',
enrich: ({ event, headers }) => {
const tenantId = headers?.[header]
if (!tenantId) {
if (!optional) {
console.warn('[evlog/tenant] missing header', header)
}
return
}
event.tenant = { id: tenantId }
},
})
}
Package layout
{
"name": "@my-org/evlog-tenant-enricher",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"files": ["dist"],
"peerDependencies": {
"evlog": "^2"
}
}
Augmenting evlog types (optional but recommended)
If your enricher writes a structured field, augment the wide event type so consumers get autocomplete on it:
declare module 'evlog' {
interface BaseWideEvent {
tenant?: { id: string }
}
}
The augmentation flows transitively to consumers via the published .d.mts — they don't have to redeclare it.
Consuming it
import { createTenantEnricher } from '@my-org/evlog-tenant-enricher'
nitroApp.hooks.hook('evlog:enrich', createTenantEnricher())
That's it — every wide event now carries tenant.id when the request had the header.
Publishing checklist
- Enricher is a pure function of
(event, headers, request)— no side effects - Failing soft when the header is missing (don't crash production over a missing tenant id)
-
declare module 'evlog'augmentingBaseWideEventfor type-level discoverability - Test with
vitestusing a fakeEnrichContext - README explaining the header / config and the field shape
Real examples to ship
@my-org/evlog-tenant-enricher— extractsx-tenant-idfrom request headers@my-org/evlog-deploy-info— addscommitHash/region/versionfrom env at boot@my-org/evlog-feature-flags— attaches the resolved feature flags for the request@my-org/evlog-trace-context— pullstraceparentand exposes parsedtrace_id/span_id@my-org/evlog-experiment-cohort— addsexperiment.cohortbased on user id
For multi-hook plugins (e.g. enrich + tail-sample + side-effect on drain), package as a plugin instead — same scaffolding, swap defineEnricher for definePlugin.
Drains as packages
Package a custom drain as a reusable npm library — `@my-org/evlog-internal-loki`, `@my-org/evlog-pubsub`, etc. Same scaffolding pattern as catalogs.
Integration as package
Package a custom framework integration as an npm library so your team — or the open-source community — can install evlog support for runtime X with one command.