Skip to content

Commit 642e7f7

Browse files
authored
Parse scope information from otel_scope_name, otel_scope_version, and otel_scope_info (#25898)
**Description:** Implements this specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope, with open-telemetry/opentelemetry-specification#3660 Instrumentation Scope > Each otel_scope_info metric point present in a batch of metrics SHOULD be dropped from the incoming scrape, and converted to an instrumentation scope. The otel_scope_name and otel_scope_version labels, if present, MUST be converted to the Name and Version of the Instrumentation Scope. Additional labels MUST be added as scope attributes, with keys and values unaltered. Other metrics in the batch which have otel_scope_name and otel_scope_version labels that match an instrumentation scope MUST be placed within the matching instrumentation scope, and MUST remove those labels. > Metrics which are not found to be associated with an instrumentation scope MUST all be placed within an empty instrumentation scope, and MUST not have any labels removed. It does this by: * For all metrics, use `otel_scope_name` and `otel_scope_version` to set the scope name and version. * For `otel_scope_info` metrics, use `otel_scope_name` and `otel_scope_version` as the scope name and version, and all other labels (other than `job`/`instance`/`__name__`) as scope attributes. * Change `map[metricName]metricFamily` to `map[scope][metricName]metricFamily` to sort metrics by instrumentation scope as they come in, and to make writing the scope easier on commit. **Link to tracking Issue:** Fixes #25870
1 parent 734cd3e commit 642e7f7

File tree

4 files changed

+190
-42
lines changed

4 files changed

+190
-42
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: prometheusreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: The otel_scope_name and otel_scope_version labels are used to populate scope name and version. otel_scope_info is used to populate scope attributes.
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [25870]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

receiver/prometheusreceiver/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,13 @@ This receiver accepts exemplars coming in Prometheus format and converts it to O
149149

150150
[sc]: https://github.com/prometheus/prometheus/blob/v2.28.1/docs/configuration/configuration.md#scrape_config
151151

152+
## Resource and Scope
153+
154+
This receiver drops the `target_info` prometheus metric, if present, and uses attributes on
155+
that metric to populate the OpenTelemetry Resource.
156+
157+
It drops `otel_scope_name` and `otel_scope_version` labels, if present, from metrics, and uses them to populate
158+
the OpenTelemetry Instrumentation Scope name and version. It drops the `otel_scope_info` metric,
159+
and uses attributes (other than `otel_scope_name` and `otel_scope_version`) to populate Scope
160+
Attributes.
161+

receiver/prometheusreceiver/internal/transaction.go

Lines changed: 110 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,38 @@ import (
2727
)
2828

2929
const (
30-
targetMetricName = "target_info"
31-
receiverName = "otelcol/prometheusreceiver"
30+
targetMetricName = "target_info"
31+
scopeMetricName = "otel_scope_info"
32+
scopeNameLabel = "otel_scope_name"
33+
scopeVersionLabel = "otel_scope_version"
34+
receiverName = "otelcol/prometheusreceiver"
3235
)
3336

3437
type transaction struct {
35-
isNew bool
36-
trimSuffixes bool
37-
ctx context.Context
38-
families map[string]*metricFamily
39-
mc scrape.MetricMetadataStore
40-
sink consumer.Metrics
41-
externalLabels labels.Labels
42-
nodeResource pcommon.Resource
43-
logger *zap.Logger
44-
buildInfo component.BuildInfo
45-
metricAdjuster MetricsAdjuster
46-
obsrecv *obsreport.Receiver
38+
isNew bool
39+
trimSuffixes bool
40+
ctx context.Context
41+
families map[scopeID]map[string]*metricFamily
42+
mc scrape.MetricMetadataStore
43+
sink consumer.Metrics
44+
externalLabels labels.Labels
45+
nodeResource pcommon.Resource
46+
scopeAttributes map[scopeID]pcommon.Map
47+
logger *zap.Logger
48+
buildInfo component.BuildInfo
49+
metricAdjuster MetricsAdjuster
50+
obsrecv *obsreport.Receiver
4751
// Used as buffer to calculate series ref hash.
4852
bufBytes []byte
4953
}
5054

55+
var emptyScopeID scopeID
56+
57+
type scopeID struct {
58+
name string
59+
version string
60+
}
61+
5162
func newTransaction(
5263
ctx context.Context,
5364
metricAdjuster MetricsAdjuster,
@@ -57,17 +68,18 @@ func newTransaction(
5768
obsrecv *obsreport.Receiver,
5869
trimSuffixes bool) *transaction {
5970
return &transaction{
60-
ctx: ctx,
61-
families: make(map[string]*metricFamily),
62-
isNew: true,
63-
trimSuffixes: trimSuffixes,
64-
sink: sink,
65-
metricAdjuster: metricAdjuster,
66-
externalLabels: externalLabels,
67-
logger: settings.Logger,
68-
buildInfo: settings.BuildInfo,
69-
obsrecv: obsrecv,
70-
bufBytes: make([]byte, 0, 1024),
71+
ctx: ctx,
72+
families: make(map[scopeID]map[string]*metricFamily),
73+
isNew: true,
74+
trimSuffixes: trimSuffixes,
75+
sink: sink,
76+
metricAdjuster: metricAdjuster,
77+
externalLabels: externalLabels,
78+
logger: settings.Logger,
79+
buildInfo: settings.BuildInfo,
80+
obsrecv: obsrecv,
81+
bufBytes: make([]byte, 0, 1024),
82+
scopeAttributes: make(map[scopeID]pcommon.Map),
7183
}
7284
}
7385

@@ -121,10 +133,17 @@ func (t *transaction) Append(_ storage.SeriesRef, ls labels.Labels, atMs int64,
121133

122134
// For the `target_info` metric we need to convert it to resource attributes.
123135
if metricName == targetMetricName {
124-
return 0, t.AddTargetInfo(ls)
136+
t.AddTargetInfo(ls)
137+
return 0, nil
138+
}
139+
140+
// For the `otel_scope_info` metric we need to convert it to scope attributes.
141+
if metricName == scopeMetricName {
142+
t.addScopeInfo(ls)
143+
return 0, nil
125144
}
126145

127-
curMF := t.getOrCreateMetricFamily(metricName)
146+
curMF := t.getOrCreateMetricFamily(getScopeID(ls), metricName)
128147
err := curMF.addSeries(t.getSeriesRef(ls, curMF.mtype), metricName, ls, atMs, val)
129148
if err != nil {
130149
t.logger.Warn("failed to add datapoint", zap.Error(err), zap.String("metric_name", metricName), zap.Any("labels", ls))
@@ -133,18 +152,22 @@ func (t *transaction) Append(_ storage.SeriesRef, ls labels.Labels, atMs int64,
133152
return 0, nil // never return errors, as that fails the whole scrape
134153
}
135154

136-
func (t *transaction) getOrCreateMetricFamily(mn string) *metricFamily {
137-
curMf, ok := t.families[mn]
155+
func (t *transaction) getOrCreateMetricFamily(scope scopeID, mn string) *metricFamily {
156+
_, ok := t.families[scope]
157+
if !ok {
158+
t.families[scope] = make(map[string]*metricFamily)
159+
}
160+
curMf, ok := t.families[scope][mn]
138161
if !ok {
139162
fn := mn
140163
if _, ok := t.mc.GetMetadata(mn); !ok {
141164
fn = normalizeMetricName(mn)
142165
}
143-
if mf, ok := t.families[fn]; ok && mf.includesMetric(mn) {
166+
if mf, ok := t.families[scope][fn]; ok && mf.includesMetric(mn) {
144167
curMf = mf
145168
} else {
146169
curMf = newMetricFamily(mn, t.mc, t.logger)
147-
t.families[curMf.name] = curMf
170+
t.families[scope][curMf.name] = curMf
148171
}
149172
}
150173
return curMf
@@ -174,7 +197,7 @@ func (t *transaction) AppendExemplar(_ storage.SeriesRef, l labels.Labels, e exe
174197
return 0, errMetricNameNotFound
175198
}
176199

177-
mf := t.getOrCreateMetricFamily(mn)
200+
mf := t.getOrCreateMetricFamily(getScopeID(l), mn)
178201
mf.addExemplar(t.getSeriesRef(l, mf.mtype), e)
179202

180203
return 0, nil
@@ -201,18 +224,47 @@ func (t *transaction) getMetrics(resource pcommon.Resource) (pmetric.Metrics, er
201224
md := pmetric.NewMetrics()
202225
rms := md.ResourceMetrics().AppendEmpty()
203226
resource.CopyTo(rms.Resource())
204-
ils := rms.ScopeMetrics().AppendEmpty()
205-
ils.Scope().SetName(receiverName)
206-
ils.Scope().SetVersion(t.buildInfo.Version)
207-
metrics := ils.Metrics()
208227

209-
for _, mf := range t.families {
210-
mf.appendMetric(metrics, t.trimSuffixes)
228+
for scope, mfs := range t.families {
229+
ils := rms.ScopeMetrics().AppendEmpty()
230+
// If metrics don't include otel_scope_name or otel_scope_version
231+
// labels, use the receiver name and version.
232+
if scope == emptyScopeID {
233+
ils.Scope().SetName(receiverName)
234+
ils.Scope().SetVersion(t.buildInfo.Version)
235+
} else {
236+
// Otherwise, use the scope that was provided with the metrics.
237+
ils.Scope().SetName(scope.name)
238+
ils.Scope().SetVersion(scope.version)
239+
// If we got an otel_scope_info metric for that scope, get scope
240+
// attributes from it.
241+
attributes, ok := t.scopeAttributes[scope]
242+
if ok {
243+
attributes.CopyTo(ils.Scope().Attributes())
244+
}
245+
}
246+
metrics := ils.Metrics()
247+
for _, mf := range mfs {
248+
mf.appendMetric(metrics, t.trimSuffixes)
249+
}
211250
}
212251

213252
return md, nil
214253
}
215254

255+
func getScopeID(ls labels.Labels) scopeID {
256+
var scope scopeID
257+
for _, lbl := range ls {
258+
if lbl.Name == scopeNameLabel {
259+
scope.name = lbl.Value
260+
}
261+
if lbl.Name == scopeVersionLabel {
262+
scope.version = lbl.Value
263+
}
264+
}
265+
return scope
266+
}
267+
216268
func (t *transaction) initTransaction(labels labels.Labels) error {
217269
target, ok := scrape.TargetFromContext(t.ctx)
218270
if !ok {
@@ -268,18 +320,34 @@ func (t *transaction) UpdateMetadata(_ storage.SeriesRef, _ labels.Labels, _ met
268320
return 0, nil
269321
}
270322

271-
func (t *transaction) AddTargetInfo(labels labels.Labels) error {
323+
func (t *transaction) AddTargetInfo(labels labels.Labels) {
272324
attrs := t.nodeResource.Attributes()
273-
274325
for _, lbl := range labels {
275326
if lbl.Name == model.JobLabel || lbl.Name == model.InstanceLabel || lbl.Name == model.MetricNameLabel {
276327
continue
277328
}
278-
279329
attrs.PutStr(lbl.Name, lbl.Value)
280330
}
331+
}
281332

282-
return nil
333+
func (t *transaction) addScopeInfo(labels labels.Labels) {
334+
attrs := pcommon.NewMap()
335+
scope := scopeID{}
336+
for _, lbl := range labels {
337+
if lbl.Name == model.JobLabel || lbl.Name == model.InstanceLabel || lbl.Name == model.MetricNameLabel {
338+
continue
339+
}
340+
if lbl.Name == scopeNameLabel {
341+
scope.name = lbl.Value
342+
continue
343+
}
344+
if lbl.Name == scopeVersionLabel {
345+
scope.version = lbl.Value
346+
continue
347+
}
348+
attrs.PutStr(lbl.Name, lbl.Value)
349+
}
350+
t.scopeAttributes[scope] = attrs
283351
}
284352

285353
func getSeriesRef(bytes []byte, ls labels.Labels, mtype pmetric.MetricType) (uint64, []byte) {

receiver/prometheusreceiver/metrics_receiver_labels_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,3 +737,46 @@ func verifyTargetInfoResourceAttributes(t *testing.T, td *testData, rms []pmetri
737737
}),
738738
})
739739
}
740+
741+
const targetInstrumentationScopes = `
742+
# HELP jvm_memory_bytes_used Used bytes of a given JVM memory area.
743+
# TYPE jvm_memory_bytes_used gauge
744+
jvm_memory_bytes_used{area="heap", otel_scope_name="fake.scope.name", otel_scope_version="v0.1.0"} 100
745+
jvm_memory_bytes_used{area="heap", otel_scope_name="scope.with.attributes", otel_scope_version="v1.5.0"} 100
746+
jvm_memory_bytes_used{area="heap"} 100
747+
# TYPE otel_scope_info gauge
748+
otel_scope_info{animal="bear", otel_scope_name="scope.with.attributes", otel_scope_version="v1.5.0"} 1
749+
`
750+
751+
func TestScopeInfoScopeAttributes(t *testing.T) {
752+
targets := []*testData{
753+
{
754+
name: "target1",
755+
pages: []mockPrometheusResponse{
756+
{code: 200, data: targetInstrumentationScopes},
757+
},
758+
validateFunc: verifyMultipleScopes,
759+
},
760+
}
761+
762+
testComponent(t, targets, false, false, "")
763+
}
764+
765+
func verifyMultipleScopes(t *testing.T, td *testData, rms []pmetric.ResourceMetrics) {
766+
verifyNumValidScrapeResults(t, td, rms)
767+
require.Greater(t, len(rms), 0, "At least one resource metric should be present")
768+
769+
sms := rms[0].ScopeMetrics()
770+
require.Equal(t, sms.Len(), 3, "At two scope metrics should be present")
771+
require.Equal(t, sms.At(0).Scope().Name(), "fake.scope.name")
772+
require.Equal(t, sms.At(0).Scope().Version(), "v0.1.0")
773+
require.Equal(t, sms.At(0).Scope().Attributes().Len(), 0)
774+
require.Equal(t, sms.At(1).Scope().Name(), "scope.with.attributes")
775+
require.Equal(t, sms.At(1).Scope().Version(), "v1.5.0")
776+
require.Equal(t, sms.At(1).Scope().Attributes().Len(), 1)
777+
scopeAttrVal, found := sms.At(1).Scope().Attributes().Get("animal")
778+
require.True(t, found)
779+
require.Equal(t, scopeAttrVal.Str(), "bear")
780+
require.Equal(t, sms.At(2).Scope().Name(), "otelcol/prometheusreceiver")
781+
require.Equal(t, sms.At(2).Scope().Attributes().Len(), 0)
782+
}

0 commit comments

Comments
 (0)