Skip to content

Commit

Permalink
Add NumbersAsStrings to EncoderConfig
Browse files Browse the repository at this point in the history
This partially fixes uber-go#884.

This implementation is limited to top level fields, like:

    Int64("int64", 123)
    Float64("float64", 456)

Or sugared:

    With("int64", 123, "float64", 456)

It does NOT work with reflected fields like:

    Reflect("array", []int{1, 2, 3})

As for reflected fields we use go's stdlib json encoder directly, which
doesn't provide such feature.
  • Loading branch information
fishy committed Nov 24, 2020
1 parent 5b4722d commit 7eff92d
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 0 deletions.
7 changes: 7 additions & 0 deletions zapcore/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,13 @@ type EncoderConfig struct {
// Configures the field separator used by the console encoder. Defaults
// to tab.
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
// If set to true, all numbers will be wrapped by double quotation marks.
// This is usally useful in JSON logging to make sure that you don't lose
// precision on number fields during log ingester's json decoding process.
// NOTE: currently this only works for top level fields, e.g.
// Int64("key", 123) or sugared With("key", 456). It doesn't work with
// reflected fields like Reflect("key", []int{1, 2, 3}).
NumbersAsStrings bool `json:"numbersAsStrings" yaml:"numbersAsStrings"`
}

// ObjectEncoder is a strongly-typed, encoding-agnostic interface for adding a
Expand Down
18 changes: 18 additions & 0 deletions zapcore/json_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,13 @@ func (enc *jsonEncoder) AppendDuration(val time.Duration) {

func (enc *jsonEncoder) AppendInt64(val int64) {
enc.addElementSeparator()
if enc.EncoderConfig.NumbersAsStrings {
enc.buf.AppendByte('"')
}
enc.buf.AppendInt(val)
if enc.EncoderConfig.NumbersAsStrings {
enc.buf.AppendByte('"')
}
}

func (enc *jsonEncoder) AppendReflected(val interface{}) error {
Expand Down Expand Up @@ -289,7 +295,13 @@ func (enc *jsonEncoder) AppendTime(val time.Time) {

func (enc *jsonEncoder) AppendUint64(val uint64) {
enc.addElementSeparator()
if enc.EncoderConfig.NumbersAsStrings {
enc.buf.AppendByte('"')
}
enc.buf.AppendUint(val)
if enc.EncoderConfig.NumbersAsStrings {
enc.buf.AppendByte('"')
}
}

func (enc *jsonEncoder) AddComplex64(k string, v complex64) { enc.AddComplex128(k, complex128(v)) }
Expand Down Expand Up @@ -454,7 +466,13 @@ func (enc *jsonEncoder) appendFloat(val float64, bitSize int) {
case math.IsInf(val, -1):
enc.buf.AppendString(`"-Inf"`)
default:
if enc.EncoderConfig.NumbersAsStrings {
enc.buf.AppendByte('"')
}
enc.buf.AppendFloat(val, bitSize)
if enc.EncoderConfig.NumbersAsStrings {
enc.buf.AppendByte('"')
}
}
}

Expand Down
97 changes: 97 additions & 0 deletions zapcore/json_encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,100 @@ func TestJSONEmptyConfig(t *testing.T) {
})
}
}

func TestJSONEncodeNumbersAsStrings(t *testing.T) {
type bar struct {
Key string `json:"key"`
Val float64 `json:"val"`
}

type foo struct {
A string `json:"aee"`
B int `json:"bee"`
C float64 `json:"cee"`
D []bar `json:"dee"`
}

tests := []struct {
desc string
expected string
ent zapcore.Entry
fields []zapcore.Field
}{
{
desc: "info entry with some fields",
expected: `{
"L": "info",
"T": "2018-06-19T16:33:42.000Z",
"N": "bob",
"M": "lob law",
"so": "passes",
"answer": "42",
"common_pie": "3.14",
"null_value": null,
"array_with_null_elements": [{}, null, null, 2],
"such": {
"aee": "lol",
"bee": 123,
"cee": 0.9999,
"dee": [
{"key": "pi", "val": 3.141592653589793},
{"key": "tau", "val": 6.283185307179586}
]
}
}`,
ent: zapcore.Entry{
Level: zapcore.InfoLevel,
Time: time.Date(2018, 6, 19, 16, 33, 42, 99, time.UTC),
LoggerName: "bob",
Message: "lob law",
},
fields: []zapcore.Field{
zap.String("so", "passes"),
zap.Int("answer", 42),
zap.Float64("common_pie", 3.14),
// Cover special-cased handling of nil in AddReflect() and
// AppendReflect(). Note that for the latter, we explicitly test
// correct results for both the nil static interface{} value
// (`nil`), as well as the non-nil interface value with a
// dynamic type and nil value (`(*struct{})(nil)`).
zap.Reflect("null_value", nil),
zap.Reflect("array_with_null_elements", []interface{}{&struct{}{}, nil, (*struct{})(nil), 2}),
zap.Reflect("such", foo{
A: "lol",
B: 123,
C: 0.9999,
D: []bar{
{"pi", 3.141592653589793},
{"tau", 6.283185307179586},
},
}),
},
},
}

enc := zapcore.NewJSONEncoder(zapcore.EncoderConfig{
MessageKey: "M",
LevelKey: "L",
TimeKey: "T",
NameKey: "N",
CallerKey: "C",
FunctionKey: "F",
StacktraceKey: "S",
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
NumbersAsStrings: true,
})

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
buf, err := enc.EncodeEntry(tt.ent, tt.fields)
if assert.NoError(t, err, "Unexpected JSON encoding error.") {
assert.JSONEq(t, tt.expected, buf.String(), "Incorrect encoded JSON entry.")
}
buf.Free()
})
}
}

0 comments on commit 7eff92d

Please sign in to comment.