Skip to content
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

[pkg/ottl] Add the ottl.ParserCollection utility #36174

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

edmocosta
Copy link
Contributor

Description

This PR is part of #29017, and adds the parser collection utility into the ottl package.

This utility is based on the existing transformprocessor's ParserCollection concept and is used to group context's parsers, abstracting the logic to infer the context from the statements, and/or rewriting them adding their paths's context if necessary.

In summary, the general idea is: given the configured contexts, parsers and statements converters, pick the right parser, and transform the parsed result (ottl.Statements[K]) into something common to the parser collection ([R]). For more details, please have a look at the transformprocessor` refactoring on the draft using this utility.

Given the ottl.Parser[K]'s have different type restrictions/generics (e.g. TransformContext), this utility heavily rely on the Go's reflection api, being the exposed interface type-safe, but manipulating untyped objects under the hood.

Considering this code is only executed during the bootstrap, IMO, it should be fine using reflection. I couldn't think about any other solution that would make this utility as generic as it's without reflection. Another approach would be hard-code all the existing ottl contexts's parsers on the collection, having fixed WithParserX functions per context and generating a direct dependency on the ottl/contexts package.

Link to tracking issue

#29017

Testing

Unit tests

Documentation

No changes

@edmocosta edmocosta marked this pull request as ready for review November 4, 2024 18:32
@edmocosta edmocosta requested a review from a team as a code owner November 4, 2024 18:32
Comment on lines +18 to +25
var _ ParsedStatementConverter[any, StatementsGetter, any] = func(
_ *ParserCollection[StatementsGetter, any],
_ *Parser[any],
_ string,
_ StatementsGetter,
_ []*Statement[any]) (any, error) {
return nil, nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #36174 (comment), but in this case it's not really necessary considering this interface is declared inside this same file.
We can definitely replace it by a comment, but IMO, I'd keep it as a safe-guard, considering changing this function signature could break the parser collection here, and in all its external usages. WDYT?

Comment on lines +14 to +16
var _ interface {
ParseStatements(statements []string) ([]*Statement[any], error)
} = (*Parser[any])(nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this enforcing that we'll be able to make the reflective call to ParseStatements?

Copy link
Contributor Author

@edmocosta edmocosta Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's an interface safe guard and is only being used to statically check the ParseStatements signature at compile time. It fails to compile If for some reason the ottl.Parser.ParseStatements changes (which could make the reflective call to fail).

Comment on lines +35 to +38
type ottlParserWrapper[S StatementsGetter] struct {
parser reflect.Value
prependContextToStatementPaths func(context string, statement string) (string, error)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this type need a generic? I don't see where it uses it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! that's correct, it does not need to be generic.


// ParserCollection is a configurable set of ottl.Parser that can handle multiple OTTL contexts
// parsings, inferring the context and choosing the right parser for the given statements.
type ParserCollection[S StatementsGetter, R any] struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is S StatementsGetter required? Would specifying parameters of type StatementGetter sufficient? Or even better can we get away with []string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is S StatementsGetter required?

Not really, I think we can drop it from the ParserCollection. It's only necessary for the ParsedStatementConverter.

// If no contexts are present in the statements, or if the inferred value is not supported by
// the [ParserCollection], it returns an error.
// If parsing the statements fails, it returns the underline [ottl.Parser.ParseStatements] error.
func (pc *ParserCollection[S, R]) ParseStatements(statements S) (R, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of taking an interface can this take []string like the parser's ParseStatements?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'd possible, but the idea of making it an interface was giving theParsedStatementConverter functions more flexibility to receive extra data, without the need of adding an extra argument for some custom data.

For example, the transformprocessor builds the parser collection and them invokes the ParseContextStatements function passing on a custom type ContextStatements argument. Besides the Statements []string property, that type has a Conditions []string property used to convert (see) the parsed ottl.Statement[K] into [R], which is only known after the parser collection is built.

In summary, having it as an interface allows the ParsedStatementConverter functions to access customised ParseStatements input arguments, avoiding extra parameters on the ParsedStatementConverter and making the parser collection reusable.

WDYT?

// argument should be set to true, so it rewrites the statements prepending the missing paths
// contexts.
// If parsing the statements fails, it returns the underline [ottl.Parser.ParseStatements] error.
func (pc *ParserCollection[S, R]) ParseStatementsWithContext(context string, statements S, prependPathsContext bool) (R, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of taking an generic S can this take []string like the parser's ParseStatements?

Copy link
Member

@TylerHelmuth TylerHelmuth Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against the use of reflection and generics (OTTL uses both a lot already), but we've also taken steps in the ottl context packages to make it easier to use OTTL in a component without needing to know about OTTL's generics. ottllog.NewParser exists so users don't have to do ottl.Parser[ottllog.TransformContext]. I believe we'll want to do something similar with this new API - It needs to be generic enough for others to provide their own context collections (thanks reflection), but our contexts should be able to hide this feature.

I believe this solution supports this but I want to call it out. In my head I want to still be able to do ottllog.NewParser and it takes care of using a ParserCollection behind the scenes, enforcing the valid contexts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this solution supports this but I want to call it out. In my head I want to still be able to do ottllog.NewParser and it takes care of using a ParserCollection behind the scenes, enforcing the valid contexts.

That's interesting, but I think it would require another type of API (builder perhaps) considering a single OTTL context (ottllog.NewParser) woudn't be able to build the whole parser collection. For example, if I want to parse OTTL log and metric contexts statements, I'd create a parser collection as:

logParser, _ := ottllog.NewParser(ottlfuncs.StandardFuncs[ottllog.TransformContext](), telemetrySettings, ottllog.WithPathContextNames())
metricParser, _ := ottlmetric.NewParser(ottlfuncs.StandardFuncs[ottlmetric.TransformContext](), telemetrySettings, ottlmetric.WithPathContextNames())

pc, err := ottl.NewParserCollection[any](
  telemetrySettings,
  ottl.WithParserCollectionContext("log", &logParser, {...}),
  ottl.WithParserCollectionContext("metric", &metricsParser, {...}),
)

pc.ParseStatements(statements{[]string{`set(log.attributes["key"], "value")`}})

It indeed does not completely hides the generic type, but I think it's somehow similar to the NewParser functions.

In my head I want to still be able to do ottllog.NewParser and it takes care of using a ParserCollection behind the scenes, enforcing the valid contexts.

The valid contexts are still being enforced by the underline parser, and not by the parser collection, for example, given the previous parser collection and the following log (new flat structure) statements:

- set(log.attributes["key"], "value")  # ok
- set(log.attributes["key"], attributes["value"]) # Parser fails and report a missing context for the `attributes["value"]` path
- set(log.attributes["key"], log.attributes["value"]) # ok
- set(log.attributes["key"], log.resource.attributes["value"]) # ok
- set(log.attributes["key"], log.instrumentation_scope.attributes["value"]) # ok
- set(log.attributes["key"], log.metric.attributes["value"]) # Parser fails as `metric` is not a valid path context name for the ottllog.NewParser (WithPathContextNames)
- set(trace.attributes["key"], trace.attributes["value"]) # ParserCollection fails as it does not support parsing `trace` context.

Please note that in this approach/initial version, the access to lower contexts would still be handled by the contexts themselves, for example logs, being the parser collection used only to parse top level contexts (inferred from the statements).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants