@@ -42,10 +42,11 @@ var removeStartTimeAdjustment = featuregate.GlobalRegistry().MustRegister(
4242var 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
590582func (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
614609func (* 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
741740func (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
772824func 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