structured, leveled logging #54763
Replies: 65 comments 294 replies
-
👋🏻 hclog developer here! Curious about seeing if the stdlib could mostly contain the interface surface area for different implementations to step into. Were that route taken, I'd propose the surface area that looks something like this: type Logger interface {
Log(level int, msg string, args ...any)
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
With(args ...any) Logger
}
var DefaultLogger Logger
func Debug(msg string, args ...any) {
DefaultLogger.Debug(msg, args...)
}
// Toplevel for Info, Warn, Error, and With
func InContext(ctx context.Context, log Logger) context.Context { ... }
func FromContext(ctx context.Context) Logger { ... } // returns DefaultLogger if none available |
Beta Was this translation helpful? Give feedback.
-
I have a use case where I would like to control logging level based on context (e.g. enabling However, this context instrumentation needs to happen before any other (for example context may already have a |
Beta Was this translation helpful? Give feedback.
-
What would happen if |
Beta Was this translation helpful? Give feedback.
-
Looks promising. Some thoughts:
Why is this being proposed now? Why not 5–8 years ago? This seems to tout handlers as an innovation over the state of the art. Do none of the existing solutions have a comparable handler design? Will this effort to establish a common interface for the community extend to other problem domains, like audio?
In my opinion, the loose What happens if a value is missing?
What if there isn't a Go error value when logging an error condition? What value should be used?
Could this hide configuration or setup mistakes, where there should have been a logger, but there wasn't?
Why Is DebugLevel 31, when the other levels increment by 10?
What is the exact intended initial format, whether or not you want to document it? What is the order of the fields? Are the "built-in" fields first? Are the pairs delimited by a space? Are strings containing whitespace quoted?
Is the JSON minimized? What is the order of the fields? Are the "built-in" fields first?
What are the default keys?
Nit: Consider using the names Field/Fields, like zap. "Attr" is a little more abstract than "field," and "WithFields" is a little clearer and reads a little better than "WithAttrs," in my humble opinion.
Why not include variants for all built-in types, like Int8, Rune or Complex64? Would generics help here?
Have you considered the potential demand for a stack trace option (as opposed to just the current file name and line)?
I don't see a Logger.WithAttrs variant. Is that because it wouldn't help avoid allocations? If so, why is that? |
Beta Was this translation helpful? Give feedback.
-
👋🏽 I'm glad to see this discussion reignited! I'm one of the original zap authors, and may have a little additional context to share. Points below notwithstanding, this proposal seems carefully thought-through and researched. Many thanks to the authors for all the work they've put in 😍 Previous artPeter Bourgon and Chris Hines made a proposal with similar goals in 2017. The overall situation hasn't changed much since then - if anything, the proliferation of log-like APIs in distributed tracing packages has made it even worse. If the authors of this proposal haven't seen the previous doc, it's worth reviewing. I'm not sure if Chris and Peter are still interested in logging, but their perspectives would be valuable here. I particularly liked the very small interface they proposed and the NamespacingI notice that this proposal doesn't include support for namespacing key-value pairs. For example, we might want a gRPC interceptor to produce JSON records like this:
Zap exposes this via Namespace. At least at Uber, this was critical for practical use: like many storage backends, ElasticSearch stops indexing if a field changes type. Implicitly, this forces each binary to have a schema for its log output. It's very hard to do this if dozens of loosely-coordinated packages are writing to a shared, flat keyspace. Namespacing cuts the problem into manageable pieces: each package uses its own name as a namespace, within which it can attempt to adhere to some implicit schema. This problem is significant enough that people have attempted to write static analysis to mitigate it. Of course, not all storage engines have this limitation - but many do. Food for thought. First-class error supportThe Zap spends a fair bit of effort to special-case Edit: on a second reading, I see the Delegating serialization & complex objectsIt's convenient for end users to let types implement their own serialization logic (a la GenericsMy experience with zap is that the mere existence of a faster API, even it's painfully verbose, puts a lot of pressure on developers to use it. Making the lower-allocation API as ergonomic as possible has a disproportionate effect on developer experience. One of my enduring regrets about zap is its fussy API for producing fields. Nowadays, this seems like the sort of problem we ought to be able to solve nicely with a type Attr[T any] struct {
key string
value T
} Minimizing allocations would require a fast path through marshaling that doesn't go through
|
Beta Was this translation helpful? Give feedback.
-
This allows changing or omitting an Attr, but not splitting it out into multiple.
I'd like to see it implement
I'd like to see a standard handler that forwards to
zerolog provides a global logger as a separate package, The other recent (big?) project that I'm aware of in logging standardization is OpenTelemetry's Log Data Model, which has recently been stabilized. There they chose a different mapping of Levels / Severity to numbers, I'll echo the desire for namespaced key-values, |
Beta Was this translation helpful? Give feedback.
-
Thanks for starting this discussion. The sheer amount of logging libraries out there seems to be an indicator that there should be a solution for this in the standard library. A couple thoughts about the proposal: OpenTelemetryI think it would make sense to look at the work the OpenTelemetry working group does...at least to make sure this proposal is not incompatible with what they are working on. I know they don't focus on libraries itself at the moment, but a wire format for logs. Sugared loggerPersonally, I'm quite fond of Zap's default logger enforcing attributes to be Maybe reflecting that in this proposal would also make sense by creating separate implementations. AttrI was wondering if it would make sense to make Attr an interface: type Attr interface {
Key() string
Value() string
} That way converting a value to string could be handed off to a custom Attr implementation. I don't know if that would affect allocations though. Maybe they would, but it's good enough for Zap. This would also allow to create separate types instead of using a single Attr with an I was wondering what the purpose of Kind is. Where would it be useful? ContextPersonally, I prefer extracting information from the context in the logger, not the other way around. Let's say a context travels through a set of layers, each of them adding information to the context that you would like to log. Therefore I'd want to pass the context to the logger somehow, not the other way around. For example, I could implement a handler that extracts all the relevant information from the context. The only question is: how do we pass a context to the logger? |
Beta Was this translation helpful? Give feedback.
-
I think this is promising. A bunch of thoughts:
|
Beta Was this translation helpful? Give feedback.
-
Using genericsIn one of the comments above, there was mention of the possibility of using generics to mitigate some of the API blowup from having many explicitly different I'll expand a bit on my reply above. I think we can use generics to mitigate allocations and reduce the API, even in the absence of #45380. Here's how the API might look. We'd remove the
Another possibility to make the performance implications of creating an
Another possible spelling for As I mentioned in the original comment, it's possible to implement this API without incurring allocations, although there is some performance overhead. |
Beta Was this translation helpful? Give feedback.
-
The docs should document what should happen when |
Beta Was this translation helpful? Give feedback.
-
"attrs" in func (l *Logger) With(attrs ...any) *Logger should be "args" like in func (l *Logger) Warn(msg string, args ...any) to not suggest that attrs must be Attrs. |
Beta Was this translation helpful? Give feedback.
-
This discussion has led to a proposal and is now finished. Please comment on the proposal.
We would like to add structured logging with levels to the standard library. Structured logging is the ability to output logs with machine-readable structure, typically key-value pairs, in addition to a human-readable message. Structured logs can be parsed, filtered, searched and analyzed faster and more reliably than logs designed only for people to read. For many programs that aren't run directly by person, like servers, logging is the main way for developers to observe the detailed behavior of the system, and often the first place they go to debug it. Logs therefore tend to be voluminous, and the ability to search and filter them quickly is essential.
In theory, one can produce structured logs with any logging package:
In practice, this is too tedious and error-prone, so structured logging packages provide an API for expressing key-value pairs. This draft proposal contains such an API.
We also propose generalizing the logging "backend." The
log
package provides control only over theio.Writer
that logs are written to. In the new package (tentative name:log/slog
), every logger has a handler that can process a log event however it wishes. Although it is possible to have a structured logger with a fixed backend (for instance, zerolog outputs only JSON), having a flexible backend provides several benefits: programs can display the logs in a variety of formats, convert them to an RPC message for a network logging service, store them for later processing, and add to or modify the data.Lastly, we include levels in our design, in a way that accommodates both traditional named levels and logr-style verbosities.
Our goals are:
Ease of use. A survey of the existing logging packages shows that programmers want an API that is light on the page and easy to understand. This proposal adopts the most popular way to express key-value pairs, alternating keys and values.
High performance. The API has been designed to minimize allocation and locking. It provides an alternative to alternating keys and values that is more cumbersome but faster (similar to Zap's
Field
s).Integration with runtime tracing. The Go team is developing an improved runtime tracing system. Logs from this package will be incorporated seamlessly into those traces, giving developers the ability to correlate their program's actions with the behavior of the runtime.
What Does Success Look Like?
Go has many popular structured logging packages, all good at what they do. We do not expect developers to rewrite their existing third-party structured logging code en masse to use this new package. We expect existing logging packages to coexist with this one for the foreseeable future.
We have tried to provide an API that is pleasant enough to prefer to existing packages in new code, if only to avoid a dependency. (Some developers may find the runtime tracing integration compelling.) We also expect newcomers to Go to become familiar with this package before learning third-party packages, so they will naturally prefer it.
But more important than any traction gained by the "frontend" is the promise of a common "backend." An application with many dependencies may find that it has linked in many logging packages. If all of the packages support the handler interface we propose, then the application can create a single handler and install it once for each logging library to get consistent logging across all its dependencies. Since this happens in the application's main function, the benefits of a unified backend can be obtained with minimal code churn. We hope that this proposal's handlers will be implemented for all popular logging formats and network protocols, and that every common logging framework will provide a shim from their own backend to a handler. Then the Go logging community can work together to build high-quality backends that all can share.
Prior Work
The existing
log
package has been in the standard library since the release of Go 1 in March 2012. It provides formatted logging, but not structured logging or levels.Logrus, one of the first structured logging packages, showed how an API could add structure while preserving the formatted printing of the
log
package. It uses maps to hold key-value pairs, which is relatively inefficient.Zap grew out of Uber's frustration with the slow log times of their high-performance servers. It showed how a logger that avoided allocations could be very fast.
zerolog reduced allocations even further, but at the cost of reducing the flexibility of the logging backend.
All the above loggers include named levels along with key-value pairs. Logr and Google's own glog use integer verbosities instead of named levels, providing a more fine-grained approach to filtering high-detail logs.
Other popular logging packages are Go-kit's log, HashiCorp's hclog, and klog.
Overview of the Design
Here is a short program that uses some of the new API:
It begins by setting the default logger to one that writes log records in an easy-to-read format similar to logfmt . (There is also a built-in handler for JSON.)
The program then outputs three log messages augmented with key-value pairs. The first logs at the Info level, passing a single key-value pair along with the message. The second logs at the Error level, passing an
error
and a key-value pair.The third produces the same output as the second, but more efficiently. Functions like
Any
andInt
constructslog.Attr
values, which are key-value pairs that avoid memory allocation for some values.slog.Attr
is modeled onzap.Field
.The Design
Interaction Between Existing and New Behavior
The
slog
package works to ensure consistent output with thelog
package. Writing toslog
's default logger without setting a handler will write structured text tolog
's default logger. Once a handler is set, as in the example above, the defaultlog
logger will send its text output to the structured handler.Handlers
A
slog.Handler
describes the logging backend. It is defined as:The main method is
Handle
. It accepts aslog.Record
with the timestamp, message, level, caller source position, and key-value pairs of the log event. Each call to aLogger
output method, likeInfo
,Error
orLogAttrs
, creates aRecord
and invokes theHandle
method.The
Enabled
method is an optimization that can save effort if the log event should be discarded.Enabled
is called early, before any arguments are processed.The
With
method is called byLogger.With
, discussed below.The
slog
package provides two handlers, one for simple textual output and one for JSON. They are described in more detail below.The
Record
TypeThe
Record
passed to a handler exportsTime
,Message
andLevel
methods, as well as four methods for accessing the sequence ofAttr
s:Attrs() []Attr
returns a copy of theAttr
s as a slice.NumAttrs() int
returns the number ofAttr
s.Attr(int) Attr
returns the i'thAttr
.SetAttrs([]Attr)
replaces the sequence ofAttr
s with the given slice.This API allows an efficient implementation of the
Attr
sequence that avoids copying and minimizes allocation.SetAttrs
supports "middleware" handlers that want to alter theAttr
s, say by removing those that contain sensitive data.The
Attr
TypeThe
Attr
type efficiently represents a key-value pair. The key is a string. The value can be any type, butAttr
improves onany
by storing common types without allocating memory. In particular, integer types and strings, which account for the vast majority of values in log messages, do not require allocation. The default version ofAttr
uses packageunsafe
to store any value in three machine words. The version withoutunsafe
requires five.There are convenience functions for constructing
Attr
s with various value types:Int(k string, v int) Attr
Int64(k string, v int64) Attr
Uint64(k string, v uint64) Attr
Float64(k string, v float64) Attr
String(k, v string) Attr
Bool(k string, v bool) Attr
Duration(k string, v time.Duration) Attr
Time(k string, v time.Time) Attr
Any(k string, v any) Attr
The last of these dispatches on the type of
v
, using a more efficient representation ifAttr
supports it and falling back to anany
field inAttr
if not.The
Attr.Key
method returns the key. Extracting values from anAttr
is reminiscent ofreflect.Value
: there is aKind
method that returns an enum, and a variety of methods likeInt64() int64
andBool() bool
that return the value or panic if it is the wrong kind.Attr
also has anEqual
method, and anAppendValue
method that efficiently appends a string representation of the value to a[]byte
, in the manner of thestrconv.AppendX
functions.Loggers
A
Logger
consists of a handler and a list ofAttr
s. There is a default logger with no attributes whose handler writes to the defaultlog.Logger
, as explained above. Create aLogger
withNew
:To add attributes to a Logger, use
With
:The arguments are interpreted as alternating string keys and and arbitrary values, which are converted to
Attr
s.Attr
s can also be passed directly. Loggers are immutable, so this actually creates a new Logger with the additional attributes. To allow handlers to preprocess attributes, the new Logger’s handler is obtained by callingHandler.With
on the old one.You can obtain a logger's handler with
Logger.Handler
.The basic logging methods are
which logs a message at the given level with a list of attributes that are interpreted just as in
Logger.With
, and the more efficientThese functions first call
Handler.Enabled(level)
to see if they should proceed. If so, they create aRecord
with the current time, the given level and message, and a list of attributes that consists of the receiver's attributes followed by the argument attributes. They then pass theRecord
toHandler.Handle
.Each of these methods has an alternative form that takes a call depth, so other functions can wrap them and adjust the source line information.
There are four convenience methods for common levels:
They all call
Log
with the appropriate level.Error
first appendsAny("err", err)
to the attributes.There are no convenience methods for
LogAttrs
. We expect that most programmers will use the more convenient API; those few who need the extra speed will have to type more, or provide wrapper functions.All the methods described in this section are also names of top-level functions that call the corresponding method on the default logger.
Context Support
Passing a logger in a
context.Context
is a common practice and a good way to include dynamically scoped information in log messages. For instance, you could construct aLogger
with information from anhttp.Request
and pass it through the code that handles the request by adding it tor.Context()
.The
slog
package has two functions to support this pattern. One adds aLogger
to a context:As an example, an HTTP server might want to create a new
Logger
for each request. The logger would contain request-wide attributes and be stored in the context for the request:To retrieve a
Logger
from a context, callFromContext
:FromContext
returns the default logger if it can't find one in the context.Levels
A level is a positive integer, where lower numbers designate more severe or important log events. The
slog
package provides names for common levels, with gaps between the assigned numbers to accommodate other level schemes. (For example, Google Cloud Platform supports a Notice level between Info and Warn.)Some logging packages like glog and Logr use verbosities instead, where a verbosity of 0 corresponds to the Info level and higher values represent less important messages. To use a verbosity of
v
with this design, passslog.InfoLevel + v
toLog
orLogAttrs
.Provided Handlers
The
slog
package includes two handlers, which behave similarly except for their output format.TextHandler
emits attributes asKEY=VALUE
, andJSONHandler
writes line-delimited JSON objects. Both can be configured with the same options:The boolean
AddSource
option controls whether the file and line of the log call. It is false by default, because there is a small cost to extracting this information.The
LevelRef
option, of typeLevelRef
, provides control over the maximum level that the handler will output. For example, setting a handler's LevelRef to Info will suppress output at Debug and higher levels. ALevelRef
is a safely mutable pointer to a level, which makes it easy to dynamically and atomically change the logging level for an entire program.To provide fine control over output, the
ReplaceAttr
option is a function that both accepts and returns anAttr
. If present, it is called for every attribute in the log record, including the four built-in ones for time, message, level and (if AddSource is true) the source position.ReplaceAttr
can be used to change the default keys of the built-in attributes, convert types (for example, to replace atime.Time
with the integer seconds since the Unix epoch), sanitize personal information, or remove attributes from the output.Interoperating with Other Log Packages
As stated earlier, we expect that this package will interoperate with other log packages.
One way that could happen is for another package's frontend to send
slog.Record
s to aslog.Handler
. For instance, alogr.LogSink
implementation could construct aRecord
from a message and list of keys and values, and pass it to aHandler
. To facilitate that,slog
provides a way to constructRecord
s directly and add attributes to it:Another way for two log packages to work together is for the other package to wrap its backend as a
slog.Handler
, so users could write code with theslog
package's API but connect the results to an existinglogr.LogSink
, for example. This involves writing aslog.Handler
that wraps the other logger's backend. Doing so doesn't seem to require any additional support from this package.Acknowledgements
Ian Cottrell's ideas about high-performance observability, captured in the
golang.org/x/exp/event
package, informed a great deal of the design and implementation of this proposal.Seth Vargo’s ideas on logging were a source of motivation and inspiration. His comments on an earlier draft helped improve the proposal.
Michael Knyszek explained how logging could work with runtime tracing.
Tim Hockin helped us understand logr's design choices, which led to significant improvements.
Abhinav Gupta helped me understand Zap in depth, which informed the design.
Russ Cox provided valuable feedback and helped shape the final design.
Appendix: API
Beta Was this translation helpful? Give feedback.
All reactions