mirror of
https://github.com/mjl-/mox.git
synced 2025-04-02 01:31:06 +03:00

we don't want external software to include internal details like mlog. slog.Logger is/will be the standard. we still have mlog for its helper functions, and its handler that logs in concise logfmt used by mox. packages that are not meant for reuse still pass around mlog.Log for convenience. we use golang.org/x/exp/slog because we also support the previous Go toolchain version. with the next Go release, we'll switch to the builtin slog.
161 lines
4.7 KiB
Go
161 lines
4.7 KiB
Go
// 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 (
|
|
"context"
|
|
"encoding"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strconv"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// TextHandler is a Handler that writes Records to an io.Writer as a
|
|
// sequence of key=value pairs separated by spaces and followed by a newline.
|
|
type TextHandler struct {
|
|
*commonHandler
|
|
}
|
|
|
|
// NewTextHandler creates a TextHandler that writes to w,
|
|
// using the given options.
|
|
// If opts is nil, the default options are used.
|
|
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
|
|
if opts == nil {
|
|
opts = &HandlerOptions{}
|
|
}
|
|
return &TextHandler{
|
|
&commonHandler{
|
|
json: false,
|
|
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 *TextHandler) Enabled(_ context.Context, level Level) bool {
|
|
return h.commonHandler.enabled(level)
|
|
}
|
|
|
|
// WithAttrs returns a new TextHandler whose attributes consists
|
|
// of h's attributes followed by attrs.
|
|
func (h *TextHandler) WithAttrs(attrs []Attr) Handler {
|
|
return &TextHandler{commonHandler: h.commonHandler.withAttrs(attrs)}
|
|
}
|
|
|
|
func (h *TextHandler) WithGroup(name string) Handler {
|
|
return &TextHandler{commonHandler: h.commonHandler.withGroup(name)}
|
|
}
|
|
|
|
// Handle formats its argument Record as a single line of space-separated
|
|
// key=value items.
|
|
//
|
|
// If the Record's time is zero, the time is omitted.
|
|
// Otherwise, the key is "time"
|
|
// and the value is output in RFC3339 format with millisecond precision.
|
|
//
|
|
// If the Record's level is zero, the level is omitted.
|
|
// Otherwise, the key is "level"
|
|
// and the value of [Level.String] is output.
|
|
//
|
|
// If the AddSource option is set and source information is available,
|
|
// the key is "source" and the value is output as FILE:LINE.
|
|
//
|
|
// The message's key is "msg".
|
|
//
|
|
// To modify these or other attributes, or remove them from the output, use
|
|
// [HandlerOptions.ReplaceAttr].
|
|
//
|
|
// If a value implements [encoding.TextMarshaler], the result of MarshalText is
|
|
// written. Otherwise, the result of fmt.Sprint is written.
|
|
//
|
|
// Keys and values are quoted with [strconv.Quote] if they contain Unicode space
|
|
// characters, non-printing characters, '"' or '='.
|
|
//
|
|
// Keys inside groups consist of components (keys or group names) separated by
|
|
// dots. No further escaping is performed.
|
|
// Thus there is no way to determine from the key "a.b.c" whether there
|
|
// are two groups "a" and "b" and a key "c", or a single group "a.b" and a key "c",
|
|
// or single group "a" and a key "b.c".
|
|
// If it is necessary to reconstruct the group structure of a key
|
|
// even in the presence of dots inside components, use
|
|
// [HandlerOptions.ReplaceAttr] to encode that information in the key.
|
|
//
|
|
// Each call to Handle results in a single serialized call to
|
|
// io.Writer.Write.
|
|
func (h *TextHandler) Handle(_ context.Context, r Record) error {
|
|
return h.commonHandler.handle(r)
|
|
}
|
|
|
|
func appendTextValue(s *handleState, v Value) error {
|
|
switch v.Kind() {
|
|
case KindString:
|
|
s.appendString(v.str())
|
|
case KindTime:
|
|
s.appendTime(v.time())
|
|
case 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 = v.append(*s.buf)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|