From 586ed30305b37ec71138b2415efe7eb7e2e3a80b Mon Sep 17 00:00:00 2001 From: Shinichi TAMURA Date: Wed, 8 Jul 2020 02:46:02 +0900 Subject: [PATCH] Added zapcore.TimeEncoderOfLayout (#629) I added `zapcore.TimeEncoderOfFormat` to make it easy to build `TimeEncoder` of custom format. You can use it in code: ```go config := zap.NewDevelopmentConfig() config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("06/01/02 03:04pm") ``` or, with JSON: ```json { "encoderConfig": { "timeEncoder": { "layout": "06/01/02 03:04pm" } } } ``` or, with YAML: ```yaml encoderConfig: timeEncoder: layout: '06/01/02 03:04pm' ``` --- go.mod | 1 + zapcore/encoder.go | 38 +++++++++++++++++++++++ zapcore/encoder_test.go | 68 ++++++++++++++++++++++++++++++++--------- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 118abda15..6ef4db70e 100644 --- a/go.mod +++ b/go.mod @@ -8,5 +8,6 @@ require ( go.uber.org/atomic v1.6.0 go.uber.org/multierr v1.5.0 golang.org/x/lint v0.0.0-20190930215403-16217165b5de + gopkg.in/yaml.v2 v2.2.2 honnef.co/go/tools v0.0.1-2019.2.3 ) diff --git a/zapcore/encoder.go b/zapcore/encoder.go index 6c78f7e49..db97aa332 100644 --- a/zapcore/encoder.go +++ b/zapcore/encoder.go @@ -21,6 +21,7 @@ package zapcore import ( + "encoding/json" "time" "go.uber.org/zap/buffer" @@ -151,6 +152,14 @@ func RFC3339NanoTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) { encodeTimeLayout(t, time.RFC3339Nano, enc) } +// TimeEncoderOfLayout returns TimeEncoder which serializes a time.Time using +// given layout. +func TimeEncoderOfLayout(layout string) TimeEncoder { + return func(t time.Time, enc PrimitiveArrayEncoder) { + encodeTimeLayout(t, layout, enc) + } +} + // UnmarshalText unmarshals text to a TimeEncoder. // "rfc3339nano" and "RFC3339Nano" are unmarshaled to RFC3339NanoTimeEncoder. // "rfc3339" and "RFC3339" are unmarshaled to RFC3339TimeEncoder. @@ -176,6 +185,35 @@ func (e *TimeEncoder) UnmarshalText(text []byte) error { return nil } +// UnmarshalYAML unmarshals YAML to a TimeEncoder. +// If value is an object with a "layout" field, it will be unmarshaled to TimeEncoder with given layout. +// timeEncoder: +// layout: 06/01/02 03:04pm +// If value is string, it uses UnmarshalText. +// timeEncoder: iso8601 +func (e *TimeEncoder) UnmarshalYAML(unmarshal func(interface{}) error) error { + var o struct { + Layout string `json:"layout" yaml:"layout"` + } + if err := unmarshal(&o); err == nil { + *e = TimeEncoderOfLayout(o.Layout) + return nil + } + + var s string + if err := unmarshal(&s); err != nil { + return err + } + return e.UnmarshalText([]byte(s)) +} + +// UnmarshalJSON unmarshals JSON to a TimeEncoder as same way UnmarshalYAML does. +func (e *TimeEncoder) UnmarshalJSON(data []byte) error { + return e.UnmarshalYAML(func(v interface{}) error { + return json.Unmarshal(data, v) + }) +} + // A DurationEncoder serializes a time.Duration to a primitive type. type DurationEncoder func(time.Duration, PrimitiveArrayEncoder) diff --git a/zapcore/encoder_test.go b/zapcore/encoder_test.go index 54556b8d4..206a7bd30 100644 --- a/zapcore/encoder_test.go +++ b/zapcore/encoder_test.go @@ -21,12 +21,14 @@ package zapcore_test import ( + "encoding/json" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" . "go.uber.org/zap/zapcore" ) @@ -523,29 +525,65 @@ func TestLevelEncoders(t *testing.T) { func TestTimeEncoders(t *testing.T) { moment := time.Unix(100, 50005000).UTC() tests := []struct { - name string + yamlDoc string + expected interface{} // output of serializing moment + }{ + {"timeEncoder: iso8601", "1970-01-01T00:01:40.050Z"}, + {"timeEncoder: ISO8601", "1970-01-01T00:01:40.050Z"}, + {"timeEncoder: millis", 100050.005}, + {"timeEncoder: nanos", int64(100050005000)}, + {"timeEncoder: {layout: 06/01/02 03:04pm}", "70/01/01 12:01am"}, + {"timeEncoder: ''", 100.050005}, + {"timeEncoder: something-random", 100.050005}, + {"timeEncoder: rfc3339", "1970-01-01T00:01:40Z"}, + {"timeEncoder: RFC3339", "1970-01-01T00:01:40Z"}, + {"timeEncoder: rfc3339nano", "1970-01-01T00:01:40.050005Z"}, + {"timeEncoder: RFC3339Nano", "1970-01-01T00:01:40.050005Z"}, + } + + for _, tt := range tests { + cfg := EncoderConfig{} + require.NoError(t, yaml.Unmarshal([]byte(tt.yamlDoc), &cfg), "Unexpected error unmarshaling %q.", tt.yamlDoc) + require.NotNil(t, cfg.EncodeTime, "Unmashalled timeEncoder is nil for %q.", tt.yamlDoc) + assertAppended( + t, + tt.expected, + func(arr ArrayEncoder) { cfg.EncodeTime(moment, arr) }, + "Unexpected output serializing %v with %q.", moment, tt.yamlDoc, + ) + } +} + +func TestTimeEncodersWrongYAML(t *testing.T) { + tests := []string{ + "timeEncoder: [1, 2, 3]", // wrong type + "timeEncoder: {foo:bar", // broken yaml + } + for _, tt := range tests { + cfg := EncoderConfig{} + assert.Error(t, yaml.Unmarshal([]byte(tt), &cfg), "Expected unmarshaling %q to become error, but not.", tt) + } +} + +func TestTimeEncodersParseFromJSON(t *testing.T) { + moment := time.Unix(100, 50005000).UTC() + tests := []struct { + jsonDoc string expected interface{} // output of serializing moment }{ - {"iso8601", "1970-01-01T00:01:40.050Z"}, - {"ISO8601", "1970-01-01T00:01:40.050Z"}, - {"millis", 100050.005}, - {"nanos", int64(100050005000)}, - {"", 100.050005}, - {"something-random", 100.050005}, - {"rfc3339", "1970-01-01T00:01:40Z"}, - {"RFC3339", "1970-01-01T00:01:40Z"}, - {"rfc3339nano", "1970-01-01T00:01:40.050005Z"}, - {"RFC3339Nano", "1970-01-01T00:01:40.050005Z"}, + {`{"timeEncoder": "iso8601"}`, "1970-01-01T00:01:40.050Z"}, + {`{"timeEncoder": {"layout": "06/01/02 03:04pm"}}`, "70/01/01 12:01am"}, } for _, tt := range tests { - var te TimeEncoder - require.NoError(t, te.UnmarshalText([]byte(tt.name)), "Unexpected error unmarshaling %q.", tt.name) + cfg := EncoderConfig{} + require.NoError(t, json.Unmarshal([]byte(tt.jsonDoc), &cfg), "Unexpected error unmarshaling %q.", tt.jsonDoc) + require.NotNil(t, cfg.EncodeTime, "Unmashalled timeEncoder is nil for %q.", tt.jsonDoc) assertAppended( t, tt.expected, - func(arr ArrayEncoder) { te(moment, arr) }, - "Unexpected output serializing %v with %q.", moment, tt.name, + func(arr ArrayEncoder) { cfg.EncodeTime(moment, arr) }, + "Unexpected output serializing %v with %q.", moment, tt.jsonDoc, ) } }