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 Sort converter #34283

Merged
merged 27 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
07215d1
Add OTTL sort converter
kaisecheng Jul 27, 2024
04366f5
support mixed of numeric types to sort as double
kaisecheng Jul 27, 2024
e2b90e1
change log
kaisecheng Jul 28, 2024
73ecb89
add support to array type
kaisecheng Jul 29, 2024
dab7c86
Update pkg/ottl/ottlfuncs/func_sort.go
kaisecheng Jul 29, 2024
2e36574
- add support to pcommon.Value
kaisecheng Jul 29, 2024
d43e95f
update doc
kaisecheng Jul 29, 2024
447df3e
lint
kaisecheng Jul 30, 2024
028744e
tidy
kaisecheng Jul 30, 2024
1eb08f3
- preserve the data types in the sort result
kaisecheng Jul 30, 2024
a0d9791
update doc
kaisecheng Jul 30, 2024
d676f1a
fix unit test
kaisecheng Jul 31, 2024
4568ca0
Merge branch 'main' into ottl_sort_func
kaisecheng Jul 31, 2024
d166048
preserve the data type for boolean array
kaisecheng Jul 31, 2024
524e546
rename
kaisecheng Jul 31, 2024
8f2acd1
Merge branch 'main' of github.com:open-telemetry/opentelemetry-collec…
kaisecheng Aug 1, 2024
a13bdbc
fix CI codegen
kaisecheng Aug 1, 2024
8c0e371
Merge branch 'main' into ottl_sort_func
kaisecheng Aug 2, 2024
1bb3d86
Update pkg/ottl/ottlfuncs/README.md
kaisecheng Aug 2, 2024
2894981
Update pkg/ottl/ottlfuncs/func_sort.go
kaisecheng Aug 2, 2024
9c22e38
Update pkg/ottl/ottlfuncs/README.md
kaisecheng Aug 2, 2024
660f4fc
Update pkg/ottl/ottlfuncs/func_sort.go
kaisecheng Aug 2, 2024
3325267
remove multierr dependency
kaisecheng Aug 2, 2024
0a9b019
Merge branch 'main' into ottl_sort_func
kaisecheng Aug 2, 2024
b2f8062
Merge branch 'main' into ottl_sort_func
kaisecheng Aug 8, 2024
6e0e66d
Merge branch 'main' into ottl_sort_func
kaisecheng Aug 8, 2024
f2145f3
add more e2e tests
kaisecheng Aug 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .chloggen/ottl_sort_func.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 `Sort` function to sort array to ascending order or descending order

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

