Skip to content

Commit

Permalink
Match spans against the instrumentation library and resource attribut…
Browse files Browse the repository at this point in the history
…es (#928)

### Add the possibility to match spans against the instrumentation library.

This is how it works:

```
// version match
//  expected actual  match
//  nil      <blank> yes
//  nil      1       yes
//  <blank>  <blank> yes
//  <blank>  1       no
//  1        <blank> no
//  1        1       yes
```

You can decide to match against all versions (expected = `nil`), a specific version (e.g. expected = `1`), or against "version not provided" (expected = `<blank>`). 

### match spans on resource attributes

Use case: drop attribute values (e.g. a password) for a certain version (`host.image.version`)

### allow regexp when matching attribute value if it is a string

Regular expressions should only be prohibited if the expected value is not a string (because an implicit conversion to string would be unexpected).
If the expected value is a string, then a regular expression should be allowed.
  • Loading branch information
zeitlinger authored Oct 8, 2020
1 parent 691ba50 commit 393e98f
Show file tree
Hide file tree
Showing 15 changed files with 586 additions and 461 deletions.
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

0 comments on commit 393e98f

Please sign in to comment.