event: a better version of labels

Change-Id: I29cf264140cca74687f4f1bf3331ee2d18a76bea
Reviewed-on: https://go-review.googlesource.com/c/exp/+/313430
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/alloc_test.go b/event/alloc_test.go
new file mode 100644
index 0000000..f66aa43
--- /dev/null
+++ b/event/alloc_test.go
@@ -0,0 +1,29 @@
+// Copyright 2020 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.
+
+// +build !race
+
+package event_test
+
+import (
+	"context"
+	"io/ioutil"
+	"testing"
+
+	"golang.org/x/exp/event"
+)
+
+func TestAllocs(t *testing.T) {
+	anInt := event.Label{Name: "int", Value: event.Int64Of(4)}
+	aString := event.Label{Name: "string", Value: event.StringOf("value")}
+
+	e := event.NewExporter(event.Printer(ioutil.Discard))
+	ctx := event.WithExporter(context.Background(), e)
+	allocs := int(testing.AllocsPerRun(5, func() {
+		event.To(ctx).With(aString).With(anInt).Log("message")
+	}))
+	if allocs != 0 {
+		t.Errorf("Got %d allocs, expect 0", allocs)
+	}
+}
diff --git a/event/bench/alloc_test.go b/event/bench/alloc_test.go
new file mode 100644
index 0000000..5996350
--- /dev/null
+++ b/event/bench/alloc_test.go
@@ -0,0 +1,15 @@
+// Copyright 2020 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.
+
+// +build !race
+
+package bench_test
+
+import (
+	"testing"
+)
+
+func TestLogEventAllocs(t *testing.T) {
+	testAllocs(t, eventPrint, eventLog, 0)
+}
diff --git a/event/bench/bench_test.go b/event/bench/bench_test.go
index fb50637..774bddb 100644
--- a/event/bench/bench_test.go
+++ b/event/bench/bench_test.go
@@ -35,11 +35,11 @@
 
 const (
 	aName = "A"
-	aMsg  = "A"
-	aMsgf = aMsg + " where a=%d"
+	aMsg  = "a"
+	aMsgf = aMsg + " where " + aName + "=%d"
 	bName = "B"
 	bMsg  = "b"
-	bMsgf = bMsg + " where b=%q"
+	bMsgf = bMsg + " where " + bName + "=%q"
 
 	timeFormat = "2006/01/02 15:04:05"
 )
@@ -61,29 +61,41 @@
 	return a + len(b)
 }
 
-func runBenchmark(t testing.TB, ctx context.Context, hooks Hooks) {
+func runOnce(ctx context.Context, hooks Hooks) {
 	var acc int
-	if b, ok := t.(*testing.B); ok {
-		b.ReportAllocs()
-		b.ResetTimer()
-		for i := 0; i < b.N; i++ {
-			for _, value := range initialList {
-				acc += benchA(ctx, hooks, value)
-			}
-		}
-		return
-	}
 	for _, value := range initialList {
 		acc += benchA(ctx, hooks, value)
 	}
 }
 
-func testBenchmark(t testing.TB, f func(io.Writer) context.Context, hooks Hooks, expect string) {
+func runBenchmark(b *testing.B, ctx context.Context, hooks Hooks) {
+	b.ReportAllocs()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		runOnce(ctx, hooks)
+	}
+}
+
+func testBenchmark(t *testing.T, f func(io.Writer) context.Context, hooks Hooks, expect string) {
 	buf := strings.Builder{}
-	runBenchmark(t, f(&buf), hooks)
+	ctx := f(&buf)
+	runOnce(ctx, hooks)
 	got := strings.TrimSpace(buf.String())
 	expect = strings.TrimSpace(expect)
 	if diff := cmp.Diff(got, expect); diff != "" {
 		t.Error(diff)
 	}
 }
+
+func testAllocs(t *testing.T, f func(io.Writer) context.Context, hooks Hooks, expect int) {
+	var acc int
+	ctx := f(io.Discard)
+	got := int(testing.AllocsPerRun(5, func() {
+		for _, value := range initialList {
+			acc += benchA(ctx, hooks, value)
+		}
+	}))
+	if got != expect {
+		t.Errorf("Got %d allocs, expect %d", got, expect)
+	}
+}
diff --git a/event/bench/event_test.go b/event/bench/event_test.go
index b8c020d..a839ace 100644
--- a/event/bench/event_test.go
+++ b/event/bench/event_test.go
@@ -101,7 +101,7 @@
 }
 
 func eventPrint(w io.Writer) context.Context {
-	e := event.NewExporter(event.NewPrinter(w))
+	e := event.NewExporter(event.Printer(w))
 	e.Now = eventtest.TestNow()
 	return event.WithExporter(context.Background(), e)
 }
@@ -128,22 +128,43 @@
 
 func TestLogEventf(t *testing.T) {
 	testBenchmark(t, eventPrint, eventLogf, `
-2020/03/05 14:27:48 [log:1] A where a=0
-2020/03/05 14:27:49 [log:2] b where b="A value"
-2020/03/05 14:27:50 [log:3] A where a=1
-2020/03/05 14:27:51 [log:4] b where b="Some other value"
-2020/03/05 14:27:52 [log:5] A where a=22
-2020/03/05 14:27:53 [log:6] b where b="Some other value"
-2020/03/05 14:27:54 [log:7] A where a=333
-2020/03/05 14:27:55 [log:8] b where b=""
-2020/03/05 14:27:56 [log:9] A where a=4444
-2020/03/05 14:27:57 [log:10] b where b="prime count of values"
-2020/03/05 14:27:58 [log:11] A where a=55555
-2020/03/05 14:27:59 [log:12] b where b="V"
-2020/03/05 14:28:00 [log:13] A where a=666666
-2020/03/05 14:28:01 [log:14] b where b="A value"
-2020/03/05 14:28:02 [log:15] A where a=7777777
-2020/03/05 14:28:03 [log:16] b where b="A value"
+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"
+`)
+}
+
+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"}
 `)
 }
 
diff --git a/event/bench/logrus_test.go b/event/bench/logrus_test.go
index 735e4d2..30344d4 100644
--- a/event/bench/logrus_test.go
+++ b/event/bench/logrus_test.go
@@ -84,21 +84,21 @@
 
 func TestLogrusf(t *testing.T) {
 	testBenchmark(t, logrusPrint, logrusLogf, `
-time="2020/03/05 14:27:48" level=info msg="A where a=0"
-time="2020/03/05 14:27:49" level=info msg="b where b=\"A value\""
-time="2020/03/05 14:27:50" level=info msg="A where a=1"
-time="2020/03/05 14:27:51" level=info msg="b where b=\"Some other value\""
-time="2020/03/05 14:27:52" level=info msg="A where a=22"
-time="2020/03/05 14:27:53" level=info msg="b where b=\"Some other value\""
-time="2020/03/05 14:27:54" level=info msg="A where a=333"
-time="2020/03/05 14:27:55" level=info msg="b where b=\"\""
-time="2020/03/05 14:27:56" level=info msg="A where a=4444"
-time="2020/03/05 14:27:57" level=info msg="b where b=\"prime count of values\""
-time="2020/03/05 14:27:58" level=info msg="A where a=55555"
-time="2020/03/05 14:27:59" level=info msg="b where b=\"V\""
-time="2020/03/05 14:28:00" level=info msg="A where a=666666"
-time="2020/03/05 14:28:01" level=info msg="b where b=\"A value\""
-time="2020/03/05 14:28:02" level=info msg="A where a=7777777"
-time="2020/03/05 14:28:03" level=info msg="b where b=\"A value\""
+time="2020/03/05 14:27:48" level=info msg="a where A=0"
+time="2020/03/05 14:27:49" level=info msg="b where B=\"A value\""
+time="2020/03/05 14:27:50" level=info msg="a where A=1"
+time="2020/03/05 14:27:51" level=info msg="b where B=\"Some other value\""
+time="2020/03/05 14:27:52" level=info msg="a where A=22"
+time="2020/03/05 14:27:53" level=info msg="b where B=\"Some other value\""
+time="2020/03/05 14:27:54" level=info msg="a where A=333"
+time="2020/03/05 14:27:55" level=info msg="b where B=\"\""
+time="2020/03/05 14:27:56" level=info msg="a where A=4444"
+time="2020/03/05 14:27:57" level=info msg="b where B=\"prime count of values\""
+time="2020/03/05 14:27:58" level=info msg="a where A=55555"
+time="2020/03/05 14:27:59" level=info msg="b where B=\"V\""
+time="2020/03/05 14:28:00" level=info msg="a where A=666666"
+time="2020/03/05 14:28:01" level=info msg="b where B=\"A value\""
+time="2020/03/05 14:28:02" level=info msg="a where A=7777777"
+time="2020/03/05 14:28:03" level=info msg="b where B=\"A value\""
 `)
 }
diff --git a/event/bench/stdlib_test.go b/event/bench/stdlib_test.go
index 410963d..b91bf68 100644
--- a/event/bench/stdlib_test.go
+++ b/event/bench/stdlib_test.go
@@ -103,42 +103,42 @@
 
 func TestLogStdlib(t *testing.T) {
 	testBenchmark(t, stdlibLoggerNoTime, stdlibLog, `
-A where a=0
-b where b="A value"
-A where a=1
-b where b="Some other value"
-A where a=22
-b where b="Some other value"
-A where a=333
-b where b=""
-A where a=4444
-b where b="prime count of values"
-A where a=55555
-b where b="V"
-A where a=666666
-b where b="A value"
-A where a=7777777
-b where b="A value"
+a where A=0
+b where B="A value"
+a where A=1
+b where B="Some other value"
+a where A=22
+b where B="Some other value"
+a where A=333
+b where B=""
+a where A=4444
+b where B="prime count of values"
+a where A=55555
+b where B="V"
+a where A=666666
+b where B="A value"
+a where A=7777777
+b where B="A value"
 `)
 }
 
 func TestLogPrintf(t *testing.T) {
 	testBenchmark(t, stdlibWriter, stdlibPrintf, `
-2020/03/05 14:27:48 A where a=0
-2020/03/05 14:27:49 b where b="A value"
-2020/03/05 14:27:50 A where a=1
-2020/03/05 14:27:51 b where b="Some other value"
-2020/03/05 14:27:52 A where a=22
-2020/03/05 14:27:53 b where b="Some other value"
-2020/03/05 14:27:54 A where a=333
-2020/03/05 14:27:55 b where b=""
-2020/03/05 14:27:56 A where a=4444
-2020/03/05 14:27:57 b where b="prime count of values"
-2020/03/05 14:27:58 A where a=55555
-2020/03/05 14:27:59 b where b="V"
-2020/03/05 14:28:00 A where a=666666
-2020/03/05 14:28:01 b where b="A value"
-2020/03/05 14:28:02 A where a=7777777
-2020/03/05 14:28:03 b where b="A value"
+2020/03/05 14:27:48 a where A=0
+2020/03/05 14:27:49 b where B="A value"
+2020/03/05 14:27:50 a where A=1
+2020/03/05 14:27:51 b where B="Some other value"
+2020/03/05 14:27:52 a where A=22
+2020/03/05 14:27:53 b where B="Some other value"
+2020/03/05 14:27:54 a where A=333
+2020/03/05 14:27:55 b where B=""
+2020/03/05 14:27:56 a where A=4444
+2020/03/05 14:27:57 b where B="prime count of values"
+2020/03/05 14:27:58 a where A=55555
+2020/03/05 14:27:59 b where B="V"
+2020/03/05 14:28:00 a where A=666666
+2020/03/05 14:28:01 b where B="A value"
+2020/03/05 14:28:02 a where A=7777777
+2020/03/05 14:28:03 b where B="A value"
 `)
 }
diff --git a/event/bench/zap_test.go b/event/bench/zap_test.go
index e66642f..7878f35 100644
--- a/event/bench/zap_test.go
+++ b/event/bench/zap_test.go
@@ -23,7 +23,7 @@
 		},
 		AEnd: func(ctx context.Context) {},
 		BStart: func(ctx context.Context, b string) context.Context {
-			zapCtx(ctx).Info(aMsg, zap.String(bName, b))
+			zapCtx(ctx).Info(bMsg, zap.String(bName, b))
 			return ctx
 		},
 		BEnd: func(ctx context.Context) {},
@@ -75,21 +75,41 @@
 
 func TestLogZapf(t *testing.T) {
 	testBenchmark(t, zapPrint, zapLogf, `
-2020/03/05 14:27:48	info	A where a=0
-2020/03/05 14:27:49	info	b where b="A value"
-2020/03/05 14:27:50	info	A where a=1
-2020/03/05 14:27:51	info	b where b="Some other value"
-2020/03/05 14:27:52	info	A where a=22
-2020/03/05 14:27:53	info	b where b="Some other value"
-2020/03/05 14:27:54	info	A where a=333
-2020/03/05 14:27:55	info	b where b=""
-2020/03/05 14:27:56	info	A where a=4444
-2020/03/05 14:27:57	info	b where b="prime count of values"
-2020/03/05 14:27:58	info	A where a=55555
-2020/03/05 14:27:59	info	b where b="V"
-2020/03/05 14:28:00	info	A where a=666666
-2020/03/05 14:28:01	info	b where b="A value"
-2020/03/05 14:28:02	info	A where a=7777777
-2020/03/05 14:28:03	info	b where b="A value"
+2020/03/05 14:27:48	info	a where A=0
+2020/03/05 14:27:49	info	b where B="A value"
+2020/03/05 14:27:50	info	a where A=1
+2020/03/05 14:27:51	info	b where B="Some other value"
+2020/03/05 14:27:52	info	a where A=22
+2020/03/05 14:27:53	info	b where B="Some other value"
+2020/03/05 14:27:54	info	a where A=333
+2020/03/05 14:27:55	info	b where B=""
+2020/03/05 14:27:56	info	a where A=4444
+2020/03/05 14:27:57	info	b where B="prime count of values"
+2020/03/05 14:27:58	info	a where A=55555
+2020/03/05 14:27:59	info	b where B="V"
+2020/03/05 14:28:00	info	a where A=666666
+2020/03/05 14:28:01	info	b where B="A value"
+2020/03/05 14:28:02	info	a where A=7777777
+2020/03/05 14:28:03	info	b where B="A value"
+`)
+}
+func TestLogZap(t *testing.T) {
+	testBenchmark(t, zapPrint, zapLog, `
+2020/03/05 14:27:48	info	a	{"A": 0}
+2020/03/05 14:27:49	info	b	{"B": "A value"}
+2020/03/05 14:27:50	info	a	{"A": 1}
+2020/03/05 14:27:51	info	b	{"B": "Some other value"}
+2020/03/05 14:27:52	info	a	{"A": 22}
+2020/03/05 14:27:53	info	b	{"B": "Some other value"}
+2020/03/05 14:27:54	info	a	{"A": 333}
+2020/03/05 14:27:55	info	b	{"B": ""}
+2020/03/05 14:27:56	info	a	{"A": 4444}
+2020/03/05 14:27:57	info	b	{"B": "prime count of values"}
+2020/03/05 14:27:58	info	a	{"A": 55555}
+2020/03/05 14:27:59	info	b	{"B": "V"}
+2020/03/05 14:28:00	info	a	{"A": 666666}
+2020/03/05 14:28:01	info	b	{"B": "A value"}
+2020/03/05 14:28:02	info	a	{"A": 7777777}
+2020/03/05 14:28:03	info	b	{"B": "A value"}
 `)
 }
diff --git a/event/bench/zerolog_test.go b/event/bench/zerolog_test.go
index f5a048d..a3d694b 100644
--- a/event/bench/zerolog_test.go
+++ b/event/bench/zerolog_test.go
@@ -58,21 +58,21 @@
 
 func TestLogZerologf(t *testing.T) {
 	testBenchmark(t, zerologPrint, zerologMsgf, `
-{"level":"info","time":"2020/03/05 14:27:48","message":"A where a=0"}
-{"level":"info","time":"2020/03/05 14:27:49","message":"b where b=\"A value\""}
-{"level":"info","time":"2020/03/05 14:27:50","message":"A where a=1"}
-{"level":"info","time":"2020/03/05 14:27:51","message":"b where b=\"Some other value\""}
-{"level":"info","time":"2020/03/05 14:27:52","message":"A where a=22"}
-{"level":"info","time":"2020/03/05 14:27:53","message":"b where b=\"Some other value\""}
-{"level":"info","time":"2020/03/05 14:27:54","message":"A where a=333"}
-{"level":"info","time":"2020/03/05 14:27:55","message":"b where b=\"\""}
-{"level":"info","time":"2020/03/05 14:27:56","message":"A where a=4444"}
-{"level":"info","time":"2020/03/05 14:27:57","message":"b where b=\"prime count of values\""}
-{"level":"info","time":"2020/03/05 14:27:58","message":"A where a=55555"}
-{"level":"info","time":"2020/03/05 14:27:59","message":"b where b=\"V\""}
-{"level":"info","time":"2020/03/05 14:28:00","message":"A where a=666666"}
-{"level":"info","time":"2020/03/05 14:28:01","message":"b where b=\"A value\""}
-{"level":"info","time":"2020/03/05 14:28:02","message":"A where a=7777777"}
-{"level":"info","time":"2020/03/05 14:28:03","message":"b where b=\"A value\""}
+{"level":"info","time":"2020/03/05 14:27:48","message":"a where A=0"}
+{"level":"info","time":"2020/03/05 14:27:49","message":"b where B=\"A value\""}
+{"level":"info","time":"2020/03/05 14:27:50","message":"a where A=1"}
+{"level":"info","time":"2020/03/05 14:27:51","message":"b where B=\"Some other value\""}
+{"level":"info","time":"2020/03/05 14:27:52","message":"a where A=22"}
+{"level":"info","time":"2020/03/05 14:27:53","message":"b where B=\"Some other value\""}
+{"level":"info","time":"2020/03/05 14:27:54","message":"a where A=333"}
+{"level":"info","time":"2020/03/05 14:27:55","message":"b where B=\"\""}
+{"level":"info","time":"2020/03/05 14:27:56","message":"a where A=4444"}
+{"level":"info","time":"2020/03/05 14:27:57","message":"b where B=\"prime count of values\""}
+{"level":"info","time":"2020/03/05 14:27:58","message":"a where A=55555"}
+{"level":"info","time":"2020/03/05 14:27:59","message":"b where B=\"V\""}
+{"level":"info","time":"2020/03/05 14:28:00","message":"a where A=666666"}
+{"level":"info","time":"2020/03/05 14:28:01","message":"b where B=\"A value\""}
+{"level":"info","time":"2020/03/05 14:28:02","message":"a where A=7777777"}
+{"level":"info","time":"2020/03/05 14:28:03","message":"b where B=\"A value\""}
 `)
 }
diff --git a/event/builder_test.go b/event/builder_test.go
index a683b2b..ecd07ce 100644
--- a/event/builder_test.go
+++ b/event/builder_test.go
@@ -5,7 +5,6 @@
 package event_test
 
 import (
-	"bytes"
 	"fmt"
 	"testing"
 
@@ -28,7 +27,7 @@
 
 	check := func(b *event.Builder, want []event.Label) {
 		t.Helper()
-		if got := b.Event.Labels; !cmp.Equal(got, want, cmp.Comparer(labelEqual)) {
+		if got := b.Event.Labels; !cmp.Equal(got, want, cmp.Comparer(valueEqual)) {
 			t.Fatalf("got %v, want %v", got, want)
 		}
 	}
@@ -62,13 +61,6 @@
 	check(b2, labels[3:5])
 }
 
-func labelEqual(l1, l2 event.Label) bool {
-	return labelString(l1) == labelString(l2)
-}
-
-func labelString(l event.Label) string {
-	var buf bytes.Buffer
-	p := event.NewPrinter(&buf)
-	p.Label(l)
-	return buf.String()
+func valueEqual(l1, l2 event.Value) bool {
+	return fmt.Sprint(l1) == fmt.Sprint(l2)
 }
diff --git a/event/event.go b/event/event.go
index ec28174..5c26cd6 100644
--- a/event/event.go
+++ b/event/event.go
@@ -6,6 +6,9 @@
 
 import (
 	"fmt"
+	"io"
+	"strconv"
+	"sync"
 	"time"
 )
 
@@ -38,31 +41,101 @@
 	AnnotateKind
 )
 
-// Find searches the labels of an event to see if one of them has the
-// supplied key.
-func (ev Event) Find(key string) Label {
-	for _, l := range ev.Labels {
-		if l.Key() == key {
-			return l
-		}
-	}
-	return Label{}
+// 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)
 }
 
-// String returns a string representation of the kind for printing.
-func (k Kind) String() string {
+// 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("}")
+	}
+}
+
+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:
-		return "log"
+		w.WriteString("log")
 	case StartKind:
-		return "start"
+		w.WriteString("start")
 	case EndKind:
-		return "end"
+		w.WriteString("end")
 	case MetricKind:
-		return "metric"
+		w.WriteString("metric")
 	case AnnotateKind:
-		return "annotate"
+		w.WriteString("annotate")
 	default:
-		return fmt.Sprint(byte(k))
+		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
+}
diff --git a/event/event_test.go b/event/event_test.go
index 665f31a..689858d 100644
--- a/event/event_test.go
+++ b/event/event_test.go
@@ -21,15 +21,6 @@
 	l3 = keys.Int("l3").Of(3)
 )
 
-type captureHandler struct {
-	printer event.Printer
-	buf     strings.Builder
-}
-
-func (e *captureHandler) Handle(ev *event.Event) {
-	e.printer.Handle(ev)
-}
-
 func TestPrint(t *testing.T) {
 	ctx := context.Background()
 	for _, test := range []struct {
@@ -40,37 +31,27 @@
 		name:   "simple",
 		events: func(ctx context.Context) { event.To(ctx).Log("a message") },
 		expect: `
-2020/03/05 14:27:48 [log:1] a message
+2020/03/05 14:27:48	[1]	log	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 [log:1] a message
-	l1=1
-`}, {
+		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 [log:1] a message
-	l1=1
-	l2=2
-`}, {
+		expect: `2020/03/05 14:27:48	[1]	log	a message	{"l1":1, "l2":2}`,
+	}, {
 		name:   "simple",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).With(l2).With(l3).Log("a message") },
-		expect: `
-2020/03/05 14:27:48 [log:1] a message
-	l1=1
-	l2=2
-	l3=3
-`}, {
+		expect: `2020/03/05 14:27:48	[1]	log	a message	{"l1":1, "l2":2, "l3":3}`,
+	}, {
 		name: "span",
 		events: func(ctx context.Context) {
 			ctx, end := event.Start(ctx, "span")
 			end()
 		},
 		expect: `
-2020/03/05 14:27:48 [start:1] span
-2020/03/05 14:27:49 [end:2:1]
+2020/03/05 14:27:48	[1]	start	span
+2020/03/05 14:27:49	[2:1]	end
 `}, {
 		name: "span nested",
 		events: func(ctx context.Context) {
@@ -81,38 +62,28 @@
 			event.To(child).Log("message")
 		},
 		expect: `
-2020/03/05 14:27:48 [start:1] parent
-2020/03/05 14:27:49 [start:2:1] child
-2020/03/05 14:27:50 [log:3:2] message
-2020/03/05 14:27:51 [end:4:2]
-2020/03/05 14:27:52 [end:5:1]
+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
 `}, {
 		name:   "metric",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).Metric() },
-		expect: `
-2020/03/05 14:27:48 [metric:1]
-	l1=1
-`}, {
+		expect: `2020/03/05 14:27:48	[1]	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 [metric:1]
-	l1=1
-	l2=2
-`}, {
+		expect: `2020/03/05 14:27:48	[1]	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 [annotate:1]
-	l1=1
-`}, {
+		expect: `2020/03/05 14:27:48	[1]	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 [annotate:1]
-	l1=1
-	l2=2
-`}, {
+		expect: `2020/03/05 14:27:48	[1]	annotate	{"l1":1, "l2":2}`,
+	}, {
 		name: "multiple events",
 		events: func(ctx context.Context) {
 			b := event.To(ctx)
@@ -120,34 +91,30 @@
 			b.With(keys.String("myString").Of("some string value")).Log("string event")
 		},
 		expect: `
-2020/03/05 14:27:48 [log:1] my event
-	myInt=6
-2020/03/05 14:27:49 [log:2] string event
-	myString="some string value"
+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"}
 `}} {
-		h := &captureHandler{}
-		h.printer = event.NewPrinter(&h.buf)
+		buf := &strings.Builder{}
+		h := event.Printer(buf)
 		e := event.NewExporter(h)
 		e.Now = eventtest.TestNow()
 		ctx := event.WithExporter(ctx, e)
 		test.events(ctx)
-		got := strings.TrimSpace(h.buf.String())
+		got := strings.TrimSpace(buf.String())
 		expect := strings.TrimSpace(test.expect)
 		if got != expect {
-			t.Errorf("%s failed\ngot   : %q\nexpect: %q", test.name, got, expect)
+			t.Errorf("%s failed\ngot   : %s\nexpect: %s", test.name, got, expect)
 		}
 	}
 }
 
 func ExampleLog() {
-	e := event.NewExporter(event.NewPrinter(os.Stdout))
+	e := event.NewExporter(event.Printer(os.Stdout))
 	e.Now = eventtest.TestNow()
 	ctx := event.WithExporter(context.Background(), e)
 	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 [log:1] my event
-	// 	myInt=6
-	// 2020/03/05 14:27:49 [log:2] error event
-	// 	myString="some string value"
+	// 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"}
 }
diff --git a/event/eventtest/eventtest.go b/event/eventtest/eventtest.go
index badd861..9e0a8de 100644
--- a/event/eventtest/eventtest.go
+++ b/event/eventtest/eventtest.go
@@ -11,6 +11,7 @@
 
 import (
 	"context"
+	"fmt"
 	"strings"
 	"testing"
 	"time"
@@ -21,20 +22,18 @@
 // NewContext returns a context you should use for the active test.
 func NewContext(ctx context.Context, tb testing.TB) context.Context {
 	h := &testHandler{tb: tb}
-	h.printer = event.NewPrinter(&h.buf)
 	return event.WithExporter(ctx, event.NewExporter(h))
 }
 
 type testHandler struct {
-	tb      testing.TB
-	printer event.Printer
-	buf     strings.Builder
+	tb  testing.TB
+	buf strings.Builder
 }
 
 func (w *testHandler) Handle(ev *event.Event) {
 	// build our log message in buffer
 	w.buf.Reset()
-	w.printer.Handle(ev)
+	fmt.Fprint(&w.buf, ev)
 	// log to the testing.TB
 	msg := w.buf.String()
 	if len(msg) > 0 {
diff --git a/event/keys/keys.go b/event/keys/keys.go
index b18b0b7..574d696 100644
--- a/event/keys/keys.go
+++ b/event/keys/keys.go
@@ -5,22 +5,18 @@
 package keys
 
 import (
-	"math"
-
 	"golang.org/x/exp/event"
 )
 
 // Value represents a key for untyped values.
 type Value string
 
-func (k Value) Name() string { return string(k) }
-
 // From can be used to get a value from a Label.
-func (k Value) From(t event.Label) interface{} { return t.UnpackValue() }
+func (k Value) From(l event.Label) interface{} { return l.Value.Interface() }
 
 // Of creates a new Label with this key and the supplied value.
-func (k Value) Of(value interface{}) event.Label {
-	return event.OfValue(string(k), dispatchValue, value)
+func (k Value) Of(v interface{}) event.Label {
+	return event.Label{Name: string(k), Value: event.ValueOf(v)}
 }
 
 // Tag represents a key for tagging labels that have no value.
@@ -29,189 +25,163 @@
 // package.
 type Tag string
 
-func (k Tag) Name() string { return string(k) }
-
 // New creates a new Label with this key.
-func (k Tag) New() event.Label { return event.OfValue(string(k), nil, nil) }
+func (k Tag) New() event.Label {
+	return event.Label{Name: string(k)}
+}
 
 // Int represents a key
 type Int string
 
-func (k Int) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k Int) Of(v int) event.Label { return event.Of64(string(k), dispatchInt, uint64(v)) }
+func (k Int) Of(v int) event.Label {
+	return event.Label{Name: string(k), Value: event.Int64Of(int64(v))}
+}
 
 // From can be used to get a value from a Label.
-func (k Int) From(l event.Label) int { return int(l.Unpack64()) }
+func (k Int) From(l event.Label) int { return int(l.Value.Int64()) }
 
 // Int8 represents a key
 type Int8 string
 
-func (k Int8) Name() string                         { return string(k) }
-func (k Int8) Print(p event.Printer, l event.Label) { p.Int(int64(k.From(l))) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k Int8) Of(v int8) event.Label { return event.Of64(string(k), dispatchInt, uint64(v)) }
+func (k Int8) Of(v int8) event.Label {
+	return event.Label{Name: string(k), Value: event.Int64Of(int64(v))}
+}
 
 // From can be used to get a value from a Label.
-func (k Int8) From(t event.Label) int8 { return int8(t.Unpack64()) }
+func (k Int8) From(l event.Label) int8 { return int8(l.Value.Int64()) }
 
 // Int16 represents a key
 type Int16 string
 
-func (k Int16) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k Int16) Of(v int16) event.Label { return event.Of64(string(k), dispatchInt, uint64(v)) }
+func (k Int16) Of(v int16) event.Label {
+	return event.Label{Name: string(k), Value: event.Int64Of(int64(v))}
+}
 
 // From can be used to get a value from a Label.
-func (k Int16) From(t event.Label) int16 { return int16(t.Unpack64()) }
+func (k Int16) From(l event.Label) int16 { return int16(l.Value.Int64()) }
 
 // Int32 represents a key
 type Int32 string
 
-func (k Int32) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k Int32) Of(v int32) event.Label { return event.Of64(string(k), dispatchInt, uint64(v)) }
+func (k Int32) Of(v int32) event.Label {
+	return event.Label{Name: string(k), Value: event.Int64Of(int64(v))}
+}
 
 // From can be used to get a value from a Label.
-func (k Int32) From(t event.Label) int32 { return int32(t.Unpack64()) }
+func (k Int32) From(l event.Label) int32 { return int32(l.Value.Int64()) }
 
 // Int64 represents a key
 type Int64 string
 
-func (k Int64) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k Int64) Of(v int64) event.Label { return event.Of64(string(k), dispatchInt, uint64(v)) }
+func (k Int64) Of(v int64) event.Label {
+	return event.Label{Name: string(k), Value: event.Int64Of(v)}
+}
 
 // From can be used to get a value from a Label.
-func (k Int64) From(t event.Label) int64 { return int64(t.Unpack64()) }
+func (k Int64) From(l event.Label) int64 { return l.Value.Int64() }
 
 // UInt represents a key
 type UInt string
 
-func (k UInt) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k UInt) Of(v uint) event.Label { return event.Of64(string(k), dispatchUint, uint64(v)) }
+func (k UInt) Of(v uint) event.Label {
+	return event.Label{Name: string(k), Value: event.Uint64Of(uint64(v))}
+}
 
 // From can be used to get a value from a Label.
-func (k UInt) From(t event.Label) uint { return uint(t.Unpack64()) }
+func (k UInt) From(l event.Label) uint { return uint(l.Value.Uint64()) }
 
 // UInt8 represents a key
 type UInt8 string
 
-func (k UInt8) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k UInt8) Of(v uint8) event.Label { return event.Of64(string(k), dispatchUint, uint64(v)) }
+func (k UInt8) Of(v uint8) event.Label {
+	return event.Label{Name: string(k), Value: event.Uint64Of(uint64(v))}
+}
 
 // From can be used to get a value from a Label.
-func (k UInt8) From(t event.Label) uint8 { return uint8(t.Unpack64()) }
+func (k UInt8) From(l event.Label) uint8 { return uint8(l.Value.Uint64()) }
 
 // UInt16 represents a key
 type UInt16 string
 
-func (k UInt16) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k UInt16) Of(v uint16) event.Label { return event.Of64(string(k), dispatchUint, uint64(v)) }
+func (k UInt16) Of(v uint16) event.Label {
+	return event.Label{Name: string(k), Value: event.Uint64Of(uint64(v))}
+}
 
 // From can be used to get a value from a Label.
-func (k UInt16) From(t event.Label) uint16 { return uint16(t.Unpack64()) }
+func (k UInt16) From(l event.Label) uint16 { return uint16(l.Value.Uint64()) }
 
 // UInt32 represents a key
 type UInt32 string
 
-func (k UInt32) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k UInt32) Of(v uint32) event.Label { return event.Of64(string(k), dispatchUint, uint64(v)) }
+func (k UInt32) Of(v uint32) event.Label {
+	return event.Label{Name: string(k), Value: event.Uint64Of(uint64(v))}
+}
 
 // From can be used to get a value from a Label.
-func (k UInt32) From(t event.Label) uint32 { return uint32(t.Unpack64()) }
+func (k UInt32) From(l event.Label) uint32 { return uint32(l.Value.Uint64()) }
 
 // UInt64 represents a key
 type UInt64 string
 
-func (k UInt64) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k UInt64) Of(v uint64) event.Label { return event.Of64(string(k), dispatchUint, v) }
+func (k UInt64) Of(v uint64) event.Label {
+	return event.Label{Name: string(k), Value: event.Uint64Of(v)}
+}
 
 // From can be used to get a value from a Label.
-func (k UInt64) From(t event.Label) uint64 { return t.Unpack64() }
+func (k UInt64) From(l event.Label) uint64 { return l.Value.Uint64() }
 
 // Float32 represents a key
 type Float32 string
 
-func (k Float32) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
 func (k Float32) Of(v float32) event.Label {
-	return event.Of64(string(k), dispatchFloat, uint64(math.Float32bits(v)))
+	return event.Label{Name: string(k), Value: event.Float64Of(float64(v))}
 }
 
 // From can be used to get a value from a Label.
-func (k Float32) From(t event.Label) float32 {
-	return math.Float32frombits(uint32(t.Unpack64()))
-}
+func (k Float32) From(l event.Label) float32 { return float32(l.Value.Float64()) }
 
 // Float64 represents a key
 type Float64 string
 
-func (k Float64) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
 func (k Float64) Of(v float64) event.Label {
-	return event.Of64(string(k), dispatchFloat, math.Float64bits(v))
+	return event.Label{Name: string(k), Value: event.Float64Of(v)}
 }
 
 // From can be used to get a value from a Label.
-func (k Float64) From(t event.Label) float64 {
-	return math.Float64frombits(t.Unpack64())
+func (k Float64) From(l event.Label) float64 {
+	return l.Value.Float64()
 }
 
 // String represents a key
 type String string
 
-func (k String) Name() string { return string(k) }
-
 // Of creates a new Label with this key and the supplied value.
-func (k String) Of(v string) event.Label { return event.OfString(string(k), dispatchString, v) }
-
-// From can be used to get a value from a Label.
-func (k String) From(t event.Label) string { return t.UnpackString() }
-
-// Boolean represents a key
-type Boolean string
-
-func (k Boolean) Name() string { return string(k) }
-
-// Of creates a new Label with this key and the supplied value.
-func (k Boolean) Of(v bool) event.Label {
-	if v {
-		return event.Of64(string(k), dispatchBoolean, 1)
-	}
-	return event.Of64(string(k), dispatchBoolean, 0)
+func (k String) Of(v string) event.Label {
+	return event.Label{Name: string(k), Value: event.StringOf(v)}
 }
 
 // From can be used to get a value from a Label.
-func (k Boolean) From(t event.Label) bool { return t.Unpack64() > 0 }
+func (k String) From(l event.Label) string { return l.Value.String() }
 
-func dispatchValue(h event.ValueHandler, l event.Label)  { h.Value(l.UnpackValue()) }
-func dispatchInt(h event.ValueHandler, l event.Label)    { h.Int(int64(l.Unpack64())) }
-func dispatchUint(h event.ValueHandler, l event.Label)   { h.Uint(l.Unpack64()) }
-func dispatchString(h event.ValueHandler, l event.Label) { h.Quote(l.UnpackString()) }
-func dispatchFloat(h event.ValueHandler, l event.Label)  { h.Float(math.Float64frombits(l.Unpack64())) }
+// Bool represents a key
+type Bool string
 
-func dispatchBoolean(h event.ValueHandler, l event.Label) {
-	if l.Unpack64() > 0 {
-		h.String("true")
-	} else {
-		h.String("false")
-	}
+// Of creates a new Label with this key and the supplied value.
+func (k Bool) Of(v bool) event.Label {
+	return event.Label{Name: string(k), Value: event.BoolOf(v)}
 }
+
+// From can be used to get a value from a Label.
+func (k Bool) From(l event.Label) bool { return l.Value.Bool() }
diff --git a/event/label.go b/event/label.go
index d867dd4..6fab1e8 100644
--- a/event/label.go
+++ b/event/label.go
@@ -6,135 +6,223 @@
 
 import (
 	"fmt"
+	"math"
 	"reflect"
 	"strconv"
+	"strings"
 	"unsafe"
 )
 
-// ValueHandler is used to safely unpack unknown labels.
-type ValueHandler interface {
-	String(v string)
-	Quote(v string)
-	Int(v int64)
-	Uint(v uint64)
-	Float(v float64)
-	Value(v interface{})
+// Value holds any value in an efficient way that avoids allocations for
+// most types.
+type Value struct {
+	packed  uint64
+	untyped interface{}
 }
 
-// LabelDispatcher is used as the identity of a Label.
-type LabelDispatcher func(h ValueHandler, l Label)
-
-// Label holds a key and value pair.
-// It is normally used when passing around lists of labels.
+// Label is a named value.
 type Label struct {
-	key      string
-	dispatch LabelDispatcher
-	packed   uint64
-	untyped  interface{}
+	Name  string
+	Value Value
 }
 
-// OfValue creates a new label from the key and value.
-// This method is for implementing new key types, label creation should
-// normally be done with the Of method of the key.
-func OfValue(k string, d LabelDispatcher, value interface{}) Label {
-	return Label{key: k, dispatch: d, untyped: value}
-}
-
-// UnpackValue assumes the label was built using LabelOfValue and returns the value
-// that was passed to that constructor.
-// This method is for implementing new key types, for type safety normal
-// access should be done with the From method of the key.
-func (l Label) UnpackValue() interface{} { return l.untyped }
-
-// Of64 creates a new label from a key and a uint64. This is often
-// used for non uint64 values that can be packed into a uint64.
-// This method is for implementing new key types, label creation should
-// normally be done with the Of method of the key.
-func Of64(k string, d LabelDispatcher, v uint64) Label {
-	return Label{key: k, dispatch: d, packed: v}
-}
-
-// Unpack64 assumes the label was built using LabelOf64 and returns the value that
-// was passed to that constructor.
-// This method is for implementing new key types, for type safety normal
-// access should be done with the From method of the key.
-func (l Label) Unpack64() uint64 { return l.packed }
-
+// stringptr is used in untyped when the Value is a string
 type stringptr unsafe.Pointer
 
-// OfString creates a new label from a key and a string.
-// This method is for implementing new key types, label creation should
-// normally be done with the Of method of the key.
-func OfString(k string, d LabelDispatcher, v string) Label {
-	hdr := (*reflect.StringHeader)(unsafe.Pointer(&v))
-	return Label{
-		key:      k,
-		dispatch: d,
-		packed:   uint64(hdr.Len),
-		untyped:  stringptr(hdr.Data),
+// int64Kind is used in untyped when the Value is a signed integer
+type int64Kind struct{}
+
+// uint64Kind is used in untyped when the Value is an unsigned integer
+type uint64Kind struct{}
+
+// float64Kind is used in untyped when the Value is a floating point number
+type float64Kind struct{}
+
+// boolKind is used in untyped when the Value is a boolean
+type boolKind struct{}
+
+// 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)
+}
+
+// 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())
 	}
 }
 
-// UnpackString assumes the label was built using LabelOfString and returns the
-// value that was passed to that constructor.
-// This method is for implementing new key types, for type safety normal
-// access should be done with the From method of the key.
-func (l Label) UnpackString() string {
-	var v string
-	hdr := (*reflect.StringHeader)(unsafe.Pointer(&v))
-	hdr.Data = uintptr(l.untyped.(stringptr))
-	hdr.Len = int(l.packed)
-	return v
+// HasValue returns true if the value is set to any type.
+func (v *Value) HasValue() bool { return v.untyped != nil }
+
+// ValueOf returns a Value for the supplied value.
+func ValueOf(value interface{}) Value {
+	return Value{untyped: value}
 }
 
-// Valid returns true if the Label is a valid one (it has a key).
-func (l Label) Valid() bool { return l.key != "" }
-
-// Key returns the key of this Label.
-func (l Label) Key() string { return l.key }
-
-// Apply calls the appropriate method of h on the label's value.
-func (l Label) Apply(h ValueHandler) {
-	if l.dispatch != nil {
-		l.dispatch(h, l)
+// Interface returns the value.
+// This will never panic, things that were not set using SetInterface will be
+// unpacked and returned anyway.
+func (v Value) Interface() interface{} {
+	switch {
+	case v.IsString():
+		return v.String()
+	case v.IsInt64():
+		return v.Int64()
+	case v.IsUint64():
+		return v.Uint64()
+	case v.IsFloat64():
+		return v.Float64()
+	case v.IsBool():
+		return v.Bool()
+	default:
+		return v.untyped
 	}
 }
 
-//////////////////////////////////////////////////////////////////////
-
-// These are more demos of what Apply can do, rather than things we'd
-// necessarily want here.
-
-// Value is an expensive but general way to get a label's value.
-func (l Label) Value() interface{} {
-	var v interface{}
-	l.Apply(vhandler{&v})
-	return v
+// StringOf returns a new Value for a string.
+func StringOf(s string) Value {
+	hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
+	return Value{packed: uint64(hdr.Len), untyped: stringptr(hdr.Data)}
 }
 
-type vhandler struct {
-	pv *interface{}
+// String returns the value as a string.
+// This does not panic if v's Kind is not String, instead, it returns a string
+// representation of the value in all cases.
+func (v Value) String() string {
+	if sp, ok := v.untyped.(stringptr); ok {
+		var s string
+		hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
+		hdr.Data = uintptr(sp)
+		hdr.Len = int(v.packed)
+		return s
+	}
+	// 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)
+	return w.String()
 }
 
-func (h vhandler) String(v string)     { *h.pv = v }
-func (h vhandler) Quote(v string)      { *h.pv = strconv.Quote(v) }
-func (h vhandler) Int(v int64)         { *h.pv = v }
-func (h vhandler) Uint(v uint64)       { *h.pv = v }
-func (h vhandler) Float(v float64)     { *h.pv = v }
-func (h vhandler) Value(v interface{}) { *h.pv = v }
-
-// AppendValue appends the value of l to *dst as text.
-func (l Label) AppendValue(dst *[]byte) {
-	l.Apply(ahandler{dst})
+// IsString returns true if the value was built with SetString.
+func (v Value) IsString() bool {
+	_, ok := v.untyped.(stringptr)
+	return ok
 }
 
-type ahandler struct {
-	b *[]byte
+// Int64Of returns a new Value for a signed integer.
+func Int64Of(u int64) Value {
+	return Value{packed: uint64(u), untyped: int64Kind{}}
 }
 
-func (h ahandler) String(v string)     { *h.b = append(*h.b, v...) }
-func (h ahandler) Quote(v string)      { *h.b = strconv.AppendQuote(*h.b, v) }
-func (h ahandler) Int(v int64)         { *h.b = strconv.AppendInt(*h.b, v, 10) }
-func (h ahandler) Uint(v uint64)       { *h.b = strconv.AppendUint(*h.b, v, 10) }
-func (h ahandler) Float(v float64)     { *h.b = strconv.AppendFloat(*h.b, v, 'E', -1, 32) }
-func (h ahandler) Value(v interface{}) { *h.b = append(*h.b, fmt.Sprint(v)...) }
+// Int64 returns the int64 from a value that was set with SetInt64.
+// It will panic for any value for which IsInt64 is not true.
+func (v Value) Int64() int64 {
+	if !v.IsInt64() {
+		panic("Int64 called on non int64 value")
+	}
+	return int64(v.packed)
+}
+
+// IsInt64 returns true if the value was built with SetInt64.
+func (v Value) IsInt64() bool {
+	_, ok := v.untyped.(int64Kind)
+	return ok
+}
+
+// Uint64Of returns a new Value for an unsigned integer.
+func Uint64Of(u uint64) Value {
+	return Value{packed: u, untyped: uint64Kind{}}
+}
+
+// Uint64 returns the uint64 from a value that was set with SetUint64.
+// It will panic for any value for which IsUint64 is not true.
+func (v Value) Uint64() uint64 {
+	if !v.IsUint64() {
+		panic("Uint64 called on non uint64 value")
+	}
+	return v.packed
+}
+
+// IsUint64 returns true if the value was built with SetUint64.
+func (v Value) IsUint64() bool {
+	_, ok := v.untyped.(uint64Kind)
+	return ok
+}
+
+// Float64Of returns a new Value for a floating point number.
+func Float64Of(f float64) Value {
+	return Value{packed: math.Float64bits(f), untyped: float64Kind{}}
+}
+
+// Float64 returns the float64 from a value that was set with SetFloat64.
+// It will panic for any value for which IsFloat64 is not true.
+func (v Value) Float64() float64 {
+	if !v.IsFloat64() {
+		panic("Float64 called on non float64 value")
+	}
+	return math.Float64frombits(v.packed)
+}
+
+// IsFloat64 returns true if the value was built with SetFloat64.
+func (v Value) IsFloat64() bool {
+	_, ok := v.untyped.(float64Kind)
+	return ok
+}
+
+// BoolOf returns a new Value for a bool.
+func BoolOf(b bool) Value {
+	if b {
+		return Value{packed: 1, untyped: boolKind{}}
+	}
+	return Value{packed: 0, untyped: boolKind{}}
+}
+
+// Bool returns the bool from a value that was set with SetBool.
+// It will panic for any value for which IsBool is not true.
+func (v Value) Bool() bool {
+	if !v.IsBool() {
+		panic("Bool called on non bool value")
+	}
+	if v.packed != 0 {
+		return true
+	}
+	return false
+}
+
+// IsBool returns true if the value was built with SetBool.
+func (v Value) IsBool() bool {
+	_, ok := v.untyped.(boolKind)
+	return ok
+}
diff --git a/event/label_test.go b/event/label_test.go
new file mode 100644
index 0000000..3a5c549
--- /dev/null
+++ b/event/label_test.go
@@ -0,0 +1,120 @@
+// Copyright 2021 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_test
+
+import (
+	"testing"
+	"time"
+
+	"golang.org/x/exp/event"
+)
+
+func TestOfAs(t *testing.T) {
+	const i = 3
+	var v event.Value
+	v = event.Int64Of(i)
+	if got := v.Int64(); got != i {
+		t.Errorf("got %v, want %v", got, i)
+	}
+	v = event.Uint64Of(i)
+	if got := v.Uint64(); got != i {
+		t.Errorf("got %v, want %v", got, i)
+	}
+	v = event.Float64Of(i)
+	if got := v.Float64(); got != i {
+		t.Errorf("got %v, want %v", got, i)
+	}
+	v = event.BoolOf(true)
+	if got := v.Bool(); got != true {
+		t.Errorf("got %v, want %v", got, true)
+	}
+	const s = "foo"
+	v = event.StringOf(s)
+	if got := v.String(); got != s {
+		t.Errorf("got %v, want %v", got, s)
+	}
+	tm := time.Now()
+	v = event.ValueOf(tm)
+	if got := v.Interface(); got != tm {
+		t.Errorf("got %v, want %v", got, tm)
+	}
+	var vnil event.Value
+	if got := vnil.Interface(); got != nil {
+		t.Errorf("got %v, want nil", got)
+	}
+}
+
+func panics(f func()) (b bool) {
+	defer func() {
+		if x := recover(); x != nil {
+			b = true
+		}
+	}()
+	f()
+	return false
+}
+
+func TestPanics(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		f    func()
+	}{
+		{"int64", func() { event.Float64Of(3).Int64() }},
+		{"uint64", func() { event.Int64Of(3).Uint64() }},
+		{"float64", func() { event.Uint64Of(3).Float64() }},
+		{"bool", func() { event.Int64Of(3).Bool() }},
+	} {
+		if !panics(test.f) {
+			t.Errorf("%s: got no panic, want panic", test.name)
+		}
+	}
+}
+
+func TestString(t *testing.T) {
+	for _, test := range []struct {
+		v    event.Value
+		want string
+	}{
+		{event.Int64Of(-3), "-3"},
+		{event.Uint64Of(3), "3"},
+		{event.Float64Of(.15), "0.15"},
+		{event.BoolOf(true), "true"},
+		{event.StringOf("foo"), "foo"},
+		{event.ValueOf(time.Duration(3 * time.Second)), "3s"},
+	} {
+		if got := test.v.String(); got != test.want {
+			t.Errorf("%#v: got %q, want %q", test.v, got, test.want)
+		}
+	}
+}
+
+func TestNoAlloc(t *testing.T) {
+	// Assign values just to make sure the compiler doesn't optimize away the statements.
+	var (
+		i int64
+		u uint64
+		f float64
+		b bool
+		s string
+		x interface{}
+		p = &i
+	)
+	a := int(testing.AllocsPerRun(5, func() {
+		i = event.Int64Of(1).Int64()
+		u = event.Uint64Of(1).Uint64()
+		f = event.Float64Of(1).Float64()
+		b = event.BoolOf(true).Bool()
+		s = event.StringOf("foo").String()
+		x = event.ValueOf(p).Interface()
+	}))
+	if a != 0 {
+		t.Errorf("got %d allocs, want zero", a)
+	}
+	_ = u
+	_ = f
+	_ = b
+	_ = s
+	_ = x
+}
diff --git a/event/printer.go b/event/printer.go
deleted file mode 100644
index f3e5388..0000000
--- a/event/printer.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// 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"
-)
-
-// Printer is the interface to something capable of printing standard types
-// used in events.
-// It is used to enable zero allocation printing support.
-// Implementations can be directly used as an Exporter.
-type Printer interface {
-	Handler
-	Label(l Label)
-	String(v string)
-	Quote(v string)
-	Int(v int64)
-	Uint(v uint64)
-	Float(v float64)
-	Value(v interface{})
-}
-
-// NewPrinter returns a Printer that prints to the supplied writer.
-func NewPrinter(w io.Writer) Printer {
-	return &printer{writer: w}
-}
-
-type printer struct {
-	buf    [128]byte
-	writer io.Writer
-}
-
-func (p *printer) Handle(ev *Event) {
-	const timeFormat = "2006/01/02 15:04:05"
-	if !ev.At.IsZero() {
-		p.writer.Write(ev.At.AppendFormat(p.buf[:0], timeFormat))
-	}
-	io.WriteString(p.writer, " [")
-	io.WriteString(p.writer, ev.Kind.String())
-	io.WriteString(p.writer, ":")
-	p.writer.Write(strconv.AppendUint(p.buf[:0], ev.ID, 10))
-	if ev.Parent != 0 {
-		io.WriteString(p.writer, ":")
-		p.writer.Write(strconv.AppendUint(p.buf[:0], ev.Parent, 10))
-	}
-	io.WriteString(p.writer, "]")
-	if ev.Message != "" {
-		io.WriteString(p.writer, " ")
-		io.WriteString(p.writer, ev.Message)
-	}
-	for _, l := range ev.Labels {
-		if !l.Valid() {
-			continue
-		}
-		io.WriteString(p.writer, "\n\t")
-		p.Label(l)
-	}
-	io.WriteString(p.writer, "\n")
-}
-
-func (p *printer) Label(l Label) {
-	io.WriteString(p.writer, l.key)
-	if l.dispatch != nil {
-		io.WriteString(p.writer, "=")
-		l.dispatch(p, l)
-	}
-}
-
-func (p *printer) String(v string) {
-	io.WriteString(p.writer, v)
-}
-
-func (p *printer) Quote(v string) {
-	p.writer.Write(strconv.AppendQuote(p.buf[:0], v))
-}
-
-func (p *printer) Int(v int64) {
-	p.writer.Write(strconv.AppendInt(p.buf[:0], v, 10))
-}
-
-func (p *printer) Uint(v uint64) {
-	p.writer.Write(strconv.AppendUint(p.buf[:0], v, 10))
-}
-
-func (p *printer) Float(v float64) {
-	p.writer.Write(strconv.AppendFloat(p.buf[:0], v, 'E', -1, 32))
-}
-
-func (p *printer) Value(v interface{}) {
-	fmt.Fprint(p.writer, v)
-}
diff --git a/event/severity/severity.go b/event/severity/severity.go
index 06dfc96..66892b5 100644
--- a/event/severity/severity.go
+++ b/event/severity/severity.go
@@ -50,18 +50,14 @@
 	Fatal = Of(FatalLevel)
 )
 
-func dispatchSeverity(h event.ValueHandler, l event.Label) {
-	h.String(Level(l.Unpack64()).String())
-}
-
 // Of creates a new Label with this key and the supplied value.
-func Of(l Level) event.Label {
-	return event.Of64(Key, dispatchSeverity, uint64(l))
+func Of(v Level) event.Label {
+	return event.Label{Name: Key, Value: event.ValueOf(v)}
 }
 
 // From can be used to get a value from a Label.
 func From(t event.Label) Level {
-	return Level(t.Unpack64())
+	return t.Value.Interface().(Level)
 }
 
 func (l Level) Class() Level {
diff --git a/event/severity/severity_test.go b/event/severity/severity_test.go
index e90cac2..01c1b05 100644
--- a/event/severity/severity_test.go
+++ b/event/severity/severity_test.go
@@ -15,6 +15,7 @@
 )
 
 func TestPrint(t *testing.T) {
+	//TODO: print the textual form of severity
 	ctx := context.Background()
 	for _, test := range []struct {
 		name   string
@@ -23,35 +24,22 @@
 	}{{
 		name:   "debug",
 		events: func(ctx context.Context) { event.To(ctx).With(severity.Debug).Log("a message") },
-		expect: `
-2020/03/05 14:27:48 [log:1] a message
-	level=debug
-`}, {
+		expect: `2020/03/05 14:27:48	[1]	log	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 [log:1] a message
-	level=info
-`}} {
-		h := &captureHandler{}
-		h.printer = event.NewPrinter(&h.buf)
+		expect: `2020/03/05 14:27:48	[1]	log	a message	{"level":info}`},
+	} {
+		buf := &strings.Builder{}
+		h := event.Printer(buf)
 		e := event.NewExporter(h)
 		e.Now = eventtest.TestNow()
 		ctx := event.WithExporter(ctx, e)
 		test.events(ctx)
-		got := strings.TrimSpace(h.buf.String())
+		got := strings.TrimSpace(buf.String())
 		expect := strings.TrimSpace(test.expect)
 		if got != expect {
 			t.Errorf("%s failed\ngot   : %q\nexpect: %q", test.name, got, expect)
 		}
 	}
 }
-
-type captureHandler struct {
-	printer event.Printer
-	buf     strings.Builder
-}
-
-func (e *captureHandler) Handle(ev *event.Event) {
-	e.printer.Handle(ev)
-}