Fix several influx parser issues (#5484)

- Add line/column position
- Allow handlers to return errors
- Fix tag value escaping
- Allow newline in string fields
This commit is contained in:
Daniel Nelson 2019-02-26 10:48:41 -08:00 committed by GitHub
parent 8da6846e53
commit 04f3c4321c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 25417 additions and 17316 deletions

View File

@ -2,16 +2,17 @@ package influx
import ( import (
"bytes" "bytes"
"errors"
"strconv"
"time" "time"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/metric" "github.com/influxdata/telegraf/metric"
"github.com/prometheus/common/log"
) )
type MetricHandler struct { type MetricHandler struct {
builder *metric.Builder builder *metric.Builder
metrics []telegraf.Metric err error
precision time.Duration precision time.Duration
} }
@ -32,75 +33,88 @@ func (h *MetricHandler) SetTimePrecision(precision time.Duration) {
} }
func (h *MetricHandler) Metric() (telegraf.Metric, error) { func (h *MetricHandler) Metric() (telegraf.Metric, error) {
return h.builder.Metric() m, err := h.builder.Metric()
h.builder.Reset()
return m, err
} }
func (h *MetricHandler) SetMeasurement(name []byte) { func (h *MetricHandler) SetMeasurement(name []byte) error {
h.builder.SetName(nameUnescape(name)) h.builder.SetName(nameUnescape(name))
return nil
} }
func (h *MetricHandler) AddTag(key []byte, value []byte) { func (h *MetricHandler) AddTag(key []byte, value []byte) error {
tk := unescape(key) tk := unescape(key)
tv := unescape(value) tv := unescape(value)
h.builder.AddTag(tk, tv) h.builder.AddTag(tk, tv)
return nil
} }
func (h *MetricHandler) AddInt(key []byte, value []byte) { func (h *MetricHandler) AddInt(key []byte, value []byte) error {
fk := unescape(key) fk := unescape(key)
fv, err := parseIntBytes(bytes.TrimSuffix(value, []byte("i")), 10, 64) fv, err := parseIntBytes(bytes.TrimSuffix(value, []byte("i")), 10, 64)
if err != nil { if err != nil {
log.Errorf("E! Received unparseable int value: %q: %v", value, err) if numerr, ok := err.(*strconv.NumError); ok {
return return numerr.Err
}
return err
} }
h.builder.AddField(fk, fv) h.builder.AddField(fk, fv)
return nil
} }
func (h *MetricHandler) AddUint(key []byte, value []byte) { func (h *MetricHandler) AddUint(key []byte, value []byte) error {
fk := unescape(key) fk := unescape(key)
fv, err := parseUintBytes(bytes.TrimSuffix(value, []byte("u")), 10, 64) fv, err := parseUintBytes(bytes.TrimSuffix(value, []byte("u")), 10, 64)
if err != nil { if err != nil {
log.Errorf("E! Received unparseable uint value: %q: %v", value, err) if numerr, ok := err.(*strconv.NumError); ok {
return return numerr.Err
}
return err
} }
h.builder.AddField(fk, fv) h.builder.AddField(fk, fv)
return nil
} }
func (h *MetricHandler) AddFloat(key []byte, value []byte) { func (h *MetricHandler) AddFloat(key []byte, value []byte) error {
fk := unescape(key) fk := unescape(key)
fv, err := parseFloatBytes(value, 64) fv, err := parseFloatBytes(value, 64)
if err != nil { if err != nil {
log.Errorf("E! Received unparseable float value: %q: %v", value, err) if numerr, ok := err.(*strconv.NumError); ok {
return return numerr.Err
}
return err
} }
h.builder.AddField(fk, fv) h.builder.AddField(fk, fv)
return nil
} }
func (h *MetricHandler) AddString(key []byte, value []byte) { func (h *MetricHandler) AddString(key []byte, value []byte) error {
fk := unescape(key) fk := unescape(key)
fv := stringFieldUnescape(value) fv := stringFieldUnescape(value)
h.builder.AddField(fk, fv) h.builder.AddField(fk, fv)
return nil
} }
func (h *MetricHandler) AddBool(key []byte, value []byte) { func (h *MetricHandler) AddBool(key []byte, value []byte) error {
fk := unescape(key) fk := unescape(key)
fv, err := parseBoolBytes(value) fv, err := parseBoolBytes(value)
if err != nil { if err != nil {
log.Errorf("E! Received unparseable boolean value: %q: %v", value, err) return errors.New("unparseable bool")
return
} }
h.builder.AddField(fk, fv) h.builder.AddField(fk, fv)
return nil
} }
func (h *MetricHandler) SetTimestamp(tm []byte) { func (h *MetricHandler) SetTimestamp(tm []byte) error {
v, err := parseIntBytes(tm, 10, 64) v, err := parseIntBytes(tm, 10, 64)
if err != nil { if err != nil {
log.Errorf("E! Received unparseable timestamp: %q: %v", tm, err) if numerr, ok := err.(*strconv.NumError); ok {
return return numerr.Err
}
return err
} }
ns := v * int64(h.precision) ns := v * int64(h.precision)
h.builder.SetTime(time.Unix(0, ns)) h.builder.SetTime(time.Unix(0, ns))
} return nil
func (h *MetricHandler) Reset() {
h.builder.Reset()
} }

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ var (
ErrTagParse = errors.New("expected tag") ErrTagParse = errors.New("expected tag")
ErrTimestampParse = errors.New("expected timestamp") ErrTimestampParse = errors.New("expected timestamp")
ErrParse = errors.New("parse error") ErrParse = errors.New("parse error")
EOF = errors.New("EOF")
) )
%%{ %%{
@ -19,58 +20,67 @@ action begin {
m.pb = m.p m.pb = m.p
} }
action yield {
yield = true
fnext align;
fbreak;
}
action name_error { action name_error {
m.err = ErrNameParse err = ErrNameParse
fhold; fhold;
fnext discard_line; fnext discard_line;
fbreak; fbreak;
} }
action field_error { action field_error {
m.err = ErrFieldParse err = ErrFieldParse
fhold; fhold;
fnext discard_line; fnext discard_line;
fbreak; fbreak;
} }
action tagset_error { action tagset_error {
m.err = ErrTagParse err = ErrTagParse
fhold; fhold;
fnext discard_line; fnext discard_line;
fbreak; fbreak;
} }
action timestamp_error { action timestamp_error {
m.err = ErrTimestampParse err = ErrTimestampParse
fhold; fhold;
fnext discard_line; fnext discard_line;
fbreak; fbreak;
} }
action parse_error { action parse_error {
m.err = ErrParse err = ErrParse
fhold; fhold;
fnext discard_line; fnext discard_line;
fbreak; fbreak;
} }
action align_error {
err = ErrParse
fnext discard_line;
fbreak;
}
action hold_recover { action hold_recover {
fhold; fhold;
fgoto main; fgoto main;
} }
action discard { action goto_align {
fgoto align; fgoto align;
} }
action found_metric {
foundMetric = true
}
action name { action name {
m.handler.SetMeasurement(m.text()) err = m.handler.SetMeasurement(m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
} }
action tagkey { action tagkey {
@ -78,7 +88,12 @@ action tagkey {
} }
action tagvalue { action tagvalue {
m.handler.AddTag(key, m.text()) err = m.handler.AddTag(key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
} }
action fieldkey { action fieldkey {
@ -86,32 +101,76 @@ action fieldkey {
} }
action integer { action integer {
m.handler.AddInt(key, m.text()) err = m.handler.AddInt(key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
} }
action unsigned { action unsigned {
m.handler.AddUint(key, m.text()) err = m.handler.AddUint(key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
} }
action float { action float {
m.handler.AddFloat(key, m.text()) err = m.handler.AddFloat(key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
} }
action bool { action bool {
m.handler.AddBool(key, m.text()) err = m.handler.AddBool(key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
} }
action string { action string {
m.handler.AddString(key, m.text()) err = m.handler.AddString(key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
} }
action timestamp { action timestamp {
m.handler.SetTimestamp(m.text()) 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 {
fnext align;
fbreak;
} }
ws = ws =
[\t\v\f ]; [\t\v\f ];
newline =
'\r'? '\n' %to(incr_newline);
non_zero_digit = non_zero_digit =
[1-9]; [1-9];
@ -155,7 +214,7 @@ fieldbool =
(true | false) >begin %bool; (true | false) >begin %bool;
fieldstringchar = fieldstringchar =
[^\n\f\r\\"] | '\\' [\\"]; [^\f\r\n\\"] | '\\' [\\"] | newline;
fieldstring = fieldstring =
fieldstringchar* >begin %string; fieldstringchar* >begin %string;
@ -172,16 +231,16 @@ fieldset =
field ( ',' field )*; field ( ',' field )*;
tagchar = tagchar =
[^\t\n\f\r ,=\\] | ( '\\' [^\t\n\f\r] ); [^\t\n\f\r ,=\\] | ( '\\' [^\t\n\f\r\\] ) | '\\\\' %to{ fhold; };
tagkey = tagkey =
tagchar+ >begin %tagkey; tagchar+ >begin %tagkey;
tagvalue = tagvalue =
tagchar+ >begin %tagvalue; tagchar+ >begin %eof(tagvalue) %tagvalue;
tagset = tagset =
(',' (tagkey '=' tagvalue) $err(tagset_error))*; ((',' tagkey '=' tagvalue) $err(tagset_error))*;
measurement_chars = measurement_chars =
[^\t\n\f\r ,\\] | ( '\\' [^\t\n\f\r] ); [^\t\n\f\r ,\\] | ( '\\' [^\t\n\f\r] );
@ -190,52 +249,71 @@ measurement_start =
measurement_chars - '#'; measurement_chars - '#';
measurement = measurement =
(measurement_start measurement_chars*) >begin %name; (measurement_start measurement_chars*) >begin %eof(name) %name;
newline = eol_break =
[\r\n]; newline %to(eol)
;
comment = metric =
'#' (any -- newline)* newline; measurement >err(name_error)
eol =
ws* newline? >yield %eof(yield);
line =
measurement
tagset tagset
(ws+ fieldset) $err(field_error) ws+ fieldset $err(field_error)
(ws+ timestamp)? $err(timestamp_error) (ws+ timestamp)? $err(timestamp_error)
eol; ;
# The main machine parses a single line of line protocol. line_with_term =
main := line $err(parse_error); ws* metric ws* eol_break
;
line_without_term =
ws* metric ws*
;
main :=
(line_with_term*
(line_with_term | line_without_term?)
) >found_metric
;
# The discard_line machine discards the current line. Useful for recovering # The discard_line machine discards the current line. Useful for recovering
# on the next line when an error occurs. # on the next line when an error occurs.
discard_line := discard_line :=
(any - newline)* newline @discard; (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 # 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 # is used to skip over whitespace and comments, keeping this logic out of the
# main machine. # 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 := align :=
(space* comment)* space* measurement_start @hold_recover %eof(yield); (emptyline | commentline | ws+)* %err(hold_recover);
series := measurement tagset $err(parse_error) eol; # Series is a machine for matching measurement+tagset
series :=
(measurement >err(name_error) tagset eol_break?)
>found_metric
;
}%% }%%
%% write data; %% write data;
type Handler interface { type Handler interface {
SetMeasurement(name []byte) SetMeasurement(name []byte) error
AddTag(key []byte, value []byte) AddTag(key []byte, value []byte) error
AddInt(key []byte, value []byte) AddInt(key []byte, value []byte) error
AddUint(key []byte, value []byte) AddUint(key []byte, value []byte) error
AddFloat(key []byte, value []byte) AddFloat(key []byte, value []byte) error
AddString(key []byte, value []byte) AddString(key []byte, value []byte) error
AddBool(key []byte, value []byte) AddBool(key []byte, value []byte) error
SetTimestamp(tm []byte) SetTimestamp(tm []byte) error
} }
type machine struct { type machine struct {
@ -243,9 +321,10 @@ type machine struct {
cs int cs int
p, pe, eof int p, pe, eof int
pb int pb int
lineno int
sol int
handler Handler handler Handler
initState int initState int
err error
} }
func NewMachine(handler Handler) *machine { func NewMachine(handler Handler) *machine {
@ -256,6 +335,7 @@ func NewMachine(handler Handler) *machine {
%% access m.; %% access m.;
%% variable p m.p; %% variable p m.p;
%% variable cs m.cs;
%% variable pe m.pe; %% variable pe m.pe;
%% variable eof m.eof; %% variable eof m.eof;
%% variable data m.data; %% variable data m.data;
@ -284,55 +364,76 @@ func (m *machine) SetData(data []byte) {
m.data = data m.data = data
m.p = 0 m.p = 0
m.pb = 0 m.pb = 0
m.lineno = 1
m.sol = 0
m.pe = len(data) m.pe = len(data)
m.eof = len(data) m.eof = len(data)
m.err = nil
%% write init; %% write init;
m.cs = m.initState m.cs = m.initState
} }
// ParseLine parses a line of input and returns true if more data can be // Next parses the next metric line and returns nil if it was successfully
// parsed. // processed. If the line contains a syntax error an error is returned,
func (m *machine) ParseLine() bool { // otherwise if the end of file is reached before finding a metric line then
if m.data == nil || m.p >= m.pe { // EOF is returned.
m.err = nil func (m *machine) Next() error {
return false if m.p == m.pe && m.pe == m.eof {
return EOF
} }
m.err = nil var err error
var key []byte var key []byte
var yield bool foundMetric := false
%% write exec; %% write exec;
// Even if there was an error, return true. On the next call to this if err != nil {
// function we will attempt to scan to the next line of input and recover. return err
if m.err != nil {
return true
} }
// Don't check the error state in the case that we just yielded, because // This would indicate an error in the machine that was reported with a
// the yield indicates we just completed parsing a line. // more specific error. We return a generic error but this should
if !yield && m.cs == LineProtocol_error { // possibly be a panic.
m.err = ErrParse if m.cs == %%{ write error; }%% {
return true m.cs = LineProtocol_en_discard_line
return ErrParse
} }
return true // 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 !foundMetric && m.p == m.pe && m.pe == m.eof {
return EOF
}
return nil
} }
// Err returns the error that occurred on the last call to ParseLine. If the // Position returns the current byte offset into the data.
// result is nil, then the line was parsed successfully.
func (m *machine) Err() error {
return m.err
}
// Position returns the current position into the input.
func (m *machine) Position() int { func (m *machine) Position() int {
return m.p 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 { func (m *machine) text() []byte {
return m.data[m.pb:m.p] return m.data[m.pb:m.p]
} }

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ package influx
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"sync" "sync"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
@ -18,16 +19,23 @@ var (
type ParseError struct { type ParseError struct {
Offset int Offset int
LineOffset int
LineNumber int
Column int
msg string msg string
buf string buf string
} }
func (e *ParseError) Error() string { func (e *ParseError) Error() string {
buffer := e.buf buffer := e.buf[e.LineOffset:]
eol := strings.IndexAny(buffer, "\r\n")
if eol >= 0 {
buffer = buffer[:eol]
}
if len(buffer) > maxErrorBufferSize { if len(buffer) > maxErrorBufferSize {
buffer = buffer[:maxErrorBufferSize] + "..." buffer = buffer[:maxErrorBufferSize] + "..."
} }
return fmt.Sprintf("metric parse error: %s at offset %d: %q", e.msg, e.Offset, buffer) return fmt.Sprintf("metric parse error: %s at %d:%d: %q", e.msg, e.LineNumber, e.Column, buffer)
} }
type Parser struct { type Parser struct {
@ -60,12 +68,18 @@ func (p *Parser) Parse(input []byte) ([]telegraf.Metric, error) {
metrics := make([]telegraf.Metric, 0) metrics := make([]telegraf.Metric, 0)
p.machine.SetData(input) p.machine.SetData(input)
for p.machine.ParseLine() { for {
err := p.machine.Err() err := p.machine.Next()
if err == EOF {
break
}
if err != nil { if err != nil {
p.handler.Reset()
return nil, &ParseError{ return nil, &ParseError{
Offset: p.machine.Position(), Offset: p.machine.Position(),
LineOffset: p.machine.LineOffset(),
LineNumber: p.machine.LineNumber(),
Column: p.machine.Column(),
msg: err.Error(), msg: err.Error(),
buf: string(input), buf: string(input),
} }
@ -75,7 +89,11 @@ func (p *Parser) Parse(input []byte) ([]telegraf.Metric, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
p.handler.Reset()
if metric == nil {
continue
}
metrics = append(metrics, metric) metrics = append(metrics, metric)
} }
@ -84,7 +102,7 @@ func (p *Parser) Parse(input []byte) ([]telegraf.Metric, error) {
} }
func (p *Parser) ParseLine(line string) (telegraf.Metric, error) { func (p *Parser) ParseLine(line string) (telegraf.Metric, error) {
metrics, err := p.Parse([]byte(line + "\n")) metrics, err := p.Parse([]byte(line))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,8 @@
package influx package influx
import ( import (
"strconv"
"strings"
"testing" "testing"
"time" "time"
@ -173,6 +175,63 @@ var ptests = []struct {
}, },
err: nil, err: nil,
}, },
{
name: "tag value escape space",
input: []byte(`cpu,host=two\ words value=42`),
metrics: []telegraf.Metric{
Metric(
metric.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: []telegraf.Metric{
Metric(
metric.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: []telegraf.Metric{
Metric(
metric.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", name: "field key escape not escapable",
input: []byte(`cpu va\lue=42`), input: []byte(`cpu va\lue=42`),
@ -259,19 +318,16 @@ var ptests = []struct {
err: nil, err: nil,
}, },
{ {
name: "field int overflow dropped", name: "field int overflow",
input: []byte("cpu value=9223372036854775808i"), input: []byte("cpu value=9223372036854775808i"),
metrics: []telegraf.Metric{ metrics: nil,
Metric( err: &ParseError{
metric.New( Offset: 30,
"cpu", LineNumber: 1,
map[string]string{}, Column: 31,
map[string]interface{}{}, msg: strconv.ErrRange.Error(),
time.Unix(42, 0), buf: "cpu value=9223372036854775808i",
),
),
}, },
err: nil,
}, },
{ {
name: "field int max value", name: "field int max value",
@ -308,19 +364,16 @@ var ptests = []struct {
err: nil, err: nil,
}, },
{ {
name: "field uint overflow dropped", name: "field uint overflow",
input: []byte("cpu value=18446744073709551616u"), input: []byte("cpu value=18446744073709551616u"),
metrics: []telegraf.Metric{ metrics: nil,
Metric( err: &ParseError{
metric.New( Offset: 31,
"cpu", LineNumber: 1,
map[string]string{}, Column: 32,
map[string]interface{}{}, msg: strconv.ErrRange.Error(),
time.Unix(42, 0), buf: "cpu value=18446744073709551616u",
),
),
}, },
err: nil,
}, },
{ {
name: "field uint max value", name: "field uint max value",
@ -407,6 +460,23 @@ var ptests = []struct {
}, },
err: nil, err: nil,
}, },
{
name: "field string newline",
input: []byte("cpu value=\"4\n2\""),
metrics: []telegraf.Metric{
Metric(
metric.New(
"cpu",
map[string]string{},
map[string]interface{}{
"value": "4\n2",
},
time.Unix(42, 0),
),
),
},
err: nil,
},
{ {
name: "no timestamp", name: "no timestamp",
input: []byte("cpu value=42"), input: []byte("cpu value=42"),
@ -498,7 +568,9 @@ var ptests = []struct {
metrics: nil, metrics: nil,
err: &ParseError{ err: &ParseError{
Offset: 3, Offset: 3,
msg: ErrFieldParse.Error(), LineNumber: 1,
Column: 4,
msg: ErrTagParse.Error(),
buf: "cpu", buf: "cpu",
}, },
}, },
@ -668,6 +740,8 @@ func TestSeriesParser(t *testing.T) {
metrics: []telegraf.Metric{}, metrics: []telegraf.Metric{},
err: &ParseError{ err: &ParseError{
Offset: 6, Offset: 6,
LineNumber: 1,
Column: 7,
msg: ErrTagParse.Error(), msg: ErrTagParse.Error(),
buf: "cpu,a=", buf: "cpu,a=",
}, },
@ -696,3 +770,37 @@ func TestSeriesParser(t *testing.T) {
}) })
} }
} }
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] + "...\"",
},
}
for _, tt := range ptests {
t.Run(tt.name, func(t *testing.T) {
handler := NewMetricHandler()
parser := NewParser(handler)
_, err := parser.Parse(tt.input)
require.Equal(t, tt.errString, err.Error())
})
}
}

View File

@ -29,10 +29,6 @@ var (
) )
stringFieldEscaper = strings.NewReplacer( stringFieldEscaper = strings.NewReplacer(
"\t", `\t`,
"\n", `\n`,
"\f", `\f`,
"\r", `\r`,
`"`, `\"`, `"`, `\"`,
`\`, `\\`, `\`, `\\`,
) )

View File

@ -335,7 +335,7 @@ var tests = []struct {
time.Unix(0, 0), time.Unix(0, 0),
), ),
), ),
output: []byte("cpu value=\"x\\ny\" 0\n"), output: []byte("cpu value=\"x\ny\" 0\n"),
}, },
{ {
name: "need more space", name: "need more space",