Skip to content

Commit eca3e6a

Browse files
committed
feat: add json support
1 parent e4a44ba commit eca3e6a

5 files changed

Lines changed: 157 additions & 45 deletions

File tree

debugo.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package debugo
22

33
import (
4-
"fmt"
4+
"encoding/json"
55
"io"
66
"maps"
77
"sync"
@@ -31,33 +31,41 @@ func New(namespace string) *Debugger {
3131
return newDebugger(namespace)
3232
}
3333

34-
func (d *Debugger) Extend(namespace string) *Debugger {
34+
// With clones the debugger instance and adds a key-value pair to its fields (json serializeable)
35+
func (d *Debugger) With(key string, value any) *Debugger {
3536
d.mutex.Lock()
3637
defer d.mutex.Unlock()
3738

38-
n := newDebugger(d.namespace + ":" + namespace)
39-
n.color = d.color
40-
n.lastLog = d.lastLog
41-
n.output = d.output
42-
n.mutex = d.mutex
43-
return n
39+
n := *d
40+
maps.Copy(n.fields, d.fields)
41+
42+
if key == "" {
43+
key = "(empty)"
44+
}
45+
46+
if value == nil {
47+
value = nil
48+
}
49+
50+
if _, err := json.Marshal(value); err != nil {
51+
value = "(not serializable)"
52+
}
53+
54+
n.fields[key] = value
55+
return &n
4456
}
4557

46-
func (d *Debugger) With(kv ...any) *Debugger {
58+
// Extend creates a new debugger instance with an extended namespace
59+
func (d *Debugger) Extend(namespace string) *Debugger {
4760
d.mutex.Lock()
4861
defer d.mutex.Unlock()
4962

5063
n := *d
51-
maps.Copy(n.fields, d.fields)
52-
53-
for i := 0; i+1 < len(kv); i += 2 {
54-
key := fmt.Sprint(kv[i]) // ensures key is always a string
55-
n.fields[key] = kv[i+1]
56-
}
57-
64+
n.namespace = d.namespace + ":" + namespace
5865
return &n
5966
}
6067

68+
// SetOutput sets the output writer for the debugger instance
6169
func (d *Debugger) SetOutput(output io.Writer) {
6270
d.mutex.Lock()
6371
defer d.mutex.Unlock()

namespace.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,24 @@ func (d *Debugger) matchNamespace() bool {
4444
}
4545

4646
func matchPattern(namespace, pattern string) bool {
47-
// Handle the "optional" case with ":?"
4847
if strings.HasSuffix(pattern, ":?") {
4948
base := strings.TrimSuffix(pattern, ":?")
5049
// Match exactly the base or the base followed by anything
5150
regexPattern := "^" + regexp.QuoteMeta(base) + "(:.*)?$"
52-
re, _ := regexp.Compile(regexPattern) // can not fail due to QuoteMeta
51+
re, err := regexp.Compile(regexPattern)
52+
if err != nil {
53+
return false
54+
}
5355
return re.MatchString(namespace)
5456
}
5557

56-
// Replace '*' with '.*' for regex matching (.* matches any sequence of characters)
58+
// replace '*' with '.*' for regex matching (.* matches any sequence of characters)
5759
regexPattern := "^" + strings.ReplaceAll(pattern, "*", ".*") + "$"
5860

59-
// Compile the pattern into a regular expression
6061
re, err := regexp.Compile(regexPattern)
6162
if err != nil {
6263
return false
6364
}
6465

65-
// Check if the namespace matches the regular expression
6666
return re.MatchString(namespace)
6767
}

runtime.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import (
66
"sync"
77
)
88

9+
type Format string
10+
11+
const (
12+
Plain Format = "plain"
13+
JSON Format = "json"
14+
)
15+
916
type config struct {
1017
namespace string
1118
timestamp *Timestamp
@@ -14,6 +21,8 @@ type config struct {
1421

1522
useColors bool
1623

24+
format Format
25+
1726
mutex *sync.RWMutex
1827
}
1928

@@ -25,13 +34,31 @@ var runtime = &config{
2534

2635
useColors: true,
2736

37+
format: Plain,
38+
2839
mutex: &sync.RWMutex{},
2940
}
3041

3142
type Timestamp struct {
3243
Format string
3344
}
3445

46+
// SetFormat sets the global output format for debugging.
47+
func SetFormat(format Format) {
48+
runtime.mutex.Lock()
49+
defer runtime.mutex.Unlock()
50+
51+
runtime.format = format
52+
}
53+
54+
// GetFormat retrieves the current global output format for debugging.
55+
func GetFormat() Format {
56+
runtime.mutex.RLock()
57+
defer runtime.mutex.RUnlock()
58+
59+
return runtime.format
60+
}
61+
3562
// SetUseColors sets the global color usage for debugging.
3663
func SetUseColors(use bool) {
3764
runtime.mutex.Lock()

write.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package debugo
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"strings"
67
"time"
78
)
89

10+
type asJSON struct {
11+
Timestamp string `json:"timestamp,omitempty"`
12+
Namespace string `json:"namespace,omitempty"`
13+
Fields map[string]any `json:"fields,omitempty"`
14+
Message string `json:"message,omitempty"`
15+
ElapsedMs int64 `json:"elapsed_ms,omitempty"`
16+
}
17+
918
func (d *Debugger) Debug(message ...any) *Debugger {
1019
d.write(message...)
1120
return d
@@ -29,6 +38,10 @@ func (d *Debugger) write(message ...any) {
2938
return
3039
}
3140

41+
if GetFormat() == JSON {
42+
d.writeJSON(message...)
43+
return
44+
}
3245
// Optional timestamp
3346
var timestamp string
3447
if t := GetTimestamp(); t != nil {
@@ -47,8 +60,12 @@ func (d *Debugger) write(message ...any) {
4760
parts = append(parts, d.namespace)
4861
}
4962

50-
if f := d.formatFields(); f != "" {
51-
parts = append(parts, f)
63+
// append fields as key=value
64+
for key, value := range d.fields {
65+
if value == nil {
66+
value = ""
67+
}
68+
parts = append(parts, fmt.Sprintf("%s=%v", key, value))
5269
}
5370

5471
parts = append(parts, msg)
@@ -64,15 +81,26 @@ func (d *Debugger) write(message ...any) {
6481
}
6582
}
6683

67-
func (d *Debugger) formatFields() string {
68-
if len(d.fields) == 0 {
69-
return ""
84+
func (d *Debugger) writeJSON(message ...any) {
85+
entry := asJSON{
86+
Namespace: d.namespace,
87+
Message: fmt.Sprint(message...),
88+
Fields: d.fields,
89+
ElapsedMs: d.elapsed().Milliseconds(),
7090
}
7191

72-
parts := make([]string, 0, len(d.fields))
73-
for k, v := range d.fields {
74-
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
92+
if t := GetTimestamp(); t != nil {
93+
entry.Timestamp = time.Now().Format(t.Format)
7594
}
7695

77-
return strings.Join(parts, " ")
96+
data, err := json.Marshal(entry)
97+
if err != nil {
98+
return
99+
}
100+
101+
if d.output != nil {
102+
_, _ = d.output.Write(append(data, '\n'))
103+
} else if o := GetOutput(); o != nil {
104+
_, _ = o.Write(append(data, '\n'))
105+
}
78106
}

write_test.go

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,41 @@ func TestDebug(t *testing.T) {
4242

4343
d.Debug(testMessage)
4444

45-
assert.True(t, hasANSI(buf.String()), "Must have no colors")
45+
assert.True(t, hasANSI(buf.String()))
4646

4747
output := strings.TrimSpace(stripANSI(buf.String())) // Strip colors and trim whitespace
4848
expected := strings.TrimSpace(testMessageExpected)
49-
assert.Equal(t, expected, output, "Must have no colors")
49+
assert.Equal(t, expected, output)
5050
}
5151

52-
func TestDebugWithFields(t *testing.T) {
52+
func TestDebugJSON(t *testing.T) {
5353
var buf bytes.Buffer
5454
d := getDebugger()
5555
d.SetOutput(&buf)
56+
SetFormat(JSON)
57+
d.Debug(testMessage)
58+
SetFormat(Plain)
5659

57-
x := d.With("key1", "value1").With("key2", 42)
58-
59-
x.Debug(testMessage)
60+
assert.False(t, hasANSI(buf.String()))
61+
output := strings.TrimSpace(stripANSI(buf.String())) // Strip colors and trim whitespace
62+
expected := strings.TrimSpace("{\"namespace\":\"" + namespace + "\",\"message\":\"" + testMessage + "\"}")
63+
assert.Equal(t, expected, output)
64+
}
6065

61-
assert.True(t, hasANSI(buf.String()), "Must have no colors")
66+
func TestDebugJSONWithTimestamp(t *testing.T) {
67+
var buf bytes.Buffer
68+
d := getDebugger()
69+
SetOutput(&buf)
70+
SetTimestamp(&Timestamp{Format: "2006"})
71+
SetFormat(JSON)
72+
d.Debug(testMessage)
73+
SetFormat(Plain)
74+
SetTimestamp(nil)
6275

76+
assert.False(t, hasANSI(buf.String()))
6377
output := strings.TrimSpace(stripANSI(buf.String())) // Strip colors and trim whitespace
64-
expected := strings.TrimSpace(fmt.Sprintf("%s key1=value1 key2=42 %s +0ms\n", namespace, testMessage))
65-
assert.Equal(t, expected, output, "Must have fields")
78+
expected := strings.TrimSpace("{\"timestamp\":\"" + fmt.Sprint(time.Now().Year()) + "\",\"namespace\":\"" + namespace + "\",\"message\":\"" + testMessage + "\"}")
79+
assert.Equal(t, expected, output)
6680
}
6781

6882
func TestDebugGlobalOutput(t *testing.T) {
@@ -74,11 +88,11 @@ func TestDebugGlobalOutput(t *testing.T) {
7488

7589
d.Debug(testMessage)
7690

77-
assert.True(t, hasANSI(buf.String()), "Must have no colors")
91+
assert.True(t, hasANSI(buf.String()))
7892

7993
output := strings.TrimSpace(stripANSI(buf.String())) // Strip colors and trim whitespace
8094
expected := strings.TrimSpace(testMessageExpected)
81-
assert.Equal(t, expected, output, "Must have no colors")
95+
assert.Equal(t, expected, output)
8296
}
8397

8498
func TestDebugNoColors(t *testing.T) {
@@ -89,7 +103,7 @@ func TestDebugNoColors(t *testing.T) {
89103

90104
d.Debug(testMessage)
91105

92-
assert.False(t, hasANSI(buf.String()), "Must have no colors")
106+
assert.False(t, hasANSI(buf.String()))
93107
}
94108

95109
func TestDebugNonMatchingNamespace(t *testing.T) {
@@ -100,7 +114,7 @@ func TestDebugNonMatchingNamespace(t *testing.T) {
100114

101115
d.Debug("")
102116

103-
assert.Empty(t, buf.String(), "Must have no message")
117+
assert.Empty(t, buf.String())
104118
}
105119

106120
func TestDebugEmptyMessage(t *testing.T) {
@@ -112,7 +126,7 @@ func TestDebugEmptyMessage(t *testing.T) {
112126
SetNamespace("does:not:exist")
113127
d.Debug("test")
114128

115-
assert.Empty(t, buf.String(), "Must have no message")
129+
assert.Empty(t, buf.String())
116130
}
117131

118132
func TestDebugWithColors(t *testing.T) {
@@ -123,7 +137,7 @@ func TestDebugWithColors(t *testing.T) {
123137

124138
d.Debug(testMessage)
125139

126-
assert.True(t, hasANSI(buf.String()), "Must have colors")
140+
assert.True(t, hasANSI(buf.String()))
127141
}
128142

129143
func TestDebugf(t *testing.T) {
@@ -172,3 +186,38 @@ func TestDebugRaceCondition(_ *testing.T) {
172186
// Optionally, verify output without colors
173187
_ = stripANSI(buf.String())
174188
}
189+
190+
func TestWriteWithFields(t *testing.T) {
191+
var buf bytes.Buffer
192+
d := getDebugger().With("foo", "bar").With("", "empty-key").With("foo", nil).With("func", func() {})
193+
d.SetOutput(&buf)
194+
SetFormat(Plain)
195+
SetUseColors(false)
196+
SetTimestamp(&Timestamp{Format: "2006"})
197+
d.Debugf("%s %s %t", "foo", "bar", true)
198+
199+
t.Log(buf.String())
200+
}
201+
202+
func TestJSONWritePrint(t *testing.T) {
203+
var buf bytes.Buffer
204+
d := getDebugger().With("foo", "bar").With("", "empty-key").With("foo", nil).With("func", func() {}).With("age", 42).With("is", true)
205+
d.SetOutput(&buf)
206+
SetFormat(JSON)
207+
SetTimestamp(&Timestamp{Format: "2006"})
208+
d.Debug("foo", "bar", true)
209+
assert.Equal(t, "{\"timestamp\":\"2025\",\"namespace\":\"test-namespace\",\"fields\":{\"(empty)\":\"empty-key\",\"age\":42,\"foo\":null,\"func\":\"(not serializable)\",\"is\":true},\"message\":\"foobartrue\"}\n", buf.String())
210+
t.Log(buf.String())
211+
}
212+
213+
func TestPlainWritePrint(t *testing.T) {
214+
var buf bytes.Buffer
215+
d := getDebugger()
216+
d.SetOutput(&buf)
217+
SetTimestamp(&Timestamp{Format: time.Kitchen})
218+
SetUseColors(false)
219+
SetFormat(Plain)
220+
SetTimestamp(&Timestamp{Format: time.Kitchen})
221+
d.Debugf("%s %s %t", "foo", "bar", true)
222+
t.Log(buf.String())
223+
}

0 commit comments

Comments
 (0)