Skip to content

Commit

Permalink
match spans on resource attributes
Browse files Browse the repository at this point in the history
allow regexp on attribute value if it is a string
match spans on instrumentation library
  • Loading branch information
zeitlinger committed Oct 6, 2020
1 parent c8aac9e commit 54454ff
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 275 deletions.
35 changes: 30 additions & 5 deletions internal/processor/filterconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ 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
Expand All @@ -96,23 +96,34 @@ 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:"resource"`

// 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
}

func (mp *MatchProperties) ValidateForLogs() error {
if len(mp.SpanNames) > 0 || len(mp.Services) > 0 {
return errors.New("neither services nor span_names should be specified for log records")
if len(mp.SpanNames) > 0 || len(mp.Services) > 0 || len(mp.Resources) > 0 || len(mp.Libraries) > 0 {
return errors.New("neither services nor span_names nor resources nor libraries should be specified for log records")
}

if len(mp.LogNames) == 0 && len(mp.Attributes) == 0 {
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"`
}
128 changes: 128 additions & 0 deletions internal/processor/filterhelper/attributematcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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 filterhelper

import (
"errors"
"fmt"
"strconv"

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

type AttributesMatcher []AttributeMatcher

// AttributeMatcher is a attribute key/value pair to match to.
type AttributeMatcher struct {
Key string
// If both AttributeValue and StringFilter are nil only check for key existence.
AttributeValue *pdata.AttributeValue
// StringFilter is needed to match against a regular expression
StringFilter filterset.FilterSet
}

var errUnexpectedAttributeType = errors.New("unexpected attribute type")

func NewAttributesMatcher(config filterset.Config, attributes []filterconfig.Attribute) (AttributesMatcher, error) {
// Convert attribute values from mp representation to in-memory representation.
var rawAttributes []AttributeMatcher
for _, attribute := range 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 := NewAttributeValueRaw(attribute.Value)
if err != nil {
return nil, err
}

if config.MatchType == filterset.Regexp {
if val.Type() != pdata.AttributeValueSTRING {
return nil, fmt.Errorf(
"%s=%s for %q only supports STRING, but found %s",
filterset.MatchTypeFieldName, filterset.Regexp, attribute.Key, val.Type(),
)
}

filter, err := filterset.CreateFilterSet([]string{val.StringVal()}, &config)
if err != nil {
return nil, err
}
entry.StringFilter = filter
} else {
entry.AttributeValue = &val
}
}

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

// match attributes specification against a span/log.
func (ma AttributesMatcher) Match(attrs pdata.AttributeMap) bool {
// If there are no attributes to match against, the span/log matches.
if len(ma) == 0 {
return true
}

// At this point, it is expected of the span/log to have attributes because of
// len(ma) != 0. This means for spans/logs 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
}

if property.StringFilter != nil {
value, err := attributeStringValue(attr)
if err != nil || !property.StringFilter.Matches(value) {
return false
}
} else if property.AttributeValue != nil {
if !attr.Equal(*property.AttributeValue) {
return false
}
}
}
return true
}

func attributeStringValue(attr pdata.AttributeValue) (string, error) {
switch attr.Type() {
case pdata.AttributeValueSTRING:
return attr.StringVal(), nil
case pdata.AttributeValueBOOL:
return strconv.FormatBool(attr.BoolVal()), nil
case pdata.AttributeValueDOUBLE:
return strconv.FormatFloat(attr.DoubleVal(), 'f', -1, 64), nil
case pdata.AttributeValueINT:
return strconv.FormatInt(attr.IntVal(), 10), nil
default:
return "", errUnexpectedAttributeType
}
}
85 changes: 4 additions & 81 deletions internal/processor/filterlog/filterlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package filterlog

import (
"errors"
"fmt"

"go.opentelemetry.io/collector/consumer/pdata"
Expand All @@ -38,16 +37,7 @@ type propertiesMatcher struct {
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
Attributes filterhelper.AttributesMatcher
}

// NewMatcher creates a LogRecord Matcher that matches based on the given MatchProperties.
Expand All @@ -62,9 +52,9 @@ func NewMatcher(mp *filterconfig.MatchProperties) (Matcher, error) {

var err error

var am attributesMatcher
var am filterhelper.AttributesMatcher
if len(mp.Attributes) > 0 {
am, err = newAttributesMatcher(mp)
am, err = filterhelper.NewAttributesMatcher(mp.Config, mp.Attributes)
if err != nil {
return nil, err
}
Expand All @@ -84,39 +74,6 @@ func NewMatcher(mp *filterconfig.MatchProperties) (Matcher, error) {
}, 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.
Expand All @@ -129,39 +86,5 @@ func (mp *propertiesMatcher) MatchLogRecord(lr pdata.LogRecord) bool {
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.Attributes.Match(lr.Attributes())
}
17 changes: 4 additions & 13 deletions internal/processor/filterlog/filterlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"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/filterset"
)

Expand Down Expand Up @@ -54,7 +55,7 @@ func TestLogRecord_validateMatchesConfiguration_InvalidConfig(t *testing.T) {
property: filterconfig.MatchProperties{
SpanNames: []string{"span"},
},
errorString: "neither services nor span_names should be specified for log records",
errorString: "neither services nor span_names nor resources nor libraries should be specified for log records",
},
{
name: "invalid_match_type",
Expand All @@ -71,16 +72,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 Down Expand Up @@ -349,7 +340,7 @@ func TestLogRecord_validateMatchesConfigurationForAttributes(t *testing.T) {
},
},
output: &propertiesMatcher{
Attributes: []attributeMatcher{
Attributes: []filterhelper.AttributeMatcher{
{
Key: "key1",
},
Expand All @@ -376,7 +367,7 @@ func TestLogRecord_validateMatchesConfigurationForAttributes(t *testing.T) {
},
},
output: &propertiesMatcher{
Attributes: []attributeMatcher{
Attributes: []filterhelper.AttributeMatcher{
{
Key: "key1",
},
Expand Down
2 changes: 1 addition & 1 deletion internal/processor/filterset/strict/strictfilterset.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func NewFilterSet(filters []string) (*FilterSet, error) {
return fs, nil
}

// Matches returns true if the given string matches any of the FitlerSet's filters.
// Matches returns true if the given string matches any of the FilterSet's filters.
func (sfs *FilterSet) Matches(toMatch string) bool {
_, ok := sfs.filters[toMatch]
return ok
Expand Down
Loading

0 comments on commit 54454ff

Please sign in to comment.