Skip to content

Commit de83272

Browse files
slim-beanEd
authored andcommitted
Add support to timestamp stage to parse Unix seconds, milliseconds, and nanosecond timestamps
1 parent 1cc25f8 commit de83272

File tree

4 files changed

+149
-33
lines changed

4 files changed

+149
-33
lines changed

docs/logentry/processing-log-lines.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,18 @@ RFC3339 = "2006-01-02T15:04:05Z07:00"
254254
RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
255255
```
256256

257+
Additionally support for common Unix timestamps is supported:
258+
259+
```go
260+
Unix = 1562708916
261+
UnixMs = 1562708916414
262+
UnixNs = 1562708916000000123
263+
```
264+
265+
Finally any custom format can be supplied, and will be passed directly in as the layout parameter in time.Parse()
266+
267+
__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`__
268+
257269
##### Example:
258270

259271
```yaml
@@ -453,4 +465,4 @@ Gauge examples will be very similar to Counter examples with additional `action`
453465
buckets: [0.001,0.0025,0.005,0.010,0.025,0.050]
454466
```
455467

456-
This would create a Histogram which looks for _response_time_ in the `extracted` data and applies the value to the histogram.
468+
This would create a Histogram which looks for _response_time_ in the `extracted` data and applies the value to the histogram.

pkg/logentry/stages/timestamp.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ const (
1515
ErrEmptyTimestampStageConfig = "timestamp stage config cannot be empty"
1616
ErrTimestampSourceRequired = "timestamp source value is required if timestamp is specified"
1717
ErrTimestampFormatRequired = "timestamp format is required"
18+
19+
Unix = "Unix"
20+
UnixMs = "UnixMs"
21+
UnixNs = "UnixNs"
1822
)
1923

2024
// TimestampConfig configures timestamp extraction
@@ -23,16 +27,19 @@ type TimestampConfig struct {
2327
Format string `mapstructure:"format"`
2428
}
2529

