feat: 注册 Prometheus 指标与 Handler
This commit is contained in:
@@ -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
|
||||
|
||||
暂无。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user