Files
opsx c44ffad61f fix(metrics): duplicate metrics collector registration panic(线上 CrashLoopBackOff)
=== 现象 ===
上一提交 b49677f 部署后平台直接 CrashLoopBackOff(5 次重启):
    Initializing database...
    panic: duplicate metrics collector registration attempted
    lokistack/pkg/metrics.Register.func1()  metrics.go:125

=== 根因 ===
prometheus/client_golang 在 init() 里已经把 GoCollector 与 ProcessCollector
注册进 **默认 registry**(prometheus.DefaultRegisterer)。我又对同一默认 registry
调用 MustRegister 注册了同一批 collector → 重复注册 → 运行时 panic。

致命点:go build / go vet / 普通单测**全都发现不了** ——
MustRegister 的重复检查只在运行期执行,只有真的调用 Register() 才会炸。
所以整个 P0 打磨在测试阶段是"全绿"的,一上线就挂。

=== 修法 ===
不再碰默认 registry,改用自有实例:

    var registry = prometheus.NewRegistry()      // 独立,不受 init() 影响
    registry.MustRegister(...)                    // 注册全部指标
    promhttp.HandlerFor(registry, ...)            // 抓取读自有 registry

好处:完全掌控注册内容,未来再加 collector 不会与库的默认行为冲突。

=== 回归测试(这才是关键收获)===
新增 backend/pkg/metrics/metrics_test.go:
  - TestRegisterIsIdempotent   : 连续调用 Register 两次,必须不 panic
  - TestHandlerExposesCustomMetrics : 断言抓取输出含 lokistack_* 与 go_goroutines
  - TestRegisterConcurrent     : 20 个 goroutine 并发调用不 panic
这三条在修复前必然失败,修复后全绿。以后再改指标定义会被它们拦住。

镜像:lghost/lokistack:latest + v0.1 = 2f8f3c85643e
2026-10-28 21:32:36 +00:00

68 lines
1.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package metrics
import (
"io"
"net/http/httptest"
"strings"
"sync"
"testing"
)
// 回归:Register 必须幂等。
// 线上事故:client_golang 在 init() 里已把 GoCollector / ProcessCollector
// 注册进默认 registry,我们又 MustRegister 到默认 registry → 进程启动即
// panic: duplicate metrics collector registration attempted。
// 这个 bug 编译期和普通单测都发现不了(只有真的调用 Register 才炸),
// 所以这里必须实际调用两次。
func TestRegisterIsIdempotent(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("Register 触发 panic(重复注册):%v", r)
}
}()
Register("test")
Register("test") // 第二次必须安全返回
}
// 抓取端点必须能输出我们自己定义的指标
func TestHandlerExposesCustomMetrics(t *testing.T) {
Register("test")
ObserveHTTP("GET", "/api/v1/deployments", 200, 0.01)
ObserveDBSave(0.002, true)
ObserveAlertEvaluation(0.3, true)
ObserveLokiQuery(true)
DBRecordsTotal.WithLabelValues("deployments").Set(3)
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics", nil)
Handler().ServeHTTP(rec, req)
body, _ := io.ReadAll(rec.Body)
out := string(body)
for _, want := range []string{
"lokistack_http_requests_total",
"lokistack_db_save_duration_seconds",
"lokistack_alert_evaluations_total",
"lokistack_loki_queries_total",
"lokistack_db_records",
"go_goroutines", // GoCollector 生效
} {
if !strings.Contains(out, want) {
t.Fatalf("抓取结果缺少指标 %q", want)
}
}
}
// 并发调用不能 panic(sync.Once 保护)
func TestRegisterConcurrent(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
Register("test")
}()
}
wg.Wait()
}