You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-03 18:35:08 +02:00
Refactor Prometheus exporter (#3239)
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>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+10
-15
@@ -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
|
||||
|
||||
+8
-13
@@ -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
|
||||
|
||||
@@ -27,14 +27,12 @@ import (
|
||||
|
||||
func benchmarkCollect(b *testing.B, n int) {
|
||||
ctx := context.Background()
|
||||
exporter := New()
|
||||
registry := prometheus.NewRegistry()
|
||||
exporter, err := New(WithRegisterer(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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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{
|
||||
Reader: reader,
|
||||
Collector: &collector{
|
||||
Reader: reader,
|
||||
},
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user