Skip to content

Commit 0d4282d

Browse files
committed
Always parse 'otel_scope_' labels. Feature gate only controls merging labels from otel_scope_info metric
Signed-off-by: Arthur Silva Sens <[email protected]>
1 parent 44a9951 commit 0d4282d

File tree

6 files changed

+391
-136
lines changed

6 files changed

+391
-136
lines changed

.chloggen/prometheusreceiver_scope_attributes_spec_compliance.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ issues: [40060]
1818
subtext: |
1919
The OpenTelemetry Prometheus specification has been updated to deprecate the `otel_scope_info` metric
2020
in favor of embedding scope attributes directly in metrics using `otel_scope_` prefixed labels.
21-
A new feature gate `receiver.prometheusreceiver.RemoveScopeInfo` controls this behavior:
22-
- When enabled (new behavior): scope attributes are extracted from `otel_scope_` prefixed labels on all metrics
23-
- When disabled (legacy behavior): scope attributes continue to be extracted from the `otel_scope_info` metric
21+
This implementation always extracts scope attributes from `otel_scope_` prefixed labels on all metrics
22+
and always filters these labels from datapoint attributes, regardless of the feature gate setting.
23+
The feature gate `receiver.prometheusreceiver.RemoveScopeInfo` only controls `otel_scope_info` processing:
24+
- When disabled: `otel_scope_info` metrics are processed for scope attributes and merged with `otel_scope_` labels.
25+
- When enabled: `otel_scope_info` metrics are treated as regular metrics.
2426
This change maintains backward compatibility while enabling compliance with the updated specification.
25-
See: https://github.com/open-telemetry/opentelemetry-specification/issues/4223
27+
See: https://github.com/open-telemetry/opentelemetry-specification/pull/4505
2628
2729
# If your change doesn't affect end users or the exported elements of any package,
2830
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.

receiver/prometheusreceiver/README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,16 @@ This receiver accepts exemplars coming in Prometheus format and converts it to O
153153
This receiver drops the `target_info` prometheus metric, if present, and uses attributes on
154154
that metric to populate the OpenTelemetry Resource.
155155

156-
It drops `otel_scope_name` and `otel_scope_version` labels, if present, from metrics, and uses them to populate
157-
the OpenTelemetry Instrumentation Scope name and version. It drops the `otel_scope_info` metric,
158-
and uses attributes (other than `otel_scope_name` and `otel_scope_version`) to populate Scope
159-
Attributes.
156+
It always extracts scope attributes from `otel_scope_` prefixed labels on metrics. Labels with the
157+
`otel_scope_name`, `otel_scope_version`, and `otel_scope_schema_url` prefixes are used to populate the
158+
OpenTelemetry Instrumentation Scope identity, while other `otel_scope_` prefixed labels become Scope Attributes.
159+
All `otel_scope_` labels are filtered from the metric datapoint attributes.
160+
161+
The processing of `otel_scope_info` metrics is controlled by the `receiver.prometheusreceiver.RemoveScopeInfo`
162+
feature gate:
163+
- When disabled: `otel_scope_info` metrics are processed for scope attributes and merged with
164+
any `otel_scope_` labels, with `otel_scope_` labels taking precedence in conflicts.
165+
- When enabled: `otel_scope_info` metrics are treated as regular metrics and not processed for scope attributes.
160166

161167
## Prometheus API Server
162168
The Prometheus API server can be enabled to host info about the Prometheus targets, config, service discovery, and metrics. The `server_config` can be specified using the OpenTelemetry confighttp package. An example configuration would be:

receiver/prometheusreceiver/internal/metricfamily.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,7 @@ func populateAttributes(mType pmetric.MetricType, ls labels.Labels, dest pcommon
361361
return
362362
}
363363

364-
// When removeScopeInfo feature gate is enabled, filter out otel_scope_ prefixed labels
365-
// as they are now extracted as scope attributes instead of datapoint attributes
366-
if RemoveScopeInfoGate.IsEnabled() && strings.HasPrefix(l.Name, "otel_scope_") {
364+
if strings.HasPrefix(l.Name, "otel_scope_") {
367365
return
368366
}
369367

receiver/prometheusreceiver/internal/metricfamily_test.go

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,8 @@ func TestMetricGroupData_toSummaryUnitTest(t *testing.T) {
776776
}
777777
}
778778

779+
// TestPopulateAttributes_ScopeLabelFiltering tests the target behavior where otel_scope_
780+
// prefixed labels should always be filtered from datapoint attributes regardless of feature gate.
779781
func TestPopulateAttributes_ScopeLabelFiltering(t *testing.T) {
780782
tests := []struct {
781783
name string
@@ -784,40 +786,24 @@ func TestPopulateAttributes_ScopeLabelFiltering(t *testing.T) {
784786
wantAttrs map[string]string
785787
}{
786788
{
787-
name: "feature_disabled_keeps_scope_labels",
789+
name: "feature_gate_disabled",
788790
labels: labels.FromStrings(
789791
"__name__", "test_metric",
790792
"job", "test-job",
791793
"instance", "localhost:8080",
792794
"method", "GET",
795+
"otel_scope_name", "my-scope",
796+
"otel_scope_version", "1.0.0",
793797
"otel_scope_animal", "bear",
794-
"otel_scope_color", "blue",
795798
),
796799
featureEnabled: false,
797-
wantAttrs: map[string]string{
798-
"method": "GET",
799-
"otel_scope_animal": "bear",
800-
"otel_scope_color": "blue",
801-
},
802-
},
803-
{
804-
name: "feature_enabled_filters_scope_labels",
805-
labels: labels.FromStrings(
806-
"__name__", "test_metric",
807-
"job", "test-job",
808-
"instance", "localhost:8080",
809-
"method", "GET",
810-
"otel_scope_animal", "bear",
811-
"otel_scope_color", "blue",
812-
),
813-
featureEnabled: true,
814800
wantAttrs: map[string]string{
815801
"method": "GET",
816-
// otel_scope_* labels should be filtered out
802+
// otel_scope_* labels should always be filtered out regardless of feature gate
817803
},
818804
},
819805
{
820-
name: "feature_enabled_keeps_standard_scope_labels_filtered",
806+
name: "feature_gate_enabled",
821807
labels: labels.FromStrings(
822808
"__name__", "test_metric",
823809
"job", "test-job",
@@ -830,7 +816,7 @@ func TestPopulateAttributes_ScopeLabelFiltering(t *testing.T) {
830816
featureEnabled: true,
831817
wantAttrs: map[string]string{
832818
"method": "GET",
833-
// All otel_scope_* labels should be filtered out
819+
// otel_scope_* labels should always be filtered out regardless of feature gate
834820
},
835821
},
836822
}
@@ -851,14 +837,12 @@ func TestPopulateAttributes_ScopeLabelFiltering(t *testing.T) {
851837
require.Equal(t, expectedValue, actualValue.AsString(), "Unexpected value for attribute %s", key)
852838
}
853839

854-
// Verify no otel_scope_ attributes when feature is enabled
855-
if tt.featureEnabled {
856-
attrs.Range(func(k string, _ pcommon.Value) bool {
857-
require.False(t, strings.HasPrefix(k, "otel_scope_"),
858-
"Found otel_scope_ prefixed attribute %s when feature gate is enabled", k)
859-
return true
860-
})
861-
}
840+
// Verify no otel_scope_ attributes are present regardless of feature gate
841+
attrs.Range(func(k string, _ pcommon.Value) bool {
842+
require.False(t, strings.HasPrefix(k, "otel_scope_"),
843+
"Found otel_scope_ prefixed attribute %s in datapoint attributes - these should always be filtered", k)
844+
return true
845+
})
862846
})
863847
}
864848
}

receiver/prometheusreceiver/internal/transaction.go

Lines changed: 112 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ var removeStartTimeAdjustment = featuregate.GlobalRegistry().MustRegister(
4242
var RemoveScopeInfoGate = featuregate.GlobalRegistry().MustRegister(
4343
"receiver.prometheusreceiver.RemoveScopeInfo",
4444
featuregate.StageAlpha,
45-
featuregate.WithRegisterDescription("When enabled, the Prometheus receiver will"+
46-
" extract scope attributes from 'otel_scope_' prefixed labels on metrics instead of"+
47-
" from the 'otel_scope_info' metric. The 'otel_scope_info' metric will be treated as"+
48-
" a regular metric when this feature is enabled."),
45+
featuregate.WithRegisterDescription("Controls the processing of 'otel_scope_info' metrics. "+
46+
"The receiver always extracts scope attributes from 'otel_scope_' prefixed labels on all metrics. "+
47+
"When this feature gate is disabled (legacy mode), 'otel_scope_info' metrics are also processed "+
48+
"for scope attributes and merged with any 'otel_scope_' labels (with 'otel_scope_' taking precedence). "+
49+
"When enabled, 'otel_scope_info' metrics are treated as regular metrics and not processed for scope attributes."),
4950
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-specification/pull/4505"),
5051
)
5152

@@ -265,8 +266,8 @@ func (t *transaction) Append(_ storage.SeriesRef, ls labels.Labels, atMs int64,
265266
return 0, nil
266267
}
267268

268-
// For the `otel_scope_info` metric we need to convert it to scope attributes.
269-
// Only in legacy mode (when removeScopeInfo feature gate is disabled)
269+
// For the `otel_scope_info` metric we have different behavior depending on the feature gate.
270+
// It becomes a new scope when the feature gate is disabled, and a regular metric when it is enabled.
270271
if metricName == prometheus.ScopeInfoMetricName && !t.removeScopeInfo {
271272
t.addScopeInfo(*rKey, ls)
272273
return 0, nil
@@ -552,15 +553,6 @@ func (t *transaction) getMetrics() (pmetric.Metrics, error) {
552553
// Copy scope attributes if they exist
553554
if scope.Attributes().Len() > 0 {
554555
scope.Attributes().CopyTo(ils.Scope().Attributes())
555-
} else if !t.removeScopeInfo {
556-
// Legacy mode: check scopeAttributes map for this scope
557-
if scopeAttributes, ok := t.scopeAttributes[rKey]; ok {
558-
if legacyScope, isLegacy := scope.(LegacyScopeID); isLegacy {
559-
if attributes, ok := scopeAttributes[legacyScope]; ok {
560-
attributes.CopyTo(ils.Scope().Attributes())
561-
}
562-
}
563-
}
564556
}
565557
}
566558
metrics := ils.Metrics()
@@ -588,27 +580,30 @@ func (t *transaction) getMetrics() (pmetric.Metrics, error) {
588580
}
589581

590582
func (t *transaction) getScopeIdentifier(ls labels.Labels) ScopeIdentifier {
591-
if t.removeScopeInfo {
592-
return t.getScopeWithAttributes(ls)
583+
// Check if this is an otel_scope_info metric when feature gate is enabled
584+
metricName := ls.Get("__name__")
585+
if t.removeScopeInfo && metricName == prometheus.ScopeInfoMetricName {
586+
// When feature gate is enabled, otel_scope_info should extract only basic scope info
587+
// (name, version, schema_url) but NOT other otel_scope_ attributes
588+
scope := ScopeID{
589+
attributes: pcommon.NewMap(),
590+
}
591+
ls.Range(func(lbl labels.Label) {
592+
switch lbl.Name {
593+
case prometheus.ScopeNameLabelKey:
594+
scope.name = lbl.Value
595+
case prometheus.ScopeVersionLabelKey:
596+
scope.version = lbl.Value
597+
case prometheus.ScopeSchemaURLLabelKey:
598+
scope.schemaURL = lbl.Value
599+
// Don't extract other otel_scope_ labels as scope attributes for otel_scope_info when gate is enabled
600+
}
601+
})
602+
return scope
593603
}
594-
// Legacy behavior: use only scope name, version, and schema URL
595-
return t.getLegacyScope(ls)
596-
}
597604

598-
func (*transaction) getLegacyScope(ls labels.Labels) LegacyScopeID {
599-
var scope LegacyScopeID
600-
ls.Range(func(lbl labels.Label) {
601-
if lbl.Name == prometheus.ScopeNameLabelKey {
602-
scope.name = lbl.Value
603-
}
604-
if lbl.Name == prometheus.ScopeVersionLabelKey {
605-
scope.version = lbl.Value
606-
}
607-
if lbl.Name == prometheus.ScopeSchemaURLLabelKey {
608-
scope.schemaURL = lbl.Value
609-
}
610-
})
611-
return scope
605+
// Always extract otel_scope_ labels as scope attributes for all other cases
606+
return t.getScopeWithAttributes(ls)
612607
}
613608

614609
func (*transaction) getScopeWithAttributes(ls labels.Labels) ScopeID {
@@ -620,14 +615,18 @@ func (*transaction) getScopeWithAttributes(ls labels.Labels) ScopeID {
620615
switch {
621616
case lbl.Name == prometheus.ScopeNameLabelKey:
622617
scope.name = lbl.Value
618+
return
623619
case lbl.Name == prometheus.ScopeVersionLabelKey:
624620
scope.version = lbl.Value
621+
return
625622
case lbl.Name == prometheus.ScopeSchemaURLLabelKey:
626623
scope.schemaURL = lbl.Value
624+
return
627625
case strings.HasPrefix(lbl.Name, "otel_scope_"):
628626
// Extract scope attributes from otel_scope_ prefixed labels
629627
attrName := strings.TrimPrefix(lbl.Name, "otel_scope_")
630628
scope.attributes.PutStr(attrName, lbl.Value)
629+
return
631630
}
632631
})
633632
return scope
@@ -740,50 +739,101 @@ func (t *transaction) AddTargetInfo(key resourceKey, ls labels.Labels) {
740739

741740
func (t *transaction) addScopeInfo(key resourceKey, ls labels.Labels) {
742741
t.addingNativeHistogram = false
743-
attrs := pcommon.NewMap()
744-
legacyScope := LegacyScopeID{}
742+
743+
// Extract scope information from otel_scope_info metric
744+
scopeInfoAttrs := pcommon.NewMap()
745+
scopeFromInfo := ScopeID{
746+
attributes: pcommon.NewMap(),
747+
}
745748

746749
ls.Range(func(lbl labels.Label) {
747750
if lbl.Name == model.JobLabel || lbl.Name == model.InstanceLabel || lbl.Name == model.MetricNameLabel {
748751
return
749752
}
750-
if lbl.Name == prometheus.ScopeNameLabelKey {
751-
legacyScope.name = lbl.Value
752-
return
753+
switch lbl.Name {
754+
case prometheus.ScopeNameLabelKey:
755+
scopeFromInfo.name = lbl.Value
756+
case prometheus.ScopeVersionLabelKey:
757+
scopeFromInfo.version = lbl.Value
758+
case prometheus.ScopeSchemaURLLabelKey:
759+
scopeFromInfo.schemaURL = lbl.Value
760+
default:
761+
// All other labels from otel_scope_info become scope attributes
762+
scopeInfoAttrs.PutStr(lbl.Name, lbl.Value)
753763
}
754-
if lbl.Name == prometheus.ScopeVersionLabelKey {
755-
legacyScope.version = lbl.Value
756-
return
764+
})
765+
766+
// Initialize scope map if needed
767+
if _, ok := t.scopeMap[key]; !ok {
768+
t.scopeMap[key] = make(map[string]ScopeIdentifier)
769+
}
770+
771+
// Look for existing scopes with the same name/version/schema (ignoring attributes for now)
772+
var existingScopeKey string
773+
var existingScope ScopeID
774+
var found bool
775+
776+
for scopeKey, scope := range t.scopeMap[key] {
777+
if scopeID, ok := scope.(ScopeID); ok {
778+
if scopeID.name == scopeFromInfo.name &&
779+
scopeID.version == scopeFromInfo.version &&
780+
scopeID.schemaURL == scopeFromInfo.schemaURL {
781+
existingScopeKey = scopeKey
782+
existingScope = scopeID
783+
found = true
784+
break
785+
}
757786
}
758-
if lbl.Name == prometheus.ScopeSchemaURLLabelKey {
759-
legacyScope.schemaURL = lbl.Value
760-
return
787+
}
788+
789+
if found {
790+
// Remove the old scope entry
791+
delete(t.scopeMap[key], existingScopeKey)
792+
793+
// Merge attributes: otel_scope_ labels take precedence over otel_scope_info.
794+
mergedAttrs := pcommon.NewMap()
795+
scopeInfoAttrs.CopyTo(mergedAttrs)
796+
existingScope.attributes.Range(func(k string, v pcommon.Value) bool {
797+
mergedAttrs.PutStr(k, v.AsString())
798+
return true
799+
})
800+
801+
// Create merged scope with new key
802+
mergedScope := ScopeID{
803+
name: scopeFromInfo.name,
804+
version: scopeFromInfo.version,
805+
schemaURL: scopeFromInfo.schemaURL,
806+
attributes: mergedAttrs,
761807
}
762-
attrs.PutStr(lbl.Name, lbl.Value)
763-
})
808+
t.scopeMap[key][mergedScope.Key()] = mergedScope
764809

765-
// Store scope attributes separately for legacy mode (similar to original implementation)
766-
if _, ok := t.scopeAttributes[key]; !ok {
767-
t.scopeAttributes[key] = make(map[LegacyScopeID]pcommon.Map)
810+
// Update any existing metric families that were using the old scope key
811+
if t.families[key] != nil {
812+
if familiesForOldScope, exists := t.families[key][existingScopeKey]; exists {
813+
t.families[key][mergedScope.Key()] = familiesForOldScope
814+
delete(t.families[key], existingScopeKey)
815+
}
816+
}
817+
} else {
818+
// No existing scope, just store the otel_scope_info attributes
819+
scopeFromInfo.attributes = scopeInfoAttrs
820+
t.scopeMap[key][scopeFromInfo.Key()] = scopeFromInfo
768821
}
769-
t.scopeAttributes[key][legacyScope] = attrs
770822
}
771823

772824
func getSeriesRef(bytes []byte, ls labels.Labels, mtype pmetric.MetricType) (uint64, []byte) {
773825
excludeLabels := getSortedNotUsefulLabels(mtype)
774826

775-
// When removeScopeInfo feature gate is enabled, also exclude otel_scope_ prefixed labels
776-
// from series reference hash generation
777-
if RemoveScopeInfoGate.IsEnabled() {
778-
var scopeLabels []string
779-
ls.Range(func(l labels.Label) {
780-
if strings.HasPrefix(l.Name, "otel_scope_") {
781-
scopeLabels = append(scopeLabels, l.Name)
782-
}
783-
})
784-
if len(scopeLabels) > 0 {
785-
excludeLabels = append(excludeLabels, scopeLabels...)
827+
// Always exclude otel_scope_ prefixed labels from series reference hash generation
828+
// as they are extracted as scope attributes instead of datapoint attributes
829+
var scopeLabels []string
830+
ls.Range(func(l labels.Label) {
831+
if strings.HasPrefix(l.Name, "otel_scope_") {
832+
scopeLabels = append(scopeLabels, l.Name)
786833
}
834+
})
835+
if len(scopeLabels) > 0 {
836+
excludeLabels = append(excludeLabels, scopeLabels...)
787837
}
788838

789839
return ls.HashWithoutLabels(bytes, excludeLabels...)

0 commit comments

Comments
 (0)