Seed from go-template-repo, renamed to simplexcalc
The template's files at a77fd30, without its history or LICENSE, after script/rename simplexcalc. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/simplexcalc/internal/config"
|
||||
)
|
||||
|
||||
// durationBuckets span a fast in-process handler through a slow
|
||||
// upstream call. Prometheus's defaults top out at 10s, which hides the
|
||||
// tail this service's request timeout permits.
|
||||
//
|
||||
//nolint:gochecknoglobals // a bucket list is a declaration, not mutable state.
|
||||
var durationBuckets = []float64{
|
||||
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30,
|
||||
}
|
||||
|
||||
// MetricsParams defines dependencies for Metrics.
|
||||
type MetricsParams struct {
|
||||
fx.In
|
||||
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// Metrics owns the registry and the HTTP series. A private registry,
|
||||
// not the global default: what this process exports is then exactly
|
||||
// what this code registered, and a linked library cannot quietly add to
|
||||
// it.
|
||||
type Metrics struct {
|
||||
registry *prometheus.Registry
|
||||
|
||||
requests *prometheus.CounterVec
|
||||
duration *prometheus.HistogramVec
|
||||
inflight prometheus.Gauge
|
||||
|
||||
user string
|
||||
password string
|
||||
}
|
||||
|
||||
// NewMetrics builds the registry and registers the collectors.
|
||||
func NewMetrics(params MetricsParams) (*Metrics, error) {
|
||||
m := &Metrics{
|
||||
registry: prometheus.NewRegistry(),
|
||||
user: params.Config.MetricsUser,
|
||||
password: params.Config.MetricsPassword,
|
||||
}
|
||||
|
||||
m.requests = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "http_requests_total",
|
||||
Help: "Total HTTP requests by method, route pattern and status code.",
|
||||
},
|
||||
// The route PATTERN, never the path: labelling by path turns
|
||||
// every distinct URL into a new time series, and a crawler
|
||||
// then owns the memory of the process.
|
||||
[]string{"method", "route", "code"},
|
||||
)
|
||||
|
||||
m.duration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request duration by method and route pattern.",
|
||||
Buckets: durationBuckets,
|
||||
},
|
||||
[]string{"method", "route"},
|
||||
)
|
||||
|
||||
m.inflight = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "http_requests_in_flight",
|
||||
Help: "HTTP requests currently being served.",
|
||||
})
|
||||
|
||||
m.registry.MustRegister(
|
||||
m.requests,
|
||||
m.duration,
|
||||
m.inflight,
|
||||
collectors.NewGoCollector(),
|
||||
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
||||
)
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Observe records one finished request.
|
||||
func (m *Metrics) Observe(method, route, code string, d time.Duration) {
|
||||
m.requests.WithLabelValues(method, route, code).Inc()
|
||||
m.duration.WithLabelValues(method, route).Observe(d.Seconds())
|
||||
}
|
||||
|
||||
// InFlightAdd adjusts the in-flight gauge.
|
||||
func (m *Metrics) InFlightAdd(delta float64) {
|
||||
m.inflight.Add(delta)
|
||||
}
|
||||
|
||||
// Registry exposes the registry so tests can gather what was recorded.
|
||||
func (m *Metrics) Registry() *prometheus.Registry {
|
||||
return m.registry
|
||||
}
|
||||
|
||||
// AuthRequired reports whether /metrics is credential-gated.
|
||||
func (m *Metrics) AuthRequired() bool {
|
||||
return m.user != "" && m.password != ""
|
||||
}
|
||||
|
||||
// Handler serves the exposition format, behind HTTP basic auth when
|
||||
// credentials are configured.
|
||||
//
|
||||
// Metrics are not public: they leak route names, traffic volume,
|
||||
// version and process memory layout. When no credentials are set the
|
||||
// endpoint is served open, which is correct for a private network and
|
||||
// documented as such in the README; config refuses the half-configured
|
||||
// case, so "open" is always something the operator chose rather than
|
||||
// something a typo produced.
|
||||
func (m *Metrics) Handler() http.Handler {
|
||||
h := promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{
|
||||
// A collector that errors should not take the scrape down
|
||||
// with a 500 the operator has to go and interpret.
|
||||
ErrorHandling: promhttp.ContinueOnError,
|
||||
})
|
||||
|
||||
if !m.AuthRequired() {
|
||||
return h
|
||||
}
|
||||
|
||||
return m.basicAuth(h)
|
||||
}
|
||||
|
||||
// basicAuth gates h. Comparison is over SHA-256 digests through
|
||||
// subtle.ConstantTimeCompare: comparing the raw strings would leak the
|
||||
// credential length and the position of the first wrong byte through
|
||||
// timing, and hashing first makes the comparison fixed-width.
|
||||
func (m *Metrics) basicAuth(h http.Handler) http.Handler {
|
||||
wantUser := sha256.Sum256([]byte(m.user))
|
||||
wantPass := sha256.Sum256([]byte(m.password))
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
gotUser := sha256.Sum256([]byte(user))
|
||||
gotPass := sha256.Sum256([]byte(pass))
|
||||
|
||||
userOK := subtle.ConstantTimeCompare(gotUser[:], wantUser[:]) == 1
|
||||
passOK := subtle.ConstantTimeCompare(gotPass[:], wantPass[:]) == 1
|
||||
|
||||
if userOK && passOK {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="metrics", charset="UTF-8"`)
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user