Skip to content

Commit 25a241a

Browse files
committed
Add OTTL delete function to delete items from an array
1 parent c93698b commit 25a241a

File tree

6 files changed

+665
-0
lines changed

6 files changed

+665
-0
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. receiver/filelog)
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: "Introducing `delete` function for deleting items from an existing array"
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: [43098]
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: [user]

pkg/ottl/e2e/e2e_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func Test_e2e_editors(t *testing.T) {
6262
tCtx.GetLogRecord().Attributes().Remove("things")
6363
tCtx.GetLogRecord().Attributes().Remove("conflict.conflict1")
6464
tCtx.GetLogRecord().Attributes().Remove("conflict")
65+
tCtx.GetLogRecord().Attributes().Remove("slice2")
6566
},
6667
},
6768
{
@@ -81,6 +82,12 @@ func Test_e2e_editors(t *testing.T) {
8182
tCtx.GetLogRecord().Attributes().PutInt("things.0.value", 2)
8283
tCtx.GetLogRecord().Attributes().PutStr("things.1.name", "bar")
8384
tCtx.GetLogRecord().Attributes().PutInt("things.1.value", 5)
85+
86+
tCtx.GetLogRecord().Attributes().Remove("slice2")
87+
tCtx.GetLogRecord().Attributes().PutStr("slice2.0", "val")
88+
tCtx.GetLogRecord().Attributes().PutStr("slice2.1", "foo")
89+
tCtx.GetLogRecord().Attributes().PutStr("slice2.2", "bar")
90+
tCtx.GetLogRecord().Attributes().PutStr("slice2.3", "baz")
8491
},
8592
},
8693
{
@@ -105,6 +112,11 @@ func Test_e2e_editors(t *testing.T) {
105112
m.PutInt("test.things.0.value", 2)
106113
m.PutStr("test.things.1.name", "bar")
107114
m.PutInt("test.things.1.value", 5)
115+
116+
m.PutStr("test.slice2.0", "val")
117+
m.PutStr("test.slice2.1", "foo")
118+
m.PutStr("test.slice2.2", "bar")
119+
m.PutStr("test.slice2.3", "baz")
108120
m.CopyTo(tCtx.GetLogRecord().Attributes())
109121
},
110122
},
@@ -133,6 +145,11 @@ func Test_e2e_editors(t *testing.T) {
133145
m.PutStr("test.things.1.name", "bar")
134146
m.PutInt("test.things.1.value", 5)
135147

148+
m.PutStr("test.slice2", "val")
149+
m.PutStr("test.slice2.0", "foo")
150+
m.PutStr("test.slice2.1", "bar")
151+
m.PutStr("test.slice2.2", "baz")
152+
136153
m.CopyTo(tCtx.GetLogRecord().Attributes())
137154
},
138155
},
@@ -149,6 +166,10 @@ func Test_e2e_editors(t *testing.T) {
149166
m.PutStr("foo.flags", "pass")
150167
m.PutStr("foo.bar", "pass")
151168
m.PutStr("foo.flags", "pass")
169+
m.PutStr("slice2.0", "val")
170+
m.PutStr("slice2.1", "foo")
171+
m.PutStr("slice2.2", "bar")
172+
m.PutStr("slice2.3", "baz")
152173
m.PutEmptySlice("foo.slice").AppendEmpty().SetStr("val")
153174
m.PutStr("conflict.conflict1.conflict2", "nopass")
154175
mm := m.PutEmptyMap("conflict.conflict1")
@@ -177,6 +198,7 @@ func Test_e2e_editors(t *testing.T) {
177198
tCtx.GetLogRecord().Attributes().Remove("things")
178199
tCtx.GetLogRecord().Attributes().Remove("conflict.conflict1")
179200
tCtx.GetLogRecord().Attributes().Remove("conflict")
201+
tCtx.GetLogRecord().Attributes().Remove("slice2")
180202
},
181203
},
182204
{
@@ -194,6 +216,7 @@ func Test_e2e_editors(t *testing.T) {
194216
tCtx.GetLogRecord().Attributes().Remove("things")
195217
tCtx.GetLogRecord().Attributes().Remove("conflict.conflict1")
196218
tCtx.GetLogRecord().Attributes().Remove("conflict")
219+
tCtx.GetLogRecord().Attributes().Remove("slice2")
197220
},
198221
},
199222
{
@@ -375,6 +398,53 @@ func Test_e2e_editors(t *testing.T) {
375398
s.AppendEmpty().SetInt(6)
376399
},
377400
},
401+
{
402+
statement: `delete(attributes["slice2"], 0)`,
403+
want: func(tCtx ottllog.TransformContext) {
404+
v, _ := tCtx.GetLogRecord().Attributes().Get("slice2")
405+
s := v.Slice()
406+
// :ToDo: Implement RemoveAt and RemoveRange in pcommon.Slice
407+
s.RemoveIf(func(v pcommon.Value) bool {
408+
return v.Str() == "val"
409+
})
410+
},
411+
},
412+
{
413+
statement: `delete(attributes["slice2"], Len(attributes["slice2"]) - 1)`,
414+
want: func(tCtx ottllog.TransformContext) {
415+
v, _ := tCtx.GetLogRecord().Attributes().Get("slice2")
416+
s := v.Slice()
417+
s.RemoveIf(func(v pcommon.Value) bool {
418+
return v.Str() == "baz"
419+
})
420+
},
421+
},
422+
{
423+
statement: `delete(attributes["slice2"], 1, length=2)`,
424+
want: func(tCtx ottllog.TransformContext) {
425+
v, _ := tCtx.GetLogRecord().Attributes().Get("slice2")
426+
s := v.Slice()
427+
s.RemoveIf(func(v pcommon.Value) bool {
428+
return (v.Str() == "foo" || v.Str() == "bar")
429+
})
430+
},
431+
},
432+
{
433+
statement: `delete(attributes["slice2"], Index(attributes["slice2"], "foo"))`,
434+
want: func(tCtx ottllog.TransformContext) {
435+
v, _ := tCtx.GetLogRecord().Attributes().Get("slice2")
436+
s := v.Slice()
437+
s.RemoveIf(func(v pcommon.Value) bool {
438+
return v.Str() == "foo"
439+
})
440+
},
441+
},
442+
{
443+
statement: `delete(attributes["slice2"], Index(attributes["slice2"], "not_found"))`,
444+
want: func(tCtx ottllog.TransformContext) {
445+
// No change as "not_found" does not exist in the slice.
446+
},
447+
},
378448
}
379449

