diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a28deaad2..87d13296190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- Prometheus exporter will register with a prometheus registerer on creation, there are options to control this. (#3239) + ### Changed - `sdktrace.TraceProvider.Shutdown` and `sdktrace.TraceProvider.ForceFlush` to not return error when no processor register. (#3268) +- The `"go.opentelemetry.io/otel/exporters/prometheus".New` now also returns an error indicating the failure to register the exporter with Prometheus. (#3239) ### Fixed diff --git a/example/prometheus/main.go b/example/prometheus/main.go index 0517da33c2f..e1d87693c22 100644 --- a/example/prometheus/main.go +++ b/example/prometheus/main.go @@ -22,11 +22,10 @@ import ( "os" "os/signal" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/otel/attribute" - otelprom "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/exporters/prometheus" "go.opentelemetry.io/otel/metric/instrument" "go.opentelemetry.io/otel/sdk/metric" ) @@ -37,12 +36,15 @@ func main() { // The exporter embeds a default OpenTelemetry Reader and // implements prometheus.Collector, allowing it to be used as // both a Reader and Collector. - exporter := otelprom.New() + exporter, err := prometheus.New() + if err != nil { + log.Fatal(err) + } provider := metric.NewMeterProvider(metric.WithReader(exporter)) meter := provider.Meter("github.com/open-telemetry/opentelemetry-go/example/prometheus") // Start the prometheus HTTP server and pass the exporter Collector to it - go serveMetrics(exporter.Collector) + go serveMetrics() attrs := []attribute.KeyValue{ attribute.Key("A").String("B"), @@ -77,17 +79,10 @@ func main() { <-ctx.Done() } -func serveMetrics(collector prometheus.Collector) { - registry := prometheus.NewRegistry() - err := registry.Register(collector) - if err != nil { - fmt.Printf("error registering collector: %v", err) - return - } - - log.Printf("serving metrics at localhost:2222/metrics") - http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) - err = http.ListenAndServe(":2222", nil) +func serveMetrics() { + log.Printf("serving metrics at localhost:2223/metrics") + http.Handle("/metrics", promhttp.Handler()) + err := http.ListenAndServe(":2223", nil) if err != nil { fmt.Printf("error serving http: %v", err) return diff --git a/example/view/main.go b/example/view/main.go index 872e12dde1f..c8f1b246590 100644 --- a/example/view/main.go +++ b/example/view/main.go @@ -22,7 +22,6 @@ import ( "os" "os/signal" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/otel/attribute" @@ -40,7 +39,10 @@ func main() { ctx := context.Background() // The exporter embeds a default OpenTelemetry Reader, allowing it to be used in WithReader. - exporter := otelprom.New() + exporter, err := otelprom.New() + if err != nil { + log.Fatal(err) + } // View to customize histogram buckets and rename a single histogram instrument. customBucketsView, err := view.New( @@ -68,7 +70,7 @@ func main() { meter := provider.Meter(meterName) // Start the prometheus HTTP server and pass the exporter Collector to it - go serveMetrics(exporter.Collector) + go serveMetrics() attrs := []attribute.KeyValue{ attribute.Key("A").String("B"), @@ -94,17 +96,10 @@ func main() { <-ctx.Done() } -func serveMetrics(collector prometheus.Collector) { - registry := prometheus.NewRegistry() - err := registry.Register(collector) - if err != nil { - fmt.Printf("error registering collector: %v", err) - return - } - +func serveMetrics() { log.Printf("serving metrics at localhost:2222/metrics") - http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) - err = http.ListenAndServe(":2222", nil) + http.Handle("/metrics", promhttp.Handler()) + err := http.ListenAndServe(":2222", nil) if err != nil { fmt.Printf("error serving http: %v", err) return diff --git a/exporters/prometheus/benchmark_test.go b/exporters/prometheus/benchmark_test.go index dee0814ed75..6c6d67cae76 100644 --- a/exporters/prometheus/benchmark_test.go +++ b/exporters/prometheus/benchmark_test.go @@ -27,13 +27,11 @@ import ( func benchmarkCollect(b *testing.B, n int) { ctx := context.Background() - exporter := New() - provider := metric.NewMeterProvider(metric.WithReader(exporter)) - meter := provider.Meter("testmeter") - registry := prometheus.NewRegistry() - err := registry.Register(exporter.Collector) + exporter, err := New(WithRegisterer(registry)) require.NoError(b, err) + provider := metric.NewMeterProvider(metric.WithReader(exporter)) + meter := provider.Meter("testmeter") for i := 0; i < n; i++ { counter, err := meter.SyncFloat64().Counter(fmt.Sprintf("foo_%d", i)) diff --git a/exporters/prometheus/confg_test.go b/exporters/prometheus/confg_test.go new file mode 100644 index 00000000000..91893d40f3a --- /dev/null +++ b/exporters/prometheus/confg_test.go @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry 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 "go.opentelemetry.io/otel/exporters/prometheus" + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" +) + +func TestNewConfig(t *testing.T) { + registry := prometheus.NewRegistry() + + testCases := []struct { + name string + options []Option + wantRegisterer prometheus.Registerer + }{ + { + name: "Default", + options: nil, + wantRegisterer: prometheus.DefaultRegisterer, + }, + + { + name: "WithRegisterer", + options: []Option{ + WithRegisterer(registry), + }, + wantRegisterer: registry, + }, + { + name: "nil options do nothing", + options: []Option{ + WithRegisterer(nil), + }, + wantRegisterer: prometheus.DefaultRegisterer, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + cfg := newConfig(tt.options...) + + assert.Equal(t, tt.wantRegisterer, cfg.registerer) + }) + } +} diff --git a/exporters/prometheus/config.go b/exporters/prometheus/config.go new file mode 100644 index 00000000000..6ee84732556 --- /dev/null +++ b/exporters/prometheus/config.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry 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 "go.opentelemetry.io/otel/exporters/prometheus" + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +// config contains options for the exporter. +type config struct { + registerer prometheus.Registerer +} + +// newConfig creates a validated config configured with options. +func newConfig(opts ...Option) config { + cfg := config{} + for _, opt := range opts { + cfg = opt.apply(cfg) + } + + if cfg.registerer == nil { + cfg.registerer = prometheus.DefaultRegisterer + } + + return cfg +} + +// Option sets exporter option values. +type Option interface { + apply(config) config +} + +type optionFunc func(config) config + +func (fn optionFunc) apply(cfg config) config { + return fn(cfg) +} + +// WithRegisterer configures which prometheus Registerer the Exporter will +// register with. If no registerer is used the prometheus DefaultRegisterer is +// used. +func WithRegisterer(reg prometheus.Registerer) Option { + return optionFunc(func(cfg config) config { + cfg.registerer = reg + return cfg + }) +} diff --git a/exporters/prometheus/exporter.go b/exporters/prometheus/exporter.go index 8b4c7f56114..007dc2f50d9 100644 --- a/exporters/prometheus/exporter.go +++ b/exporters/prometheus/exporter.go @@ -16,6 +16,7 @@ package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus" import ( "context" + "fmt" "sort" "strings" "unicode" @@ -33,39 +34,42 @@ import ( // interface for easy instantiation with a MeterProvider. type Exporter struct { metric.Reader - Collector prometheus.Collector } +var _ metric.Reader = &Exporter{} + // collector is used to implement prometheus.Collector. type collector struct { - metric.Reader -} - -// config is added here to allow for options expansion in the future. -type config struct{} - -// Option may be used in the future to apply options to a Prometheus Exporter config. -type Option interface { - apply(config) config + reader metric.Reader } // New returns a Prometheus Exporter. -func New(_ ...Option) Exporter { +func New(opts ...Option) (*Exporter, error) { + cfg := newConfig(opts...) + // this assumes that the default temporality selector will always return cumulative. // we only support cumulative temporality, so building our own reader enforces this. + // TODO (#3244): Enable some way to configure the reader, but not change temporality. reader := metric.NewManualReader() - e := Exporter{ + + collector := &collector{ + reader: reader, + } + + if err := cfg.registerer.Register(collector); err != nil { + return nil, fmt.Errorf("cannot register the collector: %w", err) + } + + e := &Exporter{ Reader: reader, - Collector: &collector{ - Reader: reader, - }, } - return e + + return e, nil } // Describe implements prometheus.Collector. func (c *collector) Describe(ch chan<- *prometheus.Desc) { - metrics, err := c.Reader.Collect(context.TODO()) + metrics, err := c.reader.Collect(context.TODO()) if err != nil { otel.Handle(err) } @@ -76,7 +80,7 @@ func (c *collector) Describe(ch chan<- *prometheus.Desc) { // Collect implements prometheus.Collector. func (c *collector) Collect(ch chan<- prometheus.Metric) { - metrics, err := c.Reader.Collect(context.TODO()) + metrics, err := c.reader.Collect(context.TODO()) if err != nil { otel.Handle(err) } diff --git a/exporters/prometheus/exporter_test.go b/exporters/prometheus/exporter_test.go index 55db838dcd2..5a1a65ce58d 100644 --- a/exporters/prometheus/exporter_test.go +++ b/exporters/prometheus/exporter_test.go @@ -137,8 +137,10 @@ func TestPrometheusExporter(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() + registry := prometheus.NewRegistry() - exporter := New() + exporter, err := New(WithRegisterer(registry)) + require.NoError(t, err) customBucketsView, err := view.New( view.MatchInstrumentName("histogram_*"), @@ -153,10 +155,6 @@ func TestPrometheusExporter(t *testing.T) { provider := metric.NewMeterProvider(metric.WithReader(exporter, customBucketsView, defaultView)) meter := provider.Meter("testmeter") - registry := prometheus.NewRegistry() - err = registry.Register(exporter.Collector) - require.NoError(t, err) - tc.recordMetrics(ctx, meter) file, err := os.Open(tc.expectedFile)