Merge pull request #1853 from kunalk16/feat-configurable-logger
feat(logging): add configurability for log levels preference
This commit is contained in:
commit
931eee92a0
9 changed files with 179 additions and 11 deletions
|
|
@ -23,16 +23,16 @@ func agentCmd(message, sessionKey, model string, debug bool) error {
|
||||||
sessionKey = "cli:default"
|
sessionKey = "cli:default"
|
||||||
}
|
}
|
||||||
|
|
||||||
if debug {
|
|
||||||
logger.SetLevel(logger.DEBUG)
|
|
||||||
fmt.Println("🔍 Debug mode enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := internal.LoadConfig()
|
cfg, err := internal.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error loading config: %w", err)
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
logger.SetLevel(logger.DEBUG)
|
||||||
|
fmt.Println("🔍 Debug mode enabled")
|
||||||
|
}
|
||||||
|
|
||||||
if model != "" {
|
if model != "" {
|
||||||
cfg.Agents.Defaults.ModelName = model
|
cfg.Agents.Defaults.ModelName = model
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Logo = "🦞"
|
const Logo = "🦞"
|
||||||
|
|
@ -27,7 +28,12 @@ func GetConfigPath() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() (*config.Config, error) {
|
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
|
// FormatVersion returns the version string with optional git commit
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
|
"log_level": "fatal",
|
||||||
"workspace": "~/.picoclaw/workspace",
|
"workspace": "~/.picoclaw/workspace",
|
||||||
"restrict_to_workspace": true,
|
"restrict_to_workspace": true,
|
||||||
"model_name": "gpt-5.4",
|
"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"`
|
MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"`
|
||||||
Routing *RoutingConfig `json:"routing,omitempty"`
|
Routing *RoutingConfig `json:"routing,omitempty"`
|
||||||
ToolFeedback ToolFeedbackConfig `json:"tool_feedback,omitempty"`
|
ToolFeedback ToolFeedbackConfig `json:"tool_feedback,omitempty"`
|
||||||
|
LogLevel string `json:"log_level,omitempty" env:"PICOCLAW_LOG_LEVEL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
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) {
|
func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
configPath := filepath.Join(dir, "config.json")
|
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)
|
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{
|
return &Config{
|
||||||
Agents: AgentsConfig{
|
Agents: AgentsConfig{
|
||||||
Defaults: AgentDefaults{
|
Defaults: AgentDefaults{
|
||||||
|
LogLevel: "fatal",
|
||||||
Workspace: workspacePath,
|
Workspace: workspacePath,
|
||||||
RestrictToWorkspace: true,
|
RestrictToWorkspace: true,
|
||||||
Provider: "",
|
Provider: "",
|
||||||
|
|
|
||||||
|
|
@ -79,16 +79,18 @@ func (p *startupBlockedProvider) GetDefaultModel() string {
|
||||||
|
|
||||||
// Run starts the gateway runtime using the configuration loaded from configPath.
|
// Run starts the gateway runtime using the configuration loaded from configPath.
|
||||||
func Run(debug bool, configPath string, allowEmptyStartup bool) error {
|
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)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error loading config: %w", err)
|
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)
|
provider, modelID, err := createStartupProvider(cfg, allowEmptyStartup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating provider: %w", err)
|
return fmt.Errorf("error creating provider: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,36 @@ func GetLevel() LogLevel {
|
||||||
return currentLevel
|
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 {
|
func EnableFileLogging(filePath string) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue