Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pkg/ottl] Basic math capabilities #15711

Merged
merged 16 commits into from
Nov 10, 2022
Merged
19 changes: 19 additions & 0 deletions .chloggen/ottl-add-math-to-grammar.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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 ability to perform basic (+, -, *, and /) arithmetic operations on ints and floats. Paths and Functions that return ints/floats are allowed.

# One or more tracking issues related to the change
issues: [15711]

# (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: |
Affected components
- routingprocessor
- transformprocessor
42 changes: 33 additions & 9 deletions pkg/ottl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The OTTL is signal agnostic; it is not aware of the type of telemetry on which i

## Grammar

The OTTL grammar includes Invocations, Values and Expressions.
The OTTL grammar includes Invocations, Values and Boolean Expressions.

### Invocations

Expand All @@ -27,7 +27,7 @@ Example Invocations

The OTTL will use reflection to determine parameter types when parsing an invocation within a statement.

The following types are supported for single parameter values:
When developing functions that represent invocations, the following types are supported for single parameter values:
- `Setter`
- `GetSetter`
- `Getter`
Expand All @@ -46,12 +46,13 @@ For slice parameters, the following types are supported:

### Values

Values are passed as input to an Invocation or are used in an Expression. Values can take the form of:
Values are passed as input to an Invocation or are used in a Boolean Expression. Values can take the form of:
- [Paths](#paths).
- [Lists](#lists).
- [Literals](#literals).
- [Enums](#enums).
- [Invocations](#invocations).
- [Math Expressions](#math_expressions)

Invocations as Values allows calling functions as parameters to other functions. See [Invocations](#invocations) for details on Invocation syntax.

Expand All @@ -71,7 +72,7 @@ Example Paths

#### Lists

A List Value comprises a sequence of Expressions or supported Literals.
A List Value comprises a sequence of Values.

Example List Values:
- `[]`
Expand Down Expand Up @@ -106,14 +107,37 @@ Within the grammar Enums are always used as `int64`. As a result, the Enum's sy

When defining a function that will be used as an Invocation by the OTTL, if the function needs to take an Enum then the function must use the `Enum` type for that argument, not an `int64`.

### Expressions
#### Math Expressions

Expressions allow a decision to be made about whether an Invocation should be called. Expressions are optional. When used, the parsed statement will include a `Condition`, which can be used to evaluate the result of the statement's Expression. Expressions always evaluate to a boolean value (true or false).
Math Expressions represent arithmetic calculations. They support `+`, `-`, `*`, and `/`, along with `()` for grouping.

Expressions consist of the literal string `where` followed by one or more Booleans (see below).
Math Expressions currently only support `int64` and `float64`.
Math Expressions support `Paths` and `Invocations` that return supported types.
Note that `*` and `/` take precedence over `+` 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.
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
It is up to the function using the Math Expression to determine what to do with that error and the default return value of `nil`.
Division by zero is gracefully handled with an error, but other arithmetic operations that would result in a panic will still result in a panic.
Division of integers results in an integer and follows Go's rules for division of integers.

Since Math Expressions support `Path` and `Invocation`, they are evaluated during data processing.
__As a result, in order for a function to be able to accept an Math Expressions as a parameter it must use a `Getter`.__

Example Math Expressions
- `1 + 1`
- `end_time_unix_nano - end_time_unix_nano`
- `sum([1, 2, 3, 4]) + (10 / 1) - 1`


### Boolean Expressions

Boolean Expressions allow a decision to be made about whether an Invocation should be called. Boolean Expressions are optional. When used, the parsed statement will include a `Condition`, which can be used to evaluate the result of the statement's Boolean Expression. Boolean Expressions always evaluate to a boolean value (true or false).

Boolean Expressions consist of the literal string `where` followed by one or more Booleans (see below).
Booleans can be joined with the literal strings `and` and `or`.
Note that `and` expressions have higher precedence than `or`.
Expressions can be grouped with parentheses to override evaluation precedence.
Note that `and` Boolean Expressions have higher precedence than `or`.
Boolean Expressions can be grouped with parentheses to override evaluation precedence.

### Booleans

Expand Down
18 changes: 10 additions & 8 deletions pkg/ottl/boolean_value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ func valueFor(x any) value {
switch {
case v == "NAME":
// if the string is NAME construct a path of "name".
val.Path = &Path{
Fields: []Field{
{
Name: "name",
val.Literal = &mathExprLiteral{
Path: &Path{
Fields: []Field{
{
Name: "name",
},
},
},
}
Expand All @@ -51,13 +53,13 @@ func valueFor(x any) value {
val.String = ottltest.Strp(v)
}
case float64:
val.Float = ottltest.Floatp(v)
val.Literal = &mathExprLiteral{Float: ottltest.Floatp(v)}
case *float64:
val.Float = v
val.Literal = &mathExprLiteral{Float: v}
case int:
val.Int = ottltest.Intp(int64(v))
val.Literal = &mathExprLiteral{Int: ottltest.Intp(int64(v))}
case *int64:
val.Int = v
val.Literal = &mathExprLiteral{Int: v}
case bool:
val.Bool = booleanp(boolean(v))
case nil:
Expand Down
37 changes: 21 additions & 16 deletions pkg/ottl/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@ func (p *Parser[K]) newGetter(val value) (Getter[K], error) {
if s := val.String; s != nil {
return &literal[K]{value: *s}, nil
}
if f := val.Float; f != nil {
return &literal[K]{value: *f}, nil
}
if i := val.Int; i != nil {
return &literal[K]{value: *i}, nil
}
if b := val.Bool; b != nil {
return &literal[K]{value: bool(*b)}, nil
}
Expand All @@ -100,19 +94,30 @@ func (p *Parser[K]) newGetter(val value) (Getter[K], error) {
return &literal[K]{value: int64(*enum)}, nil
}

if val.Path != nil {
return p.pathParser(val.Path)
if eL := val.Literal; eL != nil {
if f := eL.Float; f != nil {
return &literal[K]{value: *f}, nil
}
if i := eL.Int; i != nil {
return &literal[K]{value: *i}, nil
}
if eL.Path != nil {
return p.pathParser(eL.Path)
}
if eL.Invocation != nil {
call, err := p.newFunctionCall(*eL.Invocation)
if err != nil {
return nil, err
}
return &exprGetter[K]{
expr: call,
}, nil
}
}

if val.Invocation == nil {
if val.MathExpression == nil {
// In practice, can't happen since the DSL grammar guarantees one is set
return nil, fmt.Errorf("no value field set. This is a bug in the OpenTelemetry Transformation Language")
}
call, err := p.newFunctionCall(*val.Invocation)
if err != nil {
return nil, err
}
return &exprGetter[K]{
expr: call,
}, nil
return p.evaluateMathExpression(val.MathExpression)
}
26 changes: 17 additions & 9 deletions pkg/ottl/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,18 @@ func Test_newGetter(t *testing.T) {
{
name: "float literal",
val: value{
Float: ottltest.Floatp(1.2),
Literal: &mathExprLiteral{
Float: ottltest.Floatp(1.2),
},
},
want: 1.2,
},
{
name: "int literal",
val: value{
Int: ottltest.Intp(12),
Literal: &mathExprLiteral{
Int: ottltest.Intp(12),
},
},
want: int64(12),
},
Expand All @@ -79,12 +83,14 @@ func Test_newGetter(t *testing.T) {
want: true,
},
{
name: "path expression",
name: "path mathExpression",
val: value{
Path: &Path{
Fields: []Field{
{
Name: "name",
Literal: &mathExprLiteral{
Path: &Path{
Fields: []Field{
{
Name: "name",
},
},
},
},
Expand All @@ -94,8 +100,10 @@ func Test_newGetter(t *testing.T) {
{
name: "function call",
val: value{
Invocation: &invocation{
Function: "hello",
Literal: &mathExprLiteral{
Invocation: &invocation{
Function: "hello",
},
},
},
want: "world",
Expand Down
17 changes: 10 additions & 7 deletions pkg/ottl/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ func (p *Parser[K]) buildArg(argDef value, argType reflect.Type, index int) (any
case strings.HasPrefix(name, "Setter"):
fallthrough
case strings.HasPrefix(name, "GetSetter"):
arg, err := p.pathParser(argDef.Path)
if argDef.Literal == nil || argDef.Literal.Path == nil {
return nil, fmt.Errorf("invalid argument at position %v must be a Path", index)
}
arg, err := p.pathParser(argDef.Literal.Path)
if err != nil {
return nil, fmt.Errorf("invalid argument at position %v %w", index, err)
}
Expand All @@ -159,19 +162,19 @@ func (p *Parser[K]) buildArg(argDef value, argType reflect.Type, index int) (any
return *arg, nil
case name == reflect.String.String():
if argDef.String == nil {
return nil, fmt.Errorf("invalid argument at position %v, must be an string", index)
return nil, fmt.Errorf("invalid argument at position %v, must be a string", index)
}
return *argDef.String, nil
case name == reflect.Float64.String():
if argDef.Float == nil {
return nil, fmt.Errorf("invalid argument at position %v, must be an float", index)
if argDef.Literal == nil || argDef.Literal.Float == nil {
return nil, fmt.Errorf("invalid argument at position %v, must be a float", index)
}
return *argDef.Float, nil
return *argDef.Literal.Float, nil
case name == reflect.Int64.String():
if argDef.Int == nil {
if argDef.Literal == nil || argDef.Literal.Int == nil {
return nil, fmt.Errorf("invalid argument at position %v, must be an int", index)
}
return *argDef.Int, nil
return *argDef.Literal.Int, nil
case name == reflect.Bool.String():
if argDef.Bool == nil {
return nil, fmt.Errorf("invalid argument at position %v, must be a bool", index)
Expand Down
Loading