mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Merge pull request #1853 from kunalk16/feat-configurable-logger
feat(logging): add configurability for log levels preference
This commit is contained in:
@@ -23,16 +23,16 @@ func agentCmd(message, sessionKey, model string, debug bool) error {
|
||||
sessionKey = "cli:default"
|
||||
}
|
||||
|
||||
if debug {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("🔍 Debug mode enabled")
|
||||
}
|
||||
|
||||
cfg, err := internal.LoadConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading config: %w", err)
|
||||
}
|
||||
|
||||
if debug {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("🔍 Debug mode enabled")
|
||||
}
|
||||
|
||||
if model != "" {
|
||||
cfg.Agents.Defaults.ModelName = model
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
const Logo = "🦞"
|
||||
@@ -27,7 +28,12 @@ func GetConfigPath() string {
|
||||
}
|
||||
|
||||
func LoadConfig() (*config.Config, error) {
|
||||
return config.LoadConfig(GetConfigPath())
|
||||
cfg, err := config.LoadConfig(GetConfigPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.SetLevelFromString(cfg.Agents.Defaults.LogLevel)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// FormatVersion returns the version string with optional git commit
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"log_level": "fatal",
|
||||
"workspace": "~/.picoclaw/workspace",
|
||||
"restrict_to_workspace": true,
|
||||
"model_name": "gpt-5.4",
|
||||
|
||||
@@ -245,6 +245,7 @@ type AgentDefaults struct {
|
||||
MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"`
|
||||
Routing *RoutingConfig `json:"routing,omitempty"`
|
||||
ToolFeedback ToolFeedbackConfig `json:"tool_feedback,omitempty"`
|
||||
LogLevel string `json:"log_level,omitempty" env:"PICOCLAW_LOG_LEVEL"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -470,6 +470,13 @@ func TestDefaultConfig_CronAllowCommandEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig_LogLevel(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if cfg.Agents.Defaults.LogLevel != "fatal" {
|
||||
t.Errorf("LogLevel = %q, want \"fatal\"", cfg.Agents.Defaults.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
@@ -1057,3 +1064,38 @@ func TestLoadConfig_UsesPassphraseProvider(t *testing.T) {
|
||||
t.Errorf("api_key = %q, want %q", cfg.ModelList[0].APIKey, plainKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigParsesLogLevel(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
data := `{"agents":{"defaults":{"log_level":"debug"}}}`
|
||||
if err := os.WriteFile(cfgPath, []byte(data), 0o600); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig: %v", err)
|
||||
}
|
||||
if cfg.Agents.Defaults.LogLevel != "debug" {
|
||||
t.Errorf("LogLevel = %q, want \"debug\"", cfg.Agents.Defaults.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLogLevelEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
data := `{}`
|
||||
if err := os.WriteFile(cfgPath, []byte(data), 0o600); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig: %v", err)
|
||||
}
|
||||
// When config omits log_level, the DefaultConfig value ("fatal") is preserved.
|
||||
if cfg.Agents.Defaults.LogLevel != "fatal" {
|
||||
t.Errorf("LogLevel = %q, want \"fatal\"", cfg.Agents.Defaults.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Agents: AgentsConfig{
|
||||
Defaults: AgentDefaults{
|
||||
LogLevel: "fatal",
|
||||
Workspace: workspacePath,
|
||||
RestrictToWorkspace: true,
|
||||
Provider: "",
|
||||
|
||||
@@ -79,16 +79,18 @@ func (p *startupBlockedProvider) GetDefaultModel() string {
|
||||
|
||||
// Run starts the gateway runtime using the configuration loaded from configPath.
|
||||
func Run(debug bool, configPath string, allowEmptyStartup bool) error {
|
||||
if debug {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("🔍 Debug mode enabled")
|
||||
}
|
||||
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading config: %w", err)
|
||||
}
|
||||
|
||||
logger.SetLevelFromString(cfg.Agents.Defaults.LogLevel)
|
||||
|
||||
if debug {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("🔍 Debug mode enabled")
|
||||
}
|
||||
|
||||
provider, modelID, err := createStartupProvider(cfg, allowEmptyStartup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating provider: %w", err)
|
||||
|
||||
@@ -106,6 +106,36 @@ func GetLevel() LogLevel {
|
||||
return currentLevel
|
||||
}
|
||||
|
||||
// ParseLevel converts a case-insensitive level name to a LogLevel.
|
||||
// Returns the level and true if valid, or (INFO, false) if unrecognized.
|
||||
func ParseLevel(s string) (LogLevel, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||
case "debug":
|
||||
return DEBUG, true
|
||||
case "info":
|
||||
return INFO, true
|
||||
case "warn", "warning":
|
||||
return WARN, true
|
||||
case "error":
|
||||
return ERROR, true
|
||||
case "fatal":
|
||||
return FATAL, true
|
||||
default:
|
||||
return INFO, false
|
||||
}
|
||||
}
|
||||
|
||||
// SetLevelFromString sets the log level from a string value.
|
||||
// If the string is empty or not a recognized level name, the current level is kept.
|
||||
func SetLevelFromString(s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
if level, ok := ParseLevel(s); ok {
|
||||
SetLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
func EnableFileLogging(filePath string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
@@ -252,3 +252,88 @@ func TestFormatFieldValue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLevelIsInfo(t *testing.T) {
|
||||
// The package-level default (before any SetLevel call) should be INFO.
|
||||
// Because earlier tests may have changed it, we just verify the constant is wired correctly.
|
||||
if logLevelNames[INFO] != "INFO" {
|
||||
t.Errorf("INFO constant mapped to %q, want \"INFO\"", logLevelNames[INFO])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLevelValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want LogLevel
|
||||
}{
|
||||
{"debug", DEBUG},
|
||||
{"DEBUG", DEBUG},
|
||||
{"Debug", DEBUG},
|
||||
{"info", INFO},
|
||||
{"INFO", INFO},
|
||||
{"warn", WARN},
|
||||
{"WARN", WARN},
|
||||
{"warning", WARN},
|
||||
{"WARNING", WARN},
|
||||
{"error", ERROR},
|
||||
{"ERROR", ERROR},
|
||||
{"fatal", FATAL},
|
||||
{"FATAL", FATAL},
|
||||
{" info ", INFO},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got, ok := ParseLevel(tt.input)
|
||||
if !ok {
|
||||
t.Fatalf("ParseLevel(%q) returned ok=false, want true", tt.input)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ParseLevel(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLevelInvalid(t *testing.T) {
|
||||
tests := []string{"", "garbage", "verbose", "trace", "critical"}
|
||||
|
||||
for _, input := range tests {
|
||||
t.Run(input, func(t *testing.T) {
|
||||
_, ok := ParseLevel(input)
|
||||
if ok {
|
||||
t.Errorf("ParseLevel(%q) returned ok=true, want false", input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLevelFromString(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
// Valid string changes the level
|
||||
SetLevel(INFO)
|
||||
SetLevelFromString("error")
|
||||
if got := GetLevel(); got != ERROR {
|
||||
t.Errorf("after SetLevelFromString(\"error\"): GetLevel() = %v, want ERROR", got)
|
||||
}
|
||||
|
||||
// Empty string is a no-op
|
||||
SetLevelFromString("")
|
||||
if got := GetLevel(); got != ERROR {
|
||||
t.Errorf("after SetLevelFromString(\"\"): GetLevel() = %v, want ERROR (unchanged)", got)
|
||||
}
|
||||
|
||||
// Invalid string is a no-op
|
||||
SetLevelFromString("garbage")
|
||||
if got := GetLevel(); got != ERROR {
|
||||
t.Errorf("after SetLevelFromString(\"garbage\"): GetLevel() = %v, want ERROR (unchanged)", got)
|
||||
}
|
||||
|
||||
// Case-insensitive
|
||||
SetLevelFromString("FATAL")
|
||||
if got := GetLevel(); got != FATAL {
|
||||
t.Errorf("after SetLevelFromString(\"FATAL\"): GetLevel() = %v, want FATAL", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user