Skip to content

pkg/ottl: add context inference for expressions #40194

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

Merged
merged 12 commits into from
Jun 20, 2025
27 changes: 27 additions & 0 deletions .chloggen/ottl-inference-valuexpr.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 context inference support for OTTL value expressions

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

# (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:

# 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: [api]
52 changes: 42 additions & 10 deletions pkg/ottl/context_inferrer.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ type contextInferrer interface {
inferFromStatements(statements []string) (string, error)
// inferFromConditions returns the OTTL context inferred from the given conditions.
inferFromConditions(conditions []string) (string, error)
// infer returns the OTTL context inferred from the given statements and conditions.
infer(statements []string, conditions []string) (string, error)
// inferFromValueExpressions returns the OTTL context inferred from the given value expressions.
inferFromValueExpressions(expressions []string) (string, error)
// infer returns the OTTL context inferred from the given statements, conditions,
// and value expressions.
infer(statements, conditions, valueExpressions []string) (string, error)
}

type priorityContextInferrer struct {
Expand Down Expand Up @@ -88,15 +91,19 @@ func withContextInferrerPriorities(priorities []string) priorityContextInferrerO
}

func (s *priorityContextInferrer) inferFromConditions(conditions []string) (inferredContext string, err error) {
return s.infer(nil, conditions)
return s.infer(nil, conditions, nil)
}

func (s *priorityContextInferrer) inferFromStatements(statements []string) (inferredContext string, err error) {
return s.infer(statements, nil)
return s.infer(statements, nil, nil)
}

func (s *priorityContextInferrer) infer(statements []string, conditions []string) (inferredContext string, err error) {
var statementsHints, conditionsHints []priorityContextInferrerHints
func (s *priorityContextInferrer) inferFromValueExpressions(expressions []string) (inferredContext string, err error) {
return s.infer(nil, nil, expressions)
}

func (s *priorityContextInferrer) infer(statements, conditions, valueExprs []string) (inferredContext string, err error) {
var statementsHints, conditionsHints, valueExprsHints []priorityContextInferrerHints
if len(statements) > 0 {
statementsHints, err = s.getStatementsHints(statements)
if err != nil {
Expand All @@ -109,23 +116,30 @@ func (s *priorityContextInferrer) infer(statements []string, conditions []string
return "", err
}
}
if len(valueExprs) > 0 {
valueExprsHints, err = s.getValueExpressionsHints(valueExprs)
if err != nil {
return "", err
}
}
if s.telemetrySettings.Logger.Core().Enabled(zap.DebugLevel) {
s.telemetrySettings.Logger.Debug("Inferring context from statements and conditions",
s.telemetrySettings.Logger.Debug("Inferring OTTL context",
zap.Strings("candidates", maps.Keys(s.contextCandidate)),
zap.Any("priority", s.contextPriority),
zap.Strings("statements", statements),
zap.Strings("conditions", conditions),
zap.Strings("value_expressions", valueExprs),
)
}
return s.inferFromHints(append(statementsHints, conditionsHints...))
return s.inferFromHints(slices.Concat(statementsHints, conditionsHints, valueExprsHints))
}

func (s *priorityContextInferrer) inferFromHints(hints []priorityContextInferrerHints) (inferredContext string, err error) {
defer func() {
if inferredContext != "" {
s.telemetrySettings.Logger.Debug(fmt.Sprintf(`Inferred context: "%s"`, inferredContext))
s.telemetrySettings.Logger.Debug(fmt.Sprintf(`Inferred OTTL context: "%s"`, inferredContext))
} else {
s.telemetrySettings.Logger.Debug("Unable to infer context from statements", zap.Error(err))
s.telemetrySettings.Logger.Debug("Unable to infer OTTL context", zap.Error(err))
}
}()

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

// getValueExpressionsHints extracts all path, function (converter) names, and enumSymbol
// from the given value expressions. These values are used by the context inferrer as hints to
// select a context in which the function/enum are supported.
func (s *priorityContextInferrer) getValueExpressionsHints(exprs []string) ([]priorityContextInferrerHints, error) {
hints := make([]priorityContextInferrerHints, 0, len(exprs))
for _, expr := range exprs {
parsed, err := parseValueExpression(expr)
if err != nil {
return nil, err
}

visitor := newGrammarContextInferrerVisitor()
parsed.accept(&visitor)
hints = append(hints, visitor)
}
return hints, nil
}

// priorityContextInferrerHints is a grammarVisitor implementation that collects
// all path, function names (converter.Function and editor.Function), and enumSymbol.
type priorityContextInferrerHints struct {
Expand Down
41 changes: 35 additions & 6 deletions pkg/ottl/context_inferrer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,11 +520,12 @@ func Test_NewPriorityContextInferrer_InferConditions_DefaultContextsOrder(t *tes

func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
tests := []struct {
name string
candidates map[string]*priorityContextInferrerCandidate
statements []string
conditions []string
expected string
name string
candidates map[string]*priorityContextInferrerCandidate
statements []string
conditions []string
expressions []string
expected string
}{
{
name: "with statements",
Expand All @@ -547,6 +548,18 @@ func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
},
expected: "metric",
},
{
name: "with expressions",
candidates: map[string]*priorityContextInferrerCandidate{
"metric": defaultDummyPriorityContextInferrerCandidate,
"resource": defaultDummyPriorityContextInferrerCandidate,
},
expressions: []string{
`resource.attributes["foo"]`,
`metric.name`,
},
expected: "metric",
},
{
name: "with statements and conditions",
candidates: map[string]*priorityContextInferrerCandidate{
Expand All @@ -560,6 +573,22 @@ func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
},
expected: "metric",
},
{
name: "with statements, conditions, and value expressions",
candidates: map[string]*priorityContextInferrerCandidate{
"metric": defaultDummyPriorityContextInferrerCandidate,
"resource": defaultDummyPriorityContextInferrerCandidate,
},
statements: []string{`set(resource.attributes["foo"], "bar")`},
expressions: []string{
`resource.attributes["foo"]`,
},
conditions: []string{
`IsMatch(metric.name, "^bar.*")`,
`IsMatch(metric.name, "^foo.*")`,
},
expected: "metric",
},
}

for _, tt := range tests {
Expand All @@ -568,7 +597,7 @@ func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
componenttest.NewNopTelemetrySettings(),
tt.candidates,
)
inferredContext, err := inferrer.infer(tt.statements, tt.conditions)
inferredContext, err := inferrer.infer(tt.statements, tt.conditions, tt.expressions)
require.NoError(t, err)
assert.Equal(t, tt.expected, inferredContext)
})
Expand Down
41 changes: 40 additions & 1 deletion pkg/ottl/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,20 @@ func (p *Parser[K]) prependContextToConditionPaths(context string, condition str
})
}

// prependContextToValueExpressionPaths changes the given OTTL value expression adding the context name prefix
// to all context-less paths. No modifications are performed for paths which [Path.Context]
// value matches any WithPathContextNames value.
// The context argument must be valid WithPathContextNames value, otherwise an error is returned.
func (p *Parser[K]) prependContextToValueExpressionPaths(context string, expr string) (string, error) {
return p.prependContextToPaths(context, expr, func(ottl string) ([]path, error) {
parsed, err := parseValueExpression(ottl)
if err != nil {
return nil, err
}
return getValuePaths(parsed), nil
})
}

var (
parser = sync.OnceValue(newParser[parsedStatement])
conditionParser = sync.OnceValue(newParser[booleanExpression])
Expand Down Expand Up @@ -493,14 +507,38 @@ func (c *ConditionSequence[K]) Eval(ctx context.Context, tCtx K) (bool, error) {
// a mathematical expression.
// This allows other components using this library to extract data from the context of the incoming signal using OTTL.
type ValueExpression[K any] struct {
getter Getter[K]
getter Getter[K]
origText string
}

// 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)
}

// ParseValueExpressions parses string expressions into a ValueExpression slice ready for execution.
// Returns a slice of ValueExpression and a nil error on successful parsing.
// If parsing fails, returns nil and an error containing each error per failed condition.
func (p *Parser[K]) ParseValueExpressions(expressions []string) ([]*ValueExpression[K], error) {
parsedValueExpressions := make([]*ValueExpression[K], 0, len(expressions))
var parseErrs []error

for _, expression := range expressions {
ps, err := p.ParseValueExpression(expression)
if err != nil {
parseErrs = append(parseErrs, fmt.Errorf("unable to parse OTTL value expression %q: %w", expression, err))
continue
}
parsedValueExpressions = append(parsedValueExpressions, ps)
}

if len(parseErrs) > 0 {
return nil, errors.Join(parseErrs...)
}

return parsedValueExpressions, nil
}

// 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) {
Expand All @@ -514,6 +552,7 @@ func (p *Parser[K]) ParseValueExpression(raw string) (*ValueExpression[K], error
}

return &ValueExpression[K]{
origText: raw,
getter: &StandardGetSetter[K]{
Getter: func(ctx context.Context, tCtx K) (any, error) {
val, err := getter.Get(ctx, tCtx)
Expand Down
Loading