Skip to content

Commit c0512b9

Browse files
michalpristasTylerHelmuthevan-bradley
authored
[pkg/ottl] Added support for timezone in Time converter (#32479)
**Description:** Added support for default timezone in Time converter. Timezone is optional and can be specified as so: `Time("2023-05-26 12:34:56", "%Y-%m-%d %H:%M:%S", "America/New_York")` **Link to tracking Issue:** #32140 **Testing:** Unit tests added **Documentation:** Documentation in ottl/Readme updated --------- Co-authored-by: Tyler Helmuth <[email protected]> Co-authored-by: Evan Bradley <[email protected]>
1 parent 7fd145b commit c0512b9

File tree

4 files changed

+113
-10
lines changed

4 files changed

+113
-10
lines changed

.chloggen/ottl-time-timezone.yaml

Lines changed: 27 additions & 0 deletions
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: Added support for timezone in Time converter
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: [32140]
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/ottlfuncs/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,11 +1131,11 @@ Examples:
11311131

11321132
### Time
11331133

1134-
`Time(target, format)`
1134+
`Time(target, format, Optional[location])`
11351135

11361136
The `Time` Converter takes a string representation of a time and converts it to a Golang `time.Time`.
11371137

1138-
`target` is a string. `format` is a string.
1138+
`target` is a string. `format` is a string, `location` is an optional string.
11391139

11401140
If either `target` 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 the `target` and `format` do not follow the parsing rules used by this parser, an error is returned.
11411141

@@ -1176,13 +1176,24 @@ If either `target` or `format` are nil, an error is returned. The parser used is
11761176
|`%%` | A % sign | |
11771177
|`%c` | Date and time representation | Mon Jan 02 15:04:05 2006 |
11781178

1179+
`location` specifies a default time zone canonical ID to be used for date parsing in case it is not part of `format`.
1180+
1181+
When loading `location`, this function will look for the IANA Time Zone database in the following locations in order:
1182+
- a directory or uncompressed zip file named by the ZONEINFO environment variable
1183+
- on a Unix system, the system standard installation location
1184+
- $GOROOT/lib/time/zoneinfo.zip
1185+
- the `time/tzdata` package, if it was imported.
1186+
1187+
When building a Collector binary, importing `time/tzdata` in any Go source file will bundle the database into the binary, which guarantees the lookups will work regardless of the setup on the host setup. Note this will add roughly 500kB to binary size.
1188+
11791189
Examples:
11801190

11811191
- `Time("02/04/2023", "%m/%d/%Y")`
11821192
- `Time("Feb 15, 2023", "%b %d, %Y")`
11831193
- `Time("2023-05-26 12:34:56 HST", "%Y-%m-%d %H:%M:%S %Z")`
11841194
- `Time("1986-10-01T00:17:33 MST", "%Y-%m-%dT%H:%M:%S %Z")`
11851195
- `Time("2012-11-01T22:08:41+0000 EST", "%Y-%m-%dT%H:%M:%S%z %Z")`
1196+
- `Time("2023-05-26 12:34:56", "%Y-%m-%d %H:%M:%S", "America/New_York")`
11861197

11871198
### TraceID
11881199

pkg/ottl/ottlfuncs/func_time.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
)
1313

1414
type TimeArguments[K any] struct {
15-
Time ottl.StringGetter[K]
16-
Format string
15+
Time ottl.StringGetter[K]
16+
Format string
17+
Location ottl.Optional[string]
1718
}
1819

1920
func NewTimeFactory[K any]() ottl.Factory[K] {
@@ -26,14 +27,20 @@ func createTimeFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ot
2627
return nil, fmt.Errorf("TimeFactory args must be of type *TimeArguments[K]")
2728
}
2829

29-
return Time(args.Time, args.Format)
30+
return Time(args.Time, args.Format, args.Location)
3031
}
3132

32-
func Time[K any](inputTime ottl.StringGetter[K], format string) (ottl.ExprFunc[K], error) {
33+
func Time[K any](inputTime ottl.StringGetter[K], format string, location ottl.Optional[string]) (ottl.ExprFunc[K], error) {
3334
if format == "" {
3435
return nil, fmt.Errorf("format cannot be nil")
3536
}
36-
loc, err := timeutils.GetLocation(nil, &format)
37+
var defaultLocation *string
38+
if !location.IsEmpty() {
39+
l := location.Get()
40+
defaultLocation = &l
41+
}
42+
43+
loc, err := timeutils.GetLocation(defaultLocation, &format)
3744
if err != nil {
3845
return nil, err
3946
}

pkg/ottl/ottlfuncs/func_time_test.go

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@ import (
1515
)
1616

1717
func Test_Time(t *testing.T) {
18+
locationAmericaNewYork, _ := time.LoadLocation("America/New_York")
19+
locationAsiaShanghai, _ := time.LoadLocation("Asia/Shanghai")
20+
1821
tests := []struct {
1922
name string
2023
time ottl.StringGetter[any]
2124
format string
2225
expected time.Time
26+
location string
2327
}{
2428
{
2529
name: "simple short form",
@@ -151,10 +155,47 @@ func Test_Time(t *testing.T) {
151155
format: "%Y/%m/%d",
152156
expected: time.Date(2022, 01, 01, 0, 0, 0, 0, time.Local),
153157
},
158+
{
159+
name: "with location - America",
160+
time: &ottl.StandardStringGetter[any]{
161+
Getter: func(_ context.Context, _ any) (any, error) {
162+
return "2023-05-26 12:34:56", nil
163+
},
164+
},
165+
format: "%Y-%m-%d %H:%M:%S",
166+
location: "America/New_York",
167+
expected: time.Date(2023, 5, 26, 12, 34, 56, 0, locationAmericaNewYork),
168+
},
169+
{
170+
name: "with location - Asia",
171+
time: &ottl.StandardStringGetter[any]{
172+
Getter: func(_ context.Context, _ any) (any, error) {
173+
return "2023-05-26 12:34:56", nil
174+
},
175+
},
176+
format: "%Y-%m-%d %H:%M:%S",
177+
location: "Asia/Shanghai",
178+
expected: time.Date(2023, 5, 26, 12, 34, 56, 0, locationAsiaShanghai),
179+
},
180+
{
181+
name: "RFC 3339 in custom format before 2000, ignore default location",
182+
time: &ottl.StandardStringGetter[any]{
183+
Getter: func(_ context.Context, _ any) (any, error) {
184+
return "1986-10-01T00:17:33 MST", nil
185+
},
186+
},
187+
location: "Asia/Shanghai",
188+
format: "%Y-%m-%dT%H:%M:%S %Z",
189+
expected: time.Date(1986, 10, 01, 00, 17, 33, 00, time.FixedZone("MST", -7*60*60)),
190+
},
154191
}
155192
for _, tt := range tests {
156193
t.Run(tt.name, func(t *testing.T) {
157-
exprFunc, err := Time(tt.time, tt.format)
194+
var locOptional ottl.Optional[string]
195+
if tt.location != "" {
196+
locOptional = ottl.NewTestingOptional(tt.location)
197+
}
198+
exprFunc, err := Time(tt.time, tt.format, locOptional)
158199
assert.NoError(t, err)
159200
result, err := exprFunc(nil, nil)
160201
assert.NoError(t, err)
@@ -193,7 +234,8 @@ func Test_TimeError(t *testing.T) {
193234
}
194235
for _, tt := range tests {
195236
t.Run(tt.name, func(t *testing.T) {
196-
exprFunc, err := Time[any](tt.time, tt.format)
237+
var locOptional ottl.Optional[string]
238+
exprFunc, err := Time[any](tt.time, tt.format, locOptional)
197239
require.NoError(t, err)
198240
_, err = exprFunc(context.Background(), nil)
199241
assert.ErrorContains(t, err, tt.expectedError)
@@ -207,6 +249,7 @@ func Test_TimeFormatError(t *testing.T) {
207249
time ottl.StringGetter[any]
208250
format string
209251
expectedError string
252+
location string
210253
}{
211254
{
212255
name: "invalid short with no format",
@@ -218,10 +261,25 @@ func Test_TimeFormatError(t *testing.T) {
218261
format: "",
219262
expectedError: "format cannot be nil",
220263
},
264+
{
265+
name: "with unknown location",
266+
time: &ottl.StandardStringGetter[any]{
267+
Getter: func(_ context.Context, _ any) (any, error) {
268+
return "2023-05-26 12:34:56", nil
269+
},
270+
},
271+
format: "%Y-%m-%d %H:%M:%S",
272+
location: "Jupiter/Ganymede",
273+
expectedError: "unknown time zone Jupiter/Ganymede",
274+
},
221275
}
222276
for _, tt := range tests {
223277
t.Run(tt.name, func(t *testing.T) {
224-
_, err := Time[any](tt.time, tt.format)
278+
var locOptional ottl.Optional[string]
279+
if tt.location != "" {
280+
locOptional = ottl.NewTestingOptional(tt.location)
281+
}
282+
_, err := Time[any](tt.time, tt.format, locOptional)
225283
assert.ErrorContains(t, err, tt.expectedError)
226284
})
227285
}

0 commit comments

Comments
 (0)