【Prometheus】 在 NestJS 多租户项目中集成
metrics。
·
【Prometheus】 在 NestJS 多租户项目中集成
1. 架构概述
- 使用
@willsoto/nestjs-prometheus+prom-client提供 Prometheus 支持。 - 创建全局拦截器,自动收集所有 HTTP 请求指标。
- 通过独立端口暴露
/metrics(支持 PM2 多实例部署)。
2. 指标设计
定义以下三个核心 Prometheus 监控指标:
- 请求延迟(P95 / P99)
- 错误率
- 并发请求数
具体指标类型与名称如下:
- Histogram:
http_request_duration_seconds—— 请求延迟分布(单位:秒) - Gauge:
http_current_requests—— 当前并发请求数 - Counter:
http_requests_total—— 总请求数(含状态码等标签)
3. 实现步骤
3.1 安装依赖
npm install @willsoto/nestjs-prometheus prom-client --save
3.2 创建 Metrics 拦截器
文件路径:server/src/core/metrics.interceptor.ts
功能说明:
- 拦截所有 HTTP 请求。
- 通过
context.getClass().name获取控制器名称。 - 通过
context.getHandler().name获取方法名。 - 自动记录请求总数、延迟和并发数。
代码结构:
/**
* Prometheus 指标收集拦截器
*
* 该拦截器用于自动收集每个 HTTP 请求的以下指标:
* - 总请求数(Counter):按控制器、方法、HTTP 方法、状态码分类
* - 请求延迟(Histogram):记录请求处理耗时(秒),支持 P95/P99 等分位数计算
* - 当前并发请求数(Gauge):实时反映正在处理的请求数量
*
* 所有指标均带有标签(labels):controller(控制器类名)、handler(方法名)、method(HTTP 方法)
*/
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
constructor(
@InjectMetric('http_requests_total') private readonly counter: Counter<string>,
@InjectMetric('http_request_duration_seconds') private readonly histogram: Histogram<string>,
@InjectMetric('http_current_requests') private readonly gauge: Gauge<string>,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 从 ExecutionContext 中提取关键元数据
const controller = context.getClass().name; // 获取控制器类名(如 UserController)
const handler = context.getHandler().name; // 获取处理方法名(如 createUser)
const req = context.switchToHttp().getRequest(); // 获取原始 Express/Fastify Request 对象
const method = req.method; // 获取 HTTP 方法(GET/POST 等)
const startTime = Date.now(); // 记录请求开始时间(毫秒)
// 在请求开始时,增加并发请求数
// 标签组合 (controller, handler, method) 用于区分不同接口的并发情况
this.gauge.inc({ controller, handler, method });
// 使用 RxJS 操作符链式处理请求流
return next.handle().pipe(
tap(() => {
const res = context.switchToHttp().getResponse();
const statusCode = String(res.statusCode);
const duration = (Date.now() - startTime) / 1000;
// 记录成功请求的计数和耗时
this.counter.inc({ controller, handler, method, status_code: statusCode });
this.histogram.observe({ controller, handler, method, status_code: statusCode }, duration);
}),
catchError((err: any) => {
const status = err instanceof HttpException ? err.getStatus() : 500;
const statusCode = String(status);
const duration = (Date.now() - startTime) / 1000;
// 即使出错,也要记录请求(便于监控错误率)
this.counter.inc({ controller, handler, method, status_code: statusCode });
this.histogram.observe({ controller, handler, method, status_code: statusCode }, duration);
return throwError(() => err);
}),
finalize(() => {
// 无论成功或失败,减少并发计数
this.gauge.dec({ controller, handler, method });
})
);
}
}
3.3 修改 AppModule
文件路径:server/src/app.module.ts
操作内容:
-
导入所需模块和拦截器:
import { PrometheusModule, makeCounterProvider, makeHistogramProvider, makeGaugeProvider } from '@willsoto/nestjs-prometheus'; import { MetricsInterceptor } from './core/metrics.interceptor'; -
在
imports中注册PrometheusModule(建议放在ConfigModule之后):PrometheusModule.register({ defaultMetrics: { enabled: true }, // 启用默认系统指标 }),当
defaultMetrics.enabled = true时,prom-client会自动暴露以下进程和系统级别的监控指标(无需手动编写代码):指标示例 说明 process_cpu_user_seconds_total进程用户态 CPU 使用时间(秒) process_cpu_system_seconds_total进程内核态 CPU 使用时间 process_heap_bytesV8 堆内存使用量(字节) process_resident_memory_bytes进程常驻内存(RSS) nodejs_eventloop_lag_secondsNode.js 事件循环延迟 nodejs_active_handles_total当前活跃的 libuv 句柄数 nodejs_active_requests_total当前活跃的 libuv 请求总数 process_start_time_seconds进程启动时间(Unix 时间戳) 💡 这些指标对于监控应用的资源消耗、性能瓶颈、内存泄漏等非常有价值。
-
在
providers中定义指标并注册全局拦截器:makeCounterProvider({ name: 'http_requests_total', help: 'Total number of HTTP requests', labelNames: ['controller', 'handler', 'method', 'status_code'], }), makeHistogramProvider({ name: 'http_request_duration_seconds', help: 'HTTP request duration in seconds', labelNames: ['controller', 'handler', 'method', 'status_code'], buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], }), makeGaugeProvider({ name: 'http_current_requests', help: 'Current number of HTTP requests being processed', labelNames: ['controller', 'handler', 'method'], }), { provide: APP_INTERCEPTOR, useClass: MetricsInterceptor, },
3.4 修改 Main 入口文件
文件路径:server/src/main.ts
功能说明:为 NestJS 应用启动一个独立的 HTTP 服务器,专门用于暴露 Prometheus 监控指标(/metrics 端点),尤其适用于多实例部署(如使用 PM2 集群模式)的场景。
import * as http from 'http';
import { Registry } from 'prom-client';
import { getRegistryToken } from '@willsoto/nestjs-prometheus';
/**
* 在 NestJS 应用启动函数(bootstrap)中,通常位于 app.listen() 之后添加以下逻辑。
* 目的是:启动一个**独立于主应用端口**的 HTTP 服务,专门用于暴露 Prometheus 指标。
* 这样即使主应用因负载过高或 Bug 无法响应,监控指标仍可被采集。
*/
// 获取当前进程实例编号(由 PM2 或其他进程管理器设置)
// 例如:PM2 在 cluster 模式下会自动设置 NODE_APP_INSTANCE=0,1,2...
// 如果未设置,默认为 0(单实例场景)
const instance = Number(process.env.NODE_APP_INSTANCE ?? 0);
// 获取基础指标端口(可通过环境变量配置,便于多实例错开端口)
// 默认为 9100,即第一个实例监听 9100,第二个 9101,依此类推
const metricsBase = Number(process.env.METRICS_BASE ?? 9100);
// 计算当前实例应监听的实际指标端口
// 例如:instance=0 → 9100;instance=1 → 9101
const metricsPort = metricsBase + instance;
// 从 NestJS 的依赖注入容器中获取 Prometheus 指标注册表(Registry)实例
// getRegistryToken 是 @willsoto/nestjs-prometheus 提供的 DI token
// 所有通过 makeCounterProvider 等方式定义的指标都注册在此 Registry 中
const registry = app.get<Registry>(getRegistryToken());
/**
* 创建一个原生 HTTP 服务器,仅处理 /metrics 路径
*/
http.createServer(async (req, res) => {
// 仅当请求路径为 /metrics 时返回指标数据
if (req.url === '/metrics') {
// 设置正确的 Content-Type,Prometheus 要求为文本格式
// registry.contentType 通常是 'text/plain; version=0.0.4; charset=utf-8'
res.setHeader('Content-Type', registry.contentType);
// 异步获取所有指标的文本格式(符合 Prometheus exposition format)
// 并作为响应体返回
res.end(await registry.metrics());
} else {
// 其他路径一律返回 404,避免暴露无关信息
res.statusCode = 404;
res.end('Not Found');
}
})
// 启动该 HTTP 服务器,监听计算出的 metricsPort
.listen(metricsPort, () => {
// 启动成功后打印日志,便于运维确认端口状态
console.log(`Metrics for instance ${instance} listening on :${metricsPort}`);
});
3.5 更新 PM2 配置(可选)
文件路径:server/ecosystem.config.js
适用场景:多实例部署(如使用 PM2 cluster 模式)
module.exports = {
apps: [
{
name: "app", // 应用在 PM2 中的逻辑名称
script: "dist/main.js", // 应用入口文件路径(相对于当前目录)
instances: 2, // 启动的进程实例数量(值为 "max" 则使用所有 CPU)
/**
* exec_mode: 执行模式
* - "cluster":启用 PM2 内置的负载均衡集群模式(推荐)
* 所有实例共享同一端口(如 3000),由 PM2 自动分发请求
* - "fork":每个实例独立运行(不共享端口,需手动管理)
*
* 使用 "cluster" 模式时,主业务端口(如 3000)由 PM2 统一监听,
* 而 Prometheus 指标端口需通过 NODE_APP_INSTANCE 动态错开
*/
exec_mode: "cluster",
env: {
NODE_ENV: "development",
METRICS_BASE: 9100,
},
env_production: {
NODE_ENV: "production",
METRICS_BASE: 9100,
},
},
],
};
4. 关键文件清单
| 文件 | 操作 |
|---|---|
server/src/core/metrics.interceptor.ts |
新建 |
server/src/app.module.ts |
修改 — 添加 PrometheusModule 和指标 Provider |
server/src/main.ts |
修改 — 添加独立 metrics 端口 |
server/ecosystem.config.js |
可选修改 — 配置多实例 |
5. 验证步骤
- 安装依赖后,启动应用。
- 访问
http://localhost:9100/metrics(或对应实例端口)验证指标输出。 - 发起几个 API 请求。
- 再次访问
/metrics,确认http_requests_total包含请求数据。 - 验证指标中包含正确的
controller和handler标签。
6. PromQL 查询示例
6.1 基础请求统计
-
按控制器统计请求总数:
sum by (controller) (http_requests_total) -
按控制器统计 QPS(每秒请求数):
rate(http_requests_total[5m]) -
统计 5xx 错误请求:
sum by (controller) (http_requests_total{status_code=~"5.."})
6.2 请求延迟
-
P95 延迟:
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) -
P99 延迟:
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
6.3 错误率
-
5xx 错误率:
rate(http_requests_total{status_code=~"5.."}[5m]) -
4xx 错误率:
rate(http_requests_total{status_code=~"4.."}[5m])
6.4 并发请求数
- 当前并发请求数:
http_current_requests
更多推荐
所有评论(0)