Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pkg/stanza] Move core time parsing capabilities to coreinternal #23232

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Move core time parsing capabilities to coreinternal
  • Loading branch information
fchikwekwe authored and TylerHelmuth committed Jun 8, 2023
commit 4bdf011465bf2ce8d971cbfb2591544308be5ab6
20 changes: 20 additions & 0 deletions .chloggen/main.yaml
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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: "Adds new `Time` converter to convert a string to a Golang time struct based on a supplied format"

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [22007]

# (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:
1 change: 1 addition & 0 deletions exporter/signalfxexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/observiq/ctimefmt v1.0.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
Expand Down
3 changes: 3 additions & 0 deletions exporter/signalfxexporter/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/coreinternal/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.19
require (
github.com/cenkalti/backoff/v4 v4.2.1
github.com/docker/go-connections v0.4.0
github.com/observiq/ctimefmt v1.0.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.79.0
github.com/stretchr/testify v1.8.4
github.com/testcontainers/testcontainers-go v0.20.1
Expand Down
3 changes: 3 additions & 0 deletions internal/coreinternal/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 98 additions & 0 deletions internal/coreinternal/timeutils/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package timeutils // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/timeutils"

import (
"fmt"
"strings"
"time"

strptime "github.com/observiq/ctimefmt"
)

func StrptimeToGo(layout string) (string, error) {
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
return strptime.ToNative(layout)
}

func GetLocation(location *string, layout *string) (*time.Location, error) {
if location != nil && *location != "" {
// If "location" is specified, it must be in the local timezone database
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
loc, err := time.LoadLocation(*location)
if err != nil {
return nil, fmt.Errorf("failed to load location %s: %w", *location, err)
}
return loc, nil
}

if layout != nil && strings.HasSuffix(*layout, "Z") {
// If a timestamp ends with 'Z', it should be interpretted at Zulu (UTC) time
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
return time.UTC, nil
}

return time.Local, nil
}

func ParseGoTime(layout string, value any, location *time.Location) (time.Time, error) {
timeValue, err := parseGotime(layout, value, location)
if err != nil {
return time.Time{}, err
}
return SetTimestampYear(timeValue), nil
}

func parseGotime(layout string, value interface{}, location *time.Location) (time.Time, error) {
var str string
switch v := value.(type) {
case string:
str = v
case []byte:
str = string(v)
default:
return time.Time{}, fmt.Errorf("type %T cannot be parsed as a time", value)
}

result, err := time.ParseInLocation(layout, str, location)

// Depending on the timezone database, we may get a pseudo-matching timezone
// This is apparent when the zone is not "UTC", but the offset is still 0
zone, offset := result.Zone()
if offset != 0 || zone == "UTC" {
return result, err
}

// Manually look up the location based on the zone
loc, locErr := time.LoadLocation(zone)
if locErr != nil {
// can't correct offset, just return what we have
return result, fmt.Errorf("failed to load location %s: %w", zone, locErr)
}

// Reparse the timestamp, with the location
resultLoc, locErr := time.ParseInLocation(layout, str, loc)
if locErr != nil {
// can't correct offset, just return original result
return result, err
}

return resultLoc, locErr
}

// SetTimestampYear sets the year of a timestamp to the current year.
// This is needed because year is missing from some time formats, such as rfc3164.
func SetTimestampYear(t time.Time) time.Time {
if t.Year() > 0 {
return t
}
n := Now()
d := time.Date(n.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
// If the timestamp would be more than 7 days in the future using this year,
// assume it's from last year.
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
if d.After(n.AddDate(0, 0, 7)) {
d = d.AddDate(-1, 0, 0)
}
return d
}

// Allows tests to override with deterministic value
var Now = time.Now
63 changes: 63 additions & 0 deletions internal/coreinternal/timeutils/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package timeutils

import (
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestParseGoTimeBadLocation(t *testing.T) {
_, err := ParseGoTime(time.RFC822, "02 Jan 06 15:04 BST", time.UTC)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to load location BST")
}

func Test_setTimestampYear(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
Now = func() time.Time {
return time.Date(2020, 06, 16, 3, 31, 34, 525, time.UTC)
}

noYear := time.Date(0, 06, 16, 3, 31, 34, 525, time.UTC)
yearAdded := SetTimestampYear(noYear)
expected := time.Date(2020, 06, 16, 3, 31, 34, 525, time.UTC)
require.Equal(t, expected, yearAdded)
})

t.Run("FutureOneDay", func(t *testing.T) {
Now = func() time.Time {
return time.Date(2020, 01, 16, 3, 31, 34, 525, time.UTC)
}

noYear := time.Date(0, 01, 17, 3, 31, 34, 525, time.UTC)
yearAdded := SetTimestampYear(noYear)
expected := time.Date(2020, 01, 17, 3, 31, 34, 525, time.UTC)
require.Equal(t, expected, yearAdded)
})

t.Run("FutureEightDays", func(t *testing.T) {
Now = func() time.Time {
return time.Date(2020, 01, 16, 3, 31, 34, 525, time.UTC)
}

noYear := time.Date(0, 01, 24, 3, 31, 34, 525, time.UTC)
yearAdded := SetTimestampYear(noYear)
expected := time.Date(2019, 01, 24, 3, 31, 34, 525, time.UTC)
require.Equal(t, expected, yearAdded)
})

t.Run("RolloverYear", func(t *testing.T) {
Now = func() time.Time {
return time.Date(2020, 01, 01, 3, 31, 34, 525, time.UTC)
}

noYear := time.Date(0, 12, 31, 3, 31, 34, 525, time.UTC)
yearAdded := SetTimestampYear(noYear)
expected := time.Date(2019, 12, 31, 3, 31, 34, 525, time.UTC)
require.Equal(t, expected, yearAdded)
})
}
2 changes: 1 addition & 1 deletion pkg/stanza/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ require (
github.com/influxdata/go-syslog/v3 v3.0.1-0.20210608084020-ac565dc76ba6
github.com/jpillora/backoff v1.0.0
github.com/json-iterator/go v1.1.12
github.com/observiq/ctimefmt v1.0.0
github.com/observiq/nanojack v0.0.0-20201106172433-343928847ebc
github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.79.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.79.0
Expand Down Expand Up @@ -43,6 +42,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/observiq/ctimefmt v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
go.opencensus.io v0.24.0 // indirect
Expand Down
68 changes: 6 additions & 62 deletions pkg/stanza/operator/helper/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (
"strings"
"time"

strptime "github.com/observiq/ctimefmt"
"go.opentelemetry.io/collector/confmap"

"github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/timeutils"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/errors"
)
Expand Down Expand Up @@ -79,7 +79,7 @@ func (t *TimeParser) Validate() error {
case NativeKey, GotimeKey: // ok
case StrptimeKey:
var err error
t.Layout, err = strptime.ToNative(t.Layout)
t.Layout, err = timeutils.StrptimeToGo(t.Layout)
if err != nil {
return errors.Wrap(err, "parse strptime layout")
}
Expand Down Expand Up @@ -147,63 +147,26 @@ func (t *TimeParser) Parse(entry *entry.Entry) error {
if !ok {
return fmt.Errorf("native time.Time field required, but found %v of type %T", value, value)
}
entry.Timestamp = setTimestampYear(timeValue)
entry.Timestamp = timeutils.SetTimestampYear(timeValue)
case GotimeKey:
timeValue, err := t.parseGotime(value)
timeValue, err := timeutils.ParseGoTime(t.Layout, value, t.location)
if err != nil {
return err
}
entry.Timestamp = setTimestampYear(timeValue)
entry.Timestamp = timeValue
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
case EpochKey:
timeValue, err := t.parseEpochTime(value)
if err != nil {
return err
}
entry.Timestamp = setTimestampYear(timeValue)
entry.Timestamp = timeutils.SetTimestampYear(timeValue)
default:
return fmt.Errorf("unsupported layout type: %s", t.LayoutType)
}

return nil
}

func (t *TimeParser) parseGotime(value interface{}) (time.Time, error) {
var str string
switch v := value.(type) {
case string:
str = v
case []byte:
str = string(v)
default:
return time.Time{}, fmt.Errorf("type %T cannot be parsed as a time", value)
}

result, err := time.ParseInLocation(t.Layout, str, t.location)

// Depending on the timezone database, we may get a pseudo-matching timezone
// This is apparent when the zone is not "UTC", but the offset is still 0
zone, offset := result.Zone()
if offset != 0 || zone == "UTC" {
return result, err
}

// Manually look up the location based on the zone
loc, locErr := time.LoadLocation(zone)
if locErr != nil {
// can't correct offset, just return what we have
return result, fmt.Errorf("failed to load location %s: %w", zone, locErr)
}

// Reparse the timestamp, with the location
resultLoc, locErr := time.ParseInLocation(t.Layout, str, loc)
if locErr != nil {
// can't correct offset, just return original result
return result, err
}

return resultLoc, locErr
}

func (t *TimeParser) parseEpochTime(value interface{}) (time.Time, error) {
stamp, err := getEpochStamp(t.Layout, value)
if err != nil {
Expand Down Expand Up @@ -275,22 +238,3 @@ var toTime = map[string]toTimeFunc{
"ns": func(ns int64) time.Time { return time.Unix(0, ns) },
}
var subsecToNs = map[string]int64{"s.ms": 1e6, "s.us": 1e3, "s.ns": 1}

// setTimestampYear sets the year of a timestamp to the current year.
// This is needed because year is missing from some time formats, such as rfc3164.
func setTimestampYear(t time.Time) time.Time {
if t.Year() > 0 {
return t
}
n := now()
d := time.Date(n.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
// If the timestamp would be more than 7 days in the future using this year,
// assume it's from last year.
if d.After(n.AddDate(0, 0, 7)) {
d = d.AddDate(-1, 0, 0)
}
return d
}

// Allows tests to override with deterministic value
var now = time.Now
Loading