package common import ( "fmt" "io" "log/slog" "os" "path/filepath" "sync" "time" ) // GetLogsDir 日志目录:~/.u-desk/logs(audit 日志同目录) func GetLogsDir() string { return filepath.Join(GetUserDataDir(), "logs") } // dailyFile 按天滚动的追加写文件:每次写时发现日期变化即切换新文件。 // 只做按天滚动,不搞大小上限/压缩/清理,崩溃取证够用。 type dailyFile struct { mu sync.Mutex dir string naming func(t time.Time) string f *os.File day string } func newDailyFile(dir string, naming func(time.Time) string) *dailyFile { return &dailyFile{dir: dir, naming: naming} } func (w *dailyFile) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() now := time.Now() day := now.Format("2006-01-02") if w.f != nil && day != w.day { w.f.Close() w.f = nil } if w.f == nil { if err := os.MkdirAll(w.dir, 0755); err != nil { return 0, err } f, err := os.OpenFile(filepath.Join(w.dir, w.naming(now)), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return 0, err } w.f, w.day = f, day } return w.f.Write(p) } func (w *dailyFile) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.f == nil { return nil } err := w.f.Close() w.f = nil return err } // FileLogOut 应用文件日志的底层写出器(stderr + 按天文件 MultiWriter)。 // SetupFileLogging 之后有效;供需要独立过滤级别的 logger 复用同一落盘目的地 // (如 wails 系统日志降到 Warn,业务 slog.Default() 保持 INFO 全量)。 var FileLogOut io.Writer = os.Stderr // SetupFileLogging slog 双路落盘:stderr + ~/.u-desk/logs/app-YYYY-MM-DD.log。 // windowsgui 下 stderr 被丢弃,文件路是日志唯一可见载体; // 文件侧打不开时静默降级为仅 stderr,不因日志失败阻断启动。 func SetupFileLogging() { out := io.Writer(os.Stderr) w := newDailyFile(GetLogsDir(), func(t time.Time) string { return fmt.Sprintf("app-%s.log", t.Format("2006-01-02")) }) // 预开当天文件探测文件侧可用性(Write 内部含懒开/换天逻辑) if _, err := w.Write(nil); err == nil { // 文件必须放 MultiWriter 首位:windowsgui 下 stderr 句柄无效, // MultiWriter 按序短路,stderr 在前会吞掉后续文件路(真机零日志的根因) out = io.MultiWriter(w, os.Stderr) } else { w.Close() } FileLogOut = out slog.SetDefault(slog.New(slog.NewTextHandler(out, nil))) }