Skip to content

Commit

Permalink
Refactor Prometheus exporter (#3239)
Browse files Browse the repository at this point in the history
This change will automatically register a created exporter with a prometheus registerer, if none are provided it will use prometheus' DefaultRegisterer.

Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com>
  • Loading branch information
MadVikingGod and hanyuancheung committed Oct 14, 2022
1 parent 1e72af4 commit b6a22ab
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 56 deletions.
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

0 comments on commit b6a22ab

Please sign in to comment.