Request Logger
@vafast/request-logger 是 HTTP 访问日志中间件:记录方法、路径、耗时、脱敏后的 headers/body/response,异步上报远程服务,并可双写 stdout。
与 @vafast/logger 的区别
| 包 | 用途 |
|---|---|
@vafast/logger | 应用内 logger.info / error(不是中间件) |
@vafast/request-logger | 每个 HTTP 请求的访问日志中间件 |
安装
bash
npm install @vafast/request-logger快速开始
url 与 service 必填:
typescript
import { Server, defineRoute, defineRoutes, serve } from 'vafast'
import { requestId } from '@vafast/request-id'
import { requestLogger } from '@vafast/request-logger'
const routes = defineRoutes([
defineRoute({
method: 'GET',
path: '/',
handler: () => ({ ok: true }),
}),
])
const server = new Server(routes)
server.use(requestId())
server.use(
requestLogger({
url: 'http://log-server:9005/api/logs/ingest',
service: 'my-server',
}),
)
serve({ fetch: server.fetch, port: 3000 })默认行为:stdout JSON 双写开启、敏感字段脱敏、上报失败熔断 + 错误节流。
用法
认证头与排除路径
typescript
server.use(
requestLogger({
url: process.env.LOG_INGEST_URL!,
service: 'auth-server',
headers: {
Authorization: `Bearer ${process.env.LOG_INGEST_TOKEN}`,
},
excludePaths: ['/health', '/metrics', /^\/internal/],
}),
)excludePaths:字符串为精确匹配或「前缀 + /」;RegExp 则 test(path)。
路由级关闭
在路由定义上设 log: false(经框架 getRoute 读取):
typescript
defineRoute({
method: 'GET',
path: '/health',
log: false,
handler: () => ({ ok: true }),
})stdout 双写(K8s)
typescript
requestLogger({
url: '...',
service: 'auth-server',
stdout: {
enabled: true, // 默认 true;设 false 可关闭
format: 'json', // 或 'text'
includeBody: true,
includeResponse: false, // 响应体可能很大
},
})stdout 级别:2xx → 30 (INFO),4xx → 40 (WARN),5xx → 50 (ERROR)。
自定义业务字段
typescript
requestLogger({
url: '...',
service: 'billing-server',
getUserId: ({ req }) => req.__locals?.userInfo?.id,
getAppId: async ({ path, body }) => {
if (path !== '/notify/alipay') return undefined
const form = body as Record<string, string>
return lookupAppId(form.out_trade_no)
},
getAuthType: ({ headers }) =>
headers.authorization?.startsWith('Bearer ak_') ? 'apiKey' : undefined,
})未提供 getter 时的默认:
appId← headerapp-idauthType←Authorization前缀启发式(Bearer ak_→ apiKey,Bearer eyJ→ jwt)userId← 未验签解析 JWT payload 的sub/userId/idclientKey← headerclient-key(Ones App Client)platform← headerx-platformappVersion← headerx-app-version
端字段在 headers 脱敏前解析并写入 ingest 顶层;缺失或空串为 null。log-server 不再从 headers 兜底。
采样与熔断
typescript
requestLogger({
url: '...',
service: 'gateway',
sampleRate: 0.1, // 只记约 10%
circuitBreaker: {
failureThreshold: 5,
resetTimeout: 60_000,
},
errorThrottle: { interval: 60_000 },
onError: (error, { droppedCount }) => {
console.warn(error.message, droppedCount)
},
})与 request-id
优先 req.id,否则读 requestIdHeader(默认 x-request-id):
typescript
server.use(requestId())
server.use(requestLogger({ url: '...', service: 'my-server' }))API完整参数
requestLogger(options)
typescript
requestLogger(options: RequestLoggerOptions): MiddlewarecreateRequestLogger 为同名别名(已废弃)。
RequestLoggerOptions
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
url | string | 是 | — | 远程 ingest URL |
service | string | 是 | — | 服务标识 |
headers | Record<string, string> | 否 | {} | 上报请求额外头 |
timeout | number | 否 | 5000 | 上报超时(毫秒) |
enabled | boolean | 否 | true | 总开关 |
excludePaths | (string | RegExp)[] | 否 | [] | 排除路径 |
sanitize | SanitizeConfig | 否 | 内置默认 | body/headers/response 脱敏 |
onError | (error, { droppedCount }) => void | 否 | 结构化 warn JSON | 上报失败回调 |
circuitBreaker | CircuitBreakerConfig | 否 | 见下 | 熔断 |
errorThrottle | ErrorThrottleConfig | 否 | 见下 | 错误节流 |
stdout | StdoutConfig | 否 | 见下 | stdout 双写 |
sampleRate | number | 否 | 1 | 0–1,采样率 |
requestIdHeader | string | 否 | 'x-request-id' | 无 req.id 时读取的头 |
getAppId | ContextGetter | 否 | — | 自定义 appId |
getUserId | ContextGetter | 否 | — | 自定义 userId |
getAuthType | ContextGetter | 否 | — | 自定义 authType |
ContextGetter:
typescript
(context: RequestLoggerContext) => string | undefined | Promise<string | undefined>RequestLoggerContext 含:req、response、method、url、path、headers、body、responseData。
CircuitBreakerConfig
| 参数 | 默认 | 说明 |
|---|---|---|
failureThreshold | 5 | 连续失败次数后打开熔断 |
resetTimeout | 60000 | 熔断后等待再试(毫秒) |
ErrorThrottleConfig
| 参数 | 默认 | 说明 |
|---|---|---|
interval | 60000 | 同类错误节流间隔(毫秒) |
StdoutConfig
| 参数 | 默认 | 说明 |
|---|---|---|
enabled | true | enabled !== false 即输出 |
format | 'json' | 'json' | 'text' |
includeBody | true | 是否含请求体 |
includeResponse | false | 是否含响应体 |
SanitizeConfig
| 参数 | 默认 | 说明 |
|---|---|---|
removeFields | password / secret 等 | 字段名精确匹配(小写)→ 占位符 |
maskFields | token / authorization 等 | 字段名包含匹配 → 部分脱敏 |
placeholder | '[REDACTED]' | 占位 |
maxDepth | 10 | 递归深度 |
另导出:sanitize、sanitizeHeaders、isSensitiveField(见包源码)。
上报载荷(示意)
typescript
{
method, url, path, headers, body, query,
status, duration, service,
appId, authType, userId, clientKey, platform, appVersion,
ip, traceId, userAgent,
createdAt, response,
clientIp?, requestId?
}最佳实践
- 必填
url+service;本地可把url指到假服务,或enabled: false - 先
requestId()再requestLogger(),保证traceId/requestId一致 - 健康检查、metrics 用
excludePaths或路由log: false - 高 QPS 用
sampleRate;响应体默认不要开stdout.includeResponse - 支付回调等无登录态场景用
getAppId从 body 反查租户
注意事项
- 上报在
next()之后异步执行,不阻塞响应;失败只触发熔断 /onError,不影响业务 - 会在业务前
req.clone()读 body;跳过日志的路径(排除 /log: false/ 采样)不会读 body - 没有默认排除
/health——需自己配置excludePaths - JWT
userId默认解析不验签,仅用于日志归属 - sanitize 配置字段是
removeFields/maskFields,不是fields/mask