框架

NestJS

Source Code
Automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in NestJS applications.

evlog/nestjs 模块提供 EvlogModule.forRoot(),它会注册一个全局中间件,创建一个请求作用域的日志记录器(可通过 useLogger()req.log 访问),并在响应完成时发送一个广泛事件。

提示
在 NestJS 应用中设置 evlog。

- 安装 evlog:pnpm add evlog
- 导入 EvlogModule 并将 EvlogModule.forRoot() 添加到 AppModule 的导入中
- 每个请求会自动创建一个请求作用域的日志记录器
- 在任意控制器或服务中使用 useLogger() 访问日志记录器
- 使用 log.set() 累积上下文,使用 throw createError() 发送结构化错误
- 可选地向 forRoot() 传递 drain、enrich 和 keep 回调函数

文档:https://www.evlog.dev/frameworks/nestjs
适配器:https://www.evlog.dev/adapters

快速开始

1. 安装

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

2. 注册模块

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

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

3. 使用 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() 会作为一个全局模块注册,因此中间件会自动应用于所有路由。

广泛事件

通过在控制器和服务中逐步构建上下文,一个请求对应一个广泛事件:

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 }
  }
}

所有字段会在请求结束时合并为一个广泛事件并发出:

终端输出
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()

使用 useLogger() 可以从调用堆栈中的任何位置访问请求作用域的日志记录器,而无需将请求对象通过服务层注入:

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)
  }
}

req.loguseLogger() 返回相同的日志记录器实例。useLogger() 使用 AsyncLocalStorage 在异步边界之间传播日志记录器。

错误处理

使用 createError 创建带有 whyfixlink 字段的结构化错误。创建一个 NestJS 异常过滤器来记录并格式化错误:

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,
    })
  }
}

将其应用到控制器中:

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',
    })
  }
}

该错误会被捕获并记录,同时包含自定义上下文和结构化错误字段:

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

配置

请参阅 配置参考 了解所有可用选项(initLogger、中间件选项、采样、静默模式等)。

排水(drain)与增强器(enrichers)

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 {}

异步配置

当选项依赖于其他提供者(例如 ConfigService)时,请使用 forRootAsync()

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 {}

管道(批处理与重试)

在生产环境中,使用 createDrainPipeline 包装适配器以实现事件批处理和失败重试:

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 })
在服务器关闭时调用 drain.flush() 以确保所有缓冲事件被发送。有关所有选项,请参阅 管道文档。 ::

抽样

使用 keep 强制保留特定事件,不管头部抽样如何:
src/app.module.ts
EvlogModule.forRoot({
  drain: createAxiomDrain(),
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
})

路由过滤

通过 includeexclude 模式控制哪些路由被记录:
src/app.module.ts
EvlogModule.forRoot({
  include: ['/api/**'],
  exclude: ['/_internal/**', '/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})

本地运行

终端
git clone https://github.com/hugorcd/evlog.git
cd evlog
bun install
bun run example:nestjs
打开 http://localhost:3000 以探索交互式测试界面。

源代码

在 GitHub 上浏览完整的 NestJS 示例源代码。

下一步

深入你的 NestJS 集成:
  • 广泛事件:设计具有上下文层次结构的综合性事件
  • 适配器:将日志发送到 Axiom、Sentry、PostHog 等
  • 抽样:使用头部和尾部抽样控制日志量
  • 结构化错误:使用 whyfixlink 字段抛出错误