Nest.js 從零到壹系列(四):使用中間件、攔截器、過濾器打造日誌系統

前言

上一篇介紹瞭如何使用 JWT 進行單點登陸,接下來,要完善一下後端項目的一些基礎功能。git

首先,一個良好的服務端,應該有較完善的日誌收集功能,這樣才能在生產環境發生異常時,可以從日誌中覆盤,找出 Bug 所在。程序員

其次,要針對項目中拋出的異常進行歸類,並將信息反映在接口或日誌中。github

最後,請求接口的參數也應該被記錄,以便統計分析(主要用於大數據和惡意攻擊分析)。typescript

GitHub 項目地址,歡迎各位大佬 Star。數據庫

1、日誌系統

這裏使用的是 log4js,前身是 log4j,若是有寫過 Java 的大佬應該不會陌生。express

已經有大佬總結了 log4js 的用法,就不在贅述了:json

《Node.js 之 log4js 徹底講解》bootstrap

1. 配置

先安裝依賴包後端

$ yarn add log4js stacktrace-js -S
複製代碼

在 config 目錄下新建一個文件 log4js.ts,用於編寫配置文件:bash

// config/log4js.ts

import * as path from 'path';
const baseLogPath = path.resolve(__dirname, '../../logs'); // 日誌要寫入哪一個目錄

const log4jsConfig = {
  appenders: {
    console: {
      type: 'console', // 會打印到控制檯
    },
    access: {
      type: 'dateFile', // 會寫入文件,並按照日期分類
      filename: `${baseLogPath}/access/access.log`, // 日誌文件名,會命名爲:access.20200320.log
      alwaysIncludePattern: true,
      pattern: 'yyyyMMdd',
      daysToKeep: 60,
      numBackups: 3,
      category: 'http',
      keepFileExt: true, // 是否保留文件後綴
    },
    app: {
      type: 'dateFile',
      filename: `${baseLogPath}/app-out/app.log`,
      alwaysIncludePattern: true,
      layout: {
        type: 'pattern',
        pattern: '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
      },
      // 日誌文件按日期(天)切割
      pattern: 'yyyyMMdd',
      daysToKeep: 60,
      // maxLogSize: 10485760,
      numBackups: 3,
      keepFileExt: true,
    },
    errorFile: {
      type: 'dateFile',
      filename: `${baseLogPath}/errors/error.log`,
      alwaysIncludePattern: true,
      layout: {
        type: 'pattern',
        pattern: '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
      },
      // 日誌文件按日期(天)切割
      pattern: 'yyyyMMdd',
      daysToKeep: 60,
      // maxLogSize: 10485760,
      numBackups: 3,
      keepFileExt: true,
    },
    errors: {
      type: 'logLevelFilter',
      level: 'ERROR',
      appender: 'errorFile',
    },
  },
  categories: {
    default: {
      appenders: ['console', 'app', 'errors'],
      level: 'DEBUG',
    },
    info: { appenders: ['console', 'app', 'errors'], level: 'info' },
    access: { appenders: ['console', 'app', 'errors'], level: 'info' },
    http: { appenders: ['access'], level: 'DEBUG' },
  },
  pm2: true, // 使用 pm2 來管理項目時,打開
  pm2InstanceVar: 'INSTANCE_ID', // 會根據 pm2 分配的 id 進行區分,以避免各進程在寫日誌時形成衝突
};

export default log4jsConfig;
複製代碼

上面貼出了個人配置,並標註了一些簡單的註釋,請配合 《Node.js 之 log4js 徹底講解》 一塊兒食用。

2. 實例化

有了配置,就能夠着手寫 log4js 的實例以及一些工具函數了。

src/utils 下新建 log4js.ts:

// src/utils/log4js.ts
import * as Path from 'path';
import * as Log4js from 'log4js';
import * as Util from 'util';
import * as Moment from 'moment'; // 處理時間的工具
import * as StackTrace from 'stacktrace-js';
import Chalk from 'chalk';
import config from '../../config/log4js';

// 日誌級別
export enum LoggerLevel {
  ALL = 'ALL',
  MARK = 'MARK',
  TRACE = 'TRACE',
  DEBUG = 'DEBUG',
  INFO = 'INFO',
  WARN = 'WARN',
  ERROR = 'ERROR',
  FATAL = 'FATAL',
  OFF = 'OFF',
}

// 內容跟蹤類
export class ContextTrace {
  constructor( public readonly context: string, public readonly path?: string, public readonly lineNumber?: number, public readonly columnNumber?: number, ) {}
}

Log4js.addLayout('Awesome-nest', (logConfig: any) => {
  return (logEvent: Log4js.LoggingEvent): string => {
    let moduleName: string = '';
    let position: string = '';

    // 日誌組裝
    const messageList: string[] = [];
    logEvent.data.forEach((value: any) => {
      if (value instanceof ContextTrace) {
        moduleName = value.context;
        // 顯示觸發日誌的座標(行,列)
        if (value.lineNumber && value.columnNumber) {
          position = `${value.lineNumber}, ${value.columnNumber}`;
        }
        return;
      }

      if (typeof value !== 'string') {
        value = Util.inspect(value, false, 3, true);
      }

      messageList.push(value);
    });

    // 日誌組成部分
    const messageOutput: string = messageList.join(' ');
    const positionOutput: string = position ? ` [${position}]` : '';
    const typeOutput: string = `[${logConfig.type}] ${logEvent.pid.toString()} - `;
    const dateOutput: string = `${Moment(logEvent.startTime).format('YYYY-MM-DD HH:mm:ss')}`;
    const moduleOutput: string = moduleName ? `[${moduleName}] ` : '[LoggerService] ';
    let levelOutput: string = `[${logEvent.level}] ${messageOutput}`;

    // 根據日誌級別,用不一樣顏色區分
    switch (logEvent.level.toString()) {
      case LoggerLevel.DEBUG:
        levelOutput = Chalk.green(levelOutput);
        break;
      case LoggerLevel.INFO:
        levelOutput = Chalk.cyan(levelOutput);
        break;
      case LoggerLevel.WARN:
        levelOutput = Chalk.yellow(levelOutput);
        break;
      case LoggerLevel.ERROR:
        levelOutput = Chalk.red(levelOutput);
        break;
      case LoggerLevel.FATAL:
        levelOutput = Chalk.hex('#DD4C35')(levelOutput);
        break;
      default:
        levelOutput = Chalk.grey(levelOutput);
        break;
    }

    return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow(moduleOutput)}${levelOutput}${positionOutput}`;
  };
});

// 注入配置
Log4js.configure(config);

// 實例化
const logger = Log4js.getLogger();
logger.level = LoggerLevel.TRACE;

export class Logger {
  static trace(...args) {
    logger.trace(Logger.getStackTrace(), ...args);
  }

  static debug(...args) {
    logger.debug(Logger.getStackTrace(), ...args);
  }

  static log(...args) {
    logger.info(Logger.getStackTrace(), ...args);
  }

  static info(...args) {
    logger.info(Logger.getStackTrace(), ...args);
  }

  static warn(...args) {
    logger.warn(Logger.getStackTrace(), ...args);
  }

  static warning(...args) {
    logger.warn(Logger.getStackTrace(), ...args);
  }

  static error(...args) {
    logger.error(Logger.getStackTrace(), ...args);
  }

  static fatal(...args) {
    logger.fatal(Logger.getStackTrace(), ...args);
  }

  static access(...args) {
    const loggerCustom = Log4js.getLogger('http');
    loggerCustom.info(Logger.getStackTrace(), ...args);
  }

  // 日誌追蹤,能夠追溯到哪一個文件、第幾行第幾列
  static getStackTrace(deep: number = 2): string {
    const stackList: StackTrace.StackFrame[] = StackTrace.getSync();
    const stackInfo: StackTrace.StackFrame = stackList[deep];

    const lineNumber: number = stackInfo.lineNumber;
    const columnNumber: number = stackInfo.columnNumber;
    const fileName: string = stackInfo.fileName;
    const basename: string = Path.basename(fileName);
    return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`;
  }
}
複製代碼

上面貼出了我實例化 log4js 的過程,主要是處理日誌的組成部分(包含了時間、類型,調用文件以及調用的座標),還能夠根據日誌的不一樣級別,在控制檯中用不一樣的顏色顯示。

這個文件,不但能夠單獨調用,也能夠作成中間件使用。

3. 製做中間件

咱們但願每次用戶請求接口的時候,自動記錄請求的路由、IP、參數等信息,若是每一個路由都寫,那就太傻了,因此須要藉助中間件來實現。

