fix(telemetry): 指标写入对空接收者安全,修 nil 解引用 panic

metrics 包的全局指标(UpstreamTotal 等)默认是 nil,只有 main 调过 metrics.Init() 之后才有值。任何不经过 main 的调用路径都会在 controller/tts.go -> adapter/volcano/synthesis.go -> metrics.AdapterRecorder 处 nil 解引用,整个进程崩掉。

生产路径本来没问题(二进制一定先 Init),但这类崩溃只在运行时暴露,且漏掉指标初始化属于很容易踩的装配错误,不该以 panic 收场。

改动: Counter.Add / Gauge.Set / Gauge.Add / Histogram.Observe 增加空接收者判断,nil 时静默忽略(与 noop 语义一致);在包注释里写成显式设计约定,避免以后被当成冗余代码删掉。
This commit is contained in:
sun
2026-10-03 16:13:47 +08:00
parent 21e1fd1fbb
commit 1e2be0cd5b
4 changed files with 23 additions and 2 deletions
+5 -1
View File
@@ -36,8 +36,12 @@ func newCounter(name, help string, labelNames []string) *Counter {
func (c *Counter) Inc(labels Labels) { c.Add(1, labels) } func (c *Counter) Inc(labels Labels) { c.Add(1, labels) }
// Add 累加 v(v 必须 >= 0)。 // Add 累加 v(v 必须 >= 0)。
//
// 空接收者(nil *Counter)安全:未初始化的全局指标(如 metrics.Init() 尚未调用时
// 的 metrics.UpstreamTotal)会被静默忽略,而不是 panic。
// 这让"直接调用 handler"的测试/复用场景不会因为漏掉指标初始化而崩溃。
func (c *Counter) Add(v float64, labels Labels) { func (c *Counter) Add(v float64, labels Labels) {
if v < 0 { if c == nil || v < 0 {
return return
} }
child := c.getOrCreate(labels) child := c.getOrCreate(labels)
+8
View File
@@ -33,7 +33,11 @@ func newGauge(name, help string, labelNames []string) *Gauge {
} }
// Set 直接设置当前值。 // Set 直接设置当前值。
// 空接收者(nil *Gauge)安全,见 Counter.Add 的说明。
func (g *Gauge) Set(v float64, labels Labels) { func (g *Gauge) Set(v float64, labels Labels) {
if g == nil {
return
}
child := g.getOrCreate(labels) child := g.getOrCreate(labels)
child.bits.Store(float64bits(v)) child.bits.Store(float64bits(v))
} }
@@ -45,7 +49,11 @@ func (g *Gauge) Inc(labels Labels) { g.Add(1, labels) }
func (g *Gauge) Dec(labels Labels) { g.Add(-1, labels) } func (g *Gauge) Dec(labels Labels) { g.Add(-1, labels) }
// Add 累加 v(可负)。 // Add 累加 v(可负)。
// 空接收者(nil *Gauge)安全,见 Counter.Add 的说明。
func (g *Gauge) Add(v float64, labels Labels) { func (g *Gauge) Add(v float64, labels Labels) {
if g == nil {
return
}
child := g.getOrCreate(labels) child := g.getOrCreate(labels)
for { for {
bits := child.bits.Load() bits := child.bits.Load()
+4
View File
@@ -47,7 +47,11 @@ func newHistogram(name, help string, buckets []float64, labelNames []string) *Hi
} }
// Observe 记录一个观测值。 // Observe 记录一个观测值。
// 空接收者(nil *Histogram)安全,见 Counter.Add 的说明。
func (h *Histogram) Observe(v float64, labels Labels) { func (h *Histogram) Observe(v float64, labels Labels) {
if h == nil {
return
}
child := h.getOrCreate(labels) child := h.getOrCreate(labels)
for { for {
bits := child.sumBits.Load() bits := child.sumBits.Load()
+6 -1
View File
@@ -5,7 +5,12 @@
// - 零外部依赖,只使用标准库; // - 零外部依赖,只使用标准库;
// - label key 在指标注册时锁定,运行期不可新增(避免 cardinality 爆炸); // - label key 在指标注册时锁定,运行期不可新增(避免 cardinality 爆炸);
// - 所有并发安全由实现保证,调用方无需加锁; // - 所有并发安全由实现保证,调用方无需加锁;
// - Meter 是高层入口,NoopMeter 用于测试。 // - Meter 是高层入口,NoopMeter 用于测试;
// - **空接收者安全**: 未初始化的 *Counter / *Gauge / *Histogram(即 nil 指针)
// 上的 Inc/Add/Set/Observe 一律静默忽略,不 panic。
// 这条约定是为 metrics 包的全局指标变量服务的 —— 它们默认为 nil,只有 main
// 调用过 metrics.Init() 之后才有值;直接复用 handler 的场景(集成测试、
// 未来做成库)不应该因为漏掉指标初始化而崩掉整个请求处理。
package telemetry package telemetry
import ( import (