prom-client — Node 进程内的 Prometheus 指标客户端
已复核prom-client 是 Node.js 里把进程内数字暴露给 Prometheus 的客户端。日常类比:厨房只在黑板上用正字记“又出了一盘”,巡查员来了才把整块板誊成报表。
你写:
const client = require('prom-client');const requests = new client.Counter({ name: 'http_requests_total', help: '请求总数',});requests.inc();Prometheus 来抓时,调用 await client.register.metrics(),Registry 才把内存里的 Counter / Gauge / Histogram / Summary 序列化成文本。库不绑定 Express / Fastify;/metrics 路由要自己挂。
不理解 prom-client,下面这些事都没法解释:
- 为什么 Node 监控示例几乎都
require('prom-client'),却还要自己写 scrape handler - 为什么拉取模型能把 IO 从业务 hot path 拿走
- 为什么 cluster 下只
listen在 worker 上时,p99 会漂 - 为什么“零依赖、只做
+= 1”已经不是 15.1.3 的事实
固定 15.1.3 可以拆成五步:
-
四种类型:Counter 只增;Gauge 可
set/inc/dec;Histogram 按预设 bucket 观察;Summary 在进程内用 tdigest 算分位。构造必须有name与help,默认登记到Registry.globalRegistry。 -
写路径改哈希表:
counter.inc(labels, value)先hashObject+validateLabel,再hashMap[hash].value += value。负增量直接抛错。Histogram 对默认 11 个上界做线性findBound,只给命中桶+= 1,导出时再累加成 cumulative。 -
读路径才拼字符串:
Registry.metrics()对全部 metricPromise.all,再输出# HELP/# TYPE。默认 content type 是 Prometheus text0.0.4;OpenMetrics 要改 Registry。 -
默认指标按 scrape 采集:
collectDefaultMetrics()立刻注册采集器;CPU / 堆 / 事件循环等数字在metrics()/collect()时更新,不是另起一个业务 interval。 -
cluster 才聚合:
AggregatorRegistry.clusterMetrics()给每个 connected worker 发 IPC,5 秒超时后sum(可改min/max/average/first/omit)。该 revision 没有worker_threads聚合入口。
案例 1:Express 上 Counter + Histogram
Section titled “案例 1:Express 上 Counter + Histogram”const express = require('express');const client = require('prom-client');
const reqs = new client.Counter({ name: 'http_requests_total', help: '总请求数', labelNames: ['method', 'status'],});const lat = new client.Histogram({ name: 'http_request_duration_seconds', help: '请求耗时', labelNames: ['method'],});
const app = express();app.use((req, res, next) => { const end = lat.startTimer({method: req.method}); res.on('finish', () => { reqs.inc({method: req.method, status: String(res.statusCode)}); end(); }); next();});app.get('/metrics', async (_req, res) => { res.type(client.register.contentType); res.send(await client.register.metrics());});label 只用有限枚举。startTimer 内部是 process.hrtime(),观察值以秒写入 Histogram。
案例 2:队列的 Gauge + Counter
Section titled “案例 2:队列的 Gauge + Counter”const inFlight = new client.Gauge({name: 'queue_inflight', help: '在跑任务数'});const done = new client.Counter({ name: 'queue_done_total', help: '完成数', labelNames: ['result'],});
async function consume(job) { inFlight.inc(); try { await handle(job); done.inc({result: 'ok'}); } catch { done.inc({result: 'fail'}); } finally { inFlight.dec(); }}Gauge 看此刻堵不堵,Counter 看累计吞吐。两者不能互相替代。
案例 3:cluster 聚合要两边都构造 AggregatorRegistry
Section titled “案例 3:cluster 聚合要两边都构造 AggregatorRegistry”const cluster = require('cluster');const client = require('prom-client');const aggregator = new client.AggregatorRegistry();
if (cluster.isPrimary) { for (let i = 0; i < 4; i++) cluster.fork(); require('http').createServer(async (_req, res) => { res.setHeader('Content-Type', aggregator.contentType); res.end(await aggregator.clusterMetrics()); }).listen(3001);} else { new client.Counter({name: 'worker_jobs_total', help: '任务数'}).inc();}官方 example/cluster.js 把 new AggregatorRegistry() 放在 isMaster 判断之前,让 worker 重跑同一入口时也安装 process.on('message')。只在主进程 new、worker 只 new Counter,主进程会等到 5 秒超时。
-
高基数 label:
user_id/ 完整req.path会按值复制 time series。label 只放 method / status / 有限 route。 -
默认 Histogram 桶到 10 秒:默认
[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]。秒级离线任务会堆进导出时的+Inf。用linearBuckets/exponentialBuckets另设。 -
worker 没装 IPC 监听:
addListeners()只在AggregatorRegistry构造函数里调用。该 revision 不聚合worker_threads。 -
hot path 里
new Counter:同名重复registerMetric会抛错。metric 必须在模块顶层建一次。 -
把默认输出当成 OpenMetrics:默认是 Prometheus
0.0.4。exemplar 还要求 OpenMetrics registry,否则构造期抛 TypeError。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 已用 Prometheus 拉取模型的 Node 服务
- 需要 Counter / Gauge / Histogram / Summary 这四种进程内账本
cluster多进程要把 worker 数字合成一份 scrape
不适用:
- 非 Prometheus 生态的推送 agent → 用对应 SDK;
Pushgateway只补短任务 - 要 traces / logs / metrics 统一管道 → 这是 metric client,不是完整 OTel SDK
- Node 10/12/14 →
engines是^16 || ^18 || >=20 - 需要线程级聚合 → 固定 15.1.3 没有
worker_threads入口
固定版本边界
Section titled “固定版本边界”- 本文绑定
siimon/prom-client@c1d76c5d...,tag 与 package 均为15.1.3。 - 运行时依赖是
@opentelemetry/api(默认 CPU 指标的 exemplar 读当前 span)和tdigest(Summary)。 - 默认聚合器是
sum;clusterMetrics()超时 5 秒。 - 本文未安装依赖、未 scrape、未跑 Jest / cluster 示例,状态保持
UNVERIFIED。
- 指标是有限分类的数,不是日志——hot path 改哈希表,scrape 才负责文本与 IO。
- “只 += 1”是教学简化——真实写路径还有 label hash、校验和 Histogram 分桶。
- 拉取模型把网络责任交给抓取方——client 可以不连 Prometheus。
- 多进程聚合是显式协议——要构造
AggregatorRegistry、约定 IPC 消息名,不能指望 worker 自动汇总。
counter.inc(-1)在固定 15.1.3 会怎样?- 只在
cluster.isPrimary里new AggregatorRegistry(),worker 只new Counter()。clusterMetrics()默认会怎样? - 新建 Histogram 后
observe(12)(秒)。默认桶里哪一个会被+= 1?导出时+Inf是什么?
检查点:
- 抛错:Counter 不能减少。
- worker 没有 IPC 监听,主进程约 5 秒后超时。
- 12 > 10,没有有限桶被命中;
+Inf等于该 label 组合的count。
- 固定源码:siimon/prom-client —— 本文绑定提交
c1d76c5d497ef803f6bd90c56c713c3fa811c3e0 - 上游类型:Prometheus Metric Types
- 文本格式:OpenMetrics(需显式切换 Registry content type)
- grafana —— scrape 之后的可视化层
- express —— 最常见的手写
/metrics宿主
- grafana —— Prometheus 抓到的数据最终在这里画图
- express —— 常见 Web 宿主;本库不内置中间件
- fastify —— 社区 plugin 常包一层本库
- nginx —— 拉取链路上常见的反代,exporter 思路同类
- redis —— 连接池 / 命中率常用本库暴露
- kafka —— Node 客户端常自带 prom-client 集成
(暂无反向链接)