text_handler.go

  1// Copyright 2022 The Go Authors. All rights reserved.
  2// Use of this source code is governed by a BSD-style
  3// license that can be found in the LICENSE file.
  4
  5package slog
  6
  7import (
  8	"context"
  9	"encoding"
 10	"fmt"
 11	"io"
 12	"reflect"
 13	"strconv"
 14	"unicode"
 15	"unicode/utf8"
 16)
 17
 18// TextHandler is a Handler that writes Records to an io.Writer as a
 19// sequence of key=value pairs separated by spaces and followed by a newline.
 20type TextHandler struct {
 21	*commonHandler
 22}
 23
 24// NewTextHandler creates a TextHandler that writes to w,
 25// using the given options.
 26// If opts is nil, the default options are used.
 27func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
 28	if opts == nil {
 29		opts = &HandlerOptions{}
 30	}
 31	return &TextHandler{
 32		&commonHandler{
 33			json: false,
 34			w:    w,
 35			opts: *opts,
 36		},
 37	}
 38}
 39
 40// Enabled reports whether the handler handles records at the given level.
 41// The handler ignores records whose level is lower.
 42func (h *TextHandler) Enabled(_ context.Context, level Level) bool {
 43	return h.commonHandler.enabled(level)
 44}
 45
 46// WithAttrs returns a new TextHandler whose attributes consists
 47// of h's attributes followed by attrs.
 48func (h *TextHandler) WithAttrs(attrs []Attr) Handler {
 49	return &TextHandler{commonHandler: h.commonHandler.withAttrs(attrs)}
 50}
 51
 52func (h *TextHandler) WithGroup(name string) Handler {
 53	return &TextHandler{commonHandler: h.commonHandler.withGroup(name)}
 54}
 55
 56// Handle formats its argument Record as a single line of space-separated
 57// key=value items.
 58//
 59// If the Record's time is zero, the time is omitted.
 60// Otherwise, the key is "time"
 61// and the value is output in RFC3339 format with millisecond precision.
 62//
 63// If the Record's level is zero, the level is omitted.
 64// Otherwise, the key is "level"
 65// and the value of [Level.String] is output.
 66//
 67// If the AddSource option is set and source information is available,
 68// the key is "source" and the value is output as FILE:LINE.
 69//
 70// The message's key is "msg".
 71//
 72// To modify these or other attributes, or remove them from the output, use
 73// [HandlerOptions.ReplaceAttr].
 74//
 75// If a value implements [encoding.TextMarshaler], the result of MarshalText is
 76// written. Otherwise, the result of fmt.Sprint is written.
 77//
 78// Keys and values are quoted with [strconv.Quote] if they contain Unicode space
 79// characters, non-printing characters, '"' or '='.
 80//
 81// Keys inside groups consist of components (keys or group names) separated by
 82// dots. No further escaping is performed.
 83// Thus there is no way to determine from the key "a.b.c" whether there
 84// are two groups "a" and "b" and a key "c", or a single group "a.b" and a key "c",
 85// or single group "a" and a key "b.c".
 86// If it is necessary to reconstruct the group structure of a key
 87// even in the presence of dots inside components, use
 88// [HandlerOptions.ReplaceAttr] to encode that information in the key.
 89//
 90// Each call to Handle results in a single serialized call to
 91// io.Writer.Write.
 92func (h *TextHandler) Handle(_ context.Context, r Record) error {
 93	return h.commonHandler.handle(r)
 94}
 95
 96func appendTextValue(s *handleState, v Value) error {
 97	switch v.Kind() {
 98	case KindString:
 99		s.appendString(v.str())
100	case KindTime:
101		s.appendTime(v.time())
102	case KindAny:
103		if tm, ok := v.any.(encoding.TextMarshaler); ok {
104			data, err := tm.MarshalText()
105			if err != nil {
106				return err
107			}
108			// TODO: avoid the conversion to string.
109			s.appendString(string(data))
110			return nil
111		}
112		if bs, ok := byteSlice(v.any); ok {
113			// As of Go 1.19, this only allocates for strings longer than 32 bytes.
114			s.buf.WriteString(strconv.Quote(string(bs)))
115			return nil
116		}
117		s.appendString(fmt.Sprintf("%+v", v.Any()))
118	default:
119		*s.buf = v.append(*s.buf)
120	}
121	return nil
122}
123
124// byteSlice returns its argument as a []byte if the argument's
125// underlying type is []byte, along with a second return value of true.
126// Otherwise it returns nil, false.
127func byteSlice(a any) ([]byte, bool) {
128	if bs, ok := a.([]byte); ok {
129		return bs, true
130	}
131	// Like Printf's %s, we allow both the slice type and the byte element type to be named.
132	t := reflect.TypeOf(a)
133	if t != nil && t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 {
134		return reflect.ValueOf(a).Bytes(), true
135	}
136	return nil, false
137}
138
139func needsQuoting(s string) bool {
140	if len(s) == 0 {
141		return true
142	}
143	for i := 0; i < len(s); {
144		b := s[i]
145		if b < utf8.RuneSelf {
146			// Quote anything except a backslash that would need quoting in a
147			// JSON string, as well as space and '='
148			if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) {
149				return true
150			}
151			i++
152			continue
153		}
154		r, size := utf8.DecodeRuneInString(s[i:])
155		if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) {
156			return true
157		}
158		i += size
159	}
160	return false
161}