Access Log 中间件

VextJS 内置 Access Log 中间件,自动记录每个 HTTP 请求的方法、路径、状态码、响应时间和客户端 IP。该中间件基于洋葱模型(after-middleware 模式),在请求处理完成后输出一行紧凑的访问日志。

基本行为

Access Log 中间件默认启用,无需任何配置即可工作:

// 无需手动注册,框架自动挂载
// 每个请求完成后自动输出一行日志

请求完成后,终端输出类似:

[17:53:26.174] INFO: GET /api/users 200 3ms | 127.0.0.1

日志格式

Access Log 采用紧凑单行格式,在开发和生产环境下呈现不同的输出样式:

开发模式(Pretty)

logger.prettytrue(开发环境默认值)时,pino-pretty 会对日志进行着色和格式化:

[17:53:26.174] INFO: GET / 200 1ms | 127.0.0.1
[17:53:26.891] INFO: POST /api/users 201 45ms | 127.0.0.1
[17:53:27.003] INFO: GET /api/users/123 404 2ms | 192.168.1.10
[17:53:28.120] INFO: DELETE /api/users/456 500 312ms | 10.0.0.5

日志消息本身始终是紧凑的单行文本,格式为:

METHOD PATH STATUS TIMEms | IP

生产模式(JSON)

logger.prettyfalse(生产环境默认值)时,pino 输出结构化 JSON:

{"level":30,"time":1709712806174,"requestId":"req-1","msg":"GET / 200 1ms | 127.0.0.1"}
{"level":30,"time":1709712806891,"requestId":"req-2","msg":"POST /api/users 201 45ms | 127.0.0.1"}

每条日志是一行完整的 JSON 对象,便于 ELK、Loki 等日志收集系统解析。

注意requestId 字段由 pino mixin + AsyncLocalStorage 自动注入(见下文),无需在 access-log 中间件中手动传入。

requestId 自动注入

工作原理

VextJS 使用 AsyncLocalStorage(Node.js 内置的异步上下文传播机制)为每个请求维护独立的上下文。requestId 中间件在请求进入时生成唯一 ID 并存入 AsyncLocalStorage,后续所有日志输出都会自动携带该 ID。

请求进入

requestId 中间件:生成 req-N,存入 AsyncLocalStorage

... 其他中间件 ...

access-log 中间件:调用 logger.info("GET / 200 1ms | 127.0.0.1")

pino mixin:从 AsyncLocalStorage 读取 requestId,自动注入到日志对象

输出:{"level":30,"requestId":"req-1","msg":"GET / 200 1ms | 127.0.0.1"}

跨服务追踪

在微服务架构中,requestId 可以通过 HTTP 头传递,实现跨服务的请求链路追踪:

// 框架自动处理:
// 1. 如果请求头包含 X-Request-Id,使用该值
// 2. 否则生成新的唯一 ID
// 3. 响应头自动包含 X-Request-Id

中间件执行位置

Access Log 位于内置中间件链的第 6 位(response-wrapper 之后),确保:

  1. requestId 已在之前的中间件中生成(req.requestId 可用)
  2. 洋葱回程时 handler 已执行完毕(res.statusCode 已确定)
requestId → cors → body-parser → rateLimit → response-wrapper → 【access-log】
  → 插件全局中间件 → 路由级中间件 → validate → handler

配置项

通过 config.accessLog 进行配置:

// src/config/default.ts
export default {
  accessLog: {
    // 是否启用(默认 true)
    enabled: true,

    // 日志级别(默认 'info')
    // 设为 'debug' 可通过 logger.level 统一控制
    level: "info",

    // 跳过记录的路径列表(精确匹配)
    skipPaths: ["/health", "/ready", "/metrics"],

    // 慢请求阈值(毫秒,默认 3000)
    // 超过此阈值的请求自动提升为 warn 级别
    slowThreshold: 3000,

    // 是否记录响应体大小(默认 false)
    // 启用后在日志消息末尾追加 Content-Length
    logResponseSize: false,

    // 是否自动升级错误状态码的日志级别(默认 true)
    // 5xx → error, 4xx → warn
    autoLevelUpgrade: true,

    // skipPaths 匹配模式(默认 'exact')
    // 'exact': 精确匹配(如 '/health' 只匹配 '/health')
    // 'prefix': 前缀匹配(如 '/api/internal' 匹配 '/api/internal/xxx')
    skipMode: "exact",
  },
};

