最轻量靠谱方式是用prometheus-cpp库配合libmicrohttpd暴露/metrics端点,需设Content-Type为text/plain; version=0.0.4,指标用全局静态变量注册,Counter仅用于单调递增量,避免手写文本格式。

用 cpp-prometheus 库快速暴露 /metrics 端点
直接上结论:C++ 微服务想暴露 Prometheus 兼容的 /metrics,最轻量、最靠谱的方式是用 cpp-prometheus(官方推荐的 C++ 客户端),配合一个极简 HTTP 服务器(比如 libmicrohttpd 或 crow)来响应 GET 请求。别自己拼字符串写指标格式——Prometheus 对换行、类型声明、空格、# HELP 注释都有严格要求,手写极易出错且后续难维护。
实操建议:
- 优先选
prometheus-cpp的pull模式(即服务启动后监听一个端口,等待 Prometheus 主动拉取),不是push模式(推送到 Pushgateway)——微服务场景下拉取更符合可观测性最佳实践 - 避免用 Boost.Beast 或 POCO 自己实现完整 HTTP server 来服务
/metrics,除非你已有一套成熟网络栈;libmicrohttpd只需 10 行代码就能起一个只响应/metrics的只读 endpoint - 注意
prometheus-cpp的构建方式:它默认用shared方式编译,但若你的微服务是静态链接(如 Alpine 镜像中),需显式加-DBUILD_SHARED_LIBS=OFF并链接prometheus-cpp-core和prometheus-cpp-pull
libmicrohttpd 怎么只处理 /metrics 请求并返回指标文本
这是最容易卡住的一环:很多人以为只要把 Collect() 结果塞进 HTTP 响应体就行,但漏了关键细节——Content-Type 必须是 text/plain; version=0.0.4,否则 Prometheus 抓取会报 invalid content type 错误。
示例片段(C 风格,但 C++ 项目里调用完全没问题):
int metrics_handler(void *cls, struct MHD_Connection *connection,
const char *url, const char *method,
const char *version, const char *upload_data,
size_t *upload_data_size, void **con_cls) {
if (strcmp(url, "/metrics") != 0 || strcmp(method, "GET") != 0)
return MHD_NO;
<p>auto collector = prometheus::Registry::Instance().Collect();
std::string body = prometheus::TextSerializer().Serialize(collector);</p><p>struct MHD_Response <em>response = MHD_create_response_from_buffer(
body.size(), (void</em>)body.c_str(), MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(response, "Content-Type", "text/plain; version=0.0.4");</p><p>int ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
MHD_destroy_response(response);
return ret;
}</p>
常见错误:
- 忘了设
Content-Type,或写成text/plain(缺 version 参数) - 用
MHD_RESPMEM_MUST_FREE但没保证body生命周期覆盖整个响应过程——改用MHD_RESPMEM_MUST_COPY更安全 - 在 handler 里反复调用
Collect()却没注意线程安全:默认 registry 是全局单例,Collect()是线程安全的,但如果你手动创建了多个 registry,就得自己加锁
如何注册业务指标(counter/gauge/histogram)并确保进程重启后不丢值
Counter 和 Gauge 的值默认存在内存里,进程重启就归零——这对监控曲线来说是灾难性的。Prometheus 本身不负责持久化,所以你要做的是:让指标“语义正确”,而不是“数值不断增长”。
关键原则:
-
Counter只用于真正单调递增的量(如请求总数、错误总数),不要用它记“当前在线用户数”这种可增可减的值——那是Gauge的职责 - 如果必须记录重启不丢的累计值(比如磁盘写入总字节数),得靠外部存储(如本地文件 + 启动时读取)+ 初始化逻辑,
prometheus-cpp不提供该能力 -
Histogram的 bucket 边界必须在构造时固定,不能运行时改;且注意Observe()调用开销略高,高频路径(如每毫秒调用)要考虑采样或降频 - 所有指标对象(
Counter、Gauge等)应定义为全局或静态生命周期变量,避免函数内创建导致多次注册同名指标而 panic
示例注册:
auto& registry = prometheus::Registry::Instance();
auto& http_requests_total = BuildCounter()
.Name("http_requests_total")
.Help("Total number of HTTP requests.")
.Labels({{"app", "my-service"}})
.Register(registry);
<p>// 在请求处理逻辑里:
http_requests_total.Add({{"method", "POST"}, {"code", "200"}}, 1);</p>
Docker/K8s 下暴露 /metrics 端点要注意什么
本地跑通不等于线上可用。K8s 中 Service 或 Ingress 默认不会把 /metrics 路径透传给 Pod,容易误以为“端点没生效”。
检查点:
- Pod 内应用是否真的监听了
0.0.0.0:9090(或其他你指定的端口),而不是127.0.0.1:9090——后者容器内能 curl 通,但 K8s probe 或 Prometheus 抓取会超时 - K8s Service 的
targetPort必须和容器内监听端口一致;若用 PodMonitor/ServiceMonitor,确认path字段明确写了/metrics(默认是/metrics,但显式写出来防歧义) - SecurityContext 若限制了
NET_BIND_SERVICE,而你的服务又尝试 bind 到 1024 以下端口(如 80),会失败——直接用 9090、8080 这类高位端口更省事 - 某些 Istio sidecar 默认拦截所有出/入流量,但可能没放行
/metrics路径;临时验证可先禁用 sidecar,或在 DestinationRule 中加匹配规则
最后提醒一句:指标命名别用驼峰(如 httpRequestCount),统一用下划线分隔(http_request_count),这是 Prometheus 社区约定,也方便 Grafana 查询时自动补全。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











