From bbdd4af66d50b691cbf9ddc232f63167e58d56ff Mon Sep 17 00:00:00 2001 From: Nixevol Date: Wed, 30 Sep 2026 06:53:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B3=A8=E5=86=8C=20Prometheus=20?= =?UTF-8?q?=E6=8C=87=E6=A0=87=E4=B8=8E=20Handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DEVIATIONS.md | 16 +++++ go.mod | 8 +++ go.sum | 34 +++++++++ internal/metrics/metrics.go | 115 +++++++++++++++++++++++++++++++ internal/metrics/metrics_test.go | 58 ++++++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 internal/metrics/metrics.go create mode 100644 internal/metrics/metrics_test.go diff --git a/docs/DEVIATIONS.md b/docs/DEVIATIONS.md index 59a7ed6..ac733f3 100644 --- a/docs/DEVIATIONS.md +++ b/docs/DEVIATIONS.md @@ -221,6 +221,22 @@ - 备选方案:StdEncoding 带 padding。 - 影响:SDK/文档示例需无 `=` 结尾。 +### P4 2026-09-30 + +1. **指标包放在 `internal/metrics`** + - 原条款:TASKS 分工表未列 metrics 目录;P4 要求用 prometheus/client_golang 建注册表。 + - 实际做法:新建 `internal/metrics`,提供 `New`/`Handler`/`Registry` 字段供各线打点;不在 `serve` 挂路由(访问规则属 A 线)。 + - 原因:不宜塞进 auth/store/config。 + - 备选方案:放 `internal/httpx`(A 线目录)。 + - 影响:A/N 接线时 import 本包并挂 `/metrics`。 + +2. **指标命名** + - 原条款:列了指标含义,未规定 Prometheus 名字。 + - 实际做法:`nixmsg_connections{transport}`、`nixmsg_endpoints`、`nixmsg_deliveries_pending`、`nixmsg_messages_scheduled`、`nixmsg_dispatch_to_push_duration_seconds`、`nixmsg_ack_duration_seconds`、`nixmsg_write_queue_length`、`nixmsg_write_batch_commit_duration_seconds`、`nixmsg_password_hash_queue_length`、`nixmsg_errors_total{code}`。 + - 原因:固定可抓取文本便于联调。 + - 备选方案:更短前缀或 HistogramVec。 + - 影响:仪表盘按上述名字配置。 + ## 连接 N 暂无。 diff --git a/go.mod b/go.mod index c544a92..c87fdec 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,26 @@ module git.asio.asia/nixevol/NixMsg go 1.27 require ( + github.com/prometheus/client_golang v1.24.1 go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.57.0 modernc.org/sqlite v1.60.1 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.24 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.70.1 // indirect + github.com/prometheus/procfs v0.21.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/sys v0.48.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect modernc.org/libc v1.77.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.12.1 // indirect diff --git a/go.sum b/go.sum index e8dd51d..eceed2a 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,47 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= @@ -24,6 +54,10 @@ golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg= modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE= diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..a3347d6 --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,115 @@ +package metrics + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// Registry 持有 NixMsg 指标与独立注册表(避免污染默认全局注册表)。 +type Registry struct { + reg *prometheus.Registry + + Connections *prometheus.GaugeVec + EndpointsTotal prometheus.Gauge + DeliveriesPending prometheus.Gauge + MessagesScheduled prometheus.Gauge + DispatchToPushSeconds prometheus.Observer + AckSeconds prometheus.Observer + WriteQueueLength prometheus.Gauge + WriteCommitSeconds prometheus.Observer + PasswordHashQueue prometheus.Gauge + ErrorsTotal *prometheus.CounterVec +} + +// New 按 DEVELOPMENT 4.3 注册指标。 +func New() *Registry { + reg := prometheus.NewRegistry() + r := &Registry{reg: reg} + + r.Connections = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "nixmsg_connections", + Help: "Current online connections by transport (ws or tcp).", + }, []string{"transport"}) + + r.EndpointsTotal = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "nixmsg_endpoints", + Help: "Total number of endpoints.", + }) + + r.DeliveriesPending = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "nixmsg_deliveries_pending", + Help: "Number of pending deliveries.", + }) + + r.MessagesScheduled = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "nixmsg_messages_scheduled", + Help: "Number of scheduled messages not yet dispatched.", + }) + + dispatchHist := prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "nixmsg_dispatch_to_push_duration_seconds", + Help: "Latency from message due time to push.", + Buckets: prometheus.DefBuckets, + }) + r.DispatchToPushSeconds = dispatchHist + + ackHist := prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "nixmsg_ack_duration_seconds", + Help: "Latency from push to ack.", + Buckets: prometheus.DefBuckets, + }) + r.AckSeconds = ackHist + + r.WriteQueueLength = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "nixmsg_write_queue_length", + Help: "Number of write operations waiting or in the current batch.", + }) + + commitHist := prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "nixmsg_write_batch_commit_duration_seconds", + Help: "Duration of each merged write-queue commit.", + Buckets: prometheus.DefBuckets, + }) + r.WriteCommitSeconds = commitHist + + r.PasswordHashQueue = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "nixmsg_password_hash_queue_length", + Help: "Number of password hash jobs waiting for a pool slot.", + }) + + r.ErrorsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "nixmsg_errors_total", + Help: "Count of application error codes.", + }, []string{"code"}) + + reg.MustRegister( + r.Connections, + r.EndpointsTotal, + r.DeliveriesPending, + r.MessagesScheduled, + dispatchHist, + ackHist, + r.WriteQueueLength, + commitHist, + r.PasswordHashQueue, + r.ErrorsTotal, + ) + + // 初始化 transport 标签,便于空载抓取也能看到序列。 + r.Connections.WithLabelValues("ws").Set(0) + r.Connections.WithLabelValues("tcp").Set(0) + + return r +} + +// Handler 返回 Prometheus 文本格式的 /metrics 处理函数(访问规则由 A 线加)。 +func (r *Registry) Handler() http.Handler { + return promhttp.HandlerFor(r.reg, promhttp.HandlerOpts{}) +} + +// Gatherer 暴露底层 Gatherer(测试用)。 +func (r *Registry) Gatherer() prometheus.Gatherer { + return r.reg +} diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go new file mode 100644 index 0000000..281b58a --- /dev/null +++ b/internal/metrics/metrics_test.go @@ -0,0 +1,58 @@ +package metrics + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestMetricsHandlerExposesText(t *testing.T) { + t.Parallel() + m := New() + m.Connections.WithLabelValues("ws").Set(3) + m.Connections.WithLabelValues("tcp").Set(2) + m.EndpointsTotal.Set(10) + m.DeliveriesPending.Set(4) + m.MessagesScheduled.Set(1) + m.DispatchToPushSeconds.Observe(0.05) + m.AckSeconds.Observe(0.02) + m.WriteQueueLength.Set(7) + m.WriteCommitSeconds.Observe(0.001) + m.PasswordHashQueue.Set(2) + m.ErrorsTotal.WithLabelValues("busy").Inc() + + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) + rec := httptest.NewRecorder() + m.Handler().ServeHTTP(rec, req) + resp := rec.Result() + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + t.Fatalf("status=%d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + text := string(body) + for _, want := range []string{ + "nixmsg_connections", + `transport="ws"`, + `transport="tcp"`, + "nixmsg_endpoints", + "nixmsg_deliveries_pending", + "nixmsg_messages_scheduled", + "nixmsg_dispatch_to_push_duration_seconds", + "nixmsg_ack_duration_seconds", + "nixmsg_write_queue_length", + "nixmsg_write_batch_commit_duration_seconds", + "nixmsg_password_hash_queue_length", + "nixmsg_errors_total", + `code="busy"`, + } { + if !strings.Contains(text, want) { + t.Fatalf("metrics text missing %q\n%s", want, text) + } + } +}