Skip to content

Commit 84ec610

Browse files
[receiver/prometheusremotewrite]: handle all otel_scope_* labels per compatibility spec
1 parent 9a486ca commit 84ec610

File tree

3 files changed

+286
-51
lines changed

3 files changed

+286
-51
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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. receiver/filelog)
7+
component: receiver/prometheusremotewrite
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Handle all `otel_scope_*` prefixed labels per the Prometheus/OTLP compatibility spec."
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: [47726]
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+
`otel_scope_schema_url` is now set as the instrumentation scope schema URL, and other `otel_scope_<attr>` labels become scope attributes (with the `otel_scope_` prefix stripped), instead of being incorrectly added as metric data point attributes.
20+
21+
# If your change doesn't affect end users or the exported elements of any package,
22+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
23+
# Optional: The change log or logs in which this entry should be included.
24+
# e.g. '[user]' or '[user, api]'
25+
# Include 'user' if the change is relevant to end users.
26+
# Include 'api' if there is a change to a library API.
27+
# Default: '[user]'
28+
change_logs: [user]

receiver/prometheusremotewritereceiver/receiver.go

Lines changed: 99 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -72,38 +72,50 @@ type prometheusRemoteWriteReceiver struct {
7272
bodyBufferPool *sync.Pool
7373
}
7474

75-
// metricIdentity contains all the components that uniquely identify a metric
76-
// according to the OpenTelemetry Protocol data model.
77-
// The definition of the metric uniqueness is based on the following document. Ref: https://opentelemetry.io/docs/specs/otel/metrics/data-model/#opentelemetry-protocol-data-model
75+
// scopeInfo holds instrumentation scope fields extracted from otel_scope_* labels.
76+
type scopeInfo struct {
77+
Name string
78+
Version string
79+
SchemaURL string
80+
extraAttrs [][2]string // scope attributes with the "otel_scope_" prefix stripped
81+
}
82+
83+
func (si scopeInfo) key() string {
84+
const sep = "\xff"
85+
parts := make([]string, 0, 3+len(si.extraAttrs))
86+
parts = append(parts, si.Name, si.Version, si.SchemaURL)
87+
for _, kv := range si.extraAttrs {
88+
parts = append(parts, kv[0]+sep+kv[1])
89+
}
90+
return strings.Join(parts, sep)
91+
}
92+
93+
// metricIdentity uniquely identifies a metric per the OTLP data model.
94+
// Ref: https://opentelemetry.io/docs/specs/otel/metrics/data-model/#opentelemetry-protocol-data-model
7895
type metricIdentity struct {
79-
ResourceID string
80-
ScopeName string
81-
ScopeVersion string
82-
MetricName string
83-
Unit string
84-
Type writev2.Metadata_MetricType
96+
ResourceID string
97+
ScopeKey string
98+
MetricName string
99+
Unit string
100+
Type writev2.Metadata_MetricType
85101
}
86102

