Skip to content
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

Merged
merged 6 commits into from
Jan 12, 2017
Merged

Initial support for arrays #239

merged 6 commits into from
Jan 12, 2017

Conversation

akshayjshah
Copy link
Contributor

@akshayjshah akshayjshah commented Jan 11, 2017

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 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.

(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:

BenchmarkBoolsArrayMarshaler-4   	 1000000	      1028 ns/op	    1088 B/op	       3 allocs/op
BenchmarkBoolsReflect-4          	  500000	      3293 ns/op	    2280 B/op	       8 allocs/op

All allocations in the ArrayMarshaler benchmark come from allocating byte slices for encoders.

// and allocation-heavy.
AddReflected(key string, value interface{}) error
AddString(key, value string)
}
Copy link
Contributor Author

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 {
Copy link
Collaborator

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?

Copy link
Contributor Author

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"
Copy link
Collaborator

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 {
Copy link
Collaborator

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?

Copy link
Contributor Author

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"
Copy link
Collaborator

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.

Copy link
Contributor Author

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{}
Copy link
Collaborator

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"
)

Copy link
Collaborator

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) {
Copy link
Collaborator

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 }),
Copy link
Collaborator

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 { .. }

Akshay Shah added 5 commits January 12, 2017 11:14
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
```
@@ -0,0 +1,71 @@
// Copyright (c) 2016 Uber Technologies, Inc.
Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+100

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦃

@akshayjshah akshayjshah merged commit 9e85108 into dev Jan 12, 2017
@akshayjshah akshayjshah deleted the ajs-arrays branch January 12, 2017 21:44
akshayjshah added a commit that referenced this pull request Feb 15, 2017
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants