-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial support for arrays #239
Conversation
// and allocation-heavy. | ||
AddReflected(key string, value interface{}) error | ||
AddString(key, value string) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved most of this here from object_encoder.go
, since it seemed silly to have two interface-only files for encoders.
|
||
// Arrays wraps a slice of ArrayMarshalers so that it satisfies the | ||
// ArrayMarshaler interface. See the Array function for a usage example. | ||
func Arrays(as []zapcore.ArrayMarshaler) zapcore.ArrayMarshaler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think Arrays
and Objects
is going to be that useful to users.
If someone is implementing the ArrayMarshaler
interface, then they probably care about performance, and wouldn't want to build up a separate new slice of []zapcore.ArrayMarshaler
.
What use case are you expecting for Arrays
and Objects
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arrays
is, IMO, useful precisely because most users won't be implementing ArrayMarshaler
themselves - they'll be using the umpteen wrapper types we're providing, like Bools
. Since we're already going down a road where we support mixed-type arrays, I'd like to take that to its logical conclusion.
I'll email you about Objects
- relies on some internal examples.
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: import ordering
func (as arrayMarshalers) MarshalLogArray(arr zapcore.ArrayEncoder) error { | ||
var errs *multierror.Error | ||
for i := range as { | ||
if as[i] == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to keep arrayMarshalers
around, we should consider whether ignoring nil
is the right behaviour. Should it just be an error? I think I'm learning towards remove Arrays
and Objects
and keep things minimal. It's easy enough for others to implement and add later.
If we really want to keep this type, can we add unit tests for the == nil
branch here and in Objects
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I can add some tests. I'm going to have to come through lots and lots of zapcore
and improve test coverage over the next week.
// a single error. | ||
package multierror | ||
|
||
import "go.uber.org/zap/buffers" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this package not internal on purpose? I really think this should be internal. Otherwise one bad user of buffers
will cause all sorts of weird data races / corrupt log messages. I really don't think we want any other library to be able to put buffers into our pool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, makes sense. My bad.
return e | ||
} | ||
if e == nil { | ||
e = &Error{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a strange pattern, you have an Append
that works on a pointer, sometimes modifies the value inside, sometimes returns a new pointer. Given that, why do you need this method on the pointer? Why not always work on the value? Passing around a []error
by value is not expensive, makes the semantics cleaner, and reduces the need for *Error
That'll probably also avoid an allocation when there is an error
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One slightly odd test that I think is worth having -- you really don't want Error
to implement the error
interface, since that could cause odd issues. So we could have a test like:
var v interface{} = Error{}
_, ok := v.(error)
require.False(t, ok)
@@ -136,6 +199,57 @@ func TestJSONEncoderFields(t *testing.T) { | |||
} | |||
} | |||
|
|||
func TestJSONEncoderArrayTypes(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add unit tests for:
- empty array
- array with different types
{ | ||
"objects", | ||
Objects([]zapcore.ObjectMarshaler{ | ||
zapcore.ObjectMarshalerFunc(func(_ zapcore.ObjectEncoder) error { return nil }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here and below, since you don't use any args, you can just make this func(zapcore.ObjectEncoder) error { .. }
We'll need a multierror implementation in both zap and zapcore to support arrays. Clean up the existing implementation a bit and promote it to its own internal package.
Add basic support for logging arrays. To limit the scope of this commit, it introduces only arrays of ObjectMarshalers, arrays of arrays, and arrays of bools. It should, however, clarify how I intend to support arrays of other concrete types. Of note, I opted not to have `zap.Bools` return a `Field`. Instead, it returns a `zapcore.ArrayMarshaler`. This lets us build arrays of heterogenous arrays. For most users, this will be papered over by the upcoming `zap.Any(key string, val interface{}) zapcore.Field` function, which will basically be a huge type switch.
``` BenchmarkBoolsArrayMarshaler-4 1000000 1028 ns/op 1088 B/op 3 allocs/op BenchmarkBoolsReflect-4 500000 3293 ns/op 2280 B/op 8 allocs/op ```
fd8edcb
to
09d0045
Compare
09d0045
to
4b45c11
Compare
@@ -0,0 +1,71 @@ | |||
// Copyright (c) 2016 Uber Technologies, Inc. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, should all of these new files be 2017? When/how does that even matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that copyright applies to the creative work as a whole, which is the zap project; since that project started in 2016, all copyright starts there. (Imagine that Walt Disney updates a few of the line arcs in the original Mickey Mouse drawing. Those lines don't have a separate copyright date.)
Of course, I could easily be wrong.
// | ||
// Note that failing to use AsError will almost certainly lead to bugs with | ||
// non-nil interfaces containing nil concrete values. | ||
func (e Error) AsError() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting iteration on the MultiError pattern, so let's see if I've got the use case right:
func some() error {
var errs multierror.Error
// ...
errs = errs.Append(thisCanFailButDoesntStopUs())
// ...
return errs.AsError()
}
And so, the main value of the type Error struct
is those append ergonomics I think, since otherwise we could have:
func some() error {
var errs []error
// ...
if err := thisCanFailButDoesntStopUs(); err != nil {
errs = append(errs, err)
}
// ...
return multierror.AsError(errs)
}
To me, the latter is more immediately understandable since most go code has this pervasive if err ...
pattern; does micro perf even matter in these cases? is there a material difference anyhow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is indeed the use case. While I agree that error handling is often repetitive, I think we're often encouraged to program with error values. Where possible, I'd like to make things less repetitive. (I'm treating bufio.Reader
as my spirit animal here.)
@@ -90,7 +90,7 @@ func (enc *jsonEncoder) AddString(key, val string) { | |||
|
|||
func (enc *jsonEncoder) AddBool(key string, val bool) { | |||
enc.addKey(key) | |||
enc.bytes = strconv.AppendBool(enc.bytes, val) | |||
enc.AppendBool(val) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will probably be possible soon (desirability debatable) to have something like:
// KeyEncoder defines an encoding scheme for top-level log field names.
// TODO: maybe it needs other methods for start/end key context
type KeyEncoder interface {
AddKey(string key)
}
// GenericEncoder implements Encoder given a key and an array encoder.
type GenericEncoder struct {
keyEnc KeyEncoder
arrEnc Array Encoder
}
// TODO: text and json encoder then differ mostly (only?) along their KeyEncoder
// TODO: maybe KeyEncoder should be EntryEncoder: encompass all builtin "entry fields", plus extra field name encoding
Maybe this only works for text-ish encoders, and maybe that's okay; maybe it works for msgpack too, not sure...
@@ -63,8 +63,8 @@ func BenchmarkZapJSON(b *testing.B) { | |||
for pb.Next() { | |||
enc := newJSONEncoder(cfg) | |||
enc.AddString("str", "foo") | |||
enc.AddInt64("int", 1) | |||
enc.AddInt64("int64", 1) | |||
enc.AddInt64("int64-1", 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's up with this, it seems unrelated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, the key was just wrong. Noticed and fixed in passing.
|
||
wantTurducken := MapObjectEncoder{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+100
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🦃
* Add an internal multierror package We'll need a multierror implementation in both zap and zapcore to support arrays. Clean up the existing implementation a bit and promote it to its own internal package. * Add support for logging arrays Add basic support for logging arrays. To limit the scope of this commit, it introduces only arrays of ObjectMarshalers, arrays of arrays, and arrays of bools. It should, however, clarify how I intend to support arrays of other concrete types. Of note, I opted not to have `zap.Bools` return a `Field`. Instead, it returns a `zapcore.ArrayMarshaler`. This lets us build arrays of heterogenous arrays. For most users, this will be papered over by the upcoming `zap.Any(key string, val interface{}) zapcore.Field` function, which will basically be a huge type switch. * Simpler struct literals in tests * Update some comments * Add some simple benchmarks ``` BenchmarkBoolsArrayMarshaler-4 1000000 1028 ns/op 1088 B/op 3 allocs/op BenchmarkBoolsReflect-4 500000 3293 ns/op 2280 B/op 8 allocs/op ``` * Cleanups from CR
Add basic support for logging arrays. To limit the scope of this commit,
it introduces only arrays of ObjectMarshalers, arrays of arrays, and
arrays of bools. It should, however, clarify how I intend to support
arrays of other types.
Of note, I opted not to have
zap.Bools
return aField
. Instead, itreturns a
zapcore.ArrayMarshaler
. This lets us build arrays ofheterogenous arrays. For most users, this will be papered over by the
upcoming
zap.Any(key string, val interface{}) zapcore.Field
function,which will basically be a huge type switch.
(Along the way, I broke out our multi-error implementation into a separate
internal package.)
This starts to address #136, and is an alternative to the approach in #211.
From the small benchmarks included in this PR:
All allocations in the ArrayMarshaler benchmark come from allocating byte slices for encoders.