Before you consider this package, please read this blog post by the inimitable Dave Cheney. I really appreciate what he has to say, and it largely aligns with my own experiences. Too many choices of levels means inconsistent logs.
This package offers a purely abstract interface, based on these ideas but with a few twists. Code can depend on just this interface and have the actual logging implementation be injected from callers. Ideally only main()
knows what logging implementation is being used.
The main differences are:
fmt.Printf()
. I disagree, especially when you consider things like output locations, timestamps, file and line decorations, and structured logging. I restrict the API to just 2 types of logs: info and error.Info logs are things you want to tell the user which are not errors. Error logs are, well, errors. If your code receives an error
from a subordinate function call and is logging that error
and not returning it, use error logs.
This is a BETA grade API.
There are implementations for the following logging libraries:
Structured logs are more easily queriable: Since you've got key-value pairs, it's much easier to query your structured logs for particular values by filtering on the contents of a particular key -- think searching request logs for error codes, Kubernetes reconcilers for the name and namespace of the reconciled object, etc
Structured logging makes it easier to have cross-referencable logs: Similarly to searchability, if you maintain conventions around your keys, it becomes easy to gather all log lines related to a particular concept.
Structured logs allow better dimensions of filtering: if you have structure to your logs, you've got more precise control over how much information is logged -- you might choose in a particular configuration to log certain keys but not others, only log lines where a certain key matches a certain value, etc, instead of just having v-levels and names to key off of.
Structured logs better represent structured data: sometimes, the data that you want to log is inherently structured (think tuple-link objects). Structured logs allow you to preserve that structure when outputting.
V-levels give operators an easy way to control the chattiness of log operations. V-levels provide a way for a given package to distinguish the relative importance or verbosity of a given log message. Then, if a particular logger or package is logging too many messages, the user of the package can simply change the v-levels for that library.
Read Dave Cheney's post. Then read Differences from Dave's ideas.
Format strings negate many of the benefits of structured logs:
They're not easily searchable without resorting to fuzzy searching, regular expressions, etc
They don't store structured data well, since contents are flattened into a string
They're not cross-referencable
They don't compress easily, since the message is not constant
(unless you turn positional parameters into key-value pairs with numerical keys, at which point you've gotten key-value logging with meaningless keys)
Key-value pairs are much easier to optimize, especially around allocations. Zap (a structured logger that inspired logr's interface) has performance measurements that show this quite nicely.
While the interface ends up being a little less obvious, you get potentially better performance, plus avoid making users type map[string]string{}
every time they want to log.
That's fine. Control your V-levels on a per-logger basis, and use the WithName
function to pass different loggers to different libraries.
Generally, you should take care to ensure that you have relatively consistent V-levels within a given logger, however, as this makes deciding on what verbosity of logs to request easier.
That's not actually a question. Assuming your question is "how do I convert my mental model of logging with format strings to logging with constant messages":
figure out what the error actually is, as you'd write in a TL;DR style, and use that as a message
For every place you'd write a format specifier, look to the word before it, and add that as a key value pair
For instance, consider the following examples (all taken from spots in the Kubernetes codebase):
klog.V(4).Infof("Client is returning errors: code %v, error %v", responseCode, err)
becomes logger.Error(err, "client returned an error", "code", responseCode)
klog.V(4).Infof("Got a Retry-After %ds response for attempt %d to %v", seconds, retries, url)
becomes logger.V(4).Info("got a retry-after response when requesting url", "attempt", retries, "after seconds", seconds, "url", url)
If you really must use a format string, place it as a key value, and call fmt.Sprintf
yourself -- for instance, log.Printf("unable to reflect over type %T")
becomes logger.Info("unable to reflect over type", "type", fmt.Sprintf("%T"))
. In general though, the cases where this is necessary should be few and far between.
This is basically the only hard constraint: increase V-levels to denote more verbose or more debug-y logs.
Otherwise, you can start out with 0
as "you always want to see this", 1
as "common logging that you might possibly want to turn off", and 10
as "I would like to performance-test your log collection stack".
Then gradually choose levels in between as you need them, working your way down from 10 (for debug and trace style logs) and up from 1 (for chattier info-type logs).
While key names are mostly unrestricted (and spaces are acceptable), it's generally a good idea to stick to printable ascii characters, or at least match the general character set of your log lines.