A simple logging interface for Go

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.

Comments
  • support alternative way of skipping frames

    support alternative way of skipping frames

    This is needed for log sinks where the underlying stack unwinding doesn't support an explicit call depth, for example testing.T.Log.

    One such log sink is currently pending in https://github.com/kubernetes/klog/pull/240. It currently incorrectly logs logr.go as call site.

  • JSON LogSink loosely-coupled to Google Cloud Logging

    JSON LogSink loosely-coupled to Google Cloud Logging

    Thanks for logr.

    I've been using logr across the components of an app, deployed to different different Google Cloud Platform (GCP) services (GKE, Cloud Functions, Cloud Run) and all use Cloud Logging.

    To benefit most from Cloud Logging, I want to use structured logging but:

    1. Cloud Logging requires that log entries be JSON formatted
    2. Cloud Logging requires that logs use GCP-specific keys (e.g. logging.googleapis.com/labels, logging.googleapis.com/sourceLocation)

    For (1), I've switched from stdr to zerologr to emit JSON-formatted logs.

    This requires taking a dependency on rs/zerolog when all I really need is the standard library's encoding/json.

    Unfortunately, zerologr entries omit the caller (file, line) struct that's almost a match for Cloud Logging's SourceLocation.

    Is there a way to retain this?

    For (2), I'm using the GCP-specific keys when writing logs but I'd prefer to not be so tightly bound.

    Should I consider writing a JSON LogSink?

    Can I chain LogSinks or would I then also need to write a LogSink that combines to JSON and GCP-specific keys?

    Thanks!

    A related but different stupid question, why do LogSinks bind Info and Err to a single writer? For container logging, I'd prefer to write Info to os.Stdout but Err to os.Stderr. As it is, I can either have one or the other, or I need to have duplicate Loggers so that I can stdout.Info and stderr.Err (and never stdout.Err or stderr.Info) which seems redundant.

  • Accept `context` when logging.

    Accept `context` when logging.

    Is it possible to extend the API to accept context when logging? The reason is that we want to extract information from the context like opentelemetry trace ids (trace_id/span_id) or other information that we propagate in the app using the context.

  • Context support

    Context support

    One common pattern for logging is to pass the current logging context to called functions and types using a Go context. This decouples much of the program from a specific implementation of the logr interface. This is similar to adding a logger argument to every function but is more general since it allows non logr-aware packages to pass contexts through to logr-aware packages.

    A suggested API (these are top level functions):

    // FromContext returns a logr.Logger constructed from the context or returns a null logger if no
    // logger details are found in the context.
    func FromContext(ctx context.Context) logr.Logger
    
    // NewContext returns a new context that embeds the logger.
    func NewContext(ctx context.Context, l logr.Logger) context.Context
    

    This can already be implemented in a separate package (I created logctxr as an example) but I thought it would be worth considering as a core addition to the logr package.

    Updated 2020-10-20 to remove NewLogger function based on discussion

  • Structured Logging

    Structured Logging

    Switch the interfaces over to support/favor structured logging. Format-string style logging is no longer supported by the interface. Instead, callers should use tag-value pairs to introduce variable information in a structured way.

    The general pattern is based of go.uber.org/zap (specifically, the SugaredLogger), but we keep some ofour own opinions (log levels go up, not down, numeric levels, etc), and keep a limited interface.

  • Logger.Error: nil err allowed?

    Logger.Error: nil err allowed?

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

  • LogObject: optional interface for logging objects differently

    LogObject: optional interface for logging objects differently

    The intention is to use this when the output is structured (like JSON) when the original type would be logged as string. It also has some other use cases.

    This approach was chosen instead of a full marshaler API as in zapcore.ObjectMarshaler because the implementation is simpler. The overhead for large types is expected to be higher, but it is not certain yet whether this is relevant in practice. If it is, then a marshaler API can still be added later.

    For alternatives, see the discussion in https://github.com/kubernetes/klog/issues/262

    @wojas: @thockin and I were already discussing this a bit. Here's a formal PR, perhaps you can also have a look?

  • can remove funcr to an independent repo?

    can remove funcr to an independent repo?

    thanks to nice work, because of the funcr package use a api strconv.FormatComplex that come from go1.16, if some one want to use this log interface, he must update his golang version, this seems no need for a minimal log interface

  • docs: clarify Error semantic

    docs: clarify Error semantic

    Two aspects were not spelled out explicitly, which sometimes led to misunderstandings:

    • error messages are always printed
    • the error instance is optional

    Fixes: https://github.com/go-logr/logr/issues/118

  • funcr: Add options for hooks during rendering

    funcr: Add options for hooks during rendering

    Add new Options fields which are function pointers to optional hooks to be called.

    This allow users to manipulate the way that kv-pairs are logged. For example:

    • prefix or rename builtin keys
    • log WithValues() kv-pairs as a single pseudo-struct
    • collect telemetry around how often each log is triggered

    This adds a new type PseudoStruct which can be used to trigger special logging - a kvList is treated as a struct.

    This also exposes the Caller type, to enable hooks to recognize and use caller information.

    Fixes #97

    (builds on #110)

  • fix incremental WithCallDepth

    fix incremental WithCallDepth

    When WithCallDepth stored the new sink, it didn't update the cached withCallDepth implementation. When that then was called again by another WithCallDepth, the previously incremented call depth was overwritten.

  • funcr: split out PseudoStruct into separate package

    funcr: split out PseudoStruct into separate package

    This makes it possible to reference it in other LogSinks which want to provide the same functionality without pulling in the funcr source code.

    Doing the same with current master results in "go mod vendor" including "funcr.go" under "vendor".

  • add KeysAndValues as special type

    add KeysAndValues as special type

    This is like the existing funcr.PseudoStruct except that it uses structs as members of the slice. The reasons for that difference are making the API type-safe at compile time and (subjectively) nicer rendering when a LogSink does not support the type.

    Here's how zapr and klogr handle funcr.PseudoStruct:

    {"caller":"test/output.go:435","msg":"funcr","v":0,"parent":["a",1,"b",2]}
    I output.go:486] "funcr" parent=[a 1 b 2]
    

    Here's the same for logr.KeysAndValues:

    {"caller":"test/output.go:435","msg":"keys and values","v":0,"parent":[{"Key":"a","Value":1},{"Key":"b","Value":2}]}
    I output.go:486] "keys and values" parent=[{Key:a Value:1} {Key:b Value:2}]
    

    The tentative support for logr.KeysAndValues would lead to output like this:

    {"caller":"test/output.go:<LINE>","msg":"keys and values","v":0,"parent":{"boolsub":true,"intsub":1,"recursive":[{"Key":"sub","Value":"level2"}]}}
    I output.go:<LINE>] "keys and values" parent={ boolsub=true intsub=1 recursive={ sub="level2" } }
    

    The implementation in klogr is recursive and handles KeysAndValues nested inside KeysAndValues, the one in zapr is simpler and only handles it at the top level.

  • log values from context

    log values from context

    Having to add a modified logger to a context when adding some values to the context which are meant to be logged (like trace ID) has two drawbacks:

    • the instrumentation code which adds the values to the context must be aware of logging
    • modifying the logger incurs a cost, whether some actual log entry then gets emitted or not.

    A better approach is to add the values only to the context, then during logging extract them. This is the same approach that contextual logging in Go is going to use.

    I have experimented with a few different approaches for this, including doing it entirely in klog by wrapping the LogSink in a logger with one that injects the extra values. That is complicated and not worth discussing further.

    In logr itself there are different approaches - see individual commits in https://github.com/pohly/logr/commits/with-context-all

    The one proposed here implements logging of the additional values in Logger (no need to modify LogSinks) and passes them to the LogSink via WithValues.

  • New release to ship #143

    New release to ship #143

    Hey there 👋 PR #143 was merged a few months ago, however, it is not in any release (last release 1.2.3 does not include this feature). Any chance you could produce a new release with this feature?

    I'd like to avoid pinning my dependency to @master or to a specific commit, if possible. Thank you!

Gomol is a library for structured, multiple-output logging for Go with extensible logging outputs

gomol Gomol (Go Multi-Output Logger) is an MIT-licensed structured logging library for Go. Gomol grew from a desire to have a structured logging libra

Sep 26, 2022
FactorLog is a logging infrastructure for Go that provides numerous logging functions for whatever your style may be
FactorLog is a logging infrastructure for Go that provides numerous logging functions for whatever your style may be

FactorLog FactorLog is a fast logging infrastructure for Go that provides numerous logging functions for whatever your style may be. It could easily b

Aug 3, 2022
Package logging implements a logging infrastructure for Go
Package logging implements a logging infrastructure for Go

Golang logging library Package logging implements a logging infrastructure for Go. Its output format is customizable and supports different logging ba

Nov 10, 2021
A simple logging interface for Go

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

Dec 30, 2022
Simple and blazing fast lockfree logging library for golang
Simple and blazing fast lockfree logging library for golang

glg is simple golang logging library Requirement Go 1.11 Installation go get github.com/kpango/glg Example package main import ( "net/http" "time"

Nov 28, 2022
Simple and configurable Logging in Go, with level, formatters and writers

go-log Logging package similar to log4j for the Golang. Support dynamic log level Support customized formatter TextFormatter JSONFormatter Support mul

Sep 26, 2022
Simple, configurable and scalable Structured Logging for Go.

log Log is a simple, highly configurable, Structured Logging library Why another logging library? There's allot of great stuff out there, but also tho

Sep 26, 2022
Simple, customizable, leveled and efficient logging in Go

log Simple, customizable, leveled and efficient logging in Go Installation go get -u github.com/ermanimer/log Features log is a simple logging package

Dec 20, 2021
A simple logging framework for Go program.
A simple logging framework for Go program.

ASLP A Go language based log library, simple, convenient and concise. Three modes, standard output, file mode and common mode. Convenient, simple and

Nov 14, 2022
Logging, distilled

What is distillog? distillog aims to offer a minimalistic logging interface that also supports log levels. It takes the stdlib API and only slightly e

Dec 14, 2022
Logging library for Golang

GLO Logging library for Golang Inspired by Monolog for PHP, severity levels are identical Install go get github.com/lajosbencz/glo Severity levels Deb

Sep 26, 2022
The Simplest and worst logging library ever written

gologger A Simple Easy to use go logger library. Displays Colored log into console in any unix or windows platform. You can even store your logs in fi

Sep 26, 2022
Go implementation of systemd Journal's native API for logging

journald Package journald offers Go implementation of systemd Journal's native API for logging. Key features are: based on a connection-less socket wo

Dec 23, 2022
Structured logging package for Go.
Structured logging package for Go.

Package log implements a simple structured logging API inspired by Logrus, designed with centralization in mind. Read more on Medium. Handlers apexlog

Dec 24, 2022
LogVoyage - logging SaaS written in GoLang
LogVoyage - logging SaaS written in GoLang

No longer maintained, sorry. Completely rewritten v2 is going to be released soon. Please follow http://github.com/logvoyage LogVoyage - fast and simp

Sep 26, 2022
Structured, composable logging for Go
Structured, composable logging for Go

log15 Package log15 provides an opinionated, simple toolkit for best-practice logging in Go (golang) that is both human and machine readable. It is mo

Dec 18, 2022
Minimalistic logging library for Go.
Minimalistic logging library for Go.

logger Minimalistic logging library for Go. Blog Post Features: Advanced output filters (package and/or level) Attributes Timers for measuring perform

Nov 16, 2022
Structured, pluggable logging for Go.
Structured, pluggable logging for Go.

Logrus Logrus is a structured logger for Go (golang), completely API compatible with the standard library logger. Logrus is in maintenance-mode. We wi

Jan 9, 2023
Logur is an opinionated collection of logging best practices
Logur is an opinionated collection of logging best practices

Logur is an opinionated collection of logging best practices. Table of Contents Preface Features Installation Usage FAQ Why not just X logger? Why not

Dec 30, 2022