feat(audit): JSONL-Logger mit Größenrotation, gzip und SIGHUP-Reopen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
parent
b47ee11ba4
commit
2eb69bdcfe
5 changed files with 526 additions and 0 deletions
77
internal/audit/logger.go
Normal file
77
internal/audit/logger.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue