diff --git a/.chloggen/ottl-islist.yaml b/.chloggen/ottl-islist.yaml new file mode 100755 index 000000000000..38b56255d8f7 --- /dev/null +++ b/.chloggen/ottl-islist.yaml @@ -0,0 +1,22 @@ +# 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 `IsList` OTTL Function + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [27870] + +# 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] diff --git a/pkg/ottl/e2e/e2e_test.go b/pkg/ottl/e2e/e2e_test.go index 902145bcabdd..47995bb265e8 100644 --- a/pkg/ottl/e2e/e2e_test.go +++ b/pkg/ottl/e2e/e2e_test.go @@ -382,6 +382,12 @@ func Test_e2e_converters(t *testing.T) { tCtx.GetLogRecord().Attributes().PutStr("test", "pass") }, }, + { + statement: `set(attributes["test"], "pass") where IsList(attributes["foo"]["slice"])`, + want: func(tCtx ottllog.TransformContext) { + tCtx.GetLogRecord().Attributes().PutStr("test", "pass") + }, + }, { statement: `set(attributes["test"], "pass") where IsMatch("aa123bb", "\\d{3}")`, want: func(tCtx ottllog.TransformContext) { diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md index 9b8aff6ffdd3..e0e788fb8fe2 100644 --- a/pkg/ottl/ottlfuncs/README.md +++ b/pkg/ottl/ottlfuncs/README.md @@ -392,6 +392,7 @@ Available Converters: - [IsInt](#isint) - [IsMap](#ismap) - [IsMatch](#ismatch) +- [IsList](#islist) - [IsString](#isstring) - [Len](#len) - [Log](#log) @@ -708,6 +709,22 @@ Examples: - `IsMatch("string", ".*ring")` +### IsList + +`IsList(value)` + +The `IsList` Converter returns true if the given value is a list. + +The `value` is either a path expression to a telemetry field to retrieve or a literal. + +If `value` is a `list`, `pcommon.ValueTypeSlice`. `pcommon.Slice`, or any other list type, then returns `true`, otherwise returns `false`. + +Examples: + +- `IsList(body)` + +- `IsList(attributes["maybe a slice"])` + ### IsString `IsString(value)` diff --git a/pkg/ottl/ottlfuncs/func_is_list.go b/pkg/ottl/ottlfuncs/func_is_list.go new file mode 100644 index 000000000000..137ccb470701 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_is_list.go @@ -0,0 +1,53 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" + +import ( + "context" + "fmt" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type IsListArguments[K any] struct { + Target ottl.Getter[K] +} + +func NewIsListFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("IsList", &IsListArguments[K]{}, createIsListFunction[K]) +} + +func createIsListFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*IsListArguments[K]) + + if !ok { + return nil, fmt.Errorf("IsListFactory args must be of type *IsListArguments[K]") + } + + return isList(args.Target), nil +} + +func isList[K any](target ottl.Getter[K]) ottl.ExprFunc[K] { + return func(ctx context.Context, tCtx K) (any, error) { + val, err := target.Get(ctx, tCtx) + if err != nil { + return false, err + } + + switch valType := val.(type) { + case pcommon.Value: + return valType.Type() == pcommon.ValueTypeSlice, nil + + case pcommon.Slice, plog.LogRecordSlice, plog.ResourceLogsSlice, plog.ScopeLogsSlice, pmetric.ExemplarSlice, pmetric.ExponentialHistogramDataPointSlice, pmetric.HistogramDataPointSlice, pmetric.MetricSlice, pmetric.NumberDataPointSlice, pmetric.ResourceMetricsSlice, pmetric.ScopeMetricsSlice, pmetric.SummaryDataPointSlice, pmetric.SummaryDataPointValueAtQuantileSlice, ptrace.ResourceSpansSlice, ptrace.ScopeSpansSlice, ptrace.SpanEventSlice, ptrace.SpanLinkSlice, ptrace.SpanSlice, []string, []bool, []int64, []float64, [][]byte, []any: + return true, nil + } + + return false, nil + } +} diff --git a/pkg/ottl/ottlfuncs/func_is_list_test.go b/pkg/ottl/ottlfuncs/func_is_list_test.go new file mode 100644 index 000000000000..0a402004d1e8 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_is_list_test.go @@ -0,0 +1,189 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_IsList(t *testing.T) { + tests := []struct { + name string + value any + expected bool + }{ + { + name: "map", + value: make(map[string]any, 0), + expected: false, + }, + { + name: "ValueTypeMap", + value: pcommon.NewValueMap(), + expected: false, + }, + { + name: "not map", + value: "not a map", + expected: false, + }, + { + name: "ValueTypeSlice", + value: pcommon.NewValueSlice(), + expected: true, + }, + { + name: "nil", + value: nil, + expected: false, + }, + { + name: "plog.LogRecordSlice", + value: plog.NewLogRecordSlice(), + expected: true, + }, + { + name: "plog.ResourceLogsSlice", + value: plog.NewResourceLogsSlice(), + expected: true, + }, + { + name: "plog.ScopeLogsSlice", + value: plog.NewScopeLogsSlice(), + expected: true, + }, + { + name: "pmetric.ExemplarSlice", + value: pmetric.NewExemplarSlice(), + expected: true, + }, + { + name: "pmetric.ExponentialHistogramDataPointSlice", + value: pmetric.NewExponentialHistogramDataPointSlice(), + expected: true, + }, + { + name: "pmetric.HistogramDataPointSlice", + value: pmetric.NewHistogramDataPointSlice(), + expected: true, + }, + { + name: "pmetric.MetricSlice", + value: pmetric.NewMetricSlice(), + expected: true, + }, + { + name: "pmetric.NumberDataPointSlice", + value: pmetric.NewNumberDataPointSlice(), + expected: true, + }, + { + name: "pmetric.ResourceMetricsSlice", + value: pmetric.NewResourceMetricsSlice(), + expected: true, + }, + { + name: "pmetric.ScopeMetricsSlice", + value: pmetric.NewScopeMetricsSlice(), + expected: true, + }, + { + name: "pmetric.SummaryDataPointSlice", + value: pmetric.NewSummaryDataPointSlice(), + expected: true, + }, + { + name: "pmetric.SummaryDataPointValueAtQuantileSlice", + value: pmetric.NewSummaryDataPointValueAtQuantileSlice(), + expected: true, + }, + { + name: "ptrace.ResourceSpansSlice", + value: ptrace.NewResourceSpansSlice(), + expected: true, + }, + { + name: "ptrace.ScopeSpansSlice", + value: ptrace.NewScopeSpansSlice(), + expected: true, + }, + { + name: "ptrace.SpanEventSlice", + value: ptrace.NewSpanEventSlice(), + expected: true, + }, + { + name: "ptrace.SpanLinkSlice", + value: ptrace.NewSpanLinkSlice(), + expected: true, + }, + { + name: "ptrace.SpanSlice", + value: ptrace.NewSpanSlice(), + expected: true, + }, + { + name: "[]string", + value: []string{}, + expected: true, + }, + { + name: "[]bool", + value: []bool{}, + expected: true, + }, + { + name: "[]int64", + value: []int64{}, + expected: true, + }, + { + name: "[]float64", + value: []float64{}, + expected: true, + }, + { + name: "[][]byte", + value: [][]byte{}, + expected: true, + }, + { + name: "[]any", + value: []any{}, + expected: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc := isList[any](&ottl.StandardGetSetter[any]{ + Getter: func(context.Context, any) (any, error) { + return tt.value, nil + }, + }) + result, err := exprFunc(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func Test_IsList_Error(t *testing.T) { + exprFunc := isList[any](&ottl.StandardGetSetter[any]{ + Getter: func(context.Context, any) (any, error) { + return nil, ottl.TypeError("") + }, + }) + result, err := exprFunc(context.Background(), nil) + assert.Equal(t, false, result) + assert.IsType(t, ottl.TypeError(""), err) +} diff --git a/pkg/ottl/ottlfuncs/functions.go b/pkg/ottl/ottlfuncs/functions.go index 25a596c3c78a..23522e9a11fc 100644 --- a/pkg/ottl/ottlfuncs/functions.go +++ b/pkg/ottl/ottlfuncs/functions.go @@ -47,6 +47,7 @@ func converters[K any]() []ottl.Factory[K] { NewIntFactory[K](), NewIsBoolFactory[K](), NewIsDoubleFactory[K](), + NewIsListFactory[K](), NewIsIntFactory[K](), NewIsMapFactory[K](), NewIsMatchFactory[K](),