blob: 5ab9fbf4490ecccf5802c64d5d7d545d723d45b1 [file] [log] [blame]
// 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 (
"fmt"
"io"
"log"
"strings"
"sync"
"time"
"golang.org/x/exp/slog/internal/buffer"
)
// 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.
type Handler interface {
// Enabled reports whether the handler handles records at the given level.
// The handler ignores records whose level is lower.
Enabled(Level) bool
// Handle handles the Record.
// Handle methods that produce output should observe the following rules:
// - If r.Time() is the zero time, ignore the time.
// - If r.Level() is Level(0), ignore the level.
// - If an Attr's key is the empty string, ignore the Attr.
Handle(r Record) error
// With returns a new Handler whose attributes consist of
// the receiver's attributes concatenated with the arguments.
// The Handler owns the slice: it may retain, modify or discard it.
With(attrs []Attr) Handler
}
type defaultHandler struct {
attrs []Attr
}
func (*defaultHandler) Enabled(Level) bool { return true }
// 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 (h *defaultHandler) Handle(r Record) error {
var b strings.Builder
if r.Level() > 0 {
b.WriteString(r.Level().String())
b.WriteByte(' ')
}
r.Attrs(func(a Attr) {
fmt.Fprint(&b, a) // Attr.Format will print key=value
b.WriteByte(' ')
})
b.WriteString(r.Message())
return log.Output(4, b.String())
}
func (d *defaultHandler) With(as []Attr) Handler {
d2 := *d
d2.attrs = concat(d2.attrs, as)
return &d2
}
// HandlerOptions are options for a TextHandler or JSONHandler.
// A zero HandlerOptions consists entirely of default values.
type HandlerOptions struct {
// Add a "source" attribute to the output whose value is of the form
// "file:line".
AddSource bool
// Ignore records with levels above Level.Level.
// If nil, accept all levels.
Level *AtomicLevel
// If set, ReplaceAttr is called on each attribute of the message,
// and the returned value is used instead of the original. If the returned
// key is empty, the attribute is omitted from the output.
//
// The built-in attributes with keys "time", "level", "source", and "msg"
// are passed to this function first, except that time and level are omitted
// if zero, and source is omitted if AddSourceLine is false.
ReplaceAttr func(a Attr) Attr
}
type commonHandler struct {
newAppender func(*buffer.Buffer) appender
opts HandlerOptions
attrs []Attr
preformattedAttrs []byte
mu sync.Mutex
w io.Writer
}
// Enabled reports whether l is less than or equal to the
// maximum level.
func (h *commonHandler) Enabled(l Level) bool {
return l <= h.opts.Level.Level()
}
func (h *commonHandler) with(as []Attr) *commonHandler {
h2 := &commonHandler{
newAppender: h.newAppender,
opts: h.opts,
attrs: concat(h.attrs, as),
preformattedAttrs: h.preformattedAttrs,
w: h.w,
}
if h.opts.ReplaceAttr != nil {
for i, p := range h2.attrs[len(h.attrs):] {
h2.attrs[i] = h.opts.ReplaceAttr(p)
}
}
// Pre-format the attributes as an optimization.
app := h2.newAppender((*buffer.Buffer)(&h2.preformattedAttrs))
for _, p := range h2.attrs[len(h.attrs):] {
appendAttr(app, p)
}
return h2
}
func (h *commonHandler) handle(r Record) error {
buf := buffer.New()
defer buf.Free()
app := h.newAppender(buf)
rep := h.opts.ReplaceAttr
replace := func(a Attr) {
a = rep(a)
if a.Key() != "" {
app.appendKey(a.Key())
if err := app.appendAttrValue(a); err != nil {
app.appendString(fmt.Sprintf("!ERROR:%v", err))
}
}
}
app.appendStart()
if !r.Time().IsZero() {
key := "time"
val := r.Time().Round(0) // strip monotonic to match Attr behavior
if rep == nil {
app.appendKey(key)
if err := app.appendTime(val); err != nil {
return err
}
} else {
replace(Time(key, val))
}
app.appendSep()
}
if r.Level() != 0 {
key := "level"
val := r.Level()
if rep == nil {
app.appendKey(key)
app.appendString(val.String())
} else {
replace(Any(key, val))
}
app.appendSep()
}
if h.opts.AddSource {
file, line := r.SourceLine()
if file != "" {
key := "source"
if rep == nil {
app.appendKey(key)
app.appendSource(file, line)
} else {
buf := buffer.New()
buf.WriteString(file)
buf.WriteByte(':')
itoa((*[]byte)(buf), line, -1)
s := buf.String()
buf.Free()
replace(String(key, s))
}
app.appendSep()
}
}
key := "msg"
val := r.Message()
if rep == nil {
app.appendKey(key)
app.appendString(val)
} else {
replace(String(key, val))
}
*buf = append(*buf, h.preformattedAttrs...)
r.Attrs(func(a Attr) {
if rep != nil {
a = rep(a)
}
appendAttr(app, a)
})
app.appendEnd()
buf.WriteByte('\n')
h.mu.Lock()
defer h.mu.Unlock()
_, err := h.w.Write(*buf)
return err
}
func appendAttr(app appender, a Attr) {
if a.Key() != "" {
app.appendSep()
app.appendKey(a.Key())
if err := app.appendAttrValue(a); err != nil {
app.appendString(fmt.Sprintf("!ERROR:%v", err))
}
}
}
// An appender appends keys and values to a buffer.
// TextHandler and JSONHandler both implement it.
// It factors out the format-specific parts of the job.
// Other than growing the buffer, none of the methods should allocate.
type appender interface {
appendStart() // start a sequence of Attrs
appendEnd() // end a sequence of Attrs
appendSep() // separate one Attr from the next
appendKey(key string) // append a key
appendString(string) // append a string that may need to be escaped
appendTime(time.Time) error // append a time
appendSource(file string, line int) // append file:line
appendAttrValue(a Attr) error // append the Attr's value (but not its key)
}
// This takes half the time of Time.AppendFormat.
func appendTimeRFC3339Millis(buf []byte, t time.Time) []byte {
// TODO: try to speed up by indexing the buffer.
char := func(b byte) {
buf = append(buf, b)
}
year, month, day := t.Date()
itoa(&buf, year, 4)
char('-')
itoa(&buf, int(month), 2)
char('-')
itoa(&buf, day, 2)
char('T')
hour, min, sec := t.Clock()
itoa(&buf, hour, 2)
char(':')
itoa(&buf, min, 2)
char(':')
itoa(&buf, sec, 2)
ns := t.Nanosecond()
char('.')
itoa(&buf, ns/1e6, 3)
_, offsetSeconds := t.Zone()
if offsetSeconds == 0 {
char('Z')
} else {
offsetMinutes := offsetSeconds / 60
if offsetMinutes < 0 {
char('-')
offsetMinutes = -offsetMinutes
} else {
char('+')
}
itoa(&buf, offsetMinutes/60, 2)
char(':')
itoa(&buf, offsetMinutes%60, 2)
}
return buf
}