From c63da1a742d265f07e43da5b19a8e0f78e0f9a18 Mon Sep 17 00:00:00 2001 From: Yuri Shkuro Date: Tue, 21 Jun 2022 13:24:47 -0400 Subject: [PATCH] Copy Prometheus metrics implementation from jaeger-lib Signed-off-by: Yuri Shkuro --- cmd/agent/app/agent_test.go | 6 +- cmd/agent/main.go | 4 +- cmd/all-in-one/main.go | 18 +- cmd/collector/main.go | 4 +- cmd/flags/service.go | 13 +- cmd/flags/service_test.go | 14 - cmd/query/main.go | 3 +- internal/metrics/expvar/adapter.go | 84 ++++ internal/metrics/expvar/adapter_test.go | 33 ++ internal/metrics/expvar/factory.go | 26 ++ internal/metrics/jlibadapter/adapter.go | 80 ++++ internal/metrics/jlibadapter/adapter_test.go | 32 ++ .../metrics/metricsbuilder}/builder.go | 74 +-- .../metrics/metricsbuilder}/builder_test.go | 17 +- internal/metrics/prometheus/cache.go | 86 ++++ internal/metrics/prometheus/factory.go | 312 +++++++++++++ internal/metrics/prometheus/factory_test.go | 433 ++++++++++++++++++ 17 files changed, 1120 insertions(+), 119 deletions(-) create mode 100644 internal/metrics/expvar/adapter.go create mode 100644 internal/metrics/expvar/adapter_test.go create mode 100644 internal/metrics/expvar/factory.go create mode 100644 internal/metrics/jlibadapter/adapter.go create mode 100644 internal/metrics/jlibadapter/adapter_test.go rename {pkg/metrics => internal/metrics/metricsbuilder}/builder.go (63%) rename {pkg/metrics => internal/metrics/metricsbuilder}/builder_test.go (88%) create mode 100644 internal/metrics/prometheus/cache.go create mode 100644 internal/metrics/prometheus/factory.go create mode 100644 internal/metrics/prometheus/factory_test.go diff --git a/cmd/agent/app/agent_test.go b/cmd/agent/app/agent_test.go index 9f1b709ec0d..70e2493ad84 100644 --- a/cmd/agent/app/agent_test.go +++ b/cmd/agent/app/agent_test.go @@ -29,8 +29,8 @@ import ( "go.uber.org/zap" "github.com/jaegertracing/jaeger/internal/metrics/fork" + "github.com/jaegertracing/jaeger/internal/metrics/metricsbuilder" "github.com/jaegertracing/jaeger/pkg/metrics" - jmetrics "github.com/jaegertracing/jaeger/pkg/metrics" "github.com/jaegertracing/jaeger/pkg/testutils" ) @@ -100,7 +100,7 @@ func withRunningAgent(t *testing.T, testcase func(string, chan error)) { }, } logger, logBuf := testutils.NewLogger() - mBldr := &jmetrics.Builder{HTTPRoute: "/metrics", Backend: "prometheus"} + mBldr := &metricsbuilder.Builder{HTTPRoute: "/metrics", Backend: "prometheus"} metricsFactory, err := mBldr.CreateMetricsFactory("jaeger") mFactory := fork.New("internal", metrics.NullFactory, metricsFactory) require.NoError(t, err) @@ -155,7 +155,7 @@ func TestStartStopRace(t *testing.T) { }, } logger, logBuf := testutils.NewEchoLogger(t) - mBldr := &jmetrics.Builder{HTTPRoute: "/metrics", Backend: "prometheus"} + mBldr := &metricsbuilder.Builder{HTTPRoute: "/metrics", Backend: "prometheus"} metricsFactory, err := mBldr.CreateMetricsFactory("jaeger") mFactory := fork.New("internal", metrics.NullFactory, metricsFactory) require.NoError(t, err) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index f88572ac22a..6f3a5054645 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -21,7 +21,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - jexpvar "github.com/uber/jaeger-lib/metrics/expvar" _ "go.uber.org/automaxprocs" "go.uber.org/zap" @@ -31,6 +30,7 @@ import ( "github.com/jaegertracing/jaeger/cmd/docs" "github.com/jaegertracing/jaeger/cmd/flags" "github.com/jaegertracing/jaeger/cmd/status" + "github.com/jaegertracing/jaeger/internal/metrics/expvar" "github.com/jaegertracing/jaeger/internal/metrics/fork" "github.com/jaegertracing/jaeger/pkg/config" "github.com/jaegertracing/jaeger/pkg/metrics" @@ -56,7 +56,7 @@ func main() { Namespace(metrics.NSOptions{Name: "jaeger"}). Namespace(metrics.NSOptions{Name: "agent"}) mFactory := fork.New("internal", - metrics.NewJLibAdapter(jexpvar.NewFactory(10)), // backend for internal opts + expvar.NewFactory(10), // backend for internal opts baseFactory) version.NewInfoMetrics(mFactory) diff --git a/cmd/all-in-one/main.go b/cmd/all-in-one/main.go index 32daf8d1683..d6dcf9867bd 100644 --- a/cmd/all-in-one/main.go +++ b/cmd/all-in-one/main.go @@ -26,8 +26,6 @@ import ( "github.com/spf13/viper" jaegerClientConfig "github.com/uber/jaeger-client-go/config" jaegerClientZapLog "github.com/uber/jaeger-client-go/log/zap" - jlibmetrics "github.com/uber/jaeger-lib/metrics" - jexpvar "github.com/uber/jaeger-lib/metrics/expvar" _ "go.uber.org/automaxprocs" "go.uber.org/zap" @@ -43,7 +41,9 @@ import ( queryApp "github.com/jaegertracing/jaeger/cmd/query/app" "github.com/jaegertracing/jaeger/cmd/query/app/querysvc" "github.com/jaegertracing/jaeger/cmd/status" + "github.com/jaegertracing/jaeger/internal/metrics/expvar" "github.com/jaegertracing/jaeger/internal/metrics/fork" + "github.com/jaegertracing/jaeger/internal/metrics/jlibadapter" "github.com/jaegertracing/jaeger/pkg/config" "github.com/jaegertracing/jaeger/pkg/metrics" "github.com/jaegertracing/jaeger/pkg/version" @@ -95,14 +95,13 @@ by default uses only in-memory database.`, if err := svc.Start(v); err != nil { return err } - logger := svc.Logger // shortcut - rootMetricsFactory := svc.MetricsFactory // shortcut + logger := svc.Logger // shortcut metricsFactory := fork.New("internal", - metrics.NewJLibAdapter(jexpvar.NewFactory(10)), // backend for internal opts - rootMetricsFactory.Namespace(metrics.NSOptions{Name: "jaeger"})) + expvar.NewFactory(10), // backend for internal opts + svc.MetricsFactory.Namespace(metrics.NSOptions{Name: "jaeger"})) version.NewInfoMetrics(metricsFactory) - tracerCloser := initTracer(svc.JLibMetricsFactory, svc.Logger) + tracerCloser := initTracer(svc) storageFactory.InitFromViper(v, logger) if err := storageFactory.Initialize(metricsFactory, logger); err != nil { @@ -284,7 +283,8 @@ func startQuery( return server } -func initTracer(metricsFactory jlibmetrics.Factory, logger *zap.Logger) io.Closer { +func initTracer(svc *flags.Service) io.Closer { + logger := svc.Logger traceCfg := &jaegerClientConfig.Configuration{ ServiceName: "jaeger-query", Sampler: &jaegerClientConfig.SamplerConfig{ @@ -298,7 +298,7 @@ func initTracer(metricsFactory jlibmetrics.Factory, logger *zap.Logger) io.Close logger.Fatal("Failed to read tracer configuration", zap.Error(err)) } tracer, closer, err := traceCfg.NewTracer( - jaegerClientConfig.Metrics(metricsFactory), + jaegerClientConfig.Metrics(jlibadapter.NewAdapter(svc.MetricsFactory)), jaegerClientConfig.Logger(jaegerClientZapLog.NewLogger(logger)), ) if err != nil { diff --git a/cmd/collector/main.go b/cmd/collector/main.go index ce70a3f2119..c295fc57fde 100644 --- a/cmd/collector/main.go +++ b/cmd/collector/main.go @@ -23,7 +23,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - jexpvar "github.com/uber/jaeger-lib/metrics/expvar" _ "go.uber.org/automaxprocs" "go.uber.org/zap" @@ -33,6 +32,7 @@ import ( "github.com/jaegertracing/jaeger/cmd/env" cmdFlags "github.com/jaegertracing/jaeger/cmd/flags" "github.com/jaegertracing/jaeger/cmd/status" + "github.com/jaegertracing/jaeger/internal/metrics/expvar" "github.com/jaegertracing/jaeger/internal/metrics/fork" "github.com/jaegertracing/jaeger/pkg/config" "github.com/jaegertracing/jaeger/pkg/metrics" @@ -72,7 +72,7 @@ func main() { logger := svc.Logger // shortcut baseFactory := svc.MetricsFactory.Namespace(metrics.NSOptions{Name: "jaeger"}) metricsFactory := fork.New("internal", - metrics.NewJLibAdapter(jexpvar.NewFactory(10)), // backend for internal opts + expvar.NewFactory(10), // backend for internal opts baseFactory.Namespace(metrics.NSOptions{Name: "collector"})) version.NewInfoMetrics(metricsFactory) diff --git a/cmd/flags/service.go b/cmd/flags/service.go index d079adc099c..291659573b8 100644 --- a/cmd/flags/service.go +++ b/cmd/flags/service.go @@ -24,9 +24,9 @@ import ( grpcZap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" "github.com/spf13/viper" - jlibmetrics "github.com/uber/jaeger-lib/metrics" "go.uber.org/zap" + "github.com/jaegertracing/jaeger/internal/metrics/metricsbuilder" "github.com/jaegertracing/jaeger/pkg/healthcheck" "github.com/jaegertracing/jaeger/pkg/metrics" "github.com/jaegertracing/jaeger/ports" @@ -49,8 +49,6 @@ type Service struct { // MetricsFactory is the root factory without a namespace. MetricsFactory metrics.Factory - JLibMetricsFactory jlibmetrics.Factory - signalsChannel chan os.Signal hcStatusChannel chan healthcheck.Status @@ -77,7 +75,7 @@ func (s *Service) AddFlags(flagSet *flag.FlagSet) { } else { AddFlags(flagSet) } - metrics.AddFlags(flagSet) + metricsbuilder.AddFlags(flagSet) s.Admin.AddFlags(flagSet) } @@ -106,17 +104,12 @@ func (s *Service) Start(v *viper.Viper) error { return fmt.Errorf("cannot create logger: %w", err) } - metricsBuilder := new(metrics.Builder).InitFromViper(v) + metricsBuilder := new(metricsbuilder.Builder).InitFromViper(v) metricsFactory, err := metricsBuilder.CreateMetricsFactory("") if err != nil { return fmt.Errorf("cannot create metrics factory: %w", err) } s.MetricsFactory = metricsFactory - if jlib, ok := metricsFactory.(*metrics.JLibAdapter); ok { - s.JLibMetricsFactory = jlib.Unwrap() - } else { - s.JLibMetricsFactory = jlibmetrics.NullFactory - } if err = s.Admin.initFromViper(v, s.Logger); err != nil { return fmt.Errorf("cannot initialize admin server: %w", err) diff --git a/cmd/flags/service_test.go b/cmd/flags/service_test.go index df4959181f0..7c7601f470b 100644 --- a/cmd/flags/service_test.go +++ b/cmd/flags/service_test.go @@ -22,12 +22,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - jlibmetrics "github.com/uber/jaeger-lib/metrics" "go.uber.org/atomic" "github.com/jaegertracing/jaeger/pkg/config" "github.com/jaegertracing/jaeger/pkg/healthcheck" - "github.com/jaegertracing/jaeger/pkg/metrics" ) func TestAddFlags(t *testing.T) { @@ -114,15 +112,3 @@ func waitForEqual(t *testing.T, expected interface{}, getter func() interface{}) } assert.Equal(t, expected, getter()) } - -func TestNullMetricsFactory(t *testing.T) { - v, cmd := config.Viperize(metrics.AddFlags) - err := cmd.ParseFlags([]string{ - "--metrics-backend=none", - }) - require.NoError(t, err) - - s := NewService(0) - s.Start(v) - assert.IsType(t, jlibmetrics.NullFactory, s.JLibMetricsFactory) -} diff --git a/cmd/query/main.go b/cmd/query/main.go index 30c6b178f7e..a7cbe340e93 100644 --- a/cmd/query/main.go +++ b/cmd/query/main.go @@ -34,6 +34,7 @@ import ( "github.com/jaegertracing/jaeger/cmd/query/app" "github.com/jaegertracing/jaeger/cmd/query/app/querysvc" "github.com/jaegertracing/jaeger/cmd/status" + "github.com/jaegertracing/jaeger/internal/metrics/jlibadapter" "github.com/jaegertracing/jaeger/pkg/bearertoken" "github.com/jaegertracing/jaeger/pkg/config" "github.com/jaegertracing/jaeger/pkg/metrics" @@ -86,7 +87,7 @@ func main() { logger.Fatal("Failed to read tracer configuration", zap.Error(err)) } tracer, closer, err := traceCfg.NewTracer( - jaegerClientConfig.Metrics(svc.JLibMetricsFactory), + jaegerClientConfig.Metrics(jlibadapter.NewAdapter(svc.MetricsFactory)), jaegerClientConfig.Logger(jaegerClientZapLog.NewLogger(logger)), ) if err != nil { diff --git a/internal/metrics/expvar/adapter.go b/internal/metrics/expvar/adapter.go new file mode 100644 index 00000000000..5baa587d54a --- /dev/null +++ b/internal/metrics/expvar/adapter.go @@ -0,0 +1,84 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expvar + +import ( + jlibmetrics "github.com/uber/jaeger-lib/metrics" + + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +// adapter is temporary type used to bridge metrics API in this package +// with that of jaeger-lib. +type adapter struct { + f jlibmetrics.Factory +} + +var _ metrics.Factory = (*adapter)(nil) + +func newAdapter(f jlibmetrics.Factory) *adapter { + return &adapter{f: f} +} + +// Counter creates a Counter. +func (a *adapter) Counter(opts metrics.Options) metrics.Counter { + return a.f.Counter(jlibmetrics.Options{ + Name: opts.Name, + Tags: opts.Tags, + Help: opts.Help, + }) +} + +// Timer creates a Timer. +func (a *adapter) Timer(opts metrics.TimerOptions) metrics.Timer { + return a.f.Timer(jlibmetrics.TimerOptions{ + Name: opts.Name, + Tags: opts.Tags, + Help: opts.Help, + Buckets: opts.Buckets, + }) +} + +// Gauge creates a Gauge. +func (a *adapter) Gauge(opts metrics.Options) metrics.Gauge { + return a.f.Gauge(jlibmetrics.Options{ + Name: opts.Name, + Tags: opts.Tags, + Help: opts.Help, + }) +} + +// Histogram creates a Histogram. +func (a *adapter) Histogram(opts metrics.HistogramOptions) metrics.Histogram { + return a.f.Histogram(jlibmetrics.HistogramOptions{ + Name: opts.Name, + Tags: opts.Tags, + Help: opts.Help, + Buckets: opts.Buckets, + }) +} + +// Namespace creates a Namespace. +func (a *adapter) Namespace(opts metrics.NSOptions) metrics.Factory { + return &adapter{f: a.f.Namespace(jlibmetrics.NSOptions{ + Name: opts.Name, + Tags: opts.Tags, + })} +} + +// Unwrap returns underlying jaeger-lib factory. +func (a *adapter) Unwrap() jlibmetrics.Factory { + return a.f +} diff --git a/internal/metrics/expvar/adapter_test.go b/internal/metrics/expvar/adapter_test.go new file mode 100644 index 00000000000..cf2ffeb5835 --- /dev/null +++ b/internal/metrics/expvar/adapter_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expvar + +import ( + "testing" + + jlibmetrics "github.com/uber/jaeger-lib/metrics" + + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +func TestAdapter(t *testing.T) { + f := newAdapter(jlibmetrics.NullFactory) + f.Counter(metrics.Options{}) + f.Timer(metrics.TimerOptions{}) + f.Gauge(metrics.Options{}) + f.Histogram(metrics.HistogramOptions{}) + f.Namespace(metrics.NSOptions{}) + f.Unwrap() +} diff --git a/internal/metrics/expvar/factory.go b/internal/metrics/expvar/factory.go new file mode 100644 index 00000000000..aca574a0cfc --- /dev/null +++ b/internal/metrics/expvar/factory.go @@ -0,0 +1,26 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expvar + +import ( + "github.com/uber/jaeger-lib/metrics/expvar" + + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +// NewFactory creates metrics.Factory for expvar. +func NewFactory(buckets int) metrics.Factory { + return newAdapter(expvar.NewFactory(buckets)) +} diff --git a/internal/metrics/jlibadapter/adapter.go b/internal/metrics/jlibadapter/adapter.go new file mode 100644 index 00000000000..026e0edacd2 --- /dev/null +++ b/internal/metrics/jlibadapter/adapter.go @@ -0,0 +1,80 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jlibadapter + +import ( + jlibmetrics "github.com/uber/jaeger-lib/metrics" + + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +// adapter is temporary type used to bridge metrics API in this package +// with that of jaeger-lib. +type adapter struct { + f metrics.Factory +} + +var _ jlibmetrics.Factory = (*adapter)(nil) + +// NewAdapter wraps internal metrics.Factory to look like jaeger-lib version. +func NewAdapter(f metrics.Factory) jlibmetrics.Factory { + return &adapter{f: f} +} + +// Counter creates a Counter. +func (a *adapter) Counter(opts jlibmetrics.Options) jlibmetrics.Counter { + return a.f.Counter(metrics.Options{ + Name: opts.Name, + Tags: opts.Tags, + Help: opts.Help, + }) +} + +// Timer creates a Timer. +func (a *adapter) Timer(opts jlibmetrics.TimerOptions) jlibmetrics.Timer { + return a.f.Timer(metrics.TimerOptions{ + Name: opts.Name, + Tags: opts.Tags, + Help: opts.Help, + Buckets: opts.Buckets, + }) +} + +// Gauge creates a Gauge. +func (a *adapter) Gauge(opts jlibmetrics.Options) jlibmetrics.Gauge { + return a.f.Gauge(metrics.Options{ + Name: opts.Name, + Tags: opts.Tags, + Help: opts.Help, + }) +} + +// Histogram creates a Histogram. +func (a *adapter) Histogram(opts jlibmetrics.HistogramOptions) jlibmetrics.Histogram { + return a.f.Histogram(metrics.HistogramOptions{ + Name: opts.Name, + Tags: opts.Tags, + Help: opts.Help, + Buckets: opts.Buckets, + }) +} + +// Namespace creates a Namespace. +func (a *adapter) Namespace(opts jlibmetrics.NSOptions) jlibmetrics.Factory { + return &adapter{f: a.f.Namespace(metrics.NSOptions{ + Name: opts.Name, + Tags: opts.Tags, + })} +} diff --git a/internal/metrics/jlibadapter/adapter_test.go b/internal/metrics/jlibadapter/adapter_test.go new file mode 100644 index 00000000000..55488c91894 --- /dev/null +++ b/internal/metrics/jlibadapter/adapter_test.go @@ -0,0 +1,32 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jlibadapter + +import ( + "testing" + + jlibmetrics "github.com/uber/jaeger-lib/metrics" + + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +func TestAdapter(t *testing.T) { + f := NewAdapter(metrics.NullFactory) + f.Counter(jlibmetrics.Options{}) + f.Timer(jlibmetrics.TimerOptions{}) + f.Gauge(jlibmetrics.Options{}) + f.Histogram(jlibmetrics.HistogramOptions{}) + f.Namespace(jlibmetrics.NSOptions{}) +} diff --git a/pkg/metrics/builder.go b/internal/metrics/metricsbuilder/builder.go similarity index 63% rename from pkg/metrics/builder.go rename to internal/metrics/metricsbuilder/builder.go index 16a6672bfba..173f0028383 100644 --- a/pkg/metrics/builder.go +++ b/internal/metrics/metricsbuilder/builder.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package metrics +package metricsbuilder import ( "errors" @@ -24,9 +24,10 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/viper" - "github.com/uber/jaeger-lib/metrics" - jexpvar "github.com/uber/jaeger-lib/metrics/expvar" - jprom "github.com/uber/jaeger-lib/metrics/prometheus" + + jexpvar "github.com/jaegertracing/jaeger/internal/metrics/expvar" + jprom "github.com/jaegertracing/jaeger/internal/metrics/prometheus" + "github.com/jaegertracing/jaeger/pkg/metrics" ) const ( @@ -67,19 +68,19 @@ func (b *Builder) InitFromViper(v *viper.Viper) *Builder { // CreateMetricsFactory creates a metrics factory based on the configured type of the backend. // If the metrics backend supports HTTP endpoint for scraping, it is stored in the builder and // can be later added by RegisterHandler function. -func (b *Builder) CreateMetricsFactory(namespace string) (Factory, error) { +func (b *Builder) CreateMetricsFactory(namespace string) (metrics.Factory, error) { if b.Backend == "prometheus" { metricsFactory := jprom.New().Namespace(metrics.NSOptions{Name: namespace, Tags: nil}) b.handler = promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{DisableCompression: true}) - return NewJLibAdapter(metricsFactory), nil + return metricsFactory, nil } if b.Backend == "expvar" { metricsFactory := jexpvar.NewFactory(10).Namespace(metrics.NSOptions{Name: namespace, Tags: nil}) b.handler = expvar.Handler() - return NewJLibAdapter(metricsFactory), nil + return metricsFactory, nil } if b.Backend == "none" || b.Backend == "" { - return NullFactory, nil + return metrics.NullFactory, nil } return nil, errUnknownBackend } @@ -88,60 +89,3 @@ func (b *Builder) CreateMetricsFactory(namespace string) (Factory, error) { func (b *Builder) Handler() http.Handler { return b.handler } - -// JLibAdapter is temporary type used to bridge metrics API in this package -// with that of jaeger-lib. -type JLibAdapter struct { - f metrics.Factory -} - -var _ Factory = (*JLibAdapter)(nil) - -func NewJLibAdapter(f metrics.Factory) *JLibAdapter { - return &JLibAdapter{f: f} -} - -func (a *JLibAdapter) Counter(opts Options) Counter { - return a.f.Counter(metrics.Options{ - Name: opts.Name, - Tags: opts.Tags, - Help: opts.Help, - }) -} - -func (a *JLibAdapter) Timer(opts TimerOptions) Timer { - return a.f.Timer(metrics.TimerOptions{ - Name: opts.Name, - Tags: opts.Tags, - Help: opts.Help, - Buckets: opts.Buckets, - }) -} - -func (a *JLibAdapter) Gauge(opts Options) Gauge { - return a.f.Gauge(metrics.Options{ - Name: opts.Name, - Tags: opts.Tags, - Help: opts.Help, - }) -} - -func (a *JLibAdapter) Histogram(opts HistogramOptions) Histogram { - return a.f.Histogram(metrics.HistogramOptions{ - Name: opts.Name, - Tags: opts.Tags, - Help: opts.Help, - Buckets: opts.Buckets, - }) -} - -func (a *JLibAdapter) Namespace(opts NSOptions) Factory { - return &JLibAdapter{f: a.f.Namespace(metrics.NSOptions{ - Name: opts.Name, - Tags: opts.Tags, - })} -} - -func (a *JLibAdapter) Unwrap() metrics.Factory { - return a.f -} diff --git a/pkg/metrics/builder_test.go b/internal/metrics/metricsbuilder/builder_test.go similarity index 88% rename from pkg/metrics/builder_test.go rename to internal/metrics/metricsbuilder/builder_test.go index df4c1ed02f5..a2f7731c8a9 100644 --- a/pkg/metrics/builder_test.go +++ b/internal/metrics/metricsbuilder/builder_test.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package metrics +package metricsbuilder import ( "expvar" @@ -25,7 +25,8 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/uber/jaeger-lib/metrics" + + "github.com/jaegertracing/jaeger/pkg/metrics" ) func TestAddFlags(t *testing.T) { @@ -114,7 +115,7 @@ func TestBuilder(t *testing.T) { continue } require.NotNil(t, mf) - mf.Counter(Options{Name: "counter", Tags: nil}).Inc(1) + mf.Counter(metrics.Options{Name: "counter", Tags: nil}).Inc(1) if testCase.assert != nil { testCase.assert() } @@ -123,13 +124,3 @@ func TestBuilder(t *testing.T) { } } } - -func TestJLibAdapter(t *testing.T) { - f := NewJLibAdapter(metrics.NullFactory) - f.Counter(Options{}) - f.Timer(TimerOptions{}) - f.Gauge(Options{}) - f.Histogram(HistogramOptions{}) - f.Namespace(NSOptions{}) - f.Unwrap() -} diff --git a/internal/metrics/prometheus/cache.go b/internal/metrics/prometheus/cache.go new file mode 100644 index 00000000000..40791ebb708 --- /dev/null +++ b/internal/metrics/prometheus/cache.go @@ -0,0 +1,86 @@ +// Copyright (c) 2017 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "strings" + "sync" + + "github.com/prometheus/client_golang/prometheus" +) + +// vectorCache is used to avoid creating Prometheus vectors with the same set of labels more than once. +type vectorCache struct { + registerer prometheus.Registerer + lock sync.Mutex + cVecs map[string]*prometheus.CounterVec + gVecs map[string]*prometheus.GaugeVec + hVecs map[string]*prometheus.HistogramVec +} + +func newVectorCache(registerer prometheus.Registerer) *vectorCache { + return &vectorCache{ + registerer: registerer, + cVecs: make(map[string]*prometheus.CounterVec), + gVecs: make(map[string]*prometheus.GaugeVec), + hVecs: make(map[string]*prometheus.HistogramVec), + } +} + +func (c *vectorCache) getOrMakeCounterVec(opts prometheus.CounterOpts, labelNames []string) *prometheus.CounterVec { + c.lock.Lock() + defer c.lock.Unlock() + + cacheKey := c.getCacheKey(opts.Name, labelNames) + cv, cvExists := c.cVecs[cacheKey] + if !cvExists { + cv = prometheus.NewCounterVec(opts, labelNames) + c.registerer.MustRegister(cv) + c.cVecs[cacheKey] = cv + } + return cv +} + +func (c *vectorCache) getOrMakeGaugeVec(opts prometheus.GaugeOpts, labelNames []string) *prometheus.GaugeVec { + c.lock.Lock() + defer c.lock.Unlock() + + cacheKey := c.getCacheKey(opts.Name, labelNames) + gv, gvExists := c.gVecs[cacheKey] + if !gvExists { + gv = prometheus.NewGaugeVec(opts, labelNames) + c.registerer.MustRegister(gv) + c.gVecs[cacheKey] = gv + } + return gv +} + +func (c *vectorCache) getOrMakeHistogramVec(opts prometheus.HistogramOpts, labelNames []string) *prometheus.HistogramVec { + c.lock.Lock() + defer c.lock.Unlock() + + cacheKey := c.getCacheKey(opts.Name, labelNames) + hv, hvExists := c.hVecs[cacheKey] + if !hvExists { + hv = prometheus.NewHistogramVec(opts, labelNames) + c.registerer.MustRegister(hv) + c.hVecs[cacheKey] = hv + } + return hv +} + +func (c *vectorCache) getCacheKey(name string, labels []string) string { + return strings.Join(append([]string{name}, labels...), "||") +} diff --git a/internal/metrics/prometheus/factory.go b/internal/metrics/prometheus/factory.go new file mode 100644 index 00000000000..0f8b3a959d7 --- /dev/null +++ b/internal/metrics/prometheus/factory.go @@ -0,0 +1,312 @@ +// Copyright (c) 2017 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "sort" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +// Factory implements metrics.Factory backed by Prometheus registry. +type Factory struct { + scope string + tags map[string]string + cache *vectorCache + buckets []float64 + normalizer *strings.Replacer + separator Separator +} + +var _ metrics.Factory = (*Factory)(nil) + +type options struct { + registerer prometheus.Registerer + buckets []float64 + separator Separator +} + +// Separator represents the namespace separator to use +type Separator rune + +const ( + // SeparatorUnderscore uses an underscore as separator + SeparatorUnderscore Separator = '_' + + // SeparatorColon uses a colon as separator + SeparatorColon = ':' +) + +// Option is a function that sets some option for the Factory constructor. +type Option func(*options) + +// WithRegisterer returns an option that sets the registerer. +// If not used we fallback to prometheus.DefaultRegisterer. +func WithRegisterer(registerer prometheus.Registerer) Option { + return func(opts *options) { + opts.registerer = registerer + } +} + +// WithBuckets returns an option that sets the default buckets for histogram. +// If not used, we fallback to default Prometheus buckets. +func WithBuckets(buckets []float64) Option { + return func(opts *options) { + opts.buckets = buckets + } +} + +// WithSeparator returns an option that sets the default separator for the namespace +// If not used, we fallback to underscore. +func WithSeparator(separator Separator) Option { + return func(opts *options) { + opts.separator = separator + } +} + +func applyOptions(opts []Option) *options { + options := new(options) + for _, o := range opts { + o(options) + } + if options.registerer == nil { + options.registerer = prometheus.DefaultRegisterer + } + if options.separator == '\x00' { + options.separator = SeparatorUnderscore + } + return options +} + +// New creates a Factory backed by Prometheus registry. +// Typically the first argument should be prometheus.DefaultRegisterer. +// +// Parameter buckets defines the buckets into which Timer observations are counted. +// Each element in the slice is the upper inclusive bound of a bucket. The +// values must be sorted in strictly increasing order. There is no need +// to add a highest bucket with +Inf bound, it will be added +// implicitly. The default value is prometheus.DefBuckets. +func New(opts ...Option) *Factory { + options := applyOptions(opts) + return newFactory( + &Factory{ // dummy struct to be discarded + cache: newVectorCache(options.registerer), + buckets: options.buckets, + normalizer: strings.NewReplacer(".", "_", "-", "_"), + separator: options.separator, + }, + "", // scope + nil) // tags +} + +func newFactory(parent *Factory, scope string, tags map[string]string) *Factory { + return &Factory{ + cache: parent.cache, + buckets: parent.buckets, + normalizer: parent.normalizer, + separator: parent.separator, + scope: scope, + tags: tags, + } +} + +// Counter implements Counter of metrics.Factory. +func (f *Factory) Counter(options metrics.Options) metrics.Counter { + help := strings.TrimSpace(options.Help) + if len(help) == 0 { + help = options.Name + } + name := counterNamingConvention(f.subScope(options.Name)) + tags := f.mergeTags(options.Tags) + labelNames := f.tagNames(tags) + opts := prometheus.CounterOpts{ + Name: name, + Help: help, + } + cv := f.cache.getOrMakeCounterVec(opts, labelNames) + return &counter{ + counter: cv.WithLabelValues(f.tagsAsLabelValues(labelNames, tags)...), + } +} + +// Gauge implements Gauge of metrics.Factory. +func (f *Factory) Gauge(options metrics.Options) metrics.Gauge { + help := strings.TrimSpace(options.Help) + if len(help) == 0 { + help = options.Name + } + name := f.subScope(options.Name) + tags := f.mergeTags(options.Tags) + labelNames := f.tagNames(tags) + opts := prometheus.GaugeOpts{ + Name: name, + Help: help, + } + gv := f.cache.getOrMakeGaugeVec(opts, labelNames) + return &gauge{ + gauge: gv.WithLabelValues(f.tagsAsLabelValues(labelNames, tags)...), + } +} + +// Timer implements Timer of metrics.Factory. +func (f *Factory) Timer(options metrics.TimerOptions) metrics.Timer { + help := strings.TrimSpace(options.Help) + if len(help) == 0 { + help = options.Name + } + name := f.subScope(options.Name) + buckets := f.selectBuckets(asFloatBuckets(options.Buckets)) + tags := f.mergeTags(options.Tags) + labelNames := f.tagNames(tags) + opts := prometheus.HistogramOpts{ + Name: name, + Help: help, + Buckets: buckets, + } + hv := f.cache.getOrMakeHistogramVec(opts, labelNames) + return &timer{ + histogram: hv.WithLabelValues(f.tagsAsLabelValues(labelNames, tags)...), + } +} + +func asFloatBuckets(buckets []time.Duration) []float64 { + data := make([]float64, len(buckets)) + for i := range data { + data[i] = float64(buckets[i]) / float64(time.Second) + } + return data +} + +// Histogram implements Histogram of metrics.Factory. +func (f *Factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { + help := strings.TrimSpace(options.Help) + if len(help) == 0 { + help = options.Name + } + name := f.subScope(options.Name) + buckets := f.selectBuckets(options.Buckets) + tags := f.mergeTags(options.Tags) + labelNames := f.tagNames(tags) + opts := prometheus.HistogramOpts{ + Name: name, + Help: help, + Buckets: buckets, + } + hv := f.cache.getOrMakeHistogramVec(opts, labelNames) + return &histogram{ + histogram: hv.WithLabelValues(f.tagsAsLabelValues(labelNames, tags)...), + } +} + +// Namespace implements Namespace of metrics.Factory. +func (f *Factory) Namespace(scope metrics.NSOptions) metrics.Factory { + return newFactory(f, f.subScope(scope.Name), f.mergeTags(scope.Tags)) +} + +type counter struct { + counter prometheus.Counter +} + +func (c *counter) Inc(v int64) { + c.counter.Add(float64(v)) +} + +type gauge struct { + gauge prometheus.Gauge +} + +func (g *gauge) Update(v int64) { + g.gauge.Set(float64(v)) +} + +type observer interface { + Observe(v float64) +} + +type timer struct { + histogram observer +} + +func (t *timer) Record(v time.Duration) { + t.histogram.Observe(float64(v.Nanoseconds()) / float64(time.Second/time.Nanosecond)) +} + +type histogram struct { + histogram observer +} + +func (h *histogram) Record(v float64) { + h.histogram.Observe(v) +} + +func (f *Factory) subScope(name string) string { + if f.scope == "" { + return f.normalize(name) + } + if name == "" { + return f.normalize(f.scope) + } + return f.normalize(f.scope + string(f.separator) + name) +} + +func (f *Factory) normalize(v string) string { + return f.normalizer.Replace(v) +} + +func (f *Factory) mergeTags(tags map[string]string) map[string]string { + ret := make(map[string]string, len(f.tags)+len(tags)) + for k, v := range f.tags { + ret[k] = v + } + for k, v := range tags { + ret[k] = v + } + return ret +} + +func (f *Factory) tagNames(tags map[string]string) []string { + ret := make([]string, 0, len(tags)) + for k := range tags { + ret = append(ret, k) + } + sort.Strings(ret) + return ret +} + +func (f *Factory) tagsAsLabelValues(labels []string, tags map[string]string) []string { + ret := make([]string, 0, len(tags)) + for _, l := range labels { + ret = append(ret, tags[l]) + } + return ret +} + +func (f *Factory) selectBuckets(buckets []float64) []float64 { + if len(buckets) > 0 { + return buckets + } + return f.buckets +} + +func counterNamingConvention(name string) string { + if !strings.HasSuffix(name, "_total") { + name += "_total" + } + return name +} diff --git a/internal/metrics/prometheus/factory_test.go b/internal/metrics/prometheus/factory_test.go new file mode 100644 index 00000000000..60d64335efd --- /dev/null +++ b/internal/metrics/prometheus/factory_test.go @@ -0,0 +1,433 @@ +// Copyright (c) 2017 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus_test + +import ( + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + promModel "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + . "github.com/jaegertracing/jaeger/internal/metrics/prometheus" + "github.com/jaegertracing/jaeger/pkg/metrics" +) + +func TestOptions(t *testing.T) { + f1 := New() + assert.NotNil(t, f1) +} + +func TestSeparator(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry), WithSeparator(SeparatorColon)) + c1 := f1.Namespace(metrics.NSOptions{ + Name: "bender", + }).Counter(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"a": "b"}, + Help: "Help message", + }) + c1.Inc(1) + snapshot, err := registry.Gather() + require.NoError(t, err) + m1 := findMetric(t, snapshot, "bender:rodriguez_total", map[string]string{"a": "b"}) + assert.EqualValues(t, 1, m1.GetCounter().GetValue(), "%+v", m1) +} + +func TestCounter(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + fDummy := f1.Namespace(metrics.NSOptions{}) + f2 := fDummy.Namespace(metrics.NSOptions{ + Name: "bender", + Tags: map[string]string{"a": "b"}, + }) + f3 := f2.Namespace(metrics.NSOptions{}) + + c1 := f2.Counter(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + Help: "Help message", + }) + c2 := f2.Counter(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) + c3 := f3.Counter(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) // same tags as c2, but from f3 + c1.Inc(1) + c1.Inc(2) + c2.Inc(3) + c3.Inc(4) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "Help message", snapshot[0].GetHelp()) + + m1 := findMetric(t, snapshot, "bender_rodriguez_total", map[string]string{"a": "b", "x": "y"}) + assert.EqualValues(t, 3, m1.GetCounter().GetValue(), "%+v", m1) + + m2 := findMetric(t, snapshot, "bender_rodriguez_total", map[string]string{"a": "b", "x": "z"}) + assert.EqualValues(t, 7, m2.GetCounter().GetValue(), "%+v", m2) +} + +func TestCounterDefaultHelp(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + c1 := f1.Counter(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + }) + c1.Inc(1) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "rodriguez", snapshot[0].GetHelp()) +} + +func TestGauge(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + f2 := f1.Namespace(metrics.NSOptions{ + Name: "bender", + Tags: map[string]string{"a": "b"}, + }) + f3 := f2.Namespace(metrics.NSOptions{ + Tags: map[string]string{"a": "b"}, + }) // essentially same as f2 + g1 := f2.Gauge(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + Help: "Help message", + }) + g2 := f2.Gauge(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) + g3 := f3.Gauge(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) // same as g2, but from f3 + g1.Update(1) + g1.Update(2) + g2.Update(3) + g3.Update(4) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "Help message", snapshot[0].GetHelp()) + + m1 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "y"}) + assert.EqualValues(t, 2, m1.GetGauge().GetValue(), "%+v", m1) + + m2 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "z"}) + assert.EqualValues(t, 4, m2.GetGauge().GetValue(), "%+v", m2) +} + +func TestGaugeDefaultHelp(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + g1 := f1.Gauge(metrics.Options{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + }) + g1.Update(1) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "rodriguez", snapshot[0].GetHelp()) +} + +func TestTimer(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + f2 := f1.Namespace(metrics.NSOptions{ + Name: "bender", + Tags: map[string]string{"a": "b"}, + }) + f3 := f2.Namespace(metrics.NSOptions{ + Tags: map[string]string{"a": "b"}, + }) // essentially same as f2 + t1 := f2.Timer(metrics.TimerOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + Help: "Help message", + }) + t2 := f2.Timer(metrics.TimerOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) + t3 := f3.Timer(metrics.TimerOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) // same as t2, but from f3 + t1.Record(1 * time.Second) + t1.Record(2 * time.Second) + t2.Record(3 * time.Second) + t3.Record(4 * time.Second) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "Help message", snapshot[0].GetHelp()) + + m1 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "y"}) + assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) + assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) + for _, bucket := range m1.GetHistogram().GetBucket() { + if bucket.GetUpperBound() < 1 { + assert.EqualValues(t, 0, bucket.GetCumulativeCount()) + } else if bucket.GetUpperBound() < 2 { + assert.EqualValues(t, 1, bucket.GetCumulativeCount()) + } else { + assert.EqualValues(t, 2, bucket.GetCumulativeCount()) + } + } + + m2 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "z"}) + assert.EqualValues(t, 2, m2.GetHistogram().GetSampleCount(), "%+v", m2) + assert.EqualValues(t, 7, m2.GetHistogram().GetSampleSum(), "%+v", m2) + for _, bucket := range m2.GetHistogram().GetBucket() { + if bucket.GetUpperBound() < 3 { + assert.EqualValues(t, 0, bucket.GetCumulativeCount()) + } else if bucket.GetUpperBound() < 4 { + assert.EqualValues(t, 1, bucket.GetCumulativeCount()) + } else { + assert.EqualValues(t, 2, bucket.GetCumulativeCount()) + } + } +} + +func TestTimerDefaultHelp(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + t1 := f1.Timer(metrics.TimerOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + }) + t1.Record(1 * time.Second) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "rodriguez", snapshot[0].GetHelp()) +} + +func TestTimerCustomBuckets(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry), WithBuckets([]float64{1.5})) + // dot and dash in the metric name will be replaced with underscore + t1 := f1.Timer(metrics.TimerOptions{ + Name: "bender.bending-rodriguez", + Tags: map[string]string{"x": "y"}, + Buckets: []time.Duration{time.Nanosecond, 5 * time.Nanosecond}, + }) + t1.Record(1 * time.Second) + t1.Record(2 * time.Second) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) + assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) + assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) + assert.Len(t, m1.GetHistogram().GetBucket(), 2) +} + +func TestTimerDefaultBuckets(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry), WithBuckets([]float64{1.5, 2})) + // dot and dash in the metric name will be replaced with underscore + t1 := f1.Timer(metrics.TimerOptions{ + Name: "bender.bending-rodriguez", + Tags: map[string]string{"x": "y"}, + Buckets: nil, + }) + t1.Record(1 * time.Second) + t1.Record(2 * time.Second) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) + assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) + assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) + assert.Len(t, m1.GetHistogram().GetBucket(), 2) +} + +func TestHistogram(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + f2 := f1.Namespace(metrics.NSOptions{ + Name: "bender", + Tags: map[string]string{"a": "b"}, + }) + f3 := f2.Namespace(metrics.NSOptions{ + Tags: map[string]string{"a": "b"}, + }) // essentially same as f2 + t1 := f2.Histogram(metrics.HistogramOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + Help: "Help message", + }) + t2 := f2.Histogram(metrics.HistogramOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) + t3 := f3.Histogram(metrics.HistogramOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) // same as t2, but from f3 + t1.Record(1) + t1.Record(2) + t2.Record(3) + t3.Record(4) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "Help message", snapshot[0].GetHelp()) + + m1 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "y"}) + assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) + assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) + for _, bucket := range m1.GetHistogram().GetBucket() { + if bucket.GetUpperBound() < 1 { + assert.EqualValues(t, 0, bucket.GetCumulativeCount()) + } else if bucket.GetUpperBound() < 2 { + assert.EqualValues(t, 1, bucket.GetCumulativeCount()) + } else { + assert.EqualValues(t, 2, bucket.GetCumulativeCount()) + } + } + + m2 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "z"}) + assert.EqualValues(t, 2, m2.GetHistogram().GetSampleCount(), "%+v", m2) + assert.EqualValues(t, 7, m2.GetHistogram().GetSampleSum(), "%+v", m2) + for _, bucket := range m2.GetHistogram().GetBucket() { + if bucket.GetUpperBound() < 3 { + assert.EqualValues(t, 0, bucket.GetCumulativeCount()) + } else if bucket.GetUpperBound() < 4 { + assert.EqualValues(t, 1, bucket.GetCumulativeCount()) + } else { + assert.EqualValues(t, 2, bucket.GetCumulativeCount()) + } + } +} + +func TestHistogramDefaultHelp(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + t1 := f1.Histogram(metrics.HistogramOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + }) + t1.Record(1) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "rodriguez", snapshot[0].GetHelp()) +} + +func TestHistogramCustomBuckets(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + // dot and dash in the metric name will be replaced with underscore + t1 := f1.Histogram(metrics.HistogramOptions{ + Name: "bender.bending-rodriguez", + Tags: map[string]string{"x": "y"}, + Buckets: []float64{1.5}, + }) + t1.Record(1) + t1.Record(2) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) + assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) + assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) + assert.Len(t, m1.GetHistogram().GetBucket(), 1) +} + +func TestHistogramDefaultBuckets(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry), WithBuckets([]float64{1.5})) + // dot and dash in the metric name will be replaced with underscore + t1 := f1.Histogram(metrics.HistogramOptions{ + Name: "bender.bending-rodriguez", + Tags: map[string]string{"x": "y"}, + Buckets: nil, + }) + t1.Record(1) + t1.Record(2) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) + assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) + assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) + assert.Len(t, m1.GetHistogram().GetBucket(), 1) +} + +func findMetric(t *testing.T, snapshot []*promModel.MetricFamily, name string, tags map[string]string) *promModel.Metric { + for _, mf := range snapshot { + if mf.GetName() != name { + continue + } + for _, m := range mf.GetMetric() { + if len(m.GetLabel()) != len(tags) { + t.Fatalf("Mismatching labels for metric %v: want %v, have %v", name, tags, m.GetLabel()) + } + match := true + for _, l := range m.GetLabel() { + if v, ok := tags[l.GetName()]; !ok || v != l.GetValue() { + match = false + } + } + if match { + return m + } + } + } + t.Logf("Cannot find metric %v %v", name, tags) + for _, nf := range snapshot { + t.Logf("Family: %v", nf.GetName()) + for _, m := range nf.GetMetric() { + t.Logf("==> %v", m) + } + } + t.FailNow() + return nil +}