Files

110 lines
2.9 KiB
Go

// 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
}