// 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 }