# (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: [user]
56 changes: 56 additions & 0 deletions pkg/ottl/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,62 @@ func Test_e2e_converters(t *testing.T) {
tCtx.GetLogRecord().Attributes().PutStr("test", "5b722b307fce6c944905d132691d5e4a2214b7fe92b738920eb3fce3a90420a19511c3010a0e7712b054daef5b57bad59ecbd93b3280f210578f547f4aed4d25")
},
},
{
statement: `set(attributes["test"], Sort(Split(attributes["flags"], "|"), "desc"))`,
want: func(tCtx ottllog.TransformContext) {
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
s.AppendEmpty().SetStr("C")
s.AppendEmpty().SetStr("B")
s.AppendEmpty().SetStr("A")
},
},
{
statement: `set(attributes["test"], Sort([true, false, false]))`,
want: func(tCtx ottllog.TransformContext) {
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
s.AppendEmpty().SetBool(false)
s.AppendEmpty().SetBool(false)
s.AppendEmpty().SetBool(true)
},
},
{
statement: `set(attributes["test"], Sort([3, 6, 9], "desc"))`,
want: func(tCtx ottllog.TransformContext) {
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
s.AppendEmpty().SetInt(9)
s.AppendEmpty().SetInt(6)
s.AppendEmpty().SetInt(3)
},
},
{
statement: `set(attributes["test"], Sort([Double(1.5), Double(10.2), Double(2.3), Double(0.5)]))`,
want: func(tCtx ottllog.TransformContext) {
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
s.AppendEmpty().SetDouble(0.5)
s.AppendEmpty().SetDouble(1.5)
s.AppendEmpty().SetDouble(2.3)
s.AppendEmpty().SetDouble(10.2)
},
},
{
statement: `set(attributes["test"], Sort([Int(11), Double(2.2), Double(-1)]))`,
want: func(tCtx ottllog.TransformContext) {
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
s.AppendEmpty().SetDouble(-1)
s.AppendEmpty().SetDouble(2.2)
s.AppendEmpty().SetInt(11)
},
},
{
statement: `set(attributes["test"], Sort([false, Int(11), Double(2.2), "three"]))`,
want: func(tCtx ottllog.TransformContext) {
s := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
s.AppendEmpty().SetInt(11)
s.AppendEmpty().SetDouble(2.2)
s.AppendEmpty().SetBool(false)
s.AppendEmpty().SetStr("three")
},
},
{
statement: `set(span_id, SpanID(0x0000000000000000))`,
want: func(tCtx ottllog.TransformContext) {
Expand Down
30 changes: 29 additions & 1 deletion pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ Available Converters:
- [SHA1](#sha1)
- [SHA256](#sha256)
- [SHA512](#sha512)
- [Sort](#sort)
- [SpanID](#spanid)
- [Split](#split)
- [String](#string)
Expand Down Expand Up @@ -1204,7 +1205,6 @@ Examples:

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


- `SHA256("name")`

### SHA512
Expand All @@ -1224,6 +1224,34 @@ Examples:

- `SHA512("name")`

### Sort

`Sort(target, Optional[order])`

The `Sort` Converter sorts the `target` array in either ascending or descending order.

`target` is an array or `pcommon.Slice` typed field containing the elements to be sorted.

`order` is a string specifying the sort order. Must be either `asc` or `desc`. The default value is `asc`.

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")`

### SpanID

`SpanID(bytes)`
Expand Down
253 changes: 253 additions & 0 deletions pkg/ottl/ottlfuncs/func_sort.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"

import (
"cmp"
"context"
"fmt"
"slices"
"strconv"

"go.opentelemetry.io/collector/pdata/pcommon"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
)

const (
sortAsc = "asc"
sortDesc = "desc"
)

type SortArguments[K any] struct {
Target ottl.Getter[K]
Order ottl.Optional[string]
}

func NewSortFactory[K any]() ottl.Factory[K] {
return ottl.NewFactory("Sort", &SortArguments[K]{}, createSortFunction[K])
}

func createSortFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
args, ok := oArgs.(*SortArguments[K])

if !ok {
return nil, fmt.Errorf("SortFactory args must be of type *SortArguments[K]")
}

order := sortAsc
if !args.Order.IsEmpty() {
o := args.Order.Get()
switch o {
case sortAsc, sortDesc:
order = o
default:
return nil, fmt.Errorf("invalid arguments: %s. Order should be either \"%s\" or \"%s\"", o, sortAsc, sortDesc)
}
}

return sort(args.Target, order), nil
}

func sort[K any](target ottl.Getter[K], order string) ottl.ExprFunc[K] {
return func(ctx context.Context, tCtx K) (any, error) {
val, err := target.Get(ctx, tCtx)
if err != nil {
return nil, err
}

switch v := val.(type) {
case pcommon.Slice:
evan-bradley marked this conversation as resolved.
Show resolved Hide resolved
return sortSlice(v, order)
case pcommon.Value:
if v.Type() == pcommon.ValueTypeSlice {
return sortSlice(v.Slice(), order)
}
return nil, fmt.Errorf("sort with unsupported type: '%s'. Target is not a list", v.Type().String())
case []any:
// handle Sort([1,2,3])
slice := pcommon.NewValueSlice().SetEmptySlice()
if err := slice.FromRaw(v); err != nil {
return nil, fmt.Errorf("sort with unsupported type: '%T'. Target is not a list of primitive types; %w", v, err)
}
return sortSlice(slice, order)
case []string:
dup := makeCopy(v)
return sortTypedSlice(dup, order), nil
case []int64:
dup := makeCopy(v)
return sortTypedSlice(dup, order), nil
case []float64:
dup := makeCopy(v)
return sortTypedSlice(dup, order), nil
case []bool:
var strings []string
for _, b := range v {
strings = append(strings, strconv.FormatBool(b))
}

sortTypedSlice(strings, order)

bools := make([]bool, len(strings))
for i, s := range strings {
boolValue, _ := strconv.ParseBool(s)
bools[i] = boolValue
}
return bools, nil
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
default:
return nil, fmt.Errorf("sort with unsupported type: '%T'. Target is not a list", v)
}
}
}

// sortSlice sorts a pcommon.Slice based on the specified order.
// It gets the common type for all elements in the slice and converts all elements to this common type, creating a new copy
// Parameters:
// - slice: The pcommon.Slice to be sorted
// - order: The sort order. "asc" for ascending, "desc" for descending
//
// Returns:
// - A sorted slice as []any or the original pcommon.Slice
// - An error if an unsupported type is encountered
func sortSlice(slice pcommon.Slice, order string) (any, error) {
length := slice.Len()
if length == 0 {
return slice, nil
}

commonType, ok := findCommonValueType(slice)
if !ok {
return slice, nil
}

switch commonType {
case pcommon.ValueTypeInt:
arr := makeConvertedCopy(slice, func(idx int) int64 {
return slice.At(idx).Int()
})
return sortConvertedSlice(arr, order), nil
case pcommon.ValueTypeDouble:
arr := makeConvertedCopy(slice, func(idx int) float64 {
s := slice.At(idx)
if s.Type() == pcommon.ValueTypeInt {
return float64(s.Int())
}

return s.Double()
})
return sortConvertedSlice(arr, order), nil
case pcommon.ValueTypeStr:
arr := makeConvertedCopy(slice, func(idx int) string {
return slice.At(idx).AsString()
})
return sortConvertedSlice(arr, order), nil
default:
return nil, fmt.Errorf("sort with unsupported type: '%T'", commonType)
}
}

type targetType interface {
~int64 | ~float64 | ~string
}

// findCommonValueType determines the most appropriate common type for all elements in a pcommon.Slice.
// It returns two values:
// - A pcommon.ValueType representing the desired common type for all elements.
// Mixed Numeric types return ValueTypeDouble. Integer type returns ValueTypeInt. Double type returns ValueTypeDouble.
// String, Bool, Empty and mixed of the mentioned types return ValueTypeStr, as they require string conversion for comparison.
// - A boolean indicating whether a common type could be determined (true) or not (false).
// returns false for ValueTypeMap, ValueTypeSlice and ValueTypeBytes. They are unsupported types for sort.
func findCommonValueType(slice pcommon.Slice) (pcommon.ValueType, bool) {
length := slice.Len()
if length == 0 {
return pcommon.ValueTypeEmpty, false
}

wantType := slice.At(0).Type()
wantStr := false
wantDouble := false

for i := 0; i < length; i++ {
value := slice.At(i)
currType := value.Type()

switch currType {
case pcommon.ValueTypeInt:
if wantType == pcommon.ValueTypeDouble {
wantDouble = true
}
case pcommon.ValueTypeDouble:
if wantType == pcommon.ValueTypeInt {
wantDouble = true
}
case pcommon.ValueTypeStr, pcommon.ValueTypeBool, pcommon.ValueTypeEmpty:
wantStr = true
default:
return pcommon.ValueTypeEmpty, false
}
}

if wantStr {
wantType = pcommon.ValueTypeStr
} else if wantDouble {
wantType = pcommon.ValueTypeDouble
}

return wantType, true
}

func makeCopy[T targetType](src []T) []T {
dup := make([]T, len(src))
copy(dup, src)
return dup
}

func sortTypedSlice[T targetType](arr []T, order string) []T {
if len(arr) == 0 {
return arr
}

slices.SortFunc(arr, func(a, b T) int {
if order == sortDesc {
return cmp.Compare(b, a)
}
return cmp.Compare(a, b)
})

return arr
}

type convertedValue[T targetType] struct {
value T
originalValue any
}

func makeConvertedCopy[T targetType](slice pcommon.Slice, converter func(idx int) T) []convertedValue[T] {
length := slice.Len()
var out []convertedValue[T]
for i := 0; i < length; i++ {
cv := convertedValue[T]{
value: converter(i),
originalValue: slice.At(i).AsRaw(),
}
out = append(out, cv)
}
return out
}

func sortConvertedSlice[T targetType](cvs []convertedValue[T], order string) []any {
slices.SortFunc(cvs, func(a, b convertedValue[T]) int {
if order == sortDesc {
return cmp.Compare(b.value, a.value)
}
return cmp.Compare(a.value, b.value)
})

var out []any
for _, cv := range cvs {
out = append(out, cv.originalValue)
}

return out
}
Loading