日志

本指南介绍 AdonisJS 应用中的日志记录。你将学习如何:

  • 在 HTTP 请求期间使用请求感知的 logger 编写日志
  • 为开发环境配置美化打印的日志,为生产环境配置文件日志
  • 为应用的不同部分定义多个 logger
  • 使用依赖注入将 logger 注入到服务中
  • 创建从父级继承上下文的子 logger
  • 防止敏感数据出现在日志输出中

概述

AdonisJS 内置了一个 logger,用于将日志写入终端、文件和外部服务。底层上,该 logger 使用了 Pino ,这是 Node.js 生态中速度最快的日志库之一。日志以 NDJSON 格式 生成,便于使用标准工具进行解析和处理。

logger 与 AdonisJS 深度集成。在 HTTP 请求期间,每个请求都会自动获得自己的 logger 实例,该实例会在每条日志条目中包含请求 ID,从而可以轻松地将日志追溯到特定请求。

Note

本指南侧重于 HTTP 请求期间的日志记录。对于 CLI 应用,请参阅 Ace ANSI logger 文档 ,它提供了为命令行工具设计的、对终端友好的彩色输出。

编写你的第一条日志

导入 logger 服务并调用任意日志方法来写入消息。在开发期间,日志会以美化格式显示在终端中,包含时间戳、颜色和可读的结构。

start/routes.ts
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 为每个日志级别提供了对应的方法,从最详细到最简略排序如下:

app/controllers/posts_controller.ts
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,从而便于将单个请求产生的所有日志关联起来。

app/controllers/users_controller.ts
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。

config/logger.ts
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 会输出该级别及更高级别的日志。

级别说明
trace10极其详细的追踪信息
debug20开发期间有用的调试信息
info30一般运行信息
warn40应当被审查的警告状况
error50需要关注的错误状况
fatal60需要立即处理的严重故障

.env 文件中设置级别:

.env
LOG_LEVEL=debug

将日志写入文件

要将日志写入文件而非 stdout,请使用带文件路径的 targets.file() 辅助函数:

config/logger.ts
transport: {
  targets: [
    targets.file({ destination: '/var/log/apps/adonisjs.log' })
  ],
}

文件轮转

Pino 不包含内置的文件轮转功能。请使用 logrotate 之类的系统工具或 pino-roll 包。

npm i pino-roll
config/logger.ts
transport: {
  targets: [
    {
      target: 'pino-roll',
      level: 'info',
      options: {
        file: '/var/log/apps/adonisjs.log',
        frequency: 'daily',
        mkdir: true,
      },
    },
  ],
}

有条件地定义目标

使用 targets() 辅助函数配合条件逻辑来构建目标数组。这比使用三元运算符展开数组更清晰。

config/logger.ts
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。例如,你可能希望与支付相关的日志写入具有不同保留策略的单独文件。

config/logger.ts
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:

app/services/payment_service.ts
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。

app/services/user_service.ts
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 包。

config/logger.ts
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"}

自定义占位符或完全移除这些键:

config/logger.ts
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'