From c9d58a9011ffec1e99473ea4aeb4e4107e56464c Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 10 Jun 2020 13:32:55 +0200 Subject: [PATCH 1/7] Metrics package Added metrics package which provides tools useful for gathering and exposing system metrics for external monitoring tools. --- pkg/metrics/gauge.go | 49 +++++++++++++++++ pkg/metrics/gauge_test.go | 61 +++++++++++++++++++++ pkg/metrics/observer.go | 36 +++++++++++++ pkg/metrics/observer_test.go | 38 +++++++++++++ pkg/metrics/registry.go | 102 +++++++++++++++++++++++++++++++++++ pkg/metrics/registry_test.go | 22 ++++++++ 6 files changed, 308 insertions(+) create mode 100644 pkg/metrics/gauge.go create mode 100644 pkg/metrics/gauge_test.go create mode 100644 pkg/metrics/observer.go create mode 100644 pkg/metrics/observer_test.go create mode 100644 pkg/metrics/registry.go create mode 100644 pkg/metrics/registry_test.go diff --git a/pkg/metrics/gauge.go b/pkg/metrics/gauge.go new file mode 100644 index 0000000..4cf64de --- /dev/null +++ b/pkg/metrics/gauge.go @@ -0,0 +1,49 @@ +package metrics + +import ( + "fmt" + "strings" + "sync" + "time" +) + +// Gauge is a metric type that represents a single numerical value that can +// arbitrarily go up and down. +type Gauge struct { + name string + labels map[string]string + + value float64 + timestamp int64 + mutex sync.RWMutex +} + +// Set allows setting the gauge to an arbitrary value. +func (g *Gauge) Set(value float64) { + g.mutex.Lock() + defer g.mutex.Unlock() + + g.value = value + g.timestamp = time.Now().UnixNano() / int64(time.Millisecond) +} + +// Exposes the gauge in the text-based exposition format. +func (g *Gauge) expose() string { + g.mutex.RLock() + defer g.mutex.RUnlock() + + typeLine := fmt.Sprintf("# TYPE %v %v", g.name, "gauge") + + labelsStrings := make([]string, 0) + for name, value := range g.labels { + labelsStrings = append( + labelsStrings, + fmt.Sprintf("%v=\"%v\"", name, value), + ) + } + labels := strings.Join(labelsStrings, ",") + + body := fmt.Sprintf("%v{%v} %v %v", g.name, labels, g.value, g.timestamp) + + return fmt.Sprintf("%v\n%v", typeLine, body) +} diff --git a/pkg/metrics/gauge_test.go b/pkg/metrics/gauge_test.go new file mode 100644 index 0000000..39f170b --- /dev/null +++ b/pkg/metrics/gauge_test.go @@ -0,0 +1,61 @@ +package metrics + +import ( + "testing" +) + +func TestGaugeSet(t *testing.T) { + gauge := &Gauge{ + name: "test_gauge", + labels: map[string]string{"label": "value"}, + } + + if gauge.value != 0 { + t.Fatal("incorrect gauge initial value") + } + + if gauge.timestamp != 0 { + t.Fatal("incorrect gauge initial timestamp") + } + + newGaugeValue := float64(500) + + gauge.Set(newGaugeValue) + + if gauge.value != newGaugeValue { + t.Fatalf( + "incorrect gauge value:\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + newGaugeValue, + gauge.value, + ) + } + + if gauge.timestamp == 0 { + t.Fatal("timestamp should be set") + } +} + +func TestGaugeExpose(t *testing.T) { + gauge := &Gauge{ + name: "test_gauge", + labels: map[string]string{"label": "value"}, + value: 500, + timestamp: 1000, + } + + actualText := gauge.expose() + + expectedText := "# TYPE test_gauge gauge\ntest_gauge{label=\"value\"} 500 1000" + + if actualText != expectedText { + t.Fatalf( + "incorrect gauge expose text:\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedText, + actualText, + ) + } +} diff --git a/pkg/metrics/observer.go b/pkg/metrics/observer.go new file mode 100644 index 0000000..ffb876b --- /dev/null +++ b/pkg/metrics/observer.go @@ -0,0 +1,36 @@ +package metrics + +import ( + "context" + "time" +) + +// Source defines a source of metric data. +type Source func() float64 + +// Sink defines a destination of collected metric data. +type Sink interface { + Set(value float64) +} + +// Observe triggers a cyclic metric observation goroutine. +func Observe( + ctx context.Context, + source Source, + sink Sink, + tick time.Duration, +) { + go func() { + ticker := time.NewTicker(tick) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + sink.Set(source()) + case <-ctx.Done(): + return + } + } + }() +} diff --git a/pkg/metrics/observer_test.go b/pkg/metrics/observer_test.go new file mode 100644 index 0000000..eb17dc6 --- /dev/null +++ b/pkg/metrics/observer_test.go @@ -0,0 +1,38 @@ +package metrics + +import ( + "context" + "testing" + "time" +) + +func TestObserve(t *testing.T) { + gauge := &Gauge{} + ctx, _ := context.WithTimeout(context.Background(), 5*time.Millisecond) + + Observe( + ctx, + func() float64 { + return 5000 + }, + gauge, + 1*time.Millisecond, + ) + + <-ctx.Done() + + expectedGaugeValue := float64(5000) + if gauge.value != expectedGaugeValue { + t.Fatalf( + "incorrect gauge value:\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedGaugeValue, + gauge.value, + ) + } + + if gauge.timestamp == 0 { + t.Fatal("timestamp should be set") + } +} diff --git a/pkg/metrics/registry.go b/pkg/metrics/registry.go new file mode 100644 index 0000000..9cc5a12 --- /dev/null +++ b/pkg/metrics/registry.go @@ -0,0 +1,102 @@ +// Package `metrics` provides some tools useful for gathering and +// exposing system metrics for external monitoring tools. +// +// Currently, this package is intended to use with Prometheus but +// can be easily extended if needed. Also, not all Prometheus metric +// types are implemented. The main motivation of creating a custom +// package was a need to avoid usage of external unaudited dependencies. +// +// Following specifications was used as reference: +// - https://prometheus.io/docs/instrumenting/writing_clientlibs/ +// - https://prometheus.io/docs/instrumenting/exposition_formats/ +package metrics + +import ( + "fmt" + "io" + "net/http" + "strconv" + "strings" + "sync" + + "github.com/ipfs/go-log" +) + +var logger = log.Logger("keep-metrics") + +type metric interface { + expose() string +} + +// Registry performs all management of metrics. Specifically, it allows +// to registering new metrics and exposing them through the metrics server. +type Registry struct { + application string + identifier string + + metrics map[string]metric + metricsMutex sync.RWMutex +} + +// NewRegistry creates a new metrics registry. +func NewRegistry(application, identifier string) *Registry { + return &Registry{ + application: application, + identifier: identifier, + metrics: make(map[string]metric), + } +} + +// EnableServer enables the metrics server on the given port. Data will +// be exposed on `/metrics` path. +func (r *Registry) EnableServer(port int) { + server := &http.Server{Addr: ":" + strconv.Itoa(port)} + + http.HandleFunc("/metrics", func(response http.ResponseWriter, _ *http.Request) { + if _, err := io.WriteString(response, r.exposeMetrics()); err != nil { + logger.Errorf("could not write response: [%v]", err) + } + }) + + go func() { + if err := server.ListenAndServe(); err != http.ErrServerClosed { + logger.Errorf("metrics server error: [%v]", err) + } + }() +} + +// Exposes all registered metrics in their text format. +func (r *Registry) exposeMetrics() string { + r.metricsMutex.RLock() + defer r.metricsMutex.RUnlock() + + metrics := make([]string, 0) + for _, metric := range r.metrics { + metrics = append(metrics, metric.expose()) + } + + return strings.Join(metrics, "\n\n") +} + +// NewGauge creates and registers a new gauge metric which will be exposed +// through the metrics server. In case a metric already exists, an error +// will be returned. +func (r *Registry) NewGauge(name string) (*Gauge, error) { + r.metricsMutex.Lock() + defer r.metricsMutex.Unlock() + + if _, exists := r.metrics[name]; exists { + return nil, fmt.Errorf("gauge [%v] already exists", name) + } + + gauge := &Gauge{ + name: name, + labels: map[string]string{ + "application": r.application, + "identifier": r.identifier, + }, + } + + r.metrics[name] = gauge + return gauge, nil +} diff --git a/pkg/metrics/registry_test.go b/pkg/metrics/registry_test.go new file mode 100644 index 0000000..a5ffe6b --- /dev/null +++ b/pkg/metrics/registry_test.go @@ -0,0 +1,22 @@ +package metrics + +import ( + "testing" +) + +func TestRegistryNewGauge(t *testing.T) { + registry := NewRegistry("test-app", "test-id") + + gauge, err := registry.NewGauge("test-gauge") + if err != nil { + t.Fatal(err) + } + + if _, err = registry.NewGauge("test-gauge"); err == nil { + t.Fatalf("should fail when creating gauge with the same name") + } + + if _, exists := registry.metrics[gauge.name]; !exists { + t.Fatalf("metric with name [%v] should exist in the registry", gauge.name) + } +} From 55afd4237cafd617d315934ba175216372b95ff2 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 10 Jun 2020 15:00:35 +0200 Subject: [PATCH 2/7] Observer refactoring --- pkg/metrics/observer.go | 22 +++++++++++++--------- pkg/metrics/observer_test.go | 14 ++++++-------- pkg/metrics/registry.go | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/pkg/metrics/observer.go b/pkg/metrics/observer.go index ffb876b..f1d461c 100644 --- a/pkg/metrics/observer.go +++ b/pkg/metrics/observer.go @@ -5,19 +5,23 @@ import ( "time" ) -// Source defines a source of metric data. -type Source func() float64 +// ObserverInput defines a source of metric data. +type ObserverInput func() float64 -// Sink defines a destination of collected metric data. -type Sink interface { +// ObserverOutput defines a destination of collected metric data. +type ObserverOutput interface { Set(value float64) } -// Observe triggers a cyclic metric observation goroutine. -func Observe( +// Observer represent a definition of a cyclic metric observation process. +type Observer struct { + input ObserverInput + output ObserverOutput +} + +// Observe triggers a cyclic metric observation process. +func (o *Observer) Observe( ctx context.Context, - source Source, - sink Sink, tick time.Duration, ) { go func() { @@ -27,7 +31,7 @@ func Observe( for { select { case <-ticker.C: - sink.Set(source()) + o.output.Set(o.input()) case <-ctx.Done(): return } diff --git a/pkg/metrics/observer_test.go b/pkg/metrics/observer_test.go index eb17dc6..97c2a75 100644 --- a/pkg/metrics/observer_test.go +++ b/pkg/metrics/observer_test.go @@ -7,17 +7,15 @@ import ( ) func TestObserve(t *testing.T) { + input := func() float64 { + return 5000 + } gauge := &Gauge{} ctx, _ := context.WithTimeout(context.Background(), 5*time.Millisecond) - Observe( - ctx, - func() float64 { - return 5000 - }, - gauge, - 1*time.Millisecond, - ) + observer := &Observer{input, gauge} + + observer.Observe(ctx, 1*time.Millisecond) <-ctx.Done() diff --git a/pkg/metrics/registry.go b/pkg/metrics/registry.go index 9cc5a12..41d2936 100644 --- a/pkg/metrics/registry.go +++ b/pkg/metrics/registry.go @@ -100,3 +100,18 @@ func (r *Registry) NewGauge(name string) (*Gauge, error) { r.metrics[name] = gauge return gauge, nil } + +func (r *Registry) NewGaugeObserver( + name string, + input ObserverInput, +) (*Observer, error) { + gauge, err := r.NewGauge(name) + if err != nil { + return nil, err + } + + return &Observer{ + input: input, + output: gauge, + }, nil +} From 481df8250e847fbac7007c6647c1e574bd4fac8d Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 12 Jun 2020 12:45:54 +0200 Subject: [PATCH 3/7] Allow additional metric labels --- pkg/metrics/registry.go | 51 +++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/pkg/metrics/registry.go b/pkg/metrics/registry.go index 41d2936..828ab4f 100644 --- a/pkg/metrics/registry.go +++ b/pkg/metrics/registry.go @@ -6,7 +6,7 @@ // types are implemented. The main motivation of creating a custom // package was a need to avoid usage of external unaudited dependencies. // -// Following specifications was used as reference: +// Following specifications were used as reference: // - https://prometheus.io/docs/instrumenting/writing_clientlibs/ // - https://prometheus.io/docs/instrumenting/exposition_formats/ package metrics @@ -28,22 +28,52 @@ type metric interface { expose() string } +// Label represents an arbitrary information which will be attached to all +// metrics managed by the registry. +type Label struct { + name string + value string +} + +// NewLabel creates a new label using the given name and value. +func NewLabel(name, value string) Label { + return Label{name, value} +} + // Registry performs all management of metrics. Specifically, it allows // to registering new metrics and exposing them through the metrics server. type Registry struct { - application string - identifier string + labels map[string]string metrics map[string]metric metricsMutex sync.RWMutex } // NewRegistry creates a new metrics registry. -func NewRegistry(application, identifier string) *Registry { +func NewRegistry( + application, identifier string, + additionalLabels ...Label, +) *Registry { + labels := map[string]string{ + "application": application, + "identifier": identifier, + } + + for _, additionalLabel := range additionalLabels { + if additionalLabel.name == "" || additionalLabel.value == "" { + continue + } + + if _, exists := labels[additionalLabel.name]; exists { + continue + } + + labels[additionalLabel.name] = additionalLabel.value + } + return &Registry{ - application: application, - identifier: identifier, - metrics: make(map[string]metric), + labels: labels, + metrics: make(map[string]metric), } } @@ -90,11 +120,8 @@ func (r *Registry) NewGauge(name string) (*Gauge, error) { } gauge := &Gauge{ - name: name, - labels: map[string]string{ - "application": r.application, - "identifier": r.identifier, - }, + name: name, + labels: r.labels, } r.metrics[name] = gauge From 12829c0d1e0fc67f0ad22951066e373678e0f2de Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 12 Jun 2020 13:18:01 +0200 Subject: [PATCH 4/7] Add new metric type - Info --- pkg/metrics/info.go | 27 +++++++++++++++++ pkg/metrics/info_test.go | 24 +++++++++++++++ pkg/metrics/registry.go | 65 ++++++++++++++++++++++++++++++++-------- 3 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 pkg/metrics/info.go create mode 100644 pkg/metrics/info_test.go diff --git a/pkg/metrics/info.go b/pkg/metrics/info.go new file mode 100644 index 0000000..a524429 --- /dev/null +++ b/pkg/metrics/info.go @@ -0,0 +1,27 @@ +package metrics + +import ( + "fmt" + "strings" +) + +// Info is a metric type that represents a constant information +// that cannot change in the time. +type Info struct { + name string + labels map[string]string +} + +// Exposes the info in the text-based exposition format. +func (i *Info) expose() string { + labelsStrings := make([]string, 0) + for name, value := range i.labels { + labelsStrings = append( + labelsStrings, + fmt.Sprintf("%v=\"%v\"", name, value), + ) + } + labels := strings.Join(labelsStrings, ",") + + return fmt.Sprintf("%v{%v} %v", i.name, labels, "1") +} diff --git a/pkg/metrics/info_test.go b/pkg/metrics/info_test.go new file mode 100644 index 0000000..bf5d8c4 --- /dev/null +++ b/pkg/metrics/info_test.go @@ -0,0 +1,24 @@ +package metrics + +import "testing" + +func TestInfoExpose(t *testing.T) { + info := &Info{ + name: "test_info", + labels: map[string]string{"label": "value"}, + } + + actualText := info.expose() + + expectedText := "test_info{label=\"value\"} 1" + + if actualText != expectedText { + t.Fatalf( + "incorrect gauge expose text:\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedText, + actualText, + ) + } +} diff --git a/pkg/metrics/registry.go b/pkg/metrics/registry.go index 828ab4f..0ce172d 100644 --- a/pkg/metrics/registry.go +++ b/pkg/metrics/registry.go @@ -28,8 +28,7 @@ type metric interface { expose() string } -// Label represents an arbitrary information which will be attached to all -// metrics managed by the registry. +// Label represents an arbitrary information attached to the metrics. type Label struct { name string value string @@ -54,11 +53,24 @@ func NewRegistry( application, identifier string, additionalLabels ...Label, ) *Registry { - labels := map[string]string{ - "application": application, - "identifier": identifier, + labels := mergeLabels( + map[string]string{ + "application": application, + "identifier": identifier, + }, + additionalLabels, + ) + + return &Registry{ + labels: labels, + metrics: make(map[string]metric), } +} +func mergeLabels( + labels map[string]string, + additionalLabels []Label, +) map[string]string { for _, additionalLabel := range additionalLabels { if additionalLabel.name == "" || additionalLabel.value == "" { continue @@ -71,10 +83,7 @@ func NewRegistry( labels[additionalLabel.name] = additionalLabel.value } - return &Registry{ - labels: labels, - metrics: make(map[string]metric), - } + return labels } // EnableServer enables the metrics server on the given port. Data will @@ -111,28 +120,35 @@ func (r *Registry) exposeMetrics() string { // NewGauge creates and registers a new gauge metric which will be exposed // through the metrics server. In case a metric already exists, an error // will be returned. -func (r *Registry) NewGauge(name string) (*Gauge, error) { +func (r *Registry) NewGauge( + name string, + additionalLabels ...Label, +) (*Gauge, error) { r.metricsMutex.Lock() defer r.metricsMutex.Unlock() if _, exists := r.metrics[name]; exists { - return nil, fmt.Errorf("gauge [%v] already exists", name) + return nil, fmt.Errorf("metric [%v] already exists", name) } gauge := &Gauge{ name: name, - labels: r.labels, + labels: mergeLabels(r.labels, additionalLabels), } r.metrics[name] = gauge return gauge, nil } +// NewGaugeObserver creates and registers a gauge just like `NewGauge` method +// and wrap it with a ready to use observer of the provided input. This allows +// to easily create self-refreshing metrics. func (r *Registry) NewGaugeObserver( name string, input ObserverInput, + additionalLabels ...Label, ) (*Observer, error) { - gauge, err := r.NewGauge(name) + gauge, err := r.NewGauge(name, additionalLabels...) if err != nil { return nil, err } @@ -142,3 +158,26 @@ func (r *Registry) NewGaugeObserver( output: gauge, }, nil } + +// NewInfo creates and registers a new info metric which will be exposed +// through the metrics server. In case a metric already exists, an error +// will be returned. +func (r *Registry) NewInfo( + name string, + additionalLabels ...Label, +) (*Info, error) { + r.metricsMutex.Lock() + defer r.metricsMutex.Unlock() + + if _, exists := r.metrics[name]; exists { + return nil, fmt.Errorf("metric [%v] already exists", name) + } + + info := &Info{ + name: name, + labels: mergeLabels(r.labels, additionalLabels), + } + + r.metrics[name] = info + return info, nil +} From e1944b1626252dbbf18ab2903e2d1e0dc91e4fd0 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 12 Jun 2020 14:14:39 +0200 Subject: [PATCH 5/7] Remove registry level labels Registry level labels are redundant and can cause pointless data-series growth. --- pkg/metrics/gauge.go | 6 +++- pkg/metrics/gauge_test.go | 23 +++++++++++++ pkg/metrics/registry.go | 67 +++++++++++++++--------------------- pkg/metrics/registry_test.go | 58 ++++++++++++++++++++++++++++++- 4 files changed, 112 insertions(+), 42 deletions(-) diff --git a/pkg/metrics/gauge.go b/pkg/metrics/gauge.go index 4cf64de..f7e34aa 100644 --- a/pkg/metrics/gauge.go +++ b/pkg/metrics/gauge.go @@ -43,7 +43,11 @@ func (g *Gauge) expose() string { } labels := strings.Join(labelsStrings, ",") - body := fmt.Sprintf("%v{%v} %v %v", g.name, labels, g.value, g.timestamp) + if len(labels) > 0 { + labels = "{" + labels + "}" + } + + body := fmt.Sprintf("%v%v %v %v", g.name, labels, g.value, g.timestamp) return fmt.Sprintf("%v\n%v", typeLine, body) } diff --git a/pkg/metrics/gauge_test.go b/pkg/metrics/gauge_test.go index 39f170b..0761f79 100644 --- a/pkg/metrics/gauge_test.go +++ b/pkg/metrics/gauge_test.go @@ -59,3 +59,26 @@ func TestGaugeExpose(t *testing.T) { ) } } + +func TestGaugeWithoutLabelsExpose(t *testing.T) { + gauge := &Gauge{ + name: "test_gauge", + labels: map[string]string{}, + value: 500, + timestamp: 1000, + } + + actualText := gauge.expose() + + expectedText := "# TYPE test_gauge gauge\ntest_gauge 500 1000" + + if actualText != expectedText { + t.Fatalf( + "incorrect gauge expose text:\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedText, + actualText, + ) + } +} diff --git a/pkg/metrics/registry.go b/pkg/metrics/registry.go index 0ce172d..51c0c2a 100644 --- a/pkg/metrics/registry.go +++ b/pkg/metrics/registry.go @@ -42,50 +42,17 @@ func NewLabel(name, value string) Label { // Registry performs all management of metrics. Specifically, it allows // to registering new metrics and exposing them through the metrics server. type Registry struct { - labels map[string]string - metrics map[string]metric metricsMutex sync.RWMutex } // NewRegistry creates a new metrics registry. -func NewRegistry( - application, identifier string, - additionalLabels ...Label, -) *Registry { - labels := mergeLabels( - map[string]string{ - "application": application, - "identifier": identifier, - }, - additionalLabels, - ) - +func NewRegistry() *Registry { return &Registry{ - labels: labels, metrics: make(map[string]metric), } } -func mergeLabels( - labels map[string]string, - additionalLabels []Label, -) map[string]string { - for _, additionalLabel := range additionalLabels { - if additionalLabel.name == "" || additionalLabel.value == "" { - continue - } - - if _, exists := labels[additionalLabel.name]; exists { - continue - } - - labels[additionalLabel.name] = additionalLabel.value - } - - return labels -} - // EnableServer enables the metrics server on the given port. Data will // be exposed on `/metrics` path. func (r *Registry) EnableServer(port int) { @@ -122,7 +89,7 @@ func (r *Registry) exposeMetrics() string { // will be returned. func (r *Registry) NewGauge( name string, - additionalLabels ...Label, + labels ...Label, ) (*Gauge, error) { r.metricsMutex.Lock() defer r.metricsMutex.Unlock() @@ -133,7 +100,7 @@ func (r *Registry) NewGauge( gauge := &Gauge{ name: name, - labels: mergeLabels(r.labels, additionalLabels), + labels: processLabels(labels), } r.metrics[name] = gauge @@ -146,9 +113,9 @@ func (r *Registry) NewGauge( func (r *Registry) NewGaugeObserver( name string, input ObserverInput, - additionalLabels ...Label, + labels ...Label, ) (*Observer, error) { - gauge, err := r.NewGauge(name, additionalLabels...) + gauge, err := r.NewGauge(name, labels...) if err != nil { return nil, err } @@ -164,7 +131,7 @@ func (r *Registry) NewGaugeObserver( // will be returned. func (r *Registry) NewInfo( name string, - additionalLabels ...Label, + labels []Label, ) (*Info, error) { r.metricsMutex.Lock() defer r.metricsMutex.Unlock() @@ -173,11 +140,31 @@ func (r *Registry) NewInfo( return nil, fmt.Errorf("metric [%v] already exists", name) } + if len(labels) == 0 { + return nil, fmt.Errorf("at least one label should be set") + } + info := &Info{ name: name, - labels: mergeLabels(r.labels, additionalLabels), + labels: processLabels(labels), } r.metrics[name] = info return info, nil } + +func processLabels( + labels []Label, +) map[string]string { + result := make(map[string]string) + + for _, label := range labels { + if label.name == "" || label.value == "" { + continue + } + + result[label.name] = label.value + } + + return result +} diff --git a/pkg/metrics/registry_test.go b/pkg/metrics/registry_test.go index a5ffe6b..a4927c9 100644 --- a/pkg/metrics/registry_test.go +++ b/pkg/metrics/registry_test.go @@ -5,7 +5,7 @@ import ( ) func TestRegistryNewGauge(t *testing.T) { - registry := NewRegistry("test-app", "test-id") + registry := NewRegistry() gauge, err := registry.NewGauge("test-gauge") if err != nil { @@ -20,3 +20,59 @@ func TestRegistryNewGauge(t *testing.T) { t.Fatalf("metric with name [%v] should exist in the registry", gauge.name) } } + +func TestRegistryNewGaugeObserver(t *testing.T) { + registry := NewRegistry() + + input := func() float64 { + return 1 + } + + _, err := registry.NewGaugeObserver("test-gauge", input) + if err != nil { + t.Fatal(err) + } + + if _, err = registry.NewGaugeObserver("test-gauge", input); err == nil { + t.Fatalf("should fail when creating gauge with the same name") + } + + if _, exists := registry.metrics["test-gauge"]; !exists { + t.Fatalf("metric with name [%v] should exist in the registry", "test-gauge") + } +} + +func TestRegistryNewInfo(t *testing.T) { + registry := NewRegistry() + + if _, err := registry.NewInfo("test-info", []Label{}); err == nil { + t.Fatalf("should fail when creating info without labels") + } + + label := NewLabel("label", "value") + info, err := registry.NewInfo("test-info", []Label{label}) + if err != nil { + t.Fatal(err) + } + + if _, err = registry.NewInfo("test-info", []Label{label}); err == nil { + t.Fatalf("should fail when creating info with the same name") + } + + if _, exists := registry.metrics[info.name]; !exists { + t.Fatalf("metric with name [%v] should exist in the registry", info.name) + } + + expectedLabelValue, exists := info.labels[label.name] + if !exists { + t.Fatalf("label with name [%v] should exist", label.name) + } + + if expectedLabelValue != label.value { + t.Fatalf( + "label with name [%v] should have value [%v]", + label.name, + label.value, + ) + } +} From 5908e5b9d942cffc913145515079075287e0508f Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 30 Jun 2020 13:17:47 +0200 Subject: [PATCH 6/7] Improve docs --- pkg/metrics/registry.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/metrics/registry.go b/pkg/metrics/registry.go index 51c0c2a..53f80eb 100644 --- a/pkg/metrics/registry.go +++ b/pkg/metrics/registry.go @@ -3,8 +3,7 @@ // // Currently, this package is intended to use with Prometheus but // can be easily extended if needed. Also, not all Prometheus metric -// types are implemented. The main motivation of creating a custom -// package was a need to avoid usage of external unaudited dependencies. +// types are implemented. // // Following specifications were used as reference: // - https://prometheus.io/docs/instrumenting/writing_clientlibs/ From 0bdcc12fcf4c05c512357c97e51dc2b8062a1f4f Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 1 Jul 2020 12:05:21 +0200 Subject: [PATCH 7/7] Timestamp fix --- pkg/metrics/gauge.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/metrics/gauge.go b/pkg/metrics/gauge.go index f7e34aa..05dd8e3 100644 --- a/pkg/metrics/gauge.go +++ b/pkg/metrics/gauge.go @@ -14,7 +14,7 @@ type Gauge struct { labels map[string]string value float64 - timestamp int64 + timestamp int64 // timestamp expressed as milliseconds mutex sync.RWMutex } @@ -24,7 +24,7 @@ func (g *Gauge) Set(value float64) { defer g.mutex.Unlock() g.value = value - g.timestamp = time.Now().UnixNano() / int64(time.Millisecond) + g.timestamp = time.Now().UnixNano() / 1e6 } // Exposes the gauge in the text-based exposition format.