NestJS
evlog/nestjs 模块提供 EvlogModule.forRoot(),它会注册一个全局中间件,创建一个请求作用域的日志记录器(可通过 useLogger() 或 req.log 访问),并在响应完成时发送一个广泛事件。
在我的 NestJS 应用中设置 evlog
快速开始
1. 安装
pnpm add evlog @nestjs/common @nestjs/core @nestjs/platform-express
bun add evlog @nestjs/common @nestjs/core @nestjs/platform-express
yarn add evlog @nestjs/common @nestjs/core @nestjs/platform-express
npm install evlog @nestjs/common @nestjs/core @nestjs/platform-express
2. 注册模块
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
@Module({
imports: [
EvlogModule.forRoot(),
],
})
export class AppModule {}
3. 使用 evlog 引导应用
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() 会作为一个全局模块注册,因此中间件会自动应用于所有路由。
广泛事件
通过在控制器和服务中逐步构建上下文,一个请求对应一个广泛事件:
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() 可以从调用堆栈中的任何位置访问请求作用域的日志记录器,而无需将请求对象通过服务层注入:
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
}
}
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findUser(id)
}
}
req.log 和 useLogger() 返回相同的日志记录器实例。useLogger() 使用 AsyncLocalStorage 在异步边界之间传播日志记录器。
后台工作 (log.fork)
使用 req.log.fork(label, fn)(或同一请求中来自 useLogger() 的日志记录器)创建子广泛事件。参见 广泛事件 — 发送后。
import { useLogger } from 'evlog/nestjs'
@Post()
create(@Req() req: Express.Request) {
req.log.fork!('enqueue', async () => {
const log = useLogger()
log.set({ queued: true })
})
return { ok: true }
}
错误处理
使用 createError 创建带有 why、fix 和 link 字段的结构化错误。创建一个 NestJS 异常过滤器来记录并格式化错误:
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 {
// 在 evlog 请求作用域之外——日志不可用
}
const parsed = parseError(error)
response.status(parsed.status).json({
message: parsed.message,
why: parsed.why,
fix: parsed.fix,
link: parsed.link,
})
}
}
将其应用到控制器中:
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: '支付失败',
status: 402,
why: '卡片被发卡行拒绝',
fix: '尝试使用其他支付方式',
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() 中配置排水适配器和增强器:
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():
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({ apiKey: config.get('AXIOM_API_KEY') }),
}),
}),
],
})
export class AppModule {}
管道(批处理与重试)
在生产环境中,使用 createDrainPipeline 包装适配器以实现事件批处理和失败重试:
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 强制保留特定事件,不管头部抽样如何:
EvlogModule.forRoot({
drain: createAxiomDrain(),
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})
路由过滤
通过 include 和 exclude 模式控制哪些路由被记录:
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
pnpm install
pnpm run example:nestjs
打开 http://localhost:3000 以探索交互式测试界面。
下一步
深入你的 NestJS 集成: