diff --git a/.chloggen/prometheusexporter_createdtimestamp.yaml b/.chloggen/prometheusexporter_createdtimestamp.yaml new file mode 100644 index 000000000000..b09f0a7d6e09 --- /dev/null +++ b/.chloggen/prometheusexporter_createdtimestamp.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: exporter/prometheusexpoter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Support for Prometheus Created Timestamps. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [32521] + +# (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/exporter/prometheusexporter/collector.go b/exporter/prometheusexporter/collector.go index 9c47c31b2541..f6065307eb34 100644 --- a/exporter/prometheusexporter/collector.go +++ b/exporter/prometheusexporter/collector.go @@ -182,7 +182,13 @@ func (c *collector) convertSum(metric pmetric.Metric, resourceAttrs pcommon.Map) exemplars = convertExemplars(ip.Exemplars()) } - m, err := prometheus.NewConstMetric(desc, metricType, value, attributes...) + var m prometheus.Metric + var err error + if metricType == prometheus.CounterValue && ip.StartTimestamp().AsTime().Unix() > 0 { + m, err = prometheus.NewConstMetricWithCreatedTimestamp(desc, metricType, value, ip.StartTimestamp().AsTime(), attributes...) + } else { + m, err = prometheus.NewConstMetric(desc, metricType, value, attributes...) + } if err != nil { return nil, err } @@ -214,7 +220,13 @@ func (c *collector) convertSummary(metric pmetric.Metric, resourceAttrs pcommon. } desc, attributes := c.getMetricMetadata(metric, point.Attributes(), resourceAttrs) - m, err := prometheus.NewConstSummary(desc, point.Count(), point.Sum(), quantiles, attributes...) + var m prometheus.Metric + var err error + if point.StartTimestamp().AsTime().Unix() > 0 { + m, err = prometheus.NewConstSummaryWithCreatedTimestamp(desc, point.Count(), point.Sum(), quantiles, point.StartTimestamp().AsTime(), attributes...) + } else { + m, err = prometheus.NewConstSummary(desc, point.Count(), point.Sum(), quantiles, attributes...) + } if err != nil { return nil, err } @@ -254,7 +266,13 @@ func (c *collector) convertDoubleHistogram(metric pmetric.Metric, resourceAttrs exemplars := convertExemplars(ip.Exemplars()) - m, err := prometheus.NewConstHistogram(desc, ip.Count(), ip.Sum(), points, attributes...) + var m prometheus.Metric + var err error + if ip.StartTimestamp().AsTime().Unix() > 0 { + m, err = prometheus.NewConstHistogramWithCreatedTimestamp(desc, ip.Count(), ip.Sum(), points, ip.StartTimestamp().AsTime(), attributes...) + } else { + m, err = prometheus.NewConstHistogram(desc, ip.Count(), ip.Sum(), points, attributes...) + } if err != nil { return nil, err } diff --git a/exporter/prometheusexporter/collector_test.go b/exporter/prometheusexporter/collector_test.go index 603dab7130c6..9b5d31d7efdb 100644 --- a/exporter/prometheusexporter/collector_test.go +++ b/exporter/prometheusexporter/collector_test.go @@ -17,6 +17,7 @@ import ( conventions "go.opentelemetry.io/collector/semconv/v1.25.0" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "google.golang.org/protobuf/types/known/timestamppb" prometheustranslator "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" ) @@ -287,7 +288,7 @@ func TestCollectMetricsLabelSanitize(t *testing.T) { func TestCollectMetrics(t *testing.T) { tests := []struct { name string - metric func(time.Time) pmetric.Metric + metric func(time.Time, bool) pmetric.Metric metricType prometheus.ValueType value float64 }{ @@ -295,7 +296,7 @@ func TestCollectMetrics(t *testing.T) { name: "IntGauge", metricType: prometheus.GaugeValue, value: 42.0, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetDescription("test description") @@ -304,6 +305,9 @@ func TestCollectMetrics(t *testing.T) { dp.Attributes().PutStr("label_1", "1") dp.Attributes().PutStr("label_2", "2") dp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + dp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } return }, @@ -312,7 +316,7 @@ func TestCollectMetrics(t *testing.T) { name: "Gauge", metricType: prometheus.GaugeValue, value: 42.42, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetDescription("test description") @@ -321,6 +325,9 @@ func TestCollectMetrics(t *testing.T) { dp.Attributes().PutStr("label_1", "1") dp.Attributes().PutStr("label_2", "2") dp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + dp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } return }, @@ -329,7 +336,7 @@ func TestCollectMetrics(t *testing.T) { name: "IntSum", metricType: prometheus.GaugeValue, value: 42.0, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetEmptySum().SetIsMonotonic(false) @@ -340,6 +347,9 @@ func TestCollectMetrics(t *testing.T) { dp.Attributes().PutStr("label_1", "1") dp.Attributes().PutStr("label_2", "2") dp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + dp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } return }, @@ -348,7 +358,7 @@ func TestCollectMetrics(t *testing.T) { name: "Sum", metricType: prometheus.GaugeValue, value: 42.42, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetEmptySum().SetIsMonotonic(false) @@ -359,6 +369,9 @@ func TestCollectMetrics(t *testing.T) { dp.Attributes().PutStr("label_1", "1") dp.Attributes().PutStr("label_2", "2") dp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + dp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } return }, @@ -367,7 +380,7 @@ func TestCollectMetrics(t *testing.T) { name: "MonotonicIntSum", metricType: prometheus.CounterValue, value: 42.0, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetEmptySum().SetIsMonotonic(true) @@ -378,6 +391,9 @@ func TestCollectMetrics(t *testing.T) { dp.Attributes().PutStr("label_1", "1") dp.Attributes().PutStr("label_2", "2") dp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + dp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } return }, @@ -386,7 +402,7 @@ func TestCollectMetrics(t *testing.T) { name: "MonotonicSum", metricType: prometheus.CounterValue, value: 42.42, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetEmptySum().SetIsMonotonic(true) @@ -397,6 +413,9 @@ func TestCollectMetrics(t *testing.T) { dp.Attributes().PutStr("label_1", "1") dp.Attributes().PutStr("label_2", "2") dp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + dp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } return }, @@ -405,7 +424,7 @@ func TestCollectMetrics(t *testing.T) { name: "Unknown", metricType: prometheus.UntypedValue, value: 42.42, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetDescription("test description") @@ -415,6 +434,9 @@ func TestCollectMetrics(t *testing.T) { dp.Attributes().PutStr("label_1", "1") dp.Attributes().PutStr("label_2", "2") dp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + dp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } return }, @@ -424,6 +446,8 @@ func TestCollectMetrics(t *testing.T) { for _, tt := range tests { for _, sendTimestamp := range []bool{true, false} { name := tt.name + // In this test, sendTimestamp is used to test + // both prometheus regular timestamp and "created timestamp". if sendTimestamp { name += "/WithTimestamp" } @@ -435,7 +459,7 @@ func TestCollectMetrics(t *testing.T) { t.Run(name, func(t *testing.T) { ts := time.Now() - metric := tt.metric(ts) + metric := tt.metric(ts, sendTimestamp) c := collector{ namespace: "test_space", accumulator: &mockAccumulator{ @@ -481,8 +505,15 @@ func TestCollectMetrics(t *testing.T) { if sendTimestamp { require.Equal(t, ts.UnixNano()/1e6, *(pbMetric.TimestampMs)) + // Prometheus gauges don't have created timestamp. + if tt.metricType == prometheus.CounterValue { + require.Equal(t, timestamppb.New(ts), pbMetric.Counter.CreatedTimestamp) + } } else { require.Nil(t, pbMetric.TimestampMs) + if tt.metricType == prometheus.CounterValue { + require.Nil(t, pbMetric.Counter.CreatedTimestamp) + } } switch tt.metricType { @@ -507,7 +538,7 @@ func TestCollectMetrics(t *testing.T) { func TestAccumulateHistograms(t *testing.T) { tests := []struct { name string - metric func(time.Time) pmetric.Metric + metric func(time.Time, bool) pmetric.Metric histogramPoints map[float64]uint64 histogramSum float64 @@ -521,7 +552,7 @@ func TestAccumulateHistograms(t *testing.T) { }, histogramSum: 42.42, histogramCount: 7, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetEmptyHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -534,6 +565,9 @@ func TestAccumulateHistograms(t *testing.T) { dp.Attributes().PutStr("label_1", "1") dp.Attributes().PutStr("label_2", "2") dp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + dp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } return }, }, @@ -542,12 +576,14 @@ func TestAccumulateHistograms(t *testing.T) { for _, tt := range tests { for _, sendTimestamp := range []bool{true, false} { name := tt.name + // In this test, sendTimestamp is used to test + // both prometheus regular timestamp and "created timestamp". if sendTimestamp { name += "/WithTimestamp" } t.Run(name, func(t *testing.T) { ts := time.Now() - metric := tt.metric(ts) + metric := tt.metric(ts, sendTimestamp) c := collector{ accumulator: &mockAccumulator{ []pmetric.Metric{metric}, @@ -580,8 +616,10 @@ func TestAccumulateHistograms(t *testing.T) { if sendTimestamp { require.Equal(t, ts.UnixNano()/1e6, *(pbMetric.TimestampMs)) + require.Equal(t, timestamppb.New(ts), pbMetric.Histogram.CreatedTimestamp) } else { require.Nil(t, pbMetric.TimestampMs) + require.Nil(t, pbMetric.Histogram.CreatedTimestamp) } require.Nil(t, pbMetric.Gauge) @@ -609,7 +647,7 @@ func TestAccumulateSummary(t *testing.T) { } tests := []struct { name string - metric func(time.Time) pmetric.Metric + metric func(time.Time, bool) pmetric.Metric wantSum float64 wantCount uint64 wantQuantiles map[float64]float64 @@ -622,7 +660,7 @@ func TestAccumulateSummary(t *testing.T) { 0.50: 190, 0.99: 817, }, - metric: func(ts time.Time) (metric pmetric.Metric) { + metric: func(ts time.Time, withStartTime bool) (metric pmetric.Metric) { metric = pmetric.NewMetric() metric.SetName("test_metric") metric.SetDescription("test description") @@ -633,6 +671,9 @@ func TestAccumulateSummary(t *testing.T) { sp.Attributes().PutStr("label_1", "1") sp.Attributes().PutStr("label_2", "2") sp.SetTimestamp(pcommon.NewTimestampFromTime(ts)) + if withStartTime { + sp.SetStartTimestamp(pcommon.NewTimestampFromTime(ts)) + } fillQuantileValue(0.50, 190, sp.QuantileValues().AppendEmpty()) fillQuantileValue(0.99, 817, sp.QuantileValues().AppendEmpty()) @@ -645,12 +686,14 @@ func TestAccumulateSummary(t *testing.T) { for _, tt := range tests { for _, sendTimestamp := range []bool{true, false} { name := tt.name + // In this test, sendTimestamp is used to test + // both prometheus regular timestamp and "created timestamp". if sendTimestamp { name += "/WithTimestamp" } t.Run(name, func(t *testing.T) { ts := time.Now() - metric := tt.metric(ts) + metric := tt.metric(ts, sendTimestamp) c := collector{ accumulator: &mockAccumulator{ []pmetric.Metric{metric}, @@ -683,8 +726,10 @@ func TestAccumulateSummary(t *testing.T) { if sendTimestamp { require.Equal(t, ts.UnixNano()/1e6, *(pbMetric.TimestampMs)) + require.Equal(t, timestamppb.New(ts), pbMetric.Summary.CreatedTimestamp) } else { require.Nil(t, pbMetric.TimestampMs) + require.Nil(t, pbMetric.Summary.CreatedTimestamp) } require.Nil(t, pbMetric.Gauge) diff --git a/exporter/prometheusexporter/go.mod b/exporter/prometheusexporter/go.mod index 51c9490d2108..aee12c4f48f2 100644 --- a/exporter/prometheusexporter/go.mod +++ b/exporter/prometheusexporter/go.mod @@ -22,6 +22,7 @@ require ( go.opentelemetry.io/collector/semconv v0.108.2-0.20240829190554-7da6b618a7ee go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 + google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v2 v2.4.0 ) @@ -178,7 +179,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect