blob: c3eca4f0f1b38819326ae7155c4165ab149efb1b [file] [log] [blame]
// Copyright 2019 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 logfmt
import (
"bytes"
"context"
"fmt"
"io"
"strconv"
"time"
"unicode/utf8"
"golang.org/x/exp/event"
)
//TODO: some actual research into what this arbritray optimization number should be
const bufCap = 50
const TimeFormat = "2006/01/02 15:04:05"
type Printer struct {
QuoteValues bool
SuppressNamespace bool
buf [bufCap]byte
needSep bool
w bytes.Buffer
}
type Handler struct {
to io.Writer
Printer
}
// NewHandler returns a handler that prints the events to the supplied writer.
// Each event is printed in logfmt format on a single line.
func NewHandler(to io.Writer) *Handler {
return &Handler{to: to}
}
func (h *Handler) Event(ctx context.Context, ev *event.Event) context.Context {
h.Printer.Event(h.to, ev)
return ctx
}
func (p *Printer) Event(w io.Writer, ev *event.Event) {
p.needSep = false
if !ev.At.IsZero() {
p.separator(w)
io.WriteString(w, `time="`)
p.time(w, ev.At)
io.WriteString(w, `"`)
}
if !p.SuppressNamespace && ev.Source.Space != "" {
p.Label(w, event.String("in", ev.Source.Space))
}
if ev.Source.Owner != "" {
p.Label(w, event.String("owner", ev.Source.Owner))
}
if ev.Source.Name != "" {
p.Label(w, event.String("name", ev.Source.Name))
}
if ev.Parent != 0 {
p.separator(w)
io.WriteString(w, `parent=`)
w.Write(strconv.AppendUint(p.buf[:0], ev.Parent, 10))
}
for _, l := range ev.Labels {
if l.Name == "" {
continue
}
p.Label(w, l)
}
if ev.ID != 0 {
p.separator(w)
io.WriteString(w, `trace=`)
w.Write(strconv.AppendUint(p.buf[:0], ev.ID, 10))
}
if ev.Kind == event.EndKind {
p.separator(w)
io.WriteString(w, `end`)
}
io.WriteString(w, "\n")
}
func (p *Printer) separator(w io.Writer) {
if p.needSep {
io.WriteString(w, " ")
}
p.needSep = true
}
func (p *Printer) Label(w io.Writer, l event.Label) {
if l.Name == "" {
return
}
p.separator(w)
p.Ident(w, l.Name)
if l.HasValue() {
io.WriteString(w, "=")
switch {
case l.IsString():
p.string(w, l.String())
case l.IsBytes():
p.bytes(w, l.Bytes())
case l.IsInt64():
w.Write(strconv.AppendInt(p.buf[:0], l.Int64(), 10))
case l.IsUint64():
w.Write(strconv.AppendUint(p.buf[:0], l.Uint64(), 10))
case l.IsFloat64():
w.Write(strconv.AppendFloat(p.buf[:0], l.Float64(), 'g', -1, 64))
case l.IsBool():
if l.Bool() {
io.WriteString(w, "true")
} else {
io.WriteString(w, "false")
}
default:
v := l.Interface()
switch v := v.(type) {
case string:
p.string(w, v)
case fmt.Stringer:
p.string(w, v.String())
default:
if p.w.Cap() == 0 {
// we rely on the inliner to cause this to not allocate
p.w = *bytes.NewBuffer(p.buf[:0])
}
fmt.Fprint(&p.w, v)
b := p.w.Bytes()
p.w.Reset()
p.bytes(w, b)
}
}
}
}
func (p *Printer) Ident(w io.Writer, s string) {
if !stringNeedQuote(s) {
io.WriteString(w, s)
return
}
p.quoteString(w, s)
}
func (p *Printer) string(w io.Writer, s string) {
if p.QuoteValues || stringNeedQuote(s) {
p.quoteString(w, s)
} else {
io.WriteString(w, s)
}
}
func (p *Printer) quoteString(w io.Writer, s string) {
io.WriteString(w, `"`)
written := 0
for offset, r := range s {
q := quoteRune(r)
if len(q) == 0 {
continue
}
// write out any prefix
io.WriteString(w, s[written:offset])
written = offset + utf8.RuneLen(r)
// and write out the quoted rune
io.WriteString(w, q)
}
io.WriteString(w, s[written:])
io.WriteString(w, `"`)
}
func (p *Printer) bytes(w io.Writer, buf []byte) {
if p.QuoteValues || stringNeedQuote(string(buf)) {
p.quoteBytes(w, buf)
} else {
w.Write(buf)
}
}
// Bytes writes a byte array in string form to the printer.
func (p *Printer) quoteBytes(w io.Writer, buf []byte) {
io.WriteString(w, `"`)
written := 0
for offset := 0; offset < len(buf); {
r, size := utf8.DecodeRune(buf[offset:])
offset += size
q := quoteRune(r)
if len(q) == 0 {
continue
}
// write out any prefix
w.Write(buf[written : offset-size])
written = offset
// and write out the quoted rune
io.WriteString(w, q)
}
w.Write(buf[written:])
io.WriteString(w, `"`)
}
// time writes a timstamp in the same format as
func (p *Printer) time(w io.Writer, t time.Time) {
year, month, day := t.Date()
hour, minute, second := t.Clock()
p.padInt(w, int64(year), 4)
io.WriteString(w, `/`)
p.padInt(w, int64(month), 2)
io.WriteString(w, `/`)
p.padInt(w, int64(day), 2)
io.WriteString(w, ` `)
p.padInt(w, int64(hour), 2)
io.WriteString(w, `:`)
p.padInt(w, int64(minute), 2)
io.WriteString(w, `:`)
p.padInt(w, int64(second), 2)
}
func (p *Printer) padInt(w io.Writer, v int64, width int) {
b := strconv.AppendInt(p.buf[:0], int64(v), 10)
if len(b) < width {
io.WriteString(w, "0000"[:width-len(b)])
}
w.Write(b)
}
func stringNeedQuote(s string) bool {
if len(s) == 0 {
return true
}
for i := 0; i < len(s); i++ {
c := s[i]
if c >= utf8.RuneSelf || c == ' ' || c == '"' || c == '\n' || c == '\\' {
return true
}
}
return false
}
func quoteRune(r rune) string {
switch r {
case '"':
return `\"`
case '\n':
return `\n`
case '\\':
return `\\`
default:
return ``
}
}