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] add ValueExpression to support extraction of values from the signal context #36883

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
47a2a77
add expression parser to ottl
bacherfl Dec 17, 2024
dc1c19c
add unit and e2e tests
bacherfl Dec 18, 2024
d3f5d08
changelog entry
bacherfl Dec 18, 2024
646d56a
add doc comments
bacherfl Dec 18, 2024
1fd0406
fix failing unit test
bacherfl Dec 18, 2024
3a5e031
fix linting
bacherfl Dec 18, 2024
caf3a99
fix linting
bacherfl Dec 19, 2024
15ba699
Merge branch 'main' into feat/35621/ottl-expression-evaluation
bacherfl Jan 7, 2025
50d43f0
Apply suggestions from code review
bacherfl Jan 7, 2025
62e08ef
Merge branch 'main' into feat/35621/ottl-expression-evaluation
bacherfl Jan 7, 2025
e603e4f
Merge branch 'main' into feat/35621/ottl-expression-evaluation
bacherfl Jan 9, 2025
5365a1d
Merge branch 'main' into feat/35621/ottl-expression-evaluation
bacherfl Jan 9, 2025
3322d33
Update pkg/ottl/parser.go
TylerHelmuth Jan 9, 2025
d60f259
Merge branch 'main' into feat/35621/ottl-expression-evaluation
bacherfl Jan 13, 2025
49e437c
incorporate pr review, fix failing test after merging main
bacherfl Jan 13, 2025
2e10a1e
fix linting
bacherfl Jan 13, 2025
7cf49cc
fix linting
bacherfl Jan 13, 2025
e4cfdcf
Merge branch 'main' into feat/35621/ottl-expression-evaluation
evan-bradley Jan 13, 2025
81762e4
Merge branch 'main' into feat/35621/ottl-expression-evaluation
TylerHelmuth Jan 13, 2025
7ab77d0
Merge branch 'main' into feat/35621/ottl-expression-evaluation
TylerHelmuth Jan 13, 2025
78e8131
Merge branch 'main' into feat/35621/ottl-expression-evaluation
evan-bradley Jan 13, 2025
d45ba1a
Merge branch 'main' into feat/35621/ottl-expression-evaluation
bacherfl Jan 14, 2025
2c29caa
Merge branch 'main' into feat/35621/ottl-expression-evaluation
TylerHelmuth Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .chloggen/ottl-value-expression.yaml
Original file line number Diff line number Diff line change
@@ -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 value expression parser that enables components using ottl to retrieve values from the context, as well as the result of converter functions
bacherfl marked this conversation as resolved.
Show resolved Hide resolved

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [35621]

# (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:
bacherfl marked this conversation as resolved.
Show resolved Hide resolved

# 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: []
bacherfl marked this conversation as resolved.
Show resolved Hide resolved
55 changes: 55 additions & 0 deletions pkg/ottl/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,61 @@ func Test_e2e_ottl_features(t *testing.T) {
}
}

func Test_e2e_ottl_value_expressions(t *testing.T) {
tests := []struct {
name string
statement string
want any
}{
{
name: "string literal",
statement: `"foo"`,
want: "foo",
},
{
name: "attribute value",
statement: `resource.attributes["host.name"]`,
want: "localhost",
},
{
name: "accessing enum",
statement: `SEVERITY_NUMBER_TRACE`,
want: int64(1),
},
{
name: "Using converter",
statement: `TraceID(0x0102030405060708090a0b0c0d0e0f10)`,
want: pcommon.TraceID{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10},
},
{
name: "Adding results of two converter operations",
statement: `Len(attributes) + Len(attributes)`,
want: int64(14),
},
{
name: "Nested converter operations",
statement: `Hex(Len(attributes) + Len(attributes))`,
want: "000000000000000e",
},
}

for _, tt := range tests {
t.Run(tt.statement, func(t *testing.T) {
settings := componenttest.NewNopTelemetrySettings()
logParser, err := ottllog.NewParser(ottlfuncs.StandardFuncs[ottllog.TransformContext](), settings)
assert.NoError(t, err)
valueExpr, err := logParser.ParseValueExpression(tt.statement)
assert.NoError(t, err)

tCtx := constructLogTransformContext()
val, err := valueExpr.Eval(context.Background(), tCtx)
assert.NoError(t, err)

assert.Equal(t, tt.want, val)
})
}
}

func Test_ProcessTraces_TraceContext(t *testing.T) {
tests := []struct {
statement string
Expand Down
6 changes: 6 additions & 0 deletions pkg/ottl/grammar.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ type value struct {
List *list `parser:"| @@)"`
}

func (v *value) checkForCustomError() error {
validator := &grammarCustomErrorsVisitor{}
v.accept(validator)
return validator.join()
}

func (v *value) accept(vis grammarVisitor) {
vis.visitValue(v)
if v.Literal != nil {
Expand Down
48 changes: 46 additions & 2 deletions pkg/ottl/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,9 @@ func (p *Parser[K]) prependContextToStatementPaths(context string, statement str
}

var (
parser = newParser[parsedStatement]()
conditionParser = newParser[booleanExpression]()
parser = newParser[parsedStatement]()
conditionParser = newParser[booleanExpression]()
valueExpressionParser = newParser[value]()
)

func parseStatement(raw string) (*parsedStatement, error) {
Expand Down Expand Up @@ -262,6 +263,19 @@ func parseCondition(raw string) (*booleanExpression, error) {
return parsed, nil
}

func parseValueExpression(raw string) (*value, error) {
parsed, err := valueExpressionParser.ParseString("", raw)
if err != nil {
return nil, fmt.Errorf("condition has invalid syntax: %w", err)
bacherfl marked this conversation as resolved.
Show resolved Hide resolved
}
err = parsed.checkForCustomError()
if err != nil {
return nil, err
}

return parsed, nil
}

func insertContextIntoStatementOffsets(context string, statement string, offsets []int) (string, error) {
if len(offsets) == 0 {
return statement, nil
Expand Down Expand Up @@ -439,3 +453,33 @@ func (c *ConditionSequence[K]) Eval(ctx context.Context, tCtx K) (bool, error) {
// It is not possible to get here if any condition during an AND explicitly failed.
return c.logicOp == And && atLeastOneMatch, nil
}

// ValueExpression represents an expression that resolves to a value. The returned value can be of any type,
// and the expression can be either a literal value, a path value within the context, or the result of a converter and/or
// a mathematical expression.
// This Allows other components using this library to extract data from the context of the incoming signal using OTTL.
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
type ValueExpression[K any] struct {
getter Getter[K]
}

// Eval evaluates the given expression and returns the value the expression resolves to.
func (e *ValueExpression[K]) Eval(ctx context.Context, tCtx K) (any, error) {
return e.getter.Get(ctx, tCtx)
}

// ParseValueExpression parses an expression string into a ValueExpression. The ValueExpression's Eval
// method can then be used to extract the value from the context of the incoming signal.
func (p *Parser[K]) ParseValueExpression(raw string) (*ValueExpression[K], error) {
parsed, err := parseValueExpression(raw)
if err != nil {
return nil, err
}
getter, err := p.newGetter(*parsed)
if err != nil {
return nil, err
}

return &ValueExpression[K]{
getter: getter,
}, nil
}
95 changes: 95 additions & 0 deletions pkg/ottl/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,63 @@ func testParseEnum(val *EnumSymbol) (*Enum, error) {
return nil, fmt.Errorf("enum symbol not provided")
}

func Test_parseValueExpression_full(t *testing.T) {
time1 := time.Now()
time2 := time1.Add(5 * time.Second)
tests := []struct {
name string
valueExpression string
tCtx any
expected any
}{
{
name: "string value",
valueExpression: `"fido"`,
expected: "fido",
},
{
name: "resolve context value",
valueExpression: `attributes`,
expected: map[string]any{
"attributes": map[string]any{
"foo": "bar",
},
},
tCtx: map[string]any{
"attributes": map[string]any{
"foo": "bar",
},
},
},
{
name: "resolve math expression",
valueExpression: `time2 - time1`,
expected: 5 * time.Second,
tCtx: map[string]time.Time{
"time1": time1,
"time2": time2,
},
},
}

for _, tt := range tests {
t.Run(tt.valueExpression, func(t *testing.T) {
p, _ := NewParser(
CreateFactoryMap[any](),
testParsePath[any],
componenttest.NewNopTelemetrySettings(),
WithEnumParser[any](testParseEnum),
)
parsed, err := p.ParseValueExpression(tt.valueExpression)
assert.NoError(t, err)

v, err := parsed.Eval(context.Background(), tt.tCtx)
require.NoError(t, err)
assert.Equal(t, tt.expected, v)
})
}
}

func Test_ParseStatements_Error(t *testing.T) {
statements := []string{
`set(`,
Expand Down Expand Up @@ -2343,6 +2400,44 @@ func Test_parseCondition(t *testing.T) {
}
}

// This test doesn't validate parser results, simply checks whether the parse succeeds or not.
// It's a fast way to check a large range of possible syntaxes.
func Test_parseValueExpression(t *testing.T) {
evan-bradley marked this conversation as resolved.
Show resolved Hide resolved
converterNameErrorPrefix := "converter names must start with an uppercase letter"
editorWithIndexErrorPrefix := "only paths and converters may be indexed"

tests := []struct {
valueExpression string
wantErr bool
wantErrContaining string
}{
{valueExpression: `time_end - time_end`},
{valueExpression: `time_end - time_end - attributes["foo"]`},
{valueExpression: `Test("foo")`},
{valueExpression: `Test(Test("foo")) - attributes["bar"]`},
{valueExpression: `Test(Test("foo")) - attributes["bar"]"`, wantErr: true},
{valueExpression: `test("foo")`, wantErr: true, wantErrContaining: converterNameErrorPrefix},
{valueExpression: `test(animal)["kind"]`, wantErrContaining: editorWithIndexErrorPrefix},
{valueExpression: `Test("a"")foo"`, wantErr: true},
{valueExpression: `Test("a"") == 1"`, wantErr: true},
}
pat := regexp.MustCompile("[^a-zA-Z0-9]+")
for _, tt := range tests {
name := pat.ReplaceAllString(tt.valueExpression, "_")
t.Run(name, func(t *testing.T) {
ast, err := parseValueExpression(tt.valueExpression)
if (err != nil) != (tt.wantErr || tt.wantErrContaining != "") {
t.Errorf("parseCondition(%s) error = %v, wantErr %v", tt.valueExpression, err, tt.wantErr)
t.Errorf("AST: %+v", ast)
return
}
if tt.wantErrContaining != "" {
require.ErrorContains(t, err, tt.wantErrContaining)
}
})
}
}

func Test_Statement_Execute(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading