diff --git a/.circleci/config.yml b/.circleci/config.yml
index 317720f..44b5c4d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -3,14 +3,14 @@ jobs:
   build:
     docker:
       # specify the version
-      - image: circleci/golang:1.10.2
-      
+      - image: circleci/golang:1.12.3
+
     working_directory: /go/src/github.com/influxdata/line-protocol
     steps:
       - checkout
 
       - run: go get -v -t -d ./...
-      - run: go get honnef.co/go/tools/cmd/megacheck
-      - run: go vet -v ./...
+      - run: go get honnef.co/go/tools/...
+      - run: go vet -v -unreachable=false ./...
       - run: go test -v ./...
-      - run: megacheck ./...
+      - run: staticcheck ./...
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..75c1787
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+go.sum linguist-generated=true
+
diff --git a/README.md b/README.md
index c752ec6..89fe2f9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,22 @@
-# line-protocol
\ No newline at end of file
+# line-protocol
+
+This is an encoder for the influx [line protocol.](https://docs.influxdata.com/influxdb/latest/reference/syntax/line-protocol/)
+
+It has an interface similar to the standard library's `json.Encoder`.
+
+
+### some caveats.
+- It is not concurrency-safe.  If you want to make multiple calls to `Encoder.Encode` concurrently you have to manage the concurrency yourself.
+- It can only encode values that are uint64, int64, int, float32, float64, string, or bool. 
+- Ints are converted to int64, float32's to float64.
+- If UintSupport is not set, uint64s are converted to int64's and if they are larger than the max int64, they get truncated to the max int64 instead of overflowing.
+
+
+### Example:
+```go
+buf := &bytes.Buffer{}
+serializer := protocol.NewEncoder(buf)
+serializer.SetMaxLineBytes(1024)
+serializer.SetFieldTypeSupport(UintSupport)
+serializer.Encode(e) // where e is something that implements the protocol.Metric interface
+```
diff --git a/debian/changelog b/debian/changelog
index 01f0d97..c0ce0ba 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-influxdata-line-protocol (0.0~git20210311.9aa0e37-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 02 Jun 2021 15:14:47 -0000
+
 golang-github-influxdata-line-protocol (0.0~git20181118.934b9e6-1) unstable; urgency=medium
 
   * Initial release (Closes: #914205)
diff --git a/encoder.go b/encoder.go
index 7196be9..fa6a474 100644
--- a/encoder.go
+++ b/encoder.go
@@ -6,8 +6,15 @@ import (
 	"math"
 	"sort"
 	"strconv"
+	"time"
 )
 
+// ErrIsNaN is a field error for when a float field is NaN.
+var ErrIsNaN = &FieldError{"is NaN"}
+
+// ErrIsInf is a field error for when a float field is Inf.
+var ErrIsInf = &FieldError{"is Inf"}
+
 // Encoder marshals Metrics into influxdb line protocol.
 // It is not safe for concurrent use, make a new one!
 // The default behavior when encountering a field error is to ignore the field and move on.
@@ -22,6 +29,7 @@ type Encoder struct {
 	header           []byte
 	footer           []byte
 	pair             []byte
+	precision        time.Duration
 }
 
 // SetMaxLineBytes sets a maximum length for a line, Encode will error if the generated line is longer
@@ -48,6 +56,12 @@ func (e *Encoder) FailOnFieldErr(s bool) {
 	e.failOnFieldError = s
 }
 
+// SetPrecision sets time precision for writes
+// Default is nanoseconds precision
+func (e *Encoder) SetPrecision(p time.Duration) {
+	e.precision = p
+}
+
 // NewEncoder gives us an encoder that marshals to a writer in influxdb line protocol
 // as defined by:
 // https://docs.influxdata.com/influxdb/v1.5/write_protocols/line_protocol_reference/
@@ -58,6 +72,7 @@ func NewEncoder(w io.Writer) *Encoder {
 		footer:    make([]byte, 0, 128),
 		pair:      make([]byte, 0, 128),
 		fieldList: make([]*Field, 0, 16),
+		precision: time.Nanosecond,
 	}
 }
 
@@ -72,9 +87,9 @@ func (e *Encoder) Encode(m Metric) (int, error) {
 		return 0, err
 	}
 
-	e.buildFooter(m)
+	e.buildFooter(m.Time())
 
-	// here we make a copy of the fields so we can do an in-place sort
+	// here we make a copy of the *fields so we can do an in-place sort
 	e.fieldList = append(e.fieldList[:0], m.FieldList()...)
 
 	if e.fieldSortOrder == SortFields {
@@ -112,6 +127,7 @@ func (e *Encoder) Encode(m Metric) (int, error) {
 			if err != nil {
 				return 0, err
 			}
+			pairsLen = 0
 			totalWritten += i
 
 			bytesNeeded = len(e.header) + len(e.pair) + len(e.footer)
@@ -153,7 +169,11 @@ func (e *Encoder) Encode(m Metric) (int, error) {
 
 		}
 
-		e.w.Write(e.pair)
+		i, err = e.w.Write(e.pair)
+		if err != nil {
+			return 0, err
+		}
+		totalWritten += i
 
 		pairsLen += len(e.pair)
 		firstField = false
@@ -199,16 +219,7 @@ func (e *Encoder) buildHeader(m Metric) error {
 	return nil
 }
 
-func (e *Encoder) buildFieldPair(key string, value interface{}) error {
-	e.pair = e.pair[:0]
-	key = escape(key)
-	// Some keys are not encodeable as line protocol, such as those with a
-	// trailing '\' or empty strings.
-	if key == "" {
-		return &FieldError{"invalid field key"}
-	}
-	e.pair = append(e.pair, key...)
-	e.pair = append(e.pair, '=')
+func (e *Encoder) buildFieldVal(value interface{}) error {
 	switch v := value.(type) {
 	case uint64:
 		if e.fieldTypeSupport&UintSupport != 0 {
@@ -224,22 +235,22 @@ func (e *Encoder) buildFieldPair(key string, value interface{}) error {
 		e.pair = append(strconv.AppendInt(e.pair, int64(v), 10), 'i')
 	case float64:
 		if math.IsNaN(v) {
-			return &FieldError{"is NaN"}
+			return ErrIsNaN
 		}
 
 		if math.IsInf(v, 0) {
-			return &FieldError{"is Inf"}
+			return ErrIsInf
 		}
 
 		e.pair = strconv.AppendFloat(e.pair, v, 'f', -1, 64)
 	case float32:
 		v32 := float64(v)
 		if math.IsNaN(v32) {
-			return &FieldError{"is NaN"}
+			return ErrIsNaN
 		}
 
 		if math.IsInf(v32, 0) {
-			return &FieldError{"is Inf"}
+			return ErrIsInf
 		}
 
 		e.pair = strconv.AppendFloat(e.pair, v32, 'f', -1, 64)
@@ -248,6 +259,10 @@ func (e *Encoder) buildFieldPair(key string, value interface{}) error {
 		e.pair = append(e.pair, '"')
 		e.pair = append(e.pair, stringFieldEscape(v)...)
 		e.pair = append(e.pair, '"')
+	case []byte:
+		e.pair = append(e.pair, '"')
+		stringFieldEscapeBytes(&e.pair, v)
+		e.pair = append(e.pair, '"')
 	case bool:
 		e.pair = strconv.AppendBool(e.pair, v)
 	default:
@@ -256,9 +271,33 @@ func (e *Encoder) buildFieldPair(key string, value interface{}) error {
 	return nil
 }
 
-func (e *Encoder) buildFooter(m Metric) {
+func (e *Encoder) buildFieldPair(key string, value interface{}) error {
+	e.pair = e.pair[:0]
+	key = escape(key)
+	// Some keys are not encodeable as line protocol, such as those with a
+	// trailing '\' or empty strings.
+	if key == "" || key[:len(key)-1] == "\\" {
+		return &FieldError{"invalid field key"}
+	}
+	e.pair = append(e.pair, key...)
+	e.pair = append(e.pair, '=')
+	return e.buildFieldVal(value)
+}
+
+func (e *Encoder) buildFooter(t time.Time) {
 	e.footer = e.footer[:0]
-	e.footer = append(e.footer, ' ')
-	e.footer = strconv.AppendInt(e.footer, m.Time().UnixNano(), 10)
+	if !t.IsZero() {
+		e.footer = append(e.footer, ' ')
+		switch e.precision {
+		case time.Microsecond:
+			e.footer = strconv.AppendInt(e.footer, t.UnixNano()/1000, 10)
+		case time.Millisecond:
+			e.footer = strconv.AppendInt(e.footer, t.UnixNano()/1000000, 10)
+		case time.Second:
+			e.footer = strconv.AppendInt(e.footer, t.Unix(), 10)
+		default:
+			e.footer = strconv.AppendInt(e.footer, t.UnixNano(), 10)
+		}
+	}
 	e.footer = append(e.footer, '\n')
 }
diff --git a/encoder_test.go b/encoder_test.go
index 28952f3..bf112e1 100644
--- a/encoder_test.go
+++ b/encoder_test.go
@@ -6,8 +6,6 @@ import (
 	"sort"
 	"testing"
 	"time"
-
-	"github.com/stretchr/testify/require"
 )
 
 type MockMetric struct {
@@ -50,43 +48,9 @@ func (m *MockMetric) AddField(k string, v interface{}) {
 		}
 	}
 	m.fields = append(m.fields, &Field{Key: k, Value: convertField(v)})
-}
-
-func convertField(v interface{}) interface{} {
-	switch v := v.(type) {
-	case float64:
-		return v
-	case int64:
-		return v
-	case string:
-		return v
-	case bool:
-		return v
-	case int:
-		return int64(v)
-	case uint:
-		return uint64(v)
-	case uint64:
-		return uint64(v)
-	case []byte:
-		return string(v)
-	case int32:
-		return int64(v)
-	case int16:
-		return int64(v)
-	case int8:
-		return int64(v)
-	case uint32:
-		return uint64(v)
-	case uint16:
-		return uint64(v)
-	case uint8:
-		return uint64(v)
-	case float32:
-		return float64(v)
-	default:
-		return nil
-	}
+	sort.Slice(m.fields, func(i, j int) bool {
+		return string(m.fields[i].Key) < string(m.fields[j].Key)
+	})
 }
 
 func NewMockMetric(name string,
@@ -94,12 +58,6 @@ func NewMockMetric(name string,
 	fields map[string]interface{},
 	tm time.Time,
 ) Metric {
-	// var vtype telegraf.ValueType
-	// if len(tp) > 0 {
-	// 	vtype = tp[0]
-	// } else {
-	// 	vtype = telegraf.Untyped
-	// }
 	m := &MockMetric{
 		name:   name,
 		tags:   nil,
@@ -136,6 +94,7 @@ var tests = []struct {
 	output         []byte
 	failOnFieldErr bool
 	err            error
+	precision      time.Duration
 }{
 	{
 		name: "minimal",
@@ -353,6 +312,22 @@ var tests = []struct {
 		),
 		output: []byte("cpu abc=123i 1519194109000000042\ncpu def=456i 1519194109000000042\n"),
 	},
+	{
+		name:     "split_fields_overflow",
+		maxBytes: 43,
+		input: NewMockMetric(
+			"cpu",
+			map[string]string{},
+			map[string]interface{}{
+				"abc": 123,
+				"def": 456,
+				"ghi": 789,
+				"jkl": 123,
+			},
+			time.Unix(1519194109, 42),
+		),
+		output: []byte("cpu abc=123i,def=456i 1519194109000000042\ncpu ghi=789i,jkl=123i 1519194109000000042\n"),
+	},
 	{
 		name: "name newline",
 		input: NewMockMetric(
@@ -495,7 +470,63 @@ var tests = []struct {
 			time.Unix(0, 0),
 		),
 		failOnFieldErr: true,
-		err:            &FieldError{s: "is NaN"},
+		err:            ErrIsNaN,
+	},
+	{
+		name: "explicit nanoseconds precision",
+		input: NewMockMetric(
+			"cpu",
+			map[string]string{},
+			map[string]interface{}{
+				"x": 3,
+				"y": 42.3,
+			},
+			time.Unix(123456789, 123456789),
+		),
+		precision: time.Nanosecond,
+		output:    []byte("cpu x=3i,y=42.3 123456789123456789\n"),
+	},
+	{
+		name: "microseconds precision",
+		input: NewMockMetric(
+			"cpu",
+			map[string]string{},
+			map[string]interface{}{
+				"x": 3,
+				"y": 42.3,
+			},
+			time.Unix(123456789, 123456789),
+		),
+		precision: time.Microsecond,
+		output:    []byte("cpu x=3i,y=42.3 123456789123456\n"),
+	},
+	{
+		name: "milliseconds precision",
+		input: NewMockMetric(
+			"cpu",
+			map[string]string{},
+			map[string]interface{}{
+				"x": 3,
+				"y": 42.3,
+			},
+			time.Unix(123456789, 123456789),
+		),
+		precision: time.Millisecond,
+		output:    []byte("cpu x=3i,y=42.3 123456789123\n"),
+	},
+	{
+		name: "seconds precision",
+		input: NewMockMetric(
+			"cpu",
+			map[string]string{},
+			map[string]interface{}{
+				"x": 3,
+				"y": 42.3,
+			},
+			time.Unix(123456789, 123456789),
+		),
+		precision: time.Second,
+		output:    []byte("cpu x=3i,y=42.3 123456789\n"),
 	},
 }
 
@@ -508,13 +539,95 @@ func TestEncoder(t *testing.T) {
 			serializer.SetFieldSortOrder(SortFields)
 			serializer.SetFieldTypeSupport(tt.typeSupport)
 			serializer.FailOnFieldErr(tt.failOnFieldErr)
-			_, err := serializer.Encode(tt.input)
-			require.Equal(t, tt.err, err)
-			require.Equal(t, string(tt.output), buf.String())
+			serializer.SetPrecision(tt.precision)
+			i, err := serializer.Encode(tt.input)
+			if tt.err != err {
+				t.Fatalf("expected error %v, but got %v", tt.err, err)
+			}
+			if i != len(buf.Bytes()) {
+				t.Fatalf("expected i: %v, but got: %v", len(buf.Bytes()), i)
+			}
+			if string(tt.output) != buf.String() {
+				t.Fatalf("expected output %v, but got %v", tt.output, buf.String())
+			}
+		})
+	}
+}
+
+func TestWriter(t *testing.T) {
+	type args struct {
+		name                        []byte
+		ts                          time.Time
+		tagKeys, tagVals, fieldKeys [][]byte
+		fieldVals                   []interface{}
+	}
+	btests := make([]struct {
+		name           string
+		maxBytes       int
+		typeSupport    FieldTypeSupport
+		failOnFieldErr bool
+		fields         args
+		err            error
+		output         []byte
+		precision      time.Duration
+	}, len(tests))
+	for i, tt := range tests {
+		btests[i].name = tt.name
+		btests[i].maxBytes = tt.maxBytes
+		btests[i].typeSupport = tt.typeSupport
+		btests[i].failOnFieldErr = tt.failOnFieldErr
+		btests[i].err = tt.err
+		btests[i].output = tt.output
+		btests[i].precision = tt.precision
+		btests[i].fields.name = []byte(tt.input.Name())
+		btests[i].fields.ts = tt.input.Time()
+		btests[i].fields.fieldKeys, btests[i].fields.fieldVals = fieldsToBytes(tt.input.FieldList())
+		btests[i].fields.tagKeys, btests[i].fields.tagVals = tagsToBytes(tt.input.TagList())
+	}
+	for _, tt := range btests {
+		t.Run(tt.name, func(t *testing.T) {
+			if t.Name() == "TestWriter/split_fields_overflow" {
+				t.Skip("https://github.com/influxdata/line-protocol/issues/9")
+			}
+			buf := &bytes.Buffer{}
+			serializer := NewEncoder(buf)
+			serializer.SetMaxLineBytes(tt.maxBytes)
+			serializer.SetFieldSortOrder(SortFields)
+			serializer.SetFieldTypeSupport(tt.typeSupport)
+			serializer.FailOnFieldErr(tt.failOnFieldErr)
+			serializer.SetPrecision(tt.precision)
+			_, err := serializer.Write(tt.fields.name, tt.fields.ts, tt.fields.tagKeys, tt.fields.tagVals, tt.fields.fieldKeys, tt.fields.fieldVals)
+			if tt.err != err {
+				t.Fatalf("expected error %v, but got %v", tt.err, err)
+			}
+			if string(tt.output) != buf.String() {
+				t.Fatalf("expected output %s, but got %s", tt.output, buf.String())
+			}
 		})
 	}
 }
 
+func fieldsToBytes(tg []*Field) ([][]byte, []interface{}) {
+	b := make([][]byte, len(tg))
+	v := make([]interface{}, len(tg))
+	for i := range tg {
+		b[i] = []byte(tg[i].Key)
+		v[i] = tg[i].Value
+	}
+	return b, v
+}
+
+func tagsToBytes(tg []*Tag) ([][]byte, [][]byte) {
+	b := make([][]byte, len(tg))
+	v := make([][]byte, len(tg))
+	for i := range tg {
+		b[i] = []byte(tg[i].Key)
+		v[i] = []byte(tg[i].Value)
+	}
+	return b, v
+
+}
+
 func BenchmarkSerializer(b *testing.B) {
 	for _, tt := range tests {
 		b.Run(tt.name, func(b *testing.B) {
@@ -530,3 +643,47 @@ func BenchmarkSerializer(b *testing.B) {
 		})
 	}
 }
+
+func BenchmarkWriter(b *testing.B) {
+	type fields struct {
+		name                        []byte
+		ts                          time.Time
+		tagKeys, tagVals, fieldKeys [][]byte
+		fieldVals                   []interface{}
+	}
+	benches := make([]struct {
+		name           string
+		maxBytes       int
+		typeSupport    FieldTypeSupport
+		failOnFieldErr bool
+		fields         fields
+	}, len(tests))
+	for i, tt := range tests {
+		benches[i].name = tt.name
+		benches[i].maxBytes = tt.maxBytes
+		benches[i].typeSupport = tt.typeSupport
+		benches[i].failOnFieldErr = tt.failOnFieldErr
+		benches[i].fields.name = []byte(tt.input.Name())
+		benches[i].fields.ts = tt.input.Time()
+		benches[i].fields.fieldKeys, benches[i].fields.fieldVals = fieldsToBytes(tt.input.FieldList())
+		benches[i].fields.tagKeys, benches[i].fields.tagVals = tagsToBytes(tt.input.TagList())
+	}
+	b.ResetTimer()
+
+	for _, tt := range benches {
+		b.Run(tt.name, func(b *testing.B) {
+			buf := &bytes.Buffer{}
+			serializer := NewEncoder(buf)
+			serializer.SetMaxLineBytes(tt.maxBytes)
+			serializer.SetFieldTypeSupport(tt.typeSupport)
+			var i int
+			var err error
+			for n := 0; n < b.N; n++ {
+				i, err = serializer.Write(tt.fields.name, tt.fields.ts, tt.fields.tagKeys, tt.fields.tagVals, tt.fields.fieldKeys, tt.fields.fieldVals)
+				_ = err
+				_ = i
+			}
+			_ = buf
+		})
+	}
+}
diff --git a/escape.go b/escape.go
index 9622fe1..781b352 100644
--- a/escape.go
+++ b/escape.go
@@ -1,7 +1,12 @@
 package protocol
 
 import (
+	"bytes"
+	"reflect"
+	"strconv"
 	"strings"
+	"unicode/utf8"
+	"unsafe"
 )
 
 const (
@@ -11,7 +16,7 @@ const (
 )
 
 var (
-	escaper = strings.NewReplacer(
+	stringEscaper = strings.NewReplacer(
 		"\t", `\t`,
 		"\n", `\n`,
 		"\f", `\f`,
@@ -40,13 +45,32 @@ var (
 	)
 )
 
+var (
+	unescaper = strings.NewReplacer(
+		`\,`, `,`,
+		`\"`, `"`, // ???
+		`\ `, ` `,
+		`\=`, `=`,
+	)
+
+	nameUnescaper = strings.NewReplacer(
+		`\,`, `,`,
+		`\ `, ` `,
+	)
+
+	stringFieldUnescaper = strings.NewReplacer(
+		`\"`, `"`,
+		`\\`, `\`,
+	)
+)
+
 // The various escape functions allocate, I'd like to fix that.
 // TODO: make escape not allocate
 
 // Escape a tagkey, tagvalue, or fieldkey
 func escape(s string) string {
 	if strings.ContainsAny(s, escapes) {
-		return escaper.Replace(s)
+		return stringEscaper.Replace(s)
 	}
 	return s
 }
@@ -66,3 +90,175 @@ func stringFieldEscape(s string) string {
 	}
 	return s
 }
+
+const (
+	utf8mask  = byte(0x3F)
+	utf8bytex = byte(0x80) // 1000 0000
+	utf8len2  = byte(0xC0) // 1100 0000
+	utf8len3  = byte(0xE0) // 1110 0000
+	utf8len4  = byte(0xF0) // 1111 0000
+)
+
+func escapeBytes(dest *[]byte, b []byte) {
+	if bytes.ContainsAny(b, escapes) {
+		var r rune
+		for i, j := 0, 0; i < len(b); i += j {
+			r, j = utf8.DecodeRune(b[i:])
+			switch {
+			case r == '\t':
+				*dest = append(*dest, `\t`...)
+			case r == '\n':
+				*dest = append(*dest, `\n`...)
+			case r == '\f':
+				*dest = append(*dest, `\f`...)
+			case r == '\r':
+				*dest = append(*dest, `\r`...)
+			case r == ',':
+				*dest = append(*dest, `\,`...)
+			case r == ' ':
+				*dest = append(*dest, `\ `...)
+			case r == '=':
+				*dest = append(*dest, `\=`...)
+			case r <= 1<<7-1:
+				*dest = append(*dest, byte(r))
+			case r <= 1<<11-1:
+				*dest = append(*dest, utf8len2|byte(r>>6), utf8bytex|byte(r)&utf8mask)
+			case r <= 1<<16-1:
+				*dest = append(*dest, utf8len3|byte(r>>12), utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
+			default:
+				*dest = append(*dest, utf8len4|byte(r>>18), utf8bytex|byte(r>>12)&utf8mask, utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
+			}
+		}
+		return
+	}
+	*dest = append(*dest, b...)
+}
+
+// Escape a measurement name
+func nameEscapeBytes(dest *[]byte, b []byte) {
+	if bytes.ContainsAny(b, nameEscapes) {
+		var r rune
+		for i, j := 0, 0; i < len(b); i += j {
+			r, j = utf8.DecodeRune(b[i:])
+			switch {
+			case r == '\t':
+				*dest = append(*dest, `\t`...)
+			case r == '\n':
+				*dest = append(*dest, `\n`...)
+			case r == '\f':
+				*dest = append(*dest, `\f`...)
+			case r == '\r':
+				*dest = append(*dest, `\r`...)
+			case r == ',':
+				*dest = append(*dest, `\,`...)
+			case r == ' ':
+				*dest = append(*dest, `\ `...)
+			case r == '\\':
+				*dest = append(*dest, `\\`...)
+			case r <= 1<<7-1:
+				*dest = append(*dest, byte(r))
+			case r <= 1<<11-1:
+				*dest = append(*dest, utf8len2|byte(r>>6), utf8bytex|byte(r)&utf8mask)
+			case r <= 1<<16-1:
+				*dest = append(*dest, utf8len3|byte(r>>12), utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
+			default:
+				*dest = append(*dest, utf8len4|byte(r>>18), utf8bytex|byte(r>>12)&utf8mask, utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
+			}
+		}
+		return
+	}
+	*dest = append(*dest, b...)
+}
+
+func stringFieldEscapeBytes(dest *[]byte, b []byte) {
+	if bytes.ContainsAny(b, stringFieldEscapes) {
+		var r rune
+		for i, j := 0, 0; i < len(b); i += j {
+			r, j = utf8.DecodeRune(b[i:])
+			switch {
+			case r == '\t':
+				*dest = append(*dest, `\t`...)
+			case r == '\n':
+				*dest = append(*dest, `\n`...)
+			case r == '\f':
+				*dest = append(*dest, `\f`...)
+			case r == '\r':
+				*dest = append(*dest, `\r`...)
+			case r == ',':
+				*dest = append(*dest, `\,`...)
+			case r == ' ':
+				*dest = append(*dest, `\ `...)
+			case r == '\\':
+				*dest = append(*dest, `\\`...)
+			case r <= 1<<7-1:
+				*dest = append(*dest, byte(r))
+			case r <= 1<<11-1:
+				*dest = append(*dest, utf8len2|byte(r>>6), utf8bytex|byte(r)&utf8mask)
+			case r <= 1<<16-1:
+				*dest = append(*dest, utf8len3|byte(r>>12), utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
+			default:
+				*dest = append(*dest, utf8len4|byte(r>>18), utf8bytex|byte(r>>12)&utf8mask, utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
+			}
+		}
+		return
+	}
+	*dest = append(*dest, b...)
+}
+
+func unescape(b []byte) string {
+	if bytes.ContainsAny(b, escapes) {
+		return unescaper.Replace(unsafeBytesToString(b))
+	}
+	return string(b)
+}
+
+func nameUnescape(b []byte) string {
+	if bytes.ContainsAny(b, nameEscapes) {
+		return nameUnescaper.Replace(unsafeBytesToString(b))
+	}
+	return string(b)
+}
+
+// unsafeBytesToString converts a []byte to a string without a heap allocation.
+//
+// It is unsafe, and is intended to prepare input to short-lived functions
+// that require strings.
+func unsafeBytesToString(in []byte) string {
+	src := *(*reflect.SliceHeader)(unsafe.Pointer(&in))
+	dst := reflect.StringHeader{
+		Data: src.Data,
+		Len:  src.Len,
+	}
+	s := *(*string)(unsafe.Pointer(&dst))
+	return s
+}
+
+// parseIntBytes is a zero-alloc wrapper around strconv.ParseInt.
+func parseIntBytes(b []byte, base int, bitSize int) (i int64, err error) {
+	s := unsafeBytesToString(b)
+	return strconv.ParseInt(s, base, bitSize)
+}
+
+// parseUintBytes is a zero-alloc wrapper around strconv.ParseUint.
+func parseUintBytes(b []byte, base int, bitSize int) (i uint64, err error) {
+	s := unsafeBytesToString(b)
+	return strconv.ParseUint(s, base, bitSize)
+}
+
+// parseFloatBytes is a zero-alloc wrapper around strconv.ParseFloat.
+func parseFloatBytes(b []byte, bitSize int) (float64, error) {
+	s := unsafeBytesToString(b)
+	return strconv.ParseFloat(s, bitSize)
+}
+
+// parseBoolBytes is a zero-alloc wrapper around strconv.ParseBool.
+func parseBoolBytes(b []byte) (bool, error) {
+	return strconv.ParseBool(unsafeBytesToString(b))
+}
+
+func stringFieldUnescape(b []byte) string {
+	if bytes.ContainsAny(b, stringFieldEscapes) {
+		return stringFieldUnescaper.Replace(unsafeBytesToString(b))
+	}
+	return string(b)
+}
diff --git a/escaper_test.go b/escaper_test.go
new file mode 100644
index 0000000..f098f30
--- /dev/null
+++ b/escaper_test.go
@@ -0,0 +1,66 @@
+package protocol
+
+import (
+	"testing"
+)
+
+func BenchmarkStringEscFunc4(b *testing.B) {
+	s := "h\tello ⛵ \t \t"
+	for i := 0; i < b.N; i++ {
+		b := escape(s)
+		_ = b
+	}
+
+}
+
+func BenchmarkEscFunc4(b *testing.B) {
+	s := []byte("h\tello ⛵ \t \t")
+	dest := make([]byte, 32)
+	for i := 0; i < b.N; i++ {
+		escapeBytes(&dest, s)
+		dest = dest[:0]
+	}
+}
+
+func TestBytesEscape(t *testing.T) {
+	cases := []struct {
+		// we use strings in test because its easier to read and write them for humans
+		name string
+		arg  string
+		want string
+	}{
+		{
+			name: "sailboat",
+			arg:  `⛵`,
+			want: `⛵`,
+		},
+		{
+			name: "sentence",
+			arg:  `hello I like to ⛵but do not like ☠`,
+			want: `hello\ I\ like\ to\ ⛵but\ do\ not\ like\ ☠`,
+		},
+		{
+			name: "escapes",
+			arg:  "\t\n\f\r ,=",
+			want: `\t\n\f\r\ \,\=`,
+		},
+		{
+			name: "nameEscapes",
+			arg:  "\t\n\f\r ,",
+			want: `\t\n\f\r\ \,`,
+		},
+		{
+			name: "stringFieldEscapes",
+			arg:  "\t\n\f\r\\\"",
+			want: `\t\n\f\r\"`,
+		},
+	}
+	got := []byte{}
+	for _, x := range cases {
+		escapeBytes(&got, []byte(x.arg))
+		if string(got) != string(x.want) {
+			t.Fatalf("did not escape %s properly, expected %s got %s", x.name, x.want, got)
+		}
+		got = got[:0]
+	}
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..ec39894
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/influxdata/line-protocol
+
+go 1.13
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e69de29
diff --git a/handler.go b/handler.go
new file mode 100644
index 0000000..f28a8d0
--- /dev/null
+++ b/handler.go
@@ -0,0 +1,128 @@
+package protocol
+
+import (
+	"bytes"
+	"errors"
+	"strconv"
+	"time"
+)
+
+// MetricHandler implements the Handler interface and produces Metric.
+type MetricHandler struct {
+	timePrecision time.Duration
+	timeFunc      TimeFunc
+	metric        MutableMetric
+}
+
+func NewMetricHandler() *MetricHandler {
+	return &MetricHandler{
+		timePrecision: time.Nanosecond,
+		timeFunc:      time.Now,
+	}
+}
+
+func (h *MetricHandler) SetTimePrecision(p time.Duration) {
+	h.timePrecision = p
+	// When the timestamp is omitted from the metric, the timestamp
+	// comes from the server clock, truncated to the nearest unit of
+	// measurement provided in precision.
+	//
+	// When a timestamp is provided in the metric, precsision is
+	// overloaded to hold the unit of measurement of the timestamp.
+}
+
+func (h *MetricHandler) SetTimeFunc(f TimeFunc) {
+	h.timeFunc = f
+}
+
+func (h *MetricHandler) Metric() (Metric, error) {
+	if h.metric.Time().IsZero() {
+		h.metric.SetTime(h.timeFunc().Truncate(h.timePrecision))
+	}
+	return h.metric, nil
+}
+
+func (h *MetricHandler) SetMeasurement(name []byte) error {
+	var err error
+	h.metric, err = New(nameUnescape(name),
+		nil, nil, time.Time{})
+	return err
+}
+
+func (h *MetricHandler) AddTag(key []byte, value []byte) error {
+	tk := unescape(key)
+	tv := unescape(value)
+	h.metric.AddTag(tk, tv)
+	return nil
+}
+
+func (h *MetricHandler) AddInt(key []byte, value []byte) error {
+	fk := unescape(key)
+	fv, err := parseIntBytes(bytes.TrimSuffix(value, []byte("i")), 10, 64)
+	if err != nil {
+		if numerr, ok := err.(*strconv.NumError); ok {
+			return numerr.Err
+		}
+		return err
+	}
+	h.metric.AddField(fk, fv)
+	return nil
+}
+
+func (h *MetricHandler) AddUint(key []byte, value []byte) error {
+	fk := unescape(key)
+	fv, err := parseUintBytes(bytes.TrimSuffix(value, []byte("u")), 10, 64)
+	if err != nil {
+		if numerr, ok := err.(*strconv.NumError); ok {
+			return numerr.Err
+		}
+		return err
+	}
+	h.metric.AddField(fk, fv)
+	return nil
+}
+
+func (h *MetricHandler) AddFloat(key []byte, value []byte) error {
+	fk := unescape(key)
+	fv, err := parseFloatBytes(value, 64)
+	if err != nil {
+		if numerr, ok := err.(*strconv.NumError); ok {
+			return numerr.Err
+		}
+		return err
+	}
+	h.metric.AddField(fk, fv)
+	return nil
+}
+
+func (h *MetricHandler) AddString(key []byte, value []byte) error {
+	fk := unescape(key)
+	fv := stringFieldUnescape(value)
+	h.metric.AddField(fk, fv)
+	return nil
+}
+
+func (h *MetricHandler) AddBool(key []byte, value []byte) error {
+	fk := unescape(key)
+	fv, err := parseBoolBytes(value)
+	if err != nil {
+		return errors.New("unparseable bool")
+	}
+	h.metric.AddField(fk, fv)
+	return nil
+}
+
+func (h *MetricHandler) SetTimestamp(tm []byte) error {
+	v, err := parseIntBytes(tm, 10, 64)
+	if err != nil {
+		if numerr, ok := err.(*strconv.NumError); ok {
+			return numerr.Err
+		}
+		return err
+	}
+
+	//time precision is overloaded to mean time unit here
+	ns := v * int64(h.timePrecision)
+	h.metric.SetTime(time.Unix(0, ns))
+	return nil
+}
diff --git a/machine.go b/machine.go
new file mode 100644
index 0000000..148e6dc
--- /dev/null
+++ b/machine.go
@@ -0,0 +1,3828 @@
+//line machine.go.rl:1
+package protocol
+
+import (
+	"errors"
+	"io"
+)
+
+var (
+	ErrNameParse      = errors.New("expected measurement name")
+	ErrFieldParse     = errors.New("expected field")
+	ErrTagParse       = errors.New("expected tag")
+	ErrTimestampParse = errors.New("expected timestamp")
+	ErrParse          = errors.New("parse error")
+	EOF               = errors.New("EOF")
+)
+
+//line machine.go.rl:310
+
+//line machine.go:25
+const LineProtocol_start int = 47
+const LineProtocol_first_final int = 47
+const LineProtocol_error int = 0
+
+const LineProtocol_en_main int = 47
+const LineProtocol_en_discard_line int = 35
+const LineProtocol_en_align int = 86
+const LineProtocol_en_series int = 38
+
+//line machine.go.rl:313
+
+type Handler interface {
+	SetMeasurement(name []byte) error
+	AddTag(key []byte, value []byte) error
+	AddInt(key []byte, value []byte) error
+	AddUint(key []byte, value []byte) error
+	AddFloat(key []byte, value []byte) error
+	AddString(key []byte, value []byte) error
+	AddBool(key []byte, value []byte) error
+	SetTimestamp(tm []byte) error
+}
+
+type machine struct {
+	data         []byte
+	cs           int
+	p, pe, eof   int
+	pb           int
+	lineno       int
+	sol          int
+	handler      Handler
+	initState    int
+	key          []byte
+	beginMetric  bool
+	finishMetric bool
+}
+
+func NewMachine(handler Handler) *machine {
+	m := &machine{
+		handler:   handler,
+		initState: LineProtocol_en_align,
+	}
+
+//line machine.go.rl:346
+
+//line machine.go.rl:347
+
+//line machine.go.rl:348
+
+//line machine.go.rl:349
+
+//line machine.go.rl:350
+
+//line machine.go.rl:351
+
+//line machine.go:82
+	{
+		(m.cs) = LineProtocol_start
+	}
+
+//line machine.go.rl:352
+
+	return m
+}
+
+func NewSeriesMachine(handler Handler) *machine {
+	m := &machine{
+		handler:   handler,
+		initState: LineProtocol_en_series,
+	}
+
+//line machine.go.rl:363
+
+//line machine.go.rl:364
+
+//line machine.go.rl:365
+
+//line machine.go.rl:366
+
+//line machine.go.rl:367
+
+//line machine.go:109
+	{
+		(m.cs) = LineProtocol_start
+	}
+
+//line machine.go.rl:368
+
+	return m
+}
+
+func (m *machine) SetData(data []byte) {
+	m.data = data
+	m.p = 0
+	m.pb = 0
+	m.lineno = 1
+	m.sol = 0
+	m.pe = len(data)
+	m.eof = len(data)
+	m.key = nil
+	m.beginMetric = false
+	m.finishMetric = false
+
+//line machine.go:132
+	{
+		(m.cs) = LineProtocol_start
+	}
+
+//line machine.go.rl:385
+	m.cs = m.initState
+}
+
+// Next parses the next metric line and returns nil if it was successfully
+// processed.  If the line contains a syntax error an error is returned,
+// otherwise if the end of file is reached before finding a metric line then
+// EOF is returned.
+func (m *machine) Next() error {
+	if m.p == m.pe && m.pe == m.eof {
+		return EOF
+	}
+
+	m.key = nil
+	m.beginMetric = false
+	m.finishMetric = false
+
+	return m.exec()
+}
+
+func (m *machine) exec() error {
+	var err error
+
+//line machine.go:160
+	{
+		if (m.p) == (m.pe) {
+			goto _test_eof
+		}
+		goto _resume
+
+	_again:
+		switch m.cs {
+		case 47:
+			goto st47
+		case 1:
+			goto st1
+		case 2:
+			goto st2
+		case 3:
+			goto st3
+		case 0:
+			goto st0
+		case 4:
+			goto st4
+		case 5:
+			goto st5
+		case 6:
+			goto st6
+		case 7:
+			goto st7
+		case 48:
+			goto st48
+		case 49:
+			goto st49
+		case 50:
+			goto st50
+		case 8:
+			goto st8
+		case 9:
+			goto st9
+		case 10:
+			goto st10
+		case 11:
+			goto st11
+		case 51:
+			goto st51
+		case 52:
+			goto st52
+		case 53:
+			goto st53
+		case 54:
+			goto st54
+		case 55:
+			goto st55
+		case 56:
+			goto st56
+		case 57:
+			goto st57
+		case 58:
+			goto st58
+		case 59:
+			goto st59
+		case 60:
+			goto st60
+		case 61:
+			goto st61
+		case 62:
+			goto st62
+		case 63:
+			goto st63
+		case 64:
+			goto st64
+		case 65:
+			goto st65
+		case 66:
+			goto st66
+		case 67:
+			goto st67
+		case 68:
+			goto st68
+		case 69:
+			goto st69
+		case 70:
+			goto st70
+		case 12:
+			goto st12
+		case 13:
+			goto st13
+		case 14:
+			goto st14
+		case 15:
+			goto st15
+		case 16:
+			goto st16
+		case 71:
+			goto st71
+		case 17:
+			goto st17
+		case 18:
+			goto st18
+		case 72:
+			goto st72
+		case 73:
+			goto st73
+		case 74:
+			goto st74
+		case 75:
+			goto st75
+		case 76:
+			goto st76
+		case 77:
+			goto st77
+		case 78:
+			goto st78
+		case 79:
+			goto st79
+		case 80:
+			goto st80
+		case 19:
+			goto st19
+		case 20:
+			goto st20
+		case 21:
+			goto st21
+		case 81:
+			goto st81
+		case 22:
+			goto st22
+		case 23:
+			goto st23
+		case 24:
+			goto st24
+		case 82:
+			goto st82
+		case 25:
+			goto st25
+		case 26:
+			goto st26
+		case 83:
+			goto st83
+		case 84:
+			goto st84
+		case 27:
+			goto st27
+		case 28:
+			goto st28
+		case 29:
+			goto st29
+		case 30:
+			goto st30
+		case 31:
+			goto st31
+		case 32:
+			goto st32
+		case 33:
+			goto st33
+		case 34:
+			goto st34
+		case 35:
+			goto st35
+		case 85:
+			goto st85
+		case 38:
+			goto st38
+		case 87:
+			goto st87
+		case 88:
+			goto st88
+		case 39:
+			goto st39
+		case 40:
+			goto st40
+		case 41:
+			goto st41
+		case 42:
+			goto st42
+		case 89:
+			goto st89
+		case 43:
+			goto st43
+		case 90:
+			goto st90
+		case 44:
+			goto st44
+		case 45:
+			goto st45
+		case 46:
+			goto st46
+		case 86:
+			goto st86
+		case 36:
+			goto st36
+		case 37:
+			goto st37
+		}
+
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof
+		}
+	_resume:
+		switch m.cs {
+		case 47:
+			goto st_case_47
+		case 1:
+			goto st_case_1
+		case 2:
+			goto st_case_2
+		case 3:
+			goto st_case_3
+		case 0:
+			goto st_case_0
+		case 4:
+			goto st_case_4
+		case 5:
+			goto st_case_5
+		case 6:
+			goto st_case_6
+		case 7:
+			goto st_case_7
+		case 48:
+			goto st_case_48
+		case 49:
+			goto st_case_49
+		case 50:
+			goto st_case_50
+		case 8:
+			goto st_case_8
+		case 9:
+			goto st_case_9
+		case 10:
+			goto st_case_10
+		case 11:
+			goto st_case_11
+		case 51:
+			goto st_case_51
+		case 52:
+			goto st_case_52
+		case 53:
+			goto st_case_53
+		case 54:
+			goto st_case_54
+		case 55:
+			goto st_case_55
+		case 56:
+			goto st_case_56
+		case 57:
+			goto st_case_57
+		case 58:
+			goto st_case_58
+		case 59:
+			goto st_case_59
+		case 60:
+			goto st_case_60
+		case 61:
+			goto st_case_61
+		case 62:
+			goto st_case_62
+		case 63:
+			goto st_case_63
+		case 64:
+			goto st_case_64
+		case 65:
+			goto st_case_65
+		case 66:
+			goto st_case_66
+		case 67:
+			goto st_case_67
+		case 68:
+			goto st_case_68
+		case 69:
+			goto st_case_69
+		case 70:
+			goto st_case_70
+		case 12:
+			goto st_case_12
+		case 13:
+			goto st_case_13
+		case 14:
+			goto st_case_14
+		case 15:
+			goto st_case_15
+		case 16:
+			goto st_case_16
+		case 71:
+			goto st_case_71
+		case 17:
+			goto st_case_17
+		case 18:
+			goto st_case_18
+		case 72:
+			goto st_case_72
+		case 73:
+			goto st_case_73
+		case 74:
+			goto st_case_74
+		case 75:
+			goto st_case_75
+		case 76:
+			goto st_case_76
+		case 77:
+			goto st_case_77
+		case 78:
+			goto st_case_78
+		case 79:
+			goto st_case_79
+		case 80:
+			goto st_case_80
+		case 19:
+			goto st_case_19
+		case 20:
+			goto st_case_20
+		case 21:
+			goto st_case_21
+		case 81:
+			goto st_case_81
+		case 22:
+			goto st_case_22
+		case 23:
+			goto st_case_23
+		case 24:
+			goto st_case_24
+		case 82:
+			goto st_case_82
+		case 25:
+			goto st_case_25
+		case 26:
+			goto st_case_26
+		case 83:
+			goto st_case_83
+		case 84:
+			goto st_case_84
+		case 27:
+			goto st_case_27
+		case 28:
+			goto st_case_28
+		case 29:
+			goto st_case_29
+		case 30:
+			goto st_case_30
+		case 31:
+			goto st_case_31
+		case 32:
+			goto st_case_32
+		case 33:
+			goto st_case_33
+		case 34:
+			goto st_case_34
+		case 35:
+			goto st_case_35
+		case 85:
+			goto st_case_85
+		case 38:
+			goto st_case_38
+		case 87:
+			goto st_case_87
+		case 88:
+			goto st_case_88
+		case 39:
+			goto st_case_39
+		case 40:
+			goto st_case_40
+		case 41:
+			goto st_case_41
+		case 42:
+			goto st_case_42
+		case 89:
+			goto st_case_89
+		case 43:
+			goto st_case_43
+		case 90:
+			goto st_case_90
+		case 44:
+			goto st_case_44
+		case 45:
+			goto st_case_45
+		case 46:
+			goto st_case_46
+		case 86:
+			goto st_case_86
+		case 36:
+			goto st_case_36
+		case 37:
+			goto st_case_37
+		}
+		goto st_out
+	st47:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof47
+		}
+	st_case_47:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr33
+		case 13:
+			goto tr33
+		case 32:
+			goto tr82
+		case 35:
+			goto tr33
+		case 44:
+			goto tr33
+		case 92:
+			goto tr83
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr82
+		}
+		goto tr81
+	tr31:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st1
+	tr81:
+//line machine.go.rl:74
+
+		m.beginMetric = true
+
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st1
+	st1:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof1
+		}
+	st_case_1:
+//line machine.go:586
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr2
+		case 13:
+			goto tr2
+		case 32:
+			goto tr1
+		case 44:
+			goto tr3
+		case 92:
+			goto st9
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr1
+		}
+		goto st1
+	tr1:
+		(m.cs) = 2
+//line machine.go.rl:78
+
+		err = m.handler.SetMeasurement(m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr58:
+		(m.cs) = 2
+//line machine.go.rl:91
+
+		err = m.handler.AddTag(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st2:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof2
+		}
+	st_case_2:
+//line machine.go:634
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr7
+		case 13:
+			goto tr7
+		case 32:
+			goto st2
+		case 44:
+			goto tr7
+		case 61:
+			goto tr7
+		case 92:
+			goto tr8
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto st2
+		}
+		goto tr5
+	tr5:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st3
+	st3:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof3
+		}
+	st_case_3:
+//line machine.go:664
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr7
+		case 44:
+			goto tr7
+		case 61:
+			goto tr10
+		case 92:
+			goto st13
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr7
+		}
+		goto st3
+	tr2:
+		(m.cs) = 0
+//line machine.go.rl:38
+
+		err = ErrTagParse
+		(m.p)--
+
+		(m.cs) = 35
+		{
+			(m.p)++
+			goto _out
+		}
+
+		goto _again
+	tr7:
+		(m.cs) = 0
+//line machine.go.rl:31
+
+		err = ErrFieldParse
+		(m.p)--
+
+		(m.cs) = 35
+		{
+			(m.p)++
+			goto _out
+		}
+
+		goto _again
+	tr33:
+		(m.cs) = 0
+//line machine.go.rl:24
+
+		err = ErrNameParse
+		(m.p)--
+
+		(m.cs) = 35
+		{
+			(m.p)++
+			goto _out
+		}
+
+		goto _again
+	tr37:
+		(m.cs) = 0
+//line machine.go.rl:45
+
+		err = ErrTimestampParse
+		(m.p)--
+
+		(m.cs) = 35
+		{
+			(m.p)++
+			goto _out
+		}
+
+		goto _again
+	tr84:
+		(m.cs) = 0
+//line machine.go.rl:31
+
+		err = ErrFieldParse
+		(m.p)--
+
+		(m.cs) = 35
+		{
+			(m.p)++
+			goto _out
+		}
+
+//line machine.go.rl:45
+
+		err = ErrTimestampParse
+		(m.p)--
+
+		(m.cs) = 35
+		{
+			(m.p)++
+			goto _out
+		}
+
+		goto _again
+	tr137:
+//line machine.go.rl:65
+
+		(m.p)--
+
+		{
+			goto st47
+		}
+
+		goto st0
+//line machine.go:750
+	st_case_0:
+	st0:
+		(m.cs) = 0
+		goto _out
+	tr10:
+//line machine.go.rl:100
+
+		m.key = m.text()
+
+		goto st4
+	st4:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof4
+		}
+	st_case_4:
+//line machine.go:766
+		switch (m.data)[(m.p)] {
+		case 34:
+			goto st5
+		case 45:
+			goto tr13
+		case 46:
+			goto tr14
+		case 48:
+			goto tr15
+		case 70:
+			goto tr17
+		case 84:
+			goto tr18
+		case 102:
+			goto tr19
+		case 116:
+			goto tr20
+		}
+		if 49 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+			goto tr16
+		}
+		goto tr7
+	st5:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof5
+		}
+	st_case_5:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr22
+		case 12:
+			goto tr7
+		case 13:
+			goto tr23
+		case 34:
+			goto tr24
+		case 92:
+			goto tr25
+		}
+		goto tr21
+	tr21:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st6
+	tr22:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto st6
+	tr27:
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto st6
+	st6:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof6
+		}
+	st_case_6:
+//line machine.go:838
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr27
+		case 12:
+			goto tr7
+		case 13:
+			goto st7
+		case 34:
+			goto tr29
+		case 92:
+			goto st14
+		}
+		goto st6
+	tr23:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st7
+	st7:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof7
+		}
+	st_case_7:
+//line machine.go:863
+		if (m.data)[(m.p)] == 10 {
+			goto tr27
+		}
+		goto tr7
+	tr24:
+		(m.cs) = 48
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+//line machine.go.rl:140
+
+		err = m.handler.AddString(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr29:
+		(m.cs) = 48
+//line machine.go.rl:140
+
+		err = m.handler.AddString(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st48:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof48
+		}
+	st_case_48:
+//line machine.go:903
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr36
+		case 13:
+			goto st10
+		case 32:
+			goto st49
+		case 44:
+			goto st12
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto st49
+		}
+		goto tr84
+	tr112:
+		(m.cs) = 49
+//line machine.go.rl:122
+
+		err = m.handler.AddFloat(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr119:
+		(m.cs) = 49
+//line machine.go.rl:104
+
+		err = m.handler.AddInt(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr124:
+		(m.cs) = 49
+//line machine.go.rl:113
+
+		err = m.handler.AddUint(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr129:
+		(m.cs) = 49
+//line machine.go.rl:131
+
+		err = m.handler.AddBool(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st49:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof49
+		}
+	st_case_49:
+//line machine.go:975
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr36
+		case 13:
+			goto st10
+		case 32:
+			goto st49
+		case 45:
+			goto tr88
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto tr89
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto st49
+		}
+		goto tr37
+	tr36:
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto st50
+	tr91:
+		(m.cs) = 50
+//line machine.go.rl:149
+
+		err = m.handler.SetTimestamp(m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto _again
+	tr113:
+		(m.cs) = 50
+//line machine.go.rl:122
+
+		err = m.handler.AddFloat(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto _again
+	tr120:
+		(m.cs) = 50
+//line machine.go.rl:104
+
+		err = m.handler.AddInt(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto _again
+	tr125:
+		(m.cs) = 50
+//line machine.go.rl:113
+
+		err = m.handler.AddUint(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto _again
+	tr130:
+		(m.cs) = 50
+//line machine.go.rl:131
+
+		err = m.handler.AddBool(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto _again
+	st50:
+//line machine.go.rl:164
+
+		m.finishMetric = true
+		(m.cs) = 86
+		{
+			(m.p)++
+			goto _out
+		}
+
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof50
+		}
+	st_case_50:
+//line machine.go:1109
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr33
+		case 13:
+			goto tr33
+		case 32:
+			goto st8
+		case 35:
+			goto tr33
+		case 44:
+			goto tr33
+		case 92:
+			goto tr34
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto st8
+		}
+		goto tr31
+	tr82:
+//line machine.go.rl:74
+
+		m.beginMetric = true
+
+		goto st8
+	st8:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof8
+		}
+	st_case_8:
+//line machine.go:1139
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr33
+		case 13:
+			goto tr33
+		case 32:
+			goto st8
+		case 35:
+			goto tr33
+		case 44:
+			goto tr33
+		case 92:
+			goto tr34
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto st8
+		}
+		goto tr31
+	tr34:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st9
+	tr83:
+//line machine.go.rl:74
+
+		m.beginMetric = true
+
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st9
+	st9:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof9
+		}
+	st_case_9:
+//line machine.go:1179
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto st0
+		}
+		goto st1
+	tr92:
+		(m.cs) = 10
+//line machine.go.rl:149
+
+		err = m.handler.SetTimestamp(m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr114:
+		(m.cs) = 10
+//line machine.go.rl:122
+
+		err = m.handler.AddFloat(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr121:
+		(m.cs) = 10
+//line machine.go.rl:104
+
+		err = m.handler.AddInt(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr126:
+		(m.cs) = 10
+//line machine.go.rl:113
+
+		err = m.handler.AddUint(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr131:
+		(m.cs) = 10
+//line machine.go.rl:131
+
+		err = m.handler.AddBool(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st10:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof10
+		}
+	st_case_10:
+//line machine.go:1254
+		if (m.data)[(m.p)] == 10 {
+			goto tr36
+		}
+		goto st0
+	tr88:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st11
+	st11:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof11
+		}
+	st_case_11:
+//line machine.go:1270
+		if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+			goto st51
+		}
+		goto tr37
+	tr89:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st51
+	st51:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof51
+		}
+	st_case_51:
+//line machine.go:1286
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st53
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	tr90:
+		(m.cs) = 52
+//line machine.go.rl:149
+
+		err = m.handler.SetTimestamp(m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st52:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof52
+		}
+	st_case_52:
+//line machine.go:1322
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr36
+		case 13:
+			goto st10
+		case 32:
+			goto st52
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto st52
+		}
+		goto st0
+	st53:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof53
+		}
+	st_case_53:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st54
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st54:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof54
+		}
+	st_case_54:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st55
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st55:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof55
+		}
+	st_case_55:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st56
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st56:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof56
+		}
+	st_case_56:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st57
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st57:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof57
+		}
+	st_case_57:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st58
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st58:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof58
+		}
+	st_case_58:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st59
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st59:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof59
+		}
+	st_case_59:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st60
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st60:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof60
+		}
+	st_case_60:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st61
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st61:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof61
+		}
+	st_case_61:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st62
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st62:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof62
+		}
+	st_case_62:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st63
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st63:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof63
+		}
+	st_case_63:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st64
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st64:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof64
+		}
+	st_case_64:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st65
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st65:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof65
+		}
+	st_case_65:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st66
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st66:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof66
+		}
+	st_case_66:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st67
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st67:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof67
+		}
+	st_case_67:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st68
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st68:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof68
+		}
+	st_case_68:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st69
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st69:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof69
+		}
+	st_case_69:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st70
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr90
+		}
+		goto tr37
+	st70:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof70
+		}
+	st_case_70:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr91
+		case 13:
+			goto tr92
+		case 32:
+			goto tr90
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr90
+		}
+		goto tr37
+	tr115:
+		(m.cs) = 12
+//line machine.go.rl:122
+
+		err = m.handler.AddFloat(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr122:
+		(m.cs) = 12
+//line machine.go.rl:104
+
+		err = m.handler.AddInt(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr127:
+		(m.cs) = 12
+//line machine.go.rl:113
+
+		err = m.handler.AddUint(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr132:
+		(m.cs) = 12
+//line machine.go.rl:131
+
+		err = m.handler.AddBool(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st12:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof12
+		}
+	st_case_12:
+//line machine.go:1783
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr7
+		case 44:
+			goto tr7
+		case 61:
+			goto tr7
+		case 92:
+			goto tr8
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr7
+		}
+		goto tr5
+	tr8:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st13
+	st13:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof13
+		}
+	st_case_13:
+//line machine.go:1809
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr7
+		}
+		goto st3
+	tr25:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st14
+	st14:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof14
+		}
+	st_case_14:
+//line machine.go:1825
+		switch (m.data)[(m.p)] {
+		case 34:
+			goto st6
+		case 92:
+			goto st6
+		}
+		goto tr7
+	tr13:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st15
+	st15:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof15
+		}
+	st_case_15:
+//line machine.go:1844
+		switch (m.data)[(m.p)] {
+		case 46:
+			goto st16
+		case 48:
+			goto st73
+		}
+		if 49 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+			goto st76
+		}
+		goto tr7
+	tr14:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st16
+	st16:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof16
+		}
+	st_case_16:
+//line machine.go:1866
+		if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+			goto st71
+		}
+		goto tr7
+	st71:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof71
+		}
+	st_case_71:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr113
+		case 13:
+			goto tr114
+		case 32:
+			goto tr112
+		case 44:
+			goto tr115
+		case 69:
+			goto st17
+		case 101:
+			goto st17
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st71
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr112
+		}
+		goto tr84
+	st17:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof17
+		}
+	st_case_17:
+		switch (m.data)[(m.p)] {
+		case 34:
+			goto st18
+		case 43:
+			goto st18
+		case 45:
+			goto st18
+		}
+		if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+			goto st72
+		}
+		goto tr7
+	st18:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof18
+		}
+	st_case_18:
+		if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+			goto st72
+		}
+		goto tr7
+	st72:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof72
+		}
+	st_case_72:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr113
+		case 13:
+			goto tr114
+		case 32:
+			goto tr112
+		case 44:
+			goto tr115
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st72
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr112
+		}
+		goto tr84
+	st73:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof73
+		}
+	st_case_73:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr113
+		case 13:
+			goto tr114
+		case 32:
+			goto tr112
+		case 44:
+			goto tr115
+		case 46:
+			goto st71
+		case 69:
+			goto st17
+		case 101:
+			goto st17
+		case 105:
+			goto st75
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st74
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr112
+		}
+		goto tr84
+	st74:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof74
+		}
+	st_case_74:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr113
+		case 13:
+			goto tr114
+		case 32:
+			goto tr112
+		case 44:
+			goto tr115
+		case 46:
+			goto st71
+		case 69:
+			goto st17
+		case 101:
+			goto st17
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st74
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr112
+		}
+		goto tr84
+	st75:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof75
+		}
+	st_case_75:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr120
+		case 13:
+			goto tr121
+		case 32:
+			goto tr119
+		case 44:
+			goto tr122
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr119
+		}
+		goto tr84
+	st76:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof76
+		}
+	st_case_76:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr113
+		case 13:
+			goto tr114
+		case 32:
+			goto tr112
+		case 44:
+			goto tr115
+		case 46:
+			goto st71
+		case 69:
+			goto st17
+		case 101:
+			goto st17
+		case 105:
+			goto st75
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st76
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr112
+		}
+		goto tr84
+	tr15:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st77
+	st77:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof77
+		}
+	st_case_77:
+//line machine.go:2073
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr113
+		case 13:
+			goto tr114
+		case 32:
+			goto tr112
+		case 44:
+			goto tr115
+		case 46:
+			goto st71
+		case 69:
+			goto st17
+		case 101:
+			goto st17
+		case 105:
+			goto st75
+		case 117:
+			goto st78
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st74
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr112
+		}
+		goto tr84
+	st78:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof78
+		}
+	st_case_78:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr125
+		case 13:
+			goto tr126
+		case 32:
+			goto tr124
+		case 44:
+			goto tr127
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr124
+		}
+		goto tr84
+	tr16:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st79
+	st79:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof79
+		}
+	st_case_79:
+//line machine.go:2133
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr113
+		case 13:
+			goto tr114
+		case 32:
+			goto tr112
+		case 44:
+			goto tr115
+		case 46:
+			goto st71
+		case 69:
+			goto st17
+		case 101:
+			goto st17
+		case 105:
+			goto st75
+		case 117:
+			goto st78
+		}
+		switch {
+		case (m.data)[(m.p)] > 12:
+			if 48 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 57 {
+				goto st79
+			}
+		case (m.data)[(m.p)] >= 9:
+			goto tr112
+		}
+		goto tr84
+	tr17:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st80
+	st80:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof80
+		}
+	st_case_80:
+//line machine.go:2174
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr130
+		case 13:
+			goto tr131
+		case 32:
+			goto tr129
+		case 44:
+			goto tr132
+		case 65:
+			goto st19
+		case 97:
+			goto st22
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr129
+		}
+		goto tr84
+	st19:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof19
+		}
+	st_case_19:
+		if (m.data)[(m.p)] == 76 {
+			goto st20
+		}
+		goto tr7
+	st20:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof20
+		}
+	st_case_20:
+		if (m.data)[(m.p)] == 83 {
+			goto st21
+		}
+		goto tr7
+	st21:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof21
+		}
+	st_case_21:
+		if (m.data)[(m.p)] == 69 {
+			goto st81
+		}
+		goto tr7
+	st81:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof81
+		}
+	st_case_81:
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr130
+		case 13:
+			goto tr131
+		case 32:
+			goto tr129
+		case 44:
+			goto tr132
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr129
+		}
+		goto tr84
+	st22:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof22
+		}
+	st_case_22:
+		if (m.data)[(m.p)] == 108 {
+			goto st23
+		}
+		goto tr7
+	st23:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof23
+		}
+	st_case_23:
+		if (m.data)[(m.p)] == 115 {
+			goto st24
+		}
+		goto tr7
+	st24:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof24
+		}
+	st_case_24:
+		if (m.data)[(m.p)] == 101 {
+			goto st81
+		}
+		goto tr7
+	tr18:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st82
+	st82:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof82
+		}
+	st_case_82:
+//line machine.go:2277
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr130
+		case 13:
+			goto tr131
+		case 32:
+			goto tr129
+		case 44:
+			goto tr132
+		case 82:
+			goto st25
+		case 114:
+			goto st26
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr129
+		}
+		goto tr84
+	st25:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof25
+		}
+	st_case_25:
+		if (m.data)[(m.p)] == 85 {
+			goto st21
+		}
+		goto tr7
+	st26:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof26
+		}
+	st_case_26:
+		if (m.data)[(m.p)] == 117 {
+			goto st24
+		}
+		goto tr7
+	tr19:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st83
+	st83:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof83
+		}
+	st_case_83:
+//line machine.go:2325
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr130
+		case 13:
+			goto tr131
+		case 32:
+			goto tr129
+		case 44:
+			goto tr132
+		case 97:
+			goto st22
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr129
+		}
+		goto tr84
+	tr20:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st84
+	st84:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof84
+		}
+	st_case_84:
+//line machine.go:2353
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr130
+		case 13:
+			goto tr131
+		case 32:
+			goto tr129
+		case 44:
+			goto tr132
+		case 114:
+			goto st26
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr129
+		}
+		goto tr84
+	tr3:
+		(m.cs) = 27
+//line machine.go.rl:78
+
+		err = m.handler.SetMeasurement(m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr59:
+		(m.cs) = 27
+//line machine.go.rl:91
+
+		err = m.handler.AddTag(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st27:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof27
+		}
+	st_case_27:
+//line machine.go:2401
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr2
+		case 44:
+			goto tr2
+		case 61:
+			goto tr2
+		case 92:
+			goto tr51
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto tr50
+	tr50:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st28
+	st28:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof28
+		}
+	st_case_28:
+//line machine.go:2427
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr2
+		case 44:
+			goto tr2
+		case 61:
+			goto tr53
+		case 92:
+			goto st33
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto st28
+	tr53:
+//line machine.go.rl:87
+
+		m.key = m.text()
+
+		goto st29
+	st29:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof29
+		}
+	st_case_29:
+//line machine.go:2453
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr2
+		case 44:
+			goto tr2
+		case 61:
+			goto tr2
+		case 92:
+			goto tr56
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto tr55
+	tr55:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st30
+	st30:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof30
+		}
+	st_case_30:
+//line machine.go:2479
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr2
+		case 13:
+			goto tr2
+		case 32:
+			goto tr58
+		case 44:
+			goto tr59
+		case 61:
+			goto tr2
+		case 92:
+			goto st31
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr58
+		}
+		goto st30
+	tr56:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st31
+	st31:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof31
+		}
+	st_case_31:
+//line machine.go:2509
+		if (m.data)[(m.p)] == 92 {
+			goto st32
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto st30
+	st32:
+//line machine.go.rl:240
+		(m.p)--
+
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof32
+		}
+	st_case_32:
+//line machine.go:2525
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr2
+		case 13:
+			goto tr2
+		case 32:
+			goto tr58
+		case 44:
+			goto tr59
+		case 61:
+			goto tr2
+		case 92:
+			goto st31
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr58
+		}
+		goto st30
+	tr51:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st33
+	st33:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof33
+		}
+	st_case_33:
+//line machine.go:2555
+		if (m.data)[(m.p)] == 92 {
+			goto st34
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto st28
+	st34:
+//line machine.go.rl:240
+		(m.p)--
+
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof34
+		}
+	st_case_34:
+//line machine.go:2571
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr2
+		case 44:
+			goto tr2
+		case 61:
+			goto tr53
+		case 92:
+			goto st33
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto st28
+	st35:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof35
+		}
+	st_case_35:
+		if (m.data)[(m.p)] == 10 {
+			goto tr64
+		}
+		goto st35
+	tr64:
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+//line machine.go.rl:70
+
+		{
+			goto st86
+		}
+
+		goto st85
+	st85:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof85
+		}
+	st_case_85:
+//line machine.go:2612
+		goto st0
+	st38:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof38
+		}
+	st_case_38:
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr33
+		case 35:
+			goto tr33
+		case 44:
+			goto tr33
+		case 92:
+			goto tr68
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr33
+		}
+		goto tr67
+	tr67:
+//line machine.go.rl:74
+
+		m.beginMetric = true
+
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st87
+	st87:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof87
+		}
+	st_case_87:
+//line machine.go:2648
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr140
+		case 13:
+			goto tr141
+		case 32:
+			goto tr2
+		case 44:
+			goto tr142
+		case 92:
+			goto st46
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr2
+		}
+		goto st87
+	tr69:
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto st88
+	tr140:
+		(m.cs) = 88
+//line machine.go.rl:78
+
+		err = m.handler.SetMeasurement(m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto _again
+	tr144:
+		(m.cs) = 88
+//line machine.go.rl:91
+
+		err = m.handler.AddTag(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto _again
+	st88:
+//line machine.go.rl:164
+
+		m.finishMetric = true
+		(m.cs) = 86
+		{
+			(m.p)++
+			goto _out
+		}
+
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof88
+		}
+	st_case_88:
+//line machine.go:2722
+		goto st0
+	tr141:
+		(m.cs) = 39
+//line machine.go.rl:78
+
+		err = m.handler.SetMeasurement(m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr145:
+		(m.cs) = 39
+//line machine.go.rl:91
+
+		err = m.handler.AddTag(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st39:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof39
+		}
+	st_case_39:
+//line machine.go:2755
+		if (m.data)[(m.p)] == 10 {
+			goto tr69
+		}
+		goto st0
+	tr142:
+		(m.cs) = 40
+//line machine.go.rl:78
+
+		err = m.handler.SetMeasurement(m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	tr146:
+		(m.cs) = 40
+//line machine.go.rl:91
+
+		err = m.handler.AddTag(m.key, m.text())
+		if err != nil {
+			(m.p)--
+
+			(m.cs) = 35
+			{
+				(m.p)++
+				goto _out
+			}
+		}
+
+		goto _again
+	st40:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof40
+		}
+	st_case_40:
+//line machine.go:2791
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr2
+		case 44:
+			goto tr2
+		case 61:
+			goto tr2
+		case 92:
+			goto tr71
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto tr70
+	tr70:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st41
+	st41:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof41
+		}
+	st_case_41:
+//line machine.go:2817
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr2
+		case 44:
+			goto tr2
+		case 61:
+			goto tr73
+		case 92:
+			goto st44
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto st41
+	tr73:
+//line machine.go.rl:87
+
+		m.key = m.text()
+
+		goto st42
+	st42:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof42
+		}
+	st_case_42:
+//line machine.go:2843
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr2
+		case 44:
+			goto tr2
+		case 61:
+			goto tr2
+		case 92:
+			goto tr76
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto tr75
+	tr75:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st89
+	st89:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof89
+		}
+	st_case_89:
+//line machine.go:2869
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr144
+		case 13:
+			goto tr145
+		case 32:
+			goto tr2
+		case 44:
+			goto tr146
+		case 61:
+			goto tr2
+		case 92:
+			goto st43
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr2
+		}
+		goto st89
+	tr76:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st43
+	st43:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof43
+		}
+	st_case_43:
+//line machine.go:2899
+		if (m.data)[(m.p)] == 92 {
+			goto st90
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto st89
+	st90:
+//line machine.go.rl:240
+		(m.p)--
+
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof90
+		}
+	st_case_90:
+//line machine.go:2915
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr144
+		case 13:
+			goto tr145
+		case 32:
+			goto tr2
+		case 44:
+			goto tr146
+		case 61:
+			goto tr2
+		case 92:
+			goto st43
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto tr2
+		}
+		goto st89
+	tr71:
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st44
+	st44:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof44
+		}
+	st_case_44:
+//line machine.go:2945
+		if (m.data)[(m.p)] == 92 {
+			goto st45
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto st41
+	st45:
+//line machine.go.rl:240
+		(m.p)--
+
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof45
+		}
+	st_case_45:
+//line machine.go:2961
+		switch (m.data)[(m.p)] {
+		case 32:
+			goto tr2
+		case 44:
+			goto tr2
+		case 61:
+			goto tr73
+		case 92:
+			goto st44
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto tr2
+		}
+		goto st41
+	tr68:
+//line machine.go.rl:74
+
+		m.beginMetric = true
+
+//line machine.go.rl:20
+
+		m.pb = m.p
+
+		goto st46
+	st46:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof46
+		}
+	st_case_46:
+//line machine.go:2991
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 13 {
+			goto st0
+		}
+		goto st87
+	tr65:
+//line machine.go.rl:158
+
+		m.lineno++
+		m.sol = m.p
+		m.sol++ // next char will be the first column in the line
+
+		goto st86
+	st86:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof86
+		}
+	st_case_86:
+//line machine.go:3009
+		switch (m.data)[(m.p)] {
+		case 10:
+			goto tr65
+		case 13:
+			goto st36
+		case 32:
+			goto st86
+		case 35:
+			goto st37
+		}
+		if 9 <= (m.data)[(m.p)] && (m.data)[(m.p)] <= 12 {
+			goto st86
+		}
+		goto tr137
+	st36:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof36
+		}
+	st_case_36:
+		if (m.data)[(m.p)] == 10 {
+			goto tr65
+		}
+		goto st0
+	st37:
+		if (m.p)++; (m.p) == (m.pe) {
+			goto _test_eof37
+		}
+	st_case_37:
+		if (m.data)[(m.p)] == 10 {
+			goto tr65
+		}
+		goto st37
+	st_out:
+	_test_eof47:
+		(m.cs) = 47
+		goto _test_eof
+	_test_eof1:
+		(m.cs) = 1
+		goto _test_eof
+	_test_eof2:
+		(m.cs) = 2
+		goto _test_eof
+	_test_eof3:
+		(m.cs) = 3
+		goto _test_eof
+	_test_eof4:
+		(m.cs) = 4
+		goto _test_eof
+	_test_eof5:
+		(m.cs) = 5
+		goto _test_eof
+	_test_eof6:
+		(m.cs) = 6
+		goto _test_eof
+	_test_eof7:
+		(m.cs) = 7
+		goto _test_eof
+	_test_eof48:
+		(m.cs) = 48
+		goto _test_eof
+	_test_eof49:
+		(m.cs) = 49
+		goto _test_eof
+	_test_eof50:
+		(m.cs) = 50
+		goto _test_eof
+	_test_eof8:
+		(m.cs) = 8
+		goto _test_eof
+	_test_eof9:
+		(m.cs) = 9
+		goto _test_eof
+	_test_eof10:
+		(m.cs) = 10
+		goto _test_eof
+	_test_eof11:
+		(m.cs) = 11
+		goto _test_eof
+	_test_eof51:
+		(m.cs) = 51
+		goto _test_eof
+	_test_eof52:
+		(m.cs) = 52
+		goto _test_eof
+	_test_eof53:
+		(m.cs) = 53
+		goto _test_eof
+	_test_eof54:
+		(m.cs) = 54
+		goto _test_eof
+	_test_eof55:
+		(m.cs) = 55
+		goto _test_eof
+	_test_eof56:
+		(m.cs) = 56
+		goto _test_eof
+	_test_eof57:
+		(m.cs) = 57
+		goto _test_eof
+	_test_eof58:
+		(m.cs) = 58
+		goto _test_eof
+	_test_eof59:
+		(m.cs) = 59
+		goto _test_eof
+	_test_eof60:
+		(m.cs) = 60
+		goto _test_eof
+	_test_eof61:
+		(m.cs) = 61
+		goto _test_eof
+	_test_eof62:
+		(m.cs) = 62
+		goto _test_eof
+	_test_eof63:
+		(m.cs) = 63
+		goto _test_eof
+	_test_eof64:
+		(m.cs) = 64
+		goto _test_eof
+	_test_eof65:
+		(m.cs) = 65
+		goto _test_eof
+	_test_eof66:
+		(m.cs) = 66
+		goto _test_eof
+	_test_eof67:
+		(m.cs) = 67
+		goto _test_eof
+	_test_eof68:
+		(m.cs) = 68
+		goto _test_eof
+	_test_eof69:
+		(m.cs) = 69
+		goto _test_eof
+	_test_eof70:
+		(m.cs) = 70
+		goto _test_eof
+	_test_eof12:
+		(m.cs) = 12
+		goto _test_eof
+	_test_eof13:
+		(m.cs) = 13
+		goto _test_eof
+	_test_eof14:
+		(m.cs) = 14
+		goto _test_eof
+	_test_eof15:
+		(m.cs) = 15
+		goto _test_eof
+	_test_eof16:
+		(m.cs) = 16
+		goto _test_eof
+	_test_eof71:
+		(m.cs) = 71
+		goto _test_eof
+	_test_eof17:
+		(m.cs) = 17
+		goto _test_eof
+	_test_eof18:
+		(m.cs) = 18
+		goto _test_eof
+	_test_eof72:
+		(m.cs) = 72
+		goto _test_eof
+	_test_eof73:
+		(m.cs) = 73
+		goto _test_eof
+	_test_eof74:
+		(m.cs) = 74
+		goto _test_eof
+	_test_eof75:
+		(m.cs) = 75
+		goto _test_eof
+	_test_eof76:
+		(m.cs) = 76
+		goto _test_eof
+	_test_eof77:
+		(m.cs) = 77
+		goto _test_eof
+	_test_eof78:
+		(m.cs) = 78
+		goto _test_eof
+	_test_eof79:
+		(m.cs) = 79
+		goto _test_eof
+	_test_eof80:
+		(m.cs) = 80
+		goto _test_eof
+	_test_eof19:
+		(m.cs) = 19
+		goto _test_eof
+	_test_eof20:
+		(m.cs) = 20
+		goto _test_eof
+	_test_eof21:
+		(m.cs) = 21
+		goto _test_eof
+	_test_eof81:
+		(m.cs) = 81
+		goto _test_eof
+	_test_eof22:
+		(m.cs) = 22
+		goto _test_eof
+	_test_eof23:
+		(m.cs) = 23
+		goto _test_eof
+	_test_eof24:
+		(m.cs) = 24
+		goto _test_eof
+	_test_eof82:
+		(m.cs) = 82
+		goto _test_eof
+	_test_eof25:
+		(m.cs) = 25
+		goto _test_eof
+	_test_eof26:
+		(m.cs) = 26
+		goto _test_eof
+	_test_eof83:
+		(m.cs) = 83
+		goto _test_eof
+	_test_eof84:
+		(m.cs) = 84
+		goto _test_eof
+	_test_eof27:
+		(m.cs) = 27
+		goto _test_eof
+	_test_eof28:
+		(m.cs) = 28
+		goto _test_eof
+	_test_eof29:
+		(m.cs) = 29
+		goto _test_eof
+	_test_eof30:
+		(m.cs) = 30
+		goto _test_eof
+	_test_eof31:
+		(m.cs) = 31
+		goto _test_eof
+	_test_eof32:
+		(m.cs) = 32
+		goto _test_eof
+	_test_eof33:
+		(m.cs) = 33
+		goto _test_eof
+	_test_eof34:
+		(m.cs) = 34
+		goto _test_eof
+	_test_eof35:
+		(m.cs) = 35
+		goto _test_eof
+	_test_eof85:
+		(m.cs) = 85
+		goto _test_eof
+	_test_eof38:
+		(m.cs) = 38
+		goto _test_eof
+	_test_eof87:
+		(m.cs) = 87
+		goto _test_eof
+	_test_eof88:
+		(m.cs) = 88
+		goto _test_eof
+	_test_eof39:
+		(m.cs) = 39
+		goto _test_eof
+	_test_eof40:
+		(m.cs) = 40
+		goto _test_eof
+	_test_eof41:
+		(m.cs) = 41
+		goto _test_eof
+	_test_eof42:
+		(m.cs) = 42
+		goto _test_eof
+	_test_eof89:
+		(m.cs) = 89
+		goto _test_eof
+	_test_eof43:
+		(m.cs) = 43
+		goto _test_eof
+	_test_eof90:
+		(m.cs) = 90
+		goto _test_eof
+	_test_eof44:
+		(m.cs) = 44
+		goto _test_eof
+	_test_eof45:
+		(m.cs) = 45
+		goto _test_eof
+	_test_eof46:
+		(m.cs) = 46
+		goto _test_eof
+	_test_eof86:
+		(m.cs) = 86
+		goto _test_eof
+	_test_eof36:
+		(m.cs) = 36
+		goto _test_eof
+	_test_eof37:
+		(m.cs) = 37
+		goto _test_eof
+
+	_test_eof:
+		{
+		}
+		if (m.p) == (m.eof) {
+			switch m.cs {
+			case 8, 38:
+//line machine.go.rl:24
+
+				err = ErrNameParse
+				(m.p)--
+
+				(m.cs) = 35
+				{
+					(m.p)++
+					(m.cs) = 0
+					goto _out
+				}
+
+			case 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26:
+//line machine.go.rl:31
+
+				err = ErrFieldParse
+				(m.p)--
+
+				(m.cs) = 35
+				{
+					(m.p)++
+					(m.cs) = 0
+					goto _out
+				}
+
+			case 27, 28, 29, 31, 33, 34, 40, 41, 42, 43, 44, 45:
+//line machine.go.rl:38
+
+				err = ErrTagParse
+				(m.p)--
+
+				(m.cs) = 35
+				{
+					(m.p)++
+					(m.cs) = 0
+					goto _out
+				}
+
+			case 11:
+//line machine.go.rl:45
+
+				err = ErrTimestampParse
+				(m.p)--
+
+				(m.cs) = 35
+				{
+					(m.p)++
+					(m.cs) = 0
+					goto _out
+				}
+
+			case 87:
+//line machine.go.rl:78
+
+				err = m.handler.SetMeasurement(m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+			case 89, 90:
+//line machine.go.rl:91
+
+				err = m.handler.AddTag(m.key, m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+			case 48, 49, 50, 52:
+//line machine.go.rl:170
+
+				m.finishMetric = true
+
+			case 47:
+//line machine.go.rl:74
+
+				m.beginMetric = true
+
+//line machine.go.rl:170
+
+				m.finishMetric = true
+
+			case 1:
+//line machine.go.rl:78
+
+				err = m.handler.SetMeasurement(m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+//line machine.go.rl:38
+
+				err = ErrTagParse
+				(m.p)--
+
+				(m.cs) = 35
+				{
+					(m.p)++
+					(m.cs) = 0
+					goto _out
+				}
+
+			case 30, 32:
+//line machine.go.rl:91
+
+				err = m.handler.AddTag(m.key, m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+//line machine.go.rl:38
+
+				err = ErrTagParse
+				(m.p)--
+
+				(m.cs) = 35
+				{
+					(m.p)++
+					(m.cs) = 0
+					goto _out
+				}
+
+			case 75:
+//line machine.go.rl:104
+
+				err = m.handler.AddInt(m.key, m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+//line machine.go.rl:170
+
+				m.finishMetric = true
+
+			case 78:
+//line machine.go.rl:113
+
+				err = m.handler.AddUint(m.key, m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+//line machine.go.rl:170
+
+				m.finishMetric = true
+
+			case 71, 72, 73, 74, 76, 77, 79:
+//line machine.go.rl:122
+
+				err = m.handler.AddFloat(m.key, m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+//line machine.go.rl:170
+
+				m.finishMetric = true
+
+			case 80, 81, 82, 83, 84:
+//line machine.go.rl:131
+
+				err = m.handler.AddBool(m.key, m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+//line machine.go.rl:170
+
+				m.finishMetric = true
+
+			case 51, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70:
+//line machine.go.rl:149
+
+				err = m.handler.SetTimestamp(m.text())
+				if err != nil {
+					(m.p)--
+
+					(m.cs) = 35
+					{
+						(m.p)++
+						(m.cs) = 0
+						goto _out
+					}
+				}
+
+//line machine.go.rl:170
+
+				m.finishMetric = true
+
+//line machine.go:3322
+			}
+		}
+
+	_out:
+		{
+		}
+	}
+
+//line machine.go.rl:407
+
+	if err != nil {
+		return err
+	}
+
+	// This would indicate an error in the machine that was reported with a
+	// more specific error.  We return a generic error but this should
+	// possibly be a panic.
+	if m.cs == 0 {
+		m.cs = LineProtocol_en_discard_line
+		return ErrParse
+	}
+
+	// If we haven't found a metric line yet and we reached the EOF, report it
+	// now.  This happens when the data ends with a comment or whitespace.
+	//
+	// Otherwise we have successfully parsed a metric line, so if we are at
+	// the EOF we will report it the next call.
+	if !m.beginMetric && m.p == m.pe && m.pe == m.eof {
+		return EOF
+	}
+
+	return nil
+}
+
+// Position returns the current byte offset into the data.
+func (m *machine) Position() int {
+	return m.p
+}
+
+// LineOffset returns the byte offset of the current line.
+func (m *machine) LineOffset() int {
+	return m.sol
+}
+
+// LineNumber returns the current line number.  Lines are counted based on the
+// regular expression `\r?\n`.
+func (m *machine) LineNumber() int {
+	return m.lineno
+}
+
+// Column returns the current column.
+func (m *machine) Column() int {
+	lineOffset := m.p - m.sol
+	return lineOffset + 1
+}
+
+func (m *machine) text() []byte {
+	return m.data[m.pb:m.p]
+}
+
+type streamMachine struct {
+	machine *machine
+	reader  io.Reader
+}
+
+func NewStreamMachine(r io.Reader, handler Handler) *streamMachine {
+	m := &streamMachine{
+		machine: NewMachine(handler),
+		reader:  r,
+	}
+
+	m.machine.SetData(make([]byte, 1024))
+	m.machine.pe = 0
+	m.machine.eof = -1
+	return m
+}
+
+func (m *streamMachine) Next() error {
+	// Check if we are already at EOF, this should only happen if called again
+	// after already returning EOF.
+	if m.machine.p == m.machine.pe && m.machine.pe == m.machine.eof {
+		return EOF
+	}
+
+	copy(m.machine.data, m.machine.data[m.machine.p:])
+	m.machine.pe = m.machine.pe - m.machine.p
+	m.machine.sol = m.machine.sol - m.machine.p
+	m.machine.pb = 0
+	m.machine.p = 0
+	m.machine.eof = -1
+
+	m.machine.key = nil
+	m.machine.beginMetric = false
+	m.machine.finishMetric = false
+
+	for {
+		// Expand the buffer if it is full
+		if m.machine.pe == len(m.machine.data) {
+			expanded := make([]byte, 2*len(m.machine.data))
+			copy(expanded, m.machine.data)
+			m.machine.data = expanded
+		}
+
+		n, err := m.reader.Read(m.machine.data[m.machine.pe:])
+		if n == 0 && err == io.EOF {
+			m.machine.eof = m.machine.pe
+		} else if err != nil && err != io.EOF {
+			return err
+		}
+
+		m.machine.pe += n
+
+		err = m.machine.exec()
+		if err != nil {
+			return err
+		}
+
+		// If we have successfully parsed a full metric line break out
+		if m.machine.finishMetric {
+			break
+		}
+
+	}
+
+	return nil
+}
+
+// Position returns the current byte offset into the data.
+func (m *streamMachine) Position() int {
+	return m.machine.Position()
+}
+
+// LineOffset returns the byte offset of the current line.
+func (m *streamMachine) LineOffset() int {
+	return m.machine.LineOffset()
+}
+
+// LineNumber returns the current line number.  Lines are counted based on the
+// regular expression `\r?\n`.
+func (m *streamMachine) LineNumber() int {
+	return m.machine.LineNumber()
+}
+
+// Column returns the current column.
+func (m *streamMachine) Column() int {
+	return m.machine.Column()
+}
+
+// LineText returns the text of the current line that has been parsed so far.
+func (m *streamMachine) LineText() string {
+	return string(m.machine.data[0:m.machine.p])
+}
diff --git a/machine.go.rl b/machine.go.rl
new file mode 100644
index 0000000..12eb031
--- /dev/null
+++ b/machine.go.rl
@@ -0,0 +1,549 @@
+package protocol
+
+import (
+	"errors"
+	"io"
+)
+
+var (
+	ErrNameParse = errors.New("expected measurement name")
+	ErrFieldParse = errors.New("expected field")
+	ErrTagParse = errors.New("expected tag")
+	ErrTimestampParse = errors.New("expected timestamp")
+	ErrParse = errors.New("parse error")
+	EOF = errors.New("EOF")
+)
+
+%%{
+machine LineProtocol;
+
+action begin {
+	m.pb = m.p
+}
+
+action name_error {
+	err = ErrNameParse
+	fhold;
+	fnext discard_line;
+	fbreak;
+}
+
+action field_error {
+	err = ErrFieldParse
+	fhold;
+	fnext discard_line;
+	fbreak;
+}
+
+action tagset_error {
+	err = ErrTagParse
+	fhold;
+	fnext discard_line;
+	fbreak;
+}
+
+action timestamp_error {
+	err = ErrTimestampParse
+	fhold;
+	fnext discard_line;
+	fbreak;
+}
+
+action parse_error {
+	err = ErrParse
+	fhold;
+	fnext discard_line;
+	fbreak;
+}
+
+action align_error {
+	err = ErrParse
+	fnext discard_line;
+	fbreak;
+}
+
+action hold_recover {
+	fhold;
+	fgoto main;
+}
+
+action goto_align {
+	fgoto align;
+}
+
+action begin_metric {
+	m.beginMetric = true
+}
+
+action name {
+	err = m.handler.SetMeasurement(m.text())
+	if err != nil {
+		fhold;
+		fnext discard_line;
+		fbreak;
+	}
+}
+
+action tagkey {
+	m.key = m.text()
+}
+
+action tagvalue {
+	err = m.handler.AddTag(m.key, m.text())
+	if err != nil {
+		fhold;
+		fnext discard_line;
+		fbreak;
+	}
+}
+
+action fieldkey {
+	m.key = m.text()
+}
+
+action integer {
+	err = m.handler.AddInt(m.key, m.text())
+	if err != nil {
+		fhold;
+		fnext discard_line;
+		fbreak;
+	}
+}
+
+action unsigned {
+	err = m.handler.AddUint(m.key, m.text())
+	if err != nil {
+		fhold;
+		fnext discard_line;
+		fbreak;
+	}
+}
+
+action float {
+	err = m.handler.AddFloat(m.key, m.text())
+	if err != nil {
+		fhold;
+		fnext discard_line;
+		fbreak;
+	}
+}
+
+action bool {
+	err = m.handler.AddBool(m.key, m.text())
+	if err != nil {
+		fhold;
+		fnext discard_line;
+		fbreak;
+	}
+}
+
+action string {
+	err = m.handler.AddString(m.key, m.text())
+	if err != nil {
+		fhold;
+		fnext discard_line;
+		fbreak;
+	}
+}
+
+action timestamp {
+	err = m.handler.SetTimestamp(m.text())
+	if err != nil {
+		fhold;
+		fnext discard_line;
+		fbreak;
+	}
+}
+
+action incr_newline {
+	m.lineno++
+	m.sol = m.p
+	m.sol++ // next char will be the first column in the line
+}
+
+action eol {
+	m.finishMetric = true
+	fnext align;
+	fbreak;
+}
+
+action finish_metric {
+	m.finishMetric = true
+}
+
+ws =
+	[\t\v\f ];
+
+newline =
+	'\r'? '\n' >incr_newline;
+
+non_zero_digit =
+	[1-9];
+
+integer =
+	'-'? ( digit | ( non_zero_digit digit* ) );
+
+unsigned =
+	( digit | ( non_zero_digit digit* ) );
+
+number =
+	'-'? (digit+ ('.' digit*)? | '.' digit+);
+
+scientific =
+	number 'e'i ["\-+"]? digit+;
+
+timestamp =
+	('-'? digit{1,19}) >begin %timestamp;
+
+fieldkeychar =
+	[^\t\n\v\f\r ,=\\] | ( '\\' [^\t\n\v\f\r] );
+
+fieldkey =
+	fieldkeychar+ >begin %fieldkey;
+
+fieldfloat =
+	(scientific | number) >begin %float;
+
+fieldinteger =
+	(integer 'i') >begin %integer;
+
+fieldunsigned =
+	(unsigned 'u') >begin %unsigned;
+
+false =
+	"false" | "FALSE" | "False" | "F" | "f";
+
+true =
+	"true" | "TRUE" | "True" | "T" | "t";
+
+fieldbool =
+	(true | false) >begin %bool;
+
+fieldstringchar =
+	[^\f\r\n\\"] | '\\' [\\"] | newline;
+
+fieldstring =
+	fieldstringchar* >begin %string;
+
+fieldstringquoted =
+	'"' fieldstring '"';
+
+fieldvalue = fieldinteger | fieldunsigned | fieldfloat | fieldstringquoted | fieldbool;
+
+field =
+	fieldkey '=' fieldvalue;
+
+fieldset =
+	field ( ',' field )*;
+
+tagchar =
+	[^\t\n\v\f\r ,=\\] | ( '\\' [^\t\n\v\f\r\\] ) | '\\\\' %to{ fhold; };
+
+tagkey =
+	tagchar+ >begin %tagkey;
+
+tagvalue =
+	tagchar+ >begin %eof(tagvalue) %tagvalue;
+
+tagset =
+	((',' tagkey '=' tagvalue) $err(tagset_error))*;
+
+measurement_chars =
+	[^\t\n\v\f\r ,\\] | ( '\\' [^\t\n\v\f\r] );
+
+measurement_start =
+	measurement_chars - '#';
+
+measurement =
+	(measurement_start measurement_chars*) >begin %eof(name) %name;
+
+eol_break =
+	newline %to(eol)
+	;
+
+metric =
+	measurement >err(name_error)
+	tagset
+	ws+ fieldset $err(field_error)
+	(ws+ timestamp)? $err(timestamp_error)
+	;
+
+line_with_term =
+	ws* metric ws* eol_break
+	;
+
+line_without_term =
+	ws* metric ws*
+	;
+
+main :=
+	(line_with_term*
+	(line_with_term | line_without_term?)
+    ) >begin_metric %eof(finish_metric)
+	;
+
+# The discard_line machine discards the current line.  Useful for recovering
+# on the next line when an error occurs.
+discard_line :=
+	(any -- newline)* newline @goto_align;
+
+commentline =
+	ws* '#' (any -- newline)* newline;
+
+emptyline =
+	ws* newline;
+
+# The align machine scans forward to the start of the next line.  This machine
+# is used to skip over whitespace and comments, keeping this logic out of the
+# main machine.
+#
+# Skip valid lines that don't contain line protocol, any other data will move
+# control to the main parser via the err action.
+align :=
+	(emptyline | commentline | ws+)* %err(hold_recover);
+
+# Series is a machine for matching measurement+tagset
+series :=
+	(measurement >err(name_error) tagset eol_break?)
+	>begin_metric
+	;
+}%%
+
+%% write data;
+
+type Handler interface {
+	SetMeasurement(name []byte) error
+	AddTag(key []byte, value []byte) error
+	AddInt(key []byte, value []byte) error
+	AddUint(key []byte, value []byte) error
+	AddFloat(key []byte, value []byte) error
+	AddString(key []byte, value []byte) error
+	AddBool(key []byte, value []byte) error
+	SetTimestamp(tm []byte) error
+}
+
+type machine struct {
+	data         []byte
+	cs           int
+	p, pe, eof   int
+	pb           int
+	lineno       int
+	sol          int
+	handler      Handler
+	initState    int
+	key          []byte
+	beginMetric  bool
+	finishMetric bool
+}
+
+func NewMachine(handler Handler) *machine {
+	m := &machine{
+		handler: handler,
+		initState: LineProtocol_en_align,
+	}
+
+	%% access m.;
+	%% variable p m.p;
+	%% variable cs m.cs;
+	%% variable pe m.pe;
+	%% variable eof m.eof;
+	%% variable data m.data;
+	%% write init;
+
+	return m
+}
+
+func NewSeriesMachine(handler Handler) *machine {
+	m := &machine{
+		handler: handler,
+		initState: LineProtocol_en_series,
+	}
+
+	%% access m.;
+	%% variable p m.p;
+	%% variable pe m.pe;
+	%% variable eof m.eof;
+	%% variable data m.data;
+	%% write init;
+
+	return m
+}
+
+func (m *machine) SetData(data []byte) {
+	m.data = data
+	m.p = 0
+	m.pb = 0
+	m.lineno = 1
+	m.sol = 0
+	m.pe = len(data)
+	m.eof = len(data)
+	m.key = nil
+	m.beginMetric = false
+	m.finishMetric = false
+
+	%% write init;
+	m.cs = m.initState
+}
+
+// Next parses the next metric line and returns nil if it was successfully
+// processed.  If the line contains a syntax error an error is returned,
+// otherwise if the end of file is reached before finding a metric line then
+// EOF is returned.
+func (m *machine) Next() error {
+	if m.p == m.pe && m.pe == m.eof {
+		return EOF
+	}
+
+	m.key = nil
+	m.beginMetric = false
+	m.finishMetric = false
+
+	return m.exec()
+}
+
+func (m *machine) exec() error {
+	var err error
+	%% write exec;
+
+	if err != nil {
+		return err
+	}
+
+	// This would indicate an error in the machine that was reported with a
+	// more specific error.  We return a generic error but this should
+	// possibly be a panic.
+	if m.cs == %%{ write error; }%% {
+		m.cs = LineProtocol_en_discard_line
+		return ErrParse
+	}
+
+	// If we haven't found a metric line yet and we reached the EOF, report it
+	// now.  This happens when the data ends with a comment or whitespace.
+	//
+	// Otherwise we have successfully parsed a metric line, so if we are at
+	// the EOF we will report it the next call.
+	if !m.beginMetric && m.p == m.pe && m.pe == m.eof {
+		return EOF
+	}
+
+	return nil
+}
+
+// Position returns the current byte offset into the data.
+func (m *machine) Position() int {
+	return m.p
+}
+
+// LineOffset returns the byte offset of the current line.
+func (m *machine) LineOffset() int {
+	return m.sol
+}
+
+// LineNumber returns the current line number.  Lines are counted based on the
+// regular expression `\r?\n`.
+func (m *machine) LineNumber() int {
+	return m.lineno
+}
+
+// Column returns the current column.
+func (m *machine) Column() int {
+	lineOffset := m.p - m.sol
+	return lineOffset + 1
+}
+
+func (m *machine) text() []byte {
+	return m.data[m.pb:m.p]
+}
+
+type streamMachine struct {
+	machine *machine
+	reader  io.Reader
+}
+
+func NewStreamMachine(r io.Reader, handler Handler) *streamMachine {
+	m := &streamMachine{
+		machine: NewMachine(handler),
+		reader: r,
+	}
+
+	m.machine.SetData(make([]byte, 1024))
+	m.machine.pe = 0
+	m.machine.eof = -1
+	return m
+}
+
+func (m *streamMachine) Next() error {
+	// Check if we are already at EOF, this should only happen if called again
+	// after already returning EOF.
+	if m.machine.p == m.machine.pe && m.machine.pe == m.machine.eof {
+		return EOF
+	}
+
+	copy(m.machine.data, m.machine.data[m.machine.p:])
+	m.machine.pe = m.machine.pe - m.machine.p
+	m.machine.sol = m.machine.sol - m.machine.p
+	m.machine.pb = 0
+	m.machine.p = 0
+	m.machine.eof = -1
+
+	m.machine.key = nil
+	m.machine.beginMetric = false
+	m.machine.finishMetric = false
+
+	for {
+		// Expand the buffer if it is full
+		if m.machine.pe == len(m.machine.data) {
+			expanded := make([]byte, 2 * len(m.machine.data))
+			copy(expanded, m.machine.data)
+			m.machine.data = expanded
+		}
+
+		n, err := m.reader.Read(m.machine.data[m.machine.pe:])
+		if n == 0 && err == io.EOF {
+			m.machine.eof = m.machine.pe
+		} else if err != nil && err != io.EOF {
+			return err
+		}
+
+		m.machine.pe += n
+
+		err = m.machine.exec()
+		if err != nil {
+			return err
+		}
+
+		// If we have successfully parsed a full metric line break out
+		if m.machine.finishMetric {
+			break
+		}
+
+	}
+
+	return nil
+}
+
+// Position returns the current byte offset into the data.
+func (m *streamMachine) Position() int {
+	return m.machine.Position()
+}
+
+// LineOffset returns the byte offset of the current line.
+func (m *streamMachine) LineOffset() int {
+	return m.machine.LineOffset()
+}
+
+// LineNumber returns the current line number.  Lines are counted based on the
+// regular expression `\r?\n`.
+func (m *streamMachine) LineNumber() int {
+	return m.machine.LineNumber()
+}
+
+// Column returns the current column.
+func (m *streamMachine) Column() int {
+	return m.machine.Column()
+}
+
+// LineText returns the text of the current line that has been parsed so far.
+func (m *streamMachine) LineText() string {
+	return string(m.machine.data[0:m.machine.p])
+}
diff --git a/machine_test.go b/machine_test.go
new file mode 100644
index 0000000..ddbd7c4
--- /dev/null
+++ b/machine_test.go
@@ -0,0 +1,2229 @@
+package protocol_test
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"testing"
+
+	protocol "github.com/influxdata/line-protocol"
+)
+
+type TestingHandler struct {
+	results []Result
+}
+
+func (h *TestingHandler) SetMeasurement(name []byte) error {
+	n := make([]byte, len(name))
+	copy(n, name)
+
+	mname := Result{
+		Name:  Measurement,
+		Value: n,
+	}
+	h.results = append(h.results, mname)
+	return nil
+}
+
+func (h *TestingHandler) AddTag(key []byte, value []byte) error {
+	k := make([]byte, len(key))
+	copy(k, key)
+	v := make([]byte, len(value))
+	copy(v, value)
+
+	tagkey := Result{
+		Name:  TagKey,
+		Value: k,
+	}
+	tagvalue := Result{
+		Name:  TagValue,
+		Value: v,
+	}
+	h.results = append(h.results, tagkey, tagvalue)
+	return nil
+}
+
+func (h *TestingHandler) AddInt(key []byte, value []byte) error {
+	k := make([]byte, len(key))
+	copy(k, key)
+	v := make([]byte, len(value))
+	copy(v, value)
+
+	fieldkey := Result{
+		Name:  FieldKey,
+		Value: k,
+	}
+	fieldvalue := Result{
+		Name:  FieldInt,
+		Value: v,
+	}
+	h.results = append(h.results, fieldkey, fieldvalue)
+	return nil
+}
+
+func (h *TestingHandler) AddUint(key []byte, value []byte) error {
+	k := make([]byte, len(key))
+	copy(k, key)
+	v := make([]byte, len(value))
+	copy(v, value)
+
+	fieldkey := Result{
+		Name:  FieldKey,
+		Value: key,
+	}
+	fieldvalue := Result{
+		Name:  FieldUint,
+		Value: value,
+	}
+	h.results = append(h.results, fieldkey, fieldvalue)
+	return nil
+}
+
+func (h *TestingHandler) AddFloat(key []byte, value []byte) error {
+	k := make([]byte, len(key))
+	copy(k, key)
+	v := make([]byte, len(value))
+	copy(v, value)
+
+	fieldkey := Result{
+		Name:  FieldKey,
+		Value: k,
+	}
+	fieldvalue := Result{
+		Name:  FieldFloat,
+		Value: v,
+	}
+	h.results = append(h.results, fieldkey, fieldvalue)
+	return nil
+}
+
+func (h *TestingHandler) AddString(key []byte, value []byte) error {
+	k := make([]byte, len(key))
+	copy(k, key)
+	v := make([]byte, len(value))
+	copy(v, value)
+
+	fieldkey := Result{
+		Name:  FieldKey,
+		Value: k,
+	}
+	fieldvalue := Result{
+		Name:  FieldString,
+		Value: v,
+	}
+	h.results = append(h.results, fieldkey, fieldvalue)
+	return nil
+}
+
+func (h *TestingHandler) AddBool(key []byte, value []byte) error {
+	k := make([]byte, len(key))
+	copy(k, key)
+	v := make([]byte, len(value))
+	copy(v, value)
+
+	fieldkey := Result{
+		Name:  FieldKey,
+		Value: k,
+	}
+	fieldvalue := Result{
+		Name:  FieldBool,
+		Value: v,
+	}
+	h.results = append(h.results, fieldkey, fieldvalue)
+	return nil
+}
+
+func (h *TestingHandler) SetTimestamp(tm []byte) error {
+	t := make([]byte, len(tm))
+	copy(t, tm)
+
+	timestamp := Result{
+		Name:  Timestamp,
+		Value: t,
+	}
+	h.results = append(h.results, timestamp)
+	return nil
+}
+
+func (h *TestingHandler) Result(err error) {
+	var res Result
+	if err == nil {
+		res = Result{
+			Name: Success,
+		}
+	} else {
+		res = Result{
+			Name: Error,
+			err:  err,
+		}
+	}
+	h.results = append(h.results, res)
+}
+
+func (h *TestingHandler) Results() []Result {
+	return h.results
+}
+
+type BenchmarkingHandler struct {
+}
+
+func (h *BenchmarkingHandler) SetMeasurement(name []byte) error {
+	return nil
+}
+
+func (h *BenchmarkingHandler) AddTag(key []byte, value []byte) error {
+	return nil
+}
+
+func (h *BenchmarkingHandler) AddInt(key []byte, value []byte) error {
+	return nil
+}
+
+func (h *BenchmarkingHandler) AddUint(key []byte, value []byte) error {
+	return nil
+}
+
+func (h *BenchmarkingHandler) AddFloat(key []byte, value []byte) error {
+	return nil
+}
+
+func (h *BenchmarkingHandler) AddString(key []byte, value []byte) error {
+	return nil
+}
+
+func (h *BenchmarkingHandler) AddBool(key []byte, value []byte) error {
+	return nil
+}
+
+func (h *BenchmarkingHandler) SetTimestamp(tm []byte) error {
+	return nil
+}
+
+type TokenType int
+
+const (
+	NoMatch TokenType = iota
+	Measurement
+	TagKey
+	TagValue
+	FieldKey
+	FieldString
+	FieldInt
+	FieldUint
+	FieldFloat
+	FieldBool
+	Timestamp
+	EOL
+	EOF
+	Punc
+	WhiteSpace
+	Success
+	Error
+)
+
+func (t TokenType) String() string {
+	switch t {
+	case NoMatch:
+		return "NoMatch"
+	case Measurement:
+		return "Measurement"
+	case TagKey:
+		return "TagKey"
+	case TagValue:
+		return "TagValue"
+	case FieldKey:
+		return "FieldKey"
+	case FieldInt:
+		return "FieldInt"
+	case FieldUint:
+		return "FieldUint"
+	case FieldFloat:
+		return "FieldFloat"
+	case FieldString:
+		return "FieldString"
+	case FieldBool:
+		return "FieldBool"
+	case Timestamp:
+		return "Timestamp"
+	case EOL:
+		return "EOL"
+	case EOF:
+		return "EOF"
+	case Punc:
+		return "Punc"
+	case WhiteSpace:
+		return "WhiteSpace"
+	case Success:
+		return "Success"
+	case Error:
+		return "Error"
+	default:
+		panic("Unknown TokenType")
+	}
+}
+
+type Result struct {
+	Name  TokenType
+	Value []byte
+	err   error
+}
+
+func (r Result) String() string {
+	return fmt.Sprintf("(%s, %q, %v)", r.Name, r.Value, r.err)
+}
+
+var tests = []struct {
+	name    string
+	input   []byte
+	results []Result
+	err     error
+}{
+	{
+		name:    "empty string",
+		input:   []byte(""),
+		results: nil,
+	},
+	{
+		name:  "minimal",
+		input: []byte("cpu value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "newline",
+		input: []byte("cpu value=42\n"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "minimal with timestamp",
+		input: []byte("cpu value=42 1516241192000000000"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name:  Timestamp,
+				Value: []byte("1516241192000000000"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "measurement escape non-special",
+		input: []byte(`c\pu value=42`),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte(`c\pu`),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "measurement escaped trailing backslash",
+		input: []byte(`cpu\\ value=42`),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte(`cpu\\`),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "single char measurement",
+		input: []byte("c value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("c"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "escape backslash in measurement",
+		input: []byte(`cp\\u value=42`),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte(`cp\\u`),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "measurement escape space",
+		input: []byte(`cpu\ abc value=42`),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte(`cpu\ abc`),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "scientific float",
+		input: []byte("cpu value=42e0"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42e0"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "scientific float negative mantissa",
+		input: []byte("cpu value=-42e0"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("-42e0"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "scientific float negative exponent",
+		input: []byte("cpu value=42e-1"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42e-1"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "scientific float big e",
+		input: []byte("cpu value=42E0"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42E0"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "scientific float missing exponent",
+		input: []byte("cpu value=42E"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+		},
+	},
+	{
+		name:  "float with decimal",
+		input: []byte("cpu value=42.2"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42.2"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "negative float",
+		input: []byte("cpu value=-42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("-42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "float without integer digits",
+		input: []byte("cpu value=.42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte(".42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "float without integer digits negative",
+		input: []byte("cpu value=-.42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("-.42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "float with multiple leading 0",
+		input: []byte("cpu value=00.42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("00.42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "invalid float with only dot",
+		input: []byte("cpu value=."),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+		},
+	},
+	{
+		name:  "multiple fields",
+		input: []byte("cpu x=42,y=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("x"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("y"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "integer field",
+		input: []byte("cpu value=42i"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldInt,
+				Value: []byte("42i"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "negative integer field",
+		input: []byte("cpu value=-42i"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldInt,
+				Value: []byte("-42i"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "zero integer field",
+		input: []byte("cpu value=0i"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldInt,
+				Value: []byte("0i"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "negative zero integer field",
+		input: []byte("cpu value=-0i"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldInt,
+				Value: []byte("-0i"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "integer field overflow okay",
+		input: []byte("cpu value=9223372036854775808i"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldInt,
+				Value: []byte("9223372036854775808i"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "invalid field",
+		input: []byte("cpu value=howdy"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+		},
+	},
+	{
+		name:  "string field",
+		input: []byte("cpu value=\"42\""),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldString,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "newline in string field",
+		input: []byte("cpu value=\"4\n2\""),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldString,
+				Value: []byte("4\n2"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "bool field",
+		input: []byte("cpu value=true"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldBool,
+				Value: []byte("true"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "tag",
+		input: []byte("cpu,host=localhost value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte("host"),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte("localhost"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "tag key escape space",
+		input: []byte("cpu,h\\ ost=localhost value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte(`h\ ost`),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte("localhost"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "tag key escape comma",
+		input: []byte("cpu,h\\,ost=localhost value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte(`h\,ost`),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte("localhost"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "tag key escape equal",
+		input: []byte("cpu,h\\=ost=localhost value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte(`h\=ost`),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte("localhost"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "multiple tags",
+		input: []byte("cpu,host=localhost,cpu=cpu0 value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte("host"),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte("localhost"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte("cpu0"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "tag value escape space",
+		input: []byte(`cpu,host=two\ words value=42`),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte("host"),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte(`two\ words`),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "tag value double escape space",
+		input: []byte(`cpu,host=two\\ words value=42`),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte("host"),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte(`two\\ words`),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "tag value triple escape space",
+		input: []byte(`cpu,host=two\\\ words value=42`),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte("host"),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte(`two\\\ words`),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "tag invalid missing separator",
+		input: []byte("cpu,xyzzy value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTagParse,
+			},
+		},
+	},
+	{
+		name:  "tag invalid missing value",
+		input: []byte("cpu,xyzzy= value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTagParse,
+			},
+		},
+	},
+	{
+		name:  "tag invalid unescaped space",
+		input: []byte("cpu,h ost=localhost value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTagParse,
+			},
+		},
+	},
+	{
+		name:  "tag invalid unescaped comma",
+		input: []byte("cpu,h,ost=localhost value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTagParse,
+			},
+		},
+	},
+	{
+		name:  "tag invalid unescaped equals",
+		input: []byte("cpu,h=ost=localhost value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTagParse,
+			},
+		},
+	},
+	{
+		name:  "timestamp negative",
+		input: []byte("cpu value=42 -1"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name:  Timestamp,
+				Value: []byte("-1"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "timestamp zero",
+		input: []byte("cpu value=42 0"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name:  Timestamp,
+				Value: []byte("0"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "multiline",
+		input: []byte("cpu value=42\n\n\n\ncpu value=43"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("43"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "error recovery",
+		input: []byte("cpu value=howdy,value2=42\ncpu\ncpu value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTagParse,
+			},
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "line whitespace",
+		input: []byte("   cpu   value=42  1516241192000000000  \n\n cpu value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name:  Timestamp,
+				Value: []byte("1516241192000000000"),
+			},
+			{
+				Name: Success,
+			},
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "leading newline",
+		input: []byte("\ncpu value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "invalid missing field value",
+		input: []byte("cpu value="),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+		},
+	},
+	{
+		name:  "invalid eof field key",
+		input: []byte("cpu value"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+		},
+	},
+	{
+		name:  "invalid measurement only",
+		input: []byte("cpu"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTagParse,
+			},
+		},
+	},
+	{
+		name:  "invalid measurement char",
+		input: []byte(","),
+		results: []Result{
+			{
+				Name: Error,
+				err:  protocol.ErrNameParse,
+			},
+		},
+	},
+	{
+		name:  "invalid missing tag",
+		input: []byte("cpu, value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTagParse,
+			},
+		},
+	},
+	{
+		name:  "invalid missing field",
+		input: []byte("cpu,x=y "),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  TagKey,
+				Value: []byte("x"),
+			},
+			{
+				Name:  TagValue,
+				Value: []byte("y"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+		},
+	},
+	{
+		name:  "invalid too many fields",
+		input: []byte("cpu value=42 value=43"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTimestampParse,
+			},
+		},
+	},
+	{
+		name:  "invalid timestamp too long",
+		input: []byte("cpu value=42 12345678901234567890"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTimestampParse,
+			},
+		},
+	},
+	{
+		name:  "invalid open string field",
+		input: []byte(`cpu value="42 12345678901234567890`),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+		},
+	},
+	{
+		name:  "invalid field value",
+		input: []byte("cpu value=howdy"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrFieldParse,
+			},
+		},
+	},
+	{
+		name:  "invalid quoted timestamp",
+		input: []byte("cpu value=42 \"12345678901234567890\""),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Error,
+				err:  protocol.ErrTimestampParse,
+			},
+		},
+	},
+	{
+		name:    "comment only",
+		input:   []byte("# blah blah"),
+		results: []Result(nil),
+	},
+	{
+		name:  "commented line",
+		input: []byte("# blah blah\ncpu value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "middle comment",
+		input: []byte("cpu value=42\n# blah blah\ncpu value=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "end with comment",
+		input: []byte("cpu value=42\n# blah blah"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "end with comment and whitespace",
+		input: []byte("cpu value=42\n# blah blah\n\n  "),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "unicode",
+		input: []byte("cpu ☺=42"),
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("☺"),
+			},
+			{
+				Name:  FieldFloat,
+				Value: []byte("42"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+}
+
+func TestMachine(t *testing.T) {
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := &TestingHandler{}
+			fsm := protocol.NewMachine(handler)
+			fsm.SetData(tt.input)
+
+			for i := 0; i < 20; i++ {
+				err := fsm.Next()
+				if err != nil && err == protocol.EOF {
+					break
+				}
+				handler.Result(err)
+			}
+
+			results := handler.Results()
+
+			if len(tt.results) != len(results) {
+				t.Errorf("unexpected difference in result lengths = %d, want %v", len(tt.results), len(results))
+				return
+			}
+			for i := range tt.results {
+				if tt.results[i].String() != results[i].String() {
+					t.Errorf("Machine.Results() = %v, want %v", results, tt.results)
+				}
+			}
+		})
+	}
+}
+
+var positionTests = []struct {
+	name   string
+	input  []byte
+	lineno int
+	column int
+}{
+	{
+		name:   "empty string",
+		input:  []byte(""),
+		lineno: 1,
+		column: 1,
+	},
+	{
+		name:   "minimal",
+		input:  []byte("cpu value=42"),
+		lineno: 1,
+		column: 13,
+	},
+	{
+		name:   "one newline",
+		input:  []byte("cpu value=42\ncpu value=42"),
+		lineno: 2,
+		column: 13,
+	},
+	{
+		name:   "several newlines",
+		input:  []byte("cpu value=42\n\n\n"),
+		lineno: 4,
+		column: 1,
+	},
+	{
+		name:   "error on second line",
+		input:  []byte("cpu value=42\ncpu value=invalid"),
+		lineno: 2,
+		column: 11,
+	},
+	{
+		name:   "error after comment line",
+		input:  []byte("cpu value=42\n# comment\ncpu value=invalid"),
+		lineno: 3,
+		column: 11,
+	},
+	{
+		name:   "dos line endings",
+		input:  []byte("cpu value=42\r\ncpu value=invalid"),
+		lineno: 2,
+		column: 11,
+	},
+	{
+		name:   "mac line endings not supported",
+		input:  []byte("cpu value=42\rcpu value=invalid"),
+		lineno: 1,
+		column: 14,
+	},
+}
+
+func TestMachinePosition(t *testing.T) {
+	for _, tt := range positionTests {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := &TestingHandler{}
+			fsm := protocol.NewMachine(handler)
+			fsm.SetData(tt.input)
+
+			// Parse until an error or eof
+			for i := 0; i < 20; i++ {
+				err := fsm.Next()
+				if err != nil {
+					break
+				}
+			}
+
+			if tt.lineno != fsm.LineNumber() {
+				t.Errorf("unexpected difference in line number: %d, want = %d", tt.lineno, fsm.LineNumber())
+			}
+
+			if tt.column != fsm.Column() {
+				t.Errorf("unexpected difference in column number: %d, want = %d", tt.column, fsm.Column())
+			}
+		})
+	}
+}
+
+func BenchmarkMachine(b *testing.B) {
+	for _, tt := range tests {
+		b.Run(tt.name, func(b *testing.B) {
+			handler := &BenchmarkingHandler{}
+			fsm := protocol.NewMachine(handler)
+
+			for n := 0; n < b.N; n++ {
+				fsm.SetData(tt.input)
+
+				for {
+					err := fsm.Next()
+					if err != nil {
+						break
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestMachineProcstat(t *testing.T) {
+	input := []byte("procstat,exe=bash,process_name=bash voluntary_context_switches=42i,memory_rss=5103616i,rlimit_memory_data_hard=2147483647i,cpu_time_user=0.02,rlimit_file_locks_soft=2147483647i,pid=29417i,cpu_time_nice=0,rlimit_memory_locked_soft=65536i,read_count=259i,rlimit_memory_vms_hard=2147483647i,memory_swap=0i,rlimit_num_fds_soft=1024i,rlimit_nice_priority_hard=0i,cpu_time_soft_irq=0,cpu_time=0i,rlimit_memory_locked_hard=65536i,realtime_priority=0i,signals_pending=0i,nice_priority=20i,cpu_time_idle=0,memory_stack=139264i,memory_locked=0i,rlimit_memory_stack_soft=8388608i,cpu_time_iowait=0,cpu_time_guest=0,cpu_time_guest_nice=0,rlimit_memory_data_soft=2147483647i,read_bytes=0i,rlimit_cpu_time_soft=2147483647i,involuntary_context_switches=2i,write_bytes=106496i,cpu_time_system=0,cpu_time_irq=0,cpu_usage=0,memory_vms=21659648i,memory_data=1576960i,rlimit_memory_stack_hard=2147483647i,num_threads=1i,rlimit_memory_rss_soft=2147483647i,rlimit_realtime_priority_soft=0i,num_fds=4i,write_count=35i,rlimit_signals_pending_soft=78994i,cpu_time_steal=0,rlimit_num_fds_hard=4096i,rlimit_file_locks_hard=2147483647i,rlimit_cpu_time_hard=2147483647i,rlimit_signals_pending_hard=78994i,rlimit_nice_priority_soft=0i,rlimit_memory_rss_hard=2147483647i,rlimit_memory_vms_soft=2147483647i,rlimit_realtime_priority_hard=0i 1517620624000000000")
+	handler := &TestingHandler{}
+	fsm := protocol.NewMachine(handler)
+	fsm.SetData(input)
+	for {
+		err := fsm.Next()
+		if err != nil {
+			break
+		}
+	}
+}
+
+func BenchmarkMachineProcstat(b *testing.B) {
+	input := []byte("procstat,exe=bash,process_name=bash voluntary_context_switches=42i,memory_rss=5103616i,rlimit_memory_data_hard=2147483647i,cpu_time_user=0.02,rlimit_file_locks_soft=2147483647i,pid=29417i,cpu_time_nice=0,rlimit_memory_locked_soft=65536i,read_count=259i,rlimit_memory_vms_hard=2147483647i,memory_swap=0i,rlimit_num_fds_soft=1024i,rlimit_nice_priority_hard=0i,cpu_time_soft_irq=0,cpu_time=0i,rlimit_memory_locked_hard=65536i,realtime_priority=0i,signals_pending=0i,nice_priority=20i,cpu_time_idle=0,memory_stack=139264i,memory_locked=0i,rlimit_memory_stack_soft=8388608i,cpu_time_iowait=0,cpu_time_guest=0,cpu_time_guest_nice=0,rlimit_memory_data_soft=2147483647i,read_bytes=0i,rlimit_cpu_time_soft=2147483647i,involuntary_context_switches=2i,write_bytes=106496i,cpu_time_system=0,cpu_time_irq=0,cpu_usage=0,memory_vms=21659648i,memory_data=1576960i,rlimit_memory_stack_hard=2147483647i,num_threads=1i,rlimit_memory_rss_soft=2147483647i,rlimit_realtime_priority_soft=0i,num_fds=4i,write_count=35i,rlimit_signals_pending_soft=78994i,cpu_time_steal=0,rlimit_num_fds_hard=4096i,rlimit_file_locks_hard=2147483647i,rlimit_cpu_time_hard=2147483647i,rlimit_signals_pending_hard=78994i,rlimit_nice_priority_soft=0i,rlimit_memory_rss_hard=2147483647i,rlimit_memory_vms_soft=2147483647i,rlimit_realtime_priority_hard=0i 1517620624000000000")
+	handler := &BenchmarkingHandler{}
+	fsm := protocol.NewMachine(handler)
+	for n := 0; n < b.N; n++ {
+		fsm.SetData(input)
+		for {
+			err := fsm.Next()
+			if err != nil {
+				break
+			}
+		}
+	}
+}
+
+func TestSeriesMachine(t *testing.T) {
+	var tests = []struct {
+		name    string
+		input   []byte
+		results []Result
+		err     error
+	}{
+		{
+			name:    "empty string",
+			input:   []byte(""),
+			results: nil,
+		},
+		{
+			name:  "no tags",
+			input: []byte("cpu"),
+			results: []Result{
+				{
+					Name:  Measurement,
+					Value: []byte("cpu"),
+				},
+				{
+					Name: Success,
+				},
+			},
+		},
+		{
+			name:  "tags",
+			input: []byte("cpu,a=x,b=y"),
+			results: []Result{
+				{
+					Name:  Measurement,
+					Value: []byte("cpu"),
+				},
+				{
+					Name:  TagKey,
+					Value: []byte("a"),
+				},
+				{
+					Name:  TagValue,
+					Value: []byte("x"),
+				},
+				{
+					Name:  TagKey,
+					Value: []byte("b"),
+				},
+				{
+					Name:  TagValue,
+					Value: []byte("y"),
+				},
+				{
+					Name: Success,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := &TestingHandler{}
+			fsm := protocol.NewSeriesMachine(handler)
+			fsm.SetData(tt.input)
+
+			for {
+				err := fsm.Next()
+				if err != nil {
+					break
+				}
+				handler.Result(err)
+			}
+
+			results := handler.Results()
+			if len(tt.results) != len(results) {
+				t.Errorf("unexpected difference in result lengths = %d, want %v", len(tt.results), len(results))
+				return
+			}
+			for i := range tt.results {
+				if tt.results[i].String() != results[i].String() {
+					t.Errorf("Machine.Results() = %v, want %v", results, tt.results)
+				}
+			}
+		})
+	}
+}
+
+type MockHandler struct {
+	SetMeasurementF func(name []byte) error
+	AddTagF         func(key []byte, value []byte) error
+	AddIntF         func(key []byte, value []byte) error
+	AddUintF        func(key []byte, value []byte) error
+	AddFloatF       func(key []byte, value []byte) error
+	AddStringF      func(key []byte, value []byte) error
+	AddBoolF        func(key []byte, value []byte) error
+	SetTimestampF   func(tm []byte) error
+
+	TestingHandler
+}
+
+func (h *MockHandler) SetMeasurement(name []byte) error {
+	h.TestingHandler.SetMeasurement(name)
+	return h.SetMeasurementF(name)
+}
+
+func (h *MockHandler) AddTag(name, value []byte) error {
+	return h.AddTagF(name, value)
+}
+
+func (h *MockHandler) AddInt(name, value []byte) error {
+	err := h.AddIntF(name, value)
+	if err != nil {
+		return err
+	}
+	h.TestingHandler.AddInt(name, value)
+	return nil
+}
+
+func (h *MockHandler) AddUint(name, value []byte) error {
+	err := h.AddUintF(name, value)
+	if err != nil {
+		return err
+	}
+	h.TestingHandler.AddUint(name, value)
+	return nil
+}
+
+func (h *MockHandler) AddFloat(name, value []byte) error {
+	return h.AddFloatF(name, value)
+}
+
+func (h *MockHandler) AddString(name, value []byte) error {
+	return h.AddStringF(name, value)
+}
+
+func (h *MockHandler) AddBool(name, value []byte) error {
+	return h.AddBoolF(name, value)
+}
+
+func (h *MockHandler) SetTimestamp(tm []byte) error {
+	return h.SetTimestampF(tm)
+}
+
+var errorRecoveryTests = []struct {
+	name    string
+	input   []byte
+	handler *MockHandler
+	results []Result
+}{
+	{
+		name:  "integer",
+		input: []byte("cpu value=43i\ncpu value=42i"),
+		handler: &MockHandler{
+			SetMeasurementF: func(name []byte) error {
+				return nil
+			},
+			AddIntF: func(name, value []byte) error {
+				if string(value) != "42i" {
+					return errors.New("handler error")
+				}
+				return nil
+			},
+		},
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  errors.New("handler error"),
+			},
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldInt,
+				Value: []byte("42i"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "integer with timestamp",
+		input: []byte("cpu value=43i 1516241192000000000\ncpu value=42i"),
+		handler: &MockHandler{
+			SetMeasurementF: func(name []byte) error {
+				return nil
+			},
+			AddIntF: func(name, value []byte) error {
+				if string(value) != "42i" {
+					return errors.New("handler error")
+				}
+				return nil
+			},
+		},
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  errors.New("handler error"),
+			},
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldInt,
+				Value: []byte("42i"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+	{
+		name:  "unsigned",
+		input: []byte("cpu value=43u\ncpu value=42u"),
+		handler: &MockHandler{
+			SetMeasurementF: func(name []byte) error {
+				return nil
+			},
+			AddUintF: func(name, value []byte) error {
+				if string(value) != "42u" {
+					return errors.New("handler error")
+				}
+				return nil
+			},
+		},
+		results: []Result{
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name: Error,
+				err:  errors.New("handler error"),
+			},
+			{
+				Name:  Measurement,
+				Value: []byte("cpu"),
+			},
+			{
+				Name:  FieldKey,
+				Value: []byte("value"),
+			},
+			{
+				Name:  FieldUint,
+				Value: []byte("42u"),
+			},
+			{
+				Name: Success,
+			},
+		},
+	},
+}
+
+func TestHandlerErrorRecovery(t *testing.T) {
+	for _, tt := range errorRecoveryTests {
+		t.Run(tt.name, func(t *testing.T) {
+			fsm := protocol.NewMachine(tt.handler)
+			fsm.SetData(tt.input)
+
+			for i := 0; i < 20; i++ {
+				err := fsm.Next()
+				if err != nil && err == protocol.EOF {
+					break
+				}
+				tt.handler.Result(err)
+			}
+
+			results := tt.handler.Results()
+			if len(tt.results) != len(results) {
+				t.Errorf("unexpected difference in result lengths = %d, want %v", len(tt.results), len(results))
+				return
+			}
+			for i := range tt.results {
+				if tt.results[i].String() != results[i].String() {
+					t.Errorf("Machine.Results() = %v, want %v", results, tt.results)
+				}
+			}
+		})
+	}
+}
+
+func TestStreamMachine(t *testing.T) {
+	type testcase struct {
+		name    string
+		input   io.Reader
+		results []Result
+		err     error
+	}
+
+	var tc []testcase
+	for _, tt := range tests {
+		tc = append(tc, testcase{
+			name:    tt.name,
+			input:   bytes.NewBuffer([]byte(tt.input)),
+			results: tt.results,
+			err:     tt.err,
+		})
+	}
+
+	for _, tt := range tc {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := &TestingHandler{}
+			fsm := protocol.NewStreamMachine(tt.input, handler)
+
+			// Parse only up to 20 metrics; to avoid any bugs where the parser
+			// isn't terminated.
+			for i := 0; i < 20; i++ {
+				err := fsm.Next()
+				if err != nil && err == protocol.EOF {
+					break
+				}
+				handler.Result(err)
+			}
+
+			results := handler.Results()
+			if len(tt.results) != len(results) {
+				t.Errorf("unexpected difference in result lengths = %d, want %v", len(tt.results), len(results))
+				return
+			}
+			for i := range tt.results {
+				if tt.results[i].String() != results[i].String() {
+					t.Errorf("Machine.Results() = %v, want %v", results, tt.results)
+				}
+			}
+		})
+	}
+}
+
+func TestStreamMachinePosition(t *testing.T) {
+	type testcase struct {
+		name   string
+		input  io.Reader
+		lineno int
+		column int
+	}
+
+	var tc []testcase
+	for _, tt := range positionTests {
+		tc = append(tc, testcase{
+			name:   tt.name,
+			input:  bytes.NewBuffer([]byte(tt.input)),
+			lineno: tt.lineno,
+			column: tt.column,
+		})
+	}
+
+	for _, tt := range tc {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := &TestingHandler{}
+			fsm := protocol.NewStreamMachine(tt.input, handler)
+
+			// Parse until an error or eof
+			for i := 0; i < 20; i++ {
+				err := fsm.Next()
+				if err != nil {
+					break
+				}
+			}
+
+			if tt.lineno != fsm.LineNumber() {
+				t.Errorf("unexpected difference in line number: %d, want = %d", tt.lineno, fsm.LineNumber())
+			}
+
+			if tt.column != fsm.Column() {
+				t.Errorf("unexpected difference in column number: %d, want = %d", tt.column, fsm.Column())
+			}
+		})
+	}
+}
diff --git a/metric.go b/metric.go
index 8281b13..0b1fa2e 100644
--- a/metric.go
+++ b/metric.go
@@ -1,14 +1,19 @@
 package protocol
 
-import "time"
+import (
+	"fmt"
+	"hash/fnv"
+	"sort"
+	"time"
+)
 
-// Tag holds the keys and values for a bunch of Tag k/v pairs
+// Tag holds the keys and values for a bunch of Tag k/v pairs.
 type Tag struct {
 	Key   string
 	Value string
 }
 
-// Field holds the keys and values for a bunch of Metric Field k/v pairs
+// Field holds the keys and values for a bunch of Metric Field k/v pairs where Value can be a uint64, int64, int, float32, float64, string, or bool.
 type Field struct {
 	Key   string
 	Value interface{}
@@ -22,22 +27,30 @@ type Metric interface {
 	FieldList() []*Field
 }
 
+// MutableMetric represents a metric that can be be modified.
+type MutableMetric interface {
+	Metric
+	SetTime(time.Time)
+	AddTag(key, value string)
+	AddField(key string, value interface{})
+}
+
 // FieldSortOrder is a type for controlling if Fields are sorted
 type FieldSortOrder int
 
 const (
-	// NoSortFields tells the Decoder to not sort the fields
+	// NoSortFields tells the Decoder to not sort the fields.
 	NoSortFields FieldSortOrder = iota
 
-	// SortFields tells the Decoder to sort the fields
+	// SortFields tells the Decoder to sort the fields.
 	SortFields
 )
 
-// FieldTypeSupport is a type for the parser to understand its type support
+// FieldTypeSupport is a type for the parser to understand its type support.
 type FieldTypeSupport int
 
 const (
-	// UintSupport means the parser understands uint64s and can store them without having to convert to int64
+	// UintSupport means the parser understands uint64s and can store them without having to convert to int64.
 	UintSupport FieldTypeSupport = 1 << iota
 )
 
@@ -60,12 +73,356 @@ func (e FieldError) Error() string {
 }
 
 var (
-	// ErrNeedMoreSpace tells us that the Decoder's io.Reader is full
+	// ErrNeedMoreSpace tells us that the Decoder's io.Reader is full.
 	ErrNeedMoreSpace = &MetricError{"need more space"}
 
-	// ErrInvalidName tells us that the chosen name is invalid
+	// ErrInvalidName tells us that the chosen name is invalid.
 	ErrInvalidName = &MetricError{"invalid name"}
 
-	// ErrNoFields tells us that there were no serializable fields in the line/metric
+	// ErrNoFields tells us that there were no serializable fields in the line/metric.
 	ErrNoFields = &MetricError{"no serializable fields"}
 )
+
+type metric struct {
+	name   string
+	tags   []*Tag
+	fields []*Field
+	tm     time.Time
+}
+
+// New creates a new metric via maps.
+func New(
+	name string,
+	tags map[string]string,
+	fields map[string]interface{},
+	tm time.Time,
+) (MutableMetric, error) {
+	m := &metric{
+		name:   name,
+		tags:   nil,
+		fields: nil,
+		tm:     tm,
+	}
+
+	if len(tags) > 0 {
+		m.tags = make([]*Tag, 0, len(tags))
+		for k, v := range tags {
+			m.tags = append(m.tags,
+				&Tag{Key: k, Value: v})
+		}
+		sort.Slice(m.tags, func(i, j int) bool { return m.tags[i].Key < m.tags[j].Key })
+	}
+
+	if len(fields) > 0 {
+		m.fields = make([]*Field, 0, len(fields))
+		for k, v := range fields {
+			v := convertField(v)
+			if v == nil {
+				continue
+			}
+			m.AddField(k, v)
+		}
+	}
+
+	return m, nil
+}
+
+// FromMetric returns a deep copy of the metric with any tracking information
+// removed.
+func FromMetric(other Metric) Metric {
+	m := &metric{
+		name:   other.Name(),
+		tags:   make([]*Tag, len(other.TagList())),
+		fields: make([]*Field, len(other.FieldList())),
+		tm:     other.Time(),
+	}
+
+	for i, tag := range other.TagList() {
+		m.tags[i] = &Tag{Key: tag.Key, Value: tag.Value}
+	}
+
+	for i, field := range other.FieldList() {
+		m.fields[i] = &Field{Key: field.Key, Value: field.Value}
+	}
+	return m
+}
+
+func (m *metric) String() string {
+	return fmt.Sprintf("%s %v %v %d", m.name, m.Tags(), m.Fields(), m.tm.UnixNano())
+}
+
+func (m *metric) Name() string {
+	return m.name
+}
+
+func (m *metric) Tags() map[string]string {
+	tags := make(map[string]string, len(m.tags))
+	for _, tag := range m.tags {
+		tags[tag.Key] = tag.Value
+	}
+	return tags
+}
+
+func (m *metric) TagList() []*Tag {
+	return m.tags
+}
+
+func (m *metric) Fields() map[string]interface{} {
+	fields := make(map[string]interface{}, len(m.fields))
+	for _, field := range m.fields {
+		fields[field.Key] = field.Value
+	}
+
+	return fields
+}
+
+func (m *metric) FieldList() []*Field {
+	return m.fields
+}
+
+func (m *metric) Time() time.Time {
+	return m.tm
+}
+
+func (m *metric) SetName(name string) {
+	m.name = name
+}
+
+func (m *metric) AddPrefix(prefix string) {
+	m.name = prefix + m.name
+}
+
+func (m *metric) AddSuffix(suffix string) {
+	m.name = m.name + suffix
+}
+
+func (m *metric) AddTag(key, value string) {
+	for i, tag := range m.tags {
+		if key > tag.Key {
+			continue
+		}
+
+		if key == tag.Key {
+			tag.Value = value
+			return
+		}
+
+		m.tags = append(m.tags, nil)
+		copy(m.tags[i+1:], m.tags[i:])
+		m.tags[i] = &Tag{Key: key, Value: value}
+		return
+	}
+
+	m.tags = append(m.tags, &Tag{Key: key, Value: value})
+}
+
+func (m *metric) HasTag(key string) bool {
+	for _, tag := range m.tags {
+		if tag.Key == key {
+			return true
+		}
+	}
+	return false
+}
+
+func (m *metric) GetTag(key string) (string, bool) {
+	for _, tag := range m.tags {
+		if tag.Key == key {
+			return tag.Value, true
+		}
+	}
+	return "", false
+}
+
+func (m *metric) RemoveTag(key string) {
+	for i, tag := range m.tags {
+		if tag.Key == key {
+			copy(m.tags[i:], m.tags[i+1:])
+			m.tags[len(m.tags)-1] = nil
+			m.tags = m.tags[:len(m.tags)-1]
+			return
+		}
+	}
+}
+
+func (m *metric) AddField(key string, value interface{}) {
+	for i, field := range m.fields {
+		if key == field.Key {
+			m.fields[i] = &Field{Key: key, Value: convertField(value)}
+			return
+		}
+	}
+	m.fields = append(m.fields, &Field{Key: key, Value: convertField(value)})
+}
+
+func (m *metric) HasField(key string) bool {
+	for _, field := range m.fields {
+		if field.Key == key {
+			return true
+		}
+	}
+	return false
+}
+
+func (m *metric) GetField(key string) (interface{}, bool) {
+	for _, field := range m.fields {
+		if field.Key == key {
+			return field.Value, true
+		}
+	}
+	return nil, false
+}
+
+func (m *metric) RemoveField(key string) {
+	for i, field := range m.fields {
+		if field.Key == key {
+			copy(m.fields[i:], m.fields[i+1:])
+			m.fields[len(m.fields)-1] = nil
+			m.fields = m.fields[:len(m.fields)-1]
+			return
+		}
+	}
+}
+
+func (m *metric) SetTime(t time.Time) {
+	m.tm = t
+}
+
+func (m *metric) Copy() Metric {
+	m2 := &metric{
+		name:   m.name,
+		tags:   make([]*Tag, len(m.tags)),
+		fields: make([]*Field, len(m.fields)),
+		tm:     m.tm,
+	}
+
+	for i, tag := range m.tags {
+		m2.tags[i] = &Tag{Key: tag.Key, Value: tag.Value}
+	}
+
+	for i, field := range m.fields {
+		m2.fields[i] = &Field{Key: field.Key, Value: field.Value}
+	}
+	return m2
+}
+
+func (m *metric) HashID() uint64 {
+	h := fnv.New64a()
+	h.Write([]byte(m.name))
+	h.Write([]byte("\n"))
+	for _, tag := range m.tags {
+		h.Write([]byte(tag.Key))
+		h.Write([]byte("\n"))
+		h.Write([]byte(tag.Value))
+		h.Write([]byte("\n"))
+	}
+	return h.Sum64()
+}
+
+func (m *metric) Accept() {
+}
+
+func (m *metric) Reject() {
+}
+
+func (m *metric) Drop() {
+}
+
+// Convert field to a supported type or nil if unconvertible
+func convertField(v interface{}) interface{} {
+	switch v := v.(type) {
+	case float64:
+		return v
+	case int64:
+		return v
+	case string:
+		return v
+	case bool:
+		return v
+	case int:
+		return int64(v)
+	case uint:
+		return uint64(v)
+	case uint64:
+		return uint64(v)
+	case []byte:
+		return string(v)
+	case int32:
+		return int64(v)
+	case int16:
+		return int64(v)
+	case int8:
+		return int64(v)
+	case uint32:
+		return uint64(v)
+	case uint16:
+		return uint64(v)
+	case uint8:
+		return uint64(v)
+	case float32:
+		return float64(v)
+	case *float64:
+		if v != nil {
+			return *v
+		}
+	case *int64:
+		if v != nil {
+			return *v
+		}
+	case *string:
+		if v != nil {
+			return *v
+		}
+	case *bool:
+		if v != nil {
+			return *v
+		}
+	case *int:
+		if v != nil {
+			return int64(*v)
+		}
+	case *uint:
+		if v != nil {
+			return uint64(*v)
+		}
+	case *uint64:
+		if v != nil {
+			return uint64(*v)
+		}
+	case *[]byte:
+		if v != nil {
+			return string(*v)
+		}
+	case *int32:
+		if v != nil {
+			return int64(*v)
+		}
+	case *int16:
+		if v != nil {
+			return int64(*v)
+		}
+	case *int8:
+		if v != nil {
+			return int64(*v)
+		}
+	case *uint32:
+		if v != nil {
+			return uint64(*v)
+		}
+	case *uint16:
+		if v != nil {
+			return uint64(*v)
+		}
+	case *uint8:
+		if v != nil {
+			return uint64(*v)
+		}
+	case *float32:
+		if v != nil {
+			return float64(*v)
+		}
+	default:
+		return nil
+	}
+	return nil
+}
diff --git a/parser.go b/parser.go
new file mode 100644
index 0000000..b9eaa91
--- /dev/null
+++ b/parser.go
@@ -0,0 +1,192 @@
+package protocol
+
+import (
+	"fmt"
+	"io"
+	"strings"
+	"sync"
+	"time"
+)
+
+const (
+	maxErrorBufferSize = 1024
+)
+
+// TimeFunc is used to override the default time for a metric
+// with no specified timestamp.
+type TimeFunc func() time.Time
+
+// ParseError indicates a error in the parsing of the text.
+type ParseError struct {
+	Offset     int
+	LineOffset int
+	LineNumber int
+	Column     int
+	msg        string
+	buf        string
+}
+
+func (e *ParseError) Error() string {
+	buffer := e.buf[e.LineOffset:]
+	eol := strings.IndexAny(buffer, "\r\n")
+	if eol >= 0 {
+		buffer = buffer[:eol]
+	}
+	if len(buffer) > maxErrorBufferSize {
+		buffer = buffer[:maxErrorBufferSize] + "..."
+	}
+	return fmt.Sprintf("metric parse error: %s at %d:%d: %q", e.msg, e.LineNumber, e.Column, buffer)
+}
+
+// Parser is an InfluxDB Line Protocol parser that implements the
+// parsers.Parser interface.
+type Parser struct {
+	DefaultTags map[string]string
+
+	sync.Mutex
+	*machine
+	handler *MetricHandler
+}
+
+// NewParser returns a Parser than accepts line protocol
+func NewParser(handler *MetricHandler) *Parser {
+	return &Parser{
+		machine: NewMachine(handler),
+		handler: handler,
+	}
+}
+
+// NewSeriesParser returns a Parser than accepts a measurement and tagset
+func NewSeriesParser(handler *MetricHandler) *Parser {
+	return &Parser{
+		machine: NewSeriesMachine(handler),
+		handler: handler,
+	}
+}
+
+// SetTimeFunc allows default times to be set when no time is specified
+// for a metric in line-protocol.
+func (p *Parser) SetTimeFunc(f TimeFunc) {
+	p.handler.SetTimeFunc(f)
+}
+
+// Parse interprets line-protocol bytes as many metrics.
+func (p *Parser) Parse(input []byte) ([]Metric, error) {
+	p.Lock()
+	defer p.Unlock()
+	metrics := make([]Metric, 0)
+	p.machine.SetData(input)
+
+	for {
+		err := p.machine.Next()
+		if err == EOF {
+			break
+		}
+
+		if err != nil {
+			return nil, &ParseError{
+				Offset:     p.machine.Position(),
+				LineOffset: p.machine.LineOffset(),
+				LineNumber: p.machine.LineNumber(),
+				Column:     p.machine.Column(),
+				msg:        err.Error(),
+				buf:        string(input),
+			}
+		}
+
+		metric, err := p.handler.Metric()
+		if err != nil {
+			return nil, err
+		}
+
+		if metric == nil {
+			continue
+		}
+
+		metrics = append(metrics, metric)
+	}
+
+	return metrics, nil
+}
+
+// StreamParser is an InfluxDB Line Protocol parser.  It is not safe for
+// concurrent use in multiple goroutines.
+type StreamParser struct {
+	machine *streamMachine
+	handler *MetricHandler
+}
+
+// NewStreamParser parses from a reader and iterates the machine
+// metric by metric.  Not safe for concurrent use in multiple goroutines.
+func NewStreamParser(r io.Reader) *StreamParser {
+	handler := NewMetricHandler()
+	return &StreamParser{
+		machine: NewStreamMachine(r, handler),
+		handler: handler,
+	}
+}
+
+// SetTimeFunc changes the function used to determine the time of metrics
+// without a timestamp.  The default TimeFunc is time.Now.  Useful mostly for
+// testing, or perhaps if you want all metrics to have the same timestamp.
+func (p *StreamParser) SetTimeFunc(f TimeFunc) {
+	p.handler.SetTimeFunc(f)
+}
+
+// SetTimePrecision specifies units for the time stamp.
+func (p *StreamParser) SetTimePrecision(u time.Duration) {
+	p.handler.SetTimePrecision(u)
+}
+
+// Next parses the next item from the stream.  You can repeat calls to this
+// function until it returns EOF.
+func (p *StreamParser) Next() (Metric, error) {
+	err := p.machine.Next()
+	if err == EOF {
+		return nil, EOF
+	}
+
+	if err != nil {
+		return nil, &ParseError{
+			Offset:     p.machine.Position(),
+			LineOffset: p.machine.LineOffset(),
+			LineNumber: p.machine.LineNumber(),
+			Column:     p.machine.Column(),
+			msg:        err.Error(),
+			buf:        p.machine.LineText(),
+		}
+	}
+
+	metric, err := p.handler.Metric()
+	if err != nil {
+		return nil, err
+	}
+
+	return metric, nil
+}
+
+// Position returns the current byte offset into the data.
+func (p *StreamParser) Position() int {
+	return p.machine.Position()
+}
+
+// LineOffset returns the byte offset of the current line.
+func (p *StreamParser) LineOffset() int {
+	return p.machine.LineOffset()
+}
+
+// LineNumber returns the current line number.  Lines are counted based on the
+// regular expression `\r?\n`.
+func (p *StreamParser) LineNumber() int {
+	return p.machine.LineNumber()
+}
+
+// Column returns the current column.
+func (p *StreamParser) Column() int {
+	return p.machine.Column()
+}
+
+// LineText returns the text of the current line that has been parsed so far.
+func (p *StreamParser) LineText() string {
+	return p.machine.LineText()
+}
diff --git a/parser_test.go b/parser_test.go
new file mode 100644
index 0000000..ce926e7
--- /dev/null
+++ b/parser_test.go
@@ -0,0 +1,954 @@
+package protocol
+
+import (
+	"bytes"
+	"reflect"
+	"sort"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+)
+
+func MustMetric(v Metric, err error) Metric {
+	if err != nil {
+		panic(err)
+	}
+	return v
+}
+
+var DefaultTime = func() time.Time {
+	return time.Unix(42, 0)
+}
+
+var ptests = []struct {
+	name     string
+	input    []byte
+	timeFunc func() time.Time
+	metrics  []Metric
+	err      error
+}{
+	{
+		name:  "minimal",
+		input: []byte("cpu value=42 0"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(0, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "minimal with newline",
+		input: []byte("cpu value=42 0\n"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(0, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "measurement escape space",
+		input: []byte(`c\ pu value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"c pu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "measurement escape comma",
+		input: []byte(`c\,pu value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"c,pu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "tags",
+		input: []byte(`cpu,cpu=cpu0,host=localhost value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{
+						"cpu":  "cpu0",
+						"host": "localhost",
+					},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "tags escape unescapable",
+		input: []byte(`cpu,ho\st=localhost value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{
+						`ho\st`: "localhost",
+					},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "tags escape equals",
+		input: []byte(`cpu,ho\=st=localhost value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{
+						"ho=st": "localhost",
+					},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "tags escape comma",
+		input: []byte(`cpu,ho\,st=localhost value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{
+						"ho,st": "localhost",
+					},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "tag value escape space",
+		input: []byte(`cpu,host=two\ words value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{
+						"host": "two words",
+					},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "tag value double escape space",
+		input: []byte(`cpu,host=two\\ words value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{
+						"host": `two\ words`,
+					},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "tag value triple escape space",
+		input: []byte(`cpu,host=two\\\ words value=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{
+						"host": `two\\ words`,
+					},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field key escape not escapable",
+		input: []byte(`cpu va\lue=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						`va\lue`: 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field key escape equals",
+		input: []byte(`cpu va\=lue=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						`va=lue`: 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field key escape comma",
+		input: []byte(`cpu va\,lue=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						`va,lue`: 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field key escape space",
+		input: []byte(`cpu va\ lue=42`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						`va lue`: 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field int",
+		input: []byte("cpu value=42i"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:    "field int overflow",
+		input:   []byte("cpu value=9223372036854775808i"),
+		metrics: nil,
+		err: &ParseError{
+			Offset:     30,
+			LineNumber: 1,
+			Column:     31,
+			msg:        strconv.ErrRange.Error(),
+			buf:        "cpu value=9223372036854775808i",
+		},
+	},
+	{
+		name:  "field int max value",
+		input: []byte("cpu value=9223372036854775807i"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": int64(9223372036854775807),
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field uint",
+		input: []byte("cpu value=42u"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": uint64(42),
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:    "field uint overflow",
+		input:   []byte("cpu value=18446744073709551616u"),
+		metrics: nil,
+		err: &ParseError{
+			Offset:     31,
+			LineNumber: 1,
+			Column:     32,
+			msg:        strconv.ErrRange.Error(),
+			buf:        "cpu value=18446744073709551616u",
+		},
+	},
+	{
+		name:  "field uint max value",
+		input: []byte("cpu value=18446744073709551615u"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": uint64(18446744073709551615),
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field boolean",
+		input: []byte("cpu value=true"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": true,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field string",
+		input: []byte(`cpu value="42"`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": "42",
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field string escape quote",
+		input: []byte(`cpu value="how\"dy"`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						`value`: `how"dy`,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field string escape backslash",
+		input: []byte(`cpu value="how\\dy"`),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						`value`: `how\dy`,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "field string newline",
+		input: []byte("cpu value=\"4\n2\""),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": "4\n2",
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "no timestamp",
+		input: []byte("cpu value=42"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "no timestamp",
+		input: []byte("cpu value=42"),
+		timeFunc: func() time.Time {
+			return time.Unix(42, 123456789)
+		},
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 123456789),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:  "multiple lines",
+		input: []byte("cpu value=42\ncpu value=42"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+			MustMetric(
+				New(
+					"cpu",
+					map[string]string{},
+					map[string]interface{}{
+						"value": 42.0,
+					},
+					time.Unix(42, 0),
+				),
+			),
+		},
+		err: nil,
+	},
+	{
+		name:    "invalid measurement only",
+		input:   []byte("cpu"),
+		metrics: nil,
+		err: &ParseError{
+			Offset:     3,
+			LineNumber: 1,
+			Column:     4,
+			msg:        ErrTagParse.Error(),
+			buf:        "cpu",
+		},
+	},
+	{
+		name:  "procstat",
+		input: []byte("procstat,exe=bash,process_name=bash voluntary_context_switches=42i,memory_rss=5103616i,rlimit_memory_data_hard=2147483647i,cpu_time_user=0.02,rlimit_file_locks_soft=2147483647i,pid=29417i,cpu_time_nice=0,rlimit_memory_locked_soft=65536i,read_count=259i,rlimit_memory_vms_hard=2147483647i,memory_swap=0i,rlimit_num_fds_soft=1024i,rlimit_nice_priority_hard=0i,cpu_time_soft_irq=0,cpu_time=0i,rlimit_memory_locked_hard=65536i,realtime_priority=0i,signals_pending=0i,nice_priority=20i,cpu_time_idle=0,memory_stack=139264i,memory_locked=0i,rlimit_memory_stack_soft=8388608i,cpu_time_iowait=0,cpu_time_guest=0,cpu_time_guest_nice=0,rlimit_memory_data_soft=2147483647i,read_bytes=0i,rlimit_cpu_time_soft=2147483647i,involuntary_context_switches=2i,write_bytes=106496i,cpu_time_system=0,cpu_time_irq=0,cpu_usage=0,memory_vms=21659648i,memory_data=1576960i,rlimit_memory_stack_hard=2147483647i,num_threads=1i,rlimit_memory_rss_soft=2147483647i,rlimit_realtime_priority_soft=0i,num_fds=4i,write_count=35i,rlimit_signals_pending_soft=78994i,cpu_time_steal=0,rlimit_num_fds_hard=4096i,rlimit_file_locks_hard=2147483647i,rlimit_cpu_time_hard=2147483647i,rlimit_signals_pending_hard=78994i,rlimit_nice_priority_soft=0i,rlimit_memory_rss_hard=2147483647i,rlimit_memory_vms_soft=2147483647i,rlimit_realtime_priority_hard=0i 1517620624000000000"),
+		metrics: []Metric{
+			MustMetric(
+				New(
+					"procstat",
+					map[string]string{
+						"exe":          "bash",
+						"process_name": "bash",
+					},
+					map[string]interface{}{
+						"cpu_time":                      0,
+						"cpu_time_guest":                float64(0),
+						"cpu_time_guest_nice":           float64(0),
+						"cpu_time_idle":                 float64(0),
+						"cpu_time_iowait":               float64(0),
+						"cpu_time_irq":                  float64(0),
+						"cpu_time_nice":                 float64(0),
+						"cpu_time_soft_irq":             float64(0),
+						"cpu_time_steal":                float64(0),
+						"cpu_time_system":               float64(0),
+						"cpu_time_user":                 float64(0.02),
+						"cpu_usage":                     float64(0),
+						"involuntary_context_switches":  2,
+						"memory_data":                   1576960,
+						"memory_locked":                 0,
+						"memory_rss":                    5103616,
+						"memory_stack":                  139264,
+						"memory_swap":                   0,
+						"memory_vms":                    21659648,
+						"nice_priority":                 20,
+						"num_fds":                       4,
+						"num_threads":                   1,
+						"pid":                           29417,
+						"read_bytes":                    0,
+						"read_count":                    259,
+						"realtime_priority":             0,
+						"rlimit_cpu_time_hard":          2147483647,
+						"rlimit_cpu_time_soft":          2147483647,
+						"rlimit_file_locks_hard":        2147483647,
+						"rlimit_file_locks_soft":        2147483647,
+						"rlimit_memory_data_hard":       2147483647,
+						"rlimit_memory_data_soft":       2147483647,
+						"rlimit_memory_locked_hard":     65536,
+						"rlimit_memory_locked_soft":     65536,
+						"rlimit_memory_rss_hard":        2147483647,
+						"rlimit_memory_rss_soft":        2147483647,
+						"rlimit_memory_stack_hard":      2147483647,
+						"rlimit_memory_stack_soft":      8388608,
+						"rlimit_memory_vms_hard":        2147483647,
+						"rlimit_memory_vms_soft":        2147483647,
+						"rlimit_nice_priority_hard":     0,
+						"rlimit_nice_priority_soft":     0,
+						"rlimit_num_fds_hard":           4096,
+						"rlimit_num_fds_soft":           1024,
+						"rlimit_realtime_priority_hard": 0,
+						"rlimit_realtime_priority_soft": 0,
+						"rlimit_signals_pending_hard":   78994,
+						"rlimit_signals_pending_soft":   78994,
+						"signals_pending":               0,
+						"voluntary_context_switches":    42,
+						"write_bytes":                   106496,
+						"write_count":                   35,
+					},
+					time.Unix(0, 1517620624000000000),
+				),
+			),
+		},
+		err: nil,
+	},
+}
+
+func TestParser(t *testing.T) {
+	for _, tt := range ptests {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := NewMetricHandler()
+			parser := NewParser(handler)
+			parser.SetTimeFunc(DefaultTime)
+			if tt.timeFunc != nil {
+				parser.SetTimeFunc(tt.timeFunc)
+			}
+
+			metrics, err := parser.Parse(tt.input)
+			if (err != nil) != (tt.err != nil) {
+				t.Errorf("unexpected error difference: %v, want = %v", err, tt.err)
+				return
+			} else if tt.err != nil && err.Error() != tt.err.Error() {
+				t.Errorf("unexpected error difference: %v, want = %v", err, tt.err)
+			}
+
+			if got, want := len(metrics), len(tt.metrics); got != want {
+				t.Errorf("unexpected metric length difference: %d, want = %d", got, want)
+			}
+
+			for i, expected := range tt.metrics {
+				RequireMetricEqual(t, expected, metrics[i])
+			}
+		})
+	}
+}
+
+func BenchmarkParser(b *testing.B) {
+	for _, tt := range ptests {
+		b.Run(tt.name, func(b *testing.B) {
+			handler := NewMetricHandler()
+			parser := NewParser(handler)
+			for n := 0; n < b.N; n++ {
+				metrics, err := parser.Parse(tt.input)
+				_ = err
+				_ = metrics
+			}
+		})
+	}
+}
+
+func TestStreamParser(t *testing.T) {
+	for _, tt := range ptests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := bytes.NewBuffer(tt.input)
+			parser := NewStreamParser(r)
+			parser.SetTimeFunc(DefaultTime)
+			if tt.timeFunc != nil {
+				parser.SetTimeFunc(tt.timeFunc)
+			}
+
+			var i int
+			for {
+				m, err := parser.Next()
+				if err != nil {
+					if err == EOF {
+						break
+					}
+					if (err != nil) == (tt.err != nil) && err.Error() != tt.err.Error() {
+						t.Errorf("unexpected error difference: %v, want = %v", err, tt.err)
+					}
+					break
+				}
+
+				RequireMetricEqual(t, tt.metrics[i], m)
+				i++
+			}
+		})
+	}
+}
+
+func TestSeriesParser(t *testing.T) {
+	var tests = []struct {
+		name     string
+		input    []byte
+		timeFunc func() time.Time
+		metrics  []Metric
+		err      error
+	}{
+		{
+			name:    "empty",
+			input:   []byte(""),
+			metrics: []Metric{},
+		},
+		{
+			name:  "minimal",
+			input: []byte("cpu"),
+			metrics: []Metric{
+				MustMetric(
+					New(
+						"cpu",
+						map[string]string{},
+						map[string]interface{}{},
+						time.Unix(0, 0),
+					),
+				),
+			},
+		},
+		{
+			name:  "tags",
+			input: []byte("cpu,a=x,b=y"),
+			metrics: []Metric{
+				MustMetric(
+					New(
+						"cpu",
+						map[string]string{
+							"a": "x",
+							"b": "y",
+						},
+						map[string]interface{}{},
+						time.Unix(0, 0),
+					),
+				),
+			},
+		},
+		{
+			name:    "missing tag value",
+			input:   []byte("cpu,a="),
+			metrics: []Metric{},
+			err: &ParseError{
+				Offset:     6,
+				LineNumber: 1,
+				Column:     7,
+				msg:        ErrTagParse.Error(),
+				buf:        "cpu,a=",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := NewMetricHandler()
+			parser := NewSeriesParser(handler)
+			if tt.timeFunc != nil {
+				parser.SetTimeFunc(tt.timeFunc)
+			}
+
+			metrics, err := parser.Parse(tt.input)
+
+			if (err != nil) != (tt.err != nil) {
+				t.Errorf("unexpected error difference: %v, want = %v", err, tt.err)
+				return
+			} else if tt.err != nil && err.Error() != tt.err.Error() {
+				t.Errorf("unexpected error difference: %v, want = %v", err, tt.err)
+			}
+
+			if got, want := len(metrics), len(tt.metrics); got != want {
+				t.Errorf("unexpected metric length difference: %d, want = %d", got, want)
+			}
+
+			for i, expected := range tt.metrics {
+				if got, want := metrics[i].Name(), expected.Name(); got != want {
+					t.Errorf("unexpected metric name difference: %v, want = %v", got, want)
+				}
+				if got, want := len(metrics[i].TagList()), len(expected.TagList()); got != want {
+					t.Errorf("unexpected tag length difference: %d, want = %d", got, want)
+					break
+				}
+
+				got := metrics[i].TagList()
+				want := expected.TagList()
+				for i := range got {
+					if got[i].Key != want[i].Key {
+						t.Errorf("unexpected tag key difference: %v, want = %v", got[i].Key, want[i].Key)
+					}
+					if got[i].Value != want[i].Value {
+						t.Errorf("unexpected tag key difference: %v, want = %v", got[i].Value, want[i].Value)
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestParserErrorString(t *testing.T) {
+	var ptests = []struct {
+		name      string
+		input     []byte
+		errString string
+	}{
+		{
+			name:      "multiple line error",
+			input:     []byte("cpu value=42\ncpu value=invalid\ncpu value=42"),
+			errString: `metric parse error: expected field at 2:11: "cpu value=invalid"`,
+		},
+		{
+			name:      "handler error",
+			input:     []byte("cpu value=9223372036854775808i\ncpu value=42"),
+			errString: `metric parse error: value out of range at 1:31: "cpu value=9223372036854775808i"`,
+		},
+		{
+			name:      "buffer too long",
+			input:     []byte("cpu " + strings.Repeat("ab", maxErrorBufferSize) + "=invalid\ncpu value=42"),
+			errString: "metric parse error: expected field at 1:2054: \"cpu " + strings.Repeat("ab", maxErrorBufferSize)[:maxErrorBufferSize-4] + "...\"",
+		},
+		{
+			name:      "multiple line error",
+			input:     []byte("cpu value=42\ncpu value=invalid\ncpu value=42\ncpu value=invalid"),
+			errString: `metric parse error: expected field at 2:11: "cpu value=invalid"`,
+		},
+	}
+
+	for _, tt := range ptests {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := NewMetricHandler()
+			parser := NewParser(handler)
+
+			_, err := parser.Parse(tt.input)
+			if err.Error() != tt.errString {
+				t.Errorf("unexpected error difference: %v, want = %v", err.Error(), tt.errString)
+			}
+		})
+	}
+}
+
+func TestStreamParserErrorString(t *testing.T) {
+	var ptests = []struct {
+		name  string
+		input []byte
+		errs  []string
+	}{
+		{
+			name:  "multiple line error",
+			input: []byte("cpu value=42\ncpu value=invalid\ncpu value=42"),
+			errs: []string{
+				`metric parse error: expected field at 2:11: "cpu value="`,
+			},
+		},
+		{
+			name:  "handler error",
+			input: []byte("cpu value=9223372036854775808i\ncpu value=42"),
+			errs: []string{
+				`metric parse error: value out of range at 1:31: "cpu value=9223372036854775808i"`,
+			},
+		},
+		{
+			name:  "buffer too long",
+			input: []byte("cpu " + strings.Repeat("ab", maxErrorBufferSize) + "=invalid\ncpu value=42"),
+			errs: []string{
+				"metric parse error: expected field at 1:2054: \"cpu " + strings.Repeat("ab", maxErrorBufferSize)[:maxErrorBufferSize-4] + "...\"",
+			},
+		},
+		{
+			name:  "multiple errors",
+			input: []byte("foo value=1asdf2.0\nfoo value=2.0\nfoo value=3asdf2.0\nfoo value=4.0"),
+			errs: []string{
+				`metric parse error: expected field at 1:12: "foo value=1"`,
+				`metric parse error: expected field at 3:12: "foo value=3"`,
+			},
+		},
+	}
+
+	for _, tt := range ptests {
+		t.Run(tt.name, func(t *testing.T) {
+			parser := NewStreamParser(bytes.NewBuffer(tt.input))
+
+			var errs []error
+			for i := 0; i < 20; i++ {
+				_, err := parser.Next()
+				if err == EOF {
+					break
+				}
+
+				if err != nil {
+					errs = append(errs, err)
+				}
+			}
+
+			if got, want := len(errs), len(tt.errs); got != want {
+				t.Errorf("unexpected error length difference: %d, want = %d", got, want)
+			}
+
+			for i, err := range errs {
+				if err.Error() != tt.errs[i] {
+					t.Errorf("unexpected error difference: %v, want = %v", err.Error(), tt.errs[i])
+				}
+			}
+		})
+	}
+}
+
+// RequireMetricEqual halts the test with an error if the metrics are not
+// equal.
+func RequireMetricEqual(t *testing.T, expected, actual Metric) {
+	t.Helper()
+
+	var lhs, rhs *metricDiff
+	if expected != nil {
+		lhs = newMetricDiff(expected)
+	}
+	if actual != nil {
+		rhs = newMetricDiff(actual)
+	}
+
+	if !reflect.DeepEqual(lhs, rhs) {
+		t.Fatalf("Metric %v, want=%v", rhs, lhs)
+	}
+}
+
+type metricDiff struct {
+	Measurement string
+	Tags        []*Tag
+	Fields      []*Field
+	Time        time.Time
+}
+
+func newMetricDiff(metric Metric) *metricDiff {
+	if metric == nil {
+		return nil
+	}
+
+	m := &metricDiff{}
+	m.Measurement = metric.Name()
+	m.Tags = append(m.Tags, metric.TagList()...)
+	m.Fields = append(m.Fields, metric.FieldList()...)
+
+	sort.Slice(m.Tags, func(i, j int) bool {
+		return m.Tags[i].Key < m.Tags[j].Key
+	})
+
+	sort.Slice(m.Fields, func(i, j int) bool {
+		return m.Fields[i].Key < m.Fields[j].Key
+	})
+
+	m.Time = metric.Time()
+	return m
+}
diff --git a/writer.go b/writer.go
new file mode 100644
index 0000000..aa77533
--- /dev/null
+++ b/writer.go
@@ -0,0 +1,130 @@
+package protocol
+
+import (
+	"fmt"
+	"time"
+)
+
+// Write writes out data to a line protocol encoder.  Note: it does no sorting.  It assumes you have done your own sorting for tagValues
+func (e *Encoder) Write(name []byte, ts time.Time, tagKeys, tagVals, fieldKeys [][]byte, fieldVals []interface{}) (int, error) {
+	e.header = e.header[:0]
+	if len(name) == 0 || name[len(name)-1] == byte('\\') {
+		return 0, ErrInvalidName
+	}
+	nameEscapeBytes(&e.header, name)
+	for i := range tagKeys {
+		// Some keys and values are not encodeable as line protocol, such as
+		// those with a trailing '\' or empty strings.
+		if len(tagKeys[i]) == 0 || len(tagVals[i]) == 0 || tagKeys[i][len(tagKeys[i])-1] == byte('\\') {
+			if e.failOnFieldError {
+				return 0, fmt.Errorf("invalid field: key \"%s\", val \"%s\"", tagKeys[i], tagVals[i])
+			}
+			continue
+		}
+		e.header = append(e.header, byte(','))
+		escapeBytes(&e.header, tagKeys[i])
+		e.header = append(e.header, byte('='))
+		escapeBytes(&e.header, tagVals[i])
+	}
+	e.header = append(e.header, byte(' '))
+	e.buildFooter(ts)
+
+	i := 0
+	totalWritten := 0
+	pairsLen := 0
+	firstField := true
+	for i := range fieldKeys {
+		e.pair = e.pair[:0]
+		key := fieldKeys[i]
+		if len(key) == 0 || key[len(key)-1] == byte('\\') {
+			if e.failOnFieldError {
+				return 0, &FieldError{"invalid field key"}
+			}
+			continue
+		}
+		escapeBytes(&e.pair, key)
+		// Some keys are not encodeable as line protocol, such as those with a
+		// trailing '\' or empty strings.
+		e.pair = append(e.pair, byte('='))
+		err := e.buildFieldVal(fieldVals[i])
+		if err != nil {
+			if e.failOnFieldError {
+				return 0, err
+			}
+			continue
+		}
+
+		bytesNeeded := len(e.header) + pairsLen + len(e.pair) + len(e.footer)
+
+		// Additional length needed for field separator `,`
+		if !firstField {
+			bytesNeeded++
+		}
+
+		if e.maxLineBytes > 0 && bytesNeeded > e.maxLineBytes {
+			// Need at least one field per line
+			if firstField {
+				return 0, ErrNeedMoreSpace
+			}
+
+			i, err = e.w.Write(e.footer)
+			if err != nil {
+				return 0, err
+			}
+			totalWritten += i
+
+			bytesNeeded = len(e.header) + len(e.pair) + len(e.footer)
+
+			if e.maxLineBytes > 0 && bytesNeeded > e.maxLineBytes {
+				return 0, ErrNeedMoreSpace
+			}
+
+			i, err = e.w.Write(e.header)
+			if err != nil {
+				return 0, err
+			}
+			totalWritten += i
+
+			i, err = e.w.Write(e.pair)
+			if err != nil {
+				return 0, err
+			}
+			totalWritten += i
+
+			pairsLen += len(e.pair)
+			firstField = false
+			continue
+		}
+
+		if firstField {
+			i, err = e.w.Write(e.header)
+			if err != nil {
+				return 0, err
+			}
+			totalWritten += i
+
+		} else {
+			i, err = e.w.Write(comma)
+			if err != nil {
+				return 0, err
+			}
+			totalWritten += i
+
+		}
+
+		e.w.Write(e.pair)
+
+		pairsLen += len(e.pair)
+		firstField = false
+	}
+
+	if firstField {
+		return 0, ErrNoFields
+	}
+	i, err := e.w.Write(e.footer)
+	if err != nil {
+		return 0, err
+	}
+	totalWritten += i
+	return totalWritten, nil
+}