-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Add a slightly slower, but less verbose, logger #147
Conversation
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.
quick, non-authoritative review.
@@ -0,0 +1,54 @@ | |||
// 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.
belongs in benchmarks/
, yes?
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.
All your points about benchmarks are absolutely valid. But my intention for this file was to have means of comparison between core logger and sugared one. So I basically mirrored Benchmark10Fields
from logger_test_bench.go
.
}) | ||
} | ||
|
||
func Benchmark10FieldsSugar(b *testing.B) { |
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.
there should be three benchmarks here, BenchmarkSugarAddingFields
, BenchmarkSugarAccumulatedContext
, and BenchmarkSugarWithoutFields
func Benchmark10FieldsSugar(b *testing.B) { | ||
withBenchedSugar(b, func(log Sugar) { | ||
log.Info("Ten fields, passed at the log site.", | ||
"one", 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.
add a variety of types here, see the other benchmarks,
e.g.
"int", 1,
"int64", int64(1),
"float", 3.0,
"string", "four!",
"bool", true,
"time", time.Unix(0, 0),
"error", errExample.Error(),
"duration", time.Second,
"user-defined type", _jane,
"another string", "done!",
fields = append(fields, Duration(key, value.(time.Duration))) | ||
case fmt.Stringer: | ||
fields = append(fields, Stringer(key, value.(fmt.Stringer))) | ||
default: |
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 missing support for the error
type.
in the existing zap logger, logging an error
writes a key named "error"
. with the field count being used here, we have to decide if we want to support:
sugar.With("k1", 42, errors.New("this automatically goes into 'error'))`
or
sugar.With("k1", errors.New("nothing automatic"), "k2", errors.New("field is an errorType still"))
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 suggest sugar logger should accept optional error value as the first of variadic arguments. E.g.:
sugar.Warn("message", errors.New("this is a error"), "a", "b")
{"level":"warn","msg":"message","err":"this is a error","a":"b"}
Motivation behind my suggestion is that having more then one errors in single log message is probably pretty rare requirement which don't need to be covered by syntax sugar.
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.
(amended after looking more closely)
I'd prefer not to do this, since the common (non-error) case will then require passing a it makes our treatment of the first of the variadic arguments a bit too magical.nil
I'd prefer an API in which callers need to pass both the key and the value for errors: sugar.With("error", err)
. We'll need to add a case like this:
case error:
fields = append(fields, String(key, value.(error).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.
I missed this earlier, but it seems like both @akshayjshah and I think we should remove the first variadic argument can be error
.
As for the checking the case error
, I'd consider only allowing that for a field named "error", just in case someone wants to log a type that happens to implement error
but they acutally want the object logged.
|
||
// Log ... | ||
func (s *sugar) Log(lvl Level, msg string, args ...interface{}) error { | ||
fields, err := getSugarFields(args...) |
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.
since this may be an expensive operation due to reflection and, in some cases, calling receivers on arguments passed in, this should check to see if the log level is valid before calling getSugarFields
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.
Perfectly valid point!
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.
@akabos, thanks for this PR! Kudos for taking the initiative to tackle this issue. I've left a few comments throughout; most are pretty small issues.
I'm only available on and off over the next few weeks, so I'm leaving this as a comment instead of a "request changes" review. Please fix the issues above, and then one of the other maintainers can take another pass over the code.
"time" | ||
) | ||
|
||
// Sugar is a wrapper around core logger whith less verbose API |
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: "with", not "whith"
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.
fixed
|
||
// Log a message at the given level. Messages include any context that's | ||
// accumulated on the logger, as well as any fields added at the log site. | ||
Log(Level, string, ...interface{}) 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.
Let's get rid of this; in an interface that's designed to be terse, I don't think it adds much.
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'm not sure what exactly you'd like to get rid of. I'm pretty sure Log
method is very useful even in context of "terse interface".
noErrArgs = args[1:] | ||
default: | ||
noErrArgs = args | ||
} |
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'd prefer to remove this block; treating the first of the variadic arguments differently from the rest is a little too much magic for me.
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.
So how do you suggest treating errors then? Alternative approach is expecting an error in any position and would involve even more magic.
case error: | ||
return nil, errors.New("error can only be the first argument") | ||
default: | ||
return nil, errors.New("invalid argument type") |
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.
In addition to supporting errors (as @billf mentioned), please make sure to support all the types that zap has fields for. I believe that this switch is missing uintptr
and zap.Marshaler
.
Rather than returning an error if the argument doesn't match one of the types zap explicitly supports, fall back to using zap.Object
, which attempts to use an encoder-appropriate serialization.
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.
Added support for zap.Marshaler
and changed fall back from error to zap.Object
. And I couldn't find a singe mention of uintptr
in zap codebase.
return nil, errors.New("field name must be string") | ||
} | ||
} else { | ||
switch value.(type) { |
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.
Have you seen the switch value := value.(type)
form of type switches? It's described in Effective Go.
Using that form will reduce duplication below, since you won't have to type-cast in each case.
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.
Sorry about that. I'm still very new to Go, so your notice is really appreciated.
Fixed.
func getSugarFields(args ...interface{}) ([]Field, error) { | ||
var ( | ||
noErrArgs []interface{} | ||
fields []Field |
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.
Let's pre-allocate the right amount of space here - we should be able to do fields := make([]Field, 0, len(args)/2)
.
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.
fixed
|
||
if (len(noErrArgs) % 2) != 0 { | ||
return nil, errors.New("invalid number of arguments") | ||
} |
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.
Instead of returning an error, let's add a field to the log output describing the issue. In that case, we should ignore the last argument and include it in the error:
fields = append(fields, zap.String("numLogArgsError", fmt.Sprintf("odd number of arguments passed to logger, ignoring last arg: %v", args[len(args)-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.
I'm not sure I implemented it the way you expected, but please see b819160
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.
Latest commit makes sugar logger handle internal errors in the same way core logger does.
|
||
// Log ... | ||
func (s *sugar) Log(lvl Level, msg string, args ...interface{}) error { | ||
if cm := s.core.Check(lvl, msg); cm.OK() { |
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.
Awesome - thanks for being careful to use the checked message API!
Since we're updating getSugarFields
so that it won't need to return an error, let's not have this method (or any of the Info, Debug, Error, etc. method) return errors either.
Fatal(string, ...interface{}) error | ||
// If the logger is in development mode (via the Development option), DFatal | ||
// logs at the Fatal level. Otherwise, it logs at the Error level. | ||
DFatal(string, ...interface{}) 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.
It seems like all these methods return errors because getSugarFields
can return an error. I'd like to avoid this, since it's awkward for loggers to return errors - what's the user going to do, log the error?
I've left some comments on getSugarFields
that should remove the returned error. Once those changes are in, let's change With
, Debug
, Info
, etc. to not return errors either.
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.
fixed
any updates / timeline on this? we'd like to use it. would you like my team to take over this PR and finish addressing comments or were you planning to? cc @uber-go/service-framework |
@akabos to be clear that question was directed at you -- do you plan on working on this any more, or would you like us to pick it up for you? |
@akabos Are you continuing to work on this PR? We are running on a tight schedule and this work seems really useful and relevant for us. I am happy to take over your work, address comments and fixes for this Logger. |
@sectioneight @anuptalwalkar I'd like to finish that myself. Please give me couple of days. |
@akabos Thank you so much! I appreciate speeding up the timeline. This is fantastic work:) |
case string: | ||
key = value.(string) | ||
default: | ||
return nil, errors.New("field name must be 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.
Just my $0.02- Loggers usually don't throw/return errors and rather gulp them down to the log files. Instead of returning error for this function, you want to just add a string field with a tiny message "unparsable argument" or something. It should be on the devs to notice an fix the logging format later in the game.
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.
Fixed, please see b819160
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.
Latest commit makes sugar logger handle internal errors in the same way core logger does.
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.
Looks awesome! Can't wait. I'm a human linter, I couldn't help myself.
|
||
// Sugar is a wrapper around core logger with less verbose API | ||
type Sugar interface { | ||
// Check the minimum enabled log level. |
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 think this violates golint or vet or one of the tools? The comment should start with "Level", right?
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.
Well, I don't know much about how golint
should work, but apparently it doesn't have any objections about all these comments. Furthermore, they are basically copied over from zap.Logger
interface definition, see https://github.com/uber-go/zap/blob/master/logger.go#L33
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.
Normally comments in Go are Field is [...]
or Func does blah
. In this case, I think rather than Check the minimum enabled log level
, Level returns the minimum enabled log level.
might be a better comment
For example, look at the comments on this interface in the standard library:
https://golang.org/pkg/net/http/#CookieJar
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.
fixed
// Change the level of this logger, as well as all its ancestors and | ||
// descendants. This makes it easy to change the log level at runtime | ||
// without restarting your application. | ||
SetLevel(Level) |
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.
Same comment here
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.
Same as above
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.
fixed
SetLevel(Level) | ||
|
||
// Create a child logger, and optionally add some context to that logger. | ||
With(...interface{}) (Sugar, 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.
Samesies
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.
fixed
case error: | ||
return nil, errors.New("error can only be the first argument") | ||
fields = append(fields, Stringer(key, v)) | ||
case LogMarshaler: |
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.
@akshayjshah @anuptalwalkar @sectioneight
I looked through coverage report and eventually found that LogMarshaler
never get caught. Object
field is always used instead. I'm going to update test suite to fail in such case, but what's important is that I don't really have a clue why would it happen. Can anyone please comment on the situation?
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.
@akabos I bet (Just guessing) the LogMarshaler didn't understand unexported struct. I commented on the test below to update m9e to json serializable struct that seemed to fix the test coverage issue you are seeing.
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.
As far as I understand Go exported/unexported logic, these fields are only used by m9e
own method and the fact they are unexported should not affect anything.
type m9e struct { | ||
foo int | ||
bar bool | ||
} |
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.
You need this struct to be json formattable. This will fix the failing TestSugarLogTypes test -
type m9e struct { Foo int
json:"foo" Bar bool
json:"bar" }
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 will fix test but no the issue. When you define struct as json formatable Object
field start to mimic expected LogMarshal
output.
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 removed LogMarshaler altogether because type switch fail to catch structs implementing this interface.
} | ||
} else { | ||
switch v := value.(type) { | ||
case 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.
I'm not sure about this special casing for error. I think it's a little surprising, complicates the code, and will possibly cause unexpected errors.
For example, if some struct happens to satisfy the error interface (E.g., Thrift exceptions), then they will never be able to use it as a value.
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'm not sure either but I don't see much options here. Also I think this wrapper supposed to be merely syntax sugar. It's not supposed to satisfy all possible requirements and just have to be "ok for most uses".
I'm open to any ideas about handling errors, but as for now this is the only working and (mostly) magic free implementation I can think of.
} | ||
|
||
func BenchmarkZapSugarDisabledLevelsWithoutFields(b *testing.B) { | ||
logger := zap.NewSugar(zap.New(zap.NewJSONEncoder(), zap.ErrorLevel, zap.DiscardOutput)) |
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.
we duplicate the zap.New(...)
quite a lot, maybe a helper could help
func newSugarLogger(options [...]) zap.Sugar
and by defualt the options should probably just created a JSON encoder that with discard output
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.
fixed
|
||
// Sugar is a wrapper around core logger with less verbose API | ||
type Sugar interface { | ||
// Check the minimum enabled log level. |
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.
Normally comments in Go are Field is [...]
or Func does blah
. In this case, I think rather than Check the minimum enabled log level
, Level returns the minimum enabled log level.
might be a better comment
For example, look at the comments on this interface in the standard library:
https://golang.org/pkg/net/http/#CookieJar
// Change the level of this logger, as well as all its ancestors and | ||
// descendants. This makes it easy to change the log level at runtime | ||
// without restarting your application. | ||
SetLevel(Level) |
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.
Same as above
s.core.SetLevel(lvl) | ||
} | ||
|
||
func (s *sugar) With(args ...interface{}) (Sugar, 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.
I'm not sure if returning errors from this method is useful -- users can't really do much. I think perhaps we should just write an error to the ErrorOutput
?
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.
Fixed
fields = append(fields, Time(key, v)) | ||
case time.Duration: | ||
fields = append(fields, Duration(key, v)) | ||
case fmt.Stringer: |
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.
fmt.Stringer
should come after LogMarshaler
-- if the type knows how to serialize itself, I think we should prefer that.
Please add a unit test to ensure that's the case as well.
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 removed LogMarshaler altogether. As I mentioned in previous comments, type switch fails to catch structs implementing this interface for some reason.
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 explain more about the type switch not catching this -- I think keeping log marshaler is a good idea, and it should work,
case LogMarshaler: | ||
fields = append(fields, Marshaler(key, v)) | ||
default: | ||
fields = append(fields, Object(key, value)) |
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.
In this approach, if a type has String
, there's no way to serialize the fields using the JSON marshaller.
return fields, nil | ||
} | ||
|
||
// Log ... |
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.
incomplete comment. You can delete comments on exported methods on private types -- since the interface already has documentation
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.
removed
if cm := s.core.Check(lvl, msg); cm.OK() { | ||
fields, err = getSugarFields(args...) | ||
if err != nil { | ||
fields = []Field{Error(err)} |
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 the user has an error field, it would get overwritten by this error field.
I think we have to get away from returning errors when converting fields
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.
Everything user tried to log would get overwritten with error returned from getSugarFields
. I'm pretty sure it is valid way of handling invalid arguments.
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.
Not relevant any more. I made sugar logger to handle internal error in the same manner core logger does.
|
||
func (s *sugar) DFatal(msg string, args ...interface{}) { | ||
lvl := ErrorLevel | ||
if s.core.(*logger).Development { |
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.
Please use the underlying Logger
's DFatal
method. This method will panic if the passed in logger is wrapped via Sample
, or some other wrapper which implements the Logger
interface but is not the base logger.
Can we add a unit test that would have caught this issue?
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 made DFatal
to relay directly to core logger's DFatal
.
But speaking of unit test that would have caught this issue, I really doubt that sugar logger suite should include tests for core logger's logic.
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.
Thanks for fixing it. I'm not suggesting unit testing the core logger's DFatal
method, but a test which ensures that the DFatal
method here doesn't panic if it's passed an implementation of zap.Logger
which isn't the underlying logger
type.
Thanks for the contribution @akabos I think the big issues to figure out are:
Would appreciate feedback from others as well. My personal opinion is that the special-casing of the first field of |
Thanks for your comments! After latest commits, Sugar logger does not inject own error fields into log messages any more. I agree that it was petty awkward way to handle internal errors. Instead, I made it to behave exactly as core logger does. I hope this is going to satisfy everyone who expressed concern on the matter. |
By no means I insist on "error only on first place" approach. But this special case is the consequence of the special nature of error field. Among all fields only this one has no name so it has to be dealt with specially in any case. I chose this approach only because it seemed easiest to implement while benefits from allowing error in arbitrary position seemed questionable. I still think that having "first place" restriction for errors is valid, but I'd like to see this PR merged ASAP and I'm ready to change sugar logger to handle errors in any particular way you see appropriate. Just let me know what exactly you see appropriate. |
} | ||
|
||
func (s *sugar) internalError(msg string) { | ||
s.core.(*logger).internalError(msg) |
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.
There should be no casts from Logger
to *logger
, as that will panic if the user passes some sort of wrapped logger (e.g., like one returned from sampling).
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.
internalError
is not declared for Logger
interface. I'm not sure if there is another way to access it. Even if I re-implement this method on Sugar
, ErrorOutput
is still not declared on LoggerInterface
. What do you suggest?
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.
Feel free to defer this with a TODO, and we can circle back to it, since this will require some thinking about.
For now, make the internal error just do s.core.Error("Logger error", zap.String("message", msg))
or something.
As a separate PR, we need to think about either exposing the error output, or trying to cast the passed in logger to something that implements an internalError
method, and if it's not implemented doing the log to the normal error stream.
@akabos The Thanks for fixing the error handling, I think logging it as an internal error is definitely better. We need to figure out a way to do that that won't cause panics, but otherwise the error handling is better. I don't have strong opinions no whether we should always look for |
This sugar implementation is twice as slow if compared to core, but still twice as fast if compared to nearest competition.