// Package oplog writes bounded, privacy-safe local operation logs. package oplog import ( "errors" "fmt" "io/fs" "os" "strings" "time" "proxyctl/internal/app" "proxyctl/internal/securefs" ) type Logger struct { path string maxBytes int64 rotations int uid int gid int owned bool } func New(path string, maxBytes int64, rotations int) *Logger { return &Logger{path: path, maxBytes: maxBytes, rotations: rotations} } func NewOwned(path string, maxBytes int64, rotations, uid, gid int) *Logger { return &Logger{path: path, maxBytes: maxBytes, rotations: rotations, uid: uid, gid: gid, owned: true} } func (l *Logger) Write(entry app.LogEntry) error { if l.maxBytes <= 0 { return nil } detail := strings.NewReplacer("\n", " ", "\r", " ").Replace(app.RedactLogDetail(entry.Detail)) if entry.Time.IsZero() { entry.Time = time.Now() } line := fmt.Sprintf("time=%s operation=%s target=%s result=%s detail=%s\n", entry.Time.UTC().Format("2006-01-02T15:04:05Z"), field(entry.Operation), field(entry.Target), field(entry.Result), detail) if int64(len(line)) > l.maxBytes { truncated := []byte(line[:l.maxBytes]) if len(truncated) > 0 { truncated[len(truncated)-1] = '\n' } line = string(truncated) } store := l.store() current, err := readOptional(store, l.path) if err != nil { return fmt.Errorf("read operation log securely: %w", err) } if int64(len(current)+len(line)) > l.maxBytes { if err := l.rotate(store, current); err != nil { return err } current = nil } current = append(current, line...) if err := store.AtomicWrite(l.path, current, 0o600); err != nil { return fmt.Errorf("write operation log atomically: %w", err) } return nil } func (l *Logger) store() securefs.Store { store := securefs.Store{TrustedRoot: securefs.ExistingTrustedRoot(l.path)} if l.owned { store.Owner = &securefs.Owner{UID: l.uid, GID: l.gid} } return store } func field(value string) string { return strings.NewReplacer(" ", "_", "\n", "_", "\r", "_").Replace(value) } func (l *Logger) rotate(store securefs.Store, current []byte) error { if l.rotations <= 0 { return nil } for i := l.rotations; i >= 2; i-- { previous := fmt.Sprintf("%s.%d", l.path, i-1) data, err := readOptional(store, previous) if err != nil { return fmt.Errorf("read rotated operation log: %w", err) } if data == nil { continue } if err := store.AtomicWrite(fmt.Sprintf("%s.%d", l.path, i), data, 0o600); err != nil { return fmt.Errorf("rotate operation log: %w", err) } } if current != nil { if err := store.AtomicWrite(l.path+".1", current, 0o600); err != nil { return fmt.Errorf("rotate operation log: %w", err) } } return nil } func readOptional(store securefs.Store, path string) ([]byte, error) { data, err := store.ReadFile(path) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, os.ErrNotExist) { return nil, nil } return data, err }