diff --git a/line.go b/line.go index e9447e6..4a8ea1f 100644 --- a/line.go +++ b/line.go @@ -45,19 +45,24 @@ func (l *LineWriter) WriteEvent(e *Event) { } buf.WriteRune('[') - buf.WriteString(e.Path.String()) + if e.Path != nil { + buf.WriteString(e.Path.String()) + } buf.WriteRune(']') buf.WriteRune(' ') if e.Tags == nil { - buf.WriteString("[] ") + buf.WriteString("[]") } else { buf.WriteRune('[') writeTags(buf, e.Tags) - buf.WriteString("] ") + buf.WriteRune(']') } - buf.WriteString(strings.ReplaceAll(e.Text, string('\n'), "\n")) + if e.Text != "" { + buf.WriteRune(' ') + buf.WriteString(strings.ReplaceAll(e.Text, string('\n'), "\n")) + } l.out.Lock() l.out.Write(buf.Bytes()) diff --git a/line_test.go b/line_test.go index 758535e..7822588 100644 --- a/line_test.go +++ b/line_test.go @@ -7,20 +7,129 @@ import ( ) func TestLineWriter(t *testing.T) { - var buf bytes.Buffer - w := NewLineWriter(&buf) - w.WriteEvent(&Event{ - Level: Debug, - Time: time.Now(), - Path: NewPath("alice").Child("bob").Child("carol"), - Text: "hey you farthead", - Tags: &Tags{ - key: "poop", - parent: &Tags{ - key: "num_poops", - value: 27, - }, - }, - }) - t.Error(buf.String()) + refTime := time.Date(2020, time.January, 13, 12, 26, 47, 999999, time.UTC) + + tests := []struct { + name string + event Event + line string + }{ + { + name: "empty event", + event: Event{}, + line: `0001-01-01T00:00:00Z d [] []`, + }, + { + name: "just a time", + event: Event{ + Time: refTime, + }, + line: `2020-01-13T12:26:47Z d [] []`, + }, + { + name: "root path only", + event: Event{ + Time: refTime, + Path: NewPath("root"), + }, + line: `2020-01-13T12:26:47Z d [root] []`, + }, + { + name: "child path", + event: Event{ + Time: refTime, + Path: NewPath("root").Child("kid"), + }, + line: `2020-01-13T12:26:47Z d [root/kid] []`, + }, + { + name: "another child path", + event: Event{ + Time: refTime, + Path: NewPath("root").Child("kid").Child("fart"), + }, + line: `2020-01-13T12:26:47Z d [root/kid/fart] []`, + }, + { + name: "a message", + event: Event{ + Time: refTime, + Path: NewPath("root").Child("kid").Child("fart"), + Text: "this is a message", + }, + line: `2020-01-13T12:26:47Z d [root/kid/fart] [] this is a message`, + }, + { + name: "a message with an empty tag", + event: Event{ + Time: refTime, + Path: NewPath("root").Child("kid").Child("fart"), + Text: "this is a message", + Tags: &Tags{key: "alert"}, + }, + line: `2020-01-13T12:26:47Z d [root/kid/fart] [alert] this is a message`, + }, + { + name: "a message with two empty tags", + event: Event{ + Time: refTime, + Path: NewPath("root").Child("kid").Child("fart"), + Text: "this is a message", + Tags: &Tags{ + key: "zombo-dot-com", + parent: &Tags{key: "alert"}, + }, + }, + line: `2020-01-13T12:26:47Z d [root/kid/fart] [alert+zombo-dot-com] this is a message`, + }, + { + name: "a message with an int tag", + event: Event{ + Time: refTime, + Path: NewPath("root").Child("kid").Child("fart"), + Text: "this is a message", + Tags: &Tags{ + key: "num-users", + value: 15, + }, + }, + line: `2020-01-13T12:26:47Z d [root/kid/fart] [num-users=15] this is a message`, + }, + { + name: "a message with a variety of tags", + event: Event{ + Time: refTime, + Path: NewPath("root").Child("kid").Child("fart"), + Text: "this is a message", + Tags: &Tags{ + key: "num-users", + value: 15, + parent: &Tags{ + key: "pi", + value: 3.14, + parent: &Tags{ + key: "request-id", + value: "b49d31c7-d3bb-4bd3-96fe-34e7c7d2b0a4", + }, + }, + }, + }, + line: `2020-01-13T12:26:47Z d [root/kid/fart] [request-id=b49d31c7-d3bb-4bd3-96fe-34e7c7d2b0a4+pi=3.14+num-users=15] this is a message`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var buf bytes.Buffer + w := NewLineWriter(&buf) + w.WriteEvent(&test.event) + line := buf.String() + if line != test.line { + t.Log("expected line does not match observed line") + t.Logf("expected line: '%s'", test.line) + t.Logf("observed line: '%s'", line) + t.Fail() + } + }) + } } diff --git a/tag.go b/tag.go index 73e7a23..6e8c3e8 100644 --- a/tag.go +++ b/tag.go @@ -1,23 +1,39 @@ package blammo +// Tags represent event metadata. Tags is an ordered collection of key-value +// pairs where every key is a string. Values are optional, and may be one of +// int, float64, and string. The same key may appear multiple times. +// +// Tags is internally represented by a singly linked list of key-value pairs. A +// Tags struct is immutable once created. The only thing that can be done is to +// create new nodes that point to old nodes. This allows us to continually +// recycle tag nodes that are widely used across an application. type Tags struct { key string value interface{} parent *Tags } +// Tag creates a new Tags struct having the given key as its final key, with no +// associated value. All existing keys continue to exist. func (t *Tags) Tag(key string) *Tags { return &Tags{key: key, parent: t} } +// TagInt creates a new Tags struct having the given key-value pair as the +// final key-value pair. All existing key-value pairs continue to exist. func (t *Tags) TagInt(key string, v int) *Tags { return &Tags{key: key, value: v, parent: t} } +// TagString creates a new Tags struct having the given key-value pair as the +// final key-value pair. All existing key-value pairs continue to exist. func (t *Tags) TagString(key, v string) *Tags { return &Tags{key: key, value: v, parent: t} } +// TagFloat creates a new Tags struct having the given key-value pair as the +// final key-value pair. All existing key-value pairs continue to exist. func (t *Tags) TagFloat(key string, v float64) *Tags { return &Tags{key: key, value: v, parent: t} }