151 lines
3.4 KiB
Go
151 lines
3.4 KiB
Go
package auditlog
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// Logger is the interface for recording and querying audit log entries.
|
|
// Log() must be non-blocking (backed by a buffered channel or in-memory store).
|
|
type Logger interface {
|
|
Log(entry AuditEntry)
|
|
Query(ctx context.Context, q AuditQuery) (*AuditResult, error)
|
|
QueryCosts(ctx context.Context, q CostQuery) (*CostResult, error)
|
|
Start()
|
|
Stop()
|
|
}
|
|
|
|
// ─── MemLogger ────────────────────────────────────────────────────────────────
|
|
|
|
// MemLogger is a thread-safe in-memory Logger used in tests.
|
|
// It stores entries in insertion order and supports basic filtering.
|
|
type MemLogger struct {
|
|
mu sync.RWMutex
|
|
entries []AuditEntry
|
|
}
|
|
|
|
// NewMemLogger creates a new MemLogger.
|
|
func NewMemLogger() *MemLogger { return &MemLogger{} }
|
|
|
|
func (m *MemLogger) Log(e AuditEntry) {
|
|
m.mu.Lock()
|
|
m.entries = append(m.entries, e)
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// Entries returns a copy of all stored entries (safe to call from tests).
|
|
func (m *MemLogger) Entries() []AuditEntry {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
out := make([]AuditEntry, len(m.entries))
|
|
copy(out, m.entries)
|
|
return out
|
|
}
|
|
|
|
func (m *MemLogger) Query(_ context.Context, q AuditQuery) (*AuditResult, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
sensitivityOrder := map[string]int{
|
|
"none": 0, "low": 1, "medium": 2, "high": 3, "critical": 4,
|
|
}
|
|
minLevel := sensitivityOrder[q.MinSensitivity]
|
|
|
|
var filtered []AuditEntry
|
|
for _, e := range m.entries {
|
|
if e.TenantID != q.TenantID {
|
|
continue
|
|
}
|
|
if q.UserID != "" && e.UserID != q.UserID {
|
|
continue
|
|
}
|
|
if !q.StartTime.IsZero() && e.Timestamp.Before(q.StartTime) {
|
|
continue
|
|
}
|
|
if !q.EndTime.IsZero() && e.Timestamp.After(q.EndTime) {
|
|
continue
|
|
}
|
|
if q.Provider != "" && e.Provider != q.Provider {
|
|
continue
|
|
}
|
|
if q.MinSensitivity != "" {
|
|
if sensitivityOrder[e.SensitivityLevel] < minLevel {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, e)
|
|
}
|
|
|
|
total := len(filtered)
|
|
if q.Offset < len(filtered) {
|
|
filtered = filtered[q.Offset:]
|
|
} else {
|
|
filtered = nil
|
|
}
|
|
limit := q.Limit
|
|
if limit <= 0 || limit > 200 {
|
|
limit = 50
|
|
}
|
|
if len(filtered) > limit {
|
|
filtered = filtered[:limit]
|
|
}
|
|
|
|
return &AuditResult{Data: filtered, Total: total}, nil
|
|
}
|
|
|
|
func (m *MemLogger) QueryCosts(_ context.Context, q CostQuery) (*CostResult, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
type aggKey = string
|
|
type agg struct {
|
|
tokens int
|
|
cost float64
|
|
count int
|
|
}
|
|
totals := map[aggKey]*agg{}
|
|
|
|
for _, e := range m.entries {
|
|
if e.TenantID != q.TenantID {
|
|
continue
|
|
}
|
|
if !q.StartTime.IsZero() && e.Timestamp.Before(q.StartTime) {
|
|
continue
|
|
}
|
|
if !q.EndTime.IsZero() && e.Timestamp.After(q.EndTime) {
|
|
continue
|
|
}
|
|
var key string
|
|
switch q.GroupBy {
|
|
case "model":
|
|
key = e.ModelUsed
|
|
case "department":
|
|
key = e.Department
|
|
default:
|
|
key = e.Provider
|
|
}
|
|
if totals[key] == nil {
|
|
totals[key] = &agg{}
|
|
}
|
|
totals[key].tokens += e.TokenTotal
|
|
totals[key].cost += e.CostUSD
|
|
totals[key].count++
|
|
}
|
|
|
|
var data []CostSummary
|
|
for k, v := range totals {
|
|
data = append(data, CostSummary{
|
|
Key: k,
|
|
TotalTokens: v.tokens,
|
|
TotalCostUSD: v.cost,
|
|
RequestCount: v.count,
|
|
})
|
|
}
|
|
sort.Slice(data, func(i, j int) bool { return data[i].Key < data[j].Key })
|
|
return &CostResult{Data: data}, nil
|
|
}
|
|
|
|
func (m *MemLogger) Start() {}
|
|
func (m *MemLogger) Stop() {}
|