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

add option for resource attributes in metrics for prometheus exporter #4733

Merged
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- Improve `go.opentelemetry.io/otel/propagation.TraceContext`'s performance. (#4721)

### Added

- Add `WithResourceAsConstantLabels` option to apply resource attributes for every metric emitted by the Prometheus exporter. (#4733)

## [1.21.0/0.44.0] 2023-11-16

### Removed
Expand Down
25 changes: 18 additions & 7 deletions exporters/prometheus/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ import (

"github.com/prometheus/client_golang/prometheus"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric"
)

// config contains options for the exporter.
type config struct {
registerer prometheus.Registerer
disableTargetInfo bool
withoutUnits bool
withoutCounterSuffixes bool
readerOpts []metric.ManualReaderOption
disableScopeInfo bool
namespace string
registerer prometheus.Registerer
disableTargetInfo bool
withoutUnits bool
withoutCounterSuffixes bool
readerOpts []metric.ManualReaderOption
disableScopeInfo bool
namespace string
resourceAttributesFilter attribute.Filter
}

// newConfig creates a validated config configured with options.
Expand Down Expand Up @@ -151,3 +153,12 @@ func WithNamespace(ns string) Option {
return cfg
})
}

// WithResourceAsConstantLabels adds resource attributes as metric attributes
// on all metrics exported by the Prometheus Exporter.
codeboten marked this conversation as resolved.
Show resolved Hide resolved
func WithResourceAsConstantLabels(resourceFilter attribute.Filter) Option {
return optionFunc(func(cfg config) config {
cfg.resourceAttributesFilter = resourceFilter
return cfg
})
}
86 changes: 59 additions & 27 deletions exporters/prometheus/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,29 @@

var _ metric.Reader = &Exporter{}

// keyVals is used to store resource attribute key value pairs.
type keyVals struct {
keys []string
vals []string
}

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

withoutUnits bool
withoutCounterSuffixes bool
disableScopeInfo bool
namespace string
withoutUnits bool
withoutCounterSuffixes bool
disableScopeInfo bool
namespace string
resourceAttributesFilter attribute.Filter

mu sync.Mutex // mu protects all members below from the concurrent access.
disableTargetInfo bool
targetInfo prometheus.Metric
scopeInfos map[instrumentation.Scope]prometheus.Metric
scopeInfosInvalid map[instrumentation.Scope]struct{}
metricFamilies map[string]*dto.MetricFamily
resourceKeyVals keyVals
}

// prometheus counters MUST have a _total suffix by default:
Expand All @@ -109,15 +117,16 @@
reader := metric.NewManualReader(cfg.readerOpts...)

collector := &collector{
reader: reader,
disableTargetInfo: cfg.disableTargetInfo,
withoutUnits: cfg.withoutUnits,
withoutCounterSuffixes: cfg.withoutCounterSuffixes,
disableScopeInfo: cfg.disableScopeInfo,
scopeInfos: make(map[instrumentation.Scope]prometheus.Metric),
scopeInfosInvalid: make(map[instrumentation.Scope]struct{}),
metricFamilies: make(map[string]*dto.MetricFamily),
namespace: cfg.namespace,
reader: reader,
disableTargetInfo: cfg.disableTargetInfo,
withoutUnits: cfg.withoutUnits,
withoutCounterSuffixes: cfg.withoutCounterSuffixes,
disableScopeInfo: cfg.disableScopeInfo,
scopeInfos: make(map[instrumentation.Scope]prometheus.Metric),
scopeInfosInvalid: make(map[instrumentation.Scope]struct{}),
metricFamilies: make(map[string]*dto.MetricFamily),
namespace: cfg.namespace,
resourceAttributesFilter: cfg.resourceAttributesFilter,
}

if err := cfg.registerer.Register(collector); err != nil {
Expand Down Expand Up @@ -181,6 +190,10 @@
ch <- c.targetInfo
}

