Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add color to json output #132

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
120 changes: 91 additions & 29 deletions json.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"encoding/json"
"fmt"
"time"

"github.com/charmbracelet/lipgloss"
)

func (l *Logger) jsonFormatter(keyvals ...interface{}) {
jw := &jsonWriter{w: &l.b}
jw := &jsonWriter{w: &l.b, r: l.re, s: l.styles.Separator}
jw.start()

i := 0
Expand All @@ -33,87 +35,114 @@ func (l *Logger) jsonFormatterRoot(jw *jsonWriter, key, value any) {
switch key {
case TimestampKey:
if t, ok := value.(time.Time); ok {
jw.objectItem(TimestampKey, t.Format(l.timeFormat))
jw.objectItem(l.styles.Key, TimestampKey, l.styles.Timestamp, t.Format(l.timeFormat))
}
case LevelKey:
if level, ok := value.(Level); ok {
jw.objectItem(LevelKey, level.String())
ls, ok := l.styles.Levels[level]
if ok {
jw.objectItem(l.styles.Key, LevelKey, ls, level.String())
}
}
case CallerKey:
if caller, ok := value.(string); ok {
jw.objectItem(CallerKey, caller)
jw.objectItem(l.styles.Key, CallerKey, l.styles.Caller, caller)
}
case PrefixKey:
if prefix, ok := value.(string); ok {
jw.objectItem(PrefixKey, prefix)
jw.objectItem(l.styles.Key, PrefixKey, l.styles.Prefix, prefix)
}
case MessageKey:
if msg := value; msg != nil {
jw.objectItem(MessageKey, fmt.Sprint(msg))
jw.objectItem(l.styles.Key, MessageKey, l.styles.Message, fmt.Sprint(msg))
}
default:
l.jsonFormatterItem(jw, key, value)
l.jsonFormatterItem(jw, 0, l.styles.Key, key, l.styles.Value, value)
}
}

func (l *Logger) jsonFormatterItem(jw *jsonWriter, key, value any) {
switch k := key.(type) {
func (l *Logger) jsonFormatterItem(
jw *jsonWriter, d int, ks lipgloss.Style, anyKey any, vs lipgloss.Style, value any,
) {
var key string
switch k := anyKey.(type) {
case fmt.Stringer:
jw.objectKey(k.String())
key = k.String()
case error:
jw.objectKey(k.Error())
key = k.Error()
default:
jw.objectKey(fmt.Sprint(k))
key = fmt.Sprint(k)
}

// override styles based on root key
if d == 0 {
if s, ok := l.styles.Keys[key]; ok {
ks = s
}
if s, ok := l.styles.Values[key]; ok {
vs = s
}
}

jw.objectKey(ks, key)

switch v := value.(type) {
case error:
jw.objectValue(v.Error())
jw.objectValue(vs, v.Error())
case slogLogValuer:
l.writeSlogValue(jw, v.LogValue())
l.writeSlogValue(jw, d, ks, vs, v.LogValue())
case slogValue:
l.writeSlogValue(jw, v.Resolve())
l.writeSlogValue(jw, d, ks, vs, v.Resolve())
case fmt.Stringer:
jw.objectValue(v.String())
jw.objectValue(vs, v.String())
default:
jw.objectValue(v)
jw.objectValue(vs, v)
}
}

func (l *Logger) writeSlogValue(jw *jsonWriter, v slogValue) {
func (l *Logger) writeSlogValue(jw *jsonWriter, depth int, ks, vs lipgloss.Style, v slogValue) {
switch v.Kind() {
case slogKindGroup:
jw.start()
for _, attr := range v.Group() {
l.jsonFormatterItem(jw, attr.Key, attr.Value)
l.jsonFormatterItem(jw, depth+1, ks, attr.Key, vs, attr.Value)
}
jw.end()
default:
jw.objectValue(v.Any())
jw.objectValue(vs, v.Any())
}
}

type jsonWriter struct {
w *bytes.Buffer
r *lipgloss.Renderer
s lipgloss.Style
d int
}

func (w *jsonWriter) start() {
w.w.WriteRune('{')
objectStart := w.s.Renderer(w.r).Render("{")
w.w.WriteString(objectStart)
w.d = 0
}

func (w *jsonWriter) end() {
w.w.WriteRune('}')
objectEnd := w.s.Renderer(w.r).Render("}")
w.w.WriteString(objectEnd)
}

func (w *jsonWriter) objectItem(key string, value any) {
w.objectKey(key)
w.objectValue(value)
func (w *jsonWriter) objectItem(
ks lipgloss.Style, key string,
vs lipgloss.Style, value any,
) {
w.objectKey(ks, key)
w.objectValue(vs, value)
}

func (w *jsonWriter) objectKey(key string) {
func (w *jsonWriter) objectKey(s lipgloss.Style, key string) {
if w.d > 0 {
w.w.WriteRune(',')
itemSep := w.s.Renderer(w.r).Render(",")
w.w.WriteString(itemSep)
}
w.d++

Expand All @@ -123,16 +152,49 @@ func (w *jsonWriter) objectKey(key string) {
w.w.Truncate(pos)
w.w.WriteString(`"invalid key"`)
}
w.w.WriteRune(':')

// re-apply value with style
w.renderStyle(s, pos)

valSep := w.s.Renderer(w.r).Render(`:`)
w.w.WriteString(valSep)
}

func (w *jsonWriter) objectValue(value any) {
func (w *jsonWriter) objectValue(s lipgloss.Style, value any) {
pos := w.w.Len()
err := w.writeEncoded(value)
if err != nil {
w.w.Truncate(pos)
w.w.WriteString(`"invalid value"`)
}

// re-apply value with style
w.renderStyle(s, pos)
}

// renderStyle applies the given style to the string at the given position.
func (w *jsonWriter) renderStyle(st lipgloss.Style, pos int) {
s := w.w.String()[pos:]

// manually apply quotes
sep := ""
if len(s) > 2 && s[0] == '"' && s[len(s)-1] == '"' {
s = s[1 : len(s)-1] // apply style within quotes
sep = w.s.Renderer(w.r).Render(`"`)
} else if st.String() != "" {
sep = w.s.Renderer(w.r).Render(`"`)
}

// render with style
s = st.Renderer(w.r).Render(s)

// rewind
w.w.Truncate(pos)

// re-apply with colors
w.w.WriteString(sep)
w.w.WriteString(s)
w.w.WriteString(sep)
}

func (w *jsonWriter) writeEncoded(v any) error {
Expand Down
26 changes: 14 additions & 12 deletions json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"runtime"
"testing"

"github.com/charmbracelet/lipgloss"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -200,6 +201,7 @@ func TestJsonCustomKey(t *testing.T) {
}

func TestJsonWriter(t *testing.T) {
noStyle := lipgloss.NewStyle()
testCases := []struct {
name string
fn func(w *jsonWriter)
Expand All @@ -209,7 +211,7 @@ func TestJsonWriter(t *testing.T) {
"string",
func(w *jsonWriter) {
w.start()
w.objectItem("a", "value")
w.objectItem(noStyle, "a", noStyle, "value")
w.end()
},
`{"a":"value"}`,
Expand All @@ -218,7 +220,7 @@ func TestJsonWriter(t *testing.T) {
"int",
func(w *jsonWriter) {
w.start()
w.objectItem("a", 123)
w.objectItem(noStyle, "a", noStyle, 123)
w.end()
},
`{"a":123}`,
Expand All @@ -227,7 +229,7 @@ func TestJsonWriter(t *testing.T) {
"bytes",
func(w *jsonWriter) {
w.start()
w.objectItem("b", []byte{0x0, 0x1})
w.objectItem(noStyle, "b", noStyle, []byte{0x0, 0x1})
w.end()
},
`{"b":"AAE="}`,
Expand All @@ -244,8 +246,8 @@ func TestJsonWriter(t *testing.T) {
"multiple in asc order",
func(w *jsonWriter) {
w.start()
w.objectItem("a", "value")
w.objectItem("b", "some-other")
w.objectItem(noStyle, "a", noStyle, "value")
w.objectItem(noStyle, "b", noStyle, "some-other")
w.end()
},
`{"a":"value","b":"some-other"}`,
Expand All @@ -254,8 +256,8 @@ func TestJsonWriter(t *testing.T) {
"multiple in desc order",
func(w *jsonWriter) {
w.start()
w.objectItem("b", "some-other")
w.objectItem("a", "value")
w.objectItem(noStyle, "b", noStyle, "some-other")
w.objectItem(noStyle, "a", noStyle, "value")
w.end()
},
`{"b":"some-other","a":"value"}`,
Expand All @@ -264,7 +266,7 @@ func TestJsonWriter(t *testing.T) {
"depth",
func(w *jsonWriter) {
w.start()
w.objectItem("a", map[string]int{"b": 123})
w.objectItem(noStyle, "a", noStyle, map[string]int{"b": 123})
w.end()
},
`{"a":{"b":123}}`,
Expand All @@ -273,7 +275,7 @@ func TestJsonWriter(t *testing.T) {
"key contains reserved",
func(w *jsonWriter) {
w.start()
w.objectItem("a:\"b", "value")
w.objectItem(noStyle, "a:\"b", noStyle, "value")
w.end()
},
`{"a:\"b":"value"}`,
Expand All @@ -282,7 +284,7 @@ func TestJsonWriter(t *testing.T) {
"pointer",
func(w *jsonWriter) {
w.start()
w.objectItem("a", ptr("pointer"))
w.objectItem(noStyle, "a", noStyle, ptr("pointer"))
w.end()
},
`{"a":"pointer"}`,
Expand All @@ -291,7 +293,7 @@ func TestJsonWriter(t *testing.T) {
"double-pointer",
func(w *jsonWriter) {
w.start()
w.objectItem("a", ptr(ptr("pointer")))
w.objectItem(noStyle, "a", noStyle, ptr(ptr("pointer")))
w.end()
},
`{"a":"pointer"}`,
Expand All @@ -300,7 +302,7 @@ func TestJsonWriter(t *testing.T) {
"invalid",
func(w *jsonWriter) {
w.start()
w.objectItem("a", invalidJSON{})
w.objectItem(noStyle, "a", noStyle, invalidJSON{})
w.end()
},
`{"a":"invalid value"}`,
Expand Down
Loading