package services import ( "context" "log" "nearle/models" "nearle/repositories" "nearle/services/tools" ) // The database audit sink. // // `tools.LogAudit` writes a line and nothing else, which is enough to tail // during a rollout and useless for the question this trail exists to answer: // did anything ever try to read a shop it should not have? That needs rows you // can query. // // ── Why it also logs ──────────────────────────────────────────────────────── // // Both, not either. A database sink that silently stopped writing would take the // trail with it and nothing would look wrong; the log line is the thing that // keeps working when the table does not, and it costs one line per assistant // call — a rate set by people typing questions, not by traffic. // // ── Why a write failure is swallowed ──────────────────────────────────────── // // `AuditSink.Write` returns nothing, deliberately, so a full disk cannot take // the assistant down. A lost row is worse in theory and better in practice than // a merchant unable to ask where their orders are because logging broke. The // failure is logged loudly, which is the most that can be done without giving // the sink a veto over the product. type DBAudit struct { repo repositories.AssistantAuditRepository // The log sink underneath. Kept as the fallback rather than reimplemented. line tools.LogAudit } func NewDBAudit(repo repositories.AssistantAuditRepository) *DBAudit { return &DBAudit{repo: repo} } func (a *DBAudit) Write(_ context.Context, entry tools.AuditEntry) { a.line.Write(context.Background(), entry) if a.repo == nil { return } err := a.repo.Record(models.AssistantAudit{ At: entry.At, Agent: entry.Agent, Tool: entry.Tool, Scope: entry.Scope, Userid: entry.Userid, Tenantid: entry.Tenantid, Args: repositories.EncodeAuditArgs(entry.Args), Outcome: entry.Outcome, Detail: entry.Detail, Rows: entry.Rows, Tookms: repositories.AuditDuration(entry.Took), }) if err != nil { // Loud, because a trail that has quietly stopped recording is worse // than no trail: somebody will read the empty table as "nothing // happened" rather than as "nothing was written". log.Printf("assistant: AUDIT ROW LOST (%s/%s %s): %v", entry.Agent, entry.Tool, entry.Outcome, err) } }