Skip to content

Commit 2a2d1ec

Browse files
authored
prometheus exporter convert instrumentation scope to otel_scope_info metric (#3357)
* prometheus exporter convert instrumentation scope to otel_scope_info metric Signed-off-by: Ziqi Zhao <[email protected]> * fix for commits Signed-off-by: Ziqi Zhao <[email protected]> * fix for ci failed Signed-off-by: Ziqi Zhao <[email protected]> * add multi scopes test Signed-off-by: Ziqi Zhao <[email protected]> * fix ci failed Signed-off-by: Ziqi Zhao <[email protected]> Signed-off-by: Ziqi Zhao <[email protected]>
1 parent 8a11595 commit 2a2d1ec

15 files changed

+242
-55
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1212

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

1618
### Changed
1719

exporters/prometheus/config.go

+11
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type config struct {
2626
disableTargetInfo bool
2727
withoutUnits bool
2828
aggregation metric.AggregationSelector
29+
disableScopeInfo bool
2930
}
3031

3132
// newConfig creates a validated config configured with options.
@@ -105,3 +106,13 @@ func WithoutUnits() Option {
105106
return cfg
106107
})
107108
}
109+
110+
// WithoutScopeInfo configures the Exporter to not export the otel_scope_info metric.
111+
// If not specified, the Exporter will create a otel_scope_info metric containing
112+
// the metrics' Instrumentation Scope, and also add labels about Instrumentation Scope to all metric points.
113+
func WithoutScopeInfo() Option {
114+
return optionFunc(func(cfg config) config {
115+
cfg.disableScopeInfo = true
116+
return cfg
117+
})
118+
}

exporters/prometheus/exporter.go