87-
// createMetricIdentity creates a metricIdentity struct from the required components
88-
func createMetricIdentity(resourceID, scopeName, scopeVersion, metricName, unit string, metricType writev2.Metadata_MetricType) metricIdentity {
103+
func createMetricIdentity(resourceID string, si scopeInfo, metricName, unit string, metricType writev2.Metadata_MetricType) metricIdentity {
89104
return metricIdentity{
90-
ResourceID: resourceID,
91-
ScopeName: scopeName,
92-
ScopeVersion: scopeVersion,
93-
MetricName: metricName,
94-
Unit: unit,
95-
Type: metricType,
105+
ResourceID: resourceID,
106+
ScopeKey: si.key(),
107+
MetricName: metricName,
108+
Unit: unit,
109+
Type: metricType,
96110
}
97111
}
98112

99-
// Hash generates a unique hash for the metric identity
100113
func (mi metricIdentity) Hash() uint64 {
101114
const separator = "\xff"
102115

103116
combined := strings.Join([]string{
104117
mi.ResourceID,
105-
mi.ScopeName,
106-
mi.ScopeVersion,
118+
mi.ScopeKey,
107119
mi.MetricName,
108120
mi.Unit,
109121
fmt.Sprintf("%d", mi.Type),
@@ -348,7 +360,7 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
348360
continue
349361
}
350362

351-
scopeName, scopeVersion := prw.extractScopeInfo(ls)
363+
si := prw.extractScopeInfo(ls)
352364
metricName := metadata.Name
353365
if ts.Metadata.UnitRef >= uint32(len(req.Symbols)) {
354366
badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("unit ref %d is out of bounds of symbolsTable", ts.Metadata.UnitRef))
@@ -366,40 +378,38 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
366378
// Handle histograms separately due to their complex mixed-schema processing
367379
if ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_HISTOGRAM ||
368380
ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_UNSPECIFIED && len(ts.Histograms) > 0 {
369-
prw.processHistogramTimeSeries(otelMetrics, ls, ts, scopeName, scopeVersion, metricName, unit, description, metricCache, &stats, modifiedResourceMetric, exemplarMap)
381+
prw.processHistogramTimeSeries(otelMetrics, ls, ts, si, metricName, unit, description, metricCache, &stats, modifiedResourceMetric, exemplarMap)
370382
continue
371383
}
372384

373385
// Handle regular metrics (gauge, counter, summary)
374386
rm, _ := prw.getOrCreateRM(ls, otelMetrics, modifiedResourceMetric)
375387

376388
resourceID := identity.OfResource(rm.Resource())
377-
metricIdentity := createMetricIdentity(
389+
metricID := createMetricIdentity(
378390
resourceID.String(), // Resource identity
379-
scopeName, // Scope name
380-
scopeVersion, // Scope version
391+
si, // Scope info
381392
metricName, // Metric name
382393
unit, // Unit
383394
ts.Metadata.Type, // Metric type
384395
)
385396

386-
metricKey := metricIdentity.Hash()
397+
metricKey := metricID.Hash()
387398

388399
// Find or create scope
389400
var scope pmetric.ScopeMetrics
390401
var foundScope bool
391402
for i := 0; i < rm.ScopeMetrics().Len(); i++ {
392403
s := rm.ScopeMetrics().At(i)
393-
if s.Scope().Name() == scopeName && s.Scope().Version() == scopeVersion {
404+
if scopeMatchesInfo(s, si) {
394405
scope = s
395406
foundScope = true
396407
break
397408
}
398409
}
399410
if !foundScope {
400411
scope = rm.ScopeMetrics().AppendEmpty()
401-
scope.Scope().SetName(scopeName)
402-
scope.Scope().SetVersion(scopeVersion)
412+
applyScopeInfo(scope, si)
403413
}
404414

405415
// Get or create metric
@@ -455,7 +465,8 @@ func (prw *prometheusRemoteWriteReceiver) processHistogramTimeSeries(
455465
otelMetrics pmetric.Metrics,
456466
ls labels.Labels,
457467
ts *writev2.TimeSeries,
458-
scopeName, scopeVersion, metricName, unit, description string,
468+
si scopeInfo,
469+
metricName, unit, description string,
459470
metricCache map[uint64]pmetric.Metric,
460471
stats *promremote.WriteResponseStats,
461472
modifiedRM map[uint64]pmetric.ResourceMetrics,
@@ -505,22 +516,20 @@ func (prw *prometheusRemoteWriteReceiver) processHistogramTimeSeries(
505516
var foundScope bool
506517
for i := 0; i < rm.ScopeMetrics().Len(); i++ {
507518
s := rm.ScopeMetrics().At(i)
508-
if s.Scope().Name() == scopeName && s.Scope().Version() == scopeVersion {
519+
if scopeMatchesInfo(s, si) {
509520
scope = s
510521
foundScope = true
511522
break
512523
}
513524
}
514525
if !foundScope {
515526
scope = rm.ScopeMetrics().AppendEmpty()
516-
scope.Scope().SetName(scopeName)
517-
scope.Scope().SetVersion(scopeVersion)
527+
applyScopeInfo(scope, si)
518528
}
519529

520-
metricID := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s",
530+
metricID := fmt.Sprintf("%s:%s:%s:%s:%s:%s",
521531
resourceID.String(),
522-
scopeName,
523-
scopeVersion,
532+
si.key(),
524533
metricName,
525534
unit,
526535
fmt.Sprintf("%d", ts.Metadata.Type),
@@ -570,8 +579,8 @@ func (prw *prometheusRemoteWriteReceiver) processHistogramTimeSeries(
570579
}
571580

572581
key := exemplarKey{
573-
ScopeName: scopeName,
574-
ScopeVersion: scopeVersion,
582+
ScopeName: si.Name,
583+
ScopeVersion: si.Version,
575584
MetricName: metricName,
576585
MetricType: ts.Metadata.Type,
577586
}
@@ -775,35 +784,74 @@ func convertAbsoluteBuckets(spans []writev2.BucketSpan, counts []float64, bucket
775784
}
776785
}
777786

778-
// extractAttributes return all attributes different from job, instance, metric name and scope name/version
787+
// extractAttributes returns metric data point attributes, excluding job, instance, metric name, and all otel_scope_* labels.
779788
func extractAttributes(ls labels.Labels) pcommon.Map {
780789
attrs := pcommon.NewMap()
781-
// job, instance and metric name will always become labels
782790
attrs.EnsureCapacity(ls.Len() - 3)
783791
ls.Range(func(l labels.Label) {
784792
if l.Name != "instance" && l.Name != "job" && // Become resource attributes
785793
l.Name != model.MetricNameLabel && // Becomes metric name
786-
l.Name != "otel_scope_name" && l.Name != "otel_scope_version" { // Becomes scope name and version
794+
!strings.HasPrefix(l.Name, "otel_scope_") { // Become instrumentation scope fields
787795
attrs.PutStr(l.Name, l.Value)
788796
}
789797
})
790798
return attrs
791799
}
792800

793-
// extractScopeInfo extracts the scope name and version from the labels. If the labels do not contain the scope name/version,
794-
// it will use the default values from the settings.
795-
func (prw *prometheusRemoteWriteReceiver) extractScopeInfo(ls labels.Labels) (string, string) {
796-
scopeName := prw.settings.BuildInfo.Description
797-
scopeVersion := prw.settings.BuildInfo.Version
801+
// extractScopeInfo extracts all otel_scope_* labels into a scopeInfo per the Prometheus/OTLP compatibility spec.
802+
// Falls back to receiver build info when otel_scope_name is absent.
803+
func (prw *prometheusRemoteWriteReceiver) extractScopeInfo(ls labels.Labels) scopeInfo {
804+
si := scopeInfo{
805+
Name: prw.settings.BuildInfo.Description,
806+
Version: prw.settings.BuildInfo.Version,
807+
}
808+
809+
ls.Range(func(l labels.Label) {
810+
switch l.Name {
811+
case "otel_scope_name":
812+
if l.Value != "" {
813+
si.Name = l.Value
814+
}
815+
case "otel_scope_version":
816+
if l.Value != "" {
817+
si.Version = l.Value
818+
}
819+
case "otel_scope_schema_url":
820+
si.SchemaURL = l.Value
821+
default:
822+
if strings.HasPrefix(l.Name, "otel_scope_") {
823+
attrKey := strings.TrimPrefix(l.Name, "otel_scope_")
824+
si.extraAttrs = append(si.extraAttrs, [2]string{attrKey, l.Value})
825+
}
826+
}
827+
})
828+
829+
return si
830+
}
798831

799-
if sName := ls.Get("otel_scope_name"); sName != "" {
800-
scopeName = sName
832+
func scopeMatchesInfo(sm pmetric.ScopeMetrics, si scopeInfo) bool {
833+
if sm.Scope().Name() != si.Name || sm.Scope().Version() != si.Version || sm.SchemaUrl() != si.SchemaURL {
834+
return false
835+
}
836+
if sm.Scope().Attributes().Len() != len(si.extraAttrs) {
837+
return false
801838
}
839+
for _, kv := range si.extraAttrs {
840+
v, ok := sm.Scope().Attributes().Get(kv[0])
841+
if !ok || v.Str() != kv[1] {
842+
return false
843+
}
844+
}
845+
return true
846+
}
802847

803-
if sVersion := ls.Get("otel_scope_version"); sVersion != "" {
804-
scopeVersion = sVersion
848+
func applyScopeInfo(sm pmetric.ScopeMetrics, si scopeInfo) {
849+
sm.Scope().SetName(si.Name)
850+
sm.Scope().SetVersion(si.Version)
851+
sm.SetSchemaUrl(si.SchemaURL)
852+
for _, kv := range si.extraAttrs {
853+
sm.Scope().Attributes().PutStr(kv[0], kv[1])
805854
}
806-
return scopeName, scopeVersion
807855
}
808856

809857
// addNHCBDatapoint converts a single Native Histogram Custom Buckets (NHCB) to OpenTelemetry histogram datapoints

0 commit comments

Comments
 (0)