diff --git a/.chloggen/feat_math-time-duration.yaml b/.chloggen/feat_math-time-duration.yaml new file mode 100755 index 000000000000..aaf29bcd0852 --- /dev/null +++ b/.chloggen/feat_math-time-duration.yaml @@ -0,0 +1,20 @@ +# Use this changelog template to create an entry for release notes. +# If your change doesn't affect end users, such as a test fix or a tooling change, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. + +# 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 support for using addition and subtraction with time and duration' + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [22009] + +# (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: diff --git a/pkg/ottl/README.md b/pkg/ottl/README.md index 03e7ccdb1f84..d8f953db154b 100644 --- a/pkg/ottl/README.md +++ b/pkg/ottl/README.md @@ -172,9 +172,18 @@ When defining an OTTL function, if the function needs to take an Enum then the f Math Expressions represent arithmetic calculations. They support `+`, `-`, `*`, and `/`, along with `()` for grouping. -Math Expressions currently only support `int64` and `float64`. +Math Expressions currently support `int64`, `float64`, `time.Time` and `time.Duration`. +For `time.Time` and `time.Duration`, only `+` and `-` are supported with the following rules: + - A `time.Time` `-` a `time.Time` yields a `time.Duration`. + - A `time.Duration` `+` a `time.Time` yields a `time.Time`. + - A `time.Time` `+` a `time.Duration` yields a `time.Time`. + - A `time.Time` `-` a `time.Duration` yields a `time.Time`. + - A `time.Duration` `+` a `time.Duration` yields a `time.Duration`. + - A `time.Duration` `-` a `time.Duration` yields a `time.Duration`. + Math Expressions support `Paths` and `Editors` that return supported types. Note that `*` and `/` take precedence over `+` and `-`. +Also note that `time.Time` and `time.Duration` can only be used with `+` and `-`. Operations that share the same level of precedence will be executed in the order that they appear in the Math Expression. Math Expressions can be grouped with parentheses to override evaluation precedence. Math Expressions that mix `int64` and `float64` will result in an error. diff --git a/pkg/ottl/math.go b/pkg/ottl/math.go index e26c298ec199..897ed22c3ade 100644 --- a/pkg/ottl/math.go +++ b/pkg/ottl/math.go @@ -6,6 +6,7 @@ package ottl // import "github.com/open-telemetry/opentelemetry-collector-contri import ( "context" "fmt" + "time" ) func (p *Parser[K]) evaluateMathExpression(expr *mathExpression) (Getter[K], error) { @@ -98,14 +99,68 @@ func attemptMathOperation[K any](lhs Getter[K], op mathOp, rhs Getter[K]) Getter default: return nil, fmt.Errorf("%v must be int64 or float64", y) } + case time.Time: + return performOpTime(newX, y, op) + case time.Duration: + return performOpDuration(newX, y, op) default: - return nil, fmt.Errorf("%v must be int64 or float64", x) + return nil, fmt.Errorf("%v must be int64, float64, time.Time or time.Duration", x) } }, }, } } +func performOpTime(x time.Time, y any, op mathOp) (any, error) { + switch op { + case ADD: + switch newY := y.(type) { + case time.Duration: + result := x.Add(newY) + return result, nil + default: + return nil, fmt.Errorf("time.Time must be added to time.Duration; found %v instead", y) + } + case SUB: + switch newY := y.(type) { + case time.Time: + result := x.Sub(newY) + return result, nil + case time.Duration: + result := x.Add(-1 * newY) + return result, nil + default: + return nil, fmt.Errorf("time.Time or time.Duration must be subtracted from time.Time; found %v instead", y) + } + } + return nil, fmt.Errorf("only addition and subtraction supported for time.Time and time.Duration") +} + +func performOpDuration(x time.Duration, y any, op mathOp) (any, error) { + switch op { + case ADD: + switch newY := y.(type) { + case time.Duration: + result := x + newY + return result, nil + case time.Time: + result := newY.Add(x) + return result, nil + default: + return nil, fmt.Errorf("time.Duration must be added to time.Duration or time.Time; found %v instead", y) + } + case SUB: + switch newY := y.(type) { + case time.Duration: + result := x - newY + return result, nil + default: + return nil, fmt.Errorf("time.Duration must be subtracted from time.Duration; found %v instead", y) + } + } + return nil, fmt.Errorf("only addition and subtraction supported for time.Time and time.Duration") +} + func performOp[N int64 | float64](x N, y N, op mathOp) (N, error) { switch op { case ADD: diff --git a/pkg/ottl/math_test.go b/pkg/ottl/math_test.go index ddd59e85cec4..bb279a7bf29b 100644 --- a/pkg/ottl/math_test.go +++ b/pkg/ottl/math_test.go @@ -8,9 +8,14 @@ import ( "fmt" "math" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component/componenttest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/timeutils" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottltest" ) func mathParsePath(val *Path) (GetSetter[interface{}], error) { @@ -56,6 +61,27 @@ func threePointOne[K any]() (ExprFunc[K], error) { }, nil } +func testTime[K any](time string, format string) (ExprFunc[K], error) { + loc, err := timeutils.GetLocation(nil, &format) + if err != nil { + return nil, err + } + return func(_ context.Context, tCtx K) (interface{}, error) { + timestamp, err := timeutils.ParseStrptime(format, time, loc) + return timestamp, err + }, nil +} + +func testDuration[K any](duration string) (ExprFunc[K], error) { + if duration != "" { + return func(_ context.Context, tCtx K) (interface{}, error) { + dur, err := time.ParseDuration(duration) + return dur, err + }, nil + } + return nil, fmt.Errorf("duration cannot be empty") +} + type sumArguments struct { Ints []int64 `ottlarg:"0"` } @@ -233,13 +259,253 @@ func Test_evaluateMathExpression(t *testing.T) { func Test_evaluateMathExpression_error(t *testing.T) { tests := []struct { - name string - input string + name string + input string + mathExpr *mathExpression + errorMsg string }{ { name: "divide by 0 is gracefully handled", input: "1 / 0", }, + { + name: "time DIV time", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("2023-04-12"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: DIV, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("2023-04-12"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + errorMsg: "only addition and subtraction supported", + }, + { + name: "dur MULT dur", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("100h100m100s100ns"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: MULT, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("1h1m1s1ns"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + errorMsg: "only addition and subtraction supported", + }, + { + name: "time ADD int", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("2023-04-12"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: ADD, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Int: ottltest.Intp(1), + }, + }, + }, + }, + }, + }, + errorMsg: "time.Time must be added to time.Duration", + }, + { + name: "dur SUB int", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("1h1m1s1ns"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: SUB, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Int: ottltest.Intp(5), + }, + }, + }, + }, + }, + }, + errorMsg: "time.Duration must be subtracted from time.Duration", + }, + { + name: "time ADD time", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("2023-04-12"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: ADD, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("2022-05-11"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + errorMsg: "time.Time must be added to time.Duration", + }, + { + name: "dur SUB time", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("2h"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: SUB, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("2000-10-30"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + errorMsg: "time.Duration must be subtracted from time.Duration", + }, } functions := CreateFactoryMap( @@ -247,6 +513,13 @@ func Test_evaluateMathExpression_error(t *testing.T) { createFactory("two", &struct{}{}, two[any]), createFactory("threePointOne", &struct{}{}, threePointOne[any]), createFactory("sum", &sumArguments{}, sum[any]), + createFactory("Time", &struct { + Time string `ottlarg:"0"` + Format string `ottlarg:"1"` + }{}, testTime[any]), + createFactory("Duration", &struct { + Duration string `ottlarg:"0"` + }{}, testDuration[any]), ) p, _ := NewParser[any]( @@ -260,15 +533,532 @@ func Test_evaluateMathExpression_error(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - parsed, err := mathParser.ParseString("", tt.input) - assert.NoError(t, err) + if tt.mathExpr != nil { + getter, err := p.evaluateMathExpression(tt.mathExpr) + if err != nil { + assert.Error(t, err) + assert.ErrorContains(t, err, tt.errorMsg) + } else { + result, err := getter.Get(context.Background(), nil) + assert.Nil(t, result) + assert.Error(t, err) + assert.ErrorContains(t, err, tt.errorMsg) + } - getter, err := p.evaluateMathExpression(parsed.MathExpression) - assert.NoError(t, err) + } else { + parsed, err := mathParser.ParseString("", tt.input) + assert.NoError(t, err) + + getter, err := p.evaluateMathExpression(parsed.MathExpression) + assert.NoError(t, err) + + result, err := getter.Get(context.Background(), nil) + assert.Nil(t, result) + assert.Error(t, err) + } - result, err := getter.Get(context.Background(), nil) - assert.Nil(t, result) - assert.Error(t, err) }) } } + +func Test_evaluateMathExpressionTimeDuration(t *testing.T) { + functions := CreateFactoryMap( + createFactory("Time", &struct { + Time string `ottlarg:"0"` + Format string `ottlarg:"1"` + }{}, testTime[any]), + createFactory("Duration", &struct { + Duration string `ottlarg:"0"` + }{}, testDuration[any]), + ) + + p, _ := NewParser( + functions, + mathParsePath, + componenttest.NewNopTelemetrySettings(), + WithEnumParser[any](testParseEnum), + ) + zeroSecs, err := time.ParseDuration("0s") + require.NoError(t, err) + fourtySevenHourseFourtyTwoMinutesTwentySevenSecs, err := time.ParseDuration("47h42m27s") + require.NoError(t, err) + oneHundredOne, err := time.ParseDuration("101h101m101s101ns") + require.NoError(t, err) + oneThousandHours, err := time.ParseDuration("1000h") + require.NoError(t, err) + threeTwentyEightMins, err := time.ParseDuration("328m") + require.NoError(t, err) + tenHoursetc, err := time.ParseDuration("10h47m48s11ns") + require.NoError(t, err) + + var tests = []struct { + name string + mathExpr *mathExpression + expected any + }{ + { + name: "time SUB time, no difference", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("2023-04-12"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: SUB, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("2023-04-12"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: zeroSecs, + }, + { + name: "time SUB time", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("1986-10-30T00:17:33"), + }, + { + String: ottltest.Strp("%Y-%m-%dT%H:%M:%S"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: SUB, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("1986-11-01"), + }, + { + String: ottltest.Strp("%Y-%m-%d"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: -fourtySevenHourseFourtyTwoMinutesTwentySevenSecs, + }, + { + name: "dur ADD time", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("10h"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: ADD, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("01-01-2000"), + }, + { + String: ottltest.Strp("%m-%d-%Y"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: time.Date(2000, 1, 1, 10, 0, 0, 0, time.Local), + }, + { + name: "time ADD dur", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("Feb 15, 2023"), + }, + { + String: ottltest.Strp("%b %d, %Y"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: ADD, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("10h"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: time.Date(2023, 2, 15, 10, 0, 0, 0, time.Local), + }, + { + name: "time ADD dur, complex dur", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("02/04/2023"), + }, + { + String: ottltest.Strp("%m/%d/%Y"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: ADD, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("1h2m3s"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: time.Date(2023, 2, 4, 1, 2, 3, 0, time.Local), + }, + { + name: "time SUB dur, complex dur", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("Mar 14 2023 17:02:59"), + }, + { + String: ottltest.Strp("%b %d %Y %H:%M:%S"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: SUB, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("11h2m58s"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: time.Date(2023, 3, 14, 6, 0, 1, 0, time.Local), + }, + { + name: "time SUB dur, nanosecs", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Time", + Arguments: []value{ + { + String: ottltest.Strp("Monday, May 01, 2023"), + }, + { + String: ottltest.Strp("%A, %B %d, %Y"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: SUB, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("100ns"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: time.Date(2023, 4, 30, 23, 59, 59, 999999900, time.Local), + }, + { + name: "dur ADD dur, complex durs", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("100h100m100s100ns"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: ADD, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("1h1m1s1ns"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: oneHundredOne, + }, + { + name: "dur ADD dur, zero dur", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("0h"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: ADD, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("1000h"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: oneThousandHours, + }, + { + name: "dur SUB dur, zero dur", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("0h"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: SUB, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("328m"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: -threeTwentyEightMins, + }, + { + name: "dur SUB dur, complex durs", + mathExpr: &mathExpression{ + Left: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("11h11ns"), + }, + }, + }, + }, + }, + }, + Right: []*opAddSubTerm{ + { + Operator: SUB, + Term: &addSubTerm{ + Left: &mathValue{ + Literal: &mathExprLiteral{ + Converter: &converter{ + Function: "Duration", + Arguments: []value{ + { + String: ottltest.Strp("12m12s"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: tenHoursetc, + }, + } + for _, tt := range tests { + getter, err := p.evaluateMathExpression(tt.mathExpr) + assert.NoError(t, err) + + result, err := getter.Get(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } +}