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 May 29, 2020
1 parent d106676 commit 4073594
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 69 deletions.
14 changes: 14 additions & 0 deletions internal/processor/filterspan/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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 []Resource `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 @@ -98,6 +103,7 @@ const MatchTypeFieldName = "match_type"

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

// Attribute specifies the attribute key and optional value to match against.
type Attribute struct {
Expand All @@ -108,3 +114,11 @@ type Attribute struct {
// If it is not set, any value will match.
Value interface{} `mapstructure:"value"`
}

type Resource struct {
// Key specifies the resource key to match.
Key string `mapstructure:"key"`

// Values specify the list of items to match span name against.
Values []interface{} `mapstructure:"value"`
}
168 changes: 144 additions & 24 deletions internal/processor/filterspan/filterspan.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
"errors"
"fmt"
"regexp"
"strconv"

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

var (
Expand All @@ -38,7 +40,7 @@ var (
// 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
}

type attributesMatcher []attributeMatcher
Expand All @@ -49,6 +51,8 @@ type strictPropertiesMatcher struct {
// Service names to compare to.
Services []string

Resources attributesMatcher

// Span names to compare to.
SpanNames []string

Expand All @@ -62,6 +66,8 @@ type regexpPropertiesMatcher struct {
// Precompiled service name regexp-es.
Services []*regexp.Regexp

Resources attributesMatcher

// Precompiled span name regexp-es.
SpanNames []*regexp.Regexp

Expand All @@ -74,6 +80,8 @@ type attributeMatcher struct {
Key string
// If nil only check for key existence.
AttributeValue *pdata.AttributeValue
// Alternatively to AttributeValue match against a string version using a regular expression.
AttributeValueRegexp *regexp.Regexp
}

func NewMatcher(config *MatchProperties) (Matcher, error) {
Expand Down Expand Up @@ -108,6 +116,19 @@ func newStrictPropertiesMatcher(config *MatchProperties) (*strictPropertiesMatch
SpanNames: config.SpanNames,
}

for _, resource := range config.Resources {
for _, value := range resource.Values {
val, err := filterhelper.NewAttributeValueRaw(value)
if err != nil {
return nil, err
}
properties.Resources = append(properties.Resources, attributeMatcher{
Key: resource.Key,
AttributeValue: &val,
})
}
}

var err error
properties.Attributes, err = newAttributesMatcher(config)
if err != nil {
Expand All @@ -132,6 +153,19 @@ func newRegexpPropertiesMatcher(config *MatchProperties) (*regexpPropertiesMatch
properties.Services = append(properties.Services, g)
}

for _, resource := range config.Resources {
for _, value := range resource.Values {
r, err := regexpAttributeValue(value, ResourcesFieldName)
if err != nil {
return nil, err
}
properties.Resources = append(properties.Resources, attributeMatcher{
Key: resource.Key,
AttributeValueRegexp: r,
})
}
}

// Precompile SpanNames regexp patterns.
for _, pattern := range config.SpanNames {
g, err := regexp.Compile(pattern)
Expand All @@ -144,16 +178,42 @@ func newRegexpPropertiesMatcher(config *MatchProperties) (*regexpPropertiesMatch
properties.SpanNames = append(properties.SpanNames, g)
}

if len(config.Attributes) > 0 {
return nil, fmt.Errorf(
"%s=%s is not supported for %q",
MatchTypeFieldName, MatchTypeRegexp, AttributesFieldName,
)
for _, attribute := range config.Attributes {
r, err := regexpAttributeValue(attribute.Value, AttributesFieldName)
if err != nil {
return nil, err
}

properties.Attributes = append(properties.Attributes, attributeMatcher{
Key: attribute.Key,
AttributeValueRegexp: r,
})
}

return properties, nil
}

func regexpAttributeValue(value interface{}, fieldName string) (*regexp.Regexp, error) {
val, err := filterhelper.NewAttributeValueRaw(value)
if err != nil {
return nil, err
}
if val.Type() != pdata.AttributeValueSTRING {
return nil, fmt.Errorf(
"%s=%s for %q only supports STRING, but found %s",
MatchTypeFieldName, MatchTypeRegexp, fieldName, val.Type(),
)
}
r, err := regexp.Compile(val.StringVal())
if err != nil {
return nil, fmt.Errorf(
"error creating processor. %s is not a valid attribute value regexp pattern",
val.StringVal(),
)
}
return r, nil
}

func newAttributesMatcher(config *MatchProperties) (attributesMatcher, error) {
// Convert attribute values from config representation to in-memory representation.
var rawAttributes []attributeMatcher
Expand Down Expand Up @@ -185,17 +245,17 @@ func newAttributesMatcher(config *MatchProperties) (attributesMatcher, error) {
// 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, serviceName string) bool {
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, serviceName); !i {
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, serviceName); e {
if e := exclude.MatchSpan(span, resource); e {
return true
}
}
Expand All @@ -210,12 +270,14 @@ func SkipSpan(include Matcher, exclude Matcher, span pdata.Span, serviceName str
// 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 *strictPropertiesMatcher) MatchSpan(span pdata.Span, serviceName string) bool {
func (mp *strictPropertiesMatcher) MatchSpan(span pdata.Span, resource pdata.Resource) bool {
if len(mp.Services) > 0 {
service := serviceNameForResource(resource)

// Verify service name matches at least one of the items.
matched := false
for _, item := range mp.Services {
if item == serviceName {
if item == service {
matched = true
break
}
Expand All @@ -225,6 +287,13 @@ func (mp *strictPropertiesMatcher) MatchSpan(span pdata.Span, serviceName string
}
}

if len(mp.Resources) > 0 {
// Verify resource matches at least one of the items.
if !mp.Resources.match(resource.Attributes(), false) {
return false
}
}

if len(mp.SpanNames) > 0 {
// SpanNames condition is specified. Check if span name matches the condition.
spanName := span.Name()
Expand All @@ -243,7 +312,7 @@ func (mp *strictPropertiesMatcher) MatchSpan(span pdata.Span, serviceName string
}

// Service name and span name matched. Now match attributes.
return mp.Attributes.match(span)
return mp.Attributes.match(span.Attributes(), true)
}

// MatchSpan matches a span and service to a set of properties.
Expand All @@ -253,13 +322,15 @@ func (mp *strictPropertiesMatcher) MatchSpan(span pdata.Span, serviceName string
// 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 *regexpPropertiesMatcher) MatchSpan(span pdata.Span, serviceName string) bool {
func (mp *regexpPropertiesMatcher) MatchSpan(span pdata.Span, resource pdata.Resource) bool {

if len(mp.Services) > 0 {
service := serviceNameForResource(resource)

// Verify service name matches at least one of the regexp patterns.
matched := false
for _, re := range mp.Services {
if re.MatchString(serviceName) {
if re.MatchString(service) {
matched = true
break
}
Expand All @@ -269,6 +340,12 @@ func (mp *regexpPropertiesMatcher) MatchSpan(span pdata.Span, serviceName string
}
}

if len(mp.Resources) > 0 {
if !mp.Resources.match(resource.Attributes(), false) {
return false
}
}

if len(mp.SpanNames) > 0 {
// SpanNames condition is specified. Check if span name matches the condition.
spanName := span.Name()
Expand All @@ -287,17 +364,16 @@ func (mp *regexpPropertiesMatcher) MatchSpan(span pdata.Span, serviceName string
}

// Service name and span name matched. Now match attributes.
return mp.Attributes.match(span)
return mp.Attributes.match(span.Attributes(), true)
}

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

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 {
Expand All @@ -306,19 +382,63 @@ func (ma attributesMatcher) match(span pdata.Span) bool {

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

// This is for the case of checking that the key existed.
if property.AttributeValue == nil {
continue
func (m attributeMatcher) match(attrs pdata.AttributeMap) bool {
attr, exist := attrs.Get(m.Key)
if !exist {
return false
}

// This is for the case of checking that the key existed.
if m.AttributeValue != nil {
if !attr.Equal(*m.AttributeValue) {
return false
}
}

if !attr.Equal(*property.AttributeValue) {
if m.AttributeValueRegexp != nil {
var s string
switch attr.Type() {
case pdata.AttributeValueSTRING:
s = attr.StringVal()
case pdata.AttributeValueBOOL:
s = strconv.FormatBool(attr.BoolVal())
case pdata.AttributeValueDOUBLE:
s = strconv.FormatFloat(attr.DoubleVal(), 'f', -1, 64)
case pdata.AttributeValueINT:
s = strconv.FormatInt(attr.IntVal(), 10)
default:
//TODO return error?
//return fmt.Sprintf("<Unknown OpenTelemetry attribute value type %q>", attr.Type())
}
if s != "" && !m.AttributeValueRegexp.MatchString(s) {
return false
}
}
return true
}

// 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()
}
Loading

0 comments on commit 4073594

Please sign in to comment.