pkg/ottl/ottlfuncs/func_sort.go (199 lines of code) (raw):
// 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:
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
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
}