Skip to content

Commit 09bbb99

Browse files
[receiver/prometheusremotewrite]: handle all otel_scope_* labels per compatibility spec
1 parent 3cbf39c commit 09bbb99

File tree

3 files changed

+283
-53
lines changed

3 files changed

+283
-53
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/prometheus_remote_write
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: 101 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -69,38 +69,50 @@ type prometheusRemoteWriteReceiver struct {
6969
bodyBufferPool *sync.Pool
7070
}
7171

72-
// metricIdentity contains all the components that uniquely identify a metric
73-
// according to the OpenTelemetry Protocol data model.
74-
// 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
72+
// scopeInfo holds instrumentation scope fields extracted from otel_scope_* labels.
73+
type scopeInfo struct {
74+
Name string
75+
Version string
76+
SchemaURL string
77+
extraAttrs [][2]string // scope attributes with the "otel_scope_" prefix stripped
78+
}
79+
80+
func (si scopeInfo) key() string {
81+
const sep = "\xff"
82+
parts := make([]string, 0, 3+len(si.extraAttrs))
83+
parts = append(parts, si.Name, si.Version, si.SchemaURL)
84+
for _, kv := range si.extraAttrs {
85+
parts = append(parts, kv[0]+sep+kv[1])
86+
}
87+
return strings.Join(parts, sep)
88+
}
89+
90+
// metricIdentity uniquely identifies a metric per the OTLP data model.
91+
// Ref: https://opentelemetry.io/docs/specs/otel/metrics/data-model/#opentelemetry-protocol-data-model
7592
type metricIdentity struct {
76-
ResourceID string
77-
ScopeName string
78-
ScopeVersion string
79-
MetricName string
80-
Unit string
81-
Type writev2.Metadata_MetricType
93+
ResourceID string
94+
ScopeKey string
95+
MetricName string
96+
Unit string
97+
Type writev2.Metadata_MetricType
8298
}
8399

