blob: a663c400e35cf713b41308fcd6ae700ee4722edf [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 (
"context"
"fmt"
"io"
"strconv"
"golang.org/x/exp/event"
)
//TODO: some actual research into what this arbritray optimization number should be
const bufCap = 50
type Printer struct {
buf [bufCap]byte
needSep bool
}
type Handler struct {
to io.Writer
printer 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) Log(ctx context.Context, ev *event.Event) {
h.printer.Event(h.to, "log", ev)
}
func (h *Handler) Metric(ctx context.Context, ev *event.Event) {
h.printer.Event(h.to, "metric", ev)
}
func (h *Handler) Annotate(ctx context.Context, ev *event.Event) {
h.printer.Event(h.to, "annotate", ev)
}
func (h *Handler) Start(ctx context.Context, ev *event.Event) context.Context {
h.printer.Event(h.to, "start", ev)
return ctx
}
func (h *Handler) End(ctx context.Context, ev *event.Event) {
h.printer.Event(h.to, "end", ev)
}
func (p *Printer) Event(w io.Writer, kind string, ev *event.Event) {
const timeFormat = "2006-01-02T15:04:05"
p.needSep = false
if !ev.At.IsZero() {
p.label(w, "time", event.BytesOf(ev.At.AppendFormat(p.buf[:0], timeFormat)))
}
if ev.ID != 0 {
p.label(w, "trace", event.BytesOf(strconv.AppendUint(p.buf[:0], ev.ID, 10)))
}
if ev.Parent != 0 {
p.label(w, "parent", event.BytesOf(strconv.AppendUint(p.buf[:0], ev.Parent, 10)))
}
if kind != "log" && kind != "start" {
p.label(w, "kind", event.StringOf(kind))
}
for _, l := range ev.Labels {
if l.Name == "" {
continue
}
p.Label(w, l)
}
io.WriteString(w, "\n")
}
func (p *Printer) Label(w io.Writer, l event.Label) {
p.label(w, l.Name, l.Value)
}
func (p *Printer) Value(w io.Writer, v event.Value) {
switch {
case v.IsString():
p.Quote(w, v.String())
case v.IsBytes():
p.Bytes(w, v.Bytes())
case v.IsInt64():
w.Write(strconv.AppendInt(p.buf[:0], v.Int64(), 10))
case v.IsUint64():
w.Write(strconv.AppendUint(p.buf[:0], v.Uint64(), 10))
case v.IsFloat64():
w.Write(strconv.AppendFloat(p.buf[:0], v.Float64(), 'g', -1, 64))
case v.IsBool():
if v.Bool() {
io.WriteString(w, "true")
} else {
io.WriteString(w, "false")
}
default:
fmt.Fprint(w, v.Interface())
}
}
func (p *Printer) Ident(w io.Writer, s string) {
//TODO: this should also escape = if it occurs in an ident?
p.Quote(w, s)
}
func (p *Printer) Quote(w io.Writer, s string) {
if s == "" {
io.WriteString(w, `""`)
return
}
if !needQuote(s) {
io.WriteString(w, s)
return
}
// string needs quoting
io.WriteString(w, `"`)
written := 0
for o, r := range s {
q := quoteRune(r)
if len(q) == 0 {
continue
}
// write out any prefix
io.WriteString(w, s[written:o])
written = o + 1 // we can plus 1 because all runes we escape are ascii
// and write out the quoted rune
io.WriteString(w, q)
}
io.WriteString(w, s[written:])
io.WriteString(w, `"`)
}
// Bytes writes a byte array in string form to the printer.
func (p *Printer) Bytes(w io.Writer, buf []byte) {
//TODO: non asci chars need escaping
w.Write(buf)
}
func (p *Printer) label(w io.Writer, name string, value event.Value) {
if name == "" {
return
}
if p.needSep {
io.WriteString(w, " ")
}
p.needSep = true
p.Ident(w, name)
if value.HasValue() {
io.WriteString(w, "=")
p.Value(w, value)
}
}
func needQuote(s string) bool {
for _, r := range s {
if len(quoteRune(r)) > 0 {
return true
}
}
return false
}
func quoteRune(r rune) string {
switch r {
case '"':
return `\"`
case ' ':
return ` ` // does not change but forces quoting
case '\n':
return `\n`
case '\\':
return `\\`
default:
return ``
}
}