logr icon indicating copy to clipboard operation
logr copied to clipboard

Logger.Error: nil err allowed?

Open pohly opened this issue 3 years ago • 12 comments

In Kubernetes, sometimes klog.ErrorF is called without having an error. During the structured logging migration, that then gets translated to klog.ErrorS(nil, ...). I wonder whether the same nil error would or should be allowed for a logr.Logger. If yes, then we should document it.

Currently it is undefined behavior, so one could argue that explicitly allowing it is not an API change and just documents how loggers should have been implemented all along anyway (tolerate user errors if it was considered as one, don't crash, etc.).

pohly avatar Nov 08 '21 08:11 pohly

Example: https://github.com/kubernetes/kubernetes/pull/105969/files#diff-8f987c51b1b80004440b85620ece9d41e6001b437c1e03f2f0005fe685e135bd

pohly avatar Nov 08 '21 08:11 pohly

I guess it depends on whether we consider Error to just be a friendly alias for Info that makes it easier to log an error, or a different part of the API that does something else. I personally always considered it to be a friendly alias that I also use to log less important events, like transient connection errors.

wojas avatar Nov 09 '21 07:11 wojas

Regardless, we probably do not want to panic on any kind of log input. If the log sink requires a non-nil error, it should check the value and do something sensible (not panic) if it's nil.

wojas avatar Nov 09 '21 07:11 wojas

I guess it depends on whether we consider Error to just be a friendly alias for Info that makes it easier to log an error, or a different part of the API that does something else. I personally always considered it to be a friendly alias that I also use to log less important events, like transient connection errors.

There is one major conceptual difference: Error is always logged, Info only when it meets the verbosity threshold.

Regardless, we probably do not want to panic on any kind of log input. If the log sink requires a non-nil error, it should check the value and do something sensible (not panic) if it's nil.

Agreed.

How about we add a clarification saying "Loggers should accept a nil error because the value may intentionally or accidentally be nil."?

"Should" because we didn't specify it as a "Must" when defining the 1.0.0 API.

pohly avatar Nov 09 '21 08:11 pohly

There is one major conceptual difference: Error is always logged, Info only when it meets the verbosity threshold.

The way I have implemented it, Error is always logged at the current verbosity level, but with an extra error fields. Specifically, with logrus as a backed it simply calls WithError(err) on the logrus Entry.

The current source code comments say:

// Info() and Error() are very similar, but they are separate methods so that
// LogSink implementations can choose to do things like attach additional
// information (such as stack traces) on calls to Error().

and

// Error logs an error, with the given message and key/value pairs as context.
// It functions similarly to Info, but may have unique behavior, and should be
// preferred for logging errors (see the package documentations for more
// information).
//
// The msg argument should be used to add context to any underlying error,
// while the err argument should be used to attach the actual error that
// triggered this log line, if present.

While there is a mention of 'unique behavior' for Error, I don't think it's a good idea to implement it in the sink as 'is always logged'. External library code could call Error to simply attach an error message to a debug level message.

Of course you could decide that in klog calling ErrorS will cause it to always be logged, but then I would also bump the V-level of that message accordingly.

How about we add a clarification saying "Loggers should accept a nil error because the value may intentionally or accidentally be nil."?

"Should" because we didn't specify it as a "Must" when defining the 1.0.0 API.

I think this can be a "Must", because this is merely a clarification, not a change of behaviour. If a sink did not handle it before, the program may have crashed. We currently do not explicitly prohibit calling it with a nil error.

wojas avatar Nov 09 '21 09:11 wojas

While there is a mention of 'unique behavior' for Error, I don't think it's a good idea to implement it in the sink as 'is always logged'.

I'm surprised that the current docs don't explain this. I thought we had merged a PR which fixed that. I know that it has surprised people before. But it really is the current and intentional behavior.

Error does not call Enabled: https://github.com/go-logr/logr/blob/8d00e357f1cbbb9246ed8bbf03f2c8fe242d2ae9/logr.go#L261-L266

Whereas Info does: https://github.com/go-logr/logr/blob/8d00e357f1cbbb9246ed8bbf03f2c8fe242d2ae9/logr.go#L244-L251

LogSink.Error also doesn't get the current log level while LogSink.Info does.

This is intentional because then Error can then be used for log messages which are so important that they should always be emitted. If that is not desired, users can call Info with an error key/value pair.

pohly avatar Nov 09 '21 10:11 pohly

Regarding my original question: I just noticed that there is a "if present" in "the err argument should be used to attach the actual error that triggered this log line, if present."

That indirectly indicates that "not present = nil" is a valid way of calling Error. We can make that more obvious by explicitly stating it, but it's not an API change.

pohly avatar Nov 09 '21 10:11 pohly

I am +1 to saying "must" support nil errors in comments. @pohly - PR me?

WRT Error() - it is intended to always log. The fact that log.V(11).Error() is a thing is only because V() returns a full Logger. Circa 0.4.0 it returned an InfoLogger which had no Error() method, but that was problematic for other reasons and I didn't want to add V() and Bias() as different things. So I am +1 to clarifying that, too.

thockin avatar Nov 24 '21 20:11 thockin

Is there any possibility to improve the ergonomics past logger.Error(nil, "message")? Most other logging implementations allow e.g. logger.WithError(err).Info("message") or logger.Error("message") separately. I'm currently in a migration to contextual + structured logging and this is one of the more unpleasant translations.

stevekuznetsov avatar Aug 10 '22 19:08 stevekuznetsov

We could add a logger.ErrorMsg("message", ...). But I am not sure whether that is really a big improvement over logger.Error(nil, "message", ...).

pohly avatar Aug 11 '22 05:08 pohly

Up to you. The developer ergonomics today are not ideal - even with nil someone that's not perfectly attuned to this interface and expectations immediately has some questions on review - why nil? Did you mean to pass an error but forgot? Is that a valid input to the loggers ... ?

If not explicitly supporting the "log this always, label it as error level, without a Go-error" case was a side-effect of something, and not an intentional choice, I'd suggest maybe giving the ErrorMsg() or similar approaches some consideration. Clearly some ergonomics and clarity wins there.

stevekuznetsov avatar Aug 11 '22 11:08 stevekuznetsov

Let's reopen for discussion.

@thockin: what do you think about adding ErrorMsg as an alias for Error(nil?

pohly avatar Aug 11 '22 12:08 pohly

"ErrorMsg" is an unfortunate name, don't you think? I get the point you're making but I don't know if that makes it any clearer? I'm open to being swayed here...

thockin avatar Nov 18 '22 23:11 thockin

I don't have a strong preference here and could live with just having Error(nil, ...) myself. No idea for a better name.

pohly avatar Nov 22 '22 08:11 pohly

I'm disinclined to add another method for this, especially if slog is coming eventually. Closing.

thockin avatar Nov 22 '22 16:11 thockin