【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

操作内容

  1. 导入所需模块和拦截器:

    import { PrometheusModule, makeCounterProvider, makeHistogramProvider, makeGaugeProvider } from '@willsoto/nestjs-prometheus';
    import { MetricsInterceptor } from './core/metrics.interceptor';
    
  2. 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_bytes V8 堆内存使用量(字节)
    process_resident_memory_bytes 进程常驻内存(RSS)
    nodejs_eventloop_lag_seconds Node.js 事件循环延迟
    nodejs_active_handles_total 当前活跃的 libuv 句柄数
    nodejs_active_requests_total 当前活跃的 libuv 请求总数
    process_start_time_seconds 进程启动时间(Unix 时间戳)

    💡 这些指标对于监控应用的资源消耗、性能瓶颈、内存泄漏等非常有价值。

  3. 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. 验证步骤

  1. 安装依赖后,启动应用。
  2. 访问 http://localhost:9100/metrics(或对应实例端口)验证指标输出。
  3. 发起几个 API 请求。
  4. 再次访问 /metrics,确认 http_requests_total 包含请求数据。
  5. 验证指标中包含正确的 controllerhandler 标签。

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
    
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