Skip to content

Commit de19f99

Browse files
edmocostaArthurSens
authored andcommitted
[pkg/ottl] Add parser utility to rewrite statements appending missing paths context (open-telemetry#35716)
<!--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 adds the `ottl.Parser[K].AppendStatementPathsContext` function, allowing components to rewrite statements appending missing `ottl.path` context names. For examples, the following context-less statement: ``` set(value, 1) where name == attributes["foo.name"] ``` Would be rewritten using the `span` context as: ``` set(span.value, 1) where span.name == span.attributes["foo.name"] ``` **Why do we need to rewrite statements?** This utility will be used during the transition from structured OTTL statements to flat statements. Components such as the `transformprocessor` will leverage it to support both configuration styles, without forcing users to adapt/rewrite their existing config files. Once the component turns on the `ottl.Parser[K]` path's context validation, new configuration style usages will be validated, requiring all paths to have a context prefix, and old configuration styles will automatically rewrite the statements using this function. For more details, please have a look at the complete [draft](open-telemetry#35050) implementation. <!-- 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 3238a3f commit de19f99

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

pkg/ottl/parser.go

+53
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"sort"
11+
"strings"
1012

1113
"github.com/alecthomas/participle/v2"
1214
"go.opentelemetry.io/collector/component"
@@ -195,6 +197,33 @@ func (p *Parser[K]) ParseCondition(condition string) (*Condition[K], error) {
195197
}, nil
196198
}
197199

200+
// prependContextToStatementPaths changes the given OTTL statement adding the context name prefix
201+
// to all context-less paths. No modifications are performed for paths which [Path.Context]
202+
// value matches any WithPathContextNames value.
203+
// The context argument must be valid WithPathContextNames value, otherwise an error is returned.
204+
func (p *Parser[K]) prependContextToStatementPaths(context string, statement string) (string, error) {
205+
if _, ok := p.pathContextNames[context]; !ok {
206+
return statement, fmt.Errorf(`unknown context "%s" for parser %T, valid options are: %s`, context, p, p.buildPathContextNamesText(""))
207+
}
208+
parsed, err := parseStatement(statement)
209+
if err != nil {
210+
return "", err
211+
}
212+
paths := getParsedStatementPaths(parsed)
213+
if len(paths) == 0 {
214+
return statement, nil
215+
}
216+
217+
var missingContextOffsets []int
218+
for _, it := range paths {
219+
if _, ok := p.pathContextNames[it.Context]; !ok {
220+
missingContextOffsets = append(missingContextOffsets, it.Pos.Offset)
221+
}
222+
}
223+
224+
return insertContextIntoStatementOffsets(context, statement, missingContextOffsets)
225+
}
226+
198227
var parser = newParser[parsedStatement]()
199228
var conditionParser = newParser[booleanExpression]()
200229

@@ -226,6 +255,30 @@ func parseCondition(raw string) (*booleanExpression, error) {
226255
return parsed, nil
227256
}
228257

258+
func insertContextIntoStatementOffsets(context string, statement string, offsets []int) (string, error) {
259+
if len(offsets) == 0 {
260+
return statement, nil
261+
}
262+
263+
contextPrefix := context + "."
264+
var sb strings.Builder
265+
sb.Grow(len(statement) + (len(contextPrefix) * len(offsets)))
266+
267+
sort.Ints(offsets)
268+
left := 0
269+
for _, offset := range offsets {
270+
if offset < 0 || offset > len(statement) {
271+
return statement, fmt.Errorf(`failed to insert context "%s" into statement "%s": offset %d is out of range`, context, statement, offset)
272+
}
273+
sb.WriteString(statement[left:offset])
274+
sb.WriteString(contextPrefix)
275+
left = offset
276+
}
277+
sb.WriteString(statement[left:])
278+
279+
return sb.String(), nil
280+
}
281+
229282
// newParser returns a parser that can be used to read a string into a parsedStatement. An error will be returned if the string
230283
// is not formatted for the DSL.
231284
func newParser[G any]() *participle.Parser[G] {

pkg/ottl/parser_test.go

+137
Original file line numberDiff line numberDiff line change
@@ -2714,3 +2714,140 @@ func Test_ConditionSequence_Eval_Error(t *testing.T) {
27142714
})
27152715
}
27162716
}
2717+
2718+
func Test_prependContextToStatementPaths_InvalidStatement(t *testing.T) {
2719+
ps, err := NewParser(
2720+
CreateFactoryMap[any](),
2721+
testParsePath[any],
2722+
componenttest.NewNopTelemetrySettings(),
2723+
WithEnumParser[any](testParseEnum),
2724+
WithPathContextNames[any]([]string{"foo", "bar"}),
2725+
)
2726+
require.NoError(t, err)
2727+
_, err = ps.prependContextToStatementPaths("foo", "this is invalid")
2728+
require.ErrorContains(t, err, `statement has invalid syntax`)
2729+
}
2730+
2731+
func Test_prependContextToStatementPaths_InvalidContext(t *testing.T) {
2732+
ps, err := NewParser(
2733+
CreateFactoryMap[any](),
2734+
testParsePath[any],
2735+
componenttest.NewNopTelemetrySettings(),
2736+
WithEnumParser[any](testParseEnum),
2737+
WithPathContextNames[any]([]string{"foo", "bar"}),
2738+
)
2739+
require.NoError(t, err)
2740+
_, err = ps.prependContextToStatementPaths("foobar", "set(foo, 1)")
2741+
require.ErrorContains(t, err, `unknown context "foobar" for parser`)
2742+
}
2743+
2744+
func Test_prependContextToStatementPaths_Success(t *testing.T) {
2745+
type mockSetArguments[K any] struct {
2746+
Target Setter[K]
2747+
Value Getter[K]
2748+
}
2749+
2750+
mockSetFactory := NewFactory("set", &mockSetArguments[any]{}, func(_ FunctionContext, _ Arguments) (ExprFunc[any], error) {
2751+
return func(_ context.Context, _ any) (any, error) {
2752+
return nil, nil
2753+
}, nil
2754+
})
2755+
2756+
tests := []struct {
2757+
name string
2758+
statement string
2759+
context string
2760+
pathContextNames []string
2761+
expected string
2762+
}{
2763+
{
2764+
name: "no paths",
2765+
statement: `set("foo", 1)`,
2766+
context: "bar",
2767+
pathContextNames: []string{"bar"},
2768+
expected: `set("foo", 1)`,
2769+
},
2770+
{
2771+
name: "single path with context",
2772+
statement: `set(span.value, 1)`,
2773+
context: "span",
2774+
pathContextNames: []string{"span"},
2775+
expected: `set(span.value, 1)`,
2776+
},
2777+
{
2778+
name: "single path without context",
2779+
statement: "set(value, 1)",
2780+
context: "span",
2781+
pathContextNames: []string{"span"},
2782+
expected: "set(span.value, 1)",
2783+
},
2784+
{
2785+
name: "single path with context - multiple context names",
2786+
statement: "set(span.value, 1)",
2787+
context: "spanevent",
2788+
pathContextNames: []string{"spanevent", "span"},
2789+
expected: "set(span.value, 1)",
2790+
},
2791+
{
2792+
name: "multiple paths with the same context",
2793+
statement: `set(span.value, 1) where span.attributes["foo"] == "foo" and span.id == 1`,
2794+
context: "another",
2795+
pathContextNames: []string{"another", "span"},
2796+
expected: `set(span.value, 1) where span.attributes["foo"] == "foo" and span.id == 1`,
2797+
},
2798+
{
2799+
name: "multiple paths with different contexts",
2800+
statement: `set(another.value, 1) where span.attributes["foo"] == "foo" and another.id == 1`,
2801+
context: "another",
2802+
pathContextNames: []string{"another", "span"},
2803+
expected: `set(another.value, 1) where span.attributes["foo"] == "foo" and another.id == 1`,
2804+
},
2805+
{
2806+
name: "multiple paths with and without contexts",
2807+
statement: `set(value, 1) where span.attributes["foo"] == "foo" and id == 1`,
2808+
context: "spanevent",
2809+
pathContextNames: []string{"spanevent", "span"},
2810+
expected: `set(spanevent.value, 1) where span.attributes["foo"] == "foo" and spanevent.id == 1`,
2811+
},
2812+
{
2813+
name: "multiple paths without context",
2814+
statement: `set(value, 1) where name == attributes["foo.name"]`,
2815+
context: "span",
2816+
pathContextNames: []string{"span"},
2817+
expected: `set(span.value, 1) where span.name == span.attributes["foo.name"]`,
2818+
},
2819+
{
2820+
name: "function path parameter without context",
2821+
statement: `set(attributes["test"], "pass") where IsMatch(name, "operation[AC]")`,
2822+
context: "log",
2823+
pathContextNames: []string{"log"},
2824+
expected: `set(log.attributes["test"], "pass") where IsMatch(log.name, "operation[AC]")`,
2825+
},
2826+
{
2827+
name: "function path parameter with context",
2828+
statement: `set(attributes["test"], "pass") where IsMatch(resource.name, "operation[AC]")`,
2829+
context: "log",
2830+
pathContextNames: []string{"log", "resource"},
2831+
expected: `set(log.attributes["test"], "pass") where IsMatch(resource.name, "operation[AC]")`,
2832+
},
2833+
}
2834+
2835+
for _, tt := range tests {
2836+
t.Run(tt.name, func(t *testing.T) {
2837+
ps, err := NewParser(
2838+
CreateFactoryMap[any](mockSetFactory),
2839+
testParsePath[any],
2840+
componenttest.NewNopTelemetrySettings(),
2841+
WithEnumParser[any](testParseEnum),
2842+
WithPathContextNames[any](tt.pathContextNames),
2843+
)
2844+
2845+
require.NoError(t, err)
2846+
require.NotNil(t, ps)
2847+
2848+
result, err := ps.prependContextToStatementPaths(tt.context, tt.statement)
2849+
require.NoError(t, err)
2850+
assert.Equal(t, tt.expected, result)
2851+
})
2852+
}
2853+
}

0 commit comments

Comments
 (0)