Skip to content

Commit

Permalink
[pkg/stanza] Move core time parsing capabilities to coreinternal (ope…
Browse files Browse the repository at this point in the history
…n-telemetry#23232)

* Move core time parsing capabilities to coreinternal

* Add clarifying comment

* Apply suggestions from code review

Co-authored-by: Curtis Robert <[email protected]>

* Replace StrptimeToGo with ParseStrptime

* remove changelog

---------

Co-authored-by: Faith Chikwekwe <[email protected]>
Co-authored-by: Curtis Robert <[email protected]>
  • Loading branch information
3 people authored Jun 9, 2023
1 parent cab5ab9 commit c67489a
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 128 deletions.
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.

104 changes: 104 additions & 0 deletions internal/coreinternal/timeutils/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 ParseStrptime(layout string, value any, location *time.Location) (time.Time, error) {
goLayout, err := strptime.ToNative(layout)
if err != nil {
return time.Time{}, err
}
return ParseGotime(goLayout, value, location)
}

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
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 interpreted at Zulu (UTC) time
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())
// Assume the timestamp is from last year if its month and day are
// more than 7 days past the current date.
// i.e. If today is January 1, but the timestamp is February 1, it's safe
// to assume the timestamp is 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
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)
})
}
66 changes: 6 additions & 60 deletions pkg/stanza/operator/helper/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
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 @@ -147,63 +148,27 @@ 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)
// timeutils.ParseGotime calls timeutils.SetTimestampYear before returning the timeValue
entry.Timestamp = timeValue
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 +240,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

0 comments on commit c67489a

Please sign in to comment.