diff --git a/exporter/prometheusexporter/README.md b/exporter/prometheusexporter/README.md index 9396deca0a480..d39fd83c195c2 100644 --- a/exporter/prometheusexporter/README.md +++ b/exporter/prometheusexporter/README.md @@ -18,11 +18,11 @@ The following settings can be optionally configured: - `const_labels` (no default): key/values that are applied for every exported metric. - `namespace` (no default): if set, exports metrics under the provided value. -- `send_timestamps` (default = `false`): if true, sends the timestamp of the underlying - metric sample in the response. +- `send_timestamps` (default = `false`): if true, sends the timestamp of the underlying metric sample in the response. - `metric_expiration` (default = `5m`): defines how long metrics are exposed without updates - `resource_to_telemetry_conversion` - `enabled` (default = false): If `enabled` is `true`, all the resource attributes will be converted to metric labels by default. +- `enable_open_metrics`: (default = `false`): If true, metrics will be exported using the OpenMetrics format. Exemplars are only exported in the OpenMetrics format. Example: @@ -40,6 +40,7 @@ exporters: "another label": spaced value send_timestamps: true metric_expiration: 180m + enable_open_metrics: true resource_to_telemetry_conversion: enabled: true ``` diff --git a/exporter/prometheusexporter/collector.go b/exporter/prometheusexporter/collector.go index 93ac3daea7507..7caeb2e2c25aa 100644 --- a/exporter/prometheusexporter/collector.go +++ b/exporter/prometheusexporter/collector.go @@ -217,11 +217,36 @@ func (c *collector) convertDoubleHistogram(metric pmetric.Metric, resourceAttrs points[bucket] = cumCount } + arrLen := ip.Exemplars().Len() + exemplars := make([]prometheus.Exemplar, arrLen) + for i := 0; i < arrLen; i++ { + e := ip.Exemplars().At(i) + + labels := make(prometheus.Labels, e.FilteredAttributes().Len()) + e.FilteredAttributes().Range(func(k string, v pcommon.Value) bool { + labels[k] = v.AsString() + return true + }) + + exemplars[i] = prometheus.Exemplar{ + Value: e.DoubleVal(), + Labels: labels, + Timestamp: e.Timestamp().AsTime(), + } + } + m, err := prometheus.NewConstHistogram(desc, ip.Count(), ip.Sum(), points, attributes...) if err != nil { return nil, err } + if arrLen > 0 { + m, err = prometheus.NewMetricWithExemplars(m, exemplars...) + if err != nil { + return nil, err + } + } + if c.sendTimestamps { return prometheus.NewMetricWithTimestamp(ip.Timestamp().AsTime(), m), nil } diff --git a/exporter/prometheusexporter/collector_test.go b/exporter/prometheusexporter/collector_test.go index 3fb73e7c429da..f342d2a30f202 100644 --- a/exporter/prometheusexporter/collector_test.go +++ b/exporter/prometheusexporter/collector_test.go @@ -97,6 +97,100 @@ func TestConvertInvalidMetric(t *testing.T) { } } +func TestConvertDoubleHistogramExemplar(t *testing.T) { + // initialize empty histogram + metric := pmetric.NewMetric() + metric.SetDataType(pmetric.MetricDataTypeHistogram) + metric.SetName("test_metric") + metric.SetDescription("this is test metric") + metric.SetUnit("T") + + // initialize empty datapoint + hd := metric.Histogram().DataPoints().AppendEmpty() + + bounds := pcommon.NewImmutableFloat64Slice([]float64{5, 25, 90}) + hd.SetExplicitBounds(bounds) + bc := pcommon.NewImmutableUInt64Slice([]uint64{2, 35, 70}) + hd.SetBucketCounts(bc) + + exemplarTs, _ := time.Parse("unix", "Mon Jan _2 15:04:05 MST 2006") + exemplars := []prometheus.Exemplar{ + { + Timestamp: exemplarTs, + Value: 3, + Labels: prometheus.Labels{"test_label_0": "label_value_0"}, + }, + { + Timestamp: exemplarTs, + Value: 50, + Labels: prometheus.Labels{"test_label_1": "label_value_1"}, + }, + { + Timestamp: exemplarTs, + Value: 78, + Labels: prometheus.Labels{"test_label_2": "label_value_2"}, + }, + { + Timestamp: exemplarTs, + Value: 100, + Labels: prometheus.Labels{"test_label_3": "label_value_3"}, + }, + } + + // add each exemplar value to the metric + for _, e := range exemplars { + pde := hd.Exemplars().AppendEmpty() + pde.SetDoubleVal(e.Value) + for k, v := range e.Labels { + pde.FilteredAttributes().InsertString(k, v) + } + pde.SetTimestamp(pcommon.NewTimestampFromTime(e.Timestamp)) + } + + pMap := pcommon.NewMap() + + c := collector{ + accumulator: &mockAccumulator{ + metrics: []pmetric.Metric{metric}, + resourceAttributes: pMap, + }, + logger: zap.NewNop(), + } + + pbMetric, _ := c.convertDoubleHistogram(metric, pMap) + m := io_prometheus_client.Metric{} + err := pbMetric.Write(&m) + if err != nil { + return + } + + buckets := m.GetHistogram().GetBucket() + + require.Equal(t, 4, len(buckets)) + + require.Equal(t, 3.0, buckets[0].GetExemplar().GetValue()) + require.Equal(t, int32(128654848), buckets[0].GetExemplar().GetTimestamp().GetNanos()) + require.Equal(t, 1, len(buckets[0].GetExemplar().GetLabel())) + require.Equal(t, "test_label_0", buckets[0].GetExemplar().GetLabel()[0].GetName()) + require.Equal(t, "label_value_0", buckets[0].GetExemplar().GetLabel()[0].GetValue()) + + require.Equal(t, 0.0, buckets[1].GetExemplar().GetValue()) + require.Equal(t, int32(0), buckets[1].GetExemplar().GetTimestamp().GetNanos()) + require.Equal(t, 0, len(buckets[1].GetExemplar().GetLabel())) + + require.Equal(t, 78.0, buckets[2].GetExemplar().GetValue()) + require.Equal(t, int32(128654848), buckets[2].GetExemplar().GetTimestamp().GetNanos()) + require.Equal(t, 1, len(buckets[2].GetExemplar().GetLabel())) + require.Equal(t, "test_label_2", buckets[2].GetExemplar().GetLabel()[0].GetName()) + require.Equal(t, "label_value_2", buckets[2].GetExemplar().GetLabel()[0].GetValue()) + + require.Equal(t, 100.0, buckets[3].GetExemplar().GetValue()) + require.Equal(t, int32(128654848), buckets[3].GetExemplar().GetTimestamp().GetNanos()) + require.Equal(t, 1, len(buckets[3].GetExemplar().GetLabel())) + require.Equal(t, "test_label_3", buckets[3].GetExemplar().GetLabel()[0].GetName()) + require.Equal(t, "label_value_3", buckets[3].GetExemplar().GetLabel()[0].GetValue()) +} + // errorCheckCore keeps track of logged errors type errorCheckCore struct { errorMessages []string diff --git a/exporter/prometheusexporter/config.go b/exporter/prometheusexporter/config.go index a582cc364f77f..bb4bf37772670 100644 --- a/exporter/prometheusexporter/config.go +++ b/exporter/prometheusexporter/config.go @@ -43,6 +43,9 @@ type Config struct { // ResourceToTelemetrySettings defines configuration for converting resource attributes to metric labels. ResourceToTelemetrySettings resourcetotelemetry.Settings `mapstructure:"resource_to_telemetry_conversion"` + + // EnableOpenMetrics enables the use of the OpenMetrics encoding option for the prometheus exporter. + EnableOpenMetrics bool `mapstructure:"enable_open_metrics"` } var _ config.Exporter = (*Config)(nil) diff --git a/exporter/prometheusexporter/factory.go b/exporter/prometheusexporter/factory.go index b2ff53fec0fbd..0ebaf5d1e5709 100644 --- a/exporter/prometheusexporter/factory.go +++ b/exporter/prometheusexporter/factory.go @@ -43,10 +43,11 @@ func NewFactory() component.ExporterFactory { func createDefaultConfig() config.Exporter { return &Config{ - ExporterSettings: config.NewExporterSettings(config.NewComponentID(typeStr)), - ConstLabels: map[string]string{}, - SendTimestamps: false, - MetricExpiration: time.Minute * 5, + ExporterSettings: config.NewExporterSettings(config.NewComponentID(typeStr)), + ConstLabels: map[string]string{}, + SendTimestamps: false, + MetricExpiration: time.Minute * 5, + EnableOpenMetrics: false, } } diff --git a/exporter/prometheusexporter/prometheus.go b/exporter/prometheusexporter/prometheus.go index 7b52d25f84435..bd20641933cf6 100644 --- a/exporter/prometheusexporter/prometheus.go +++ b/exporter/prometheusexporter/prometheus.go @@ -58,8 +58,9 @@ func newPrometheusExporter(config *Config, set component.ExporterCreateSettings) handler: promhttp.HandlerFor( registry, promhttp.HandlerOpts{ - ErrorHandling: promhttp.ContinueOnError, - ErrorLog: newPromLogger(set.Logger), + ErrorHandling: promhttp.ContinueOnError, + ErrorLog: newPromLogger(set.Logger), + EnableOpenMetrics: config.EnableOpenMetrics, }, ), }, nil diff --git a/unreleased/prometheusexporter-exemplars.yaml b/unreleased/prometheusexporter-exemplars.yaml new file mode 100644 index 0000000000000..8d65b11f3d538 --- /dev/null +++ b/unreleased/prometheusexporter-exemplars.yaml @@ -0,0 +1,16 @@ +# 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: prometheusexporter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Adds a feature to prometheusexporter that enables it to export exemplars along with histogram metrics. + +# One or more tracking issues related to the change +issues: [5192] + +# (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: