mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-01-07 23:02:15 +02:00
81b2a33e1b
Resolve https://github.com/open-telemetry/opentelemetry-go/issues/5249 ### Spec > exemplar_reservoir: A functional type that generates an exemplar reservoir a MeterProvider will use when storing exemplars. This functional type needs to be a factory or callback similar to aggregation selection functionality which allows different reservoirs to be chosen by the aggregation. > Users can provide an exemplar_reservoir, but it is up to their discretion. Therefore, the stream configuration parameter needs to be structured to accept an exemplar_reservoir, but MUST NOT obligate a user to provide one. If the user does not provide an exemplar_reservoir value, the MeterProvider MUST apply a [default exemplar reservoir](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplar-defaults). Also, > the reservoir MUST be given the Attributes associated with its timeseries point either at construction so that additional sampling performed by the reservoir has access to all attributes from a measurement in the "offer" method. ### Changes In sdk/metric/exemplar, add: * `exemplar.ReservoirProvider` * `exemplar.HistogramReservoirProvider` * `exemplar.FixedSizeReservoirProvider` In sdk/metric, add: * `metric.ExemplarReservoirProviderSelector` (func Aggregation -> ReservoirProvider) * `metric.DefaultExemplarReservoirProviderSelector` (our default implementation) * `ExemplarReservoirProviderSelector` added to `metric.Stream` Note: the only required public types are `metric.ExemplarReservoirProviderSelector` and `ExemplarReservoirProviderSelector` in `metric.Stream`. Others are for convenience and readability. ### Alternatives considered #### Add ExemplarReservoirProvider directly to metric.Stream, instead of ExemplarReservoirProviderSelector This would mean users can configure a `func() exemplar.Reservoir` instead of a `func(Aggregation) func() exemplar.Reservoir`. I don't think this complies with the statement: `This functional type needs to be a factory or callback similar to aggregation selection functionality which allows different reservoirs to be chosen by the aggregation.`. I'm interpreting "allows different reservoirs to be chosen by the aggregation" as meaning "allows different reservoirs to be chosen **based on the** aggregation", rather than meaning that the aggregation is somehow choosing the reservoir. ### Future work There is some refactoring I plan to do after this to simplify the interaction between the internal/aggregate and exemplar package. I've omitted that from this PR to keep the diff smaller. --------- Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> Co-authored-by: Robert Pająk <pellared@hotmail.com>
119 lines
3.7 KiB
Go
119 lines
3.7 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package metric // import "go.opentelemetry.io/otel/sdk/metric"
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"go.opentelemetry.io/otel/internal/global"
|
|
)
|
|
|
|
var (
|
|
errMultiInst = errors.New("name replacement for multiple instruments")
|
|
errEmptyView = errors.New("no criteria provided for view")
|
|
|
|
emptyView = func(Instrument) (Stream, bool) { return Stream{}, false }
|
|
)
|
|
|
|
// View is an override to the default behavior of the SDK. It defines how data
|
|
// should be collected for certain instruments. It returns true and the exact
|
|
// Stream to use for matching Instruments. Otherwise, if the view does not
|
|
// match, false is returned.
|
|
type View func(Instrument) (Stream, bool)
|
|
|
|
// NewView returns a View that applies the Stream mask for all instruments that
|
|
// match criteria. The returned View will only apply mask if all non-zero-value
|
|
// fields of criteria match the corresponding Instrument passed to the view. If
|
|
// no criteria are provided, all field of criteria are their zero-values, a
|
|
// view that matches no instruments is returned. If you need to match a
|
|
// zero-value field, create a View directly.
|
|
//
|
|
// The Name field of criteria supports wildcard pattern matching. The "*"
|
|
// wildcard is recognized as matching zero or more characters, and "?" is
|
|
// recognized as matching exactly one character. For example, a pattern of "*"
|
|
// matches all instrument names.
|
|
//
|
|
// The Stream mask only applies updates for non-zero-value fields. By default,
|
|
// the Instrument the View matches against will be use for the Name,
|
|
// Description, and Unit of the returned Stream and no Aggregation or
|
|
// AttributeFilter are set. All non-zero-value fields of mask are used instead
|
|
// of the default. If you need to zero out an Stream field returned from a
|
|
// View, create a View directly.
|
|
func NewView(criteria Instrument, mask Stream) View {
|
|
if criteria.IsEmpty() {
|
|
global.Error(
|
|
errEmptyView, "dropping view",
|
|
"mask", mask,
|
|
)
|
|
return emptyView
|
|
}
|
|
|
|
var matchFunc func(Instrument) bool
|
|
if strings.ContainsAny(criteria.Name, "*?") {
|
|
if mask.Name != "" {
|
|
global.Error(
|
|
errMultiInst, "dropping view",
|
|
"criteria", criteria,
|
|
"mask", mask,
|
|
)
|
|
return emptyView
|
|
}
|
|
|
|
// Handle branching here in NewView instead of criteria.matches so
|
|
// criteria.matches remains inlinable for the simple case.
|
|
pattern := regexp.QuoteMeta(criteria.Name)
|
|
pattern = "^" + pattern + "$"
|
|
pattern = strings.ReplaceAll(pattern, `\?`, ".")
|
|
pattern = strings.ReplaceAll(pattern, `\*`, ".*")
|
|
re := regexp.MustCompile(pattern)
|
|
matchFunc = func(i Instrument) bool {
|
|
return re.MatchString(i.Name) &&
|
|
criteria.matchesDescription(i) &&
|
|
criteria.matchesKind(i) &&
|
|
criteria.matchesUnit(i) &&
|
|
criteria.matchesScope(i)
|
|
}
|
|
} else {
|
|
matchFunc = criteria.matches
|
|
}
|
|
|
|
var agg Aggregation
|
|
if mask.Aggregation != nil {
|
|
agg = mask.Aggregation.copy()
|
|
if err := agg.err(); err != nil {
|
|
global.Error(
|
|
err, "not using aggregation with view",
|
|
"criteria", criteria,
|
|
"mask", mask,
|
|
)
|
|
agg = nil
|
|
}
|
|
}
|
|
|
|
return func(i Instrument) (Stream, bool) {
|
|
if matchFunc(i) {
|
|
return Stream{
|
|
Name: nonZero(mask.Name, i.Name),
|
|
Description: nonZero(mask.Description, i.Description),
|
|
Unit: nonZero(mask.Unit, i.Unit),
|
|
Aggregation: agg,
|
|
AttributeFilter: mask.AttributeFilter,
|
|
ExemplarReservoirProviderSelector: mask.ExemplarReservoirProviderSelector,
|
|
}, true
|
|
}
|
|
return Stream{}, false
|
|
}
|
|
}
|
|
|
|
// nonZero returns v if it is non-zero-valued, otherwise alt.
|
|
func nonZero[T comparable](v, alt T) T {
|
|
var zero T
|
|
if v != zero {
|
|
return v
|
|
}
|
|
return alt
|
|
}
|