Skip to content

Commit 7ee40ba

Browse files
committed
backend: pkg: add config flag and env var to configure log levels, update tests and development docs
Changes: - Added a backend configuration flag --log-level with environment variable support via HEADLAMP_CONFIG_LOG_LEVEL. - Ensured invalid, empty, or missing log level values safely fall back to info. - Added unit tests for both flag and environment based log level configuration. - Updated backend and development documentation to describe log level configuration.
1 parent f4551cd commit 7ee40ba

File tree

7 files changed

+174
-0
lines changed

7 files changed

+174
-0
lines changed

backend/cmd/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ func main() {
5555
os.Exit(1)
5656
}
5757

58+
logger.Init(conf.LogLevel)
59+
5860
if conf.Version {
5961
fmt.Printf("%s %s (%s/%s)\n", kubeconfig.AppName, kubeconfig.Version, runtime.GOOS, runtime.GOARCH)
6062
return

backend/pkg/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Config struct {
3737
InClusterContextName string `koanf:"in-cluster-context-name"`
3838
DevMode bool `koanf:"dev"`
3939
InsecureSsl bool `koanf:"insecure-ssl"`
40+
LogLevel string `koanf:"log-level"`
4041
// NoBrowser disables automatically opening the default browser when running
4142
// a locally embedded Headlamp binary (non in-cluster with spa.UseEmbeddedFiles == true).
4243
// It has no effect in in-cluster mode or when running without embedded frontend.
@@ -418,6 +419,7 @@ func addGeneralFlags(f *flag.FlagSet) {
418419
f.Bool("cache-enabled", false, "K8s cache in backend")
419420
f.Bool("no-browser", false, "Disable automatically opening the browser when using embedded frontend")
420421
f.Bool("insecure-ssl", false, "Accept/Ignore all server SSL certificates")
422+
f.String("log-level", "info", "Backend log level (debug, info, warn, error)")
421423
f.Bool("enable-dynamic-clusters", false, "Enable dynamic clusters, which stores stateless clusters in the frontend.")
422424
// Note: When running in-cluster and if not explicitly set, this flag defaults to false.
423425
f.Bool("watch-plugins-changes", true, "Reloads plugins when there are changes to them or their directory")

backend/pkg/config/config_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func TestParseBasic(t *testing.T) {
4444
assert.Equal(t, config.DefaultMeUsernamePath, conf.MeUsernamePath)
4545
assert.Equal(t, config.DefaultMeEmailPath, conf.MeEmailPath)
4646
assert.Equal(t, config.DefaultMeGroupsPath, conf.MeGroupsPath)
47+
assert.Equal(t, "info", conf.LogLevel)
4748
},
4849
},
4950
{
@@ -130,6 +131,16 @@ var ParseWithEnvTests = []struct {
130131
assert.Equal(t, "mycluster", conf.InClusterContextName)
131132
},
132133
},
134+
{
135+
name: "log_level_from_env",
136+
args: []string{"go run ./cmd"},
137+
env: map[string]string{
138+
"HEADLAMP_CONFIG_LOG_LEVEL": "warn",
139+
},
140+
verify: func(t *testing.T, conf *config.Config) {
141+
assert.Equal(t, "warn", conf.LogLevel)
142+
},
143+
},
133144
}
134145

135146
func TestParseWithEnv(t *testing.T) {
@@ -232,6 +243,13 @@ func TestParseFlags(t *testing.T) {
232243
assert.Equal(t, "mycluster", conf.InClusterContextName)
233244
},
234245
},
246+
{
247+
name: "log_level_flag",
248+
args: []string{"go run ./cmd", "--log-level=warn"},
249+
verify: func(t *testing.T, conf *config.Config) {
250+
assert.Equal(t, "warn", conf.LogLevel)
251+
},
252+
},
235253
}
236254

