package metrics import ( "net/http" "strconv" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/netbirdio/netbird/proxy/internal/proxy" "github.com/netbirdio/netbird/proxy/internal/responsewriter" ) type Metrics struct { requestsTotal prometheus.Counter activeRequests prometheus.Gauge configuredDomains prometheus.Gauge pathsPerDomain *prometheus.GaugeVec requestDuration *prometheus.HistogramVec backendDuration *prometheus.HistogramVec } func New(reg prometheus.Registerer) *Metrics { promFactory := promauto.With(reg) return &Metrics{ requestsTotal: promFactory.NewCounter(prometheus.CounterOpts{ Name: "netbird_proxy_requests_total", Help: "Total number of requests made to the netbird proxy", }), activeRequests: promFactory.NewGauge(prometheus.GaugeOpts{ Name: "netbird_proxy_active_requests_count", Help: "Current in-flight requests handled by the netbird proxy", }), configuredDomains: promFactory.NewGauge(prometheus.GaugeOpts{ Name: "netbird_proxy_domains_count", Help: "Current number of domains configured on the netbird proxy", }), pathsPerDomain: promFactory.NewGaugeVec( prometheus.GaugeOpts{ Name: "netbird_proxy_paths_count", Help: "Current number of paths configured on the netbird proxy labelled by domain", }, []string{"domain"}, ), requestDuration: promFactory.NewHistogramVec( prometheus.HistogramOpts{ Name: "netbird_proxy_request_duration_seconds", Help: "Duration of requests made to the netbird proxy", Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10}, }, []string{"status", "size", "method", "host", "path"}, ), backendDuration: promFactory.NewHistogramVec(prometheus.HistogramOpts{ Name: "netbird_proxy_backend_duration_seconds", Help: "Duration of peer round trip time from the netbird proxy", Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10}, }, []string{"status", "size", "method", "host", "path"}, ), } } type responseInterceptor struct { *responsewriter.PassthroughWriter status int size int } func (w *responseInterceptor) WriteHeader(status int) { w.status = status w.PassthroughWriter.WriteHeader(status) } func (w *responseInterceptor) Write(b []byte) (int, error) { size, err := w.PassthroughWriter.Write(b) w.size += size return size, err } func (m *Metrics) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m.requestsTotal.Inc() m.activeRequests.Inc() interceptor := &responseInterceptor{PassthroughWriter: responsewriter.New(w)} start := time.Now() next.ServeHTTP(interceptor, r) duration := time.Since(start) m.activeRequests.Desc() m.requestDuration.With(prometheus.Labels{ "status": strconv.Itoa(interceptor.status), "size": strconv.Itoa(interceptor.size), "method": r.Method, "host": r.Host, "path": r.URL.Path, }).Observe(duration.Seconds()) }) } type roundTripperFunc func(*http.Request) (*http.Response, error) func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func (m *Metrics) RoundTripper(next http.RoundTripper) http.RoundTripper { return roundTripperFunc(func(req *http.Request) (*http.Response, error) { labels := prometheus.Labels{ "method": req.Method, "host": req.Host, // Fill potentially empty labels with default values to avoid cardinality issues. "path": "/", "status": "0", "size": "0", } if req.URL != nil { labels["path"] = req.URL.Path } start := time.Now() res, err := next.RoundTrip(req) duration := time.Since(start) // Not all labels will be available if there was an error. if res != nil { labels["status"] = strconv.Itoa(res.StatusCode) labels["size"] = strconv.Itoa(int(res.ContentLength)) } m.backendDuration.With(labels).Observe(duration.Seconds()) return res, err }) } func (m *Metrics) AddMapping(mapping proxy.Mapping) { m.configuredDomains.Inc() m.pathsPerDomain.With(prometheus.Labels{ "domain": mapping.Host, }).Set(float64(len(mapping.Paths))) } func (m *Metrics) RemoveMapping(mapping proxy.Mapping) { m.configuredDomains.Dec() m.pathsPerDomain.With(prometheus.Labels{ "domain": mapping.Host, }).Set(0) }