Frameworks

NestJS

Source Code
Using evlog with NestJS — automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in NestJS applications.

The evlog/nestjs module provides EvlogModule.forRoot() which registers a global middleware, creating a request-scoped logger accessible via useLogger() or req.log, emitting a wide event when the response completes.

Quick Start

1. Install

bun add evlog @nestjs/common @nestjs/core @nestjs/platform-express

2. Register the module

src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'

@Module({
  imports: [
    EvlogModule.forRoot(),
  ],
})
export class AppModule {}

3. Bootstrap with evlog

src/main.ts
import 'reflect-metadata'
import { NestFactory } from '@nestjs/core'
import { initLogger } from 'evlog'
import { AppModule } from './app.module'

initLogger({
  env: { service: 'my-api' },
})

const app = await NestFactory.create(AppModule)
await app.listen(3000)

EvlogModule.forRoot() registers as a global module — the middleware is automatically applied to all routes.

Wide Events

Build up context progressively through your controllers and services. One request = one wide event:

src/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common'
import { useLogger } from 'evlog/nestjs'

@Controller('users')
export class UsersController {
  @Get(':id')
  async findOne(@Param('id') id: string) {
    const log = useLogger()

    log.set({ user: { id } })

    const user = await db.findUser(id)
    log.set({ user: { name: user.name, plan: user.plan } })

    const orders = await db.findOrders(id)
    log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })

    return { user, orders }
  }
}

All fields are merged into a single wide event emitted when the request completes:

Terminal output
14:58:15 INFO [my-api] GET /users/usr_123 200 in 12ms
  ├─ orders: count=2 totalRevenue=6298
  ├─ user: id=usr_123 name=Alice plan=pro
  └─ requestId: 4a8ff3a8-...

useLogger()

Use useLogger() to access the request-scoped logger from anywhere in the call stack — no need to inject the request object through your service layer:

src/users.service.ts
import { useLogger } from 'evlog/nestjs'

export class UsersService {
  async findUser(id: string) {
    const log = useLogger()
    log.set({ user: { id } })

    const user = await db.findUser(id)
    log.set({ user: { name: user.name, plan: user.plan } })

    return user
  }
}
src/users.controller.ts
@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findUser(id)
  }
}

Both req.log and useLogger() return the same logger instance. useLogger() uses AsyncLocalStorage to propagate the logger across async boundaries.

Error Handling

Use createError for structured errors with why, fix, and link fields. Create a NestJS exception filter to log and format errors:

src/evlog-exception.filter.ts
import { Catch } from '@nestjs/common'
import type { ExceptionFilter, ArgumentsHost } from '@nestjs/common'
import { parseError } from 'evlog'
import { useLogger } from 'evlog/nestjs'

@Catch()
export class EvlogExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse()
    const error = exception instanceof Error ? exception : new Error(String(exception))

    try { useLogger().error(error) } catch {}

    const parsed = parseError(error)
    response.status(parsed.status).json({
      message: parsed.message,
      why: parsed.why,
      fix: parsed.fix,
      link: parsed.link,
    })
  }
}

Apply it to your controllers:

src/checkout.controller.ts
import { Controller, Get, UseFilters } from '@nestjs/common'
import { createError } from 'evlog'
import { EvlogExceptionFilter } from './evlog-exception.filter'

@Controller()
@UseFilters(new EvlogExceptionFilter())
export class CheckoutController {
  @Get('checkout')
  checkout() {
    throw createError({
      message: 'Payment failed',
      status: 402,
      why: 'Card declined by issuer',
      fix: 'Try a different payment method',
      link: 'https://docs.example.com/payments/declined',
    })
  }
}

The error is captured and logged with both the custom context and structured error fields:

Terminal output
14:58:20 ERROR [my-api] GET /checkout 402 in 3ms
  ├─ error: name=EvlogError message=Payment failed status=402
  └─ requestId: 880a50ac-...

Drain & Enrichers

Configure drain adapters and enrichers in EvlogModule.forRoot():

src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'

const userAgent = createUserAgentEnricher()

@Module({
  imports: [
    EvlogModule.forRoot({
      drain: createAxiomDrain(),
      enrich: (ctx) => {
        userAgent(ctx)
        ctx.event.region = process.env.FLY_REGION
      },
    }),
  ],
})
export class AppModule {}

Async Configuration

Use forRootAsync() when options depend on other providers (e.g. ConfigService):

src/app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { EvlogModule } from 'evlog/nestjs'
import { createAxiomDrain } from 'evlog/axiom'

@Module({
  imports: [
    ConfigModule.forRoot(),
    EvlogModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        drain: createAxiomDrain({ token: config.get('AXIOM_TOKEN') }),
      }),
    }),
  ],
})
export class AppModule {}

Pipeline (Batching & Retry)

For production, wrap your adapter with createDrainPipeline to batch events and retry on failure:

src/app.module.ts
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())

EvlogModule.forRoot({ drain })
Call drain.flush() on server shutdown to ensure all buffered events are sent. See the Pipeline docs for all options.

Tail Sampling

Use keep to force-retain specific events regardless of head sampling:

src/app.module.ts
EvlogModule.forRoot({
  drain: createAxiomDrain(),
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})

Route Filtering

Control which routes are logged with include and exclude patterns:

src/app.module.ts
EvlogModule.forRoot({
  include: ['/api/**'],
  exclude: ['/_internal/**', '/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})

Run Locally

git clone https://github.com/HugoRCD/evlog.git
cd evlog
bun install
bun run example:nestjs

Open http://localhost:3000 to explore the interactive test UI.

Source Code

Browse the complete NestJS example source on GitHub.