// Package logger 提供一个轻量日志器,把消息写入指定目录下的 service.log。 // // 设计要点: // // - 文件日志始终启用,便于事后取证 // - 所有写入加锁,frpc 输出 + 本服务输出可能并发 // - 服务模式下,日志必须写到 EXE 同目录(SCM 默认工作目录是 System32) // - 不再同时写 stdout - 服务模式已经 FreeConsole,CLI 模式 stdout 由 main 自己处理 package logger import ( "fmt" "io" "os" "path/filepath" "sync" "time" ) // Log 是简易同步日志器。 type Log struct { mu sync.Mutex f *os.File } const timeLayout = "2006-01-02 15:04:05" // New 在 logDir 下创建日志文件并返回 Log。 // // logDir 必须传绝对路径;不存在会自动创建。 func New(logDir string) (*Log, error) { if err := os.MkdirAll(logDir, 0o755); err != nil { return nil, err } f, err := os.OpenFile( filepath.Join(logDir, "service.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644, ) if err != nil { return nil, err } return &Log{f: f}, nil } // Close 关闭底层文件句柄。 func (l *Log) Close() error { if l == nil || l.f == nil { return nil } return l.f.Close() } // Info 写一条普通日志。 func (l *Log) Info(format string, args ...any) { l.write("INFO", format, args...) } func (l *Log) Warn(format string, args ...any) { l.write("WARN", format, args...) } func (l *Log) Error(format string, args ...any) { l.write("ERROR", format, args...) } // FrpcRedirect 返回一个 io.Writer,frpc 的 stdout/stderr 重定向到这里。 func (l *Log) FrpcRedirect() io.Writer { return &frpcWriter{l: l} } type frpcWriter struct{ l *Log } func (w *frpcWriter) Write(p []byte) (int, error) { w.l.write("FRPC", "%s", string(p)) return len(p), nil } func (l *Log) write(level, format string, args ...any) { l.mu.Lock() defer l.mu.Unlock() msg := fmt.Sprintf(format, args...) line := fmt.Sprintf("[%s] [%s] %s\n", time.Now().Format(timeLayout), level, msg) if l.f != nil { _, _ = l.f.WriteString(line) } }