// Package logging 提供结构化文本日志的创建,同时写入控制台与本地文件。 package logging import ( "errors" "fmt" "io" "log/slog" "os" "path/filepath" ) // New 创建同时写入 stdout 和本地文件的结构化文本日志。 // stdout 由 systemd/journald 收集,本地文件用于现场诊断和受控远程读取。 // 返回的 closer 用于在退出前同步并关闭日志文件。 func New(filePath string) (*slog.Logger, io.Closer, error) { return NewWithConsole(os.Stdout, filePath) } // NewWithConsole 允许调用方指定控制台 writer,主要用于测试和嵌入运行。 // console 与 filePath 均不能为空;日志级别固定为 Info。 func NewWithConsole(console io.Writer, filePath string) (*slog.Logger, io.Closer, error) { if console == nil { return nil, nil, errors.New("console writer is required") } if filePath == "" { return nil, nil, errors.New("log file path is required") } absPath, err := filepath.Abs(filePath) if err != nil { return nil, nil, fmt.Errorf("resolve log file path: %w", err) } if err := os.MkdirAll(filepath.Dir(absPath), 0o750); err != nil { return nil, nil, fmt.Errorf("create log directory: %w", err) } file, err := os.OpenFile(absPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o640) if err != nil { return nil, nil, fmt.Errorf("open log file: %w", err) } handler := slog.NewTextHandler(io.MultiWriter(console, file), &slog.HandlerOptions{Level: slog.LevelInfo}) return slog.New(handler), &syncFileCloser{file: file}, nil } // syncFileCloser 负责在关闭时先同步再关闭日志文件。 type syncFileCloser struct { file *os.File } // Close 同步并关闭日志文件,重复调用是安全的。 func (c *syncFileCloser) Close() error { if c == nil || c.file == nil { return nil } file := c.file c.file = nil return errors.Join(file.Sync(), file.Close()) }