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:
parent
7e6da12625
commit
cdd9353641
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user