Scenarios

Tenant-aware logging

Every wide event automatically carries the right tenant id, drawn from the request — no per-call-site setup, no risk of forgetting.

The problem

You run a multi-tenant app. Every request is "for" a specific tenant (header, JWT claim, subdomain). You want every wide event from a tenant's request to carry their id, so you can:

  • Filter logs by tenant when debugging a customer issue
  • Group analytics by tenant
  • Apply tail sampling differently per tier (enterprise always kept, free heavily sampled)

You don't want to add logger.set({ tenant: { id } }) at every handler — error-prone.

The full code

1. The plugin

A single plugin handles three concerns: extract the id, attach it on onRequestStart, force-keep enterprise errors via tail sampling.

server/plugins/evlog-tenant.ts
import { definePlugin, getGlobalPluginRunner } from 'evlog/toolkit'

interface TenantPlanMap {
  [id: string]: 'free' | 'pro' | 'enterprise'
}

// In a real app this would come from a cache / DB lookup.
// Keeping it inline for the recipe.
const TENANT_PLANS: TenantPlanMap = {
  acme: 'enterprise',
  pied_piper: 'pro',
}

export default definePlugin({
  name: 'tenant',
  onRequestStart({ logger, headers }) {
    const tenantId = headers?.['x-tenant-id']
    if (!tenantId) return

    const plan = TENANT_PLANS[tenantId] ?? 'free'
    logger.set({ tenant: { id: tenantId, plan } })
  },
  keep(ctx) {
    // Always keep errors from enterprise tenants
    if (ctx.context.tenant?.plan === 'enterprise' && ctx.status >= 500) {
      ctx.shouldKeep = true
    }
  },
})

// Register on app boot
getGlobalPluginRunner().add(plugin)

2. Type augmentation (so logger.set({ tenant }) autocompletes)

types/evlog.d.ts
import 'evlog'

declare module 'evlog' {
  interface BaseWideEvent {
    tenant?: {
      id: string
      plan: 'free' | 'pro' | 'enterprise'
    }
  }
}

3. Adjust sampling in your evlog config

nuxt.config.ts
evlog: {
  sampling: {
    rates: { info: 5 },        // aggressive: 5% of info events
    keep: [{ status: 400 }],   // keep 4xx + 5xx (the plugin force-keeps enterprise 5xx)
  },
}

That's it. Every wide event from a request with x-tenant-id: acme carries tenant: { id: 'acme', plan: 'enterprise' }, and 5xx from enterprise tenants are always recorded regardless of sampling.

What it gives you

  • Zero per-call-site code — handlers just call logger.error(err) or whatever, the tenant context rides along automatically
  • Type-safelogger.set({ tenant: { ... } }) autocompletes the right shape
  • Compatible with sampling — heavy sampling for free tier, full retention for enterprise errors

Variants

  • JWT claim instead of header — read getRequestHeader('authorization') in onRequestStart, decode, extract claim
  • Subdomain-based tenant — read getRequestHost(), parse the leftmost label
  • Resolved against a DB — make onRequestStart async (note: blocks the request until resolved; cache aggressively)

Where to go next