From 0028929caf230713e3a02ea65d07d8dda85fcc6a Mon Sep 17 00:00:00 2001 From: Chetan Date: Thu, 6 Nov 2025 23:37:28 +0530 Subject: [PATCH 1/2] accept unspecified histograms --- .chloggen/support-unspecified-histogram.yaml | 27 +++ .../prometheusremotewritereceiver/receiver.go | 10 +- .../receiver_test.go | 168 +++++++++++++++++- 3 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 .chloggen/support-unspecified-histogram.yaml diff --git a/.chloggen/support-unspecified-histogram.yaml b/.chloggen/support-unspecified-histogram.yaml new file mode 100644 index 0000000000000..0ad078c1eb29f --- /dev/null +++ b/.chloggen/support-unspecified-histogram.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. receiver/filelog) +component: receiver/prometheusremotewrite + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: prometheusremotewrite receiver now accepts metric type unspcified histograms. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [41840] + +# (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] \ No newline at end of file diff --git a/receiver/prometheusremotewritereceiver/receiver.go b/receiver/prometheusremotewritereceiver/receiver.go index f0f396205d659..e90fc04e3355e 100644 --- a/receiver/prometheusremotewritereceiver/receiver.go +++ b/receiver/prometheusremotewritereceiver/receiver.go @@ -341,7 +341,8 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr description := req.Symbols[ts.Metadata.HelpRef] // Handle histograms separately due to their complex mixed-schema processing - if ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_HISTOGRAM { + if ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_HISTOGRAM || + ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_UNSPECIFIED && len(ts.Histograms) > 0 { prw.processHistogramTimeSeries(otelMetrics, ls, ts, scopeName, scopeVersion, metricName, unit, description, metricCache, &stats, modifiedResourceMetric) continue } @@ -513,6 +514,13 @@ func (prw *prometheusRemoteWriteReceiver) processHistogramTimeSeries( hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) } metricCache[metricIDHash] = histMetric + switch ts.Metadata.Type { + case writev2.Metadata_METRIC_TYPE_HISTOGRAM: + histMetric.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") + case writev2.Metadata_METRIC_TYPE_UNSPECIFIED: + histMetric.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "unknown") + default: + } } else if len(histMetric.Description()) < len(description) { // When the new description is longer than the existing one, we should update the metric description. // Reference to this behavior: https://opentelemetry.io/docs/specs/otel/metrics/data-model/#opentelemetry-protocol-data-model-producer-recommendations diff --git a/receiver/prometheusremotewritereceiver/receiver_test.go b/receiver/prometheusremotewritereceiver/receiver_test.go index 31228bda50ffa..461448077f5e6 100644 --- a/receiver/prometheusremotewritereceiver/receiver_test.go +++ b/receiver/prometheusremotewritereceiver/receiver_test.go @@ -466,7 +466,7 @@ func TestTranslateV2(t *testing.T) { cbneMetric.SetName("test_metric") cbneMetric.SetUnit("") cbneMetric.SetDescription("") - // cbneMetric.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") + cbneMetric.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") hist := cbneMetric.SetEmptyHistogram() hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -497,7 +497,7 @@ func TestTranslateV2(t *testing.T) { expMetric.SetName("test_metric") expMetric.SetUnit("") expMetric.SetDescription("") - // expMetric.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") + expMetric.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") expHist := expMetric.SetEmptyExponentialHistogram() expHist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -744,7 +744,7 @@ func TestTranslateV2(t *testing.T) { m.SetName("test_metric") m.SetUnit("") m.SetDescription("") - // m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") + m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") hist := m.SetEmptyExponentialHistogram() hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -828,7 +828,7 @@ func TestTranslateV2(t *testing.T) { m.SetName("test_metric") m.SetUnit("") m.SetDescription("") - // m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") + m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") hist := m.SetEmptyExponentialHistogram() hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -891,7 +891,7 @@ func TestTranslateV2(t *testing.T) { m := sm.Metrics().AppendEmpty() m.SetName("test_metric") m.SetUnit("") - // m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") + m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") hist := m.SetEmptyExponentialHistogram() hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -981,7 +981,7 @@ func TestTranslateV2(t *testing.T) { m := sm.Metrics().AppendEmpty() m.SetName("test_metric") m.SetUnit("") - // m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") + m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") hist := m.SetEmptyExponentialHistogram() hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -1177,6 +1177,7 @@ func TestTranslateV2(t *testing.T) { m1.SetName("test_hncb_histogram") m1.SetUnit("") m1.SetDescription("") + m1.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") hist := m1.SetEmptyHistogram() hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -1243,6 +1244,7 @@ func TestTranslateV2(t *testing.T) { m1.SetName("test_hncb_histogram_stale") m1.SetUnit("") m1.SetDescription("") + m1.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") hist := m1.SetEmptyHistogram() hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -1395,7 +1397,7 @@ func TestTranslateV2(t *testing.T) { m2.SetName("test_mixed_histogram") m2.SetUnit("") m2.SetDescription("") - // m2.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") + m2.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "histogram") hist2 := m2.SetEmptyExponentialHistogram() hist2.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -1410,6 +1412,158 @@ func TestTranslateV2(t *testing.T) { dp2.Positive().SetOffset(0) dp2.Positive().BucketCounts().FromRaw([]uint64{30, 70}) + return metrics + }(), + }, + { + name: "unspecified histogram", + request: &writev2.Request{ + Symbols: []string{ + "", + "__name__", "test_metric", // 1, 2 + "job", "service-x/test", // 3, 4 + "instance", "107cn001", // 5, 6 + "otel_scope_name", "scope1", // 7, 8 + "otel_scope_version", "v1", // 9, 10 + "attr1", "attr1", // 11, 12 + }, + Timeseries: []writev2.TimeSeries{ + { + CreatedTimestamp: 1, + Metadata: writev2.Metadata{}, + Histograms: []writev2.Histogram{ + { + Count: &writev2.Histogram_CountInt{ + CountInt: 20, + }, + Sum: 30, + Timestamp: 1, + ZeroThreshold: 1, + ZeroCount: &writev2.Histogram_ZeroCountInt{ + ZeroCountInt: 2, + }, + Schema: -4, + PositiveSpans: []writev2.BucketSpan{{Offset: 1, Length: 2}, {Offset: 3, Length: 1}}, + NegativeSpans: []writev2.BucketSpan{{Offset: 0, Length: 1}, {Offset: 2, Length: 1}}, + PositiveDeltas: []int64{100, 244, 221}, + NegativeDeltas: []int64{1, 2}, + }, + }, + LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + }, + }, + }, + expectedStats: remote.WriteResponseStats{ + Confirmed: true, + Samples: 0, + Histograms: 1, + Exemplars: 0, + }, + expectedMetrics: func() pmetric.Metrics { + metrics := pmetric.NewMetrics() + rm := metrics.ResourceMetrics().AppendEmpty() + attrs := rm.Resource().Attributes() + attrs.PutStr("service.namespace", "service-x") + attrs.PutStr("service.name", "test") + attrs.PutStr("service.instance.id", "107cn001") + + sm := rm.ScopeMetrics().AppendEmpty() + sm.Scope().SetName("scope1") + sm.Scope().SetVersion("v1") + + m := sm.Metrics().AppendEmpty() + m.SetName("test_metric") + m.SetUnit("") + m.SetDescription("") + m.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "unknown") + + hist := m.SetEmptyExponentialHistogram() + hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + + dp := hist.DataPoints().AppendEmpty() + dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond))) + dp.SetStartTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond))) + dp.SetScale(-4) + dp.SetSum(30) + dp.SetCount(20) + dp.SetZeroCount(2) + dp.SetZeroThreshold(1) + dp.Positive().SetOffset(0) + dp.Positive().BucketCounts().FromRaw([]uint64{100, 344, 0, 0, 0, 565}) + dp.Negative().BucketCounts().FromRaw([]uint64{1}) + dp.Negative().SetOffset(-1) + dp.Negative().BucketCounts().FromRaw([]uint64{1, 0, 0, 3}) + dp.Attributes().PutStr("attr1", "attr1") + return metrics + }(), + }, + { + name: "unspecified nhcb", + request: &writev2.Request{ + Symbols: []string{ + "", + "__name__", + "test_hncb_histogram", + "job", + "test", + "instance", + "localhost:8080", + "seconds", + "Test NHCB histogram", + }, + Timeseries: []writev2.TimeSeries{ + { + LabelsRefs: []uint32{1, 2, 3, 4, 5, 6}, // __name__=test_hncb_histogram, job=test, instance=localhost:8080 + CreatedTimestamp: 123456000, + Metadata: writev2.Metadata{}, + Histograms: []writev2.Histogram{ + { + Timestamp: 123456789, + Schema: -53, // NHCB schema + Sum: 100.5, + Count: &writev2.Histogram_CountInt{CountInt: 180}, + CustomValues: []float64{1.0, 2.0, 5.0, 10.0}, // Custom bucket boundaries + PositiveSpans: []writev2.BucketSpan{ + {Offset: 0, Length: 5}, // 5 buckets: 4 custom + 1 overflow + }, + PositiveDeltas: []int64{10, 15, 20, 5, 0}, // Delta counts for each bucket + }, + }, + }, + }, + }, + expectedStats: remote.WriteResponseStats{ + Confirmed: true, + Samples: 0, + Histograms: 1, + Exemplars: 0, + }, + expectedMetrics: func() pmetric.Metrics { + metrics := pmetric.NewMetrics() + rm := metrics.ResourceMetrics().AppendEmpty() + attrs := rm.Resource().Attributes() + attrs.PutStr("service.name", "test") + attrs.PutStr("service.instance.id", "localhost:8080") + + sm := rm.ScopeMetrics().AppendEmpty() + sm.Scope().SetName("OpenTelemetry Collector") + sm.Scope().SetVersion("latest") + m1 := sm.Metrics().AppendEmpty() + m1.SetName("test_hncb_histogram") + m1.SetUnit("") + m1.SetDescription("") + m1.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "unknown") + hist := m1.SetEmptyHistogram() + hist.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + + dp := hist.DataPoints().AppendEmpty() + dp.SetStartTimestamp(pcommon.Timestamp(123456000 * int64(time.Millisecond))) + dp.SetTimestamp(pcommon.Timestamp(123456789 * int64(time.Millisecond))) + dp.SetSum(100.5) + dp.SetCount(180) + dp.ExplicitBounds().FromRaw([]float64{1.0, 2.0, 5.0, 10.0}) + dp.BucketCounts().FromRaw([]uint64{10, 25, 45, 50, 50}) + return metrics }(), }, From f43b1bfb5214eeb42c37e1638c8c2acc3a3d9104 Mon Sep 17 00:00:00 2001 From: Chetan Date: Thu, 13 Nov 2025 18:52:53 +0530 Subject: [PATCH 2/2] cleanup --- receiver/prometheusremotewritereceiver/receiver.go | 3 +++ .../prometheusremotewritereceiver/receiver_test.go | 14 +++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/receiver/prometheusremotewritereceiver/receiver.go b/receiver/prometheusremotewritereceiver/receiver.go index e90fc04e3355e..59fa3ca975d6f 100644 --- a/receiver/prometheusremotewritereceiver/receiver.go +++ b/receiver/prometheusremotewritereceiver/receiver.go @@ -520,6 +520,9 @@ func (prw *prometheusRemoteWriteReceiver) processHistogramTimeSeries( case writev2.Metadata_METRIC_TYPE_UNSPECIFIED: histMetric.Metadata().PutStr(prometheus.MetricMetadataTypeKey, "unknown") default: + // This default case should not be reached as this function is only called when: + // 1. ts.Metadata.Type == METRIC_TYPE_HISTOGRAM, or + // 2. ts.Metadata.Type == METRIC_TYPE_UNSPECIFIED && len(ts.Histograms) > 0 } } else if len(histMetric.Description()) < len(description) { // When the new description is longer than the existing one, we should update the metric description. diff --git a/receiver/prometheusremotewritereceiver/receiver_test.go b/receiver/prometheusremotewritereceiver/receiver_test.go index 461448077f5e6..5e86f4701f003 100644 --- a/receiver/prometheusremotewritereceiver/receiver_test.go +++ b/receiver/prometheusremotewritereceiver/receiver_test.go @@ -1502,18 +1502,14 @@ func TestTranslateV2(t *testing.T) { request: &writev2.Request{ Symbols: []string{ "", - "__name__", - "test_hncb_histogram", - "job", - "test", - "instance", - "localhost:8080", - "seconds", - "Test NHCB histogram", + "__name__", "test_hncb_histogram", // 1,2 + "job", "test", // 3, 4 + "instance", "localhost:8080", // 5, 6 + "seconds", "Test NHCB histogram", // 7, 8 }, Timeseries: []writev2.TimeSeries{ { - LabelsRefs: []uint32{1, 2, 3, 4, 5, 6}, // __name__=test_hncb_histogram, job=test, instance=localhost:8080 + LabelsRefs: []uint32{1, 2, 3, 4, 5, 6}, CreatedTimestamp: 123456000, Metadata: writev2.Metadata{}, Histograms: []writev2.Histogram{