mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-22 02:06:39 +00:00
* **New Features** * Access logs now include bytes_upload and bytes_download (API and schemas updated, fields required). * Certificate issuance duration is now recorded as a metric. * **Refactor** * Metrics switched from Prometheus client to OpenTelemetry-backed meters; health endpoint now exposes OpenMetrics via OTLP exporter. * **Tests** * Metric tests updated to use OpenTelemetry Prometheus exporter and MeterProvider.
198 lines
5.0 KiB
Go
198 lines
5.0 KiB
Go
package metrics
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel/metric"
|
|
|
|
"github.com/netbirdio/netbird/proxy/internal/proxy"
|
|
"github.com/netbirdio/netbird/proxy/internal/responsewriter"
|
|
)
|
|
|
|
type Metrics struct {
|
|
ctx context.Context
|
|
requestsTotal metric.Int64Counter
|
|
activeRequests metric.Int64UpDownCounter
|
|
configuredDomains metric.Int64UpDownCounter
|
|
totalPaths metric.Int64UpDownCounter
|
|
requestDuration metric.Int64Histogram
|
|
backendDuration metric.Int64Histogram
|
|
certificateIssueDuration metric.Int64Histogram
|
|
|
|
mappingsMux sync.Mutex
|
|
mappingPaths map[string]int
|
|
}
|
|
|
|
func New(ctx context.Context, meter metric.Meter) (*Metrics, error) {
|
|
requestsTotal, err := meter.Int64Counter(
|
|
"proxy.http.request.counter",
|
|
metric.WithUnit("1"),
|
|
metric.WithDescription("Total number of requests made to the netbird proxy"),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
activeRequests, err := meter.Int64UpDownCounter(
|
|
"proxy.http.active_requests",
|
|
metric.WithUnit("1"),
|
|
metric.WithDescription("Current in-flight requests handled by the netbird proxy"),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configuredDomains, err := meter.Int64UpDownCounter(
|
|
"proxy.domains.count",
|
|
metric.WithUnit("1"),
|
|
metric.WithDescription("Current number of domains configured on the netbird proxy"),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
totalPaths, err := meter.Int64UpDownCounter(
|
|
"proxy.paths.count",
|
|
metric.WithUnit("1"),
|
|
metric.WithDescription("Total number of paths configured on the netbird proxy"),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
requestDuration, err := meter.Int64Histogram(
|
|
"proxy.http.request.duration.ms",
|
|
metric.WithUnit("milliseconds"),
|
|
metric.WithDescription("Duration of requests made to the netbird proxy"),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
backendDuration, err := meter.Int64Histogram(
|
|
"proxy.backend.duration.ms",
|
|
metric.WithUnit("milliseconds"),
|
|
metric.WithDescription("Duration of peer round trip time from the netbird proxy"),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certificateIssueDuration, err := meter.Int64Histogram(
|
|
"proxy.certificate.issue.duration.ms",
|
|
metric.WithUnit("milliseconds"),
|
|
metric.WithDescription("Duration of ACME certificate issuance"),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Metrics{
|
|
ctx: ctx,
|
|
requestsTotal: requestsTotal,
|
|
activeRequests: activeRequests,
|
|
configuredDomains: configuredDomains,
|
|
totalPaths: totalPaths,
|
|
requestDuration: requestDuration,
|
|
backendDuration: backendDuration,
|
|
certificateIssueDuration: certificateIssueDuration,
|
|
mappingPaths: make(map[string]int),
|
|
}, nil
|
|
}
|
|
|
|
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.Add(m.ctx, 1)
|
|
m.activeRequests.Add(m.ctx, 1)
|
|
|
|
interceptor := &responseInterceptor{PassthroughWriter: responsewriter.New(w)}
|
|
|
|
start := time.Now()
|
|
defer func() {
|
|
duration := time.Since(start)
|
|
m.activeRequests.Add(m.ctx, -1)
|
|
m.requestDuration.Record(m.ctx, duration.Milliseconds())
|
|
}()
|
|
|
|
next.ServeHTTP(interceptor, r)
|
|
})
|
|
}
|
|
|
|
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) {
|
|
start := time.Now()
|
|
res, err := next.RoundTrip(req)
|
|
duration := time.Since(start)
|
|
|
|
m.backendDuration.Record(m.ctx, duration.Milliseconds())
|
|
|
|
return res, err
|
|
})
|
|
}
|
|
|
|
func (m *Metrics) AddMapping(mapping proxy.Mapping) {
|
|
m.mappingsMux.Lock()
|
|
defer m.mappingsMux.Unlock()
|
|
|
|
newPathCount := len(mapping.Paths)
|
|
oldPathCount, exists := m.mappingPaths[mapping.Host]
|
|
|
|
if !exists {
|
|
m.configuredDomains.Add(m.ctx, 1)
|
|
}
|
|
|
|
pathDelta := newPathCount - oldPathCount
|
|
if pathDelta != 0 {
|
|
m.totalPaths.Add(m.ctx, int64(pathDelta))
|
|
}
|
|
|
|
m.mappingPaths[mapping.Host] = newPathCount
|
|
}
|
|
|
|
func (m *Metrics) RemoveMapping(mapping proxy.Mapping) {
|
|
m.mappingsMux.Lock()
|
|
defer m.mappingsMux.Unlock()
|
|
|
|
oldPathCount, exists := m.mappingPaths[mapping.Host]
|
|
if !exists {
|
|
// Nothing to remove
|
|
return
|
|
}
|
|
|
|
m.configuredDomains.Add(m.ctx, -1)
|
|
m.totalPaths.Add(m.ctx, -int64(oldPathCount))
|
|
|
|
delete(m.mappingPaths, mapping.Host)
|
|
}
|
|
|
|
// RecordCertificateIssuance records the duration of a certificate issuance.
|
|
func (m *Metrics) RecordCertificateIssuance(duration time.Duration) {
|
|
m.certificateIssueDuration.Record(m.ctx, duration.Milliseconds())
|
|
}
|