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:
parent
404f999fd0
commit
2e780d8e39
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
124
sdk/metric/view.go
Normal 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
471
sdk/metric/view_test.go
Normal 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())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user