-
-
Notifications
You must be signed in to change notification settings - Fork 2.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
Add filtered leveled logging and error promotion #252
Conversation
Adds a couple new options to levels.Levels, including: - FilterLogLevel: specify the minimum log level you want to see actually hit your logs. Anything below this will be discarded in as cheap a fashion as possible. Defaults to Debug, that is, showing everything. - PromoteErrors: if true, a log line that contains an error will be promoted to a higher log level, potentially bypassing the FilterLogLevel. This allows a developer to do a bunch of debug logging of function call results and, in production, not see any of them unless the log line actually has an error. By default this flag is set to false. - ErrorKey: the key in the context where we should search for the existence of an error. If we find a `nil` value at that location, or if that is not a key in the context, we treat it as not having an error. Defaults to "error". - PromoteErrorToLogLevel: the log level that error logs should be promoted to. Promotion only occurs if the log level explicitly specified is lower than this value, so if you log at critical level and include an error, it doesn't get downgraded. Default for this is Error. Creates an ordered enumeration of LogLevels from Debug up to Crit, so users can specify the FilterLogLevel and PromoteErrorToLogLevel options noted above. To detect the presence of errors in the context, I had to add a HasValue utility method on log.Context. I would have preferred that this method not be exported at all, but because levels and log are different packages, it is not visible to be used in levels unless it is exported.
// the associated level string as a prefix to every log line. Log level may be | ||
// affected by error promotion; and log lines may be suppressed based on the value | ||
// of FilterLogLevel. | ||
type levelCommittedLogger struct { |
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 would much rather see this as a distinct implementation of the Logger (or Levels) interface, than as a change to the behavior of Levels. Otherwise, systems that currently depend on kitlog behavior will be encouraged (or forced) to fork go-kit.
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.
With the default options for filterLogLevel
, promoteErrors
, errorKey
, and promoteErrorToLevel
, there shouldn't be any changes to the behavior of go-kit/log or go-kit/log/levels... Can you give a specific instance of existing behavior that would be altered?
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.
To be more verbose...
The Logger
interface is very short:
type Logger interface {
Log(keyvals ...interface{}) error
}
levelCommittedLogger
implements the Log
method at line 130 in this file, so it satisfies the log.Logger
interface.
Let's look specifically at the levels.Debug
case. Previously it returned a log.Logger
, and it still does. Before this change, it returned l.ctx.WithPrefix(l.levelKey, l.debugValue)
With this change, it returns a levelCommittedLogger{l, Debug}
. Whose Log
method becomes:
lvl, ctx := l.committedLevel, l.levels.ctx.With(keyvals...)
//...
if l.levels.promoteErrors {...} // By default, l.levels.promoteErrors is false, so short circuit this block
//...
if lvl < l.levels.filterLogLevel {...} // By default l.levels.filerLogLevel is 0 and lvl ranges from 0 to 4,
// so short circuit this block
//...
var levelValue string
switch lvl {
case Debug:
levelValue = l.levels.debugValue
// Short circuit the rest of the cases, because case Debug is true
}
//...
return ctx.WithPrefix(l.levels.levelKey, levelValue).Log()
In other words, with appropriate substitutions, that last line becomes:
return l.levels.ctx.With(keyvals...).WithPrefix(l.levels.levelKey, l.levels.debugValue).Log()
The only difference between this line and
l.ctx.WithPrefix(l.levelKey, l.debugValue).Log(keyvals...)
...is that you can't overwrite l.levelKey
with a different ad-hoc value, which you shouldn't be doing anyway if you're using the levels
package.
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 could be misunderstanding this, but it looks like each call for a logger (e.g. "l.Debug()") creates a struct, (levelCommittedLogger) where currently it extends a context. I want to confirm that the memory behavior is similar.
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.
For a first pass, log.Context.With()
also creates a new struct, so the Debug()
call should have the same or fewer allocations. The Log()
call in levelCommittedLogger
might have a few more allocations since it does a log.Context.With()
and a log.Context.WithPrefix()
, but they will immediately go out of scope and become eligible for GC.
I don't envision it being a huge difference, but I will add some memory benchmarks to the test suite to check!
Thanks for the well documented PR! I don't have time for a full review, but I believe returning the levelCommitted logger from the Levels methods will interact badly with log.DefaultCaller. Can you add test code to check that. See #131 for information about when we changed levels.Levels to work correctly with log.DefaultCaller. We should try not to cause a regression in this area. I will review in more detail when I get the time. |
@ChrisHines I will check that out and write some tests. |
@ChrisHines Your intuition is correct, it messes up
I'm racking my brain trying to come up with a workaround for this that doesn't involve tacking on more junk to the public API of We can jettison the error promotion, and this becomes super easy: in I will consider it more this weekend but I'd love it if you had some ideas. |
Possibly the cleanest solution I can come up with is to add some kind of The hook would be of the signature
Which would allow consumer packages to add a hook that checks and potentially replaces the keyvals that are about to be logged, and bails out on an error, which could include "don't log this because of the log level." But it feels clunky and complicated and overkill to support something that isn't a prominent use case. |
@bishoprook I agree that The idea you mentioned in #250 to avoid evaluation of expensive logger := log.NewLogfmtLogger(w)
logger = LazyValueLogger(logger)
logger = levels.NewLevelFilterLogger(logger, logLevel)
logger = levels.NewEscalateErrLogger(logger)
lvllog := levels.New(logger).With("t", log.DefaultTimestampUTC, "caller", log.DefaultCaller) In this way I am not suggesting we add |
I think future discussion about the log package, especially levels, will be in the direction established by #357. Accordingly, I'll close this PR. Thanks for the thoughts and effort! |
Addresses #250
Adds a couple new options to levels.Levels, including:
actually hit your logs. Anything below this will be discarded
in as cheap a fashion as possible. Defaults to Debug, that is,
showing everything.
be promoted to a higher log level, potentially bypassing the
FilterLogLevel. This allows a developer to do a bunch of debug
logging of function call results and, in production, not see any
of them unless the log line actually has an error. By default
this flag is set to false.
existence of an error. If we find a
nil
value at that location,or if that is not a key in the context, we treat it as not having
an error. Defaults to "error".
promoted to. Promotion only occurs if the log level explicitly
specified is lower than this value, so if you log at critical
level and include an error, it doesn't get downgraded. Default
for this is Error.
Creates an ordered enumeration of LogLevels from Debug up to Crit,
so users can specify the FilterLogLevel and PromoteErrorToLogLevel
options noted above.
To detect the presence of errors in the context, I had to add a
HasValue utility method on log.Context. I would have preferred that
this method not be exported at all, but because levels and log are
different packages, it is not visible to be used in levels unless
it is exported.