Skip to content

Commit 5debbfb

Browse files
Copilotmoshloop
andcommitted
Add controller-runtime logging adapter with level shifting and default warn level
- Add logging.go with levelShiftHandler that adapts controller-runtime logs to flanksource/commons/logger, shifting levels down (info→debug, debug→trace, error→warn) via slog.Handler wrapper - Add NewControllerRuntimeLogger() using commons/logger with default warn level - Update manager.go to use the new adapter instead of logr.Discard() - Add comprehensive logging_test.go verifying level shifting, suppression, WithAttrs/WithGroup propagation, and default warn level behavior Co-authored-by: moshloop <1489660+moshloop@users.noreply.github.com>
1 parent 973b988 commit 5debbfb

File tree

3 files changed

+273
-3
lines changed

3 files changed

+273
-3
lines changed

logging.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package kopper
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
7+
"github.com/flanksource/commons/logger"
8+
"github.com/go-logr/logr"
9+
)
10+
11+
// slogLevelShift is the amount to shift slog levels by when adapting
12+
// controller-runtime logs to flanksource/commons/logger levels.
13+
// A shift of -4 maps:
14+
// - controller-runtime Error (slog 8) → commons Warn (slog 4)
15+
// - controller-runtime Info/V(0) (slog 0) → commons Debug (slog -4)
16+
// - controller-runtime V(1) (slog -1) → commons Trace (slog -5)
17+
const slogLevelShift = slog.Level(-4)
18+
19+
// levelShiftHandler wraps an slog.Handler and shifts all log levels down
20+
// so that controller-runtime logs are mapped to appropriate commons/logger levels.
21+
type levelShiftHandler struct {
22+
handler slog.Handler
23+
}
24+
25+
func (h *levelShiftHandler) Enabled(ctx context.Context, level slog.Level) bool {
26+
return h.handler.Enabled(ctx, level+slogLevelShift)
27+
}
28+
29+
func (h *levelShiftHandler) Handle(ctx context.Context, record slog.Record) error {
30+
record.Level = record.Level + slogLevelShift
31+
return h.handler.Handle(ctx, record)
32+
}
33+
34+
func (h *levelShiftHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
35+
return &levelShiftHandler{handler: h.handler.WithAttrs(attrs)}
36+
}
37+
38+
func (h *levelShiftHandler) WithGroup(name string) slog.Handler {
39+
return &levelShiftHandler{handler: h.handler.WithGroup(name)}
40+
}
41+
42+
// NewControllerRuntimeLogger creates a logr.Logger for controller-runtime
43+
// that routes logs through flanksource/commons/logger with level shifting.
44+
// The default log level is set to warn, suppressing most controller-runtime
45+
// noise while allowing important messages through.
46+
func NewControllerRuntimeLogger() logr.Logger {
47+
l := logger.GetLogger("controller-runtime")
48+
l.SetLogLevel(logger.Warn)
49+
slogLogger := l.GetSlogLogger()
50+
handler := &levelShiftHandler{
51+
handler: slogLogger.Handler(),
52+
}
53+
return logr.FromSlogHandler(handler)
54+
}

