Files
Messenger/server/internal/platform/metrics/metrics.go
T

128 lines
3.3 KiB
Go
Raw Normal View History

// Package metrics exposes Prometheus instrumentation for the HTTP API
// and the realtime hub.
package metrics
import (
"bufio"
"errors"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "messenger_http_requests_total",
Help: "HTTP requests handled, partitioned by method, normalized path and status.",
},
[]string{"method", "path", "status"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "messenger_http_request_duration_seconds",
Help: "HTTP request latency.",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
wsConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "messenger_ws_connections",
Help: "Current number of open WebSocket connections.",
},
)
buildInfo = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "messenger_build_info",
Help: "Build information.",
},
[]string{"version"},
)
)
// Init registers all custom metrics. The default registry already carries
// the standard Go and process collectors.
func Init(version string) {
prometheus.MustRegister(requestsTotal, requestDuration, wsConnections, buildInfo)
buildInfo.WithLabelValues(version).Set(1)
}
// Handler returns the Prometheus scrape endpoint.
func Handler() http.Handler {
return promhttp.Handler()
}
// RecordWS increments or decrements the WebSocket connection gauge.
func RecordWS(delta float64) {
wsConnections.Add(delta)
}
// Middleware instruments every request with counters and a latency histogram.
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &recorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
path := NormalizePath(r.URL.Path)
requestsTotal.WithLabelValues(r.Method, path, strconv.Itoa(rec.status)).Inc()
requestDuration.WithLabelValues(r.Method, path).Observe(time.Since(start).Seconds())
})
}
type recorder struct {
http.ResponseWriter
status int
}
func (r *recorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
// Hijack forwards the WebSocket upgrade to the underlying writer. Without
// this, wrapping the mux breaks every WebSocket handshake.
func (r *recorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hj, ok := r.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("metrics: response writer does not support hijacking")
}
return hj.Hijack()
}
// Flush forwards periodic flush calls for streaming responses.
func (r *recorder) Flush() {
if f, ok := r.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// NormalizePath replaces numeric path segments with a placeholder so that
// per-route cardinality stays bounded regardless of resource IDs.
func NormalizePath(p string) string {
parts := strings.Split(p, "/")
for i, part := range parts {
if part != "" && isNumeric(part) {
parts[i] = "{id}"
}
}
return strings.Join(parts, "/")
}
func isNumeric(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}