1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-02-07 13:31:42 +02:00

Add View, NewView, Instrument, Stream, and InstrumentKind to sdk/metric with unit tests (#3459)

* Add the InstrumentKind type and vars to sdk/metric

* Add the Instrument type to sdk/metric

* Add the Stream type to sdk/metric

* Add the View type to sdk/metric

* Add NewView to create Views matching OTel spec

* Add unit tests for NewView

* Add changes to changelog

* Apply suggestions from code review

Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>

* Update CHANGELOG.md

* Update match and mask comments of Instrument

* Explain wildcard logic in NewView with comment

* Drop views that replace name for multi-inst match

* Comment how users are expected to match zero-vals

* Remove InstrumentKind and Scope from Stream

* Fix redundant word in NewView comment

Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com>
This commit is contained in:
Tyler Yahn 2022-11-16 08:18:02 -08:00 committed by GitHub
parent 404f999fd0
commit 2e780d8e39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 733 additions and 0 deletions

View File

@ -24,6 +24,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE`
- `OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE`
- `OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE`
- The `View` type and related `NewView` function to create a view according to the OpenTelemetry specification are added to `go.opentelemetry.io/otel/sdk/metric`.
These additions are replacements for the `View` type and `New` function from `go.opentelemetry.io/otel/sdk/metric/view`. (#3459)
- The `Instrument` and `InstrumentKind` type are added to `go.opentelemetry.io/otel/sdk/metric`.
These additions are replacements for the `Instrument` and `InstrumentKind` types from `go.opentelemetry.io/otel/sdk/metric/view`. (#3459)
- The `Stream` type is added to `go.opentelemetry.io/otel/sdk/metric` to define a metric data stream a view will produce. (#3459)
### Changed

View File

@ -24,10 +24,133 @@ import (
"go.opentelemetry.io/otel/metric/instrument/syncfloat64"
"go.opentelemetry.io/otel/metric/instrument/syncint64"
"go.opentelemetry.io/otel/metric/unit"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric/aggregation"
"go.opentelemetry.io/otel/sdk/metric/internal"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
var (
zeroUnit unit.Unit
zeroInstrumentKind InstrumentKind
zeroScope instrumentation.Scope
)
// InstrumentKind is the identifier of a group of instruments that all
// performing the same function.
type InstrumentKind uint8
const (
// instrumentKindUndefined is an undefined instrument kind, it should not
// be used by any initialized type.
instrumentKindUndefined InstrumentKind = iota // nolint:deadcode,varcheck,unused
// InstrumentKindSyncCounter identifies a group of instruments that record
// increasing values synchronously with the code path they are measuring.
InstrumentKindSyncCounter
// InstrumentKindSyncUpDownCounter identifies a group of instruments that
// record increasing and decreasing values synchronously with the code path
// they are measuring.
InstrumentKindSyncUpDownCounter
// InstrumentKindSyncHistogram identifies a group of instruments that
// record a distribution of values synchronously with the code path they
// are measuring.
InstrumentKindSyncHistogram
// InstrumentKindAsyncCounter identifies a group of instruments that record
// increasing values in an asynchronous callback.
InstrumentKindAsyncCounter
// InstrumentKindAsyncUpDownCounter identifies a group of instruments that
// record increasing and decreasing values in an asynchronous callback.
InstrumentKindAsyncUpDownCounter
// InstrumentKindAsyncGauge identifies a group of instruments that record
// current values in an asynchronous callback.
InstrumentKindAsyncGauge
)
type nonComparable [0]func() // nolint: unused // This is indeed used.
// Instrument describes properties an instrument is created with.
type Instrument struct {
// Name is the human-readable identifier of the instrument.
Name string
// Description describes the purpose of the instrument.
Description string
// Kind defines the functional group of the instrument.
Kind InstrumentKind
// Unit is the unit of measurement recorded by the instrument.
Unit unit.Unit
// Scope identifies the instrumentation that created the instrument.
Scope instrumentation.Scope
// Ensure forward compatibility if non-comparable fields need to be added.
nonComparable // nolint: unused
}
// empty returns if all fields of i are their zero-value.
func (i Instrument) empty() bool {
return i.Name == "" &&
i.Description == "" &&
i.Kind == zeroInstrumentKind &&
i.Unit == zeroUnit &&
i.Scope == zeroScope
}
// matches returns whether all the non-zero-value fields of i match the
// corresponding fields of other. If i is empty it will match all other, and
// true will always be returned.
func (i Instrument) matches(other Instrument) bool {
return i.matchesName(other) &&
i.matchesDescription(other) &&
i.matchesKind(other) &&
i.matchesUnit(other) &&
i.matchesScope(other)
}
// matchesName returns true if the Name of i is "" or it equals the Name of
// other, otherwise false.
func (i Instrument) matchesName(other Instrument) bool {
return i.Name == "" || i.Name == other.Name
}
// matchesDescription returns true if the Description of i is "" or it equals
// the Description of other, otherwise false.
func (i Instrument) matchesDescription(other Instrument) bool {
return i.Description == "" || i.Description == other.Description
}
// matchesKind returns true if the Kind of i is its zero-value or it equals the
// Kind of other, otherwise false.
func (i Instrument) matchesKind(other Instrument) bool {
return i.Kind == zeroInstrumentKind || i.Kind == other.Kind
}
// matchesUnit returns true if the Unit of i is its zero-value or it equals the
// Unit of other, otherwise false.
func (i Instrument) matchesUnit(other Instrument) bool {
return i.Unit == zeroUnit || i.Unit == other.Unit
}
// matchesScope returns true if the Scope of i is its zero-value or it equals
// the Scope of other, otherwise false.
func (i Instrument) matchesScope(other Instrument) bool {
return (i.Scope.Name == "" || i.Scope.Name == other.Scope.Name) &&
(i.Scope.Version == "" || i.Scope.Version == other.Scope.Version) &&
(i.Scope.SchemaURL == "" || i.Scope.SchemaURL == other.Scope.SchemaURL)
}
// Stream describes the stream of data an instrument produces.
type Stream struct {
// Name is the human-readable identifier of the stream.
Name string
// Description describes the purpose of the data.
Description string
// Unit is the unit of measurement recorded.
Unit unit.Unit
// Aggregation the stream uses for an instrument.
Aggregation aggregation.Aggregation
// AttributeFilter applied to all attributes recorded for an instrument.
AttributeFilter attribute.Filter
}
// instrumentID are the identifying properties of an instrument.
type instrumentID struct {
// Name is the name of the instrument.

View File

@ -374,6 +374,7 @@ func TestPipelineRegistryCreateAggregatorsIncompatibleInstrument(t *testing.T) {
type logCounter struct {
logr.LogSink
errN uint32
infoN uint32
}
@ -386,6 +387,15 @@ func (l *logCounter) InfoN() int {
return int(atomic.SwapUint32(&l.infoN, 0))
}
func (l *logCounter) Error(err error, msg string, keysAndValues ...interface{}) {
atomic.AddUint32(&l.errN, 1)
l.LogSink.Error(err, msg, keysAndValues...)
}
func (l *logCounter) ErrorN() int {
return int(atomic.SwapUint32(&l.errN, 0))
}
func TestResolveAggregatorsDuplicateErrors(t *testing.T) {
tLog := testr.NewWithOptions(t, testr.Options{Verbosity: 6})
l := &logCounter{LogSink: tLog.GetSink()}

124
sdk/metric/view.go Normal file
View File

@ -0,0 +1,124 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metric // import "go.opentelemetry.io/otel/sdk/metric"
import (
"errors"
"regexp"
"strings"
"go.opentelemetry.io/otel/internal/global"
"go.opentelemetry.io/otel/sdk/metric/aggregation"
)
var (
errMultiInst = errors.New("name replacement for multiple instruments")
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 "*" will match
// 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.empty() {
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.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
}

471
sdk/metric/view_test.go Normal file
View File

@ -0,0 +1,471 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metric // import "go.opentelemetry.io/otel/sdk/metric"
import (
"testing"
"github.com/go-logr/logr"
"github.com/go-logr/logr/testr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric/unit"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric/aggregation"
)
var (
schemaURL = "https://opentelemetry.io/schemas/1.0.0"
completeIP = Instrument{
Name: "foo",
Description: "foo desc",
Kind: InstrumentKindSyncCounter,
Unit: unit.Bytes,
Scope: instrumentation.Scope{
Name: "TestNewViewMatch",
Version: "v0.1.0",
SchemaURL: schemaURL,
},
}
)
func scope(name, ver, url string) instrumentation.Scope {
return instrumentation.Scope{Name: name, Version: ver, SchemaURL: url}
}
func testNewViewMatchName() func(t *testing.T) {
tests := []struct {
name string
criteria string
match []string
notMatch []string
}{
{
name: "Exact",
criteria: "foo",
match: []string{"foo"},
notMatch: []string{"", "bar", "foobar", "barfoo", "ffooo"},
},
{
name: "Wildcard/*",
criteria: "*",
match: []string{"", "foo", "foobar", "barfoo", "barfoobaz"},
},
{
name: "Wildcard/Front?",
criteria: "?oo",
match: []string{"foo", "1oo"},
notMatch: []string{"", "bar", "foobar", "barfoo", "barfoobaz"},
},
{
name: "Wildcard/Back?",
criteria: "fo?",
match: []string{"foo", "fo1"},
notMatch: []string{"", "bar", "foobar", "barfoo", "barfoobaz"},
},
{
name: "Wildcard/Front*",
criteria: "*foo",
match: []string{"foo", "123foo", "barfoo"},
notMatch: []string{"", "bar", "foobar", "barfoobaz"},
},
{
name: "Wildcard/Back*",
criteria: "foo*",
match: []string{"foo", "foo1", "foobar"},
notMatch: []string{"", "bar", "barfoo", "barfoobaz"},
},
{
name: "Wildcard/FrontBack*",
criteria: "*foo*",
match: []string{"foo", "foo1", "1foo", "1foo1", "foobar", "barfoobaz"},
notMatch: []string{"", "bar"},
},
{
name: "Wildcard/Front**",
criteria: "**foo",
match: []string{"foo", "123foo", "barfoo", "afoo"},
notMatch: []string{"", "bar", "foobar", "barfoobaz"},
},
{
name: "Wildcard/Back**",
criteria: "foo**",
match: []string{"foo", "foo1", "fooa", "foobar"},
notMatch: []string{"", "bar", "barfoo", "barfoobaz"},
},
{
name: "Wildcard/Front*?",
criteria: "*?oo",
match: []string{"foo", "123foo", "barfoo", "afoo"},
notMatch: []string{"", "fo", "bar", "foobar", "barfoobaz"},
},
{
name: "Wildcard/Back*?",
criteria: "fo*?",
match: []string{"foo", "foo1", "fooa", "foobar"},
notMatch: []string{"", "bar", "barfoo", "barfoobaz"},
},
{
name: "Wildcard/Front?*",
criteria: "?*oo",
match: []string{"foo", "123foo", "barfoo", "afoo"},
notMatch: []string{"", "oo", "fo", "bar", "foobar", "barfoobaz"},
},
{
name: "Wildcard/Back?*",
criteria: "fo?*",
match: []string{"foo", "foo1", "fooa", "foobar"},
notMatch: []string{"", "fo", "bar", "barfoo", "barfoobaz"},
},
{
name: "Wildcard/Middle*",
criteria: "f*o",
match: []string{"fo", "foo", "fooo", "fo12baro"},
notMatch: []string{"", "bar", "barfoo", "barfoobaz"},
},
{
name: "Wildcard/Middle?",
criteria: "f?o",
match: []string{"foo", "f1o"},
notMatch: []string{"", "fo", "fooo", "fo12baro", "bar"},
},
{
name: "Wildcard/MetaCharacters",
criteria: "*.+()|[]{}^$-_?",
match: []string{"aa.+()|[]{}^$-_b", ".+()|[]{}^$-_b"},
notMatch: []string{"", "foo", ".+()|[]{}^$-_"},
},
}
return func(t *testing.T) {
for _, test := range tests {
v := NewView(Instrument{Name: test.criteria}, Stream{})
t.Run(test.name, func(t *testing.T) {
for _, n := range test.match {
_, matches := v(Instrument{Name: n})
assert.Truef(t, matches, "%s does not match %s", test.criteria, n)
}
for _, n := range test.notMatch {
_, matches := v(Instrument{Name: n})
assert.Falsef(t, matches, "%s matches %s", test.criteria, n)
}
})
}
}
}
func TestNewViewMatch(t *testing.T) {
// Avoid boilerplate for name match testing.
t.Run("Name", testNewViewMatchName())
tests := []struct {
name string
criteria Instrument
matches []Instrument
notMatches []Instrument
}{
{
name: "Empty",
notMatches: []Instrument{{}, {Name: "foo"}, completeIP},
},
{
name: "Description",
criteria: Instrument{Description: "foo desc"},
matches: []Instrument{{Description: "foo desc"}, completeIP},
notMatches: []Instrument{{}, {Description: "foo"}, {Description: "desc"}},
},
{
name: "Kind",
criteria: Instrument{Kind: InstrumentKindSyncCounter},
matches: []Instrument{{Kind: InstrumentKindSyncCounter}, completeIP},
notMatches: []Instrument{
{},
{Kind: InstrumentKindSyncUpDownCounter},
{Kind: InstrumentKindSyncHistogram},
{Kind: InstrumentKindAsyncCounter},
{Kind: InstrumentKindAsyncUpDownCounter},
{Kind: InstrumentKindAsyncGauge},
},
},
{
name: "Unit",
criteria: Instrument{Unit: unit.Bytes},
matches: []Instrument{{Unit: unit.Bytes}, completeIP},
notMatches: []Instrument{
{},
{Unit: unit.Dimensionless},
{Unit: unit.Unit("K")},
},
},
{
name: "ScopeName",
criteria: Instrument{Scope: scope("TestNewViewMatch", "", "")},
matches: []Instrument{
{Scope: scope("TestNewViewMatch", "", "")},
completeIP,
},
notMatches: []Instrument{
{},
{Scope: scope("PrefixTestNewViewMatch", "", "")},
{Scope: scope("TestNewViewMatchSuffix", "", "")},
{Scope: scope("alt", "", "")},
},
},
{
name: "ScopeVersion",
criteria: Instrument{Scope: scope("", "v0.1.0", "")},
matches: []Instrument{
{Scope: scope("", "v0.1.0", "")},
completeIP,
},
notMatches: []Instrument{
{},
{Scope: scope("", "v0.1.0-RC1", "")},
{Scope: scope("", "v0.1.1", "")},
},
},
{
name: "ScopeSchemaURL",
criteria: Instrument{Scope: scope("", "", schemaURL)},
matches: []Instrument{
{Scope: scope("", "", schemaURL)},
completeIP,
},
notMatches: []Instrument{
{},
{Scope: scope("", "", schemaURL+"/path")},
{Scope: scope("", "", "https://go.dev")},
},
},
{
name: "Scope",
criteria: Instrument{Scope: scope("TestNewViewMatch", "v0.1.0", schemaURL)},
matches: []Instrument{
{Scope: scope("TestNewViewMatch", "v0.1.0", schemaURL)},
completeIP,
},
notMatches: []Instrument{
{},
{Scope: scope("CompleteMisMatch", "v0.2.0", "https://go.dev")},
{Scope: scope("NameMisMatch", "v0.1.0", schemaURL)},
},
},
{
name: "Complete",
criteria: completeIP,
matches: []Instrument{completeIP},
notMatches: []Instrument{
{},
{Name: "foo"},
{
Name: "Wrong Name",
Description: "foo desc",
Kind: InstrumentKindSyncCounter,
Unit: unit.Bytes,
Scope: scope("TestNewViewMatch", "v0.1.0", schemaURL),
},
{
Name: "foo",
Description: "Wrong Description",
Kind: InstrumentKindSyncCounter,
Unit: unit.Bytes,
Scope: scope("TestNewViewMatch", "v0.1.0", schemaURL),
},
{
Name: "foo",
Description: "foo desc",
Kind: InstrumentKindAsyncUpDownCounter,
Unit: unit.Bytes,
Scope: scope("TestNewViewMatch", "v0.1.0", schemaURL),
},
{
Name: "foo",
Description: "foo desc",
Kind: InstrumentKindSyncCounter,
Unit: unit.Dimensionless,
Scope: scope("TestNewViewMatch", "v0.1.0", schemaURL),
},
{
Name: "foo",
Description: "foo desc",
Kind: InstrumentKindSyncCounter,
Unit: unit.Bytes,
Scope: scope("Wrong Scope Name", "v0.1.0", schemaURL),
},
{
Name: "foo",
Description: "foo desc",
Kind: InstrumentKindSyncCounter,
Unit: unit.Bytes,
Scope: scope("TestNewViewMatch", "v1.4.3", schemaURL),
},
{
Name: "foo",
Description: "foo desc",
Kind: InstrumentKindSyncCounter,
Unit: unit.Bytes,
Scope: scope("TestNewViewMatch", "v0.1.0", "https://go.dev"),
},
},
},
}
for _, test := range tests {
v := NewView(test.criteria, Stream{})
t.Run(test.name, func(t *testing.T) {
for _, instrument := range test.matches {
_, matches := v(instrument)
assert.Truef(t, matches, "view does not match %#v", instrument)
}
for _, instrument := range test.notMatches {
_, matches := v(instrument)
assert.Falsef(t, matches, "view matches %#v", instrument)
}
})
}
}
func TestNewViewReplace(t *testing.T) {
alt := "alternative value"
tests := []struct {
name string
mask Stream
want func(Instrument) Stream
}{
{
name: "Nothing",
want: func(i Instrument) Stream {
return Stream{
Name: i.Name,
Description: i.Description,
Unit: i.Unit,
}
},
},
{
name: "Name",
mask: Stream{Name: alt},
want: func(i Instrument) Stream {
return Stream{
Name: alt,
Description: i.Description,
Unit: i.Unit,
}
},
},
{
name: "Description",
mask: Stream{Description: alt},
want: func(i Instrument) Stream {
return Stream{
Name: i.Name,
Description: alt,
Unit: i.Unit,
}
},
},
{
name: "Unit",
mask: Stream{Unit: unit.Dimensionless},
want: func(i Instrument) Stream {
return Stream{
Name: i.Name,
Description: i.Description,
Unit: unit.Dimensionless,
}
},
},
{
name: "Aggregation",
mask: Stream{Aggregation: aggregation.LastValue{}},
want: func(i Instrument) Stream {
return Stream{
Name: i.Name,
Description: i.Description,
Unit: i.Unit,
Aggregation: aggregation.LastValue{},
}
},
},
{
name: "Complete",
mask: Stream{
Name: alt,
Description: alt,
Unit: unit.Dimensionless,
Aggregation: aggregation.LastValue{},
},
want: func(i Instrument) Stream {
return Stream{
Name: alt,
Description: alt,
Unit: unit.Dimensionless,
Aggregation: aggregation.LastValue{},
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, match := NewView(completeIP, test.mask)(completeIP)
require.True(t, match, "view did not match exact criteria")
assert.Equal(t, test.want(completeIP), got)
})
}
// Go does not allow for the comparison of function values, even their
// addresses. Therefore, the AttributeFilter field needs an alternative
// testing strategy.
t.Run("AttributeFilter", func(t *testing.T) {
allowed := attribute.String("key", "val")
filter := func(kv attribute.KeyValue) bool {
return kv == allowed
}
mask := Stream{AttributeFilter: filter}
got, match := NewView(completeIP, mask)(completeIP)
require.True(t, match, "view did not match exact criteria")
require.NotNil(t, got.AttributeFilter, "AttributeFilter not set")
assert.True(t, got.AttributeFilter(allowed), "wrong AttributeFilter")
other := attribute.String("key", "other val")
assert.False(t, got.AttributeFilter(other), "wrong AttributeFilter")
})
}
type badAgg struct {
aggregation.Aggregation
err error
}
func (a badAgg) Copy() aggregation.Aggregation { return a }
func (a badAgg) Err() error { return a.err }
func TestNewViewAggregationErrorLogged(t *testing.T) {
tLog := testr.NewWithOptions(t, testr.Options{Verbosity: 6})
l := &logCounter{LogSink: tLog.GetSink()}
otel.SetLogger(logr.New(l))
agg := badAgg{err: assert.AnError}
mask := Stream{Aggregation: agg}
got, match := NewView(completeIP, mask)(completeIP)
require.True(t, match, "view did not match exact criteria")
assert.Nil(t, got.Aggregation, "erroring aggregation used")
assert.Equal(t, 1, l.ErrorN())
}