diff --git a/CHANGELOG.md b/CHANGELOG.md index 80da7fb1e79..ffd0a424181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## main / unreleased +* [ENHANCEMENT] Introduced `AttributePolicyMatch` & `IntrinsicPolicyMatch` structures to match span attributes based on strongly typed values & precompiled regexp [#3025](https://github.com/grafana/tempo/pull/3025) (@andriusluk) * [CHANGE] TraceQL/Structural operators performance improvement. [#3088](https://github.com/grafana/tempo/pull/3088) (@joe-elliott) * [FEATURE] Introduce list_blocks_concurrency on GCS and S3 backends to control backend load and performance. [#2652](https://github.com/grafana/tempo/pull/2652) (@zalegrala) * [BUGFIX] Include statusMessage intrinsic attribute in tag search. [#3084](https://github.com/grafana/tempo/pull/3084) (@rcrowe) diff --git a/pkg/spanfilter/config/config.go b/pkg/spanfilter/config/config.go index 025d925780a..02a4b7414ac 100644 --- a/pkg/spanfilter/config/config.go +++ b/pkg/spanfilter/config/config.go @@ -72,7 +72,7 @@ func ValidatePolicyMatch(match *PolicyMatch) error { switch a.Intrinsic { case traceql.IntrinsicKind, traceql.IntrinsicName, traceql.IntrinsicStatus: // currently supported default: - return fmt.Errorf("currently unsupported intrinsic: %s; supported intrinsics: %q", a.Intrinsic, supportedIntrinsics) + return fmt.Errorf("unsupported intrinsic: %s; supported intrinsics: %q", a.Intrinsic, supportedIntrinsics) } } } diff --git a/pkg/spanfilter/policymatch/attribute.go b/pkg/spanfilter/policymatch/attribute.go new file mode 100644 index 00000000000..b292cd15b96 --- /dev/null +++ b/pkg/spanfilter/policymatch/attribute.go @@ -0,0 +1,151 @@ +package policymatch + +import ( + "fmt" + "regexp" + + "github.com/grafana/tempo/pkg/spanfilter/config" + commonv1 "github.com/grafana/tempo/pkg/tempopb/common/v1" +) + +// AttributePolicyMatch is a set of attribute filters that must match a span for the span to match the policy. +type AttributePolicyMatch struct { + filters []AttributeFilter +} + +// NewAttributePolicyMatch returns a new AttributePolicyMatch with the given filters. If no filters are given, then the policy matches all spans. +func NewAttributePolicyMatch(filters []AttributeFilter) *AttributePolicyMatch { + return &AttributePolicyMatch{filters: filters} +} + +// Matches returns true if the given span matches the policy. +func (p *AttributePolicyMatch) Matches(attrs []*commonv1.KeyValue) bool { + // If there are no filters, then the span matches. + if len(p.filters) == 0 { + return true + } + + // If there are no attributes, then the span does not match. + if len(attrs) == 0 { + return false + } + + for _, pa := range p.filters { + if !matchesAnyFilter(pa, attrs) { + return false + } + } + + return true +} + +func matchesAnyFilter(pa AttributeFilter, attrs []*commonv1.KeyValue) bool { + for _, attr := range attrs { + // If the attribute key does not match, then it cannot match the policy. + if pa.key != attr.Key { + continue + } + switch pa.typ { + case StringAttributeFilter: + return pa.stringValue == attr.Value.GetStringValue() + case Int64AttributeFilter: + return pa.int64Value == attr.Value.GetIntValue() + case Float64AttributeFilter: + return pa.float64Value == attr.Value.GetDoubleValue() + case BoolAttributeFilter: + return pa.boolValue == attr.Value.GetBoolValue() + case RegexAttributeFilter: + return pa.regex.MatchString(attr.Value.GetStringValue()) + } + } + return false +} + +type AttributeFilterType int + +const ( + StringAttributeFilter AttributeFilterType = iota + Int64AttributeFilter + Float64AttributeFilter + BoolAttributeFilter + RegexAttributeFilter +) + +// AttributeFilter is a filter that matches spans based on their attributes. +type AttributeFilter struct { + key string + typ AttributeFilterType + stringValue string + int64Value int64 + float64Value float64 + boolValue bool + regex *regexp.Regexp +} + +// NewAttributeFilter returns a new AttributeFilter based on the match type. +func NewAttributeFilter(matchType config.MatchType, key string, value interface{}) (AttributeFilter, error) { + if matchType == config.Regex { + return NewRegexpAttributeFilter(key, value) + } + return NewStrictAttributeFilter(key, value) +} + +// NewStrictAttributeFilter returns a new AttributeFilter that matches against the given value. +func NewStrictAttributeFilter(key string, value interface{}) (AttributeFilter, error) { + switch v := value.(type) { + case string: + return AttributeFilter{ + key: key, + typ: StringAttributeFilter, + stringValue: v, + }, nil + case int: + return AttributeFilter{ + key: key, + typ: Int64AttributeFilter, + int64Value: int64(v), + }, nil + case int64: + return AttributeFilter{ + key: key, + typ: Int64AttributeFilter, + int64Value: v, + }, nil + case float64: + return AttributeFilter{ + key: key, + typ: Float64AttributeFilter, + float64Value: v, + }, nil + case bool: + return AttributeFilter{ + key: key, + typ: BoolAttributeFilter, + boolValue: v, + }, nil + default: + return AttributeFilter{}, fmt.Errorf("value type not supported: %T", value) + } +} + +// NewRegexpAttributeFilter returns a new AttributeFilter that matches against the given regex value. +func NewRegexpAttributeFilter(key string, regex interface{}) (AttributeFilter, error) { + filter := AttributeFilter{ + key: key, + typ: RegexAttributeFilter, + } + if stringValue, ok := regex.(string); ok { + compiled, err := regexp.Compile(stringValue) + if err != nil { + return filter, fmt.Errorf("invalid attribute filter regexp: %v", err) + } + filter.regex = compiled + } + if regexpValue, ok := regex.(*regexp.Regexp); ok { + filter.regex = regexpValue + } + if filter.regex == nil { + return filter, fmt.Errorf("invalid attribute filter regexp value: %v", regex) + } + return filter, nil +} diff --git a/pkg/spanfilter/policymatch/attribute_test.go b/pkg/spanfilter/policymatch/attribute_test.go new file mode 100644 index 00000000000..c8e34dace70 --- /dev/null +++ b/pkg/spanfilter/policymatch/attribute_test.go @@ -0,0 +1,358 @@ +package policymatch + +import ( + "testing" + + commonv1 "github.com/grafana/tempo/pkg/tempopb/common/v1" + "github.com/stretchr/testify/require" +) + +func Test_strictAttributeFilter_Matches(t *testing.T) { + cases := []struct { + policy *AttributePolicyMatch + attrs []*commonv1.KeyValue + expect bool + name string + }{ + { + name: "single string match", + expect: true, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("foo", "bar")), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "foo", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "bar", + }, + }, + }, + }, + }, + { + name: "multiple string match", + expect: true, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("foo", "bar")), + must(NewStrictAttributeFilter("otherfoo", "notbar")), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "foo", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "bar", + }, + }, + }, + { + Key: "otherfoo", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "notbar", + }, + }, + }, + }, + }, + { + name: "multiple string non match", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("foo", "bar")), + must(NewStrictAttributeFilter("otherfoo", "nope")), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "foo", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "bar", + }, + }, + }, + { + Key: "otherfoo", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "notbar", + }, + }, + }, + }, + }, + { + name: "combination match", + expect: true, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("one", "1")), + must(NewStrictAttributeFilter("oneone", 11)), + must(NewStrictAttributeFilter("oneonepointone", 11.1)), + must(NewStrictAttributeFilter("matching", true)), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "one", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "1", + }, + }, + }, + { + Key: "oneone", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_IntValue{ + IntValue: 11, + }, + }, + }, + { + Key: "oneonepointone", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_DoubleValue{ + DoubleValue: 11.1, + }, + }, + }, + { + Key: "matching", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_BoolValue{ + BoolValue: true, + }, + }, + }, + }, + }, + { + name: "regex basic match", + expect: true, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewRegexpAttributeFilter("dd", `\d\d\w{5}`)), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "dd", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "11xxxxx", + }, + }, + }, + }, + }, + { + name: "value type mismatch string", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("dd", true)), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "dd", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "11xxxxx", + }, + }, + }, + }, + }, + { + name: "value type mismatch string/int", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("dd", "value")), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "dd", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_IntValue{ + IntValue: 11, + }, + }, + }, + }, + }, + { + name: "value type mismatch string/float", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("11", "eleven")), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "11", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_DoubleValue{ + DoubleValue: 11.1, + }, + }, + }, + }, + }, + { + name: "value type mismatch string/bool", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("11", "eleven")), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "11", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_BoolValue{ + BoolValue: false, + }, + }, + }, + }, + }, + { + name: "value type mismatch int/string", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("11", 11)), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "11", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: "11", + }, + }, + }, + }, + }, + { + name: "value mismatch int", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("11", 11)), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "11", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_IntValue{ + IntValue: 12, + }, + }, + }, + }, + }, + { + name: "value mismatch bool", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("11", true)), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "11", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_BoolValue{ + BoolValue: false, + }, + }, + }, + }, + }, + { + name: "value mismatch float", + expect: false, + policy: &AttributePolicyMatch{ + filters: []AttributeFilter{ + must(NewStrictAttributeFilter("11", 11.0)), + }, + }, + attrs: []*commonv1.KeyValue{ + { + Key: "11", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_DoubleValue{ + DoubleValue: 11.1, + }, + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r := tc.policy.Matches(tc.attrs) + require.Equal(t, tc.expect, r) + }) + } +} + +func Test_regexpAttributeFilter_Matches(t *testing.T) { + cases := []struct { + s string + pattern string + expect bool + }{ + { + s: "foo", + pattern: "foo", + expect: true, + }, + { + s: "foo", + pattern: "bar", + expect: false, + }, + } + + for _, tc := range cases { + a := NewAttributePolicyMatch([]AttributeFilter{must(NewRegexpAttributeFilter("server", tc.pattern))}) + r := a.Matches([]*commonv1.KeyValue{ + { + Key: "server", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ + StringValue: tc.s, + }, + }, + }, + }) + require.Equal(t, tc.expect, r) + } +} + +func must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} diff --git a/pkg/spanfilter/policymatch/intrinsic.go b/pkg/spanfilter/policymatch/intrinsic.go new file mode 100644 index 00000000000..e5a1898ce1f --- /dev/null +++ b/pkg/spanfilter/policymatch/intrinsic.go @@ -0,0 +1,129 @@ +package policymatch + +import ( + "fmt" + "regexp" + + tracev1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" + "github.com/grafana/tempo/pkg/traceql" +) + +// IntrinsicPolicyMatch is a set of filters that must match a span for the span to match the policy. +type IntrinsicPolicyMatch struct { + filters []IntrinsicFilter +} + +// NewIntrinsicPolicyMatch returns a new IntrinsicPolicyMatch with the given filters. If no filters are given, then the policy matches all spans. +func NewIntrinsicPolicyMatch(filters []IntrinsicFilter) *IntrinsicPolicyMatch { + return &IntrinsicPolicyMatch{filters: filters} +} + +// Matches returns true if the given span matches the policy. +func (p *IntrinsicPolicyMatch) Matches(span *tracev1.Span) bool { + if len(p.filters) == 0 { + return true + } + for _, attr := range p.filters { + if !attr.Matches(span) { + return false + } + } + return true +} + +// IntrinsicFilter is a filter that matches spans based on their intrinsic attributes. +type IntrinsicFilter struct { + intrinsic traceql.Intrinsic + name string + statusCode tracev1.Status_StatusCode + kind tracev1.Span_SpanKind + regex *regexp.Regexp +} + +// NewStrictIntrinsicFilter returns a new IntrinsicFilter that matches spans based on the given intrinsic and value. +func NewStrictIntrinsicFilter(intrinsic traceql.Intrinsic, value interface{}) (IntrinsicFilter, error) { + switch intrinsic { + case traceql.IntrinsicKind: + if v, ok := value.(string); ok { + if kind, ok := tracev1.Span_SpanKind_value[v]; ok { + return NewKindIntrinsicFilter(tracev1.Span_SpanKind(kind)), nil + } + return IntrinsicFilter{}, fmt.Errorf("unsupported kind intrinsic string value: %s", v) + } + return IntrinsicFilter{}, fmt.Errorf("invalid kind intrinsic value: %v", value) + case traceql.IntrinsicStatus: + if v, ok := value.(string); ok { + if code, ok := tracev1.Status_StatusCode_value[v]; ok { + return NewStatusIntrinsicFilter(tracev1.Status_StatusCode(code)), nil + } + return IntrinsicFilter{}, fmt.Errorf("unsupported status intrinsic string value: %s", v) + } + return IntrinsicFilter{}, fmt.Errorf("unsupported status intrinsic value: %v", value) + case traceql.IntrinsicName: + if v, ok := value.(string); ok { + return NewNameIntrinsicFilter(v), nil + } + return IntrinsicFilter{}, fmt.Errorf("unsupported name intrinsic value: %v", value) + default: + return IntrinsicFilter{}, fmt.Errorf("unsupported intrinsic: %v", intrinsic) + } +} + +// NewKindIntrinsicFilter returns a new IntrinsicFilter that matches spans with the given kind. +func NewKindIntrinsicFilter(kind tracev1.Span_SpanKind) IntrinsicFilter { + return IntrinsicFilter{intrinsic: traceql.IntrinsicKind, kind: kind} +} + +// NewStatusIntrinsicFilter returns a new IntrinsicFilter that matches spans with the given status code. +func NewStatusIntrinsicFilter(statusCode tracev1.Status_StatusCode) IntrinsicFilter { + return IntrinsicFilter{intrinsic: traceql.IntrinsicStatus, statusCode: statusCode} +} + +// NewNameIntrinsicFilter returns a new IntrinsicFilter that matches spans with the given name. +func NewNameIntrinsicFilter(value string) IntrinsicFilter { + return IntrinsicFilter{intrinsic: traceql.IntrinsicName, name: value} +} + +// NewRegexpIntrinsicFilter returns a new IntrinsicFilter that matches spans based on the given regex and intrinsic. +func NewRegexpIntrinsicFilter(intrinsic traceql.Intrinsic, value interface{}) (IntrinsicFilter, error) { + var ( + stringValue string + ok bool + ) + if stringValue, ok = value.(string); !ok { + return IntrinsicFilter{}, fmt.Errorf("unsupported intrinsic filter regex value: %v", value) + } + r, err := regexp.Compile(stringValue) + if err != nil { + return IntrinsicFilter{}, fmt.Errorf("invalid intrinsic filter regex: %v", err) + } + switch intrinsic { + case traceql.IntrinsicName, traceql.IntrinsicStatus, traceql.IntrinsicKind: + return IntrinsicFilter{intrinsic: intrinsic, regex: r}, nil + default: + return IntrinsicFilter{}, fmt.Errorf("intrinsic not supported %s", intrinsic) + } +} + +// Matches returns true if the given span matches the filter. +func (a *IntrinsicFilter) Matches(span *tracev1.Span) bool { + switch a.intrinsic { + case traceql.IntrinsicName: + if a.regex != nil { + return a.regex.MatchString(span.Name) + } + return a.name == span.Name + case traceql.IntrinsicStatus: + if a.regex != nil { + return a.regex.MatchString(span.GetStatus().GetCode().String()) + } + return a.statusCode == span.GetStatus().GetCode() + case traceql.IntrinsicKind: + if a.regex != nil { + return a.regex.MatchString(span.Kind.String()) + } + return a.kind == span.Kind + default: + return false + } +} diff --git a/pkg/spanfilter/policymatch/intrinsic_test.go b/pkg/spanfilter/policymatch/intrinsic_test.go new file mode 100644 index 00000000000..a2a3a82b104 --- /dev/null +++ b/pkg/spanfilter/policymatch/intrinsic_test.go @@ -0,0 +1,149 @@ +package policymatch + +import ( + "testing" + + tracev1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" + "github.com/grafana/tempo/pkg/traceql" + "github.com/stretchr/testify/require" +) + +func TestIntrinsicPolicyMatch_Matches(t *testing.T) { + cases := []struct { + policy *IntrinsicPolicyMatch + span *tracev1.Span + expect bool + name string + }{ + { + name: "match on name, kind and status", + expect: true, + policy: &IntrinsicPolicyMatch{ + filters: []IntrinsicFilter{ + NewKindIntrinsicFilter(tracev1.Span_SPAN_KIND_SERVER), + NewStatusIntrinsicFilter(tracev1.Status_STATUS_CODE_OK), + NewNameIntrinsicFilter("test"), + }, + }, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_SERVER, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, + }, + Name: "test", + }, + }, + { + name: "unmatched name", + expect: false, + policy: &IntrinsicPolicyMatch{ + filters: []IntrinsicFilter{ + NewKindIntrinsicFilter(tracev1.Span_SPAN_KIND_SERVER), + NewStatusIntrinsicFilter(tracev1.Status_STATUS_CODE_OK), + NewNameIntrinsicFilter("test"), + }, + }, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_SERVER, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, + }, + Name: "test2", + }, + }, + { + name: "unmatched status", + expect: false, + policy: &IntrinsicPolicyMatch{ + filters: []IntrinsicFilter{ + NewKindIntrinsicFilter(tracev1.Span_SPAN_KIND_SERVER), + NewStatusIntrinsicFilter(tracev1.Status_STATUS_CODE_OK), + NewNameIntrinsicFilter("test"), + }, + }, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_CLIENT, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_ERROR, + }, + Name: "test", + }, + }, + { + name: "unmatched kind", + expect: false, + policy: &IntrinsicPolicyMatch{ + filters: []IntrinsicFilter{ + NewKindIntrinsicFilter(tracev1.Span_SPAN_KIND_SERVER), + NewStatusIntrinsicFilter(tracev1.Status_STATUS_CODE_OK), + NewNameIntrinsicFilter("test"), + }, + }, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_CLIENT, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, + }, + Name: "test", + }, + }, + { + name: "matched regex kind and status", + expect: true, + policy: &IntrinsicPolicyMatch{ + filters: []IntrinsicFilter{ + must(NewRegexpIntrinsicFilter(traceql.IntrinsicKind, ".*_KIND_.*")), + must(NewRegexpIntrinsicFilter(traceql.IntrinsicStatus, ".*_CODE_.*")), + }, + }, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_SERVER, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, + }, + Name: "test", + }, + }, + { + name: "unmatched regex kind", + expect: false, + policy: &IntrinsicPolicyMatch{ + filters: []IntrinsicFilter{ + must(NewRegexpIntrinsicFilter(traceql.IntrinsicKind, ".*_CLIENT")), + must(NewRegexpIntrinsicFilter(traceql.IntrinsicStatus, ".*_OK")), + }, + }, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_SERVER, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, + }, + Name: "test", + }, + }, + { + name: "unmatched regex status", + expect: false, + policy: &IntrinsicPolicyMatch{ + filters: []IntrinsicFilter{ + must(NewRegexpIntrinsicFilter(traceql.IntrinsicKind, ".*_SERVER")), + must(NewRegexpIntrinsicFilter(traceql.IntrinsicStatus, ".*_ERROR")), + }, + }, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_SERVER, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, + }, + Name: "test", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r := tc.policy.Matches(tc.span) + require.Equal(t, tc.expect, r) + }) + } +} diff --git a/pkg/spanfilter/spanfilter.go b/pkg/spanfilter/spanfilter.go index 8a8ddde4312..26d9ce60f75 100644 --- a/pkg/spanfilter/spanfilter.go +++ b/pkg/spanfilter/spanfilter.go @@ -1,14 +1,9 @@ package spanfilter import ( - "reflect" - "regexp" - "github.com/grafana/tempo/pkg/spanfilter/config" - v1_common "github.com/grafana/tempo/pkg/tempopb/common/v1" v1 "github.com/grafana/tempo/pkg/tempopb/resource/v1" - v1_trace "github.com/grafana/tempo/pkg/tempopb/trace/v1" - "github.com/grafana/tempo/pkg/traceql" + tracev1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" ) type SpanFilter struct { @@ -20,31 +15,32 @@ type filterPolicy struct { Exclude *splitPolicy } -// SplitPolicy is the result of parsing a policy from the config file to be -// specific about the area the given policy is applied to. -type splitPolicy struct { - ResourceMatch *config.PolicyMatch - SpanMatch *config.PolicyMatch - IntrinsicMatch *config.PolicyMatch -} - +// NewSpanFilter returns a SpanFilter that will filter spans based on the given filter policies. func NewSpanFilter(filterPolicies []config.FilterPolicy) (*SpanFilter, error) { var policies []*filterPolicy - var err error for _, policy := range filterPolicies { - err = config.ValidateFilterPolicy(policy) + err := config.ValidateFilterPolicy(policy) if err != nil { return nil, err } - p := &filterPolicy{ - Include: getSplitPolicy(policy.Include), - Exclude: getSplitPolicy(policy.Exclude), + include, err := getSplitPolicy(policy.Include) + if err != nil { + return nil, err + } + + exclude, err := getSplitPolicy(policy.Exclude) + if err != nil { + return nil, err + } + p := filterPolicy{ + Include: include, + Exclude: exclude, } if p.Include != nil || p.Exclude != nil { - policies = append(policies, p) + policies = append(policies, &p) } } @@ -53,215 +49,29 @@ func NewSpanFilter(filterPolicies []config.FilterPolicy) (*SpanFilter, error) { }, nil } -// applyFilterPolicy returns true if the span should be included in the metrics. -func (f *SpanFilter) ApplyFilterPolicy(rs *v1.Resource, span *v1_trace.Span) bool { +// ApplyFilterPolicy returns true if the span should be included in the metrics. +func (f *SpanFilter) ApplyFilterPolicy(rs *v1.Resource, span *tracev1.Span) bool { // With no filter policies specified, all spans are included. if len(f.filterPolicies) == 0 { return true } for _, policy := range f.filterPolicies { - if policy.Include != nil { - if !policyMatch(policy.Include, rs, span) { - return false - } + if policy.Include != nil && !policy.Include.Match(rs, span) { + return false } - if policy.Exclude != nil { - if policyMatch(policy.Exclude, rs, span) { - return false - } + if policy.Exclude != nil && policy.Exclude.Match(rs, span) { + return false } } return true } -func stringMatch(matchType config.MatchType, s, pattern string) bool { - switch matchType { - case config.Strict: - return s == pattern - case config.Regex: - re := regexp.MustCompile(pattern) - return re.MatchString(s) - default: - return false - } -} - -// policyMatch returns true when the resource attribtues and span attributes match the policy. -func policyMatch(policy *splitPolicy, rs *v1.Resource, span *v1_trace.Span) bool { - return policyMatchAttrs(policy.ResourceMatch, rs.Attributes) && - policyMatchAttrs(policy.SpanMatch, span.Attributes) && - policyMatchIntrinsicAttrs(policy.IntrinsicMatch, span) -} - -// policyMatchIntrinsicAttrs returns true when all intrinsic values in the policy match the span. -func policyMatchIntrinsicAttrs(policy *config.PolicyMatch, span *v1_trace.Span) bool { - matches := 0 - - var attr traceql.Attribute - var spanKind, policyKind v1_trace.Span_SpanKind - var spanStatusCode, policyStatusCode v1_trace.Status_StatusCode - - for _, pa := range policy.Attributes { - attr = traceql.MustParseIdentifier(pa.Key) - switch attr.Intrinsic { - // case traceql.IntrinsicDuration: - // case traceql.IntrinsicChildCount: - // case traceql.IntrinsicParent: - case traceql.IntrinsicName: - if !stringMatch(policy.MatchType, span.GetName(), pa.Value.(string)) { - return false - } - matches++ - case traceql.IntrinsicStatus: - switch pa.Value.(type) { - case v1_trace.Status_StatusCode: - spanStatusCode = span.GetStatus().GetCode() - policyStatusCode = pa.Value.(v1_trace.Status_StatusCode) - if policy.MatchType == config.Strict && spanStatusCode != policyStatusCode { - return false - } - default: - if !stringMatch(policy.MatchType, span.GetStatus().GetCode().String(), pa.Value.(string)) { - return false - } - } - matches++ - case traceql.IntrinsicKind: - switch pa.Value.(type) { - case v1_trace.Span_SpanKind: - spanKind = span.GetKind() - policyKind = pa.Value.(v1_trace.Span_SpanKind) - if policy.MatchType == config.Strict && spanKind != policyKind { - return false - } - default: - if !stringMatch(policy.MatchType, span.GetKind().String(), pa.Value.(string)) { - return false - } - - } - matches++ - } - } - - return len(policy.Attributes) == matches -} - -// policyMatchAttrs returns true if all attributes in the policy match the attributes in the span. String, bool, int, and floats are supported. Regex MatchType may be applied to string span attributes. -func policyMatchAttrs(policy *config.PolicyMatch, attrs []*v1_common.KeyValue) bool { - matches := 0 - var v *v1_common.AnyValue - var pAttrValueType string - - for _, pa := range policy.Attributes { - pAttrValueType = reflect.TypeOf(pa.Value).String() - - for _, attr := range attrs { - if attr.GetKey() == pa.Key { - v = attr.GetValue() - - // For each type of value, check if the policy attribute value matches the span attribute value. - switch v.Value.(type) { - case *v1_common.AnyValue_StringValue: - if pAttrValueType != "string" { - return false - } - - if !stringMatch(policy.MatchType, v.GetStringValue(), pa.Value.(string)) { - return false - } - matches++ - case *v1_common.AnyValue_IntValue: - if pAttrValueType != "int" { - return false - } - - if v.GetIntValue() != int64(pa.Value.(int)) { - return false - } - matches++ - case *v1_common.AnyValue_DoubleValue: - if pAttrValueType != "float64" { - return false - } - - if v.GetDoubleValue() != pa.Value.(float64) { - return false - } - matches++ - case *v1_common.AnyValue_BoolValue: - if pAttrValueType != "bool" { - return false - } - - if v.GetBoolValue() != pa.Value.(bool) { - return false - } - matches++ - } - } - } - } - - return len(policy.Attributes) == matches -} - -func getSplitPolicy(policy *config.PolicyMatch) *splitPolicy { +func getSplitPolicy(policy *config.PolicyMatch) (*splitPolicy, error) { if policy == nil { - return nil - } - - // A policy to match against the resource attributes - resourcePolicy := &config.PolicyMatch{ - MatchType: policy.MatchType, - Attributes: make([]config.MatchPolicyAttribute, 0), - } - - // A policy to match against the span attributes - spanPolicy := &config.PolicyMatch{ - MatchType: policy.MatchType, - Attributes: make([]config.MatchPolicyAttribute, 0), - } - - intrinsicPolicy := &config.PolicyMatch{ - MatchType: policy.MatchType, - Attributes: make([]config.MatchPolicyAttribute, 0), - } - - for _, pa := range policy.Attributes { - attr := traceql.MustParseIdentifier(pa.Key) - - attribute := config.MatchPolicyAttribute{ - Key: attr.Name, - Value: pa.Value, - } - - if attr.Intrinsic > 0 { - if policy.MatchType == config.Strict { - switch attr.Intrinsic { - case traceql.IntrinsicStatus: - attribute.Value = v1_trace.Status_StatusCode(v1_trace.Status_StatusCode_value[pa.Value.(string)]) - case traceql.IntrinsicKind: - attribute.Value = v1_trace.Span_SpanKind(v1_trace.Span_SpanKind_value[pa.Value.(string)]) - } - } - intrinsicPolicy.Attributes = append(intrinsicPolicy.Attributes, attribute) - } else { - switch attr.Scope { - case traceql.AttributeScopeSpan: - spanPolicy.Attributes = append(spanPolicy.Attributes, attribute) - case traceql.AttributeScopeResource: - resourcePolicy.Attributes = append(resourcePolicy.Attributes, attribute) - } - } - } - - return &splitPolicy{ - ResourceMatch: resourcePolicy, - SpanMatch: spanPolicy, - IntrinsicMatch: intrinsicPolicy, + return nil, nil } + return newSplitPolicy(policy) } diff --git a/pkg/spanfilter/spanfilter_test.go b/pkg/spanfilter/spanfilter_test.go index 2a290282b37..b137c5abc35 100644 --- a/pkg/spanfilter/spanfilter_test.go +++ b/pkg/spanfilter/spanfilter_test.go @@ -3,13 +3,14 @@ package spanfilter import ( "fmt" "os" + "runtime" "testing" "github.com/grafana/tempo/pkg/spanfilter/config" "github.com/grafana/tempo/pkg/tempopb" - common_v1 "github.com/grafana/tempo/pkg/tempopb/common/v1" + commonv1 "github.com/grafana/tempo/pkg/tempopb/common/v1" v1 "github.com/grafana/tempo/pkg/tempopb/resource/v1" - trace_v1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" + tracev1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" "github.com/stretchr/testify/require" ) @@ -48,11 +49,11 @@ func TestSpanFilter_NewSpanFilter(t *testing.T) { } } -func TestSpanFilter_policyMatch(t *testing.T) { +func Test_splitPolicy_Match(t *testing.T) { cases := []struct { policy *config.PolicyMatch resource *v1.Resource - span *trace_v1.Span + span *tracev1.Span expect bool testName string }{ @@ -69,14 +70,14 @@ func TestSpanFilter_policyMatch(t *testing.T) { }, }, resource: &v1.Resource{ - Attributes: []*common_v1.KeyValue{}, + Attributes: []*commonv1.KeyValue{}, }, - span: &trace_v1.Span{ - Attributes: []*common_v1.KeyValue{ + span: &tracev1.Span{ + Attributes: []*commonv1.KeyValue{ { Key: "kind", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "SPAN_KIND_CLIENT", }, }, @@ -97,10 +98,10 @@ func TestSpanFilter_policyMatch(t *testing.T) { }, }, resource: &v1.Resource{ - Attributes: []*common_v1.KeyValue{}, + Attributes: []*commonv1.KeyValue{}, }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_CLIENT, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_CLIENT, }, }, { @@ -132,40 +133,40 @@ func TestSpanFilter_policyMatch(t *testing.T) { }, }, resource: &v1.Resource{ - Attributes: []*common_v1.KeyValue{ + Attributes: []*commonv1.KeyValue{ { Key: "name", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "test", }, }, }, { Key: "location", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "earth", }, }, }, { Key: "othervalue", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "somethinginteresting", }, }, }, }, }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_CLIENT, - Attributes: []*common_v1.KeyValue{ + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_CLIENT, + Attributes: []*commonv1.KeyValue{ { Key: "status.code", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "STATUS_CODE_OK", }, }, @@ -189,12 +190,12 @@ func TestSpanFilter_policyMatch(t *testing.T) { }, }, resource: &v1.Resource{ - Attributes: []*common_v1.KeyValue{}, + Attributes: []*commonv1.KeyValue{}, }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_CLIENT, - Status: &trace_v1.Status{Message: "OK", Code: trace_v1.Status_STATUS_CODE_OK}, - Attributes: []*common_v1.KeyValue{}, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_CLIENT, + Status: &tracev1.Status{Message: "OK", Code: tracev1.Status_STATUS_CODE_OK}, + Attributes: []*commonv1.KeyValue{}, }, }, { @@ -214,613 +215,42 @@ func TestSpanFilter_policyMatch(t *testing.T) { }, }, resource: &v1.Resource{ - Attributes: []*common_v1.KeyValue{ + Attributes: []*commonv1.KeyValue{ { Key: "location", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "earth", }, }, }, { Key: "othervalue", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "somethinginteresting", }, }, }, }, }, - span: &trace_v1.Span{ - Attributes: []*common_v1.KeyValue{}, + span: &tracev1.Span{ + Attributes: []*commonv1.KeyValue{}, }, }, } for _, tc := range cases { t.Run(tc.testName, func(t *testing.T) { - r := policyMatch(getSplitPolicy(tc.policy), tc.resource, tc.span) - require.Equal(t, tc.expect, r) - }) - } -} - -func TestSpanFilter_policyMatchIntrinsicAttrs(t *testing.T) { - cases := []struct { - policy *config.PolicyMatch - span *trace_v1.Span - expect bool - name string - }{ - { - name: "match on name, kind and status", - expect: true, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: trace_v1.Span_SPAN_KIND_SERVER, - }, - { - Key: "status", - Value: trace_v1.Status_STATUS_CODE_OK, - }, - { - Key: "name", - Value: "test", - }, - }, - }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_SERVER, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, - }, - Name: "test", - }, - }, - { - name: "unmatched name", - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: trace_v1.Span_SPAN_KIND_SERVER, - }, - { - Key: "status", - Value: trace_v1.Status_STATUS_CODE_OK, - }, - { - Key: "name", - Value: "test", - }, - }, - }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_SERVER, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, - }, - Name: "test2", - }, - }, - { - name: "unmatched status", - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: trace_v1.Span_SPAN_KIND_CLIENT, - }, - { - Key: "status", - Value: trace_v1.Status_STATUS_CODE_OK, - }, - { - Key: "name", - Value: "test", - }, - }, - }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_CLIENT, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_ERROR, - }, - Name: "test", - }, - }, - { - name: "unmatched kind", - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: trace_v1.Span_SPAN_KIND_SERVER, - }, - { - Key: "status", - Value: trace_v1.Status_STATUS_CODE_OK, - }, - { - Key: "name", - Value: "test", - }, - }, - }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_CLIENT, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, - }, - Name: "test", - }, - }, - { - name: "matched regex kind and status", - expect: true, - policy: &config.PolicyMatch{ - MatchType: config.Regex, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: ".*_KIND_.*", - }, - { - Key: "status", - Value: ".*_CODE_.*", - }, - }, - }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_SERVER, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, - }, - Name: "test", - }, - }, - { - name: "unmatched regex kind", - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Regex, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: ".*_CLIENT", - }, - { - Key: "status", - Value: ".*_OK", - }, - }, - }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_SERVER, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, - }, - Name: "test", - }, - }, - { - name: "unmatched regex status", - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Regex, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: ".*_SERVER", - }, - { - Key: "status", - Value: ".*_ERROR", - }, - }, - }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_SERVER, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, - }, - Name: "test", - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - r := policyMatchIntrinsicAttrs(tc.policy, tc.span) + policy, err := getSplitPolicy(tc.policy) + require.NoError(t, err) + require.NotNil(t, policy) + r := policy.Match(tc.resource, tc.span) require.Equal(t, tc.expect, r) }) } } -func TestSpanFilter_policyMatchAttrs(t *testing.T) { - cases := []struct { - policy *config.PolicyMatch - attrs []*common_v1.KeyValue - expect bool - }{ - // Single string match - { - expect: true, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "foo", - Value: "bar", - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "foo", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "bar", - }, - }, - }, - }, - }, - // Multiple string match - { - expect: true, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "foo", - Value: "bar", - }, - { - Key: "otherfoo", - Value: "notbar", - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "foo", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "bar", - }, - }, - }, - { - Key: "otherfoo", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "notbar", - }, - }, - }, - }, - }, - // Multiple string non match - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "foo", - Value: "bar", - }, - { - Key: "otherfoo", - Value: "nope", - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "foo", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "bar", - }, - }, - }, - { - Key: "otherfoo", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "notbar", - }, - }, - }, - }, - }, - // Combination match - { - expect: true, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "one", - Value: "1", - }, - { - Key: "oneone", - Value: 11, - }, - { - Key: "oneonepointone", - Value: 11.1, - }, - { - Key: "matching", - Value: true, - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "one", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "1", - }, - }, - }, - { - Key: "oneone", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_IntValue{ - IntValue: 11, - }, - }, - }, - { - Key: "oneonepointone", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_DoubleValue{ - DoubleValue: 11.1, - }, - }, - }, - { - Key: "matching", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_BoolValue{ - BoolValue: true, - }, - }, - }, - }, - }, - // Regex basic match - { - expect: true, - policy: &config.PolicyMatch{ - MatchType: config.Regex, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "dd", - Value: `\d\d\w{5}`, - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "dd", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "11xxxxx", - }, - }, - }, - }, - }, - // Value type mismatch string - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "dd", - Value: true, - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "dd", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "11xxxxx", - }, - }, - }, - }, - }, - // Value type mismatch string/int - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "dd", - Value: "value", - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "dd", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_IntValue{ - IntValue: 11, - }, - }, - }, - }, - }, - // Value type mismatch string/float - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "11", - Value: "eleven", - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "11", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_DoubleValue{ - DoubleValue: 11.1, - }, - }, - }, - }, - }, - // Value type mismatch string/bool - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "11", - Value: "eleven", - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "11", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_BoolValue{ - BoolValue: false, - }, - }, - }, - }, - }, - // Value type mismatch int/string - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "11", - Value: 11, - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "11", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ - StringValue: "11", - }, - }, - }, - }, - }, - // Value mismatch int - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "11", - Value: 11, - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "11", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_IntValue{ - IntValue: 12, - }, - }, - }, - }, - }, - // Value mismatch bool - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "11", - Value: true, - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "11", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_BoolValue{ - BoolValue: false, - }, - }, - }, - }, - }, - // Value mismatch bool - { - expect: false, - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "11", - Value: 11.0, - }, - }, - }, - attrs: []*common_v1.KeyValue{ - { - Key: "11", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_DoubleValue{ - DoubleValue: 11.1, - }, - }, - }, - }, - }, - } - - for _, tc := range cases { - r := policyMatchAttrs(tc.policy, tc.attrs) - require.Equal(t, tc.expect, r) - } -} - func TestSpanMetrics_applyFilterPolicy(t *testing.T) { cases := []struct { name string @@ -828,7 +258,7 @@ func TestSpanMetrics_applyFilterPolicy(t *testing.T) { filterPolicies []config.FilterPolicy expect bool resource *v1.Resource - span *trace_v1.Span + span *tracev1.Span }{ { name: "no policies matches", @@ -873,37 +303,37 @@ func TestSpanMetrics_applyFilterPolicy(t *testing.T) { }, }, resource: &v1.Resource{ - Attributes: []*common_v1.KeyValue{ + Attributes: []*commonv1.KeyValue{ { Key: "name", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "test", }, }, }, { Key: "location", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "earth", }, }, }, { Key: "othervalue", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "somethinginteresting", }, }, }, }, }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_SERVER, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_SERVER, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, }, Name: "test", }, @@ -930,37 +360,37 @@ func TestSpanMetrics_applyFilterPolicy(t *testing.T) { }, }, resource: &v1.Resource{ - Attributes: []*common_v1.KeyValue{ + Attributes: []*commonv1.KeyValue{ { Key: "name", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "test", }, }, }, { Key: "location", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "earth", }, }, }, { Key: "othervalue", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "somethinginteresting", }, }, }, }, }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_SERVER, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_SERVER, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, }, Name: "test", }, @@ -996,37 +426,37 @@ func TestSpanMetrics_applyFilterPolicy(t *testing.T) { }, }, resource: &v1.Resource{ - Attributes: []*common_v1.KeyValue{ + Attributes: []*commonv1.KeyValue{ { Key: "name", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "test", }, }, }, { Key: "location", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "earth", }, }, }, { Key: "othervalue", - Value: &common_v1.AnyValue{ - Value: &common_v1.AnyValue_StringValue{ + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{ StringValue: "somethinginteresting", }, }, }, }, }, - span: &trace_v1.Span{ - Kind: trace_v1.Span_SPAN_KIND_SERVER, - Status: &trace_v1.Status{ - Code: trace_v1.Status_STATUS_CODE_OK, + span: &tracev1.Span{ + Kind: tracev1.Span_SPAN_KIND_SERVER, + Status: &tracev1.Status{ + Code: tracev1.Status_STATUS_CODE_OK, }, Name: "test", }, @@ -1046,128 +476,25 @@ func TestSpanMetrics_applyFilterPolicy(t *testing.T) { } } -func TestSpanFilter_stringMatch(t *testing.T) { - cases := []struct { - matchType config.MatchType - s string - pattern string - expect bool - }{ - { - matchType: config.Strict, - s: "foo", - pattern: "foo", - expect: true, - }, - { - matchType: config.Strict, - s: "foo", - pattern: "bar", - expect: false, - }, - } - - for _, tc := range cases { - r := stringMatch(tc.matchType, tc.s, tc.pattern) - require.Equal(t, tc.expect, r) - } -} - -func TestSpanFilter_getSplitPolicy(t *testing.T) { - cases := []struct { - policy *config.PolicyMatch - split *splitPolicy - name string - }{ - { - name: "basic kind matching", - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: "SPAN_KIND_CLIENT", - }, - }, - }, - split: &splitPolicy{ - IntrinsicMatch: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "kind", - Value: trace_v1.Span_SPAN_KIND_CLIENT, - }, - }, - }, - }, - }, - { - name: "basic status matching", - policy: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "status", - Value: "STATUS_CODE_OK", - }, - }, - }, - split: &splitPolicy{ - IntrinsicMatch: &config.PolicyMatch{ - MatchType: config.Strict, - Attributes: []config.MatchPolicyAttribute{ - { - Key: "status", - Value: trace_v1.Status_STATUS_CODE_OK, - }, - }, - }, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - s := getSplitPolicy(tc.policy) - - require.NotNil(t, s) - require.NotNil(t, s.IntrinsicMatch) - require.NotNil(t, s.SpanMatch) - require.NotNil(t, s.ResourceMatch) - - if tc.split.IntrinsicMatch != nil { - require.Equal(t, tc.split.IntrinsicMatch, s.IntrinsicMatch) - } - if tc.split.SpanMatch != nil { - require.Equal(t, tc.split.SpanMatch, s.SpanMatch) - } - if tc.split.ResourceMatch != nil { - require.Equal(t, tc.split.ResourceMatch, s.ResourceMatch) - } - }) - } -} - func BenchmarkSpanFilter_applyFilterPolicyNone(b *testing.B) { // Generate a batch of 100k spans // r, done := test.NewRandomBatcher() // defer done() // batch := r.GenerateBatch(1e6) // data, _ := batch.Marshal() - // _ = ioutil.WriteFile("testbatch100k", data, 0600) + //_ = os.WriteFile("testbatch100k", data, 0600) // Read the file generated above data, err := os.ReadFile("testbatch100k") require.NoError(b, err) - batch := &trace_v1.ResourceSpans{} + batch := &tracev1.ResourceSpans{} err = batch.Unmarshal(data) require.NoError(b, err) // b.Logf("size: %s", humanize.Bytes(uint64(batch.Size()))) // b.Logf("span count: %d", len(batch.ScopeSpans)) - policies := []config.FilterPolicy{} + var policies []config.FilterPolicy benchmarkFilterPolicy(b, policies, batch) } @@ -1176,7 +503,7 @@ func BenchmarkSpanFilter_applyFilterPolicySmall(b *testing.B) { // Read the file generated above data, err := os.ReadFile("testbatch100k") require.NoError(b, err) - batch := &trace_v1.ResourceSpans{} + batch := &tracev1.ResourceSpans{} err = batch.Unmarshal(data) require.NoError(b, err) @@ -1201,7 +528,7 @@ func BenchmarkSpanFilter_applyFilterPolicyMedium(b *testing.B) { // Read the file generated above data, err := os.ReadFile("testbatch100k") require.NoError(b, err) - batch := &trace_v1.ResourceSpans{} + batch := &tracev1.ResourceSpans{} err = batch.Unmarshal(data) require.NoError(b, err) @@ -1234,22 +561,87 @@ func BenchmarkSpanFilter_applyFilterPolicyMedium(b *testing.B) { benchmarkFilterPolicy(b, policies, batch) } -func benchmarkFilterPolicy(b *testing.B, policies []config.FilterPolicy, batch *trace_v1.ResourceSpans) { +func BenchmarkSpanFilter_applyFilterPolicyRegex(b *testing.B) { + // Read the file generated above + data, err := os.ReadFile("testbatch100k") + require.NoError(b, err) + batch := &tracev1.ResourceSpans{} + err = batch.Unmarshal(data) + require.NoError(b, err) + + policies := []config.FilterPolicy{ + { + Include: &config.PolicyMatch{ + MatchType: config.Regex, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "span.foo", + Value: ".*foo.*", + }, + { + Key: "span.x", + Value: ".+value.+", + }, + }, + }, + }, + } + + benchmarkFilterPolicy(b, policies, batch) +} + +func BenchmarkSpanFilter_applyFilterPolicyIntrinsic(b *testing.B) { + // Read the file generated above + data, err := os.ReadFile("testbatch100k") + require.NoError(b, err) + batch := &tracev1.ResourceSpans{} + err = batch.Unmarshal(data) + require.NoError(b, err) + + policies := []config.FilterPolicy{ + { + Include: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "kind", + Value: "SPAN_KIND_INTERNAL", + }, + { + Key: "status", + Value: "STATUS_CODE_OK", + }, + }, + }, + }, + } + + benchmarkFilterPolicy(b, policies, batch) +} + +func benchmarkFilterPolicy(b *testing.B, policies []config.FilterPolicy, batch *tracev1.ResourceSpans) { filter, err := NewSpanFilter(policies) require.NoError(b, err) b.ResetTimer() + c := 0 // to prevent compiler optimizations for n := 0; n < b.N; n++ { - pushspans(&tempopb.PushSpansRequest{Batches: []*trace_v1.ResourceSpans{batch}}, filter) + c += pushspans(&tempopb.PushSpansRequest{Batches: []*tracev1.ResourceSpans{batch}}, filter) } + runtime.KeepAlive(c) } -func pushspans(req *tempopb.PushSpansRequest, filter *SpanFilter) { +func pushspans(req *tempopb.PushSpansRequest, filter *SpanFilter) int { + c := 0 for _, rs := range req.Batches { for _, ils := range rs.ScopeSpans { for _, span := range ils.Spans { - filter.ApplyFilterPolicy(rs.Resource, span) + v := filter.ApplyFilterPolicy(rs.Resource, span) + if v { + c++ + } } } } + return c } diff --git a/pkg/spanfilter/splitpolicy.go b/pkg/spanfilter/splitpolicy.go new file mode 100644 index 00000000000..b1f558f00de --- /dev/null +++ b/pkg/spanfilter/splitpolicy.go @@ -0,0 +1,90 @@ +package spanfilter + +import ( + "fmt" + + "github.com/grafana/tempo/pkg/spanfilter/config" + "github.com/grafana/tempo/pkg/spanfilter/policymatch" + v1 "github.com/grafana/tempo/pkg/tempopb/resource/v1" + tracev1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" + "github.com/grafana/tempo/pkg/traceql" +) + +// splitPolicy is the result of parsing a policy from the config file to be +// specific about the area the given policy is applied to. +type splitPolicy struct { + // ResourceMatch is a set of resource attributes that must match a span for the span to match the policy. + ResourceMatch *policymatch.AttributePolicyMatch + // SpanMatch is a set of span attributes that must match a span for the span to match the policy. + SpanMatch *policymatch.AttributePolicyMatch + // IntrinsicMatch is a set of intrinsic attributes that must match a span for the span to match the policy. + IntrinsicMatch *policymatch.IntrinsicPolicyMatch +} + +func newSplitPolicy(policy *config.PolicyMatch) (*splitPolicy, error) { + var resourceAttributeFilters []policymatch.AttributeFilter + var spanAttributeFilters []policymatch.AttributeFilter + var intrinsicFilters []policymatch.IntrinsicFilter + + for _, pa := range policy.Attributes { + attr, err := traceql.ParseIdentifier(pa.Key) + if err != nil { + return nil, fmt.Errorf("invalid policy match attribute: %v", err) + } + + if attr.Intrinsic > 0 { + var filter policymatch.IntrinsicFilter + if policy.MatchType == config.Strict { + filter, err = policymatch.NewStrictIntrinsicFilter(attr.Intrinsic, pa.Value) + if err != nil { + return nil, err + } + } else { + filter, err = policymatch.NewRegexpIntrinsicFilter(attr.Intrinsic, pa.Value) + if err != nil { + return nil, err + } + } + intrinsicFilters = append(intrinsicFilters, filter) + } else { + switch attr.Scope { + case traceql.AttributeScopeSpan: + filter, err := policymatch.NewAttributeFilter(policy.MatchType, attr.Name, pa.Value) + if err != nil { + return nil, err + } + spanAttributeFilters = append(spanAttributeFilters, filter) + case traceql.AttributeScopeResource: + filter, err := policymatch.NewAttributeFilter(policy.MatchType, attr.Name, pa.Value) + if err != nil { + return nil, err + } + resourceAttributeFilters = append(resourceAttributeFilters, filter) + default: + return nil, fmt.Errorf("invalid or unsupported attribute scope: %v", attr.Scope) + } + } + } + + sp := splitPolicy{} + if len(resourceAttributeFilters) > 0 { + sp.ResourceMatch = policymatch.NewAttributePolicyMatch(resourceAttributeFilters) + } + + if len(intrinsicFilters) > 0 { + sp.IntrinsicMatch = policymatch.NewIntrinsicPolicyMatch(intrinsicFilters) + } + + if len(spanAttributeFilters) > 0 { + sp.SpanMatch = policymatch.NewAttributePolicyMatch(spanAttributeFilters) + } + + return &sp, nil +} + +// Match returns true when the resource attributes and span attributes match the policy. +func (p *splitPolicy) Match(rs *v1.Resource, span *tracev1.Span) bool { + return (p.ResourceMatch == nil || p.ResourceMatch.Matches(rs.Attributes)) && + (p.SpanMatch == nil || p.SpanMatch.Matches(span.Attributes)) && + (p.IntrinsicMatch == nil || p.IntrinsicMatch.Matches(span)) +} diff --git a/pkg/spanfilter/splitpolicy_test.go b/pkg/spanfilter/splitpolicy_test.go new file mode 100644 index 00000000000..57a31c28180 --- /dev/null +++ b/pkg/spanfilter/splitpolicy_test.go @@ -0,0 +1,359 @@ +package spanfilter + +import ( + "errors" + "testing" + + "github.com/grafana/tempo/pkg/traceql" + + "github.com/grafana/tempo/pkg/spanfilter/policymatch" + tracev1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" + "github.com/stretchr/testify/require" + + "github.com/grafana/tempo/pkg/spanfilter/config" +) + +func Test_newSplitPolicy(t *testing.T) { + cases := []struct { + policy *config.PolicyMatch + split *splitPolicy + name string + err error + }{ + { + name: "strict kind matching", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "kind", + Value: "SPAN_KIND_CLIENT", + }, + }, + }, + split: &splitPolicy{ + IntrinsicMatch: policymatch.NewIntrinsicPolicyMatch( + []policymatch.IntrinsicFilter{ + policymatch.NewKindIntrinsicFilter(tracev1.Span_SPAN_KIND_CLIENT), + }), + }, + }, + { + name: "strict status matching", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "status", + Value: "STATUS_CODE_OK", + }, + }, + }, + split: &splitPolicy{ + IntrinsicMatch: policymatch.NewIntrinsicPolicyMatch( + []policymatch.IntrinsicFilter{ + policymatch.NewStatusIntrinsicFilter(tracev1.Status_STATUS_CODE_OK), + }), + }, + }, + { + name: "strict name matching", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "name", + Value: "foo", + }, + }, + }, + split: &splitPolicy{ + IntrinsicMatch: policymatch.NewIntrinsicPolicyMatch( + []policymatch.IntrinsicFilter{ + policymatch.NewNameIntrinsicFilter("foo"), + }, + ), + }, + }, + { + name: "regex name matching", + policy: &config.PolicyMatch{ + MatchType: config.Regex, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "name", + Value: "foo.*", + }, + }, + }, + split: &splitPolicy{ + IntrinsicMatch: policymatch.NewIntrinsicPolicyMatch( + []policymatch.IntrinsicFilter{ + must(policymatch.NewRegexpIntrinsicFilter(traceql.IntrinsicName, "foo.*")), + }, + ), + }, + }, + { + name: "regex kind matching", + policy: &config.PolicyMatch{ + MatchType: config.Regex, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "kind", + Value: ".*_CLIENT", + }, + }, + }, + split: &splitPolicy{ + IntrinsicMatch: policymatch.NewIntrinsicPolicyMatch( + []policymatch.IntrinsicFilter{ + must(policymatch.NewRegexpIntrinsicFilter(traceql.IntrinsicKind, ".*_CLIENT")), + }, + ), + }, + }, + { + name: "regex status matching", + policy: &config.PolicyMatch{ + MatchType: config.Regex, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "status", + Value: ".*_OK", + }, + }, + }, + split: &splitPolicy{ + IntrinsicMatch: policymatch.NewIntrinsicPolicyMatch( + []policymatch.IntrinsicFilter{ + must(policymatch.NewRegexpIntrinsicFilter(traceql.IntrinsicStatus, ".*_OK")), + }, + ), + }, + }, + { + name: "strict resource attribute matching", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "resource.foo", + Value: "bar", + }, + }, + }, + split: &splitPolicy{ + ResourceMatch: policymatch.NewAttributePolicyMatch( + []policymatch.AttributeFilter{ + must(policymatch.NewStrictAttributeFilter("foo", "bar")), + }, + ), + }, + }, + { + name: "regex resource attribute matching", + policy: &config.PolicyMatch{ + MatchType: config.Regex, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "resource.foo", + Value: ".*", + }, + }, + }, + split: &splitPolicy{ + ResourceMatch: policymatch.NewAttributePolicyMatch( + []policymatch.AttributeFilter{ + must(policymatch.NewRegexpAttributeFilter("foo", ".*")), + }, + ), + }, + }, + { + name: "strict span attribute matching", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "span.foo", + Value: "bar", + }, + }, + }, + split: &splitPolicy{ + SpanMatch: policymatch.NewAttributePolicyMatch( + []policymatch.AttributeFilter{ + must(policymatch.NewStrictAttributeFilter("foo", "bar")), + }, + ), + }, + }, + { + name: "regex span attribute matching", + policy: &config.PolicyMatch{ + MatchType: config.Regex, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "span.foo", + Value: ".*", + }, + }, + }, + split: &splitPolicy{ + SpanMatch: policymatch.NewAttributePolicyMatch( + []policymatch.AttributeFilter{ + must(policymatch.NewRegexpAttributeFilter("foo", ".*")), + }, + ), + }, + }, + { + name: "invalid regex span attribute matching", + policy: &config.PolicyMatch{ + MatchType: config.Regex, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "span.foo", + Value: ".*(", + }, + }, + }, + err: errors.New("invalid attribute filter regexp: error parsing regexp: missing closing ): `.*(`"), + }, + { + name: "invalid regex kind intrinsic matching", + policy: &config.PolicyMatch{ + MatchType: config.Regex, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "kind", + Value: ".*(", + }, + }, + }, + err: errors.New("invalid intrinsic filter regex: error parsing regexp: missing closing ): `.*(`"), + }, + { + name: "invalid intrinsic", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "unsupported", + Value: "foo", + }, + }, + }, + err: errors.New("invalid policy match attribute: tag name is not valid intrinsic or scoped attribute: unsupported"), + }, + { + name: "unsupported kind intrinsic string value", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "kind", + Value: "foo", + }, + }, + }, + err: errors.New("unsupported kind intrinsic string value: foo"), + }, + { + name: "unsupported status intrinsic string value", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "status", + Value: "foo", + }, + }, + }, + err: errors.New("unsupported status intrinsic string value: foo"), + }, + { + name: "unsupported status intrinsic value", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "status", + Value: true, + }, + }, + }, + err: errors.New("unsupported status intrinsic value: true"), + }, + { + name: "unsupported kind intrinsic value", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "kind", + Value: true, + }, + }, + }, + err: errors.New("invalid kind intrinsic value: true"), + }, + { + name: "unsupported name intrinsic value", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "name", + Value: true, + }, + }, + }, + err: errors.New("unsupported name intrinsic value: true"), + }, + { + name: "unsupported intrinsic", + policy: &config.PolicyMatch{ + MatchType: config.Strict, + Attributes: []config.MatchPolicyAttribute{ + { + Key: "childCount", + Value: "foo", + }, + }, + }, + err: errors.New("unsupported intrinsic: childCount"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s, err := newSplitPolicy(tc.policy) + if tc.err != nil { + require.Equal(t, tc.err, err) + return + } + + require.NoError(t, err) + require.NotNil(t, s) + + if tc.split.IntrinsicMatch != nil { + require.Equal(t, tc.split.IntrinsicMatch, s.IntrinsicMatch) + } + if tc.split.SpanMatch != nil { + require.Equal(t, tc.split.SpanMatch, s.SpanMatch) + } + if tc.split.ResourceMatch != nil { + require.Equal(t, tc.split.ResourceMatch, s.ResourceMatch) + } + }) + } +} + +func must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +}