init
This commit is contained in:
100
internal/store/audit.go
Normal file
100
internal/store/audit.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuditEvent struct {
|
||||
Time string `json:"time"`
|
||||
Actor string `json:"actor"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
UA string `json:"ua,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type AuditLog struct {
|
||||
mu sync.Mutex
|
||||
path string
|
||||
fh *os.File
|
||||
}
|
||||
|
||||
func NewAuditLog(path string) (*AuditLog, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AuditLog{path: path, fh: f}, nil
|
||||
}
|
||||
|
||||
func (a *AuditLog) Close() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.fh != nil {
|
||||
return a.fh.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AuditLog) Append(ev AuditEvent) {
|
||||
ev.Time = time.Now().UTC().Format(time.RFC3339Nano)
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.fh == nil {
|
||||
return
|
||||
}
|
||||
b, _ := json.Marshal(ev)
|
||||
_, _ = a.fh.Write(append(b, '\n'))
|
||||
}
|
||||
|
||||
func (a *AuditLog) Tail(max int) ([]AuditEvent, error) {
|
||||
// Simple tail by reading whole file (OK for small audit logs).
|
||||
// You can replace with a smarter tail if needed.
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.fh == nil {
|
||||
return nil, nil
|
||||
}
|
||||
_ = a.fh.Sync()
|
||||
b, err := os.ReadFile(a.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := splitLines(b)
|
||||
if max > 0 && len(lines) > max {
|
||||
lines = lines[len(lines)-max:]
|
||||
}
|
||||
out := make([]AuditEvent, 0, len(lines))
|
||||
for _, ln := range lines {
|
||||
var ev AuditEvent
|
||||
if json.Unmarshal(ln, &ev) == nil {
|
||||
out = append(out, ev)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func splitLines(b []byte) [][]byte {
|
||||
var out [][]byte
|
||||
start := 0
|
||||
for i := 0; i < len(b); i++ {
|
||||
if b[i] == '\n' {
|
||||
if i > start {
|
||||
out = append(out, b[start:i])
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(b) {
|
||||
out = append(out, b[start:])
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user