Extract app metrics to a separate struct (#520)

This commit is contained in:
Misha Bragin 2022-10-22 11:50:21 +02:00 committed by GitHub
parent ed2214f9a9
commit 84879a356b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 119 additions and 64 deletions

View File

@ -8,14 +8,8 @@ import (
"flag" "flag"
"fmt" "fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/mux"
httpapi "github.com/netbirdio/netbird/management/server/http" httpapi "github.com/netbirdio/netbird/management/server/http"
"github.com/netbirdio/netbird/management/server/metrics" "github.com/netbirdio/netbird/management/server/metrics"
prometheus2 "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel/exporters/prometheus"
metric2 "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/metric"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
@ -26,7 +20,6 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"reflect"
"strings" "strings"
"time" "time"
@ -162,18 +155,17 @@ var (
gRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials)) gRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials))
tlsEnabled = true tlsEnabled = true
} }
appMetrics, err := metrics.NewDefaultAppMetrics(cmd.Context())
metricsListener, err := net.Listen("tcp4", fmt.Sprintf(":%d", mgmtMetricsPort))
if err != nil { if err != nil {
return err return err
} }
meter, err := exposeMetrics(metricsListener) err = appMetrics.Expose(mgmtMetricsPort, "/metrics")
if err != nil { if err != nil {
return err return err
} }
httpAPIHandler, err := httpapi.APIHandler(accountManager, httpAPIHandler, err := httpapi.APIHandler(cmd.Context(), accountManager, config.HttpConfig.AuthIssuer,
config.HttpConfig.AuthIssuer, config.HttpConfig.AuthAudience, config.HttpConfig.AuthKeysLocation, meter) config.HttpConfig.AuthAudience, config.HttpConfig.AuthKeysLocation, appMetrics)
if err != nil { if err != nil {
return fmt.Errorf("failed creating HTTP API handler: %v", err) return fmt.Errorf("failed creating HTTP API handler: %v", err)
} }
@ -246,7 +238,7 @@ var (
SetupCloseHandler() SetupCloseHandler()
<-stopCh <-stopCh
_ = metricsListener.Close() _ = appMetrics.Close()
_ = listener.Close() _ = listener.Close()
if certManager != nil { if certManager != nil {
_ = certManager.Listener().Close() _ = certManager.Listener().Close()
@ -259,29 +251,6 @@ var (
} }
) )
func exposeMetrics(lis net.Listener) (metric2.Meter, error) {
exporter, err := prometheus.New()
if err != nil {
return nil, err
}
pkg := reflect.TypeOf(ManagementLegacyPort).PkgPath()
provider := metric.NewMeterProvider(metric.WithReader(exporter))
meter := provider.Meter(pkg)
rootRouter := mux.NewRouter()
rootRouter.Handle("/metrics", promhttp.HandlerFor(
prometheus2.DefaultGatherer,
promhttp.HandlerOpts{EnableOpenMetrics: true}))
go func() {
err := http.Serve(lis, rootRouter)
if err != nil {
return
}
}()
log.Infof("metrics enabled for package %v and listening on %s", pkg, lis.Addr().String())
return meter, nil
}
func notifyStop(msg string) { func notifyStop(msg string) {
select { select {
case stopCh <- 1: case stopCh <- 1:
@ -472,7 +441,7 @@ func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
return nil, err return nil, err
} }
// Create the credentials and return it // NewDefaultAppMetrics the credentials and return it
config := &tls.Config{ config := &tls.Config{
Certificates: []tls.Certificate{serverCert}, Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.NoClientCert, ClientAuth: tls.NoClientCert,

View File

@ -5,14 +5,14 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
s "github.com/netbirdio/netbird/management/server" s "github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http/middleware" "github.com/netbirdio/netbird/management/server/http/middleware"
"github.com/netbirdio/netbird/management/server/metrics"
"github.com/rs/cors" "github.com/rs/cors"
"go.opentelemetry.io/otel/metric"
"net/http" "net/http"
) )
// APIHandler creates the Management service HTTP API handler registering all the available endpoints. // APIHandler creates the Management service HTTP API handler registering all the available endpoints.
func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience string, authKeysLocation string, func APIHandler(ctx context.Context, accountManager s.AccountManager, authIssuer string, authAudience string, authKeysLocation string,
meter metric.Meter) (http.Handler, error) { appMetrics metrics.AppMetrics) (http.Handler, error) {
jwtMiddleware, err := middleware.NewJwtMiddleware( jwtMiddleware, err := middleware.NewJwtMiddleware(
authIssuer, authIssuer,
authAudience, authAudience,
@ -29,13 +29,13 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
accountManager.IsUserAdmin) accountManager.IsUserAdmin)
rootRouter := mux.NewRouter() rootRouter := mux.NewRouter()
metrics, err := middleware.NewMetricsMiddleware(context.Background(), meter) metricsMiddleware, err := metrics.NewMetricsMiddleware(ctx, appMetrics)
if err != nil { if err != nil {
return nil, err return nil, err
} }
apiHandler := rootRouter.PathPrefix("/api").Subrouter() apiHandler := rootRouter.PathPrefix("/api").Subrouter()
apiHandler.Use(metrics.Handler, corsMiddleware.Handler, jwtMiddleware.Handler, acMiddleware.Handler) apiHandler.Use(metricsMiddleware.Handler, corsMiddleware.Handler, jwtMiddleware.Handler, acMiddleware.Handler)
groupsHandler := NewGroups(accountManager, authAudience) groupsHandler := NewGroups(accountManager, authAudience)
rulesHandler := NewRules(accountManager, authAudience) rulesHandler := NewRules(accountManager, authAudience)
@ -95,7 +95,7 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
if err != nil { if err != nil {
return err return err
} }
err = metrics.AddHTTPRequestResponseCounter(template, method) err = metricsMiddleware.AddHTTPRequestResponseCounter(template, method)
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,87 @@
package metrics
import (
"context"
"fmt"
"github.com/gorilla/mux"
prometheus2 "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/exporters/prometheus"
metric2 "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/metric"
"net"
"net/http"
"reflect"
)
const defaultEndpoint = "/metrics"
// AppMetrics is metrics interface
type AppMetrics interface {
GetMeter() metric2.Meter
Close() error
Expose(port int, endpoint string) error
}
// defaultAppMetrics are core application metrics based on OpenTelemetry https://opentelemetry.io/
type defaultAppMetrics struct {
// Meter can be used by different application parts to create counters and measure things
Meter metric2.Meter
listener net.Listener
ctx context.Context
}
// Close stop application metrics HTTP handler and closes listener.
func (appMetrics *defaultAppMetrics) Close() error {
if appMetrics.listener == nil {
return nil
}
return appMetrics.listener.Close()
}
// Expose metrics on a given port and endpoint. If endpoint is empty a defaultEndpoint one will be used.
// Exposes metrics in the Prometheus format https://prometheus.io/
func (appMetrics *defaultAppMetrics) Expose(port int, endpoint string) error {
if endpoint == "" {
endpoint = defaultEndpoint
}
rootRouter := mux.NewRouter()
rootRouter.Handle(endpoint, promhttp.HandlerFor(
prometheus2.DefaultGatherer,
promhttp.HandlerOpts{EnableOpenMetrics: true}))
listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", port))
if err != nil {
return err
}
appMetrics.listener = listener
go func() {
err := http.Serve(listener, rootRouter)
if err != nil {
return
}
}()
log.Infof("enabled application metrics and exposing on http://%s", listener.Addr().String())
return nil
}
// GetMeter returns metrics meter that can be used to add various counters
func (appMetrics *defaultAppMetrics) GetMeter() metric2.Meter {
return appMetrics.Meter
}
// NewDefaultAppMetrics and expose them via defaultEndpoint on a given HTTP port
func NewDefaultAppMetrics(ctx context.Context) (AppMetrics, error) {
exporter, err := prometheus.New()
if err != nil {
return nil, err
}
provider := metric.NewMeterProvider(metric.WithReader(exporter))
pkg := reflect.TypeOf(defaultEndpoint).PkgPath()
meter := provider.Meter(pkg)
return &defaultAppMetrics{Meter: meter, ctx: ctx}, nil
}

View File

@ -1,10 +1,9 @@
package middleware package metrics
import ( import (
"context" "context"
"fmt" "fmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
metric2 "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/instrument" "go.opentelemetry.io/otel/metric/instrument"
"go.opentelemetry.io/otel/metric/instrument/syncint64" "go.opentelemetry.io/otel/metric/instrument/syncint64"
"hash/fnv" "hash/fnv"
@ -46,14 +45,14 @@ func (rw *WrappedResponseWriter) WriteHeader(code int) {
rw.wroteHeader = true rw.wroteHeader = true
} }
// MetricsMiddleware handler used to collect metrics of every request/response coming to the API. // HTTPMiddleware handler used to collect metrics of every request/response coming to the API.
// Also adds request tracing (logging). // Also adds request tracing (logging).
type MetricsMiddleware struct { type HTTPMiddleware struct {
meter metric2.Meter appMetrics AppMetrics
ctx context.Context ctx context.Context
// endpoint & method // defaultEndpoint & method
httpRequestCounters map[string]syncint64.Counter httpRequestCounters map[string]syncint64.Counter
// endpoint & method & status code // defaultEndpoint & method & status code
httpResponseCounters map[string]syncint64.Counter httpResponseCounters map[string]syncint64.Counter
// all HTTP requests // all HTTP requests
totalHTTPRequestsCounter syncint64.Counter totalHTTPRequestsCounter syncint64.Counter
@ -63,11 +62,11 @@ type MetricsMiddleware struct {
totalHTTPResponseCodeCounters map[int]syncint64.Counter totalHTTPResponseCodeCounters map[int]syncint64.Counter
} }
// AddHTTPRequestResponseCounter adds a new meter for an HTTP endpoint and Method (GET, POST, etc) // AddHTTPRequestResponseCounter adds a new meter for an HTTP defaultEndpoint and Method (GET, POST, etc)
// Creates one request counter and multiple response counters (one per http response status code). // Creates one request counter and multiple response counters (one per http response status code).
func (m *MetricsMiddleware) AddHTTPRequestResponseCounter(endpoint string, method string) error { func (m *HTTPMiddleware) AddHTTPRequestResponseCounter(endpoint string, method string) error {
meterKey := getRequestCounterKey(endpoint, method) meterKey := getRequestCounterKey(endpoint, method)
httpReqCounter, err := m.meter.SyncInt64().Counter(meterKey, instrument.WithUnit("1")) httpReqCounter, err := m.appMetrics.GetMeter().SyncInt64().Counter(meterKey, instrument.WithUnit("1"))
if err != nil { if err != nil {
return err return err
} }
@ -75,14 +74,14 @@ func (m *MetricsMiddleware) AddHTTPRequestResponseCounter(endpoint string, metho
respCodes := []int{200, 204, 400, 401, 403, 404, 500, 502, 503} respCodes := []int{200, 204, 400, 401, 403, 404, 500, 502, 503}
for _, code := range respCodes { for _, code := range respCodes {
meterKey = getResponseCounterKey(endpoint, method, code) meterKey = getResponseCounterKey(endpoint, method, code)
httpRespCounter, err := m.meter.SyncInt64().Counter(meterKey, instrument.WithUnit("1")) httpRespCounter, err := m.appMetrics.GetMeter().SyncInt64().Counter(meterKey, instrument.WithUnit("1"))
if err != nil { if err != nil {
return err return err
} }
m.httpResponseCounters[meterKey] = httpRespCounter m.httpResponseCounters[meterKey] = httpRespCounter
meterKey = fmt.Sprintf("%s_%d_total", httpResponseCounterPrefix, code) meterKey = fmt.Sprintf("%s_%d_total", httpResponseCounterPrefix, code)
totalHTTPResponseCodeCounter, err := m.meter.SyncInt64().Counter(meterKey, instrument.WithUnit("1")) totalHTTPResponseCodeCounter, err := m.appMetrics.GetMeter().SyncInt64().Counter(meterKey, instrument.WithUnit("1"))
if err != nil { if err != nil {
return err return err
} }
@ -92,27 +91,27 @@ func (m *MetricsMiddleware) AddHTTPRequestResponseCounter(endpoint string, metho
return nil return nil
} }
// NewMetricsMiddleware creates a new MetricsMiddleware // NewMetricsMiddleware creates a new HTTPMiddleware
func NewMetricsMiddleware(ctx context.Context, meter metric2.Meter) (*MetricsMiddleware, error) { func NewMetricsMiddleware(ctx context.Context, appMetrics AppMetrics) (*HTTPMiddleware, error) {
totalHTTPRequestsCounter, err := meter.SyncInt64().Counter( totalHTTPRequestsCounter, err := appMetrics.GetMeter().SyncInt64().Counter(
fmt.Sprintf("%s_total", httpRequestCounterPrefix), fmt.Sprintf("%s_total", httpRequestCounterPrefix),
instrument.WithUnit("1")) instrument.WithUnit("1"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
totalHTTPResponseCounter, err := meter.SyncInt64().Counter( totalHTTPResponseCounter, err := appMetrics.GetMeter().SyncInt64().Counter(
fmt.Sprintf("%s_total", httpResponseCounterPrefix), fmt.Sprintf("%s_total", httpResponseCounterPrefix),
instrument.WithUnit("1")) instrument.WithUnit("1"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &MetricsMiddleware{ return &HTTPMiddleware{
ctx: ctx, ctx: ctx,
httpRequestCounters: map[string]syncint64.Counter{}, httpRequestCounters: map[string]syncint64.Counter{},
httpResponseCounters: map[string]syncint64.Counter{}, httpResponseCounters: map[string]syncint64.Counter{},
totalHTTPResponseCodeCounters: map[int]syncint64.Counter{}, totalHTTPResponseCodeCounters: map[int]syncint64.Counter{},
meter: meter, appMetrics: appMetrics,
totalHTTPRequestsCounter: totalHTTPRequestsCounter, totalHTTPRequestsCounter: totalHTTPRequestsCounter,
totalHTTPResponseCounter: totalHTTPResponseCounter, totalHTTPResponseCounter: totalHTTPResponseCounter,
}, },
@ -130,7 +129,7 @@ func getResponseCounterKey(endpoint, method string, status int) string {
} }
// Handler logs every request and response and adds the, to metrics. // Handler logs every request and response and adds the, to metrics.
func (m *MetricsMiddleware) Handler(h http.Handler) http.Handler { func (m *HTTPMiddleware) Handler(h http.Handler) http.Handler {
fn := func(rw http.ResponseWriter, r *http.Request) { fn := func(rw http.ResponseWriter, r *http.Request) {
traceID := hash(fmt.Sprintf("%v", r)) traceID := hash(fmt.Sprintf("%v", r))
log.Tracef("HTTP request %v: %v %v", traceID, r.Method, r.URL) log.Tracef("HTTP request %v: %v %v", traceID, r.Method, r.URL)

View File

@ -17,7 +17,7 @@ import (
const ( const (
// PayloadEvent identifies an event type // PayloadEvent identifies an event type
PayloadEvent = "self-hosted stats" PayloadEvent = "self-hosted stats"
// payloadEndpoint metrics endpoint to send anonymous data // payloadEndpoint metrics defaultEndpoint to send anonymous data
payloadEndpoint = "https://metrics.netbird.io" payloadEndpoint = "https://metrics.netbird.io"
// defaultPushInterval default interval to push metrics // defaultPushInterval default interval to push metrics
defaultPushInterval = 24 * time.Hour defaultPushInterval = 24 * time.Hour