Skip to content

Commit fe95cef

Browse files
committed
Add OTTL delete function to delete items from an array
1 parent 7a90970 commit fe95cef

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
{
@@ -92,6 +93,12 @@ func Test_e2e_editors(t *testing.T) {
9293
tCtx.GetLogRecord().Attributes().PutInt("things.0.value", 2)
9394
tCtx.GetLogRecord().Attributes().PutStr("things.1.name", "bar")
9495
tCtx.GetLogRecord().Attributes().PutInt("things.1.value", 5)
96+
97+
tCtx.GetLogRecord().Attributes().Remove("slice2")
98+
tCtx.GetLogRecord().Attributes().PutStr("slice2.0", "val")
99+
tCtx.GetLogRecord().Attributes().PutStr("slice2.1", "foo")
100+
tCtx.GetLogRecord().Attributes().PutStr("slice2.2", "bar")
101+
tCtx.GetLogRecord().Attributes().PutStr("slice2.3", "baz")
95102
},
96103
},
97104
{
@@ -116,6 +123,11 @@ func Test_e2e_editors(t *testing.T) {
116123
m.PutInt("test.things.0.value", 2)
117124
m.PutStr("test.things.1.name", "bar")
118125
m.PutInt("test.things.1.value", 5)
126+
127+
m.PutStr("test.slice2.0", "val")
128+
m.PutStr("test.slice2.1", "foo")
129+
m.PutStr("test.slice2.2", "bar")
130+
m.PutStr("test.slice2.3", "baz")
119131
m.CopyTo(tCtx.GetLogRecord().Attributes())
120132
},
121133
},
@@ -144,6 +156,11 @@ func Test_e2e_editors(t *testing.T) {
144156
m.PutStr("test.things.1.name", "bar")
145157
m.PutInt("test.things.1.value", 5)
146158

159+
m.PutStr("test.slice2", "val")
160+
m.PutStr("test.slice2.0", "foo")
161+
m.PutStr("test.slice2.1", "bar")
162+
m.PutStr("test.slice2.2", "baz")
163+
147164
m.CopyTo(tCtx.GetLogRecord().Attributes())
148165
},
149166
},
@@ -160,6 +177,10 @@ func Test_e2e_editors(t *testing.T) {
160177
m.PutStr("foo.flags", "pass")
161178
m.PutStr("foo.bar", "pass")
162179
m.PutStr("foo.flags", "pass")
180+
m.PutStr("slice2.0", "val")
181+
m.PutStr("slice2.1", "foo")
182+
m.PutStr("slice2.2", "bar")
183+
m.PutStr("slice2.3", "baz")
163184
m.PutEmptySlice("foo.slice").AppendEmpty().SetStr("val")
164185
m.PutStr("conflict.conflict1.conflict2", "nopass")
165186
mm := m.PutEmptyMap("conflict.conflict1")
@@ -188,6 +209,7 @@ func Test_e2e_editors(t *testing.T) {
188209
tCtx.GetLogRecord().Attributes().Remove("things")
189210
tCtx.GetLogRecord().Attributes().Remove("conflict.conflict1")
190211
tCtx.GetLogRecord().Attributes().Remove("conflict")
212+
tCtx.GetLogRecord().Attributes().Remove("slice2")
191213
},
192214
},
193215
{
@@ -205,6 +227,7 @@ func Test_e2e_editors(t *testing.T) {
205227
tCtx.GetLogRecord().Attributes().Remove("things")
206228
tCtx.GetLogRecord().Attributes().Remove("conflict.conflict1")
207229
tCtx.GetLogRecord().Attributes().Remove("conflict")
230+
tCtx.GetLogRecord().Attributes().Remove("slice2")
208231
},
209232
},
210233
{
@@ -386,6 +409,53 @@ func Test_e2e_editors(t *testing.T) {
386409
s.AppendEmpty().SetInt(6)
387410
},
388411
},
412+
{
413+
statement: `delete(attributes["slice2"], 0)`,
414+
want: func(tCtx ottllog.TransformContext) {
415+
v, _ := tCtx.GetLogRecord().Attributes().Get("slice2")
416+
s := v.Slice()
417+
// :ToDo: Implement RemoveAt and RemoveRange in pcommon.Slice
418+
s.RemoveIf(func(v pcommon.Value) bool {
419+
return v.Str() == "val"
420+
})
421+
},
422+
},
423+
{
424+
statement: `delete(attributes["slice2"], Len(attributes["slice2"]) - 1)`,
425+
want: func(tCtx ottllog.TransformContext) {
426+
v, _ := tCtx.GetLogRecord().Attributes().Get("slice2")
427+
s := v.Slice()
428+
s.RemoveIf(func(v pcommon.Value) bool {
429+
return v.Str() == "baz"
430+
})
431+
},
432+
},
433+
{
434+
statement: `delete(attributes["slice2"], 1, length=2)`,
435+
want: func(tCtx ottllog.TransformContext) {
436+
v, _ := tCtx.GetLogRecord().Attributes().Get("slice2")
437+
s := v.Slice()
438+
s.RemoveIf(func(v pcommon.Value) bool {
439+
return (v.Str() == "foo" || v.Str() == "bar")
440+
})
441+
},
442+
},
443+
{
444+
statement: `delete(attributes["slice2"], Index(attributes["slice2"], "foo"))`,
445+
want: func(tCtx ottllog.TransformContext) {
446+
v, _ := tCtx.GetLogRecord().Attributes().Get("slice2")
447+
s := v.Slice()
448+
s.RemoveIf(func(v pcommon.Value) bool {
449+
return v.Str() == "foo"
450+
})
451+
},
452+
},
453+
{
454+
statement: `delete(attributes["slice2"], Index(attributes["slice2"], "not_found"))`,
455+
want: func(tCtx ottllog.TransformContext) {
456+
// No change as "not_found" does not exist in the slice.
457+
},
458+
},
389459
}
390460

391461
for _, tt := range tests {
@@ -2013,6 +2083,12 @@ func constructLogTransformContextEditors() ottllog.TransformContext {
20132083
thing2.PutStr("name", "bar")
20142084
thing2.PutInt("value", 5)
20152085

2086+
s3 := logRecord.Attributes().PutEmptySlice("slice2")
2087+
s3.AppendEmpty().SetStr("val")
2088+
s3.AppendEmpty().SetStr("foo")
2089+
s3.AppendEmpty().SetStr("bar")
2090+
s3.AppendEmpty().SetStr("baz")
2091+
20162092
return ottllog.NewTransformContext(logRecord, scope, resource, plog.NewScopeLogs(), plog.NewResourceLogs())
20172093
}
20182094

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)