Skip to content

Commit 6029d59

Browse files
committed
Use slog interally, add WithSlogLogger
1 parent adf54aa commit 6029d59

File tree

7 files changed

+159
-28
lines changed

7 files changed

+159
-28
lines changed

analytics.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,16 @@ func NewAnalyticsProcessor(ctx context.Context, client *resty.Client, baseURL st
3636
endpoint: baseURL + AnalyticsEndpoint,
3737
log: log,
3838
}
39+
log.Debugf("analytics processor starting")
3940
go processor.start(ctx, tickerInterval)
4041
return &processor
4142
}
4243

4344
func (a *AnalyticsProcessor) start(ctx context.Context, tickerInterval int) {
4445
ticker := time.NewTicker(time.Duration(tickerInterval) * time.Millisecond)
46+
defer func() {
47+
a.log.Debugf("analytics processor stopped")
48+
}()
4549
for {
4650
select {
4751
case <-ticker.C:

analytics_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestAnalytics(t *testing.T) {
4343
client.SetHeader("X-Environment-Key", EnvironmentAPIKey)
4444

4545
// Now let's create the processor
46-
processor := NewAnalyticsProcessor(context.Background(), client, server.URL+"/api/v1/", &analyticsTimer, createLogger())
46+
processor := NewAnalyticsProcessor(context.Background(), client, server.URL+"/api/v1/", &analyticsTimer, newSlogToLoggerAdapter(createLogger()))
4747

4848
// and, track some features
4949
processor.TrackFeature("feature_1")

client.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package flagsmith
33
import (
44
"context"
55
"fmt"
6+
"log/slog"
67
"strings"
78
"sync/atomic"
89
"time"
@@ -34,7 +35,7 @@ type Client struct {
3435
client *resty.Client
3536
ctxLocalEval context.Context
3637
ctxAnalytics context.Context
37-
log Logger
38+
log *slog.Logger
3839
offlineHandler OfflineHandler
3940
errorHandler func(handler *FlagsmithAPIError)
4041
}
@@ -70,7 +71,18 @@ func NewClient(apiKey string, options ...Option) *Client {
7071
opt(c)
7172
}
7273
}
73-
c.client.SetLogger(c.log)
74+
c.client.SetLogger(newSlogToRestyAdapter(c.log))
75+
76+
c.log.Debug("initialising Flagsmith client",
77+
"base_url", c.config.baseURL,
78+
"local_evaluation", c.config.localEvaluation,
79+
"offline", c.config.offlineMode,
80+
"analytics", c.config.enableAnalytics,
81+
"realtime", c.config.useRealtime,
82+
"realtime_url", c.config.realtimeBaseUrl,
83+
"env_refresh_interval", c.config.envRefreshInterval,
84+
"timeout", c.config.timeout,
85+
)
7486

7587
if c.config.offlineMode && c.offlineHandler == nil {
7688
panic("offline handler must be provided to use offline mode.")
@@ -97,7 +109,15 @@ func NewClient(apiKey string, options ...Option) *Client {
97109
}
98110
// Initialise analytics processor
99111
if c.config.enableAnalytics {
100-
c.analyticsProcessor = NewAnalyticsProcessor(c.ctxAnalytics, c.client, c.config.baseURL, nil, c.log)
112+
c.analyticsProcessor = NewAnalyticsProcessor(
113+
c.ctxAnalytics,
114+
c.client,
115+
c.config.baseURL,
116+
nil,
117+
newSlogToLoggerAdapter(
118+
c.log.With(slog.String("worker", "analytics")),
119+
),
120+
)
101121
}
102122
return c
103123
}
@@ -319,7 +339,7 @@ func (c *Client) pollEnvironment(ctx context.Context) {
319339
defer cancel()
320340
err := c.UpdateEnvironment(ctx)
321341
if err != nil {
322-
c.log.Errorf("Failed to update environment: %v", err)
342+
c.log.Error("failed to update environment", "error", err)
323343
}
324344
}
325345
update()
@@ -364,6 +384,7 @@ func (c *Client) UpdateEnvironment(ctx context.Context) error {
364384
}
365385
c.identitiesWithOverrides.Store(identitiesWithOverrides)
366386

387+
c.log.Info("environment updated", "environment", env.APIKey)
367388
return nil
368389
}
369390

client_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"log/slog"
89
"net/http"
910
"net/http/httptest"
11+
"strings"
1012
"sync"
1113
"testing"
1214
"time"
@@ -959,3 +961,19 @@ data: {"updated_at": %f}
959961
// Flush the event to the client
960962
flusher.Flush()
961963
}
964+
965+
func TestWithSlogLogger(t *testing.T) {
966+
// Given
967+
var logOutput strings.Builder
968+
slogLogger := slog.New(slog.NewTextHandler(&logOutput, &slog.HandlerOptions{
969+
Level: slog.LevelDebug,
970+
}))
971+
972+
// When
973+
_ = flagsmith.NewClient(fixtures.EnvironmentAPIKey, flagsmith.WithSlogLogger(slogLogger))
974+
975+
// Then
976+
logStr := logOutput.String()
977+
t.Log(logStr)
978+
assert.Contains(t, logStr, "initialising Flagsmith client")
979+
}

logger.go

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package flagsmith
22

33
import (
4-
"log"
4+
"context"
5+
"fmt"
6+
"log/slog"
57
"os"
8+
"strings"
69
)
710

811
// Logger is the interface used for logging by flagsmith client. This interface defines the methods
@@ -19,33 +22,99 @@ type Logger interface {
1922
Debugf(format string, v ...interface{})
2023
}
2124

22-
func createLogger() *logger {
23-
l := &logger{l: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)}
24-
return l
25+
// slogToRestyAdapter adapts a slog.Logger to resty.Logger
26+
type slogToRestyAdapter struct {
27+
logger *slog.Logger
28+
}
29+
30+
func newSlogToRestyAdapter(logger *slog.Logger) *slogToRestyAdapter {
31+
return &slogToRestyAdapter{logger: logger}
32+
}
33+
34+
func (l *slogToRestyAdapter) Errorf(format string, v ...interface{}) {
35+
l.logger.Error(format, v...)
36+
}
37+
38+
func (l *slogToRestyAdapter) Warnf(format string, v ...interface{}) {
39+
l.logger.Warn(format, v...)
40+
}
41+
42+
func (l *slogToRestyAdapter) Debugf(format string, v ...interface{}) {
43+
l.logger.Debug(format, v...)
44+
}
45+
46+
// slogToLoggerAdapter adapts a slog.Logger to our Logger interface
47+
type slogToLoggerAdapter struct {
48+
logger *slog.Logger
49+
}
50+
51+
func newSlogToLoggerAdapter(logger *slog.Logger) *slogToLoggerAdapter {
52+
return &slogToLoggerAdapter{logger: logger}
2553
}
2654

27-
var _ Logger = (*logger)(nil)
55+
func (l *slogToLoggerAdapter) Errorf(format string, v ...interface{}) {
56+
l.logger.Error(fmt.Sprintf(format, v...))
57+
}
58+
59+
func (l *slogToLoggerAdapter) Warnf(format string, v ...interface{}) {
60+
l.logger.Warn(fmt.Sprintf(format, v...))
61+
}
2862

29-
type logger struct {
30-
l *log.Logger
63+
func (l *slogToLoggerAdapter) Debugf(format string, v ...interface{}) {
64+
l.logger.Debug(fmt.Sprintf(format, v...))
3165
}
3266

33-
func (l *logger) Errorf(format string, v ...interface{}) {
34-
l.output("ERROR FLAGSMITH: "+format, v...)
67+
// loggerToSlogAdapter adapts our Logger interface to a slog.Logger
68+
type loggerToSlogAdapter struct {
69+
logger Logger
3570
}
3671

37-
func (l *logger) Warnf(format string, v ...interface{}) {
38-
l.output("WARN FLAGSMITH: "+format, v...)
72+
func newLoggerToSlogAdapter(logger Logger) *slog.Logger {
73+
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
74+
Level: slog.LevelDebug,
75+
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
76+
// We don't need to modify any attributes since we're using the existing logger
77+
return a
78+
},
79+
}))
3980
}
4081

41-
func (l *logger) Debugf(format string, v ...interface{}) {
42-
l.output("DEBUG FLAGSMITH: "+format, v...)
82+
// implement slog.Handler interface to adapt our Logger interface to a slog.Logger
83+
84+
func (l *loggerToSlogAdapter) Enabled(ctx context.Context, level slog.Level) bool {
85+
return true
4386
}
4487

45-
func (l *logger) output(format string, v ...interface{}) {
46-
if len(v) == 0 {
47-
l.l.Print(format)
48-
return
88+
func (l *loggerToSlogAdapter) Handle(ctx context.Context, r slog.Record) error {
89+
msg := r.Message
90+
var attrs strings.Builder
91+
r.Attrs(func(a slog.Attr) bool {
92+
attrs.WriteString(a.String() + " ")
93+
return true
94+
})
95+
msg += attrs.String()
96+
97+
switch r.Level {
98+
case slog.LevelError:
99+
l.logger.Errorf(msg)
100+
case slog.LevelWarn:
101+
l.logger.Warnf(msg)
102+
case slog.LevelDebug:
103+
l.logger.Debugf(msg)
49104
}
50-
l.l.Printf(format, v...)
105+
return nil
106+
}
107+
108+
func (l *loggerToSlogAdapter) WithAttrs(_ []slog.Attr) slog.Handler {
109+
return l
110+
}
111+
112+
func (l *loggerToSlogAdapter) WithGroup(_ string) slog.Handler {
113+
return l
114+
}
115+
116+
func createLogger() *slog.Logger {
117+
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
118+
Level: slog.LevelDebug,
119+
}))
51120
}

options.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"strings"
66
"time"
7+
8+
"log/slog"
79
)
810

911
type Option func(c *Client)
@@ -22,6 +24,8 @@ var _ = []Option{
2224
WithProxy(""),
2325
WithRealtime(),
2426
WithRealtimeBaseURL(""),
27+
WithLogger(nil),
28+
WithSlogLogger(nil),
2529
}
2630

2731
func WithBaseURL(url string) Option {
@@ -93,6 +97,13 @@ func WithDefaultHandler(handler func(string) (Flag, error)) Option {
9397

9498
// Allows the client to use any logger that implements the `Logger` interface.
9599
func WithLogger(logger Logger) Option {
100+
return func(c *Client) {
101+
c.log = newLoggerToSlogAdapter(logger)
102+
}
103+
}
104+
105+
// WithSlogLogger allows the client to use a slog.Logger for logging.
106+
func WithSlogLogger(logger *slog.Logger) Option {
96107
return func(c *Client) {
97108
c.log = logger
98109
}

realtime.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"errors"
8+
"log/slog"
89
"net/http"
910
"strings"
1011
"time"
@@ -20,41 +21,48 @@ func (c *Client) startRealtimeUpdates(ctx context.Context) {
2021
env, _ := c.environment.Load().(*environments.EnvironmentModel)
2122
stream_url := c.config.realtimeBaseUrl + "sse/environments/" + env.APIKey + "/stream"
2223
envUpdatedAt := env.UpdatedAt
24+
log := c.log.With(
25+
slog.String("worker", "realtime"),
26+
slog.String("stream", stream_url),
27+
)
28+
defer func() {
29+
log.Info("realtime stopped")
30+
}()
2331
for {
2432
select {
2533
case <-ctx.Done():
2634
return
2735
default:
2836
resp, err := http.Get(stream_url)
2937
if err != nil {
30-
c.log.Errorf("Error connecting to realtime server: %v", err)
38+
log.Error("failed to connect to realtime stream", "error", err)
3139
continue
3240
}
3341
defer resp.Body.Close()
42+
log.Info("connected")
3443

3544
scanner := bufio.NewScanner(resp.Body)
3645
for scanner.Scan() {
3746
line := scanner.Text()
3847
if strings.HasPrefix(line, "data: ") {
3948
parsedTime, err := parseUpdatedAtFromSSE(line)
4049
if err != nil {
41-
c.log.Errorf("Error reading realtime stream: %v", err)
50+
log.Error("failed to parse event message", "error", err, "message", line)
4251
continue
4352
}
4453
if parsedTime.After(envUpdatedAt) {
4554
err = c.UpdateEnvironment(ctx)
4655
if err != nil {
47-
c.log.Errorf("Failed to update the environment: %v", err)
56+
log.Error("failed to update environment after receiving event", "error", err)
4857
continue
4958
}
5059
env, _ := c.environment.Load().(*environments.EnvironmentModel)
51-
5260
envUpdatedAt = env.UpdatedAt
5361
}
5462
}
5563
}
5664
if err := scanner.Err(); err != nil {
57-
c.log.Errorf("Error reading realtime stream: %v", err)
65+
log.Error("error reading from realtime stream", "error", err)
5866
}
5967
}
6068
}

0 commit comments

Comments
 (0)