日志
简介
Midway 为不同场景提供了一套统一的日志接入方式。通过 @midwayjs/logger
包导出的方法,可以方便的接入不同场景的日志系统。
Midway 的日志系统基于社区的 winston,是现在社区非常受欢迎的日志库。
普通使用
首先我们学会 Midway 的日常日志使用方法。
import { Get } from '@midwayjs/decorator';
import { Inject, Controller, Provide } from '@midwayjs/decorator';
@Provide()
@Controller()
export class HelloController {
@Inject()
logger; // 获取上下文日志,等价于 ctx.logger
@Logger('logger')
appLogger; // 获取 appLogger,非请求链路下,比如 configuration 或者单例中
@Inject()
ctx;
@Get('/')
async ctx() {
// 请求链路的日志,包含请求的 url,请求协议等信息
this.logger.info('hello world');
// 普通日志
this.appLogger.info('hello world');
}
}
访问后,我们能在两个地方看到日志输出:
- console 栏看到输出。
- 日志目录的 midway-app.log 文件中。
输出结果:
2021-07-22 14:50:59,388 INFO 7739 [-/::ffff:127.0.0.1/-/0ms GET /api/get_user] hello world
以上是用户在项目开发的基本使用。如果有更多高阶用法,请继续阅读接下来的章节。
默认日志对象
默认情况下,Midway 已经将日志库埋入到整个框架中,在框架启动时已经能够自动输出信息到控制台,以及输出到文件。
Midway 日志的默认逻辑为:
- 将日志输出到控制台和写入文件
- 按日期每天切割,以及按大小切割
- 将错误(
.error()
输出的内容)统一输出到一个固定的错误文件
Midway 默认在框架提供了三种不同的日志,对应三种不同的行为。
日志 | 释义 | 描述 | 常见使用 |
---|---|---|---|
coreLogger | 框架,组件层面的日志 | 默认会输出控制台日志和文本日志 midway-core.log ,并且默认会将错误日志发送到 common-error.log 。 | 框架和组件的错误,一般会打印到其中。 |
appLogger | 业务层面的日志 | 默认会输出控制台 日志和文本日志 midway-app.log ,并且默认会将错误日志发送到 common-error.log 。 | 业务使用的日志,一般业务日志会打印到其中。 |
上下文日志(复用 appLogger 的配置) | 请求链路的日志 | 默认使用 appLogger 进行输出,除了会将错误日志发送到 common-error.log 之外,还增加了上下文信息。 | 修改日志输出的标记(Label),不同的框架有不同的请求标记,比如 HTTP 下就会输出路由信息。 |
日志路径和文件
默认情况下,Midway 会在本地开发和服务器部署时输出日志到日志根目录。
- 本地的日志根目录为
${app.appDir}/logs/项目名
目录下 - 服务器的日志根目录为用户目录
${process.env.HOME}/logs/项目名
(Linux/Mac)以及${process.env.USERPROFILE}/logs/项目名
(Windows)下,例如/home/admin/logs/example-app
。
Midway 会在日志根目录创建一些默认的文件。
midway-core.log
框架、组件打印信息的日志,对应coreLogger
。midway-app.log
应用打印信息的日志,对应appLogger
common-error.log
所有错误的日志(所有 Midway 创建出来的日志,都会将错误重复打印一份到该文件中)
在 EggJS 下,为了兼容以前的日志,依旧处理将日志打印在 midway-web.log
下。
使用日志对象
一般来说,框架开发者需要获取到 coreLogger
,记录框架,组件层面的日志。而业务开发者,需要获取到 appLogger
来记录业务日志。在业务和请求相关的流程中,需要拿到上下文日志对象,方便追踪请求。
使用装饰器获取日志对象
在任意类中,我们可以通过装饰器来获取日志对象。下面是一个通过装饰器获取各种默认日志对象的方式。
1、获取 coreLogger
import { Provide, Logger } from '@midwayjs/decorator';
import { ILogger } from '@midwayjs/logger';
@Provide()
export class UserService {
@Logger()
coreLogger: ILogger; // 获取 coreLogger
@Logger('coreLogger')
anotherLogger: ILogger; // 这里和依赖注入的规则相同,依旧获取的是 coreLogger
async getUser() {
// this.coreLogger === this.anotherLogger
this.coreLogger.warn('warn message');
}
}
2、获取 appLogger
为了使用更简单,我们将 appLogger
的 key 变为了最为普通的 logger
。
import { Provide, Logger } from '@midwayjs/decorator';
import { ILogger } from '@midwayjs/logger';
@Provide()
export class UserService {
@Logger()
logger: ILogger; // 获取 appLogger
async getUser() {
this.logger.info('hello user');
this.coreLogger.warn('warn message');
}
}
3、获取上下文日志对象(Context Logger)
上下文日志是在每个请求实例中动态创建的日志对象,因此它和请求作用域绑定,即和请求实例绑定。
Midway 默认会将上下文日志对象挂载到上下文(ctx)上,即 ctx.logger
。
import { Provide, Logger } from '@midwayjs/decorator';
import { ILogger } from '@midwayjs/logger';
@Provide()
export class UserService {
@Inject()
logger: ILogger; // 获取上下文日志
async getUser() {
this.logger.info('hello user');
}
}
和全局的日志不同,上下文日志对象,默认会放在请求作用域的依赖注入容器中,它的 key 为 logger,所以可以使用 Inject
装饰器注入它。
使用 API 接口获取日志对象
有时候,我们不在 Class 的场景下,我们可以从 app
上的方法来获取这些默认的日志对象。
import { Provide, Logger } from '@midwayjs/decorator';
import { ILogger } from '@midwayjs/logger';
@Provide()
export class UserService {
@App()
app: IMidwayApplication;
@Inject()
ctx;
async getUser() {
this.app.getLogger('logger').info('hello user'); // 获取 appLogger
this.app.getLogger('coreLogger').warn('warn message'); // 获取 coreLogger
// this.ctx.logger 获取请求上下文日志
}
}
输出方法和格式
Midway 的日志对象继承与 winston 的日志对象,一般情况下,只提供 error()
, warn()
, info()
, debug
四种方法。
示例如下。
logger.debug('debug info');
logger.info('启动耗时 %d ms', Date.now() - start);
logger.warn('warning!');
logger.error(new Error('my error'));
默认的输出行为
在大部分的普通类型下,日志库都能工作的很好。
比如:
logger.info('hello world'); // 输出字符串
logger.info(123); // 输出数字
logger.info(['b', 'c']); // 输出数组
logger.info(new Set([2, 3, 4])); // 输出 Set
logger.info(
new Map([
['key1', 'value1'],
['key2', 'value2'],
])
); // 输出 Map
Midway 针对 winston 无法输出的 Array
, Set
, Map
类型,做了特殊定制,使其也能够正常的输出。
不过需要注意的是,日志对象在一般情况下,只能传入一个参数,它的第二个参数有其他作用。
logger.info('plain error message', 321); // 会忽略 321
错误输出
针对错误对象,Midway 也对 winston 做了定制,使其能够方便的和普通文本结合到一起输出。
// 输出错误对象
logger.error(new Error('error instance'));
// 输出自定义的错误对象
const error = new Error('named error instance');
error.name = 'NamedError';
logger.error(error);
// 文本在前,加上 error 实例
logger.info('text before error', new Error('error instance after text'));
注意,错误对象只能放在最后,且有且只有一个,其后面的所有参数都会被忽略。
格式化内容
基于 util.format
的格式化方式。
logger.info('%s %d', 'aaa', 222);
常用的有
%s
字符串占位%d
数字占位%j
json 占位
更多的占位和详细信息,请参考 node.js 的 util.format 方法。
输出自定义对象或者复杂类型
基于性能考虑,Midway(winston)大部分时间只会输出基本类型,所以当输出的参数为高级对象时,需要用户手动转换为需要打印的字符串。
如下示例,将不会得到希望的结果。
const obj = { a: 1 };
logger.info(obj); // 默认情况下,输出 [object Object]
需要手动输出希望打印的内容。
const obj = {a: 1};
logger.info(JSON.stringify(obj)); // 可以输出格式化文本
logger.info(a.1); // 直接输出属性值
logger.info('%j', a); // 直接占位符输出整个 json
纯输出内容
特殊场景下,我们需要单纯的输出内容,不希望输出时间戳,label 等和格式相关的信息。这种需求我们可以使用 write
方法。
write
方法是个非常底层的方法,并且不管什么级别的日志,它都会写入到文件中。
虽然 write
方法在每个 logger 上都有,但是我们只在 IMidwayLogger
定义中提供它,我们希望你能明确的知道自己希望调用它。
(logger as IMidwayLogger).write('hello world'); // 文件中只会有 hello world
日志定义
默认的情况,用户应该使用最简单的 ILogger
定义。
import { Provide, Logger } from '@midwayjs/decorator';
import { ILogger } from '@midwayjs/logger';
@Provide()
export class UserService {
@Inject()
logger: ILogger; // 获取上下文日志
async getUser() {
this.logger.info('hello user');
}
}
ILogger
定义只提供最简单的 debug
, info
, warn
以及 error
方法。
在某些场景下,我们需要更为复杂的定义,比如修改日志属性或者动态调节,这个时候需要使用更为复杂的 IMidwayLogger
定义。
import { Provide, Logger } from '@midwayjs/decorator';
import { IMidwayLogger } from '@midwayjs/logger';
@Provide()
export class UserService {
@Inject()
logger: IMidwayLogger; // 获取上下文日志
async getUser() {
this.logger.disableConsole(); // 禁止控制台输出
this.logger.info('hello user'); // 这句话在控制台看不到
this.logger.enableConsole(); // 开启控制台输出
this.logger.info('hello user'); // 这句话在控制台可以看到
}
}
IMidwayLogger
的定义可以参考 interface 中的描述,或者查看 代码。
日志等级
winston 的日志等级分为下面几类,日志等级依次降低(数字越大,等级越低):
const levels = {
none: 0,
error: 1,
trace: 2,
warn: 3,
info: 4,
verbose: 5,
debug: 6,
silly: 7,
all: 8,
}
在 Midway 中,为了简化,一般情况下,我们只会使用 error
, warn
, info
, debug
这四种等级。
日志等级表示当前可输出日志的最低等级。比如当你的日志 level 设置为 warn
时,仅 warn
以及更高的 error
等级的日志能被输出。
框架的默认等级
在 Midway 中,有着自己的默认日志等级。
- 在开发环境下(local,test,unittest),日志等级统一为
info
。 - 在服务器环境(除开发环境外),为减少日志数量,日志等级统一为
warn
。
动态调整等级
在开发调试时,我们往往有动态调整等级的诉求。在 Midway 的日志下,我们可以使用方法动态的调整日志等级。
logger.updateLevel('debug'); // 动态调整等级为 debug
也可以单独调整文本和控制台输出的等级。
// 动态调整文件的日志等级
logger.updateFileLevel('warn');
// 动态调整控制台输出日志等级
logger.updateConsoleLevel('error');
日志输出管道(Transport)
Midway 的日志对象基于 Winston 日志,默认包含三个日志管道。
ConsoleTransport
用于向控制台输出日志FileTransport
用于向文件写入日志ErrorTransport
用于将 Error 级别输出到特定的错误日志
我们可以通过方法动态更新这三个管道,控制输出。
logger.enableFile();
logger.disableFile();
logger.enableConsole();
logger.disableConsole();
logger.enableError();
logger.disableError();
同时,还提供了判断的 API。
logger.isEnableConsole();
logger.isEnableFile();
logger.isEnableError();
日志切割(轮转)
默认行为下,同一个日志对象会生成两个文件。
以 midway-core.log
为例,应用启动时会生成一个带当日时间戳 midway-core.YYYY-MM-DD
格式的文件,以及一个不带时间戳的 midway-core.log
的软链文件。
为方便配置日志采集和查看,该软链文件永远指向最新的日志文件。
当凌晨 00:00
时,会生成一个以当天日志结尾 midway-core.log.YYYY-MM-DD
的形式的新文件。
同时,当单个日志文件超过 200M 时,也会自动切割,产生新的日志文件。
日志标签(label)
日志标签(label)指的是日志输出时,带有 [xx]
的部分。