日志
本指南介绍 AdonisJS 应用中的日志记录。你将学习如何:
- 在 HTTP 请求期间使用请求感知的 logger 编写日志
- 为开发环境配置美化打印的日志,为生产环境配置文件日志
- 为应用的不同部分定义多个 logger
- 使用依赖注入将 logger 注入到服务中
- 创建从父级继承上下文的子 logger
- 防止敏感数据出现在日志输出中
概述
AdonisJS 内置了一个 logger,用于将日志写入终端、文件和外部服务。底层上,该 logger 使用了 Pino ,这是 Node.js 生态中速度最快的日志库之一。日志以 NDJSON 格式 生成,便于使用标准工具进行解析和处理。
logger 与 AdonisJS 深度集成。在 HTTP 请求期间,每个请求都会自动获得自己的 logger 实例,该实例会在每条日志条目中包含请求 ID,从而可以轻松地将日志追溯到特定请求。
本指南侧重于 HTTP 请求期间的日志记录。对于 CLI 应用,请参阅 Ace ANSI logger 文档 ,它提供了为命令行工具设计的、对终端友好的彩色输出。
编写你的第一条日志
导入 logger 服务并调用任意日志方法来写入消息。在开发期间,日志会以美化格式显示在终端中,包含时间戳、颜色和可读的结构。
import router from '@adonisjs/core/services/router'
import logger from '@adonisjs/core/services/logger'
router.get('/', async () => {
logger.info('Processing home page request')
return { hello: 'world' }
})
当你访问该路由时,会在终端中看到类似以下的输出:
[10:24:36.842] INFO: Processing home page request
logger 为每个日志级别提供了对应的方法,从最详细到最简略排序如下:
import logger from '@adonisjs/core/services/logger'
export default class PostsController {
async store() {
logger.trace({ config }, 'Using config') // 最详细,用于追踪执行过程
logger.debug('User details: %o', { id: 1 }) // 调试信息
logger.info('Creating new post') // 一般信息
logger.warn('Rate limit approaching') // 警告状况
logger.error({ err }, 'Failed to save post') // 错误状况
logger.fatal({ err }, 'Database connection lost') // 严重故障
}
}
为日志添加上下文
将对象作为第一个参数传入,以在日志条目中包含额外数据。对象的属性会被合并到 JSON 输出中。
const user = { id: 1, email: 'virk@adonisjs.com' }
logger.info({ user }, 'User logged in')
记录错误时,请使用 err 键,以便 Pino 内置的序列化器能配合堆栈跟踪正确地格式化错误:
try {
await riskyOperation()
} catch (error) {
logger.error({ err: error }, 'Operation failed')
}
字符串插值
日志消息支持 printf 风格的插值,用于将值直接嵌入到消息字符串中:
logger.info('User %s logged in from %s', username, ipAddress)
logger.debug('Request body: %o', requestBody) // %o 用于对象
logger.info('Processing %d items', items.length) // %d 用于数字
请求感知的日志记录
在 HTTP 请求期间,请使用 ctx.logger,而不是直接导入 logger 服务。该上下文 logger 会自动在每条日志条目中包含请求 ID,从而便于将单个请求产生的所有日志关联起来。
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
export default class UsersController {
async show({ logger, params }: HttpContext) {
logger.info('Fetching user by id %s', params.id)
const user = await User.find(params.id)
if (!user) {
logger.warn('User not found')
return { error: 'Not found' }
}
logger.info('User retrieved successfully')
return user
}
}
输出中会包含请求 ID,使你能够针对特定请求筛选日志:
[10:24:36.842] INFO (request_id=cjkl3402k0001...): Fetching user by id 42
[10:24:36.901] INFO (request_id=cjkl3402k0001...): User retrieved successfully
配置 logger
logger 的配置位于 config/logger.ts。默认配置在开发环境使用美化打印输出,在生产环境使用结构化 JSON。
import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { defineConfig, syncDestination, targets } from '@adonisjs/core/logger'
const loggerConfig = defineConfig({
default: 'app',
loggers: {
app: {
enabled: true,
name: env.get('APP_NAME'),
level: env.get('LOG_LEVEL'),
destination: !app.inProduction ? await syncDestination() : undefined,
transport: {
targets: [targets.file({ destination: 1 })],
},
},
},
})
export default loggerConfig
declare module '@adonisjs/core/types' {
export interface LoggersList extends InferLoggers<typeof loggerConfig> {}
}
理解配置
syncDestination() 辅助函数配置了用于开发的、同步的、美化打印的输出。默认情况下,Pino 会异步写入日志以获得更好的性能,但这在调试时可能让你更难将日志与产生它们的代码关联起来。同步目标会在代码执行时以内联方式写入日志,并带有人类可读的格式。
在生产环境中,destination 保留为 undefined,这意味着日志会流经配置的 transport 目标。targets.file({ destination: 1 }) 目标会将 JSON 日志写入 stdout(文件描述符 1),这是容器化部署中的标准做法,由日志聚合器收集 stdout。
配置参考
| 属性 | 说明 |
|---|---|
default | 在未指定 logger 时调用 logger.info() 所使用的 logger 名称 |
enabled | 设为 false 可完全禁用该 logger |
name | 包含在每个日志条目中的名称,用于标识来源应用 |
level | 记录的最低级别。低于此级别的日志会被忽略 |
destination | 自定义的 destination 流。使用 syncDestination() 可获得同步美化输出 |
transport | 用于处理并路由日志的 Pino transports 配置 |
日志级别
logger 支持六个级别,从最详细到最简略排序。设置某个级别后,logger 会输出该级别及更高级别的日志。
| 级别 | 值 | 说明 |
|---|---|---|
trace | 10 | 极其详细的追踪信息 |
debug | 20 | 开发期间有用的调试信息 |
info | 30 | 一般运行信息 |
warn | 40 | 应当被审查的警告状况 |
error | 50 | 需要关注的错误状况 |
fatal | 60 | 需要立即处理的严重故障 |
在 .env 文件中设置级别:
LOG_LEVEL=debug
将日志写入文件
要将日志写入文件而非 stdout,请使用带文件路径的 targets.file() 辅助函数:
transport: {
targets: [
targets.file({ destination: '/var/log/apps/adonisjs.log' })
],
}
文件轮转
Pino 不包含内置的文件轮转功能。请使用 logrotate 之类的系统工具或 pino-roll 包。
npm i pino-roll
transport: {
targets: [
{
target: 'pino-roll',
level: 'info',
options: {
file: '/var/log/apps/adonisjs.log',
frequency: 'daily',
mkdir: true,
},
},
],
}
有条件地定义目标
使用 targets() 辅助函数配合条件逻辑来构建目标数组。这比使用三元运算符展开数组更清晰。
import app from '@adonisjs/core/services/app'
import { defineConfig, targets } from '@adonisjs/core/logger'
export default defineConfig({
default: 'app',
loggers: {
app: {
enabled: true,
name: env.get('APP_NAME'),
level: env.get('LOG_LEVEL'),
transport: {
targets: targets()
.pushIf(!app.inProduction, targets.pretty())
.pushIf(app.inProduction, targets.file({ destination: 1 }))
.toArray(),
},
},
},
})
pushIf 方法仅在条件为真时才添加该目标,从而保持配置的可读性。
使用多个 logger
当应用的不同部分需要独立的日志记录行为时,可以在配置中定义多个 logger。例如,你可能希望与支付相关的日志写入具有不同保留策略的单独文件。
export default defineConfig({
default: 'app',
loggers: {
app: {
enabled: true,
name: env.get('APP_NAME'),
level: env.get('LOG_LEVEL'),
destination: !app.inProduction ? await syncDestination() : undefined,
transport: {
targets: [targets.file({ destination: 1 })],
},
},
payments: {
enabled: true,
name: 'payments',
level: 'info',
transport: {
targets: [
targets.file({ destination: '/var/log/apps/payments.log' }),
],
},
},
},
})
使用 logger.use() 方法访问特定的 logger:
import logger from '@adonisjs/core/services/logger'
export default class PaymentService {
async processPayment(amount: number) {
const paymentLogger = logger.use('payments')
paymentLogger.info({ amount }, 'Processing payment')
// ... 支付逻辑
paymentLogger.info('Payment completed successfully')
}
}
不带参数调用 logger.use() 会返回默认 logger。
依赖注入
使用依赖注入时,对 Logger 类进行类型提示,IoC 容器会解析出默认 logger 的一个实例。如果该类是在 HTTP 请求期间构造的,容器会自动注入带有请求 ID 的请求感知 logger。
import { inject } from '@adonisjs/core'
import { Logger } from '@adonisjs/core/logger'
import User from '#models/user'
@inject()
export default class UserService {
constructor(protected logger: Logger) {}
async find(userId: string | number) {
this.logger.info('Fetching user by id %s', userId)
return User.find(userId)
}
}
子 logger
子 logger 会从父级继承配置与绑定,同时允许你添加额外的上下文。当你希望某个特定操作产生的所有日志都包含共享元数据时,这会很有用。
import logger from '@adonisjs/core/services/logger'
const orderLogger = logger.child({ orderId: 'order_123' })
orderLogger.info('Processing order') // 包含 orderId
orderLogger.info('Validating items') // 包含 orderId
orderLogger.info('Order complete') // 包含 orderId
来自 orderLogger 的每条日志条目都会自动包含 orderId 字段。你也可以为子 logger 覆盖日志级别:
const verboseLogger = logger.child({}, { level: 'trace' })
有条件地记录日志
如果为日志消息计算数据的代价很高,请先检查该级别是否已启用,再执行相关工作:
import logger from '@adonisjs/core/services/logger'
if (logger.isLevelEnabled('debug')) {
const data = await computeExpensiveDebugData()
logger.debug(data, 'Debug information')
}
ifLevelEnabled 方法提供了一种基于回调的替代写法:
logger.ifLevelEnabled('debug', async () => {
const data = await computeExpensiveDebugData()
logger.debug(data, 'Debug information')
})
隐藏敏感值
日志可能会在不经意间暴露敏感数据。请使用 redact 选项自动隐藏特定键的值。底层上它使用了
fast-redact
包。
loggers: {
app: {
enabled: true,
name: env.get('APP_NAME'),
level: env.get('LOG_LEVEL'),
redact: {
paths: ['password', '*.password', 'creditCard'],
},
},
}
logger.info({ username: 'virk', password: 'secret123' }, 'User signup')
// 输出:{"username":"virk","password":"[Redacted]","msg":"User signup"}
自定义占位符或完全移除这些键:
redact: {
paths: ['password', '*.password'],
censor: '[PRIVATE]',
}
// 或完全移除该属性
redact: {
paths: ['password'],
remove: true,
}
使用 Secret 类
redact 的一种替代方案是将敏感值包裹在 Secret 类中。logger 会自动 redact 任何 Secret 实例。
另见: Secret 类文档
import { Secret } from '@adonisjs/core/helpers'
const password = new Secret(request.input('password'))
logger.info({ username, password }, 'User signup')
// 输出:{"username":"virk","password":"[redacted]","msg":"User signup"}
Pino 静态成员
@adonisjs/core/logger 模块重新导出了 Pino 的静态方法与属性,用于进阶用例。
有关这些导出的详细信息,请参阅 Pino 文档 。
import {
multistream,
destination,
transport,
stdSerializers,
stdTimeFunctions,
symbols,
pinoVersion,
} from '@adonisjs/core/logger'