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