diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 38d45c1..c2ab987 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -300,7 +300,7 @@ The application respects these environment variables: - `PROMETHEUS_URL` - Prometheus server URL - `GRAFANA_URL` - Grafana server URL - `GRAFANA_API_KEY` - Grafana API authentication key -- `LOG_LEVEL` - Logging level (debug, info, warn, error) +- `LOG_LEVEL` - Logging level (debug, info, warn, error); unset or invalid values fall back to info ### Configuration Files diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 908a658..f8fd676 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -4,6 +4,7 @@ import ( "context" "log/slog" "os" + "strings" "go.opentelemetry.io/otel/trace" ) @@ -12,6 +13,10 @@ import ( // stderr, keeping stdout free for the MCP stdio protocol. const UseStderrEnv = "KAGENT_USE_STDERR" +// LogLevelEnv names the environment variable that sets the minimum log level +// (debug, info, warn or error). Unset or invalid values fall back to info. +const LogLevelEnv = "LOG_LEVEL" + var globalLogger *slog.Logger // UseStderr reports whether console output must go to stderr. @@ -19,12 +24,26 @@ func UseStderr() bool { return os.Getenv(UseStderrEnv) == "true" } +// levelFromEnv returns the level named by LOG_LEVEL, or info when it is unset +// or not a valid slog level. +func levelFromEnv() slog.Level { + value := strings.TrimSpace(os.Getenv(LogLevelEnv)) + if value == "" { + return slog.LevelInfo + } + var level slog.Level + if err := level.UnmarshalText([]byte(value)); err != nil { + return slog.LevelInfo + } + return level +} + // Init initializes the global logger // If useStderr is true, logs will be written to stderr (for stdio mode) // If useStderr is false, logs will be written to stdout (for HTTP mode) func Init(useStderr bool) { opts := &slog.HandlerOptions{ - Level: slog.LevelInfo, + Level: levelFromEnv(), } // Choose output destination based on mode diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index e4ca6d6..b170bb5 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -132,3 +132,30 @@ func TestUseStderr(t *testing.T) { t.Setenv(UseStderrEnv, "false") require.False(t, UseStderr()) } + +func TestInitHonorsLogLevel(t *testing.T) { + cases := []struct { + value string + wantDebug bool + wantInfo bool + wantWarn bool + }{ + {value: "", wantDebug: false, wantInfo: true, wantWarn: true}, + {value: "debug", wantDebug: true, wantInfo: true, wantWarn: true}, + {value: "DEBUG", wantDebug: true, wantInfo: true, wantWarn: true}, + {value: "info", wantDebug: false, wantInfo: true, wantWarn: true}, + {value: "warn", wantDebug: false, wantInfo: false, wantWarn: true}, + {value: "error", wantDebug: false, wantInfo: false, wantWarn: false}, + {value: "verbose", wantDebug: false, wantInfo: true, wantWarn: true}, + } + for _, tc := range cases { + t.Run("LOG_LEVEL="+tc.value, func(t *testing.T) { + t.Setenv("LOG_LEVEL", tc.value) + Init(true) + ctx := context.Background() + assert.Equal(t, tc.wantDebug, Get().Enabled(ctx, slog.LevelDebug), "debug") + assert.Equal(t, tc.wantInfo, Get().Enabled(ctx, slog.LevelInfo), "info") + assert.Equal(t, tc.wantWarn, Get().Enabled(ctx, slog.LevelWarn), "warn") + }) + } +}