enabled

类型默认值说明
booleantrue是否启用 access-log 中间件

设为 false 时,中间件直接调用 next(),零开销跳过。

// src/config/development.ts — 开发环境关闭 access log 减少噪音
export default {
  accessLog: { enabled: false },
};

level

类型默认值可选值说明
string'info''info' | 'debug'日志输出级别

设为 'debug' 后,可以在生产环境通过 logger.level 统一控制是否输出访问日志。当 autoLevelUpgrade 启用时,此配置作为基础级别,4xx/5xx 请求会自动提升。

skipPaths

类型默认值说明
string[][]不记录访问日志的路径列表

内部使用 Set 实现 O(1) 查找(精确匹配模式)或前缀扫描(前缀匹配模式),不影响性能。

常见用途:排除健康检查、Kubernetes 探针、Prometheus metrics 等高频路径:

export default {
  accessLog: {
    skipPaths: ["/health", "/ready", "/metrics", "/favicon.ico"],
  },
};

skipMode

类型默认值可选值说明
string'exact''exact' | 'prefix'skipPaths 的匹配方式
  • 'exact':路径必须完全匹配(如 '/health' 只跳过 /health,不跳过 /health/detail
  • 'prefix':路径前缀匹配(如 '/api/internal' 会跳过 /api/internal/api/internal/status 等)
export default {
  accessLog: {
    skipPaths: ["/api/internal", "/_next"],
    skipMode: "prefix",
  },
};

slowThreshold

类型默认值说明
number3000慢请求阈值(毫秒)

响应时间超过此阈值的请求,日志级别自动提升为 warn,并在消息中追加 [SLOW] 标记:

[17:53:30.500] WARN: GET /api/reports 200 5231ms | 10.0.0.1 [SLOW]

logResponseSize

类型默认值说明
booleanfalse是否在日志中包含响应体大小

启用后,日志消息会在 IP 之后追加 Content-Length(如果响应头中存在):

[17:53:26.174] INFO: GET /api/users 200 3ms | 127.0.0.1 [1.2KB]

autoLevelUpgrade

类型默认值说明
booleantrue是否根据状态码自动提升日志级别

启用后的级别映射:

状态码范围日志级别说明
1xx / 2xx / 3xx配置的 level(默认 info正常请求
4xxwarn客户端错误
5xxerror服务端错误
[17:53:27.003] WARN: GET /api/users/999 404 2ms | 192.168.1.10
[17:53:28.120] ERROR: POST /api/payment 500 312ms | 10.0.0.5

这对生产环境的告警监控非常有用——可以在日志系统中针对 error 级别设置告警规则,自动捕获 5xx 错误。

性能优化

Access Log 中间件在内部做了多项性能优化:

  1. Set 预计算skipPaths 在初始化时转换为 Set,查找复杂度 O(1)
  2. 方法预绑定logger.info.bind(logger) 在初始化时绑定,避免每次请求的动态查找
  3. 快速跳过enabled: false 时立即 return next(),无任何额外开销
  4. 单行消息 — 使用字符串拼接而非结构化对象,避免 pino-pretty 将字段展开为多行

TypeScript 类型

interface VextAccessLogConfig {
  /** 是否启用 access-log(默认 true) */
  enabled?: boolean;

  /** 日志输出级别(默认 'info') */
  level?: "info" | "debug";

  /** 跳过记录的路径列表 */
  skipPaths?: string[];

  /** skipPaths 匹配模式(默认 'exact') */
  skipMode?: "exact" | "prefix";

  /** 慢请求阈值(毫秒,默认 3000) */
  slowThreshold?: number;

  /** 是否记录响应体大小(默认 false) */
  logResponseSize?: boolean;

  /** 是否根据状态码自动提升日志级别(默认 true) */
  autoLevelUpgrade?: boolean;
}

与日志存储的关系

Access Log 的输出走框架统一的 app.logger(基于 pino),因此所有在 日志文档 中描述的存储方案都适用:

  • pino-roll — 按大小/时间轮转到 logs/access.log
  • Filebeat → ELK — 采集 JSON 日志到 Elasticsearch
  • pino-elasticsearch — 直连 Elasticsearch
  • Docker → Loki — 容器日志驱动
  • stdout → Cloud — 云原生日志管道

如需将 access log 单独存储到独立文件,可使用 pino 多目标 transport 配合日志级别过滤。

下一步