文章 / 后端工程 / Nest搭建(2)

Nest搭建(2)

nest主要组件

2024年5月21日27 min8083 words
Nest搭建(2)
图 1-1 cover.webp

中间件

  • log
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

使用log4js

config

import * as path from 'path';
const baseLogPath = 'logs';

const log4jsConfig = {
  appenders: {
    console: {
      type: 'console', //打印到控制台
    },
    access: {
      type: 'dateFile', //会写入文件,并且按照日期分类
      filename: `${baseLogPath}/access/access.log`, //日志文件名,会命名为:access.当前时间.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,
      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,
      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;

log4js

import * as Path from 'path';
import * as Log4js from 'log4js';
import * as Util from 'util';
import * as Moment from 'dayjs'; // 处理时间的工具
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 ? `''` : '';
        const typeOutput: string = `''${logEvent.pid.toString()} - `;
        const dateOutput: string = `${Moment(logEvent.startTime).format('YYYY-MM-DD HH:mm:ss')}`;
        const moduleOutput: string = moduleName ? `''` : '[LoggerService]';
        let levelOutput: string = `''${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)}`
    }
})

// 注入配置
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];
        if (!stackInfo) return `no stack information`;
        const lineNumber: number = stackInfo.lineNumber || 0;
        const columnNumber: number = stackInfo.columnNumber|| 0;
        const fileName: string = stackInfo.fileName || '';
        const basename: string = Path.basename(fileName);
        return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`;
    }
}

logger.middleware

import { Injectable, NestMiddleware } from "@nestjs/common";
import { NextFunction, Request, Response } from "express";
import { Logger } from '../utils/log4js';

export function logger(req: Request, res: Response, next: NextFunction) {
    const code = res.statusCode;
    next();
    //  组装日志信息
    const logFormat = `Method:${req.method}  Request original url: ${req.originalUrl}  IP:${req.ip}  Status code:${code}
    ${JSON.stringify(req.body)}
    `;
    // 根据状态码进行日志类型区分
    if (code >= 500) {
        Logger.error(logFormat);
    } else if (code >= 400) {
        Logger.warn(logFormat);
    } else {
        Logger.access(logFormat);
        Logger.log(logFormat);
    }
}

interceptor

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { map } from "rxjs";
import { ResponseEnum } from "../enum/response.enum";
import { BaseResponse } from "../utils/baseResponse.entity";
import { Logger } from "../utils/log4js";

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler) {
        const req = context.getArgByIndex(1).req;
        return next.handle().pipe(
            map((data) => {
                let res;
                if (data instanceof BaseResponse) {
                    res = data;
                } else if (data instanceof ResponseEnum) {
                    res = new BaseResponse(data.data, data.code, data.message);
                } else {
                    res = new BaseResponse(data);
                    Logger.info(`Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip} UserId: ${JSON.stringify(req.userId)}
    ${JSON.stringify(res)}
                        `);
                }
                return res;
            }
            ),
        );
    }
}

filter

import {
  ArgumentsHost,
  BadRequestException,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
  UnauthorizedException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { Logger } from '../utils/log4js';

@Catch()
export class CommonExceptionFilter implements ExceptionFilter {
  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();
    let status;
    let logFormat;
    switch (true) {
      case exception instanceof UnauthorizedException: {
        status = HttpStatus.UNAUTHORIZED;
        logFormat = `Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip}}`;
        break;
      }
      case exception instanceof BadRequestException: {
        if (exception instanceof BadRequestException) {
          status = exception.getStatus();
          const badRequestException = exception.getResponse();
          let message;
          if (typeof badRequestException === 'string') {
            message = badRequestException;
          } else if (badRequestException.hasOwnProperty('message')) {
            message = badRequestException['message'][0];
          }
          exception.message = message;
          logFormat = `Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip}}
                      ${message}
          ${exception.stack}`;
        }
        break;
      }
      case exception instanceof HttpException: {
        if (exception instanceof HttpException) status = exception.getStatus();
        logFormat = `Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip}}
                ${exception.stack}`;
        break;
      }
      default: {
        status = HttpStatus.INTERNAL_SERVER_ERROR;
        logFormat = `Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip}}
                ${exception.stack}`;
      }
    }
    Logger.error(logFormat);
    response.status(status).json({
      code: status,
      msg: exception.message,
      data: '',
    });
  }
}

评论 (0)

加载评论中...
RELATED NOTES
05 entries
01给 AI Agent 设计可靠的发布系统:幂等、断点续跑与跨平台一致性阅读文章 →02docker镜像资源阅读文章 →03mcp服务的node实现阅读文章 →04kafka阅读文章 →05知乎邀请指定用户阅读文章 →