1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2024-12-14 10:13:10 +02:00
opentelemetry-go/exporters/prometheus/exporter.go

236 lines
7.3 KiB
Go

// 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.Emit()}
} else {
// if the sanitized key is a duplicate, append to the list of keys
keysMap[key] = append(keysMap[key], kv.Value.Emit())
}
}
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 '_'
}