Express
evlog/express 中间件会在 req.log 上自动创建一个请求范围的日志记录器,并在响应结束时发射一个宽事件。
在我的 Express 应用中设置 evlog
快速开始
1. 安装
pnpm add evlog express
bun add evlog express
yarn add evlog express
npm install evlog express
2. 初始化并注册中间件
import express from 'express'
import { initLogger } from 'evlog'
import { evlog } from 'evlog/express'
initLogger({
env: { service: 'my-api' },
})
const app = express()
app.use(evlog())
app.get('/health', (req, res) => {
req.log.set({ route: 'health' })
res.json({ ok: true })
})
app.listen(3000)
日志记录器通过模块增强在所有地方都可通过 req.log 获得完整的 TypeScript 支持,因此无需额外的类型注解。
宽事件
在处理函数中逐步构建上下文。一个请求 = 一个宽事件:
app.get('/users/:id', async (req, res) => {
const userId = req.params.id
req.log.set({ user: { id: userId } })
const user = await db.findUser(userId)
req.log.set({ user: { name: user.name, plan: user.plan } })
const orders = await db.findOrders(userId)
req.log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })
res.json({ 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() 可以从调用栈中的任何位置访问请求范围的日志记录器,而无需将 req 传递到服务层:
import { useLogger } from 'evlog/express'
export async function 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
}
import { findUser } from './services/user'
app.get('/users/:id', async (req, res) => {
const user = await findUser(req.params.id)
res.json(user)
})
req.log 和 useLogger() 返回相同的日志记录器实例。useLogger() 使用 AsyncLocalStorage 在异步边界之间传播日志记录器。
后台工作 (log.fork)
在响应结束后才完成的即发即弃异步工作,不能再更新请求宽事件(日志记录器在发射后会被封存)。请使用 req.log.fork(label, fn),这样 fn 内部的 useLogger() 会指向一个子日志记录器,该记录器会发射自己的事件,并带有 operation 和 _parentRequestId。参见 宽事件 — 发射后。
import { evlog, useLogger } from 'evlog/express'
app.use(evlog())
app.post('/orders', (req, res) => {
req.log.set({ orderId: 'ord_1' })
req.log.fork!('fulfill_order', async () => {
const log = useLogger()
log.set({ step: 'inventory_ok' })
})
res.json({ ok: true })
})
错误处理
使用 createError 创建包含 why、fix 和 link 字段的结构化错误。Express 使用 4 个参数的错误处理中间件:
import { createError, parseError } from 'evlog'
app.get('/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',
})
})
app.use((err, req, res, next) => {
req.log.error(err)
const parsed = parseError(err)
res.status(parsed.status).json({
message: parsed.message,
why: parsed.why,
fix: parsed.fix,
link: parsed.link,
})
})
错误会被捕获并记录,同时包含自定义上下文和结构化错误字段:
14:58:20 ERROR [my-api] GET /checkout 402 in 3ms
├─ error: name=EvlogError message=Payment failed status=402
└─ requestId: 880a50ac-...
配置
有关所有可用选项(initLogger、中间件选项、采样、静默模式等),请参见 配置参考。
排水与增强器
在 middleware 选项中直接配置排水适配器和增强器:
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'
const userAgent = createUserAgentEnricher()
app.use(evlog({
drain: createAxiomDrain(),
enrich: (ctx) => {
userAgent(ctx)
ctx.event.region = process.env.FLY_REGION
},
}))
管道(批处理与重试)
对于生产环境,使用 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())
app.use(evlog({ drain }))
drain.flush() 以确保所有缓冲事件都已发送。有关所有选项,请参见 Pipeline 文档。尾部采样
使用 keep 强制保留特定事件,无论头部采样如何:
app.use(evlog({
drain: createAxiomDrain(),
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}))
路由过滤
使用 include 和 exclude 控制记录哪些路由:
app.use(evlog({
include: ['/api/**'],
exclude: ['/_internal/**', '/health'],
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
},
}))
客户端日志记录
使用 evlog/http 从任何前端向 Express 服务器发送结构化日志。这适用于任何客户端框架(React、Vue、Svelte、原生 JS)。
浏览器设置
import { initLogger, log } from 'evlog'
import { createHttpLogDrain } from 'evlog/http'
const drain = createHttpLogDrain({
drain: { endpoint: '/v1/ingest' },
})
initLogger({ drain })
log.info({ action: 'page_view', path: location.pathname })
摄入端点
添加一个 POST 路由接收来自浏览器的批量 DrainContext[]:
import type { DrainContext } from 'evlog'
app.post('/v1/ingest', express.json(), (req, res) => {
const batch = req.body as DrainContext[]
for (const ctx of batch) {
console.log('[BROWSER]', JSON.stringify(ctx.event))
}
res.sendStatus(204)
})
本地运行
git clone https://github.com/hugorcd/evlog.git
cd evlog
pnpm install
pnpm run example:express
打开 http://localhost:3000 探索交互式测试 UI。
下一步
深入你的 Express 集成:
- Wide Events: 使用上下文分层设计完整事件
- Adapters: 将日志发送到 Axiom、Sentry、PostHog 等
- Sampling: 使用头部和尾部采样控制日志量
- Structured Errors: 抛出包含
why、fix和link字段的错误