event: logfmt improvements

Added some configuration options.
Added reasonably complete test cases.
Fixed some things that the tests found to be broken.

Change-Id: I5f975672026cdb42cb24077f5f51fac8bb37477b
Reviewed-on: https://go-review.googlesource.com/c/exp/+/329369
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/adapter/logfmt/logfmt.go b/event/adapter/logfmt/logfmt.go
index e12b580..9735b38 100644
--- a/event/adapter/logfmt/logfmt.go
+++ b/event/adapter/logfmt/logfmt.go
@@ -5,6 +5,7 @@
 package logfmt
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io"
@@ -21,13 +22,16 @@
 const TimeFormat = "2006/01/02 15:04:05"
 
 type Printer struct {
-	buf     [bufCap]byte
-	needSep bool
+	QuoteValues       bool
+	SuppressNamespace bool
+	buf               [bufCap]byte
+	needSep           bool
+	w                 bytes.Buffer
 }
 
 type Handler struct {
-	to      io.Writer
-	printer Printer
+	to io.Writer
+	Printer
 }
 
 // NewHandler returns a handler that prints the events to the supplied writer.
@@ -37,24 +41,24 @@
 }
 
 func (h *Handler) Log(ctx context.Context, ev *event.Event) {
-	h.printer.Event(h.to, ev)
+	h.Printer.Event(h.to, ev)
 }
 
 func (h *Handler) Metric(ctx context.Context, ev *event.Event) {
-	h.printer.Event(h.to, ev)
+	h.Printer.Event(h.to, ev)
 }
 
 func (h *Handler) Annotate(ctx context.Context, ev *event.Event) {
-	h.printer.Event(h.to, ev)
+	h.Printer.Event(h.to, ev)
 }
 
 func (h *Handler) Start(ctx context.Context, ev *event.Event) context.Context {
-	h.printer.Event(h.to, ev)
+	h.Printer.Event(h.to, ev)
 	return ctx
 }
 
 func (h *Handler) End(ctx context.Context, ev *event.Event) {
-	h.printer.Event(h.to, ev)
+	h.Printer.Event(h.to, ev)
 }
 
 func (p *Printer) Event(w io.Writer, ev *event.Event) {
@@ -63,6 +67,10 @@
 		p.label(w, "time", event.BytesOf(ev.At.AppendFormat(p.buf[:0], TimeFormat)))
 	}
 
+	if !p.SuppressNamespace && ev.Namespace != "" {
+		p.label(w, "in", event.StringOf(ev.Namespace))
+	}
+
 	if ev.Parent != 0 {
 		p.label(w, "parent", event.BytesOf(strconv.AppendUint(p.buf[:0], ev.Parent, 10)))
 	}
@@ -90,6 +98,10 @@
 		p.label(w, "end", event.Value{})
 	}
 
+	if ev.Error != nil {
+		p.label(w, "err", event.ValueOf(ev.Error))
+	}
+
 	io.WriteString(w, "\n")
 }
 
