Source File
handler.go
Belonging Package
log/slog
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package slog
import (
)
// A Handler handles log records produced by a Logger.
//
// A typical handler may print log records to standard error,
// or write them to a file or database, or perhaps augment them
// with additional attributes and pass them on to another handler.
//
// Any of the Handler's methods may be called concurrently with itself
// or with other methods. It is the responsibility of the Handler to
// manage this concurrency.
//
// Users of the slog package should not invoke Handler methods directly.
// They should use the methods of [Logger] instead.
type Handler interface {
// Enabled reports whether the handler handles records at the given level.
// The handler ignores records whose level is lower.
// It is called early, before any arguments are processed,
// to save effort if the log event should be discarded.
// If called from a Logger method, the first argument is the context
// passed to that method, or context.Background() if nil was passed
// or the method does not take a context.
// The context is passed so Enabled can use its values
// to make a decision.
Enabled(context.Context, Level) bool
// Handle handles the Record.
// It will only be called when Enabled returns true.
// The Context argument is as for Enabled.
// It is present solely to provide Handlers access to the context's values.
// Canceling the context should not affect record processing.
// (Among other things, log messages may be necessary to debug a
// cancellation-related problem.)
//
// Handle methods that produce output should observe the following rules:
// - If r.Time is the zero time, ignore the time.
// - If r.PC is zero, ignore it.
// - Attr's values should be resolved.
// - If an Attr's key and value are both the zero value, ignore the Attr.
// This can be tested with attr.Equal(Attr{}).
// - If a group's key is empty, inline the group's Attrs.
// - If a group has no Attrs (even if it has a non-empty key),
// ignore it.
Handle(context.Context, Record) error
// WithAttrs returns a new Handler whose attributes consist of
// both the receiver's attributes and the arguments.
// The Handler owns the slice: it may retain, modify or discard it.
WithAttrs(attrs []Attr) Handler
// WithGroup returns a new Handler with the given group appended to
// the receiver's existing groups.
// The keys of all subsequent attributes, whether added by With or in a
// Record, should be qualified by the sequence of group names.
//
// How this qualification happens is up to the Handler, so long as
// this Handler's attribute keys differ from those of another Handler
// with a different sequence of group names.
//
// A Handler should treat WithGroup as starting a Group of Attrs that ends
// at the end of the log event. That is,
//
// logger.WithGroup("s").LogAttrs(ctx, level, msg, slog.Int("a", 1), slog.Int("b", 2))
//
// should behave like
//
// logger.LogAttrs(ctx, level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2)))
//
// If the name is empty, WithGroup returns the receiver.
WithGroup(name string) Handler
}
type defaultHandler struct {
ch *commonHandler
// internal.DefaultOutput, except for testing
output func(pc uintptr, data []byte) error
}
func newDefaultHandler( func(uintptr, []byte) error) *defaultHandler {
return &defaultHandler{
ch: &commonHandler{json: false},
output: ,
}
}
func (*defaultHandler) ( context.Context, Level) bool {
return >= logLoggerLevel.Level()
}
// Collect the level, attributes and message in a string and
// write it with the default log.Logger.
// Let the log.Logger handle time and file/line.
func ( *defaultHandler) ( context.Context, Record) error {
:= buffer.New()
.WriteString(.Level.String())
.WriteByte(' ')
.WriteString(.Message)
:= .ch.newHandleState(, true, " ")
defer .free()
.appendNonBuiltIns()
return .output(.PC, *)
}
func ( *defaultHandler) ( []Attr) Handler {
return &defaultHandler{.ch.withAttrs(), .output}
}
func ( *defaultHandler) ( string) Handler {
return &defaultHandler{.ch.withGroup(), .output}
}
// HandlerOptions are options for a [TextHandler] or [JSONHandler].
// A zero HandlerOptions consists entirely of default values.
type HandlerOptions struct {
// AddSource causes the handler to compute the source code position
// of the log statement and add a SourceKey attribute to the output.
AddSource bool
// Level reports the minimum record level that will be logged.
// The handler discards records with lower levels.
// If Level is nil, the handler assumes LevelInfo.
// The handler calls Level.Level for each record processed;
// to adjust the minimum level dynamically, use a LevelVar.
Level Leveler
// ReplaceAttr is called to rewrite each non-group attribute before it is logged.
// The attribute's value has been resolved (see [Value.Resolve]).
// If ReplaceAttr returns a zero Attr, the attribute is discarded.
//
// The built-in attributes with keys "time", "level", "source", and "msg"
// are passed to this function, except that time is omitted
// if zero, and source is omitted if AddSource is false.
//
// The first argument is a list of currently open groups that contain the
// Attr. It must not be retained or modified. ReplaceAttr is never called
// for Group attributes, only their contents. For example, the attribute
// list
//
// Int("a", 1), Group("g", Int("b", 2)), Int("c", 3)
//
// results in consecutive calls to ReplaceAttr with the following arguments:
//
// nil, Int("a", 1)
// []string{"g"}, Int("b", 2)
// nil, Int("c", 3)
//
// ReplaceAttr can be used to change the default keys of the built-in
// attributes, convert types (for example, to replace a `time.Time` with the
// integer seconds since the Unix epoch), sanitize personal information, or
// remove attributes from the output.
ReplaceAttr func(groups []string, a Attr) Attr
}
// Keys for "built-in" attributes.
const (
// TimeKey is the key used by the built-in handlers for the time
// when the log method is called. The associated Value is a [time.Time].
TimeKey = "time"
// LevelKey is the key used by the built-in handlers for the level
// of the log call. The associated value is a [Level].
LevelKey = "level"
// MessageKey is the key used by the built-in handlers for the
// message of the log call. The associated value is a string.
MessageKey = "msg"
// SourceKey is the key used by the built-in handlers for the source file
// and line of the log call. The associated value is a *[Source].
SourceKey = "source"
)
type commonHandler struct {
json bool // true => output JSON; false => output text
opts HandlerOptions
preformattedAttrs []byte
// groupPrefix is for the text handler only.
// It holds the prefix for groups that were already pre-formatted.
// A group will appear here when a call to WithGroup is followed by
// a call to WithAttrs.
groupPrefix string
groups []string // all groups started from WithGroup
nOpenGroups int // the number of groups opened in preformattedAttrs
mu *sync.Mutex
w io.Writer
}
func ( *commonHandler) () *commonHandler {
// We can't use assignment because we can't copy the mutex.
return &commonHandler{
json: .json,
opts: .opts,
preformattedAttrs: slices.Clip(.preformattedAttrs),
groupPrefix: .groupPrefix,
groups: slices.Clip(.groups),
nOpenGroups: .nOpenGroups,
w: .w,
mu: .mu, // mutex shared among all clones of this handler
}
}
// enabled reports whether l is greater than or equal to the
// minimum level.
func ( *commonHandler) ( Level) bool {
:= LevelInfo
if .opts.Level != nil {
= .opts.Level.Level()
}
return >=
}
func ( *commonHandler) ( []Attr) *commonHandler {
// We are going to ignore empty groups, so if the entire slice consists of
// them, there is nothing to do.
if countEmptyGroups() == len() {
return
}
:= .clone()
// Pre-format the attributes as an optimization.
:= .newHandleState((*buffer.Buffer)(&.preformattedAttrs), false, "")
defer .free()
.prefix.WriteString(.groupPrefix)
if := .preformattedAttrs; len() > 0 {
.sep = .attrSep()
if .json && [len()-1] == '{' {
.sep = ""
}
}
// Remember the position in the buffer, in case all attrs are empty.
:= .buf.Len()
.openGroups()
if !.appendAttrs() {
.buf.SetLen()
} else {
// Remember the new prefix for later keys.
.groupPrefix = .prefix.String()
// Remember how many opened groups are in preformattedAttrs,
// so we don't open them again when we handle a Record.
.nOpenGroups = len(.groups)
}
return
}
func ( *commonHandler) ( string) *commonHandler {
:= .clone()
.groups = append(.groups, )
return
}
// handle is the internal implementation of Handler.Handle
// used by TextHandler and JSONHandler.
func ( *commonHandler) ( Record) error {
:= .newHandleState(buffer.New(), true, "")
defer .free()
if .json {
.buf.WriteByte('{')
}
// Built-in attributes. They are not in a group.
:= .groups
.groups = nil // So ReplaceAttrs sees no groups instead of the pre groups.
:= .opts.ReplaceAttr
// time
if !.Time.IsZero() {
:= TimeKey
:= .Time.Round(0) // strip monotonic to match Attr behavior
if == nil {
.appendKey()
.appendTime()
} else {
.appendAttr(Time(, ))
}
}
// level
:= LevelKey
:= .Level
if == nil {
.appendKey()
.appendString(.String())
} else {
.appendAttr(Any(, ))
}
// source
if .opts.AddSource {
.appendAttr(Any(SourceKey, .source()))
}
= MessageKey
:= .Message
if == nil {
.appendKey()
.appendString()
} else {
.appendAttr(String(, ))
}
.groups = // Restore groups passed to ReplaceAttrs.
.appendNonBuiltIns()
.buf.WriteByte('\n')
.mu.Lock()
defer .mu.Unlock()
, := .w.Write(*.buf)
return
}
func ( *handleState) ( Record) {
// preformatted Attrs
if := .h.preformattedAttrs; len() > 0 {
.buf.WriteString(.sep)
.buf.Write()
.sep = .h.attrSep()
if .h.json && [len()-1] == '{' {
.sep = ""
}
}
// Attrs in Record -- unlike the built-in ones, they are in groups started
// from WithGroup.
// If the record has no Attrs, don't output any groups.
:= .h.nOpenGroups
if .NumAttrs() > 0 {
.prefix.WriteString(.h.groupPrefix)
// The group may turn out to be empty even though it has attrs (for
// example, ReplaceAttr may delete all the attrs).
// So remember where we are in the buffer, to restore the position
// later if necessary.
:= .buf.Len()
.openGroups()
= len(.h.groups)
:= true
.Attrs(func( Attr) bool {
if .appendAttr() {
= false
}
return true
})
if {
.buf.SetLen()
= .h.nOpenGroups
}
}
if .h.json {
// Close all open groups.
for range .h.groups[:] {
.buf.WriteByte('}')
}
// Close the top-level object.
.buf.WriteByte('}')
}
}
// attrSep returns the separator between attributes.
func ( *commonHandler) () string {
if .json {
return ","
}
return " "
}
// handleState holds state for a single call to commonHandler.handle.
// The initial value of sep determines whether to emit a separator
// before the next key, after which it stays true.
type handleState struct {
h *commonHandler
buf *buffer.Buffer
freeBuf bool // should buf be freed?
sep string // separator to write before next key
prefix *buffer.Buffer // for text: key prefix
groups *[]string // pool-allocated slice of active groups, for ReplaceAttr
}
var groupPool = sync.Pool{New: func() any {
:= make([]string, 0, 10)
return &
}}
func ( *commonHandler) ( *buffer.Buffer, bool, string) handleState {
:= handleState{
h: ,
buf: ,
freeBuf: ,
sep: ,
prefix: buffer.New(),
}
if .opts.ReplaceAttr != nil {
.groups = groupPool.Get().(*[]string)
*.groups = append(*.groups, .groups[:.nOpenGroups]...)
}
return
}
func ( *handleState) () {
if .freeBuf {
.buf.Free()
}
if := .groups; != nil {
* = (*)[:0]
groupPool.Put()
}
.prefix.Free()
}
func ( *handleState) () {
for , := range .h.groups[.h.nOpenGroups:] {
.openGroup()
}
}
// Separator for group names and keys.
const keyComponentSep = '.'
// openGroup starts a new group of attributes
// with the given name.
func ( *handleState) ( string) {
if .h.json {
.appendKey()
.buf.WriteByte('{')
.sep = ""
} else {
.prefix.WriteString()
.prefix.WriteByte(keyComponentSep)
}
// Collect group names for ReplaceAttr.
if .groups != nil {
*.groups = append(*.groups, )
}
}
// closeGroup ends the group with the given name.
func ( *handleState) ( string) {
if .h.json {
.buf.WriteByte('}')
} else {
(*.prefix) = (*.prefix)[:len(*.prefix)-len()-1 /* for keyComponentSep */]
}
.sep = .h.attrSep()
if .groups != nil {
*.groups = (*.groups)[:len(*.groups)-1]
}
}
// appendAttrs appends the slice of Attrs.
// It reports whether something was appended.
func ( *handleState) ( []Attr) bool {
:= false
for , := range {
if .appendAttr() {
= true
}
}
return
}
// appendAttr appends the Attr's key and value.
// It handles replacement and checking for an empty key.
// It reports whether something was appended.
func ( *handleState) ( Attr) bool {
.Value = .Value.Resolve()
if := .h.opts.ReplaceAttr; != nil && .Value.Kind() != KindGroup {
var []string
if .groups != nil {
= *.groups
}
// a.Value is resolved before calling ReplaceAttr, so the user doesn't have to.
= (, )
// The ReplaceAttr function may return an unresolved Attr.
.Value = .Value.Resolve()
}
// Elide empty Attrs.
if .isEmpty() {
return false
}
// Special case: Source.
if := .Value; .Kind() == KindAny {
if , := .Any().(*Source); {
if .h.json {
.Value = .group()
} else {
.Value = StringValue(fmt.Sprintf("%s:%d", .File, .Line))
}
}
}
if .Value.Kind() == KindGroup {
:= .Value.Group()
// Output only non-empty groups.
if len() > 0 {
// The group may turn out to be empty even though it has attrs (for
// example, ReplaceAttr may delete all the attrs).
// So remember where we are in the buffer, to restore the position
// later if necessary.
:= .buf.Len()
// Inline a group with an empty key.
if .Key != "" {
.openGroup(.Key)
}
if !.appendAttrs() {
.buf.SetLen()
return false
}
if .Key != "" {
.closeGroup(.Key)
}
}
} else {
.appendKey(.Key)
.appendValue(.Value)
}
return true
}
func ( *handleState) ( error) {
.appendString(fmt.Sprintf("!ERROR:%v", ))
}
func ( *handleState) ( string) {
.buf.WriteString(.sep)
if .prefix != nil && len(*.prefix) > 0 {
// TODO: optimize by avoiding allocation.
.appendString(string(*.prefix) + )
} else {
.appendString()
}
if .h.json {
.buf.WriteByte(':')
} else {
.buf.WriteByte('=')
}
.sep = .h.attrSep()
}
func ( *handleState) ( string) {
if .h.json {
.buf.WriteByte('"')
*.buf = appendEscapedJSONString(*.buf, )
.buf.WriteByte('"')
} else {
// text
if needsQuoting() {
*.buf = strconv.AppendQuote(*.buf, )
} else {
.buf.WriteString()
}
}
}
func ( *handleState) ( Value) {
defer func() {
if := recover(); != nil {
// If it panics with a nil pointer, the most likely cases are
// an encoding.TextMarshaler or error fails to guard against nil,
// in which case "<nil>" seems to be the feasible choice.
//
// Adapted from the code in fmt/print.go.
if := reflect.ValueOf(.any); .Kind() == reflect.Pointer && .IsNil() {
.appendString("<nil>")
return
}
// Otherwise just print the original panic message.
.appendString(fmt.Sprintf("!PANIC: %v", ))
}
}()
var error
if .h.json {
= appendJSONValue(, )
} else {
= appendTextValue(, )
}
if != nil {
.appendError()
}
}
func ( *handleState) ( time.Time) {
if .h.json {
appendJSONTime(, )
} else {
*.buf = appendRFC3339Millis(*.buf, )
}
}
func appendRFC3339Millis( []byte, time.Time) []byte {
// Format according to time.RFC3339Nano since it is highly optimized,
// but truncate it to use millisecond resolution.
// Unfortunately, that format trims trailing 0s, so add 1/10 millisecond
// to guarantee that there are exactly 4 digits after the period.
const = len("2006-01-02T15:04:05.000")
:= len()
= .Truncate(time.Millisecond).Add(time.Millisecond / 10)
= .AppendFormat(, time.RFC3339Nano)
= append([:+], [++1:]...) // drop the 4th digit
return
}
The pages are generated with Golds v0.7.0-preview. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |