Skip to content

日志记录器

AdonisJS 有一个内置的日志记录器,支持将日志写入文件标准输出外部日志服务。在底层,我们使用 pino。Pino 是 Node.js 生态系统中最快的日志库之一,以 NDJSON 格式生成日志。

使用

首先,您可以导入 Logger 服务从应用程序的任何位置写入日志。日志写入 stdout,将显示在终端上。

ts
import logger from '@adonisjs/core/services/logger'

logger.info('this is an info message')
logger.error({ err: error }, 'Something went wrong')

建议在 HTTP 请求期间使用 ctx.logger 属性。HTTP 上下文持有请求感知日志记录器的实例,该实例会在每个日志语句中添加当前请求 ID。

ts
import router from '@adonisjs/core/services/router'
import User from '#models/user'

router.get('/users/:id', async ({ logger, params }) => {
  logger.info('Fetching user by id %s', params.id)
  const user = await User.find(params.id)
})

配置

日志记录器的配置存储在 config/logger.ts 文件中。默认情况下,只配置了一个日志记录器。但是,如果您想在应用程序中使用多个日志记录器,可以定义多个日志记录器的配置。

ts
// title: config/logger.ts
import env from '#start/env'
import { defineConfig } from '@adonisjs/core/logger'

export default defineConfig({
  default: 'app',
  
  loggers: {
    app: {
      enabled: true,
      name: env.get('APP_NAME'),
      level: env.get('LOG_LEVEL', 'info')
    },
  }
})

default

default 属性是对同一文件中 loggers 对象下配置的日志记录器之一的引用。

除非您在使用日志记录器 API 时选择特定的日志记录器,否则将使用默认日志记录器写入日志。

loggers

loggers 对象是一个键值对,用于配置多个日志记录器。键是日志记录器的名称,值是 pino 接受的配置对象

传输目标

Pino 中的传输在将日志写入目标方面起着重要作用。您可以在配置文件中配置多个目标,pino 将向所有目标传递日志。每个目标还可以指定它想要接收日志的级别。

TIP

如果您没有在目标配置中定义 level,配置的目标将从父日志记录器继承它。

此行为与 pino 不同。在 Pino 中,目标不会从父日志记录器继承级别。

ts
{
  loggers: {
    app: {
      enabled: true,
      name: env.get('APP_NAME'),
      level: env.get('LOG_LEVEL', 'info'),
      
      // highlight-start
      transport: {
        targets: [
          {
            target: 'pino/file',
            level: 'info',
            options: {
              destination: 1
            }
          },
          {
            target: 'pino-pretty',
            level: 'info',
            options: {}
          },
        ]
      }
      // highlight-end
    }
  }
}

File target

pino/file 目标将日志写入文件描述符。destination = 1 意味着写入日志到 stdout(这是标准的 unix 文件描述符约定)。

Pretty target

pino-pretty 目标使用 pino-pretty npm 模块将日志美化打印到文件描述符。

条件定义目标

根据代码运行的环境注册目标是很常见的。例如,在开发中使用 pino-pretty 目标,在生产中使用 pino/file 目标。

如下所示,使用条件构造 targets 数组会使配置文件看起来不整洁。

ts
import app from '@adonisjs/core/services/app'

loggers: {
  app: {
    transport: {
      targets: [
        ...(!app.inProduction
          ? [{ target: 'pino-pretty', level: 'info' }]
          : []
        ),
        ...(app.inProduction
          ? [{ target: 'pino/file', level: 'info' }]
          : []
        ),
      ]
    }
  } 
}

因此,您可以使用 targets 助手通过流畅的 API 定义条件数组项。在以下示例中,我们使用 targets.pushIf 方法表达相同的条件。

ts
import { targets, defineConfig } from '@adonisjs/core/logger'

loggers: {
  app: {
    transport: {
      targets: targets()
       .pushIf(
         !app.inProduction,
         { target: 'pino-pretty', level: 'info' }
       )
       .pushIf(
         app.inProduction,
         { target: 'pino/file', level: 'info' }
       )
       .toArray()
    }
  } 
}

为了进一步简化代码,您可以使用 targets.prettytargets.file 方法为 pino/filepino-pretty 目标定义配置对象。

ts
import { targets, defineConfig } from '@adonisjs/core/logger'

loggers: {
  app: {
    transport: {
      targets: targets()
       .pushIf(app.inDev, targets.pretty())
       .pushIf(app.inProduction, targets.file())
       .toArray()
    }
  }
}

使用多个日志记录器

AdonisJS 为配置多个日志记录器提供了一流的支持。日志记录器的唯一名称和配置在 config/logger.ts 文件中定义。

ts
export default defineConfig({
  default: 'app',
  
  loggers: {
    // highlight-start
    app: {
      enabled: true,
      name: env.get('APP_NAME'),
      level: env.get('LOG_LEVEL', 'info')
    },
    payments: {
      enabled: true,
      name: 'payments',
      level: env.get('LOG_LEVEL', 'info')
    },
    // highlight-start
  }
})

配置后,您可以使用 logger.use 方法访问命名的日志记录器。

ts
import logger from '@adonisjs/core/services/logger'

logger.use('payments')
logger.use('app')

// 获取默认日志记录器的实例
logger.use()

依赖注入

使用依赖注入时,您可以将 Logger 类类型提示为依赖项,IoC 容器将解析配置文件中定义的默认日志记录器的实例。

如果类是在 HTTP 请求期间构造的,则容器将注入请求感知的 Logger 实例。

