package audit import ( "context" "time" "tm/pkg/logger" ) // Entry represents a security-relevant audit event. type Entry struct { ActorID string ActorType string TargetType string TargetID string Action string At int64 IP string RequestID string UserAgent string Success bool Metadata map[string]string } // Logger records audit events. type Logger interface { Log(ctx context.Context, entry Entry) } type auditLogger struct { logger logger.Logger } // NewLogger creates an audit logger backed by structured application logs only. func NewLogger(log logger.Logger) Logger { return &auditLogger{logger: log} } func (a *auditLogger) Log(ctx context.Context, entry Entry) { entry = enrichEntry(ctx, entry) if entry.At == 0 { entry.At = time.Now().Unix() } a.logger.Info("audit event", map[string]interface{}{ "audit": true, "actor_id": entry.ActorID, "actor_type": entry.ActorType, "target_type": entry.TargetType, "target_id": entry.TargetID, "action": entry.Action, "at": entry.At, "ip": entry.IP, "request_id": entry.RequestID, "user_agent": entry.UserAgent, "success": entry.Success, "metadata": entry.Metadata, }) } type compositeLogger struct { fileLogger Logger store Store logger logger.Logger } // NewCompositeLogger writes audit events to structured logs and an optional store. func NewCompositeLogger(log logger.Logger, store Store) Logger { if store == nil { store = NoopStore() } return &compositeLogger{ fileLogger: NewLogger(log), store: store, logger: log, } } func (c *compositeLogger) Log(ctx context.Context, entry Entry) { entry = enrichEntry(ctx, entry) if entry.At == 0 { entry.At = time.Now().Unix() } c.fileLogger.Log(ctx, entry) doc := entryToDocument(entry) if err := c.store.Save(ctx, doc); err != nil { c.logger.Error("Failed to persist audit event", map[string]interface{}{ "error": err.Error(), "action": entry.Action, }) } }