| <!-- Autogenerated by weave; DO NOT EDIT --> |
| |
| # A Guide to Writing `slog` Handlers |
| |
| This document is maintained by Jonathan Amsterdam `jba@google.com`. |
| |
| |
| # Contents |
| |
| 1. [Introduction](#introduction) |
| 1. [Loggers and their handlers](#loggers-and-their-handlers) |
| 1. [Implementing `Handler` methods](#implementing-`handler`-methods) |
| 1. [The `Enabled` method](#the-`enabled`-method) |
| 1. [The `Handle` method](#the-`handle`-method) |
| 1. [The `WithAttrs` method](#the-`withattrs`-method) |
| 1. [The `WithGroup` method](#the-`withgroup`-method) |
| 1. [Testing](#testing) |
| 1. [General considerations](#general-considerations) |
| 1. [Concurrency safety](#concurrency-safety) |
| 1. [Robustness](#robustness) |
| 1. [Speed](#speed) |
| |
| |
| # Introduction |
| |
| The standard library’s `log/slog` package has a two-part design. |
| A "frontend," implemented by the `Logger` type, |
| gathers stuctured log information like a message, level, and attributes, |
| and passes them to a "backend," an implementation of the `Handler` interface. |
| The package comes with two built-in handlers that usually should be adequate. |
| But you may need to write your own handler, and that is not always straightforward. |
| This guide is here to help. |
| |
| |
| # Loggers and their handlers |
| |
| Writing a handler requires an understanding of how the `Logger` and `Handler` |
| types work together. |
| |
| Each logger contains a handler. Certain `Logger` methods do some preliminary work, |
| such as gathering key-value pairs into `Attr`s, and then call one or more |
| `Handler` methods. These `Logger` methods are `With`, `WithGroup`, |
| and the output methods. |
| |
| An output method fulfills the main role of a logger: producing log output. |
| Here is an example call to an output method: |
| |
| logger.Info("hello", "key", value) |
| |
| There are two general output methods, `Log`, and `LogAttrs`. For convenience, |
| there is an output method for each of four common levels (`Debug`, `Info`, |
| `Warn` and `Error`), and corresponding methods that take a context (`DebugContext`, |
| `InfoContext`, `WarnContext` and `ErrorContext`). |
| |
| Each `Logger` output method first calls its handler's `Enabled` method. If that call |
| returns true, the method constructs a `Record` from its arguments and calls |
| the handler's `Handle` method. |
| |
| As a convenience and an optimization, attributes can be added to |
| `Logger` by calling the `With` method: |
| |
| logger = logger.With("k", v) |
| |
| This call creates a new `Logger` value with the argument attributes; the |
| original remains unchanged. |
| All subsequent output from `logger` will include those attributes. |
| A logger's `With` method calls its handler's `WithAttrs` method. |
| |
| The `WithGroup` method is used to avoid avoid key collisions in large programs |
| by establishing separate namespaces. |
| |
| This call creates a new `Logger` value with a group named "g": |
| |
| logger = logger.WithGroup("g") |
| |
| All subsequent keys for `logger` will be qualified by the group name "g". |
| Exactly what "qualified" means depends on how the logger's handler formats the |
| output. |
| The built-in `TextHandler` treats the group as a prefix to the key, separated by |
| a dot: `g.k` for a key `k`, for example. |
| The built-in `JSONHandler` uses the group as a key for a nested JSON object: |
| |
| {"g": {"k": v}} |
| |
| A logger's `WithGroup` method calls its handler's `WithGroup` method. |
| |
| |
| # Implementing `Handler` methods |
| |
| We can now talk about the four `Handler` methods in detail. |
| Along the way, we will write a handler that formats logs using a format |
| reminsicent of YAML. It will display this log output call: |
| |
| logger.Info("hello", "key", 23) |
| |
| something like this: |
| |
| time: 2023-05-15T16:29:00 |
| level: INFO |
| message: "hello" |
| key: 23 |
| --- |
| |
| Although this particular output is valid YAML, |
| our implementation doesn't consider the subtleties of YAML syntax, |
| so it will sometimes produce invalid YAML. |
| For example, it doesn't quote keys that have colons in them. |
| We'll call it `IndentHandler` to forestall disappointment. |
| |
| We begin with the `IndentHandler` type |
| and the `New` function that constructs it from an `io.Writer` and options: |
| |
| ``` |
| type IndentHandler struct { |
| opts Options |
| // TODO: state for WithGroup and WithAttrs |
| mu *sync.Mutex |
| out io.Writer |
| } |
| |
| type Options struct { |
| // Level reports the minimum level to log. |
| // Levels with lower levels are discarded. |
| // If nil, the Handler uses [slog.LevelInfo]. |
| Level slog.Leveler |
| } |
| |
| func New(out io.Writer, opts *Options) *IndentHandler { |
| h := &IndentHandler{out: out, mu: &sync.Mutex{}} |
| if opts != nil { |
| h.opts = *opts |
| } |
| if h.opts.Level == nil { |
| h.opts.Level = slog.LevelInfo |
| } |
| return h |
| } |
| ``` |
| |
| We'll support only one option, the ability to set a minimum level in order to |
| supress detailed log output. |
| Handlers should always use the `slog.Leveler` type for this option. |
| `Leveler` is implemented by both `Level` and `LevelVar`. |
| A `Level` value is easy for the user to provide, |
| but changing the level of multiple handlers requires tracking them all. |
| If the user instead passes a `LevelVar`, then a single change to that `LevelVar` |
| will change the behavior of all handlers that contain it. |
| Changes to `LevelVar`s are goroutine-safe. |
| |
| The mutex will be used to ensure that writes to the `io.Writer` happen atomically. |
| Unusually, `IndentHandler` holds a pointer to a `sync.Mutex` rather than holding a |
| `sync.Mutex` directly. |
| There is a good reason for that, which we'll explain later. |
| |
| TODO(jba): add link to that later explanation. |
| |
| Our handler will need additional state to track calls to `WithGroup` and `WithAttrs`. |
| We will describe that state when we get to those methods. |
| |
| ## The `Enabled` method |
| |
| The `Enabled` method is an optimization that can avoid unnecessary work. |
| A `Logger` output method will call `Enabled` before it processes any of its arguments, |
| to see if it should proceed. |
| |
| The signature is |
| |
| Enabled(context.Context, Level) bool |
| |
| The context is available to allow decisions based on contextual information. |
| For example, a custom HTTP request header could specify a minimum level, |
| which the server adds to the context used for processing that request. |
| A handler's `Enabled` method could report whether the argument level |
| is greater than or equal to the context value, allowing the verbosity |
| of the work done by each request to be controlled independently. |
| |
| Our `IndentHandler` doesn't use the context. It just compares the argument level |
| with its configured minimum level: |
| |
| ``` |
| func (h *IndentHandler) Enabled(ctx context.Context, level slog.Level) bool { |
| return level >= h.opts.Level.Level() |
| } |
| ``` |
| |
| ## The `Handle` method |
| |
| The `Handle` method is passed a `Record` containing all the information to be |
| logged for a single call to a `Logger` output method. |
| The `Handle` method should deal with it in some way. |
| One way is to output the `Record` in some format, as `TextHandler` and `JSONHandler` do. |
| But other options are to modify the `Record` and pass it on to another handler, |
| enqueue the `Record` for later processing, or ignore it. |
| |
| The signature of `Handle` is |
| |
| Handle(context.Context, Record) error |
| |
| The context is provided to support applications that provide logging information |
| along the call chain. In a break with usual Go practice, the `Handle` method |
| should not treat a canceled context as a signal to stop work. |
| |
| If `Handle` processes its `Record`, it should follow the rules in the |
| [documentation](https://pkg.go.dev/log/slog#Handler.Handle). |
| For example, a zero `Time` field should be ignored, as should zero `Attr`s. |
| |
| A `Handle` method that is going to produce output should carry out the following steps: |
| |
| 1. Allocate a buffer, typically a `[]byte`, to hold the output. |
| It's best to construct the output in memory first, |
| then write it with a single call to `io.Writer.Write`, |
| to minimize interleaving with other goroutines using the same writer. |
| |
| 2. Format the special fields: time, level, message, and source location (PC). |
| As a general rule, these fields should appear first and are not nested in |
| groups established by `WithGroup`. |
| |
| 3. Format the result of `WithGroup` and `WithAttrs` calls. |
| |
| 4. Format the attributes in the `Record`. |
| |
| 5. Output the buffer. |
| |
| That is how our `IndentHandler`'s `Handle` method is structured: |
| |
| ``` |
| func (h *IndentHandler) Handle(ctx context.Context, r slog.Record) error { |
| buf := make([]byte, 0, 1024) |
| if !r.Time.IsZero() { |
| buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time), 0) |
| } |
| buf = h.appendAttr(buf, slog.Any(slog.LevelKey, r.Level), 0) |
| if r.PC != 0 { |
| fs := runtime.CallersFrames([]uintptr{r.PC}) |
| f, _ := fs.Next() |
| buf = h.appendAttr(buf, slog.String(slog.SourceKey, fmt.Sprintf("%s:%d", f.File, f.Line)), 0) |
| } |
| buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message), 0) |
| indentLevel := 0 |
| // TODO: output the Attrs and groups from WithAttrs and WithGroup. |
| r.Attrs(func(a slog.Attr) bool { |
| buf = h.appendAttr(buf, a, indentLevel) |
| return true |
| }) |
| buf = append(buf, "---\n"...) |
| h.mu.Lock() |
| defer h.mu.Unlock() |
| _, err := h.out.Write(buf) |
| return err |
| } |
| ``` |
| |
| The first line allocates a `[]byte` that should be large enough for most log |
| output. |
| Allocating a buffer with some initial, fairly large capacity is a simple but |
| significant optimization: it avoids the repeated copying and allocation that |
| happen when the initial slice is empty or small. |
| We'll return to this line in the section on [speed](#speed) |
| and show how we can do even better. |
| |
| The next part of our `Handle` method formats the special attributes, |
| observing the rules to ignore a zero time and a zero PC. |
| |
| Next, the method processes the result of `WithAttrs` and `WithGroup` calls. |
| We'll skip that for now. |
| |
| Then it's time to process the attributes in the argument record. |
| We use the `Record.Attrs` method to iterate over the attributes |
| in the order the user passed them to the `Logger` output method. |
| Handlers are free to reorder or de-duplicate the attributes, |
| but ours does not. |
| |
| Lastly, after adding the line "---" to the output to separate log records, |
| our handler makes a single call to `h.out.Write` with the buffer we've accumulated. |
| We hold the lock for this write to make it atomic with respect to other |
| goroutines that may be calling `Handle` at the same time. |
| |
| At the heart of the handler is the `appendAttr` method, responsible for |
| formatting a single attribute: |
| |
| ``` |
| func (h *IndentHandler) appendAttr(buf []byte, a slog.Attr, indentLevel int) []byte { |
| // Resolve the Attr's value before doing anything else. |
| a.Value = a.Value.Resolve() |
| // Ignore empty Attrs. |
| if a.Equal(slog.Attr{}) { |
| return buf |
| } |
| // Indent 4 spaces per level. |
| buf = fmt.Appendf(buf, "%*s", indentLevel*4, "") |
| switch a.Value.Kind() { |
| case slog.KindString: |
| // Quote string values, to make them easy to parse. |
| buf = fmt.Appendf(buf, "%s: %q\n", a.Key, a.Value.String()) |
| case slog.KindTime: |
| // Write times in a standard way, without the monotonic time. |
| buf = fmt.Appendf(buf, "%s: %s\n", a.Key, a.Value.Time().Format(time.RFC3339Nano)) |
| case slog.KindGroup: |
| attrs := a.Value.Group() |
| // Ignore empty groups. |
| if len(attrs) == 0 { |
| return buf |
| } |
| // If the key is non-empty, write it out and indent the rest of the attrs. |
| // Otherwise, inline the attrs. |
| if a.Key != "" { |
| buf = fmt.Appendf(buf, "%s:\n", a.Key) |
| indentLevel++ |
| } |
| for _, ga := range attrs { |
| buf = h.appendAttr(buf, ga, indentLevel) |
| } |
| default: |
| buf = fmt.Appendf(buf, "%s: %s\n", a.Key, a.Value) |
| } |
| return buf |
| } |
| ``` |
| |
| It begins by resolving the attribute, to run the `LogValuer.LogValue` method of |
| the value if it has one. All handlers should resolve every attribute they |
| process. |
| |
| Next, it follows the handler rule that says that empty attributes should be |
| ignored. |
| |
| Then it switches on the attribute kind to determine what format to use. For most |
| kinds (the default case of the switch), it relies on `slog.Value`'s `String` method to |
| produce something reasonable. It handles strings and times specially: |
| strings by quoting them, and times by formatting them in a standard way. |
| |
| When `appendAttr` sees a `Group`, it calls itself recursively on the group's |
| attributes, after applying two more handler rules. |
| First, a group with no attributes is ignored—not even its key is displayed. |
| Second, a group with an empty key is inlined: the group boundary isn't marked in |
| any way. In our case, that means the group's attributes aren't indented. |
| |
| ## The `WithAttrs` method |
| |
| One of `slog`'s performance optimizations is support for pre-formatting |
| attributes. The `Logger.With` method converts key-value pairs into `Attr`s and |
| then calls `Handler.WithAttrs`. |
| The handler may store the attributes for later consumption by the `Handle` method, |
| or it may take the opportunity to format the attributes now, once, |
| rather than doing so repeatedly on each call to `Handle`. |
| |
| The signature of the `WithAttrs` method is |
| |
| WithAttrs(attrs []Attr) Handler |
| |
| The attributes are the processed key-value pairs passed to `Logger.With`. |
| The return value should be a new instance of your handler that contains |
| the attributes, possibly pre-formatted. |
| |
| `WithAttrs` must return a new handler with the additional attributes, leaving |
| the original handler (its receiver) unchanged. For example, this call: |
| |
| logger2 := logger1.With("k", v) |
| |
| creates a new logger, `logger2`, with an additional attribute, but has no |
| effect on `logger1`. |
| |
| We will show example implementations of `WithAttrs` below, when we discuss `WithGroup`. |
| |
| ## The `WithGroup` method |
| |
| `Logger.WithGroup` calls `Handler.WithGroup` directly, with the same |
| argument, the group name. |
| A handler should remember the name so it can use it to qualify all subsequent |
| attributes. |
| |
| The signature of `WithGroup` is: |
| |
| WithGroup(name string) Handler |
| |
| Like `WithAttrs`, the `WithGroup` method should return a new handler, not modify |
| the receiver. |
| |
| The implementations of `WithGroup` and `WithAttrs` are intertwined. |
| Consider this statement: |
| |
| logger = logger.WithGroup("g1").With("k1", 1).WithGroup("g2").With("k2", 2) |
| |
| Subsequent `logger` output should qualify key "k1" with group "g1", |
| and key "k2" with groups "g1" and "g2". |
| The order of the `Logger.WithGroup` and `Logger.With` calls must be respected by |
| the implementations of `Handler.WithGroup` and `Handler.WithAttrs`. |
| |
| We will look at two implementations of `WithGroup` and `WithAttrs`, one that pre-formats and |
| one that doesn't. |
| |
| ### Without pre-formatting |
| |
| Our first implementation will collect the information from `WithGroup` and |
| `WithAttrs` calls to build up a slice of group names and attribute lists, |
| and loop over that slice in `Handle`. We start with a struct that can hold |
| either a group name or some attributes: |
| |
| ``` |
| // groupOrAttrs holds either a group name or a list of slog.Attrs. |
| type groupOrAttrs struct { |
| group string // group name if non-empty |
| attrs []slog.Attr // attrs if non-empty |
| } |
| ``` |
| |
| Then we add a slice of `groupOrAttrs` to our handler: |
| |
| ``` |
| type IndentHandler struct { |
| opts Options |
| goas []groupOrAttrs |
| mu *sync.Mutex |
| out io.Writer |
| } |
| ``` |
| |
| As stated above, The `WithGroup` and `WithAttrs` methods should not modify their |
| receiver. |
| To that end, we define a method that will copy our handler struct |
| and append one `groupOrAttrs` to the copy: |
| |
| ``` |
| func (h *IndentHandler) withGroupOrAttrs(goa groupOrAttrs) *IndentHandler { |
| h2 := *h |
| h2.goas = make([]groupOrAttrs, len(h.goas)+1) |
| copy(h2.goas, h.goas) |
| h2.goas[len(h2.goas)-1] = goa |
| return &h2 |
| } |
| ``` |
| |
| Most of the fields of `IndentHandler` can be copied shallowly, but the slice of |
| `groupOrAttrs` requires a deep copy, or the clone and the original will point to |
| the same underlying array. If we used `append` instead of making an explicit |
| copy, we would introduce that subtle aliasing bug. |
| |
| Using `withGroupOrAttrs`, the `With` methods are easy: |
| |
| ``` |
| func (h *IndentHandler) WithGroup(name string) slog.Handler { |
| if name == "" { |
| return h |
| } |
| return h.withGroupOrAttrs(groupOrAttrs{group: name}) |
| } |
| |
| func (h *IndentHandler) WithAttrs(attrs []slog.Attr) slog.Handler { |
| if len(attrs) == 0 { |
| return h |
| } |
| return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) |
| } |
| ``` |
| |
| The `Handle` method can now process the groupOrAttrs slice after |
| the built-in attributes and before the ones in the record: |
| |
| ``` |
| func (h *IndentHandler) Handle(ctx context.Context, r slog.Record) error { |
| buf := make([]byte, 0, 1024) |
| if !r.Time.IsZero() { |
| buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time), 0) |
| } |
| buf = h.appendAttr(buf, slog.Any(slog.LevelKey, r.Level), 0) |
| if r.PC != 0 { |
| fs := runtime.CallersFrames([]uintptr{r.PC}) |
| f, _ := fs.Next() |
| buf = h.appendAttr(buf, slog.String(slog.SourceKey, fmt.Sprintf("%s:%d", f.File, f.Line)), 0) |
| } |
| buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message), 0) |
| indentLevel := 0 |
| // Handle state from WithGroup and WithAttrs. |
| goas := h.goas |
| if r.NumAttrs() == 0 { |
| // If the record has no Attrs, remove groups at the end of the list; they are empty. |
| for len(goas) > 0 && goas[len(goas)-1].group != "" { |
| goas = goas[:len(goas)-1] |
| } |
| } |
| for _, goa := range goas { |
| if goa.group != "" { |
| buf = fmt.Appendf(buf, "%*s%s:\n", indentLevel*4, "", goa.group) |
| indentLevel++ |
| } else { |
| for _, a := range goa.attrs { |
| buf = h.appendAttr(buf, a, indentLevel) |
| } |
| } |
| } |
| r.Attrs(func(a slog.Attr) bool { |
| buf = h.appendAttr(buf, a, indentLevel) |
| return true |
| }) |
| buf = append(buf, "---\n"...) |
| h.mu.Lock() |
| defer h.mu.Unlock() |
| _, err := h.out.Write(buf) |
| return err |
| } |
| ``` |
| |
| You may have noticed that our algorithm for |
| recording `WithGroup` and `WithAttrs` information is quadratic in the |
| number of calls to those methods, because of the repeated copying. |
| That is unlikely to matter in practice, but if it bothers you, |
| you can use a linked list instead, |
| which `Handle` will have to reverse or visit recursively. |
| See [github.com/jba/slog/withsupport](https://github.com/jba/slog/withsupport) for an implementation. |
| |
| ### With pre-formatting |
| |
| Our second implementation implements pre-formatting. |
| This implementation is more complicated than the previous one. |
| Is the extra complexity worth it? |
| That depends on your circumstances, but here is one circumstance where |
| it might be. |
| Say that you wanted your server to log a lot of information about an incoming |
| request with every log message that happens during that request. A typical |
| handler might look something like this: |
| |
| func (s *Server) handleWidgets(w http.ResponseWriter, r *http.Request) { |
| logger := s.logger.With( |
| "url", r.URL, |
| "traceID": r.Header.Get("X-Cloud-Trace-Context"), |
| // many other attributes |
| ) |
| // ... |
| } |
| |
| A single handleWidgets request might generate hundreds of log lines. |
| For instance, it might contain code like this: |
| |
| for _, w := range widgets { |
| logger.Info("processing widget", "name", w.Name) |
| // ... |
| } |
| |
| For every such line, the `Handle` method we wrote above will format all |
| the attributes that were added using `With` above, in addition to the |
| ones on the log line itself. |
| |
| Maybe all that extra work doesn't slow down your server significantly, because |
| it does so much other work that time spent logging is just noise. |
| But perhaps your server is fast enough that all that extra formatting appears high up |
| in your CPU profiles. That is when pre-formatting can make a big difference, |
| by formatting the attributes in a call to `With` just once. |
| |
| To pre-format the arguments to `WithAttrs`, we need to keep track of some |
| additional state in the `IndentHandler` struct. |
| |
| ``` |
| type IndentHandler struct { |
| opts Options |
| preformatted []byte // data from WithGroup and WithAttrs |
| unopenedGroups []string // groups from WithGroup that haven't been opened |
| indentLevel int // same as number of opened groups so far |
| mu *sync.Mutex |
| out io.Writer |
| } |
| ``` |
| |
| Mainly, we need a buffer to hold the pre-formatted data. |
| But we also need to keep track of which groups |
| we've seen but haven't output yet. We'll call those groups "unopened." |
| We also need to track how many groups we've opened, which we can do |
| with a simple counter, since an opened group's only effect is to change the |
| indentation level. |
| |
| The `WithGroup` implementation is a lot like the previous one: just remember the |
| new group, which is unopened initially. |
| |
| ``` |
| func (h *IndentHandler) WithGroup(name string) slog.Handler { |
| if name == "" { |
| return h |
| } |
| h2 := *h |
| // Add an unopened group to h2 without modifying h. |
| h2.unopenedGroups = make([]string, len(h.unopenedGroups)+1) |
| copy(h2.unopenedGroups, h.unopenedGroups) |
| h2.unopenedGroups[len(h2.unopenedGroups)-1] = name |
| return &h2 |
| } |
| ``` |
| |
| `WithAttrs` does all the pre-formatting: |
| |
| ``` |
| func (h *IndentHandler) WithAttrs(attrs []slog.Attr) slog.Handler { |
| if len(attrs) == 0 { |
| return h |
| } |
| h2 := *h |
| // Force an append to copy the underlying array. |
| pre := slices.Clip(h.preformatted) |
| // Add all groups from WithGroup that haven't already been added. |
| h2.preformatted = h2.appendUnopenedGroups(pre, h2.indentLevel) |
| // Each of those groups increased the indent level by 1. |
| h2.indentLevel += len(h2.unopenedGroups) |
| // Now all groups have been opened. |
| h2.unopenedGroups = nil |
| // Pre-format the attributes. |
| for _, a := range attrs { |
| h2.preformatted = h2.appendAttr(h2.preformatted, a, h2.indentLevel) |
| } |
| return &h2 |
| } |
| |
| func (h *IndentHandler) appendUnopenedGroups(buf []byte, indentLevel int) []byte { |
| for _, g := range h.unopenedGroups { |
| buf = fmt.Appendf(buf, "%*s%s:\n", indentLevel*4, "", g) |
| indentLevel++ |
| } |
| return buf |
| } |
| ``` |
| |
| It first opens any unopened groups. This handles calls like: |
| |
| logger.WithGroup("g").WithGroup("h").With("a", 1) |
| |
| Here, `WithAttrs` must output "g" and "h" before "a". Since a group established |
| by `WithGroup` is in effect for the rest of the log line, `WithAttrs` increments |
| the indentation level for each group it opens. |
| |
| Lastly, `WithAttrs` formats its argument attributes, using the same `appendAttr` |
| method we saw above. |
| |
| It's the `Handle` method's job to insert the pre-formatted material in the right |
| place, which is after the built-in attributes and before the ones in the record: |
| |
| ``` |
| func (h *IndentHandler) Handle(ctx context.Context, r slog.Record) error { |
| buf := make([]byte, 0, 1024) |
| if !r.Time.IsZero() { |
| buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time), 0) |
| } |
| buf = h.appendAttr(buf, slog.Any(slog.LevelKey, r.Level), 0) |
| if r.PC != 0 { |
| fs := runtime.CallersFrames([]uintptr{r.PC}) |
| f, _ := fs.Next() |
| buf = h.appendAttr(buf, slog.String(slog.SourceKey, fmt.Sprintf("%s:%d", f.File, f.Line)), 0) |
| } |
| buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message), 0) |
| // Insert preformatted attributes just after built-in ones. |
| buf = append(buf, h.preformatted...) |
| if r.NumAttrs() > 0 { |
| buf = h.appendUnopenedGroups(buf, h.indentLevel) |
| r.Attrs(func(a slog.Attr) bool { |
| buf = h.appendAttr(buf, a, h.indentLevel+len(h.unopenedGroups)) |
| return true |
| }) |
| } |
| buf = append(buf, "---\n"...) |
| h.mu.Lock() |
| defer h.mu.Unlock() |
| _, err := h.out.Write(buf) |
| return err |
| } |
| ``` |
| |
| It must also open any groups that haven't yet been opened. The logic covers |
| log lines like this one: |
| |
| logger.WithGroup("g").Info("msg", "a", 1) |
| |
| where "g" is unopened before `Handle` is called and must be written to produce |
| the correct output: |
| |
| level: INFO |
| msg: "msg" |
| g: |
| a: 1 |
| |
| The check for `r.NumAttrs() > 0` handles this case: |
| |
| logger.WithGroup("g").Info("msg") |
| |
| Here there are no record attributes, so no group to open. |
| |
| ## Testing |
| |
| To verify that your handler follows these rules and generally produces proper |
| output, use the [testing/slogtest package](https://pkg.go.dev/log/slog). |
| |
| TODO(jba): show the test function. |
| |
| TODO(jba): reintroduce the material on Record.Clone that used to be here. |
| |
| # General considerations |
| |
| ## Concurrency safety |
| |
| A handler must work properly when a single `Logger` is shared among several |
| goroutines. |
| That means that mutable state must be protected with a lock or some other mechanism. |
| In practice, this is not hard to achieve, because many handlers won't have any |
| mutable state. |
| |
| - The `Enabled` method typically consults only its arguments and a configured |
| level. The level is often either set once initially, or is held in a |
| `LevelVar`, which is already concurrency-safe. |
| |
| - The `WithAttrs` and `WithGroup` methods should not modify the receiver, |
| for reasons discussed above. |
| |
| - The `Handle` method typically works only with its arguments and stored fields. |
| |
| Calls to output methods like `io.Writer.Write` should be synchronized unless |
| it can be verified that no locking is needed. Beware of facile claims like |
| "Unix writes are atomic"; the situation is a lot more nuanced than that. |
| |
| Some handlers have legitimate reasons for keeping state. |
| For example, a handler might support a `SetLevel` method to change its configured level |
| dynamically. |
| Or it might output the time between sucessive calls to `Handle`, |
| which requires a mutable field holding the last output time. |
| Synchronize all accesses to such fields, both reads and writes. |
| |
| The built-in handlers have no directly mutable state. |
| They use a mutex only to sequence calls to their contained `io.Writer`. |
| |
| ## Robustness |
| |
| Logging is often the debugging technique of last resort. When it is difficult or |
| impossible to inspect a system, as is typically the case with a production |
| server, logs provide the most detailed way to understand its behavior. |
| Therefore, your handler should be robust to bad input. |
| |
| For example, the usual advice when when a function discovers a problem, |
| like an invalid argument, is to panic or return an error. |
| The built-in handlers do not follow that advice. |
| Few things are more frustrating than being unable to debug a problem that |
| causes logging to fail; |
| our feeling is that it is |
| better to produce some output, however imperfect, than to produce none at all. |
| That is why methods like `Logger.Info` convert programming bugs in their list of |
| key-value pairs, like missing values or malformed keys, |
| into `Attr`s that contain as much information as possible. |
| |
| One place to avoid panics is in processing attribute values. A handler that wants |
| to format a value will probably switch on the value's kind: |
| |
| switch attr.Value.Kind() { |
| case KindString: ... |
| case KindTime: ... |
| // all other Kinds |
| default: ... |
| } |
| |
| What should happen in the default case, when the handler encounters a `Kind` |
| that it doesn't know about? |
| The built-in handlers try to muddle through by using the result of the value's |
| `String` method, as our example handler does. |
| They do not panic or return an error. |
| Your own handlers might in addition want to report the problem through your production monitoring |
| or error-tracking telemetry system. |
| The most likely explanation for the issue is that a newer version of the `slog` package added |
| a new `Kind`—a backwards-compatible change under the Go 1 Compatibility |
| Promise—and the handler wasn't updated. |
| That is certainly a problem, but it shouldn't deprive |
| readers of the logs from seeing the rest of the output. |
| |
| There is one circumstance where returning an error from `Handler.Handle` is appropriate. |
| If the output operation itself fails, the best course of action is to report |
| this failure by returning the error. For instance, the last two lines of the |
| built-in `Handle` methods are |
| |
| _, err := h.w.Write(*state.buf) |
| return err |
| |
| Although the output methods of `Logger` ignore the error, one could write a |
| handler that does something with it, perhaps falling back to writing to standard |
| error. |
| |
| ## Speed |
| |
| TODO(jba): discuss |
| |
| TODO(jba): show how to pool a []byte. |