package auditlog import ( "context" "sync" "time" "go.uber.org/zap" ) // Flusher is implemented by storage backends (e.g. ClickHouseLogger). type Flusher interface { InsertBatch(ctx context.Context, entries []AuditEntry) error } // BatchWriter wraps a Flusher with an async buffered channel. // It flushes when batchSize entries are accumulated OR flushInterval elapses, // whichever comes first. On channel overflow it drops the entry and logs a warning. type BatchWriter struct { ch chan AuditEntry batchSize int flushInterval time.Duration flusher Flusher logger *zap.Logger done chan struct{} wg sync.WaitGroup } // NewBatchWriter creates a production BatchWriter (cap=10 000, size=100, interval=1s). func NewBatchWriter(flusher Flusher, logger *zap.Logger) *BatchWriter { return NewBatchWriterForTest(flusher, 100, time.Second, logger) } // NewBatchWriterForTest creates a BatchWriter with configurable parameters for unit tests. func NewBatchWriterForTest(flusher Flusher, batchSize int, flushInterval time.Duration, logger *zap.Logger) *BatchWriter { return &BatchWriter{ ch: make(chan AuditEntry, 10_000), batchSize: batchSize, flushInterval: flushInterval, flusher: flusher, logger: logger, done: make(chan struct{}), } } // Log enqueues an entry. Non-blocking: drops the entry if the channel is full. func (bw *BatchWriter) Log(entry AuditEntry) { select { case bw.ch <- entry: default: bw.logger.Warn("audit log channel full — entry dropped", zap.String("request_id", entry.RequestID)) } } // Start launches the background flush goroutine. func (bw *BatchWriter) Start() { bw.wg.Add(1) go bw.run() } // Stop signals the flush goroutine to drain remaining entries and exit. func (bw *BatchWriter) Stop() { close(bw.done) bw.wg.Wait() } func (bw *BatchWriter) run() { defer bw.wg.Done() ticker := time.NewTicker(bw.flushInterval) defer ticker.Stop() batch := make([]AuditEntry, 0, bw.batchSize) flush := func() { if len(batch) == 0 { return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := bw.flusher.InsertBatch(ctx, batch); err != nil { bw.logger.Error("audit log batch insert failed", zap.Error(err), zap.Int("count", len(batch))) } batch = batch[:0] } for { select { case entry := <-bw.ch: batch = append(batch, entry) if len(batch) >= bw.batchSize { flush() } case <-ticker.C: flush() case <-bw.done: // Drain remaining entries from channel. for { select { case entry := <-bw.ch: batch = append(batch, entry) default: flush() return } } } } } // Query is not supported on BatchWriter; use the underlying Logger (e.g. ClickHouseLogger). func (bw *BatchWriter) Query(_ context.Context, _ AuditQuery) (*AuditResult, error) { return &AuditResult{}, nil } // QueryCosts is not supported on BatchWriter. func (bw *BatchWriter) QueryCosts(_ context.Context, _ CostQuery) (*CostResult, error) { return &CostResult{}, nil }