diff --git a/cmd/server/main.go b/cmd/server/main.go index 770e3ea..579b924 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -63,8 +63,8 @@ func main() { } // Create logs directory - if err := os.MkdirAll(cfg.Instances.Logging.LogsDir, 0755); err != nil { - log.Printf("Error creating log directory %s: %v\nInstance logs will not be available.", cfg.Instances.Logging.LogsDir, err) + if err := os.MkdirAll(cfg.Instances.LogsDir, 0755); err != nil { + log.Printf("Error creating log directory %s: %v\nInstance logs will not be available.", cfg.Instances.LogsDir, err) } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 6a06098..fdb5e30 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -88,21 +88,11 @@ type DatabaseConfig struct { ConnMaxLifetime time.Duration `yaml:"connection_max_lifetime" json:"connection_max_lifetime" swaggertype:"string" example:"1h"` } -// LoggingConfig contains all logging-related configuration for instances -type LoggingConfig struct { - // Logs directory override (relative to data_dir if not absolute) - LogsDir string `yaml:"logs_dir" json:"logs_dir"` - - // Log rotation configuration - LogRotation LogRotationConfig `yaml:"log_rotation" json:"log_rotation"` -} - // LogRotationConfig contains log rotation settings for instances type LogRotationConfig struct { - Enabled bool `yaml:"enabled" default:"true"` - MaxSizeMB int `yaml:"max_size_mb" default:"100"` // MB - MaxBackups int `yaml:"max_backups" default:"3"` - Compress bool `yaml:"compress" default:"false"` + Enabled bool `yaml:"enabled" default:"true"` + MaxSizeMB int `yaml:"max_size_mb" default:"100"` // MB + Compress bool `yaml:"compress" default:"false"` } // InstancesConfig contains instance management configuration @@ -143,8 +133,17 @@ type InstancesConfig struct { // Interval for checking instance timeouts (in minutes) TimeoutCheckInterval int `yaml:"timeout_check_interval" json:"timeout_check_interval"` - // Logging configuration - Logging LoggingConfig `yaml:"logging" json:"logging"` + // Logs directory override (relative to data_dir if not absolute) + LogsDir string `yaml:"logs_dir" json:"logs_dir"` + + // Log rotation enabled + LogRotationEnabled bool `yaml:"log_rotation_enabled" default:"true"` + + // Maximum log file size in MB before rotation + LogRotationMaxSizeMB int `yaml:"log_rotation_max_size_mb" default:"100"` + + // Whether to compress rotated log files + LogRotationCompress bool `yaml:"log_rotation_compress" default:"false"` } // AuthConfig contains authentication settings @@ -235,15 +234,10 @@ func LoadConfig(configPath string) (AppConfig, error) { DefaultOnDemandStart: true, OnDemandStartTimeout: 120, // 2 minutes TimeoutCheckInterval: 5, // Check timeouts every 5 minutes - Logging: LoggingConfig{ - LogsDir: "", // Will be set to data_dir/logs if empty - LogRotation: LogRotationConfig{ - Enabled: true, - MaxSizeMB: 100, - MaxBackups: 3, - Compress: false, - }, - }, + LogsDir: "", // Will be set to data_dir/logs if empty + LogRotationEnabled: true, + LogRotationMaxSizeMB: 100, + LogRotationCompress: false, }, Database: DatabaseConfig{ Path: "", // Will be set to data_dir/llamactl.db if empty @@ -285,8 +279,8 @@ func LoadConfig(configPath string) (AppConfig, error) { // Log deprecation warning if using custom instances dir log.Println("⚠️ Instances directory is deprecated and will be removed in future versions. Instances are persisted in the database.") } - if cfg.Instances.Logging.LogsDir == "" { - cfg.Instances.Logging.LogsDir = filepath.Join(cfg.DataDir, "logs") + if cfg.Instances.LogsDir == "" { + cfg.Instances.LogsDir = filepath.Join(cfg.DataDir, "logs") } if cfg.Database.Path == "" { cfg.Database.Path = filepath.Join(cfg.DataDir, "llamactl.db") @@ -353,7 +347,7 @@ func loadEnvVars(cfg *AppConfig) { cfg.Instances.InstancesDir = instancesDir } if logsDir := os.Getenv("LLAMACTL_LOGS_DIR"); logsDir != "" { - cfg.Instances.Logging.LogsDir = logsDir + cfg.Instances.LogsDir = logsDir } if autoCreate := os.Getenv("LLAMACTL_AUTO_CREATE_DATA_DIR"); autoCreate != "" { if b, err := strconv.ParseBool(autoCreate); err == nil { @@ -574,6 +568,23 @@ func loadEnvVars(cfg *AppConfig) { cfg.Database.ConnMaxLifetime = d } } + + // Log rotation config + if logRotationEnabled := os.Getenv("LLAMACTL_LOG_ROTATION_ENABLED"); logRotationEnabled != "" { + if b, err := strconv.ParseBool(logRotationEnabled); err == nil { + cfg.Instances.LogRotationEnabled = b + } + } + if logRotationMaxSizeMB := os.Getenv("LLAMACTL_LOG_ROTATION_MAX_SIZE_MB"); logRotationMaxSizeMB != "" { + if m, err := strconv.Atoi(logRotationMaxSizeMB); err == nil { + cfg.Instances.LogRotationMaxSizeMB = m + } + } + if logRotationCompress := os.Getenv("LLAMACTL_LOG_ROTATION_COMPRESS"); logRotationCompress != "" { + if b, err := strconv.ParseBool(logRotationCompress); err == nil { + cfg.Instances.LogRotationCompress = b + } + } } // ParsePortRange parses port range from string formats like "8000-9000" or "8000,9000" diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 71fc6ae..e90a24f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -44,8 +44,8 @@ func TestLoadConfig_Defaults(t *testing.T) { if cfg.Instances.InstancesDir != filepath.Join(homedir, ".local", "share", "llamactl", "instances") { t.Errorf("Expected default instances directory '%s', got %q", filepath.Join(homedir, ".local", "share", "llamactl", "instances"), cfg.Instances.InstancesDir) } - if cfg.Instances.Logging.LogsDir != filepath.Join(homedir, ".local", "share", "llamactl", "logs") { - t.Errorf("Expected default logs directory '%s', got %q", filepath.Join(homedir, ".local", "share", "llamactl", "logs"), cfg.Instances.Logging.LogsDir) + if cfg.Instances.LogsDir != filepath.Join(homedir, ".local", "share", "llamactl", "logs") { + t.Errorf("Expected default logs directory '%s', got %q", filepath.Join(homedir, ".local", "share", "llamactl", "logs"), cfg.Instances.LogsDir) } if !cfg.Instances.AutoCreateDirs { t.Error("Expected default instances auto-create to be true") @@ -79,8 +79,7 @@ server: instances: port_range: [7000, 8000] max_instances: 5 - logging: - logs_dir: "/custom/logs" + logs_dir: "/custom/logs" llama_executable: "/usr/bin/llama-server" default_auto_restart: false default_max_restarts: 10 @@ -107,8 +106,8 @@ instances: if cfg.Instances.PortRange != [2]int{7000, 8000} { t.Errorf("Expected port range [7000, 8000], got %v", cfg.Instances.PortRange) } - if cfg.Instances.Logging.LogsDir != "/custom/logs" { - t.Errorf("Expected logs directory '/custom/logs', got %q", cfg.Instances.Logging.LogsDir) + if cfg.Instances.LogsDir != "/custom/logs" { + t.Errorf("Expected logs directory '/custom/logs', got %q", cfg.Instances.LogsDir) } if cfg.Instances.MaxInstances != 5 { t.Errorf("Expected max instances 5, got %d", cfg.Instances.MaxInstances) @@ -158,8 +157,8 @@ func TestLoadConfig_EnvironmentOverrides(t *testing.T) { if cfg.Instances.PortRange != [2]int{5000, 6000} { t.Errorf("Expected port range [5000, 6000], got %v", cfg.Instances.PortRange) } - if cfg.Instances.Logging.LogsDir != "/env/logs" { - t.Errorf("Expected logs directory '/env/logs', got %q", cfg.Instances.Logging.LogsDir) + if cfg.Instances.LogsDir != "/env/logs" { + t.Errorf("Expected logs directory '/env/logs', got %q", cfg.Instances.LogsDir) } if cfg.Instances.MaxInstances != 20 { t.Errorf("Expected max instances 20, got %d", cfg.Instances.MaxInstances) diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index 6b3a259..d2e9d8d 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -68,10 +68,15 @@ func New(name string, globalConfig *config.AppConfig, opts *Options, onStatusCha // Only create logger, proxy, and process for local instances if !instance.IsRemote() { + logRotationConfig := &config.LogRotationConfig{ + Enabled: globalInstanceSettings.LogRotationEnabled, + MaxSizeMB: globalInstanceSettings.LogRotationMaxSizeMB, + Compress: globalInstanceSettings.LogRotationCompress, + } instance.logger = newLogger( name, - globalInstanceSettings.Logging.LogsDir, - &globalInstanceSettings.Logging.LogRotation, + globalInstanceSettings.LogsDir, + logRotationConfig, ) instance.process = newProcess(instance) } diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go index 351feed..e7ee3e3 100644 --- a/pkg/instance/instance_test.go +++ b/pkg/instance/instance_test.go @@ -27,10 +27,8 @@ func TestNewInstance(t *testing.T) { }, }, Instances: config.InstancesConfig{ - DefaultAutoRestart: true, - Logging: config.LoggingConfig{ - LogsDir: "/tmp/test", - }, + DefaultAutoRestart: true, + LogsDir: "/tmp/test", DefaultMaxRestarts: 3, DefaultRestartDelay: 5, }, @@ -122,10 +120,8 @@ func TestSetOptions(t *testing.T) { }, }, Instances: config.InstancesConfig{ - DefaultAutoRestart: true, - Logging: config.LoggingConfig{ - LogsDir: "/tmp/test", - }, + DefaultAutoRestart: true, + LogsDir: "/tmp/test", DefaultMaxRestarts: 3, DefaultRestartDelay: 5, }, @@ -180,7 +176,7 @@ func TestMarshalJSON(t *testing.T) { Backends: config.BackendConfig{ LlamaCpp: config.BackendSettings{Command: "llama-server"}, }, - Instances: config.InstancesConfig{Logging: config.LoggingConfig{LogsDir: "/tmp/test"}}, + Instances: config.InstancesConfig{LogsDir: "/tmp/test"}, Nodes: map[string]config.NodeConfig{}, LocalNode: "main", } @@ -317,9 +313,7 @@ func TestCreateOptionsValidation(t *testing.T) { }, }, Instances: config.InstancesConfig{ - Logging: config.LoggingConfig{ - LogsDir: "/tmp/test", - }, + LogsDir: "/tmp/test", }, Nodes: map[string]config.NodeConfig{}, LocalNode: "main", @@ -364,7 +358,7 @@ func TestStatusChangeCallback(t *testing.T) { Backends: config.BackendConfig{ LlamaCpp: config.BackendSettings{Command: "llama-server"}, }, - Instances: config.InstancesConfig{Logging: config.LoggingConfig{LogsDir: "/tmp/test"}}, + Instances: config.InstancesConfig{LogsDir: "/tmp/test"}, Nodes: map[string]config.NodeConfig{}, LocalNode: "main", } @@ -406,7 +400,7 @@ func TestSetOptions_NodesPreserved(t *testing.T) { Backends: config.BackendConfig{ LlamaCpp: config.BackendSettings{Command: "llama-server"}, }, - Instances: config.InstancesConfig{Logging: config.LoggingConfig{LogsDir: "/tmp/test"}}, + Instances: config.InstancesConfig{LogsDir: "/tmp/test"}, Nodes: map[string]config.NodeConfig{}, LocalNode: "main", } @@ -488,7 +482,7 @@ func TestProcessErrorCases(t *testing.T) { Backends: config.BackendConfig{ LlamaCpp: config.BackendSettings{Command: "llama-server"}, }, - Instances: config.InstancesConfig{Logging: config.LoggingConfig{LogsDir: "/tmp/test"}}, + Instances: config.InstancesConfig{LogsDir: "/tmp/test"}, Nodes: map[string]config.NodeConfig{}, LocalNode: "main", } @@ -524,7 +518,7 @@ func TestRemoteInstanceOperations(t *testing.T) { Backends: config.BackendConfig{ LlamaCpp: config.BackendSettings{Command: "llama-server"}, }, - Instances: config.InstancesConfig{Logging: config.LoggingConfig{LogsDir: "/tmp/test"}}, + Instances: config.InstancesConfig{LogsDir: "/tmp/test"}, Nodes: map[string]config.NodeConfig{ "remote-node": {Address: "http://remote-node:8080"}, }, @@ -572,7 +566,7 @@ func TestIdleTimeout(t *testing.T) { Backends: config.BackendConfig{ LlamaCpp: config.BackendSettings{Command: "llama-server"}, }, - Instances: config.InstancesConfig{Logging: config.LoggingConfig{LogsDir: "/tmp/test"}}, + Instances: config.InstancesConfig{LogsDir: "/tmp/test"}, Nodes: map[string]config.NodeConfig{}, LocalNode: "main", } diff --git a/pkg/instance/logger.go b/pkg/instance/logger.go index bca3fee..ef754a6 100644 --- a/pkg/instance/logger.go +++ b/pkg/instance/logger.go @@ -49,7 +49,7 @@ func (l *logger) create() error { t := &timber.Logger{ Filename: logPath, MaxSize: l.cfg.MaxSizeMB, - MaxBackups: l.cfg.MaxBackups, + MaxBackups: 0, // No limit on backups - use index-based naming // Compression: "gzip" if Compress is true, else "none" Compression: func() string { if l.cfg.Compress { @@ -57,8 +57,8 @@ func (l *logger) create() error { } return "none" }(), - FileMode: 0644, // default; timberjack uses 640 if 0 - LocalTime: true, // use local time for consistency with lumberjack + FileMode: 0644, // default; timberjack uses 640 if 0 + LocalTime: false, // Use index-based naming instead of timestamps } // If rotation is disabled, set MaxSize to 0 so no rotation occurs diff --git a/pkg/manager/manager_test.go b/pkg/manager/manager_test.go index a4dadd8..78ab04d 100644 --- a/pkg/manager/manager_test.go +++ b/pkg/manager/manager_test.go @@ -201,15 +201,13 @@ func createTestAppConfig(instancesDir string) *config.AppConfig { }, }, Instances: config.InstancesConfig{ - PortRange: [2]int{8000, 9000}, - InstancesDir: instancesDir, - MaxInstances: 10, - MaxRunningInstances: 10, - DefaultAutoRestart: true, - DefaultMaxRestarts: 3, - Logging: config.LoggingConfig{ - LogsDir: instancesDir, - }, + PortRange: [2]int{8000, 9000}, + InstancesDir: instancesDir, + MaxInstances: 10, + MaxRunningInstances: 10, + DefaultAutoRestart: true, + DefaultMaxRestarts: 3, + LogsDir: instancesDir, DefaultRestartDelay: 5, TimeoutCheckInterval: 5, },