Skip to content

Commit a4dda5b

Browse files
authored
[exporter/awsemfexporter] add exponential histogram support (open-telemetry#22626)
**Description:** This PR adds [exponential histogram](https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram) support in `awsemfexporter`. The exponential histogram metrics are exported in Embedded Metric Format (EMF) log. The Count, Sum, Max and Min are set as Statistical Set. The mid-point values and counts of exponential histogram buckets are translated into Values/Counts array of EMF log entry as well. **Testing:** The unit test is added and covers positive, zero and negative values. The integration test is performed with following OTEL collector configuration. ``` extensions: health_check: receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch/metrics: timeout: 60s exporters: logging: verbosity: detailed awsemf: region: 'us-east-1' namespace: "Test" dimension_rollup_option: "NoDimensionRollup" service: pipelines: metrics: receivers: [otlp] processors: [batch/metrics] exporters: [awsemf, logging] extensions: [health_check] telemetry: logs: level: "debug" ``` It generated EMF log for histogram metrics in following JSON format. Notes: It doesn't cover negative values since histograms can [only record non-negative values](https://opentelemetry.io/docs/specs/otel/metrics/api/#histogram) and will [drop negative values](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogram.java#L38C7-L44). ``` "latency": { "Values": [ 309.4277237034415, 323.12725941969757, 326.64588457862067, 344.8221530867399, 520.3933272846809, 531.7884573308439, 537.579253961712, 543.4331082335607, 549.3507067990806, 555.3327437881196, 561.3799208891041, 567.4929474313465, 720.1774681373079, 0 ], "Counts": [ 1, 1, 1, 1, 1, 3, 4, 2, 2, 3, 1, 1, 1, 22 ], "Max": 720, "Min": 0, "Count": 44, "Sum": 11265 } ```
1 parent f66845f commit a4dda5b

File tree

4 files changed

+272
-0
lines changed

4 files changed

+272
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Use this changelog template to create an entry for release notes.
2+
# If your change doesn't affect end users, such as a test fix or a tooling change,
3+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
4+
5+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
6+
change_type: enhancement
7+
8+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
9+
component: awsemfexporter
10+
11+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
12+
note: Add exponential histogram support.
13+
14+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
15+
issues: [22626]
16+
17+
# (Optional) One or more lines of additional information to render under the primary note.
18+
# These lines will be padded with 2 spaces and then inserted directly into the document.
19+
# Use pipe (|) for multiline entries.
20+
subtext:

exporter/awsemfexporter/datapoint.go

+96
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package awsemfexporter // import "github.com/open-telemetry/opentelemetry-collec
55

66
import (
77
"fmt"
8+
"math"
89
"strconv"
910
"time"
1011

@@ -86,6 +87,13 @@ type histogramDataPointSlice struct {
8687
pmetric.HistogramDataPointSlice
8788
}
8889

90+
type exponentialHistogramDataPointSlice struct {
91+
// TODO: Calculate delta value for count and sum value with exponential histogram
92+
// https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18245
93+
deltaMetricMetadata
94+
pmetric.ExponentialHistogramDataPointSlice
95+
}
96+
8997
// summaryDataPointSlice is a wrapper for pmetric.SummaryDataPointSlice
9098
type summaryDataPointSlice struct {
9199
deltaMetricMetadata
@@ -156,6 +164,88 @@ func (dps histogramDataPointSlice) CalculateDeltaDatapoints(i int, instrumentati
156164
}}, true
157165
}
158166

167+
// CalculateDeltaDatapoints retrieves the ExponentialHistogramDataPoint at the given index.
168+
func (dps exponentialHistogramDataPointSlice) CalculateDeltaDatapoints(idx int, instrumentationScopeName string, _ bool) ([]dataPoint, bool) {
169+
metric := dps.ExponentialHistogramDataPointSlice.At(idx)
170+
171+
scale := metric.Scale()
172+
base := math.Pow(2, math.Pow(2, float64(-scale)))
173+
arrayValues := []float64{}
174+
arrayCounts := []float64{}
175+
var bucketBegin float64
176+
var bucketEnd float64
177+
178+
// Set mid-point of positive buckets in values/counts array.
179+
positiveBuckets := metric.Positive()
180+
positiveOffset := positiveBuckets.Offset()
181+
positiveBucketCounts := positiveBuckets.BucketCounts()
182+
bucketBegin = 0
183+
bucketEnd = 0
184+
for i := 0; i < positiveBucketCounts.Len(); i++ {
185+
index := i + int(positiveOffset)
186+
if bucketBegin == 0 {
187+
bucketBegin = math.Pow(base, float64(index))
188+
} else {
189+
bucketBegin = bucketEnd
190+
}
191+
bucketEnd = math.Pow(base, float64(index+1))
192+
metricVal := (bucketBegin + bucketEnd) / 2
193+
count := positiveBucketCounts.At(i)
194+
if count > 0 {
195+
arrayValues = append(arrayValues, metricVal)
196+
arrayCounts = append(arrayCounts, float64(count))
197+
}
198+
}
199+
200+
// Set count of zero bucket in values/counts array.
201+
if metric.ZeroCount() > 0 {
202+
arrayValues = append(arrayValues, 0)
203+
arrayCounts = append(arrayCounts, float64(metric.ZeroCount()))
204+
}
205+
206+
// Set mid-point of negative buckets in values/counts array.
207+
// According to metrics spec, the value in histogram is expected to be non-negative.
208+
// https://opentelemetry.io/docs/specs/otel/metrics/api/#histogram
209+
// However, the negative support is defined in metrics data model.
210+
// https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram
211+
// The negative is also supported but only verified with unit test.
212+
213+
negativeBuckets := metric.Negative()
214+
negativeOffset := negativeBuckets.Offset()
215+
negativeBucketCounts := negativeBuckets.BucketCounts()
216+
bucketBegin = 0
217+
bucketEnd = 0
218+
for i := 0; i < negativeBucketCounts.Len(); i++ {
219+
index := i + int(negativeOffset)
220+
if bucketEnd == 0 {
221+
bucketEnd = -math.Pow(base, float64(index))
222+
} else {
223+
bucketEnd = bucketBegin
224+
}
225+
bucketBegin = -math.Pow(base, float64(index+1))
226+
metricVal := (bucketBegin + bucketEnd) / 2
227+
count := negativeBucketCounts.At(i)
228+
if count > 0 {
229+
arrayValues = append(arrayValues, metricVal)
230+
arrayCounts = append(arrayCounts, float64(count))
231+
}
232+
}
233+
234+
return []dataPoint{{
235+
name: dps.metricName,
236+
value: &cWMetricHistogram{
237+
Values: arrayValues,
238+
Counts: arrayCounts,
239+
Count: metric.Count(),
240+
Sum: metric.Sum(),
241+
Max: metric.Max(),
242+
Min: metric.Min(),
243+
},
244+
labels: createLabels(metric.Attributes(), instrumentationScopeName),
245+
timestampMs: unixNanoToMilliseconds(metric.Timestamp()),
246+
}}, true
247+
}
248+
159249
// CalculateDeltaDatapoints retrieves the SummaryDataPoint at the given index and perform calculation with sum and count while retain the quantile value.
160250
func (dps summaryDataPointSlice) CalculateDeltaDatapoints(i int, instrumentationScopeName string, detailedMetrics bool) ([]dataPoint, bool) {
161251
metric := dps.SummaryDataPointSlice.At(i)
@@ -263,6 +353,12 @@ func getDataPoints(pmd pmetric.Metric, metadata cWMetricMetadata, logger *zap.Lo
263353
metricMetadata,
264354
metric.DataPoints(),
265355
}
356+
case pmetric.MetricTypeExponentialHistogram:
357+
metric := pmd.ExponentialHistogram()
358+
dps = exponentialHistogramDataPointSlice{
359+
metricMetadata,
360+
metric.DataPoints(),
361+
}
266362
case pmetric.MetricTypeSummary:
267363
metric := pmd.Summary()
268364
// For summaries coming from the prometheus receiver, the sum and count are cumulative, whereas for summaries

exporter/awsemfexporter/datapoint_test.go

+145
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,34 @@ func generateTestHistogramMetric(name string) pmetric.Metrics {
9191
return otelMetrics
9292
}
9393

94+
func generateTestExponentialHistogramMetric(name string) pmetric.Metrics {
95+
otelMetrics := pmetric.NewMetrics()
96+
rs := otelMetrics.ResourceMetrics().AppendEmpty()
97+
metrics := rs.ScopeMetrics().AppendEmpty().Metrics()
98+
metric := metrics.AppendEmpty()
99+
metric.SetName(name)
100+
metric.SetUnit("Seconds")
101+
exponentialHistogramMetric := metric.SetEmptyExponentialHistogram()
102+
103+
exponentialHistogramDatapoint := exponentialHistogramMetric.DataPoints().AppendEmpty()
104+
exponentialHistogramDatapoint.SetCount(4)
105+
exponentialHistogramDatapoint.SetSum(0)
106+
exponentialHistogramDatapoint.SetMin(-4)
107+
exponentialHistogramDatapoint.SetMax(4)
108+
exponentialHistogramDatapoint.SetZeroCount(0)
109+
exponentialHistogramDatapoint.SetScale(1)
110+
exponentialHistogramDatapoint.Positive().SetOffset(1)
111+
exponentialHistogramDatapoint.Positive().BucketCounts().FromRaw([]uint64{
112+
1, 0, 1,
113+
})
114+
exponentialHistogramDatapoint.Negative().SetOffset(1)
115+
exponentialHistogramDatapoint.Negative().BucketCounts().FromRaw([]uint64{
116+
1, 0, 1,
117+
})
118+
exponentialHistogramDatapoint.Attributes().PutStr("label1", "value1")
119+
return otelMetrics
120+
}
121+
94122
func generateTestSummaryMetric(name string) pmetric.Metrics {
95123
otelMetrics := pmetric.NewMetrics()
96124
rs := otelMetrics.ResourceMetrics().AppendEmpty()
@@ -347,6 +375,106 @@ func TestCalculateDeltaDatapoints_HistogramDataPointSlice(t *testing.T) {
347375

348376
}
349377

378+
func TestCalculateDeltaDatapoints_ExponentialHistogramDataPointSlice(t *testing.T) {
379+
deltaMetricMetadata := generateDeltaMetricMetadata(false, "foo", false)
380+
381+
testCases := []struct {
382+
name string
383+
histogramDPS pmetric.ExponentialHistogramDataPointSlice
384+
expectedDatapoint dataPoint
385+
}{
386+
{
387+
name: "Exponential histogram with min and max",
388+
histogramDPS: func() pmetric.ExponentialHistogramDataPointSlice {
389+
histogramDPS := pmetric.NewExponentialHistogramDataPointSlice()
390+
histogramDP := histogramDPS.AppendEmpty()
391+
histogramDP.SetCount(uint64(17))
392+
histogramDP.SetSum(17.13)
393+
histogramDP.SetMin(10)
394+
histogramDP.SetMax(30)
395+
histogramDP.Attributes().PutStr("label1", "value1")
396+
return histogramDPS
397+
}(),
398+
expectedDatapoint: dataPoint{
399+
name: "foo",
400+
value: &cWMetricHistogram{Values: []float64{}, Counts: []float64{}, Sum: 17.13, Count: 17, Min: 10, Max: 30},
401+
labels: map[string]string{oTellibDimensionKey: instrLibName, "label1": "value1"},
402+
},
403+
},
404+
{
405+
name: "Exponential histogram without min and max",
406+
histogramDPS: func() pmetric.ExponentialHistogramDataPointSlice {
407+
histogramDPS := pmetric.NewExponentialHistogramDataPointSlice()
408+
histogramDP := histogramDPS.AppendEmpty()
409+
histogramDP.SetCount(uint64(17))
410+
histogramDP.SetSum(17.13)
411+
histogramDP.Attributes().PutStr("label1", "value1")
412+
return histogramDPS
413+
414+
}(),
415+
expectedDatapoint: dataPoint{
416+
name: "foo",
417+
value: &cWMetricHistogram{Values: []float64{}, Counts: []float64{}, Sum: 17.13, Count: 17, Min: 0, Max: 0},
418+
labels: map[string]string{oTellibDimensionKey: instrLibName, "label1": "value1"},
419+
},
420+
},
421+
{
422+
name: "Exponential histogram with buckets",
423+
histogramDPS: func() pmetric.ExponentialHistogramDataPointSlice {
424+
histogramDPS := pmetric.NewExponentialHistogramDataPointSlice()
425+
histogramDP := histogramDPS.AppendEmpty()
426+
histogramDP.Positive().BucketCounts().FromRaw([]uint64{1, 2, 3})
427+
histogramDP.SetZeroCount(4)
428+
histogramDP.Negative().BucketCounts().FromRaw([]uint64{1, 2, 3})
429+
histogramDP.Attributes().PutStr("label1", "value1")
430+
return histogramDPS
431+
}(),
432+
expectedDatapoint: dataPoint{
433+
name: "foo",
434+
value: &cWMetricHistogram{Values: []float64{1.5, 3, 6, 0, -1.5, -3, -6}, Counts: []float64{1, 2, 3, 4, 1, 2, 3}},
435+
labels: map[string]string{oTellibDimensionKey: instrLibName, "label1": "value1"},
436+
},
437+
},
438+
{
439+
name: "Exponential histogram with different scale/offset/labels",
440+
histogramDPS: func() pmetric.ExponentialHistogramDataPointSlice {
441+
histogramDPS := pmetric.NewExponentialHistogramDataPointSlice()
442+
histogramDP := histogramDPS.AppendEmpty()
443+
histogramDP.SetScale(-1)
444+
histogramDP.Positive().SetOffset(-1)
445+
histogramDP.Positive().BucketCounts().FromRaw([]uint64{1, 2, 3})
446+
histogramDP.SetZeroCount(4)
447+
histogramDP.Negative().SetOffset(-1)
448+
histogramDP.Negative().BucketCounts().FromRaw([]uint64{1, 2, 3})
449+
histogramDP.Attributes().PutStr("label1", "value1")
450+
histogramDP.Attributes().PutStr("label2", "value2")
451+
return histogramDPS
452+
}(),
453+
expectedDatapoint: dataPoint{
454+
name: "foo",
455+
value: &cWMetricHistogram{Values: []float64{0.625, 2.5, 10, 0, -0.625, -2.5, -10}, Counts: []float64{1, 2, 3, 4, 1, 2, 3}},
456+
labels: map[string]string{oTellibDimensionKey: instrLibName, "label1": "value1", "label2": "value2"},
457+
},
458+
},
459+
}
460+
461+
for _, tc := range testCases {
462+
t.Run(tc.name, func(_ *testing.T) {
463+
// Given the histogram datapoints
464+
exponentialHistogramDatapointSlice := exponentialHistogramDataPointSlice{deltaMetricMetadata, tc.histogramDPS}
465+
466+
// When calculate the delta datapoints for histograms
467+
dps, retained := exponentialHistogramDatapointSlice.CalculateDeltaDatapoints(0, instrLibName, false)
468+
469+
// Then receiving the following datapoint with an expected length
470+
assert.True(t, retained)
471+
assert.Equal(t, 1, exponentialHistogramDatapointSlice.Len())
472+
assert.Equal(t, tc.expectedDatapoint, dps[0])
473+
})
474+
}
475+
476+
}
477+
350478
func TestCalculateDeltaDatapoints_SummaryDataPointSlice(t *testing.T) {
351479
for _, retainInitialValueOfDeltaMetric := range []bool{true, false} {
352480
deltaMetricMetadata := generateDeltaMetricMetadata(true, "foo", retainInitialValueOfDeltaMetric)
@@ -486,6 +614,13 @@ func TestGetDataPoints(t *testing.T) {
486614
expectedDatapointSlice: histogramDataPointSlice{cumulativeDeltaMetricMetadata, pmetric.HistogramDataPointSlice{}},
487615
expectedAttributes: map[string]interface{}{"label1": "value1"},
488616
},
617+
{
618+
name: "ExponentialHistogram",
619+
isPrometheusMetrics: false,
620+
metric: generateTestExponentialHistogramMetric("foo"),
621+
expectedDatapointSlice: exponentialHistogramDataPointSlice{cumulativeDeltaMetricMetadata, pmetric.ExponentialHistogramDataPointSlice{}},
622+
expectedAttributes: map[string]interface{}{"label1": "value1"},
623+
},
489624
{
490625
name: "Summary from SDK",
491626
isPrometheusMetrics: false,
@@ -540,6 +675,15 @@ func TestGetDataPoints(t *testing.T) {
540675
assert.Equal(t, uint64(18), dp.Count())
541676
assert.Equal(t, []float64{0, 10}, dp.ExplicitBounds().AsRaw())
542677
assert.Equal(t, tc.expectedAttributes, dp.Attributes().AsRaw())
678+
case exponentialHistogramDataPointSlice:
679+
assert.Equal(t, 1, convertedDPS.Len())
680+
dp := convertedDPS.ExponentialHistogramDataPointSlice.At(0)
681+
assert.Equal(t, float64(0), dp.Sum())
682+
assert.Equal(t, uint64(4), dp.Count())
683+
assert.Equal(t, []uint64{1, 0, 1}, dp.Positive().BucketCounts().AsRaw())
684+
assert.Equal(t, []uint64{1, 0, 1}, dp.Negative().BucketCounts().AsRaw())
685+
assert.Equal(t, uint64(0), dp.ZeroCount())
686+
assert.Equal(t, tc.expectedAttributes, dp.Attributes().AsRaw())
543687
case summaryDataPointSlice:
544688
expectedDPS := tc.expectedDatapointSlice.(summaryDataPointSlice)
545689
assert.Equal(t, expectedDPS.deltaMetricMetadata, convertedDPS.deltaMetricMetadata)
@@ -587,6 +731,7 @@ func BenchmarkGetAndCalculateDeltaDataPoints(b *testing.B) {
587731
generateTestGaugeMetric("int-gauge", intValueType),
588732
generateTestGaugeMetric("int-gauge", doubleValueType),
589733
generateTestHistogramMetric("histogram"),
734+
generateTestExponentialHistogramMetric("exponential-histogram"),
590735
generateTestSumMetric("int-sum", intValueType),
591736
generateTestSumMetric("double-sum", doubleValueType),
592737
generateTestSummaryMetric("summary"),

exporter/awsemfexporter/metric_translator.go

+11
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ type cWMetricStats struct {
5656
Sum float64
5757
}
5858

59+
// The SampleCount of CloudWatch metrics will be calculated by the sum of the 'Counts' array.
60+
// The 'Count' field should be same as the sum of the 'Counts' array and will be ignored in CloudWatch.
61+
type cWMetricHistogram struct {
62+
Values []float64
63+
Counts []float64
64+
Max float64
65+
Min float64
66+
Count uint64
67+
Sum float64
68+
}
69+
5970
type groupedMetricMetadata struct {
6071
namespace string
6172
timestampMs int64

0 commit comments

Comments
 (0)