照片由 Campaign Creators 在 Unsplash 拍摄
问题所在最近,我在整合不同提供商的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性能,更快地解决故障,并确保应用程序外部互动的责任性。
下一步是为进一步加强实施:
- 集成监控工具: 通过集成像 Prometheus 或者 Elasticsearch 这样的工具,可以实现对日志请求的高级可视化和分析。
- 处理较大负载: 实现逻辑来处理或截断大请求和响应负载,以防止数据库存储问题。
- 添加更多元数据: 扩展元数据,包含如 IP 地址、用户代理或自定义头等额外信息(如有必要)。
请求日志是构建可靠程序的关键方面,特别是那些依赖第三方API的应用程序。通过这种做法,你为项目打下了更好的调试基础。
如果您有任何问题或想法,欢迎随时留言分享哦。