237255
for _, tt := range tests {

backend/pkg/logger/logger.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package logger
1818

1919
import (
2020
"runtime"
21+
"strings"
2122

2223
"github.com/rs/zerolog"
2324
zlog "github.com/rs/zerolog/log"
@@ -41,6 +42,35 @@ type LogFunc func(level uint, str map[string]string, err interface{}, msg string
4142
// logFunc holds the actual logging function.
4243
var logFunc LogFunc = log
4344

45+
// Init configures the global zerolog log level from environment variables.
46+
// The HEADLAMP_CONFIG_LOG_LEVEL environment variable controls the global log level.
47+
func Init(loglevel string) {
48+
logLevel := strings.ToLower(strings.TrimSpace(loglevel))
49+
50+
// If no log level is provided, default to info.
51+
if logLevel == "" {
52+
zerolog.SetGlobalLevel(zerolog.InfoLevel)
53+
return
54+
}
55+
56+
level, err := zerolog.ParseLevel(logLevel)
57+
// If an invalid log level is provided, log a warning and default to info.
58+
if err != nil {
59+
zlog.Warn().
60+
Str("value", logLevel).
61+
Msg("Invalid HEADLAMP_CONFIG_LOG_LEVEL, defaulting to info")
62+
zerolog.SetGlobalLevel(zerolog.InfoLevel)
63+
64+
return
65+
}
66+
67+
// Set the global log level.
68+
zerolog.SetGlobalLevel(level)
69+
zlog.Info().
70+
Str("level", level.String()).
71+
Msg("Log level set from HEADLAMP_CONFIG_LOG_LEVEL")
72+
}
73+
4474
// Log logs the message, source file, and line number at the specified level.
4575
func Log(level uint, str map[string]string, err interface{}, msg string) {
4676
logFunc(level, str, err, msg)

backend/pkg/logger/logger_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ package logger_test
1818

1919
import (
2020
"fmt"
21+
"os"
2122
"testing"
2223

2324
"github.com/kubernetes-sigs/headlamp/backend/pkg/logger"
25+
"github.com/rs/zerolog"
2426
)
2527

2628
var capturedLogs []string
@@ -83,3 +85,86 @@ func TestLog(t *testing.T) {
8385
capturedLogs = nil
8486
}
8587
}
88+
89+
// Sets global log level from HEADLAMP_CONFIG_LOG_LEVEL.
90+
func TestLogLevelsFromEnv(t *testing.T) {
91+
orig := zerolog.GlobalLevel()
92+
93+
t.Cleanup(func() {
94+
zerolog.SetGlobalLevel(orig)
95+
})
96+
97+
tests := []struct {
98+
name string
99+
envValue string
100+
expected zerolog.Level
101+
}{
102+
{"debug lowercase", "debug", zerolog.DebugLevel},
103+
{"info lowercase", "info", zerolog.InfoLevel},
104+
{"warn lowercase", "warn", zerolog.WarnLevel},
105+
{"error lowercase", "error", zerolog.ErrorLevel},
106+
107+
{"uppercase INFO", "INFO", zerolog.InfoLevel},
108+
{"mixed case Info", "Info", zerolog.InfoLevel},
109+
110+
{"leading whitespace", " warn", zerolog.WarnLevel},
111+
{"trailing whitespace", "error ", zerolog.ErrorLevel},
112+
{"both sides whitespace", " debug ", zerolog.DebugLevel},
113+
}
114+
115+
for _, tt := range tests {
116+
t.Run(tt.name, func(t *testing.T) {
117+
t.Setenv("HEADLAMP_CONFIG_LOG_LEVEL", tt.envValue)
118+
119+
logger.Init(os.Getenv("HEADLAMP_CONFIG_LOG_LEVEL"))
120+
121+
if got := zerolog.GlobalLevel(); got != tt.expected {
122+
t.Fatalf("expected %v, got %v", tt.expected, got)
123+
}
124+
})
125+
}
126+
}
127+
128+
// Falls back to info on invalid log level.
129+
func TestInvalidLevelDefaultsToInfo(t *testing.T) {
130+
orig := zerolog.GlobalLevel()
131+
132+
t.Cleanup(func() {
133+
zerolog.SetGlobalLevel(orig)
134+
})
135+
136+
t.Setenv("HEADLAMP_CONFIG_LOG_LEVEL", "not-a-level")
137+
138+
logger.Init(os.Getenv("HEADLAMP_CONFIG_LOG_LEVEL"))
139+
140+
if got := zerolog.GlobalLevel(); got != zerolog.InfoLevel {
141+
t.Fatalf("expected fallback to info, got %v", got)
142+
}
143+
}
144+
145+
// Defaults to info when env is empty or missing.
146+
func TestEmptyOrMissingEnvDefaultsToInfo(t *testing.T) {
147+
orig := zerolog.GlobalLevel()
148+
149+
t.Cleanup(func() {
150+
zerolog.SetGlobalLevel(orig)
151+
})
152+
153+
t.Run("empty", func(t *testing.T) {
154+
t.Setenv("HEADLAMP_CONFIG_LOG_LEVEL", "")
155+
logger.Init(os.Getenv("HEADLAMP_CONFIG_LOG_LEVEL"))
156+
157+
if zerolog.GlobalLevel() != zerolog.InfoLevel {
158+
t.Fatalf("expected info for empty env")
159+
}
160+
})
161+
162+
t.Run("missing", func(t *testing.T) {
163+
os.Unsetenv("HEADLAMP_CONFIG_LOG_LEVEL")
164+
logger.Init(os.Getenv("HEADLAMP_CONFIG_LOG_LEVEL"))
165+
166+
if zerolog.GlobalLevel() != zerolog.InfoLevel {
167+
t.Fatalf("expected info when env missing")
168+
}
169+
})
170+
}

docs/development/backend.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ Once built, it can be run in development mode (insecure / don't use in productio
2727
npm run backend:start
2828
```
2929

30+
## Logging configuration
31+
32+
Headlamp’s backend supports configurable log levels to control verbosity.
33+
34+
Log level can be configured using either a flag or an environment variable:
35+
- the log level: `--log-level` or env var `HEADLAMP_CONFIG_LOG_LEVEL`
36+
37+
Supported Values:
38+
- `debug`
39+
- `info` (default)
40+
- `warn`
41+
- `error`
42+
43+
> **Note:** Headlamp uses zerolog defaults.
44+
> Zerolog’s default debug level is `info`, and Headlamp follows this behavior.
45+
46+
### Examples
47+
48+
Run with warning level:
49+
50+
```bash
51+
./headlamp-server --log-level warn
52+
```
53+
3054
## Lint
3155

3256
To lint the backend/ code.

docs/development/index.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ and in a different terminal instance:
6060
npm run frontend:start
6161
```
6262

63+
### Backend logging
64+
65+
Backend log verbosity can be controlled using either a flag or an environment variable.
66+
- `--log-level`
67+
- `HEADLAMP_CONFIG_LOG_LEVEL`
68+
69+
Supported Values: `debug`, `info`, `warn`, `error` (default: `info`)
70+
71+
Example:
72+
```bash
73+
npm run backend:start -- --log-level warn
74+
```
75+
6376
## Generate API documentation
6477

6578
To generate the TypeScript API documentation:

0 commit comments

Comments
 (0)