日志 (Logger)
VextJS 基于 pino 提供高性能结构化日志,通过 app.logger 在框架的任意位置使用。内置 requestId 自动注入、child logger、pretty/JSON 双模式等企业级能力。
基本用法
app.logger 在路由、服务、插件和中间件中均可直接使用:
import { defineRoutes } from "vextjs";
export default defineRoutes((app) => {
app.get("/users", async (req, res) => {
app.logger.info("获取用户列表");
app.logger.debug({ page: 1, limit: 20 }, "查询参数");
const users = await app.services.user.findAll();
app.logger.info({ count: users.length }, "查询完成");
res.json(users);
});
});
日志级别
VextJS 支持 6 个日志级别,按严重程度从低到高排列:
配置日志级别
// src/config/default.ts
export default {
logger: {
level: "debug", // 开发环境输出所有级别
},
};
// src/config/production.ts
export default {
logger: {
level: "info", // 生产环境只输出 info 及以上
},
};
设置某个级别后,低于该级别的日志不会输出。例如 level: 'info' 时,debug() 调用会被静默忽略(零开销)。
生命周期日志分层
除了常规 logger.level 外,VextJS 还提供 logger.lifecycleLevel,专门控制框架自己的启动 / 加载 / 热重载系统日志:
export default {
logger: {
level: "info",
lifecycleLevel: "concise", // "concise" | "verbose"
},
};
concise(默认):仅输出初始化开始、聚合加载数量、ready、cold restart / hot reload 单行结果
verbose:额外输出逐插件 / 逐服务 / watcher 文件列表 / reload 分阶段耗时
也可以通过环境变量或 CLI 覆盖:
VEXT_LIFECYCLE_LEVEL=verbose vext start
VEXT_VERBOSE_LIFECYCLE=1 vext dev
结构化日志
pino 的核心理念是结构化日志——每条日志都是一个 JSON 对象,便于机器解析和查询。
调用签名
// 纯消息
app.logger.info("服务启动");
// 对象 + 消息(推荐)
app.logger.info({ port: 3000, adapter: "native" }, "服务启动");
// 对象(无消息)
app.logger.info({ event: "startup", port: 3000 });
推荐写法
始终使用 logger.info(object, message) 的形式——结构化字段便于日志系统索引和过滤,消息便于人类阅读。
JSON 输出格式
生产环境(NODE_ENV=production)下,日志输出为 JSON 格式:
{"level":30,"time":"2026-03-05T14:23:05.123Z","requestId":"abc-123","msg":"→ GET /api/users 200 45ms"}
{"level":30,"time":"2026-03-05T14:23:05.200Z","requestId":"abc-123","service":"UserService","msg":"查询完成","count":42}
Pretty 输出格式
开发环境(默认)下使用 pino-pretty,输出彩色格式化日志。默认启用单行模式(prettySingleLine: true),结构化字段以 JSON 形式内联附加在消息末尾:
[2026-03-05 14:23:05.123] INFO: 服务启动 {"port":3000,"adapter":"native"}
[2026-03-05 14:23:05.200] DEBUG: 查询参数 {"page":1,"limit":20}
[2026-03-05 14:23:05.300] INFO: Seed data loaded {"count":3,"service":"UserService"}
如果设置 prettySingleLine: false,则恢复 pino-pretty 的多行展开格式:
[2026-03-05 14:23:05.123] INFO 服务启动
port: 3000
adapter: "native"
[2026-03-05 14:23:05.200] DEBUG 查询参数
page: 1
limit: 20
注意:requestId 默认被 pino-pretty 的 ignore 列表排除(prettyIgnore 配置项),不会在 pretty 模式下输出。这使开发日志更紧凑。requestId 仍然存在于生产环境的 JSON 输出中。如需在 pretty 模式下显示 requestId,可通过 prettyIgnore 配置项移除它(见下方配置说明)。
配置 Pretty 模式
// src/config/default.ts
export default {
logger: {
level: "debug",
pretty: true, // 开发环境使用 pretty 格式(默认行为)
},
};
// src/config/production.ts
export default {
logger: {
level: "info",
pretty: false, // 生产环境使用 JSON 格式(默认行为)
},
};
pretty 默认值取决于 NODE_ENV:
NODE_ENV !== 'production' → pretty: true
NODE_ENV === 'production' → pretty: false
单行 vs 多行格式
通过 prettySingleLine 配置项可以控制 pino-pretty 在开发模式下的结构化字段展示方式。默认值为 true(单行模式)。
// src/config/default.ts — 默认行为(单行输出)
export default {
logger: {
pretty: true,
// prettySingleLine 默认值: true
// 输出: [14:23:05] INFO: Seed data loaded {"count":3,"service":"UserService"}
},
};
如果偏好多行展开格式(pino-pretty 原始行为),可以设置为 false:
// src/config/development.ts — 多行展开
export default {
logger: {
pretty: true,
prettySingleLine: false,
// 输出:
// [14:23:05] INFO Seed data loaded
// count: 3
// service: "UserService"
},
};
注意:prettySingleLine 仅影响 pretty 模式(开发环境)。生产环境的 JSON 输出格式不受影响。
自定义 Pretty 忽略字段
通过 prettyIgnore 配置项可以控制 pino-pretty 在开发模式下隐藏哪些结构化字段。默认值为 "pid,hostname,requestId",即隐藏进程 ID、主机名和请求 ID,避免开发日志中出现不必要的字段噪音。
// src/config/default.ts — 默认行为(requestId 被隐藏)
export default {
logger: {
pretty: true,
// prettyIgnore 默认值: "pid,hostname,requestId"
},
};
如果需要在 pretty 模式下显示 requestId(例如调试请求链路时),可以将其从 ignore 列表中移除:
// src/config/development.ts — 显示 requestId
export default {
logger: {
pretty: true,
prettyIgnore: "pid,hostname", // 不再忽略 requestId
},
};
也可以添加额外的忽略字段:
// 隐藏 requestId + 自定义字段
export default {
logger: {
prettyIgnore: "pid,hostname,requestId,traceId,spanId",
},
};
注意:prettyIgnore 仅影响 pretty 模式(开发环境)。生产环境的 JSON 输出始终包含所有字段(包括 requestId),确保日志收集系统能完整解析。
pino-pretty 支持 messageFormat 模板,可以将结构化字段内联到消息文本中,而非以 JSON 对象形式追加在末尾。这是一个进阶用法,适用于需要高度定制开发日志格式的场景。
何时使用 messageFormat
大多数场景下,默认的 prettySingleLine: true(JSON 内联)已经足够紧凑。messageFormat 适用于以下需求:
- 想让某些关键字段(如
requestId、service)直接出现在消息文本中
- 想完全控制日志行的可读格式
- 配合
prettyIgnore 隐藏已内联的字段,避免重复显示
:::
基本用法
messageFormat 使用 {fieldName} 占位符引用结构化字段:
// src/config/development.ts
import pino from "pino";
// 注意:messageFormat 需要直接配置 pino transport,
// 框架的 prettySingleLine / prettyIgnore 不影响 messageFormat 行为。
const logger = pino({
level: "debug",
transport: {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
ignore: "pid,hostname",
// 将 requestId 和 service 内联到消息中
messageFormat: "[{requestId}] [{service}] {msg}",
},
},
});
// 输出效果:
// [2026-03-06 14:23:05] INFO: [req-abc123] [UserService] 查询完成 {"count":42}
配合 prettyIgnore 避免重复
当字段已通过 messageFormat 内联到消息中时,可将其加入 prettyIgnore 避免在末尾重复显示:
// messageFormat 内联了 requestId 和 service → 将它们加入 ignore
export default {
logger: {
pretty: true,
prettyIgnore: "pid,hostname,requestId,service",
// 然后在 pino transport 层配置 messageFormat
// (需要自定义 pino 实例,框架层不直接暴露 messageFormat 配置)
},
};
条件格式
messageFormat 支持条件语法,当字段不存在时跳过对应部分:
// 仅在有 requestId 时显示前缀
messageFormat: "{requestId} {msg}";
// 有 requestId 时: "req-abc123 查询完成"
// 无 requestId 时: " 查询完成"(前面有空格)
注意:messageFormat 是 pino-pretty 的原生功能,框架层不直接提供配置项(因为大多数用户只需 prettySingleLine + prettyIgnore 即可满足需求)。如需使用 messageFormat,请参考 pino-pretty 文档 了解完整语法。
requestId 自动注入
这是 VextJS 日志系统最重要的特性之一。无需手动传入 requestId,所有日志自动携带当前请求的 requestId。
工作原理
请求进入 → requestId 中间件生成 ID → 写入 requestContext(AsyncLocalStorage)
↓
app.logger.info('xxx') ← pino mixin 钩子自动读取 requestId
↓
输出: {"requestId":"abc-123","msg":"xxx"}
pino 的 mixin 钩子在每条日志写入前调用,从 requestContext(基于 AsyncLocalStorage)中读取当前请求的 requestId 并附加到日志字段。这意味着:
- handler 中的日志:自动携带 requestId ✅
- service 中的日志:自动携带 requestId ✅
- 中间件中的日志:自动携带 requestId ✅
- 启动阶段的日志:无 requestId(非请求上下文)✅
// 不需要这样做 ❌
app.logger.info({ requestId: req.requestId }, "处理请求");
// 直接这样就行 ✅
app.logger.info("处理请求");
// 输出自动包含 requestId
性能优化
mixin 钩子在每条日志写入时都会调用。VextJS 做了两项优化:
- 预分配空对象常量:非请求上下文(如启动日志)不创建新对象,复用常量减少 GC
- ALS 禁用检测:当 AsyncLocalStorage 被禁用时,跳过
getStore() 调用
Child Logger
child() 方法创建子 logger,子 logger 继承父 logger 的所有配置(级别、格式、mixin),并额外携带指定的绑定字段:
// 创建带 service 字段的子 logger
const serviceLogger = app.logger.child({ service: "UserService" });
serviceLogger.info("初始化完成");
// 输出: {"service":"UserService","msg":"初始化完成"}
serviceLogger.info({ userId: "123" }, "查询用户");
// 输出: {"service":"UserService","userId":"123","msg":"查询用户"}
在 Service 中使用
推荐在 Service 构造函数中创建 child logger:
export class UserService {
private logger;
constructor(private app: any) {
// 创建带 service 标识的子 logger
this.logger = app.logger.child({ service: "UserService" });
}
async findById(userId: string) {
this.logger.debug({ userId }, "查询用户");
const user = await this.app.db.collection("users").findOne({ _id: userId });
if (!user) {
this.logger.warn({ userId }, "用户不存在");
this.app.throw(404, "用户不存在");
}
this.logger.info({ userId, event: "user.found" }, "用户查询成功");
return user;
}
}
输出示例:
{"level":20,"time":"...","requestId":"abc-123","service":"UserService","userId":"u-001","msg":"查询用户"}
{"level":30,"time":"...","requestId":"abc-123","service":"UserService","userId":"u-001","event":"user.found","msg":"用户查询成功"}
嵌套 Child Logger
child logger 可以嵌套创建,字段会累积:
const dbLogger = app.logger.child({ module: "database" });
const queryLogger = dbLogger.child({ collection: "users" });
queryLogger.debug("执行查询");
// 输出: {"module":"database","collection":"users","msg":"执行查询"}
错误日志
记录 Error 对象
pino 自动序列化 Error 对象(保留 message、stack、name):
try {
await someOperation();
} catch (err) {
app.logger.error({ err }, "操作失败");
// pino 会自动序列化 Error:
// {"err":{"type":"Error","message":"xxx","stack":"..."},"msg":"操作失败"}
}
:::warning 注意
将 Error 对象放在第一个参数的 err 字段中(pino 的约定),而不是直接传 Error:
// ✅ 正确
app.logger.error({ err: error }, "操作失败");
// ❌ 避免 — pino 无法正确序列化
app.logger.error(error, "操作失败");
记录错误上下文
async function processPayment(orderId: string, amount: number) {
try {
const result = await paymentGateway.charge(amount);
app.logger.info({ orderId, amount, chargeId: result.id }, "支付成功");
return result;
} catch (err) {
app.logger.error({ err, orderId, amount, gateway: "stripe" }, "支付失败");
throw err;
}
}
扩展 Logger:setLogger()
app.setLogger(wrapper) 是插件专用的 API,允许你在不替换原始 pino logger 的情况下,对所有日志方法进行包装——常见用途是将框架日志同时转发到外部系统(OTel Logs、Sentry、云日志平台等)。
函数签名
setLogger(wrapper: (original: VextLogger) => VextLogger): void;
wrapper 接收当前 logger(原始 pino 实现),返回新的 VextLogger 实现。可以在新实现中:
- 调用外部 SDK 上报日志
- 过滤或采样某些级别
- 注入全局字段
典型用法:桥接到 OpenTelemetry Logs
当你使用 vextjs-opentelemetry 插件时,它会在 setup() 中调用 app.setLogger() 自动将 app.logger 的所有调用转发到 OTel Logs SDK,无需额外配置:
// src/plugins/otel.ts
import { opentelemetryPlugin } from "vextjs-opentelemetry/vextjs";
export default opentelemetryPlugin({
endpoint: "grpc://collector:4317",
protocol: "grpc",
logs: {
bridgeAppLogger: true, // 默认 true(endpoint 有效时自动开启)
globalAttributes: {
"app.version": "1.2.0",
},
},
});
// → app.logger.info("xxx") 同时上报到 OTel Collector + 输出到 stdout
自定义 Logger 扩展示例
import { definePlugin } from "vextjs";
import type { VextLogger } from "vextjs";
export default definePlugin({
name: "sentry-logger",
setup(app) {
app.setLogger((original) => ({
...original,
error(...args: unknown[]) {
// 上报 error 级别日志到 Sentry
const msg = typeof args[0] === "string" ? args[0] : String(args[1] ?? "");
Sentry.captureMessage(msg, "error");
// 原始 pino 输出不变
(original.error as (...a: unknown[]) => void)(...args);
},
// child logger 保持原逻辑
child: (bindings) => original.child(bindings),
}));
},
});
与 setThrow 模式一致
setLogger 采用与 setThrow 完全相同的 wrapper 模式:接收原始实现,返回包装后的实现。这意味着:
- 可以多次调用(每次包裹上一次的结果)
- 原始 pino 功能(requestId 注入、pretty 格式、child logger)完整保留
- 包装函数中抛出的异常不会影响原始 logger
:::
:::warning child logger 不自动桥接
child() 方法继续返回原始 pino child logger,不会重复经过 wrapper 逻辑,避免同一条日志被多次转发到外部系统。如需子 logger 也桥接,请在 child logger 上另行包装。
日志存储与收集
生产环境中有多种方式将日志持久化存储和收集分析。下面从简到繁介绍各种方案。
方案概览
推荐日志目录结构
project/
├── logs/ # .gitignore 中排除
│ ├── app.log # 当前应用日志
│ ├── app.1.log # 轮转后的历史日志
│ ├── app.2.log
│ ├── error.log # 仅 error 及以上级别
│ └── access.log # 访问日志(可选)
├── src/
└── dist/
Warning
确保 .gitignore 中包含 logs/ 目录,不要将日志文件提交到版本库。
方案一:pino/file 简单文件输出
最基础的文件写入方案,适合小型项目或开发环境:
import pino from "pino";
const transport = pino.transport({
target: "pino/file",
options: {
destination: "./logs/app.log",
mkdir: true, // 自动创建 logs/ 目录
},
});
// 在 VextJS 中,可以通过插件在 logger 创建后替换
// 或在 src/index.ts 启动前配置
缺点:文件会无限增长,没有自动轮转。适合搭配外部轮转工具(如 logrotate)使用。
方案二:pino-roll 日志轮转(推荐单机方案)
pino-roll 是 pino 官方推荐的日志轮转 transport,支持按文件大小和时间间隔自动切割。
安装
按文件大小轮转
import pino from "pino";
const logger = pino(
{
level: "info",
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.transport({
target: "pino-roll",
options: {
file: "./logs/app.log", // 日志文件路径
size: "10m", // 单个文件最大 10MB
limit: {
count: 10, // 最多保留 10 个历史文件
},
mkdir: true, // 自动创建目录
},
}),
);
轮转后的文件命名:app.1.log、app.2.log、...
按时间间隔轮转
import pino from "pino";
const logger = pino(
{
level: "info",
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.transport({
target: "pino-roll",
options: {
file: "./logs/app.log",
frequency: "daily", // 每天轮转一次(也支持 'hourly')
dateFormat: "yyyy-MM-dd", // 历史文件名中的日期格式
limit: {
count: 30, // 保留最近 30 天
},
mkdir: true,
},
}),
);
轮转后的文件命名:app.2026-03-05.log、app.2026-03-04.log、...
同时按大小 + 时间轮转
import pino from "pino";
const logger = pino(
{
level: "info",
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.transport({
target: "pino-roll",
options: {
file: "./logs/app.log",
frequency: "daily",
size: "50m", // 单日内超过 50MB 也会切割
dateFormat: "yyyy-MM-dd",
limit: {
count: 30,
},
mkdir: true,
},
}),
);
多目标:控制台 + 文件轮转
import pino from "pino";
const logger = pino(
{
level: "info",
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.transport({
targets: [
// 控制台 pretty 输出(开发体验)
{
target: "pino-pretty",
options: { colorize: true, translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l" },
level: "debug",
},
// 文件轮转(所有级别)
{
target: "pino-roll",
options: {
file: "./logs/app.log",
size: "10m",
limit: { count: 10 },
mkdir: true,
},
level: "info",
},
// 错误日志单独文件
{
target: "pino-roll",
options: {
file: "./logs/error.log",
size: "10m",
limit: { count: 20 },
mkdir: true,
},
level: "error",
},
],
}),
);
方案三:系统级 logrotate(Linux)
如果使用 PM2 或 systemd 管理进程,可以用系统自带的 logrotate 管理日志轮转:
# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
copytruncate
}
方案四:Filebeat → Elasticsearch → Kibana (ELK)
完整的 ELK 日志分析管线。适合需要全文搜索、聚合分析、可视化面板的中大型项目。
架构
VextJS (JSON stdout)
→ PM2 (写入文件)
→ Filebeat (采集文件)
→ Elasticsearch (存储 + 索引)
→ Kibana (可视化 + 查询)
步骤 1:PM2 输出到文件
// ecosystem.config.cjs
module.exports = {
apps: [
{
name: "myapp",
script: "dist/index.js",
env: { NODE_ENV: "production" },
error_file: "/var/log/myapp/error.log",
out_file: "/var/log/myapp/app.log",
log_date_format: "YYYY-MM-DD HH:mm:ss.SSS",
merge_logs: true,
},
],
};
步骤 2:Filebeat 采集
# /etc/filebeat/filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/myapp/app.log
json.keys_under_root: true # JSON 字段提升到顶层
json.overwrite_keys: true # 覆盖同名字段
json.add_error_key: true # JSON 解析失败时添加 error 字段
fields:
app: myapp
env: production
fields_under_root: true
- type: log
enabled: true
paths:
- /var/log/myapp/error.log
json.keys_under_root: true
json.overwrite_keys: true
fields:
app: myapp
env: production
log_type: error
fields_under_root: true
output.elasticsearch:
hosts: ["http://elasticsearch:9200"]
index: "myapp-%{+yyyy.MM.dd}"
username: "${ELASTIC_USER}"
password: "${ELASTIC_PASSWORD}"
# 索引模板(可选,优化映射)
setup.template.name: "myapp"
setup.template.pattern: "myapp-*"
setup.template.settings:
index.number_of_shards: 1
index.number_of_replicas: 0
步骤 3:Kibana 索引模式
- 打开 Kibana → Stack Management → Index Patterns
- 创建索引模式:
myapp-*
- 时间字段选择
time(pino 的 ISO 时间戳)
- 在 Discover 中即可搜索日志
常用查询:
- 按 requestId 追踪:
requestId: "abc-123"
- 按错误级别过滤:
level: 50(pino level 50 = error)
- 按服务过滤:
service: "UserService"
方案五:pino-elasticsearch 直连
pino-elasticsearch 是 pino 官方的 Elasticsearch transport,无需 Filebeat 中间层,直接从 Node.js 进程写入 ES。
安装
npm install pino-elasticsearch
配置
import pino from "pino";
const logger = pino(
{
level: "info",
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.transport({
target: "pino-elasticsearch",
options: {
index: "myapp", // 索引名前缀(自动追加日期)
node: "http://localhost:9200",
esVersion: 8, // Elasticsearch 版本
flushBytes: 1000, // 缓冲区达到 1000 字节后批量写入
flushInterval: 5000, // 或每 5 秒写入一次
auth: {
username: process.env.ELASTIC_USER,
password: process.env.ELASTIC_PASSWORD,
},
// 可选:自定义索引名(按天分割)
"op.type": "create",
},
}),
);
多目标:控制台 + ES
import pino from "pino";
const logger = pino(
{
level: "info",
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.transport({
targets: [
{
target: "pino-pretty",
options: { colorize: true },
level: "debug",
},
{
target: "pino-elasticsearch",
options: {
index: "myapp",
node: process.env.ELASTICSEARCH_URL || "http://localhost:9200",
esVersion: 8,
flushBytes: 1000,
flushInterval: 5000,
},
level: "info",
},
],
}),
);
pino-elasticsearch vs Filebeat
- pino-elasticsearch:部署简单,无需额外进程;但如果 ES 不可用,日志会丢失
- Filebeat:先落盘再采集,ES 宕机时日志不丢失;需要额外部署 Filebeat
生产环境推荐 Filebeat 方案(更可靠),开发/测试环境可用 pino-elasticsearch(更简单)。
方案六:Docker → Loki
容器化部署时,使用 Docker logging driver 直接推送到 Grafana Loki:
# docker-compose.yml
services:
app:
build: .
logging:
driver: loki
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki-batch-size: "400"
loki-retries: "3"
loki-external-labels: "app=myapp,env=production"
在 Grafana 中添加 Loki 数据源即可查询日志:
- 按 requestId 查询:
{app="myapp"} |= "abc-123"
- 按 JSON 字段过滤:
{app="myapp"} | json | level >= 50
方案七:stdout → Cloud 原生
在 Kubernetes / AWS ECS / Google Cloud Run 等平台中,直接输出到 stdout,由平台自动采集:
# 不需要额外配置,JSON 日志直接输出到 stdout
NODE_ENV=production node dist/index.js
这是最简单也最推荐的云原生方案——不做任何日志配置,让平台处理一切。
日志与 OpenTelemetry
结合 OpenTelemetry,可以在日志中自动注入 trace_id 和 span_id,实现日志与链路追踪的关联:
// src/config/production.ts
import { trace } from "@opentelemetry/api";
export default {
logger: {
level: "info",
mixin() {
const span = trace.getActiveSpan();
if (!span?.isRecording()) return {};
const ctx = span.spanContext();
return {
trace_id: ctx.traceId, // 注入到每条日志的 trace_id 字段(OTEL 语义约定)
span_id: ctx.spanId, // 注入到每条日志的 span_id 字段
};
},
},
};
工作原理:
mixin() 在每条日志写入前被调用,返回值与内置的 requestId 字段合并注入
- 框架内置 mixin(注入
requestId)与用户 mixin 并存,互不覆盖(用户字段优先)
- 未配置
mixin 时,行为与之前完全一致(零 overhead)
- 框架不依赖
@opentelemetry/api,该包由用户在 tracing 初始化时引入
与 F-03(ALS 自动注入)的关系:如果你在 tracing 中间件中向 requestContext 写入了 traceId / spanId,框架内置 mixin 会自动将其注入日志——无需配置 mixin 选项。mixin 配置适用于需要直接从 OTEL Context API 实时读取当前活跃 Span 的场景。
详见 OpenTelemetry 接入示例 中的日志关联章节。
VextLogger 接口
interface VextLogger {
info(...args: unknown[]): void;
warn(...args: unknown[]): void;
error(...args: unknown[]): void;
debug(...args: unknown[]): void;
fatal(...args: unknown[]): void;
child(bindings: Record<string, unknown>): VextLogger;
}
VextLogger 是对 pino 的接口适配。你可以在类型声明中使用这个接口:
import type { VextLogger } from "vextjs";
class PaymentService {
private logger: VextLogger;
constructor(app: VextApp) {
this.logger = app.logger.child({ service: "PaymentService" });
}
}
配置参考
最佳实践
1. 使用结构化字段而非字符串拼接
// ✅ 结构化字段 — 可索引、可过滤
app.logger.info({ userId, action: "login", ip: req.ip }, "用户登录");
// ❌ 字符串拼接 — 难以解析和过滤
app.logger.info(`用户 ${userId} 从 ${req.ip} 登录`);
2. 为每个 Service 创建 Child Logger
// ✅ 推荐 — 日志自动携带 service 标识
this.logger = app.logger.child({ service: 'OrderService' });
// ❌ 避免 — 每条日志都要手动加 service
app.logger.info({ service: 'OrderService', ... }, 'xxx');
3. 不要在日志中输出敏感信息
// ✅ 安全
app.logger.info({ userId, action: "password_change" }, "密码已修改");
// ❌ 危险 — 密码泄漏到日志
app.logger.info({ userId, newPassword }, "密码已修改");
// ❌ 危险 — token 泄漏到日志
app.logger.debug({ token: req.headers.authorization }, "认证信息");
4. 合理使用日志级别
// debug — 详细调试信息(生产环境不输出)
app.logger.debug({ sql: query, params }, "执行数据库查询");
// info — 重要业务事件
app.logger.info({ orderId, total }, "订单创建成功");
// warn — 需要关注但不影响运行
app.logger.warn({ retryCount: 3, url }, "请求重试");
// error — 出错了
app.logger.error({ err, orderId }, "支付处理失败");
// fatal — 应用无法继续运行
app.logger.fatal({ err }, "数据库连接断开,无法恢复");
5. 在生产环境使用 JSON 格式
JSON 日志是日志收集系统(ELK、Loki、Datadog 等)的标准输入格式。确保生产环境 pretty: false(默认行为)。
下一步