Skip to content

Commit

Permalink
converts Resource into a target_info metric on the prometheus exp…
Browse files Browse the repository at this point in the history
…orter (#3285)

* converts `Resource` into a `target_info` metric on the prometheus exporter
  • Loading branch information
paivagustavo committed Oct 19, 2022
1 parent 05aca23 commit 2d02a2f
Show file tree
Hide file tree
Showing 14 changed files with 218 additions and 28 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- Prometheus exporter will register with a prometheus registerer on creation, there are options to control this. (#3239)
- Added the `WithAggregationSelector` option to the `go.opentelemetry.io/otel/exporters/prometheus` package to change the `AggregationSelector` used. (#3341)
- Prometheus exporter will convert metrics `Resource` into a `target_info` metric. (#3285)

### Changed

Expand Down
50 changes: 35 additions & 15 deletions exporters/prometheus/confg_test.go
Expand Up @@ -20,7 +20,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"

"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/aggregation"
"go.opentelemetry.io/otel/sdk/metric/view"
)
Expand All @@ -31,53 +30,74 @@ func TestNewConfig(t *testing.T) {
aggregationSelector := func(view.InstrumentKind) aggregation.Aggregation { return nil }

testCases := []struct {
name string
options []Option
wantRegisterer prometheus.Registerer
wantAggregation metric.AggregationSelector
name string
options []Option
wantConfig config
}{
{
name: "Default",
options: nil,
wantRegisterer: prometheus.DefaultRegisterer,
name: "Default",
options: nil,
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
},
},
{
name: "WithRegisterer",
options: []Option{
WithRegisterer(registry),
},
wantRegisterer: registry,
wantConfig: config{
registerer: registry,
},
},
{
name: "WithAggregationSelector",
options: []Option{
WithAggregationSelector(aggregationSelector),
},
wantRegisterer: prometheus.DefaultRegisterer,
wantAggregation: aggregationSelector,
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
},
},
{
name: "With Multiple Options",
options: []Option{
WithRegisterer(registry),
WithAggregationSelector(aggregationSelector),
},
wantRegisterer: registry,
wantAggregation: aggregationSelector,

wantConfig: config{
registerer: registry,
},
},
{
name: "nil options do nothing",
options: []Option{
WithRegisterer(nil),
},
wantRegisterer: prometheus.DefaultRegisterer,
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
},
},
{
name: "without target_info metric",
options: []Option{
WithoutTargetInfo(),
},
wantConfig: config{
registerer: prometheus.DefaultRegisterer,
disableTargetInfo: true,
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
cfg := newConfig(tt.options...)

assert.Equal(t, tt.wantRegisterer, cfg.registerer)
// tested by TestConfigManualReaderOptions
cfg.aggregation = nil

assert.Equal(t, tt.wantConfig, cfg)
})
}
}
Expand Down
15 changes: 13 additions & 2 deletions exporters/prometheus/config.go
Expand Up @@ -22,8 +22,9 @@ import (

// config contains options for the exporter.
type config struct {
registerer prometheus.Registerer
aggregation metric.AggregationSelector
registerer prometheus.Registerer
disableTargetInfo bool
aggregation metric.AggregationSelector
}

// newConfig creates a validated config configured with options.
Expand Down Expand Up @@ -78,3 +79,13 @@ func WithAggregationSelector(agg metric.AggregationSelector) Option {
return cfg
})
}

// WithoutTargetInfo configures the Exporter to not export the resource target_info metric.
// If not specified, the Exporter will create a target_info metric containing
// the metrics' resource.Resource attributes.
func WithoutTargetInfo() Option {
return optionFunc(func(cfg config) config {
cfg.disableTargetInfo = true
return cfg
})
}
50 changes: 45 additions & 5 deletions exporters/prometheus/exporter.go
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"sort"
"strings"
"sync"
"unicode"
"unicode/utf8"

Expand All @@ -28,6 +29,12 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/resource"
)

const (
targetInfoMetricName = "target_info"
targetInfoDescription = "Target metadata"
)

// Exporter is a Prometheus Exporter that embeds the OTel metric.Reader
Expand All @@ -41,6 +48,10 @@ var _ metric.Reader = &Exporter{}
// collector is used to implement prometheus.Collector.
type collector struct {
reader metric.Reader

disableTargetInfo bool
targetInfo *metricData
createTargetInfoOnce sync.Once
}

// New returns a Prometheus Exporter.
Expand All @@ -53,7 +64,8 @@ func New(opts ...Option) (*Exporter, error) {
reader := metric.NewManualReader(cfg.manualReaderOptions()...)

collector := &collector{
reader: reader,
reader: reader,
disableTargetInfo: cfg.disableTargetInfo,
}

if err := cfg.registerer.Register(collector); err != nil {
Expand Down Expand Up @@ -81,11 +93,12 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
metrics, err := c.reader.Collect(context.TODO())
if err != nil {
otel.Handle(err)
if err == metric.ErrReaderNotRegistered {
return
}
}

// TODO(#3166): convert otel resource to target_info
// see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#resource-attributes-1
for _, metricData := range getMetricData(metrics) {
for _, metricData := range c.getMetricData(metrics) {
if metricData.valueType == prometheus.UntypedValue {
m, err := prometheus.NewConstHistogram(metricData.description, metricData.histogramCount, metricData.histogramSum, metricData.histogramBuckets, metricData.attributeValues...)
if err != nil {
Expand Down Expand Up @@ -118,8 +131,18 @@ type metricData struct {
histogramBuckets map[float64]uint64
}

func getMetricData(metrics metricdata.ResourceMetrics) []*metricData {
func (c *collector) getMetricData(metrics metricdata.ResourceMetrics) []*metricData {
allMetrics := make([]*metricData, 0)

c.createTargetInfoOnce.Do(func() {
// Resource should be immutable, we don't need to compute again
c.targetInfo = c.createInfoMetricData(targetInfoMetricName, targetInfoDescription, metrics.Resource)
})

if c.targetInfo != nil {
allMetrics = append(allMetrics, c.targetInfo)
}

for _, scopeMetrics := range metrics.ScopeMetrics {
for _, m := range scopeMetrics.Metrics {
switch v := m.Data.(type) {
Expand Down Expand Up @@ -234,6 +257,23 @@ func getAttrs(attrs attribute.Set) ([]string, []string) {
return keys, values
}

func (c *collector) createInfoMetricData(name, description string, res *resource.Resource) *metricData {
if c.disableTargetInfo {
return nil
}

keys, values := getAttrs(*res.Set())

desc := prometheus.NewDesc(name, description, keys, nil)
return &metricData{
name: name,
description: desc,
attributeValues: values,
valueType: prometheus.GaugeValue,
value: float64(1),
}
}

func sanitizeRune(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == ':' || r == '_' {
return r
Expand Down
98 changes: 93 additions & 5 deletions exporters/prometheus/exporter_test.go
Expand Up @@ -29,13 +29,18 @@ import (
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/aggregation"
"go.opentelemetry.io/otel/sdk/metric/view"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
)

func TestPrometheusExporter(t *testing.T) {
testCases := []struct {
name string
recordMetrics func(ctx context.Context, meter otelmetric.Meter)
expectedFile string
name string
emptyResource bool
customResouceAttrs []attribute.KeyValue
recordMetrics func(ctx context.Context, meter otelmetric.Meter)
withoutTargetInfo bool
expectedFile string
}{
{
name: "counter",
Expand Down Expand Up @@ -132,14 +137,76 @@ func TestPrometheusExporter(t *testing.T) {
histogram.Record(ctx, 23, attrs...)
},
},
{
name: "empty resource",
emptyResource: true,
expectedFile: "testdata/empty_resource.txt",
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
attrs := []attribute.KeyValue{
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
attribute.Key("E").Bool(true),
attribute.Key("F").Int(42),
}
counter, err := meter.SyncFloat64().Counter("foo", instrument.WithDescription("a simple counter"))
require.NoError(t, err)
counter.Add(ctx, 5, attrs...)
counter.Add(ctx, 10.3, attrs...)
counter.Add(ctx, 9, attrs...)
},
},
{
name: "custom resource",
customResouceAttrs: []attribute.KeyValue{
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
},
expectedFile: "testdata/custom_resource.txt",
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
attrs := []attribute.KeyValue{
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
attribute.Key("E").Bool(true),
attribute.Key("F").Int(42),
}
counter, err := meter.SyncFloat64().Counter("foo", instrument.WithDescription("a simple counter"))
require.NoError(t, err)
counter.Add(ctx, 5, attrs...)
counter.Add(ctx, 10.3, attrs...)
counter.Add(ctx, 9, attrs...)
},
},
{
name: "without target_info",
withoutTargetInfo: true,
expectedFile: "testdata/without_target_info.txt",
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
attrs := []attribute.KeyValue{
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
attribute.Key("E").Bool(true),
attribute.Key("F").Int(42),
}
counter, err := meter.SyncFloat64().Counter("foo", instrument.WithDescription("a simple counter"))
require.NoError(t, err)
counter.Add(ctx, 5, attrs...)
counter.Add(ctx, 10.3, attrs...)
counter.Add(ctx, 9, attrs...)
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
registry := prometheus.NewRegistry()

exporter, err := New(WithRegisterer(registry))
opts := []Option{WithRegisterer(registry)}
if tc.withoutTargetInfo {
opts = append(opts, WithoutTargetInfo())
}

exporter, err := New(opts...)
require.NoError(t, err)

customBucketsView, err := view.New(
Expand All @@ -152,7 +219,28 @@ func TestPrometheusExporter(t *testing.T) {
defaultView, err := view.New(view.MatchInstrumentName("*"))
require.NoError(t, err)

provider := metric.NewMeterProvider(metric.WithReader(exporter, customBucketsView, defaultView))
var res *resource.Resource

if tc.emptyResource {
res = resource.Empty()
} else {
res, err = resource.New(ctx,
// always specify service.name because the default depends on the running OS
resource.WithAttributes(semconv.ServiceNameKey.String("prometheus_test")),
// Overwrite the semconv.TelemetrySDKVersionKey value so we don't need to update every version
resource.WithAttributes(semconv.TelemetrySDKVersionKey.String("latest")),
resource.WithAttributes(tc.customResouceAttrs...),
)
require.NoError(t, err)

res, err = resource.Merge(resource.Default(), res)
require.NoError(t, err)
}

provider := metric.NewMeterProvider(
metric.WithResource(res),
metric.WithReader(exporter, customBucketsView, defaultView),
)
meter := provider.Meter("testmeter")

tc.recordMetrics(ctx, meter)
Expand Down
2 changes: 1 addition & 1 deletion exporters/prometheus/go.mod
Expand Up @@ -7,6 +7,7 @@ require (
github.com/stretchr/testify v1.8.0
go.opentelemetry.io/otel v1.11.0
go.opentelemetry.io/otel/metric v0.32.3
go.opentelemetry.io/otel/sdk v1.11.0
go.opentelemetry.io/otel/sdk/metric v0.32.3
)

Expand All @@ -22,7 +23,6 @@ require (
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
go.opentelemetry.io/otel/sdk v1.11.0 // indirect
go.opentelemetry.io/otel/trace v1.11.0 // indirect
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
google.golang.org/protobuf v1.28.1 // indirect
Expand Down
3 changes: 3 additions & 0 deletions exporters/prometheus/testdata/counter.txt
@@ -1,3 +1,6 @@
# HELP foo a simple counter
# TYPE foo counter
foo{A="B",C="D",E="true",F="42"} 24.3
# HELP target_info Target metadata
# TYPE target_info gauge
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
6 changes: 6 additions & 0 deletions exporters/prometheus/testdata/custom_resource.txt
@@ -0,0 +1,6 @@
# HELP foo a simple counter
# TYPE foo counter
foo{A="B",C="D",E="true",F="42"} 24.3
# HELP target_info Target metadata
# TYPE target_info gauge
target_info{A="B",C="D",service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
6 changes: 6 additions & 0 deletions exporters/prometheus/testdata/empty_resource.txt
@@ -0,0 +1,6 @@
# HELP foo a simple counter
# TYPE foo counter
foo{A="B",C="D",E="true",F="42"} 24.3
# HELP target_info Target metadata
# TYPE target_info gauge
target_info 1

0 comments on commit 2d02a2f

Please sign in to comment.