Skip to content

Commit 3bdb7ef

Browse files
committed
feat: integrate flexible slog-based logging with dependency injection
- Add a new slog-based Logger implementation with support for text and JSON output formats - Enable dependency injection of custom slog.Logger instances via an option - Introduce flexible logger instantiation using option pattern (text, JSON, or custom) - Provide tests for slog logger covering text mode, JSON mode, and custom logger injection Signed-off-by: appleboy <appleboy.tw@gmail.com>
1 parent 5637dfb commit 3bdb7ef

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

logger_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,23 @@ package graceful
22

33
import (
44
"bytes"
5+
"context"
6+
"log/slog"
57
"os"
68
"strings"
79
"testing"
810
)
911

12+
// handlerFunc is a mock slog.Handler (test only)
13+
type handlerFunc struct {
14+
fn func(r slog.Record) error
15+
}
16+
17+
func (h handlerFunc) Handle(_ context.Context, r slog.Record) error { return h.fn(r) }
18+
func (h handlerFunc) Enabled(_ context.Context, _ slog.Level) bool { return true }
19+
func (h handlerFunc) WithAttrs(_ []slog.Attr) slog.Handler { return h }
20+
func (h handlerFunc) WithGroup(_ string) slog.Handler { return h }
21+
1022
func TestNewLogger_Infof_Errorf(t *testing.T) {
1123
// Save original stdout and stderr
1224
origStdout := os.Stdout
@@ -38,3 +50,76 @@ func TestNewLogger_Infof_Errorf(t *testing.T) {
3850
t.Errorf("Errorf did not write expected message to stderr: %q", bufErr.String())
3951
}
4052
}
53+
54+
func TestNewSlogLogger_Text(t *testing.T) {
55+
origStdout := os.Stdout
56+
origStderr := os.Stderr
57+
defer func() {
58+
os.Stdout = origStdout
59+
os.Stderr = origStderr
60+
}()
61+
rOut, wOut, _ := os.Pipe()
62+
rErr, wErr, _ := os.Pipe()
63+
os.Stdout = wOut
64+
os.Stderr = wErr
65+
66+
logger := NewSlogLogger()
67+
logger.Infof("info-text: %s", "foo")
68+
logger.Errorf("error-text: %s", "bar")
69+
70+
wOut.Close()
71+
wErr.Close()
72+
var bufOut, bufErr bytes.Buffer
73+
bufOut.ReadFrom(rOut)
74+
bufErr.ReadFrom(rErr)
75+
76+
// Text handler should print plaintext, not JSON/bracketed
77+
outStr := bufOut.String()
78+
if !strings.Contains(outStr, "info-text: foo") {
79+
t.Errorf("Text mode Infof missing: %q", outStr)
80+
}
81+
}
82+
83+
func TestNewSlogLogger_Json(t *testing.T) {
84+
origStdout := os.Stdout
85+
origStderr := os.Stderr
86+
defer func() {
87+
os.Stdout = origStdout
88+
os.Stderr = origStderr
89+
}()
90+
rOut, wOut, _ := os.Pipe()
91+
rErr, wErr, _ := os.Pipe()
92+
os.Stdout = wOut
93+
os.Stderr = wErr
94+
95+
logger := NewSlogLogger(WithJSON())
96+
logger.Infof("info-json: %s", "foo")
97+
logger.Errorf("error-json: %s", "bar")
98+
99+
wOut.Close()
100+
wErr.Close()
101+
var bufOut, bufErr bytes.Buffer
102+
bufOut.ReadFrom(rOut)
103+
bufErr.ReadFrom(rErr)
104+
105+
// JSON handler should output JSON encoded log
106+
outStr := bufOut.String()
107+
if !strings.Contains(outStr, "\"msg\":\"info-json: foo\"") {
108+
t.Errorf("JSON mode Infof missing/invalid: %q", outStr)
109+
}
110+
}
111+
112+
func TestNewSlogLogger_WithSlog(t *testing.T) {
113+
var captured []string
114+
l := slog.New(handlerFunc{fn: func(r slog.Record) error {
115+
var buf bytes.Buffer
116+
buf.WriteString(r.Message)
117+
captured = append(captured, buf.String())
118+
return nil
119+
}})
120+
logger := NewSlogLogger(WithSlog(l))
121+
logger.Infof("injected: %s", "foo")
122+
if len(captured) != 1 || captured[0] != "injected: foo" {
123+
t.Errorf("Custom slog.Logger was not used/injected properly, got %+v", captured)
124+
}
125+
}

slog.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Package graceful provides a Logger implementation using Go's log/slog.
2+
package graceful
3+
4+
import (
5+
"fmt"
6+
"log/slog"
7+
"os"
8+
)
9+
10+
// slogLogger implements Logger interface using log/slog.
11+
type slogLogger struct {
12+
logger *slog.Logger
13+
}
14+
15+
// SlogLoggerOption applies configuration to NewSlogLogger.
16+
type SlogLoggerOption func(*slogLoggerOptions)
17+
18+
type slogLoggerOptions struct {
19+
logger *slog.Logger
20+
json bool
21+
}
22+
23+
// WithJSON returns an option to set output as JSON format.
24+
func WithJSON() SlogLoggerOption {
25+
return func(opt *slogLoggerOptions) { opt.json = true }
26+
}
27+
28+
// WithSlog injects a custom *slog.Logger instance.
29+
func WithSlog(logger *slog.Logger) SlogLoggerOption {
30+
return func(opt *slogLoggerOptions) { opt.logger = logger }
31+
}
32+
33+
// NewSlogLogger creates a Logger using flexible option pattern.
34+
//
35+
// Usage:
36+
//
37+
// NewSlogLogger() // text mode (default)
38+
// NewSlogLogger(WithJson()) // json mode
39+
// NewSlogLogger(WithSlog(loggerObj)) // inject custom *slog.Logger, which overrides other options
40+
func NewSlogLogger(opts ...SlogLoggerOption) Logger {
41+
var o slogLoggerOptions
42+
for _, f := range opts {
43+
f(&o)
44+
}
45+
if o.logger != nil {
46+
return &slogLogger{logger: o.logger}
47+
}
48+
var handler slog.Handler
49+
if o.json {
50+
handler = slog.NewJSONHandler(os.Stdout, nil)
51+
} else {
52+
handler = slog.NewTextHandler(os.Stdout, nil)
53+
}
54+
return &slogLogger{
55+
logger: slog.New(handler),
56+
}
57+
}
58+
59+
func (l *slogLogger) Infof(format string, args ...interface{}) {
60+
msg := fmt.Sprintf(format, args...)
61+
l.logger.Info(msg)
62+
}
63+
64+
func (l *slogLogger) Errorf(format string, args ...interface{}) {
65+
msg := fmt.Sprintf(format, args...)
66+
l.logger.Error(msg)
67+
}

0 commit comments

Comments
 (0)