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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added

- Added an example of using metric views to customize instruments. (#3177)
- Prometheus exporter will register with a prometheus registerer on creation, there are options to control this (#3239)
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved

### Changed
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved

Expand Down
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,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(WithGatherer(registry), 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
84 changes: 84 additions & 0 deletions exporters/prometheus/confg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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
wantGatherer prometheus.Gatherer
}{
{
name: "Default",
options: nil,
wantRegisterer: prometheus.DefaultRegisterer,
wantGatherer: prometheus.DefaultGatherer,
},

{
name: "WithGatherer",
options: []Option{
WithGatherer(registry),
},
wantRegisterer: prometheus.DefaultRegisterer,
wantGatherer: registry,
},
{
name: "WithRegisterer",
options: []Option{
WithRegisterer(registry),
},
wantRegisterer: registry,
wantGatherer: prometheus.DefaultGatherer,
},
{
name: "Multiple Options",
options: []Option{
WithGatherer(registry),
WithRegisterer(registry),
},
wantRegisterer: registry,
wantGatherer: registry,
},
{
name: "nil options do nothing",
options: []Option{
WithGatherer(nil),
WithRegisterer(nil),
},
wantRegisterer: prometheus.DefaultRegisterer,
wantGatherer: prometheus.DefaultGatherer,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
cfg := newConfig(tt.options...)

// 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)
})
}
}
70 changes: 70 additions & 0 deletions exporters/prometheus/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 is added here to allow for options expansion in the future.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
type config struct {
registerer prometheus.Registerer
gatherer prometheus.Gatherer
}

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

if cfg.gatherer == nil {
cfg.gatherer = prometheus.DefaultGatherer
}
if cfg.registerer == nil {
cfg.registerer = prometheus.DefaultRegisterer
}

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)
}

// 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
})
}

// WithRegisterer configures which prometheus Gatherer the Exporter will
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
// Gather from. If no gatherer is used the prometheus DefaultGatherer is
// used.
func WithGatherer(gatherer prometheus.Gatherer) Option {
return optionFunc(func(cfg config) config {
cfg.gatherer = gatherer
return cfg
})
}
55 changes: 35 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,50 @@ 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
}

// collector is used to implement prometheus.Collector.
type collector struct {
metric.Reader
handler http.Handler
}

// config is added here to allow for options expansion in the future.
type config struct{}
var _ metric.Reader = &Exporter{}

// Option may be used in the future to apply options to a Prometheus Exporter config.
type Option interface {
apply(config) config
// collector is used to implement prometheus.Collector.
type collector struct {
reader metric.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
func New(opts ...Option) (*Exporter, error) {
cfg := newConfig(opts...)

// TODO (#????): Enable some way to configure the reader, but not change temporality.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
reader := metric.NewManualReader()
e := Exporter{
Reader: reader,
Collector: &collector{
Reader: reader,
},

handler := promhttp.HandlerFor(cfg.gatherer, promhttp.HandlerOpts{})
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
collector := &collector{
reader: reader,
}
return e

if err := cfg.registerer.Register(collector); err != nil {
return nil, fmt.Errorf("cannot register the collector: %w", err)
}

e := &Exporter{
Reader: 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 +91,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 @@ -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(WithGatherer(registry), 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