mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
* Fix WebSocket support by implementing Hijacker interface Add responsewriter.PassthroughWriter to preserve optional HTTP interfaces (Hijacker, Flusher, Pusher) when wrapping http.ResponseWriter in middleware. Without this delegation: - WebSocket connections fail (can't hijack the connection) - Streaming breaks (can't flush buffers) - HTTP/2 push doesn't work * Add HijackTracker to manage hijacked connections during graceful shutdown * Refactor HijackTracker to use middleware for tracking hijacked connections * Refactor server handler chain setup for improved readability and maintainability
152 lines
4.3 KiB
Go
152 lines
4.3 KiB
Go
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)
|
|
}
|