|
3 | 3 |
|
4 | 4 | package ottl // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
|
5 | 5 |
|
6 |
| -import "math" |
| 6 | +import ( |
| 7 | + "cmp" |
| 8 | + "math" |
| 9 | + "slices" |
| 10 | +) |
7 | 11 |
|
8 | 12 | var defaultContextInferPriority = []string{
|
9 | 13 | "log",
|
10 |
| - "metric", |
11 | 14 | "datapoint",
|
| 15 | + "metric", |
12 | 16 | "spanevent",
|
13 | 17 | "span",
|
14 | 18 | "resource",
|
15 | 19 | "scope",
|
16 | 20 | "instrumentation_scope",
|
17 | 21 | }
|
18 | 22 |
|
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. |
20 | 24 | 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. |
22 | 26 | infer(statements []string) (string, error)
|
23 | 27 | }
|
24 | 28 |
|
25 | 29 | 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 | + } |
27 | 75 | }
|
28 | 76 |
|
29 | 77 | func (s *priorityContextInferrer) infer(statements []string) (string, error) {
|
| 78 | + requiredFunctions := map[string]struct{}{} |
| 79 | + requiredEnums := map[enumSymbol]struct{}{} |
| 80 | + |
30 | 81 | var inferredContext string
|
31 | 82 | var inferredContextPriority int
|
32 |
| - |
33 | 83 | for _, statement := range statements {
|
34 | 84 | parsed, err := parseStatement(statement)
|
35 | 85 | if err != nil {
|
36 |
| - return inferredContext, err |
| 86 | + return "", err |
37 | 87 | }
|
38 | 88 |
|
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] |
41 | 93 | if !ok {
|
42 |
| - // Lowest priority |
43 |
| - pathContextPriority = math.MaxInt |
| 94 | + candidatePriority = math.MaxInt |
44 | 95 | }
|
45 |
| - |
46 |
| - if inferredContext == "" || pathContextPriority < inferredContextPriority { |
47 |
| - inferredContext = p.Context |
48 |
| - inferredContextPriority = pathContextPriority |
| 96 | + if inferredContext == "" || candidatePriority < inferredContextPriority { |
| 97 | + inferredContext = candidate |
| 98 | + inferredContextPriority = candidatePriority |
49 | 99 | }
|
50 | 100 | }
|
| 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 | + } |
51 | 142 | }
|
| 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 | +} |
52 | 189 |
|
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 |
54 | 200 | }
|
55 | 201 |
|
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{} |
60 | 208 | }
|
61 | 209 |
|
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{}), |
73 | 215 | }
|
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{}{} |
76 | 231 | }
|
77 | 232 | }
|
| 233 | + |
| 234 | +func (v *priorityContextInferrerHintsVisitor) visitPath(value *path) { |
| 235 | + v.paths = append(v.paths, *value) |
| 236 | +} |
0 commit comments