Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Prometheus exporter #3239

Merged
merged 13 commits into from Oct 14, 2022
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
25 changes: 10 additions & 15 deletions example/prometheus/main.go
Expand Up @@ -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"
)
Expand All @@ -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"),
Expand Down Expand Up @@ -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
Expand Down
21 changes: 8 additions & 13 deletions example/view/main.go
Expand Up @@ -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"
Expand All @@ -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(
Expand Down Expand Up @@ -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"),
Expand All @@ -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
Expand Down
8 changes: 3 additions & 5 deletions exporters/prometheus/benchmark_test.go
Expand Up @@ -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))
Expand Down
60 changes: 60 additions & 0 deletions 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)
})
}
}
59 changes: 59 additions & 0 deletions 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
})
}
40 changes: 22 additions & 18 deletions exporters/prometheus/exporter.go
Expand Up @@ -16,6 +16,7 @@ package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus"

import (
"context"
"fmt"
"sort"
"strings"
"unicode"
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
8 changes: 3 additions & 5 deletions exporters/prometheus/exporter_test.go
Expand Up @@ -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_*"),
Expand All @@ -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)
Expand Down