diff --git a/.chloggen/feat_time-to-int.yaml b/.chloggen/feat_time-to-int.yaml new file mode 100755 index 000000000000..21bc178c4a10 --- /dev/null +++ b/.chloggen/feat_time-to-int.yaml @@ -0,0 +1,20 @@ +# Use this changelog template to create an entry for release notes. +# If your change doesn't affect end users, such as a test fix or a tooling change, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. + +# 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 converters to convert time to unix nanoseconds, unix microseconds, unix milliseconds or unix seconds" + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [24686] + +# (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: diff --git a/pkg/ottl/expression.go b/pkg/ottl/expression.go index 47f38dc99415..9fbfa9c7756b 100644 --- a/pkg/ottl/expression.go +++ b/pkg/ottl/expression.go @@ -583,6 +583,36 @@ func (p *Parser[K]) newGetterFromConverter(c converter) (Getter[K], error) { }, nil } +// TimeGetter is a Getter that must return an time.Time. +type TimeGetter[K any] interface { + // Get retrieves an time.Time value. + Get(ctx context.Context, tCtx K) (time.Time, error) +} + +// StandardTimeGetter is a basic implementation of IntGetter +type StandardTimeGetter[K any] struct { + Getter func(ctx context.Context, tCtx K) (interface{}, error) +} + +// Get retrieves a time.Time value. +// If the value is not a time.Time, a new TypeError is returned. +// If there is an error getting the value it will be returned. +func (g StandardTimeGetter[K]) Get(ctx context.Context, tCtx K) (time.Time, error) { + val, err := g.Getter(ctx, tCtx) + if err != nil { + return time.Time{}, fmt.Errorf("error getting value in %T: %w", g, err) + } + if val == nil { + return time.Time{}, TypeError("expected time but got nil") + } + switch v := val.(type) { + case time.Time: + return v, nil + default: + return time.Time{}, TypeError(fmt.Sprintf("expected time but got %T", val)) + } +} + // DurationGetter is a Getter that must return an time.Duration. type DurationGetter[K any] interface { // Get retrieves an int64 value. diff --git a/pkg/ottl/expression_test.go b/pkg/ottl/expression_test.go index 304d37f9ce3a..605921d78b20 100644 --- a/pkg/ottl/expression_test.go +++ b/pkg/ottl/expression_test.go @@ -1657,3 +1657,92 @@ func Test_StandardDurationGetter_WrappedError(t *testing.T) { _, ok := err.(TypeError) assert.False(t, ok) } + +func Test_StandardTimeGetter(t *testing.T) { + tests := []struct { + name string + getter StandardTimeGetter[interface{}] + want string + valid bool + expectedErrorMsg string + }{ + { + name: "2023 time", + getter: StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2023, 8, 17, 1, 1, 1, 1, time.UTC), nil + }, + }, + want: "2023-08-17T01:01:01.000000001Z", + valid: true, + }, + { + name: "before 2000 time", + getter: StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(1999, 12, 1, 10, 59, 58, 57, time.UTC), nil + }, + }, + want: "1999-12-01T10:59:58.000000057Z", + valid: true, + }, + { + name: "wrong type - duration", + getter: StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.ParseDuration("70ns") + }, + }, + valid: false, + expectedErrorMsg: "expected time but got time.Duration", + }, + { + name: "wrong type - bool", + getter: StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return true, nil + }, + }, + valid: false, + expectedErrorMsg: "expected time but got bool", + }, + { + name: "nil", + getter: StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return nil, nil + }, + }, + valid: false, + expectedErrorMsg: "expected time but got nil", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.getter.Get(context.Background(), nil) + if tt.valid { + assert.NoError(t, err) + var want time.Time + want, err = time.Parse("2006-01-02T15:04:05.000000000Z", tt.want) + assert.NoError(t, err) + assert.Equal(t, want, val) + } else { + assert.ErrorContains(t, err, tt.expectedErrorMsg) + } + }) + } +} + +// nolint:errorlint +func Test_StandardTimeGetter_WrappedError(t *testing.T) { + getter := StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return nil, TypeError("") + }, + } + _, err := getter.Get(context.Background(), nil) + assert.Error(t, err) + _, ok := err.(TypeError) + assert.False(t, ok) +} diff --git a/pkg/ottl/ottlfuncs/func_unix_micro.go b/pkg/ottl/ottlfuncs/func_unix_micro.go new file mode 100644 index 000000000000..623903660c3f --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_unix_micro.go @@ -0,0 +1,38 @@ +// 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" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type UnixMicroArguments[K any] struct { + Time ottl.TimeGetter[K] `ottlarg:"0"` +} + +func NewUnixMicroFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("UnixMicro", &UnixMicroArguments[K]{}, createUnixMicroFunction[K]) +} +func createUnixMicroFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*UnixMicroArguments[K]) + + if !ok { + return nil, fmt.Errorf("UnixMicroFactory args must be of type *UnixMicroArguments[K]") + } + + return UnixMicro(args.Time) +} + +func UnixMicro[K any](inputTime ottl.TimeGetter[K]) (ottl.ExprFunc[K], error) { + return func(ctx context.Context, tCtx K) (interface{}, error) { + t, err := inputTime.Get(ctx, tCtx) + if err != nil { + return nil, err + } + return t.UnixMicro(), nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_unix_micro_test.go b/pkg/ottl/ottlfuncs/func_unix_micro_test.go new file mode 100644 index 000000000000..bc4a3e5435bd --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_unix_micro_test.go @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_TimeUnixMicro(t *testing.T) { + tests := []struct { + name string + time ottl.TimeGetter[interface{}] + expected time.Time + }{ + { + name: "January 1, 2023", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local), nil + }, + }, + expected: time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local), + }, + { + name: "April 30, 2001, 3pm", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2001, 4, 30, 15, 0, 0, 0, time.Local), nil + }, + }, + expected: time.Date(2001, 4, 30, 15, 0, 0, 0, time.Local), + }, + { + name: "November 12, 1980, 4:35:01am", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(1980, 11, 12, 4, 35, 1, 0, time.Local), nil + }, + }, + expected: time.Date(1980, 11, 12, 4, 35, 1, 0, time.Local), + }, + { + name: "October 4, 2020, 5:05 5 microseconds 5 nanosecs", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2020, 10, 4, 5, 5, 5, 5, time.Local), nil + }, + }, + expected: time.Date(2020, 10, 4, 5, 5, 5, 5, time.Local), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := UnixMicro(tt.time) + assert.NoError(t, err) + result, err := exprFunc(nil, nil) + assert.NoError(t, err) + want := tt.expected.UnixMicro() + assert.Equal(t, want, result) + }) + } +} diff --git a/pkg/ottl/ottlfuncs/func_unix_milli.go b/pkg/ottl/ottlfuncs/func_unix_milli.go new file mode 100644 index 000000000000..94a60e7388c2 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_unix_milli.go @@ -0,0 +1,38 @@ +// 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" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type UnixMilliArguments[K any] struct { + Time ottl.TimeGetter[K] `ottlarg:"0"` +} + +func NewUnixMilliFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("UnixMilli", &UnixMilliArguments[K]{}, createUnixMilliFunction[K]) +} +func createUnixMilliFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*UnixMilliArguments[K]) + + if !ok { + return nil, fmt.Errorf("UnixMilliFactory args must be of type *UnixMilliArguments[K]") + } + + return UnixMilli(args.Time) +} + +func UnixMilli[K any](inputTime ottl.TimeGetter[K]) (ottl.ExprFunc[K], error) { + return func(ctx context.Context, tCtx K) (interface{}, error) { + t, err := inputTime.Get(ctx, tCtx) + if err != nil { + return nil, err + } + return t.UnixMilli(), nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_unix_milli_test.go b/pkg/ottl/ottlfuncs/func_unix_milli_test.go new file mode 100644 index 000000000000..d77cca91f928 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_unix_milli_test.go @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_TimeUnixMilli(t *testing.T) { + tests := []struct { + name string + time ottl.TimeGetter[interface{}] + expected time.Time + }{ + { + name: "January 1, 2022", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local), nil + }, + }, + expected: time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local), + }, + { + name: "May 30, 2002, 3pm", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2002, 5, 30, 15, 0, 0, 0, time.Local), nil + }, + }, + expected: time.Date(2002, 5, 30, 15, 0, 0, 0, time.Local), + }, + { + name: "September 12, 1980, 4:35:01am", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(1980, 9, 12, 4, 35, 1, 0, time.Local), nil + }, + }, + expected: time.Date(1980, 9, 12, 4, 35, 1, 0, time.Local), + }, + { + name: "October 4, 2020, 5:05 5 microseconds 5 nanosecs", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2020, 10, 4, 5, 5, 5, 5, time.Local), nil + }, + }, + expected: time.Date(2020, 10, 4, 5, 5, 5, 5, time.Local), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := UnixMilli(tt.time) + assert.NoError(t, err) + result, err := exprFunc(nil, nil) + assert.NoError(t, err) + want := tt.expected.UnixMilli() + assert.Equal(t, want, result) + }) + } +} diff --git a/pkg/ottl/ottlfuncs/func_unix_nano.go b/pkg/ottl/ottlfuncs/func_unix_nano.go new file mode 100644 index 000000000000..b8e396479b6c --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_unix_nano.go @@ -0,0 +1,38 @@ +// 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" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type UnixNanoArguments[K any] struct { + Time ottl.TimeGetter[K] `ottlarg:"0"` +} + +func NewUnixNanoFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("UnixNano", &UnixNanoArguments[K]{}, createUnixNanoFunction[K]) +} +func createUnixNanoFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*UnixNanoArguments[K]) + + if !ok { + return nil, fmt.Errorf("UnixNanoFactory args must be of type *UnixNanoArguments[K]") + } + + return UnixNano(args.Time) +} + +func UnixNano[K any](inputTime ottl.TimeGetter[K]) (ottl.ExprFunc[K], error) { + return func(ctx context.Context, tCtx K) (interface{}, error) { + t, err := inputTime.Get(ctx, tCtx) + if err != nil { + return nil, err + } + return t.UnixNano(), nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_unix_nano_test.go b/pkg/ottl/ottlfuncs/func_unix_nano_test.go new file mode 100644 index 000000000000..6e65a1b89746 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_unix_nano_test.go @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_TimeUnixNano(t *testing.T) { + tests := []struct { + name string + time ottl.TimeGetter[interface{}] + expected time.Time + }{ + { + name: "January 1, 2023", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local), nil + }, + }, + expected: time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local), + }, + { + name: "April 30, 2000, 1pm", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2000, 4, 30, 13, 0, 0, 0, time.Local), nil + }, + }, + expected: time.Date(2000, 4, 30, 13, 0, 0, 0, time.Local), + }, + { + name: "December 12, 1980, 4:35:01am", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(1980, 12, 12, 4, 35, 1, 0, time.Local), nil + }, + }, + expected: time.Date(1980, 12, 12, 4, 35, 1, 0, time.Local), + }, + { + name: "October 4, 2020, 5:05 5 microseconds 5 nanosecs", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2020, 10, 4, 5, 5, 5, 5, time.Local), nil + }, + }, + expected: time.Date(2020, 10, 4, 5, 5, 5, 5, time.Local), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := UnixNano(tt.time) + assert.NoError(t, err) + result, err := exprFunc(nil, nil) + assert.NoError(t, err) + want := tt.expected.UnixNano() + assert.Equal(t, want, result) + }) + } +} diff --git a/pkg/ottl/ottlfuncs/func_unix_seconds.go b/pkg/ottl/ottlfuncs/func_unix_seconds.go new file mode 100644 index 000000000000..9d8e8ebd254b --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_unix_seconds.go @@ -0,0 +1,38 @@ +// 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" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type UnixSecondsArguments[K any] struct { + Time ottl.TimeGetter[K] `ottlarg:"0"` +} + +func NewUnixSecondsFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("UnixSeconds", &UnixSecondsArguments[K]{}, createUnixSecondsFunction[K]) +} +func createUnixSecondsFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*UnixSecondsArguments[K]) + + if !ok { + return nil, fmt.Errorf("UnixSecondsFactory args must be of type *UnixSecondsArguments[K]") + } + + return UnixSeconds(args.Time) +} + +func UnixSeconds[K any](inputTime ottl.TimeGetter[K]) (ottl.ExprFunc[K], error) { + return func(ctx context.Context, tCtx K) (interface{}, error) { + t, err := inputTime.Get(ctx, tCtx) + if err != nil { + return nil, err + } + return t.Unix(), nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_unix_seconds_test.go b/pkg/ottl/ottlfuncs/func_unix_seconds_test.go new file mode 100644 index 000000000000..22fe577c48ee --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_unix_seconds_test.go @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_TimeUnixSeconds(t *testing.T) { + tests := []struct { + name string + time ottl.TimeGetter[interface{}] + expected time.Time + }{ + { + name: "January 1, 2023", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local), nil + }, + }, + expected: time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local), + }, + { + name: "March 31, 2000, 4pm", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2000, 3, 31, 16, 0, 0, 0, time.Local), nil + }, + }, + expected: time.Date(2000, 3, 31, 16, 0, 0, 0, time.Local), + }, + { + name: "December 12, 1980, 4:35:01am", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(1980, 12, 12, 4, 35, 1, 0, time.Local), nil + }, + }, + expected: time.Date(1980, 12, 12, 4, 35, 1, 0, time.Local), + }, + { + name: "October 4, 2020, 5:05 5 microseconds 5 nanosecs", + time: &ottl.StandardTimeGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return time.Date(2020, 10, 4, 5, 5, 5, 5, time.Local), nil + }, + }, + expected: time.Date(2020, 10, 4, 5, 5, 5, 5, time.Local), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := UnixSeconds(tt.time) + assert.NoError(t, err) + result, err := exprFunc(nil, nil) + assert.NoError(t, err) + want := tt.expected.Unix() + assert.Equal(t, want, result) + }) + } +} diff --git a/pkg/ottl/ottlfuncs/functions.go b/pkg/ottl/ottlfuncs/functions.go index 2dab492a0ae5..62538a0b0666 100644 --- a/pkg/ottl/ottlfuncs/functions.go +++ b/pkg/ottl/ottlfuncs/functions.go @@ -59,6 +59,10 @@ func converters[K any]() []ottl.Factory[K] { NewSubstringFactory[K](), NewTimeFactory[K](), NewTraceIDFactory[K](), + NewUnixMicroFactory[K](), + NewUnixMilliFactory[K](), + NewUnixNanoFactory[K](), + NewUnixSecondsFactory[K](), NewUUIDFactory[K](), } }