if c.resourceAttributesFilter != nil {
c.resourceAttributes(metrics.Resource)
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
}

for _, scopeMetrics := range metrics.ScopeMetrics {
var keys, values [2]string

Expand Down Expand Up @@ -219,26 +232,26 @@

switch v := m.Data.(type) {
case metricdata.Histogram[int64]:
addHistogramMetric(ch, v, m, keys, values, name)
addHistogramMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
case metricdata.Histogram[float64]:
addHistogramMetric(ch, v, m, keys, values, name)
addHistogramMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
case metricdata.Sum[int64]:
addSumMetric(ch, v, m, keys, values, name)
addSumMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
case metricdata.Sum[float64]:
addSumMetric(ch, v, m, keys, values, name)
addSumMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
case metricdata.Gauge[int64]:
addGaugeMetric(ch, v, m, keys, values, name)
addGaugeMetric(ch, v, m, keys, values, name, c.resourceKeyVals)

Check warning on line 243 in exporters/prometheus/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporters/prometheus/exporter.go#L243

Added line #L243 was not covered by tests
case metricdata.Gauge[float64]:
addGaugeMetric(ch, v, m, keys, values, name)
addGaugeMetric(ch, v, m, keys, values, name, c.resourceKeyVals)

Check warning on line 245 in exporters/prometheus/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporters/prometheus/exporter.go#L245

Added line #L245 was not covered by tests
}
}
}
}

func addHistogramMetric[N int64 | float64](ch chan<- prometheus.Metric, histogram metricdata.Histogram[N], m metricdata.Metrics, ks, vs [2]string, name string) {
func addHistogramMetric[N int64 | float64](ch chan<- prometheus.Metric, histogram metricdata.Histogram[N], m metricdata.Metrics, ks, vs [2]string, name string, resourceKV keyVals) {
// TODO(https://github.com/open-telemetry/opentelemetry-go/issues/3163): support exemplars
for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes, ks, vs)
keys, values := getAttrs(dp.Attributes, ks, vs, resourceKV)

desc := prometheus.NewDesc(name, m.Description, keys, nil)
buckets := make(map[float64]uint64, len(dp.Bounds))
Expand All @@ -257,14 +270,14 @@
}
}

func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata.Sum[N], m metricdata.Metrics, ks, vs [2]string, name string) {
func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata.Sum[N], m metricdata.Metrics, ks, vs [2]string, name string, resourceKV keyVals) {
valueType := prometheus.CounterValue
if !sum.IsMonotonic {
valueType = prometheus.GaugeValue
}

for _, dp := range sum.DataPoints {
keys, values := getAttrs(dp.Attributes, ks, vs)
keys, values := getAttrs(dp.Attributes, ks, vs, resourceKV)

desc := prometheus.NewDesc(name, m.Description, keys, nil)
m, err := prometheus.NewConstMetric(desc, valueType, float64(dp.Value), values...)
Expand All @@ -276,9 +289,9 @@
}
}

func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metricdata.Gauge[N], m metricdata.Metrics, ks, vs [2]string, name string) {
func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metricdata.Gauge[N], m metricdata.Metrics, ks, vs [2]string, name string, resourceKV keyVals) {

Check warning on line 292 in exporters/prometheus/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporters/prometheus/exporter.go#L292

Added line #L292 was not covered by tests
for _, dp := range gauge.DataPoints {
keys, values := getAttrs(dp.Attributes, ks, vs)
keys, values := getAttrs(dp.Attributes, ks, vs, resourceKV)

Check warning on line 294 in exporters/prometheus/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporters/prometheus/exporter.go#L294

Added line #L294 was not covered by tests

desc := prometheus.NewDesc(name, m.Description, keys, nil)
m, err := prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(dp.Value), values...)
Expand All @@ -293,7 +306,7 @@
// getAttrs parses the attribute.Set to two lists of matching Prometheus-style
// keys and values. It sanitizes invalid characters and handles duplicate keys
// (due to sanitization) by sorting and concatenating the values following the spec.
func getAttrs(attrs attribute.Set, ks, vs [2]string) ([]string, []string) {
func getAttrs(attrs attribute.Set, ks, vs [2]string, resourceKV keyVals) ([]string, []string) {
keysMap := make(map[string][]string)
itr := attrs.Iter()
for itr.Next() {
Expand Down Expand Up @@ -321,11 +334,17 @@
keys = append(keys, ks[:]...)
values = append(values, vs[:]...)
}

for idx := range resourceKV.keys {
keys = append(keys, resourceKV.keys[idx])
values = append(values, resourceKV.vals[idx])
}

return keys, values
}

func createInfoMetric(name, description string, res *resource.Resource) (prometheus.Metric, error) {
keys, values := getAttrs(*res.Set(), [2]string{}, [2]string{})
keys, values := getAttrs(*res.Set(), [2]string{}, [2]string{}, keyVals{})
desc := prometheus.NewDesc(name, description, keys, nil)
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), values...)
}
Expand Down Expand Up @@ -473,6 +492,19 @@
return nil
}

func (c *collector) resourceAttributes(res *resource.Resource) {
c.mu.Lock()
defer c.mu.Unlock()

if len(c.resourceKeyVals.keys) > 0 {
return
}

Check warning on line 501 in exporters/prometheus/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporters/prometheus/exporter.go#L500-L501

Added lines #L500 - L501 were not covered by tests

resourceAttrs, _ := res.Set().Filter(c.resourceAttributesFilter)
resourceKeys, resourceValues := getAttrs(resourceAttrs, [2]string{}, [2]string{}, keyVals{})
c.resourceKeyVals = keyVals{keys: resourceKeys, vals: resourceValues}
}

func (c *collector) scopeInfo(scope instrumentation.Scope) (prometheus.Metric, error) {
c.mu.Lock()
defer c.mu.Unlock()
Expand Down
40 changes: 40 additions & 0 deletions exporters/prometheus/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,46 @@ func TestPrometheusExporter(t *testing.T) {
counter.Add(ctx, 9, opt)
},
},
{
name: "with resource attributes filter",
expectedFile: "testdata/with_resource_attributes_filter.txt",
options: []Option{
WithResourceAsConstantLabels(attribute.NewDenyKeysFilter()),
},
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
attribute.Key("E").Bool(true),
attribute.Key("F").Int(42),
)
counter, err := meter.Float64Counter("foo", otelmetric.WithDescription("a simple counter"))
require.NoError(t, err)
counter.Add(ctx, 5, opt)
counter.Add(ctx, 10.1, opt)
counter.Add(ctx, 9.8, opt)
},
},
{
name: "with some resource attributes filter",
expectedFile: "testdata/with_allow_resource_attributes_filter.txt",
options: []Option{
WithResourceAsConstantLabels(attribute.NewAllowKeysFilter("service.name")),
},
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
opt := otelmetric.WithAttributes(
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
attribute.Key("E").Bool(true),
attribute.Key("F").Int(42),
)
counter, err := meter.Float64Counter("foo", otelmetric.WithDescription("a simple counter"))
require.NoError(t, err)
counter.Add(ctx, 5, opt)
counter.Add(ctx, 5.9, opt)
counter.Add(ctx, 5.3, opt)
},
},
}

for _, tc := range testCases {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# HELP foo_total a simple counter
# TYPE foo_total counter
foo_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0",service_name="prometheus_test"} 16.2
# HELP otel_scope_info Instrumentation Scope metadata
# TYPE otel_scope_info gauge
otel_scope_info{otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
# 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# HELP foo_total a simple counter
# TYPE foo_total counter
foo_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0",service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 24.9
# HELP otel_scope_info Instrumentation Scope metadata
# TYPE otel_scope_info gauge
otel_scope_info{otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
# 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