1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2024-12-12 10:04:29 +02:00
opentelemetry-go/sdk/metric/view_test.go
Tyler Yahn dbf960c8e1
Add view example tests (#3460)
* 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

* Add view example tests

* Update comments to examples

* Fix broken English

Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
2022-11-21 07:45:47 -08:00

605 lines
16 KiB
Go

// 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 (
"fmt"
"regexp"
"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())
}
func ExampleNewView() {
// Create a view that renames the "latency" instrument from the v0.34.0
// version of the "http" instrumentation library as "request.latency".
view := NewView(Instrument{
Name: "latency",
Scope: instrumentation.Scope{
Name: "http",
Version: "v0.34.0",
},
}, Stream{Name: "request.latency"})
// The created view can then be registered with the OpenTelemetry metric
// SDK using the WithView option. Below is an example of how the view will
// function in the SDK for certain instruments.
stream, _ := view(Instrument{
Name: "latency",
Description: "request latency",
Unit: unit.Milliseconds,
Kind: InstrumentKindSyncCounter,
Scope: instrumentation.Scope{
Name: "http",
Version: "v0.34.0",
SchemaURL: "https://opentelemetry.io/schemas/1.0.0",
},
})
fmt.Println("name:", stream.Name)
fmt.Println("description:", stream.Description)
fmt.Println("unit:", stream.Unit)
// Output:
// name: request.latency
// description: request latency
// unit: ms
}
func ExampleNewView_drop() {
// Create a view that sets the drop aggregator for all instrumentation from
// the "db" library, effectively turning-off all instrumentation from that
// library.
view := NewView(
Instrument{Scope: instrumentation.Scope{Name: "db"}},
Stream{Aggregation: aggregation.Drop{}},
)
// The created view can then be registered with the OpenTelemetry metric
// SDK using the WithView option. Below is an example of how the view will
// function in the SDK for certain instruments.
stream, _ := view(Instrument{
Name: "queries",
Kind: InstrumentKindSyncCounter,
Scope: instrumentation.Scope{Name: "db", Version: "v0.4.0"},
})
fmt.Println("name:", stream.Name)
fmt.Printf("aggregation: %#v", stream.Aggregation)
// Output:
// name: queries
// aggregation: aggregation.Drop{}
}
func ExampleNewView_wildcard() {
// Create a view that sets unit to milliseconds for any instrument with a
// name suffix of ".ms".
view := NewView(
Instrument{Name: "*.ms"},
Stream{Unit: unit.Milliseconds},
)
// The created view can then be registered with the OpenTelemetry metric
// SDK using the WithView option. Below is an example of how the view
// function in the SDK for certain instruments.
stream, _ := view(Instrument{
Name: "computation.time.ms",
Unit: unit.Dimensionless,
})
fmt.Println("name:", stream.Name)
fmt.Println("unit:", stream.Unit)
// Output:
// name: computation.time.ms
// unit: ms
}
func ExampleView() {
// The NewView function provides convenient creation of common Views
// construction. However, it is limited in what it can create.
//
// When NewView is not able to provide the functionally needed, a custom
// View can be constructed directly. Here a custom View is constructed that
// uses Go's regular expression matching to ensure all data stream names
// have a suffix of the units it uses.
re := regexp.MustCompile(`[._](ms|byte)$`)
var view View = func(i Instrument) (Stream, bool) {
s := Stream{Name: i.Name, Description: i.Description, Unit: i.Unit}
// Any instrument that does not have a unit suffix defined, but has a
// dimensional unit defined, update the name with a unit suffix.
if re.MatchString(i.Name) {
return s, false
}
switch i.Unit {
case unit.Milliseconds:
s.Name += ".ms"
case unit.Bytes:
s.Name += ".byte"
default:
return s, false
}
return s, true
}
// The created view can then be registered with the OpenTelemetry metric
// SDK using the WithView option. Below is an example of how the view will
// function in the SDK for certain instruments.
stream, _ := view(Instrument{
Name: "computation.time.ms",
Unit: unit.Milliseconds,
})
fmt.Println("name:", stream.Name)
stream, _ = view(Instrument{
Name: "heap.size",
Unit: unit.Bytes,
})
fmt.Println("name:", stream.Name)
// Output:
// name: computation.time.ms
// name: heap.size.byte
}