Nest 中間件實際上等價於 express 中間件。

中間件函數能夠執行如下任務:

  • 執行任何代碼;
  • 對請求和響應對象進行更改;
  • 結束請求-響應週期;
  • 調用堆棧中的下一個中間件函數;
  • 若是當前的中間件函數沒有【結束請求】或【響應週期】, 它必須調用 next() 將控制傳遞給下一個中間件函數。不然,請求將被掛起;

執行下列命令,建立中間件文件:

$ nest g middleware logger middleware
複製代碼

而後,src 目錄下,就多出了一個 middleware 的文件夾,裏面的 logger.middleware.ts 就是接下來的主角,Nest 預設的中間件模板長這樣:

// src/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    next();
  }
}
複製代碼

這裏只是實現了 NestMiddleware 接口,它接收 3 個參數:

  • req:即 Request,請求信息;
  • res:即 Response ,響應信息;
  • next:將控制傳遞到下一個中間件,寫過 Vue、Koa 的應該不會陌生;

接下來,咱們將日誌功能寫入中間件:

// src/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { Logger } from '../utils/log4js';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    const code = res.statusCode; // 響應狀態碼
    next();
    // 組裝日誌信息
    const logFormat = `Method: ${req.method} \n Request original url: ${req.originalUrl} \n IP: ${req.ip} \n Status code: ${code} \n`;
    // 根據狀態碼,進行日誌類型區分
    if (code >= 500) {
      Logger.error(logFormat);
    } else if (code >= 400) {
      Logger.warn(logFormat);
    } else {
      Logger.access(logFormat);
      Logger.log(logFormat);
    }
  }
}
複製代碼

同時,Nest 也支持【函數式中間件】,咱們將上面的功能用函數式實現一下:

// src/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { Logger } from '../utils/log4js';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  ...
}

// 函數式中間件
export function logger(req: Request, res: Response, next: () => any) {
  const code = res.statusCode; // 響應狀態碼
  next();
  // 組裝日誌信息
  const logFormat = ` >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    Request original url: ${req.originalUrl}
    Method: ${req.method}
    IP: ${req.ip}
    Status code: ${code}
    Parmas: ${JSON.stringify(req.params)}
    Query: ${JSON.stringify(req.query)}
    Body: ${JSON.stringify(req.body)} \n  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  `;
  // 根據狀態碼,進行日誌類型區分
  if (code >= 500) {
    Logger.error(logFormat);
  } else if (code >= 400) {
    Logger.warn(logFormat);
  } else {
    Logger.access(logFormat);
    Logger.log(logFormat);
  }
}
複製代碼

上面的日誌格式進行了一些改動,主要是爲了方便查看。

至於使用 Nest 提供的仍是函數式中間件,能夠視需求決定。固然,Nest 原生的中間件高級玩法會更多一些。

4. 應用中間件

作好中間件後,咱們只須要將中間件引入 main.ts 中就行了:

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

import { logger } from './middleware/logger.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 監聽全部的請求路由,並打印日誌
  app.use(logger);
  app.setGlobalPrefix('nest-zero-to-one');
  await app.listen(3000);
}
bootstrap();
複製代碼

保存代碼後,就會發現,項目目錄下就多了幾個文件:

這就是以前 config/log4js.ts 中配置的成果

接下來,咱們試着請求一下登陸接口:

發現雖然是打印了,可是沒有請求參數信息。

因而,咱們還要作一部操做,將請求參數處理一下:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import { logger } from './middleware/logger.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(express.json()); // For parsing application/json
  app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
  // 監聽全部的請求路由,並打印日誌
  app.use(logger);
  app.setGlobalPrefix('nest-zero-to-one');
  await app.listen(3000);
}
bootstrap();
複製代碼

再請求一次,發現參數已經出來了:

上面的打印信息,IP 爲 ::1 是由於我全部的東西都跑在本地,正常狀況下,會打印對方的 IP 的。

再去看看 logs/ 文件夾下:

上圖能夠看到日誌已經寫入文件了。

5. 初探攔截器

前面已經示範了怎麼打印入參,可是光有入參信息,沒有出參信息確定不行的,否則怎麼定位 Bug 呢。

Nest 提供了一種叫作 Interceptors(攔截器) 的東東,你能夠理解爲關卡,除非遇到關羽這樣的能夠過五關斬六將,不然全部的參數都會通過這裏進行處理,正所謂雁過拔毛。

詳細的使用方法會在後面的教程進行講解,這裏只是先大體介紹一下怎麼使用:

執行下列指令,建立 transform文件

$ nest g interceptor transform interceptor
複製代碼

而後編寫出參打印邏輯,intercept 接受兩個參數,當前的上下文和傳遞函數,這裏還使用了 pipe(管道),用於傳遞響應數據:

// src/interceptor/transform.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Logger } from '../utils/log4js';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.getArgByIndex(1).req;
    return next.handle().pipe(
      map(data => {
        const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip} User: ${JSON.stringify(req.user)} Response data:\n ${JSON.stringify(data.data)} <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<`;
        Logger.info(logFormat);
        Logger.access(logFormat);
        return data;
      }),
    );
  }
}
複製代碼

保存文件,而後在 main.ts 中引入,使用 useGlobalInterceptors 調用全局攔截器:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import { logger } from './middleware/logger.middleware';
import { TransformInterceptor } from './interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(express.json()); // For parsing application/json
  app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
  // 監聽全部的請求路由,並打印日誌
  app.use(logger);
  // 使用全局攔截器打印出參
  app.useGlobalInterceptors(new TransformInterceptor());
  app.setGlobalPrefix('nest-zero-to-one');
  await app.listen(3000);
}
bootstrap();
複製代碼

咱們再試一次登陸接口:

能夠看到,出參的日誌已經出來了,User 爲 undefiend 是由於登陸接口沒有使用 JWT 守衛,若路由加了 @UseGuards(AuthGuard('jwt')),則會把用戶信息綁定在 req 上,具體操做可回顧上一篇教程。

2、異常處理

在開發的過程當中,不免會寫出各式各樣的「八阿哥」,否則程序員就要失業了。一個富有愛心的程序員應該在輸出代碼的同時創造出3個崗位(手動狗頭)。

迴歸正題,光有入參出參日誌還不夠,異常的捕獲和拋出也須要記錄。

接下來,咱們先故意寫錯語法,看看控制檯打印什麼:

如圖,只會記錄入參以及控制檯默認的報錯信息,而默認的報錯信息,是不會寫入日誌文件的。

再看看請求的返回數據:

如圖,這裏只會看到 "Internal server error",其餘什麼信息都沒有。

這樣就會有隱患了,用戶在使用過程當中報錯了,可是日誌沒有記錄報錯的緣由,就沒法統計影響範圍,若是是簡單的報錯還好,若是涉及數據庫各類事務或者併發問題,就很難追蹤定位了,總不能一直看着控制檯吧。

所以,咱們須要捕獲代碼中未捕獲的異常,並記錄日誌到 logs/errors 裏,方便登陸線上服務器,對錯誤日誌進行篩選、排查。

1. 初探過濾器

Nest 不光提供了攔截器,也提供了過濾器,就代碼結構而言,和攔截器很類似。

內置的異常層負責處理整個應用程序中的全部拋出的異常。當捕獲到未處理的異常時,最終用戶將收到友好的響應。

咱們先新建一個 http-exception.filter 試試:

$ nest g filter http-exception filter
複製代碼

打開文件,默認代碼長這樣:

// src/filter/http-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';

@Catch()
export class HttpExceptionFilter<T> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {}
}
複製代碼

能夠看到,和攔截器的結構大同小異,也是接收 2 個參數,只不過用了 @Catch() 來修飾。

2. HTTP 錯誤的捕獲

Nest提供了一個內置的 HttpException 類,它從 @nestjs/common 包中導入。對於典型的基於 HTTP REST/GraphQL API 的應用程序,最佳實踐是在發生某些錯誤狀況時發送標準 HTTP 響應對象。

HttpException 構造函數有兩個必要的參數來決定響應:

  • response 參數定義 JSON 響應體。它能夠是 string 或 object,以下所述。
  • status參數定義HTTP狀態代碼。

默認狀況下,JSON 響應主體包含兩個屬性:

  • statusCode:默認爲 status 參數中提供的 HTTP 狀態代碼
  • message:基於狀態的 HTTP 錯誤的簡短描述

咱們先來編寫捕獲打印的邏輯:

// src/filter/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
import { Logger } from '../utils/log4js';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Request original url: ${request.originalUrl} Method: ${request.method} IP: ${request.ip} Status code: ${status} Response: ${exception.toString()} \n <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< `;
    Logger.info(logFormat);
    response.status(status).json({
      statusCode: status,
      error: exception.message,
      msg: `${status >= 500 ? 'Service Error' : 'Client Error'}`,
    });
  }
}
複製代碼

上面代碼表示如何捕獲 HTTP 異常,並組裝成更友好的信息返回給用戶。

咱們測試一下,先把註冊接口的 Token 去掉,請求:

上圖是尚未加過濾器的請求結果。

咱們在 main.ts 中引入 http-exception

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import { logger } from './middleware/logger.middleware';
import { TransformInterceptor } from './interceptor/transform.interceptor';
import { HttpExceptionFilter } from './filter/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(express.json()); // For parsing application/json
  app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
  // 監聽全部的請求路由,並打印日誌
  app.use(logger);
  // 使用攔截器打印出參
  app.useGlobalInterceptors(new TransformInterceptor());
  app.setGlobalPrefix('nest-zero-to-one');
  // 過濾處理 HTTP 異常
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();
複製代碼

使用全局過濾器 useGlobalFilters 調用 http-exception,再請求:

再看控制檯打印:

如此一來,就能夠看到未帶 Token 請求的結果了,具體信息的組裝,能夠根據我的喜愛進行修改。

3. 內置HTTP異常

爲了減小樣板代碼,Nest 提供了一系列繼承自核心異常 HttpException 的可用異常。全部這些均可以在 @nestjs/common包中找到:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableException
  • InternalServerErrorException
  • NotImplementedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException

結合這些,能夠自定義拋出的異常類型,好比後面的教程說到權限管理的時候,就能夠拋出 ForbiddenException 異常了。

4. 其餘錯誤的捕獲

除了 HTTP 相關的異常,還能夠捕獲項目中出現的全部異常,咱們新建 any-exception.filter

$ nest g filter any-exception filter
複製代碼

同樣的套路:

// src/filter/any-exception.filter.ts
/** * 捕獲全部異常 */
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Logger } from '../utils/log4js';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

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

    const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Request original url: ${request.originalUrl} Method: ${request.method} IP: ${request.ip} Status code: ${status} Response: ${exception} \n <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< `;
    Logger.error(logFormat);
    response.status(status).json({
      statusCode: status,
      msg: `Service Error: ${exception}`,
    });
  }
}
複製代碼

http-exception 的惟一區別就是 exception 的類型是 unknown

咱們將 any-exception 引入 main.ts:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import { logger } from './middleware/logger.middleware';
import { TransformInterceptor } from './interceptor/transform.interceptor';
import { HttpExceptionFilter } from './filter/http-exception.filter';
import { AllExceptionsFilter } from './filter/any-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(express.json()); // For parsing application/json
  app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
  // 監聽全部的請求路由,並打印日誌
  app.use(logger);
  // 使用攔截器打印出參
  app.useGlobalInterceptors(new TransformInterceptor());
  app.setGlobalPrefix('nest-zero-to-one');
  app.useGlobalFilters(new AllExceptionsFilter());
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();
複製代碼

注意:AllExceptionsFilter 要在 HttpExceptionFilter 的上面,不然 HttpExceptionFilter 就不生效了,全被 AllExceptionsFilter 捕獲了。

而後,咱們帶上 Token (爲了跳過 401 報錯)再請求一次:

再看看控制檯:

已經有了明顯的區別,再看看 errors.log,也寫進了日誌中:

如此一來,代碼中未捕獲的錯誤也能從日誌中查到了。

總結

本篇介紹瞭如何使用 log4js 來管理日誌,製做中間件和攔截器對入參出參進行記錄,以及使用過濾器對異常進行處理。

文中日誌的打印格式能夠按照本身喜愛進行排版,不必定侷限於此。

良好的日誌管理能幫咱們快速排查 Bug,減小加班,不作資本家的奴隸,把有限的精力投入到無限的可能上。

下一篇將介紹如何使用 DTO 對參數進行驗證,解脫各類 if - else。

參考資料:

Nest.js 官方文檔

Nest.js 中文文檔

《Node.js 之 log4js 徹底講解》

`

相關文章
相關標籤/搜索