Skip to content

Commit 116d1dd

Browse files
odubajDTedmocosta
authored andcommitted
[pkg/ottl] introduce FormatTime() converter function (open-telemetry#37112)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Adds a new `FormatTime(time, format)` converter to convert any time to a human readable string with the specified format <!-- Issue number (e.g. open-telemetry#1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes open-telemetry#36870 --------- Signed-off-by: odubajDT <[email protected]> Co-authored-by: Edmo Vamerlatti Costa <[email protected]>
1 parent 2d75533 commit 116d1dd

File tree

6 files changed

+311
-0
lines changed

6 files changed

+311
-0
lines changed

.chloggen/ottl-timestamp.yaml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: pkg/ottl
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Add the `FormatTime` function to convert `time.Time` values to human-readable strings"
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [36870]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

pkg/ottl/e2e/e2e_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,12 @@ func Test_e2e_converters(t *testing.T) {
937937
tCtx.GetLogRecord().SetTimestamp(pcommon.NewTimestampFromTime(TestLogTimestamp.AsTime().Truncate(time.Second)))
938938
},
939939
},
940+
{
941+
statement: `set(attributes["time"], FormatTime(time, "%Y-%m-%d"))`,
942+
want: func(tCtx ottllog.TransformContext) {
943+
tCtx.GetLogRecord().Attributes().PutStr("time", "2020-02-11")
944+
},
945+
},
940946
{
941947
statement: `set(attributes["test"], "pass") where UnixMicro(time) > 0`,
942948
want: func(tCtx ottllog.TransformContext) {

pkg/ottl/ottlfuncs/README.md

+57
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ Available Converters:
422422
- [ExtractGrokPatterns](#extractgrokpatterns)
423423
- [FNV](#fnv)
424424
- [Format](#format)
425+
- [FormatTime](#formattime)
425426
- [GetXML](#getxml)
426427
- [Hex](#hex)
427428
- [Hour](#hour)
@@ -806,6 +807,62 @@ Examples:
806807
- `Format("%04d-%02d-%02d", [Year(Now()), Month(Now()), Day(Now())])`
807808
- `Format("%s/%s/%04d-%02d-%02d.log", [attributes["hostname"], body["program"], Year(Now()), Month(Now()), Day(Now())])`
808809

810+
### FormatTime
811+
812+
`FormatTime(time, format)`
813+
814+
The `FormatTime` Converter takes a `time.Time` and converts it to a human-readable string representation of the time according to the specified format.
815+
816+
`time` is `time.Time`. If `time` is another type an error is returned. `format` is a string.
817+
818+
If either `time` or `format` are 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.
819+
820+
`format` denotes a human-readable textual 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:
821+
| substitution | description | examples |
822+
|-----|-----|-----|
823+
|`%Y` | Year as a zero-padded number | 0001, 0002, ..., 2019, 2020, ..., 9999 |
824+
|`%y` | Year, last two digits as a zero-padded number | 01, ..., 99 |
825+
|`%m` | Month as a zero-padded number | 01, 02, ..., 12 |
826+
|`%o` | Month as a space-padded number | 1, 2, ..., 12 |
827+
|`%q` | Month as an unpadded number | 1,2,...,12 |
828+
|`%b`, `%h` | Abbreviated month name | Jan, Feb, ... |
829+
|`%B` | Full month name | January, February, ... |
830+
|`%d` | Day of the month as a zero-padded number | 01, 02, ..., 31 |
831+
|`%e` | Day of the month as a space-padded number| 1, 2, ..., 31 |
832+
|`%g` | Day of the month as a unpadded number | 1,2,...,31 |
833+
|`%a` | Abbreviated weekday name | Sun, Mon, ... |
834+
|`%A` | Full weekday name | Sunday, Monday, ... |
835+
|`%H` | Hour (24-hour clock) as a zero-padded number | 00, ..., 24 |
836+
|`%I` | Hour (12-hour clock) as a zero-padded number | 00, ..., 12 |
837+
|`%l` | Hour 12-hour clock | 0, ..., 24 |
838+
|`%p` | Locale’s equivalent of either AM or PM | AM, PM |
839+
|`%P` | Locale’s equivalent of either am or pm | am, pm |
840+
|`%M` | Minute as a zero-padded number | 00, 01, ..., 59 |
841+
|`%S` | Second as a zero-padded number | 00, 01, ..., 59 |
842+
|`%L` | Millisecond as a zero-padded number | 000, 001, ..., 999 |
843+
|`%f` | Microsecond as a zero-padded number | 000000, ..., 999999 |
844+
|`%s` | Nanosecond as a zero-padded number | 00000000, ..., 99999999 |
845+
|`%z` | UTC offset in the form ±HHMM[SS[.ffffff]] or empty | +0000, -0400 |
846+
|`%Z` | Timezone name or abbreviation or empty | UTC, EST, CST |
847+
|`%i` | Timezone as +/-HH | -07 |
848+
|`%j` | Timezone as +/-HH:MM | -07:00 |
849+
|`%k` | Timezone as +/-HH:MM:SS | -07:00:00 |
850+
|`%w` | Timezone as +/-HHMMSS | -070000 |
851+
|`%D`, `%x` | Short MM/DD/YYYY date, equivalent to %m/%d/%y | 01/21/2031 |
852+
|`%F` | Short YYYY-MM-DD date, equivalent to %Y-%m-%d | 2031-01-21 |
853+
|`%T`,`%X` | ISO 8601 time format (HH:MM:SS), equivalent to %H:%M:%S | 02:55:02 |
854+
|`%r` | 12-hour clock time | 02:55:02 pm |
855+
|`%R` | 24-hour HH:MM time, equivalent to %H:%M | 13:55 |
856+
|`%n` | New-line character ('\n') | |
857+
|`%t` | Horizontal-tab character ('\t') | |
858+
|`%%` | A % sign | |
859+
|`%c` | Date and time representation | Mon Jan 02 15:04:05 2006 |
860+
861+
Examples:
862+
863+
- `FormatTime(Time("02/04/2023", "%m/%d/%Y"), "%A %h %e %Y")`
864+
- `FormatTime(UnixNano(attributes["time_nanoseconds"]), "%b %d %Y %H:%M:%S")`
865+
- `FormatTime(TruncateTime(time, Duration("10h 20m"))), "%Y-%m-%d %H:%M:%S")`
809866

810867
### GetXML
811868

pkg/ottl/ottlfuncs/func_formattime.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"
5+
6+
import (
7+
"context"
8+
"errors"
9+
10+
"github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/timeutils"
11+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
12+
)
13+
14+
type FormatTimeArguments[K any] struct {
15+
Time ottl.TimeGetter[K]
16+
Format string
17+
}
18+
19+
func NewFormatTimeFactory[K any]() ottl.Factory[K] {
20+
return ottl.NewFactory("FormatTime", &FormatTimeArguments[K]{}, createFormatTimeFunction[K])
21+
}
22+
23+
func createFormatTimeFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
24+
args, ok := oArgs.(*FormatTimeArguments[K])
25+
26+
if !ok {
27+
return nil, errors.New("FormatTimeFactory args must be of type *FormatTimeArguments[K]")
28+
}
29+
30+
return FormatTime(args.Time, args.Format)
31+
}
32+
33+
func FormatTime[K any](timeValue ottl.TimeGetter[K], format string) (ottl.ExprFunc[K], error) {
34+
if format == "" {
35+
return nil, errors.New("format cannot be nil")
36+
}
37+
38+
gotimeFormat, err := timeutils.StrptimeToGotime(format)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
return func(ctx context.Context, tCtx K) (any, error) {
44+
t, err := timeValue.Get(ctx, tCtx)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
return t.Format(gotimeFormat), nil
50+
}, nil
51+
}
+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ottlfuncs
5+
6+
import (
7+
"context"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/assert"
12+
13+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
14+
)
15+
16+
func Test_FormatTime(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
time ottl.TimeGetter[any]
20+
format string
21+
expected string
22+
errorMsg string
23+
funcErrorMsg string
24+
}{
25+
{
26+
name: "empty format",
27+
time: &ottl.StandardTimeGetter[any]{},
28+
format: "",
29+
errorMsg: "format cannot be nil",
30+
},
31+
{
32+
name: "invalid time",
33+
time: &ottl.StandardTimeGetter[any]{
34+
Getter: func(_ context.Context, _ any) (any, error) {
35+
return "something", nil
36+
},
37+
},
38+
format: "%Y-%m-%d",
39+
funcErrorMsg: "expected time but got string",
40+
},
41+
{
42+
name: "simple short form",
43+
time: &ottl.StandardTimeGetter[any]{
44+
Getter: func(_ context.Context, _ any) (any, error) {
45+
return time.Date(2023, 4, 12, 0, 0, 0, 0, time.Local), nil
46+
},
47+
},
48+
format: "%Y-%m-%d",
49+
expected: "2023-04-12",
50+
},
51+
{
52+
name: "simple short form with short year and slashes",
53+
time: &ottl.StandardTimeGetter[any]{
54+
Getter: func(_ context.Context, _ any) (any, error) {
55+
return time.Date(2011, 11, 11, 0, 0, 0, 0, time.Local), nil
56+
},
57+
},
58+
format: "%d/%m/%y",
59+
expected: "11/11/11",
60+
},
61+
{
62+
name: "month day year",
63+
time: &ottl.StandardTimeGetter[any]{
64+
Getter: func(_ context.Context, _ any) (any, error) {
65+
return time.Date(2023, 2, 4, 0, 0, 0, 0, time.Local), nil
66+
},
67+
},
68+
format: "%m/%d/%Y",
69+
expected: "02/04/2023",
70+
},
71+
{
72+
name: "simple long form",
73+
time: &ottl.StandardTimeGetter[any]{
74+
Getter: func(_ context.Context, _ any) (any, error) {
75+
return time.Date(1993, 7, 31, 0, 0, 0, 0, time.Local), nil
76+
},
77+
},
78+
format: "%B %d, %Y",
79+
expected: "July 31, 1993",
80+
},
81+
{
82+
name: "date with FormatTime",
83+
time: &ottl.StandardTimeGetter[any]{
84+
Getter: func(_ context.Context, _ any) (any, error) {
85+
return time.Date(2023, 3, 14, 17, 0o2, 59, 0, time.Local), nil
86+
},
87+
},
88+
format: "%b %d %Y %H:%M:%S",
89+
expected: "Mar 14 2023 17:02:59",
90+
},
91+
{
92+
name: "day of the week long form",
93+
time: &ottl.StandardTimeGetter[any]{
94+
Getter: func(_ context.Context, _ any) (any, error) {
95+
return time.Date(2023, 5, 1, 0, 0, 0, 0, time.Local), nil
96+
},
97+
},
98+
format: "%A, %B %d, %Y",
99+
expected: "Monday, May 01, 2023",
100+
},
101+
{
102+
name: "short weekday, short month, long format",
103+
time: &ottl.StandardTimeGetter[any]{
104+
Getter: func(_ context.Context, _ any) (any, error) {
105+
return time.Date(2023, 5, 20, 0, 0, 0, 0, time.Local), nil
106+
},
107+
},
108+
format: "%a, %b %d, %Y",
109+
expected: "Sat, May 20, 2023",
110+
},
111+
{
112+
name: "short months",
113+
time: &ottl.StandardTimeGetter[any]{
114+
Getter: func(_ context.Context, _ any) (any, error) {
115+
return time.Date(2023, 2, 15, 0, 0, 0, 0, time.Local), nil
116+
},
117+
},
118+
format: "%b %d, %Y",
119+
expected: "Feb 15, 2023",
120+
},
121+
{
122+
name: "simple short form with time",
123+
time: &ottl.StandardTimeGetter[any]{
124+
Getter: func(_ context.Context, _ any) (any, error) {
125+
return time.Date(2023, 5, 26, 12, 34, 56, 0, time.Local), nil
126+
},
127+
},
128+
format: "%Y-%m-%d %H:%M:%S",
129+
expected: "2023-05-26 12:34:56",
130+
},
131+
{
132+
name: "RFC 3339 in custom format",
133+
time: &ottl.StandardTimeGetter[any]{
134+
Getter: func(_ context.Context, _ any) (any, error) {
135+
return time.Date(2012, 11, 0o1, 22, 8, 41, 0, time.Local), nil
136+
},
137+
},
138+
format: "%Y-%m-%dT%H:%M:%S",
139+
expected: "2012-11-01T22:08:41",
140+
},
141+
{
142+
name: "RFC 3339 in custom format before 2000",
143+
time: &ottl.StandardTimeGetter[any]{
144+
Getter: func(_ context.Context, _ any) (any, error) {
145+
return time.Date(1986, 10, 0o1, 0o0, 17, 33, 0o0, time.Local), nil
146+
},
147+
},
148+
format: "%Y-%m-%dT%H:%M:%S",
149+
expected: "1986-10-01T00:17:33",
150+
},
151+
}
152+
for _, tt := range tests {
153+
t.Run(tt.name, func(t *testing.T) {
154+
exprFunc, err := FormatTime(tt.time, tt.format)
155+
if tt.errorMsg != "" {
156+
assert.Contains(t, err.Error(), tt.errorMsg)
157+
} else {
158+
assert.NoError(t, err)
159+
result, err := exprFunc(nil, nil)
160+
if tt.funcErrorMsg != "" {
161+
assert.Contains(t, err.Error(), tt.funcErrorMsg)
162+
} else {
163+
assert.NoError(t, err)
164+
assert.Equal(t, tt.expected, result)
165+
}
166+
}
167+
})
168+
}
169+
}

pkg/ottl/ottlfuncs/functions.go

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func converters[K any]() []ottl.Factory[K] {
8989
NewStringFactory[K](),
9090
NewSubstringFactory[K](),
9191
NewTimeFactory[K](),
92+
NewFormatTimeFactory[K](),
9293
NewTrimFactory[K](),
9394
NewToKeyValueStringFactory[K](),
9495
NewTruncateTimeFactory[K](),

0 commit comments

Comments
 (0)