From a60ad92c2003efa08089f2069c7b17dd1367c78d Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 18 Sep 2024 11:56:20 +0200 Subject: [PATCH] [processor/tailsampling]: Enable inverse filtering for boolean attribute filter (#34730) **Description:** This PR adds the `invert_match` option for `boolean_attribute` filters, as discussed in https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/34296#issuecomment-2293236381 **Link to tracking Issue:** #34296 **Testing:** Added unit tests **Documentation:** The existing documentation already covered the semantics of the `invert_match` option --------- Signed-off-by: Florian Bacher --- ...ling_processor_inverted_bool_sampling.yaml | 27 ++++++++++++ processor/tailsamplingprocessor/README.md | 11 ++++- processor/tailsamplingprocessor/config.go | 4 ++ .../internal/sampling/boolean_tag_filter.go | 35 ++++++++++++---- .../sampling/boolean_tag_filter_test.go | 42 ++++++++++++++++++- processor/tailsamplingprocessor/processor.go | 2 +- 6 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 .chloggen/tail_sampling_processor_inverted_bool_sampling.yaml diff --git a/.chloggen/tail_sampling_processor_inverted_bool_sampling.yaml b/.chloggen/tail_sampling_processor_inverted_bool_sampling.yaml new file mode 100644 index 000000000000..4c6eadd1d344 --- /dev/null +++ b/.chloggen/tail_sampling_processor_inverted_bool_sampling.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: tailsamplingprocessor + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Fix the behavior for numeric tag filters with `inverse_match` set to `true`. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [34296] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/processor/tailsamplingprocessor/README.md b/processor/tailsamplingprocessor/README.md index 8cda67c03c9f..e1cec9d0e974 100644 --- a/processor/tailsamplingprocessor/README.md +++ b/processor/tailsamplingprocessor/README.md @@ -58,7 +58,7 @@ Each policy will result in a decision, and the processor will evaluate them to m - When there's a "inverted sample" decision and no "not sample" decisions, the trace is sampled; - In all other cases, the trace is NOT sampled -An "inverted" decision is the one made based on the "invert_match" attribute, such as the one from the string tag policy. +An "inverted" decision is the one made based on the "invert_match" attribute, such as the one from the string, numeric or boolean tag policy. Examples: @@ -223,6 +223,8 @@ Imagine that you wish to configure the processor to implement the following rule 1. **Rule 6:** Add an escape hatch. If there is an attribute called `app.force_sample` in the span, then sample the trace at 100 percent. +1. **Rule 7:** Force spans with `app.do_not_sample` set to `true` to not be sampled, even if the result of the other rules yield a sampling decision. + Here is what the configuration would look like: ```yaml @@ -407,6 +409,13 @@ tail_sampling: type: boolean_attribute, boolean_attribute: { key: app.force_sample, value: true }, }, + { + # Rule 7: + # never sample if the do_not_sample attribute is set to true + name: team_a-do-not-sample, + type: boolean_attribute, + boolean_attribute: { key: app.do_not_sample, value: true, invert_match: true }, + }, # END: policies for team_a ] ``` diff --git a/processor/tailsamplingprocessor/config.go b/processor/tailsamplingprocessor/config.go index 01bb17116c34..af2342f723f5 100644 --- a/processor/tailsamplingprocessor/config.go +++ b/processor/tailsamplingprocessor/config.go @@ -210,6 +210,10 @@ type BooleanAttributeCfg struct { // Value indicate the bool value, either true or false to use when matching against attribute values. // BooleanAttribute Policy will apply exact value match on Value Value bool `mapstructure:"value"` + // InvertMatch indicates that values must not match against attribute values. + // If InvertMatch is true and Values is equal to 'true', all other values will be sampled except 'true'. + // Also, if the specified Key does not match any resource or span attributes, data will be sampled. + InvertMatch bool `mapstructure:"invert_match"` } // OTTLConditionCfg holds the configurable setting to create a OTTL condition filter diff --git a/processor/tailsamplingprocessor/internal/sampling/boolean_tag_filter.go b/processor/tailsamplingprocessor/internal/sampling/boolean_tag_filter.go index 3e1b223fedb3..97e6d82082a7 100644 --- a/processor/tailsamplingprocessor/internal/sampling/boolean_tag_filter.go +++ b/processor/tailsamplingprocessor/internal/sampling/boolean_tag_filter.go @@ -13,20 +13,22 @@ import ( ) type booleanAttributeFilter struct { - key string - value bool - logger *zap.Logger + key string + value bool + logger *zap.Logger + invertMatch bool } var _ PolicyEvaluator = (*booleanAttributeFilter)(nil) // NewBooleanAttributeFilter creates a policy evaluator that samples all traces with // the given attribute that match the supplied boolean value. -func NewBooleanAttributeFilter(settings component.TelemetrySettings, key string, value bool) PolicyEvaluator { +func NewBooleanAttributeFilter(settings component.TelemetrySettings, key string, value bool, invertMatch bool) PolicyEvaluator { return &booleanAttributeFilter{ - key: key, - value: value, - logger: settings.Logger, + key: key, + value: value, + logger: settings.Logger, + invertMatch: invertMatch, } } @@ -36,6 +38,25 @@ func (baf *booleanAttributeFilter) Evaluate(_ context.Context, _ pcommon.TraceID defer trace.Unlock() batches := trace.ReceivedBatches + if baf.invertMatch { + return invertHasResourceOrSpanWithCondition( + batches, + func(resource pcommon.Resource) bool { + if v, ok := resource.Attributes().Get(baf.key); ok { + value := v.Bool() + return value != baf.value + } + return true + }, + func(span ptrace.Span) bool { + if v, ok := span.Attributes().Get(baf.key); ok { + value := v.Bool() + return value != baf.value + } + return true + }, + ), nil + } return hasResourceOrSpanWithCondition( batches, func(resource pcommon.Resource) bool { diff --git a/processor/tailsamplingprocessor/internal/sampling/boolean_tag_filter_test.go b/processor/tailsamplingprocessor/internal/sampling/boolean_tag_filter_test.go index 71bdf180849f..2ff998bb3e86 100644 --- a/processor/tailsamplingprocessor/internal/sampling/boolean_tag_filter_test.go +++ b/processor/tailsamplingprocessor/internal/sampling/boolean_tag_filter_test.go @@ -17,7 +17,7 @@ import ( func TestBooleanTagFilter(t *testing.T) { var empty = map[string]any{} - filter := NewBooleanAttributeFilter(componenttest.NewNopTelemetrySettings(), "example", true) + filter := NewBooleanAttributeFilter(componenttest.NewNopTelemetrySettings(), "example", true, false) resAttr := map[string]any{} resAttr["example"] = 8 @@ -54,6 +54,46 @@ func TestBooleanTagFilter(t *testing.T) { } } +func TestBooleanTagFilterInverted(t *testing.T) { + + var empty = map[string]any{} + filter := NewBooleanAttributeFilter(componenttest.NewNopTelemetrySettings(), "example", true, true) + + resAttr := map[string]any{} + resAttr["example"] = 8 + + cases := []struct { + Desc string + Trace *TraceData + Decision Decision + }{ + { + Desc: "non-matching span attribute", + Trace: newTraceBoolAttrs(empty, "non_matching", true), + Decision: InvertSampled, + }, + { + Desc: "span attribute with non matching boolean value", + Trace: newTraceBoolAttrs(empty, "example", false), + Decision: InvertSampled, + }, + { + Desc: "span attribute with matching boolean value", + Trace: newTraceBoolAttrs(empty, "example", true), + Decision: InvertNotSampled, + }, + } + + for _, c := range cases { + t.Run(c.Desc, func(t *testing.T) { + u, _ := uuid.NewRandom() + decision, err := filter.Evaluate(context.Background(), pcommon.TraceID(u), c.Trace) + assert.NoError(t, err) + assert.Equal(t, decision, c.Decision) + }) + } +} + func newTraceBoolAttrs(nodeAttrs map[string]any, spanAttrKey string, spanAttrValue bool) *TraceData { traces := ptrace.NewTraces() rs := traces.ResourceSpans().AppendEmpty() diff --git a/processor/tailsamplingprocessor/processor.go b/processor/tailsamplingprocessor/processor.go index 78d65225ec8f..4515290198ac 100644 --- a/processor/tailsamplingprocessor/processor.go +++ b/processor/tailsamplingprocessor/processor.go @@ -231,7 +231,7 @@ func getSharedPolicyEvaluator(settings component.TelemetrySettings, cfg *sharedP return sampling.NewTraceStateFilter(settings, tsfCfg.Key, tsfCfg.Values), nil case BooleanAttribute: bafCfg := cfg.BooleanAttributeCfg - return sampling.NewBooleanAttributeFilter(settings, bafCfg.Key, bafCfg.Value), nil + return sampling.NewBooleanAttributeFilter(settings, bafCfg.Key, bafCfg.Value, bafCfg.InvertMatch), nil case OTTLCondition: ottlfCfg := cfg.OTTLConditionCfg return sampling.NewOTTLConditionFilter(settings, ottlfCfg.SpanConditions, ottlfCfg.SpanEventConditions, ottlfCfg.ErrorMode)