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() },