diff --git a/internal/processor/filterconfig/config.go b/internal/processor/filterconfig/config.go index 7ba24d3a219..ff6913ec7da 100644 --- a/internal/processor/filterconfig/config.go +++ b/internal/processor/filterconfig/config.go @@ -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 @@ -96,6 +96,16 @@ 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 { @@ -103,16 +113,17 @@ func (mp *MatchProperties) ValidateForSpans() error { 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 { @@ -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 yes + // nil 1 yes + // yes + // 1 no + // 1 no + // 1 1 yes + Version *string `mapstructure:"version"` +} diff --git a/internal/processor/filterhelper/attributematcher.go b/internal/processor/filterhelper/attributematcher.go new file mode 100644 index 00000000000..5ee218a9519 --- /dev/null +++ b/internal/processor/filterhelper/attributematcher.go @@ -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 + } +} diff --git a/internal/processor/filterlog/filterlog.go b/internal/processor/filterlog/filterlog.go index 968653ec4c3..6c9d2ed813f 100644 --- a/internal/processor/filterlog/filterlog.go +++ b/internal/processor/filterlog/filterlog.go @@ -15,7 +15,6 @@ package filterlog import ( - "errors" "fmt" "go.opentelemetry.io/collector/consumer/pdata" @@ -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. @@ -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 } @@ -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. @@ -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()) } diff --git a/internal/processor/filterlog/filterlog_test.go b/internal/processor/filterlog/filterlog_test.go index 1ce9b6be5cf..06394d0c7c5 100644 --- a/internal/processor/filterlog/filterlog_test.go +++ b/internal/processor/filterlog/filterlog_test.go @@ -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" ) @@ -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", @@ -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{ @@ -349,7 +340,7 @@ func TestLogRecord_validateMatchesConfigurationForAttributes(t *testing.T) { }, }, output: &propertiesMatcher{ - Attributes: []attributeMatcher{ + Attributes: []filterhelper.AttributeMatcher{ { Key: "key1", }, @@ -376,7 +367,7 @@ func TestLogRecord_validateMatchesConfigurationForAttributes(t *testing.T) { }, }, output: &propertiesMatcher{ - Attributes: []attributeMatcher{ + Attributes: []filterhelper.AttributeMatcher{ { Key: "key1", }, diff --git a/internal/processor/filterset/strict/strictfilterset.go b/internal/processor/filterset/strict/strictfilterset.go index ebe912b82ac..459f55866ca 100644 --- a/internal/processor/filterset/strict/strictfilterset.go +++ b/internal/processor/filterset/strict/strictfilterset.go @@ -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 diff --git a/internal/processor/filterspan/filterspan.go b/internal/processor/filterspan/filterspan.go index 1e347bfba66..b8c3151fe54 100644 --- a/internal/processor/filterspan/filterspan.go +++ b/internal/processor/filterspan/filterspan.go @@ -15,13 +15,13 @@ package filterspan 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/filterset" + "go.opentelemetry.io/collector/translator/conventions" ) // TODO: Modify Matcher to invoke both the include and exclude properties so @@ -29,7 +29,7 @@ import ( // Matcher is an interface that allows matching a span against a configuration // of a match. type Matcher interface { - MatchSpan(span pdata.Span, serviceName string) bool + MatchSpan(span pdata.Span, resource pdata.Resource, library pdata.InstrumentationLibrary) bool } // propertiesMatcher allows matching a span against various span properties. @@ -40,17 +40,18 @@ type propertiesMatcher struct { // Span names to compare to. nameFilters filterset.FilterSet + // Instrumentation libraries to compare against + Libraries []instrumentationLibraryMatcher + // The attribute values are stored in the internal format. - Attributes attributesMatcher -} + Attributes filterhelper.AttributesMatcher -type attributesMatcher []attributeMatcher + Resources filterhelper.AttributesMatcher +} -// 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 +type instrumentationLibraryMatcher struct { + Name filterset.FilterSet + Version filterset.FilterSet } // NewMatcher creates a span Matcher that matches based on the given MatchProperties. @@ -63,13 +64,39 @@ func NewMatcher(mp *filterconfig.MatchProperties) (Matcher, error) { return nil, err } - var err error + var lm []instrumentationLibraryMatcher + for _, library := range mp.Libraries { + name, err := filterset.CreateFilterSet([]string{library.Name}, &mp.Config) + if err != nil { + return nil, fmt.Errorf("error creating library name filters: %v", err) + } + + var version filterset.FilterSet + if library.Version != nil { + filter, err := filterset.CreateFilterSet([]string{*library.Version}, &mp.Config) + if err != nil { + return nil, fmt.Errorf("error creating library version filters: %v", err) + } + version = filter + } + + lm = append(lm, instrumentationLibraryMatcher{Name: name, Version: version}) + } - var am attributesMatcher + var err error + 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, fmt.Errorf("error creating attribute filters: %v", err) + } + } + + var rm filterhelper.AttributesMatcher + if len(mp.Resources) > 0 { + rm, err = filterhelper.NewAttributesMatcher(mp.Config, mp.Resources) if err != nil { - return nil, err + return nil, fmt.Errorf("error creating resource filters: %v", err) } } @@ -92,41 +119,34 @@ func NewMatcher(mp *filterconfig.MatchProperties) (Matcher, error) { return &propertiesMatcher{ serviceFilters: serviceFS, nameFilters: nameFS, + Libraries: lm, Attributes: am, + Resources: rm, }, 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") +// SkipSpan determines if a span should be processed. +// True is returned when a span should be skipped. +// False is returned when a span should not be skipped. +// The logic determining if a span should be processed is set +// in the attribute configuration with the include and exclude settings. +// Include properties are checked before exclude settings are checked. +func SkipSpan(include Matcher, exclude Matcher, span pdata.Span, resource pdata.Resource, library pdata.InstrumentationLibrary) bool { + if include != nil { + // A false returned in this case means the span should not be processed. + if i := include.MatchSpan(span, resource, library); !i { + return true } + } - entry := attributeMatcher{ - Key: attribute.Key, + if exclude != nil { + // A true returned in this case means the span should not be processed. + if e := exclude.MatchSpan(span, resource, library); e { + return true } - 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 + + return false } // MatchSpan matches a span and service to a set of properties. @@ -136,49 +156,46 @@ func newAttributesMatcher(mp *filterconfig.MatchProperties) (attributesMatcher, // At least one of services, span 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) MatchSpan(span pdata.Span, serviceName string) bool { +func (mp *propertiesMatcher) MatchSpan(span pdata.Span, resource pdata.Resource, library pdata.InstrumentationLibrary) bool { // If a set of properties was not in the mp, all spans are considered to match on that property - if mp.serviceFilters != nil && !mp.serviceFilters.Matches(serviceName) { - return false + if mp.serviceFilters != nil { + serviceName := serviceNameForResource(resource) + if !mp.serviceFilters.Matches(serviceName) { + return false + } } if mp.nameFilters != nil && !mp.nameFilters.Matches(span.Name()) { return false } - // Service name and span name matched. Now match attributes. - return mp.Attributes.match(span) -} - -// match attributes specification against a span. -func (ma attributesMatcher) match(span pdata.Span) bool { - // If there are no attributes to match against, the span matches. - if len(ma) == 0 { - return true + for _, matcher := range mp.Libraries { + if !matcher.Name.Matches(library.Name()) { + return false + } + if matcher.Version != nil && !matcher.Version.Matches(library.Version()) { + return false + } } - attrs := span.Attributes() - // At this point, it is expected of the span to have attributes because of - // len(ma) != 0. This means for spans with no attributes, it does not match. - if attrs.Len() == 0 { + attributes := span.Attributes() + if mp.Resources != nil && !mp.Resources.Match(attributes) { return false } - // Check that all expected properties are set. - for _, property := range ma { - attr, exist := attrs.Get(property.Key) - if !exist { - return false - } + return mp.Attributes.Match(attributes) +} - // This is for the case of checking that the key existed. - if property.AttributeValue == nil { - continue - } +// serviceNameForResource gets the service name for a specified Resource. +func serviceNameForResource(resource pdata.Resource) string { + if resource.IsNil() { + return "" + } - if !attr.Equal(*property.AttributeValue) { - return false - } + service, found := resource.Attributes().Get(conventions.AttributeServiceName) + if !found { + return "" } - return true + + return service.StringVal() } diff --git a/internal/processor/filterspan/filterspan_test.go b/internal/processor/filterspan/filterspan_test.go index 0ad461f0ac4..1e90990f6cd 100644 --- a/internal/processor/filterspan/filterspan_test.go +++ b/internal/processor/filterspan/filterspan_test.go @@ -21,8 +21,11 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/consumer/pdata" + "go.opentelemetry.io/collector/internal/data/testdata" "go.opentelemetry.io/collector/internal/processor/filterconfig" + "go.opentelemetry.io/collector/internal/processor/filterhelper" "go.opentelemetry.io/collector/internal/processor/filterset" + "go.opentelemetry.io/collector/translator/conventions" ) func createConfig(matchType filterset.MatchType) *filterset.Config { @@ -32,6 +35,7 @@ func createConfig(matchType filterset.MatchType) *filterset.Config { } func TestSpan_validateMatchesConfiguration_InvalidConfig(t *testing.T) { + version := "[" testcases := []struct { name string property filterconfig.MatchProperties @@ -72,17 +76,27 @@ func TestSpan_validateMatchesConfiguration_InvalidConfig(t *testing.T) { errorString: "error creating service name filters: unrecognized match_type: '', valid types are: [regexp strict]", }, { - name: "regexp_match_type_for_attributes", + name: "regexp_match_type_for_int_attribute", property: filterconfig.MatchProperties{ Config: *createConfig(filterset.Regexp), Attributes: []filterconfig.Attribute{ - {Key: "key", Value: "value"}, + {Key: "key", Value: 1}, }, }, - errorString: `match_type=regexp is not supported for "attributes"`, + errorString: `error creating attribute filters: match_type=regexp for "key" only supports STRING, but found INT`, }, { - name: "invalid_regexp_pattern", + name: "unknown_attribute_value", + property: filterconfig.MatchProperties{ + Config: *createConfig(filterset.Strict), + Attributes: []filterconfig.Attribute{ + {Key: "key", Value: []string{}}, + }, + }, + errorString: `error creating attribute filters: error unsupported value type "[]string"`, + }, + { + name: "invalid_regexp_pattern_service", property: filterconfig.MatchProperties{ Config: *createConfig(filterset.Regexp), Services: []string{"["}, @@ -90,13 +104,46 @@ func TestSpan_validateMatchesConfiguration_InvalidConfig(t *testing.T) { errorString: "error creating service name filters: error parsing regexp: missing closing ]: `[`", }, { - name: "invalid_regexp_pattern2", + name: "invalid_regexp_pattern_span", property: filterconfig.MatchProperties{ Config: *createConfig(filterset.Regexp), SpanNames: []string{"["}, }, errorString: "error creating span name filters: error parsing regexp: missing closing ]: `[`", }, + { + name: "invalid_regexp_pattern_attribute", + property: filterconfig.MatchProperties{ + Config: *createConfig(filterset.Regexp), + SpanNames: []string{"["}, + Attributes: []filterconfig.Attribute{{Key: "key", Value: "["}}, + }, + errorString: "error creating attribute filters: error parsing regexp: missing closing ]: `[`", + }, + { + name: "invalid_regexp_pattern_resource", + property: filterconfig.MatchProperties{ + Config: *createConfig(filterset.Regexp), + Resources: []filterconfig.Attribute{{Key: "key", Value: "["}}, + }, + errorString: "error creating resource filters: error parsing regexp: missing closing ]: `[`", + }, + { + name: "invalid_regexp_pattern_library_name", + property: filterconfig.MatchProperties{ + Config: *createConfig(filterset.Regexp), + Libraries: []filterconfig.InstrumentationLibrary{{Name: "["}}, + }, + errorString: "error creating library name filters: error parsing regexp: missing closing ]: `[`", + }, + { + name: "invalid_regexp_pattern_library_version", + property: filterconfig.MatchProperties{ + Config: *createConfig(filterset.Regexp), + Libraries: []filterconfig.InstrumentationLibrary{{Name: "lib", Version: &version}}, + }, + errorString: "error creating library version filters: error parsing regexp: missing closing ]: `[`", + }, { name: "empty_key_name_in_attributes_list", property: filterconfig.MatchProperties{ @@ -108,20 +155,20 @@ func TestSpan_validateMatchesConfiguration_InvalidConfig(t *testing.T) { }, }, }, - errorString: "error creating processor. Can't have empty key in the list of attributes", + errorString: "error creating attribute filters: error creating processor. Can't have empty key in the list of attributes", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { output, err := NewMatcher(&tc.property) assert.Nil(t, output) - require.NotNil(t, err) - assert.Equal(t, tc.errorString, err.Error()) + assert.EqualError(t, err, tc.errorString) }) } } func TestSpan_Matching_False(t *testing.T) { + version := "wrong" testcases := []struct { name string properties *filterconfig.MatchProperties @@ -167,7 +214,24 @@ func TestSpan_Matching_False(t *testing.T) { }, { - name: "wrong_property_value", + name: "wrong_library_name", + properties: &filterconfig.MatchProperties{ + Config: *createConfig(filterset.Strict), + Services: []string{}, + Libraries: []filterconfig.InstrumentationLibrary{{Name: "wrong"}}, + }, + }, + { + name: "wrong_library_version", + properties: &filterconfig.MatchProperties{ + Config: *createConfig(filterset.Strict), + Services: []string{}, + Libraries: []filterconfig.InstrumentationLibrary{{Name: "lib", Version: &version}}, + }, + }, + + { + name: "wrong_attribute_value", properties: &filterconfig.MatchProperties{ Config: *createConfig(filterset.Strict), Services: []string{}, @@ -180,7 +244,20 @@ func TestSpan_Matching_False(t *testing.T) { }, }, { - name: "incompatible_property_value", + name: "wrong_resource_value", + properties: &filterconfig.MatchProperties{ + Config: *createConfig(filterset.Strict), + Services: []string{}, + Resources: []filterconfig.Attribute{ + { + Key: "keyInt", + Value: 1234, + }, + }, + }, + }, + { + name: "incompatible_attribute_value", properties: &filterconfig.MatchProperties{ Config: *createConfig(filterset.Strict), Services: []string{}, @@ -192,6 +269,19 @@ func TestSpan_Matching_False(t *testing.T) { }, }, }, + { + name: "unsupported_attribute_value", + properties: &filterconfig.MatchProperties{ + Config: *createConfig(filterset.Regexp), + Services: []string{}, + Attributes: []filterconfig.Attribute{ + { + Key: "keyMap", + Value: "123", + }, + }, + }, + }, { name: "property_key_does_not_exist", properties: &filterconfig.MatchProperties{ @@ -210,18 +300,34 @@ func TestSpan_Matching_False(t *testing.T) { span := pdata.NewSpan() span.InitEmpty() span.SetName("spanName") - span.Attributes().InitFromMap(map[string]pdata.AttributeValue{"keyInt": pdata.NewAttributeValueInt(123)}) + span.Attributes().InitFromMap(map[string]pdata.AttributeValue{ + "keyInt": pdata.NewAttributeValueInt(123), + "keyMap": pdata.NewAttributeValueMap(), + }) + + library := pdata.NewInstrumentationLibrary() + library.InitEmpty() + library.SetName("lib") + library.SetVersion("ver") + for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { matcher, err := NewMatcher(tc.properties) - assert.Nil(t, err) + require.NoError(t, err) assert.NotNil(t, matcher) - assert.False(t, matcher.MatchSpan(span, "wrongSvc")) + assert.False(t, matcher.MatchSpan(span, resource("wrongSvc"), library)) }) } } +func resource(service string) pdata.Resource { + r := pdata.NewResource() + r.InitEmpty() + r.Attributes().InitFromMap(map[string]pdata.AttributeValue{conventions.AttributeServiceName: pdata.NewAttributeValueString(service)}) + return r +} + func TestSpan_MatchingCornerCases(t *testing.T) { cfg := &filterconfig.MatchProperties{ Config: *createConfig(filterset.Strict), @@ -240,7 +346,7 @@ func TestSpan_MatchingCornerCases(t *testing.T) { emptySpan := pdata.NewSpan() emptySpan.InitEmpty() - assert.False(t, mp.MatchSpan(emptySpan, "svcA")) + assert.False(t, mp.MatchSpan(emptySpan, resource("svcA"), pdata.NewInstrumentationLibrary())) } func TestSpan_MissingServiceName(t *testing.T) { @@ -255,10 +361,12 @@ func TestSpan_MissingServiceName(t *testing.T) { emptySpan := pdata.NewSpan() emptySpan.InitEmpty() - assert.False(t, mp.MatchSpan(emptySpan, "")) + assert.False(t, mp.MatchSpan(emptySpan, resource(""), pdata.NewInstrumentationLibrary())) } func TestSpan_Matching_True(t *testing.T) { + ver := "v.*" + testcases := []struct { name string properties *filterconfig.MatchProperties @@ -279,6 +387,22 @@ func TestSpan_Matching_True(t *testing.T) { Attributes: []filterconfig.Attribute{}, }, }, + { + name: "library_match", + properties: &filterconfig.MatchProperties{ + Config: *createConfig(filterset.Regexp), + Libraries: []filterconfig.InstrumentationLibrary{{Name: "li.*"}}, + Attributes: []filterconfig.Attribute{}, + }, + }, + { + name: "library_match_with_version", + properties: &filterconfig.MatchProperties{ + Config: *createConfig(filterset.Regexp), + Libraries: []filterconfig.InstrumentationLibrary{{Name: "li.*", Version: &ver}}, + Attributes: []filterconfig.Attribute{}, + }, + }, { name: "span_name_match", properties: &filterconfig.MatchProperties{ @@ -301,7 +425,7 @@ func TestSpan_Matching_True(t *testing.T) { }, }, { - name: "property_exact_value_match", + name: "attribute_exact_value_match", properties: &filterconfig.MatchProperties{ Config: *createConfig(filterset.Strict), Services: []string{}, @@ -325,6 +449,42 @@ func TestSpan_Matching_True(t *testing.T) { }, }, }, + { + name: "attribute_regex_value_match", + properties: &filterconfig.MatchProperties{ + Config: *createConfig(filterset.Regexp), + Attributes: []filterconfig.Attribute{ + { + Key: "keyString", + Value: "arith.*", + }, + { + Key: "keyInt", + Value: "12.*", + }, + { + Key: "keyDouble", + Value: "324.*", + }, + { + Key: "keyBool", + Value: "tr.*", + }, + }, + }, + }, + { + name: "resource_exact_value_match", + properties: &filterconfig.MatchProperties{ + Config: *createConfig(filterset.Strict), + Resources: []filterconfig.Attribute{ + { + Key: "keyString", + Value: "arithmetic", + }, + }, + }, + }, { name: "property_exists", properties: &filterconfig.MatchProperties{ @@ -367,15 +527,27 @@ func TestSpan_Matching_True(t *testing.T) { "keyBool": pdata.NewAttributeValueBool(true), "keyExists": pdata.NewAttributeValueString("present"), }) + assert.NotNil(t, span) + + resource := pdata.NewResource() + resource.InitEmpty() + resource.Attributes().InitFromMap(map[string]pdata.AttributeValue{ + conventions.AttributeServiceName: pdata.NewAttributeValueString("svcA"), + "keyString": pdata.NewAttributeValueString("arithmetic"), + }) + + library := pdata.NewInstrumentationLibrary() + library.InitEmpty() + library.SetName("lib") + library.SetVersion("ver") for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mp, err := NewMatcher(tc.properties) - assert.Nil(t, err) + require.NoError(t, err) assert.NotNil(t, mp) - assert.NotNil(t, span) - assert.True(t, mp.MatchSpan(span, "svcA")) + assert.True(t, mp.MatchSpan(span, resource, library)) }) } } @@ -401,7 +573,7 @@ func TestSpan_validateMatchesConfigurationForAttributes(t *testing.T) { }, }, output: &propertiesMatcher{ - Attributes: []attributeMatcher{ + Attributes: []filterhelper.AttributeMatcher{ { Key: "key1", }, @@ -428,7 +600,7 @@ func TestSpan_validateMatchesConfigurationForAttributes(t *testing.T) { }, }, output: &propertiesMatcher{ - Attributes: []attributeMatcher{ + Attributes: []filterhelper.AttributeMatcher{ { Key: "key1", }, @@ -453,3 +625,15 @@ func newAttributeValueInt(v int64) *pdata.AttributeValue { attr := pdata.NewAttributeValueInt(v) return &attr } + +func TestServiceNameForResource(t *testing.T) { + td := testdata.GenerateTraceDataOneSpanNoResource() + require.Equal(t, serviceNameForResource(td.ResourceSpans().At(0).Resource()), "") + + td = testdata.GenerateTraceDataOneSpan() + resource := td.ResourceSpans().At(0).Resource() + require.Equal(t, serviceNameForResource(resource), "") + + resource.Attributes().InsertString(conventions.AttributeServiceName, "test-service") + require.Equal(t, serviceNameForResource(resource), "test-service") +} diff --git a/processor/attributesprocessor/attributes.go b/processor/attributesprocessor/attributes.go index 273c8930b5c..34ef35f55cb 100644 --- a/processor/attributesprocessor/attributes.go +++ b/processor/attributesprocessor/attributes.go @@ -19,7 +19,6 @@ import ( "go.opentelemetry.io/collector/consumer/pdata" "go.opentelemetry.io/collector/internal/processor/filterspan" - "go.opentelemetry.io/collector/processor" "go.opentelemetry.io/collector/processor/processorhelper" ) @@ -48,7 +47,7 @@ func (a *attributesProcessor) ProcessTraces(_ context.Context, td pdata.Traces) if rs.IsNil() { continue } - serviceName := processor.ServiceNameForResource(rs.Resource()) + resource := rs.Resource() ilss := rss.At(i).InstrumentationLibrarySpans() for j := 0; j < ilss.Len(); j++ { ils := ilss.At(j) @@ -56,6 +55,7 @@ func (a *attributesProcessor) ProcessTraces(_ context.Context, td pdata.Traces) continue } spans := ils.Spans() + library := ils.InstrumentationLibrary() for k := 0; k < spans.Len(); k++ { span := spans.At(k) if span.IsNil() { @@ -63,7 +63,7 @@ func (a *attributesProcessor) ProcessTraces(_ context.Context, td pdata.Traces) continue } - if a.skipSpan(span, serviceName) { + if filterspan.SkipSpan(a.include, a.exclude, span, resource, library) { continue } @@ -73,27 +73,3 @@ func (a *attributesProcessor) ProcessTraces(_ context.Context, td pdata.Traces) } return td, nil } - -// skipSpan determines if a span should be processed. -// True is returned when a span should be skipped. -// False is returned when a span should not be skipped. -// The logic determining if a span should be processed is set -// in the attribute configuration with the include and exclude settings. -// Include properties are checked before exclude settings are checked. -func (a *attributesProcessor) skipSpan(span pdata.Span, serviceName string) bool { - if a.include != nil { - // A false returned in this case means the span should not be processed. - if include := a.include.MatchSpan(span, serviceName); !include { - return true - } - } - - if a.exclude != nil { - // A true returned in this case means the span should not be processed. - if exclude := a.exclude.MatchSpan(span, serviceName); exclude { - return true - } - } - - return false -} diff --git a/processor/metrics.go b/processor/metrics.go index 3530f4b353e..f319ec392d7 100644 --- a/processor/metrics.go +++ b/processor/metrics.go @@ -148,21 +148,6 @@ func ServiceNameForNode(node *commonpb.Node) string { } } -// ServiceNameForResource gets the service name for a specified Resource. -// TODO: Find a better package for this function. -func ServiceNameForResource(resource pdata.Resource) string { - if resource.IsNil() { - return "" - } - - service, found := resource.Attributes().Get(conventions.AttributeServiceName) - if !found { - return "" - } - - return service.StringVal() -} - // RecordsSpanCountMetrics reports span count metrics for specified measure. func RecordsSpanCountMetrics(ctx context.Context, scm *SpanCountStats, measure *stats.Int64Measure) { if scm.isDetailed { diff --git a/processor/metrics_test.go b/processor/metrics_test.go index 08a3f9b7b6e..61fe4e70f21 100644 --- a/processor/metrics_test.go +++ b/processor/metrics_test.go @@ -20,21 +20,8 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/internal/data/testdata" - "go.opentelemetry.io/collector/translator/conventions" ) -func TestServiceNameForResource(t *testing.T) { - td := testdata.GenerateTraceDataOneSpanNoResource() - require.Equal(t, ServiceNameForResource(td.ResourceSpans().At(0).Resource()), "") - - td = testdata.GenerateTraceDataOneSpan() - resource := td.ResourceSpans().At(0).Resource() - require.Equal(t, ServiceNameForResource(resource), "") - - resource.Attributes().InsertString(conventions.AttributeServiceName, "test-service") - require.Equal(t, ServiceNameForResource(resource), "test-service") -} - func TestSpanCountByResourceStringAttribute(t *testing.T) { td := testdata.GenerateTraceDataEmpty() require.EqualValues(t, 0, len(spanCountByResourceStringAttribute(td, "resource-attr"))) diff --git a/processor/spanprocessor/span.go b/processor/spanprocessor/span.go index c86a3220507..386e47999d8 100644 --- a/processor/spanprocessor/span.go +++ b/processor/spanprocessor/span.go @@ -23,7 +23,6 @@ import ( "go.opentelemetry.io/collector/consumer/pdata" "go.opentelemetry.io/collector/internal/processor/filterspan" - "go.opentelemetry.io/collector/processor" ) type spanProcessor struct { @@ -87,21 +86,22 @@ func (sp *spanProcessor) ProcessTraces(_ context.Context, td pdata.Traces) (pdat if rs.IsNil() { continue } - serviceName := processor.ServiceNameForResource(rs.Resource()) - ilss := rss.At(i).InstrumentationLibrarySpans() + ilss := rs.InstrumentationLibrarySpans() + resource := rs.Resource() for j := 0; j < ilss.Len(); j++ { ils := ilss.At(j) if ils.IsNil() { continue } spans := ils.Spans() + library := ils.InstrumentationLibrary() for k := 0; k < spans.Len(); k++ { s := spans.At(k) if s.IsNil() { continue } - if sp.skipSpan(s, serviceName) { + if filterspan.SkipSpan(sp.include, sp.exclude, s, resource, library) { continue } sp.processFromAttributes(s) @@ -229,27 +229,3 @@ func (sp *spanProcessor) processToAttributes(span pdata.Span) { } } } - -// skipSpan determines if a span should be processed. -// True is returned when a span should be skipped. -// False is returned when a span should not be skipped. -// The logic determining if a span should be processed is set -// in the attribute configuration with the include and exclude settings. -// Include properties are checked before exclude settings are checked. -func (sp *spanProcessor) skipSpan(span pdata.Span, serviceName string) bool { - if sp.include != nil { - // A false returned in this case means the span should not be processed. - if include := sp.include.MatchSpan(span, serviceName); !include { - return true - } - } - - if sp.exclude != nil { - // A true returned in this case means the span should not be processed. - if exclude := sp.exclude.MatchSpan(span, serviceName); exclude { - return true - } - } - - return false -}