Skip to content

Commit 3fa10ff

Browse files
kaisechengevan-bradley
authored andcommitted
[pkg/ottl]: Add Sort converter (open-telemetry#34283)
**Description:** <Describe what has changed.> Add `Sort` function to sort array to ascending order or descending order `Sort(target, Optional[order])` The Sort Converter preserves the data type of the original elements while sorting. The behavior varies based on the types of elements in the target slice: | Element Types | Sorting Behavior | Return Value | |---------------|-------------------------------------|--------------| | Integers | Sorts as integers | Sorted array of integers | | Doubles | Sorts as doubles | Sorted array of doubles | | Integers and doubles | Converts all to doubles, then sorts | Sorted array of integers and doubles | | Strings | Sorts as strings | Sorted array of strings | | Booleans | Converts all to strings, then sorts | Sorted array of booleans | | Mix of integers, doubles, booleans, and strings | Converts all to strings, then sorts | Sorted array of mixed types | | Any other types | N/A | Returns an error | Examples: - `Sort(attributes["device.tags"])` - `Sort(attributes["device.tags"], "desc")` **Link to tracking Issue:** <Issue number if applicable> open-telemetry#34200 **Testing:** <Describe what testing was performed and which tests were added.> - Unit tests - E2E tests **Documentation:** <Describe the documentation added.> readme for Sort function --------- Co-authored-by: Evan Bradley <[email protected]>
1 parent 659ba41 commit 3fa10ff

File tree

6 files changed

+646
-1
lines changed

6 files changed

+646
-1
lines changed

.chloggen/ottl_sort_func.yaml

+27
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 `Sort` function to sort array to ascending order or descending order
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: [34200]
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

+56
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,62 @@ func Test_e2e_converters(t *testing.T) {
643643
tCtx.GetLogRecord().Attributes().PutStr("test", "5b722b307fce6c944905d132691d5e4a2214b7fe92b738920eb3fce3a90420a19511c3010a0e7712b054daef5b57bad59ecbd93b3280f210578f547f4aed4d25")
644644
},
645645
},
646+
{
647+
statement: `set(attributes["test"], Sort(Split(attributes["flags"], "|"), "desc"))`,
648+
want: func(tCtx ottllog.TransformContext) {
649+
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
650+
s.AppendEmpty().SetStr("C")
651+
s.AppendEmpty().SetStr("B")
652+
s.AppendEmpty().SetStr("A")
653+
},
654+
},
655+
{
656+
statement: `set(attributes["test"], Sort([true, false, false]))`,
657+
want: func(tCtx ottllog.TransformContext) {
658+
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
659+
s.AppendEmpty().SetBool(false)
660+
s.AppendEmpty().SetBool(false)
661+
s.AppendEmpty().SetBool(true)
662+
},
663+
},
664+
{
665+
statement: `set(attributes["test"], Sort([3, 6, 9], "desc"))`,
666+
want: func(tCtx ottllog.TransformContext) {
667+
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
668+
s.AppendEmpty().SetInt(9)
669+
s.AppendEmpty().SetInt(6)
670+
s.AppendEmpty().SetInt(3)
671+
},
672+
},
673+
{
674+
statement: `set(attributes["test"], Sort([Double(1.5), Double(10.2), Double(2.3), Double(0.5)]))`,
675+
want: func(tCtx ottllog.TransformContext) {
676+
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
677+
s.AppendEmpty().SetDouble(0.5)
678+
s.AppendEmpty().SetDouble(1.5)
679+
s.AppendEmpty().SetDouble(2.3)
680+
s.AppendEmpty().SetDouble(10.2)
681+
},
682+
},
683+
{
684+
statement: `set(attributes["test"], Sort([Int(11), Double(2.2), Double(-1)]))`,
685+
want: func(tCtx ottllog.TransformContext) {
686+
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
687+
s.AppendEmpty().SetDouble(-1)
688+
s.AppendEmpty().SetDouble(2.2)
689+
s.AppendEmpty().SetInt(11)
690+
},
691+
},
692+
{
693+
statement: `set(attributes["test"], Sort([false, Int(11), Double(2.2), "three"]))`,
694+
want: func(tCtx ottllog.TransformContext) {
695+
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
696+
s.AppendEmpty().SetInt(11)
697+
s.AppendEmpty().SetDouble(2.2)
698+
s.AppendEmpty().SetBool(false)
699+
s.AppendEmpty().SetStr("three")
700+
},
701+
},
646702
{
647703
statement: `set(span_id, SpanID(0x0000000000000000))`,
648704
want: func(tCtx ottllog.TransformContext) {

pkg/ottl/ottlfuncs/README.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ Available Converters:
449449
- [SHA1](#sha1)
450450
- [SHA256](#sha256)
451451
- [SHA512](#sha512)
452+
- [Sort](#sort)
452453
- [SpanID](#spanid)
453454
- [Split](#split)
454455
- [String](#string)
@@ -1318,7 +1319,6 @@ Examples:
13181319

13191320
- `SHA256(attributes["device.name"])`
13201321

1321-
13221322
- `SHA256("name")`
13231323

13241324
### SHA512
@@ -1338,6 +1338,34 @@ Examples:
13381338

13391339
- `SHA512("name")`
13401340

1341+
### Sort
1342+
1343+
`Sort(target, Optional[order])`
1344+
1345+
The `Sort` Converter sorts the `target` array in either ascending or descending order.
1346+
1347+
`target` is an array or `pcommon.Slice` typed field containing the elements to be sorted.
1348+
1349+
`order` is a string specifying the sort order. Must be either `asc` or `desc`. The default value is `asc`.
1350+
1351+
The Sort Converter preserves the data type of the original elements while sorting.
1352+
The behavior varies based on the types of elements in the target slice:
1353+
1354+
| Element Types | Sorting Behavior | Return Value |
1355+
|---------------|-------------------------------------|--------------|
1356+
| Integers | Sorts as integers | Sorted array of integers |
1357+
| Doubles | Sorts as doubles | Sorted array of doubles |
1358+
| Integers and doubles | Converts all to doubles, then sorts | Sorted array of integers and doubles |
1359+
| Strings | Sorts as strings | Sorted array of strings |
1360+
| Booleans | Converts all to strings, then sorts | Sorted array of booleans |
1361+
| Mix of integers, doubles, booleans, and strings | Converts all to strings, then sorts | Sorted array of mixed types |
1362+
| Any other types | N/A | Returns an error |
1363+
1364+
Examples:
1365+
1366+
- `Sort(attributes["device.tags"])`
1367+
- `Sort(attributes["device.tags"], "desc")`
1368+
13411369
### SpanID
13421370

13431371
`SpanID(bytes)`

pkg/ottl/ottlfuncs/func_sort.go

+253
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
"cmp"
8+
"context"
9+
"fmt"
10+
"slices"
11+
"strconv"
12+
13+
"go.opentelemetry.io/collector/pdata/pcommon"
14+
15+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
16+
)
17+
18+
const (
19+
sortAsc = "asc"
20+
sortDesc = "desc"
21+
)
22+
23+
type SortArguments[K any] struct {
24+
Target ottl.Getter[K]
25+
Order ottl.Optional[string]
26+
}
27+
28+
func NewSortFactory[K any]() ottl.Factory[K] {
29+
return ottl.NewFactory("Sort", &SortArguments[K]{}, createSortFunction[K])
30+
}
31+
32+
func createSortFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
33+
args, ok := oArgs.(*SortArguments[K])
34+
35+
if !ok {
36+
return nil, fmt.Errorf("SortFactory args must be of type *SortArguments[K]")
37+
}
38+
39+
order := sortAsc
40+
if !args.Order.IsEmpty() {
41+
o := args.Order.Get()
42+
switch o {
43+
case sortAsc, sortDesc:
44+
order = o
45+
default:
46+
return nil, fmt.Errorf("invalid arguments: %s. Order should be either \"%s\" or \"%s\"", o, sortAsc, sortDesc)
47+
}
48+
}
49+
50+
return sort(args.Target, order), nil
51+
}
52+
53+
func sort[K any](target ottl.Getter[K], order string) ottl.ExprFunc[K] {
54+
return func(ctx context.Context, tCtx K) (any, error) {
55+
val, err := target.Get(ctx, tCtx)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
switch v := val.(type) {
61+
case pcommon.Slice:
62+
return sortSlice(v, order)
63+
case pcommon.Value:
64+
if v.Type() == pcommon.ValueTypeSlice {
65+
return sortSlice(v.Slice(), order)
66+
}
67+
return nil, fmt.Errorf("sort with unsupported type: '%s'. Target is not a list", v.Type().String())
68+
case []any:
69+
// handle Sort([1,2,3])
70+
slice := pcommon.NewValueSlice().SetEmptySlice()
71+
if err := slice.FromRaw(v); err != nil {
72+
return nil, fmt.Errorf("sort with unsupported type: '%T'. Target is not a list of primitive types; %w", v, err)
73+
}
74+
return sortSlice(slice, order)
75+
case []string:
76+
dup := makeCopy(v)
77+
return sortTypedSlice(dup, order), nil
78+
case []int64:
79+
dup := makeCopy(v)
80+
return sortTypedSlice(dup, order), nil
81+
case []float64:
82+
dup := makeCopy(v)
83+
return sortTypedSlice(dup, order), nil
84+
case []bool:
85+
var strings []string
86+
for _, b := range v {
87+
strings = append(strings, strconv.FormatBool(b))
88+
}
89+
90+
sortTypedSlice(strings, order)
91+
92+
bools := make([]bool, len(strings))
93+
for i, s := range strings {
94+
boolValue, _ := strconv.ParseBool(s)
95+
bools[i] = boolValue
96+
}
97+
return bools, nil
98+
default:
99+
return nil, fmt.Errorf("sort with unsupported type: '%T'. Target is not a list", v)
100+
}
101+
}
102+
}
103+
104+
// sortSlice sorts a pcommon.Slice based on the specified order.
105+
// It gets the common type for all elements in the slice and converts all elements to this common type, creating a new copy
106+
// Parameters:
107+
// - slice: The pcommon.Slice to be sorted
108+
// - order: The sort order. "asc" for ascending, "desc" for descending
109+
//
110+
// Returns:
111+
// - A sorted slice as []any or the original pcommon.Slice
112+
// - An error if an unsupported type is encountered
113+
func sortSlice(slice pcommon.Slice, order string) (any, error) {
114+
length := slice.Len()
115+
if length == 0 {
116+
return slice, nil
117+
}
118+
119+
commonType, ok := findCommonValueType(slice)
120+
if !ok {
121+
return slice, nil
122+
}
123+
124+
switch commonType {
125+
case pcommon.ValueTypeInt:
126+
arr := makeConvertedCopy(slice, func(idx int) int64 {
127+
return slice.At(idx).Int()
128+
})
129+
return sortConvertedSlice(arr, order), nil
130+
case pcommon.ValueTypeDouble:
131+
arr := makeConvertedCopy(slice, func(idx int) float64 {
132+
s := slice.At(idx)
133+
if s.Type() == pcommon.ValueTypeInt {
134+
return float64(s.Int())
135+
}
136+
137+
return s.Double()
138+
})
139+
return sortConvertedSlice(arr, order), nil
140+
case pcommon.ValueTypeStr:
141+
arr := makeConvertedCopy(slice, func(idx int) string {
142+
return slice.At(idx).AsString()
143+
})
144+
return sortConvertedSlice(arr, order), nil
145+
default:
146+
return nil, fmt.Errorf("sort with unsupported type: '%T'", commonType)
147+
}
148+
}
149+
150+
type targetType interface {
151+
~int64 | ~float64 | ~string
152+
}
153+
154+
// findCommonValueType determines the most appropriate common type for all elements in a pcommon.Slice.
155+
// It returns two values:
156+
// - A pcommon.ValueType representing the desired common type for all elements.
157+
// Mixed Numeric types return ValueTypeDouble. Integer type returns ValueTypeInt. Double type returns ValueTypeDouble.
158+
// String, Bool, Empty and mixed of the mentioned types return ValueTypeStr, as they require string conversion for comparison.
159+
// - A boolean indicating whether a common type could be determined (true) or not (false).
160+
// returns false for ValueTypeMap, ValueTypeSlice and ValueTypeBytes. They are unsupported types for sort.
161+
func findCommonValueType(slice pcommon.Slice) (pcommon.ValueType, bool) {
162+
length := slice.Len()
163+
if length == 0 {
164+
return pcommon.ValueTypeEmpty, false
165+
}
166+
167+
wantType := slice.At(0).Type()
168+
wantStr := false
169+
wantDouble := false
170+
171+
for i := 0; i < length; i++ {
172+
value := slice.At(i)
173+
currType := value.Type()
174+
175+
switch currType {
176+
case pcommon.ValueTypeInt:
177+
if wantType == pcommon.ValueTypeDouble {
178+
wantDouble = true
179+
}
180+
case pcommon.ValueTypeDouble:
181+
if wantType == pcommon.ValueTypeInt {
182+
wantDouble = true
183+
}
184+
case pcommon.ValueTypeStr, pcommon.ValueTypeBool, pcommon.ValueTypeEmpty:
185+
wantStr = true
186+
default:
187+
return pcommon.ValueTypeEmpty, false
188+
}
189+
}
190+
191+
if wantStr {
192+
wantType = pcommon.ValueTypeStr
193+
} else if wantDouble {
194+
wantType = pcommon.ValueTypeDouble
195+
}
196+
197+
return wantType, true
198+
}
199+
200+
func makeCopy[T targetType](src []T) []T {
201+
dup := make([]T, len(src))
202+
copy(dup, src)
203+
return dup
204+
}
205+
206+
func sortTypedSlice[T targetType](arr []T, order string) []T {
207+
if len(arr) == 0 {
208+
return arr
209+
}
210+
211+
slices.SortFunc(arr, func(a, b T) int {
212+
if order == sortDesc {
213+
return cmp.Compare(b, a)
214+
}
215+
return cmp.Compare(a, b)
216+
})
217+
218+
return arr
219+
}
220+
221+
type convertedValue[T targetType] struct {
222+
value T
223+
originalValue any
224+
}
225+
226+
func makeConvertedCopy[T targetType](slice pcommon.Slice, converter func(idx int) T) []convertedValue[T] {
227+
length := slice.Len()
228+
var out []convertedValue[T]
229+
for i := 0; i < length; i++ {
230+
cv := convertedValue[T]{
231+
value: converter(i),
232+
originalValue: slice.At(i).AsRaw(),
233+
}
234+
out = append(out, cv)
235+
}
236+
return out
237+
}
238+
239+
func sortConvertedSlice[T targetType](cvs []convertedValue[T], order string) []any {
240+
slices.SortFunc(cvs, func(a, b convertedValue[T]) int {
241+
if order == sortDesc {
242+
return cmp.Compare(b.value, a.value)
243+
}
244+
return cmp.Compare(a.value, b.value)
245+
})
246+
247+
var out []any
248+
for _, cv := range cvs {
249+
out = append(out, cv.originalValue)
250+
}
251+
252+
return out
253+
}

0 commit comments

Comments
 (0)