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>
112 lines
3.7 KiB
Go
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())
|
|
})
|
|
}
|