package engine // audit_local.go - 本地文件 AuditSink(JSONL 格式). // // 设计定位: // // LocalAuditSink 是 AuditSink 的默认实现,将审计记录以 JSONL 格式 // (每行一个 JSON 对象)追加写入本地文件. // // 格式选择:JSONL(JSON Lines)而非 CSV 或纯文本. // 理由: // 1. 机器可读:可直接被 jq,grep,ELK Stack,DataDog 消费 // 2. 人类可读:每行独立,无需解析整个文件 // 3. 追加安全:断电后不会破坏已有记录(每行原子完整) // 4. 压缩友好:重复字段名利于 gzip 压缩 // // 升华改进(ELEVATED): 早期方案没有本地审计日志,所有数据上报 Statsig(SaaS 专属). // LocalAuditSink 让离线/嵌入式/合规场景也能有完整的审计轨迹. // 默认写入路径:~/.flyto/audit.jsonl(与配置目录统一). // 替代方案: - 否决原因:引入外部依赖,违反零依赖原则. import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "git.flytoex.net/yuanwei/flyto-agent/pkg/security" ) // LocalAuditSink 将审计记录以 JSONL 格式追加写入本地文件. // 线程安全:内部用 sync.Mutex 保证并发写入不交叉. type LocalAuditSink struct { path string mu sync.Mutex file *os.File } // NewLocalAuditSink 创建本地文件 AuditSink. // path 是目标文件路径(如 ~/.flyto/audit.jsonl). // 如果父目录不存在,自动创建(类似 mkdir -p). // 文件以追加模式打开,已有记录不会被覆盖. func NewLocalAuditSink(path string) (*LocalAuditSink, error) { // 自动创建父目录 dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0700); err != nil { return nil, fmt.Errorf("audit_local: create dir %s: %w", dir, err) } // 以追加模式打开(O_APPEND 保证每次 Write 是原子的末尾追加) // 精妙之处(CLEVER): O_SYNC 在某些操作系统上可以保证 write() 完成后 // 数据刷入磁盘,但会降低性能.这里不用 O_SYNC--审计日志丢失几条 // 比应用变慢更可接受.调用方可以显式 Flush/Close 来保证持久化. f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) if err != nil { return nil, fmt.Errorf("audit_local: open %s: %w", path, err) } return &LocalAuditSink{path: path, file: f}, nil } // Write 将一条审计记录以 JSON 序列化后追加到文件(换行结尾). // 线程安全. func (s *LocalAuditSink) Write(entry security.AuditEntry) error { // JSON 序列化在锁外进行--序列化是 CPU 操作,不需要持有文件锁 data, err := json.Marshal(entry) if err != nil { return fmt.Errorf("audit_local: marshal entry: %w", err) } data = append(data, '\n') // JSONL:每条记录以换行结尾 s.mu.Lock() defer s.mu.Unlock() _, err = s.file.Write(data) if err != nil { return fmt.Errorf("audit_local: write to %s: %w", s.path, err) } return nil } // Close 刷新缓冲区并关闭文件. // 调用 Close 后不应再调用 Write. func (s *LocalAuditSink) Close() error { s.mu.Lock() defer s.mu.Unlock() if s.file == nil { return nil } // Close 前先 Sync--确保最后一条审计记录刷入磁盘. // Write 时不 Sync(性能优先),但 Close 时必须 Sync(可靠性兜底). _ = s.file.Sync() err := s.file.Close() s.file = nil return err } // Path 返回审计文件路径(用于日志/调试). func (s *LocalAuditSink) Path() string { return s.path } // DefaultAuditPath 返回默认审计日志路径(~/.flyto/audit.jsonl). // 与项目配置目录统一,避免散落在文件系统各处. func DefaultAuditPath() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("audit_local: get home dir: %w", err) } return filepath.Join(home, ".flyto", "audit.jsonl"), nil }