Skip to content

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

快速开始

urlservice 必填

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:字符串为精确匹配或「前缀 + /」;RegExptest(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 ← header app-id
  • authTypeAuthorization 前缀启发式(Bearer ak_ → apiKey,Bearer eyJ → jwt)
  • userId ← 未验签解析 JWT payload 的 sub / userId / id
  • clientKey ← header client-key(Ones App Client)
  • platform ← header x-platform
  • appVersion ← header x-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): Middleware

createRequestLogger 为同名别名(已废弃)。

RequestLoggerOptions

参数类型必填默认说明
urlstring远程 ingest URL
servicestring服务标识
headersRecord<string, string>{}上报请求额外头
timeoutnumber5000上报超时(毫秒)
enabledbooleantrue总开关
excludePaths(string | RegExp)[][]排除路径
sanitizeSanitizeConfig内置默认body/headers/response 脱敏
onError(error, { droppedCount }) => void结构化 warn JSON上报失败回调
circuitBreakerCircuitBreakerConfig见下熔断
errorThrottleErrorThrottleConfig见下错误节流
stdoutStdoutConfig见下stdout 双写
sampleRatenumber10–1,采样率
requestIdHeaderstring'x-request-id'req.id 时读取的头
getAppIdContextGetter自定义 appId
getUserIdContextGetter自定义 userId
getAuthTypeContextGetter自定义 authType

ContextGetter

typescript
(context: RequestLoggerContext) => string | undefined | Promise<string | undefined>

RequestLoggerContext 含:reqresponsemethodurlpathheadersbodyresponseData

CircuitBreakerConfig

参数默认说明
failureThreshold5连续失败次数后打开熔断
resetTimeout60000熔断后等待再试(毫秒)

ErrorThrottleConfig

参数默认说明
interval60000同类错误节流间隔(毫秒)

StdoutConfig

参数默认说明
enabledtrueenabled !== false 即输出
format'json''json' | 'text'
includeBodytrue是否含请求体
includeResponsefalse是否含响应体

SanitizeConfig

参数默认说明
removeFieldspassword / secret 等字段名精确匹配(小写)→ 占位符
maskFieldstoken / authorization 等字段名包含匹配 → 部分脱敏
placeholder'[REDACTED]'占位
maxDepth10递归深度

另导出:sanitizesanitizeHeadersisSensitiveField(见包源码)。

上报载荷(示意)

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

相关链接