diff --git a/libbeat/common/dtfmt/builder.go b/libbeat/common/dtfmt/builder.go
index 935a24784076..458878ffcc98 100644
--- a/libbeat/common/dtfmt/builder.go
+++ b/libbeat/common/dtfmt/builder.go
@@ -207,7 +207,9 @@ func (b *builder) monthOfYearShortText() {
b.appendShortText(ftMonthOfYear)
}
-// TODO: add timezone support
+func (b *builder) timeZoneOffsetText() {
+ b.appendText(ftTimeZoneOffset)
+}
func (b *builder) appendRune(r rune) {
b.add(runeLiteral{r})
diff --git a/libbeat/common/dtfmt/ctx.go b/libbeat/common/dtfmt/ctx.go
index 7b8314f0b192..faa405e050a1 100644
--- a/libbeat/common/dtfmt/ctx.go
+++ b/libbeat/common/dtfmt/ctx.go
@@ -33,16 +33,19 @@ type ctx struct {
hour, min, sec int
millis int
+ tzOffset int
+
buf []byte
}
type ctxConfig struct {
- date bool
- clock bool
- weekday bool
- yearday bool
- millis bool
- iso bool
+ date bool
+ clock bool
+ weekday bool
+ yearday bool
+ millis bool
+ iso bool
+ tzOffset bool
}
func (c *ctx) initTime(config *ctxConfig, t time.Time) {
@@ -67,6 +70,10 @@ func (c *ctx) initTime(config *ctxConfig, t time.Time) {
if config.weekday {
c.weekday = t.Weekday()
}
+
+ if config.tzOffset {
+ _, c.tzOffset = t.Zone()
+ }
}
func (c *ctxConfig) enableDate() {
@@ -93,6 +100,10 @@ func (c *ctxConfig) enableISO() {
c.iso = true
}
+func (c *ctxConfig) enableTimeZoneOffset() {
+ c.tzOffset = true
+}
+
func isLeap(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
diff --git a/libbeat/common/dtfmt/dtfmt_test.go b/libbeat/common/dtfmt/dtfmt_test.go
index 262fe0fa9f07..61af8ab17cc4 100644
--- a/libbeat/common/dtfmt/dtfmt_test.go
+++ b/libbeat/common/dtfmt/dtfmt_test.go
@@ -98,6 +98,11 @@ func TestFormat(t *testing.T) {
{mkDateTime(2017, 1, 2, 4, 6, 7, 123),
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"2017-01-02T04:06:07.123Z"},
+
+ // beats timestamp
+ {mkDateTimeWithLocation(2017, 1, 2, 4, 6, 7, 123, time.FixedZone("PST", -8*60*60)),
+ "yyyy-MM-dd'T'HH:mm:ss.SSSz",
+ "2017-01-02T04:06:07.123-08:00"},
}
for i, test := range tests {
@@ -123,5 +128,9 @@ func mkTime(h, m, s, S int) time.Time {
}
func mkDateTime(y, M, d, h, m, s, S int) time.Time {
- return time.Date(y, time.Month(M), d, h, m, s, S*1000000, time.UTC)
+ return mkDateTimeWithLocation(y, M, d, h, m, s, S, time.UTC)
+}
+
+func mkDateTimeWithLocation(y, M, d, h, m, s, S int, l *time.Location) time.Time {
+ return time.Date(y, time.Month(M), d, h, m, s, S*1000000, l)
}
diff --git a/libbeat/common/dtfmt/elems.go b/libbeat/common/dtfmt/elems.go
index 4ffb6cd9083d..a86f67a7fc5d 100644
--- a/libbeat/common/dtfmt/elems.go
+++ b/libbeat/common/dtfmt/elems.go
@@ -142,6 +142,8 @@ func (f textField) requires(c *ctxConfig) error {
c.enableDate()
case ftDayOfWeek:
c.enableWeekday()
+ case ftTimeZoneOffset:
+ c.enableTimeZoneOffset()
default:
return fmt.Errorf("time field %v not supported by text", f.ft)
}
@@ -162,6 +164,8 @@ func (f textField) estimateSize() int {
return 6
}
return 9 // max(month) = len(September)
+ case ftTimeZoneOffset:
+ return 6
default:
return 0
}
diff --git a/libbeat/common/dtfmt/fields.go b/libbeat/common/dtfmt/fields.go
index 29fc2a1dc541..08b0d1c6dc10 100644
--- a/libbeat/common/dtfmt/fields.go
+++ b/libbeat/common/dtfmt/fields.go
@@ -43,6 +43,7 @@ const (
ftSecondOfMinute
ftMillisOfDay
ftMillisOfSecond
+ ftTimeZoneOffset
)
func getIntField(ft fieldType, ctx *ctx, t time.Time) (int, error) {
@@ -125,11 +126,34 @@ func getTextField(ft fieldType, ctx *ctx, t time.Time) (string, error) {
return ctx.weekday.String(), nil
case ftMonthOfYear:
return ctx.month.String(), nil
+ case ftTimeZoneOffset:
+ return tzOffsetString(ctx)
default:
return "", errors.New("no text field")
}
}
+func tzOffsetString(ctx *ctx) (string, error) {
+ buf := make([]byte, 6)
+
+ tzOffsetMinutes := ctx.tzOffset / 60 // convert to minutes
+ if tzOffsetMinutes >= 0 {
+ buf[0] = '+'
+ } else {
+ buf[0] = '-'
+ tzOffsetMinutes = -tzOffsetMinutes
+ }
+
+ tzOffsetHours := tzOffsetMinutes / 60
+ tzOffsetMinutes = tzOffsetMinutes % 60
+ buf[1] = byte(tzOffsetHours/10) + '0'
+ buf[2] = byte(tzOffsetHours%10) + '0'
+ buf[3] = ':'
+ buf[4] = byte(tzOffsetMinutes/10) + '0'
+ buf[5] = byte(tzOffsetMinutes%10) + '0'
+ return string(buf), nil
+}
+
func getTextFieldShort(ft fieldType, ctx *ctx, t time.Time) (string, error) {
switch ft {
case ftHalfdayOfDay:
diff --git a/libbeat/common/dtfmt/fmt.go b/libbeat/common/dtfmt/fmt.go
index d4ae87a19663..acc4989acd9a 100644
--- a/libbeat/common/dtfmt/fmt.go
+++ b/libbeat/common/dtfmt/fmt.go
@@ -211,6 +211,9 @@ func parsePatternTo(b *builder, pattern string) error {
case 'S': // fraction of second
b.millisOfSecond(tokLen)
+ case 'z': // timezone offset
+ b.timeZoneOffsetText()
+
case '\'': // literal
if tokLen == 1 {
b.appendRune(rune(tokText[0]))
diff --git a/libbeat/outputs/codec/common.go b/libbeat/outputs/codec/common.go
index c3e967b050cc..d3df85dc5346 100644
--- a/libbeat/outputs/codec/common.go
+++ b/libbeat/outputs/codec/common.go
@@ -25,15 +25,34 @@ import (
"github.com/elastic/go-structform"
)
+// MakeTimestampEncoder creates encoder function that formats time
+// into RFC3339 representation with UTC timezone in the output.
func MakeTimestampEncoder() func(*time.Time, structform.ExtVisitor) error {
- formatter, err := dtfmt.NewFormatter("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
+ return MakeUTCOrLocalTimestampEncoder(false)
+}
+
+// MakeUTCOrLocalTimestampEncoder creates encoder function that formats time into RFC3339 representation
+// with UTC or local timezone in the output (based on localTime boolean parameter).
+func MakeUTCOrLocalTimestampEncoder(localTime bool) func(*time.Time, structform.ExtVisitor) error {
+ var dtPattern string
+ if localTime {
+ dtPattern = "yyyy-MM-dd'T'HH:mm:ss.SSSz"
+ } else {
+ dtPattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
+ }
+
+ formatter, err := dtfmt.NewFormatter(dtPattern)
if err != nil {
panic(err)
}
buf := make([]byte, 0, formatter.EstimateSize())
return func(t *time.Time, v structform.ExtVisitor) error {
- tmp, err := formatter.AppendTo(buf, (*t).UTC())
+ outTime := *t
+ if !localTime {
+ outTime = outTime.UTC()
+ }
+ tmp, err := formatter.AppendTo(buf, outTime)
if err != nil {
return err
}
@@ -43,6 +62,8 @@ func MakeTimestampEncoder() func(*time.Time, structform.ExtVisitor) error {
}
}
+// MakeBCTimestampEncoder creates encoder function that formats beats common time
+// into RFC3339 representation with UTC timezone in the output.
func MakeBCTimestampEncoder() func(*common.Time, structform.ExtVisitor) error {
enc := MakeTimestampEncoder()
return func(t *common.Time, v structform.ExtVisitor) error {
diff --git a/libbeat/outputs/codec/json/json.go b/libbeat/outputs/codec/json/json.go
index 8f1a1540708b..45b875b3ba9f 100644
--- a/libbeat/outputs/codec/json/json.go
+++ b/libbeat/outputs/codec/json/json.go
@@ -41,11 +41,13 @@ type Encoder struct {
type Config struct {
Pretty bool
EscapeHTML bool
+ LocalTime bool
}
var defaultConfig = Config{
Pretty: false,
EscapeHTML: false,
+ LocalTime: false,
}
func init() {
@@ -77,7 +79,7 @@ func (e *Encoder) reset() {
// create new encoder with custom time.Time encoding
e.folder, err = gotype.NewIterator(visitor,
gotype.Folders(
- codec.MakeTimestampEncoder(),
+ codec.MakeUTCOrLocalTimestampEncoder(e.config.LocalTime),
codec.MakeBCTimestampEncoder(),
),
)
diff --git a/libbeat/outputs/codec/json/json_bench_test.go b/libbeat/outputs/codec/json/json_bench_test.go
new file mode 100644
index 000000000000..8d60362b3d5e
--- /dev/null
+++ b/libbeat/outputs/codec/json/json_bench_test.go
@@ -0,0 +1,60 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package json
+
+import (
+ "testing"
+ "time"
+
+ "github.com/elastic/beats/libbeat/beat"
+ "github.com/elastic/beats/libbeat/common"
+)
+
+var result []byte
+
+func BenchmarkUTCTime(b *testing.B) {
+ var r []byte
+ codec := New("1.2.3", Config{})
+ fields := common.MapStr{"msg": "message"}
+ var t time.Time
+ var d time.Duration = 1000000000
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ t = t.Add(d)
+ r, _ = codec.Encode("test", &beat.Event{Fields: fields, Timestamp: t})
+ }
+ result = r
+}
+
+func BenchmarkLocalTime(b *testing.B) {
+ var r []byte
+ codec := New("1.2.3", Config{LocalTime: true})
+ fields := common.MapStr{"msg": "message"}
+ var t time.Time
+ var d time.Duration = 1000000000
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ t = t.Add(d)
+ r, _ = codec.Encode("test", &beat.Event{Fields: fields, Timestamp: t})
+ }
+ result = r
+}
diff --git a/libbeat/outputs/codec/json/json_test.go b/libbeat/outputs/codec/json/json_test.go
index fb858c34e3a7..dc01e397e0bd 100644
--- a/libbeat/outputs/codec/json/json_test.go
+++ b/libbeat/outputs/codec/json/json_test.go
@@ -19,6 +19,7 @@ package json
import (
"testing"
+ "time"
"github.com/elastic/beats/libbeat/beat"
"github.com/elastic/beats/libbeat/common"
@@ -27,6 +28,7 @@ import (
func TestJsonCodec(t *testing.T) {
type testCase struct {
config Config
+ ts time.Time
in common.MapStr
expected string
}
@@ -60,14 +62,25 @@ func TestJsonCodec(t *testing.T) {
in: common.MapStr{"msg": "world"},
expected: `{"@timestamp":"0001-01-01T00:00:00.000Z","@metadata":{"beat":"test","type":"_doc","version":"1.2.3"},"msg":"world"}`,
},
+ "UTC timezone offset": testCase{
+ config: Config{LocalTime: true},
+ in: common.MapStr{"msg": "message"},
+ expected: `{"@timestamp":"0001-01-01T00:00:00.000+00:00","@metadata":{"beat":"test","type":"_doc","version":"1.2.3"},"msg":"message"}`,
+ },
+ "PST timezone offset": testCase{
+ config: Config{LocalTime: true},
+ ts: time.Time{}.In(time.FixedZone("PST", -8*60*60)),
+ in: common.MapStr{"msg": "message"},
+ expected: `{"@timestamp":"0000-12-31T16:00:00.000-08:00","@metadata":{"beat":"test","type":"_doc","version":"1.2.3"},"msg":"message"}`,
+ },
}
for name, test := range cases {
- cfg, fields, expected := test.config, test.in, test.expected
+ cfg, ts, fields, expected := test.config, test.ts, test.in, test.expected
t.Run(name, func(t *testing.T) {
codec := New("1.2.3", cfg)
- actual, err := codec.Encode("test", &beat.Event{Fields: fields})
+ actual, err := codec.Encode("test", &beat.Event{Fields: fields, Timestamp: ts})
if err != nil {
t.Errorf("Error during event write %v", err)