Files
Warp/server/internal/metrics/metrics.go
T
tajniak81andClaude Opus 5 b4fb60a8ab
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Phase 0: bootstrap the Warp homeserver
Scaffolds the server module and everything Phase 0 of PLAN.md calls for:
a binary that boots, serves its discovery endpoints and nothing else yet.

- config: one YAML file plus per-field WARP_* environment overrides,
  unknown fields rejected, validation that reports every problem at once
- logging and metrics: slog, a Prometheus registry, per-route HTTP
  instrumentation labelled by pattern rather than path
- httputil: the Matrix error envelope, error-returning handlers, CORS,
  request logging, panic recovery, and a router over http.ServeMux
- endpoints: /_matrix/client/versions, /.well-known/matrix/client,
  /.well-known/matrix/server (gated), /healthz, /metrics, and
  M_UNRECOGNIZED for everything else
- storage: driver registry, shared database/sql base, checksummed
  migration runner, sqlite and postgres drivers, pocketbase stub
- settings: server_settings table behind a cached service, so the
  federation and registration switches change without a restart
- federation: the gate that makes federation a runtime mode
- cmd/warp: serve, migrate, generate-keys, version
- Makefile, golangci-lint v2, GitHub Actions CI (Linux and Windows tests,
  race on Linux, storage tests against PostgreSQL, lint, boot smoke test),
  Dockerfile, example config and docs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 17:01:13 +02:00

112 lines
3.7 KiB
Go

// Package metrics owns the Prometheus registry and the HTTP instrumentation.
package metrics
import (
"net/http"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/dariuszjaniak/warp/server/internal/buildinfo"
"github.com/dariuszjaniak/warp/server/internal/httputil"
)
// Namespace prefixes every metric Warp exports.
const Namespace = "warp"
// Registry is a Prometheus registry plus the collectors Warp always exports.
// Components register their own collectors on it at construction time.
type Registry struct {
*prometheus.Registry
httpRequests *prometheus.CounterVec
httpDuration *prometheus.HistogramVec
httpInFlight prometheus.Gauge
}
// NewRegistry builds a registry with the Go runtime, process and build-info
// collectors already registered.
func NewRegistry() *Registry {
reg := prometheus.NewRegistry()
reg.MustRegister(
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
buildInfo := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "build_info",
Help: "Warp build information; the value is always 1.",
}, []string{"version", "commit"})
buildInfo.WithLabelValues(buildinfo.Version, buildinfo.Commit).Set(1)
reg.MustRegister(buildInfo)
r := &Registry{
Registry: reg,
httpRequests: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: "http",
Name: "requests_total",
Help: "Total HTTP requests by route, method and status.",
}, []string{"route", "method", "status"}),
httpDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: Namespace,
Subsystem: "http",
Name: "request_duration_seconds",
Help: "HTTP request duration by route and method.",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 30},
}, []string{"route", "method"}),
httpInFlight: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: "http",
Name: "requests_in_flight",
Help: "HTTP requests currently being served.",
}),
}
reg.MustRegister(r.httpRequests, r.httpDuration, r.httpInFlight)
return r
}
// Handler serves the metrics endpoint.
func (r *Registry) Handler() http.Handler {
return promhttp.HandlerFor(r.Registry, promhttp.HandlerOpts{
ErrorHandling: promhttp.HTTPErrorOnError,
})
}
// InFlight tracks concurrent requests. It is a global middleware: the
// gauge carries no route label.
func (r *Registry) InFlight() httputil.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r.httpInFlight.Inc()
defer r.httpInFlight.Dec()
next.ServeHTTP(w, req)
})
}
}
// Instrument wraps one route's handler. The label is the route pattern it
// was registered under, not the request path, so per-user and per-room URLs
// cannot explode the metric cardinality. It is passed to the router as an
// httputil.Instrumenter, which is why it takes the route explicitly: the
// pattern ServeMux records on the request is not visible to an outer
// middleware.
func (r *Registry) Instrument(route string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
rec := &httputil.ResponseRecorder{ResponseWriter: w}
next.ServeHTTP(rec, req)
status := rec.Status
if status == 0 {
status = http.StatusOK
}
r.httpRequests.WithLabelValues(route, req.Method, strconv.Itoa(status)).Inc()
r.httpDuration.WithLabelValues(route, req.Method).Observe(time.Since(start).Seconds())
})
}