mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2024-12-04 09:43:23 +02:00
dbf960c8e1
* 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>
605 lines
16 KiB
Go
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
|
|
}
|