Skip to content

Commit d417754

Browse files
authored
[pkg/ottl] Add parser collection option to allow configuring extra context inferrer conditions (#39465)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Added a new option `ottl.WithContextInferenceConditions` to the `ParserCollection.ParseStataments` so API users can provide extra context inferrer's conditions and have theirs hints considered by the default context inferrer. Follow up: #39463 (includes this PR changes) <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Relates to #39455 <!--Describe what testing was performed and which tests were added.--> #### Testing Unit tests
1 parent c19392a commit d417754

5 files changed

+239
-51
lines changed
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 `ottl.WithContextInferenceConditions` option to allow configuring extra context inferrer OTTL conditions"
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: [39455]
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

+65-45
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ type contextInferrer interface {
3232
inferFromStatements(statements []string) (string, error)
3333
// inferFromConditions returns the OTTL context inferred from the given conditions.
3434
inferFromConditions(conditions []string) (string, error)
35+
// infer returns the OTTL context inferred from the given statements and conditions.
36+
infer(statements []string, conditions []string) (string, error)
3537
}
3638

3739
type priorityContextInferrer struct {
@@ -85,23 +87,37 @@ func withContextInferrerPriorities(priorities []string) priorityContextInferrerO
8587
}
8688

8789
func (s *priorityContextInferrer) inferFromConditions(conditions []string) (inferredContext string, err error) {
88-
return s.infer(conditions, s.getConditionHints)
90+
return s.infer(nil, conditions)
8991
}
9092

9193
func (s *priorityContextInferrer) inferFromStatements(statements []string) (inferredContext string, err error) {
92-
return s.infer(statements, s.getStatementHints)
94+
return s.infer(statements, nil)
9395
}
9496

95-
// hinterFunc is used by the infer function to generate the hints (paths, functions, enums, etc.) for the given OTTL.
96-
type hinterFunc func(string) ([]path, map[string]struct{}, map[enumSymbol]struct{}, error)
97-
98-
func (s *priorityContextInferrer) infer(ottls []string, hinter hinterFunc) (inferredContext string, err error) {
99-
s.telemetrySettings.Logger.Debug("Inferring context from OTTL",
97+
func (s *priorityContextInferrer) infer(statements []string, conditions []string) (inferredContext string, err error) {
98+
var statementsHints, conditionsHints []priorityContextInferrerHints
99+
if len(statements) > 0 {
100+
statementsHints, err = s.getStatementsHints(statements)
101+
if err != nil {
102+
return "", err
103+
}
104+
}
105+
if len(conditions) > 0 {
106+
conditionsHints, err = s.getConditionsHints(conditions)
107+
if err != nil {
108+
return "", err
109+
}
110+
}
111+
s.telemetrySettings.Logger.Debug("Inferring context from statements and conditions",
100112
zap.Strings("candidates", maps.Keys(s.contextCandidate)),
101113
zap.Any("priority", s.contextPriority),
102-
zap.Strings("values", ottls),
114+
zap.Strings("statements", statements),
115+
zap.Strings("conditions", conditions),
103116
)
117+
return s.inferFromHints(append(statementsHints, conditionsHints...))
118+
}
104119

120+
func (s *priorityContextInferrer) inferFromHints(hints []priorityContextInferrerHints) (inferredContext string, err error) {
105121
defer func() {
106122
if inferredContext != "" {
107123
s.telemetrySettings.Logger.Debug(fmt.Sprintf(`Inferred context: "%s"`, inferredContext))
@@ -114,12 +130,8 @@ func (s *priorityContextInferrer) infer(ottls []string, hinter hinterFunc) (infe
114130
requiredEnums := map[enumSymbol]struct{}{}
115131

116132
var inferredContextPriority int
117-
for _, ottl := range ottls {
118-
ottlPaths, ottlFunctions, ottlEnums, hinterErr := hinter(ottl)
119-
if hinterErr != nil {
120-
return "", hinterErr
121-
}
122-
for _, p := range ottlPaths {
133+
for _, hint := range hints {
134+
for _, p := range hint.paths {
123135
candidate := p.Context
124136
candidatePriority, ok := s.contextPriority[candidate]
125137
if !ok {
@@ -130,10 +142,10 @@ func (s *priorityContextInferrer) infer(ottls []string, hinter hinterFunc) (infe
130142
inferredContextPriority = candidatePriority
131143
}
132144
}
133-
for function := range ottlFunctions {
145+
for function := range hint.functions {
134146
requiredFunctions[function] = struct{}{}
135147
}
136-
for enum := range ottlEnums {
148+
for enum := range hint.enumsSymbols {
137149
requiredEnums[enum] = struct{}{}
138150
}
139151
}
@@ -231,68 +243,76 @@ func (s *priorityContextInferrer) sortContextCandidates(candidates []string) {
231243
})
232244
}
233245

234-
// getConditionHints extracts all path, function names (editor and converter), and enumSymbol
246+
// getConditionsHints extracts all path, function names (editor and converter), and enumSymbol
235247
// from the given condition. These values are used by the context inferrer as hints to
236248
// select a context in which the function/enum are supported.
237-
func (s *priorityContextInferrer) getConditionHints(condition string) ([]path, map[string]struct{}, map[enumSymbol]struct{}, error) {
238-
parsed, err := parseCondition(condition)
239-
if err != nil {
240-
return nil, nil, nil, err
241-
}
249+
func (s *priorityContextInferrer) getConditionsHints(conditions []string) ([]priorityContextInferrerHints, error) {
250+
hints := make([]priorityContextInferrerHints, 0, len(conditions))
251+
for _, condition := range conditions {
252+
parsed, err := parseCondition(condition)
253+
if err != nil {
254+
return nil, err
255+
}
242256

243-
visitor := newGrammarContextInferrerVisitor()
244-
parsed.accept(&visitor)
245-
return visitor.paths, visitor.functions, visitor.enumsSymbols, nil
257+
visitor := newGrammarContextInferrerVisitor()
258+
parsed.accept(&visitor)
259+
hints = append(hints, visitor)
260+
}
261+
return hints, nil
246262
}
247263

248-
// getStatementHints extracts all path, function names (editor and converter), and enumSymbol
264+
// getStatementsHints extracts all path, function names (editor and converter), and enumSymbol
249265
// from the given statement. These values are used by the context inferrer as hints to
250266
// select a context in which the function/enum are supported.
251-
func (s *priorityContextInferrer) getStatementHints(statement string) ([]path, map[string]struct{}, map[enumSymbol]struct{}, error) {
252-
parsed, err := parseStatement(statement)
253-
if err != nil {
254-
return nil, nil, nil, err
255-
}
256-
visitor := newGrammarContextInferrerVisitor()
257-
parsed.Editor.accept(&visitor)
258-
if parsed.WhereClause != nil {
259-
parsed.WhereClause.accept(&visitor)
267+
func (s *priorityContextInferrer) getStatementsHints(statements []string) ([]priorityContextInferrerHints, error) {
268+
hints := make([]priorityContextInferrerHints, 0, len(statements))
269+
for _, statement := range statements {
270+
parsed, err := parseStatement(statement)
271+
if err != nil {
272+
return nil, err
273+
}
274+
visitor := newGrammarContextInferrerVisitor()
275+
parsed.Editor.accept(&visitor)
276+
if parsed.WhereClause != nil {
277+
parsed.WhereClause.accept(&visitor)
278+
}
279+
hints = append(hints, visitor)
260280
}
261-
return visitor.paths, visitor.functions, visitor.enumsSymbols, nil
281+
return hints, nil
262282
}
263283

264-
// priorityContextInferrerHintsVisitor is a grammarVisitor implementation that collects
284+
// priorityContextInferrerHints is a grammarVisitor implementation that collects
265285
// all path, function names (converter.Function and editor.Function), and enumSymbol.
266-
type priorityContextInferrerHintsVisitor struct {
286+
type priorityContextInferrerHints struct {
267287
paths []path
268288
functions map[string]struct{}
269289
enumsSymbols map[enumSymbol]struct{}
270290
}
271291

272-
func newGrammarContextInferrerVisitor() priorityContextInferrerHintsVisitor {
273-
return priorityContextInferrerHintsVisitor{
292+
func newGrammarContextInferrerVisitor() priorityContextInferrerHints {
293+
return priorityContextInferrerHints{
274294
paths: []path{},
275295
functions: make(map[string]struct{}),
276296
enumsSymbols: make(map[enumSymbol]struct{}),
277297
}
278298
}
279299

280-
func (v *priorityContextInferrerHintsVisitor) visitMathExprLiteral(_ *mathExprLiteral) {}
300+
func (v *priorityContextInferrerHints) visitMathExprLiteral(_ *mathExprLiteral) {}
281301

282-
func (v *priorityContextInferrerHintsVisitor) visitEditor(e *editor) {
302+
func (v *priorityContextInferrerHints) visitEditor(e *editor) {
283303
v.functions[e.Function] = struct{}{}
284304
}
285305

286-
func (v *priorityContextInferrerHintsVisitor) visitConverter(c *converter) {
306+
func (v *priorityContextInferrerHints) visitConverter(c *converter) {
287307
v.functions[c.Function] = struct{}{}
288308
}
289309

290-
func (v *priorityContextInferrerHintsVisitor) visitValue(va *value) {
310+
func (v *priorityContextInferrerHints) visitValue(va *value) {
291311
if va.Enum != nil {
292312
v.enumsSymbols[*va.Enum] = struct{}{}
293313
}
294314
}
295315

296-
func (v *priorityContextInferrerHintsVisitor) visitPath(value *path) {
316+
func (v *priorityContextInferrerHints) visitPath(value *path) {
297317
v.paths = append(v.paths, *value)
298318
}

pkg/ottl/context_inferrer_test.go

+65-1
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,14 @@ func Test_NewPriorityContextInferrer_InvalidStatement(t *testing.T) {
279279
inferrer := newPriorityContextInferrer(componenttest.NewNopTelemetrySettings(), map[string]*priorityContextInferrerCandidate{})
280280
statements := []string{"set(foo.field,"}
281281
_, err := inferrer.inferFromStatements(statements)
282-
require.ErrorContains(t, err, "unexpected token")
282+
require.ErrorContains(t, err, "statement has invalid syntax")
283+
}
284+
285+
func Test_NewPriorityContextInferrer_InvalidCondition(t *testing.T) {
286+
inferrer := newPriorityContextInferrer(componenttest.NewNopTelemetrySettings(), map[string]*priorityContextInferrerCandidate{})
287+
conditions := []string{"foo.field,"}
288+
_, err := inferrer.inferFromConditions(conditions)
289+
require.ErrorContains(t, err, "condition has invalid syntax")
283290
}
284291

285292
func Test_NewPriorityContextInferrer_DefaultPriorityList(t *testing.T) {
@@ -487,3 +494,60 @@ func Test_NewPriorityContextInferrer_InferConditions_DefaultContextsOrder(t *tes
487494
})
488495
}
489496
}
497+
498+
func Test_NewPriorityContextInferrer_Infer(t *testing.T) {
499+
tests := []struct {
500+
name string
501+
candidates map[string]*priorityContextInferrerCandidate
502+
statements []string
503+
conditions []string
504+
expected string
505+
}{
506+
{
507+
name: "with statements",
508+
candidates: map[string]*priorityContextInferrerCandidate{
509+
"metric": defaultDummyPriorityContextInferrerCandidate,
510+
"resource": defaultDummyPriorityContextInferrerCandidate,
511+
},
512+
statements: []string{`set(resource.attributes["foo"], "bar")`},
513+
expected: "resource",
514+
},
515+
{
516+
name: "with conditions",
517+
candidates: map[string]*priorityContextInferrerCandidate{
518+
"metric": defaultDummyPriorityContextInferrerCandidate,
519+
"resource": defaultDummyPriorityContextInferrerCandidate,
520+
},
521+
conditions: []string{
522+
`IsMatch(metric.name, "^bar.*")`,
523+
`IsMatch(metric.name, "^foo.*")`,
524+
},
525+
expected: "metric",
526+
},
527+
{
528+
name: "with statements and conditions",
529+
candidates: map[string]*priorityContextInferrerCandidate{
530+
"metric": defaultDummyPriorityContextInferrerCandidate,
531+
"resource": defaultDummyPriorityContextInferrerCandidate,
532+
},
533+
statements: []string{`set(resource.attributes["foo"], "bar")`},
534+
conditions: []string{
535+
`IsMatch(metric.name, "^bar.*")`,
536+
`IsMatch(metric.name, "^foo.*")`,
537+
},
538+
expected: "metric",
539+
},
540+
}
541+
542+
for _, tt := range tests {
543+
t.Run(tt.name, func(t *testing.T) {
544+
inferrer := newPriorityContextInferrer(
545+
componenttest.NewNopTelemetrySettings(),
546+
tt.candidates,
547+
)
548+
inferredContext, err := inferrer.infer(tt.statements, tt.conditions)
549+
require.NoError(t, err)
550+
assert.Equal(t, tt.expected, inferredContext)
551+
})
552+
}
553+
}

pkg/ottl/parser_collection.go

+45-5
Original file line numberDiff line numberDiff line change
@@ -320,29 +320,69 @@ func EnableParserCollectionModifiedPathsLogging[R any](enabled bool) ParserColle
320320
}
321321
}
322322

323+
type parseCollectionContextInferenceOptions struct {
324+
conditions []string
325+
}
326+
327+
// ParserCollectionContextInferenceOption allows configuring the context inference and use
328+
// this option with the supported parsing functions.
329+
//
330+
// Experimental: *NOTE* this API is subject to change or removal in the future.
331+
type ParserCollectionContextInferenceOption func(p *parseCollectionContextInferenceOptions)
332+
333+
// WithContextInferenceConditions sets additional OTTL conditions to be used to enhance
334+
// the context inference process. This is particularly useful when the statements alone are
335+
// insufficient for determine the correct context, or when a less-specific context is desired.
336+
//
337+
// Experimental: *NOTE* this API is subject to change or removal in the future.
338+
func WithContextInferenceConditions(conditions []string) ParserCollectionContextInferenceOption {
339+
return func(p *parseCollectionContextInferenceOptions) {
340+
p.conditions = conditions
341+
}
342+
}
343+
323344
// ParseStatements parses the given statements into [R] using the configured context's ottl.Parser
324345
// and subsequently calling the ParsedStatementsConverter function.
325346
// The statement's context is automatically inferred from the [Path.Context] values, choosing the
326347
// highest priority context found.
327348
// If no contexts are present in the statements, or if the inferred value is not supported by
328349
// the [ParserCollection], it returns an error.
329350
// If parsing the statements fails, it returns the underlying [ottl.Parser.ParseStatements] error.
351+
// If the provided StatementsGetter also implements ContextInferenceHintsProvider, it uses the
352+
// additional OTTL conditions to enhance the context inference. This is particularly useful when
353+
// the statements alone are insufficient for determine the correct context, or if an less-specific
354+
// parser is desired.
330355
//
331356
// Experimental: *NOTE* this API is subject to change or removal in the future.
332-
func (pc *ParserCollection[R]) ParseStatements(statements StatementsGetter) (R, error) {
357+
func (pc *ParserCollection[R]) ParseStatements(statements StatementsGetter, options ...ParserCollectionContextInferenceOption) (R, error) {
333358
statementsValues := statements.GetStatements()
334-
inferredContext, err := pc.contextInferrer.inferFromStatements(statementsValues)
359+
360+
parseStatementsOpts := parseCollectionContextInferenceOptions{}
361+
for _, opt := range options {
362+
opt(&parseStatementsOpts)
363+
}
364+
365+
conditionsValues := parseStatementsOpts.conditions
366+
367+
var inferredContext string
368+
var err error
369+
if len(conditionsValues) > 0 {
370+
inferredContext, err = pc.contextInferrer.infer(statementsValues, conditionsValues)
371+
} else {
372+
inferredContext, err = pc.contextInferrer.inferFromStatements(statementsValues)
373+
}
374+
335375
if err != nil {
336-
return *new(R), fmt.Errorf("unable to infer a valid context (%+q) from statements %+q: %w", pc.supportedContextNames(), statementsValues, err)
376+
return *new(R), fmt.Errorf("unable to infer a valid context (%+q) from statements %+q and conditions %+q: %w", pc.supportedContextNames(), statementsValues, conditionsValues, err)
337377
}
338378

339379
if inferredContext == "" {
340-
return *new(R), fmt.Errorf("unable to infer context from statements, path's first segment must be a valid context name: %+q, and at least one context must be capable of parsing all statements: %+q", pc.supportedContextNames(), statementsValues)
380+
return *new(R), fmt.Errorf("unable to infer context from statements %+q and conditions %+q, path's first segment must be a valid context name %+q, and at least one context must be capable of parsing all statements", pc.supportedContextNames(), statementsValues, conditionsValues)
341381
}
342382

343383
_, ok := pc.contextParsers[inferredContext]
344384
if !ok {
345-
return *new(R), fmt.Errorf(`context "%s" inferred from the statements %+q is not a supported context: %+q`, inferredContext, statementsValues, pc.supportedContextNames())
385+
return *new(R), fmt.Errorf(`context "%s" inferred from the statements %+q and conditions %+q is not a supported context: %+q`, inferredContext, statementsValues, conditionsValues, pc.supportedContextNames())
346386
}
347387

348388
return pc.ParseStatementsWithContext(inferredContext, statements, false)

0 commit comments

Comments
 (0)