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

prometheus exporter convert instrumentation scope to otel_scope_info metric #3357

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- The `WithView` `Option` is added to the `go.opentelemetry.io/otel/sdk/metric` package.
This option is used to configure the view(s) a `MeterProvider` will use for all `Reader`s that are registered with it. (#3387)
- Add Instrumentation Scope and Version as info metric and label in Prometheus exporter.
This can be disabled using the `WithoutScopeInfo()` option added to that package.(#3273, #3357)

### Changed

Expand Down
11 changes: 11 additions & 0 deletions exporters/prometheus/config.go
Expand Up @@ -26,6 +26,7 @@ type config struct {
disableTargetInfo bool
withoutUnits bool
aggregation metric.AggregationSelector
disableScopeInfo bool
}

// newConfig creates a validated config configured with options.
Expand Down Expand Up @@ -105,3 +106,13 @@ func WithoutUnits() Option {
return cfg
})
}

// WithoutScopeInfo configures the Exporter to not export the otel_scope_info metric.
// If not specified, the Exporter will create a otel_scope_info metric containing
// the metrics' Instrumentation Scope, and also add labels about Instrumentation Scope to all metric points.
func WithoutScopeInfo() Option {
return optionFunc(func(cfg config) config {
cfg.disableScopeInfo = true
return cfg
})
}
67 changes: 54 additions & 13 deletions exporters/prometheus/exporter.go
Expand Up @@ -28,6 +28,7 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric/unit"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/resource"
Expand All @@ -36,8 +37,13 @@ import (
const (
targetInfoMetricName = "target_info"
targetInfoDescription = "Target metadata"

scopeInfoMetricName = "otel_scope_info"
scopeInfoDescription = "Instrumentation Scope metadata"
)

var scopeInfoKeys = [2]string{"otel_scope_name", "otel_scope_version"}

// Exporter is a Prometheus Exporter that embeds the OTel metric.Reader
// interface for easy instantiation with a MeterProvider.
type Exporter struct {
Expand All @@ -53,7 +59,9 @@ type collector struct {
disableTargetInfo bool
withoutUnits bool
targetInfo prometheus.Metric
disableScopeInfo bool
createTargetInfoOnce sync.Once
scopeInfos map[instrumentation.Scope]prometheus.Metric
}

// prometheus counters MUST have a _total suffix:
Expand All @@ -73,6 +81,8 @@ func New(opts ...Option) (*Exporter, error) {
reader: reader,
disableTargetInfo: cfg.disableTargetInfo,
withoutUnits: cfg.withoutUnits,
disableScopeInfo: cfg.disableScopeInfo,
scopeInfos: make(map[instrumentation.Scope]prometheus.Metric),
}

if err := cfg.registerer.Register(collector); err != nil {
Expand Down Expand Up @@ -118,28 +128,46 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
if !c.disableTargetInfo {
ch <- c.targetInfo
}

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

if !c.disableScopeInfo {
scopeInfo, ok := c.scopeInfos[scopeMetrics.Scope]
if !ok {
scopeInfo, err = createScopeInfoMetric(scopeMetrics.Scope)
if err != nil {
otel.Handle(err)
}
c.scopeInfos[scopeMetrics.Scope] = scopeInfo
}
ch <- scopeInfo
keys = scopeInfoKeys
values = [2]string{scopeMetrics.Scope.Name, scopeMetrics.Scope.Version}
}

for _, m := range scopeMetrics.Metrics {
switch v := m.Data.(type) {
case metricdata.Histogram:
addHistogramMetric(ch, v, m, c.getName(m))
addHistogramMetric(ch, v, m, keys, values, c.getName(m))
case metricdata.Sum[int64]:
addSumMetric(ch, v, m, c.getName(m))
addSumMetric(ch, v, m, keys, values, c.getName(m))
case metricdata.Sum[float64]:
addSumMetric(ch, v, m, c.getName(m))
addSumMetric(ch, v, m, keys, values, c.getName(m))
case metricdata.Gauge[int64]:
addGaugeMetric(ch, v, m, c.getName(m))
addGaugeMetric(ch, v, m, keys, values, c.getName(m))
case metricdata.Gauge[float64]:
addGaugeMetric(ch, v, m, c.getName(m))
addGaugeMetric(ch, v, m, keys, values, c.getName(m))
}
}
}
}

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

desc := prometheus.NewDesc(name, m.Description, keys, nil)
buckets := make(map[float64]uint64, len(dp.Bounds))

Expand All @@ -157,7 +185,7 @@ func addHistogramMetric(ch chan<- prometheus.Metric, histogram metricdata.Histog
}
}

