README

A more minimal logging API for Go

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.

Differences from Dave's ideas

The main differences are:

  1. Dave basically proposes doing away with the notion of a logging API in favor of 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.

  1. Verbosity-levels on info logs. This gives developers a chance to indicate arbitrary grades of importance for info logs, without assigning names with semantic meaning such as "warning", "trace", and "debug". Superficially this may feel very similar, but the primary difference is the lack of semantics. Because verbosity is a numerical value, it's safe to assume that an app running with higher verbosity means more (and less important) logs will be generated.

This is a BETA grade API.

There are implementations for the following logging libraries:

  • github.com/google/glog: glogr
  • k8s.io/klog: klogr
  • go.uber.org/zap: zapr
  • log (the Go standard library logger): stdr
  • github.com/sirupsen/logrus: logrusr
  • github.com/wojas/genericr: genericr (makes it easy to implement your own backend)
  • logfmt (Heroku style logging): logfmtr

FAQ

Conceptual

Why structured logging?

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

Why V-levels?

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.

Why not more named levels, like Warning?

Read Dave Cheney's post. Then read Differences from Dave's ideas.

Why not allow format strings, too?

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)

Practical

Why key-value pairs, and not a map?

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.

What if my V-levels differ between libraries?

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.

But I really want to use a format string!

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

  1. figure out what the error actually is, as you'd write in a TL;DR style, and use that as a message

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

How do I choose my V-levels?

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

How do I choose my keys

  • make your keys human-readable
  • constant keys are generally a good idea
  • be consistent across your codebase
  • keys should naturally match parts of the message string

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.

Expand ▾ Collapse ▴

Documentation

Overview

Package logr defines abstract interfaces for logging. Packages can depend on these interfaces and callers can implement logging in whatever way is appropriate.

Usage

Logging is done using a Logger. Loggers can have name prefixes and named values attached, so that all log messages logged with that Logger have some base context associated.

The term "key" is used to refer to the name associated with a particular value, to disambiguate it from the general Logger name.

For instance, suppose we're trying to reconcile the state of an object, and we want to log that we've made some decision.

With the traditional log package, we might write:

log.Printf("decided to set field foo to value %q for object %s/%s",
    targetValue, object.Namespace, object.Name)

With logr's structured logging, we'd write:

// elsewhere in the file, set up the logger to log with the prefix of
// "reconcilers", and the named value target-type=Foo, for extra context.
log := mainLogger.WithName("reconcilers").WithValues("target-type", "Foo")

// later on...
log.Info("setting foo on object", "value", targetValue, "object", object)

Depending on our logging implementation, we could then make logging decisions based on field values (like only logging such events for objects in a certain namespace), or copy the structured information into a structured log store.

For logging errors, Logger has a method called Error. Suppose we wanted to log an error while reconciling. With the traditional log package, we might write:

log.Errorf("unable to reconcile object %s/%s: %v", object.Namespace, object.Name, err)

With logr, we'd instead write:

// assuming the above setup for log
log.Error(err, "unable to reconcile object", "object", object)

This functions similarly to:

log.Info("unable to reconcile object", "error", err, "object", object)

However, it ensures that a standard key for the error value ("error") is used across all error logging. Furthermore, certain implementations may choose to attach additional information (such as stack traces) on calls to Error, so it's preferred to use Error to log errors.

Parts of a log line

Each log message from a Logger has four types of context: logger name, log verbosity, log message, and the named values.

The Logger name consists of a series of name "segments" added by successive calls to WithName. These name segments will be joined in some way by the underlying implementation. It is strongly recommended that name segments contain simple identifiers (letters, digits, and hyphen), and do not contain characters that could muddle the log output or confuse the joining operation (e.g. whitespace, commas, periods, slashes, brackets, quotes, etc).

Log verbosity represents how little a log matters. Level zero, the default, matters most. Increasing levels matter less and less. Try to avoid lots of different verbosity levels, and instead provide useful keys, logger names, and log messages for users to filter on. It's illegal to pass a log level below zero.

The log message consists of a constant message attached to the log line. This should generally be a simple description of what's occurring, and should never be a format string.

Variable information can then be attached using named values (key/value pairs). Keys are arbitrary strings, while values may be any Go value.

Key Naming Conventions

Keys are not strictly required to conform to any specification or regex, but it is recommended that they:

* be human-readable and meaningful (not auto-generated or simple ordinals)
* be constant (not dependent on input data)
* contain only printable characters
* not contain whitespace or punctuation

These guidelines help ensure that log data is processed properly regardless of the log implementation. For example, log implementations will try to output JSON data or will store data for later database (e.g. SQL) queries.

While users are generally free to use key names of their choice, it's generally best to avoid using the following keys, as they're frequently used by implementations:

* `"caller"`: the calling information (file/line) of a particular log line.
* `"error"`: the underlying error value in the `Error` method.
* `"level"`: the log level.
* `"logger"`: the name of the associated logger.
* `"msg"`: the log message.
* `"stacktrace"`: the stack trace associated with a particular log line or
                  error (often from the `Error` message).
* `"ts"`: the timestamp for a log line.

Implementations are encouraged to make use of these keys to represent the above concepts, when necessary (for example, in a pure-JSON output form, it would be necessary to represent at least message and timestamp as ordinary named values).

Implementations may choose to give callers access to the underlying logging implementation. The recommended pattern for this is:

// Underlier exposes access to the underlying logging implementation.
// Since callers only have a logr.Logger, they have to know which
// implementation is in use, so this interface is less of an abstraction
// and more of way to test type conversion.
type Underlier interface {
    GetUnderlying() <underlying-type>
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewContext

func NewContext(ctx context.Context, l Logger) context.Context

    NewContext returns a new context derived from ctx that embeds the Logger.

    Types

    type CallDepthLogger

    type CallDepthLogger interface {
    	Logger
    
    	// WithCallDepth returns a Logger that will offset the call stack by the
    	// specified number of frames when logging call site information.  If depth
    	// is 0 the attribution should be to the direct caller of this method.  If
    	// depth is 1 the attribution should skip 1 call frame, and so on.
    	// Successive calls to this are additive.
    	WithCallDepth(depth int) Logger
    }

      CallDepthLogger represents a Logger that knows how to climb the call stack to identify the original call site and can offset the depth by a specified number of frames. This is useful for users who have helper functions between the "real" call site and the actual calls to Logger methods. Implementations that log information about the call site (such as file, function, or line) would otherwise log information about the intermediate helper functions.

      This is an optional interface and implementations are not required to support it.

      type DiscardLogger

      type DiscardLogger struct{}

        DiscardLogger is a Logger that discards all messages.

        func (DiscardLogger) Enabled

        func (l DiscardLogger) Enabled() bool

        func (DiscardLogger) Error

        func (l DiscardLogger) Error(err error, msg string, keysAndValues ...interface{})

        func (DiscardLogger) Info

        func (l DiscardLogger) Info(msg string, keysAndValues ...interface{})

        func (DiscardLogger) V

        func (l DiscardLogger) V(level int) Logger

        func (DiscardLogger) WithName

        func (l DiscardLogger) WithName(name string) Logger

        func (DiscardLogger) WithValues

        func (l DiscardLogger) WithValues(keysAndValues ...interface{}) Logger

        type InfoLogger

        type InfoLogger = Logger

          InfoLogger provides compatibility with code that relies on the v0.1.0 interface.

          Deprecated: InfoLogger is an artifact of early versions of this API. New users should never use it and existing users should use Logger instead. This will be removed in a future release.

          type Logger

          type Logger interface {
          	// Enabled tests whether this Logger is enabled.  For example, commandline
          	// flags might be used to set the logging verbosity and disable some info
          	// logs.
          	Enabled() bool
          
          	// Info logs a non-error message with the given key/value pairs as context.
          	//
          	// The msg argument should be used to add some constant description to
          	// the log line.  The key/value pairs can then be used to add additional
          	// variable information.  The key/value pairs should alternate string
          	// keys and arbitrary values.
          	Info(msg string, keysAndValues ...interface{})
          
          	// Error logs an error, with the given message and key/value pairs as context.
          	// It functions similarly to calling Info with the "error" named value, but may
          	// have unique behavior, and should be preferred for logging errors (see the
          	// package documentations for more information).
          	//
          	// The msg field should be used to add context to any underlying error,
          	// while the err field should be used to attach the actual error that
          	// triggered this log line, if present.
          	Error(err error, msg string, keysAndValues ...interface{})
          
          	// V returns an Logger value for a specific verbosity level, relative to
          	// this Logger.  In other words, V values are additive.  V higher verbosity
          	// level means a log message is less important.  It's illegal to pass a log
          	// level less than zero.
          	V(level int) Logger
          
          	// WithValues adds some key-value pairs of context to a logger.
          	// See Info for documentation on how key/value pairs work.
          	WithValues(keysAndValues ...interface{}) Logger
          
          	// WithName adds a new element to the logger's name.
          	// Successive calls with WithName continue to append
          	// suffixes to the logger's name.  It's strongly recommended
          	// that name segments contain only letters, digits, and hyphens
          	// (see the package documentation for more information).
          	WithName(name string) Logger
          }

            Logger represents the ability to log messages, both errors and not.

            func Discard

            func Discard() Logger

              Discard returns a valid Logger that discards all messages logged to it. It can be used whenever the caller is not interested in the logs.

              func FromContext

              func FromContext(ctx context.Context) Logger

                FromContext returns a Logger constructed from ctx or nil if no logger details are found.

                func FromContextOrDiscard

                func FromContextOrDiscard(ctx context.Context) Logger

                  FromContextOrDiscard returns a Logger constructed from ctx or a Logger that discards all messages if no logger details are found.

                  func WithCallDepth

                  func WithCallDepth(logger Logger, depth int) Logger

                    WithCallDepth returns a Logger that will offset the call stack by the specified number of frames when logging call site information, if possible. This is useful for users who have helper functions between the "real" call site and the actual calls to Logger methods. If depth is 0 the attribution should be to the direct caller of this function. If depth is 1 the attribution should skip 1 call frame, and so on. Successive calls to this are additive.

                    If the underlying log implementation supports the CallDepthLogger interface, the WithCallDepth method will be called and the result returned. If the implementation does not support CallDepthLogger, the original Logger will be returned.

                    Callers which care about whether this was supported or not should test for CallDepthLogger support themselves.

                    Source Files

                    Directories

                    Path Synopsis