-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
dc28dd2
commit 11f43f8
Showing
5 changed files
with
238 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |