// 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, }, 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 }