From 22d92928167ca6786d0fc34901e37c8a76005c81 Mon Sep 17 00:00:00 2001 From: odubajDT Date: Thu, 9 Jan 2025 15:59:12 +0100 Subject: [PATCH] [pkg/ottl] introduce Timestamp() converter function Signed-off-by: odubajDT --- .chloggen/ottl-timestamp.yaml | 27 ++++ pkg/ottl/e2e/e2e_test.go | 6 + pkg/ottl/ottlfuncs/README.md | 56 +++++++ pkg/ottl/ottlfuncs/func_timestamp.go | 51 +++++++ pkg/ottl/ottlfuncs/func_timestamp_test.go | 169 ++++++++++++++++++++++ pkg/ottl/ottlfuncs/functions.go | 1 + 6 files changed, 310 insertions(+) create mode 100644 .chloggen/ottl-timestamp.yaml create mode 100644 pkg/ottl/ottlfuncs/func_timestamp.go create mode 100644 pkg/ottl/ottlfuncs/func_timestamp_test.go diff --git a/.chloggen/ottl-timestamp.yaml b/.chloggen/ottl-timestamp.yaml new file mode 100644 index 000000000000..dd862c673175 --- /dev/null +++ b/.chloggen/ottl-timestamp.yaml @@ -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: "Introduce new Timestamp() converter function." + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [36870] + +# (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: [] diff --git a/pkg/ottl/e2e/e2e_test.go b/pkg/ottl/e2e/e2e_test.go index 72077e66dd23..8fbcd880ff7c 100644 --- a/pkg/ottl/e2e/e2e_test.go +++ b/pkg/ottl/e2e/e2e_test.go @@ -856,6 +856,12 @@ func Test_e2e_converters(t *testing.T) { tCtx.GetLogRecord().SetTimestamp(pcommon.NewTimestampFromTime(TestLogTimestamp.AsTime().Truncate(time.Second))) }, }, + { + statement: `set(attributes["time"],Timestamp(time, "%Y-%m-%d"))`, + want: func(tCtx ottllog.TransformContext) { + tCtx.GetLogRecord().Attributes().PutStr("time", "2020-02-11") + }, + }, { statement: `set(attributes["test"], "pass") where UnixMicro(time) > 0`, want: func(tCtx ottllog.TransformContext) { diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md index 8e48eb3083b2..0e5878597a27 100644 --- a/pkg/ottl/ottlfuncs/README.md +++ b/pkg/ottl/ottlfuncs/README.md @@ -463,6 +463,7 @@ Available Converters: - [String](#string) - [Substring](#substring) - [Time](#time) +- [Timestamp](#timestamp) - [ToKeyValueString](#tokeyvaluestring) - [TraceID](#traceid) - [TruncateTime](#truncatetime) @@ -1931,6 +1932,61 @@ Examples: - `Time("mercoledì set 4 2024", "%A %h %e %Y", "", "it")` - `Time("Febrero 25 lunes, 2002, 02:03:04 p.m.", "%B %d %A, %Y, %r", "America/New_York", "es-ES")` +### Timestamp + +`Timestamp(time, format)` + +The `Timestamp` Converter takes a `time.Time` and converts it to a human readable string representations of the time according to the specidied format. + +`time` is `time.Time`. If `time` is another type an error is returned. `format` is a string. + +If `format` is nil, an error is returned. The parser used is the parser at [internal/coreinternal/parser](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/internal/coreinternal/timeutils). If `format` does not follow the parsing rules used by this parser, an error is returned. + +`format` denotes a textual and human readable representation of the resulting time value formatted according to ctime-like format string. It follows [standard Go Layout formatting](https://pkg.go.dev/time#pkg-constants) with few additional substitutes: +| substitution | description | examples | +|-----|-----|-----| +|`%Y` | Year as a zero-padded number | 0001, 0002, ..., 2019, 2020, ..., 9999 | +|`%y` | Year, last two digits as a zero-padded number | 01, ..., 99 | +|`%m` | Month as a zero-padded number | 01, 02, ..., 12 | +|`%o` | Month as a space-padded number | 1, 2, ..., 12 | +|`%q` | Month as an unpadded number | 1,2,...,12 | +|`%b`, `%h` | Abbreviated month name | Jan, Feb, ... | +|`%B` | Full month name | January, February, ... | +|`%d` | Day of the month as a zero-padded number | 01, 02, ..., 31 | +|`%e` | Day of the month as a space-padded number| 1, 2, ..., 31 | +|`%g` | Day of the month as a unpadded number | 1,2,...,31 | +|`%a` | Abbreviated weekday name | Sun, Mon, ... | +|`%A` | Full weekday name | Sunday, Monday, ... | +|`%H` | Hour (24-hour clock) as a zero-padded number | 00, ..., 24 | +|`%I` | Hour (12-hour clock) as a zero-padded number | 00, ..., 12 | +|`%l` | Hour 12-hour clock | 0, ..., 24 | +|`%p` | Locale’s equivalent of either AM or PM | AM, PM | +|`%P` | Locale’s equivalent of either am or pm | am, pm | +|`%M` | Minute as a zero-padded number | 00, 01, ..., 59 | +|`%S` | Second as a zero-padded number | 00, 01, ..., 59 | +|`%L` | Millisecond as a zero-padded number | 000, 001, ..., 999 | +|`%f` | Microsecond as a zero-padded number | 000000, ..., 999999 | +|`%s` | Nanosecond as a zero-padded number | 00000000, ..., 99999999 | +|`%z` | UTC offset in the form ±HHMM[SS[.ffffff]] or empty | +0000, -0400 | +|`%Z` | Timezone name or abbreviation or empty | UTC, EST, CST | +|`%i` | Timezone as +/-HH | -07 | +|`%j` | Timezone as +/-HH:MM | -07:00 | +|`%k` | Timezone as +/-HH:MM:SS | -07:00:00 | +|`%w` | Timezone as +/-HHMMSS | -070000 | +|`%D`, `%x` | Short MM/DD/YYYY date, equivalent to %m/%d/%y | 01/21/2031 | +|`%F` | Short YYYY-MM-DD date, equivalent to %Y-%m-%d | 2031-01-21 | +|`%T`,`%X` | ISO 8601 time format (HH:MM:SS), equivalent to %H:%M:%S | 02:55:02 | +|`%r` | 12-hour clock time | 02:55:02 pm | +|`%R` | 24-hour HH:MM time, equivalent to %H:%M | 13:55 | +|`%n` | New-line character ('\n') | | +|`%t` | Horizontal-tab character ('\t') | | +|`%%` | A % sign | | +|`%c` | Date and time representation | Mon Jan 02 15:04:05 2006 | + +Examples: + +- `Timestamp(Time("02/04/2023", "%m/%d/%Y"), "%A %h %e %Y")` + ### ToKeyValueString `ToKeyValueString(target, Optional[delimiter], Optional[pair_delimiter], Optional[sort_output])` diff --git a/pkg/ottl/ottlfuncs/func_timestamp.go b/pkg/ottl/ottlfuncs/func_timestamp.go new file mode 100644 index 000000000000..425e9e165ece --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_timestamp.go @@ -0,0 +1,51 @@ +// 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/internal/coreinternal/timeutils" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type TimestampArguments[K any] struct { + Time ottl.TimeGetter[K] + Format string +} + +func NewTimestampFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("Timestamp", &TimestampArguments[K]{}, createTimestampFunction[K]) +} + +func createTimestampFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*TimestampArguments[K]) + + if !ok { + return nil, fmt.Errorf("TimestampFactory args must be of type *TimestampArguments[K]") + } + + return Timestamp(args.Time, args.Format) +} + +func Timestamp[K any](timeValue ottl.TimeGetter[K], format string) (ottl.ExprFunc[K], error) { + if format == "" { + return nil, fmt.Errorf("format cannot be nil") + } + + gotimeFormat, err := timeutils.StrptimeToGotime(format) + if err != nil { + return nil, err + } + + return func(ctx context.Context, tCtx K) (any, error) { + t, err := timeValue.Get(ctx, tCtx) + if err != nil { + return nil, err + } + + return t.Format(gotimeFormat), nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_timestamp_test.go b/pkg/ottl/ottlfuncs/func_timestamp_test.go new file mode 100644 index 000000000000..b4b64e5616f1 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_timestamp_test.go @@ -0,0 +1,169 @@ +// 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_Timestamp(t *testing.T) { + tests := []struct { + name string + time ottl.TimeGetter[any] + format string + expected string + errorMsg string + funcErrorMsg string + }{ + { + name: "empty format", + time: &ottl.StandardTimeGetter[any]{}, + format: "", + errorMsg: "format cannot be nil", + }, + { + name: "invalid time", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "something", nil + }, + }, + format: "%Y-%m-%d", + funcErrorMsg: "expected time but got string", + }, + { + name: "simple short form", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2023, 4, 12, 0, 0, 0, 0, time.Local), nil + }, + }, + format: "%Y-%m-%d", + expected: "2023-04-12", + }, + { + name: "simple short form with short year and slashes", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2011, 11, 11, 0, 0, 0, 0, time.Local), nil + }, + }, + format: "%d/%m/%y", + expected: "11/11/11", + }, + { + name: "month day year", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2023, 2, 4, 0, 0, 0, 0, time.Local), nil + }, + }, + format: "%m/%d/%Y", + expected: "02/04/2023", + }, + { + name: "simple long form", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(1993, 7, 31, 0, 0, 0, 0, time.Local), nil + }, + }, + format: "%B %d, %Y", + expected: "July 31, 1993", + }, + { + name: "date with timestamp", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2023, 3, 14, 17, 0o2, 59, 0, time.Local), nil + }, + }, + format: "%b %d %Y %H:%M:%S", + expected: "Mar 14 2023 17:02:59", + }, + { + name: "day of the week long form", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2023, 5, 1, 0, 0, 0, 0, time.Local), nil + }, + }, + format: "%A, %B %d, %Y", + expected: "Monday, May 01, 2023", + }, + { + name: "short weekday, short month, long format", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2023, 5, 20, 0, 0, 0, 0, time.Local), nil + }, + }, + format: "%a, %b %d, %Y", + expected: "Sat, May 20, 2023", + }, + { + name: "short months", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2023, 2, 15, 0, 0, 0, 0, time.Local), nil + }, + }, + format: "%b %d, %Y", + expected: "Feb 15, 2023", + }, + { + name: "simple short form with time", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2023, 5, 26, 12, 34, 56, 0, time.Local), nil + }, + }, + format: "%Y-%m-%d %H:%M:%S", + expected: "2023-05-26 12:34:56", + }, + { + name: "RFC 3339 in custom format", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(2012, 11, 0o1, 22, 8, 41, 0, time.Local), nil + }, + }, + format: "%Y-%m-%dT%H:%M:%S", + expected: "2012-11-01T22:08:41", + }, + { + name: "RFC 3339 in custom format before 2000", + time: &ottl.StandardTimeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return time.Date(1986, 10, 0o1, 0o0, 17, 33, 0o0, time.Local), nil + }, + }, + format: "%Y-%m-%dT%H:%M:%S", + expected: "1986-10-01T00:17:33", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := Timestamp(tt.time, tt.format) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg) + } else { + assert.NoError(t, err) + result, err := exprFunc(nil, nil) + if tt.funcErrorMsg != "" { + assert.Contains(t, err.Error(), tt.funcErrorMsg) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + } + }) + } +} diff --git a/pkg/ottl/ottlfuncs/functions.go b/pkg/ottl/ottlfuncs/functions.go index efd8480fc691..afc7ca9060cb 100644 --- a/pkg/ottl/ottlfuncs/functions.go +++ b/pkg/ottl/ottlfuncs/functions.go @@ -87,6 +87,7 @@ func converters[K any]() []ottl.Factory[K] { NewStringFactory[K](), NewSubstringFactory[K](), NewTimeFactory[K](), + NewTimestampFactory[K](), NewTrimFactory[K](), NewToKeyValueStringFactory[K](), NewTruncateTimeFactory[K](),