Skip to content

Commit bd22578

Browse files
axwedmocostaevan-bradley
authored
pkg/ottl: add context inference for expressions (#40194)
#### Description Adds support for context inference when parsing OTTL value expressions. This is not used anywhere yet, but I intend to use it in a new processor that partitions batches of data based on OTTL expressions. #### Link to tracking issue Fixes #39158 #### Testing Added/updated unit tests based on existing ones for ParseConditions. #### Documentation None --------- Co-authored-by: Edmo Vamerlatti Costa <[email protected]> Co-authored-by: Evan Bradley <[email protected]>
1 parent 90c0415 commit bd22578

File tree

9 files changed

+746
-24
lines changed

9 files changed

+746
-24
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: pkg/ottl
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add context inference support for OTTL value expressions
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [39158]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [api]

pkg/ottl/context_inferrer.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ type contextInferrer interface {
3333
inferFromStatements(statements []string) (string, error)
3434
// inferFromConditions returns the OTTL context inferred from the given conditions.
3535
inferFromConditions(conditions []string) (string, error)
36-
// infer returns the OTTL context inferred from the given statements and conditions.
37-
infer(statements []string, conditions []string) (string, error)
36+
// inferFromValueExpressions returns the OTTL context inferred from the given value expressions.
37+
inferFromValueExpressions(expressions []string) (string, error)
38+
// infer returns the OTTL context inferred from the given statements, conditions,
39+
// and value expressions.
40+
infer(statements, conditions, valueExpressions []string) (string, error)
3841
}
3942

4043
type priorityContextInferrer struct {
@@ -88,15 +91,19 @@ func withContextInferrerPriorities(priorities []string) priorityContextInferrerO
8891
}
8992

9093
func (s *priorityContextInferrer) inferFromConditions(conditions []string) (inferredContext string, err error) {
91-
return s.infer(nil, conditions)
94+
return s.infer(nil, conditions, nil)
9295
}
9396

9497
func (s *priorityContextInferrer) inferFromStatements(statements []string) (inferredContext string, err error) {
95-
return s.infer(statements, nil)
98+
return s.infer(statements, nil, nil)
9699
}
97100

98-
func (s *priorityContextInferrer) infer(statements []string, conditions []string) (inferredContext string, err error) {
99-
var statementsHints, conditionsHints []priorityContextInferrerHints
101+
func (s *priorityContextInferrer) inferFromValueExpressions(expressions []string) (inferredContext string, err error) {
102+
return s.infer(nil, nil, expressions)
103+
}
104+
105+
func (s *priorityContextInferrer) infer(statements, conditions, valueExprs []string) (inferredContext string, err error) {
106+
var statementsHints, conditionsHints, valueExprsHints []priorityContextInferrerHints
100107
if len(statements) > 0 {
101108
statementsHints, err = s.getStatementsHints(statements)
102109
if err != nil {
@@ -109,23 +116,30 @@ func (s *priorityContextInferrer) infer(statements []string, conditions []string
109116
return "", err
110117
}
111118
}
119+
if len(valueExprs) > 0 {
120+
valueExprsHints, err = s.getValueExpressionsHints(valueExprs)
121+
if err != nil {
122+
return "", err
123+
}
124+
}
112125
if s.telemetrySettings.Logger.Core().Enabled(zap.DebugLevel) {
113-
s.telemetrySettings.Logger.Debug("Inferring context from statements and conditions",
126+
s.telemetrySettings.Logger.Debug("Inferring OTTL context",
114127
zap.Strings("candidates", maps.Keys(s.contextCandidate)),
115128
zap.Any("priority", s.contextPriority),
116129
zap.Strings("statements", statements),
117130
zap.Strings("conditions", conditions),
131+
zap.Strings("value_expressions", valueExprs),
118132
)
119133
}
120-
return s.inferFromHints(append(statementsHints, conditionsHints...))
134+
return s.inferFromHints(slices.Concat(statementsHints, conditionsHints, valueExprsHints))
121135
}
122136

123137
func (s *priorityContextInferrer) inferFromHints(hints []priorityContextInferrerHints) (inferredContext string, err error) {
124138
defer func() {
125139
if inferredContext != "" {
126-
s.telemetrySettings.Logger.Debug(fmt.Sprintf(`Inferred context: "%s"`, inferredContext))
140+
s.telemetrySettings.Logger.Debug(fmt.Sprintf(`Inferred OTTL context: "%s"`, inferredContext))
127141
} else {
128-
s.telemetrySettings.Logger.Debug("Unable to infer context from statements", zap.Error(err))
142+
s.telemetrySettings.Logger.Debug("Unable to infer OTTL context", zap.Error(err))
129143
}
130144
}()
131145

@@ -284,6 +298,24 @@ func (s *priorityContextInferrer) getStatementsHints(statements []string) ([]pri
284298
return hints, nil
285299
}
286300

301+
// getValueExpressionsHints extracts all path, function (converter) names, and enumSymbol
302+
// from the given value expressions. These values are used by the context inferrer as hints to
303+
// select a context in which the function/enum are supported.
304+
func (s *priorityContextInferrer) getValueExpressionsHints(exprs []string) ([]priorityContextInferrerHints, error) {
305+
hints := make([]priorityContextInferrerHints, 0, len(exprs))
306+
for _, expr := range exprs {
307+
parsed, err := parseValueExpression(expr)
308+
if err != nil {
309+
return nil, err
310+
}
311+
312+
visitor := newGrammarContextInferrerVisitor()
313+
parsed.accept(&visitor)
314+
hints = append(hints, visitor)
315+
}
316+
return hints, nil
317+
}
318+
287319
// priorityContextInferrerHints is a grammarVisitor implementation that collects
288320
// all path, function names (converter.Function and editor.Function), and enumSymbol.
289321
type priorityContextInferrerHints struct {

pkg/ottl/context_inferrer_test.go

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -520,11 +520,12 @@ func Test_NewPriorityContextInferrer_InferConditions_DefaultContextsOrder(t *tes
520520

521521
func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
522522
tests := []struct {
523-
name string
524-
candidates map[string]*priorityContextInferrerCandidate
525-
statements []string
526-
conditions []string
527-
expected string
523+
name string
524+
candidates map[string]*priorityContextInferrerCandidate
525+
statements []string
526+
conditions []string
527+
expressions []string
528+
expected string
528529
}{
529530
{
530531
name: "with statements",
@@ -547,6 +548,18 @@ func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
547548
},
548549
expected: "metric",
549550
},
551+
{
552+
name: "with expressions",
553+
candidates: map[string]*priorityContextInferrerCandidate{
554+
"metric": defaultDummyPriorityContextInferrerCandidate,
555+
"resource": defaultDummyPriorityContextInferrerCandidate,
556+
},
557+
expressions: []string{
558+
`resource.attributes["foo"]`,
559+
`metric.name`,
560+
},
561+
expected: "metric",
562+
},
550563
{
551564
name: "with statements and conditions",
552565
candidates: map[string]*priorityContextInferrerCandidate{
@@ -560,6 +573,22 @@ func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
560573
},
561574
expected: "metric",
562575
},
576+
{
577+
name: "with statements, conditions, and value expressions",
578+
candidates: map[string]*priorityContextInferrerCandidate{
579+
"metric": defaultDummyPriorityContextInferrerCandidate,
580+
"resource": defaultDummyPriorityContextInferrerCandidate,
581+
},
582+
statements: []string{`set(resource.attributes["foo"], "bar")`},
583+
expressions: []string{
584+
`resource.attributes["foo"]`,
585+
},
586+
conditions: []string{
587+
`IsMatch(metric.name, "^bar.*")`,
588+
`IsMatch(metric.name, "^foo.*")`,
589+
},
590+
expected: "metric",
591+
},
563592
}
564593

565594
for _, tt := range tests {
@@ -568,7 +597,7 @@ func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
568597
componenttest.NewNopTelemetrySettings(),
569598
tt.candidates,
570599
)
571-
inferredContext, err := inferrer.infer(tt.statements, tt.conditions)
600+
inferredContext, err := inferrer.infer(tt.statements, tt.conditions, tt.expressions)
572601
require.NoError(t, err)
573602
assert.Equal(t, tt.expected, inferredContext)
574603
})

pkg/ottl/parser.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,20 @@ func (p *Parser[K]) prependContextToConditionPaths(context string, condition str
259259
})
260260
}
261261

262+
// prependContextToValueExpressionPaths changes the given OTTL value expression adding the context name prefix
263+
// to all context-less paths. No modifications are performed for paths which [Path.Context]
264+
// value matches any WithPathContextNames value.
265+
// The context argument must be valid WithPathContextNames value, otherwise an error is returned.
266+
func (p *Parser[K]) prependContextToValueExpressionPaths(context string, expr string) (string, error) {
267+
return p.prependContextToPaths(context, expr, func(ottl string) ([]path, error) {
268+
parsed, err := parseValueExpression(ottl)
269+
if err != nil {
270+
return nil, err
271+
}
272+
return getValuePaths(parsed), nil
273+
})
274+
}
275+
262276
var (
263277
parser = sync.OnceValue(newParser[parsedStatement])
264278
conditionParser = sync.OnceValue(newParser[booleanExpression])
@@ -493,14 +507,38 @@ func (c *ConditionSequence[K]) Eval(ctx context.Context, tCtx K) (bool, error) {
493507
// a mathematical expression.
494508
// This allows other components using this library to extract data from the context of the incoming signal using OTTL.
495509
type ValueExpression[K any] struct {
496-
getter Getter[K]
510+
getter Getter[K]
511+
origText string
497512
}
498513

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

519+
// ParseValueExpressions parses string expressions into a ValueExpression slice ready for execution.
520+
// Returns a slice of ValueExpression and a nil error on successful parsing.
521+
// If parsing fails, returns nil and an error containing each error per failed condition.
522+
func (p *Parser[K]) ParseValueExpressions(expressions []string) ([]*ValueExpression[K], error) {
523+
parsedValueExpressions := make([]*ValueExpression[K], 0, len(expressions))
524+
var parseErrs []error
525+
526+
for _, expression := range expressions {
527+
ps, err := p.ParseValueExpression(expression)
528+
if err != nil {
529+
parseErrs = append(parseErrs, fmt.Errorf("unable to parse OTTL value expression %q: %w", expression, err))
530+
continue
531+
}
532+
parsedValueExpressions = append(parsedValueExpressions, ps)
533+
}
534+
535+
if len(parseErrs) > 0 {
536+
return nil, errors.Join(parseErrs...)
537+
}
538+
539+
return parsedValueExpressions, nil
540+
}
541+
504542
// ParseValueExpression parses an expression string into a ValueExpression. The ValueExpression's Eval
505543
// method can then be used to extract the value from the context of the incoming signal.
506544
func (p *Parser[K]) ParseValueExpression(raw string) (*ValueExpression[K], error) {
@@ -514,6 +552,7 @@ func (p *Parser[K]) ParseValueExpression(raw string) (*ValueExpression[K], error
514552
}
515553

516554
return &ValueExpression[K]{
555+
origText: raw,
517556
getter: &StandardGetSetter[K]{
518557
Getter: func(ctx context.Context, tCtx K) (any, error) {
519558
val, err := getter.Get(ctx, tCtx)

0 commit comments

Comments
 (0)