event: better printing

Change-Id: Ica914a861f7c174ccbba8de4234a0669f486248c
Reviewed-on: https://go-review.googlesource.com/c/exp/+/314949
Trust: Ian Cottrell <iancottrell@google.com>
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/event/bench/event_test.go b/event/bench/event_test.go
index d98562b..4f0663c 100644
--- a/event/bench/event_test.go
+++ b/event/bench/event_test.go
@@ -123,43 +123,43 @@
 
 func TestLogEventf(t *testing.T) {
 	testBenchmark(t, eventPrint, eventLogf, `
-2020/03/05 14:27:48	[1]	log	a where A=0
-2020/03/05 14:27:49	[2]	log	b where B="A value"
-2020/03/05 14:27:50	[3]	log	a where A=1
-2020/03/05 14:27:51	[4]	log	b where B="Some other value"
-2020/03/05 14:27:52	[5]	log	a where A=22
-2020/03/05 14:27:53	[6]	log	b where B="Some other value"
-2020/03/05 14:27:54	[7]	log	a where A=333
-2020/03/05 14:27:55	[8]	log	b where B=""
-2020/03/05 14:27:56	[9]	log	a where A=4444
-2020/03/05 14:27:57	[10]	log	b where B="prime count of values"
-2020/03/05 14:27:58	[11]	log	a where A=55555
-2020/03/05 14:27:59	[12]	log	b where B="V"
-2020/03/05 14:28:00	[13]	log	a where A=666666
-2020/03/05 14:28:01	[14]	log	b where B="A value"
-2020/03/05 14:28:02	[15]	log	a where A=7777777
-2020/03/05 14:28:03	[16]	log	b where B="A value"
+time=2020-03-05T14:27:48 id=1 kind=log msg="a where A=0"
+time=2020-03-05T14:27:49 id=2 kind=log msg="b where B=\"A value\""
+time=2020-03-05T14:27:50 id=3 kind=log msg="a where A=1"
+time=2020-03-05T14:27:51 id=4 kind=log msg="b where B=\"Some other value\""
+time=2020-03-05T14:27:52 id=5 kind=log msg="a where A=22"
+time=2020-03-05T14:27:53 id=6 kind=log msg="b where B=\"Some other value\""
+time=2020-03-05T14:27:54 id=7 kind=log msg="a where A=333"
+time=2020-03-05T14:27:55 id=8 kind=log msg="b where B=\"\""
+time=2020-03-05T14:27:56 id=9 kind=log msg="a where A=4444"
+time=2020-03-05T14:27:57 id=10 kind=log msg="b where B=\"prime count of values\""
+time=2020-03-05T14:27:58 id=11 kind=log msg="a where A=55555"
+time=2020-03-05T14:27:59 id=12 kind=log msg="b where B=\"V\""
+time=2020-03-05T14:28:00 id=13 kind=log msg="a where A=666666"
+time=2020-03-05T14:28:01 id=14 kind=log msg="b where B=\"A value\""
+time=2020-03-05T14:28:02 id=15 kind=log msg="a where A=7777777"
+time=2020-03-05T14:28:03 id=16 kind=log msg="b where B=\"A value\""
 `)
 }
 
 func TestLogEvent(t *testing.T) {
 	testBenchmark(t, eventPrint, eventLog, `
-2020/03/05 14:27:48	[1]	log	a	{"A":0}
-2020/03/05 14:27:49	[2]	log	b	{"B":"A value"}
-2020/03/05 14:27:50	[3]	log	a	{"A":1}
-2020/03/05 14:27:51	[4]	log	b	{"B":"Some other value"}
-2020/03/05 14:27:52	[5]	log	a	{"A":22}
-2020/03/05 14:27:53	[6]	log	b	{"B":"Some other value"}
-2020/03/05 14:27:54	[7]	log	a	{"A":333}
-2020/03/05 14:27:55	[8]	log	b	{"B":""}
-2020/03/05 14:27:56	[9]	log	a	{"A":4444}
-2020/03/05 14:27:57	[10]	log	b	{"B":"prime count of values"}
-2020/03/05 14:27:58	[11]	log	a	{"A":55555}
-2020/03/05 14:27:59	[12]	log	b	{"B":"V"}
-2020/03/05 14:28:00	[13]	log	a	{"A":666666}
-2020/03/05 14:28:01	[14]	log	b	{"B":"A value"}
-2020/03/05 14:28:02	[15]	log	a	{"A":7777777}
-2020/03/05 14:28:03	[16]	log	b	{"B":"A value"}
+time=2020-03-05T14:27:48 id=1 kind=log msg=a A=0
+time=2020-03-05T14:27:49 id=2 kind=log msg=b B="A value"
+time=2020-03-05T14:27:50 id=3 kind=log msg=a A=1
+time=2020-03-05T14:27:51 id=4 kind=log msg=b B="Some other value"
+time=2020-03-05T14:27:52 id=5 kind=log msg=a A=22
+time=2020-03-05T14:27:53 id=6 kind=log msg=b B="Some other value"
+time=2020-03-05T14:27:54 id=7 kind=log msg=a A=333
+time=2020-03-05T14:27:55 id=8 kind=log msg=b B=""
+time=2020-03-05T14:27:56 id=9 kind=log msg=a A=4444
+time=2020-03-05T14:27:57 id=10 kind=log msg=b B="prime count of values"
+time=2020-03-05T14:27:58 id=11 kind=log msg=a A=55555
+time=2020-03-05T14:27:59 id=12 kind=log msg=b B=V
+time=2020-03-05T14:28:00 id=13 kind=log msg=a A=666666
+time=2020-03-05T14:28:01 id=14 kind=log msg=b B="A value"
+time=2020-03-05T14:28:02 id=15 kind=log msg=a A=7777777
+time=2020-03-05T14:28:03 id=16 kind=log msg=b B="A value"
 `)
 }
 
diff --git a/event/event.go b/event/event.go
index 5c26cd6..7e71918 100644
--- a/event/event.go
+++ b/event/event.go
@@ -6,9 +6,6 @@
 
 import (
 	"fmt"
-	"io"
-	"strconv"
-	"sync"
 	"time"
 )
 
@@ -43,99 +40,9 @@
 
 // Format prints the value in a standard form.
 func (e *Event) Format(f fmt.State, verb rune) {
-	buf := bufPool.Get().(*buffer)
-	e.format(f.(writer), buf.data[:0])
-	bufPool.Put(buf)
-}
-
-// Format prints the value in a standard form.
-func (e *Event) format(w writer, buf []byte) {
-	const timeFormat = "2006/01/02 15:04:05"
-	if !e.At.IsZero() {
-		w.Write(e.At.AppendFormat(buf[:0], timeFormat))
-		w.WriteString("\t")
-	}
-	//TODO: pick a standard format for the event id and parent
-	w.WriteString("[")
-	w.Write(strconv.AppendUint(buf[:0], e.ID, 10))
-	if e.Parent != 0 {
-		w.WriteString(":")
-		w.Write(strconv.AppendUint(buf[:0], e.Parent, 10))
-	}
-	w.WriteString("]")
-
-	//TODO: pick a standard format for the kind
-	w.WriteString("\t")
-	e.Kind.format(w, buf)
-
-	if e.Message != "" {
-		w.WriteString("\t")
-		w.WriteString(e.Message)
-	}
-
-	first := true
-	for _, l := range e.Labels {
-		if l.Name == "" {
-			continue
-		}
-		if first {
-			w.WriteString("\t{")
-			first = false
-		} else {
-			w.WriteString(", ")
-		}
-		l.format(w, buf)
-	}
-	if !first {
-		w.WriteString("}")
-	}
+	newPrinter(f).Event(e)
 }
 
 func (k Kind) Format(f fmt.State, verb rune) {
-	buf := bufPool.Get().(*buffer)
-	k.format(f.(writer), buf.data[:0])
-	bufPool.Put(buf)
-}
-
-func (k Kind) format(w writer, buf []byte) {
-	switch k {
-	case LogKind:
-		w.WriteString("log")
-	case StartKind:
-		w.WriteString("start")
-	case EndKind:
-		w.WriteString("end")
-	case MetricKind:
-		w.WriteString("metric")
-	case AnnotateKind:
-		w.WriteString("annotate")
-	default:
-		w.Write(strconv.AppendUint(buf[:0], uint64(k), 10))
-	}
-}
-
-// Printer returns a handler that prints the events to the supplied writer.
-// Each event is printed in normal %v mode on its own line.
-func Printer(to io.Writer) Handler {
-	return &printHandler{to: to}
-}
-
-type printHandler struct {
-	to io.Writer
-}
-
-func (h *printHandler) Handle(ev *Event) {
-	fmt.Fprintln(h.to, ev)
-}
-
-//TODO: some actual research into what this arbritray optimization number should be
-const bufCap = 50
-
-type buffer struct{ data [bufCap]byte }
-
-var bufPool = sync.Pool{New: func() interface{} { return new(buffer) }}
-
-type writer interface {
-	io.Writer
-	io.StringWriter
+	newPrinter(f).Kind(k)
 }
diff --git a/event/event_test.go b/event/event_test.go
index 689858d..d26a6e4 100644
--- a/event/event_test.go
+++ b/event/event_test.go
@@ -30,19 +30,19 @@
 	}{{
 		name:   "simple",
 		events: func(ctx context.Context) { event.To(ctx).Log("a message") },
-		expect: `
-2020/03/05 14:27:48	[1]	log	a message
+		expect: `time=2020-03-05T14:27:48 id=1 kind=log msg="a message"
 `}, {
 		name:   "log 1",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).Log("a message") },
-		expect: `2020/03/05 14:27:48	[1]	log	a message	{"l1":1}`}, {
-		name:   "simple",
-		events: func(ctx context.Context) { event.To(ctx).With(l1).With(l2).Log("a message") },
-		expect: `2020/03/05 14:27:48	[1]	log	a message	{"l1":1, "l2":2}`,
+		expect: `time=2020-03-05T14:27:48 id=1 kind=log msg="a message" l1=1`,
 	}, {
-		name:   "simple",
+		name:   "log 2",
+		events: func(ctx context.Context) { event.To(ctx).With(l1).With(l2).Log("a message") },
+		expect: `time=2020-03-05T14:27:48 id=1 kind=log msg="a message" l1=1 l2=2`,
+	}, {
+		name:   "log 3",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).With(l2).With(l3).Log("a message") },
-		expect: `2020/03/05 14:27:48	[1]	log	a message	{"l1":1, "l2":2, "l3":3}`,
+		expect: `time=2020-03-05T14:27:48 id=1 kind=log msg="a message" l1=1 l2=2 l3=3`,
 	}, {
 		name: "span",
 		events: func(ctx context.Context) {
@@ -50,8 +50,8 @@
 			end()
 		},
 		expect: `
-2020/03/05 14:27:48	[1]	start	span
-2020/03/05 14:27:49	[2:1]	end
+time=2020-03-05T14:27:48 id=1 kind=start msg=span
+time=2020-03-05T14:27:49 id=2 span=1 kind=end
 `}, {
 		name: "span nested",
 		events: func(ctx context.Context) {
@@ -62,27 +62,27 @@
 			event.To(child).Log("message")
 		},
 		expect: `
-2020/03/05 14:27:48	[1]	start	parent
-2020/03/05 14:27:49	[2:1]	start	child
-2020/03/05 14:27:50	[3:2]	log	message
-2020/03/05 14:27:51	[4:2]	end
-2020/03/05 14:27:52	[5:1]	end
+time=2020-03-05T14:27:48 id=1 kind=start msg=parent
+time=2020-03-05T14:27:49 id=2 span=1 kind=start msg=child
+time=2020-03-05T14:27:50 id=3 span=2 kind=log msg=message
+time=2020-03-05T14:27:51 id=4 span=2 kind=end
+time=2020-03-05T14:27:52 id=5 span=1 kind=end
 `}, {
 		name:   "metric",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).Metric() },
-		expect: `2020/03/05 14:27:48	[1]	metric	{"l1":1}`,
+		expect: `time=2020-03-05T14:27:48 id=1 kind=metric l1=1`,
 	}, {
 		name:   "metric 2",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).With(l2).Metric() },
-		expect: `2020/03/05 14:27:48	[1]	metric	{"l1":1, "l2":2}`,
+		expect: `time=2020-03-05T14:27:48 id=1 kind=metric l1=1 l2=2`,
 	}, {
 		name:   "annotate",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).Annotate() },
-		expect: `2020/03/05 14:27:48	[1]	annotate	{"l1":1}`,
+		expect: `time=2020-03-05T14:27:48 id=1 kind=annotate l1=1`,
 	}, {
 		name:   "annotate 2",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).With(l2).Annotate() },
-		expect: `2020/03/05 14:27:48	[1]	annotate	{"l1":1, "l2":2}`,
+		expect: `time=2020-03-05T14:27:48 id=1 kind=annotate l1=1 l2=2`,
 	}, {
 		name: "multiple events",
 		events: func(ctx context.Context) {
@@ -91,8 +91,8 @@
 			b.With(keys.String("myString").Of("some string value")).Log("string event")
 		},
 		expect: `
-2020/03/05 14:27:48	[1]	log	my event	{"myInt":6}
-2020/03/05 14:27:49	[2]	log	string event	{"myString":"some string value"}
+time=2020-03-05T14:27:48 id=1 kind=log msg="my event" myInt=6
+time=2020-03-05T14:27:49 id=2 kind=log msg="string event" myString="some string value"
 `}} {
 		buf := &strings.Builder{}
 		h := event.Printer(buf)
@@ -115,6 +115,6 @@
 	event.To(ctx).With(keys.Int("myInt").Of(6)).Log("my event")
 	event.To(ctx).With(keys.String("myString").Of("some string value")).Log("error event")
 	// Output:
-	// 2020/03/05 14:27:48	[1]	log	my event	{"myInt":6}
-	// 2020/03/05 14:27:49	[2]	log	error event	{"myString":"some string value"}
+	// time=2020-03-05T14:27:48 id=1 kind=log msg="my event" myInt=6
+	// time=2020-03-05T14:27:49 id=2 kind=log msg="error event" myString="some string value"
 }
diff --git a/event/label.go b/event/label.go
index 6fab1e8..8011a2f 100644
--- a/event/label.go
+++ b/event/label.go
@@ -8,7 +8,6 @@
 	"fmt"
 	"math"
 	"reflect"
-	"strconv"
 	"strings"
 	"unsafe"
 )
@@ -43,43 +42,12 @@
 
 // Format prints the label in a standard form.
 func (l *Label) Format(f fmt.State, verb rune) {
-	buf := bufPool.Get().(*buffer)
-	l.format(f.(writer), buf.data[:0])
-	bufPool.Put(buf)
-}
-
-func (l *Label) format(w writer, buf []byte) {
-	w.Write(strconv.AppendQuote(buf[:0], l.Name))
-	w.WriteString(":")
-	l.Value.format(w, buf)
+	newPrinter(f).Label(l)
 }
 
 // Format prints the value in a standard form.
 func (v *Value) Format(f fmt.State, verb rune) {
-	buf := bufPool.Get().(*buffer)
-	v.format(f.(writer), buf.data[:0])
-	bufPool.Put(buf)
-}
-
-func (v *Value) format(w writer, buf []byte) {
-	switch {
-	case v.IsString():
-		w.Write(strconv.AppendQuote(buf[:0], v.String()))
-	case v.IsInt64():
-		w.Write(strconv.AppendInt(buf[:0], v.Int64(), 10))
-	case v.IsUint64():
-		w.Write(strconv.AppendUint(buf[:0], v.Uint64(), 10))
-	case v.IsFloat64():
-		w.Write(strconv.AppendFloat(buf[:0], v.Float64(), 'g', -1, 64))
-	case v.IsBool():
-		if v.Bool() {
-			w.WriteString("true")
-		} else {
-			w.WriteString("false")
-		}
-	default:
-		fmt.Fprint(w, v.Interface())
-	}
+	newPrinter(f).Value(v)
 }
 
 // HasValue returns true if the value is set to any type.
@@ -129,9 +97,7 @@
 	}
 	// not a string, so invoke the formatter to build one
 	w := &strings.Builder{}
-	buf := bufPool.Get().(*buffer)
-	v.format(w, buf.data[:0])
-	bufPool.Put(buf)
+	newPrinter(w).Value(&v)
 	return w.String()
 }
 
diff --git a/event/logfmt.go b/event/logfmt.go
new file mode 100644
index 0000000..1ea8566
--- /dev/null
+++ b/event/logfmt.go
@@ -0,0 +1,184 @@
+// 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 event
+
+import (
+	"fmt"
+	"io"
+	"strconv"
+)
+
+//TODO: some actual research into what this arbritray optimization number should be
+const bufCap = 50
+
+type printer struct {
+	io.Writer
+	io.StringWriter
+
+	buf [bufCap]byte
+}
+
+type stringWriter struct {
+	io.Writer
+}
+
+// Printer returns a handler that prints the events to the supplied writer.
+// Each event is printed in logfmt format on a single line.
+func Printer(to io.Writer) Handler {
+	return newPrinter(to)
+}
+
+func newPrinter(to io.Writer) *printer {
+	p := &printer{Writer: to}
+	ok := false
+	p.StringWriter, ok = to.(io.StringWriter)
+	if !ok {
+		p.StringWriter = &stringWriter{to}
+	}
+	return p
+}
+
+// Handle makes printer implement the Handler interface so it can be used
+// directly to print all handled events.
+func (p *printer) Handle(ev *Event) {
+	p.Event(ev)
+	p.WriteString("\n")
+}
+
+func (p *printer) Event(ev *Event) {
+	const timeFormat = "2006-01-02T15:04:05"
+	if !ev.At.IsZero() {
+		p.WriteString("time=")
+		p.Write(ev.At.AppendFormat(p.buf[:0], timeFormat))
+		p.WriteString(" ")
+	}
+
+	p.WriteString("id=")
+	p.Write(strconv.AppendUint(p.buf[:0], ev.ID, 10))
+	if ev.Parent != 0 {
+		p.WriteString(" span=")
+		p.Write(strconv.AppendUint(p.buf[:0], ev.Parent, 10))
+	}
+
+	p.WriteString(" kind=")
+	p.Kind(ev.Kind)
+
+	if ev.Message != "" {
+		p.WriteString(" msg=")
+		p.Quote(ev.Message)
+	}
+
+	for _, l := range ev.Labels {
+		if l.Name == "" {
+			continue
+		}
+		p.WriteString(" ")
+		p.Label(&l)
+	}
+}
+
+func (p *printer) Kind(k Kind) {
+	switch k {
+	case LogKind:
+		p.WriteString("log")
+	case StartKind:
+		p.WriteString("start")
+	case EndKind:
+		p.WriteString("end")
+	case MetricKind:
+		p.WriteString("metric")
+	case AnnotateKind:
+		p.WriteString("annotate")
+	default:
+		p.Write(strconv.AppendUint(p.buf[:0], uint64(k), 10))
+	}
+}
+
+func (p *printer) Label(l *Label) {
+	p.Ident(l.Name)
+	p.WriteString("=")
+	p.Value(&l.Value)
+}
+
+func (p *printer) Value(v *Value) {
+	switch {
+	case v.IsString():
+		p.Quote(v.String())
+	case v.IsInt64():
+		p.Write(strconv.AppendInt(p.buf[:0], v.Int64(), 10))
+	case v.IsUint64():
+		p.Write(strconv.AppendUint(p.buf[:0], v.Uint64(), 10))
+	case v.IsFloat64():
+		p.Write(strconv.AppendFloat(p.buf[:0], v.Float64(), 'g', -1, 64))
+	case v.IsBool():
+		if v.Bool() {
+			p.WriteString("true")
+		} else {
+			p.WriteString("false")
+		}
+	default:
+		fmt.Fprint(p, v.Interface())
+	}
+}
+
+func (p *printer) Ident(s string) {
+	//TODO: this should also escape = if it occurs in an ident?
+	p.Quote(s)
+}
+
+func (p *printer) Quote(s string) {
+	if s == "" {
+		p.WriteString(`""`)
+		return
+	}
+	if !needQuote(s) {
+		p.WriteString(s)
+		return
+	}
+	// string needs quoting
+	p.WriteString(`"`)
+	written := 0
+	for o, r := range s {
+		q := quoteRune(r)
+		if len(q) == 0 {
+			continue
+		}
+		// write out any prefix
+		p.WriteString(s[written:o])
+		written = o + 1 // we can plus 1 because all runes we escape are ascii
+		// and write out the quoted rune
+		p.WriteString(q)
+	}
+	p.WriteString(s[written:])
+	p.WriteString(`"`)
+}
+
+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 ``
+	}
+}
+
+func (w *stringWriter) WriteString(s string) (int, error) {
+	return w.Write([]byte(s))
+}
diff --git a/event/severity/severity_test.go b/event/severity/severity_test.go
index 01c1b05..22662a8 100644
--- a/event/severity/severity_test.go
+++ b/event/severity/severity_test.go
@@ -24,11 +24,11 @@
 	}{{
 		name:   "debug",
 		events: func(ctx context.Context) { event.To(ctx).With(severity.Debug).Log("a message") },
-		expect: `2020/03/05 14:27:48	[1]	log	a message	{"level":debug}`,
+		expect: `time=2020-03-05T14:27:48 id=1 kind=log msg="a message" level=debug`,
 	}, {
 		name:   "info",
 		events: func(ctx context.Context) { event.To(ctx).With(severity.Info).Log("a message") },
-		expect: `2020/03/05 14:27:48	[1]	log	a message	{"level":info}`},
+		expect: `time=2020-03-05T14:27:48 id=1 kind=log msg="a message" level=info`},
 	} {
 		buf := &strings.Builder{}
 		h := event.Printer(buf)