ts
import { inject } from '@adonisjs/core'
import { Logger } from '@adonisjs/core/logger'

// highlight-start
@inject()
// highlight-end
class UserService {
  // highlight-start
  constructor(protected logger: Logger) {}
  // highlight-end

  async find(userId: string | number) {
    this.logger.info('Fetching user by id %s', userId)
    const user = await User.find(userId)
  }
}

日志方法

Logger API 几乎与 Pino 相同,除了 AdonisJS 日志记录器不是 Event emitter 的实例(而 Pino 是)。除此之外,日志方法与 pino 具有相同的 API。

ts
import logger from '@adonisjs/core/services/logger'

logger.trace(config, 'using config')
logger.debug('user details: %o', { username: 'virk' })
logger.info('hello %s', 'world')
logger.warn('Unable to connect to database')
logger.error({ err: Error }, 'Something went wrong')
logger.fatal({ err: Error }, 'Something went wrong')

可以将附加的合并对象作为第一个参数传递。然后对象属性将添加到输出 JSON 中。

ts
logger.info({ user: user }, 'Fetched user by id %s', user.id)

要显示错误,您可以使用 err来指定错误值。

ts
logger.error({ err: error }, 'Unable to lookup user')

条件日志

日志记录器为配置文件中配置的级别及以上级别生成日志。例如,如果级别设置为 warn,则 infodebugtrace 级别的日志将被忽略。

如果为日志消息计算数据的开销很大,您应该在计算数据之前检查给定的日志级别是否已启用。

ts
import logger from '@adonisjs/core/services/logger'

if (logger.isLevelEnabled('debug')) {
  const data = await getLogData()
  logger.debug(data, 'Debug message')
}

您可以使用 ifLevelEnabled 方法表达相同的条件。该方法接受回调作为第二个参数,该回调在指定的日志级别启用时执行。

ts
logger.ifLevelEnabled('debug', async () => {
  const data = await getLogData()
  logger.debug(data, 'Debug message')
})

子日志记录器

子日志记录器是一个独立的实例,继承父日志记录器的配置和绑定。

可以使用 logger.child 方法创建子日志记录器的实例。该方法接受绑定作为第一个参数,可选的配置对象作为第二个参数。

ts
import logger from '@adonisjs/core/services/logger'

const requestLogger = logger.child({ requestId: ctx.request.id() })

子日志记录器也可以在不同的日志级别下记录。

ts
logger.child({}, { level: 'warn' })

Pino 静态方法

Pino 静态方法和属性由 @adonisjs/core/logger 模块导出。

ts
import { 
  multistream,
  destination,
  transport,
  stdSerializers,
  stdTimeFunctions,
  symbols,
  pinoVersion
} from '@adonisjs/core/logger'

写入日志到文件

Pino 附带一个 pino/file 目标,您可以使用它将日志写入文件。在目标选项中,您可以指定日志文件的目标路径。

ts
app: {
  enabled: true,
  name: env.get('APP_NAME'),
  level: env.get('LOG_LEVEL', 'info')

  transport: {
    targets: targets()
      .push({
         transport: 'pino/file',
         level: 'info',
         options: {
           destination: '/var/log/apps/adonisjs.log'
         }
      })
      .toArray()
  }
}

文件轮换

Pino 没有内置的文件轮换支持,因此您必须使用系统级工具如 logrotate 或使用第三方包如 pino-roll

sh
npm i pino-roll
ts
app: {
  enabled: true,
  name: env.get('APP_NAME'),
  level: env.get('LOG_LEVEL', 'info')

  transport: {
    targets: targets()
      // highlight-start
      .push({
        target: 'pino-roll',
        level: 'info',
        options: {
          file: '/var/log/apps/adonisjs.log',
          frequency: 'daily',
          mkdir: true
        }
      })
      // highlight-end
     .toArray()
  }
}

隐藏敏感值

日志可能成为泄露敏感数据的来源。因此,建议观察您的日志并从输出中删除/隐藏敏感值。

在 Pino 中,您可以使用 redact 选项从日志中隐藏/删除敏感的键值对。在底层,使用 fast-redact 包,您可以查阅其文档以查看可用的表达式。

ts
// title: config/logger.ts
app: {
  enabled: true,
  name: env.get('APP_NAME'),
  level: env.get('LOG_LEVEL', 'info')

  // highlight-start
  redact: {
    paths: ['password', '*.password']
  }
  // highlight-end
}
ts
import logger from '@adonisjs/core/services/logger'

const username = request.input('username')
const password = request.input('password')

logger.info({ username, password }, 'user signup')
// 输出: {"username":"virk","password":"[Redacted]","msg":"user signup"}

默认情况下,该值被替换为 [Redacted] 占位符。您可以定义自定义占位符或删除键值对。

ts
redact: {
  paths: ['password', '*.password'],
  censor: '[PRIVATE]'
}

// 删除属性
redact: {
  paths: ['password', '*.password'],
  remove: true
}

使用 Secret 数据类型

脱敏的另一种方法是将敏感值包装在 Secret 类中。例如:

另请参阅:Secret 类使用文档

ts
import { Secret } from '@adonisjs/core/helpers'

const username = request.input('username')
// delete-start
const password = request.input('password')
// delete-end
// insert-start
const password = new Secret(request.input('password'))
// insert-end

logger.info({ username, password }, 'user signup')
// 输出: {"username":"virk","password":"[redacted]","msg":"user signup"}