Skip to content

Commit

Permalink
Add TODOs, drop based on temporality, and sanitize labels
Browse files Browse the repository at this point in the history
  • Loading branch information
damemi committed Sep 12, 2022
1 parent 1e76b67 commit 4acd7ca
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 3 deletions.
53 changes: 50 additions & 3 deletions exporters/prometheus/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus"

import (
"context"
"sort"
"strings"
"unicode"

"github.com/prometheus/client_golang/prometheus"

"go.opentelemetry.io/otel/internal/global"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric"
Expand Down Expand Up @@ -67,6 +72,8 @@ func (e *exporter) Collect(ch chan<- prometheus.Metric) {
otel.Handle(err)
}

// TODO(damemi): 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...)
Expand All @@ -86,6 +93,8 @@ func (e *exporter) Collect(ch chan<- prometheus.Metric) {

// 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
Expand Down Expand Up @@ -119,6 +128,13 @@ func getMetricData(metrics metricdata.ResourceMetrics) []*metricData {
}

func getHistogramMetricData(histogram metricdata.Histogram, m metricdata.Metrics) []*metricData {
// Drop histograms with delta aggregation temporality
if histogram.Temporality == metricdata.DeltaTemporality {
global.Info("dropping histogram with delta temporality", "name", m.Name)
return []*metricData{}
}

// TODO(https://github.com/open-telemetry/opentelemetry-go/issues/3163): support exemplars
dataPoints := make([]*metricData, 0)
for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes)
Expand All @@ -142,6 +158,13 @@ func getHistogramMetricData(histogram metricdata.Histogram, m metricdata.Metrics
}

func getSumMetricData[N int64 | float64](sum metricdata.Sum[N], m metricdata.Metrics) []*metricData {
// TODO(damemi): convert delta aggregation temporality to cumulative
// see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#sums-1
if sum.Temporality == metricdata.DeltaTemporality {
global.Info("dropping sum with delta temporality", "name", m.Name)
return []*metricData{}
}

dataPoints := make([]*metricData, 0)
for _, dp := range sum.DataPoints {
keys, values := getAttrs(dp.Attributes)
Expand Down Expand Up @@ -175,12 +198,36 @@ func getGaugeMetricData[N int64 | float64](gauge metricdata.Gauge[N], m metricda
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)
for _, kv := range attrs.ToSlice() {
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 _, kv := range attrs.ToSlice() {
keys = append(keys, string(kv.Key))
values = append(values, kv.Value.AsString())
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) || string(r) == ":" || string(r) == "_" {
return r
}
return '_'
}
20 changes: 20 additions & 0 deletions exporters/prometheus/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ func TestPrometheusExporter(t *testing.T) {
histogram.Record(ctx, 105, labels...)
},
},
{
name: "sanitized attributes to labels",
expectedFile: "testdata/sanitized_labels.txt",
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
labels := []attribute.KeyValue{
// exact match, value should be overwritten
attribute.Key("A.B").String("X"),
attribute.Key("A.B").String("Q"),

// unintended match due to sanitization, values should be concatenated
attribute.Key("C.D").String("Y"),
attribute.Key("C/D").String("Z"),
}
counter, err := meter.SyncFloat64().Counter("foo", instrument.WithDescription("a sanitary counter"))
require.NoError(t, err)
counter.Add(ctx, 5, labels...)
counter.Add(ctx, 10.3, labels...)
counter.Add(ctx, 9, labels...)
},
},
}

for _, tc := range testCases {
Expand Down
3 changes: 3 additions & 0 deletions exporters/prometheus/testdata/sanitized_labels.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# HELP foo a sanitary counter
# TYPE foo counter
foo{A_B="Q",C_D="Y;Z"} 24.3

0 comments on commit 4acd7ca

Please sign in to comment.