继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

使用 Axios 和 NestJS 实现 HTTP 请求的日志记录

慕沐林林
关注TA
已关注
手记 262
粉丝 29
获赞 116

照片由 Campaign CreatorsUnsplash 拍摄

问题所在

最近,我在整合不同提供商的API时遇到了一个有趣的挑战。除了标准化适用于所有解决方案的数据之外,记录对第三方网站的每一次请求可以使调试和追责变得更加容易。这可以使得调试和追责变得更加容易。

为此,我就开始找可以重复使用且安装后就可以不用操心的选项。由于开发过程中已经在用了 Axios,所以我选择了用拦截器,因为这样一旦实现就可以不用再管了。

Axios拦截器是你可以添加到请求或响应上的函数,以便在请求发送前或收到响应后运行特定逻辑。拦截器让我能够测量请求所花的时间,并保存其内容以备后用。

我们来创建一个日志模块(注释)

我假设你已经有一个设置好的NestJS项目,但如果你需要帮助你,可以参考他们的文档。第一步是创建我们的logger模块,这里存放所有的日志逻辑。稍后我们会将这个服务注入到新的Axios实例中。我使用TypeORM和MySQL数据库来构建这个模块,但你的应用程序也可以采用不同的日志记录方法,所以你可以根据需要编辑这个文件并添加相应的依赖。

    // src/logger/entities/HttpLog.entity.ts

    import {  
      Entity,  
      PrimaryGeneratedColumn,  
      Column,  
    } from 'typeorm';  

    // HTTP日志实体类,用于存储HTTP请求和响应的详细信息
    @Entity('http_logs')  
    export class HttpLogEntity {  
      // 主键,自增列
      @PrimaryGeneratedColumn()  
      id: number;  

      // 时间戳,精度为毫秒
      @Column({ type: 'timestamp', precision: 3 })  
      timestamp: Date;  

      // 请求方法,如GET、POST等
      @Column({ type: 'varchar', length: 10 })  
      method: string;  

      // 请求URL
      @Column({ type: 'varchar', length: 512 })  
      url: string;  

      // 请求头,可为空
      @Column({ type: 'text', nullable: true })  
      requestHeaders?: string;  

      // 请求体,可为空
      @Column({ type: 'text', nullable: true })  
      requestBody?: string;  

      // 响应状态码,可为空
      @Column({ type: 'int', nullable: true })  
      responseStatus?: number;  

      // 响应头,可为空
      @Column({ type: 'text', nullable: true })  
      responseHeaders?: string;  

      // 响应体,可为空
      @Column({ type: 'text', nullable: true })  
      responseBody?: string;  

      // 响应时间(毫秒),可为空
      @Column({ type: 'int', nullable: true })  
      responseTimeMs?: number;  

      // 错误信息,可为空
      @Column({ type: 'varchar', length: 255, nullable: true })  
      error?: string;  
    }

日志记录服务仅仅把日志存入数据库并返回保存的记录。

    // src/logger/http-log.service.ts

    import { Injectable } from '@nestjs/common';  
    import { InjectRepository } from '@nestjs/typeorm';  
    import { HttpLogEntity } from 'src/logger/entities/HttpLog.entity';  
    import { Repository } from 'typeorm';  

    @Injectable()  
    export class HttpLogService {  
      // HTTP日志服务的构造函数,注入HttpLogEntity的Repository
      constructor(  
        @InjectRepository(HttpLogEntity)  
        private httpLogRepository: Repository<HttpLogEntity>,  
      ) {}  

      // 异步创建日志记录,参数为部分HttpLogEntity数据,返回HttpLogEntity对象
      async createLog(logData: Partial<HttpLogEntity>): Promise<HttpLogEntity> {  
        const log = this.httpLogRepository.create(logData);  
        return await this.httpLogRepository.save(log);  
      }  
    }
类型扩展(Type Extending)

如果你在使用 JavaScript,可以直接跳到下一节,但如果你在使用 TypeScript,则需要在我们的 src 文件夹内创建一个名为 @types 的目录,并在其中创建一个名为 axios.d.ts 的新文件,这将使我们能够扩展 Axios 配置类型定义并添加所需的元数据。

/src  
├── /@types  
│   ├── axios.d.ts

注:以上为文件结构示意图,文件路径和文件名未进行翻译,因为它们是技术规范。

