Skip to content

Commit 98f321b

Browse files
bacherfldjaglowskievan-bradley
authored andcommitted
[pkg/ottl]: add SliceToMap function (open-telemetry#35412)
**Description:** This PR adds a function that converts slices to maps, as described in the linked issue. Currently still WIP, but creating a draft PR already to show how this could be implemented and used **Link to tracking Issue:** open-telemetry#35256 **Testing:** Added unit and end to end tests **Documentation:** Added description for the new function in the readme file --------- Signed-off-by: Florian Bacher <[email protected]> Co-authored-by: Daniel Jaglowski <[email protected]> Co-authored-by: Evan Bradley <[email protected]>
1 parent 1ef1f83 commit 98f321b

File tree

6 files changed

+577
-3
lines changed

6 files changed

+577
-3
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 SliceToMap function
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: [35256]
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+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

pkg/ottl/e2e/e2e_test.go

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func Test_e2e_editors(t *testing.T) {
5757
tCtx.GetLogRecord().Attributes().Remove("flags")
5858
tCtx.GetLogRecord().Attributes().Remove("total.string")
5959
tCtx.GetLogRecord().Attributes().Remove("foo")
60+
tCtx.GetLogRecord().Attributes().Remove("things")
6061
},
6162
},
6263
{
@@ -67,6 +68,15 @@ func Test_e2e_editors(t *testing.T) {
6768
tCtx.GetLogRecord().Attributes().PutStr("foo.flags", "pass")
6869
tCtx.GetLogRecord().Attributes().PutStr("foo.slice.0", "val")
6970
tCtx.GetLogRecord().Attributes().PutStr("foo.nested.test", "pass")
71+
72+
tCtx.GetLogRecord().Attributes().Remove("things")
73+
m1 := tCtx.GetLogRecord().Attributes().PutEmptyMap("things.0")
74+
m1.PutStr("name", "foo")
75+
m1.PutInt("value", 2)
76+
77+
m2 := tCtx.GetLogRecord().Attributes().PutEmptyMap("things.1")
78+
m2.PutStr("name", "bar")
79+
m2.PutInt("value", 5)
7080
},
7181
},
7282
{
@@ -84,12 +94,29 @@ func Test_e2e_editors(t *testing.T) {
8494
m.PutStr("test.foo.flags", "pass")
8595
m.PutStr("test.foo.slice.0", "val")
8696
m.PutStr("test.foo.nested.test", "pass")
97+
98+
m1 := m.PutEmptyMap("test.things.0")
99+
m1.PutStr("name", "foo")
100+
m1.PutInt("value", 2)
101+
102+
m2 := m.PutEmptyMap("test.things.1")
103+
m2.PutStr("name", "bar")
104+
m2.PutInt("value", 5)
87105
m.CopyTo(tCtx.GetLogRecord().Attributes())
88106
},
89107
},
90108
{
91109
statement: `flatten(attributes, depth=0)`,
92-
want: func(_ ottllog.TransformContext) {},
110+
want: func(tCtx ottllog.TransformContext) {
111+
tCtx.GetLogRecord().Attributes().Remove("things")
112+
m1 := tCtx.GetLogRecord().Attributes().PutEmptyMap("things.0")
113+
m1.PutStr("name", "foo")
114+
m1.PutInt("value", 2)
115+
116+
m2 := tCtx.GetLogRecord().Attributes().PutEmptyMap("things.1")
117+
m2.PutStr("name", "bar")
118+
m2.PutInt("value", 5)
119+
},
93120
},
94121
{
95122
statement: `flatten(attributes, depth=1)`,
@@ -105,8 +132,17 @@ func Test_e2e_editors(t *testing.T) {
105132
m.PutStr("foo.bar", "pass")
106133
m.PutStr("foo.flags", "pass")
107134
m.PutStr("foo.slice.0", "val")
108-
m2 := m.PutEmptyMap("foo.nested")
109-
m2.PutStr("test", "pass")
135+
136+
m1 := m.PutEmptyMap("things.0")
137+
m1.PutStr("name", "foo")
138+
m1.PutInt("value", 2)
139+
140+
m2 := m.PutEmptyMap("things.1")
141+
m2.PutStr("name", "bar")
142+
m2.PutInt("value", 5)
143+
144+
m3 := m.PutEmptyMap("foo.nested")
145+
m3.PutStr("test", "pass")
110146
m.CopyTo(tCtx.GetLogRecord().Attributes())
111147
},
112148
},
@@ -117,6 +153,7 @@ func Test_e2e_editors(t *testing.T) {
117153
tCtx.GetLogRecord().Attributes().Remove("http.path")
118154
tCtx.GetLogRecord().Attributes().Remove("http.url")
119155
tCtx.GetLogRecord().Attributes().Remove("foo")
156+
tCtx.GetLogRecord().Attributes().Remove("things")
120157
},
121158
},
122159
{
@@ -131,6 +168,7 @@ func Test_e2e_editors(t *testing.T) {
131168
tCtx.GetLogRecord().Attributes().Remove("http.url")
132169
tCtx.GetLogRecord().Attributes().Remove("flags")
133170
tCtx.GetLogRecord().Attributes().Remove("foo")
171+
tCtx.GetLogRecord().Attributes().Remove("things")
134172
},
135173
},
136174
{
@@ -914,6 +952,28 @@ func Test_e2e_converters(t *testing.T) {
914952
m.PutStr("user_agent.version", "7.81.0")
915953
},
916954
},
955+
{
956+
statement: `set(attributes["test"], SliceToMap(attributes["things"], ["name"]))`,
957+
want: func(tCtx ottllog.TransformContext) {
958+
m := tCtx.GetLogRecord().Attributes().PutEmptyMap("test")
959+
thing1 := m.PutEmptyMap("foo")
960+
thing1.PutStr("name", "foo")
961+
thing1.PutInt("value", 2)
962+
963+
thing2 := m.PutEmptyMap("bar")
964+
thing2.PutStr("name", "bar")
965+
thing2.PutInt("value", 5)
966+
},
967+
},
968+
{
969+
statement: `set(attributes["test"], SliceToMap(attributes["things"], ["name"], ["value"]))`,
970+
want: func(tCtx ottllog.TransformContext) {
971+
m := tCtx.GetLogRecord().Attributes().PutEmptyMap("test")
972+
m.PutInt("foo", 2)
973+
m.PutInt("bar", 5)
974+
975+
},
976+
},
917977
}
918978

