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

Added date without year support to timestamp stage #760

Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions docs/logentry/processing-log-lines.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ A match stage will take the provided label `selector` and determine if a group o

### timestamp

A timestamp stage will parse data from the `extracted` map and set the `time` value which will be stored by Loki.
A timestamp stage will parse data from the `extracted` map and set the `time` value which will be stored by Loki. The timestamp stage is important for having log entries in the correct order. In the absence of this stage, promtail will associate the current timestamp to the log entry.

```yaml
- timestamp:
Expand Down Expand Up @@ -394,7 +394,7 @@ UnixMs = 1562708916414
UnixNs = 1562708916000000123
```

Finally any custom format can be supplied, and will be passed directly in as the layout parameter in time.Parse()
Finally any custom format can be supplied, and will be passed directly in as the layout parameter in `time.Parse()`. If the custom format has no year component specified (ie. syslog's default logs), promtail will assume the current year should be used, correctly handling the edge cases around new year's eve.

__Read the [time.parse](https://golang.org/pkg/time/#Parse) docs closely if passing a custom format and make sure your custom format uses the special date they specify: `Mon Jan 2 15:04:05 -0700 MST 2006`__

Expand Down
11 changes: 10 additions & 1 deletion pkg/logentry/stages/timestamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestTimestampValidation(t *testing.T) {
testString: "2012-11-01T22:08:41-04:00",
expectedTime: time.Date(2012, 11, 01, 22, 8, 41, 0, time.FixedZone("", -4*60*60)),
},
"custom format": {
"custom format with year": {
config: &TimestampConfig{
Source: "source1",
Format: "2006-01-02",
Expand All @@ -82,6 +82,15 @@ func TestTimestampValidation(t *testing.T) {
testString: "2009-01-01",
expectedTime: time.Date(2009, 01, 01, 00, 00, 00, 0, time.UTC),
},
"custom format without year": {
config: &TimestampConfig{
Source: "source1",
Format: "Jan 02 15:04:05",
},
err: nil,
testString: "Jul 15 01:02:03",
expectedTime: time.Date(time.Now().Year(), 7, 15, 1, 2, 3, 0, time.UTC),
},
"unix_ms": {
config: &TimestampConfig{
Source: "source1",
Expand Down
37 changes: 37 additions & 0 deletions pkg/logentry/stages/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ package stages
import (
"fmt"
"strconv"
"strings"
"time"
)

const (
ErrTimestampContainsYear = "timestamp '%s' is expected to not contain the year date component"
)

// convertDateLayout converts pre-defined date format layout into date format
func convertDateLayout(predef string) parser {
switch predef {
Expand Down Expand Up @@ -74,12 +79,44 @@ func convertDateLayout(predef string) parser {
return time.Unix(0, i), nil
}
default:
if !strings.Contains(predef, "2006") {
return func(t string) (time.Time, error) {
return parseTimestampWithoutYear(predef, t, time.Now())
}
}
return func(t string) (time.Time, error) {
return time.Parse(predef, t)
}
}
}

// parseTimestampWithoutYear parses the input timestamp without the year component,
// assuming the timestamp is related to a point in time close to "now", and correctly
// handling the edge cases around new year's eve
func parseTimestampWithoutYear(layout string, timestamp string, now time.Time) (time.Time, error) {
parsedTime, err := time.Parse(layout, timestamp)
if err != nil {
return parsedTime, err
}

// Ensure the year component of the input date string has not been
// parsed for real
if parsedTime.Year() != 0 {
return parsedTime, fmt.Errorf(ErrTimestampContainsYear, timestamp)
}

// Handle the case we're crossing the new year's eve midnight
if parsedTime.Month() == 12 && now.Month() == 1 {
parsedTime = parsedTime.AddDate(now.Year()-1, 0, 0)
} else if parsedTime.Month() == 1 && now.Month() == 12 {
parsedTime = parsedTime.AddDate(now.Year()+1, 0, 0)
} else {
parsedTime = parsedTime.AddDate(now.Year(), 0, 0)
}

return parsedTime, nil
}

// getString will convert the input variable to a string if possible
func getString(unk interface{}) (string, error) {

Expand Down
96 changes: 96 additions & 0 deletions pkg/logentry/stages/util_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package stages

import (
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -65,3 +66,98 @@ func TestGetString(t *testing.T) {
assert.Equal(t, "2.02", s32)
assert.Equal(t, "1562723913000", s64_1)
}

func TestConvertDateLayout(t *testing.T) {
t.Parallel()

tests := map[string]struct {
layout string
timestamp string
expected time.Time
}{
"custom layout with year": {
"2006 Jan 02 15:04:05",
"2019 Jul 15 01:02:03",
time.Date(2019, 7, 15, 1, 2, 3, 0, time.UTC),
},
"custom layout without year": {
"Jan 02 15:04:05",
"Jul 15 01:02:03",
time.Date(time.Now().Year(), 7, 15, 1, 2, 3, 0, time.UTC),
},
}

for testName, testData := range tests {
testData := testData

t.Run(testName, func(t *testing.T) {
t.Parallel()

parser := convertDateLayout(testData.layout)
parsed, err := parser(testData.timestamp)
if err != nil {
t.Errorf("convertDateLayout() parser returned an unexpected error = %v", err)
return
}

assert.Equal(t, testData.expected, parsed)
})
}
}

func TestParseTimestampWithoutYear(t *testing.T) {
t.Parallel()

tests := map[string]struct {
layout string
timestamp string
now time.Time
expected time.Time
err error
}{
"parse timestamp within current year": {
"Jan 02 15:04:05",
"Jul 15 01:02:03",
time.Date(2019, 7, 14, 0, 0, 0, 0, time.UTC),
time.Date(2019, 7, 15, 1, 2, 3, 0, time.UTC),
nil,
},
"parse timestamp on 31th Dec and today is 1st Jan": {
"Jan 02 15:04:05",
"Dec 31 23:59:59",
time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC),
time.Date(2018, 12, 31, 23, 59, 59, 0, time.UTC),
nil,
},
"parse timestamp on 1st Jan and today is 31st Dec": {
"Jan 02 15:04:05",
"Jan 01 01:02:03",
time.Date(2018, 12, 31, 23, 59, 59, 0, time.UTC),
time.Date(2019, 1, 1, 1, 2, 3, 0, time.UTC),
nil,
},
"error if the input layout actually includes the year component": {
"2006 Jan 02 15:04:05",
"2019 Jan 01 01:02:03",
time.Date(2019, 1, 1, 1, 2, 3, 0, time.UTC),
time.Date(2019, 1, 1, 1, 2, 3, 0, time.UTC),
fmt.Errorf(ErrTimestampContainsYear, "2019 Jan 01 01:02:03"),
},
}

for testName, testData := range tests {
testData := testData

t.Run(testName, func(t *testing.T) {
t.Parallel()

parsed, err := parseTimestampWithoutYear(testData.layout, testData.timestamp, testData.now)
if ((err != nil) != (testData.err != nil)) || (err != nil && testData.err != nil && err.Error() != testData.err.Error()) {
t.Errorf("parseTimestampWithoutYear() expected error = %v, actual error = %v", testData.err, err)
return
}

assert.Equal(t, testData.expected, parsed)
})
}
}