Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ctx/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,21 @@ func Interface(k string, v any) logger.Field {
}
}

// Group returns a field that writes all the given fields inside a named group.
func Group(name string, fields ...logger.Field) logger.Field {
return func(e *logger.Event) {
if len(fields) == 0 {
return
}

e.OpenGroup(name)
for _, f := range fields {
f(e)
}
e.CloseGroup()
}
}

// Span represents an open telemetry span.
type Span interface {
IsRecording() bool
Expand Down
39 changes: 26 additions & 13 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,43 @@ var eventPool = &sync.Pool{

// Event is a log event.
type Event struct {
fmtr Formatter
buf *bytes.Buffer
fmtr Formatter
buf *bytes.Buffer
prefix []byte
}

func newEvent(fmtr Formatter) *Event {
e := eventPool.Get().(*Event)
e.fmtr = fmtr
e.buf.Reset()
e.prefix = e.prefix[:0]
return e
}

func putEvent(e *Event) {
eventPool.Put(e)
}

// OpenGroup opens a named group in the event output. Every OpenGroup call
// must be paired with a matching CloseGroup call.
func (e *Event) OpenGroup(name string) {
e.prefix = e.fmtr.AppendGroupStart(e.buf, e.prefix, name)
}

// CloseGroup closes the most recently opened group.
func (e *Event) CloseGroup() {
e.prefix = e.fmtr.AppendGroupEnd(e.buf, e.prefix)
}

// AppendString appends a string to the event.
func (e *Event) AppendString(k, s string) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendString(e.buf, s)
}

// AppendStrings appends strings to the event.
func (e *Event) AppendStrings(k string, s []string) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendArrayStart(e.buf)
for i, ss := range s {
if i > 0 {
Expand All @@ -53,7 +66,7 @@ func (e *Event) AppendStrings(k string, s []string) {

// AppendBytes appends bytes to the event.
func (e *Event) AppendBytes(k string, p []byte) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendArrayStart(e.buf)
for i, b := range p {
if i > 0 {
Expand All @@ -66,19 +79,19 @@ func (e *Event) AppendBytes(k string, p []byte) {

// AppendBool appends a bool to the event.
func (e *Event) AppendBool(k string, b bool) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendBool(e.buf, b)
}

// AppendInt appends an int to the event.
func (e *Event) AppendInt(k string, i int64) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendInt(e.buf, i)
}

// AppendInts appends ints to the event.
func (e *Event) AppendInts(k string, a []int) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendArrayStart(e.buf)
for i, ii := range a {
if i > 0 {
Expand All @@ -91,30 +104,30 @@ func (e *Event) AppendInts(k string, a []int) {

// AppendUint appends a uint to the event.
func (e *Event) AppendUint(k string, i uint64) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendUint(e.buf, i)
}

// AppendFloat appends a float to the event.
func (e *Event) AppendFloat(k string, f float64) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendFloat(e.buf, f)
}

// AppendTime appends a time to the event.
func (e *Event) AppendTime(k string, d time.Time) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendTime(e.buf, d)
}

// AppendDuration appends a duration to the event.
func (e *Event) AppendDuration(k string, d time.Duration) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendDuration(e.buf, d)
}

// AppendInterface appends a interface to the event.
func (e *Event) AppendInterface(k string, v any) {
e.fmtr.AppendKey(e.buf, k)
e.fmtr.AppendKey(e.buf, e.prefix, k)
e.fmtr.AppendInterface(e.buf, v)
}
67 changes: 62 additions & 5 deletions format.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ type Formatter interface {
AppendArrayStart(buf *bytes.Buffer)
AppendArraySep(buf *bytes.Buffer)
AppendArrayEnd(buf *bytes.Buffer)
AppendKey(buf *bytes.Buffer, key string)
AppendKey(buf *bytes.Buffer, prefix []byte, key string)
AppendGroupStart(buf *bytes.Buffer, prefix []byte, name string) []byte
AppendGroupEnd(buf *bytes.Buffer, prefix []byte) []byte
AppendString(buf *bytes.Buffer, s string)
AppendBool(buf *bytes.Buffer, b bool)
AppendInt(buf *bytes.Buffer, i int64)
Expand Down Expand Up @@ -86,12 +88,32 @@ func (j *json) AppendArrayEnd(buf *bytes.Buffer) {
buf.WriteByte(']')
}

func (j *json) AppendKey(buf *bytes.Buffer, key string) {
buf.WriteString(`,"`)
func (j *json) AppendKey(buf *bytes.Buffer, _ []byte, key string) {
if buf.Peek() != '{' {
buf.WriteString(`,"`)
} else {
buf.WriteByte('"')
}
buf.WriteString(key)
buf.WriteString(`":`)
}

func (j *json) AppendGroupStart(buf *bytes.Buffer, prefix []byte, name string) []byte {
if buf.Peek() != '{' {
buf.WriteString(`,"`)
} else {
buf.WriteByte('"')
}
buf.WriteString(name)
buf.WriteString(`":{`)
return prefix
}

func (j *json) AppendGroupEnd(buf *bytes.Buffer, prefix []byte) []byte {
buf.WriteByte('}')
return prefix
}

func (j *json) AppendString(buf *bytes.Buffer, s string) {
appendString(buf, s, true)
}
Expand Down Expand Up @@ -184,12 +206,24 @@ func (l *logfmt) AppendArraySep(buf *bytes.Buffer) {

func (l *logfmt) AppendArrayEnd(_ *bytes.Buffer) {}

func (l *logfmt) AppendKey(buf *bytes.Buffer, key string) {
func (l *logfmt) AppendKey(buf *bytes.Buffer, prefix []byte, key string) {
buf.WriteByte(' ')
if len(prefix) > 0 {
buf.Write(prefix)
}
buf.WriteString(key)
buf.WriteByte('=')
}

func (l *logfmt) AppendGroupStart(_ *bytes.Buffer, prefix []byte, name string) []byte {
prefix = append(prefix, name...)
return append(prefix, '.')
}

func (l *logfmt) AppendGroupEnd(_ *bytes.Buffer, prefix []byte) []byte {
return trimLastGroup(prefix)
}

func (l *logfmt) AppendString(buf *bytes.Buffer, s string) {
appendString(buf, s, l.needsQuote(s))
}
Expand Down Expand Up @@ -327,7 +361,7 @@ func (c *console) AppendArraySep(buf *bytes.Buffer) {

func (c *console) AppendArrayEnd(_ *bytes.Buffer) {}

func (c *console) AppendKey(buf *bytes.Buffer, key string) {
func (c *console) AppendKey(buf *bytes.Buffer, prefix []byte, key string) {
buf.WriteByte(' ')

col := newColor(colorCyan)
Expand All @@ -336,11 +370,23 @@ func (c *console) AppendKey(buf *bytes.Buffer, key string) {
}

withColor(col, buf, func() {
if len(prefix) > 0 {
buf.Write(prefix)
}
buf.WriteString(key)
buf.WriteByte('=')
})
}

func (c *console) AppendGroupStart(_ *bytes.Buffer, prefix []byte, name string) []byte {
prefix = append(prefix, name...)
return append(prefix, '.')
}

func (c *console) AppendGroupEnd(_ *bytes.Buffer, prefix []byte) []byte {
return trimLastGroup(prefix)
}

func (c *console) AppendString(buf *bytes.Buffer, s string) {
appendString(buf, s, false)
}
Expand Down Expand Up @@ -456,3 +502,14 @@ func tryAddASCII(buf *bytes.Buffer, b byte) bool {
}
return true
}

// trimLastGroup removes the last "name." segment from prefix by scanning
// backwards for the dot preceding the final segment.
func trimLastGroup(prefix []byte) []byte {
for i := len(prefix) - 2; i >= 0; i-- {
if prefix[i] == '.' {
return prefix[:i+1]
}
}
return prefix[:0]
}
95 changes: 92 additions & 3 deletions format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestJsonFormat(t *testing.T) {
buf := bytes.NewBuffer(512)
fmtr.AppendBeginMarker(buf)
fmtr.WriteMessage(buf, time.Unix(123, 0).UTC(), logger.Error, "some message")
fmtr.AppendKey(buf, "error")
fmtr.AppendKey(buf, nil, "error")
fmtr.AppendString(buf, "some error")
fmtr.AppendEndMarker(buf)
fmtr.AppendLineBreak(buf)
Expand Down Expand Up @@ -171,13 +171,51 @@ func TestJsonFormat_Interface(t *testing.T) {
assert.Equal(t, `"{Name:test}"null`, string(buf.Bytes()))
}

func TestJsonFormat_Group(t *testing.T) {
fmtr := logger.JSONFormat()

buf := bytes.NewBuffer(512)
fmtr.AppendBeginMarker(buf)
fmtr.WriteMessage(buf, time.Time{}, logger.Info, "msg")
prefix := fmtr.AppendGroupStart(buf, nil, "db")
fmtr.AppendKey(buf, prefix, "host")
fmtr.AppendString(buf, "localhost")
fmtr.AppendKey(buf, prefix, "port")
fmtr.AppendInt(buf, 5432)
fmtr.AppendGroupEnd(buf, prefix)
fmtr.AppendKey(buf, nil, "other")
fmtr.AppendString(buf, "val")
fmtr.AppendEndMarker(buf)

want := `{"lvl":"info","msg":"msg","db":{"host":"localhost","port":5432},"other":"val"}`
assert.Equal(t, want, string(buf.Bytes()))
}

func TestJsonFormat_NestedGroup(t *testing.T) {
fmtr := logger.JSONFormat()

buf := bytes.NewBuffer(512)
fmtr.AppendBeginMarker(buf)
fmtr.WriteMessage(buf, time.Time{}, logger.Info, "msg")
outer := fmtr.AppendGroupStart(buf, nil, "outer")
inner := fmtr.AppendGroupStart(buf, outer, "inner")
fmtr.AppendKey(buf, inner, "k")
fmtr.AppendString(buf, "v")
fmtr.AppendGroupEnd(buf, inner)
fmtr.AppendGroupEnd(buf, outer)
fmtr.AppendEndMarker(buf)

want := `{"lvl":"info","msg":"msg","outer":{"inner":{"k":"v"}}}`
assert.Equal(t, want, string(buf.Bytes()))
}

func TestLogfmtFormat(t *testing.T) {
fmtr := logger.LogfmtFormat()

buf := bytes.NewBuffer(512)
fmtr.AppendBeginMarker(buf)
fmtr.WriteMessage(buf, time.Unix(123, 0).UTC(), logger.Error, "some message")
fmtr.AppendKey(buf, "error")
fmtr.AppendKey(buf, nil, "error")
fmtr.AppendString(buf, "some error")
fmtr.AppendEndMarker(buf)
fmtr.AppendLineBreak(buf)
Expand Down Expand Up @@ -333,13 +371,48 @@ func TestLogfmtFormat_Interface(t *testing.T) {
assert.Equal(t, `{Name:test}`, string(buf.Bytes()))
}

func TestLogfmtFormat_Group(t *testing.T) {
fmtr := logger.LogfmtFormat()

buf := bytes.NewBuffer(512)
fmtr.WriteMessage(buf, time.Time{}, logger.Info, "msg")
prefix := fmtr.AppendGroupStart(buf, nil, "db")
fmtr.AppendKey(buf, prefix, "host")
fmtr.AppendString(buf, "localhost")
fmtr.AppendKey(buf, prefix, "port")
fmtr.AppendInt(buf, 5432)
prefix = fmtr.AppendGroupEnd(buf, prefix)
fmtr.AppendKey(buf, prefix, "other")
fmtr.AppendString(buf, "val")

want := `lvl=info msg=msg db.host=localhost db.port=5432 other=val`
assert.Equal(t, want, string(buf.Bytes()))
}

func TestLogfmtFormat_NestedGroup(t *testing.T) {
fmtr := logger.LogfmtFormat()

buf := bytes.NewBuffer(512)
fmtr.WriteMessage(buf, time.Time{}, logger.Info, "msg")
outer := fmtr.AppendGroupStart(buf, nil, "a")
inner := fmtr.AppendGroupStart(buf, outer, "b")
fmtr.AppendKey(buf, inner, "k")
fmtr.AppendString(buf, "v")
outer = fmtr.AppendGroupEnd(buf, inner)
fmtr.AppendKey(buf, outer, "x")
fmtr.AppendString(buf, "y")

want := `lvl=info msg=msg a.b.k=v a.x=y`
assert.Equal(t, want, string(buf.Bytes()))
}

func TestConsoleFormat(t *testing.T) {
fmtr := logger.ConsoleFormat()

buf := bytes.NewBuffer(512)
fmtr.AppendBeginMarker(buf)
fmtr.WriteMessage(buf, time.Unix(123, 0).UTC(), logger.Error, "some message")
fmtr.AppendKey(buf, "error")
fmtr.AppendKey(buf, nil, "error")
fmtr.AppendString(buf, "some error")
fmtr.AppendEndMarker(buf)
fmtr.AppendLineBreak(buf)
Expand Down Expand Up @@ -482,3 +555,19 @@ func TestConsoleFormat_Interface(t *testing.T) {

assert.Equal(t, `{Name:test}`, string(buf.Bytes()))
}

func TestConsoleFormat_Group(t *testing.T) {
fmtr := logger.ConsoleFormat()

buf := bytes.NewBuffer(512)
fmtr.WriteMessage(buf, time.Time{}, logger.Info, "msg")
prefix := fmtr.AppendGroupStart(buf, nil, "db")
fmtr.AppendKey(buf, prefix, "host")
fmtr.AppendString(buf, "localhost")
prefix = fmtr.AppendGroupEnd(buf, prefix)
fmtr.AppendKey(buf, prefix, "other")
fmtr.AppendString(buf, "val")

want := "\x1b[32mINFO\x1b[0m msg \x1b[36mdb.host=\x1b[0mlocalhost \x1b[36mother=\x1b[0mval"
assert.Equal(t, want, string(buf.Bytes()))
}
Loading
Loading