Skip to content

Commit

Permalink
Added zapcore.TimeEncoderOfLayout (#629)
Browse files Browse the repository at this point in the history
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'
```
  • Loading branch information
tmshn committed Jul 7, 2020
1 parent 39aa3a1 commit 275c926
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 15 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -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
)
38 changes: 38 additions & 0 deletions zapcore/encoder.go
Expand Up @@ -21,6 +21,7 @@
package zapcore

import (
"encoding/json"
"time"

"go.uber.org/zap/buffer"
Expand Down Expand Up @@ -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.
Expand All @@ -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)

Expand Down
68 changes: 53 additions & 15 deletions zapcore/encoder_test.go
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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,
)
}
}
Expand Down

0 comments on commit 275c926

Please sign in to comment.