diff --git a/CHANGELOG.md b/CHANGELOG.md index db722c702e3..1b5c60dd94d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The next release will require at least [Go 1.21]. - Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108) - Support [Go 1.22]. (#5082) +- Add support for Summary metrics to `go.opentelemetry.io/contrib/bridges/prometheus`. (#5089) ### Removed diff --git a/bridges/prometheus/doc.go b/bridges/prometheus/doc.go index abb73f7c6e7..a4bdb16be37 100644 --- a/bridges/prometheus/doc.go +++ b/bridges/prometheus/doc.go @@ -19,7 +19,6 @@ // to be used with OpenTelemetry exporters, including OTLP. // // Limitations: -// - Summary metrics are dropped by the bridge. // - Prometheus histograms are translated to OpenTelemetry fixed-bucket // histograms, rather than exponential histograms. // diff --git a/bridges/prometheus/producer.go b/bridges/prometheus/producer.go index 6c27a29b156..e0ad803df12 100644 --- a/bridges/prometheus/producer.go +++ b/bridges/prometheus/producer.go @@ -101,8 +101,10 @@ func convertPrometheusMetricsInto(promMetrics []*dto.MetricFamily, now time.Time newMetric.Data = convertCounter(pm.GetMetric(), now) case dto.MetricType_HISTOGRAM: newMetric.Data = convertHistogram(pm.GetMetric(), now) + case dto.MetricType_SUMMARY: + newMetric.Data = convertSummary(pm.GetMetric(), now) default: - // MetricType_GAUGE_HISTOGRAM, MetricType_SUMMARY, MetricType_UNTYPED + // MetricType_GAUGE_HISTOGRAM, MetricType_UNTYPED errs = append(errs, fmt.Errorf("%w: %v for metric %v", errUnsupportedType, pm.GetType(), pm.GetName())) continue } @@ -204,6 +206,43 @@ func convertBuckets(buckets []*dto.Bucket) ([]float64, []uint64, []metricdata.Ex return bounds, bucketCounts, exemplars } +func convertSummary(metrics []*dto.Metric, now time.Time) metricdata.Summary { + otelSummary := metricdata.Summary{ + DataPoints: make([]metricdata.SummaryDataPoint, len(metrics)), + } + for i, m := range metrics { + dp := metricdata.SummaryDataPoint{ + Attributes: convertLabels(m.GetLabel()), + StartTime: processStartTime, + Time: now, + Count: m.GetSummary().GetSampleCount(), + Sum: m.GetSummary().GetSampleSum(), + QuantileValues: convertQuantiles(m.GetSummary().GetQuantile()), + } + createdTs := m.GetSummary().GetCreatedTimestamp() + if createdTs.IsValid() { + dp.StartTime = createdTs.AsTime() + } + if t := m.GetTimestampMs(); t != 0 { + dp.Time = time.UnixMilli(t) + } + otelSummary.DataPoints[i] = dp + } + return otelSummary +} + +func convertQuantiles(quantiles []*dto.Quantile) []metricdata.QuantileValue { + otelQuantiles := make([]metricdata.QuantileValue, len(quantiles)) + for i, quantile := range quantiles { + dp := metricdata.QuantileValue{ + Quantile: quantile.GetQuantile(), + Value: quantile.GetValue(), + } + otelQuantiles[i] = dp + } + return otelQuantiles +} + func convertLabels(labels []*dto.LabelPair) attribute.Set { kvs := make([]attribute.KeyValue, len(labels)) for i, l := range labels { diff --git a/bridges/prometheus/producer_test.go b/bridges/prometheus/producer_test.go index d1f41feb083..0d859de3477 100644 --- a/bridges/prometheus/producer_test.go +++ b/bridges/prometheus/producer_test.go @@ -128,11 +128,12 @@ func TestProduce(t *testing.T) { }}, }, { - name: "summary dropped", + name: "summary", testFn: func(reg *prometheus.Registry) { metric := prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "test_summary_metric", - Help: "A summary metric for testing", + Name: "test_summary_metric", + Help: "A summary metric for testing", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, ConstLabels: prometheus.Labels(map[string]string{ "foo": "bar", }), @@ -140,7 +141,31 @@ func TestProduce(t *testing.T) { reg.MustRegister(metric) metric.Observe(15.0) }, - wantErr: errUnsupportedType, + expected: []metricdata.ScopeMetrics{{ + Scope: instrumentation.Scope{ + Name: scopeName, + }, + Metrics: []metricdata.Metrics{ + { + Name: "test_summary_metric", + Description: "A summary metric for testing", + Data: metricdata.Summary{ + DataPoints: []metricdata.SummaryDataPoint{ + { + Count: 1, + Sum: 15.0, + QuantileValues: []metricdata.QuantileValue{ + {Quantile: 0.5, Value: 15}, + {Quantile: 0.9, Value: 15}, + {Quantile: 0.99, Value: 15}, + }, + Attributes: attribute.NewSet(attribute.String("foo", "bar")), + }, + }, + }, + }, + }, + }}, }, { name: "histogram", @@ -207,12 +232,13 @@ func TestProduce(t *testing.T) { }) reg.MustRegister(metric) metric.Set(123.4) - unsupportedMetric := prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "test_summary_metric", - Help: "A summary metric for testing", + unsupportedMetric := prometheus.NewUntypedFunc(prometheus.UntypedOpts{ + Name: "test_untyped_metric", + Help: "An untyped metric for testing", + }, func() float64 { + return 135.8 }) reg.MustRegister(unsupportedMetric) - unsupportedMetric.Observe(15.0) }, expected: []metricdata.ScopeMetrics{{ Scope: instrumentation.Scope{ @@ -315,6 +341,27 @@ func TestProduceForStartTime(t *testing.T) { return sts }, }, + { + name: "summary", + testFn: func(reg *prometheus.Registry) { + metric := prometheus.NewSummary(prometheus.SummaryOpts{ + Name: "test_summary_metric", + Help: "A summary metric for testing", + ConstLabels: prometheus.Labels(map[string]string{ + "foo": "bar", + }), + }) + reg.MustRegister(metric) + }, + startTimeFn: func(aggr metricdata.Aggregation) []time.Time { + dps := aggr.(metricdata.Summary).DataPoints + sts := make([]time.Time, len(dps)) + for i, dp := range dps { + sts[i] = dp.StartTime + } + return sts + }, + }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) {