@@ -100,9 +112,19 @@
 func (p *Printer) Value(w io.Writer, v event.Value) {
 	switch {
 	case v.IsString():
-		p.Quote(w, v.String())
+		s := v.String()
+		if p.QuoteValues || stringNeedQuote(s) {
+			p.quoteString(w, s)
+		} else {
+			io.WriteString(w, s)
+		}
 	case v.IsBytes():
-		p.Bytes(w, v.Bytes())
+		buf := v.Bytes()
+		if p.QuoteValues || bytesNeedQuote(buf) {
+			p.quoteBytes(w, buf)
+		} else {
+			w.Write(buf)
+		}
 	case v.IsInt64():
 		w.Write(strconv.AppendInt(p.buf[:0], v.Int64(), 10))
 	case v.IsUint64():
@@ -116,19 +138,30 @@
 			io.WriteString(w, "false")
 		}
 	default:
-		fmt.Fprint(w, v.Interface())
+		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.Interface())
+		b := p.w.Bytes()
+		p.w.Reset()
+		if p.QuoteValues || bytesNeedQuote(b) {
+			p.quoteBytes(w, b)
+		} else {
+			w.Write(b)
+		}
 	}
 }
 
 func (p *Printer) Ident(w io.Writer, s string) {
-	p.Quote(w, s)
-}
-
-func (p *Printer) Quote(w io.Writer, s string) {
 	if !stringNeedQuote(s) {
 		io.WriteString(w, s)
 		return
 	}
+	p.quoteString(w, s)
+}
+
+func (p *Printer) quoteString(w io.Writer, s string) {
 	io.WriteString(w, `"`)
 	written := 0
 	for offset, r := range s {
@@ -147,11 +180,7 @@
 }
 
 // Bytes writes a byte array in string form to the printer.
-func (p *Printer) Bytes(w io.Writer, buf []byte) {
-	if !bytesNeedQuote(buf) {
-		w.Write(buf)
-		return
-	}
+func (p *Printer) quoteBytes(w io.Writer, buf []byte) {
 	io.WriteString(w, `"`)
 	written := 0
 	for offset := 0; offset < len(buf); {
diff --git a/event/adapter/logfmt/logfmt_test.go b/event/adapter/logfmt/logfmt_test.go
new file mode 100644
index 0000000..ca764e3
--- /dev/null
+++ b/event/adapter/logfmt/logfmt_test.go
@@ -0,0 +1,226 @@
+// 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_test
+
+import (
+	"errors"
+	"strings"
+	"testing"
+	"time"
+
+	"golang.org/x/exp/event"
+	"golang.org/x/exp/event/adapter/logfmt"
+)
+
+func TestPrint(t *testing.T) {
+	var p logfmt.Printer
+	buf := &strings.Builder{}
+	// the exporter is not used, but we need it to build the events
+	at, _ := time.Parse(logfmt.TimeFormat, "2020/03/05 14:27:48")
+	for _, test := range []struct {
+		name   string
+		event  event.Event
+		expect string
+	}{{
+		name:   "empty",
+		event:  event.Event{},
+		expect: ``,
+	}, {
+		name:   "span",
+		event:  event.Event{TraceID: 34},
+		expect: `trace=34`,
+	}, {
+		name:   "parent",
+		event:  event.Event{Parent: 14},
+		expect: `parent=14`,
+	}, {
+		name:   "namespace",
+		event:  event.Event{Namespace: "golang.org/x/exp/event"},
+		expect: `in="golang.org/x/exp/event"`,
+	}, {
+		name:   "name",
+		event:  event.Event{Name: "named"},
+		expect: `name=named`,
+	}, {
+		name:   "at",
+		event:  event.Event{At: at},
+		expect: `time="2020/03/05 14:27:48"`,
+	}, {
+		name:   "message",
+		event:  event.Event{Message: "a message"},
+		expect: `msg="a message"`,
+	}, {
+		name:   "error",
+		event:  event.Event{Error: errors.New("an error")},
+		expect: `err="an error"`,
+	}, {
+		name:   "end",
+		event:  event.Event{Kind: event.TraceKind},
+		expect: `end`,
+	}, {
+		name: "string",
+		event: event.Event{
+			Labels: []event.Label{{
+				Name:  "v1",
+				Value: event.StringOf("text"),
+			}, {
+				Name:  "v2",
+				Value: event.StringOf("text with quotes"),
+			}, {
+				Name:  "empty",
+				Value: event.StringOf(""),
+			}},
+		},
+		expect: `v1=text v2="text with quotes" empty=""`,
+	}, {
+		name: "int",
+		event: event.Event{
+			Labels: []event.Label{{
+				Name:  "value",
+				Value: event.Int64Of(67),
+			}},
+		},
+		expect: `value=67`,
+	}, {
+		name: "float",
+		event: event.Event{
+			Labels: []event.Label{{
+				Name:  "value",
+				Value: event.Float64Of(263.2),
+			}},
+		},
+		expect: `value=263.2`,
+	}, {
+		name: "bool",
+		event: event.Event{
+			Labels: []event.Label{{
+				Name:  "v1",
+				Value: event.BoolOf(true),
+			}, {
+				Name:  "v2",
+				Value: event.BoolOf(false),
+			}},
+		},
+		expect: `v1=true v2=false`,
+	}, {
+		name: "value",
+		event: event.Event{
+			Labels: []event.Label{{
+				Name:  "v1",
+				Value: event.ValueOf(notString{"simple"}),
+			}, {
+				Name:  "v2",
+				Value: event.ValueOf(notString{"needs quoting"}),
+			}},
+		},
+		expect: `v1=simple v2="needs quoting"`,
+	}, {
+		name: "empty label",
+		event: event.Event{
+			Labels: []event.Label{{
+				Name: "before",
+			}, {
+				Name:  "",
+				Value: event.StringOf("text"),
+			}, {
+				Name: "after",
+			}},
+		},
+		expect: `before after`,
+	}, {
+		name: "quoted ident",
+		event: event.Event{
+			Labels: []event.Label{{
+				Name:  "name with space",
+				Value: event.StringOf("text"),
+			}},
+		},
+		expect: `"name with space"=text`,
+	}, {
+		name:   "quoting quote",
+		event:  event.Event{Message: `with"middle`},
+		expect: `msg="with\"middle"`,
+	}, {
+		name:   "quoting newline",
+		event:  event.Event{Message: "with\nmiddle"},
+		expect: `msg="with\nmiddle"`,
+	}, {
+		name:   "quoting slash",
+		event:  event.Event{Message: `with\middle`},
+		expect: `msg="with\\middle"`,
+	}, {
+		name: "quoting bytes",
+		event: event.Event{
+			Labels: []event.Label{{
+				Name:  "value",
+				Value: event.BytesOf(([]byte)(`bytes "need" quote`)),
+			}},
+		},
+		expect: `value="bytes \"need\" quote"`,
+	}} {
+		t.Run(test.name, func(t *testing.T) {
+			buf.Reset()
+			p.Event(buf, &test.event)
+			got := strings.TrimSpace(buf.String())
+			if got != test.expect {
+				t.Errorf("got: \n%q\nexpect:\n%q\n", got, test.expect)
+			}
+		})
+	}
+}
+
+type notString struct {
+	value string
+}
+
+func (v notString) String() string { return v.value }
+
+func TestPrinterFlags(t *testing.T) {
+	var reference logfmt.Printer
+	buf := &strings.Builder{}
+	// the exporter is not used, but we need it to build the events
+	for _, test := range []struct {
+		name    string
+		printer logfmt.Printer
+		event   event.Event
+		before  string
+		after   string
+	}{{
+		name:    "quote values",
+		printer: logfmt.Printer{QuoteValues: true},
+		event: event.Event{
+			Labels: []event.Label{{
+				Name:  "value",
+				Value: event.StringOf("text"),
+			}},
+		},
+		before: `value=text`,
+		after:  `value="text"`,
+	}, {
+		name:    "suppress namespace",
+		printer: logfmt.Printer{SuppressNamespace: true},
+		event: event.Event{
+			Namespace: "golang.org/x/exp/event",
+			Message:   "some text",
+		},
+		before: `in="golang.org/x/exp/event" msg="some text"`,
+		after:  `msg="some text"`,
+	}} {
+		t.Run(test.name, func(t *testing.T) {
+			buf.Reset()
+			reference.Event(buf, &test.event)
+			gotBefore := strings.TrimSpace(buf.String())
+			buf.Reset()
+			test.printer.Event(buf, &test.event)
+			gotAfter := strings.TrimSpace(buf.String())
+			if gotBefore != test.before {
+				t.Errorf("got: \n%q\nexpect:\n%q\n", gotBefore, test.before)
+			}
+			if gotAfter != test.after {
+				t.Errorf("got: \n%q\nexpect:\n%q\n", gotAfter, test.after)
+			}
+		})
+	}
+}
diff --git a/event/event_test.go b/event/event_test.go
index 54158a6..fe4501a 100644
--- a/event/event_test.go
+++ b/event/event_test.go
@@ -77,17 +77,17 @@
 `}, {
 		name:   "counter",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).Metric(counter.Record(2)) },
-		expect: `time="2020/03/05 14:27:48" l1=1 metricValue=2 metric=Metric("golang.org/x/exp/event_test/hits")`,
+		expect: `time="2020/03/05 14:27:48" in="golang.org/x/exp/event_test" l1=1 metricValue=2 metric="Metric(\"golang.org/x/exp/event_test/hits\")"`,
 	}, {
 		name:   "gauge",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).Metric(gauge.Record(98.6)) },
-		expect: `time="2020/03/05 14:27:48" l1=1 metricValue=98.6 metric=Metric("golang.org/x/exp/event_test/temperature")`,
+		expect: `time="2020/03/05 14:27:48" in="golang.org/x/exp/event_test" l1=1 metricValue=98.6 metric="Metric(\"golang.org/x/exp/event_test/temperature\")"`,
 	}, {
 		name: "duration",
 		events: func(ctx context.Context) {
 			event.To(ctx).With(l1).With(l2).Metric(latency.Record(3 * time.Second))
 		},
-		expect: `time="2020/03/05 14:27:48" l1=1 l2=2 metricValue=3s metric=Metric("golang.org/x/exp/event_test/latency")`,
+		expect: `time="2020/03/05 14:27:48" in="golang.org/x/exp/event_test" l1=1 l2=2 metricValue=3s metric="Metric(\"golang.org/x/exp/event_test/latency\")"`,
 	}, {
 		name:   "annotate",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).Annotate() },