diff --git a/.chloggen/ottl-condition-sequence.yaml b/.chloggen/ottl-condition-sequence.yaml new file mode 100755 index 000000000000..ce128ae84ac8 --- /dev/null +++ b/.chloggen/ottl-condition-sequence.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: pkg/ottl + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add `ConditionSequence` for evaluating lists of conditions + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [29339] + +# (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: [api] diff --git a/connector/countconnector/config_test.go b/connector/countconnector/config_test.go index 1ee183f3b094..72a5e43d5d82 100644 --- a/connector/countconnector/config_test.go +++ b/connector/countconnector/config_test.go @@ -413,7 +413,7 @@ func TestConfigErrors(t *testing.T) { }, }, }, - expect: fmt.Sprintf("spans condition: metric %q: unable to parse OTTL statement", defaultMetricNameSpans), + expect: fmt.Sprintf("spans condition: metric %q: unable to parse OTTL condition", defaultMetricNameSpans), }, { name: "invalid_condition_spanevent", @@ -425,7 +425,7 @@ func TestConfigErrors(t *testing.T) { }, }, }, - expect: fmt.Sprintf("spanevents condition: metric %q: unable to parse OTTL statement", defaultMetricNameSpanEvents), + expect: fmt.Sprintf("spanevents condition: metric %q: unable to parse OTTL condition", defaultMetricNameSpanEvents), }, { name: "invalid_condition_metric", @@ -437,7 +437,7 @@ func TestConfigErrors(t *testing.T) { }, }, }, - expect: fmt.Sprintf("metrics condition: metric %q: unable to parse OTTL statement", defaultMetricNameMetrics), + expect: fmt.Sprintf("metrics condition: metric %q: unable to parse OTTL condition", defaultMetricNameMetrics), }, { name: "invalid_condition_datapoint", @@ -449,7 +449,7 @@ func TestConfigErrors(t *testing.T) { }, }, }, - expect: fmt.Sprintf("datapoints condition: metric %q: unable to parse OTTL statement", defaultMetricNameDataPoints), + expect: fmt.Sprintf("datapoints condition: metric %q: unable to parse OTTL condition", defaultMetricNameDataPoints), }, { name: "invalid_condition_log", @@ -461,7 +461,7 @@ func TestConfigErrors(t *testing.T) { }, }, }, - expect: fmt.Sprintf("logs condition: metric %q: unable to parse OTTL statement", defaultMetricNameLogs), + expect: fmt.Sprintf("logs condition: metric %q: unable to parse OTTL condition", defaultMetricNameLogs), }, } diff --git a/internal/filter/filterottl/filter.go b/internal/filter/filterottl/filter.go index 4493ba0bd6ea..6324c8a35bd9 100644 --- a/internal/filter/filterottl/filter.go +++ b/internal/filter/filterottl/filter.go @@ -20,132 +20,94 @@ import ( // The passed in functions should use the ottlspan.TransformContext. // If a function named `match` is not present in the function map it will be added automatically so that parsing works as expected func NewBoolExprForSpan(conditions []string, functions map[string]ottl.Factory[ottlspan.TransformContext], errorMode ottl.ErrorMode, set component.TelemetrySettings) (expr.BoolExpr[ottlspan.TransformContext], error) { - match := newMatchFactory[ottlspan.TransformContext]() - if _, ok := functions[match.Name()]; !ok { - functions[match.Name()] = match - } - statmentsStr := conditionsToStatements(conditions) parser, err := ottlspan.NewParser(functions, set) if err != nil { return nil, err } - statements, err := parser.ParseStatements(statmentsStr) + statements, err := parser.ParseConditions(conditions) if err != nil { return nil, err } - s := ottlspan.NewStatements(statements, set, ottlspan.WithErrorMode(errorMode)) - return &s, nil + c := ottlspan.NewConditionSequence(statements, set, ottlspan.WithConditionSequenceErrorMode(errorMode)) + return &c, nil } // NewBoolExprForSpanEvent creates a BoolExpr[ottlspanevent.TransformContext] that will return true if any of the given OTTL conditions evaluate to true. // The passed in functions should use the ottlspanevent.TransformContext. // If a function named `match` is not present in the function map it will be added automatically so that parsing works as expected func NewBoolExprForSpanEvent(conditions []string, functions map[string]ottl.Factory[ottlspanevent.TransformContext], errorMode ottl.ErrorMode, set component.TelemetrySettings) (expr.BoolExpr[ottlspanevent.TransformContext], error) { - match := newMatchFactory[ottlspanevent.TransformContext]() - if _, ok := functions[match.Name()]; !ok { - functions[match.Name()] = match - } - statmentsStr := conditionsToStatements(conditions) parser, err := ottlspanevent.NewParser(functions, set) if err != nil { return nil, err } - statements, err := parser.ParseStatements(statmentsStr) + statements, err := parser.ParseConditions(conditions) if err != nil { return nil, err } - s := ottlspanevent.NewStatements(statements, set, ottlspanevent.WithErrorMode(errorMode)) - return &s, nil + c := ottlspanevent.NewConditionSequence(statements, set, ottlspanevent.WithConditionSequenceErrorMode(errorMode)) + return &c, nil } // NewBoolExprForMetric creates a BoolExpr[ottlmetric.TransformContext] that will return true if any of the given OTTL conditions evaluate to true. // The passed in functions should use the ottlmetric.TransformContext. // If a function named `match` is not present in the function map it will be added automatically so that parsing works as expected func NewBoolExprForMetric(conditions []string, functions map[string]ottl.Factory[ottlmetric.TransformContext], errorMode ottl.ErrorMode, set component.TelemetrySettings) (expr.BoolExpr[ottlmetric.TransformContext], error) { - match := newMatchFactory[ottlmetric.TransformContext]() - if _, ok := functions[match.Name()]; !ok { - functions[match.Name()] = match - } - statmentsStr := conditionsToStatements(conditions) parser, err := ottlmetric.NewParser(functions, set) if err != nil { return nil, err } - statements, err := parser.ParseStatements(statmentsStr) + statements, err := parser.ParseConditions(conditions) if err != nil { return nil, err } - s := ottlmetric.NewStatements(statements, set, ottlmetric.WithErrorMode(errorMode)) - return &s, nil + c := ottlmetric.NewConditionSequence(statements, set, ottlmetric.WithConditionSequenceErrorMode(errorMode)) + return &c, nil } // NewBoolExprForDataPoint creates a BoolExpr[ottldatapoint.TransformContext] that will return true if any of the given OTTL conditions evaluate to true. // The passed in functions should use the ottldatapoint.TransformContext. // If a function named `match` is not present in the function map it will be added automatically so that parsing works as expected func NewBoolExprForDataPoint(conditions []string, functions map[string]ottl.Factory[ottldatapoint.TransformContext], errorMode ottl.ErrorMode, set component.TelemetrySettings) (expr.BoolExpr[ottldatapoint.TransformContext], error) { - match := newMatchFactory[ottldatapoint.TransformContext]() - if _, ok := functions[match.Name()]; !ok { - functions[match.Name()] = match - } - statmentsStr := conditionsToStatements(conditions) parser, err := ottldatapoint.NewParser(functions, set) if err != nil { return nil, err } - statements, err := parser.ParseStatements(statmentsStr) + statements, err := parser.ParseConditions(conditions) if err != nil { return nil, err } - s := ottldatapoint.NewStatements(statements, set, ottldatapoint.WithErrorMode(errorMode)) - return &s, nil + c := ottldatapoint.NewConditionSequence(statements, set, ottldatapoint.WithConditionSequenceErrorMode(errorMode)) + return &c, nil } // NewBoolExprForLog creates a BoolExpr[ottllog.TransformContext] that will return true if any of the given OTTL conditions evaluate to true. // The passed in functions should use the ottllog.TransformContext. // If a function named `match` is not present in the function map it will be added automatically so that parsing works as expected func NewBoolExprForLog(conditions []string, functions map[string]ottl.Factory[ottllog.TransformContext], errorMode ottl.ErrorMode, set component.TelemetrySettings) (expr.BoolExpr[ottllog.TransformContext], error) { - match := newMatchFactory[ottllog.TransformContext]() - if _, ok := functions[match.Name()]; !ok { - functions[match.Name()] = match - } - statmentsStr := conditionsToStatements(conditions) parser, err := ottllog.NewParser(functions, set) if err != nil { return nil, err } - statements, err := parser.ParseStatements(statmentsStr) + statements, err := parser.ParseConditions(conditions) if err != nil { return nil, err } - s := ottllog.NewStatements(statements, set, ottllog.WithErrorMode(errorMode)) - return &s, nil + c := ottllog.NewConditionSequence(statements, set, ottllog.WithConditionSequenceErrorMode(errorMode)) + return &c, nil } // NewBoolExprForResource creates a BoolExpr[ottlresource.TransformContext] that will return true if any of the given OTTL conditions evaluate to true. // The passed in functions should use the ottlresource.TransformContext. // If a function named `match` is not present in the function map it will be added automatically so that parsing works as expected func NewBoolExprForResource(conditions []string, functions map[string]ottl.Factory[ottlresource.TransformContext], errorMode ottl.ErrorMode, set component.TelemetrySettings) (expr.BoolExpr[ottlresource.TransformContext], error) { - match := newMatchFactory[ottlresource.TransformContext]() - if _, ok := functions[match.Name()]; !ok { - functions[match.Name()] = match - } - statmentsStr := conditionsToStatements(conditions) parser, err := ottlresource.NewParser(functions, set) if err != nil { return nil, err } - statements, err := parser.ParseStatements(statmentsStr) + statements, err := parser.ParseConditions(conditions) if err != nil { return nil, err } - s := ottlresource.NewStatements(statements, set, ottlresource.WithErrorMode(errorMode)) - return &s, nil -} - -func conditionsToStatements(conditions []string) []string { - statements := make([]string, len(conditions)) - for i, condition := range conditions { - statements[i] = "match() where " + condition - } - return statements + c := ottlresource.NewConditionSequence(statements, set, ottlresource.WithConditionSequenceErrorMode(errorMode)) + return &c, nil } diff --git a/internal/filter/filterottl/functions.go b/internal/filter/filterottl/functions.go index 82c9c6b79997..355a148f6cb6 100644 --- a/internal/filter/filterottl/functions.go +++ b/internal/filter/filterottl/functions.go @@ -20,15 +20,15 @@ import ( ) func StandardSpanFuncs() map[string]ottl.Factory[ottlspan.TransformContext] { - return standardFuncs[ottlspan.TransformContext]() + return ottlfuncs.StandardConverters[ottlspan.TransformContext]() } func StandardSpanEventFuncs() map[string]ottl.Factory[ottlspanevent.TransformContext] { - return standardFuncs[ottlspanevent.TransformContext]() + return ottlfuncs.StandardConverters[ottlspanevent.TransformContext]() } func StandardMetricFuncs() map[string]ottl.Factory[ottlmetric.TransformContext] { - m := standardFuncs[ottlmetric.TransformContext]() + m := ottlfuncs.StandardConverters[ottlmetric.TransformContext]() hasAttributeOnDatapointFactory := newHasAttributeOnDatapointFactory() hasAttributeKeyOnDatapointFactory := newHasAttributeKeyOnDatapointFactory() m[hasAttributeOnDatapointFactory.Name()] = hasAttributeOnDatapointFactory @@ -37,36 +37,15 @@ func StandardMetricFuncs() map[string]ottl.Factory[ottlmetric.TransformContext] } func StandardDataPointFuncs() map[string]ottl.Factory[ottldatapoint.TransformContext] { - return standardFuncs[ottldatapoint.TransformContext]() + return ottlfuncs.StandardConverters[ottldatapoint.TransformContext]() } func StandardLogFuncs() map[string]ottl.Factory[ottllog.TransformContext] { - return standardFuncs[ottllog.TransformContext]() + return ottlfuncs.StandardConverters[ottllog.TransformContext]() } func StandardResourceFuncs() map[string]ottl.Factory[ottlresource.TransformContext] { - return standardFuncs[ottlresource.TransformContext]() -} - -func standardFuncs[K any]() map[string]ottl.Factory[K] { - m := ottlfuncs.StandardConverters[K]() - f := newMatchFactory[K]() - m[f.Name()] = f - return m -} - -func newMatchFactory[K any]() ottl.Factory[K] { - return ottl.NewFactory("match", nil, createMatchFunction[K]) -} - -func createMatchFunction[K any](_ ottl.FunctionContext, _ ottl.Arguments) (ottl.ExprFunc[K], error) { - return matchFn[K]() -} - -func matchFn[K any]() (ottl.ExprFunc[K], error) { - return func(context.Context, K) (any, error) { - return true, nil - }, nil + return ottlfuncs.StandardConverters[ottlresource.TransformContext]() } type hasAttributeOnDatapointArguments struct { diff --git a/pkg/ottl/contexts/ottldatapoint/datapoint.go b/pkg/ottl/contexts/ottldatapoint/datapoint.go index 3d2ac0e22a2f..6b95f27ea06d 100644 --- a/pkg/ottl/contexts/ottldatapoint/datapoint.go +++ b/pkg/ottl/contexts/ottldatapoint/datapoint.go @@ -98,6 +98,22 @@ func NewStatements(statements []*ottl.Statement[TransformContext], telemetrySett return s } +type ConditionSequenceOption func(*ottl.ConditionSequence[TransformContext]) + +func WithConditionSequenceErrorMode(errorMode ottl.ErrorMode) ConditionSequenceOption { + return func(c *ottl.ConditionSequence[TransformContext]) { + ottl.WithConditionSequenceErrorMode[TransformContext](errorMode)(c) + } +} + +func NewConditionSequence(conditions []*ottl.Condition[TransformContext], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption) ottl.ConditionSequence[TransformContext] { + c := ottl.NewConditionSequence(conditions, telemetrySettings) + for _, op := range options { + op(&c) + } + return c +} + var symbolTable = map[ottl.EnumSymbol]ottl.Enum{ "FLAG_NONE": 0, "FLAG_NO_RECORDED_VALUE": 1, diff --git a/pkg/ottl/contexts/ottllog/log.go b/pkg/ottl/contexts/ottllog/log.go index c9fe394bc4c5..24e1a80e02d7 100644 --- a/pkg/ottl/contexts/ottllog/log.go +++ b/pkg/ottl/contexts/ottllog/log.go @@ -88,6 +88,22 @@ func NewStatements(statements []*ottl.Statement[TransformContext], telemetrySett return s } +type ConditionSequenceOption func(*ottl.ConditionSequence[TransformContext]) + +func WithConditionSequenceErrorMode(errorMode ottl.ErrorMode) ConditionSequenceOption { + return func(c *ottl.ConditionSequence[TransformContext]) { + ottl.WithConditionSequenceErrorMode[TransformContext](errorMode)(c) + } +} + +func NewConditionSequence(conditions []*ottl.Condition[TransformContext], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption) ottl.ConditionSequence[TransformContext] { + c := ottl.NewConditionSequence(conditions, telemetrySettings) + for _, op := range options { + op(&c) + } + return c +} + var symbolTable = map[ottl.EnumSymbol]ottl.Enum{ "SEVERITY_NUMBER_UNSPECIFIED": ottl.Enum(plog.SeverityNumberUnspecified), "SEVERITY_NUMBER_TRACE": ottl.Enum(plog.SeverityNumberTrace), diff --git a/pkg/ottl/contexts/ottlmetric/metrics.go b/pkg/ottl/contexts/ottlmetric/metrics.go index 92267ecadc7d..8b97f04be700 100644 --- a/pkg/ottl/contexts/ottlmetric/metrics.go +++ b/pkg/ottl/contexts/ottlmetric/metrics.go @@ -92,6 +92,22 @@ func NewStatements(statements []*ottl.Statement[TransformContext], telemetrySett return s } +type ConditionSequenceOption func(*ottl.ConditionSequence[TransformContext]) + +func WithConditionSequenceErrorMode(errorMode ottl.ErrorMode) ConditionSequenceOption { + return func(c *ottl.ConditionSequence[TransformContext]) { + ottl.WithConditionSequenceErrorMode[TransformContext](errorMode)(c) + } +} + +func NewConditionSequence(conditions []*ottl.Condition[TransformContext], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption) ottl.ConditionSequence[TransformContext] { + c := ottl.NewConditionSequence(conditions, telemetrySettings) + for _, op := range options { + op(&c) + } + return c +} + var symbolTable = internal.MetricSymbolTable func parseEnum(val *ottl.EnumSymbol) (*ottl.Enum, error) { diff --git a/pkg/ottl/contexts/ottlresource/resource.go b/pkg/ottl/contexts/ottlresource/resource.go index 921d00311342..a908d57085dc 100644 --- a/pkg/ottl/contexts/ottlresource/resource.go +++ b/pkg/ottl/contexts/ottlresource/resource.go @@ -71,6 +71,22 @@ func NewStatements(statements []*ottl.Statement[TransformContext], telemetrySett return s } +type ConditionSequenceOption func(*ottl.ConditionSequence[TransformContext]) + +func WithConditionSequenceErrorMode(errorMode ottl.ErrorMode) ConditionSequenceOption { + return func(c *ottl.ConditionSequence[TransformContext]) { + ottl.WithConditionSequenceErrorMode[TransformContext](errorMode)(c) + } +} + +func NewConditionSequence(conditions []*ottl.Condition[TransformContext], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption) ottl.ConditionSequence[TransformContext] { + c := ottl.NewConditionSequence(conditions, telemetrySettings) + for _, op := range options { + op(&c) + } + return c +} + func parseEnum(_ *ottl.EnumSymbol) (*ottl.Enum, error) { return nil, fmt.Errorf("resource context does not provide Enum support") } diff --git a/pkg/ottl/contexts/ottlscope/scope.go b/pkg/ottl/contexts/ottlscope/scope.go index 9c8ce9406c81..67e88b8a175b 100644 --- a/pkg/ottl/contexts/ottlscope/scope.go +++ b/pkg/ottl/contexts/ottlscope/scope.go @@ -78,6 +78,22 @@ func NewStatements(statements []*ottl.Statement[TransformContext], telemetrySett return s } +type ConditionSequenceOption func(*ottl.ConditionSequence[TransformContext]) + +func WithConditionSequenceErrorMode(errorMode ottl.ErrorMode) ConditionSequenceOption { + return func(c *ottl.ConditionSequence[TransformContext]) { + ottl.WithConditionSequenceErrorMode[TransformContext](errorMode)(c) + } +} + +func NewConditionSequence(conditions []*ottl.Condition[TransformContext], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption) ottl.ConditionSequence[TransformContext] { + c := ottl.NewConditionSequence(conditions, telemetrySettings) + for _, op := range options { + op(&c) + } + return c +} + func parseEnum(_ *ottl.EnumSymbol) (*ottl.Enum, error) { return nil, fmt.Errorf("instrumentation scope context does not provide Enum support") } diff --git a/pkg/ottl/contexts/ottlspan/span.go b/pkg/ottl/contexts/ottlspan/span.go index 34f3d2bcf599..f1585b550e44 100644 --- a/pkg/ottl/contexts/ottlspan/span.go +++ b/pkg/ottl/contexts/ottlspan/span.go @@ -85,6 +85,22 @@ func NewStatements(statements []*ottl.Statement[TransformContext], telemetrySett return s } +type ConditionSequenceOption func(*ottl.ConditionSequence[TransformContext]) + +func WithConditionSequenceErrorMode(errorMode ottl.ErrorMode) ConditionSequenceOption { + return func(c *ottl.ConditionSequence[TransformContext]) { + ottl.WithConditionSequenceErrorMode[TransformContext](errorMode)(c) + } +} + +func NewConditionSequence(conditions []*ottl.Condition[TransformContext], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption) ottl.ConditionSequence[TransformContext] { + c := ottl.NewConditionSequence(conditions, telemetrySettings) + for _, op := range options { + op(&c) + } + return c +} + func parseEnum(val *ottl.EnumSymbol) (*ottl.Enum, error) { if val != nil { if enum, ok := internal.SpanSymbolTable[*val]; ok { diff --git a/pkg/ottl/contexts/ottlspanevent/span_events.go b/pkg/ottl/contexts/ottlspanevent/span_events.go index 70014a486560..00fcf82597d0 100644 --- a/pkg/ottl/contexts/ottlspanevent/span_events.go +++ b/pkg/ottl/contexts/ottlspanevent/span_events.go @@ -93,6 +93,22 @@ func NewStatements(statements []*ottl.Statement[TransformContext], telemetrySett return s } +type ConditionSequenceOption func(*ottl.ConditionSequence[TransformContext]) + +func WithConditionSequenceErrorMode(errorMode ottl.ErrorMode) ConditionSequenceOption { + return func(c *ottl.ConditionSequence[TransformContext]) { + ottl.WithConditionSequenceErrorMode[TransformContext](errorMode)(c) + } +} + +func NewConditionSequence(conditions []*ottl.Condition[TransformContext], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption) ottl.ConditionSequence[TransformContext] { + c := ottl.NewConditionSequence(conditions, telemetrySettings) + for _, op := range options { + op(&c) + } + return c +} + func parseEnum(val *ottl.EnumSymbol) (*ottl.Enum, error) { if val != nil { if enum, ok := internal.SpanSymbolTable[*val]; ok { diff --git a/pkg/ottl/parser.go b/pkg/ottl/parser.go index f1dbc18f3f4c..61308972b56f 100644 --- a/pkg/ottl/parser.go +++ b/pkg/ottl/parser.go @@ -32,6 +32,25 @@ func (e *ErrorMode) UnmarshalText(text []byte) error { } } +// TODO: move this and ErrorMode to a config.go file +type LogicOperation string + +const ( + And LogicOperation = "and" + Or LogicOperation = "or" +) + +func (l *LogicOperation) UnmarshalText(text []byte) error { + str := LogicOperation(strings.ToLower(string(text))) + switch str { + case And, Or: + *l = str + return nil + default: + return fmt.Errorf("unknown LogicOperation %v", str) + } +} + // Statement holds a top level Statement for processing telemetry data. A Statement is a combination of a function // invocation and the boolean expression to match telemetry for invoking the function. type Statement[K any] struct { @@ -306,3 +325,83 @@ func (s *Statements[K]) Eval(ctx context.Context, tCtx K) (bool, error) { } return false, nil } + +// ConditionSequence represents a list of Conditions that will be evaluated sequentially for a TransformContext +// and will handle errors returned by conditions based on an ErrorMode. +// By default, the conditions are ORed together, but they can be ANDed together using the WithLogicOperation option. +type ConditionSequence[K any] struct { + conditions []*Condition[K] + errorMode ErrorMode + telemetrySettings component.TelemetrySettings + logicOp LogicOperation +} + +type ConditionSequenceOption[K any] func(*ConditionSequence[K]) + +// WithConditionSequenceErrorMode sets the ErrorMode of a ConditionSequence +func WithConditionSequenceErrorMode[K any](errorMode ErrorMode) ConditionSequenceOption[K] { + return func(c *ConditionSequence[K]) { + c.errorMode = errorMode + } +} + +// WithLogicOperation sets the LogicOperation of a ConditionSequence +// When setting AND the conditions will be ANDed together. +// When setting OR the conditions will be ORed together. +func WithLogicOperation[K any](logicOp LogicOperation) ConditionSequenceOption[K] { + return func(c *ConditionSequence[K]) { + c.logicOp = logicOp + } +} + +// NewConditionSequence creates a new ConditionSequence with the provided Condition slice, ErrorMode, and component.TelemetrySettings. +// You may also augment the ConditionSequence with a slice of ConditionSequenceOption. +func NewConditionSequence[K any](conditions []*Condition[K], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption[K]) ConditionSequence[K] { + c := ConditionSequence[K]{ + conditions: conditions, + errorMode: PropagateError, + telemetrySettings: telemetrySettings, + logicOp: Or, + } + for _, op := range options { + op(&c) + } + return c +} + +// Eval evaluates the result of each Condition in the ConditionSequence. +// The boolean logic between conditions is based on the ConditionSequence's Logic Operator. +// If using the default OR LogicOperation, if any Condition evaluates to true, then true is returned and if all Conditions evaluate to false, then false is returned. +// If using the AND LogicOperation, if any Condition evaluates to false, then false is returned and if all Conditions evaluate to true, then true is returned. +// When the ErrorMode of the ConditionSequence is `propagate`, errors cause the evaluation to be false and an error is returned. +// When the ErrorMode of the ConditionSequence is `ignore`, errors cause the evaluation to continue to the next condition. +// When using the AND LogicOperation with the `ignore` ErrorMode the sequence will evaluate to false if all conditions error. +func (c *ConditionSequence[K]) Eval(ctx context.Context, tCtx K) (bool, error) { + var atLeastOneMatch bool + for _, condition := range c.conditions { + match, err := condition.Eval(ctx, tCtx) + if err != nil { + if c.errorMode == PropagateError { + err = fmt.Errorf("failed to eval condition: %v, %w", condition.origText, err) + return false, err + } + c.telemetrySettings.Logger.Warn("failed to eval condition", zap.Error(err), zap.String("condition", condition.origText)) + continue + } + if match { + if c.logicOp == Or { + return true, nil + } + atLeastOneMatch = true + } + if !match && c.logicOp == And { + return false, nil + } + } + // When ANDing it is possible to arrive here not because everything was true, but because everything errored and was ignored. + // In that situation, we don't want to return True when no conditions actually passed. In a situation when everything failed + // we are essentially left with an empty set, which is normally evaluated in mathematics as False. We will use that + // idea to return False when ANDing and everything errored. We use atLeastOneMatch here to return true if anything did match. + // It is not possible to get here if any condition during an AND explicitly failed. + return c.logicOp == And && atLeastOneMatch, nil +} diff --git a/pkg/ottl/parser_test.go b/pkg/ottl/parser_test.go index b5365ca6a2f4..cee79d9bf5e4 100644 --- a/pkg/ottl/parser_test.go +++ b/pkg/ottl/parser_test.go @@ -2233,3 +2233,181 @@ func Test_Statements_Eval_Error(t *testing.T) { }) } } + +func Test_ConditionSequence_Eval(t *testing.T) { + tests := []struct { + name string + conditions []boolExpressionEvaluator[any] + function ExprFunc[any] + errorMode ErrorMode + logicOp LogicOperation + expectedResult bool + }{ + { + name: "True with OR", + conditions: []boolExpressionEvaluator[any]{ + alwaysTrue[any], + }, + errorMode: IgnoreError, + logicOp: Or, + expectedResult: true, + }, + { + name: "At least one True with OR", + conditions: []boolExpressionEvaluator[any]{ + alwaysFalse[any], + alwaysFalse[any], + alwaysTrue[any], + }, + errorMode: IgnoreError, + logicOp: Or, + expectedResult: true, + }, + { + name: "False with OR", + conditions: []boolExpressionEvaluator[any]{ + alwaysFalse[any], + alwaysFalse[any], + }, + errorMode: IgnoreError, + logicOp: Or, + expectedResult: false, + }, + { + name: "Single erroring condition is treated as false when using Ignore with OR", + conditions: []boolExpressionEvaluator[any]{ + func(context.Context, any) (bool, error) { + return true, fmt.Errorf("test") + }, + }, + errorMode: IgnoreError, + logicOp: Or, + expectedResult: false, + }, + { + name: "erroring condition is ignored when using Ignore with OR", + conditions: []boolExpressionEvaluator[any]{ + func(context.Context, any) (bool, error) { + return true, fmt.Errorf("test") + }, + alwaysTrue[any], + }, + errorMode: IgnoreError, + logicOp: Or, + expectedResult: true, + }, + { + name: "True with AND", + conditions: []boolExpressionEvaluator[any]{ + alwaysTrue[any], + alwaysTrue[any], + }, + errorMode: IgnoreError, + logicOp: And, + expectedResult: true, + }, + { + name: "At least one False with AND", + conditions: []boolExpressionEvaluator[any]{ + alwaysFalse[any], + alwaysTrue[any], + alwaysTrue[any], + }, + errorMode: IgnoreError, + logicOp: And, + expectedResult: false, + }, + { + name: "False with AND", + conditions: []boolExpressionEvaluator[any]{ + alwaysFalse[any], + }, + errorMode: IgnoreError, + logicOp: And, + expectedResult: false, + }, + { + name: "Single erroring condition is treated as false when using Ignore with AND", + conditions: []boolExpressionEvaluator[any]{ + func(context.Context, any) (bool, error) { + return true, fmt.Errorf("test") + }, + }, + errorMode: IgnoreError, + logicOp: And, + expectedResult: false, + }, + { + name: "erroring condition is ignored when using Ignore with AND", + conditions: []boolExpressionEvaluator[any]{ + func(context.Context, any) (bool, error) { + return true, fmt.Errorf("test") + }, + alwaysTrue[any], + }, + errorMode: IgnoreError, + logicOp: And, + expectedResult: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var rawStatements []*Condition[any] + for _, condition := range tt.conditions { + rawStatements = append(rawStatements, &Condition[any]{ + condition: BoolExpr[any]{condition}, + }) + } + + conditions := ConditionSequence[any]{ + conditions: rawStatements, + telemetrySettings: componenttest.NewNopTelemetrySettings(), + errorMode: tt.errorMode, + logicOp: tt.logicOp, + } + + result, err := conditions.Eval(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + }) + } +} + +func Test_ConditionSequence_Eval_Error(t *testing.T) { + tests := []struct { + name string + conditions []boolExpressionEvaluator[any] + function ExprFunc[any] + errorMode ErrorMode + }{ + { + name: "Propagate Error from function", + conditions: []boolExpressionEvaluator[any]{ + func(context.Context, any) (bool, error) { + return true, fmt.Errorf("test") + }, + }, + errorMode: PropagateError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var rawConditions []*Condition[any] + for _, condition := range tt.conditions { + rawConditions = append(rawConditions, &Condition[any]{ + condition: BoolExpr[any]{condition}, + }) + } + + conditions := ConditionSequence[any]{ + conditions: rawConditions, + telemetrySettings: componenttest.NewNopTelemetrySettings(), + errorMode: tt.errorMode, + } + + result, err := conditions.Eval(context.Background(), nil) + assert.Error(t, err) + assert.False(t, result) + }) + } +}