nest学习笔记(7) :日志模块

  • 官方文档

  • Nest附带一个默认的内部日志记录器实现,它在实例化过程中以及在一些不同的情况下使用,比如发生异常等等(例如系统记录)。这由 @nestjs/common 包中的 Logger 类实现。你可以全面控制如下的日志系统的行为:

  1. 完全禁用日志
  2. 指定日志系统详细水平(例如,展示错误,警告,调试信息等)
  3. 覆盖默认日志记录器的时间戳(例如使用 ISO8601 标准作为日期格式)
  4. 完全覆盖默认日志记录器
  5. 通过扩展自定义默认日志记录器
  6. 使用依赖注入来简化编写和测试你的应用
  7. 你也可以使用内置日志记录器,或者创建你自己的应用来记录你自己应用水平的事件和消息。

更多高级的日志功能,可以使用任何 Node.js 日志包,比如WinstonPino,来生成一个完全自定义的生产环境水平的日志系统。

日志等级

  • Log : 通用日志,按需进行记录(打印)
  • Warning:警告日志,比如: 尝试多次进行数据库操作
  • Error:严重日志,比如:数据库异常
  • Debug: 调试日志,比如:加载数据日志
  • Verbose:详细日志,所有的操作与详细信息(非必要不打印)

功能分类日志

  • 错误日志 -> 方便定位问题,给用户友好的提示
  • 调试日志 -> 方便开发
  • 请求日志 -> 记录敏感行为

日志记录位置

  • 控制台日志 -> 方便监看(调试用)
  • 文件日志 -> 方便回溯与追踪(24小时滚动)
  • 数据库日志 -> 敏感操作、敏感数据记录

Nest中记录日志

image

Nest内置Logger模块

  • nest为我们内置了一个日志模块,如果不将日志数据存入文件的话基本上使用这个模块是完全足够的

1. 全局使用(在main.ts中)

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule, {
// 全局关闭日志功能,即不再会输出任何的log
// logger: false,
logger: ['error', 'warn'], // 设置日志输出的等级,只输出数组内的日志
});
await app.listen(3000);
}
bootstrap();

2. 在其他模块中使用

// user.controller.ts
import {
Controller,
Get,
Inject,
Logger,
} from '@nestjs/common';
import { BoysService } from './boys.service';

@Controller('boys')
export class BoysController {
// 定义一个logger日志模块
private logger = new Logger('模块1'); // 可以传入一段字符串用于区分

// 创建一个构造函数 (注入依赖)
constructor(
// 注入service依赖
@Inject('boys') private BoysService: BoysService,
) {
/*
BoysService: BoysService 等价于 this.BoysService = new BoysService()
*/

// 对象初始化时输出
this.logger.log('userController init !!!!');
}

// 一对一关系的查询
@Get('/user_profile')
getUser_Profile(): any {
// 请求发出时输出
this.logger.log('请求发出...')
// 返回建立user表与profile表的userId的外键约束数据
return this.BoysService.findUser_Profile(1); // 传入user的id字段
}
}

结果展示

image

使用第三方插件Pino

  • 官方文档

  • 相较于官方的日志模块,pino这个第三方模块会自动的帮我们打印请求日志,这也正是懒人日志模块称号的由来

1. 安装

npm install nestjs-pino

2. 使用

  • user.module.ts(依赖注入)
import {
Module,
NestModule,
MiddlewareConsumer,
RequestMethod,
} from '@nestjs/common';
/*
import ......
*/
import { LoggerModule } from 'nestjs-pino'; // 引入 pino 日志模块

@Module({
imports: [
TypeOrmModule.forFeature([Boys, User, Logs, Profile]),
LoggerModule.forRoot() // 依赖注入,注册第三方日志模块(pino)
],
/*
..........
*/
}
  • user.controller.ts(使用)
import {
Controller,
Get,
} from '@nestjs/common';
import { BoysService } from './boys.service';
import { Logger } from 'nestjs-pino';

@Controller('boys')
export class BoysController {
// 定义一个logger日志模块
// private logger = new Logger('模块1'); // 可以传入一段字符串用于区分

// 创建一个构造函数 (注入依赖)
constructor(
private logger: Logger, // 导入第三方的logger
) {
/*
BoysService: BoysService 等价于 this.BoysService = new BoysService()
*/
this.logger.log('userController init !!!!'); // 输出日志信息
}

// 一对一关系的查询
@Get('/user_profile')
getUser_Profile(): any {
// this.logger.log('请求发出...'); // 第三方日志模块 pino会自动输出日志
// 返回建立user表与profile表的userId的外键约束数据
return this.BoysService.findUser_Profile(1); // 传入user的id字段
}
}

结果展示:

image

设置美化日志输出格式以及保存方式配置

  • 由上图我们可以看出,原生的pino输出的日志信息是非常的丑陋的,因此我们可以使用它的配套插件,将整体的日志格式美化一下

1. 安装

npm i pino-pretty # 美化格式
npm i pino-roll # 保存为文件

2. 配置(app.modules.ts)

import { Module } from '@nestjs/common';
import { BoysModule } from './boys/boys.module';
// import .....

// 导入数据库实体(映射关系)
// import ......
import { LoggerModule } from 'nestjs-pino'; // 引入pino
import { join } from 'path';

// 在 package.json 中配置运行脚本时通过第三方库 cross-env配置当前环境(生产,开发)
const envFilePath = `.env.${process.env.NODE_ENV || `development`}`;

@Module({
imports: [
// 配置 第三方 Logger 的输出以及保存 (依赖注入)
LoggerModule.forRoot({
pinoHttp: {
transport:
process.env.NODE_ENV === 'development' // 判断是否为开发环境
? {
// 一般为开发环境使用
target: 'pino-pretty', // 修改日志的输出格式
options: {
colorize: true, // 开启颜色区别
},
}
: {
// 一般为生产环境使用
target: 'pino-roll', // 亦文件形式保存日志数据
options: {
file: join('logs', 'log.txt'), // 保存的日志路径
frequency: 'daily', // 周期,还有 hourly等
size: '10M', // 文件残生滚动的大小(文件保存的日志数据一旦超锅这个限制就创建一个新闻假案保存)
mkdir: true, // 自动创建对应目录文件
},
},
},
}),
BoysModule,
],
controllers: [],
providers: [],
})
export class AppModule {}

结果展示:

image

使用第三方插件winston

  • 这同样是一个第三方的日志插件,与pino一样,但是它可以自定义日志记录的模板,不考虑性能的话更推荐使用它
  • 官方文档

异常处理-异常过滤器(重点)

  • 我们在nodejs开发中经常会遇见接口报错的情况出现,那么此时我们常用的解决方法就是在对应的就口中做try…catch,但是当接口非常的多的时候,这种方法的效率是非常的低的,由此Nestjs官方为我们提供了一个解决方案,那就设置一个全局的异常捕获过滤器

  • 官方文档

image

由上图可见,所有的异常处理都会响应给Nest内置的异常处理器,作统一的消息返回,下面讲一下Nest中一些内置的异常类

// 比如我们可以在写接口的时候抛出一个异常
@Get('exception_test')
exception_Test(): any {
// 自定义丢出异常
// throw new HttpException(
// // 异常捕获后返回信息
// {
// status: HttpStatus.FORBIDDEN,
// error: 'test_error!!!!',
// senter: 'lam',
// },
// HttpStatus.FORBIDDEN, // 语义化错误码,可以点击自行跳转
// );

// 丢出内置异常
throw new ForbiddenException('测试异常!!!');
}

/*
HttpException 构造函数有两个必要的参数来决定响应:
response 参数定义 JSON 响应体。它可以是 string 或 object,如下所述。
status参数定义HTTP状态代码。

默认情况下,JSON 响应主体包含两个属性:
statusCode:默认为 status 参数中提供的 HTTP 状态代码
message:基于状态的 HTTP 错误的简短描述

仅覆盖 JSON 响应主体的消息部分,请在 response参数中提供一个 string。要覆盖整个 JSON 响应主体,请在response 参数中传递一个object。 Nest将序列化对象,并将其作为JSON 响应返回。第二个构造函数参数-status-是有效的 HTTP 状态代码。 最佳实践是使用从@nestjs/common导入的 HttpStatus枚举。
*/
  • 结果展示:

image
image

开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理类型 HttpException(及其子类)的异常。每个发生的异常都由全局异常过滤器处理, 当这个异常无法被识别时 (既不是 HttpException 也不是继承的类 HttpException ) , 用户将收到以下 JSON 响应:

{
"statusCode": 500,
"message": "Internal server error"
}

在项目中构建全局的异常捕获处理器

  • 首先在项目的根目录(src)下创建一个新的文件夹为logs,再在其下面创建一个http-exception.filter.ts的文件用于配置全局的http异常捕获处理过滤器

image

  • http-exception.filter.ts
/*
构建全局的异常捕获处理过滤器(固定写法)
*/
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
// import { PinoLogger } from 'nestjs-pino';

/*
Catch装饰符
(里面需要传入要捕获的错误类型,这里是http异常,不写则是相当于把所有的错误异常均捕获)
*/
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
// constructor(private readonly logger: PinoLogger) {}
// exception:错误异常 , host:请求参数等整个(可以通过它来获取请求的上下文)
catch(exception: HttpException, host: ArgumentsHost) {
// 获取上下文对象(了解该请求从哪来,到哪去)
const ctx = host.switchToHttp();
// 响应和请求对象
const response = ctx.getResponse(); // 获取响应体
const request = ctx.getRequest(); // 获取请求体
const status = exception.getStatus(); // 获取请求状态码

// console.log('请求数据信息', response, request, status);
// this.logger.error('错误信息', exception.message);

// 捕获到异常过后返回给前端页面提示
response.status(status).json({
code: status, // 返回请求状态码
timestamp: new Date().toISOString(), // 返回请求时间
path: request.url, // 返回请求路径
method: request.method, // 返回请求方法
message: exception.message || HttpException.name, // 返回错误请求报文
});
}
}
  • main.ts中注册使用
import { HttpExceptionFilter } from './filter/http-exception.filter';
/* .... */
app.useGlobalFilters(new HttpExceptionFilter()); // 开启全局http异常捕获处理过滤器
/* .... */

结合 pino 将日志数据导出至文件中

  • 基本与上面是一致的,只不过是将原来的全局过滤器从main.ts中挂起移动到app.modules.ts中挂起而已,具体实现步骤如下:
  1. NestJS的根模块(通常是app.module.ts)中导入PinoLoggerModule并将其添加到imports数组中。这将启用Pino日志记录器:
import { Module } from '@nestjs/common';
import { PinoLoggerModule } from 'nestjs-pino';// 或者LoggerModule均可

@Module({
imports: [PinoLoggerModule.forRoot()], // 或者LoggerModule均可
})
export class AppModule {}
  1. 创建一个全局异常过滤器(例如global-exception.filter.ts),并实现ExceptionFilter接口。在catch()方法中,你可以使用this.logger.error()方法将异常信息记录到日志文件中:
// 与上面的全局过滤器的设置基本一致
import { Catch, ExceptionFilter, ArgumentsHost } from '@nestjs/common';
import { PinoLogger } from 'nestjs-pino';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: PinoLogger) {}

catch(exception: any, host: ArgumentsHost) {
this.logger.error(exception.message, exception.stack);
}
}
  1. NestJS的根模块中将全局异常过滤器添加到providers数组中,并使用useClass属性指定它的类名:
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './global-exception.filter';

@Module({
providers: [
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
],
})
export class AppModule {}
  1. 现在,当你的应用程序抛出异常时,全局异常过滤器将捕获异常并将其记录到日志文件中。你可以在Pino的配置中指定日志文件的路径(app.modules.ts)和其他选项。例如,你可以在main.ts文件中进行配置:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from 'nestjs-pino';

async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new Logger({
pinoHttp: {
// 配置日志文件路径和其他选项
// 例如:`logFilePath: './logs/app.log'`
},
}),
});
await app.listen(3000);
}
bootstrap();

拓展知识 - 全局错误异常捕获处理过滤器(包括socket等报错)

// all-exception.filter.ts
/*
所有异常过滤器(包括socket报错等...)
*/
import {
ExceptionFilter,
HttpAdapterHost,
HttpException,
HttpStatus,
LoggerService,
} from '@nestjs/common';
import { ArgumentsHost, Catch } from '@nestjs/common';

import * as requestIp from 'request-ip'; // 获取请求ip的第三方库

@Catch()
export class AllExceptionFilter implements ExceptionFilter {
constructor(
private readonly logger: LoggerService,
private readonly httpAdapterHost: HttpAdapterHost,
) {}
catch(exception: unknown, host: ArgumentsHost) {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();

const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;

const responseBody = {
headers: request.headers,
query: request.query,
body: request.body,
params: request.params,
timestamp: new Date().toISOString(),
// 还可以加入一些用户信息
// IP信息
ip: requestIp.getClientIp(request),
exceptioin: exception['name'],
error: exception['response'] || 'Internal Server Error',
};

this.logger.error('[lam]', responseBody);
httpAdapter.reply(response, responseBody, httpStatus);
}
}
  • main.ts
const httpAdapter = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionFilter(logger, httpAdapter));