380450
for _, tt := range tests {
@@ -1994,6 +2064,12 @@ func constructLogTransformContextEditors() ottllog.TransformContext {
19942064
thing2.PutStr("name", "bar")
19952065
thing2.PutInt("value", 5)
19962066

2067+
s3 := logRecord.Attributes().PutEmptySlice("slice2")
2068+
s3.AppendEmpty().SetStr("val")
2069+
s3.AppendEmpty().SetStr("foo")
2070+
s3.AppendEmpty().SetStr("bar")
2071+
s3.AppendEmpty().SetStr("baz")
2072+
19972073
return ottllog.NewTransformContext(logRecord, scope, resource, plog.NewScopeLogs(), plog.NewResourceLogs())
19982074
}
19992075

pkg/ottl/ottlfuncs/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Editors:
4646
Available Editors:
4747

4848
- [append](#append)
49+
- [delete](#delete)
4950
- [delete_key](#delete_key)
5051
- [delete_matching_keys](#delete_matching_keys)
5152
- [keep_matching_keys](#keep_matching_keys)
@@ -73,6 +74,22 @@ Resulting field is always of type `pcommon.Slice` and will not convert the types
7374
- `append(log.attributes["tags"], values = ["staging", "staging:east"])`
7475
- `append(log.attributes["tags_copy"], log.attributes["tags"])`
7576

77+
### delete
78+
79+
`delete(target, index, Optional[length])`
80+
81+
The `delete` function deletes elements starting at `index` to `index + length` from target array/slice. If lengh is not provided, only element at `target[index]` will be deleted. If `index` is calculated using `Index(target, value)` and there's no match in the slice, `target` array won't be changed.
82+
83+
Examples:
84+
85+
- `delete(attributes["tags"], 0)` # delete first
86+
87+
- `delete(attributes["tags"], Len(attributes["tags"]) - 1)` # delete last
88+
89+
- `delete(attributes["tags"], 0, 3)` # delete indexes 0, 1 & 2
90+
91+
- `delete(attributes["tags"], Index(attributes["tags"], "unparsed"))` # delete first occurrence of "unparsed"
92+
7693
### delete_key
7794

7895
`delete_key(target, key)`

pkg/ottl/ottlfuncs/func_delete.go

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+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"slices"
11+
12+
"go.opentelemetry.io/collector/pdata/pcommon"
13+
14+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
15+
)
16+
17+
type DeleteArguments[K any] struct {
18+
Target ottl.GetSetter[K]
19+
Index ottl.IntGetter[K]
20+
Length ottl.Optional[ottl.IntGetter[K]]
21+
}
22+
23+
func NewDeleteFactory[K any]() ottl.Factory[K] {
24+
return ottl.NewFactory("delete", &DeleteArguments[K]{}, createDeleteFunction[K])
25+
}
26+
27+
func createDeleteFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
28+
args, ok := oArgs.(*DeleteArguments[K])
29+
if !ok {
30+
return nil, errors.New("DeleteFactory args must be of type *DeleteArguments[K]")
31+
}
32+
33+
return deleteFrom(args.Target, args.Index, args.Length)
34+
}
35+
36+
func deleteFrom[K any](target ottl.GetSetter[K], indexGetter ottl.IntGetter[K], lengthGetter ottl.Optional[ottl.IntGetter[K]]) (ottl.ExprFunc[K], error) {
37+
return func(ctx context.Context, tCtx K) (any, error) {
38+
if target == nil {
39+
return nil, errors.New("target is nil")
40+
}
41+
42+
t, err := target.Get(ctx, tCtx)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
index, err := indexGetter.Get(ctx, tCtx)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
if index == -1 {
53+
// If we get -1 as index, do nothing and return nil.
54+
// Example: Calling delete with 'Index(log.attributes["tags"], "error")'
55+
// as index input value will result in -1 index, that means nothing to delete.
56+
return nil, target.Set(ctx, tCtx, t)
57+
}
58+
59+
length := int64(1)
60+
if !lengthGetter.IsEmpty() {
61+
length, err = lengthGetter.Get().Get(ctx, tCtx)
62+
if err != nil {
63+
return nil, err
64+
}
65+
}
66+
67+
var sourceSlice []any
68+
switch targetType := t.(type) {
69+
case pcommon.Slice:
70+
sourceSlice = targetType.AsRaw()
71+
case pcommon.Value:
72+
if targetType.Type() == pcommon.ValueTypeSlice {
73+
sourceSlice = targetType.Slice().AsRaw()
74+
} else {
75+
return nil, fmt.Errorf("target is not a slice, got pcommon.Value of type %q", targetType.Type())
76+
}
77+
default:
78+
return nil, fmt.Errorf("target must be a slice type, got %T", t)
79+
}
80+
81+
sliceLen := int64(len(sourceSlice))
82+
if index < -1 || index >= sliceLen {
83+
return nil, fmt.Errorf("index %d out of bounds for slice of length %d", index, sliceLen)
84+
}
85+
86+
if length <= 0 {
87+
return nil, fmt.Errorf("length must be positive, got %d", length)
88+
}
89+
90+
endIndex := index + length
91+
if endIndex > sliceLen {
92+
endIndex = sliceLen
93+
}
94+
95+
res := slices.Delete(slices.Clone(sourceSlice), int(index), int(endIndex))
96+
97+
// Convert back to pcommon.Slice
98+
resSlice := pcommon.NewSlice()
99+
if err := resSlice.FromRaw(res); err != nil {
100+
return nil, err
101+
}
102+
103+
return nil, target.Set(ctx, tCtx, resSlice)
104+
}, nil
105+
}

0 commit comments

Comments
 (0)