package aiapm import ( "database/sql" "log" "time" "github.com/google/uuid" ) // Collector receives AI call records and writes them to the database asynchronously type Collector struct { db *sql.DB ch chan AICallRecord done chan struct{} } // NewCollector creates a new Collector with a buffered channel and background writer func NewCollector(db *sql.DB, bufferSize int) *Collector { if bufferSize <= 0 { bufferSize = 1000 } c := &Collector{ db: db, ch: make(chan AICallRecord, bufferSize), done: make(chan struct{}), } go c.backgroundWriter() return c } // Collect validates and enqueues a record for async storage func (c *Collector) Collect(r AICallRecord) { if r.ID == "" { r.ID = uuid.New().String() } if r.Timestamp.IsZero() { r.Timestamp = time.Now() } if r.Status == "" { r.Status = "success" } // Estimate cost if not provided if r.EstimatedCost == 0 && (r.TokensIn > 0 || r.TokensOut > 0) { r.EstimatedCost = EstimateCost(r.Vendor, r.Model, r.TokensIn, r.TokensOut, r.TokensCacheRead, r.TokensCacheWrite) } select { case c.ch <- r: default: // Channel full — write synchronously to avoid data loss if err := InsertCall(c.db, r); err != nil { log.Printf("ai-apm: sync insert error: %v", err) } } } // CollectBatch validates and enqueues multiple records func (c *Collector) CollectBatch(records []AICallRecord) { for i := range records { c.Collect(records[i]) } } // Stop gracefully stops the background writer func (c *Collector) Stop() { close(c.ch) <-c.done } func (c *Collector) backgroundWriter() { defer close(c.done) batch := make([]AICallRecord, 0, 100) ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() flush := func() { if len(batch) == 0 { return } if err := InsertCallBatch(c.db, batch); err != nil { log.Printf("ai-apm: batch insert error (%d records): %v", len(batch), err) // Fallback: insert one by one for _, r := range batch { if err := InsertCall(c.db, r); err != nil { log.Printf("ai-apm: single insert error: %v", err) } } } batch = batch[:0] } for { select { case r, ok := <-c.ch: if !ok { flush() return } batch = append(batch, r) if len(batch) >= 100 { flush() } case <-ticker.C: flush() } } }