Last active
June 6, 2023 08:03
-
-
Save dlsniper/014d1e3aecc4288e56cf6dbfe522a990 to your computer and use it in GitHub Desktop.
Custom logger based on slog
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// The code in this file is partially taken from the | |
// golang.org/x/exp/[email protected] | |
// | |
// 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 logger | |
import ( | |
"context" | |
"encoding" | |
"fmt" | |
"io" | |
"reflect" | |
"runtime" | |
"strconv" | |
"sync" | |
"time" | |
"unicode" | |
"unicode/utf8" | |
"golang.org/x/exp/slices" | |
"golang.org/x/exp/slog" | |
"golang.org/x/exp/slog/internal/buffer" | |
) | |
// tabDelimitedHandler is a Handler derived from the slog.TextHandler implementation. | |
type tabDelimitedHandler struct { | |
*commonHandler | |
} | |
// NewTextHandler creates a TextHandler that writes to w, | |
// using the given options. | |
// If opts is nil, the default options are used. | |
func newTabDelimited(w io.Writer, opts *slog.HandlerOptions) *tabDelimitedHandler { | |
if opts == nil { | |
opts = &slog.HandlerOptions{} | |
} | |
return &tabDelimitedHandler{ | |
&commonHandler{ | |
w: w, | |
opts: *opts, | |
}, | |
} | |
} | |
// Enabled reports whether the handler handles records at the given level. | |
// The handler ignores records whose level is lower. | |
func (h *tabDelimitedHandler) Enabled(_ context.Context, level slog.Level) bool { | |
minLevel := slog.LevelInfo | |
if h.opts.Level != nil { | |
minLevel = h.opts.Level.Level() | |
} | |
return level >= minLevel | |
} | |
// WithAttrs returns a new TextHandler whose attributes consists | |
// of h's attributes followed by attrs. | |
func (h *tabDelimitedHandler) WithAttrs(attrs []slog.Attr) slog.Handler { | |
return &tabDelimitedHandler{commonHandler: h.commonHandler.withAttrs(attrs)} | |
} | |
func (h *tabDelimitedHandler) WithGroup(name string) slog.Handler { | |
return &tabDelimitedHandler{commonHandler: h.commonHandler.withGroup(name)} | |
} | |
// Handle formats its argument Record as a single line of space-separated | |
// key=value items. | |
func (h *tabDelimitedHandler) Handle(_ context.Context, r slog.Record) error { | |
return h.commonHandler.handle(r) | |
} | |
type commonHandler struct { | |
opts slog.HandlerOptions | |
preformattedAttrs []byte | |
groupPrefix string // for text: prefix of groups opened in preformatting | |
groups []string // all groups started from WithGroup | |
nOpenGroups int // the number of groups opened in preformattedAttrs | |
mu sync.Mutex | |
w io.Writer | |
} | |
func (h *commonHandler) clone() *commonHandler { | |
// We can't use assignment because we can't copy the mutex. | |
return &commonHandler{ | |
opts: h.opts, | |
preformattedAttrs: slices.Clip(h.preformattedAttrs), | |
groupPrefix: h.groupPrefix, | |
groups: slices.Clip(h.groups), | |
nOpenGroups: h.nOpenGroups, | |
w: h.w, | |
} | |
} | |
// enabled reports whether l is greater than or equal to the | |
// minimum level. | |
func (h *commonHandler) enabled(l slog.Level) bool { | |
minLevel := slog.LevelInfo | |
if h.opts.Level != nil { | |
minLevel = h.opts.Level.Level() | |
} | |
return l >= minLevel | |
} | |
func (h *commonHandler) withAttrs(as []slog.Attr) *commonHandler { | |
h2 := h.clone() | |
// Pre-format the attributes as an optimization. | |
prefix := buffer.New() | |
defer prefix.Free() | |
prefix.WriteString(h.groupPrefix) | |
state := h2.newHandleState((*buffer.Buffer)(&h2.preformattedAttrs), false, "", prefix) | |
defer state.free() | |
if len(h2.preformattedAttrs) > 0 { | |
state.sep = h.attrSep() | |
} | |
state.openGroups() | |
for _, a := range as { | |
state.appendAttr(a) | |
} | |
// Remember the new prefix for later keys. | |
h2.groupPrefix = state.prefix.String() | |
// Remember how many opened groups are in preformattedAttrs, | |
// so we don't open them again when we handle a Record. | |
h2.nOpenGroups = len(h2.groups) | |
return h2 | |
} | |
func (h *commonHandler) withGroup(name string) *commonHandler { | |
if name == "" { | |
return h | |
} | |
h2 := h.clone() | |
h2.groups = append(h2.groups, name) | |
return h2 | |
} | |
func (h *commonHandler) handle(r slog.Record) error { | |
state := h.newHandleState(buffer.New(), true, "", nil) | |
defer state.free() | |
// Built-in attributes. They are not in a group. | |
stateGroups := state.groups | |
state.groups = nil // So ReplaceAttrs sees no groups instead of the pre groups. | |
rep := h.opts.ReplaceAttr | |
// time | |
if !r.Time.IsZero() { | |
key := slog.TimeKey | |
val := r.Time.Round(0) // strip monotonic to match Attr behavior | |
if rep == nil { | |
state.appendKey(key) | |
state.appendTime(val) | |
} else { | |
state.appendAttr(slog.Time(key, val)) | |
} | |
} | |
// level | |
key := slog.LevelKey | |
val := r.Level | |
if rep == nil { | |
state.appendKey(key) | |
state.appendString(val.String()) | |
} else { | |
state.appendAttr(slog.Any(key, val)) | |
} | |
// source | |
if h.opts.AddSource { | |
fs := runtime.CallersFrames([]uintptr{r.PC}) | |
f, _ := fs.Next() | |
src := &slog.Source{ | |
Function: f.Function, | |
File: f.File, | |
Line: f.Line, | |
} | |
state.appendAttr(slog.Any(slog.SourceKey, src)) | |
} | |
key = slog.MessageKey | |
msg := r.Message | |
if rep == nil { | |
state.appendKey(key) | |
state.appendString(msg) | |
} else { | |
state.appendAttr(slog.String(key, msg)) | |
} | |
state.groups = stateGroups // Restore groups passed to ReplaceAttrs. | |
state.appendNonBuiltIns(r) | |
state.buf.WriteByte('\n') | |
h.mu.Lock() | |
defer h.mu.Unlock() | |
_, err := h.w.Write(*state.buf) | |
return err | |
} | |
func (s *handleState) appendNonBuiltIns(r slog.Record) { | |
// preformatted Attrs | |
if len(s.h.preformattedAttrs) > 0 { | |
s.buf.WriteString(s.sep) | |
s.buf.Write(s.h.preformattedAttrs) | |
s.sep = s.h.attrSep() | |
} | |
// Attrs in Record -- unlike the built-in ones, they are in groups started | |
// from WithGroup. | |
s.prefix = buffer.New() | |
defer s.prefix.Free() | |
s.prefix.WriteString(s.h.groupPrefix) | |
s.openGroups() | |
r.Attrs(func(a slog.Attr) bool { | |
s.appendAttr(a) | |
return true | |
}) | |
} | |
// attrSep returns the separator between attributes. | |
func (h *commonHandler) attrSep() string { | |
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 { | |
s := make([]string, 0, 10) | |
return &s | |
}} | |
func (h *commonHandler) newHandleState(buf *buffer.Buffer, freeBuf bool, sep string, prefix *buffer.Buffer) handleState { | |
s := handleState{ | |
h: h, | |
buf: buf, | |
freeBuf: freeBuf, | |
sep: sep, | |
prefix: prefix, | |
} | |
if h.opts.ReplaceAttr != nil { | |
s.groups = groupPool.Get().(*[]string) | |
*s.groups = append(*s.groups, h.groups[:h.nOpenGroups]...) | |
} | |
return s | |
} | |
func (s *handleState) free() { | |
if s.freeBuf { | |
s.buf.Free() | |
} | |
if gs := s.groups; gs != nil { | |
*gs = (*gs)[:0] | |
groupPool.Put(gs) | |
} | |
} | |
func (s *handleState) openGroups() { | |
for _, n := range s.h.groups[s.h.nOpenGroups:] { | |
s.openGroup(n) | |
} | |
} | |
// Separator for group names and keys. | |
const keyComponentSep = '.' | |
// openGroup starts a new group of attributes | |
// with the given name. | |
func (s *handleState) openGroup(name string) { | |
s.prefix.WriteString(name) | |
s.prefix.WriteByte(keyComponentSep) | |
// Collect group names for ReplaceAttr. | |
if s.groups != nil { | |
*s.groups = append(*s.groups, name) | |
} | |
} | |
// closeGroup ends the group with the given name. | |
func (s *handleState) closeGroup(name string) { | |
(*s.prefix) = (*s.prefix)[:len(*s.prefix)-len(name)-1 /* for keyComponentSep */] | |
s.sep = s.h.attrSep() | |
if s.groups != nil { | |
*s.groups = (*s.groups)[:len(*s.groups)-1] | |
} | |
} | |
// appendAttr appends the Attr's key and value using app. | |
// It handles replacement and checking for an empty key. | |
// after replacement). | |
func (s *handleState) appendAttr(a slog.Attr) { | |
if rep := s.h.opts.ReplaceAttr; rep != nil && a.Value.Kind() != slog.KindGroup { | |
var gs []string | |
if s.groups != nil { | |
gs = *s.groups | |
} | |
// Resolve before calling ReplaceAttr, so the user doesn't have to. | |
a.Value = a.Value.Resolve() | |
a = rep(gs, a) | |
} | |
a.Value = a.Value.Resolve() | |
// Elide empty Attrs. | |
if a.Equal(slog.Attr{}) { | |
return | |
} | |
// Special case: Source. | |
if v := a.Value; v.Kind() == slog.KindAny { | |
if src, ok := v.Any().(*slog.Source); ok { | |
a.Value = slog.StringValue(fmt.Sprintf("%s:%d", src.File, src.Line)) | |
} | |
} | |
if a.Value.Kind() == slog.KindGroup { | |
attrs := a.Value.Group() | |
// Output only non-empty groups. | |
if len(attrs) > 0 { | |
// Inline a group with an empty key. | |
if a.Key != "" { | |
s.openGroup(a.Key) | |
} | |
for _, aa := range attrs { | |
s.appendAttr(aa) | |
} | |
if a.Key != "" { | |
s.closeGroup(a.Key) | |
} | |
} | |
} else { | |
s.appendKey(a.Key) | |
s.appendValue(a.Value) | |
} | |
} | |
func (s *handleState) appendError(err error) { | |
s.appendString(fmt.Sprintf("!ERROR:%v", err)) | |
} | |
func (s *handleState) appendKey(key string) { | |
s.buf.WriteString(s.sep) | |
if s.prefix != nil { | |
// TODO: optimize by avoiding allocation. | |
s.appendString(string(*s.prefix) + key) | |
} else { | |
s.appendString(key) | |
} | |
s.buf.WriteByte('=') | |
s.sep = s.h.attrSep() | |
} | |
func (s *handleState) appendString(str string) { | |
if needsQuoting(str) { | |
*s.buf = strconv.AppendQuote(*s.buf, str) | |
} else { | |
s.buf.WriteString(str) | |
} | |
} | |
func (s *handleState) appendValue(v slog.Value) { | |
err := appendTextValue(s, v) | |
if err != nil { | |
s.appendError(err) | |
} | |
} | |
func (s *handleState) appendTime(t time.Time) { | |
writeTimeRFC3339Millis(s.buf, t) | |
} | |
// This takes half the time of Time.AppendFormat. | |
func writeTimeRFC3339Millis(buf *buffer.Buffer, t time.Time) { | |
year, month, day := t.Date() | |
buf.WritePosIntWidth(year, 4) | |
buf.WriteByte('-') | |
buf.WritePosIntWidth(int(month), 2) | |
buf.WriteByte('-') | |
buf.WritePosIntWidth(day, 2) | |
buf.WriteByte('T') | |
hour, min, sec := t.Clock() | |
buf.WritePosIntWidth(hour, 2) | |
buf.WriteByte(':') | |
buf.WritePosIntWidth(min, 2) | |
buf.WriteByte(':') | |
buf.WritePosIntWidth(sec, 2) | |
ns := t.Nanosecond() | |
buf.WriteByte('.') | |
buf.WritePosIntWidth(ns/1e6, 3) | |
_, offsetSeconds := t.Zone() | |
if offsetSeconds == 0 { | |
buf.WriteByte('Z') | |
} else { | |
offsetMinutes := offsetSeconds / 60 | |
if offsetMinutes < 0 { | |
buf.WriteByte('-') | |
offsetMinutes = -offsetMinutes | |
} else { | |
buf.WriteByte('+') | |
} | |
buf.WritePosIntWidth(offsetMinutes/60, 2) | |
buf.WriteByte(':') | |
buf.WritePosIntWidth(offsetMinutes%60, 2) | |
} | |
} | |
func appendTextValue(s *handleState, v slog.Value) error { | |
switch v.Kind() { | |
case slog.KindString: | |
s.appendString(v.String()) | |
case slog.KindTime: | |
s.appendTime(v.Time()) | |
case slog.KindAny: | |
if tm, ok := v.Any().(encoding.TextMarshaler); ok { | |
data, err := tm.MarshalText() | |
if err != nil { | |
return err | |
} | |
// TODO: avoid the conversion to string. | |
s.appendString(string(data)) | |
return nil | |
} | |
if bs, ok := byteSlice(v.Any()); ok { | |
// As of Go 1.19, this only allocates for strings longer than 32 bytes. | |
s.buf.WriteString(strconv.Quote(string(bs))) | |
return nil | |
} | |
s.appendString(fmt.Sprintf("%+v", v.Any())) | |
default: | |
*s.buf = myAppend(v, *s.buf) | |
} | |
return nil | |
} | |
// append appends a text representation of v to dst. | |
// v is formatted as with fmt.Sprint. | |
func myAppend(v slog.Value, dst []byte) []byte { | |
switch v.Kind() { | |
case slog.KindString: | |
return append(dst, []byte(v.String())...) | |
case slog.KindInt64: | |
return strconv.AppendInt(dst, v.Int64(), 10) | |
case slog.KindUint64: | |
return strconv.AppendUint(dst, v.Uint64(), 10) | |
case slog.KindFloat64: | |
return strconv.AppendFloat(dst, v.Float64(), 'g', -1, 64) | |
case slog.KindBool: | |
return strconv.AppendBool(dst, v.Bool()) | |
case slog.KindDuration: | |
return append(dst, v.Duration().String()...) | |
case slog.KindTime: | |
return append(dst, v.Time().String()...) | |
case slog.KindGroup: | |
return fmt.Append(dst, v.Group()) | |
case slog.KindAny, slog.KindLogValuer: | |
return fmt.Append(dst, v.Any()) | |
default: | |
panic(fmt.Sprintf("bad kind: %s", v.Kind())) | |
} | |
} | |
// byteSlice returns its argument as a []byte if the argument's | |
// underlying type is []byte, along with a second return value of true. | |
// Otherwise it returns nil, false. | |
func byteSlice(a any) ([]byte, bool) { | |
if bs, ok := a.([]byte); ok { | |
return bs, true | |
} | |
// Like Printf's %s, we allow both the slice type and the byte element type to be named. | |
t := reflect.TypeOf(a) | |
if t != nil && t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 { | |
return reflect.ValueOf(a).Bytes(), true | |
} | |
return nil, false | |
} | |
func needsQuoting(s string) bool { | |
if len(s) == 0 { | |
return true | |
} | |
for i := 0; i < len(s); { | |
b := s[i] | |
if b < utf8.RuneSelf { | |
// Quote anything except a backslash that would need quoting in a | |
// JSON string, as well as space and '=' | |
if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) { | |
return true | |
} | |
i++ | |
continue | |
} | |
r, size := utf8.DecodeRuneInString(s[i:]) | |
if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) { | |
return true | |
} | |
i += size | |
} | |
return false | |
} | |
// Copied from encoding/json/tables.go. | |
// | |
// safeSet holds the value true if the ASCII character with the given array | |
// position can be represented inside a JSON string without any further | |
// escaping. | |
// | |
// All values are true except for the ASCII control characters (0-31), the | |
// double quote ("), and the backslash character ("\"). | |
var safeSet = [utf8.RuneSelf]bool{ | |
' ': true, | |
'!': true, | |
'"': false, | |
'#': true, | |
'$': true, | |
'%': true, | |
'&': true, | |
'\'': true, | |
'(': true, | |
')': true, | |
'*': true, | |
'+': true, | |
',': true, | |
'-': true, | |
'.': true, | |
'/': true, | |
'0': true, | |
'1': true, | |
'2': true, | |
'3': true, | |
'4': true, | |
'5': true, | |
'6': true, | |
'7': true, | |
'8': true, | |
'9': true, | |
':': true, | |
';': true, | |
'<': true, | |
'=': true, | |
'>': true, | |
'?': true, | |
'@': true, | |
'A': true, | |
'B': true, | |
'C': true, | |
'D': true, | |
'E': true, | |
'F': true, | |
'G': true, | |
'H': true, | |
'I': true, | |
'J': true, | |
'K': true, | |
'L': true, | |
'M': true, | |
'N': true, | |
'O': true, | |
'P': true, | |
'Q': true, | |
'R': true, | |
'S': true, | |
'T': true, | |
'U': true, | |
'V': true, | |
'W': true, | |
'X': true, | |
'Y': true, | |
'Z': true, | |
'[': true, | |
'\\': false, | |
']': true, | |
'^': true, | |
'_': true, | |
'`': true, | |
'a': true, | |
'b': true, | |
'c': true, | |
'd': true, | |
'e': true, | |
'f': true, | |
'g': true, | |
'h': true, | |
'i': true, | |
'j': true, | |
'k': true, | |
'l': true, | |
'm': true, | |
'n': true, | |
'o': true, | |
'p': true, | |
'q': true, | |
'r': true, | |
's': true, | |
't': true, | |
'u': true, | |
'v': true, | |
'w': true, | |
'x': true, | |
'y': true, | |
'z': true, | |
'{': true, | |
'|': true, | |
'}': true, | |
'~': true, | |
'\u007f': true, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment