Skip to content

Commit 8bb4853

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

File tree

5 files changed

+117
-56
lines changed

5 files changed

+117
-56
lines changed

debugo.go

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

33
import (
4-
"fmt"
54
"io"
6-
"maps"
75
"sync"
86
"time"
97

@@ -22,42 +20,24 @@ type Debugger struct {
2220

2321
output io.Writer
2422

25-
fields map[string]any
26-
2723
mutex *sync.Mutex
2824
}
2925

3026
func New(namespace string) *Debugger {
3127
return newDebugger(namespace)
3228
}
3329

30+
// Extend creates a new debugger instance with an extended namespace
3431
func (d *Debugger) Extend(namespace string) *Debugger {
3532
d.mutex.Lock()
3633
defer d.mutex.Unlock()
3734

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
44-
}
45-
46-
func (d *Debugger) With(kv ...any) *Debugger {
47-
d.mutex.Lock()
48-
defer d.mutex.Unlock()
49-
5035
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-
36+
n.namespace = d.namespace + ":" + namespace
5837
return &n
5938
}
6039

40+
// SetOutput sets the output writer for the debugger instance
6141
func (d *Debugger) SetOutput(output io.Writer) {
6242
d.mutex.Lock()
6343
defer d.mutex.Unlock()
@@ -74,7 +54,5 @@ func newDebugger(namespace string) *Debugger {
7454

7555
output: nil,
7656

77-
fields: make(map[string]any),
78-
7957
mutex: &sync.Mutex{}}
8058
}

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: 31 additions & 11 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,11 @@ func (d *Debugger) write(message ...any) {
2938
return
3039
}
3140

41+
if GetFormat() == JSON {
42+
d.writeJSON(message...)
43+
return
44+
}
45+
3246
// Optional timestamp
3347
var timestamp string
3448
if t := GetTimestamp(); t != nil {
@@ -47,10 +61,6 @@ func (d *Debugger) write(message ...any) {
4761
parts = append(parts, d.namespace)
4862
}
4963

50-
if f := d.formatFields(); f != "" {
51-
parts = append(parts, f)
52-
}
53-
5464
parts = append(parts, msg)
5565
parts = append(parts, fmt.Sprintf("+%s", prettyPrintDuration(d.elapsed())))
5666

@@ -64,15 +74,25 @@ func (d *Debugger) write(message ...any) {
6474
}
6575
}
6676

67-
func (d *Debugger) formatFields() string {
68-
if len(d.fields) == 0 {
69-
return ""
77+
func (d *Debugger) writeJSON(message ...any) {
78+
entry := asJSON{
79+
Namespace: d.namespace,
80+
Message: fmt.Sprint(message...),
81+
ElapsedMs: d.elapsed().Milliseconds(),
7082
}
7183

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))
84+
if t := GetTimestamp(); t != nil {
85+
entry.Timestamp = time.Now().Format(t.Format)
86+
}
87+
88+
data, err := json.Marshal(entry)
89+
if err != nil {
90+
return
7591
}
7692

77-
return strings.Join(parts, " ")
93+
if d.output != nil {
94+
_, _ = d.output.Write(append(data, '\n'))
95+
} else if o := GetOutput(); o != nil {
96+
_, _ = o.Write(append(data, '\n'))
97+
}
7898
}

write_test.go

Lines changed: 51 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,25 @@ func TestDebugRaceCondition(_ *testing.T) {
172186
// Optionally, verify output without colors
173187
_ = stripANSI(buf.String())
174188
}
189+
190+
func TestJSONWritePrint(t *testing.T) {
191+
var buf bytes.Buffer
192+
d := getDebugger()
193+
d.SetOutput(&buf)
194+
SetFormat(JSON)
195+
SetTimestamp(&Timestamp{Format: time.Kitchen})
196+
d.Debug("foo", "bar", true)
197+
t.Log(buf.String())
198+
}
199+
200+
func TestPlainWritePrint(t *testing.T) {
201+
var buf bytes.Buffer
202+
d := getDebugger()
203+
d.SetOutput(&buf)
204+
SetTimestamp(&Timestamp{Format: time.Kitchen})
205+
SetUseColors(false)
206+
SetFormat(Plain)
207+
SetTimestamp(&Timestamp{Format: time.Kitchen})
208+
d.Debugf("%s %s %t", "foo", "bar", true)
209+
t.Log(buf.String())
210+
}

0 commit comments

Comments
 (0)