Skip to content

Commit bf85471

Browse files
edmocostaTylerHelmuthevan-bradley
authored
[pkg/ottl] Add the ottl.ParserCollection utility (open-telemetry#36174)
Co-authored-by: Tyler Helmuth <[email protected]> Co-authored-by: Evan Bradley <[email protected]>
1 parent 75b86ee commit bf85471

File tree

3 files changed

+766
-0
lines changed

3 files changed

+766
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 the `ottl.ParserCollection` utility to help handling parsers for multiple OTTL contexts"
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: [29017]
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+
The `ottl.ParserCollection` groups contexts' `ottl.Parser`s, choosing the suitable one
20+
to parse a given statement. It supports context inference using the given statements,
21+
and allows prepending the context name to the statements' paths.
22+
23+
# If your change doesn't affect end users or the exported elements of any package,
24+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
25+
# Optional: The change log or logs in which this entry should be included.
26+
# e.g. '[user]' or '[user, api]'
27+
# Include 'user' if the change is relevant to end users.
28+
# Include 'api' if there is a change to a library API.
29+
# Default: '[user]'
30+
change_logs: [api]

pkg/ottl/parser_collection.go

+334
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ottl // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
5+
6+
import (
7+
"fmt"
8+
"reflect"
9+
10+
"go.opentelemetry.io/collector/component"
11+
"go.uber.org/zap"
12+
)
13+
14+
// Safeguard to statically ensure the Parser.ParseStatements method can be reflectively
15+
// invoked by the ottlParserWrapper.parseStatements
16+
var _ interface {
17+
ParseStatements(statements []string) ([]*Statement[any], error)
18+
} = (*Parser[any])(nil)
19+
20+
// Safeguard to statically ensure any ParsedStatementConverter method can be reflectively
21+
// invoked by the statementsConverterWrapper.call
22+
var _ ParsedStatementConverter[any, any] = func(
23+
_ *ParserCollection[any],
24+
_ *Parser[any],
25+
_ string,
26+
_ StatementsGetter,
27+
_ []*Statement[any],
28+
) (any, error) {
29+
return nil, nil
30+
}
31+
32+
// StatementsGetter represents a set of statements to be parsed.
33+
//
34+
// Experimental: *NOTE* this API is subject to change or removal in the future.
35+
type StatementsGetter interface {
36+
// GetStatements retrieves the OTTL statements to be parsed
37+
GetStatements() []string
38+
}
39+
40+
type defaultStatementsGetter []string
41+
42+
func (d defaultStatementsGetter) GetStatements() []string {
43+
return d
44+
}
45+
46+
// NewStatementsGetter creates a new StatementsGetter.
47+
//
48+
// Experimental: *NOTE* this API is subject to change or removal in the future.
49+
func NewStatementsGetter(statements []string) StatementsGetter {
50+
return defaultStatementsGetter(statements)
51+
}
52+
53+
// ottlParserWrapper wraps an ottl.Parser using reflection, so it can invoke exported
54+
// methods without knowing its generic type (transform context).
55+
type ottlParserWrapper struct {
56+
parser reflect.Value
57+
prependContextToStatementPaths func(context string, statement string) (string, error)
58+
}
59+
60+
func newParserWrapper[K any](parser *Parser[K]) *ottlParserWrapper {
61+
return &ottlParserWrapper{
62+
parser: reflect.ValueOf(parser),
63+
prependContextToStatementPaths: parser.prependContextToStatementPaths,
64+
}
65+
}
66+
67+
func (g *ottlParserWrapper) parseStatements(statements []string) (reflect.Value, error) {
68+
method := g.parser.MethodByName("ParseStatements")
69+
parseStatementsRes := method.Call([]reflect.Value{reflect.ValueOf(statements)})
70+
err := parseStatementsRes[1]
71+
if !err.IsNil() {
72+
return reflect.Value{}, err.Interface().(error)
73+
}
74+
return parseStatementsRes[0], nil
75+
}
76+
77+
func (g *ottlParserWrapper) prependContextToStatementsPaths(context string, statements []string) ([]string, error) {
78+
result := make([]string, 0, len(statements))
79+
for _, s := range statements {
80+
prependedStatement, err := g.prependContextToStatementPaths(context, s)
81+
if err != nil {
82+
return nil, err
83+
}
84+
result = append(result, prependedStatement)
85+
}
86+
return result, nil
87+
}
88+
89+
// statementsConverterWrapper is a reflection-based wrapper to the ParsedStatementConverter function,
90+
// which does not require knowing all generic parameters to be called.
91+
type statementsConverterWrapper reflect.Value
92+
93+
func newStatementsConverterWrapper[K any, R any](converter ParsedStatementConverter[K, R]) statementsConverterWrapper {
94+
return statementsConverterWrapper(reflect.ValueOf(converter))
95+
}
96+
97+
func (s statementsConverterWrapper) call(
98+
parserCollection reflect.Value,
99+
ottlParser *ottlParserWrapper,
100+
context string,
101+
statements StatementsGetter,
102+
parsedStatements reflect.Value,
103+
) (reflect.Value, error) {
104+
result := reflect.Value(s).Call([]reflect.Value{
105+
parserCollection,
106+
ottlParser.parser,
107+
reflect.ValueOf(context),
108+
reflect.ValueOf(statements),
109+
parsedStatements,
110+
})
111+
112+
resultValue := result[0]
113+
resultError := result[1]
114+
if !resultError.IsNil() {
115+
return reflect.Value{}, resultError.Interface().(error)
116+
}
117+
118+
return resultValue, nil
119+
}
120+
121+
// parserCollectionParser holds an ottlParserWrapper and its respectively
122+
// statementsConverter function.
123+
type parserCollectionParser struct {
124+
ottlParser *ottlParserWrapper
125+
statementsConverter statementsConverterWrapper
126+
}
127+
128+
// ParserCollection is a configurable set of ottl.Parser that can handle multiple OTTL contexts
129+
// parsings, inferring the context, choosing the right parser for the given statements, and
130+
// transforming the parsed ottl.Statement[K] slice into a common result of type R.
131+
//
132+
// Experimental: *NOTE* this API is subject to change or removal in the future.
133+
type ParserCollection[R any] struct {
134+
contextParsers map[string]*parserCollectionParser
135+
contextInferrer contextInferrer
136+
modifiedStatementLogging bool
137+
Settings component.TelemetrySettings
138+
ErrorMode ErrorMode
139+
}
140+
141+
// ParserCollectionOption is a configurable ParserCollection option.
142+
//
143+
// Experimental: *NOTE* this API is subject to change or removal in the future.
144+
type ParserCollectionOption[R any] func(*ParserCollection[R]) error
145+
146+
// NewParserCollection creates a new ParserCollection.
147+
//
148+
// Experimental: *NOTE* this API is subject to change or removal in the future.
149+
func NewParserCollection[R any](
150+
settings component.TelemetrySettings,
151+
options ...ParserCollectionOption[R],
152+
) (*ParserCollection[R], error) {
153+
pc := &ParserCollection[R]{
154+
Settings: settings,
155+
contextParsers: map[string]*parserCollectionParser{},
156+
contextInferrer: defaultPriorityContextInferrer(),
157+
}
158+
159+
for _, op := range options {
160+
err := op(pc)
161+
if err != nil {
162+
return nil, err
163+
}
164+
}
165+
166+
return pc, nil
167+
}
168+
169+
// ParsedStatementConverter is a function that converts the parsed ottl.Statement[K] into
170+
// a common representation to all parser collection contexts passed through WithParserCollectionContext.
171+
// Given each parser has its own transform context type, they must agree on a common type [R]
172+
// so it can be returned by the ParserCollection.ParseStatements and ParserCollection.ParseStatementsWithContext
173+
// functions.
174+
//
175+
// Experimental: *NOTE* this API is subject to change or removal in the future.
176+
type ParsedStatementConverter[K any, R any] func(
177+
collection *ParserCollection[R],
178+
parser *Parser[K],
179+
context string,
180+
statements StatementsGetter,
181+
parsedStatements []*Statement[K],
182+
) (R, error)
183+
184+
func newNopParsedStatementConverter[K any]() ParsedStatementConverter[K, any] {
185+
return func(
186+
_ *ParserCollection[any],
187+
_ *Parser[K],
188+
_ string,
189+
_ StatementsGetter,
190+
parsedStatements []*Statement[K],
191+
) (any, error) {
192+
return parsedStatements, nil
193+
}
194+
}
195+
196+
// WithParserCollectionContext configures an ottl.Parser for the given context.
197+
// The provided ottl.Parser must be configured to support the provided context using
198+
// the ottl.WithPathContextNames option.
199+
//
200+
// Experimental: *NOTE* this API is subject to change or removal in the future.
201+
func WithParserCollectionContext[K any, R any](
202+
context string,
203+
parser *Parser[K],
204+
converter ParsedStatementConverter[K, R],
205+
) ParserCollectionOption[R] {
206+
return func(mp *ParserCollection[R]) error {
207+
if _, ok := parser.pathContextNames[context]; !ok {
208+
return fmt.Errorf(`context "%s" must be a valid "%T" path context name`, context, parser)
209+
}
210+
mp.contextParsers[context] = &parserCollectionParser{
211+
ottlParser: newParserWrapper[K](parser),
212+
statementsConverter: newStatementsConverterWrapper(converter),
213+
}
214+
return nil
215+
}
216+
}
217+
218+
// WithParserCollectionErrorMode has no effect on the ParserCollection, but might be used
219+
// by the ParsedStatementConverter functions to handle/create StatementSequence.
220+
//
221+
// Experimental: *NOTE* this API is subject to change or removal in the future.
222+
func WithParserCollectionErrorMode[R any](errorMode ErrorMode) ParserCollectionOption[R] {
223+
return func(tp *ParserCollection[R]) error {
224+
tp.ErrorMode = errorMode
225+
return nil
226+
}
227+
}
228+
229+
// EnableParserCollectionModifiedStatementLogging controls the statements modification logs.
230+
// When enabled, it logs any statements modifications performed by the parsing operations,
231+
// instructing users to rewrite the statements accordingly.
232+
//
233+
// Experimental: *NOTE* this API is subject to change or removal in the future.
234+
func EnableParserCollectionModifiedStatementLogging[R any](enabled bool) ParserCollectionOption[R] {
235+
return func(tp *ParserCollection[R]) error {
236+
tp.modifiedStatementLogging = enabled
237+
return nil
238+
}
239+
}
240+
241+
// ParseStatements parses the given statements into [R] using the configured context's ottl.Parser
242+
// and subsequently calling the ParsedStatementConverter function.
243+
// The statement's context is automatically inferred from the [Path.Context] values, choosing the
244+
// highest priority context found.
245+
// If no contexts are present in the statements, or if the inferred value is not supported by
246+
// the [ParserCollection], it returns an error.
247+
// If parsing the statements fails, it returns the underlying [ottl.Parser.ParseStatements] error.
248+
//
249+
// Experimental: *NOTE* this API is subject to change or removal in the future.
250+
func (pc *ParserCollection[R]) ParseStatements(statements StatementsGetter) (R, error) {
251+
statementsValues := statements.GetStatements()
252+
inferredContext, err := pc.contextInferrer.infer(statementsValues)
253+
if err != nil {
254+
return *new(R), err
255+
}
256+
257+
if inferredContext == "" {
258+
return *new(R), fmt.Errorf("unable to infer context from statements [%v], path's first segment must be a valid context name", statementsValues)
259+
}
260+
261+
return pc.ParseStatementsWithContext(inferredContext, statements, false)
262+
}
263+
264+
// ParseStatementsWithContext parses the given statements into [R] using the configured
265+
// context's ottl.Parser and subsequently calling the ParsedStatementConverter function.
266+
// Unlike ParseStatements, it uses the provided context and does not infer it
267+
// automatically. The context value must be supported by the [ParserCollection],
268+
// otherwise an error is returned.
269+
// If the statement's Path does not provide their Path.Context value, the prependPathsContext
270+
// argument should be set to true, so it rewrites the statements prepending the missing paths
271+
// contexts.
272+
// If parsing the statements fails, it returns the underlying [ottl.Parser.ParseStatements] error.
273+
//
274+
// Experimental: *NOTE* this API is subject to change or removal in the future.
275+
func (pc *ParserCollection[R]) ParseStatementsWithContext(context string, statements StatementsGetter, prependPathsContext bool) (R, error) {
276+
contextParser, ok := pc.contextParsers[context]
277+
if !ok {
278+
return *new(R), fmt.Errorf(`unknown context "%s" for stataments: %v`, context, statements.GetStatements())
279+
}
280+
281+
var err error
282+
var parsingStatements []string
283+
if prependPathsContext {
284+
originalStatements := statements.GetStatements()
285+
parsingStatements, err = contextParser.ottlParser.prependContextToStatementsPaths(context, originalStatements)
286+
if err != nil {
287+
return *new(R), err
288+
}
289+
if pc.modifiedStatementLogging {
290+
pc.logModifiedStatements(originalStatements, parsingStatements)
291+
}
292+
} else {
293+
parsingStatements = statements.GetStatements()
294+
}
295+
296+
parsedStatements, err := contextParser.ottlParser.parseStatements(parsingStatements)
297+
if err != nil {
298+
return *new(R), err
299+
}
300+
301+
convertedStatements, err := contextParser.statementsConverter.call(
302+
reflect.ValueOf(pc),
303+
contextParser.ottlParser,
304+
context,
305+
statements,
306+
parsedStatements,
307+
)
308+
if err != nil {
309+
return *new(R), err
310+
}
311+
312+
if convertedStatements.IsNil() {
313+
return *new(R), nil
314+
}
315+
316+
return convertedStatements.Interface().(R), nil
317+
}
318+
319+
func (pc *ParserCollection[R]) logModifiedStatements(originalStatements, modifiedStatements []string) {
320+
var fields []zap.Field
321+
for i, original := range originalStatements {
322+
if modifiedStatements[i] != original {
323+
statementKey := fmt.Sprintf("[%v]", i)
324+
fields = append(fields, zap.Dict(
325+
statementKey,
326+
zap.String("original", original),
327+
zap.String("modified", modifiedStatements[i])),
328+
)
329+
}
330+
}
331+
if len(fields) > 0 {
332+
pc.Settings.Logger.Info("one or more statements were modified to include their paths context, please rewrite them accordingly", zap.Dict("statements", fields...))
333+
}
334+
}

0 commit comments

Comments
 (0)