+54-13
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"go.opentelemetry.io/otel"
2929
"go.opentelemetry.io/otel/attribute"
3030
"go.opentelemetry.io/otel/metric/unit"
31+
"go.opentelemetry.io/otel/sdk/instrumentation"
3132
"go.opentelemetry.io/otel/sdk/metric"
3233
"go.opentelemetry.io/otel/sdk/metric/metricdata"
3334
"go.opentelemetry.io/otel/sdk/resource"
@@ -36,8 +37,13 @@ import (
3637
const (
3738
targetInfoMetricName = "target_info"
3839
targetInfoDescription = "Target metadata"
40+
41+
scopeInfoMetricName = "otel_scope_info"
42+
scopeInfoDescription = "Instrumentation Scope metadata"
3943
)
4044

45+
var scopeInfoKeys = [2]string{"otel_scope_name", "otel_scope_version"}
46+
4147
// Exporter is a Prometheus Exporter that embeds the OTel metric.Reader
4248
// interface for easy instantiation with a MeterProvider.
4349
type Exporter struct {
@@ -53,7 +59,9 @@ type collector struct {
5359
disableTargetInfo bool
5460
withoutUnits bool
5561
targetInfo prometheus.Metric
62+
disableScopeInfo bool
5663
createTargetInfoOnce sync.Once
64+
scopeInfos map[instrumentation.Scope]prometheus.Metric
5765
}
5866

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

7888
if err := cfg.registerer.Register(collector); err != nil {
@@ -118,28 +128,46 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
118128
if !c.disableTargetInfo {
119129
ch <- c.targetInfo
120130
}
131+
121132
for _, scopeMetrics := range metrics.ScopeMetrics {
133+
var keys, values [2]string
134+
135+
if !c.disableScopeInfo {
136+
scopeInfo, ok := c.scopeInfos[scopeMetrics.Scope]
137+
if !ok {
138+
scopeInfo, err = createScopeInfoMetric(scopeMetrics.Scope)
139+
if err != nil {
140+
otel.Handle(err)
141+
}
142+
c.scopeInfos[scopeMetrics.Scope] = scopeInfo
143+
}
144+
ch <- scopeInfo
145+
keys = scopeInfoKeys
146+
values = [2]string{scopeMetrics.Scope.Name, scopeMetrics.Scope.Version}
147+
}
148+
122149
for _, m := range scopeMetrics.Metrics {
123150
switch v := m.Data.(type) {
124151
case metricdata.Histogram:
125-
addHistogramMetric(ch, v, m, c.getName(m))
152+
addHistogramMetric(ch, v, m, keys, values, c.getName(m))
126153
case metricdata.Sum[int64]:
127-
addSumMetric(ch, v, m, c.getName(m))
154+
addSumMetric(ch, v, m, keys, values, c.getName(m))
128155
case metricdata.Sum[float64]:
129-
addSumMetric(ch, v, m, c.getName(m))
156+
addSumMetric(ch, v, m, keys, values, c.getName(m))
130157
case metricdata.Gauge[int64]:
131-
addGaugeMetric(ch, v, m, c.getName(m))
158+
addGaugeMetric(ch, v, m, keys, values, c.getName(m))
132159
case metricdata.Gauge[float64]:
133-
addGaugeMetric(ch, v, m, c.getName(m))
160+
addGaugeMetric(ch, v, m, keys, values, c.getName(m))
134161
}
135162
}
136163
}
137164
}
138165

139-
func addHistogramMetric(ch chan<- prometheus.Metric, histogram metricdata.Histogram, m metricdata.Metrics, name string) {
166+
func addHistogramMetric(ch chan<- prometheus.Metric, histogram metricdata.Histogram, m metricdata.Metrics, ks, vs [2]string, name string) {
140167
// TODO(https://github.com/open-telemetry/opentelemetry-go/issues/3163): support exemplars
141168
for _, dp := range histogram.DataPoints {
142-
keys, values := getAttrs(dp.Attributes)
169+
keys, values := getAttrs(dp.Attributes, ks, vs)
170+
143171
desc := prometheus.NewDesc(name, m.Description, keys, nil)
144172
buckets := make(map[float64]uint64, len(dp.Bounds))
145173

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

160-
func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata.Sum[N], m metricdata.Metrics, name string) {
188+
func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata.Sum[N], m metricdata.Metrics, ks, vs [2]string, name string) {
161189
valueType := prometheus.CounterValue
162190
if !sum.IsMonotonic {
163191
valueType = prometheus.GaugeValue
@@ -167,7 +195,8 @@ func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata
167195
name += counterSuffix
168196
}
169197
for _, dp := range sum.DataPoints {
170-
keys, values := getAttrs(dp.Attributes)
198+
keys, values := getAttrs(dp.Attributes, ks, vs)
199+
171200
desc := prometheus.NewDesc(name, m.Description, keys, nil)
172201
m, err := prometheus.NewConstMetric(desc, valueType, float64(dp.Value), values...)
173202
if err != nil {
@@ -178,9 +207,10 @@ func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata
178207
}
179208
}
180209

181-
func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metricdata.Gauge[N], m metricdata.Metrics, name string) {
210+
func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metricdata.Gauge[N], m metricdata.Metrics, ks, vs [2]string, name string) {
182211
for _, dp := range gauge.DataPoints {
183-
keys, values := getAttrs(dp.Attributes)
212+
keys, values := getAttrs(dp.Attributes, ks, vs)
213+
184214
desc := prometheus.NewDesc(name, m.Description, keys, nil)
185215
m, err := prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(dp.Value), values...)
186216
if err != nil {
@@ -194,7 +224,7 @@ func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metric
194224
// getAttrs parses the attribute.Set to two lists of matching Prometheus-style
195225
// keys and values. It sanitizes invalid characters and handles duplicate keys
196226
// (due to sanitization) by sorting and concatenating the values following the spec.
197-
func getAttrs(attrs attribute.Set) ([]string, []string) {
227+
func getAttrs(attrs attribute.Set, ks, vs [2]string) ([]string, []string) {
198228
keysMap := make(map[string][]string)
199229
itr := attrs.Iter()
200230
for itr.Next() {
@@ -217,15 +247,26 @@ func getAttrs(attrs attribute.Set) ([]string, []string) {
217247
})
218248
values = append(values, strings.Join(vals, ";"))
219249
}
250+
251+
if ks[0] != "" {
252+
keys = append(keys, ks[:]...)
253+
values = append(values, vs[:]...)
254+
}
220255
return keys, values
221256
}
222257

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

264+
func createScopeInfoMetric(scope instrumentation.Scope) (prometheus.Metric, error) {
265+
keys := scopeInfoKeys[:]
266+
desc := prometheus.NewDesc(scopeInfoMetricName, scopeInfoDescription, keys, nil)
267+
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), scope.Name, scope.Version)
268+
}
269+
229270
func sanitizeRune(r rune) rune {
230271
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == ':' || r == '_' {
231272
return r

exporters/prometheus/exporter_test.go

+88-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/prometheus/client_golang/prometheus"
2323
"github.com/prometheus/client_golang/prometheus/testutil"
24+
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
2526

2627
"go.opentelemetry.io/otel/attribute"
@@ -221,6 +222,44 @@ func TestPrometheusExporter(t *testing.T) {
221222
counter.Add(ctx, 9, attrs...)
222223
},
223224
},
225+
{
226+
name: "without scope_info",
227+
options: []Option{WithoutScopeInfo()},
228+
expectedFile: "testdata/without_scope_info.txt",
229+
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
230+
attrs := []attribute.KeyValue{
231+
attribute.Key("A").String("B"),
232+
attribute.Key("C").String("D"),
233+
}
234+
gauge, err := meter.SyncInt64().UpDownCounter(
235+
"bar",
236+
instrument.WithDescription("a fun little gauge"),
237+
instrument.WithUnit(unit.Dimensionless),
238+
)
239+
require.NoError(t, err)
240+
gauge.Add(ctx, 2, attrs...)
241+
gauge.Add(ctx, -1, attrs...)
242+
},
243+
},
244+
{
245+
name: "without scope_info and target_info",
246+
options: []Option{WithoutScopeInfo(), WithoutTargetInfo()},
247+
expectedFile: "testdata/without_scope_and_target_info.txt",
248+
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
249+
attrs := []attribute.KeyValue{
250+
attribute.Key("A").String("B"),
251+
attribute.Key("C").String("D"),
252+
}
253+
counter, err := meter.SyncInt64().Counter(
254+
"bar",
255+
instrument.WithDescription("a fun little counter"),
256+
instrument.WithUnit(unit.Bytes),
257+
)
258+
require.NoError(t, err)
259+
counter.Add(ctx, 2, attrs...)
260+
counter.Add(ctx, 1, attrs...)
261+
},
262+
},
224263
}
225264

226265
for _, tc := range testCases {
@@ -263,7 +302,10 @@ func TestPrometheusExporter(t *testing.T) {
263302
metric.WithReader(exporter),
264303
metric.WithView(customBucketsView, defaultView),
265304
)
266-
meter := provider.Meter("testmeter")
305+
meter := provider.Meter(
306+
"testmeter",
307+
otelmetric.WithInstrumentationVersion("v0.1.0"),
308+
)
267309

268310
tc.recordMetrics(ctx, meter)
269311

@@ -306,3 +348,48 @@ func TestSantitizeName(t *testing.T) {
306348
require.Equalf(t, test.want, sanitizeName(test.input), "input: %q", test.input)
307349
}
308350
}
351+
352+
func TestMultiScopes(t *testing.T) {
353+
ctx := context.Background()
354+
registry := prometheus.NewRegistry()
355+
exporter, err := New(WithRegisterer(registry))
356+
require.NoError(t, err)
357+
358+
res, err := resource.New(ctx,
359+
// always specify service.name because the default depends on the running OS
360+
resource.WithAttributes(semconv.ServiceNameKey.String("prometheus_test")),
361+
// Overwrite the semconv.TelemetrySDKVersionKey value so we don't need to update every version
362+
resource.WithAttributes(semconv.TelemetrySDKVersionKey.String("latest")),
363+
)
364+
require.NoError(t, err)
365+
res, err = resource.Merge(resource.Default(), res)
366+
require.NoError(t, err)
367+
368+
provider := metric.NewMeterProvider(
369+
metric.WithReader(exporter),
370+
metric.WithResource(res),
371+
)
372+
373+
fooCounter, err := provider.Meter("meterfoo", otelmetric.WithInstrumentationVersion("v0.1.0")).
374+
SyncInt64().Counter(
375+
"foo",
376+
instrument.WithUnit(unit.Milliseconds),
377+
instrument.WithDescription("meter foo counter"))
378+
assert.NoError(t, err)
379+
fooCounter.Add(ctx, 100, attribute.String("type", "foo"))
380+
381+
barCounter, err := provider.Meter("meterbar", otelmetric.WithInstrumentationVersion("v0.1.0")).
382+
SyncInt64().Counter(
383+
"bar",
384+
instrument.WithUnit(unit.Milliseconds),
385+
instrument.WithDescription("meter bar counter"))
386+
assert.NoError(t, err)
387+
barCounter.Add(ctx, 200, attribute.String("type", "bar"))
388+
389+
file, err := os.Open("testdata/multi_scopes.txt")
390+
require.NoError(t, err)
391+
t.Cleanup(func() { require.NoError(t, file.Close()) })
392+
393+
err = testutil.GatherAndCompare(registry, file)
394+
require.NoError(t, err)
395+
}
+5-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# HELP foo_milliseconds_total a simple counter
22
# TYPE foo_milliseconds_total counter
3-
foo_milliseconds_total{A="B",C="D",E="true",F="42"} 24.3
4-
foo_milliseconds_total{A="D",C="B",E="true",F="42"} 5
3+
foo_milliseconds_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
4+
foo_milliseconds_total{A="D",C="B",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 5
5+
# HELP otel_scope_info Instrumentation Scope metadata
6+
# TYPE otel_scope_info gauge
7+
otel_scope_info{otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
58
# HELP target_info Target metadata
69
# TYPE target_info gauge
710
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# HELP foo_total a simple counter
22
# TYPE foo_total counter
3-
foo_total{A="B",C="D",E="true",F="42"} 24.3
3+
foo_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
4+
# HELP otel_scope_info Instrumentation Scope metadata
5+
# TYPE otel_scope_info gauge
6+
otel_scope_info{otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
47
# HELP target_info Target metadata
58
# TYPE target_info gauge
69
target_info{A="B",C="D",service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# HELP foo_total a simple counter
22
# TYPE foo_total counter
3-
foo_total{A="B",C="D",E="true",F="42"} 24.3
3+
foo_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
4+
# HELP otel_scope_info Instrumentation Scope metadata
5+
# TYPE otel_scope_info gauge
6+
otel_scope_info{otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
47
# HELP target_info Target metadata
58
# TYPE target_info gauge
69
target_info 1
+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# HELP bar_ratio a fun little gauge
22
# TYPE bar_ratio gauge
3-
bar_ratio{A="B",C="D"} .75
3+
bar_ratio{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} .75
4+
# HELP otel_scope_info Instrumentation Scope metadata
5+
# TYPE otel_scope_info gauge
6+
otel_scope_info{otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
47
# HELP target_info Target metadata
58
# TYPE target_info gauge
69
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
+16-13
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
# HELP histogram_baz_bytes a very nice histogram
22
# TYPE histogram_baz_bytes histogram
3-
histogram_baz_bytes_bucket{A="B",C="D",le="0"} 0
4-
histogram_baz_bytes_bucket{A="B",C="D",le="5"} 0
5-
histogram_baz_bytes_bucket{A="B",C="D",le="10"} 1
6-
histogram_baz_bytes_bucket{A="B",C="D",le="25"} 2
7-
histogram_baz_bytes_bucket{A="B",C="D",le="50"} 2
8-
histogram_baz_bytes_bucket{A="B",C="D",le="75"} 2
9-
histogram_baz_bytes_bucket{A="B",C="D",le="100"} 2
10-
histogram_baz_bytes_bucket{A="B",C="D",le="250"} 4
11-
histogram_baz_bytes_bucket{A="B",C="D",le="500"} 4
12-
histogram_baz_bytes_bucket{A="B",C="D",le="1000"} 4
13-
histogram_baz_bytes_bucket{A="B",C="D",le="+Inf"} 4
14-
histogram_baz_bytes_sum{A="B",C="D"} 236
15-
histogram_baz_bytes_count{A="B",C="D"} 4
3+
histogram_baz_bytes_bucket{A="B",C="D",le="0",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 0
4+
histogram_baz_bytes_bucket{A="B",C="D",le="5",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 0
5+
histogram_baz_bytes_bucket{A="B",C="D",le="10",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
6+
histogram_baz_bytes_bucket{A="B",C="D",le="25",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 2
7+
histogram_baz_bytes_bucket{A="B",C="D",le="50",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 2
8+
histogram_baz_bytes_bucket{A="B",C="D",le="75",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 2
9+
histogram_baz_bytes_bucket{A="B",C="D",le="100",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 2
10+
histogram_baz_bytes_bucket{A="B",C="D",le="250",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
11+
histogram_baz_bytes_bucket{A="B",C="D",le="500",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
12+
histogram_baz_bytes_bucket{A="B",C="D",le="1000",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
13+
histogram_baz_bytes_bucket{A="B",C="D",le="+Inf",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
14+
histogram_baz_bytes_sum{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 236
15+
histogram_baz_bytes_count{A="B",C="D",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 4
16+
# HELP otel_scope_info Instrumentation Scope metadata
17+
# TYPE otel_scope_info gauge
18+
otel_scope_info{otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
1619
# HELP target_info Target metadata
1720
# TYPE target_info gauge
1821
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1

0 commit comments

Comments
 (0)