package audit import ( "encoding/json" "fmt" "io" "sync" "time" ) // Options steuert Rotation und Nebenausgabe. type Options struct { MaxSizeMB int MaxBackups int Compress bool // Stdout erhält eine Kopie jeder Zeile (systemd-Journal). Nil = keine Kopie. Stdout io.Writer // Clock ist injizierbar für Tests. Nil = time.Now. Clock func() time.Time } // Logger schreibt Events zeilenweise als JSON. type Logger struct { mu sync.Mutex file *rotatingFile stdout io.Writer clock func() time.Time } // New öffnet das Audit-Log und liefert einen Logger. // Ein fehlendes Verzeichnis wird angelegt. func New(path string, opts Options) (*Logger, error) { if opts.Clock == nil { opts.Clock = time.Now } rf, err := openRotating(path, opts) if err != nil { return nil, err } return &Logger{file: rf, stdout: opts.Stdout, clock: opts.Clock}, nil } // Log schreibt ein Ereignis. Fehler beim Schreiben werden nach stdout gemeldet, // dürfen den laufenden Request aber nie scheitern lassen. func (l *Logger) Log(e Event) { if e.TS == "" { e.TS = l.clock().Format(time.RFC3339) } line, err := json.Marshal(e) if err != nil { return // Event enthält nur Strings; kann praktisch nicht passieren } line = append(line, '\n') l.mu.Lock() defer l.mu.Unlock() if _, err := l.file.Write(line); err != nil && l.stdout != nil { fmt.Fprintf(l.stdout, "{\"ts\":%q,\"event\":\"audit_write_failed\",\"detail\":%q}\n", e.TS, err.Error()) } if l.stdout != nil { l.stdout.Write(line) } } // Reopen schließt die aktuelle Datei und öffnet sie neu (SIGHUP/logrotate). func (l *Logger) Reopen() error { l.mu.Lock() defer l.mu.Unlock() return l.file.reopen() } func (l *Logger) Close() error { l.mu.Lock() defer l.mu.Unlock() return l.file.Close() }