Skip to content

Commit cc176a4

Browse files
edmocostazeck-ops
authored andcommitted
[pkg/ottl] Change context inferrer to use functions and enums as hints (open-telemetry#36869)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description This PR is part of open-telemetry#29017, and a spin-off from open-telemetry#36820. It changes the existing context inferrer logic to also take into consideration the functions and enums used on the statements. (open-telemetry#36820 (comment)) New logic: - Find all `path`, function names(`editor`, `converter`), and `enumSymbol` on the statements - Pick the highest priority context (same existing logic) based on the `path.Context` values - If the chosen context does not support all used functions and enums, it goes through it's lower contexts (wide scope contexts that does support the chosen context as a path context) testing them and choosing the first one that supports them. - If no context that can handle the functions and enums is found, the inference fail and an empty value is returned. The parser collection was adapted to support the new context inferrer configuration requirements. **Other important changes:** Currently, it's possible to have paths to contexts root objects, for example: `set(attributes["body"], resource)`. Given `resource` has no dot separators on the path, the grammar extracts it into the `path.Fields` slice, letting the `path.Context` value empty. Why? This grammar behaviour is still necessary to keep backward compatibility with paths without context, otherwise it would start requiring contexts for all paths independently of the parser configuration. Previous PRs didn't take this edge case into consideration, and a few places needed to be changed to address it: - Context inferrer (`getContextCandidate`) - Parser `prependContextToStatementPaths` function. - Reusable OTTL contexts (`contexts/internal`) (not part of this PR, it will be fixed by open-telemetry#36820) When/If we reach the point to deprecate paths _without_ context, all those conditions can be removed, and the grammar changed to require and extract the `path` context properly. <!-- Issue number (e.g. open-telemetry#1234) or full URL to issue, if applicable. --> #### Link to tracking issue open-telemetry#29017 <!--Describe what testing was performed and which tests were added.--> #### Testing Unit tests <!--Describe the documentation added.--> #### Documentation No changes <!--Please delete paragraphs that you did not use before submitting.-->
1 parent 0132c15 commit cc176a4

6 files changed

+382
-55
lines changed

pkg/ottl/context_inferrer.go

+192-33
Original file line numberDiff line numberDiff line change
@@ -3,75 +3,234 @@
33

44
package ottl // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
55

6-
import "math"
6+
import (
7+
"cmp"
8+
"math"
9+
"slices"
10+
)
711

812
var defaultContextInferPriority = []string{
913
"log",
10-
"metric",
1114
"datapoint",
15+
"metric",
1216
"spanevent",
1317
"span",
1418
"resource",
1519
"scope",
1620
"instrumentation_scope",
1721
}
1822

19-
// contextInferrer is an interface used to infer the OTTL context from statements paths.
23+
// contextInferrer is an interface used to infer the OTTL context from statements.
2024
type contextInferrer interface {
21-
// infer returns the OTTL context inferred from the given statements paths.
25+
// infer returns the OTTL context inferred from the given statements.
2226
infer(statements []string) (string, error)
2327
}
2428

2529
type priorityContextInferrer struct {
26-
contextPriority map[string]int
30+
contextPriority map[string]int
31+
contextCandidate map[string]*priorityContextInferrerCandidate
32+
}
33+
34+
type priorityContextInferrerCandidate struct {
35+
hasEnumSymbol func(enum *EnumSymbol) bool
36+
hasFunctionName func(name string) bool
37+
getLowerContexts func(context string) []string
38+
}
39+
40+
type priorityContextInferrerOption func(*priorityContextInferrer)
41+
42+
// newPriorityContextInferrer creates a new priority-based context inferrer. To infer the context,
43+
// it uses a slice of priorities (withContextInferrerPriorities) and a set of hints extracted from
44+
// the parsed statements.
45+
//
46+
// To be eligible, a context must support all functions and enums symbols present on the statements.
47+
// If the path context with the highest priority does not meet this requirement, it falls back to its
48+
// lower contexts, testing them with the same logic and choosing the first one that meets all requirements.
49+
//
50+
// If non-prioritized contexts are found on the statements, they get assigned the lowest possible priority,
51+
// and are only selected if no other prioritized context is found.
52+
func newPriorityContextInferrer(contextsCandidate map[string]*priorityContextInferrerCandidate, options ...priorityContextInferrerOption) contextInferrer {
53+
c := &priorityContextInferrer{
54+
contextCandidate: contextsCandidate,
55+
}
56+
for _, opt := range options {
57+
opt(c)
58+
}
59+
if len(c.contextPriority) == 0 {
60+
withContextInferrerPriorities(defaultContextInferPriority)(c)
61+
}
62+
return c
63+
}
64+
65+
// withContextInferrerPriorities sets the contexts candidates priorities. The lower the
66+
// context position is in the array, the more priority it will have over other items.
67+
func withContextInferrerPriorities(priorities []string) priorityContextInferrerOption {
68+
return func(c *priorityContextInferrer) {
69+
contextPriority := map[string]int{}
70+
for pri, context := range priorities {
71+
contextPriority[context] = pri
72+
}
73+
c.contextPriority = contextPriority
74+
}
2775
}
2876

2977
func (s *priorityContextInferrer) infer(statements []string) (string, error) {
78+
requiredFunctions := map[string]struct{}{}
79+
requiredEnums := map[enumSymbol]struct{}{}
80+
3081
var inferredContext string
3182
var inferredContextPriority int
32-
3383
for _, statement := range statements {
3484
parsed, err := parseStatement(statement)
3585
if err != nil {
36-
return inferredContext, err
86+
return "", err
3787
}
3888

39-
for _, p := range getParsedStatementPaths(parsed) {
40-
pathContextPriority, ok := s.contextPriority[p.Context]
89+
statementPaths, statementFunctions, statementEnums := s.getParsedStatementHints(parsed)
90+
for _, p := range statementPaths {
91+
candidate := p.Context
92+
candidatePriority, ok := s.contextPriority[candidate]
4193
if !ok {
42-
// Lowest priority
43-
pathContextPriority = math.MaxInt
94+
candidatePriority = math.MaxInt
4495
}
45-
46-
if inferredContext == "" || pathContextPriority < inferredContextPriority {
47-
inferredContext = p.Context
48-
inferredContextPriority = pathContextPriority
96+
if inferredContext == "" || candidatePriority < inferredContextPriority {
97+
inferredContext = candidate
98+
inferredContextPriority = candidatePriority
4999
}
50100
}
101+
for function := range statementFunctions {
102+
requiredFunctions[function] = struct{}{}
103+
}
104+
for enum := range statementEnums {
105+
requiredEnums[enum] = struct{}{}
106+
}
107+
}
108+
// No inferred context or nothing left to verify.
109+
if inferredContext == "" || (len(requiredFunctions) == 0 && len(requiredEnums) == 0) {
110+
return inferredContext, nil
111+
}
112+
ok := s.validateContextCandidate(inferredContext, requiredFunctions, requiredEnums)
113+
if ok {
114+
return inferredContext, nil
115+
}
116+
return s.inferFromLowerContexts(inferredContext, requiredFunctions, requiredEnums), nil
117+
}
118+
119+
// validateContextCandidate checks if the given context candidate has all required functions names
120+
// and enums symbols. The functions arity are not verified.
121+
func (s *priorityContextInferrer) validateContextCandidate(
122+
context string,
123+
requiredFunctions map[string]struct{},
124+
requiredEnums map[enumSymbol]struct{},
125+
) bool {
126+
candidate, ok := s.contextCandidate[context]
127+
if !ok {
128+
return false
129+
}
130+
if len(requiredFunctions) == 0 && len(requiredEnums) == 0 {
131+
return true
132+
}
133+
for function := range requiredFunctions {
134+
if !candidate.hasFunctionName(function) {
135+
return false
136+
}
137+
}
138+
for enum := range requiredEnums {
139+
if !candidate.hasEnumSymbol((*EnumSymbol)(&enum)) {
140+
return false
141+
}
51142
}
143+
return true
144+
}
145+
146+
// inferFromLowerContexts returns the first lower context that supports all required functions
147+
// and enum symbols used on the statements.
148+
// If no lower context meets the requirements, or if the context candidate is unknown, it
149+
// returns an empty string.
150+
func (s *priorityContextInferrer) inferFromLowerContexts(
151+
context string,
152+
requiredFunctions map[string]struct{},
153+
requiredEnums map[enumSymbol]struct{},
154+
) string {
155+
inferredContextCandidate, ok := s.contextCandidate[context]
156+
if !ok {
157+
return ""
158+
}
159+
160+
lowerContextCandidates := inferredContextCandidate.getLowerContexts(context)
161+
if len(lowerContextCandidates) == 0 {
162+
return ""
163+
}
164+
165+
s.sortContextCandidates(lowerContextCandidates)
166+
for _, lowerCandidate := range lowerContextCandidates {
167+
ok = s.validateContextCandidate(lowerCandidate, requiredFunctions, requiredEnums)
168+
if ok {
169+
return lowerCandidate
170+
}
171+
}
172+
return ""
173+
}
174+
175+
// sortContextCandidates sorts the slice candidates using the priorityContextInferrer.contextsPriority order.
176+
func (s *priorityContextInferrer) sortContextCandidates(candidates []string) {
177+
slices.SortFunc(candidates, func(l, r string) int {
178+
lp, ok := s.contextPriority[l]
179+
if !ok {
180+
lp = math.MaxInt
181+
}
182+
rp, ok := s.contextPriority[r]
183+
if !ok {
184+
rp = math.MaxInt
185+
}
186+
return cmp.Compare(lp, rp)
187+
})
188+
}
52189

53-
return inferredContext, nil
190+
// getParsedStatementHints extracts all path, function names (editor and converter), and enumSymbol
191+
// from the given parsed statements. These values are used by the context inferrer as hints to
192+
// select a context in which the function/enum are supported.
193+
func (s *priorityContextInferrer) getParsedStatementHints(parsed *parsedStatement) ([]path, map[string]struct{}, map[enumSymbol]struct{}) {
194+
visitor := newGrammarContextInferrerVisitor()
195+
parsed.Editor.accept(&visitor)
196+
if parsed.WhereClause != nil {
197+
parsed.WhereClause.accept(&visitor)
198+
}
199+
return visitor.paths, visitor.functions, visitor.enumsSymbols
54200
}
55201

56-
// defaultPriorityContextInferrer is like newPriorityContextInferrer, but using the default
57-
// context priorities and ignoring unknown/non-prioritized contexts.
58-
func defaultPriorityContextInferrer() contextInferrer {
59-
return newPriorityContextInferrer(defaultContextInferPriority)
202+
// priorityContextInferrerHintsVisitor is a grammarVisitor implementation that collects
203+
// all path, function names (converter.Function and editor.Function), and enumSymbol.
204+
type priorityContextInferrerHintsVisitor struct {
205+
paths []path
206+
functions map[string]struct{}
207+
enumsSymbols map[enumSymbol]struct{}
60208
}
61209

62-
// newPriorityContextInferrer creates a new priority-based context inferrer.
63-
// To infer the context, it compares all [ottl.Path.Context] values, prioritizing them based
64-
// on the provide contextsPriority argument, the lower the context position is in the array,
65-
// the more priority it will have over other items.
66-
// If unknown/non-prioritized contexts are found on the statements, they can be either ignored
67-
// or considered when no other prioritized context is found. To skip unknown contexts, the
68-
// ignoreUnknownContext argument must be set to false.
69-
func newPriorityContextInferrer(contextsPriority []string) contextInferrer {
70-
contextPriority := make(map[string]int, len(contextsPriority))
71-
for i, ctx := range contextsPriority {
72-
contextPriority[ctx] = i
210+
func newGrammarContextInferrerVisitor() priorityContextInferrerHintsVisitor {
211+
return priorityContextInferrerHintsVisitor{
212+
paths: []path{},
213+
functions: make(map[string]struct{}),
214+
enumsSymbols: make(map[enumSymbol]struct{}),
73215
}
74-
return &priorityContextInferrer{
75-
contextPriority: contextPriority,
216+
}
217+
218+
func (v *priorityContextInferrerHintsVisitor) visitMathExprLiteral(_ *mathExprLiteral) {}
219+
220+
func (v *priorityContextInferrerHintsVisitor) visitEditor(e *editor) {
221+
v.functions[e.Function] = struct{}{}
222+
}
223+
224+
func (v *priorityContextInferrerHintsVisitor) visitConverter(c *converter) {
225+
v.functions[c.Function] = struct{}{}
226+
}
227+
228+
func (v *priorityContextInferrerHintsVisitor) visitValue(va *value) {
229+
if va.Enum != nil {
230+
v.enumsSymbols[*va.Enum] = struct{}{}
76231
}
77232
}
233+
234+
func (v *priorityContextInferrerHintsVisitor) visitPath(value *path) {
235+
v.paths = append(v.paths, *value)
236+
}

0 commit comments

Comments
 (0)