text_handler.go raw

   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  
   5  package slog
   6  
   7  import (
   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.
  20  type 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.
  27  func 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.
  42  func (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.
  48  func (h *TextHandler) WithAttrs(attrs []Attr) Handler {
  49  	return &TextHandler{commonHandler: h.commonHandler.withAttrs(attrs)}
  50  }
  51  
  52  func (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.
  92  func (h *TextHandler) Handle(_ context.Context, r Record) error {
  93  	return h.commonHandler.handle(r)
  94  }
  95  
  96  func 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.
 127  func 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  
 139  func 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  }
 162