919979
for _, tt := range tests {
@@ -1112,6 +1172,15 @@ func constructLogTransformContext() ottllog.TransformContext {
11121172
m2 := m.PutEmptyMap("nested")
11131173
m2.PutStr("test", "pass")
11141174

1175+
s2 := logRecord.Attributes().PutEmptySlice("things")
1176+
thing1 := s2.AppendEmpty().SetEmptyMap()
1177+
thing1.PutStr("name", "foo")
1178+
thing1.PutInt("value", 2)
1179+
1180+
thing2 := s2.AppendEmpty().SetEmptyMap()
1181+
thing2.PutStr("name", "bar")
1182+
thing2.PutInt("value", 5)
1183+
11151184
return ottllog.NewTransformContext(logRecord, scope, resource, plog.NewScopeLogs(), plog.NewResourceLogs())
11161185
}
11171186

pkg/ottl/ottlfuncs/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ Available Converters:
456456
- [SHA1](#sha1)
457457
- [SHA256](#sha256)
458458
- [SHA512](#sha512)
459+
- [SliceToMap](#slicetomap)
459460
- [Sort](#sort)
460461
- [SpanID](#spanid)
461462
- [Split](#split)
@@ -1668,6 +1669,67 @@ Examples:
16681669

16691670
- `SHA512("name")`
16701671

1672+
### SliceToMap
1673+
1674+
`SliceToMap(target, keyPath, Optional[valuePath])`
1675+
1676+
The `SliceToMap` converter converts a slice of objects to a map. The arguments are as follows:
1677+
1678+
- `target`: A list of maps containing the entries to be converted.
1679+
- `keyPath`: A string array that determines the name of the keys for the map entries by pointing to the value of an attribute within each slice item. Note that
1680+
the `keyPath` must resolve to a string value, otherwise the converter will not be able to convert the item
1681+
to a map entry.
1682+
- `valuePath`: This optional string array determines which attribute should be used as the value for the map entry. If no
1683+
`valuePath` is defined, the value of the map entry will be the same as the original slice item.
1684+
1685+
Examples:
1686+
1687+
The examples below will convert the following input:
1688+
1689+
```yaml
1690+
attributes:
1691+
hello: world
1692+
things:
1693+
- name: foo
1694+
value: 2
1695+
- name: bar
1696+
value: 5
1697+
```
1698+
1699+
- `SliceToMap(attributes["things"], ["name"])`:
1700+
1701+
This converts the input above to the following:
1702+
1703+
```yaml
1704+
attributes:
1705+
hello: world
1706+
things:
1707+
foo:
1708+
name: foo
1709+
value: 2
1710+
bar:
1711+
name: bar
1712+
value: 5
1713+
```
1714+
1715+
- `SliceToMap(attributes["things"], ["name"], ["value"])`:
1716+
1717+
This converts the input above to the following:
1718+
1719+
```yaml
1720+
attributes:
1721+
hello: world
1722+
things:
1723+
foo: 2
1724+
bar: 5
1725+
```
1726+
1727+
Once the `SliceToMap` function has been applied to a value, the converted entries are addressable via their keys:
1728+
1729+
- `set(attributes["thingsMap"], SliceToMap(attributes["things"], ["name"]))`
1730+
- `set(attributes["element_1"], attributes["thingsMap"]["foo'])`
1731+
- `set(attributes["element_2"], attributes["thingsMap"]["bar'])`
1732+
16711733
### Sort
16721734

16731735
`Sort(target, Optional[order])`
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"
5+
import (
6+
"fmt"
7+
8+
"go.opentelemetry.io/collector/pdata/pcommon"
9+
"golang.org/x/net/context"
10+
11+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
12+
)
13+
14+
type SliceToMapArguments[K any] struct {
15+
Target ottl.Getter[K]
16+
KeyPath []string
17+
ValuePath ottl.Optional[[]string]
18+
}
19+
20+
func NewSliceToMapFactory[K any]() ottl.Factory[K] {
21+
return ottl.NewFactory("SliceToMap", &SliceToMapArguments[K]{}, sliceToMapFunction[K])
22+
}
23+
24+
func sliceToMapFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
25+
args, ok := oArgs.(*SliceToMapArguments[K])
26+
if !ok {
27+
return nil, fmt.Errorf("SliceToMapFactory args must be of type *SliceToMapArguments[K")
28+
}
29+
30+
return getSliceToMapFunc(args.Target, args.KeyPath, args.ValuePath)
31+
}
32+
33+
func getSliceToMapFunc[K any](target ottl.Getter[K], keyPath []string, valuePath ottl.Optional[[]string]) (ottl.ExprFunc[K], error) {
34+
if len(keyPath) == 0 {
35+
return nil, fmt.Errorf("key path must contain at least one element")
36+
}
37+
return func(ctx context.Context, tCtx K) (any, error) {
38+
val, err := target.Get(ctx, tCtx)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
switch v := val.(type) {
44+
case []any:
45+
return sliceToMap(v, keyPath, valuePath)
46+
case pcommon.Slice:
47+
return sliceToMap(v.AsRaw(), keyPath, valuePath)
48+
default:
49+
return nil, fmt.Errorf("unsupported type provided to SliceToMap function: %T", v)
50+
}
51+
}, nil
52+
}
53+
54+
func sliceToMap(v []any, keyPath []string, valuePath ottl.Optional[[]string]) (any, error) {
55+
result := make(map[string]any, len(v))
56+
for _, elem := range v {
57+
e, ok := elem.(map[string]any)
58+
if !ok {
59+
return nil, fmt.Errorf("could not cast element '%v' to map[string]any", elem)
60+
}
61+
extractedKey, err := extractValue(e, keyPath)
62+
if err != nil {
63+
return nil, fmt.Errorf("could not extract key from element: %w", err)
64+
}
65+
66+
key, ok := extractedKey.(string)
67+
if !ok {
68+
return nil, fmt.Errorf("extracted key attribute is not of type string")
69+
}
70+
71+
if valuePath.IsEmpty() {
72+
result[key] = e
73+
continue
74+
}
75+
extractedValue, err := extractValue(e, valuePath.Get())
76+
if err != nil {
77+
return nil, fmt.Errorf("could not extract value from element: %w", err)
78+
}
79+
result[key] = extractedValue
80+
}
81+
m := pcommon.NewMap()
82+
if err := m.FromRaw(result); err != nil {
83+
return nil, fmt.Errorf("could not create pcommon.Map from result: %w", err)
84+
}
85+
86+
return m, nil
87+
}
88+
89+
func extractValue(v map[string]any, path []string) (any, error) {
90+
if len(path) == 0 {
91+
return nil, fmt.Errorf("must provide at least one path item")
92+
}
93+
obj, ok := v[path[0]]
94+
if !ok {
95+
return nil, fmt.Errorf("provided object does not contain the path %v", path)
96+
}
97+
if len(path) == 1 {
98+
return obj, nil
99+
}
100+
101+
if o, ok := obj.(map[string]any); ok {
102+
return extractValue(o, path[1:])
103+
}
104+
return nil, fmt.Errorf("provided object does not contain the path %v", path)
105+
}

0 commit comments

Comments
 (0)