Skip to content

Commit

Permalink
Adds metrics middleware v4 (#305)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joesantos418 committed Apr 25, 2024
1 parent dc28dd2 commit 11f43f8
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 0 deletions.
44 changes: 44 additions & 0 deletions gokitmiddlewares/metricsmiddleware/v4/config.go
Original file line number Diff line number Diff line change
@@ -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
}
92 changes: 92 additions & 0 deletions gokitmiddlewares/metricsmiddleware/v4/example/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
8 changes: 8 additions & 0 deletions gokitmiddlewares/metricsmiddleware/v4/external_metrics.go
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions gokitmiddlewares/metricsmiddleware/v4/labels.go
Original file line number Diff line number Diff line change
@@ -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
}
81 changes: 81 additions & 0 deletions gokitmiddlewares/metricsmiddleware/v4/middleware.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 11f43f8

Please sign in to comment.