diff --git a/.chloggen/prwreceiver-checkscope.yaml b/.chloggen/prwreceiver-checkscope.yaml new file mode 100644 index 000000000000..81d9d8f7bc43 --- /dev/null +++ b/.chloggen/prwreceiver-checkscope.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: receiver/prometheusremotewrite + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Check if Scope is already present comparing with the received labels + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [36927] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/receiver/prometheusremotewritereceiver/receiver.go b/receiver/prometheusremotewritereceiver/receiver.go index 007d5a08199e..24da216a6ea8 100644 --- a/receiver/prometheusremotewritereceiver/receiver.go +++ b/receiver/prometheusremotewritereceiver/receiver.go @@ -234,9 +234,26 @@ func addGaugeDatapoints(rm pmetric.ResourceMetrics, ls labels.Labels, ts writev2 // In OTel name+type+unit is the unique identifier of a metric and we should not create // a new metric if it already exists. - // TODO: Check if Scope is already present by comparing labels "otel_scope_name" and "otel_scope_version" - // with Scope.Name and Scope.Version. If it is present, we should append to the existing Scope. - m := rm.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty().SetEmptyGauge() + scopeName := ls.Get("otel_scope_name") + scopeVersion := ls.Get("otel_scope_version") + // TODO: If the scope version or scope name is empty, get the information from the collector build tags. + // More: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#:~:text=Metrics%20which%20do%20not%20have%20an%20otel_scope_name%20or%20otel_scope_version%20label%20MUST%20be%20assigned%20an%20instrumentation%20scope%20identifying%20the%20entity%20performing%20the%20translation%20from%20Prometheus%20to%20OpenTelemetry%20(e.g.%20the%20collector%E2%80%99s%20prometheus%20receiver) + + // Check if the name and version present in the labels are already present in the ResourceMetrics. + // If it is not present, we should create a new ScopeMetrics. + // Otherwise, we should append to the existing ScopeMetrics. + for j := 0; j < rm.ScopeMetrics().Len(); j++ { + scope := rm.ScopeMetrics().At(j) + if scopeName == scope.Scope().Name() && scopeVersion == scope.Scope().Version() { + addDatapoints(scope.Metrics().AppendEmpty().SetEmptyGauge().DataPoints(), ls, ts) + return + } + } + + scope := rm.ScopeMetrics().AppendEmpty() + scope.Scope().SetName(scopeName) + scope.Scope().SetVersion(scopeVersion) + m := scope.Metrics().AppendEmpty().SetEmptyGauge() addDatapoints(m.DataPoints(), ls, ts) } diff --git a/receiver/prometheusremotewritereceiver/receiver_test.go b/receiver/prometheusremotewritereceiver/receiver_test.go index c1e46602452d..dd8b9527f410 100644 --- a/receiver/prometheusremotewritereceiver/receiver_test.go +++ b/receiver/prometheusremotewritereceiver/receiver_test.go @@ -143,6 +143,63 @@ func TestTranslateV2(t *testing.T) { expectedMetrics pmetric.Metrics expectedStats remote.WriteResponseStats }{ + { + name: "duplicated scope name and version", + request: &writev2.Request{ + Symbols: []string{ + "", + "__name__", "test_metric", + "job", "service-x/test", + "instance", "107cn001", + "otel_scope_name", "scope1", + "otel_scope_version", "v1", + "otel_scope_name", "scope2", + "otel_scope_version", "v2", + "d", "e", + "foo", "bar", + }, + Timeseries: []writev2.TimeSeries{ + { + Metadata: writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE}, + LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16}, // Same scope: scope_name: scope1. scope_version v1 + Samples: []writev2.Sample{{Value: 1, Timestamp: 1}}, + }, + { + Metadata: writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE}, + LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16}, // Same scope: scope_name: scope1. scope_version v1 + Samples: []writev2.Sample{{Value: 2, Timestamp: 2}}, + }, + { + Metadata: writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE}, + LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 11, 12, 13, 14, 17, 18}, // Different scope: scope_name: scope2. scope_version v2 + Samples: []writev2.Sample{{Value: 3, Timestamp: 3}}, + }, + }, + }, + expectedMetrics: func() pmetric.Metrics { + expected := pmetric.NewMetrics() + rm1 := expected.ResourceMetrics().AppendEmpty() + rmAttributes1 := rm1.Resource().Attributes() + rmAttributes1.PutStr("service.namespace", "service-x") + rmAttributes1.PutStr("service.name", "test") + rmAttributes1.PutStr("service.instance.id", "107cn001") + sm1 := rm1.ScopeMetrics().AppendEmpty() + sm1.Scope().SetName("scope1") + sm1.Scope().SetVersion("v1") + sm1Attributes := sm1.Metrics().AppendEmpty().SetEmptyGauge().DataPoints().AppendEmpty().Attributes() + sm1Attributes.PutStr("d", "e") + sm2Attributes := sm1.Metrics().AppendEmpty().SetEmptyGauge().DataPoints().AppendEmpty().Attributes() + sm2Attributes.PutStr("d", "e") + + sm2 := rm1.ScopeMetrics().AppendEmpty() + sm2.Scope().SetName("scope2") + sm2.Scope().SetVersion("v2") + sm3Attributes := sm2.Metrics().AppendEmpty().SetEmptyGauge().DataPoints().AppendEmpty().Attributes() + sm3Attributes.PutStr("foo", "bar") + return expected + }(), + expectedStats: remote.WriteResponseStats{}, + }, { name: "missing metric name", request: &writev2.Request{ @@ -185,8 +242,7 @@ func TestTranslateV2(t *testing.T) { sm1Attributes.PutStr("foo", "bar") // Since we don't check "scope_name" and "scope_version", we end up with duplicated scope metrics for repeated series. // TODO: Properly handle scope metrics. - sm2 := rm1.ScopeMetrics().AppendEmpty() - sm2Attributes := sm2.Metrics().AppendEmpty().SetEmptyGauge().DataPoints().AppendEmpty().Attributes() + sm2Attributes := sm1.Metrics().AppendEmpty().SetEmptyGauge().DataPoints().AppendEmpty().Attributes() sm2Attributes.PutStr("d", "e") sm2Attributes.PutStr("foo", "bar")