30+
// parser can convert the time string into a time.Time value
31+
type parser func(string) (time.Time, error)
32+
2633
// validateTimestampConfig validates a timestampStage configuration
27-
func validateTimestampConfig(cfg *TimestampConfig) (string, error) {
34+
func validateTimestampConfig(cfg *TimestampConfig) (parser, error) {
2835
if cfg == nil {
29-
return "", errors.New(ErrEmptyTimestampStageConfig)
36+
return nil, errors.New(ErrEmptyTimestampStageConfig)
3037
}
3138
if cfg.Source == "" {
32-
return "", errors.New(ErrTimestampSourceRequired)
39+
return nil, errors.New(ErrTimestampSourceRequired)
3340
}
3441
if cfg.Format == "" {
35-
return "", errors.New(ErrTimestampFormatRequired)
42+
return nil, errors.New(ErrTimestampFormatRequired)
3643
}
3744
return convertDateLayout(cfg.Format), nil
3845

@@ -45,22 +52,22 @@ func newTimestampStage(logger log.Logger, config interface{}) (*timestampStage,
4552
if err != nil {
4653
return nil, err
4754
}
48-
format, err := validateTimestampConfig(cfg)
55+
parser, err := validateTimestampConfig(cfg)
4956
if err != nil {
5057
return nil, err
5158
}
5259
return &timestampStage{
5360
cfgs: cfg,
5461
logger: logger,
55-
format: format,
62+
parser: parser,
5663
}, nil
5764
}
5865

5966
// timestampStage will set the timestamp using extracted data
6067
type timestampStage struct {
6168
cfgs *TimestampConfig
6269
logger log.Logger
63-
format string
70+
parser parser
6471
}
6572

6673
// Process implements Stage
@@ -73,7 +80,8 @@ func (ts *timestampStage) Process(labels model.LabelSet, extracted map[string]in
7380
if err != nil {
7481
level.Debug(ts.logger).Log("msg", "failed to convert extracted time to string", "err", err, "type", reflect.TypeOf(v).String())
7582
}
76-
parsedTs, err := time.Parse(ts.format, s)
83+
84+
parsedTs, err := ts.parser(s)
7785
if err != nil {
7886
level.Debug(ts.logger).Log("msg", "failed to parse time", "err", err, "format", ts.cfgs.Format, "value", s)
7987
} else {

pkg/logentry/stages/timestamp_test.go

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ func TestTimestampPipeline(t *testing.T) {
4545

4646
func TestTimestampValidation(t *testing.T) {
4747
tests := map[string]struct {
48-
config *TimestampConfig
49-
err error
50-
expectedFormat string
48+
config *TimestampConfig
49+
err error
50+
testString string
51+
expectedTime time.Time
5152
}{
5253
"missing config": {
5354
config: nil,
@@ -68,23 +69,34 @@ func TestTimestampValidation(t *testing.T) {
6869
Source: "source1",
6970
Format: time.RFC3339,
7071
},
71-
err: nil,
72-
expectedFormat: time.RFC3339,
72+
err: nil,
73+
testString: "2012-11-01T22:08:41-04:00",
74+
expectedTime: time.Date(2012, 11, 01, 22, 8, 41, 0, time.FixedZone("", -4*60*60)),
7375
},
7476
"custom format": {
7577
config: &TimestampConfig{
7678
Source: "source1",
77-
Format: "2006-01-23",
79+
Format: "2006-01-02",
7880
},
79-
err: nil,
80-
expectedFormat: "2006-01-23",
81+
err: nil,
82+
testString: "2009-01-01",
83+
expectedTime: time.Date(2009, 01, 01, 00, 00, 00, 0, time.UTC),
84+
},
85+
"unix_ms": {
86+
config: &TimestampConfig{
87+
Source: "source1",
88+
Format: "UnixMs",
89+
},
90+
err: nil,
91+
testString: "1562708916919",
92+
expectedTime: time.Date(2019, 7, 9, 21, 48, 36, 919*1000000, time.UTC),
8193
},
8294
}
8395
for name, test := range tests {
8496
test := test
8597
t.Run(name, func(t *testing.T) {
8698
t.Parallel()
87-
format, err := validateTimestampConfig(test.config)
99+
parser, err := validateTimestampConfig(test.config)
88100
if (err != nil) != (test.err != nil) {
89101
t.Errorf("validateOutputConfig() expected error = %v, actual error = %v", test.err, err)
90102
return
@@ -93,8 +105,13 @@ func TestTimestampValidation(t *testing.T) {
93105
t.Errorf("validateOutputConfig() expected error = %v, actual error = %v", test.err, err)
94106
return
95107
}
96-
if test.expectedFormat != "" {
97-
assert.Equal(t, test.expectedFormat, format)
108+
if test.testString != "" {
109+
ts, err := parser(test.testString)
110+
if err != nil {
111+
t.Errorf("validateOutputConfig() unexpected error parsing test time: %v", err)
112+
return
113+
}
114+
assert.Equal(t, test.expectedTime.UnixNano(), ts.UnixNano())
98115
}
99116
})
100117
}
@@ -117,6 +134,39 @@ func TestTimestampStage_Process(t *testing.T) {
117134
},
118135
time.Date(2106, 01, 02, 23, 04, 05, 0, time.FixedZone("", -4*60*60)),
119136
},
137+
"unix success": {
138+
TimestampConfig{
139+
Source: "ts",
140+
Format: "Unix",
141+
},
142+
map[string]interface{}{
143+
"somethigelse": "notimportant",
144+
"ts": "1562708916",
145+
},
146+
time.Date(2019, 7, 9, 21, 48, 36, 0, time.UTC),
147+
},
148+
"unix millisecond success": {
149+
TimestampConfig{
150+
Source: "ts",
151+
Format: "UnixMs",
152+
},
153+
map[string]interface{}{
154+
"somethigelse": "notimportant",
155+
"ts": "1562708916414",
156+
},
157+
time.Date(2019, 7, 9, 21, 48, 36, 414*1000000, time.UTC),
158+
},
159+
"unix nano success": {
160+
TimestampConfig{
161+
Source: "ts",
162+
Format: "UnixNs",
163+
},
164+
map[string]interface{}{
165+
"somethigelse": "notimportant",
166+
"ts": "1562708916000000123",
167+
},
168+
time.Date(2019, 7, 9, 21, 48, 36, 123, time.UTC),
169+
},
120170
}
121171
for name, test := range tests {
122172
test := test
@@ -129,7 +179,7 @@ func TestTimestampStage_Process(t *testing.T) {
129179
ts := time.Now()
130180
lbls := model.LabelSet{}
131181
st.Process(lbls, test.extracted, &ts, nil)
132-
assert.Equal(t, test.expected, ts)
182+
assert.Equal(t, test.expected.UnixNano(), ts.UnixNano())
133183

134184
})
135185
}

pkg/logentry/stages/util.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,76 @@ import (
77
)
88

99
// convertDateLayout converts pre-defined date format layout into date format
10-
func convertDateLayout(predef string) string {
10+
func convertDateLayout(predef string) parser {
1111
switch predef {
1212
case "ANSIC":
13-
return time.ANSIC
13+
return func(t string) (time.Time, error) {
14+
return time.Parse(time.ANSIC, t)
15+
}
1416
case "UnixDate":
15-
return time.UnixDate
17+
return func(t string) (time.Time, error) {
18+
return time.Parse(time.UnixDate, t)
19+
}
1620
case "RubyDate":
17-
return time.RubyDate
21+
return func(t string) (time.Time, error) {
22+
return time.Parse(time.RubyDate, t)
23+
}
1824
case "RFC822":
19-
return time.RFC822
25+
return func(t string) (time.Time, error) {
26+
return time.Parse(time.RFC822, t)
27+
}
2028
case "RFC822Z":
21-
return time.RFC822Z
29+
return func(t string) (time.Time, error) {
30+
return time.Parse(time.RFC822Z, t)
31+
}
2232
case "RFC850":
23-
return time.RFC850
33+
return func(t string) (time.Time, error) {
34+
return time.Parse(time.RFC850, t)
35+
}
2436
case "RFC1123":
25-
return time.RFC1123
37+
return func(t string) (time.Time, error) {
38+
return time.Parse(time.RFC1123, t)
39+
}
2640
case "RFC1123Z":
27-
return time.RFC1123Z
41+
return func(t string) (time.Time, error) {
42+
return time.Parse(time.RFC1123Z, t)
43+
}
2844
case "RFC3339":
29-
return time.RFC3339
45+
return func(t string) (time.Time, error) {
46+
return time.Parse(time.RFC3339, t)
47+
}
3048
case "RFC3339Nano":
31-
return time.RFC3339Nano
49+
return func(t string) (time.Time, error) {
50+
return time.Parse(time.RFC3339Nano, t)
51+
}
52+
case "Unix":
53+
return func(t string) (time.Time, error) {
54+
i, err := strconv.ParseInt(t, 10, 64)
55+
if err != nil {
56+
return time.Time{}, err
57+
}
58+
return time.Unix(i, 0), nil
59+
}
60+
case "UnixMs":
61+
return func(t string) (time.Time, error) {
62+
i, err := strconv.ParseInt(t, 10, 64)
63+
if err != nil {
64+
return time.Time{}, err
65+
}
66+
return time.Unix(0, i*int64(time.Millisecond)), nil
67+
}
68+
case "UnixNs":
69+
return func(t string) (time.Time, error) {
70+
i, err := strconv.ParseInt(t, 10, 64)
71+
if err != nil {
72+
return time.Time{}, err
73+
}
74+
return time.Unix(0, i), nil
75+
}
3276
default:
33-
return predef
77+
return func(t string) (time.Time, error) {
78+
return time.Parse(predef, t)
79+
}
3480
}
3581
}
3682

0 commit comments

Comments
 (0)