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 preform basic (+, -, *, and /) arithmetic operations on ints and floats. Paths and Functions that return ints/floats are allowed.
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved

# 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
34 changes: 27 additions & 7 deletions pkg/ottl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
- [Expressions](#expressions)

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

Expand Down Expand Up @@ -106,15 +107,34 @@ 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
#### Expressions
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved

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).
Expressions represent an arithmetic equation. The support `+`, `-`, `*`, and `/`, along with `()` for grouping.
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved

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 currently only support `int64` and `float64`.
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
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 Expression.
Expressions can be grouped with parentheses to override evaluation precedence.

Since 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 Expressions as a parameter it must use a `Getter`.__

Example 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` Boolean Expressions have higher precedence than `or`.
Boolean Expressions can be grouped with parentheses to override evaluation precedence.

### Booleans

Booleans can be either:
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 = &exprLiteral{
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 = &exprLiteral{Float: ottltest.Floatp(v)}
case *float64:
val.Float = v
val.Literal = &exprLiteral{Float: v}
case int:
val.Int = ottltest.Intp(int64(v))
val.Literal = &exprLiteral{Int: ottltest.Intp(int64(v))}
case *int64:
val.Int = v
val.Literal = &exprLiteral{Int: v}
case bool:
val.Bool = booleanp(boolean(v))
case nil:
Expand Down
8 changes: 4 additions & 4 deletions pkg/ottl/contexts/internal/ottlcommon/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,13 @@ func accessParentSpanID[K SpanContext]() ottl.StandardGetSetter[K] {

func accessStringParentSpanID[K SpanContext]() ottl.StandardGetSetter[K] {
return ottl.StandardGetSetter[K]{
Getter: func(ctx K) (interface{}, error) {
return ctx.GetSpan().ParentSpanID().HexString(), nil
Getter: func(ctx context.Context, tCtx K) (interface{}, error) {
return tCtx.GetSpan().ParentSpanID().HexString(), nil
},
Setter: func(ctx K, val interface{}) error {
Setter: func(ctx context.Context, tCtx K, val interface{}) error {
if str, ok := val.(string); ok {
if spanID, err := parseSpanID(str); err == nil {
ctx.GetSpan().SetParentSpanID(spanID)
tCtx.GetSpan().SetParentSpanID(spanID)
}
}
return 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)
}
24 changes: 16 additions & 8 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: &exprLiteral{
Float: ottltest.Floatp(1.2),
},
},
want: 1.2,
},
{
name: "int literal",
val: value{
Int: ottltest.Intp(12),
Literal: &exprLiteral{
Int: ottltest.Intp(12),
},
},
want: int64(12),
},
Expand Down Expand Up @@ -81,10 +85,12 @@ func Test_newGetter(t *testing.T) {
{
name: "path expression",
val: value{
Path: &Path{
Fields: []Field{
{
Name: "name",
Literal: &exprLiteral{
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: &exprLiteral{
Invocation: &invocation{
Function: "hello",
},
},
},
want: "world",
Expand Down
13 changes: 8 additions & 5 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 an 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 @@ -163,15 +166,15 @@ func (p *Parser[K]) buildArg(argDef value, argType reflect.Type, index int) (any
}
return *argDef.String, nil
case name == reflect.Float64.String():
if argDef.Float == nil {
if argDef.Literal == nil || argDef.Literal.Float == nil {
return nil, fmt.Errorf("invalid argument at position %v, must be an float", index)
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
}
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