Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 20 additions & 1 deletion internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"log/slog"
"os"
"strings"

"go.opentelemetry.io/otel/trace"
)
Expand All @@ -12,19 +13,37 @@ 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.
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
Expand Down
27 changes: 27 additions & 0 deletions internal/logger/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
}