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
  • Loading branch information
zeitlinger committed Aug 27, 2020
1 parent eb0896e commit 044f5a4
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 128 deletions.
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
8 changes: 5 additions & 3 deletions internal/processor/filterspan/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ type MatchProperties struct {
// This is an optional field.
Services []string `mapstructure:"services"`

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

// SpanNames specify the list of items to match span name against.
// A match occurs if the span name matches at least one item in this list.
// This is an optional field.
Expand All @@ -88,9 +93,6 @@ type MatchProperties struct {
Attributes []Attribute `mapstructure:"attributes"`
}

// MatchTypeFieldName is the mapstructure field name for MatchProperties.Attributes field.
const AttributesFieldName = "attributes"

// Attribute specifies the attribute key and optional value to match against.
type Attribute struct {
// Key specifies the attribute key.
Expand Down
139 changes: 112 additions & 27 deletions internal/processor/filterspan/filterspan.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,29 @@ package filterspan
import (
"errors"
"fmt"
"strconv"

"go.opentelemetry.io/collector/consumer/pdata"
"go.opentelemetry.io/collector/internal/processor/filterhelper"
"go.opentelemetry.io/collector/internal/processor/filterset"
"go.opentelemetry.io/collector/translator/conventions"
)

var (
// TODO Add processor type invoking the NewMatcher in error text.
errAtLeastOneMatchFieldNeeded = errors.New(
`error creating processor. At least one ` +
`of "services", "span_names" or "attributes" field must be specified"`)

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

// TODO: Modify Matcher to invoke both the include and exclude properties so
// calling processors will always have the same logic.
// 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) bool
}

// propertiesMatcher allows matching a span against various span properties.
Expand All @@ -48,15 +52,18 @@ type propertiesMatcher struct {

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

Resources 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.
// If both AttributeValue and StringFilter are nil only check for key existence.
AttributeValue *pdata.AttributeValue
StringFilter filterset.FilterSet
}

// NewMatcher creates a span Matcher that matches based on the given MatchProperties.
Expand All @@ -65,17 +72,25 @@ func NewMatcher(mp *MatchProperties) (Matcher, error) {
return nil, nil
}

if len(mp.Services) == 0 && len(mp.SpanNames) == 0 && len(mp.Attributes) == 0 {
if len(mp.Services) == 0 && len(mp.SpanNames) == 0 && len(mp.Attributes) == 0 && len(mp.Resources) == 0 {
return nil, errAtLeastOneMatchFieldNeeded
}

var err error

var am attributesMatcher
if len(mp.Attributes) > 0 {
am, err = newAttributesMatcher(mp)
am, err = newAttributesMatcher(mp.Config, mp.Attributes)
if err != nil {
return nil, fmt.Errorf("error creating attribute filters: %v", err)
}
}

var rm attributesMatcher
if len(mp.Resources) > 0 {
rm, err = newAttributesMatcher(mp.Config, mp.Resources)
if err != nil {
return nil, err
return nil, fmt.Errorf("error creating resource filters: %v", err)
}
}

Expand All @@ -99,21 +114,14 @@ func NewMatcher(mp *MatchProperties) (Matcher, error) {
serviceFilters: serviceFS,
nameFilters: nameFS,
Attributes: am,
Resources: rm,
}, nil
}

func newAttributesMatcher(mp *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, AttributesFieldName,
)
}

func newAttributesMatcher(config filterset.Config, attributes []Attribute) (attributesMatcher, error) {
// Convert attribute values from mp representation to in-memory representation.
var rawAttributes []attributeMatcher
for _, attribute := range mp.Attributes {
for _, attribute := range attributes {

if attribute.Key == "" {
return nil, errors.New("error creating processor. Can't have empty key in the list of attributes")
Expand All @@ -127,35 +135,95 @@ func newAttributesMatcher(mp *MatchProperties) (attributesMatcher, error) {
if err != nil {
return nil, err
}
entry.AttributeValue = &val

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
}

// 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) bool {
if include != nil {
// A false returned in this case means the span should not be processed.
if i := include.MatchSpan(span, resource); !i {
return true
}
}

if exclude != nil {
// A true returned in this case means the span should not be processed.
if e := exclude.MatchSpan(span, resource); e {
return true
}
}

return false
}

// MatchSpan matches a span and service to a set of properties.
// There are 3 sets of properties to match against.
// The service name is checked first, if specified. Then span names are matched, if specified.
// The attributes are checked last, if specified.
// 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) 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.
if mp.Resources != nil && !mp.Resources.match(span) {
return false
}

return mp.Attributes.match(span)
}

// serviceNameForResource gets the service name for a specified Resource.
func serviceNameForResource(resource pdata.Resource) string {
if resource.IsNil() {
return "<nil-resource>"
}

service, found := resource.Attributes().Get(conventions.AttributeServiceName)
if !found {
return "<nil-service-name>"
}

return service.StringVal()
}

// 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.
Expand All @@ -177,14 +245,31 @@ func (ma attributesMatcher) match(span pdata.Span) bool {
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
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
}
}
Loading

0 comments on commit 044f5a4

Please sign in to comment.