这种配置允许我们将自定义属性添加到AxiosRequestConfig,从而实现更复杂的请求处理。以下是对每个属性用途的简要说明:

  • logResponse: 一个布尔标志,用来决定是否记录响应。这在省略占用大量空间的大型响应(如 PDF 文件)时很有用。
  • metadata: 一个存储请求生命周期信息的对象,包括:
    • startTime: 请求开始的时间戳。
    • endTime: 请求结束的时间戳。
    • duration: 请求的总耗时(以毫秒为单位),计算为 endTime - startTime
    // src/@types/axios.d.ts  
    import 'axios';  

    // https://stackoverflow.com/questions/76199376/axios-1-4-0-interceptors-how-to-add-a-meta-data-to-a-request  
    declare module 'axios' {  
      export interface AxiosRequestConfig {  
        logResponse?: boolean; // 用于控制日志输出的自定义属性  
        metadata?: {  
          startTime?: number; // 请求开始时间  
          endTime?: number; // 请求结束时间  
          duration?: number; // 请求时长  
        };  
      }  
    }
自定义的 Axios 实例

我们现在可以创建文件,这个文件将包含我们的新 Axios 实例,位于 logger 模块内的 axios-logger.ts

/src  
├── /logger  
│   ├── /classes  
│   │   ├── axios-logger.ts (Axios日志记录器文件)

// 源代码目录结构

我先来展示每个拦截器的具体实现,最后再展示完整的代码。

拦截器

请求拦截器(request interceptor)在每个请求的开始时初始化每个请求的 metadata 属性以记录每个请求的开始时间。这对于后续测量请求持续时间是必不可少的。

// 为axiosLogger添加请求拦截器,记录请求开始时间
axiosLogger.interceptors.request.use(async (config) => {  
  config.metadata = { startTime: Date.now() };  
  return config;  
});
响应拦截

响应拦截器(response interceptor)会计算请求耗时,使用 HttpLogService 记录响应详情,并将响应传递给链中的下一个处理器。

    axiosLogger.interceptors.response.use(  
      async (response) => {  
        // 获取元数据并记录结束时间与持续时间
        const metadata = response.config.metadata;  
        metadata.endTime = Date.now();  
        metadata.duration = metadata.endTime - metadata.startTime;  

        // 创建日志记录
        await httpLogService.createLog({  
          method: response.config.method,  
          url: `${response.config.url}?${new URLSearchParams(response.config.params).toString()}`,  
          requestHeaders: JSON.stringify(response.config.headers),  
          requestBody: JSON.stringify(response.config.data),  
          responseStatus: response.status,  
          responseHeaders: JSON.stringify(response.headers),  
          responseBody:  
            // 如果 logResponse 不是 false,则记录响应体
            response.config.logResponse !== false  
              ? JSON.stringify(response.data)  
              : null,  
          responseTimeMs: metadata.duration,  
          timestamp: new Date(metadata.startTime),  
        });  

        return response;  
      }
错误捕获器

错误捕获器捕获任何失败的请求,记录下错误,并在如果有可用的元数据时计算出时间。最后再把错误传递给调用的代码。

    async (error: AxiosError) => {  
      const 元数据 = error.config.metadata;  
      if (元数据) {  
        元数据.endTime = Date.now();  
        元数据.duration = 元数据.endTime - 元数据.startTime;  
      }  

      await httpLogService.createLog({  
        method: error.config.method,  
        url: `${error.config.url}?${new URLSearchParams(error.config.params).toString()}`,  
        requestHeaders: JSON.stringify(error.config.headers),  
        requestBody: JSON.stringify(error.config.data),  
        responseStatus: error.response?.status,  
        responseHeaders: JSON.stringify(error.response?.headers),  
        responseBody: JSON.stringify(error.response?.data),  
        responseTimeMs: 元数据?.duration,  
        error: error.message,  
        timestamp: new Date(元数据?.startTime || Date.now()),  
      });  

      return Promise.reject(error);  
    }
完整代码
    // src/logger/classes/axios-logger.ts  

    import axios, { AxiosError } from 'axios';  
    import { Http日志服务 } from '../http-log.service';  

    function 创建Axios日志器(Http日志服务: Http日志服务) {  
      const axiosLogger = axios.create();  

      axiosLogger.interceptors.request.use(async (config) => {  
        config.metadata = { startTime: Date.now() };  
        return config;  
      });  

      axiosLogger.interceptors.response.use(  
        async (response) => {  
          const metadata = response.config.metadata;  
          metadata.endTime = Date.now();  
          metadata.duration = metadata.endTime - metadata.startTime;  

          await Http日志服务.createLog({  
            method: response.config.method,  
            url: `${response.config.url}?${new URLSearchParams(response.config.params).toString()}`,  
            requestHeaders: JSON.stringify(response.config.headers),  
            requestBody: JSON.stringify(response.config.data),  
            responseStatus: response.status,  
            responseHeaders: JSON.stringify(response.headers),  
            responseBody:  
              response.config.logResponse !== false  
                ? JSON.stringify(response.data)  
                : null,  
            responseTimeMs: metadata.duration,  
            timestamp: new Date(metadata.startTime),  
          });  

          // 等待Http日志服务创建日志并返回响应
          return response;  
        },  
        async (error: AxiosError) => {  
          const metadata = error.config.metadata;  
          if (metadata) {  
            metadata.endTime = Date.now();  
            metadata.duration = metadata.endTime - metadata.startTime;  
          }  

          await Http日志服务.createLog({  
            method: error.config.method,  
            url: `${error.config.url}?${new URLSearchParams(error.config.params).toString()}`,  
            requestHeaders: JSON.stringify(error.config.headers),  
            requestBody: JSON.stringify(error.config.data),  
            responseStatus: error.response?.status,  
            responseHeaders: JSON.stringify(error.response?.headers),  
            responseBody: JSON.stringify(error.response?.data),  
            responseTimeMs: metadata?.duration,  
            error: error.message,  
            timestamp: new Date(metadata?.startTime || Date.now()),  
          });  

          // 等待Http日志服务创建日志并返回错误
          return Promise.reject(error);  
        },  
      );  

      // 返回创建的axiosLogger
      return axiosLogger;  
    }  

    export default 创建Axios日志器;
模块可用性
模块可用性

(修正后的翻译为:"# 使模块可用",但由于Markdown标题格式要求,这里仍然保留了“# 模块可用性”作为标题,内容部分修改为“使模块可用”。)

使模块可用

在我们能够注入新的 Axios 实例之前,需要将其声明为 NestJS 应用程序中的全局可用提供器。这涉及创建一个工厂来生成自定义的 Axios 实例,并将其从模块中导出,以便整个应用程序都能使用。通过将模块标记为 @Global(),可以确保自定义的 Axios 实例(AXIOS_LOGGER)可以在应用程序的任何地方被注入,而无需再导入模块。

    import { Global, Module } from 'nestjs/common';  
    import { LoggerController } from './logger.controller';  
    import { TypeOrmModule } from 'nestjs/typeorm';  
    import { HttpLogEntity } from './entities/HttpLog.entity';  
    import { HttpLogService } from './http-log.service';  
    import createAxiosLogger from './classes/axios-logger';  

    @Global() // 全局的模块  
    @Module({  
      controllers: [LoggerController],  
      providers: [  
        HttpLogService,  
        {  
          // Axios 日志提供者  
          provide: 'AXIOS_LOGGER',  
          useFactory: (httpLogService: HttpLogService) =>  
            createAxiosLogger(httpLogService),  
          inject: [HttpLogService],  
        },  
      ],  
      imports: [TypeOrmModule.forFeature([HttpLogEntity])],  
      exports: [TypeOrmModule, 'AXIOS_LOGGER'], // 导出提供器  
    })  
    export class LoggerModule {}
使用新的 Axios 日志器

一旦配置好 LoggerModule,你就可以在应用中的任何位置注入 AXIOS_LOGGER,比如在服务或控制器里。

    import { Injectable, Inject } from '@nestjs/common';  
    import { AxiosInstance } from 'axios';  

    @Injectable()  
    export class SomeService {  
      constructor(  
        @Inject('AXIOS_LOGGER') private readonly axiosLogger: AxiosInstance,  
      ) {}  

      async fetchData() {  
        try {  
          const response = await this.axiosLogger.get(  
            'https://jsonplaceholder.typicode.com/posts/1', {  
            // 如果无需记录响应,可以取消注释下面的行:// logResponse: false  
          });  
          return response.data;  
        } catch (error) {  
          console.error('获取数据失败:', error.message);  
          throw error;  
        }  
      }  
    }

通过实现一个带有日志功能的自定义 Axios 实例,我们获得了一个强大的工具来追踪和调试 HTTP 请求的过程。使用 Axios 拦截器提供了一种优雅的方式来捕获元数据,记录请求和响应详情,并有效地处理错误。NestJS 的模块化设计使我们可以将日志记录逻辑集中在专用模块中,使其可重用且易于维护。

此解决方案高度适应性:你可以引入不同的日志策略、数据库等,甚至外部监控服务,以符合项目的需求。这样一来,你可以轻松跟踪API性能,更快地解决故障,并确保应用程序外部互动的责任性。

下一步是

为进一步加强实施:

  1. 集成监控工具: 通过集成像 Prometheus 或者 Elasticsearch 这样的工具,可以实现对日志请求的高级可视化和分析。
  2. 处理较大负载: 实现逻辑来处理或截断大请求和响应负载,以防止数据库存储问题。
  3. 添加更多元数据: 扩展元数据,包含如 IP 地址、用户代理或自定义头等额外信息(如有必要)。
最后的想法

请求日志是构建可靠程序的关键方面,特别是那些依赖第三方API的应用程序。通过这种做法,你为项目打下了更好的调试基础。

如果您有任何问题或想法,欢迎随时留言分享哦。

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP