Skip to content

Commit a578498

Browse files
committed
Migrate logging to slog and more structured logs
1 parent 2fb408b commit a578498

32 files changed

Lines changed: 198 additions & 123 deletions

balancer/main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"log"
6+
"log/slog"
67
"net/http"
78
"os"
89

@@ -52,7 +53,7 @@ func StartBalancerServer(b *bundle.Bundle) {
5253
router := http.NewServeMux()
5354
routes.AddRoutes(router, b)
5455

55-
b.Log.Println("Starting MultiJuicer balancer on :8080")
56+
b.Log.Info("Starting MultiJuicer balancer on :8080")
5657
server := &http.Server{
5758
Addr: ":8080",
5859
Handler: router,
@@ -63,14 +64,14 @@ func StartBalancerServer(b *bundle.Bundle) {
6364
}
6465
}
6566

66-
func StartLLMGatewayServer(gateway *llmgateway.Gateway, logger *log.Logger) {
67+
func StartLLMGatewayServer(gateway *llmgateway.Gateway, logger *slog.Logger) {
6768
router := http.NewServeMux()
6869
router.Handle("/", gateway)
6970
server := &http.Server{
7071
Addr: ":8082",
7172
Handler: router,
7273
}
73-
logger.Println("Starting LLM gateway on :8082")
74+
logger.Info("Starting LLM gateway on :8082")
7475
if err := server.ListenAndServe(); err != nil {
7576
log.Fatalf("Failed to start LLM gateway server: %v", err)
7677
}

balancer/pkg/bundle/bundle.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8-
"log"
8+
"log/slog"
99
"os"
10+
"strings"
1011
"time"
1112

1213
"github.com/juice-shop/multi-juicer/balancer/pkg/passcode"
@@ -28,7 +29,7 @@ type Bundle struct {
2829
BcryptRounds int
2930
StaticAssetsDirectory string `json:"staticAssetsDirectory"`
3031
Config *Config
31-
Log *log.Logger
32+
Log *slog.Logger
3233

3334
// LongPollDefaultWaitTimeout amount of time that HTTP Long Polling Endpoints wait for new data to arrive before returning a empty no changes response
3435
LongPollDefaultWaitTimeout time.Duration
@@ -184,6 +185,21 @@ type NotificationService interface {
184185
SetEndDate(ctx context.Context, endDate *time.Time) error
185186
}
186187

188+
// ParseLogLevel converts a log level string to a slog.Level.
189+
// Valid values: "debug", "info", "warn"/"warning", "error". Defaults to info.
190+
func ParseLogLevel(level string) slog.Level {
191+
switch strings.ToLower(level) {
192+
case "debug":
193+
return slog.LevelDebug
194+
case "warn", "warning":
195+
return slog.LevelWarn
196+
case "error":
197+
return slog.LevelError
198+
default:
199+
return slog.LevelInfo
200+
}
201+
}
202+
187203
func getJuiceShopUrlForTeam(team string, bundle *Bundle) string {
188204
return fmt.Sprintf("http://juiceshop-%s.%s.svc.cluster.local:3000", team, bundle.RuntimeEnvironment.Namespace)
189205
}
@@ -251,7 +267,7 @@ func New() *Bundle {
251267
GeneratePasscode: passcode.GetPasscodeGeneratorWithPasscodeLength(config.TeamPasscodeLength),
252268
GetJuiceShopUrlForTeam: getJuiceShopUrlForTeam,
253269
BcryptRounds: bcrypt.DefaultCost,
254-
Log: log.New(os.Stdout, "", log.LstdFlags),
270+
Log: slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: ParseLogLevel(os.Getenv("LOG_LEVEL"))})),
255271
LongPollDefaultWaitTimeout: 25 * time.Second,
256272
Config: config,
257273
JuiceShopChallenges: challenges,

balancer/pkg/llmgateway/gateway.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"io"
7-
"log"
7+
"log/slog"
88
"net/http"
99
"net/http/httputil"
1010
"net/url"
@@ -29,11 +29,11 @@ type Gateway struct {
2929
upstreamURL *url.URL
3030
apiKey string
3131
usage *UsageTracker
32-
logger *log.Logger
32+
logger *slog.Logger
3333
}
3434

3535
// NewGateway creates a new LLM gateway.
36-
func NewGateway(signingKey string, upstreamURL string, apiKey string, usage *UsageTracker, logger *log.Logger) (*Gateway, error) {
36+
func NewGateway(signingKey string, upstreamURL string, apiKey string, usage *UsageTracker, logger *slog.Logger) (*Gateway, error) {
3737
u, err := url.Parse(upstreamURL)
3838
if err != nil {
3939
return nil, err
@@ -65,7 +65,7 @@ func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6565

6666
// Check if this is a chat completions request (for usage tracking)
6767
isChatCompletion := strings.Contains(r.URL.Path, "/chat/completions")
68-
g.logger.Printf("LLM gateway: request from team '%s': %s %s (isChatCompletion=%v)", team, r.Method, r.URL.Path, isChatCompletion)
68+
g.logger.Debug("LLM gateway: request", "team", team, "method", r.Method, "path", r.URL.Path, "isChatCompletion", isChatCompletion)
6969

7070
// Create reverse proxy
7171
proxy := &httputil.ReverseProxy{
@@ -84,7 +84,7 @@ func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8484
}
8585

8686
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
87-
g.logger.Printf("LLM gateway proxy error for team '%s': %v", team, err)
87+
g.logger.Error("LLM gateway proxy error", "team", team, "error", err)
8888
http.Error(w, `{"error":"upstream LLM API error"}`, http.StatusBadGateway)
8989
}
9090

@@ -102,7 +102,7 @@ func (g *Gateway) extractUsage(resp *http.Response, team string) error {
102102
body, err := io.ReadAll(resp.Body)
103103
resp.Body.Close()
104104
if err != nil {
105-
g.logger.Printf("LLM gateway: failed to read response body for team '%s': %v", team, err)
105+
g.logger.Error("LLM gateway: failed to read response body", "team", team, "error", err)
106106
resp.Body = io.NopCloser(bytes.NewReader(body))
107107
return nil
108108
}
@@ -124,7 +124,7 @@ func (g *Gateway) extractUsageFromJSON(body []byte, team string) {
124124
return
125125
}
126126
if result.Usage != nil {
127-
g.logger.Printf("LLM gateway: usage for team '%s': input_tokens=%d, output_tokens=%d", team, result.Usage.InputTokens, result.Usage.OutputTokens)
127+
g.logger.Debug("LLM gateway: usage", "team", team, "input_tokens", result.Usage.InputTokens, "output_tokens", result.Usage.OutputTokens)
128128
g.usage.Add(team, result.Usage.InputTokens, result.Usage.OutputTokens)
129129
}
130130
}
@@ -148,10 +148,10 @@ func (g *Gateway) extractUsageFromSSE(body []byte, team string) {
148148
continue
149149
}
150150
if chunk.Usage != nil {
151-
g.logger.Printf("LLM gateway: SSE usage for team '%s': input_tokens=%d, output_tokens=%d", team, chunk.Usage.InputTokens, chunk.Usage.OutputTokens)
151+
g.logger.Debug("LLM gateway: SSE usage", "team", team, "input_tokens", chunk.Usage.InputTokens, "output_tokens", chunk.Usage.OutputTokens)
152152
g.usage.Add(team, chunk.Usage.InputTokens, chunk.Usage.OutputTokens)
153153
return
154154
}
155155
}
156-
g.logger.Printf("LLM gateway: no usage data found in SSE stream for team '%s'", team)
156+
g.logger.Debug("LLM gateway: no usage data found in SSE stream", "team", team)
157157
}

balancer/pkg/llmgateway/gateway_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package llmgateway
33
import (
44
"encoding/json"
55
"io"
6-
"log"
6+
"log/slog"
77
"net/http"
88
"net/http/httptest"
99
"os"
@@ -22,7 +22,7 @@ func signToken(team string) string {
2222

2323
func TestGateway_MissingAuth(t *testing.T) {
2424
usage := NewUsageTracker()
25-
gw, _ := NewGateway(testSigningKey, "http://localhost:11434", "real-key", usage, log.New(os.Stdout, "", 0))
25+
gw, _ := NewGateway(testSigningKey, "http://localhost:11434", "real-key", usage, slog.New(slog.NewTextHandler(os.Stdout, nil)))
2626

2727
req := httptest.NewRequest("POST", "/v1/chat/completions", nil)
2828
w := httptest.NewRecorder()
@@ -35,7 +35,7 @@ func TestGateway_MissingAuth(t *testing.T) {
3535

3636
func TestGateway_InvalidToken(t *testing.T) {
3737
usage := NewUsageTracker()
38-
gw, _ := NewGateway(testSigningKey, "http://localhost:11434", "real-key", usage, log.New(os.Stdout, "", 0))
38+
gw, _ := NewGateway(testSigningKey, "http://localhost:11434", "real-key", usage, slog.New(slog.NewTextHandler(os.Stdout, nil)))
3939

4040
req := httptest.NewRequest("POST", "/v1/chat/completions", nil)
4141
req.Header.Set("Authorization", "Bearer bad-token")
@@ -63,7 +63,7 @@ func TestGateway_ProxiesWithRealKey(t *testing.T) {
6363
defer upstream.Close()
6464

6565
usage := NewUsageTracker()
66-
gw, _ := NewGateway(testSigningKey, upstream.URL, "real-api-key", usage, log.New(os.Stdout, "", 0))
66+
gw, _ := NewGateway(testSigningKey, upstream.URL, "real-api-key", usage, slog.New(slog.NewTextHandler(os.Stdout, nil)))
6767

6868
req := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(`{"model":"test","messages":[]}`))
6969
req.Header.Set("Authorization", "Bearer "+signToken("team-a"))
@@ -99,7 +99,7 @@ func TestGateway_NonCompletionEndpoint_NoUsageTracking(t *testing.T) {
9999
defer upstream.Close()
100100

101101
usage := NewUsageTracker()
102-
gw, _ := NewGateway(testSigningKey, upstream.URL, "real-api-key", usage, log.New(os.Stdout, "", 0))
102+
gw, _ := NewGateway(testSigningKey, upstream.URL, "real-api-key", usage, slog.New(slog.NewTextHandler(os.Stdout, nil)))
103103

104104
req := httptest.NewRequest("GET", "/v1/models", nil)
105105
req.Header.Set("Authorization", "Bearer "+signToken("team-a"))
@@ -128,7 +128,7 @@ func TestGateway_ResponseBodyPassedThrough(t *testing.T) {
128128
defer upstream.Close()
129129

130130
usage := NewUsageTracker()
131-
gw, _ := NewGateway(testSigningKey, upstream.URL, "real-api-key", usage, log.New(os.Stdout, "", 0))
131+
gw, _ := NewGateway(testSigningKey, upstream.URL, "real-api-key", usage, slog.New(slog.NewTextHandler(os.Stdout, nil)))
132132

133133
req := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(`{"model":"test","messages":[]}`))
134134
req.Header.Set("Authorization", "Bearer "+signToken("team-a"))
@@ -158,7 +158,7 @@ data: [DONE]
158158
defer upstream.Close()
159159

160160
usage := NewUsageTracker()
161-
gw, _ := NewGateway(testSigningKey, upstream.URL, "real-api-key", usage, log.New(os.Stdout, "", 0))
161+
gw, _ := NewGateway(testSigningKey, upstream.URL, "real-api-key", usage, slog.New(slog.NewTextHandler(os.Stdout, nil)))
162162

163163
req := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(`{"model":"test","messages":[]}`))
164164
req.Header.Set("Authorization", "Bearer "+signToken("team-a"))

balancer/pkg/llmgateway/usage.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package llmgateway
33
import (
44
"context"
55
"fmt"
6-
"log"
6+
"log/slog"
77
"strconv"
88
"sync"
99
"time"
@@ -52,15 +52,15 @@ func (t *UsageTracker) Add(team string, inputTokens, outputTokens int64) {
5252
}
5353

5454
// FlushToAnnotations writes accumulated usage to deployment annotations and resets the counters.
55-
func (t *UsageTracker) FlushToAnnotations(ctx context.Context, clientset kubernetes.Interface, namespace string, logger *log.Logger) {
55+
func (t *UsageTracker) FlushToAnnotations(ctx context.Context, clientset kubernetes.Interface, namespace string, logger *slog.Logger) {
5656
t.mu.Lock()
5757
pending := t.usage
5858
t.usage = make(map[string]*TeamUsage)
5959
t.mu.Unlock()
6060

6161
for team, usage := range pending {
6262
if err := t.updateTeamAnnotations(ctx, clientset, namespace, team, usage, logger); err != nil {
63-
logger.Printf("Failed to flush LLM usage for team '%s': %v", team, err)
63+
logger.Error("Failed to flush LLM usage", "team", team, "error", err)
6464
// Put the usage back so it's not lost
6565
t.Add(team, usage.InputTokens, usage.OutputTokens)
6666
}
@@ -69,7 +69,7 @@ func (t *UsageTracker) FlushToAnnotations(ctx context.Context, clientset kuberne
6969

7070
// updateTeamAnnotations uses optimistic concurrency (read resourceVersion, retry on conflict)
7171
// to safely increment token counters even when multiple balancer replicas are running.
72-
func (t *UsageTracker) updateTeamAnnotations(ctx context.Context, clientset kubernetes.Interface, namespace, team string, delta *TeamUsage, logger *log.Logger) error {
72+
func (t *UsageTracker) updateTeamAnnotations(ctx context.Context, clientset kubernetes.Interface, namespace, team string, delta *TeamUsage, logger *slog.Logger) error {
7373
deploymentName := fmt.Sprintf("juiceshop-%s", team)
7474

7575
for attempt := range maxRetries {
@@ -109,14 +109,14 @@ func (t *UsageTracker) updateTeamAnnotations(ctx context.Context, clientset kube
109109
if !errors.IsConflict(err) {
110110
return fmt.Errorf("failed to update deployment: %w", err)
111111
}
112-
logger.Printf("LLM usage update conflict for team '%s' (attempt %d/%d), retrying", team, attempt+1, maxRetries)
112+
logger.Warn("LLM usage update conflict, retrying", "team", team, "attempt", attempt+1, "maxRetries", maxRetries)
113113
}
114114

115115
return fmt.Errorf("failed to update deployment after %d retries due to conflicts", maxRetries)
116116
}
117117

118118
// StartFlusher periodically flushes accumulated usage to deployment annotations.
119-
func (t *UsageTracker) StartFlusher(ctx context.Context, clientset kubernetes.Interface, namespace string, logger *log.Logger) {
119+
func (t *UsageTracker) StartFlusher(ctx context.Context, clientset kubernetes.Interface, namespace string, logger *slog.Logger) {
120120
ticker := time.NewTicker(10 * time.Second)
121121
defer ticker.Stop()
122122
for {

balancer/pkg/llmgateway/usage_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package llmgateway
22

33
import (
44
"context"
5-
"log"
5+
"log/slog"
66
"os"
77
"testing"
88

@@ -45,7 +45,7 @@ func TestUsageTracker_FlushToAnnotations(t *testing.T) {
4545
tracker := NewUsageTracker()
4646
tracker.Add("team-a", 10, 20)
4747

48-
logger := log.New(os.Stdout, "", 0)
48+
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
4949
tracker.FlushToAnnotations(context.Background(), clientset, "default", logger)
5050

5151
// Verify annotations were updated
@@ -76,7 +76,7 @@ func TestUsageTracker_FlushRetainsOnError(t *testing.T) {
7676
tracker := NewUsageTracker()
7777
tracker.Add("nonexistent", 10, 20)
7878

79-
logger := log.New(os.Stdout, "", 0)
79+
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
8080
tracker.FlushToAnnotations(context.Background(), clientset, "default", logger)
8181

8282
// Usage should be retained

balancer/pkg/notification/notification.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func (s *NotificationService) StartNotificationWatcher(ctx context.Context) {
7878
for {
7979
select {
8080
case <-ctx.Done():
81-
s.bundle.Log.Printf("MultiJuicer context canceled. Exiting notification watcher.")
81+
s.bundle.Log.Info("MultiJuicer context canceled. Exiting notification watcher.")
8282
return
8383
default:
8484
s.watchConfigMap(ctx)
@@ -99,10 +99,10 @@ func (s *NotificationService) watchConfigMap(ctx context.Context) {
9999
)
100100
if err != nil {
101101
if errors.IsNotFound(err) {
102-
s.bundle.Log.Printf("Notification ConfigMap not found. Treating as no notification.")
102+
s.bundle.Log.Debug("Notification ConfigMap not found. Treating as no notification.")
103103
s.parseAndUpdateNotification(nil)
104104
} else {
105-
s.bundle.Log.Printf("Failed to get notification ConfigMap: %v", err)
105+
s.bundle.Log.Error("Failed to get notification ConfigMap", "error", err)
106106
return
107107
}
108108
} else {
@@ -117,18 +117,18 @@ func (s *NotificationService) watchConfigMap(ctx context.Context) {
117117
},
118118
)
119119
if err != nil {
120-
s.bundle.Log.Printf("Failed to start watch for notification ConfigMap: %v", err)
120+
s.bundle.Log.Error("Failed to start watch for notification ConfigMap", "error", err)
121121
return
122122
}
123123
defer watcher.Stop()
124124

125-
s.bundle.Log.Printf("Started watching notification ConfigMap")
125+
s.bundle.Log.Debug("Started watching notification ConfigMap")
126126

127127
for {
128128
select {
129129
case event, ok := <-watcher.ResultChan():
130130
if !ok {
131-
s.bundle.Log.Printf("Notification ConfigMap watcher closed. Reconnecting...")
131+
s.bundle.Log.Warn("Notification ConfigMap watcher closed. Reconnecting...")
132132
return
133133
}
134134

@@ -137,11 +137,11 @@ func (s *NotificationService) watchConfigMap(ctx context.Context) {
137137
configMap := event.Object.(*corev1.ConfigMap)
138138
s.parseAndUpdateNotification(configMap)
139139
case watch.Deleted:
140-
s.bundle.Log.Printf("Notification ConfigMap deleted")
140+
s.bundle.Log.Info("Notification ConfigMap deleted")
141141
s.parseAndUpdateNotification(nil)
142142
}
143143
case <-ctx.Done():
144-
s.bundle.Log.Printf("Context canceled. Exiting notification watcher.")
144+
s.bundle.Log.Info("Context canceled. Exiting notification watcher.")
145145
return
146146
}
147147
}
@@ -159,23 +159,23 @@ func (s *NotificationService) parseAndUpdateNotification(cm *corev1.ConfigMap) {
159159

160160
jsonData, ok := cm.Data["notification.json"]
161161
if !ok {
162-
s.bundle.Log.Printf("Notification ConfigMap missing 'notification.json' key")
162+
s.bundle.Log.Warn("Notification ConfigMap missing 'notification.json' key")
163163
s.currentNotification = nil
164164
s.lastUpdate = timeutil.TruncateToMillisecond(time.Now())
165165
return
166166
}
167167

168168
var notification bundle.Notification
169169
if err := json.Unmarshal([]byte(jsonData), &notification); err != nil {
170-
s.bundle.Log.Printf("Failed to parse notification JSON: %v", err)
170+
s.bundle.Log.Error("Failed to parse notification JSON", "error", err)
171171
s.currentNotification = nil
172172
s.lastUpdate = timeutil.TruncateToMillisecond(time.Now())
173173
return
174174
}
175175

176176
s.currentNotification = &notification
177177
s.lastUpdate = timeutil.TruncateToMillisecond(time.Now())
178-
s.bundle.Log.Printf("Updated notification: enabled=%v, message=%q", notification.Enabled, notification.Message)
178+
s.bundle.Log.Info("Updated notification", "enabled", notification.Enabled, "message", notification.Message)
179179
}
180180

181181
// getOrCreateConfigMap retrieves the existing notification ConfigMap or returns a new empty one.

0 commit comments

Comments
 (0)