84-
// createMetricIdentity creates a metricIdentity struct from the required components
85-
func createMetricIdentity(resourceID, scopeName, scopeVersion, metricName, unit string, metricType writev2.Metadata_MetricType) metricIdentity {
100+
func createMetricIdentity(resourceID string, si scopeInfo, metricName, unit string, metricType writev2.Metadata_MetricType) metricIdentity {
86101
return metricIdentity{
87-
ResourceID: resourceID,
88-
ScopeName: scopeName,
89-
ScopeVersion: scopeVersion,
90-
MetricName: metricName,
91-
Unit: unit,
92-
Type: metricType,
102+
ResourceID: resourceID,
103+
ScopeKey: si.key(),
104+
MetricName: metricName,
105+
Unit: unit,
106+
Type: metricType,
93107
}
94108
}
95109

96-
// Hash generates a unique hash for the metric identity
97110
func (mi metricIdentity) Hash() uint64 {
98111
const separator = "\xff"
99112

100113
combined := strings.Join([]string{
101114
mi.ResourceID,
102-
mi.ScopeName,
103-
mi.ScopeVersion,
115+
mi.ScopeKey,
104116
mi.MetricName,
105117
mi.Unit,
106118
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
@@ -440,8 +450,8 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
440450
case writev2.Metadata_METRIC_TYPE_COUNTER:
441451
addNumberDatapoints(metric.Sum().DataPoints(), ls, ts, &stats)
442452
key := exemplarKey{
443-
ScopeName: scopeName,
444-
ScopeVersion: scopeVersion,
453+
ScopeName: si.Name,
454+
ScopeVersion: si.Version,
445455
MetricName: metricName,
446456
MetricType: ts.Metadata.Type,
447457
}
@@ -466,7 +476,8 @@ func (prw *prometheusRemoteWriteReceiver) processHistogramTimeSeries(
466476
otelMetrics pmetric.Metrics,
467477
ls labels.Labels,
468478
ts *writev2.TimeSeries,
469-
scopeName, scopeVersion, metricName, unit, description string,
479+
si scopeInfo,
480+
metricName, unit, description string,
470481
metricCache map[uint64]pmetric.Metric,
471482
stats *promremote.WriteResponseStats,
472483
modifiedRM map[uint64]pmetric.ResourceMetrics,
@@ -516,22 +527,20 @@ func (prw *prometheusRemoteWriteReceiver) processHistogramTimeSeries(
516527
var foundScope bool
517528
for i := 0; i < rm.ScopeMetrics().Len(); i++ {
518529
s := rm.ScopeMetrics().At(i)
519-
if s.Scope().Name() == scopeName && s.Scope().Version() == scopeVersion {
530+
if scopeMatchesInfo(s, si) {
520531
scope = s
521532
foundScope = true
522533
break
523534
}
524535
}
525536
if !foundScope {
526537
scope = rm.ScopeMetrics().AppendEmpty()
527-
scope.Scope().SetName(scopeName)
528-
scope.Scope().SetVersion(scopeVersion)
538+
applyScopeInfo(scope, si)
529539
}
530540

531-
metricID := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s",
541+
metricID := fmt.Sprintf("%s:%s:%s:%s:%s:%s",
532542
resourceID.String(),
533-
scopeName,
534-
scopeVersion,
543+
si.key(),
535544
metricName,
536545
unit,
537546
fmt.Sprintf("%d", ts.Metadata.Type),
@@ -581,8 +590,8 @@ func (prw *prometheusRemoteWriteReceiver) processHistogramTimeSeries(
581590
}
582591

583592
key := exemplarKey{
584-
ScopeName: scopeName,
585-
ScopeVersion: scopeVersion,
593+
ScopeName: si.Name,
594+
ScopeVersion: si.Version,
586595
MetricName: metricName,
587596
MetricType: ts.Metadata.Type,
588597
}
@@ -786,35 +795,74 @@ func convertAbsoluteBuckets(spans []writev2.BucketSpan, counts []float64, bucket
786795
}
787796
}
788797

789-
// extractAttributes return all attributes different from job, instance, metric name and scope name/version
798+
// extractAttributes returns metric data point attributes, excluding job, instance, metric name, and all otel_scope_* labels.
790799
func extractAttributes(ls labels.Labels) pcommon.Map {
791800
attrs := pcommon.NewMap()
792-
// job, instance and metric name will always become labels
793801
attrs.EnsureCapacity(ls.Len() - 3)
794802
ls.Range(func(l labels.Label) {
795803
if l.Name != "instance" && l.Name != "job" && // Become resource attributes
796804
l.Name != model.MetricNameLabel && // Becomes metric name
797-
l.Name != "otel_scope_name" && l.Name != "otel_scope_version" { // Becomes scope name and version
805+
!strings.HasPrefix(l.Name, "otel_scope_") { // Become instrumentation scope fields
798806
attrs.PutStr(l.Name, l.Value)
799807
}
800808
})
801809
return attrs
802810
}
803811

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

810-
if sName := ls.Get("otel_scope_name"); sName != "" {
811-
scopeName = sName
843+
func scopeMatchesInfo(sm pmetric.ScopeMetrics, si scopeInfo) bool {
844+
if sm.Scope().Name() != si.Name || sm.Scope().Version() != si.Version || sm.SchemaUrl() != si.SchemaURL {
845+
return false
846+
}
847+
if sm.Scope().Attributes().Len() != len(si.extraAttrs) {
848+
return false
812849
}
850+
for _, kv := range si.extraAttrs {
851+
v, ok := sm.Scope().Attributes().Get(kv[0])
852+
if !ok || v.Str() != kv[1] {
853+
return false
854+
}
855+
}
856+
return true
857+
}
813858

814-
if sVersion := ls.Get("otel_scope_version"); sVersion != "" {
815-
scopeVersion = sVersion
859+
func applyScopeInfo(sm pmetric.ScopeMetrics, si scopeInfo) {
860+
sm.Scope().SetName(si.Name)
861+
sm.Scope().SetVersion(si.Version)
862+
sm.SetSchemaUrl(si.SchemaURL)
863+
for _, kv := range si.extraAttrs {
864+
sm.Scope().Attributes().PutStr(kv[0], kv[1])
816865
}
817-
return scopeName, scopeVersion
818866
}
819867

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

0 commit comments

Comments
 (0)