feat: 注册 Prometheus 指标与 Handler
This commit is contained in:
@@ -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