package logger import ( "database/sql" "fmt" "os" "path/filepath" "time" _ "github.com/mattn/go-sqlite3" ) const ( LevelDebug = "debug" LevelInfo = "info" LevelError = "error" ) type Logger struct { db *sql.DB dbPath string retentionDays int } // New opens (or creates) logs.db inside logDir, applies the schema, and runs // an initial retention purge. retentionDays <= 0 falls back to 7. func New(logDir string, retentionDays int) (*Logger, error) { if retentionDays <= 0 { retentionDays = 7 } if err := os.MkdirAll(logDir, 0755); err != nil { return nil, fmt.Errorf("creating log directory: %w", err) } dbPath := filepath.Join(logDir, "logs.db") db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000") if err != nil { return nil, fmt.Errorf("opening log db: %w", err) } schema := ` CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, level TEXT NOT NULL, message TEXT NOT NULL, file TEXT, extra TEXT ); CREATE INDEX IF NOT EXISTS idx_logs_ts_level ON logs(ts, level); ` if _, err := db.Exec(schema); err != nil { db.Close() return nil, fmt.Errorf("applying log schema: %w", err) } l := &Logger{db: db, dbPath: dbPath, retentionDays: retentionDays} l.purge() return l, nil } func (l *Logger) purge() { cutoff := fmt.Sprintf("-%d days", l.retentionDays) _, _ = l.db.Exec(`DELETE FROM logs WHERE ts < datetime('now', ?)`, cutoff) } func (l *Logger) write(level, message, file, extra string) { _, _ = l.db.Exec( `INSERT INTO logs(level, message, file, extra) VALUES(?, ?, ?, ?)`, level, message, nullIfEmpty(file), nullIfEmpty(extra), ) } func nullIfEmpty(s string) interface{} { if s == "" { return nil } return s } func stamp() string { return time.Now().Format("2006-01-02 15:04:05") } func (l *Logger) Info(message string) { fmt.Fprintf(os.Stdout, "[%s] INFO: %s\n", stamp(), message) l.write(LevelInfo, message, "", "") } func (l *Logger) Error(message, errStr string) { msg := fmt.Sprintf("%s: %s", message, errStr) fmt.Fprintf(os.Stderr, "[%s] ERROR: %s\n", stamp(), msg) l.write(LevelError, msg, "", "") } func (l *Logger) ErrorFile(file, message, errStr string) { msg := fmt.Sprintf("%s: %s", message, errStr) fmt.Fprintf(os.Stderr, "[%s] ERROR: %s (file: %s)\n", stamp(), msg, file) l.write(LevelError, msg, file, "") } // Debug records a debug entry to the database only. extra is an opaque string, // typically JSON, used for ffprobe output, API response bodies, or command // stdout/stderr captures. Pass "" if not applicable. func (l *Logger) Debug(message, file, extra string) { l.write(LevelDebug, message, file, extra) } func (l *Logger) Close() { if l.db == nil { return } l.purge() l.db.Close() l.db = nil }