应使用装饰器函数封装prometheus指标采集逻辑,全局注册指标变量并在拦截器中仅负责埋点;避免重复注册、高基数标签和错误采样,核心指标控制在3–5个。

gRPC服务端拦截器里怎么加Prometheus指标
直接在每个 UnaryServerInterceptor 或 StreamServerInterceptor 里手动调用 prometheus.CounterVec.Inc() 不现实,也违背单一职责。必须用装饰器函数封装指标采集逻辑,让拦截器只负责“埋点”,不负责指标定义和注册。
关键点:指标注册必须在服务启动前完成(比如 init() 或 main() 开头),而采集逻辑要绑定到具体 RPC 方法名、状态码、延迟等维度。常见错误是把 prometheus.NewCounterVec() 放在拦截器内部——会导致重复注册 panic。
- 定义全局指标变量,如
grpcServerHandledCounter和grpcServerHandledHistogram,在包初始化时注册 - 拦截器函数接收这些指标变量作为参数,或通过闭包捕获,避免全局依赖污染
- 从
info.FullMethod解析服务名和方法名,用作标签值;status.Code()转字符串填入code标签 - 延迟用
time.Since(start)计算,单位设为秒(Seconds()),直传给Histogram.Observe()
为什么不能直接用 prometheus.UnaryServerInterceptor
官方 prometheus.UnaryServerInterceptor 是个空壳——它根本不存在。Prometheus 官方 Go SDK 不提供 gRPC 拦截器实现,社区常见误以为有现成封装,结果查文档发现只有 promhttp 和基础指标类型。
真正可用的是 github.com/grpc-ecosystem/go-grpc-prometheus,但它已归档,且默认指标命名(如 grpc_server_handled_total)与 Prometheus 最佳实践(推荐 grpc_server_handled_total 带 grpc_method、grpc_code 标签)一致,但它的拦截器不支持自定义标签或采样逻辑,扩展性差。
- 如果你需要按业务模块过滤方法、跳过健康检查接口(如
/grpc.health.v1.Health/Check),必须自己写拦截器 - 它的
EnableHandlingTimeHistogram()默认用毫秒桶,若你已有统一秒级 SLA 看板,需重定义Buckets避免数据错位 - 它不暴露
Observer接口,无法对接 OpenTelemetry 的 trace ID 关联,调试链路时指标和日志对不上
装饰器函数怎么写才够灵活
所谓“装饰器”,本质是返回拦截器函数的高阶函数,接受指标、过滤条件、采样率等参数,返回可直接传给 grpc.Server 的 UnaryServerInterceptor。
示例签名:func NewPrometheusUnaryInterceptor(counter *prometheus.CounterVec, hist *prometheus.HistogramVec, skip func(string) bool, sampleRate float64) grpc.UnaryServerInterceptor。这样调用时能自由组合:
srv := grpc.NewServer(
grpc.UnaryInterceptor(NewPrometheusUnaryInterceptor(
grpcServerHandledCounter,
grpcServerHandledHistogram,
func(fullMethod string) bool { return strings.HasPrefix(fullMethod, "/health.") },
0.1, // 10% 采样率,降低高频接口指标写入压力
)),
)
-
skip函数比硬编码路径前缀更安全,避免正则误杀或漏掉新接口 -
sampleRate对counter.Inc()做概率调用,但hist.Observe()必须全量——延迟统计不能采样 - 装饰器内部用
ctx.Value()提取 trace ID 并打到日志,但不要往 metrics 标签里塞,否则 cardinality 爆炸
指标 label 设计容易踩的坑
gRPC 方法名(FullMethod)直接当 label 值会引发高基数问题,比如带用户 ID 的路径 /user.UserService/GetById?id=123 —— 实际上 FullMethod 是 /user.UserService/GetById,不包含 query,这点没问题;但若前端拼了动态路由如 /v1/order/{id}/status,gRPC 本身不解析 path param,所以不会出问题。
真正危险的是把 err.Error() 当 grpc_code 标签——它可能含敏感信息或堆栈,且不同错误文本导致 label 组合爆炸。必须用 status.Convert(err).Code() 转标准 codes.Code 枚举。
-
grpc_service标签建议从FullMethod提取第一个/后、第二个/前的部分(如user.UserService→user),避免服务名过长 - 不要给每个指标都加
instance或job标签,由 Prometheus server 通过relabel_configs注入更可控 - 如果用了 Istio,注意
grpc_code可能被 sidecar 覆盖为Unavailable,需在拦截器里优先读取原始 context 中的状态
指标不是越多越好,一个服务通常只需 3–5 个核心指标:请求数、错误数、P99 延迟、活跃连接数、流式连接数。其余靠日志 + trace 补充,别让 Prometheus 成为性能瓶颈。











