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
2 changes: 1 addition & 1 deletion example/prometheus/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module go.opentelemetry.io/otel/example/prometheus
go 1.18

require (
github.com/prometheus/client_golang v1.13.0
go.opentelemetry.io/otel v1.10.0
go.opentelemetry.io/otel/exporters/prometheus v0.32.1
go.opentelemetry.io/otel/metric v0.32.1
Expand All @@ -17,6 +16,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
Expand Down
27 changes: 10 additions & 17 deletions example/prometheus/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@ 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 +34,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(exporter)

attrs := []attribute.KeyValue{
attribute.Key("A").String("B"),
Expand Down Expand Up @@ -77,17 +77,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(exp *prometheus.Exporter) {
log.Printf("serving metrics at localhost:2223/metrics")
http.Handle("/metrics", exp)
err := http.ListenAndServe(":2223", nil)
if err != nil {
fmt.Printf("error serving http: %v", err)
return
Expand Down
5 changes: 4 additions & 1 deletion example/view/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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
8 changes: 3 additions & 5 deletions exporters/prometheus/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,12 @@ import (

func benchmarkCollect(b *testing.B, n int) {
ctx := context.Background()
exporter := New()
registry := prometheus.NewRegistry() // This is the default behavior, this is used to manually gather.
exporter, err := New(WithRegistry(registry))
require.NoError(b, err)
provider := metric.NewMeterProvider(metric.WithReader(exporter))
meter := provider.Meter("testmeter")

registry := prometheus.NewRegistry()
err := registry.Register(exporter.Collector)
require.NoError(b, err)

for i := 0; i < n; i++ {
counter, err := meter.SyncFloat64().Counter(fmt.Sprintf("foo_%d", i))
require.NoError(b, err)
Expand Down
109 changes: 109 additions & 0 deletions exporters/prometheus/confg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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 (
"context"
"testing"

"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)

type testExporter struct{}

func (e testExporter) Export(_ context.Context, _ metricdata.ResourceMetrics) error {
return nil
}

func (e testExporter) ForceFlush(_ context.Context) error {
return nil
}

func (e testExporter) Shutdown(_ context.Context) error {
return nil
}

func TestNewConfig(t *testing.T) {
registry := prometheus.NewRegistry()

testCases := []struct {
name string
options []Option
wantReaderType metric.Reader
wantRegisterer prometheus.Registerer
wantGatherer prometheus.Gatherer
}{
{
name: "Default",
options: nil,
wantReaderType: metric.NewManualReader(),
wantRegisterer: prometheus.DefaultRegisterer,
wantGatherer: prometheus.DefaultGatherer,
},
{
name: "WithReader",
options: []Option{
WithReader(metric.NewPeriodicReader(testExporter{})),
},
wantReaderType: metric.NewPeriodicReader(testExporter{}),
wantRegisterer: prometheus.DefaultRegisterer,
wantGatherer: prometheus.DefaultGatherer,
},
{
name: "WithRegistry",
options: []Option{
WithRegistry(registry),
},
wantReaderType: metric.NewManualReader(),
wantRegisterer: registry,
wantGatherer: registry,
},
{
name: "Multiple Options",
options: []Option{
WithReader(metric.NewPeriodicReader(testExporter{})),
WithRegistry(registry),
},
wantReaderType: metric.NewPeriodicReader(testExporter{}),
wantRegisterer: registry,
wantGatherer: registry,
},
{
name: "nil options do nothing",
options: []Option{
WithReader(nil),
WithRegistry(nil),
},
wantReaderType: metric.NewManualReader(),
wantRegisterer: prometheus.DefaultRegisterer,
wantGatherer: prometheus.DefaultGatherer,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
cfg := newConfig(tt.options...)

// If no reader is provided you should get a new ManualReader.
assert.IsType(t, tt.wantReaderType, cfg.reader)

// If no Registry is provided you should get the DefaultRegisterer and DefaultGatherer.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
assert.Equal(t, tt.wantRegisterer, cfg.registerer)
assert.Equal(t, tt.wantGatherer, cfg.gatherer)
})
}
}
74 changes: 74 additions & 0 deletions exporters/prometheus/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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"

"go.opentelemetry.io/otel/sdk/metric"
) // config is added here to allow for options expansion in the future.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
type config struct {
reader metric.Reader

registry *prometheus.Registry
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
registerer prometheus.Registerer
gatherer prometheus.Gatherer
}

func newConfig(opts ...Option) config {
cfg := config{}
for _, opt := range opts {
cfg = opt.apply(cfg)
}

if cfg.reader == nil {
cfg.reader = metric.NewManualReader()
}

if cfg.registry != nil {
cfg.registerer = cfg.registry
cfg.gatherer = cfg.registry
} else {
cfg.registerer = prometheus.DefaultRegisterer
cfg.gatherer = prometheus.DefaultGatherer
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
}

return cfg
}

// Option may be used in the future to apply options to a Prometheus Exporter config.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
type Option interface {
apply(config) config
}

type optionFunc func(config) config

func (fn optionFunc) apply(cfg config) config {
return fn(cfg)
}

func WithReader(rdr metric.Reader) Option {
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
return optionFunc(func(cfg config) config {
cfg.reader = rdr
return cfg
})
}

func WithRegistry(reg *prometheus.Registry) Option {
return optionFunc(func(cfg config) config {
cfg.registry = reg
return cfg
})
}
52 changes: 32 additions & 20 deletions exporters/prometheus/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus"

import (
"context"
"fmt"
"net/http"
"sort"
"strings"
"unicode"
"unicode/utf8"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
Expand All @@ -34,38 +37,47 @@ import (
type Exporter struct {
metric.Reader
Collector prometheus.Collector
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved

handler http.Handler
}

var _ metric.Reader = &Exporter{}

// collector is used to implement prometheus.Collector.
type collector struct {
metric.Reader
reader metric.Reader
}

// config is added here to allow for options expansion in the future.
type config struct{}
// New returns a Prometheus Exporter.
func New(opts ...Option) (*Exporter, error) {
cfg := newConfig(opts...)

// Option may be used in the future to apply options to a Prometheus Exporter config.
type Option interface {
apply(config) config
}
handler := promhttp.HandlerFor(cfg.gatherer, promhttp.HandlerOpts{})
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
collector := &collector{
reader: cfg.reader,
}

// New returns a Prometheus Exporter.
func New(_ ...Option) Exporter {
// this assumes that the default temporality selector will always return cumulative.
// we only support cumulative temporality, so building our own reader enforces this.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
reader := metric.NewManualReader()
e := Exporter{
Reader: reader,
Collector: &collector{
Reader: reader,
},
if err := cfg.registerer.Register(collector); err != nil {
return nil, fmt.Errorf("cannot register the collector: %w", err)
}
return e

e := &Exporter{
Reader: cfg.reader,
Collector: collector,

handler: handler,
}

return e, nil
}

func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
e.handler.ServeHTTP(w, r)
}

// 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 +88,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
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,13 @@ func TestPrometheusExporter(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
registry := prometheus.NewRegistry() // This is the default behavior, this is used to manually gather.

exporter := New()
exporter, err := New(WithRegistry(registry))
require.NoError(t, err)
provider := metric.NewMeterProvider(metric.WithReader(exporter))
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