logging_test.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package kopper
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"log/slog"
7+
"strings"
8+
"testing"
9+
10+
"github.com/go-logr/logr"
11+
)
12+
13+
func TestLevelShiftHandler(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
baseLevel slog.Level
17+
logFunc func(logr.Logger)
18+
expectLogged bool
19+
expectLevel string
20+
}{
21+
{
22+
name: "info shifted to debug, base at debug level",
23+
baseLevel: slog.LevelDebug,
24+
logFunc: func(l logr.Logger) { l.Info("test info message") },
25+
expectLogged: true,
26+
expectLevel: "DEBUG",
27+
},
28+
{
29+
name: "info shifted to debug, base at warn - suppressed",
30+
baseLevel: slog.LevelWarn,
31+
logFunc: func(l logr.Logger) { l.Info("test info message") },
32+
expectLogged: false,
33+
},
34+
{
35+
name: "error shifted to warn, base at warn - logged",
36+
baseLevel: slog.LevelWarn,
37+
logFunc: func(l logr.Logger) { l.Error(nil, "error message") },
38+
expectLogged: true,
39+
expectLevel: "WARN",
40+
},
41+
{
42+
name: "error always logged via logr (bypasses level check)",
43+
baseLevel: slog.LevelError,
44+
logFunc: func(l logr.Logger) { l.Error(nil, "error message") },
45+
expectLogged: true,
46+
expectLevel: "WARN",
47+
},
48+
{
49+
name: "V(1) shifted to trace, base at trace level",
50+
baseLevel: slog.LevelDebug - 1, // trace level
51+
logFunc: func(l logr.Logger) { l.V(1).Info("debug message") },
52+
expectLogged: true,
53+
},
54+
{
55+
name: "V(1) shifted to trace, base at warn - suppressed",
56+
baseLevel: slog.LevelWarn,
57+
logFunc: func(l logr.Logger) { l.V(1).Info("debug message") },
58+
expectLogged: false,
59+
},
60+
{
61+
name: "V(1) shifted to trace, base at debug - suppressed",
62+
baseLevel: slog.LevelDebug,
63+
logFunc: func(l logr.Logger) { l.V(1).Info("debug message") },
64+
expectLogged: false,
65+
},
66+
}
67+
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
var buf bytes.Buffer
71+
baseHandler := slog.NewTextHandler(&buf, &slog.HandlerOptions{
72+
Level: tt.baseLevel,
73+
})
74+
handler := &levelShiftHandler{handler: baseHandler}
75+
logger := logr.FromSlogHandler(handler)
76+
77+
tt.logFunc(logger)
78+
79+
logged := buf.Len() > 0
80+
if logged != tt.expectLogged {
81+
t.Errorf("expected logged=%v, got logged=%v, output: %q", tt.expectLogged, logged, buf.String())
82+
}
83+
84+
if tt.expectLogged && tt.expectLevel != "" {
85+
if !strings.Contains(buf.String(), "level="+tt.expectLevel) {
86+
t.Errorf("expected level=%s in output, got: %q", tt.expectLevel, buf.String())
87+
}
88+
}
89+
})
90+
}
91+
}
92+
93+
func TestLevelShiftHandlerEnabled(t *testing.T) {
94+
tests := []struct {
95+
name string
96+
baseLevel slog.Level
97+
testLevel slog.Level
98+
expected bool
99+
}{
100+
{
101+
name: "info enabled when base is debug",
102+
baseLevel: slog.LevelDebug,
103+
testLevel: slog.LevelInfo,
104+
expected: true,
105+
},
106+
{
107+
name: "info disabled when base is warn (info shifts to debug)",
108+
baseLevel: slog.LevelWarn,
109+
testLevel: slog.LevelInfo,
110+
expected: false,
111+
},
112+
{
113+
name: "error enabled when base is warn (error shifts to warn)",
114+
baseLevel: slog.LevelWarn,
115+
testLevel: slog.LevelError,
116+
expected: true,
117+
},
118+
{
119+
name: "error disabled when base is error+1 (error shifts to warn)",
120+
baseLevel: slog.LevelError + 1,
121+
testLevel: slog.LevelError,
122+
expected: false,
123+
},
124+
}
125+
126+
for _, tt := range tests {
127+
t.Run(tt.name, func(t *testing.T) {
128+
baseHandler := slog.NewTextHandler(&bytes.Buffer{}, &slog.HandlerOptions{
129+
Level: tt.baseLevel,
130+
})
131+
handler := &levelShiftHandler{handler: baseHandler}
132+
133+
result := handler.Enabled(context.Background(), tt.testLevel)
134+
if result != tt.expected {
135+
t.Errorf("Enabled(%v) = %v, want %v (base level: %v, shifted: %v)",
136+
tt.testLevel, result, tt.expected, tt.baseLevel, tt.testLevel+slogLevelShift)
137+
}
138+
})
139+
}
140+
}
141+
142+
func TestLevelShiftHandlerWithAttrs(t *testing.T) {
143+
var buf bytes.Buffer
144+
baseHandler := slog.NewTextHandler(&buf, &slog.HandlerOptions{
145+
Level: slog.LevelDebug,
146+
})
147+
handler := &levelShiftHandler{handler: baseHandler}
148+
149+
// WithAttrs should preserve the level shift
150+
attrHandler := handler.WithAttrs([]slog.Attr{slog.String("key", "value")})
151+
logger := logr.FromSlogHandler(attrHandler)
152+
153+
logger.Info("test with attrs")
154+
155+
output := buf.String()
156+
if !strings.Contains(output, "key=value") {
157+
t.Errorf("expected attrs in output, got: %q", output)
158+
}
159+
if !strings.Contains(output, "level=DEBUG") {
160+
t.Errorf("expected level=DEBUG (shifted from info), got: %q", output)
161+
}
162+
}
163+
164+
func TestLevelShiftHandlerWithGroup(t *testing.T) {
165+
var buf bytes.Buffer
166+
baseHandler := slog.NewTextHandler(&buf, &slog.HandlerOptions{
167+
Level: slog.LevelDebug,
168+
})
169+
handler := &levelShiftHandler{handler: baseHandler}
170+
171+
// WithGroup should preserve the level shift
172+
groupHandler := handler.WithGroup("testgroup")
173+
logger := logr.FromSlogHandler(groupHandler)
174+
175+
logger.Info("test with group", "nested", "val")
176+
177+
output := buf.String()
178+
if !strings.Contains(output, "testgroup.nested=val") {
179+
t.Errorf("expected grouped attrs in output, got: %q", output)
180+
}
181+
if !strings.Contains(output, "level=DEBUG") {
182+
t.Errorf("expected level=DEBUG (shifted from info), got: %q", output)
183+
}
184+
}
185+
186+
func TestDefaultWarnLevelSuppression(t *testing.T) {
187+
// Simulate the default warn level behavior:
188+
// With warn level, only controller-runtime errors (shifted to warn) should pass through.
189+
var buf bytes.Buffer
190+
baseHandler := slog.NewTextHandler(&buf, &slog.HandlerOptions{
191+
Level: slog.LevelWarn,
192+
})
193+
handler := &levelShiftHandler{handler: baseHandler}
194+
logger := logr.FromSlogHandler(handler)
195+
196+
// Info should be suppressed (shifted to debug, below warn)
197+
logger.Info("info message")
198+
if buf.Len() > 0 {
199+
t.Errorf("info message should be suppressed at warn level, got: %q", buf.String())
200+
}
201+
202+
// V(1) debug should be suppressed (shifted to trace, below warn)
203+
logger.V(1).Info("debug message")
204+
if buf.Len() > 0 {
205+
t.Errorf("V(1) debug message should be suppressed at warn level, got: %q", buf.String())
206+
}
207+
208+
// Error should be logged (shifted to warn, equals warn)
209+
logger.Error(nil, "error message")
210+
if buf.Len() == 0 {
211+
t.Error("error message should be logged at warn level")
212+
}
213+
if !strings.Contains(buf.String(), "error message") {
214+
t.Errorf("expected error message in output, got: %q", buf.String())
215+
}
216+
}

manager.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55

66
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
77
// to ensure that exec-entrypoint and run can make use of them.
8-
"github.com/go-logr/logr"
98
_ "k8s.io/client-go/plugin/pkg/client/auth"
109

1110
"k8s.io/apimachinery/pkg/runtime"
@@ -37,12 +36,13 @@ func Manager(opts *ManagerOptions) (manager.Manager, error) {
3736

3837
utilruntime.Must(opts.AddToSchemeFunc(scheme))
3938

40-
logf.SetLogger(logr.Discard())
39+
crLogger := NewControllerRuntimeLogger()
40+
logf.SetLogger(crLogger)
4141
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
4242
Scheme: scheme,
4343
LeaderElection: len(opts.LeaderElectionID) > 0,
4444
LeaderElectionID: opts.LeaderElectionID,
45-
Logger: logr.Discard(),
45+
Logger: crLogger,
4646
Metrics: ctrlMetrics.Options{
4747
BindAddress: "0",
4848
},

0 commit comments

Comments
 (0)