From 11f43f87385b4bb9e139d5a97a07ec6d6b83ec21 Mon Sep 17 00:00:00 2001 From: Joe Santos <54154621+joesantos418@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:33:57 -0300 Subject: [PATCH] Adds metrics middleware v4 (#305) Adds metrics middleware v4 Adds a new version of the metrics middleware with predefined metrics prefixed with "fkit_" to avoid problems with a collector overwriting labels. --- .../metricsmiddleware/v4/config.go | 44 +++++++++ .../metricsmiddleware/v4/example/main.go | 92 +++++++++++++++++++ .../metricsmiddleware/v4/external_metrics.go | 8 ++ .../metricsmiddleware/v4/labels.go | 13 +++ .../metricsmiddleware/v4/middleware.go | 81 ++++++++++++++++ 5 files changed, 238 insertions(+) create mode 100644 gokitmiddlewares/metricsmiddleware/v4/config.go create mode 100644 gokitmiddlewares/metricsmiddleware/v4/example/main.go create mode 100644 gokitmiddlewares/metricsmiddleware/v4/external_metrics.go create mode 100644 gokitmiddlewares/metricsmiddleware/v4/labels.go create mode 100644 gokitmiddlewares/metricsmiddleware/v4/middleware.go diff --git a/gokitmiddlewares/metricsmiddleware/v4/config.go b/gokitmiddlewares/metricsmiddleware/v4/config.go new file mode 100644 index 0000000..6830de7 --- /dev/null +++ b/gokitmiddlewares/metricsmiddleware/v4/config.go @@ -0,0 +1,44 @@ +package metricsmiddleware + +import ( + "github.com/arquivei/foundationkit/metrifier" +) + +// Config is used to configure a metrics middleware. +type Config struct { + // Metrifier is the metrifier configuration + Metrifier metrifier.Config + + // LabelsDecoder extracts labels from the request, response or error. + // This is optional, can be nil + LabelsDecoder LabelsDecoder + + // ExternalMetrics is executed after the main metrifier is called. + // This is intended to calculate custom metrics. + // This is optional, can be nil. + ExternalMetrics ExternalMetrics +} + +// WithLabelsDecoder adds a LabelsDecoder to the metrics middleware. +func (c Config) WithLabelsDecoder(d LabelsDecoder) Config { + c.LabelsDecoder = d + c.Metrifier.ExtraLabels = d.Labels() + return c +} + +// WithExternalMetrics adds ExternalMetrics to the metrics middleware. +func (c Config) WithExternalMetrics(m ExternalMetrics) Config { + c.ExternalMetrics = m + return c +} + +// NewDefaultConfig returns a new Config with sane defaults. +func NewDefaultConfig(endpoint string) Config { + config := Config{ + Metrifier: metrifier.NewDefaultConfig("endpoint", ""), + } + config.Metrifier.ConstLabels = map[string]string{ + "fkit_endpoint": endpoint, + } + return config +} diff --git a/gokitmiddlewares/metricsmiddleware/v4/example/main.go b/gokitmiddlewares/metricsmiddleware/v4/example/main.go new file mode 100644 index 0000000..b4edd4f --- /dev/null +++ b/gokitmiddlewares/metricsmiddleware/v4/example/main.go @@ -0,0 +1,92 @@ +// This is a simple example that shows how to setup the metrics middleware. +// This can be run with `go run main.go` +package main + +import ( + "context" + "os" + + "github.com/arquivei/foundationkit/gokitmiddlewares/metricsmiddleware/v3" + + "github.com/go-kit/kit/endpoint" + kitprometheus "github.com/go-kit/kit/metrics/prometheus" + stdprometheus "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// request is a request to the greeter endpoint +type request struct { + Name string +} + +// response is the response of the greeter endpoint +type response struct { + Message string +} + +// greeter is an endpoint that takes a name and says hello. +func greeter(_ context.Context, req interface{}) (interface{}, error) { + resp := response{ + Message: "Hello " + req.(request).Name + "!", + } + + return resp, nil +} + +// labelsDecoder is an example that creates labels for the greeter endpoint. To +// avoid overwriting of your labels by the metrics collector, you may want to +// prefix them with a system-specific name +type labelsDecoder struct{} + +func (labelsDecoder) Labels() []string { + return []string{"greeter_empty_name"} +} + +func (labelsDecoder) Decode(ctx context.Context, req, resp interface{}, err error) map[string]string { + if req.(request).Name == "" { + return map[string]string{"greeter_empty_name": "true"} + } + return map[string]string{"greeter_empty_name": "false"} +} + +// newExternalMetrics is an example on how to implement external metrics. +func newExternalMetrics(system, subsystem string) func(ctx context.Context, req, resp interface{}, err error) { + count := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{ + Namespace: system, + Subsystem: subsystem, + Name: "letters", + Help: "Total amount letters.", + }, nil) + + return func(ctx context.Context, req, resp interface{}, err error) { + count.Add((float64(len(req.(request).Name)))) + } +} + +func main() { + // Just some basic logger initialization + zerolog.SetGlobalLevel(zerolog.InfoLevel) + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + + // Create some endpoint + e := greeter + + // Chain metrics middleware + e = endpoint.Chain( + metricsmiddleware.MustNew( + metricsmiddleware.NewDefaultConfig("endpointTest"). + WithLabelsDecoder(labelsDecoder{}). + WithExternalMetrics(newExternalMetrics("system", "subsystem")), + ), + )(e) + + // Let's just run the example for fun. + ctx := context.Background() + req := request{Name: "World"} + resp, err := e(ctx, req) + if err != nil { + log.Fatal().Err(err).Msg("") + } + log.Info().Msg(resp.(response).Message) +} diff --git a/gokitmiddlewares/metricsmiddleware/v4/external_metrics.go b/gokitmiddlewares/metricsmiddleware/v4/external_metrics.go new file mode 100644 index 0000000..95a7810 --- /dev/null +++ b/gokitmiddlewares/metricsmiddleware/v4/external_metrics.go @@ -0,0 +1,8 @@ +package metricsmiddleware + +import "context" + +// ExternalMetrics is called after the internal metrifier is called. +// This functions should compute other metrics that are not computed by +// the internal metrifier (request latency and count). +type ExternalMetrics func(ctx context.Context, req, resp interface{}, err error) diff --git a/gokitmiddlewares/metricsmiddleware/v4/labels.go b/gokitmiddlewares/metricsmiddleware/v4/labels.go new file mode 100644 index 0000000..7a4cf0c --- /dev/null +++ b/gokitmiddlewares/metricsmiddleware/v4/labels.go @@ -0,0 +1,13 @@ +package metricsmiddleware + +import "context" + +// LabelsDecoder defines an interface to decode labels for the internal metrifier. +type LabelsDecoder interface { + // Labels return the complete list of all available labels that will be + // returned by the Decoder. This is called once during setup of the middleware. + Labels() []string + // Decode extracts a map of labels considering the request, response and error. + // The map returned must contain only labels returned by the Labels() function. + Decode(ctx context.Context, req, resp interface{}, err error) map[string]string +} diff --git a/gokitmiddlewares/metricsmiddleware/v4/middleware.go b/gokitmiddlewares/metricsmiddleware/v4/middleware.go new file mode 100644 index 0000000..1bf3820 --- /dev/null +++ b/gokitmiddlewares/metricsmiddleware/v4/middleware.go @@ -0,0 +1,81 @@ +package metricsmiddleware + +import ( + "context" + + "github.com/arquivei/foundationkit/errors" + "github.com/arquivei/foundationkit/gokitmiddlewares" + logutil "github.com/arquivei/foundationkit/log" + "github.com/arquivei/foundationkit/metrifier" + "github.com/go-kit/kit/endpoint" + "github.com/rs/zerolog/log" +) + +// MustNew returns a new metrics middleware but panics in case of error. +func MustNew(c Config) endpoint.Middleware { + return gokitmiddlewares.Must(New(c)) +} + +// New returns a new metrics middleware. +func New(c Config) (endpoint.Middleware, error) { + m, err := metrifier.New(c.Metrifier) + if err != nil { + return nil, err + } + + return func(next endpoint.Endpoint) endpoint.Endpoint { + log.Debug().Str("config", logutil.Flatten(c)).Msg("[metricsmiddleware] New metrics endpoint middleware") + + return func(ctx context.Context, req interface{}) (resp interface{}, err error) { + defer func(s metrifier.Span) { + var r interface{} + // Panics are handled as errors and re-raised + if r = recover(); r != nil { + err = errors.E(errors.NewFromRecover(r), errors.SeverityFatal, errors.CodePanic) + log.Ctx(ctx).Warn().Err(err). + Msg("[metricsmiddleware] Metrics endpoint middleware is handling an uncaught a panic. Please fix it!") + } + metrify(ctx, c.LabelsDecoder, s, req, resp, err) + if panicErr := tryRunExternalMetrics(ctx, c.ExternalMetrics, req, resp, err); panicErr != nil { + log.Ctx(ctx).Error().Err(panicErr). + Msg("[metricsmiddleware] External Metrics panicked. Please check you ExternalMetrics function.") + } + + // re-raise panic + if r != nil { + panic(r) + } + }(m.Begin()) + return next(ctx, req) + } + }, nil +} + +func metrify(ctx context.Context, labelsDecoder LabelsDecoder, s metrifier.Span, req, resp interface{}, err error) { + defer func() { + if r := recover(); r != nil { + log.Ctx(ctx).Error(). + Err(errors.NewFromRecover(r)). + Msg("[metricsmiddleware] Metrics middleware panicked! Please check your code and configuration.") + } + }() + if labelsDecoder != nil { + s = s.WithLabels(labelsDecoder.Decode(ctx, req, resp, err)) + } + s.End(err) +} + +func tryRunExternalMetrics(ctx context.Context, externalMetrics ExternalMetrics, req, resp interface{}, err error) (panicErr error) { + if externalMetrics == nil { + return + } + + defer func() { + if r := recover(); r != nil { + panicErr = errors.E(errors.NewFromRecover(r), errors.SeverityFatal, errors.CodePanic) + } + }() + + externalMetrics(ctx, req, resp, err) + return +}