Skip to content

Match spans against the instrumentation library and resource attributes #928

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions internal/processor/filterconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ type MatchProperties struct {
// Config configures the matching patterns used when matching span properties.
filterset.Config `mapstructure:",squash"`

// Note: For spans, one of Services, SpanNames or Attributes must be specified with a
// Note: For spans, one of Services, SpanNames, Attributes, Resources or Libraries must be specified with a
// non-empty value for a valid configuration.

// For logs, one of LogNames or Attributes must be specified with a
// For logs, one of LogNames, Attributes, Resources or Libraries must be specified with a
// non-empty value for a valid configuration.

// Services specify the list of of items to match service name against.
Expand All @@ -96,15 +96,26 @@ type MatchProperties struct {
// Only match_type=strict is allowed if "attributes" are specified.
// This is an optional field.
Attributes []Attribute `mapstructure:"attributes"`

// Resources specify the list of items to match the resources against.
// A match occurs if the span's resources matches at least one item in this list.
// This is an optional field.
Resources []Attribute `mapstructure:"resources"`

// Libraries specify the list of items to match the implementation library against.
// A match occurs if the span's implementation library matches at least one item in this list.
// This is an optional field.
Libraries []InstrumentationLibrary `mapstructure:"libraries"`
}

func (mp *MatchProperties) ValidateForSpans() error {
if len(mp.LogNames) > 0 {
return errors.New("log_names should not be specified for trace spans")
}

if len(mp.Services) == 0 && len(mp.SpanNames) == 0 && len(mp.Attributes) == 0 {
return errors.New(`at least one of "services", "span_names" or "attributes" field must be specified`)
if len(mp.Services) == 0 && len(mp.SpanNames) == 0 && len(mp.Attributes) == 0 &&
len(mp.Libraries) == 0 && len(mp.Resources) == 0 {
return errors.New(`at least one of "services", "span_names", "attributes", "libraries" or "resources" field must be specified`)
}

return nil
Expand All @@ -115,8 +126,8 @@ func (mp *MatchProperties) ValidateForLogs() error {
return errors.New("neither services nor span_names should be specified for log records")
}

if len(mp.LogNames) == 0 && len(mp.Attributes) == 0 {
return errors.New(`at least one of "log_names" or "attributes" field must be specified`)
if len(mp.LogNames) == 0 && len(mp.Attributes) == 0 && len(mp.Libraries) == 0 && len(mp.Resources) == 0 {
return errors.New(`at least one of "log_names", "attributes", "libraries" or "resources" field must be specified`)
}

return nil
Expand All @@ -134,3 +145,17 @@ type Attribute struct {
// If it is not set, any value will match.
Value interface{} `mapstructure:"value"`
}

// InstrumentationLibrary specifies the instrumentation library and optional version to match against.
type InstrumentationLibrary struct {
Name string `mapstructure:"name"`
// version match
// expected actual match
// nil <blank> yes
// nil 1 yes
// <blank> <blank> yes
// <blank> 1 no
// 1 <blank> no
// 1 1 yes
Version *string `mapstructure:"version"`
}
105 changes: 11 additions & 94 deletions internal/processor/filterlog/filterlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
package filterlog

import (
"errors"
"fmt"

"go.opentelemetry.io/collector/consumer/pdata"
"go.opentelemetry.io/collector/internal/processor/filterconfig"
"go.opentelemetry.io/collector/internal/processor/filterhelper"
"go.opentelemetry.io/collector/internal/processor/filtermatcher"
"go.opentelemetry.io/collector/internal/processor/filterset"
)

Expand All @@ -29,25 +28,15 @@ import (
// Matcher is an interface that allows matching a log record against a
// configuration of a match.
type Matcher interface {
MatchLogRecord(lr pdata.LogRecord) bool
MatchLogRecord(lr pdata.LogRecord, resource pdata.Resource, library pdata.InstrumentationLibrary) bool
}

// propertiesMatcher allows matching a log record against various log record properties.
type propertiesMatcher struct {
filtermatcher.PropertiesMatcher

// log names to compare to.
nameFilters filterset.FilterSet

// The attribute values are stored in the internal format.
Attributes attributesMatcher
}

type attributesMatcher []attributeMatcher

// attributeMatcher is a attribute key/value pair to match to.
type attributeMatcher struct {
Key string
// If nil only check for key existence.
AttributeValue *pdata.AttributeValue
}

// NewMatcher creates a LogRecord Matcher that matches based on the given MatchProperties.
Expand All @@ -60,14 +49,9 @@ func NewMatcher(mp *filterconfig.MatchProperties) (Matcher, error) {
return nil, err
}

var err error

var am attributesMatcher
if len(mp.Attributes) > 0 {
am, err = newAttributesMatcher(mp)
if err != nil {
return nil, err
}
rm, err := filtermatcher.NewMatcher(mp)
if err != nil {
return nil, err
}

var nameFS filterset.FilterSet = nil
Expand All @@ -79,89 +63,22 @@ func NewMatcher(mp *filterconfig.MatchProperties) (Matcher, error) {
}

return &propertiesMatcher{
nameFilters: nameFS,
Attributes: am,
PropertiesMatcher: rm,
nameFilters: nameFS,
}, nil
}

func newAttributesMatcher(mp *filterconfig.MatchProperties) (attributesMatcher, error) {
// attribute matching is only supported with strict matching
if mp.Config.MatchType != filterset.Strict {
return nil, fmt.Errorf(
"%s=%s is not supported for %q",
filterset.MatchTypeFieldName, filterset.Regexp, filterconfig.AttributesFieldName,
)
}

// Convert attribute values from mp representation to in-memory representation.
var rawAttributes []attributeMatcher
for _, attribute := range mp.Attributes {

if attribute.Key == "" {
return nil, errors.New("error creating processor. Can't have empty key in the list of attributes")
}

entry := attributeMatcher{
Key: attribute.Key,
}
if attribute.Value != nil {
val, err := filterhelper.NewAttributeValueRaw(attribute.Value)
if err != nil {
return nil, err
}
entry.AttributeValue = &val
}

rawAttributes = append(rawAttributes, entry)
}
return rawAttributes, nil
}

// MatchLogRecord matches a log record to a set of properties.
// There are 3 sets of properties to match against.
// The log record names are matched, if specified.
// The attributes are then checked, if specified.
// At least one of log record names or attributes must be specified. It is
// supported to have more than one of these specified, and all specified must
// evaluate to true for a match to occur.
func (mp *propertiesMatcher) MatchLogRecord(lr pdata.LogRecord) bool {
func (mp *propertiesMatcher) MatchLogRecord(lr pdata.LogRecord, resource pdata.Resource, library pdata.InstrumentationLibrary) bool {
if mp.nameFilters != nil && !mp.nameFilters.Matches(lr.Name()) {
return false
}

return mp.Attributes.match(lr)
}

// match attributes specification against a log record.
func (ma attributesMatcher) match(lr pdata.LogRecord) bool {
// If there are no attributes to match against, the log matches.
if len(ma) == 0 {
return true
}

attrs := lr.Attributes()
// At this point, it is expected of the log record to have attributes
// because of len(ma) != 0. This means for log records with no attributes,
// it does not match.
if attrs.Len() == 0 {
return false
}

// Check that all expected properties are set.
for _, property := range ma {
attr, exist := attrs.Get(property.Key)
if !exist {
return false
}

// This is for the case of checking that the key existed.
if property.AttributeValue == nil {
continue
}

if !attr.Equal(*property.AttributeValue) {
return false
}
}
return true
return mp.PropertiesMatcher.Match(lr.Attributes(), resource, library)
}
98 changes: 7 additions & 91 deletions internal/processor/filterlog/filterlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ func TestLogRecord_validateMatchesConfiguration_InvalidConfig(t *testing.T) {
{
name: "empty_property",
property: filterconfig.MatchProperties{},
errorString: "at least one of \"log_names\" or \"attributes\" field must be specified",
errorString: "at least one of \"log_names\", \"attributes\", \"libraries\" or \"resources\" field must be specified",
},
{
name: "empty_log_names_and_attributes",
property: filterconfig.MatchProperties{
LogNames: []string{},
},
errorString: "at least one of \"log_names\" or \"attributes\" field must be specified",
errorString: "at least one of \"log_names\", \"attributes\", \"libraries\" or \"resources\" field must be specified",
},
{
name: "span_properties",
Expand All @@ -71,16 +71,6 @@ func TestLogRecord_validateMatchesConfiguration_InvalidConfig(t *testing.T) {
},
errorString: "error creating log record name filters: unrecognized match_type: '', valid types are: [regexp strict]",
},
{
name: "regexp_match_type_for_attributes",
property: filterconfig.MatchProperties{
Config: *createConfig(filterset.Regexp),
Attributes: []filterconfig.Attribute{
{Key: "key", Value: "value"},
},
},
errorString: `match_type=regexp is not supported for "attributes"`,
},
{
name: "invalid_regexp_pattern",
property: filterconfig.MatchProperties{
Expand All @@ -107,7 +97,7 @@ func TestLogRecord_validateMatchesConfiguration_InvalidConfig(t *testing.T) {
},
},
},
errorString: "error creating processor. Can't have empty key in the list of attributes",
errorString: "error creating attribute filters: can't have empty key in the list of attributes",
},
}
for _, tc := range testcases {
Expand Down Expand Up @@ -195,7 +185,7 @@ func TestLogRecord_Matching_False(t *testing.T) {
assert.Nil(t, err)
assert.NotNil(t, matcher)

assert.False(t, matcher.MatchLogRecord(lr))
assert.False(t, matcher.MatchLogRecord(lr, pdata.Resource{}, pdata.InstrumentationLibrary{}))
})
}
}
Expand All @@ -218,10 +208,10 @@ func TestLogRecord_MatchingCornerCases(t *testing.T) {

emptyLogRecord := pdata.NewLogRecord()
emptyLogRecord.InitEmpty()
assert.False(t, mp.MatchLogRecord(emptyLogRecord))
assert.False(t, mp.MatchLogRecord(emptyLogRecord, pdata.Resource{}, pdata.InstrumentationLibrary{}))

emptyLogRecord.SetName("svcA")
assert.False(t, mp.MatchLogRecord(emptyLogRecord))
assert.False(t, mp.MatchLogRecord(emptyLogRecord, pdata.Resource{}, pdata.InstrumentationLibrary{}))
}

func TestLogRecord_Matching_True(t *testing.T) {
Expand Down Expand Up @@ -323,81 +313,7 @@ func TestLogRecord_Matching_True(t *testing.T) {
assert.NotNil(t, mp)

assert.NotNil(t, lr)
assert.True(t, mp.MatchLogRecord(lr))
assert.True(t, mp.MatchLogRecord(lr, pdata.Resource{}, pdata.InstrumentationLibrary{}))
})
}
}

func TestLogRecord_validateMatchesConfigurationForAttributes(t *testing.T) {
testcase := []struct {
name string
input filterconfig.MatchProperties
output Matcher
}{
{
name: "attributes_build",
input: filterconfig.MatchProperties{
Config: *createConfig(filterset.Strict),
Attributes: []filterconfig.Attribute{
{
Key: "key1",
},
{
Key: "key2",
Value: 1234,
},
},
},
output: &propertiesMatcher{
Attributes: []attributeMatcher{
{
Key: "key1",
},
{
Key: "key2",
AttributeValue: newAttributeValueInt(1234),
},
},
},
},

{
name: "both_set_of_attributes",
input: filterconfig.MatchProperties{
Config: *createConfig(filterset.Strict),
Attributes: []filterconfig.Attribute{
{
Key: "key1",
},
{
Key: "key2",
Value: 1234,
},
},
},
output: &propertiesMatcher{
Attributes: []attributeMatcher{
{
Key: "key1",
},
{
Key: "key2",
AttributeValue: newAttributeValueInt(1234),
},
},
},
},
}
for _, tc := range testcase {
t.Run(tc.name, func(t *testing.T) {
output, err := NewMatcher(&tc.input)
require.NoError(t, err)
assert.Equal(t, tc.output, output)
})
}
}

func newAttributeValueInt(v int64) *pdata.AttributeValue {
attr := pdata.NewAttributeValueInt(v)
return &attr
}
1 change: 1 addition & 0 deletions internal/processor/filtermatcher/.nocover
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tested in filterspan package
Loading