blob: b67a6509428025da46e806b49ff30a9959369811 [file] [log] [blame]
// 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 (
"encoding"
"fmt"
"io"
"strconv"
"time"
"unicode"
"unicode/utf8"
"golang.org/x/exp/slog/internal/buffer"
)
// 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 default options.
func NewTextHandler(w io.Writer) *TextHandler {
return (HandlerOptions{}).NewTextHandler(w)
}
// NewTextHandler creates a TextHandler with the given options that writes to w.
func (opts HandlerOptions) NewTextHandler(w io.Writer) *TextHandler {
return &TextHandler{
&commonHandler{
newAppender: func(buf *buffer.Buffer) appender {
return (*textAppender)(buf)
},
w: w,
opts: opts,
},
}
}
// With returns a new TextHandler whose attributes consists
// of h's attributes followed by attrs.
func (h *TextHandler) With(attrs []Attr) Handler {
return &TextHandler{commonHandler: h.commonHandler.with(attrs)}
}
// 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 "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 if they contain Unicode space characters,
// non-printing characters, '"' or '='.
//
// Each call to Handle results in a single serialized call to
// io.Writer.Write.
func (h *TextHandler) Handle(r Record) error {
return h.commonHandler.handle(r)
}
type textAppender buffer.Buffer
func (a *textAppender) buf() *buffer.Buffer { return (*buffer.Buffer)(a) }
func (a *textAppender) appendKey(key string) {
a.appendString(key)
a.buf().WriteByte('=')
}
func (a *textAppender) appendString(s string) {
if needsQuoting(s) {
*a.buf() = strconv.AppendQuote(*a.buf(), s)
} else {
a.buf().WriteString(s)
}
}
func (a *textAppender) appendStart() {}
func (a *textAppender) appendEnd() {}
func (a *textAppender) appendSep() { a.buf().WriteByte(' ') }
func (a *textAppender) appendTime(t time.Time) error {
*a.buf() = appendTimeRFC3339Millis(*a.buf(), t)
return nil
}
func (a *textAppender) appendSource(file string, line int) {
if needsQuoting(file) {
a.appendString(file + ":" + strconv.Itoa(line))
} else {
// common case: no quoting needed.
a.appendString(file)
a.buf().WriteByte(':')
itoa((*[]byte)(a.buf()), line, -1)
}
}
func (ap *textAppender) appendAttrValue(a Attr) error {
switch a.Kind() {
case StringKind:
ap.appendString(a.str())
case TimeKind:
if err := ap.appendTime(a.Time()); err != nil {
return err
}
case AnyKind:
if tm, ok := a.any.(encoding.TextMarshaler); ok {
data, err := tm.MarshalText()
if err != nil {
ap.appendString("!ERROR:" + err.Error())
} else {
// TODO: avoid the conversion to string.
ap.appendString(string(data))
}
return nil
}
ap.appendString(fmt.Sprint(a.Value()))
default:
*ap.buf() = a.appendValue(*ap.buf())
}
return nil
}
func needsQuoting(s string) bool {
for i := 0; i < len(s); {
b := s[i]
if b < utf8.RuneSelf {
if needsQuotingSet[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
}
var needsQuotingSet = [utf8.RuneSelf]bool{
'"': true,
'=': true,
}
func init() {
for i := 0; i < utf8.RuneSelf; i++ {
r := rune(i)
if unicode.IsSpace(r) || !unicode.IsPrint(r) {
needsQuotingSet[i] = true
}
}
}