1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-16 02:47:20 +02:00

Add summary data type to metricdata (#4622)

* add summary datatype

* support comparing summaries

* update wording based on SIG meeting feedback

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
This commit is contained in:
David Ashpole 2023-10-25 10:38:19 -04:00 committed by GitHub
parent 7e6da12625
commit cdd9353641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 249 additions and 1 deletions

View File

@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add view configuration to `go.opentelemetry.io/otel/example/prometheus`. (#4649)
- Add `Version` function in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`. (#4660)
- Add `Version` function in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#4660)
- Add Summary, SummaryDataPoint, and QuantileValue to `go.opentelemetry.io/sdk/metric/metricdata`. (#4622)
### Deprecated

View File

@ -240,3 +240,54 @@ type Exemplar[N int64 | float64] struct {
// be empty.
TraceID []byte `json:",omitempty"`
}
// Summary metric data are used to convey quantile summaries,
// a Prometheus (see: https://prometheus.io/docs/concepts/metric_types/#summary)
// data type.
//
// These data points cannot always be merged in a meaningful way. The Summary
// type is only used by bridges from other metrics libraries, and cannot be
// produced using OpenTelemetry instrumentation.
type Summary struct {
// DataPoints are the individual aggregated measurements with unique
// attributes.
DataPoints []SummaryDataPoint
}
func (Summary) privateAggregation() {}
// SummaryDataPoint is a single data point in a timeseries that describes the
// time-varying values of a Summary metric.
type SummaryDataPoint struct {
// Attributes is the set of key value pairs that uniquely identify the
// timeseries.
Attributes attribute.Set
// StartTime is when the timeseries was started.
StartTime time.Time
// Time is the time when the timeseries was recorded.
Time time.Time
// Count is the number of updates this summary has been calculated with.
Count uint64
// Sum is the sum of the values recorded.
Sum float64
// (Optional) list of values at different quantiles of the distribution calculated
// from the current snapshot. The quantiles must be strictly increasing.
QuantileValues []QuantileValue
}
// QuantileValue is the value at a given quantile of a summary.
type QuantileValue struct {
// Quantile is the quantile of this value.
//
// Must be in the interval [0.0, 1.0].
Quantile float64
// Value is the value at the given quantile of a summary.
//
// Quantile values must NOT be negative.
Value float64
}

View File

@ -46,7 +46,10 @@ type Datatypes interface {
metricdata.ExponentialHistogram[int64] |
metricdata.ExponentialHistogramDataPoint[float64] |
metricdata.ExponentialHistogramDataPoint[int64] |
metricdata.ExponentialBucket
metricdata.ExponentialBucket |
metricdata.Summary |
metricdata.SummaryDataPoint |
metricdata.QuantileValue
// Interface types are not allowed in union types, therefore the
// Aggregation and Value type from metricdata are not included here.
@ -177,6 +180,12 @@ func AssertEqual[T Datatypes](t TestingT, expected, actual T, opts ...Option) bo
r = equalExponentialHistogramDataPoints(e, aIface.(metricdata.ExponentialHistogramDataPoint[int64]), cfg)
case metricdata.ExponentialBucket:
r = equalExponentialBuckets(e, aIface.(metricdata.ExponentialBucket), cfg)
case metricdata.Summary:
r = equalSummary(e, aIface.(metricdata.Summary), cfg)
case metricdata.SummaryDataPoint:
r = equalSummaryDataPoint(e, aIface.(metricdata.SummaryDataPoint), cfg)
case metricdata.QuantileValue:
r = equalQuantileValue(e, aIface.(metricdata.QuantileValue), cfg)
default:
// We control all types passed to this, panic to signal developers
// early they changed things in an incompatible way.
@ -251,6 +260,12 @@ func AssertHasAttributes[T Datatypes](t TestingT, actual T, attrs ...attribute.K
reasons = hasAttributesExponentialHistogramDataPoints(e, attrs...)
case metricdata.ExponentialBucket:
// Nothing to check.
case metricdata.Summary:
reasons = hasAttributesSummary(e, attrs...)
case metricdata.SummaryDataPoint:
reasons = hasAttributesSummaryDataPoint(e, attrs...)
case metricdata.QuantileValue:
// Nothing to check.
default:
// We control all types passed to this, panic to signal developers
// early they changed things in an incompatible way.

View File

@ -257,6 +257,45 @@ var (
Exemplars: []metricdata.Exemplar[float64]{exemplarFloat64A},
}
quantileValueA = metricdata.QuantileValue{
Quantile: 0.0,
Value: 0.1,
}
quantileValueB = metricdata.QuantileValue{
Quantile: 0.1,
Value: 0.2,
}
summaryDataPointA = metricdata.SummaryDataPoint{
Attributes: attrA,
StartTime: startA,
Time: endA,
Count: 2,
Sum: 3,
QuantileValues: []metricdata.QuantileValue{quantileValueA},
}
summaryDataPointB = metricdata.SummaryDataPoint{
Attributes: attrB,
StartTime: startB,
Time: endB,
Count: 3,
QuantileValues: []metricdata.QuantileValue{quantileValueB},
}
summaryDataPointC = metricdata.SummaryDataPoint{
Attributes: attrA,
StartTime: startB,
Time: endB,
Count: 2,
Sum: 3,
QuantileValues: []metricdata.QuantileValue{quantileValueA},
}
summaryDataPointD = metricdata.SummaryDataPoint{
Attributes: attrA,
StartTime: startA,
Time: endA,
Count: 3,
QuantileValues: []metricdata.QuantileValue{quantileValueB},
}
exponentialBucket2 = metricdata.ExponentialBucket{
Offset: 2,
Counts: []uint64{1, 1},
@ -514,6 +553,22 @@ var (
DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{exponentialHistogramDataPointFloat64D},
}
summaryA = metricdata.Summary{
DataPoints: []metricdata.SummaryDataPoint{summaryDataPointA},
}
summaryB = metricdata.Summary{
DataPoints: []metricdata.SummaryDataPoint{summaryDataPointB},
}
summaryC = metricdata.Summary{
DataPoints: []metricdata.SummaryDataPoint{summaryDataPointC},
}
summaryD = metricdata.Summary{
DataPoints: []metricdata.SummaryDataPoint{summaryDataPointD},
}
metricsA = metricdata.Metrics{
Name: "A",
Description: "A desc",
@ -646,6 +701,9 @@ func TestAssertEqual(t *testing.T) {
t.Run("ExponentialHistogramDataPointInt64", testDatatype(exponentialHistogramDataPointInt64A, exponentialHistogramDataPointInt64B, equalExponentialHistogramDataPoints[int64]))
t.Run("ExponentialHistogramDataPointFloat64", testDatatype(exponentialHistogramDataPointFloat64A, exponentialHistogramDataPointFloat64B, equalExponentialHistogramDataPoints[float64]))
t.Run("ExponentialBuckets", testDatatype(exponentialBucket2, exponentialBucket3, equalExponentialBuckets))
t.Run("Summary", testDatatype(summaryA, summaryB, equalSummary))
t.Run("SummaryDataPoint", testDatatype(summaryDataPointA, summaryDataPointB, equalSummaryDataPoint))
t.Run("QuantileValues", testDatatype(quantileValueA, quantileValueB, equalQuantileValue))
}
func TestAssertEqualIgnoreTime(t *testing.T) {
@ -670,6 +728,8 @@ func TestAssertEqualIgnoreTime(t *testing.T) {
t.Run("ExponentialHistogramFloat64", testDatatypeIgnoreTime(exponentialHistogramFloat64A, exponentialHistogramFloat64C, equalExponentialHistograms[float64]))
t.Run("ExponentialHistogramDataPointInt64", testDatatypeIgnoreTime(exponentialHistogramDataPointInt64A, exponentialHistogramDataPointInt64C, equalExponentialHistogramDataPoints[int64]))
t.Run("ExponentialHistogramDataPointFloat64", testDatatypeIgnoreTime(exponentialHistogramDataPointFloat64A, exponentialHistogramDataPointFloat64C, equalExponentialHistogramDataPoints[float64]))
t.Run("Summary", testDatatypeIgnoreTime(summaryA, summaryC, equalSummary))
t.Run("SummaryDataPoint", testDatatypeIgnoreTime(summaryDataPointA, summaryDataPointC, equalSummaryDataPoint))
}
func TestAssertEqualIgnoreExemplars(t *testing.T) {
@ -718,6 +778,8 @@ func TestAssertEqualIgnoreValue(t *testing.T) {
t.Run("ExponentialHistogramFloat64", testDatatypeIgnoreValue(exponentialHistogramFloat64A, exponentialHistogramFloat64D, equalExponentialHistograms[float64]))
t.Run("ExponentialHistogramDataPointInt64", testDatatypeIgnoreValue(exponentialHistogramDataPointInt64A, exponentialHistogramDataPointInt64D, equalExponentialHistogramDataPoints[int64]))
t.Run("ExponentialHistogramDataPointFloat64", testDatatypeIgnoreValue(exponentialHistogramDataPointFloat64A, exponentialHistogramDataPointFloat64D, equalExponentialHistogramDataPoints[float64]))
t.Run("Summary", testDatatypeIgnoreValue(summaryA, summaryD, equalSummary))
t.Run("SummaryDataPoint", testDatatypeIgnoreValue(summaryDataPointA, summaryDataPointD, equalSummaryDataPoint))
}
type unknownAggregation struct {
@ -734,6 +796,7 @@ func TestAssertAggregationsEqual(t *testing.T) {
AssertAggregationsEqual(t, histogramFloat64A, histogramFloat64A)
AssertAggregationsEqual(t, exponentialHistogramInt64A, exponentialHistogramInt64A)
AssertAggregationsEqual(t, exponentialHistogramFloat64A, exponentialHistogramFloat64A)
AssertAggregationsEqual(t, summaryA, summaryA)
r := equalAggregations(sumInt64A, nil, config{})
assert.Len(t, r, 1, "should return nil comparison mismatch only")
@ -815,6 +878,15 @@ func TestAssertAggregationsEqual(t *testing.T) {
r = equalAggregations(exponentialHistogramFloat64A, exponentialHistogramFloat64D, config{ignoreValue: true})
assert.Len(t, r, 0, "value should be ignored: %v == %v", exponentialHistogramFloat64A, exponentialHistogramFloat64D)
r = equalAggregations(summaryA, summaryB, config{})
assert.Greaterf(t, len(r), 0, "summaries should not be equal: %v == %v", summaryA, summaryB)
r = equalAggregations(summaryA, summaryC, config{ignoreTimestamp: true})
assert.Len(t, r, 0, "summaries should be equal: %v", r)
r = equalAggregations(summaryA, summaryD, config{ignoreValue: true})
assert.Len(t, r, 0, "value should be ignored: %v == %v", summaryA, summaryD)
}
func TestAssertAttributes(t *testing.T) {
@ -839,6 +911,9 @@ func TestAssertAttributes(t *testing.T) {
AssertHasAttributes(t, exponentialHistogramInt64A, attribute.Bool("A", true))
AssertHasAttributes(t, exponentialHistogramFloat64A, attribute.Bool("A", true))
AssertHasAttributes(t, exponentialBucket2, attribute.Bool("A", true)) // No-op, always pass.
AssertHasAttributes(t, summaryDataPointA, attribute.Bool("A", true))
AssertHasAttributes(t, summaryA, attribute.Bool("A", true))
AssertHasAttributes(t, quantileValueA, attribute.Bool("A", true)) // No-op, always pass.
r := hasAttributesAggregation(gaugeInt64A, attribute.Bool("A", true))
assert.Equal(t, len(r), 0, "gaugeInt64A has A=True")
@ -856,6 +931,8 @@ func TestAssertAttributes(t *testing.T) {
assert.Equal(t, len(r), 0, "exponentialHistogramInt64A has A=True")
r = hasAttributesAggregation(exponentialHistogramFloat64A, attribute.Bool("A", true))
assert.Equal(t, len(r), 0, "exponentialHistogramFloat64A has A=True")
r = hasAttributesAggregation(summaryA, attribute.Bool("A", true))
assert.Equal(t, len(r), 0, "summaryA has A=True")
r = hasAttributesAggregation(gaugeInt64A, attribute.Bool("A", false))
assert.Greater(t, len(r), 0, "gaugeInt64A does not have A=False")
@ -873,6 +950,8 @@ func TestAssertAttributes(t *testing.T) {
assert.Greater(t, len(r), 0, "exponentialHistogramInt64A does not have A=False")
r = hasAttributesAggregation(exponentialHistogramFloat64A, attribute.Bool("A", false))
assert.Greater(t, len(r), 0, "exponentialHistogramFloat64A does not have A=False")
r = hasAttributesAggregation(summaryA, attribute.Bool("A", false))
assert.Greater(t, len(r), 0, "summaryA does not have A=False")
r = hasAttributesAggregation(gaugeInt64A, attribute.Bool("B", true))
assert.Greater(t, len(r), 0, "gaugeInt64A does not have Attribute B")
@ -890,6 +969,8 @@ func TestAssertAttributes(t *testing.T) {
assert.Greater(t, len(r), 0, "exponentialHistogramIntA does not have Attribute B")
r = hasAttributesAggregation(exponentialHistogramFloat64A, attribute.Bool("B", true))
assert.Greater(t, len(r), 0, "exponentialHistogramFloatA does not have Attribute B")
r = hasAttributesAggregation(summaryA, attribute.Bool("B", true))
assert.Greater(t, len(r), 0, "summaryA does not have Attribute B")
}
func TestAssertAttributesFail(t *testing.T) {
@ -914,6 +995,10 @@ func TestAssertAttributesFail(t *testing.T) {
assert.False(t, AssertHasAttributes(fakeT, exponentialHistogramDataPointFloat64A, attribute.Bool("B", true)))
assert.False(t, AssertHasAttributes(fakeT, exponentialHistogramInt64A, attribute.Bool("A", false)))
assert.False(t, AssertHasAttributes(fakeT, exponentialHistogramFloat64A, attribute.Bool("B", true)))
assert.False(t, AssertHasAttributes(fakeT, summaryDataPointA, attribute.Bool("A", false)))
assert.False(t, AssertHasAttributes(fakeT, summaryDataPointA, attribute.Bool("B", true)))
assert.False(t, AssertHasAttributes(fakeT, summaryA, attribute.Bool("A", false)))
assert.False(t, AssertHasAttributes(fakeT, summaryA, attribute.Bool("B", true)))
sum := metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,

View File

@ -155,6 +155,12 @@ func equalAggregations(a, b metricdata.Aggregation, cfg config) (reasons []strin
reasons = append(reasons, "ExponentialHistogram not equal:")
reasons = append(reasons, r...)
}
case metricdata.Summary:
r := equalSummary(v, b.(metricdata.Summary), cfg)
if len(r) > 0 {
reasons = append(reasons, "Summary not equal:")
reasons = append(reasons, r...)
}
default:
reasons = append(reasons, fmt.Sprintf("Aggregation of unknown types %T", a))
}
@ -426,6 +432,69 @@ func equalExponentialBuckets(a, b metricdata.ExponentialBucket, _ config) (reaso
return reasons
}
func equalSummary(a, b metricdata.Summary, cfg config) (reasons []string) {
r := compareDiff(diffSlices(
a.DataPoints,
b.DataPoints,
func(a, b metricdata.SummaryDataPoint) bool {
r := equalSummaryDataPoint(a, b, cfg)
return len(r) == 0
},
))
if r != "" {
reasons = append(reasons, fmt.Sprintf("Summary DataPoints not equal:\n%s", r))
}
return reasons
}
func equalSummaryDataPoint(a, b metricdata.SummaryDataPoint, cfg config) (reasons []string) {
if !a.Attributes.Equals(&b.Attributes) {
reasons = append(reasons, notEqualStr(
"Attributes",
a.Attributes.Encoded(attribute.DefaultEncoder()),
b.Attributes.Encoded(attribute.DefaultEncoder()),
))
}
if !cfg.ignoreTimestamp {
if !a.StartTime.Equal(b.StartTime) {
reasons = append(reasons, notEqualStr("StartTime", a.StartTime.UnixNano(), b.StartTime.UnixNano()))
}
if !a.Time.Equal(b.Time) {
reasons = append(reasons, notEqualStr("Time", a.Time.UnixNano(), b.Time.UnixNano()))
}
}
if !cfg.ignoreValue {
if a.Count != b.Count {
reasons = append(reasons, notEqualStr("Count", a.Count, b.Count))
}
if a.Sum != b.Sum {
reasons = append(reasons, notEqualStr("Sum", a.Sum, b.Sum))
}
r := compareDiff(diffSlices(
a.QuantileValues,
a.QuantileValues,
func(a, b metricdata.QuantileValue) bool {
r := equalQuantileValue(a, b, cfg)
return len(r) == 0
},
))
if r != "" {
reasons = append(reasons, r)
}
}
return reasons
}
func equalQuantileValue(a, b metricdata.QuantileValue, _ config) (reasons []string) {
if a.Quantile != b.Quantile {
reasons = append(reasons, notEqualStr("Quantile", a.Quantile, b.Quantile))
}
if a.Value != b.Value {
reasons = append(reasons, notEqualStr("Value", a.Value, b.Value))
}
return reasons
}
func notEqualStr(prefix string, expected, actual interface{}) string {
return fmt.Sprintf("%s not equal:\nexpected: %v\nactual: %v", prefix, expected, actual)
}
@ -716,6 +785,8 @@ func hasAttributesAggregation(agg metricdata.Aggregation, attrs ...attribute.Key
reasons = hasAttributesExponentialHistogram(agg, attrs...)
case metricdata.ExponentialHistogram[float64]:
reasons = hasAttributesExponentialHistogram(agg, attrs...)
case metricdata.Summary:
reasons = hasAttributesSummary(agg, attrs...)
default:
reasons = []string{fmt.Sprintf("unknown aggregation %T", agg)}
}
@ -752,3 +823,28 @@ func hasAttributesResourceMetrics(rm metricdata.ResourceMetrics, attrs ...attrib
}
return reasons
}
func hasAttributesSummary(summary metricdata.Summary, attrs ...attribute.KeyValue) (reasons []string) {
for n, dp := range summary.DataPoints {
reas := hasAttributesSummaryDataPoint(dp, attrs...)
if len(reas) > 0 {
reasons = append(reasons, fmt.Sprintf("summary datapoint %d attributes:\n", n))
reasons = append(reasons, reas...)
}
}
return reasons
}
func hasAttributesSummaryDataPoint(dp metricdata.SummaryDataPoint, attrs ...attribute.KeyValue) (reasons []string) {
for _, attr := range attrs {
val, ok := dp.Attributes.Value(attr.Key)
if !ok {
reasons = append(reasons, missingAttrStr(string(attr.Key)))
continue
}
if val != attr.Value {
reasons = append(reasons, notEqualStr(string(attr.Key), attr.Value.Emit(), val.Emit()))
}
}
return reasons
}