「项目实战」从0搭建NestJS后端服务(六):日志系统的集成

前言

大家好,我是 elk。今天我们要给项目加上一个超级重要的功能------日志系统!日志系统就像是应用程序的 "黑匣子",记录着它的一举一动,比如控制台输出、文件记录、HTTP 请求、数据库操作等等。有了它,我们就能轻松地监控应用的运行状态,快速定位问题,让开发和运维变得更加高效。

日志系统选型

技术选型

  • Winston:Node.js生态最主流的日志库,支持多种传输方式
  • DailyRotateFile:实现日志文件按时间/大小自动轮转
  • NestJS整合:通过拦截器+过滤器实现无侵入式日志记录

结构设计

bash 复制代码
日志系统工作流:
客户端请求 -> 全局拦截器(记录入参) 
          -> 业务处理 
          -> 全局拦截器(记录响应)
          -> 若异常 -> 异常过滤器(记录错误详情)

插件安装

bash 复制代码
# 核心依赖
pnpm install --save nest-winston winston  winston-daily-rotate-file

创建日志服务

新建公共的日志服务

目录:src/module/common/logger/logger.service.ts|logger.module.ts

kotlin 复制代码
import { Module, Global } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Global()
@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export class LoggerModule {}
typescript 复制代码
// 从@nestjs/common导入Injectable装饰器
import { Injectable } from '@nestjs/common';
// 从winston导入日志相关功能
import { createLogger, format, transports, Logger } from 'winston';
// 导入winston-daily-rotate-file用于日志文件轮转
import 'winston-daily-rotate-file';
​
// 使用Injectable装饰器,将该类标记为可注入的服务
@Injectable()
export class LoggerService {
  private logger: Logger; // 日志记录器实例
  constructor() {
    // 初始化日志记录器
    this.logger = createLogger({
      level: 'info', // 默认日志级别
      format: format.combine(
        // 日志格式组合
        format.timestamp({
          // 时间戳格式
          format: 'YYYY-MM-DD HH:mm:ss',
        }),
        format.printf(({ message, timestamp, level, ...metadata }) => {
          // 自定义日志格式
          let LogMessage = `${timestamp} ${level}: ${message}`;
          if (Object.keys(metadata).length > 0) {
            if (metadata.message !== 'message') {
              LogMessage += ` - ${JSON.stringify(metadata, null, 2)}`;
            }
          }
          return LogMessage;
        }),
      ),
      transports: [
        // 日志传输方式
        new transports.Console({
          // 控制台输出
          format: format.combine(format.colorize(), format.simple()),
        }),
        new transports.DailyRotateFile({
          // 每日轮转文件输出
          filename: 'logs/application-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true, // 是否压缩归档
          maxSize: '20m', // 单个文件最大大小
          maxFiles: '14d', // 保留天数
          level: 'info', // 日志级别
          format: format.combine(
            // 文件日志格式
            format.timestamp({
              format: 'YYYY-MM-DD HH:mm:ss',
            }),
            format.printf((info) => {
              const paramsInfo = JSON.parse(JSON.stringify(info));
              return `${info.timestamp} [${info.level}]: ${info.message} ${
                Object.keys(info).length > 0
                  ? JSON.stringify(paramsInfo)
                  : ''
              }`;
            }),
          ),
        }),
      ],
    });
  }
  // 记录日志信息,支持附加参数
  log(message: string, params: object = {}) {
    if (typeof params === 'object' && Object.keys(params).length > 0) {
      // 如果有附加参数,将参数与消息合并
      const logMessage = {
        message,
        ...params,
        level: 'info', // 日志级别为info
      };
      this.logger.log(logMessage); // 记录日志
    } else {
      // 如果没有附加参数,只记录消息
      this.logger.log({ message, level: 'info' });
    }
  }
  // 记录错误日志,支持附加参数和堆栈跟踪
  error(message: string, params: object = {}, trace = '') {
    if (typeof params === 'object' && Object.keys(params).length > 0) {
      const logMessage = {
        message,
        ...params,
        level: 'error', // 日志级别为error
        trace, // 堆栈跟踪信息
      };
      this.logger.error(logMessage); // 记录错误日志
    } else {
      this.logger.error({ message, level: 'error' });
    }
  }
  // 记录警告日志,支持附加参数
  warn(message: string, params: object = {}) {
    if (typeof params === 'object' && Object.keys(params).length > 0) {
      const logMessage = {
        message,
        ...params,
        level: 'warn', // 日志级别为warn
      };
      this.logger.warn(logMessage); // 记录警告日志
    } else {
      this.logger.warn({ message, level: 'warn' });
    }
  }
  // 记录调试日志,支持附加参数
  debug(message: string, params: object = {}) {
    if (typeof params === 'object' && Object.keys(params).length > 0) {
      const logMessage = {
        message,
        ...params,
        level: 'debug', // 日志级别为debug
      };
      this.logger.debug(logMessage); // 记录调试日志
    } else {
      this.logger.debug({ message, level: 'debug' });
    }
  }
  // 记录信息日志,支持附加参数
  info(message: string, params: object = {}) {
    if (typeof params === 'object' && Object.keys(params).length > 0) {
      const logMessage = {
        message,
        ...params,
        level: 'info', // 日志级别为info
      };
      this.logger.info(logMessage); // 记录信息日志
    } else {
      this.logger.info({ message, level: 'info' });
    }
  }
}

配置错误过滤和响应拦截中的日志记录

错误过滤器中请求和响应打印错误日志内容,进行记录

目录:src/common/filters/all-exceptions.filters.ts

typescript 复制代码
// 从@nestjs/common导入所需的装饰器和接口
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
// 从express导入Response和Request类型
import { Response, Request } from 'express';
​
// 引入日志服务
import { LoggerService } from '@/module/common/logger/logger.service';
​
// 使用Catch装饰器,指定捕获HttpException类型的异常
@Catch(HttpException)
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly logger: LoggerService) {}
  // 实现catch方法,处理捕获的异常
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取HTTP上下文
    const response = ctx.getResponse<Response>(); // 获取响应对象
    const request = ctx.getRequest<Request>(); // 获取请求对象
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR; // 获取状态码,默认为500
    const message =
      exception instanceof HttpException ? exception.message : '服务器错误'; // 获取错误信息,默认为'服务器错误'
    // 返回统一的错误响应格式
    response.status(status).json({
      code: status, // 状态码
      message, // 错误信息
      timestamp: new Date().toISOString(), // 时间戳
      path: request.url, // 请求路径
    });
    const { headers, url, params, query, body, method } = request;
    this.logger.log('请求信息', {
      headers,
      url,
      params,
      query,
      body,
      method,
    });
    this.logger.log('响应信息', {
      url,
      method,
      status,
      message,
      timestamp: new Date().toISOString(),
    });
  }
}

统一的请求响应拦截器进行请求和响应的日志打印,进行记录

目录:src/common/interceptors/response.interceptor.ts

typescript 复制代码
// 从@nestjs/common导入所需的装饰器和接口
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
// 导入RxJS的Observable
import { Observable } from 'rxjs';
// 导入RxJS的map操作符
import { map, tap } from 'rxjs/operators';
// 导入自定义的ApiResponse接口
import { ApiResponse } from '@/common/interfaces/api-response';
// 引入日志服务
import { LoggerService } from '@/module/common/logger/logger.service';
import { Request } from 'express';
​
// 使用Injectable装饰器,将该类标记为可注入的拦截器
@Injectable()
export class ResponseInterceptor<T>
  implements NestInterceptor<T, ApiResponse<T>>
{
  // 构造函数,接收LoggerService实例
  constructor(private readonly logger: LoggerService) {}
  // 实现拦截器方法,用于处理响应数据
  intercept(
    context: ExecutionContext, // 执行上下文
    next: CallHandler, // 下一个处理程序
  ): Observable<ApiResponse<T>> {
    const request = context.switchToHttp().getRequest<Request>();
    const { method, url, params, query, body, headers } = request;
    this.logger.log('请求信息', {
      method,
      url,
      params,
      query,
      body,
      headers,
    });
    return next.handle().pipe(
      tap((data) => {
        this.logger.log('响应信息', {
          url,
          method,
          status: 200,
          msg: '请求成功',
          data,
        });
      }),
      // 使用map操作符转换响应数据
      map((data) => ({
        code: 200, // 响应状态码
        message: '请求成功', // 响应消息
        data, // 实际响应数据
      })),
    );
  }
}

启动日志系统

在入口文件中进行日志服务的注册,将日志服务传递到错误过滤器和响应拦截器中

main.ts

csharp 复制代码
// 引入logger日志服务
import { LoggerService } from '@/module/common/logger/logger.service';
​
// 全局日志服务
const loggerService = app.get(LoggerService);
​
// 全局响应拦截器
app.useGlobalInterceptors(new ResponseInterceptor(loggerService));
// 全局异常过滤器
app.useGlobalFilters(new AllExceptionsFilter(loggerService));
​

📍 下期预告

《从0搭建NestJS后端服务(六):JWT身份验证以及管道参数验证》

我们将探讨:

  • JWT 身份验证的原理和实现步骤
  • 如何创建自定义管道进行参数验证
  • 如何处理验证失败的情况

🤝 互动时间

你在日志实践中遇到过哪些"坑"?欢迎留言讨论!

💬 如何平衡日志详尽性与性能损耗?

💬 日志脱敏有哪些最佳实践?

💬 是否遇到过日志文件权限问题?

一起交流成长,让开发更高效!🚀

相关推荐
茉莉蜜茶only3 分钟前
【前端实习岗位】淘天集团2026届春季实习生招聘
前端·招聘
Book_熬夜!17 分钟前
Vue2——组件的注册与通信方式、默认插槽、具名插槽、插槽的作用域
前端·javascript·vue.js·前端框架·ecmascript
夕水24 分钟前
后端说,这个超大数字idxxx会变成xxx,让我知道了js的一个陷阱
前端
关二哥拉二胡27 分钟前
向零基础前端介绍什么是 MCP
前端·面试·mcp
Bigger28 分钟前
websocket 推送的数据丢了,怎么回事?
前端·websocket·react.js
YiHanXii38 分钟前
Axios的二次封装
开发语言·前端
纸醉金迷金手指1 小时前
GHCTF-web-wp
前端·jvm
猫猫不是喵喵.1 小时前
vue 脚手架解决跨域问题
前端·javascript·vue.js
不能只会打代码1 小时前
六十天前端强化训练之第三十一天之Webpack 基础配置 大师级讲解(接下来几天给大家讲讲工具链与工程化)
前端·webpack·node.js