// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build go1.18 // +build go1.18 package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus" import ( "context" "sort" "strings" "unicode" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" ) // Exporter is a Prometheus Exporter that embeds the OTel metric.Reader // interface for easy instantiation with a MeterProvider. type Exporter struct { metric.Reader Collector prometheus.Collector } // collector is used to implement prometheus.Collector. type collector struct { metric.Reader } // config is added here to allow for options expansion in the future. type config struct{} // Option may be used in the future to apply options to a Prometheus Exporter config. type Option interface { apply(config) config } // New returns a Prometheus Exporter. func New(_ ...Option) Exporter { // this assumes that the default temporality selector will always return cumulative. // we only support cumulative temporality, so building our own reader enforces this. reader := metric.NewManualReader() e := Exporter{ Reader: reader, Collector: &collector{ Reader: reader, }, } return e } // Describe implements prometheus.Collector. func (c *collector) Describe(ch chan<- *prometheus.Desc) { metrics, err := c.Reader.Collect(context.TODO()) if err != nil { otel.Handle(err) } for _, metricData := range getMetricData(metrics) { ch <- metricData.description } } // Collect implements prometheus.Collector. func (c *collector) Collect(ch chan<- prometheus.Metric) { metrics, err := c.Reader.Collect(context.TODO()) if err != nil { otel.Handle(err) } // TODO(#3166): convert otel resource to target_info // see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#resource-attributes-1 for _, metricData := range getMetricData(metrics) { if metricData.valueType == prometheus.UntypedValue { m, err := prometheus.NewConstHistogram(metricData.description, metricData.histogramCount, metricData.histogramSum, metricData.histogramBuckets, metricData.attributeValues...) if err != nil { otel.Handle(err) continue } ch <- m } else { m, err := prometheus.NewConstMetric(metricData.description, metricData.valueType, metricData.value, metricData.attributeValues...) if err != nil { otel.Handle(err) continue } ch <- m } } } // metricData holds the metadata as well as values for individual data points. type metricData struct { // name should include the unit as a suffix (before _total on counters) // see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#metric-metadata-1 name string description *prometheus.Desc attributeValues []string valueType prometheus.ValueType value float64 histogramCount uint64 histogramSum float64 histogramBuckets map[float64]uint64 } func getMetricData(metrics metricdata.ResourceMetrics) []*metricData { allMetrics := make([]*metricData, 0) for _, scopeMetrics := range metrics.ScopeMetrics { for _, m := range scopeMetrics.Metrics { switch v := m.Data.(type) { case metricdata.Histogram: allMetrics = append(allMetrics, getHistogramMetricData(v, m)...) case metricdata.Sum[int64]: allMetrics = append(allMetrics, getSumMetricData(v, m)...) case metricdata.Sum[float64]: allMetrics = append(allMetrics, getSumMetricData(v, m)...) case metricdata.Gauge[int64]: allMetrics = append(allMetrics, getGaugeMetricData(v, m)...) case metricdata.Gauge[float64]: allMetrics = append(allMetrics, getGaugeMetricData(v, m)...) } } } return allMetrics } func getHistogramMetricData(histogram metricdata.Histogram, m metricdata.Metrics) []*metricData { // TODO(https://github.com/open-telemetry/opentelemetry-go/issues/3163): support exemplars dataPoints := make([]*metricData, 0, len(histogram.DataPoints)) for _, dp := range histogram.DataPoints { keys, values := getAttrs(dp.Attributes) desc := prometheus.NewDesc(m.Name, m.Description, keys, nil) buckets := make(map[float64]uint64, len(dp.Bounds)) for i, bound := range dp.Bounds { buckets[bound] = dp.BucketCounts[i] } md := &metricData{ name: m.Name, description: desc, attributeValues: values, valueType: prometheus.UntypedValue, histogramCount: dp.Count, histogramSum: dp.Sum, histogramBuckets: buckets, } dataPoints = append(dataPoints, md) } return dataPoints } func getSumMetricData[N int64 | float64](sum metricdata.Sum[N], m metricdata.Metrics) []*metricData { dataPoints := make([]*metricData, 0, len(sum.DataPoints)) for _, dp := range sum.DataPoints { keys, values := getAttrs(dp.Attributes) desc := prometheus.NewDesc(m.Name, m.Description, keys, nil) md := &metricData{ name: m.Name, description: desc, attributeValues: values, valueType: prometheus.CounterValue, value: float64(dp.Value), } dataPoints = append(dataPoints, md) } return dataPoints } func getGaugeMetricData[N int64 | float64](gauge metricdata.Gauge[N], m metricdata.Metrics) []*metricData { dataPoints := make([]*metricData, 0, len(gauge.DataPoints)) for _, dp := range gauge.DataPoints { keys, values := getAttrs(dp.Attributes) desc := prometheus.NewDesc(m.Name, m.Description, keys, nil) md := &metricData{ name: m.Name, description: desc, attributeValues: values, valueType: prometheus.GaugeValue, value: float64(dp.Value), } dataPoints = append(dataPoints, md) } return dataPoints } // getAttrs parses the attribute.Set to two lists of matching Prometheus-style // keys and values. It sanitizes invalid characters and handles duplicate keys // (due to sanitization) by sorting and concatenating the values following the spec. func getAttrs(attrs attribute.Set) ([]string, []string) { keysMap := make(map[string][]string) itr := attrs.Iter() for itr.Next() { kv := itr.Attribute() key := strings.Map(sanitizeRune, string(kv.Key)) if _, ok := keysMap[key]; !ok { keysMap[key] = []string{kv.Value.AsString()} } else { // if the sanitized key is a duplicate, append to the list of keys keysMap[key] = append(keysMap[key], kv.Value.AsString()) } } keys := make([]string, 0, attrs.Len()) values := make([]string, 0, attrs.Len()) for key, vals := range keysMap { keys = append(keys, key) sort.Slice(vals, func(i, j int) bool { return i < j }) values = append(values, strings.Join(vals, ";")) } return keys, values } func sanitizeRune(r rune) rune { if unicode.IsLetter(r) || unicode.IsDigit(r) || r == ':' || r == '_' { return r } return '_' }