From da4159513efaf950b2e22fa335fd15f24cc78936 Mon Sep 17 00:00:00 2001 From: Prawn Date: Tue, 18 Aug 2020 19:27:50 +1200 Subject: [PATCH] Metrics interface and Prometheus implementation (#1929) * Metrics interface * Prometheus implementation * NoOp implementation Co-authored-by: chris --- go.mod | 2 +- go.sum | 5 ++ metrics/README.md | 22 ++++++ metrics/noop/reporter.go | 37 ++++++++++ metrics/noop/reporter_test.go | 20 +++++ metrics/options.go | 65 +++++++++++++++++ metrics/options_test.go | 18 +++++ metrics/prometheus/README.md | 26 +++++++ metrics/prometheus/metric_family.go | 109 ++++++++++++++++++++++++++++ metrics/prometheus/metrics.go | 68 +++++++++++++++++ metrics/prometheus/reporter.go | 69 ++++++++++++++++++ metrics/prometheus/reporter_test.go | 73 +++++++++++++++++++ metrics/reporter.go | 13 ++++ metrics/wrapper/metrics_wrapper.go | 51 +++++++++++++ 14 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 metrics/README.md create mode 100644 metrics/noop/reporter.go create mode 100644 metrics/noop/reporter_test.go create mode 100644 metrics/options.go create mode 100644 metrics/options_test.go create mode 100644 metrics/prometheus/README.md create mode 100644 metrics/prometheus/metric_family.go create mode 100644 metrics/prometheus/metrics.go create mode 100644 metrics/prometheus/reporter.go create mode 100644 metrics/prometheus/reporter_test.go create mode 100644 metrics/reporter.go create mode 100644 metrics/wrapper/metrics_wrapper.go diff --git a/go.mod b/go.mod index ccce9c37..81207edf 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.7.0 github.com/soheilhy/cmux v0.1.4 // indirect github.com/stretchr/testify v1.5.1 github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf @@ -69,4 +70,3 @@ require ( ) replace google.golang.org/grpc => google.golang.org/grpc v1.26.0 - diff --git a/go.sum b/go.sum index 6e1c92d9..03c1dbc5 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,7 @@ github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6R github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= @@ -407,17 +408,20 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U= github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -425,6 +429,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= diff --git a/metrics/README.md b/metrics/README.md new file mode 100644 index 00000000..424a0164 --- /dev/null +++ b/metrics/README.md @@ -0,0 +1,22 @@ +metrics +======= + +The metrics package provides a simple metrics "Reporter" interface which allows the user to submit counters, gauges and timings (along with key/value tags). + +Implementations +--------------- + +* Prometheus (pull): will be first +* Prometheus (push): certainly achievable +* InfluxDB: could quite easily be done +* Telegraf: almost identical to the InfluxDB implementation +* Micro: Could we provide metrics over Micro's server interface? + + +Todo +---- + +* Include a handler middleware which uses the Reporter interface to generate per-request level metrics + - Throughput + - Errors + - Duration diff --git a/metrics/noop/reporter.go b/metrics/noop/reporter.go new file mode 100644 index 00000000..4cf36884 --- /dev/null +++ b/metrics/noop/reporter.go @@ -0,0 +1,37 @@ +package noop + +import ( + "time" + + log "github.com/micro/go-micro/v3/logger" + "github.com/micro/go-micro/v3/metrics" +) + +// Reporter is an implementation of metrics.Reporter: +type Reporter struct { + options metrics.Options +} + +// New returns a configured noop reporter: +func New(opts ...metrics.Option) *Reporter { + log.Info("Metrics/NoOp - not doing anything") + + return &Reporter{ + options: metrics.NewOptions(opts...), + } +} + +// Count implements the metrics.Reporter interface Count method: +func (r *Reporter) Count(metricName string, value int64, tags metrics.Tags) error { + return nil +} + +// Gauge implements the metrics.Reporter interface Gauge method: +func (r *Reporter) Gauge(metricName string, value float64, tags metrics.Tags) error { + return nil +} + +// Timing implements the metrics.Reporter interface Timing method: +func (r *Reporter) Timing(metricName string, value time.Duration, tags metrics.Tags) error { + return nil +} diff --git a/metrics/noop/reporter_test.go b/metrics/noop/reporter_test.go new file mode 100644 index 00000000..f4c34ba0 --- /dev/null +++ b/metrics/noop/reporter_test.go @@ -0,0 +1,20 @@ +package noop + +import ( + "testing" + + "github.com/micro/go-micro/v3/metrics" + + "github.com/stretchr/testify/assert" +) + +func TestNoopReporter(t *testing.T) { + + // Make a Reporter: + reporter := New(metrics.Path("/noop")) + assert.NotNil(t, reporter) + assert.Equal(t, "/noop", reporter.options.Path) + + // Check that our implementation is valid: + assert.Implements(t, new(metrics.Reporter), reporter) +} diff --git a/metrics/options.go b/metrics/options.go new file mode 100644 index 00000000..e760ef1a --- /dev/null +++ b/metrics/options.go @@ -0,0 +1,65 @@ +package metrics + +var ( + // The Prometheus metrics will be made available on this port: + defaultPrometheusListenAddress = ":9000" + // This is the endpoint where the Prometheus metrics will be made available ("/metrics" is the default with Prometheus): + defaultPath = "/metrics" + // timingObjectives is the default spread of stats we maintain for timings / histograms: + defaultTimingObjectives = map[float64]float64{0.0: 0, 0.5: 0.05, 0.75: 0.04, 0.90: 0.03, 0.95: 0.02, 0.98: 0.001, 1: 0} +) + +// Option powers the configuration for metrics implementations: +type Option func(*Options) + +// Options for metrics implementations: +type Options struct { + Address string + Path string + DefaultTags Tags + TimingObjectives map[float64]float64 +} + +// NewOptions prepares a set of options: +func NewOptions(opt ...Option) Options { + opts := Options{ + Address: defaultPrometheusListenAddress, + DefaultTags: make(Tags), + Path: defaultPath, + TimingObjectives: defaultTimingObjectives, + } + + for _, o := range opt { + o(&opts) + } + + return opts +} + +// Path used to serve metrics over HTTP: +func Path(value string) Option { + return func(o *Options) { + o.Path = value + } +} + +// Address is the listen address to serve metrics on: +func Address(value string) Option { + return func(o *Options) { + o.Address = value + } +} + +// DefaultTags will be added to every metric: +func DefaultTags(value Tags) Option { + return func(o *Options) { + o.DefaultTags = value + } +} + +// TimingObjectives defines the desired spread of statistics for histogram / timing metrics: +func TimingObjectives(value map[float64]float64) Option { + return func(o *Options) { + o.TimingObjectives = value + } +} diff --git a/metrics/options_test.go b/metrics/options_test.go new file mode 100644 index 00000000..c367f8d0 --- /dev/null +++ b/metrics/options_test.go @@ -0,0 +1,18 @@ +package metrics + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptions(t *testing.T) { + + // Make some new options: + options := NewOptions(Path("/prometheus"), DefaultTags(map[string]string{"service": "prometheus-test"})) + + // Check that the defaults and overrides were accepted: + assert.Equal(t, ":9000", options.Address) + assert.Equal(t, "/prometheus", options.Path) + assert.Equal(t, "prometheus-test", options.DefaultTags["service"]) +} diff --git a/metrics/prometheus/README.md b/metrics/prometheus/README.md new file mode 100644 index 00000000..b950aa19 --- /dev/null +++ b/metrics/prometheus/README.md @@ -0,0 +1,26 @@ +Prometheus +========== + +A Prometheus "pull" based implementation of the metrics Reporter interface. + + +Capabilities +------------ + +* Go runtime metrics are handled natively by the Prometheus client library (CPU / MEM / GC / GoRoutines etc). +* User-defined metrics are registered in the Prometheus client dynamically (they must be pre-registered, hence all of the faffing around in metric_family.go). +* The metrics are made available on a Prometheus-compatible HTTP endpoint, which can be scraped at any time. This means that the user can very easily access stats even running locally as a standalone binary. +* Requires a micro.Server parameter (from which it gathers the service name and version). These are included as tags with every metric. + + +Usage +----- + +```golang + prometheusReporter := metrics.New(server) + tags := metrics.Tags{"greeter": "Janos"} + err := prometheusReporter.Count("hellos", 1, tags) + if err != nil { + fmt.Printf("Error setting a Count metric: %v", err) + } +``` diff --git a/metrics/prometheus/metric_family.go b/metrics/prometheus/metric_family.go new file mode 100644 index 00000000..fd9602cb --- /dev/null +++ b/metrics/prometheus/metric_family.go @@ -0,0 +1,109 @@ +package prometheus + +import ( + "sync" + + "github.com/prometheus/client_golang/prometheus" +) + +// metricFamily stores our cached metrics: +type metricFamily struct { + counters map[string]*prometheus.CounterVec + gauges map[string]*prometheus.GaugeVec + timings map[string]*prometheus.SummaryVec + defaultLabels prometheus.Labels + mutex sync.Mutex + prometheusRegistry *prometheus.Registry + timingObjectives map[float64]float64 +} + +// newMetricFamily returns a new metricFamily (useful in case we want to change the structure later): +func (r *Reporter) newMetricFamily() metricFamily { + return metricFamily{ + counters: make(map[string]*prometheus.CounterVec), + gauges: make(map[string]*prometheus.GaugeVec), + timings: make(map[string]*prometheus.SummaryVec), + defaultLabels: r.convertTags(r.options.DefaultTags), + prometheusRegistry: r.prometheusRegistry, + timingObjectives: r.options.TimingObjectives, + } +} + +// getCounter either gets a counter, or makes a new one: +func (mf *metricFamily) getCounter(name string, labelNames []string) *prometheus.CounterVec { + mf.mutex.Lock() + defer mf.mutex.Unlock() + + // See if we already have this counter: + counter, ok := mf.counters[name] + if !ok { + + // Make a new counter: + counter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: name, + ConstLabels: mf.defaultLabels, + }, + labelNames, + ) + + // Register it and add it to our list: + mf.prometheusRegistry.MustRegister(counter) + mf.counters[name] = counter + } + + return counter +} + +// getGauge either gets a gauge, or makes a new one: +func (mf *metricFamily) getGauge(name string, labelNames []string) *prometheus.GaugeVec { + mf.mutex.Lock() + defer mf.mutex.Unlock() + + // See if we already have this gauge: + gauge, ok := mf.gauges[name] + if !ok { + + // Make a new gauge: + gauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: name, + ConstLabels: mf.defaultLabels, + }, + labelNames, + ) + + // Register it and add it to our list: + mf.prometheusRegistry.MustRegister(gauge) + mf.gauges[name] = gauge + } + + return gauge +} + +// getTiming either gets a timing, or makes a new one: +func (mf *metricFamily) getTiming(name string, labelNames []string) *prometheus.SummaryVec { + mf.mutex.Lock() + defer mf.mutex.Unlock() + + // See if we already have this timing: + timing, ok := mf.timings[name] + if !ok { + + // Make a new timing: + timing = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: name, + ConstLabels: mf.defaultLabels, + Objectives: mf.timingObjectives, + }, + labelNames, + ) + + // Register it and add it to our list: + mf.prometheusRegistry.MustRegister(timing) + mf.timings[name] = timing + } + + return timing +} diff --git a/metrics/prometheus/metrics.go b/metrics/prometheus/metrics.go new file mode 100644 index 00000000..32af323b --- /dev/null +++ b/metrics/prometheus/metrics.go @@ -0,0 +1,68 @@ +package prometheus + +import ( + "errors" + "time" + + "github.com/micro/go-micro/v3/metrics" +) + +// ErrPrometheusPanic is a catch-all for the panics which can be thrown by the Prometheus client: +var ErrPrometheusPanic = errors.New("The Prometheus client panicked. Did you do something like change the tag cardinality or the type of a metric?") + +// Count is a counter with key/value tags: +// New values are added to any previous one (eg "number of hits") +func (r *Reporter) Count(name string, value int64, tags metrics.Tags) (err error) { + defer func() { + if r := recover(); r != nil { + err = ErrPrometheusPanic + } + }() + + counter := r.metrics.getCounter(r.stripUnsupportedCharacters(name), r.listTagKeys(tags)) + metric, err := counter.GetMetricWith(r.convertTags(tags)) + if err != nil { + return err + } + + metric.Add(float64(value)) + return err +} + +// Gauge is a register with key/value tags: +// New values simply override any previous one (eg "current connections") +func (r *Reporter) Gauge(name string, value float64, tags metrics.Tags) (err error) { + defer func() { + if r := recover(); r != nil { + err = ErrPrometheusPanic + } + }() + + gauge := r.metrics.getGauge(r.stripUnsupportedCharacters(name), r.listTagKeys(tags)) + metric, err := gauge.GetMetricWith(r.convertTags(tags)) + if err != nil { + return err + } + + metric.Set(value) + return err +} + +// Timing is a histogram with key/valye tags: +// New values are added into a series of aggregations +func (r *Reporter) Timing(name string, value time.Duration, tags metrics.Tags) (err error) { + defer func() { + if r := recover(); r != nil { + err = ErrPrometheusPanic + } + }() + + timing := r.metrics.getTiming(r.stripUnsupportedCharacters(name), r.listTagKeys(tags)) + metric, err := timing.GetMetricWith(r.convertTags(tags)) + if err != nil { + return err + } + + metric.Observe(value.Seconds()) + return err +} diff --git a/metrics/prometheus/reporter.go b/metrics/prometheus/reporter.go new file mode 100644 index 00000000..79614ab7 --- /dev/null +++ b/metrics/prometheus/reporter.go @@ -0,0 +1,69 @@ +package prometheus + +import ( + "net/http" + "strings" + + log "github.com/micro/go-micro/v3/logger" + "github.com/micro/go-micro/v3/metrics" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// Reporter is an implementation of metrics.Reporter: +type Reporter struct { + options metrics.Options + prometheusRegistry *prometheus.Registry + metrics metricFamily +} + +// New returns a configured prometheus reporter: +func New(opts ...metrics.Option) (*Reporter, error) { + options := metrics.NewOptions(opts...) + + // Make a prometheus registry (this keeps track of any metrics we generate): + prometheusRegistry := prometheus.NewRegistry() + prometheusRegistry.Register(prometheus.NewGoCollector()) + prometheusRegistry.Register(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{Namespace: "goruntime"})) + + // Make a new Reporter: + newReporter := &Reporter{ + options: options, + prometheusRegistry: prometheusRegistry, + } + + // Add metrics families for each type: + newReporter.metrics = newReporter.newMetricFamily() + + // Handle the metrics endpoint with prometheus: + log.Infof("Metrics/Prometheus [http] Listening on %s%s", options.Address, options.Path) + http.Handle(options.Path, promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError})) + go http.ListenAndServe(options.Address, nil) + + return newReporter, nil +} + +// convertTags turns Tags into prometheus labels: +func (r *Reporter) convertTags(tags metrics.Tags) prometheus.Labels { + labels := prometheus.Labels{} + for key, value := range tags { + labels[key] = r.stripUnsupportedCharacters(value) + } + return labels +} + +// listTagKeys returns a list of tag keys (we need to provide this to the Prometheus client): +func (r *Reporter) listTagKeys(tags metrics.Tags) (labelKeys []string) { + for key := range tags { + labelKeys = append(labelKeys, key) + } + return +} + +// stripUnsupportedCharacters cleans up a metrics key or value: +func (r *Reporter) stripUnsupportedCharacters(metricName string) string { + valueWithoutDots := strings.Replace(metricName, ".", "_", -1) + valueWithoutCommas := strings.Replace(valueWithoutDots, ",", "_", -1) + valueWIthoutSpaces := strings.Replace(valueWithoutCommas, " ", "", -1) + return valueWIthoutSpaces +} diff --git a/metrics/prometheus/reporter_test.go b/metrics/prometheus/reporter_test.go new file mode 100644 index 00000000..4b7f87a9 --- /dev/null +++ b/metrics/prometheus/reporter_test.go @@ -0,0 +1,73 @@ +package prometheus + +import ( + "testing" + "time" + + "github.com/micro/go-micro/v3/metrics" + + "github.com/stretchr/testify/assert" +) + +func TestPrometheusReporter(t *testing.T) { + + // Make a Reporter: + reporter, err := New(metrics.Path("/prometheus"), metrics.DefaultTags(map[string]string{"service": "prometheus-test"})) + assert.NoError(t, err) + assert.NotNil(t, reporter) + assert.Equal(t, "prometheus-test", reporter.options.DefaultTags["service"]) + assert.Equal(t, ":9000", reporter.options.Address) + assert.Equal(t, "/prometheus", reporter.options.Path) + + // Check that our implementation is valid: + assert.Implements(t, new(metrics.Reporter), reporter) + + // Test tag conversion: + tags := metrics.Tags{ + "tag1": "false", + "tag2": "true", + } + convertedTags := reporter.convertTags(tags) + assert.Equal(t, "false", convertedTags["tag1"]) + assert.Equal(t, "true", convertedTags["tag2"]) + + // Test tag enumeration: + listedTags := reporter.listTagKeys(tags) + assert.Contains(t, listedTags, "tag1") + assert.Contains(t, listedTags, "tag2") + + // Test string cleaning: + preparedMetricName := reporter.stripUnsupportedCharacters("some.kind,of tag") + assert.Equal(t, "some_kind_oftag", preparedMetricName) + + // Test MetricFamilies: + metricFamily := reporter.newMetricFamily() + + // Counters: + assert.NotNil(t, metricFamily.getCounter("testCounter", []string{"test", "counter"})) + assert.Len(t, metricFamily.counters, 1) + + // Gauges: + assert.NotNil(t, metricFamily.getGauge("testGauge", []string{"test", "gauge"})) + assert.Len(t, metricFamily.gauges, 1) + + // Timings: + assert.NotNil(t, metricFamily.getTiming("testTiming", []string{"test", "timing"})) + assert.Len(t, metricFamily.timings, 1) + + // Test submitting metrics through the interface methods: + assert.NoError(t, reporter.Count("test.counter.1", 6, tags)) + assert.NoError(t, reporter.Count("test.counter.2", 19, tags)) + assert.NoError(t, reporter.Count("test.counter.1", 5, tags)) + assert.NoError(t, reporter.Gauge("test.gauge.1", 99, tags)) + assert.NoError(t, reporter.Gauge("test.gauge.2", 55, tags)) + assert.NoError(t, reporter.Gauge("test.gauge.1", 98, tags)) + assert.NoError(t, reporter.Timing("test.timing.1", time.Second, tags)) + assert.NoError(t, reporter.Timing("test.timing.2", time.Minute, tags)) + assert.Len(t, reporter.metrics.counters, 2) + assert.Len(t, reporter.metrics.gauges, 2) + assert.Len(t, reporter.metrics.timings, 2) + + // Test reading back the metrics: + // This could be done by hitting the /metrics endpoint +} diff --git a/metrics/reporter.go b/metrics/reporter.go new file mode 100644 index 00000000..c81da1bb --- /dev/null +++ b/metrics/reporter.go @@ -0,0 +1,13 @@ +package metrics + +import "time" + +// Tags is a map of fields to add to a metric: +type Tags map[string]string + +// Reporter is the standard metrics interface: +type Reporter interface { + Count(metricName string, value int64, tags Tags) error + Gauge(metricName string, value float64, tags Tags) error + Timing(metricName string, value time.Duration, tags Tags) error +} diff --git a/metrics/wrapper/metrics_wrapper.go b/metrics/wrapper/metrics_wrapper.go new file mode 100644 index 00000000..570775db --- /dev/null +++ b/metrics/wrapper/metrics_wrapper.go @@ -0,0 +1,51 @@ +package wrapper + +import ( + "time" + + "context" + + "github.com/micro/go-micro/v3/metrics" + "github.com/micro/go-micro/v3/server" +) + +// Wrapper provides a HandlerFunc for metrics.Reporter implementations: +type Wrapper struct { + reporter metrics.Reporter +} + +// New returns a *Wrapper configured with the given metrics.Reporter: +func New(reporter metrics.Reporter) *Wrapper { + return &Wrapper{ + reporter: reporter, + } +} + +// HandlerFunc instruments handlers registered to a service: +func (w *Wrapper) HandlerFunc(handlerFunction server.HandlerFunc) server.HandlerFunc { + return func(ctx context.Context, req server.Request, rsp interface{}) error { + + // Build some tags to describe the call: + tags := metrics.Tags{ + "method": req.Method(), + } + + // Start the clock: + callTime := time.Now() + + // Run the handlerFunction: + err := handlerFunction(ctx, req, rsp) + + // Add a result tag: + if err != nil { + tags["result"] = "failure" + } else { + tags["result"] = "failure" + } + + // Instrument the result (if the DefaultClient has been configured): + w.reporter.Timing("service.handler", time.Since(callTime), tags) + + return err + } +}