package server import ( "context" "errors" "log" "strings" "time" "unicode/utf8" ) const ( maxLogFieldBytes = 512 maxLogErrorBytes = 4096 maxLogUsageBytes = 16384 logCleanupBatch = 1000 ) func (s *Server) SetLogRetention(days int) { if days < 0 { days = 0 } s.logRetentionDays.Store(int64(days)) } func truncateLogField(value string, limit int) string { if len(value) <= limit { return value } for limit > 0 && !utf8.RuneStart(value[limit]) { limit-- } return strings.Clone(value[:limit]) } func boundedLog(l requestLog) requestLog { for _, field := range []*string{&l.CreatedAt, &l.KeyID, &l.KeyName, &l.Channel, &l.RequestModel, &l.ActualModel, &l.Downstream, &l.Upstream} { *field = truncateLogField(*field, maxLogFieldBytes) } l.Error = truncateLogField(l.Error, maxLogErrorBytes) if len(l.Usage.Raw) > maxLogUsageBytes { // Keep valid JSON without retaining an attacker-controlled oversized object. l.Usage.Raw = []byte(`{"truncated":true}`) } l.RawUsage = nil return l } func (s *Server) purgeExpiredLogs(ctx context.Context, days int) error { if days <= 0 { return nil } cutoff := time.Now().UTC().AddDate(0, 0, -days).Truncate(time.Second) _, err := s.db.ExecContext(ctx, "DELETE FROM logs WHERE id IN (SELECT id FROM logs WHERE created_at 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Second) err := s.purgeExpiredLogs(ctx, int(days)) cancel() if err != nil { log.Print("ai-router: request log retention cleanup failed") } } } } } func (s *Server) persistLogs(batch []requestLog) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() for _, l := range batch { if err := insertLog(ctx, tx, l); err != nil { return err } } return tx.Commit() }