package main import ( "fmt" "log/slog" "os" "path/filepath" "sync" ) // Logger writes structured logs to both stderr (dev) and a rotating file under the user // config dir. Acceso seguro entre goroutines via sync.Mutex. type Logger struct { mu sync.Mutex file *os.File slogger *slog.Logger logPath string } var globalLogger *Logger func InitLogger() (*Logger, error) { cfgDir, err := os.UserConfigDir() if err != nil { cfgDir = filepath.Join(os.Getenv("HOME"), ".config") } dir := filepath.Join(cfgDir, "matrix_client_pc") if err := os.MkdirAll(dir, 0o700); err != nil { return nil, fmt.Errorf("mkdir log dir: %w", err) } logPath := filepath.Join(dir, "app.log") f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) if err != nil { return nil, fmt.Errorf("open log file: %w", err) } // File only — Wails GUI apps on Windows have closed stderr handle, which // breaks MultiWriter (one failing writer aborts the chain in some Go versions). handler := slog.NewTextHandler(f, &slog.HandlerOptions{ Level: slog.LevelDebug, }) l := &Logger{ file: f, slogger: slog.New(handler), logPath: logPath, } globalLogger = l l.Info("logger initialized", "path", logPath) return l, nil } func (l *Logger) Close() error { l.mu.Lock() defer l.mu.Unlock() if l.file != nil { _ = l.file.Sync() return l.file.Close() } return nil } func (l *Logger) Path() string { return l.logPath } func (l *Logger) Debug(msg string, args ...any) { l.slogger.Debug(msg, args...) } func (l *Logger) Info(msg string, args ...any) { l.slogger.Info(msg, args...) } func (l *Logger) Warn(msg string, args ...any) { l.slogger.Warn(msg, args...) } func (l *Logger) Error(msg string, args ...any) { l.slogger.Error(msg, args...) } // Package-level helpers — no-op if InitLogger not called yet (defensive for tests). func logDebug(msg string, args ...any) { if globalLogger != nil { globalLogger.Debug(msg, args...) } } func logInfo(msg string, args ...any) { if globalLogger != nil { globalLogger.Info(msg, args...) } } func logWarn(msg string, args ...any) { if globalLogger != nil { globalLogger.Warn(msg, args...) } } func logError(msg string, args ...any) { if globalLogger != nil { globalLogger.Error(msg, args...) } } // TailLog returns the last N lines of the log file. func TailLog(n int) ([]string, error) { if globalLogger == nil { return nil, fmt.Errorf("logger not initialized") } b, err := os.ReadFile(globalLogger.logPath) if err != nil { return nil, fmt.Errorf("read log: %w", err) } // Split + return last N. lines := splitLines(string(b)) if len(lines) <= n { return lines, nil } return lines[len(lines)-n:], nil } func splitLines(s string) []string { out := []string{} cur := "" for _, r := range s { if r == '\n' { out = append(out, cur) cur = "" continue } cur += string(r) } if cur != "" { out = append(out, cur) } return out }