Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add summary data type to metricdata #4622

Merged
merged 11 commits into from
Oct 25, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
51 changes: 51 additions & 0 deletions sdk/metric/metricdata/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,54 @@
// 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() {}

Check warning on line 257 in sdk/metric/metricdata/data.go

View check run for this annotation

Codecov / codecov/patch

sdk/metric/metricdata/data.go#L257

Added line #L257 was not covered by tests

// 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
}
MrAlias marked this conversation as resolved.
Show resolved Hide resolved

// 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
}
17 changes: 16 additions & 1 deletion sdk/metric/metricdata/metricdatatest/assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
85 changes: 85 additions & 0 deletions sdk/metric/metricdata/metricdatatest/assertion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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")
Expand Down Expand Up @@ -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) {
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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) {
Expand All @@ -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,
Expand Down
96 changes: 96 additions & 0 deletions sdk/metric/metricdata/metricdatatest/comparisons.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@
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))
}
Expand Down Expand Up @@ -426,6 +432,69 @@
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)
}

Check warning on line 483 in sdk/metric/metricdata/metricdatatest/comparisons.go

View check run for this annotation

Codecov / codecov/patch

sdk/metric/metricdata/metricdatatest/comparisons.go#L482-L483

Added lines #L482 - L483 were not covered by tests
}
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)
}
Expand Down Expand Up @@ -716,6 +785,8 @@
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)}
}
Expand Down Expand Up @@ -752,3 +823,28 @@
}
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
}