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 support for map literals #34168

Merged
merged 13 commits into from
Aug 12, 2024
Merged
27 changes: 27 additions & 0 deletions .chloggen/ottl-map-literals.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: pkg/ottl

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add support for map literals in OTTL

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [32388]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
11 changes: 11 additions & 0 deletions pkg/ottl/LANGUAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Values are passed as function parameters or are used in a Boolean Expression. Va
- [Enums](#enums)
- [Converters](#converters)
- [Math Expressions](#math-expressions)
- [Maps](#maps)

### Paths

Expand Down Expand Up @@ -155,6 +156,16 @@ Example List Values:
- `["1", "2", "3"]`
- `["a", attributes["key"], Concat(["a", "b"], "-")]`

### Maps

A Map Value comprises a set of key Value pairs.

Example Map Values:
- `{}`
- `{"foo": "bar"}`
- `{"foo": {"a": 2}}`
- `{"foo": {"a": attributes["key"]}}`

### Literals

Literals are literal interpretations of the Value into a Go value. Accepted literals are:
Expand Down
19 changes: 19 additions & 0 deletions pkg/ottl/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,25 @@ func Test_e2e_ottl_features(t *testing.T) {
statement: `set(attributes["test"], attributes["metadata"]["uid"])`,
want: func(_ ottllog.TransformContext) {},
},
{
name: "map value",
evan-bradley marked this conversation as resolved.
Show resolved Hide resolved
statement: `set(body, {"_raw": body, "test": {"result": attributes["foo"]["bar"], "time": UnixNano(time)}})`,
want: func(tCtx ottllog.TransformContext) {
originalBody := tCtx.GetLogRecord().Body().AsString()
mapValue := tCtx.GetLogRecord().Body().SetEmptyMap()
mapValue.PutStr("_raw", originalBody)
mv1 := mapValue.PutEmptyMap("test")
mv1.PutStr("result", "pass")
mv1.PutInt("time", 1581452772000000321)
},
},
{
name: "map value as input to function",
statement: `set(attributes["isMap"], IsMap({"foo": {"bar": "baz", "test": "pass"}}))`,
want: func(tCtx ottllog.TransformContext) {
tCtx.GetLogRecord().Attributes().PutBool("isMap", true)
},
},
}

for _, tt := range tests {
Expand Down
38 changes: 38 additions & 0 deletions pkg/ottl/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ func (l *listGetter[K]) Get(ctx context.Context, tCtx K) (any, error) {
return evaluated, nil
}

type mapGetter[K any] struct {
mapValues map[string]Getter[K]
}

func (m *mapGetter[K]) Get(ctx context.Context, tCtx K) (any, error) {
evaluated := map[string]any{}
for k, v := range m.mapValues {
val, err := v.Get(ctx, tCtx)
if err != nil {
return nil, err
}
switch t := val.(type) {
case pcommon.Map:
evaluated[k] = t.AsRaw()
default:
evaluated[k] = t
}

}
result := pcommon.NewMap()
if err := result.FromRaw(evaluated); err != nil {
return nil, err
}
return result, nil
}

// TypeError represents that a value was not an expected type.
type TypeError string

Expand Down Expand Up @@ -738,6 +764,18 @@ func (p *Parser[K]) newGetter(val value) (Getter[K], error) {
return &lg, nil
}

if val.Map != nil {
mg := mapGetter[K]{mapValues: map[string]Getter[K]{}}
for _, kvp := range val.Map.Values {
getter, err := p.newGetter(*kvp.Value)
if err != nil {
return nil, err
}
mg.mapValues[*kvp.Key] = getter
}
return &mg, nil
evan-bradley marked this conversation as resolved.
Show resolved Hide resolved
}

if val.MathExpression == nil {
// In practice, can't happen since the DSL grammar guarantees one is set
return nil, fmt.Errorf("no value field set. This is a bug in the OpenTelemetry Transformation Language")
Expand Down
120 changes: 119 additions & 1 deletion pkg/ottl/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,116 @@ func Test_newGetter(t *testing.T) {
},
want: []any{"test0", int64(1)},
},
{
name: "map",
val: value{
Map: &mapValue{
Values: []mapItem{
{
Key: ottltest.Strp("stringAttr"),
Value: &value{String: ottltest.Strp("value")},
},
{
Key: ottltest.Strp("intAttr"),
Value: &value{
Literal: &mathExprLiteral{
Int: ottltest.Intp(3),
},
},
},
{
Key: ottltest.Strp("floatAttr"),
Value: &value{
Literal: &mathExprLiteral{
Float: ottltest.Floatp(2.5),
},
},
},
{
Key: ottltest.Strp("boolAttr"),
Value: &value{Bool: (*boolean)(ottltest.Boolp(true))},
},
{
Key: ottltest.Strp("byteAttr"),
Value: &value{Bytes: (*byteSlice)(&[]byte{1, 2, 3, 4, 5, 6, 7, 8})},
},
{
Key: ottltest.Strp("enumAttr"),
Value: &value{Enum: (*enumSymbol)(ottltest.Strp("TEST_ENUM_ONE"))},
},
{
Key: ottltest.Strp("pathAttr"),
Value: &value{
Literal: &mathExprLiteral{
Path: &path{
Fields: []field{
{
Name: "name",
},
},
},
},
},
},
{
Key: ottltest.Strp("mapAttr"),
Value: &value{
Map: &mapValue{
Values: []mapItem{
{
Key: ottltest.Strp("foo"),
Value: &value{
Map: &mapValue{
Values: []mapItem{
{
Key: ottltest.Strp("test"),
Value: &value{String: ottltest.Strp("value")},
},
},
},
},
},
{
Key: ottltest.Strp("listAttr"),
Value: &value{
List: &list{
Values: []value{
{
String: ottltest.Strp("test0"),
},
{
Literal: &mathExprLiteral{
Int: ottltest.Intp(1),
},
},
},
},
},
},
},
},
},
},
},
},
},
ctx: "bear",
want: map[string]any{
"enumAttr": int64(1),
"pathAttr": "bear",
"mapAttr": map[string]any{
"foo": map[string]any{
"test": "value",
},
"listAttr": []any{"test0", int64(1)},
},
"stringAttr": "value",
"intAttr": int64(3),
"floatAttr": 2.5,
"boolAttr": true,
"byteAttr": []byte{1, 2, 3, 4, 5, 6, 7, 8},
},
},
}

functions := CreateFactoryMap(
Expand Down Expand Up @@ -444,7 +554,15 @@ func Test_newGetter(t *testing.T) {

val, err := reader.Get(context.Background(), tCtx)
assert.NoError(t, err)
assert.Equal(t, tt.want, val)

switch v := val.(type) {
case pcommon.Map:
// need to compare the raw map here as require.EqualValues can not seem to handle
// the comparison of pcommon.Map
assert.EqualValues(t, tt.want, v.AsRaw())
default:
assert.EqualValues(t, tt.want, v)
}
})
}

Expand Down
13 changes: 13 additions & 0 deletions pkg/ottl/grammar.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ type value struct {
String *string `parser:"| @String"`
Bool *boolean `parser:"| @Boolean"`
Enum *enumSymbol `parser:"| @Uppercase (?! Lowercase)"`
Map *mapValue `parser:"| @@"`
List *list `parser:"| @@)"`
}

Expand Down Expand Up @@ -270,6 +271,15 @@ type list struct {
Values []value `parser:"'[' (@@)* (',' @@)* ']'"`
}

type mapValue struct {
Values []mapItem `parser:"'{' (@@ ','?)* '}'"`
}

type mapItem struct {
Key *string `parser:"@String ':'"`
Value *value `parser:"@@"`
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
}

// byteSlice type for capturing byte slices
type byteSlice []byte

Expand Down Expand Up @@ -444,6 +454,9 @@ func buildLexer() *lexer.StatefulDefinition {
{Name: `Equal`, Pattern: `=`},
{Name: `LParen`, Pattern: `\(`},
{Name: `RParen`, Pattern: `\)`},
{Name: `LBrace`, Pattern: `\{`},
{Name: `RBrace`, Pattern: `\}`},
{Name: `Colon`, Pattern: `\:`},
{Name: `Punct`, Pattern: `[,.\[\]]`},
{Name: `Uppercase`, Pattern: `[A-Z][A-Z0-9_]*`},
{Name: `Lowercase`, Pattern: `[a-z][a-z0-9_]*`},
Expand Down
9 changes: 8 additions & 1 deletion pkg/ottl/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func Test_lexer(t *testing.T) {
{"OpNot", "not"},
{"Boolean", "false"},
}},
{"nothing_recognizable", "{}", true, []result{
{"nothing_recognizable", "|", true, []result{
{"", ""},
}},
{"basic_ident_expr", `set(attributes["bytes"], 0x0102030405060708)`, false, []result{
Expand Down Expand Up @@ -123,6 +123,13 @@ func Test_lexer(t *testing.T) {
{"OpMultDiv", "*"},
{"Float", "2.9"},
}},
{"Map", `{"foo":"bar"}`, false, []result{
{"LBrace", "{"},
{"String", `"foo"`},
{"Colon", ":"},
{"String", `"bar"`},
{"RBrace", "}"},
}},
}

for _, tt := range tests {
Expand Down
Loading