Files
Logging-System/sink/file.go
T
weichenandCursor bed0dbf076
ci / vet-and-test (push) Successful in 4s
ci / docker-checkout-hostgateway (push) Successful in 2s
Add a structured logger and collector so runs can be traced across processes.
Sync and async sinks, redaction, and a queryable HTTP service keep concurrent logs tied to a trace id.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 13:51:16 +00:00

117 lines
2.1 KiB
Go

package sink
import (
"fmt"
"os"
"path/filepath"
"sync"
"logging-system/logsys"
)
// File 以互斥锁保护的方式追加日志,可选按大小滚动。
// MaxBytes 为 0 时不滚动。Keep 是保留的历史文件份数(不含当前文件)。
type File struct {
mu sync.Mutex
Path string
Format string
MaxBytes int64
Keep int
f *os.File
size int64
}
// Write 追加一条日志,必要时先滚动。
func (f *File) Write(e logsys.Entry) error {
line, err := formatLine(f.Format, e, false)
if err != nil {
return err
}
f.mu.Lock()
defer f.mu.Unlock()
if err := f.open(); err != nil {
return err
}
if f.MaxBytes > 0 && f.size > 0 && f.size+int64(len(line)) > f.MaxBytes {
if err := f.rotate(); err != nil {
return err
}
}
n, err := f.f.Write(line)
f.size += int64(n)
return err
}
// Sync 把文件缓冲刷到磁盘。
func (f *File) Sync() error {
f.mu.Lock()
defer f.mu.Unlock()
if f.f == nil {
return nil
}
return f.f.Sync()
}
// Close 关闭文件句柄。
func (f *File) Close() error {
f.mu.Lock()
defer f.mu.Unlock()
if f.f == nil {
return nil
}
err := f.f.Close()
f.f = nil
return err
}
func (f *File) open() error {
if f.f != nil {
return nil
}
if f.Path == "" {
return fmt.Errorf("sink: file path is required")
}
if err := os.MkdirAll(filepath.Dir(f.Path), 0o755); err != nil {
return err
}
fp, err := os.OpenFile(f.Path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return err
}
info, err := fp.Stat()
if err != nil {
_ = fp.Close()
return err
}
f.f = fp
f.size = info.Size()
return nil
}
func (f *File) rotate() error {
if f.f != nil {
if err := f.f.Close(); err != nil {
return err
}
f.f = nil
}
keep := f.Keep
if keep < 1 {
keep = 1
}
_ = os.Remove(fmt.Sprintf("%s.%d", f.Path, keep))
for i := keep - 1; i >= 1; i-- {
src := fmt.Sprintf("%s.%d", f.Path, i)
dst := fmt.Sprintf("%s.%d", f.Path, i+1)
if _, err := os.Stat(src); err == nil {
_ = os.Rename(src, dst)
}
}
if err := os.Rename(f.Path, f.Path+".1"); err != nil && !os.IsNotExist(err) {
return err
}
f.size = 0
return f.open()
}