func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata.Sum[N], m metricdata.Metrics, name string) {
func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata.Sum[N], m metricdata.Metrics, ks, vs [2]string, name string) {
valueType := prometheus.CounterValue
if !sum.IsMonotonic {
valueType = prometheus.GaugeValue
Expand All @@ -167,7 +195,8 @@ func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata
name += counterSuffix
}
for _, dp := range sum.DataPoints {
keys, values := getAttrs(dp.Attributes)
keys, values := getAttrs(dp.Attributes, ks, vs)

desc := prometheus.NewDesc(name, m.Description, keys, nil)
m, err := prometheus.NewConstMetric(desc, valueType, float64(dp.Value), values...)
if err != nil {
Expand All @@ -178,9 +207,10 @@ func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata
}
}

func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metricdata.Gauge[N], m metricdata.Metrics, name string) {
func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metricdata.Gauge[N], m metricdata.Metrics, ks, vs [2]string, name string) {
for _, dp := range gauge.DataPoints {
keys, values := getAttrs(dp.Attributes)
keys, values := getAttrs(dp.Attributes, ks, vs)

desc := prometheus.NewDesc(name, m.Description, keys, nil)
m, err := prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(dp.Value), values...)
if err != nil {
Expand All @@ -194,7 +224,7 @@ func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metric
// 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) ([]string, []string) {
func getAttrs(attrs attribute.Set, ks, vs [2]string) ([]string, []string) {
keysMap := make(map[string][]string)
itr := attrs.Iter()
for itr.Next() {
Expand All @@ -217,15 +247,26 @@ func getAttrs(attrs attribute.Set) ([]string, []string) {
})
values = append(values, strings.Join(vals, ";"))
}

if ks[0] != "" {
keys = append(keys, ks[:]...)
values = append(values, vs[:]...)
}
return keys, values
}

func (c *collector) createInfoMetric(name, description string, res *resource.Resource) (prometheus.Metric, error) {
keys, values := getAttrs(*res.Set())
keys, values := getAttrs(*res.Set(), [2]string{}, [2]string{})
desc := prometheus.NewDesc(name, description, keys, nil)
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), values...)
}

func createScopeInfoMetric(scope instrumentation.Scope) (prometheus.Metric, error) {
keys := scopeInfoKeys[:]
desc := prometheus.NewDesc(scopeInfoMetricName, scopeInfoDescription, keys, nil)
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), scope.Name, scope.Version)
}

func sanitizeRune(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == ':' || r == '_' {
return r
Expand Down
75 changes: 74 additions & 1 deletion exporters/prometheus/exporter_test.go
Expand Up @@ -221,6 +221,44 @@ func TestPrometheusExporter(t *testing.T) {
counter.Add(ctx, 9, attrs...)
},
},
{
name: "without scope_info",
options: []Option{WithoutScopeInfo()},
expectedFile: "testdata/without_scope_info.txt",
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
attrs := []attribute.KeyValue{
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
}
gauge, err := meter.SyncInt64().UpDownCounter(
"bar",
instrument.WithDescription("a fun little gauge"),
instrument.WithUnit(unit.Dimensionless),
)
require.NoError(t, err)
gauge.Add(ctx, 2, attrs...)
gauge.Add(ctx, -1, attrs...)
},
},
{
name: "without scope_info and target_info",
options: []Option{WithoutScopeInfo(), WithoutTargetInfo()},
expectedFile: "testdata/without_scope_and_target_info.txt",
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
attrs := []attribute.KeyValue{
attribute.Key("A").String("B"),
attribute.Key("C").String("D"),
}
counter, err := meter.SyncInt64().Counter(
"bar",
instrument.WithDescription("a fun little counter"),
instrument.WithUnit(unit.Bytes),
)
require.NoError(t, err)
counter.Add(ctx, 2, attrs...)
counter.Add(ctx, 1, attrs...)
},
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -263,7 +301,10 @@ func TestPrometheusExporter(t *testing.T) {
metric.WithReader(exporter),
metric.WithView(customBucketsView, defaultView),
)
meter := provider.Meter("testmeter")
meter := provider.Meter(
"testmeter",
otelmetric.WithInstrumentationVersion("v0.1.0"),
)
MrAlias marked this conversation as resolved.
Show resolved Hide resolved

tc.recordMetrics(ctx, meter)

Expand Down Expand Up @@ -306,3 +347,35 @@ func TestSantitizeName(t *testing.T) {
require.Equalf(t, test.want, sanitizeName(test.input), "input: %q", test.input)
}
}

// func TestMetricWithSameName(t *testing.T) {
// exporter, err := New()
// assert.NoError(t, err)

// provider := metric.NewMeterProvider(
// metric.WithReader(exporter),
// )

// httpCounter, err := provider.Meter("http").
// SyncInt64().Counter(
// "error_count",
// instrument.WithUnit(unit.Dimensionless))
// assert.NoError(t, err)
// httpCounter.Add(context.TODO(), 1, attribute.String("type", "bar1"))
// httpCounter.Add(context.TODO(), 2, attribute.String("type", "bar2"))

// // sqlCounter, err := provider.Meter("sql").
// // SyncInt64().UpDownCounter(
// // "error_count",
// // instrument.WithUnit(unit.Dimensionless))
// // assert.NoError(t, err)
// // sqlCounter.Add(context.TODO(), 1)

// t.Logf("serving metrics at localhost:2223/metrics")
// http.Handle("/metrics", promhttp.Handler())
// err = http.ListenAndServe(":2223", nil)
// if err != nil {
// t.Fatalf("error serving http: %v", err)
// return
// }
// }
fatsheep9146 marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 5 additions & 2 deletions exporters/prometheus/testdata/counter.txt
@@ -1,7 +1,10 @@
# HELP foo_milliseconds_total a simple counter
# TYPE foo_milliseconds_total counter
foo_milliseconds_total{A="B",C="D",E="true",F="42"} 24.3
foo_milliseconds_total{A="D",C="B",E="true",F="42"} 5
foo_milliseconds_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
foo_milliseconds_total{A="D",C="B",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 5
# 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
5 changes: 4 additions & 1 deletion exporters/prometheus/testdata/custom_resource.txt
@@ -1,6 +1,9 @@
# HELP foo_total a simple counter
# TYPE foo_total counter
foo_total{A="B",C="D",E="true",F="42"} 24.3
foo_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
# 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{A="B",C="D",service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
5 changes: 4 additions & 1 deletion exporters/prometheus/testdata/empty_resource.txt
@@ -1,6 +1,9 @@
# HELP foo_total a simple counter
# TYPE foo_total counter
foo_total{A="B",C="D",E="true",F="42"} 24.3
foo_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
# 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 1
5 changes: 4 additions & 1 deletion exporters/prometheus/testdata/gauge.txt
@@ -1,6 +1,9 @@
# HELP bar_ratio a fun little gauge
# TYPE bar_ratio gauge
bar_ratio{A="B",C="D"} .75
bar_ratio{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} .75
# 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
29 changes: 16 additions & 13 deletions exporters/prometheus/testdata/histogram.txt
@@ -1,18 +1,21 @@
# HELP histogram_baz_bytes a very nice histogram
# TYPE histogram_baz_bytes histogram
histogram_baz_bytes_bucket{A="B",C="D",le="0"} 0
histogram_baz_bytes_bucket{A="B",C="D",le="5"} 0
histogram_baz_bytes_bucket{A="B",C="D",le="10"} 1
histogram_baz_bytes_bucket{A="B",C="D",le="25"} 2
histogram_baz_bytes_bucket{A="B",C="D",le="50"} 2
histogram_baz_bytes_bucket{A="B",C="D",le="75"} 2
histogram_baz_bytes_bucket{A="B",C="D",le="100"} 2
histogram_baz_bytes_bucket{A="B",C="D",le="250"} 4
histogram_baz_bytes_bucket{A="B",C="D",le="500"} 4
histogram_baz_bytes_bucket{A="B",C="D",le="1000"} 4
histogram_baz_bytes_bucket{A="B",C="D",le="+Inf"} 4
histogram_baz_bytes_sum{A="B",C="D"} 236
histogram_baz_bytes_count{A="B",C="D"} 4
histogram_baz_bytes_bucket{A="B",C="D",le="0",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 0
histogram_baz_bytes_bucket{A="B",C="D",le="5",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 0
histogram_baz_bytes_bucket{A="B",C="D",le="10",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
histogram_baz_bytes_bucket{A="B",C="D",le="25",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 2
histogram_baz_bytes_bucket{A="B",C="D",le="50",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 2
histogram_baz_bytes_bucket{A="B",C="D",le="75",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 2
histogram_baz_bytes_bucket{A="B",C="D",le="100",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 2
histogram_baz_bytes_bucket{A="B",C="D",le="250",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
histogram_baz_bytes_bucket{A="B",C="D",le="500",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
histogram_baz_bytes_bucket{A="B",C="D",le="1000",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
histogram_baz_bytes_bucket{A="B",C="D",le="+Inf",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
histogram_baz_bytes_sum{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 236
histogram_baz_bytes_count{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
# 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
5 changes: 4 additions & 1 deletion exporters/prometheus/testdata/sanitized_labels.txt
@@ -1,6 +1,9 @@
# HELP foo_total a sanitary counter
# TYPE foo_total counter
foo_total{A_B="Q",C_D="Y;Z"} 24.3
foo_total{A_B="Q",C_D="Y;Z",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
# 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
Expand Down