@resolveio/server-lib 22.3.221 → 22.3.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai/assistant-core-heuristics.d.ts +11 -0
- package/ai/assistant-core-heuristics.js +356 -0
- package/ai/assistant-core-heuristics.js.map +1 -0
- package/ai/resolveio-platform-intelligence-memory-corpus.d.ts +3 -0
- package/ai/resolveio-platform-intelligence-memory-corpus.js +214 -0
- package/ai/resolveio-platform-intelligence-memory-corpus.js.map +1 -0
- package/ai/resolveio-platform-intelligence-memory.d.ts +20 -0
- package/ai/resolveio-platform-intelligence-memory.js +341 -0
- package/ai/resolveio-platform-intelligence-memory.js.map +1 -0
- package/{src/ai/resolveio-platform-intelligence-types.ts → ai/resolveio-platform-intelligence-types.d.ts} +15 -20
- package/ai/resolveio-platform-intelligence-types.js +4 -0
- package/ai/resolveio-platform-intelligence-types.js.map +1 -0
- package/ai/resolveio-platform-intelligence.d.ts +6 -0
- package/ai/resolveio-platform-intelligence.js +463 -0
- package/ai/resolveio-platform-intelligence.js.map +1 -0
- package/client-server-app.d.ts +1 -0
- package/client-server-app.js +68 -0
- package/client-server-app.js.map +1 -0
- package/collections/ai-run.collection.d.ts +3 -0
- package/collections/ai-run.collection.js +170 -0
- package/collections/ai-run.collection.js.map +1 -0
- package/collections/ai-terminal-conversation.collection.d.ts +2 -0
- package/collections/ai-terminal-conversation.collection.js +140 -0
- package/collections/ai-terminal-conversation.collection.js.map +1 -0
- package/collections/ai-terminal-issue-report.collection.d.ts +2 -0
- package/collections/ai-terminal-issue-report.collection.js +148 -0
- package/collections/ai-terminal-issue-report.collection.js.map +1 -0
- package/collections/ai-terminal-message.collection.d.ts +2 -0
- package/collections/ai-terminal-message.collection.js +121 -0
- package/collections/ai-terminal-message.collection.js.map +1 -0
- package/collections/app-setting.collection.d.ts +3 -0
- package/collections/app-setting.collection.js +103 -0
- package/collections/app-setting.collection.js.map +1 -0
- package/collections/app-status.collection.d.ts +3 -0
- package/collections/app-status.collection.js +57 -0
- package/collections/app-status.collection.js.map +1 -0
- package/collections/communication-metric.collection.d.ts +2 -0
- package/collections/communication-metric.collection.js +133 -0
- package/collections/communication-metric.collection.js.map +1 -0
- package/collections/counter.collection.d.ts +3 -0
- package/collections/counter.collection.js +56 -0
- package/collections/counter.collection.js.map +1 -0
- package/collections/cron-job-history.collection.d.ts +3 -0
- package/collections/cron-job-history.collection.js +137 -0
- package/collections/cron-job-history.collection.js.map +1 -0
- package/collections/cron-job.collection.d.ts +3 -0
- package/collections/cron-job.collection.js +92 -0
- package/collections/cron-job.collection.js.map +1 -0
- package/collections/customer-notification.collection.d.ts +3 -0
- package/collections/customer-notification.collection.js +130 -0
- package/collections/customer-notification.collection.js.map +1 -0
- package/collections/customer-portal-password.collection.d.ts +3 -0
- package/collections/customer-portal-password.collection.js +75 -0
- package/collections/customer-portal-password.collection.js.map +1 -0
- package/collections/email-history.collection.d.ts +3 -0
- package/collections/email-history.collection.js +134 -0
- package/collections/email-history.collection.js.map +1 -0
- package/collections/email-verified.collection.d.ts +3 -0
- package/collections/email-verified.collection.js +62 -0
- package/collections/email-verified.collection.js.map +1 -0
- package/collections/file.collection.d.ts +3 -0
- package/collections/file.collection.js +74 -0
- package/collections/file.collection.js.map +1 -0
- package/collections/flag-update.collection.d.ts +3 -0
- package/collections/flag-update.collection.js +57 -0
- package/collections/flag-update.collection.js.map +1 -0
- package/collections/flag.collection.d.ts +3 -0
- package/collections/flag.collection.js +57 -0
- package/collections/flag.collection.js.map +1 -0
- package/collections/log-method-latency.collection.d.ts +3 -0
- package/collections/log-method-latency.collection.js +77 -0
- package/collections/log-method-latency.collection.js.map +1 -0
- package/collections/log-subscription.collection.d.ts +3 -0
- package/collections/log-subscription.collection.js +80 -0
- package/collections/log-subscription.collection.js.map +1 -0
- package/collections/log.collection.d.ts +3 -0
- package/collections/log.collection.js +93 -0
- package/collections/log.collection.js.map +1 -0
- package/collections/logged-in-users.collection.d.ts +3 -0
- package/collections/logged-in-users.collection.js +67 -0
- package/collections/logged-in-users.collection.js.map +1 -0
- package/collections/monitor-cpu.collection.d.ts +3 -0
- package/collections/monitor-cpu.collection.js +65 -0
- package/collections/monitor-cpu.collection.js.map +1 -0
- package/collections/monitor-function.collection.d.ts +3 -0
- package/collections/monitor-function.collection.js +74 -0
- package/collections/monitor-function.collection.js.map +1 -0
- package/collections/monitor-memory.collection.d.ts +3 -0
- package/collections/monitor-memory.collection.js +77 -0
- package/collections/monitor-memory.collection.js.map +1 -0
- package/collections/monitor-mongo.collection.d.ts +3 -0
- package/collections/monitor-mongo.collection.js +71 -0
- package/collections/monitor-mongo.collection.js.map +1 -0
- package/collections/notification.collection.d.ts +3 -0
- package/collections/notification.collection.js +57 -0
- package/collections/notification.collection.js.map +1 -0
- package/collections/openai-usage-ledger.collection.d.ts +2 -0
- package/collections/openai-usage-ledger.collection.js +188 -0
- package/collections/openai-usage-ledger.collection.js.map +1 -0
- package/collections/report-builder-dashboard-builder.collection.d.ts +3 -0
- package/collections/report-builder-dashboard-builder.collection.js +109 -0
- package/collections/report-builder-dashboard-builder.collection.js.map +1 -0
- package/collections/report-builder-library.collection.d.ts +3 -0
- package/collections/report-builder-library.collection.js +87 -0
- package/collections/report-builder-library.collection.js.map +1 -0
- package/collections/report-builder-report.collection.d.ts +4 -0
- package/collections/report-builder-report.collection.js +184 -0
- package/collections/report-builder-report.collection.js.map +1 -0
- package/collections/user-group.collection.d.ts +4 -0
- package/collections/user-group.collection.js +89 -0
- package/collections/user-group.collection.js.map +1 -0
- package/collections/user-guide.collection.d.ts +3 -0
- package/collections/user-guide.collection.js +57 -0
- package/collections/user-guide.collection.js.map +1 -0
- package/collections/user.collection.d.ts +4 -0
- package/collections/user.collection.js +180 -0
- package/collections/user.collection.js.map +1 -0
- package/cron/cron.d.ts +14 -0
- package/cron/cron.js +216 -0
- package/cron/cron.js.map +1 -0
- package/fixtures/cron-jobs.d.ts +1 -0
- package/fixtures/cron-jobs.js +150 -0
- package/fixtures/cron-jobs.js.map +1 -0
- package/fixtures/init.d.ts +1 -0
- package/fixtures/init.js +91 -0
- package/fixtures/init.js.map +1 -0
- package/http/auth.d.ts +2 -0
- package/http/auth.js +951 -0
- package/http/auth.js.map +1 -0
- package/http/health.d.ts +1 -0
- package/http/health.js +11 -0
- package/http/health.js.map +1 -0
- package/http/home.d.ts +1 -0
- package/http/home.js +134 -0
- package/http/home.js.map +1 -0
- package/http/slow-query-publication.d.ts +2 -0
- package/http/slow-query-publication.js +99 -0
- package/http/slow-query-publication.js.map +1 -0
- package/index.d.ts +1 -0
- package/index.js +19 -0
- package/index.js.map +1 -0
- package/managers/ai-assistant-codex-manager.manager.d.ts +67 -0
- package/managers/ai-assistant-codex-manager.manager.js +1113 -0
- package/managers/ai-assistant-codex-manager.manager.js.map +1 -0
- package/managers/ai-run-evidence.manager.d.ts +36 -0
- package/managers/ai-run-evidence.manager.js +377 -0
- package/managers/ai-run-evidence.manager.js.map +1 -0
- package/managers/communication-metric.manager.d.ts +16 -0
- package/managers/communication-metric.manager.js +134 -0
- package/managers/communication-metric.manager.js.map +1 -0
- package/managers/cron.manager.d.ts +20 -0
- package/managers/cron.manager.js +534 -0
- package/managers/cron.manager.js.map +1 -0
- package/managers/customer-notification-content.manager.d.ts +55 -0
- package/managers/customer-notification-content.manager.js +158 -0
- package/managers/customer-notification-content.manager.js.map +1 -0
- package/managers/diagnostic-manager-bootstrap.d.ts +9 -0
- package/managers/diagnostic-manager-bootstrap.js +260 -0
- package/managers/diagnostic-manager-bootstrap.js.map +1 -0
- package/managers/error-auto-fix.manager.d.ts +149 -0
- package/managers/error-auto-fix.manager.js +3064 -0
- package/managers/error-auto-fix.manager.js.map +1 -0
- package/managers/local-log.manager.d.ts +18 -0
- package/managers/local-log.manager.js +88 -0
- package/managers/local-log.manager.js.map +1 -0
- package/managers/method.manager.d.ts +84 -0
- package/managers/method.manager.js +1964 -0
- package/managers/method.manager.js.map +1 -0
- package/managers/mongo.manager.d.ts +224 -0
- package/managers/mongo.manager.js +5000 -0
- package/managers/mongo.manager.js.map +1 -0
- package/managers/monitor.manager.d.ts +70 -0
- package/managers/monitor.manager.js +550 -0
- package/managers/monitor.manager.js.map +1 -0
- package/managers/openai-usage-ledger.manager.d.ts +30 -0
- package/managers/openai-usage-ledger.manager.js +142 -0
- package/managers/openai-usage-ledger.manager.js.map +1 -0
- package/managers/slow-query-verifier.manager.d.ts +144 -0
- package/managers/slow-query-verifier.manager.js +3857 -0
- package/managers/slow-query-verifier.manager.js.map +1 -0
- package/managers/slow-query.manager.d.ts +28 -0
- package/managers/slow-query.manager.js +468 -0
- package/managers/slow-query.manager.js.map +1 -0
- package/managers/subscription.manager.d.ts +169 -0
- package/managers/subscription.manager.js +3434 -0
- package/managers/subscription.manager.js.map +1 -0
- package/managers/websocket.manager.d.ts +73 -0
- package/managers/websocket.manager.js +673 -0
- package/managers/websocket.manager.js.map +1 -0
- package/managers/worker-dispatcher.manager.d.ts +120 -0
- package/managers/worker-dispatcher.manager.js +1266 -0
- package/managers/worker-dispatcher.manager.js.map +1 -0
- package/managers/worker-server.manager.d.ts +35 -0
- package/managers/worker-server.manager.js +582 -0
- package/managers/worker-server.manager.js.map +1 -0
- package/methods/accounts.d.ts +2 -0
- package/methods/accounts.js +624 -0
- package/methods/accounts.js.map +1 -0
- package/methods/ai-terminal.d.ts +458 -0
- package/methods/ai-terminal.js +27991 -0
- package/methods/ai-terminal.js.map +1 -0
- package/methods/app-settings.d.ts +2 -0
- package/methods/app-settings.js +169 -0
- package/methods/app-settings.js.map +1 -0
- package/methods/aws.d.ts +2 -0
- package/methods/aws.js +877 -0
- package/methods/aws.js.map +1 -0
- package/methods/collections.d.ts +2 -0
- package/methods/collections.js +719 -0
- package/methods/collections.js.map +1 -0
- package/methods/counters.d.ts +2 -0
- package/methods/counters.js +113 -0
- package/methods/counters.js.map +1 -0
- package/methods/cron-jobs.d.ts +2 -0
- package/methods/cron-jobs.js +2475 -0
- package/methods/cron-jobs.js.map +1 -0
- package/methods/customer-notifications.d.ts +2 -0
- package/methods/customer-notifications.js +528 -0
- package/methods/customer-notifications.js.map +1 -0
- package/methods/diagnostics.d.ts +2 -0
- package/methods/diagnostics.js +703 -0
- package/methods/diagnostics.js.map +1 -0
- package/methods/flag-updates.d.ts +2 -0
- package/methods/flag-updates.js +8 -0
- package/methods/flag-updates.js.map +1 -0
- package/methods/flags.d.ts +2 -0
- package/methods/flags.js +8 -0
- package/methods/flags.js.map +1 -0
- package/methods/logs.d.ts +2 -0
- package/methods/logs.js +751 -0
- package/methods/logs.js.map +1 -0
- package/methods/mongo-explorer.d.ts +2 -0
- package/methods/mongo-explorer.js +1808 -0
- package/methods/mongo-explorer.js.map +1 -0
- package/methods/monitor.d.ts +2 -0
- package/methods/monitor.js +543 -0
- package/methods/monitor.js.map +1 -0
- package/methods/pdf.d.ts +2 -0
- package/methods/pdf.js +1216 -0
- package/methods/pdf.js.map +1 -0
- package/methods/publications.d.ts +1 -0
- package/methods/publications.js +183 -0
- package/methods/publications.js.map +1 -0
- package/methods/report-builder.d.ts +2 -0
- package/methods/report-builder.js +3094 -0
- package/methods/report-builder.js.map +1 -0
- package/methods/support.d.ts +2 -0
- package/methods/support.js +430 -0
- package/methods/support.js.map +1 -0
- package/models/ai-run.model.d.ts +19 -0
- package/models/ai-run.model.js +4 -0
- package/models/ai-run.model.js.map +1 -0
- package/models/ai-terminal-conversation.model.d.ts +17 -0
- package/models/ai-terminal-conversation.model.js +4 -0
- package/models/ai-terminal-conversation.model.js.map +1 -0
- package/models/ai-terminal-issue-report.model.d.ts +19 -0
- package/models/ai-terminal-issue-report.model.js +4 -0
- package/models/ai-terminal-issue-report.model.js.map +1 -0
- package/models/ai-terminal-message.model.d.ts +22 -0
- package/models/ai-terminal-message.model.js +4 -0
- package/models/ai-terminal-message.model.js.map +1 -0
- package/models/app-setting.model.d.ts +16 -0
- package/models/app-setting.model.js +4 -0
- package/models/app-setting.model.js.map +1 -0
- package/{src/models/app-status.model.ts → models/app-status.model.d.ts} +2 -3
- package/models/app-status.model.js +4 -0
- package/models/app-status.model.js.map +1 -0
- package/{src/models/billing-logged-in-users.model.ts → models/billing-logged-in-users.model.d.ts} +4 -5
- package/models/billing-logged-in-users.model.js +4 -0
- package/models/billing-logged-in-users.model.js.map +1 -0
- package/models/collection-document.model.d.ts +21 -0
- package/models/collection-document.model.js +4 -0
- package/models/collection-document.model.js.map +1 -0
- package/models/communication-metric.model.d.ts +20 -0
- package/models/communication-metric.model.js +4 -0
- package/models/communication-metric.model.js.map +1 -0
- package/{src/models/counter.model.ts → models/counter.model.d.ts} +3 -4
- package/models/counter.model.js +4 -0
- package/models/counter.model.js.map +1 -0
- package/models/cron-job-history.model.d.ts +15 -0
- package/models/cron-job-history.model.js +4 -0
- package/models/cron-job-history.model.js.map +1 -0
- package/models/cron-job.model.d.ts +14 -0
- package/models/cron-job.model.js +4 -0
- package/models/cron-job.model.js.map +1 -0
- package/models/customer-notification.model.d.ts +26 -0
- package/models/customer-notification.model.js +4 -0
- package/models/customer-notification.model.js.map +1 -0
- package/models/customer-portal-password.model.d.ts +11 -0
- package/models/customer-portal-password.model.js +4 -0
- package/models/customer-portal-password.model.js.map +1 -0
- package/models/dialog.model.d.ts +23 -0
- package/models/dialog.model.js +4 -0
- package/models/dialog.model.js.map +1 -0
- package/models/email-history.model.d.ts +32 -0
- package/{src/models/email-history.model.ts → models/email-history.model.js} +4 -36
- package/models/email-history.model.js.map +1 -0
- package/{src/models/email-verified.model.ts → models/email-verified.model.d.ts} +5 -6
- package/models/email-verified.model.js +4 -0
- package/models/email-verified.model.js.map +1 -0
- package/{src/models/file.model.ts → models/file.model.d.ts} +7 -8
- package/models/file.model.js +4 -0
- package/models/file.model.js.map +1 -0
- package/{src/models/flag-update.model.ts → models/flag-update.model.d.ts} +3 -4
- package/models/flag-update.model.js +4 -0
- package/models/flag-update.model.js.map +1 -0
- package/{src/models/flag.model.ts → models/flag.model.d.ts} +3 -4
- package/models/flag.model.js +4 -0
- package/models/flag.model.js.map +1 -0
- package/models/log-method-latency.model.d.ts +10 -0
- package/models/log-method-latency.model.js +4 -0
- package/models/log-method-latency.model.js.map +1 -0
- package/{src/models/log-subscription.model.ts → models/log-subscription.model.d.ts} +9 -11
- package/models/log-subscription.model.js +4 -0
- package/models/log-subscription.model.js.map +1 -0
- package/models/log.model.d.ts +17 -0
- package/models/log.model.js +4 -0
- package/models/log.model.js.map +1 -0
- package/{src/models/logged-in-users.model.ts → models/logged-in-users.model.d.ts} +5 -6
- package/models/logged-in-users.model.js +4 -0
- package/models/logged-in-users.model.js.map +1 -0
- package/{src/models/method-response.model.ts → models/method-response.model.d.ts} +6 -7
- package/models/method-response.model.js +4 -0
- package/models/method-response.model.js.map +1 -0
- package/models/method.model.d.ts +26 -0
- package/models/method.model.js +4 -0
- package/models/method.model.js.map +1 -0
- package/{src/models/monitor-cpu.model.ts → models/monitor-cpu.model.d.ts} +7 -9
- package/models/monitor-cpu.model.js +4 -0
- package/models/monitor-cpu.model.js.map +1 -0
- package/models/monitor-function.model.d.ts +14 -0
- package/models/monitor-function.model.js +4 -0
- package/models/monitor-function.model.js.map +1 -0
- package/models/monitor-memory.model.d.ts +15 -0
- package/models/monitor-memory.model.js +4 -0
- package/models/monitor-memory.model.js.map +1 -0
- package/models/monitor-mongo.model.d.ts +13 -0
- package/models/monitor-mongo.model.js +4 -0
- package/models/monitor-mongo.model.js.map +1 -0
- package/{src/models/notification.model.ts → models/notification.model.d.ts} +4 -6
- package/models/notification.model.js +4 -0
- package/models/notification.model.js.map +1 -0
- package/models/openai-usage-ledger.model.d.ts +30 -0
- package/models/openai-usage-ledger.model.js +4 -0
- package/models/openai-usage-ledger.model.js.map +1 -0
- package/models/pagination.model.d.ts +11 -0
- package/models/pagination.model.js +28 -0
- package/models/pagination.model.js.map +1 -0
- package/models/permission.model.d.ts +12 -0
- package/models/permission.model.js +4 -0
- package/models/permission.model.js.map +1 -0
- package/models/report-builder-dashboard-builder.model.d.ts +25 -0
- package/models/report-builder-dashboard-builder.model.js +4 -0
- package/models/report-builder-dashboard-builder.model.js.map +1 -0
- package/models/report-builder-library.model.d.ts +17 -0
- package/models/report-builder-library.model.js +4 -0
- package/models/report-builder-library.model.js.map +1 -0
- package/models/report-builder-report.model.d.ts +121 -0
- package/models/report-builder-report.model.js +4 -0
- package/models/report-builder-report.model.js.map +1 -0
- package/models/report-builder.model.d.ts +61 -0
- package/models/report-builder.model.js +4 -0
- package/models/report-builder.model.js.map +1 -0
- package/models/select-data-label.model.d.ts +9 -0
- package/models/select-data-label.model.js +4 -0
- package/models/select-data-label.model.js.map +1 -0
- package/models/server-message.model.d.ts +32 -0
- package/models/server-message.model.js +4 -0
- package/models/server-message.model.js.map +1 -0
- package/models/slow-query-report.model.d.ts +23 -0
- package/models/slow-query-report.model.js +4 -0
- package/models/slow-query-report.model.js.map +1 -0
- package/models/subscription.model.d.ts +31 -0
- package/models/subscription.model.js +4 -0
- package/models/subscription.model.js.map +1 -0
- package/models/support-ticket.model.d.ts +87 -0
- package/models/support-ticket.model.js +4 -0
- package/models/support-ticket.model.js.map +1 -0
- package/models/user-group.model.d.ts +20 -0
- package/models/user-group.model.js +4 -0
- package/models/user-group.model.js.map +1 -0
- package/{src/models/user-guide.model.ts → models/user-guide.model.d.ts} +4 -5
- package/models/user-guide.model.js +4 -0
- package/models/user-guide.model.js.map +1 -0
- package/models/user.model.d.ts +84 -0
- package/models/user.model.js +4 -0
- package/models/user.model.js.map +1 -0
- package/package.json +1 -1
- package/private/images/ResolveIO.png +0 -0
- package/public_api.js +127 -0
- package/public_api.js.map +1 -0
- package/publications/ai-terminal.d.ts +1 -0
- package/publications/ai-terminal.js +122 -0
- package/publications/ai-terminal.js.map +1 -0
- package/publications/app-settings.d.ts +2 -0
- package/publications/app-settings.js +28 -0
- package/publications/app-settings.js.map +1 -0
- package/publications/app-status.d.ts +2 -0
- package/publications/app-status.js +16 -0
- package/publications/app-status.js.map +1 -0
- package/publications/cron-jobs.d.ts +2 -0
- package/publications/cron-jobs.js +88 -0
- package/publications/cron-jobs.js.map +1 -0
- package/publications/customer-notifications.d.ts +2 -0
- package/publications/customer-notifications.js +161 -0
- package/publications/customer-notifications.js.map +1 -0
- package/publications/files.d.ts +2 -0
- package/publications/files.js +36 -0
- package/publications/files.js.map +1 -0
- package/publications/flags-update.d.ts +2 -0
- package/publications/flags-update.js +22 -0
- package/publications/flags-update.js.map +1 -0
- package/publications/flags.d.ts +2 -0
- package/publications/flags.js +22 -0
- package/publications/flags.js.map +1 -0
- package/publications/logs.d.ts +2 -0
- package/publications/logs.js +164 -0
- package/publications/logs.js.map +1 -0
- package/publications/notifications.d.ts +2 -0
- package/publications/notifications.js +16 -0
- package/publications/notifications.js.map +1 -0
- package/publications/report-builder-dashboard-builders.d.ts +2 -0
- package/publications/report-builder-dashboard-builders.js +42 -0
- package/publications/report-builder-dashboard-builders.js.map +1 -0
- package/publications/report-builder-libraries.d.ts +2 -0
- package/publications/report-builder-libraries.js +90 -0
- package/publications/report-builder-libraries.js.map +1 -0
- package/publications/report-builder-reports.d.ts +2 -0
- package/publications/report-builder-reports.js +50 -0
- package/publications/report-builder-reports.js.map +1 -0
- package/publications/super-admin.d.ts +2 -0
- package/publications/super-admin.js +16 -0
- package/publications/super-admin.js.map +1 -0
- package/publications/user-groups.d.ts +1 -0
- package/publications/user-groups.js +16 -0
- package/publications/user-groups.js.map +1 -0
- package/publications/user-guides.d.ts +1 -0
- package/publications/user-guides.js +16 -0
- package/publications/user-guides.js.map +1 -0
- package/resolveio-server-app.d.ts +70 -0
- package/resolveio-server-app.js +801 -0
- package/resolveio-server-app.js.map +1 -0
- package/server-app.d.ts +228 -0
- package/server-app.js +3566 -0
- package/server-app.js.map +1 -0
- package/services/codex-client.d.ts +128 -0
- package/services/codex-client.js +1629 -0
- package/services/codex-client.js.map +1 -0
- package/services/openai-client.d.ts +46 -0
- package/services/openai-client.js +318 -0
- package/services/openai-client.js.map +1 -0
- package/types/error-report.d.ts +25 -0
- package/types/error-report.js +4 -0
- package/types/error-report.js.map +1 -0
- package/types/slow-query-report.d.ts +27 -0
- package/types/slow-query-report.js +6 -0
- package/types/slow-query-report.js.map +1 -0
- package/util/ai-qa-policy.d.ts +124 -0
- package/util/ai-qa-policy.js +736 -0
- package/util/ai-qa-policy.js.map +1 -0
- package/util/ai-run-evidence-adapters.d.ts +109 -0
- package/util/ai-run-evidence-adapters.js +7234 -0
- package/util/ai-run-evidence-adapters.js.map +1 -0
- package/util/ai-run-evidence-dashboard.d.ts +88 -0
- package/util/ai-run-evidence-dashboard.js +343 -0
- package/util/ai-run-evidence-dashboard.js.map +1 -0
- package/util/ai-run-evidence-eval.d.ts +86 -0
- package/util/ai-run-evidence-eval.js +1018 -0
- package/util/ai-run-evidence-eval.js.map +1 -0
- package/util/ai-run-evidence.d.ts +244 -0
- package/util/ai-run-evidence.js +1096 -0
- package/util/ai-run-evidence.js.map +1 -0
- package/util/ai-runner-artifacts.d.ts +82 -0
- package/util/ai-runner-artifacts.js +713 -0
- package/util/ai-runner-artifacts.js.map +1 -0
- package/util/ai-runner-manager-autopilot.d.ts +210 -0
- package/util/ai-runner-manager-autopilot.js +642 -0
- package/util/ai-runner-manager-autopilot.js.map +1 -0
- package/util/ai-runner-manager-policy.d.ts +807 -0
- package/util/ai-runner-manager-policy.js +3501 -0
- package/util/ai-runner-manager-policy.js.map +1 -0
- package/util/ai-runner-qa-auth.d.ts +5 -0
- package/util/ai-runner-qa-auth.js +839 -0
- package/util/ai-runner-qa-auth.js.map +1 -0
- package/util/ai-runner-qa-tools.d.ts +26 -0
- package/util/ai-runner-qa-tools.js +3520 -0
- package/util/ai-runner-qa-tools.js.map +1 -0
- package/util/aicoder-runner-v6.d.ts +426 -0
- package/util/aicoder-runner-v6.js +2464 -0
- package/util/aicoder-runner-v6.js.map +1 -0
- package/util/common.d.ts +31 -0
- package/util/common.js +683 -0
- package/util/common.js.map +1 -0
- package/util/customer-portal-password.d.ts +13 -0
- package/util/customer-portal-password.js +209 -0
- package/util/customer-portal-password.js.map +1 -0
- package/util/error-reporter.d.ts +52 -0
- package/util/error-reporter.js +326 -0
- package/util/error-reporter.js.map +1 -0
- package/util/error-tracking.d.ts +13 -0
- package/util/error-tracking.js +120 -0
- package/util/error-tracking.js.map +1 -0
- package/util/openai-usage-cost.d.ts +6 -0
- package/util/openai-usage-cost.js +103 -0
- package/util/openai-usage-cost.js.map +1 -0
- package/util/report-builder-unwinds.d.ts +15 -0
- package/util/report-builder-unwinds.js +156 -0
- package/util/report-builder-unwinds.js.map +1 -0
- package/util/runner-process-janitor.d.ts +27 -0
- package/util/runner-process-janitor.js +208 -0
- package/util/runner-process-janitor.js.map +1 -0
- package/util/schema-report-builder.d.ts +6 -0
- package/util/schema-report-builder.js +481 -0
- package/util/schema-report-builder.js.map +1 -0
- package/util/slow-query-reporter.d.ts +28 -0
- package/util/slow-query-reporter.js +226 -0
- package/util/slow-query-reporter.js.map +1 -0
- package/util/subscription-dependency-context.d.ts +34 -0
- package/util/subscription-dependency-context.js +1283 -0
- package/util/subscription-dependency-context.js.map +1 -0
- package/util/support-runner-v5.d.ts +1426 -0
- package/util/support-runner-v5.js +7643 -0
- package/util/support-runner-v5.js.map +1 -0
- package/util/tokenizer.d.ts +5 -0
- package/util/tokenizer.js +41 -0
- package/util/tokenizer.js.map +1 -0
- package/workers/codex-runner.worker.d.ts +1 -0
- package/workers/codex-runner.worker.js +192 -0
- package/workers/codex-runner.worker.js.map +1 -0
- package/.nodemon.json +0 -5
- package/.vscode/settings.json +0 -21
- package/AGENTS.md +0 -195
- package/README.md +0 -22
- package/build_package.sh +0 -5
- package/compileDTS.pl +0 -64
- package/docs/ai-assistant-nightly-eval.md +0 -65
- package/docs/ai-assistant-preflight-checklist.md +0 -23
- package/docs/ai-assistant-report-builder-bridge-playbook.md +0 -115
- package/eslint-plugin-custom/index.js +0 -7
- package/eslint-plugin-custom/rules/no-filter-zero-index.js +0 -44
- package/eslint.config.js +0 -103
- package/gulpfile.js +0 -216
- package/methodAndPublicationListGenerator.py +0 -375
- package/mongodbensurers.js +0 -2
- package/mongostop.js +0 -3
- package/scripts/cleanup-bypassed-callmethod-logs.js +0 -616
- package/settings.development.json +0 -25
- package/settings.development.redacted.json +0 -25
- package/src/.env +0 -12
- package/src/ai/assistant-core-heuristics.ts +0 -379
- package/src/ai/resolveio-platform-intelligence-memory-corpus.ts +0 -185
- package/src/ai/resolveio-platform-intelligence-memory.ts +0 -325
- package/src/ai/resolveio-platform-intelligence.ts +0 -462
- package/src/client-server-app.ts +0 -12
- package/src/collections/ai-run.collection.ts +0 -117
- package/src/collections/ai-terminal-conversation.collection.ts +0 -91
- package/src/collections/ai-terminal-issue-report.collection.ts +0 -99
- package/src/collections/ai-terminal-message.collection.ts +0 -77
- package/src/collections/app-setting.collection.ts +0 -104
- package/src/collections/app-status.collection.ts +0 -58
- package/src/collections/communication-metric.collection.ts +0 -84
- package/src/collections/counter.collection.ts +0 -56
- package/src/collections/cron-job-history.collection.ts +0 -94
- package/src/collections/cron-job.collection.ts +0 -92
- package/src/collections/customer-notification.collection.ts +0 -131
- package/src/collections/customer-portal-password.collection.ts +0 -76
- package/src/collections/email-history.collection.ts +0 -134
- package/src/collections/email-verified.collection.ts +0 -62
- package/src/collections/file.collection.ts +0 -74
- package/src/collections/flag-update.collection.ts +0 -57
- package/src/collections/flag.collection.ts +0 -57
- package/src/collections/log-method-latency.collection.ts +0 -77
- package/src/collections/log-subscription.collection.ts +0 -80
- package/src/collections/log.collection.ts +0 -93
- package/src/collections/logged-in-users.collection.ts +0 -67
- package/src/collections/monitor-cpu.collection.ts +0 -65
- package/src/collections/monitor-function.collection.ts +0 -74
- package/src/collections/monitor-memory.collection.ts +0 -77
- package/src/collections/monitor-mongo.collection.ts +0 -71
- package/src/collections/notification.collection.ts +0 -57
- package/src/collections/openai-usage-ledger.collection.ts +0 -131
- package/src/collections/report-builder-dashboard-builder.collection.ts +0 -109
- package/src/collections/report-builder-library.collection.ts +0 -89
- package/src/collections/report-builder-report.collection.ts +0 -184
- package/src/collections/user-group.collection.ts +0 -89
- package/src/collections/user-guide.collection.ts +0 -57
- package/src/collections/user.collection.ts +0 -181
- package/src/cron/cron.ts +0 -117
- package/src/fixtures/cron-jobs.ts +0 -95
- package/src/fixtures/init.ts +0 -35
- package/src/http/auth.ts +0 -818
- package/src/http/health.ts +0 -7
- package/src/http/home.ts +0 -90
- package/src/http/slow-query-publication.ts +0 -49
- package/src/index.ts +0 -1
- package/src/managers/ai-assistant-codex-manager.manager.ts +0 -1131
- package/src/managers/ai-run-evidence.manager.ts +0 -264
- package/src/managers/communication-metric.manager.ts +0 -82
- package/src/managers/cron.manager.ts +0 -333
- package/src/managers/customer-notification-content.manager.ts +0 -236
- package/src/managers/diagnostic-manager-bootstrap.ts +0 -165
- package/src/managers/error-auto-fix.manager.ts +0 -2767
- package/src/managers/local-log.manager.ts +0 -113
- package/src/managers/method.manager.ts +0 -1857
- package/src/managers/mongo.manager.ts +0 -4575
- package/src/managers/monitor.manager.ts +0 -507
- package/src/managers/openai-usage-ledger.manager.ts +0 -112
- package/src/managers/slow-query-verifier.manager.ts +0 -3590
- package/src/managers/slow-query.manager.ts +0 -519
- package/src/managers/subscription.manager.ts +0 -3128
- package/src/managers/websocket.manager.ts +0 -746
- package/src/managers/worker-dispatcher.manager.ts +0 -1360
- package/src/managers/worker-server.manager.ts +0 -536
- package/src/methods/accounts.ts +0 -532
- package/src/methods/ai-terminal.ts +0 -29070
- package/src/methods/app-settings.ts +0 -114
- package/src/methods/aws.ts +0 -649
- package/src/methods/collections.ts +0 -641
- package/src/methods/counters.ts +0 -69
- package/src/methods/cron-jobs.ts +0 -2614
- package/src/methods/customer-notifications.ts +0 -458
- package/src/methods/diagnostics.ts +0 -616
- package/src/methods/flag-updates.ts +0 -7
- package/src/methods/flags.ts +0 -7
- package/src/methods/logs.ts +0 -657
- package/src/methods/mongo-explorer.ts +0 -1880
- package/src/methods/monitor.ts +0 -540
- package/src/methods/pdf.ts +0 -1236
- package/src/methods/publications.ts +0 -129
- package/src/methods/report-builder.ts +0 -3300
- package/src/methods/support.ts +0 -335
- package/src/models/ai-run.model.ts +0 -27
- package/src/models/ai-terminal-conversation.model.ts +0 -19
- package/src/models/ai-terminal-issue-report.model.ts +0 -21
- package/src/models/ai-terminal-message.model.ts +0 -24
- package/src/models/app-setting.model.ts +0 -17
- package/src/models/collection-document.model.ts +0 -24
- package/src/models/communication-metric.model.ts +0 -23
- package/src/models/cron-job-history.model.ts +0 -16
- package/src/models/cron-job.model.ts +0 -15
- package/src/models/customer-notification.model.ts +0 -28
- package/src/models/customer-portal-password.model.ts +0 -12
- package/src/models/dialog.model.ts +0 -25
- package/src/models/log-method-latency.model.ts +0 -11
- package/src/models/log.model.ts +0 -19
- package/src/models/method.model.ts +0 -25
- package/src/models/monitor-function.model.ts +0 -16
- package/src/models/monitor-memory.model.ts +0 -17
- package/src/models/monitor-mongo.model.ts +0 -15
- package/src/models/openai-usage-ledger.model.ts +0 -56
- package/src/models/pagination.model.ts +0 -35
- package/src/models/permission.model.ts +0 -14
- package/src/models/report-builder-dashboard-builder.model.ts +0 -29
- package/src/models/report-builder-library.model.ts +0 -20
- package/src/models/report-builder-report.model.ts +0 -136
- package/src/models/report-builder.model.ts +0 -68
- package/src/models/select-data-label.model.ts +0 -9
- package/src/models/server-message.model.ts +0 -31
- package/src/models/slow-query-report.model.ts +0 -23
- package/src/models/subscription.model.ts +0 -73
- package/src/models/support-ticket.model.ts +0 -104
- package/src/models/user-group.model.ts +0 -24
- package/src/models/user.model.ts +0 -96
- package/src/private/images/ResolveIO.png +0 -0
- package/src/publications/ai-terminal.ts +0 -73
- package/src/publications/app-settings.ts +0 -25
- package/src/publications/app-status.ts +0 -13
- package/src/publications/cron-jobs.ts +0 -40
- package/src/publications/customer-notifications.ts +0 -101
- package/src/publications/files.ts +0 -33
- package/src/publications/flags-update.ts +0 -19
- package/src/publications/flags.ts +0 -19
- package/src/publications/logs.ts +0 -163
- package/src/publications/notifications.ts +0 -13
- package/src/publications/report-builder-dashboard-builders.ts +0 -39
- package/src/publications/report-builder-libraries.ts +0 -41
- package/src/publications/report-builder-reports.ts +0 -47
- package/src/publications/super-admin.ts +0 -13
- package/src/publications/user-groups.ts +0 -12
- package/src/publications/user-guides.ts +0 -12
- package/src/resolveio-server-app.ts +0 -617
- package/src/server-app.ts +0 -3354
- package/src/services/codex-client.ts +0 -1231
- package/src/services/openai-client.ts +0 -265
- package/src/types/error-report.ts +0 -26
- package/src/types/js-tiktoken.d.ts +0 -11
- package/src/types/slow-query-report.ts +0 -28
- package/src/util/ai-qa-policy.ts +0 -925
- package/src/util/ai-run-evidence-adapters.ts +0 -8347
- package/src/util/ai-run-evidence-dashboard.ts +0 -323
- package/src/util/ai-run-evidence-eval.ts +0 -1057
- package/src/util/ai-run-evidence.ts +0 -1430
- package/src/util/ai-runner-artifacts.ts +0 -586
- package/src/util/ai-runner-manager-autopilot.ts +0 -961
- package/src/util/ai-runner-manager-policy.ts +0 -5011
- package/src/util/ai-runner-qa-auth.ts +0 -838
- package/src/util/ai-runner-qa-tools.ts +0 -3536
- package/src/util/aicoder-runner-v6.ts +0 -3121
- package/src/util/common.ts +0 -649
- package/src/util/customer-portal-password.ts +0 -183
- package/src/util/error-reporter.ts +0 -332
- package/src/util/error-tracking.ts +0 -79
- package/src/util/openai-usage-cost.ts +0 -114
- package/src/util/report-builder-unwinds.ts +0 -180
- package/src/util/runner-process-janitor.ts +0 -219
- package/src/util/schema-report-builder.ts +0 -448
- package/src/util/slow-query-reporter.ts +0 -216
- package/src/util/subscription-dependency-context.ts +0 -1096
- package/src/util/support-runner-v5.ts +0 -10040
- package/src/util/tokenizer.ts +0 -38
- package/src/workers/codex-runner.worker.ts +0 -142
- package/start_server.sh +0 -5
- package/tests/ai-assistant-corpus-build.ts +0 -484
- package/tests/ai-assistant-corpus-replay-e2e.ts +0 -774
- package/tests/ai-assistant-data-parity-e2e.ts +0 -1989
- package/tests/ai-assistant-eval-triage.ts +0 -831
- package/tests/ai-assistant-openai-e2e.ts +0 -1061
- package/tests/ai-assistant-openai-git-e2e.ts +0 -155
- package/tests/ai-assistant-preflight-matrix.ts +0 -215
- package/tests/ai-assistant-routing-eval.test.ts +0 -585
- package/tests/ai-assistant-snf-live-eval.ts +0 -975
- package/tests/ai-assistant-utils.test.ts +0 -4834
- package/tests/ai-manager-autopilot-snapshot.test.ts +0 -193
- package/tests/ai-manager-recovery-checkpoint.test.ts +0 -1383
- package/tests/ai-run-eval.test.ts +0 -132
- package/tests/ai-run-evidence.test.ts +0 -3773
- package/tests/ai-runner-contract.test.ts +0 -515
- package/tests/aicoder-runner-v6.test.ts +0 -822
- package/tests/error-reporter.test.ts +0 -145
- package/tests/method-publication-generator.test.ts +0 -46
- package/tests/report-builder-linking.test.ts +0 -79
- package/tests/resolveio-platform-intelligence.test.ts +0 -352
- package/tests/server-app-cron-owner.test.ts +0 -127
- package/tests/subscription-connect-race.test.ts +0 -158
- package/tests/subscription-dependency-context.test.ts +0 -324
- package/tests/subscription-manager-collection-tracking.test.ts +0 -86
- package/tests/subscription-manager-invalidation.test.ts +0 -86
- package/tests/support-runner-v5.test.ts +0 -3201
- package/tsconfig.json +0 -34
- /package/{src/private → private}/email-templates/enrollment.html +0 -0
- /package/{src/private → private}/email-templates/forgot-password.html +0 -0
- /package/{src/private → private}/email-templates/support-ticket-deleted.html +0 -0
- /package/{src/private → private}/email-templates/support-ticket-modified.html +0 -0
- /package/{src/private → private}/email-templates/support-ticket.html +0 -0
- /package/{src/public_api.ts → public_api.d.ts} +0 -0
|
@@ -1,3590 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
import { ResolveIOServer } from '../resolveio-server-app';
|
|
3
|
-
import { pad, round } from '../util/common';
|
|
4
|
-
import { MongoClient } from 'mongodb';
|
|
5
|
-
import { Users } from '../collections/user.collection';
|
|
6
|
-
import { AppSettings } from '../collections/app-setting.collection';
|
|
7
|
-
import { buildGeneratedSlowQuerySummary, buildResolveIOProjectSlowQueryDetails } from './customer-notification-content.manager';
|
|
8
|
-
|
|
9
|
-
type AIDashboardJob = Record<string, any>;
|
|
10
|
-
type AICoderAppModel = Record<string, any>;
|
|
11
|
-
type ClientDBModel = Record<string, any>;
|
|
12
|
-
type SlowQueryExecutionOverride = Record<string, any>;
|
|
13
|
-
type SlowQueryVerificationStatus = 'pending' | 'passed' | 'failed';
|
|
14
|
-
type SlowQueryVerificationRun = Record<string, any>;
|
|
15
|
-
type SlowQueryLogModel = Record<string, any>;
|
|
16
|
-
|
|
17
|
-
export interface TokenEligibilityResult {
|
|
18
|
-
eligible: boolean;
|
|
19
|
-
reason?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type CheckAICoderTokenEligibilityFn = Function;
|
|
23
|
-
|
|
24
|
-
export interface SlowQueryVerifierDependencies {
|
|
25
|
-
ClientDBs?: any;
|
|
26
|
-
Clients?: any;
|
|
27
|
-
SlowQueryLogs: any;
|
|
28
|
-
AICoderApps?: any;
|
|
29
|
-
AIDashboardJobs?: any;
|
|
30
|
-
checkAICoderTokenEligibility?: CheckAICoderTokenEligibilityFn;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const OPTIONAL_COLLECTION = {
|
|
34
|
-
findOne: () => Promise.resolve(null),
|
|
35
|
-
find: () => Promise.resolve([])
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
let AICoderApps: any = OPTIONAL_COLLECTION;
|
|
39
|
-
let AIDashboardJobs: any = OPTIONAL_COLLECTION;
|
|
40
|
-
let ClientDBs: any = OPTIONAL_COLLECTION;
|
|
41
|
-
let Clients: any = OPTIONAL_COLLECTION;
|
|
42
|
-
let SlowQueryLogs: any = null;
|
|
43
|
-
const DEFAULT_CHECK_AICODER_TOKEN_ELIGIBILITY: CheckAICoderTokenEligibilityFn = () => {
|
|
44
|
-
return Promise.resolve({
|
|
45
|
-
eligible: true
|
|
46
|
-
});
|
|
47
|
-
};
|
|
48
|
-
let checkAICoderTokenEligibility: CheckAICoderTokenEligibilityFn = DEFAULT_CHECK_AICODER_TOKEN_ELIGIBILITY;
|
|
49
|
-
let configuredSlowQueryVerifierDependencies: SlowQueryVerifierDependencies | null = null;
|
|
50
|
-
|
|
51
|
-
function resolveSlowQueryVerifierDependencies(
|
|
52
|
-
overrides?: Partial<SlowQueryVerifierDependencies>
|
|
53
|
-
): SlowQueryVerifierDependencies {
|
|
54
|
-
const resolved: SlowQueryVerifierDependencies = {
|
|
55
|
-
...(configuredSlowQueryVerifierDependencies || {} as SlowQueryVerifierDependencies),
|
|
56
|
-
...(overrides || {})
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
if (!resolved.SlowQueryLogs) {
|
|
60
|
-
throw new Error('SlowQueryVerifier dependencies are not configured.');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return resolved;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function applySlowQueryVerifierDependencies(dependencies: SlowQueryVerifierDependencies): void {
|
|
67
|
-
configuredSlowQueryVerifierDependencies = dependencies;
|
|
68
|
-
ClientDBs = dependencies.ClientDBs || OPTIONAL_COLLECTION;
|
|
69
|
-
Clients = dependencies.Clients || OPTIONAL_COLLECTION;
|
|
70
|
-
SlowQueryLogs = dependencies.SlowQueryLogs;
|
|
71
|
-
AICoderApps = dependencies.AICoderApps || OPTIONAL_COLLECTION;
|
|
72
|
-
AIDashboardJobs = dependencies.AIDashboardJobs || OPTIONAL_COLLECTION;
|
|
73
|
-
checkAICoderTokenEligibility = dependencies.checkAICoderTokenEligibility || DEFAULT_CHECK_AICODER_TOKEN_ELIGIBILITY;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function registerSlowQueryVerifierDependencies(dependencies: SlowQueryVerifierDependencies): void {
|
|
77
|
-
applySlowQueryVerifierDependencies(dependencies);
|
|
78
|
-
ensureSlowQueryVerifier();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const VERIFICATION_CHECK_INTERVAL_MS = 60 * 1000;
|
|
82
|
-
const VERIFICATION_INTERVAL_MS = 5 * 60 * 1000;
|
|
83
|
-
const VERIFICATION_ATTEMPTS = 3;
|
|
84
|
-
const VERIFICATION_THRESHOLD_MS = 2000;
|
|
85
|
-
const AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO = 0.7;
|
|
86
|
-
const AUTO_OPTIMIZE_DEFAULT_RETURNED_DOC_TOLERANCE = 0.15;
|
|
87
|
-
const AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_QUERY = 3;
|
|
88
|
-
const AUTO_OPTIMIZE_DEFAULT_COOLDOWN_MINUTES = 120;
|
|
89
|
-
const AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_FINGERPRINT = 4;
|
|
90
|
-
const AUTO_OPTIMIZE_DEFAULT_FINGERPRINT_WINDOW_HOURS = 24;
|
|
91
|
-
const AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS = 10000;
|
|
92
|
-
const DEFAULT_ERROR_ALERT_EMAIL = 'dev@resolveio.com';
|
|
93
|
-
const MAX_LOCAL_NOTIFICATION_USERS = 5000;
|
|
94
|
-
|
|
95
|
-
interface ExplainStageSummary {
|
|
96
|
-
stage: string;
|
|
97
|
-
path: string;
|
|
98
|
-
executionTimeMs?: number;
|
|
99
|
-
docsExamined?: number;
|
|
100
|
-
keysExamined?: number;
|
|
101
|
-
nReturned?: number;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function ensureSlowQueryVerifier(serverConfig?: any): SlowQueryVerifier | null {
|
|
105
|
-
const existing = ResolveIOServer['SlowQueryVerifier'] as SlowQueryVerifier | null;
|
|
106
|
-
if (existing) {
|
|
107
|
-
return existing;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!configuredSlowQueryVerifierDependencies) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const resolvedServerConfig = serverConfig || ResolveIOServer.getServerConfig();
|
|
115
|
-
if (!resolvedServerConfig) {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const verifier = new SlowQueryVerifier(resolvedServerConfig);
|
|
120
|
-
ResolveIOServer['SlowQueryVerifier'] = verifier;
|
|
121
|
-
return verifier;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
interface ExecutionMetrics {
|
|
125
|
-
durationMs?: number;
|
|
126
|
-
docsExamined?: number;
|
|
127
|
-
nReturned?: number;
|
|
128
|
-
topStages: ExplainStageSummary[];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
interface OutputFingerprint {
|
|
132
|
-
explicitSort: boolean;
|
|
133
|
-
docsCompared: number;
|
|
134
|
-
truncated: boolean;
|
|
135
|
-
orderedDigest: string;
|
|
136
|
-
unorderedDigest: string;
|
|
137
|
-
firstDocDigest: string;
|
|
138
|
-
lastDocDigest: string;
|
|
139
|
-
durationMs: number;
|
|
140
|
-
maxDocs: number;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
interface OutputEquivalenceResult {
|
|
144
|
-
passed: boolean;
|
|
145
|
-
reason: string;
|
|
146
|
-
mode: 'ordered' | 'unordered' | 'unknown';
|
|
147
|
-
baseline?: OutputFingerprint;
|
|
148
|
-
after?: OutputFingerprint;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
interface ExplainResult {
|
|
152
|
-
durationMs: number;
|
|
153
|
-
explainPlan: Record<string, any>;
|
|
154
|
-
explainStats: Record<string, any>;
|
|
155
|
-
stageSummaries: ExplainStageSummary[];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
type SlowQueryVerifierErrorCode =
|
|
159
|
-
'client_db_not_found' |
|
|
160
|
-
'client_db_missing_uri' |
|
|
161
|
-
'client_db_missing_database' |
|
|
162
|
-
'main_db_unavailable' |
|
|
163
|
-
'aggregate_write_stage' |
|
|
164
|
-
'bson_too_large' |
|
|
165
|
-
'collection_missing';
|
|
166
|
-
|
|
167
|
-
class SlowQueryVerifierError extends Error {
|
|
168
|
-
public readonly code: SlowQueryVerifierErrorCode;
|
|
169
|
-
|
|
170
|
-
constructor(code: SlowQueryVerifierErrorCode, message: string) {
|
|
171
|
-
super(message);
|
|
172
|
-
this.code = code;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
interface SlowQueryVerifierConfig {
|
|
177
|
-
enabled: boolean;
|
|
178
|
-
fallbackToMainDB: boolean;
|
|
179
|
-
debugLogging: boolean;
|
|
180
|
-
configSource: string;
|
|
181
|
-
autofixRepoRoot: string;
|
|
182
|
-
autofixGithubOwner: string;
|
|
183
|
-
autofixGithubRepo: string;
|
|
184
|
-
autoOptimizeEnabled: boolean;
|
|
185
|
-
autoOptimizeWaitTimeoutMs: number;
|
|
186
|
-
autoOptimizeDurationRatioTarget: number;
|
|
187
|
-
autoOptimizeDocsRatioTarget: number;
|
|
188
|
-
autoOptimizeReturnedDocsTolerance: number;
|
|
189
|
-
autoOptimizeMaxAttemptsPerQuery: number;
|
|
190
|
-
autoOptimizeCooldownMinutes: number;
|
|
191
|
-
autoOptimizeMaxAttemptsPerFingerprint: number;
|
|
192
|
-
autoOptimizeFingerprintWindowHours: number;
|
|
193
|
-
autoOptimizeRequiredTokens: number;
|
|
194
|
-
autoOptimizeOutputCompareEnabled: boolean;
|
|
195
|
-
autoOptimizeRequireExactOutput: boolean;
|
|
196
|
-
autoOptimizeOutputMismatchRetryCount: number;
|
|
197
|
-
autoOptimizeOutputCompareMaxDocs: number;
|
|
198
|
-
escalationEmails: string[];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
interface ExplainTarget {
|
|
202
|
-
type: 'client' | 'main';
|
|
203
|
-
dbName: string;
|
|
204
|
-
uri?: string;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
type ExplainVerbosity = 'queryPlanner' | 'executionStats';
|
|
208
|
-
|
|
209
|
-
export class SlowQueryVerifier {
|
|
210
|
-
private readonly _timer?: NodeJS.Timeout;
|
|
211
|
-
private readonly config: SlowQueryVerifierConfig;
|
|
212
|
-
private readonly autoOptimizeInFlight = new Set<string>();
|
|
213
|
-
private readonly autoOptimizeDependenciesAvailable: boolean;
|
|
214
|
-
private static readonly APP_SETTINGS_CACHE_TTL_MS = 10000;
|
|
215
|
-
private appSettingsAutoOptimizeCacheExpiresAt = 0;
|
|
216
|
-
private appSettingsAutoOptimizeCacheValue: boolean | null = null;
|
|
217
|
-
|
|
218
|
-
constructor(serverConfig?: any, dependencies?: Partial<SlowQueryVerifierDependencies>) {
|
|
219
|
-
const resolvedDependencies = resolveSlowQueryVerifierDependencies(dependencies);
|
|
220
|
-
applySlowQueryVerifierDependencies(resolvedDependencies);
|
|
221
|
-
|
|
222
|
-
this.config = SlowQueryVerifier.resolveConfig(serverConfig);
|
|
223
|
-
this.autoOptimizeDependenciesAvailable = !!(resolvedDependencies.AICoderApps && resolvedDependencies.AIDashboardJobs);
|
|
224
|
-
|
|
225
|
-
if (this.config.enabled) {
|
|
226
|
-
this._timer = setInterval(() => {
|
|
227
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
228
|
-
this.poll().catch(err => {
|
|
229
|
-
console.error('Slow query verifier poll failed', err);
|
|
230
|
-
});
|
|
231
|
-
}, VERIFICATION_CHECK_INTERVAL_MS);
|
|
232
|
-
|
|
233
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
234
|
-
this.poll().catch(err => {
|
|
235
|
-
console.error('Slow query verifier failed to start', err);
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private static resolveConfig(serverConfig?: any): SlowQueryVerifierConfig {
|
|
241
|
-
const slowQueryConfig = (serverConfig && (serverConfig.slowQuery || serverConfig.SLOW_QUERY)) || {};
|
|
242
|
-
const verifierConfig = (slowQueryConfig && (slowQueryConfig.verifier || slowQueryConfig.slowQueryVerifier)) || {};
|
|
243
|
-
const autofixConfig = (serverConfig && (serverConfig.autofix || serverConfig.AUTOFIX)) || {};
|
|
244
|
-
|
|
245
|
-
const getBoolean = (envKey: string, configKey: string, fallback = true) => {
|
|
246
|
-
if (typeof process.env[envKey] !== 'undefined') {
|
|
247
|
-
return process.env[envKey] === 'true';
|
|
248
|
-
}
|
|
249
|
-
if (typeof verifierConfig[configKey] !== 'undefined') {
|
|
250
|
-
return !!verifierConfig[configKey];
|
|
251
|
-
}
|
|
252
|
-
return fallback;
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
const getNumber = (envKey: string, configKey: string, fallback: number): number => {
|
|
256
|
-
if (typeof process.env[envKey] !== 'undefined') {
|
|
257
|
-
const parsedEnv = Number(process.env[envKey]);
|
|
258
|
-
if (Number.isFinite(parsedEnv)) {
|
|
259
|
-
return parsedEnv;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (typeof verifierConfig[configKey] !== 'undefined') {
|
|
263
|
-
const parsedConfig = Number(verifierConfig[configKey]);
|
|
264
|
-
if (Number.isFinite(parsedConfig)) {
|
|
265
|
-
return parsedConfig;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return fallback;
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const getString = (envKey: string, configKey: string, fallback = ''): string => {
|
|
272
|
-
if (typeof process.env[envKey] !== 'undefined') {
|
|
273
|
-
return String(process.env[envKey] || '').trim();
|
|
274
|
-
}
|
|
275
|
-
if (typeof verifierConfig[configKey] !== 'undefined') {
|
|
276
|
-
return String(verifierConfig[configKey] || '').trim();
|
|
277
|
-
}
|
|
278
|
-
if (typeof slowQueryConfig[configKey] !== 'undefined') {
|
|
279
|
-
return String(slowQueryConfig[configKey] || '').trim();
|
|
280
|
-
}
|
|
281
|
-
if (typeof autofixConfig[configKey] !== 'undefined') {
|
|
282
|
-
return String(autofixConfig[configKey] || '').trim();
|
|
283
|
-
}
|
|
284
|
-
return fallback;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
const parseEmails = (value: any): string[] => {
|
|
288
|
-
if (Array.isArray(value)) {
|
|
289
|
-
return value.map(item => `${item || ''}`.trim().toLowerCase()).filter(Boolean);
|
|
290
|
-
}
|
|
291
|
-
if (typeof value === 'string') {
|
|
292
|
-
return value.split(',').map(item => item.trim().toLowerCase()).filter(Boolean);
|
|
293
|
-
}
|
|
294
|
-
return [];
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const clampRatio = (value: number, fallback: number): number => {
|
|
298
|
-
if (!Number.isFinite(value)) {
|
|
299
|
-
return fallback;
|
|
300
|
-
}
|
|
301
|
-
if (value <= 0 || value >= 1) {
|
|
302
|
-
return fallback;
|
|
303
|
-
}
|
|
304
|
-
return value;
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const normalizePositiveInt = (value: number, fallback: number): number => {
|
|
308
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
309
|
-
return fallback;
|
|
310
|
-
}
|
|
311
|
-
return Math.floor(value);
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const normalizeNonNegativeInt = (value: number, fallback = 0): number => {
|
|
315
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
316
|
-
return fallback;
|
|
317
|
-
}
|
|
318
|
-
return Math.floor(value);
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
const escalationEmails = parseEmails(
|
|
322
|
-
process.env.SLOW_QUERY_ESCALATION_EMAILS
|
|
323
|
-
|| verifierConfig.escalationEmails
|
|
324
|
-
|| slowQueryConfig.escalationEmails
|
|
325
|
-
|| slowQueryConfig.notifyEmails
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
enabled: true,
|
|
330
|
-
fallbackToMainDB: getBoolean('SLOW_QUERY_VERIFIER_FALLBACK_MAIN_DB', 'fallbackToMainDB', true),
|
|
331
|
-
debugLogging: getBoolean('SLOW_QUERY_VERIFIER_DEBUG_LOGS', 'debugLogging', false),
|
|
332
|
-
configSource: Object.keys(verifierConfig).length ? 'serverConfig' : 'defaults',
|
|
333
|
-
autofixRepoRoot: getString('AUTOFIX_REPO_ROOT', 'repoRoot', '/var/app/current'),
|
|
334
|
-
autofixGithubOwner: getString('AUTOFIX_GITHUB_OWNER', 'githubOwner', 'resolveio'),
|
|
335
|
-
autofixGithubRepo: getString('AUTOFIX_GITHUB_REPO', 'githubRepo', ''),
|
|
336
|
-
autoOptimizeEnabled: getBoolean('SLOW_QUERY_AUTO_OPTIMIZE_ENABLED', 'autoOptimizeEnabled', false),
|
|
337
|
-
autoOptimizeWaitTimeoutMs: getNumber('SLOW_QUERY_AUTO_OPTIMIZE_WAIT_TIMEOUT_MS', 'autoOptimizeWaitTimeoutMs', 45 * 60 * 1000),
|
|
338
|
-
autoOptimizeDurationRatioTarget: clampRatio(
|
|
339
|
-
getNumber('SLOW_QUERY_AUTO_OPTIMIZE_DURATION_RATIO', 'autoOptimizeDurationRatioTarget', AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO),
|
|
340
|
-
AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO
|
|
341
|
-
),
|
|
342
|
-
autoOptimizeDocsRatioTarget: clampRatio(
|
|
343
|
-
getNumber('SLOW_QUERY_AUTO_OPTIMIZE_DOCS_RATIO', 'autoOptimizeDocsRatioTarget', AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO),
|
|
344
|
-
AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO
|
|
345
|
-
),
|
|
346
|
-
autoOptimizeReturnedDocsTolerance: clampRatio(
|
|
347
|
-
getNumber('SLOW_QUERY_AUTO_OPTIMIZE_RETURNED_DOCS_TOLERANCE', 'autoOptimizeReturnedDocsTolerance', AUTO_OPTIMIZE_DEFAULT_RETURNED_DOC_TOLERANCE),
|
|
348
|
-
AUTO_OPTIMIZE_DEFAULT_RETURNED_DOC_TOLERANCE
|
|
349
|
-
),
|
|
350
|
-
autoOptimizeMaxAttemptsPerQuery: normalizePositiveInt(
|
|
351
|
-
getNumber('SLOW_QUERY_AUTO_OPTIMIZE_MAX_ATTEMPTS_PER_QUERY', 'autoOptimizeMaxAttemptsPerQuery', AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_QUERY),
|
|
352
|
-
AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_QUERY
|
|
353
|
-
),
|
|
354
|
-
autoOptimizeCooldownMinutes: normalizeNonNegativeInt(
|
|
355
|
-
getNumber('SLOW_QUERY_AUTO_OPTIMIZE_COOLDOWN_MINUTES', 'autoOptimizeCooldownMinutes', AUTO_OPTIMIZE_DEFAULT_COOLDOWN_MINUTES),
|
|
356
|
-
AUTO_OPTIMIZE_DEFAULT_COOLDOWN_MINUTES
|
|
357
|
-
),
|
|
358
|
-
autoOptimizeMaxAttemptsPerFingerprint: normalizePositiveInt(
|
|
359
|
-
getNumber(
|
|
360
|
-
'SLOW_QUERY_AUTO_OPTIMIZE_MAX_ATTEMPTS_PER_FINGERPRINT',
|
|
361
|
-
'autoOptimizeMaxAttemptsPerFingerprint',
|
|
362
|
-
AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_FINGERPRINT
|
|
363
|
-
),
|
|
364
|
-
AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_FINGERPRINT
|
|
365
|
-
),
|
|
366
|
-
autoOptimizeFingerprintWindowHours: normalizePositiveInt(
|
|
367
|
-
getNumber(
|
|
368
|
-
'SLOW_QUERY_AUTO_OPTIMIZE_FINGERPRINT_WINDOW_HOURS',
|
|
369
|
-
'autoOptimizeFingerprintWindowHours',
|
|
370
|
-
AUTO_OPTIMIZE_DEFAULT_FINGERPRINT_WINDOW_HOURS
|
|
371
|
-
),
|
|
372
|
-
AUTO_OPTIMIZE_DEFAULT_FINGERPRINT_WINDOW_HOURS
|
|
373
|
-
),
|
|
374
|
-
autoOptimizeRequiredTokens: normalizeNonNegativeInt(
|
|
375
|
-
getNumber('SLOW_QUERY_AUTO_OPTIMIZE_REQUIRED_TOKENS', 'autoOptimizeRequiredTokens', 0),
|
|
376
|
-
0
|
|
377
|
-
),
|
|
378
|
-
autoOptimizeOutputCompareEnabled: getBoolean('SLOW_QUERY_AUTO_OPTIMIZE_OUTPUT_COMPARE_ENABLED', 'autoOptimizeOutputCompareEnabled', true),
|
|
379
|
-
autoOptimizeRequireExactOutput: getBoolean('SLOW_QUERY_AUTO_OPTIMIZE_REQUIRE_EXACT_OUTPUT', 'autoOptimizeRequireExactOutput', true),
|
|
380
|
-
autoOptimizeOutputMismatchRetryCount: normalizeNonNegativeInt(
|
|
381
|
-
getNumber('SLOW_QUERY_AUTO_OPTIMIZE_OUTPUT_MISMATCH_RETRY_COUNT', 'autoOptimizeOutputMismatchRetryCount', 2),
|
|
382
|
-
2
|
|
383
|
-
),
|
|
384
|
-
autoOptimizeOutputCompareMaxDocs: normalizePositiveInt(
|
|
385
|
-
getNumber(
|
|
386
|
-
'SLOW_QUERY_AUTO_OPTIMIZE_OUTPUT_COMPARE_MAX_DOCS',
|
|
387
|
-
'autoOptimizeOutputCompareMaxDocs',
|
|
388
|
-
AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS
|
|
389
|
-
),
|
|
390
|
-
AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS
|
|
391
|
-
),
|
|
392
|
-
escalationEmails
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
private parseBooleanEnv(envKey: string): boolean | null {
|
|
397
|
-
if (typeof process.env[envKey] === 'undefined') {
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
return process.env[envKey] === 'true';
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
private async resolveAutoOptimizeEnabled(): Promise<boolean> {
|
|
404
|
-
if (!this.autoOptimizeDependenciesAvailable) {
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const now = Date.now();
|
|
409
|
-
if (this.appSettingsAutoOptimizeCacheValue !== null && now < this.appSettingsAutoOptimizeCacheExpiresAt) {
|
|
410
|
-
return this.appSettingsAutoOptimizeCacheValue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const envValue = this.parseBooleanEnv('SLOW_QUERY_AUTO_OPTIMIZE_ENABLED');
|
|
414
|
-
let enabled = envValue !== null ? envValue : !!this.config.autoOptimizeEnabled;
|
|
415
|
-
try {
|
|
416
|
-
if (AppSettings && typeof AppSettings.findOne === 'function') {
|
|
417
|
-
const activeSetting = await AppSettings.findOne({
|
|
418
|
-
is_active: {
|
|
419
|
-
$ne: false
|
|
420
|
-
}
|
|
421
|
-
}, {
|
|
422
|
-
sort: {
|
|
423
|
-
updatedAt: -1,
|
|
424
|
-
createdAt: -1
|
|
425
|
-
}
|
|
426
|
-
}) || await AppSettings.findOne({}, {
|
|
427
|
-
sort: {
|
|
428
|
-
updatedAt: -1,
|
|
429
|
-
createdAt: -1
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
if (activeSetting && typeof activeSetting.enable_slow_query_optimizer === 'boolean') {
|
|
434
|
-
enabled = !!activeSetting.enable_slow_query_optimizer;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
if (this.config?.debugLogging) {
|
|
440
|
-
console.warn('SlowQueryVerifier failed to read app settings slow-query optimizer toggle', error);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
this.appSettingsAutoOptimizeCacheValue = enabled;
|
|
445
|
-
this.appSettingsAutoOptimizeCacheExpiresAt = now + SlowQueryVerifier.APP_SETTINGS_CACHE_TTL_MS;
|
|
446
|
-
return enabled;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private async poll() {
|
|
450
|
-
const autoOptimizeEnabled = await this.resolveAutoOptimizeEnabled();
|
|
451
|
-
if (!autoOptimizeEnabled) {
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const now = new Date();
|
|
456
|
-
const candidates = await SlowQueryLogs.find({
|
|
457
|
-
ignored: {
|
|
458
|
-
$ne: true
|
|
459
|
-
},
|
|
460
|
-
verification_status: {
|
|
461
|
-
$in: [null, 'pending']
|
|
462
|
-
},
|
|
463
|
-
$or: [
|
|
464
|
-
{
|
|
465
|
-
verification_next_run_at: {
|
|
466
|
-
$exists: false
|
|
467
|
-
}
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
verification_next_run_at: {
|
|
471
|
-
$lte: now
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
]
|
|
475
|
-
}, {
|
|
476
|
-
sort: {
|
|
477
|
-
verification_next_run_at: 1,
|
|
478
|
-
last_seen_at: -1
|
|
479
|
-
},
|
|
480
|
-
limit: 5
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
for (const candidate of candidates) {
|
|
484
|
-
await this.processCandidate(candidate);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
private async processCandidate(candidate: SlowQueryLogModel) {
|
|
489
|
-
if (!candidate || !candidate._id) {
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const claimAt = new Date(Date.now() + VERIFICATION_INTERVAL_MS);
|
|
494
|
-
await SlowQueryLogs.updateOne({_id: candidate._id}, {
|
|
495
|
-
$set: {
|
|
496
|
-
verification_next_run_at: claimAt
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
try {
|
|
501
|
-
const result = await this.runExplain(candidate);
|
|
502
|
-
await this.handleExplainResult(candidate, result);
|
|
503
|
-
}
|
|
504
|
-
catch (err) {
|
|
505
|
-
await this.handleExplainError(candidate, err);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
public async generateExplainForLog(logId: string): Promise<void> {
|
|
510
|
-
if (!logId) {
|
|
511
|
-
throw new Error('Slow query log ID is required.');
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const log = await SlowQueryLogs.findOne({_id: logId});
|
|
515
|
-
|
|
516
|
-
if (!log) {
|
|
517
|
-
throw new Error('Slow query log not found.');
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
try {
|
|
521
|
-
const result = await this.runExplain(log);
|
|
522
|
-
await this.handleExplainResult(log, result);
|
|
523
|
-
}
|
|
524
|
-
catch (err) {
|
|
525
|
-
await this.handleExplainError(log, err);
|
|
526
|
-
throw err;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
public async runLog(logId: string, options?: { force?: boolean }): Promise<{
|
|
531
|
-
status: 'in_progress' | 'success' | 'failed' | 'updated' | 'not_found';
|
|
532
|
-
reason?: string;
|
|
533
|
-
log?: SlowQueryLogModel;
|
|
534
|
-
}> {
|
|
535
|
-
if (!logId) {
|
|
536
|
-
throw new Error('Slow query log ID is required.');
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const existing = await SlowQueryLogs.findOne({_id: logId});
|
|
540
|
-
if (!existing) {
|
|
541
|
-
return {
|
|
542
|
-
status: 'not_found',
|
|
543
|
-
reason: 'Slow query log not found.'
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
if (existing.ignored) {
|
|
548
|
-
throw new Error('Slow query log is ignored.');
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (this.autoOptimizeInFlight.has(logId) || existing.auto_fix_status === 'running' || existing.status === 'queued') {
|
|
552
|
-
return {
|
|
553
|
-
status: 'in_progress',
|
|
554
|
-
reason: 'Slow query optimization is already running.',
|
|
555
|
-
log: existing
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
this.autoOptimizeInFlight.add(logId);
|
|
560
|
-
try {
|
|
561
|
-
await this.runLogWithRetries(logId, {
|
|
562
|
-
force: !!options?.force,
|
|
563
|
-
retryOutputMismatch: true
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
finally {
|
|
567
|
-
this.autoOptimizeInFlight.delete(logId);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const updated = await SlowQueryLogs.findOne({_id: logId});
|
|
571
|
-
if (!updated) {
|
|
572
|
-
return {
|
|
573
|
-
status: 'not_found',
|
|
574
|
-
reason: 'Slow query log no longer exists.'
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (updated.auto_fix_status === 'running' || updated.status === 'queued') {
|
|
579
|
-
return {
|
|
580
|
-
status: 'in_progress',
|
|
581
|
-
reason: 'Slow query optimization queued.',
|
|
582
|
-
log: updated
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (updated.auto_fix_status === 'completed' || updated.status === 'optimized') {
|
|
587
|
-
return {
|
|
588
|
-
status: 'success',
|
|
589
|
-
reason: 'Slow query optimization completed.',
|
|
590
|
-
log: updated
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (updated.auto_fix_status === 'failed') {
|
|
595
|
-
return {
|
|
596
|
-
status: 'failed',
|
|
597
|
-
reason: updated.verification_notes || updated.auto_fix_disabled_reason || 'Slow query optimization failed.',
|
|
598
|
-
log: updated
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
return {
|
|
603
|
-
status: 'updated',
|
|
604
|
-
reason: updated.verification_notes || '',
|
|
605
|
-
log: updated
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
public async queueLogRun(logId: string, options?: { force?: boolean }): Promise<{
|
|
610
|
-
status: 'in_progress' | 'not_found' | 'failed';
|
|
611
|
-
reason?: string;
|
|
612
|
-
log?: SlowQueryLogModel;
|
|
613
|
-
}> {
|
|
614
|
-
if (!logId) {
|
|
615
|
-
throw new Error('Slow query log ID is required.');
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const existing = await SlowQueryLogs.findOne({_id: logId});
|
|
619
|
-
if (!existing) {
|
|
620
|
-
return {
|
|
621
|
-
status: 'not_found',
|
|
622
|
-
reason: 'Slow query log not found.'
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
if (existing.ignored) {
|
|
627
|
-
throw new Error('Slow query log is ignored.');
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
if (this.autoOptimizeInFlight.has(logId) || existing.auto_fix_status === 'running' || existing.status === 'queued') {
|
|
631
|
-
return {
|
|
632
|
-
status: 'in_progress',
|
|
633
|
-
reason: 'Slow query optimization is already running.',
|
|
634
|
-
log: existing
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const queuedAt = new Date();
|
|
639
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
640
|
-
$set: {
|
|
641
|
-
status: 'queued',
|
|
642
|
-
auto_fix_status: 'queued',
|
|
643
|
-
verification_notes: 'Slow query optimization queued from super-admin.',
|
|
644
|
-
last_triaged_by: 'super-admin',
|
|
645
|
-
last_triaged_at: queuedAt
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
const queuedLog = (await SlowQueryLogs.findOne({_id: logId})) || existing;
|
|
649
|
-
|
|
650
|
-
this.autoOptimizeInFlight.add(logId);
|
|
651
|
-
setImmediate(async () => {
|
|
652
|
-
try {
|
|
653
|
-
await this.runLogWithRetries(logId, {
|
|
654
|
-
force: !!options?.force,
|
|
655
|
-
retryOutputMismatch: true
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
catch (error) {
|
|
659
|
-
console.error('Slow query queued run failed', {logId, error: (error as Error)?.message || error});
|
|
660
|
-
}
|
|
661
|
-
finally {
|
|
662
|
-
this.autoOptimizeInFlight.delete(logId);
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
return {
|
|
667
|
-
status: 'in_progress',
|
|
668
|
-
reason: 'Slow query optimization queued.',
|
|
669
|
-
log: queuedLog
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
public async deployLog(logId: string): Promise<{
|
|
674
|
-
status: 'success' | 'failed' | 'not_found';
|
|
675
|
-
reason?: string;
|
|
676
|
-
log?: SlowQueryLogModel;
|
|
677
|
-
}> {
|
|
678
|
-
if (!logId) {
|
|
679
|
-
throw new Error('Slow query log ID is required.');
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const log = await SlowQueryLogs.findOne({_id: logId});
|
|
683
|
-
if (!log) {
|
|
684
|
-
return {
|
|
685
|
-
status: 'not_found',
|
|
686
|
-
reason: 'Slow query log not found.'
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const jobId = String(log.openai_task_id || '').trim();
|
|
691
|
-
if (!jobId) {
|
|
692
|
-
throw new Error('Slow query log does not have a dashboard job id to deploy.');
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
const job = await this.publishDashboardJob(jobId);
|
|
697
|
-
const publishOutcome = this.evaluateDashboardPublishOutcome(job);
|
|
698
|
-
const now = new Date();
|
|
699
|
-
const existingResult = (log.auto_fix_result && typeof log.auto_fix_result === 'object')
|
|
700
|
-
? {...log.auto_fix_result}
|
|
701
|
-
: {};
|
|
702
|
-
existingResult.manual_deploy = {
|
|
703
|
-
job_id: jobId,
|
|
704
|
-
requested_at: now,
|
|
705
|
-
success: publishOutcome.success,
|
|
706
|
-
message: publishOutcome.message,
|
|
707
|
-
branch_name: publishOutcome.branchName || existingResult.publish_branch || ''
|
|
708
|
-
};
|
|
709
|
-
if (publishOutcome.branchName) {
|
|
710
|
-
existingResult.publish_branch = publishOutcome.branchName;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
714
|
-
$set: {
|
|
715
|
-
auto_fix_result: existingResult,
|
|
716
|
-
verification_notes: publishOutcome.success
|
|
717
|
-
? `Manual deploy completed: ${publishOutcome.message}`
|
|
718
|
-
: `Manual deploy failed: ${publishOutcome.message}`,
|
|
719
|
-
last_triaged_by: 'super-admin',
|
|
720
|
-
last_triaged_at: now
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
const refreshed = (await SlowQueryLogs.findOne({_id: logId})) || log;
|
|
725
|
-
return {
|
|
726
|
-
status: publishOutcome.success ? 'success' : 'failed',
|
|
727
|
-
reason: publishOutcome.message,
|
|
728
|
-
log: refreshed
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
catch (error) {
|
|
732
|
-
const message = (error as Error)?.message || 'Manual deploy failed.';
|
|
733
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
734
|
-
$set: {
|
|
735
|
-
verification_notes: `Manual deploy failed: ${message}`,
|
|
736
|
-
last_triaged_by: 'super-admin',
|
|
737
|
-
last_triaged_at: new Date()
|
|
738
|
-
}
|
|
739
|
-
});
|
|
740
|
-
const refreshed = await SlowQueryLogs.findOne({_id: logId});
|
|
741
|
-
return {
|
|
742
|
-
status: 'failed',
|
|
743
|
-
reason: message,
|
|
744
|
-
log: refreshed || log
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
private async runLogWithRetries(
|
|
750
|
-
logId: string,
|
|
751
|
-
options: { force: boolean; retryOutputMismatch: boolean }
|
|
752
|
-
): Promise<SlowQueryLogModel | null> {
|
|
753
|
-
const retryBudget = options.retryOutputMismatch
|
|
754
|
-
? (Number.isFinite(Number(this.config.autoOptimizeOutputMismatchRetryCount))
|
|
755
|
-
? Number(this.config.autoOptimizeOutputMismatchRetryCount)
|
|
756
|
-
: 0)
|
|
757
|
-
: 0;
|
|
758
|
-
let retriesUsed = 0;
|
|
759
|
-
|
|
760
|
-
while (true) {
|
|
761
|
-
await this.runAutoOptimization(logId, options.force);
|
|
762
|
-
const latest = await SlowQueryLogs.findOne({_id: logId});
|
|
763
|
-
if (!latest) {
|
|
764
|
-
return null;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if (
|
|
768
|
-
!options.retryOutputMismatch
|
|
769
|
-
|| !this.shouldRetryForOutputMismatch(latest)
|
|
770
|
-
|| retriesUsed >= retryBudget
|
|
771
|
-
) {
|
|
772
|
-
return latest;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const maxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerQuery))
|
|
776
|
-
? Number(this.config.autoOptimizeMaxAttemptsPerQuery)
|
|
777
|
-
: 0;
|
|
778
|
-
const attemptsUsed = Number.isFinite(Number(latest.auto_fix_attempt_count))
|
|
779
|
-
? Number(latest.auto_fix_attempt_count)
|
|
780
|
-
: 0;
|
|
781
|
-
if (maxAttempts > 0 && attemptsUsed >= maxAttempts) {
|
|
782
|
-
return latest;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
retriesUsed += 1;
|
|
786
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
787
|
-
$set: {
|
|
788
|
-
verification_notes: `Output equivalence mismatch detected. Retrying (${retriesUsed}/${retryBudget}).`,
|
|
789
|
-
last_triaged_by: 'auto-slow-query',
|
|
790
|
-
last_triaged_at: new Date()
|
|
791
|
-
}
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
private shouldRetryForOutputMismatch(log: SlowQueryLogModel): boolean {
|
|
797
|
-
if (!log || log.ignored || log.auto_fix_status !== 'failed') {
|
|
798
|
-
return false;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const result = (log.auto_fix_result && typeof log.auto_fix_result === 'object')
|
|
802
|
-
? log.auto_fix_result
|
|
803
|
-
: {};
|
|
804
|
-
const validation = (result.validation && typeof result.validation === 'object')
|
|
805
|
-
? result.validation
|
|
806
|
-
: {};
|
|
807
|
-
const outputEquivalence = result.output_equivalence;
|
|
808
|
-
const validationReason = String(validation.reason || '').toLowerCase();
|
|
809
|
-
const notes = String(log.verification_notes || '').toLowerCase();
|
|
810
|
-
|
|
811
|
-
if (outputEquivalence && outputEquivalence.passed === false) {
|
|
812
|
-
return true;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
return validationReason.includes('output equivalence')
|
|
816
|
-
|| notes.includes('output equivalence');
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
private async runExplain(log: SlowQueryLogModel, overrides?: SlowQueryExecutionOverride): Promise<ExplainResult> {
|
|
820
|
-
const collectionName = log.collection;
|
|
821
|
-
|
|
822
|
-
if (!collectionName) {
|
|
823
|
-
throw new Error('Slow query missing collection name.');
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
const target = await this.resolveExplainTarget(log);
|
|
827
|
-
let client: MongoClient | undefined;
|
|
828
|
-
let db: any;
|
|
829
|
-
|
|
830
|
-
try {
|
|
831
|
-
if (target.type === 'client') {
|
|
832
|
-
if (!target.uri) {
|
|
833
|
-
throw new SlowQueryVerifierError('client_db_missing_uri', 'Client DB missing uri.');
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
client = await MongoClient.connect(target.uri, {
|
|
837
|
-
connectTimeoutMS: 10000,
|
|
838
|
-
serverSelectionTimeoutMS: 10000,
|
|
839
|
-
readPreference: 'secondaryPreferred'
|
|
840
|
-
});
|
|
841
|
-
db = client.db(target.dbName);
|
|
842
|
-
}
|
|
843
|
-
else {
|
|
844
|
-
db = ResolveIOServer.getMainDB();
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (!db) {
|
|
848
|
-
throw new SlowQueryVerifierError('main_db_unavailable', 'Main server DB is not available.');
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const effectiveLog = SlowQueryVerifier.applyQueryOverrides(log, overrides);
|
|
852
|
-
const pipeline = SlowQueryVerifier.extractPipelineFromLog(effectiveLog);
|
|
853
|
-
const filter = effectiveLog.filter ?? {};
|
|
854
|
-
const findOptions = SlowQueryVerifier.extractFindOptions(effectiveLog.options);
|
|
855
|
-
const aggregateOptions = SlowQueryVerifier.extractAggregateOptions(effectiveLog.options);
|
|
856
|
-
let explainResponse: Record<string, any>;
|
|
857
|
-
let usedVerbosity: ExplainVerbosity = 'executionStats';
|
|
858
|
-
|
|
859
|
-
const explainAggregate = async (verbosity: ExplainVerbosity): Promise<Record<string, any>> => {
|
|
860
|
-
if (SlowQueryVerifier.pipelineHasWriteStage(pipeline as any[])) {
|
|
861
|
-
throw new SlowQueryVerifierError('aggregate_write_stage', 'Aggregate pipeline includes a write stage; verification skipped.');
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
try {
|
|
865
|
-
return await db.collection(collectionName)
|
|
866
|
-
.aggregate(pipeline, {
|
|
867
|
-
...(aggregateOptions || {}),
|
|
868
|
-
readPreference: 'secondaryPreferred'
|
|
869
|
-
})
|
|
870
|
-
.explain(verbosity);
|
|
871
|
-
}
|
|
872
|
-
catch (err: any) {
|
|
873
|
-
if (SlowQueryVerifier.isAggregateExplainWriteConcernError(err)) {
|
|
874
|
-
return await db.command(
|
|
875
|
-
SlowQueryVerifier.buildAggregateExplainCommand(collectionName, pipeline as any[], aggregateOptions, verbosity),
|
|
876
|
-
{readPreference: 'secondaryPreferred'}
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
throw err;
|
|
881
|
-
}
|
|
882
|
-
};
|
|
883
|
-
|
|
884
|
-
try {
|
|
885
|
-
if (pipeline) {
|
|
886
|
-
explainResponse = await explainAggregate('executionStats');
|
|
887
|
-
}
|
|
888
|
-
else {
|
|
889
|
-
const cursor = SlowQueryVerifier.buildFindCursor(db.collection(collectionName), filter, findOptions);
|
|
890
|
-
explainResponse = await cursor.explain('executionStats');
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
catch (err: any) {
|
|
894
|
-
const code = err?.code;
|
|
895
|
-
const codeName = `${err?.codeName || ''}`.trim();
|
|
896
|
-
const message = `${err?.message || ''}`.toLowerCase();
|
|
897
|
-
|
|
898
|
-
if (code === 26 || codeName === 'NamespaceNotFound' || message.includes('ns does not exist')) {
|
|
899
|
-
throw new SlowQueryVerifierError('collection_missing', `Collection '${collectionName}' not found in ${target.type} DB.`);
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
if (pipeline) {
|
|
903
|
-
if (SlowQueryVerifier.isBsonObjectTooLargeError(err)) {
|
|
904
|
-
try {
|
|
905
|
-
explainResponse = await explainAggregate('queryPlanner');
|
|
906
|
-
usedVerbosity = 'queryPlanner';
|
|
907
|
-
}
|
|
908
|
-
catch (fallbackErr: any) {
|
|
909
|
-
if (SlowQueryVerifier.isBsonObjectTooLargeError(fallbackErr)) {
|
|
910
|
-
const durationMs = await SlowQueryVerifier.measureExecution(db, collectionName, pipeline, filter, findOptions, aggregateOptions);
|
|
911
|
-
if (!SlowQueryVerifier.isValidDuration(durationMs)) {
|
|
912
|
-
throw new SlowQueryVerifierError('bson_too_large', 'Explain/query payload too large to verify.');
|
|
913
|
-
}
|
|
914
|
-
return {
|
|
915
|
-
durationMs,
|
|
916
|
-
explainPlan: {},
|
|
917
|
-
explainStats: {},
|
|
918
|
-
stageSummaries: []
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
throw fallbackErr;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
else {
|
|
926
|
-
throw err;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
else {
|
|
930
|
-
if (SlowQueryVerifier.isBsonObjectTooLargeError(err)) {
|
|
931
|
-
try {
|
|
932
|
-
const cursor = SlowQueryVerifier.buildFindCursor(db.collection(collectionName), filter, findOptions);
|
|
933
|
-
explainResponse = await cursor.explain('queryPlanner');
|
|
934
|
-
usedVerbosity = 'queryPlanner';
|
|
935
|
-
}
|
|
936
|
-
catch {
|
|
937
|
-
const durationMs = await SlowQueryVerifier.measureExecution(db, collectionName, pipeline, filter, findOptions, aggregateOptions);
|
|
938
|
-
if (!SlowQueryVerifier.isValidDuration(durationMs)) {
|
|
939
|
-
throw new SlowQueryVerifierError('bson_too_large', 'Explain/query payload too large to verify.');
|
|
940
|
-
}
|
|
941
|
-
return {
|
|
942
|
-
durationMs,
|
|
943
|
-
explainPlan: {},
|
|
944
|
-
explainStats: {},
|
|
945
|
-
stageSummaries: []
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
else {
|
|
950
|
-
throw err;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
let durationMs = SlowQueryVerifier.resolveDurationMs(explainResponse);
|
|
956
|
-
|
|
957
|
-
if (!SlowQueryVerifier.isValidDuration(durationMs)) {
|
|
958
|
-
durationMs = await SlowQueryVerifier.measureExecution(db, collectionName, pipeline, filter, findOptions, aggregateOptions);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const explainPlanRaw = explainResponse?.queryPlanner ?? explainResponse ?? {};
|
|
962
|
-
const explainStatsRaw = usedVerbosity === 'queryPlanner' ? {} : (explainResponse?.executionStats ?? {});
|
|
963
|
-
const stageSummaries = SlowQueryVerifier.extractStageSummaries(explainResponse, explainStatsRaw);
|
|
964
|
-
|
|
965
|
-
const explainPlan = SlowQueryVerifier.sanitizeExplainPayload(SlowQueryVerifier.normalizeExplainPayload(explainPlanRaw));
|
|
966
|
-
const explainStats = SlowQueryVerifier.sanitizeExplainPayload(SlowQueryVerifier.normalizeExplainPayload(explainStatsRaw));
|
|
967
|
-
|
|
968
|
-
return {
|
|
969
|
-
durationMs,
|
|
970
|
-
explainPlan,
|
|
971
|
-
explainStats,
|
|
972
|
-
stageSummaries
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
finally {
|
|
976
|
-
if (client) {
|
|
977
|
-
await client.close();
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
public async measureQuery(log: SlowQueryLogModel, overrides?: SlowQueryExecutionOverride): Promise<ExplainResult> {
|
|
983
|
-
return this.runExplain(log, overrides);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
private async handleExplainResult(log: SlowQueryLogModel, result: ExplainResult) {
|
|
987
|
-
if (!log._id) {
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const existingRuns = (Array.isArray(log.verification_runs) ? log.verification_runs : [])
|
|
992
|
-
.filter(run => SlowQueryVerifier.isValidDuration(run.duration_ms));
|
|
993
|
-
const durationValid = SlowQueryVerifier.isValidDuration(result.durationMs);
|
|
994
|
-
|
|
995
|
-
if (!durationValid) {
|
|
996
|
-
if (this.config.debugLogging) {
|
|
997
|
-
console.warn('Slow query verification reported invalid duration; keeping log pending.', log.collection, result.durationMs);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const previousInvalidAttempts = typeof log.verification_invalid_attempts === 'number'
|
|
1001
|
-
? log.verification_invalid_attempts
|
|
1002
|
-
: 0;
|
|
1003
|
-
const invalidAttempts = previousInvalidAttempts + 1;
|
|
1004
|
-
const shouldFail = invalidAttempts >= VERIFICATION_ATTEMPTS;
|
|
1005
|
-
|
|
1006
|
-
const nextRunAt = new Date(Date.now() + VERIFICATION_INTERVAL_MS);
|
|
1007
|
-
const updateOps: Record<string, any> = {
|
|
1008
|
-
$set: {
|
|
1009
|
-
verification_runs: existingRuns,
|
|
1010
|
-
verification_status: shouldFail ? 'failed' : 'pending',
|
|
1011
|
-
verification_invalid_attempts: invalidAttempts,
|
|
1012
|
-
explain_plan: result.explainPlan,
|
|
1013
|
-
explain_execution_stats: result.explainStats,
|
|
1014
|
-
explain_generated_at: new Date(),
|
|
1015
|
-
verification_notes: shouldFail ? 'Verification failed: invalid duration' : 'Explain returned invalid duration'
|
|
1016
|
-
}
|
|
1017
|
-
};
|
|
1018
|
-
|
|
1019
|
-
if (shouldFail) {
|
|
1020
|
-
updateOps.$unset = {
|
|
1021
|
-
verification_next_run_at: ''
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
else {
|
|
1025
|
-
updateOps.$set.verification_next_run_at = nextRunAt;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
await SlowQueryLogs.updateOne({_id: log._id}, updateOps);
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
const runs: SlowQueryVerificationRun[] = existingRuns
|
|
1033
|
-
.concat([
|
|
1034
|
-
{
|
|
1035
|
-
timestamp: new Date(),
|
|
1036
|
-
duration_ms: result.durationMs
|
|
1037
|
-
}
|
|
1038
|
-
])
|
|
1039
|
-
.slice(-VERIFICATION_ATTEMPTS);
|
|
1040
|
-
|
|
1041
|
-
const hasFastRun = runs.some(run => run.duration_ms < VERIFICATION_THRESHOLD_MS);
|
|
1042
|
-
|
|
1043
|
-
if (hasFastRun) {
|
|
1044
|
-
// const durations = runs.map(run => `${run.duration_ms}ms`).join(', ');
|
|
1045
|
-
// console.info('Removing slow query log; not consistently slow.', log.collection, durations);
|
|
1046
|
-
await SlowQueryLogs.deleteOne({_id: log._id});
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const hasEnoughRuns = runs.length >= VERIFICATION_ATTEMPTS;
|
|
1051
|
-
let status: SlowQueryVerificationStatus = hasEnoughRuns ? 'passed' : 'pending';
|
|
1052
|
-
let nextRunAt: Date | undefined;
|
|
1053
|
-
|
|
1054
|
-
if (!hasEnoughRuns) {
|
|
1055
|
-
nextRunAt = new Date(Date.now() + VERIFICATION_INTERVAL_MS);
|
|
1056
|
-
}
|
|
1057
|
-
const updateOps: Record<string, any> = {
|
|
1058
|
-
$set: {
|
|
1059
|
-
verification_runs: runs,
|
|
1060
|
-
verification_status: status,
|
|
1061
|
-
verification_invalid_attempts: 0,
|
|
1062
|
-
explain_plan: result.explainPlan,
|
|
1063
|
-
explain_execution_stats: result.explainStats,
|
|
1064
|
-
explain_generated_at: new Date()
|
|
1065
|
-
}
|
|
1066
|
-
};
|
|
1067
|
-
|
|
1068
|
-
if (nextRunAt) {
|
|
1069
|
-
updateOps.$set.verification_next_run_at = nextRunAt;
|
|
1070
|
-
}
|
|
1071
|
-
else {
|
|
1072
|
-
updateOps.$unset = updateOps.$unset || {};
|
|
1073
|
-
updateOps.$unset.verification_next_run_at = '';
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
await SlowQueryLogs.updateOne({_id: log._id}, updateOps);
|
|
1077
|
-
|
|
1078
|
-
if (status === 'passed') {
|
|
1079
|
-
await this.assignSlowQueryCounter(log._id);
|
|
1080
|
-
this.scheduleAutoOptimization(log._id);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
private async handleExplainError(log: SlowQueryLogModel, err: any) {
|
|
1085
|
-
if (!log._id) {
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const message = typeof err === 'string' ? err : err?.message || 'Unknown error';
|
|
1090
|
-
const code = err instanceof SlowQueryVerifierError ? err.code : null;
|
|
1091
|
-
const permanentFailure = !!code && ['client_db_not_found', 'client_db_missing_uri', 'client_db_missing_database', 'main_db_unavailable', 'aggregate_write_stage', 'bson_too_large', 'collection_missing'].includes(code);
|
|
1092
|
-
|
|
1093
|
-
if (this.config.debugLogging) {
|
|
1094
|
-
console.error('Slow query verification error for', log.collection, message);
|
|
1095
|
-
}
|
|
1096
|
-
else if (!permanentFailure) {
|
|
1097
|
-
console.warn('Slow query verification error for', log.collection, message);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
if (permanentFailure) {
|
|
1101
|
-
await SlowQueryLogs.updateOne({_id: log._id}, {
|
|
1102
|
-
$set: {
|
|
1103
|
-
verification_status: 'failed',
|
|
1104
|
-
verification_notes: `Verification failed: ${message}`
|
|
1105
|
-
},
|
|
1106
|
-
$unset: {
|
|
1107
|
-
verification_next_run_at: ''
|
|
1108
|
-
}
|
|
1109
|
-
});
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
const nextRunAt = new Date(Date.now() + VERIFICATION_INTERVAL_MS);
|
|
1114
|
-
await SlowQueryLogs.updateOne({_id: log._id}, {
|
|
1115
|
-
$set: {
|
|
1116
|
-
verification_next_run_at: nextRunAt,
|
|
1117
|
-
verification_notes: `Verification error: ${message}`
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
private async assignSlowQueryCounter(logId: string) {
|
|
1123
|
-
if (!logId) {
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
const latest = await SlowQueryLogs.findOne({_id: logId});
|
|
1128
|
-
|
|
1129
|
-
if (!latest) {
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
const hasNumber = typeof latest.slow_query_count === 'number' && latest.slow_query_count > 0;
|
|
1134
|
-
|
|
1135
|
-
if (hasNumber) {
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
const counterValue = await ResolveIOServer.getMainServer().getMethodManager().callMethod('incCounter', 'slow_query');
|
|
1140
|
-
const counterString = pad(counterValue, 6);
|
|
1141
|
-
|
|
1142
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
1143
|
-
$set: {
|
|
1144
|
-
slow_query_count: counterValue,
|
|
1145
|
-
slow_query_count_string: counterString
|
|
1146
|
-
}
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
private scheduleAutoOptimization(logId: string): void {
|
|
1151
|
-
if (!logId) {
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
if (this.autoOptimizeInFlight.has(logId)) {
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
this.autoOptimizeInFlight.add(logId);
|
|
1159
|
-
this.runAutoOptimizationInBackground(logId);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
private runAutoOptimizationInBackground(logId: string): void {
|
|
1163
|
-
setImmediate(async () => {
|
|
1164
|
-
try {
|
|
1165
|
-
await this.runAutoOptimization(logId);
|
|
1166
|
-
}
|
|
1167
|
-
catch (error) {
|
|
1168
|
-
console.error('Slow query auto optimization failed', {logId, error: (error as Error)?.message || error});
|
|
1169
|
-
}
|
|
1170
|
-
finally {
|
|
1171
|
-
this.autoOptimizeInFlight.delete(logId);
|
|
1172
|
-
}
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
private escapeRegex(value: string): string {
|
|
1177
|
-
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
private getEscalationRecipients(): string[] {
|
|
1181
|
-
const unique = new Set<string>();
|
|
1182
|
-
(this.config.escalationEmails || []).forEach((entry) => {
|
|
1183
|
-
const normalized = String(entry || '').trim().toLowerCase();
|
|
1184
|
-
if (normalized) {
|
|
1185
|
-
unique.add(normalized);
|
|
1186
|
-
}
|
|
1187
|
-
});
|
|
1188
|
-
if (!unique.size) {
|
|
1189
|
-
unique.add(DEFAULT_ERROR_ALERT_EMAIL);
|
|
1190
|
-
}
|
|
1191
|
-
return Array.from(unique);
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
private async sendSlowQueryEscalationNotice(log: SlowQueryLogModel, reason: string): Promise<void> {
|
|
1195
|
-
const recipients = this.getEscalationRecipients();
|
|
1196
|
-
if (!recipients.length || !log?._id) {
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const subject = `ResolveIO Slow Query Escalation: ${log.slow_query_count_string || log._id}`;
|
|
1201
|
-
const body = [
|
|
1202
|
-
'Slow-query auto optimization budget was exhausted and additional autonomous attempts were stopped.',
|
|
1203
|
-
'',
|
|
1204
|
-
`Reason: ${reason}`,
|
|
1205
|
-
`Slow Query Log ID: ${log._id}`,
|
|
1206
|
-
`Slow Query Counter: ${log.slow_query_count_string || 'n/a'}`,
|
|
1207
|
-
`Collection: ${log.collection || 'unknown'}`,
|
|
1208
|
-
`Query Hash: ${log.query_hash || 'n/a'}`,
|
|
1209
|
-
`Client: ${log.client_name || log.client_slug || 'unknown'}`,
|
|
1210
|
-
`Environment: ${log.environment || 'unknown'}`,
|
|
1211
|
-
`Attempts: ${log.auto_fix_attempt_count || 0}/${this.config.autoOptimizeMaxAttemptsPerQuery}`,
|
|
1212
|
-
'',
|
|
1213
|
-
'The query has been marked ignored to protect customer AI credits. Manual tuning or explicit allow-list override is required.'
|
|
1214
|
-
].join('\n');
|
|
1215
|
-
|
|
1216
|
-
const methodManager = ResolveIOServer.getMainServer().getMethodManager();
|
|
1217
|
-
for (const recipient of recipients) {
|
|
1218
|
-
try {
|
|
1219
|
-
await methodManager.sendEmail(recipient, subject, body);
|
|
1220
|
-
}
|
|
1221
|
-
catch (error) {
|
|
1222
|
-
console.error('Failed sending slow-query escalation email', {recipient, logId: log._id, error});
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
private async resolveSlowQueryClientTarget(log: SlowQueryLogModel): Promise<{ idClient: string; clientName: string }> {
|
|
1228
|
-
const directId = String((log as any)?.id_client || '').trim();
|
|
1229
|
-
if (directId) {
|
|
1230
|
-
return {
|
|
1231
|
-
idClient: directId,
|
|
1232
|
-
clientName: String(log?.client_name || '').trim()
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
const candidates = [
|
|
1237
|
-
String(log?.client_slug || '').trim(),
|
|
1238
|
-
String(log?.client_name || '').trim(),
|
|
1239
|
-
String(log?.source_app || '').trim()
|
|
1240
|
-
].filter(Boolean);
|
|
1241
|
-
for (const candidate of candidates) {
|
|
1242
|
-
const escaped = this.escapeRegex(candidate);
|
|
1243
|
-
const match = await Clients.findOne({
|
|
1244
|
-
$or: [
|
|
1245
|
-
{name: {$regex: `^${escaped}$`, $options: 'i'}},
|
|
1246
|
-
{demo_name: {$regex: `^${escaped}$`, $options: 'i'}},
|
|
1247
|
-
{project: {$regex: `^${escaped}$`, $options: 'i'}},
|
|
1248
|
-
{repo: {$regex: `^${escaped}$`, $options: 'i'}}
|
|
1249
|
-
]
|
|
1250
|
-
}, {
|
|
1251
|
-
projection: {
|
|
1252
|
-
_id: 1,
|
|
1253
|
-
name: 1
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
if (match?._id) {
|
|
1257
|
-
return {
|
|
1258
|
-
idClient: String(match._id || '').trim(),
|
|
1259
|
-
clientName: String(match.name || log?.client_name || candidate || '').trim()
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
return {
|
|
1265
|
-
idClient: '',
|
|
1266
|
-
clientName: String(log?.client_name || '').trim()
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
private buildLocalInternalUserCriteria(): Record<string, any> {
|
|
1271
|
-
return {
|
|
1272
|
-
$and: [
|
|
1273
|
-
{
|
|
1274
|
-
is_customer: {
|
|
1275
|
-
$ne: true
|
|
1276
|
-
}
|
|
1277
|
-
},
|
|
1278
|
-
{
|
|
1279
|
-
$nor: [
|
|
1280
|
-
{
|
|
1281
|
-
'other.id_customer': {
|
|
1282
|
-
$exists: true,
|
|
1283
|
-
$nin: ['', null]
|
|
1284
|
-
}
|
|
1285
|
-
},
|
|
1286
|
-
{
|
|
1287
|
-
id_customer: {
|
|
1288
|
-
$exists: true,
|
|
1289
|
-
$nin: ['', null]
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
]
|
|
1293
|
-
}
|
|
1294
|
-
]
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
private buildLocalAdminUserCriteria(): Record<string, any> {
|
|
1299
|
-
return {
|
|
1300
|
-
$or: [
|
|
1301
|
-
{
|
|
1302
|
-
'roles.super_admin': true
|
|
1303
|
-
},
|
|
1304
|
-
{
|
|
1305
|
-
'roles.groups.name': {
|
|
1306
|
-
$regex: 'admin',
|
|
1307
|
-
$options: 'i'
|
|
1308
|
-
}
|
|
1309
|
-
},
|
|
1310
|
-
{
|
|
1311
|
-
'roles.groups.views': {
|
|
1312
|
-
$regex: '^/manage'
|
|
1313
|
-
}
|
|
1314
|
-
},
|
|
1315
|
-
{
|
|
1316
|
-
'roles.groups.views': '/manage'
|
|
1317
|
-
}
|
|
1318
|
-
]
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
private async resolveLocalNotificationUserIds(isGeneratedApp: boolean): Promise<string[]> {
|
|
1323
|
-
const andClauses: Record<string, any>[] = [
|
|
1324
|
-
{
|
|
1325
|
-
active: true
|
|
1326
|
-
},
|
|
1327
|
-
{
|
|
1328
|
-
username: {
|
|
1329
|
-
$exists: true
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
];
|
|
1333
|
-
|
|
1334
|
-
if (isGeneratedApp) {
|
|
1335
|
-
andClauses.push(this.buildLocalInternalUserCriteria());
|
|
1336
|
-
}
|
|
1337
|
-
else {
|
|
1338
|
-
andClauses.push(this.buildLocalAdminUserCriteria());
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
const users = await Users.find({
|
|
1342
|
-
$and: andClauses
|
|
1343
|
-
}, {
|
|
1344
|
-
projection: {
|
|
1345
|
-
_id: 1
|
|
1346
|
-
},
|
|
1347
|
-
limit: MAX_LOCAL_NOTIFICATION_USERS
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
if (!Array.isArray(users) || !users.length) {
|
|
1351
|
-
return [];
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
const unique = new Set<string>();
|
|
1355
|
-
for (const user of users) {
|
|
1356
|
-
const idUser = String(user?._id || '').trim();
|
|
1357
|
-
if (idUser) {
|
|
1358
|
-
unique.add(idUser);
|
|
1359
|
-
}
|
|
1360
|
-
if (unique.size >= MAX_LOCAL_NOTIFICATION_USERS) {
|
|
1361
|
-
break;
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
return Array.from(unique);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
private async isGeneratedAICoderClient(idClient: string): Promise<boolean> {
|
|
1368
|
-
const normalizedClientId = String(idClient || '').trim();
|
|
1369
|
-
if (!normalizedClientId) {
|
|
1370
|
-
return false;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
const match = await AICoderApps.findOne({
|
|
1374
|
-
client_id: normalizedClientId
|
|
1375
|
-
}, {
|
|
1376
|
-
projection: {
|
|
1377
|
-
_id: 1
|
|
1378
|
-
}
|
|
1379
|
-
});
|
|
1380
|
-
|
|
1381
|
-
return !!match?._id;
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
private buildAICoderSlowQuerySummary(
|
|
1385
|
-
stage: 'detected_auto_optimize_enabled' | 'completed_success' | 'completed_failed',
|
|
1386
|
-
log: SlowQueryLogModel,
|
|
1387
|
-
extra?: { reason?: string; notes?: string }
|
|
1388
|
-
): string {
|
|
1389
|
-
return buildGeneratedSlowQuerySummary(stage, {
|
|
1390
|
-
collection: String(log?.collection || '').trim(),
|
|
1391
|
-
source_app: String(log?.source_app || '').trim(),
|
|
1392
|
-
environment: String(log?.environment || '').trim(),
|
|
1393
|
-
baseline_duration_ms: Number((log as any)?.auto_fix_result?.baseline?.durationMs),
|
|
1394
|
-
after_duration_ms: Number((log as any)?.auto_fix_result?.after?.durationMs),
|
|
1395
|
-
reason: extra?.reason,
|
|
1396
|
-
notes: extra?.notes
|
|
1397
|
-
}, extra);
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
private buildResolveIOProjectSlowQueryDetails(log: SlowQueryLogModel, extra?: { reason?: string; notes?: string }): string {
|
|
1401
|
-
const runDurations = Array.isArray(log?.verification_runs)
|
|
1402
|
-
? log.verification_runs
|
|
1403
|
-
.map(run => Number(run?.duration_ms))
|
|
1404
|
-
.filter(duration => Number.isFinite(duration) && duration >= 0)
|
|
1405
|
-
: [];
|
|
1406
|
-
|
|
1407
|
-
return buildResolveIOProjectSlowQueryDetails({
|
|
1408
|
-
collection: log.collection || '',
|
|
1409
|
-
client_name: log.client_name || log.client_slug || '',
|
|
1410
|
-
source_app: log.source_app || '',
|
|
1411
|
-
environment: log.environment || '',
|
|
1412
|
-
query_hash: log.query_hash || '',
|
|
1413
|
-
baseline_duration_ms: Number((log as any)?.auto_fix_result?.baseline?.durationMs),
|
|
1414
|
-
after_duration_ms: Number((log as any)?.auto_fix_result?.after?.durationMs),
|
|
1415
|
-
verification_run_durations_ms: runDurations,
|
|
1416
|
-
log_id: log._id || '',
|
|
1417
|
-
reason: extra?.reason,
|
|
1418
|
-
notes: extra?.notes
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
private async notifyCustomerSlowQueryStatus(
|
|
1423
|
-
stage: 'detected_auto_optimize_enabled' | 'completed_success' | 'completed_failed',
|
|
1424
|
-
log: SlowQueryLogModel,
|
|
1425
|
-
extra?: { reason?: string; notes?: string }
|
|
1426
|
-
): Promise<void> {
|
|
1427
|
-
if (!log?._id) {
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
const target = await this.resolveSlowQueryClientTarget(log);
|
|
1432
|
-
let isGeneratedApp = false;
|
|
1433
|
-
if (target.idClient) {
|
|
1434
|
-
isGeneratedApp = await this.isGeneratedAICoderClient(target.idClient);
|
|
1435
|
-
}
|
|
1436
|
-
else {
|
|
1437
|
-
const generatedApp = await this.resolveAutoOptimizeApp(log);
|
|
1438
|
-
isGeneratedApp = !!generatedApp;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
if (!isGeneratedApp && stage !== 'completed_success') {
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
const issueKey = String(log.query_hash || log._id || '').trim();
|
|
1446
|
-
const dedupeKey = `slow-query:${issueKey}:${stage}`;
|
|
1447
|
-
const metadata = {
|
|
1448
|
-
log_id: log._id,
|
|
1449
|
-
query_hash: log.query_hash || '',
|
|
1450
|
-
collection: log.collection || '',
|
|
1451
|
-
environment: log.environment || '',
|
|
1452
|
-
source_app: log.source_app || '',
|
|
1453
|
-
reason: extra?.reason || '',
|
|
1454
|
-
notes: extra?.notes || ''
|
|
1455
|
-
};
|
|
1456
|
-
const targetPayload: Record<string, any> = {};
|
|
1457
|
-
if (target.idClient) {
|
|
1458
|
-
targetPayload.target_type = isGeneratedApp ? 'client_internal' : 'client_admins';
|
|
1459
|
-
targetPayload.id_client = target.idClient;
|
|
1460
|
-
targetPayload.client_name = target.clientName || undefined;
|
|
1461
|
-
}
|
|
1462
|
-
else {
|
|
1463
|
-
const idUsers = await this.resolveLocalNotificationUserIds(isGeneratedApp);
|
|
1464
|
-
if (!idUsers.length) {
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
targetPayload.target_type = 'users';
|
|
1468
|
-
targetPayload.id_users = idUsers;
|
|
1469
|
-
targetPayload.client_name = target.clientName || undefined;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
let payload: Record<string, any> = {};
|
|
1473
|
-
if (isGeneratedApp) {
|
|
1474
|
-
if (stage === 'detected_auto_optimize_enabled') {
|
|
1475
|
-
payload = {
|
|
1476
|
-
title: 'Slow query detected. Auto-optimization is running.',
|
|
1477
|
-
message: 'We detected a slow query and started an automatic optimization workflow. We will notify you when it is complete.',
|
|
1478
|
-
severity: 'info',
|
|
1479
|
-
details: this.buildAICoderSlowQuerySummary(stage, log, extra)
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
else if (stage === 'completed_success') {
|
|
1483
|
-
payload = {
|
|
1484
|
-
title: 'Slow query optimization completed.',
|
|
1485
|
-
message: `We optimized slow performance for ${log.collection || 'a collection'} and published the improvement.`,
|
|
1486
|
-
severity: 'success',
|
|
1487
|
-
details: this.buildAICoderSlowQuerySummary(stage, log, extra)
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
else {
|
|
1491
|
-
payload = {
|
|
1492
|
-
title: 'Slow query needs manual review.',
|
|
1493
|
-
message: 'We could not complete automatic optimization for a slow query. Manual tuning is required.',
|
|
1494
|
-
severity: 'warning',
|
|
1495
|
-
details: this.buildAICoderSlowQuerySummary(stage, log, extra)
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
else {
|
|
1500
|
-
payload = {
|
|
1501
|
-
title: 'ResolveIO project slow query optimized',
|
|
1502
|
-
message: `Slow-query optimization completed for ${log.collection || 'a collection'}.`,
|
|
1503
|
-
severity: 'success',
|
|
1504
|
-
details: this.buildResolveIOProjectSlowQueryDetails(log, extra)
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
try {
|
|
1509
|
-
await ResolveIOServer.getMainServer().getMethodManager().callMethod('createCustomerNotification', {
|
|
1510
|
-
...targetPayload,
|
|
1511
|
-
category: 'slow-query',
|
|
1512
|
-
source: 'slow-query-auto-optimize',
|
|
1513
|
-
source_id: String(log._id || '').trim() || undefined,
|
|
1514
|
-
dedupe_key: dedupeKey,
|
|
1515
|
-
metadata,
|
|
1516
|
-
...payload
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
catch (error) {
|
|
1520
|
-
if (this.config.debugLogging) {
|
|
1521
|
-
console.warn('Slow query customer notification failed', {
|
|
1522
|
-
logId: log._id,
|
|
1523
|
-
stage,
|
|
1524
|
-
error: error?.message || error
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
private parseDate(value: any): Date | null {
|
|
1531
|
-
if (!value) {
|
|
1532
|
-
return null;
|
|
1533
|
-
}
|
|
1534
|
-
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
1535
|
-
return value;
|
|
1536
|
-
}
|
|
1537
|
-
const parsed = new Date(value);
|
|
1538
|
-
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
private resolveFingerprintScopeKey(log: SlowQueryLogModel): string {
|
|
1542
|
-
const explicit = String((log as any)?.fingerprint_scope_key || '').trim();
|
|
1543
|
-
if (explicit) {
|
|
1544
|
-
return explicit;
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
const normalize = (value: any): string => {
|
|
1548
|
-
const normalized = String(value || '')
|
|
1549
|
-
.trim()
|
|
1550
|
-
.toLowerCase()
|
|
1551
|
-
.replace(/\s+/g, ' ');
|
|
1552
|
-
return normalized || '_';
|
|
1553
|
-
};
|
|
1554
|
-
|
|
1555
|
-
const clientKey = normalize(log?.client_slug || log?.client_name || '');
|
|
1556
|
-
const sourceApp = normalize(log?.source_app || '');
|
|
1557
|
-
const environment = normalize(log?.environment || '');
|
|
1558
|
-
const publication = normalize(log?.publication || '');
|
|
1559
|
-
const serverUrl = normalize((log as any)?.server_url || '');
|
|
1560
|
-
return [clientKey, sourceApp, environment, publication, serverUrl].join('|');
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
private buildFingerprintMatchQuery(log: SlowQueryLogModel): Record<string, any> {
|
|
1564
|
-
const conditions: Record<string, any>[] = [];
|
|
1565
|
-
const collection = String(log?.collection || '').trim();
|
|
1566
|
-
if (collection) {
|
|
1567
|
-
conditions.push({collection});
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
const explicitScopeKey = String((log as any)?.fingerprint_scope_key || '').trim();
|
|
1571
|
-
const scopeKey = this.resolveFingerprintScopeKey(log);
|
|
1572
|
-
if (scopeKey) {
|
|
1573
|
-
const legacyScopeQuery: Record<string, any> = {};
|
|
1574
|
-
if (String(log?.client_slug || '').trim()) {
|
|
1575
|
-
legacyScopeQuery.client_slug = log.client_slug;
|
|
1576
|
-
}
|
|
1577
|
-
if (String(log?.environment || '').trim()) {
|
|
1578
|
-
legacyScopeQuery.environment = log.environment;
|
|
1579
|
-
}
|
|
1580
|
-
if (String(log?.source_app || '').trim()) {
|
|
1581
|
-
legacyScopeQuery.source_app = log.source_app;
|
|
1582
|
-
}
|
|
1583
|
-
if (String(log?.publication || '').trim()) {
|
|
1584
|
-
legacyScopeQuery.publication = log.publication;
|
|
1585
|
-
}
|
|
1586
|
-
if (String((log as any)?.server_url || '').trim()) {
|
|
1587
|
-
legacyScopeQuery.server_url = (log as any).server_url;
|
|
1588
|
-
}
|
|
1589
|
-
if (explicitScopeKey) {
|
|
1590
|
-
conditions.push({fingerprint_scope_key: scopeKey});
|
|
1591
|
-
}
|
|
1592
|
-
else {
|
|
1593
|
-
const scopeCandidates: Record<string, any>[] = [{_id: log?._id || ''}, {fingerprint_scope_key: scopeKey}];
|
|
1594
|
-
if (Object.keys(legacyScopeQuery).length) {
|
|
1595
|
-
scopeCandidates.push(legacyScopeQuery);
|
|
1596
|
-
}
|
|
1597
|
-
conditions.push({$or: scopeCandidates});
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
const canonicalHash = String((log as any)?.canonical_fingerprint_hash || '').trim();
|
|
1602
|
-
const queryHash = String(log?.query_hash || '').trim();
|
|
1603
|
-
if (canonicalHash && queryHash && canonicalHash !== queryHash) {
|
|
1604
|
-
conditions.push({
|
|
1605
|
-
$or: [
|
|
1606
|
-
{canonical_fingerprint_hash: canonicalHash},
|
|
1607
|
-
{
|
|
1608
|
-
canonical_fingerprint_hash: {$exists: false},
|
|
1609
|
-
query_hash: queryHash
|
|
1610
|
-
}
|
|
1611
|
-
]
|
|
1612
|
-
});
|
|
1613
|
-
}
|
|
1614
|
-
else if (canonicalHash) {
|
|
1615
|
-
conditions.push({canonical_fingerprint_hash: canonicalHash});
|
|
1616
|
-
}
|
|
1617
|
-
else if (queryHash) {
|
|
1618
|
-
conditions.push({query_hash: queryHash});
|
|
1619
|
-
}
|
|
1620
|
-
else if (log?._id) {
|
|
1621
|
-
conditions.push({_id: log._id});
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
if (!conditions.length) {
|
|
1625
|
-
return {_id: log?._id || ''};
|
|
1626
|
-
}
|
|
1627
|
-
if (conditions.length === 1) {
|
|
1628
|
-
return conditions[0];
|
|
1629
|
-
}
|
|
1630
|
-
return {$and: conditions};
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
private countAttemptsWithinWindow(log: SlowQueryLogModel, windowStart: Date): number {
|
|
1634
|
-
const history = Array.isArray((log as any)?.auto_fix_attempt_history)
|
|
1635
|
-
? ((log as any).auto_fix_attempt_history as any[])
|
|
1636
|
-
: [];
|
|
1637
|
-
let attemptsInWindow = 0;
|
|
1638
|
-
history.forEach((entry) => {
|
|
1639
|
-
const attemptAt = this.parseDate(entry);
|
|
1640
|
-
if (attemptAt && attemptAt.getTime() >= windowStart.getTime()) {
|
|
1641
|
-
attemptsInWindow += 1;
|
|
1642
|
-
}
|
|
1643
|
-
});
|
|
1644
|
-
if (attemptsInWindow > 0) {
|
|
1645
|
-
return attemptsInWindow;
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
const lastAttemptAt = this.parseDate((log as any)?.auto_fix_last_attempt_at);
|
|
1649
|
-
const attemptsUsed = Number.isFinite(Number((log as any)?.auto_fix_attempt_count))
|
|
1650
|
-
? Number((log as any).auto_fix_attempt_count)
|
|
1651
|
-
: 0;
|
|
1652
|
-
if (lastAttemptAt && lastAttemptAt.getTime() >= windowStart.getTime() && attemptsUsed > 0) {
|
|
1653
|
-
return 1;
|
|
1654
|
-
}
|
|
1655
|
-
return 0;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
private async resolveFingerprintAttemptsInWindow(log: SlowQueryLogModel, windowStart: Date): Promise<number> {
|
|
1659
|
-
const matchQuery = this.buildFingerprintMatchQuery(log);
|
|
1660
|
-
const matches = await SlowQueryLogs.find(matchQuery, {
|
|
1661
|
-
limit: 200,
|
|
1662
|
-
projection: {
|
|
1663
|
-
_id: 1,
|
|
1664
|
-
auto_fix_attempt_history: 1,
|
|
1665
|
-
auto_fix_attempt_count: 1,
|
|
1666
|
-
auto_fix_last_attempt_at: 1
|
|
1667
|
-
}
|
|
1668
|
-
}) as SlowQueryLogModel[];
|
|
1669
|
-
|
|
1670
|
-
let attempts = 0;
|
|
1671
|
-
matches.forEach((entry) => {
|
|
1672
|
-
attempts += this.countAttemptsWithinWindow(entry, windowStart);
|
|
1673
|
-
});
|
|
1674
|
-
return attempts;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
private resolveCooldownDeadline(log: SlowQueryLogModel): Date | null {
|
|
1678
|
-
const cooldownMinutes = Number.isFinite(Number(this.config.autoOptimizeCooldownMinutes))
|
|
1679
|
-
? Number(this.config.autoOptimizeCooldownMinutes)
|
|
1680
|
-
: 0;
|
|
1681
|
-
if (cooldownMinutes <= 0) {
|
|
1682
|
-
return null;
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
const lastAttemptAt = this.parseDate(log?.auto_fix_last_attempt_at);
|
|
1686
|
-
if (!lastAttemptAt) {
|
|
1687
|
-
return null;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
return new Date(lastAttemptAt.getTime() + (cooldownMinutes * 60 * 1000));
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
private async markAutoOptimizeCooldownActive(log: SlowQueryLogModel, cooldownUntil: Date): Promise<void> {
|
|
1694
|
-
if (!log?._id) {
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1697
|
-
const reason = `Auto optimize cooldown is active until ${cooldownUntil.toISOString()}.`;
|
|
1698
|
-
await SlowQueryLogs.updateOne({_id: log._id}, {
|
|
1699
|
-
$set: {
|
|
1700
|
-
status: 'investigating',
|
|
1701
|
-
auto_fix_status: 'failed',
|
|
1702
|
-
auto_fix_disabled_reason: reason,
|
|
1703
|
-
verification_notes: reason,
|
|
1704
|
-
verification_status: 'pending',
|
|
1705
|
-
verification_next_run_at: cooldownUntil,
|
|
1706
|
-
last_triaged_by: 'auto-slow-query',
|
|
1707
|
-
last_triaged_at: new Date()
|
|
1708
|
-
}
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
private async markAutoOptimizeTokenIneligible(log: SlowQueryLogModel, reason: string): Promise<void> {
|
|
1713
|
-
if (!log?._id) {
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
const message = `Auto optimize blocked by AI credit preflight: ${reason}`;
|
|
1718
|
-
await SlowQueryLogs.updateOne({_id: log._id}, {
|
|
1719
|
-
$set: {
|
|
1720
|
-
status: 'ignored',
|
|
1721
|
-
ignored: true,
|
|
1722
|
-
auto_fix_status: 'failed',
|
|
1723
|
-
auto_fix_disabled_reason: message,
|
|
1724
|
-
verification_notes: message,
|
|
1725
|
-
last_triaged_by: 'auto-slow-query',
|
|
1726
|
-
last_triaged_at: new Date()
|
|
1727
|
-
},
|
|
1728
|
-
$unset: {
|
|
1729
|
-
verification_next_run_at: ''
|
|
1730
|
-
}
|
|
1731
|
-
});
|
|
1732
|
-
|
|
1733
|
-
const refreshed = (await SlowQueryLogs.findOne({_id: log._id})) || log;
|
|
1734
|
-
await this.sendSlowQueryEscalationNotice(refreshed, message);
|
|
1735
|
-
await this.notifyCustomerSlowQueryStatus('completed_failed', refreshed, {reason: message});
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
private async markAutoOptimizeBudgetExceeded(log: SlowQueryLogModel, reason: string): Promise<void> {
|
|
1739
|
-
if (!log?._id) {
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
const attemptsUsed = Number.isFinite(Number(log.auto_fix_attempt_count))
|
|
1743
|
-
? Number(log.auto_fix_attempt_count)
|
|
1744
|
-
: 0;
|
|
1745
|
-
const maxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerQuery))
|
|
1746
|
-
? Number(this.config.autoOptimizeMaxAttemptsPerQuery)
|
|
1747
|
-
: 0;
|
|
1748
|
-
const attemptSummary = maxAttempts > 0
|
|
1749
|
-
? `Attempts on this log: ${attemptsUsed}/${maxAttempts}.`
|
|
1750
|
-
: `Attempts on this log: ${attemptsUsed}.`;
|
|
1751
|
-
const message = `${reason} Query marked ignored pending manual intervention. ${attemptSummary}`;
|
|
1752
|
-
const matchQuery = this.buildFingerprintMatchQuery(log);
|
|
1753
|
-
|
|
1754
|
-
await SlowQueryLogs.updateMany(matchQuery, {
|
|
1755
|
-
$set: {
|
|
1756
|
-
status: 'ignored',
|
|
1757
|
-
ignored: true,
|
|
1758
|
-
auto_fix_status: 'failed',
|
|
1759
|
-
auto_fix_disabled_reason: message,
|
|
1760
|
-
verification_notes: message,
|
|
1761
|
-
verification_status: 'failed',
|
|
1762
|
-
last_triaged_by: 'auto-slow-query',
|
|
1763
|
-
last_triaged_at: new Date()
|
|
1764
|
-
},
|
|
1765
|
-
$unset: {
|
|
1766
|
-
verification_next_run_at: ''
|
|
1767
|
-
}
|
|
1768
|
-
});
|
|
1769
|
-
|
|
1770
|
-
const refreshed = (await SlowQueryLogs.findOne({_id: log._id})) || log;
|
|
1771
|
-
await this.sendSlowQueryEscalationNotice(refreshed, reason);
|
|
1772
|
-
await this.notifyCustomerSlowQueryStatus('completed_failed', refreshed, {reason});
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
private async maybeStopAutoOptimizeAfterFailure(logId: string, reason: string): Promise<void> {
|
|
1776
|
-
if (!logId) {
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
const maxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerQuery))
|
|
1780
|
-
? Number(this.config.autoOptimizeMaxAttemptsPerQuery)
|
|
1781
|
-
: 0;
|
|
1782
|
-
if (maxAttempts <= 0) {
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
const latest = await SlowQueryLogs.findOne({_id: logId});
|
|
1786
|
-
if (!latest || latest.ignored) {
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
const attemptsUsed = Number.isFinite(Number(latest.auto_fix_attempt_count))
|
|
1790
|
-
? Number(latest.auto_fix_attempt_count)
|
|
1791
|
-
: 0;
|
|
1792
|
-
if (attemptsUsed < maxAttempts) {
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
1795
|
-
await this.markAutoOptimizeBudgetExceeded(latest, reason);
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
private shouldFallbackDashboardMethod(error: any): boolean {
|
|
1799
|
-
const message = `${error?.message || ''}`.toLowerCase();
|
|
1800
|
-
return message.includes('not found')
|
|
1801
|
-
|| message.includes('worker dispatch unavailable')
|
|
1802
|
-
|| message.includes('no worker')
|
|
1803
|
-
|| message.includes('unavailable for aidashboard');
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
private async createDashboardJob(payload: Record<string, any>): Promise<AIDashboardJob> {
|
|
1807
|
-
const methodManager = ResolveIOServer.getMainServer().getMethodManager();
|
|
1808
|
-
try {
|
|
1809
|
-
return await methodManager.callMethod('aiDashboardCreateJob', payload);
|
|
1810
|
-
}
|
|
1811
|
-
catch (error) {
|
|
1812
|
-
if (!this.shouldFallbackDashboardMethod(error)) {
|
|
1813
|
-
throw error;
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
const manager: any = ResolveIOServer['AIDashboardManager'];
|
|
1818
|
-
if (manager && manager.isEnabled && manager.isEnabled()) {
|
|
1819
|
-
return await manager.createJob(payload);
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
throw new Error('AI Dashboard manager is not available.');
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
private async waitForDashboardJobStop(jobId: string, timeoutMs: number): Promise<void> {
|
|
1826
|
-
const methodManager = ResolveIOServer.getMainServer().getMethodManager();
|
|
1827
|
-
try {
|
|
1828
|
-
await methodManager.callMethod('aiDashboardWaitForJobStop', jobId, timeoutMs);
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
catch (error) {
|
|
1832
|
-
if (!this.shouldFallbackDashboardMethod(error)) {
|
|
1833
|
-
throw error;
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
const manager: any = ResolveIOServer['AIDashboardManager'];
|
|
1838
|
-
if (manager && manager.isEnabled && manager.isEnabled()) {
|
|
1839
|
-
await manager.waitForJobStop(jobId, timeoutMs);
|
|
1840
|
-
return;
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
throw new Error('AI Dashboard manager is not available.');
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
private async isDashboardJobRunning(jobId: string): Promise<boolean> {
|
|
1847
|
-
const methodManager = ResolveIOServer.getMainServer().getMethodManager();
|
|
1848
|
-
try {
|
|
1849
|
-
const running = await methodManager.callMethod('aiDashboardIsJobRunning', jobId);
|
|
1850
|
-
return !!running;
|
|
1851
|
-
}
|
|
1852
|
-
catch (error) {
|
|
1853
|
-
if (!this.shouldFallbackDashboardMethod(error)) {
|
|
1854
|
-
throw error;
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
const manager: any = ResolveIOServer['AIDashboardManager'];
|
|
1859
|
-
if (manager && manager.isEnabled && manager.isEnabled()) {
|
|
1860
|
-
return !!manager.isJobRunning(jobId);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
throw new Error('AI Dashboard manager is not available.');
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
private async publishDashboardJob(jobId: string): Promise<AIDashboardJob> {
|
|
1867
|
-
const methodManager = ResolveIOServer.getMainServer().getMethodManager();
|
|
1868
|
-
try {
|
|
1869
|
-
return await methodManager.callMethod('aiDashboardPublishJob', jobId);
|
|
1870
|
-
}
|
|
1871
|
-
catch (error) {
|
|
1872
|
-
if (!this.shouldFallbackDashboardMethod(error)) {
|
|
1873
|
-
throw error;
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
const manager: any = ResolveIOServer['AIDashboardManager'];
|
|
1878
|
-
if (manager && manager.isEnabled && manager.isEnabled() && typeof manager.publishJob === 'function') {
|
|
1879
|
-
return await manager.publishJob(jobId);
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
throw new Error('AI Dashboard manager is not available.');
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
private evaluateDashboardPublishOutcome(job: AIDashboardJob): {success: boolean; message: string; branchName?: string} {
|
|
1886
|
-
const logEntries = Array.isArray(job?.log) ? job.log : [];
|
|
1887
|
-
const lastMatch = (predicate): string => {
|
|
1888
|
-
for (let i = logEntries.length - 1; i >= 0; i -= 1) {
|
|
1889
|
-
if (predicate(logEntries[i] || '')) {
|
|
1890
|
-
return logEntries[i];
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
return '';
|
|
1894
|
-
};
|
|
1895
|
-
|
|
1896
|
-
const failureEntry = lastMatch(entry => /publish failed|artifact publish failed|deploy failed/i.test(entry || ''));
|
|
1897
|
-
if (failureEntry) {
|
|
1898
|
-
return {success: false, message: failureEntry};
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
const publishEntry = lastMatch(entry => /Published build to /i.test(entry || ''));
|
|
1902
|
-
if (publishEntry) {
|
|
1903
|
-
const branchMatch = publishEntry.match(/\(([^()]+)\)\.?$/);
|
|
1904
|
-
return {
|
|
1905
|
-
success: true,
|
|
1906
|
-
message: publishEntry,
|
|
1907
|
-
branchName: (branchMatch && branchMatch[1]) ? branchMatch[1].trim() : undefined
|
|
1908
|
-
};
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
const skippedEntry = lastMatch(entry => /Publish skipped/i.test(entry || ''));
|
|
1912
|
-
if (skippedEntry) {
|
|
1913
|
-
return {success: false, message: skippedEntry};
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
return {
|
|
1917
|
-
success: false,
|
|
1918
|
-
message: 'Dashboard job completed without a publish confirmation log entry.'
|
|
1919
|
-
};
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
private resolveBaselineDurationMs(log: SlowQueryLogModel): number | undefined {
|
|
1923
|
-
const runs = Array.isArray(log.verification_runs) ? log.verification_runs : [];
|
|
1924
|
-
for (let i = runs.length - 1; i >= 0; i -= 1) {
|
|
1925
|
-
const duration = runs[i]?.duration_ms;
|
|
1926
|
-
if (SlowQueryVerifier.isValidDuration(duration)) {
|
|
1927
|
-
return duration;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
if (SlowQueryVerifier.isValidDuration(log.duration_ms)) {
|
|
1931
|
-
return log.duration_ms;
|
|
1932
|
-
}
|
|
1933
|
-
if (SlowQueryVerifier.isValidDuration(log.avg_duration_ms)) {
|
|
1934
|
-
return log.avg_duration_ms;
|
|
1935
|
-
}
|
|
1936
|
-
return undefined;
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
private static collectMetricValues(node: any, keySet: Set<string>, output: number[]): void {
|
|
1940
|
-
if (!node || typeof node !== 'object') {
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
if (Array.isArray(node)) {
|
|
1945
|
-
node.forEach((entry) => SlowQueryVerifier.collectMetricValues(entry, keySet, output));
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
Object.keys(node).forEach((key) => {
|
|
1950
|
-
const value = (node as any)[key];
|
|
1951
|
-
if (keySet.has(key) && typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
1952
|
-
output.push(value);
|
|
1953
|
-
}
|
|
1954
|
-
SlowQueryVerifier.collectMetricValues(value, keySet, output);
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
private static asNonNegativeNumber(value: any): number | undefined {
|
|
1959
|
-
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
1960
|
-
? value
|
|
1961
|
-
: undefined;
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
private static maxNumber(values: Array<number | undefined>): number | undefined {
|
|
1965
|
-
const candidates = values.filter((value): value is number => typeof value === 'number');
|
|
1966
|
-
return candidates.length ? Math.max(...candidates) : undefined;
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
private static sortStageSummaries(input: ExplainStageSummary[]): ExplainStageSummary[] {
|
|
1970
|
-
return input.slice().sort((left, right) => {
|
|
1971
|
-
const timeDiff = (right.executionTimeMs ?? -1) - (left.executionTimeMs ?? -1);
|
|
1972
|
-
if (timeDiff !== 0) {
|
|
1973
|
-
return timeDiff;
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
const docsDiff = (right.docsExamined ?? -1) - (left.docsExamined ?? -1);
|
|
1977
|
-
if (docsDiff !== 0) {
|
|
1978
|
-
return docsDiff;
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
const keysDiff = (right.keysExamined ?? -1) - (left.keysExamined ?? -1);
|
|
1982
|
-
if (keysDiff !== 0) {
|
|
1983
|
-
return keysDiff;
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
return `${left.path}|${left.stage}`.localeCompare(`${right.path}|${right.stage}`);
|
|
1987
|
-
});
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
private static collectExecutionTreeStages(node: any, path: string, output: ExplainStageSummary[]): void {
|
|
1991
|
-
if (!node || typeof node !== 'object') {
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
const stageName = typeof node.stage === 'string' ? node.stage : 'execution-stage';
|
|
1996
|
-
const executionTimeMs = SlowQueryVerifier.maxNumber([
|
|
1997
|
-
SlowQueryVerifier.asNonNegativeNumber(node.executionTimeMillis),
|
|
1998
|
-
SlowQueryVerifier.asNonNegativeNumber(node.executionTimeMillisEstimate)
|
|
1999
|
-
]);
|
|
2000
|
-
const docsExamined = SlowQueryVerifier.maxNumber([
|
|
2001
|
-
SlowQueryVerifier.asNonNegativeNumber(node.totalDocsExamined),
|
|
2002
|
-
SlowQueryVerifier.asNonNegativeNumber(node.docsExamined)
|
|
2003
|
-
]);
|
|
2004
|
-
const keysExamined = SlowQueryVerifier.maxNumber([
|
|
2005
|
-
SlowQueryVerifier.asNonNegativeNumber(node.totalKeysExamined),
|
|
2006
|
-
SlowQueryVerifier.asNonNegativeNumber(node.keysExamined)
|
|
2007
|
-
]);
|
|
2008
|
-
const nReturned = SlowQueryVerifier.maxNumber([
|
|
2009
|
-
SlowQueryVerifier.asNonNegativeNumber(node.nReturned)
|
|
2010
|
-
]);
|
|
2011
|
-
|
|
2012
|
-
if (
|
|
2013
|
-
typeof executionTimeMs === 'number'
|
|
2014
|
-
|| typeof docsExamined === 'number'
|
|
2015
|
-
|| typeof keysExamined === 'number'
|
|
2016
|
-
|| typeof nReturned === 'number'
|
|
2017
|
-
) {
|
|
2018
|
-
output.push({
|
|
2019
|
-
stage: stageName,
|
|
2020
|
-
path: path || 'executionStats.executionStages',
|
|
2021
|
-
executionTimeMs,
|
|
2022
|
-
docsExamined,
|
|
2023
|
-
keysExamined,
|
|
2024
|
-
nReturned
|
|
2025
|
-
});
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
const recurse = (child: any, childPath: string) => {
|
|
2029
|
-
if (!child) {
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
if (Array.isArray(child)) {
|
|
2033
|
-
child.forEach((entry, index) => {
|
|
2034
|
-
SlowQueryVerifier.collectExecutionTreeStages(entry, `${childPath}[${index}]`, output);
|
|
2035
|
-
});
|
|
2036
|
-
return;
|
|
2037
|
-
}
|
|
2038
|
-
SlowQueryVerifier.collectExecutionTreeStages(child, childPath, output);
|
|
2039
|
-
};
|
|
2040
|
-
|
|
2041
|
-
recurse(node.inputStage, `${path}.inputStage`);
|
|
2042
|
-
recurse(node.inputStages, `${path}.inputStages`);
|
|
2043
|
-
recurse(node.executionStages, `${path}.executionStages`);
|
|
2044
|
-
recurse(node.outerStage, `${path}.outerStage`);
|
|
2045
|
-
recurse(node.innerStage, `${path}.innerStage`);
|
|
2046
|
-
recurse(node.leftChild, `${path}.leftChild`);
|
|
2047
|
-
recurse(node.rightChild, `${path}.rightChild`);
|
|
2048
|
-
recurse(node.thenStage, `${path}.thenStage`);
|
|
2049
|
-
recurse(node.elseStage, `${path}.elseStage`);
|
|
2050
|
-
recurse(node.shards, `${path}.shards`);
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
private static extractStageSummaries(explainResponse?: Record<string, any>, explainStats?: Record<string, any>): ExplainStageSummary[] {
|
|
2054
|
-
const summaries: ExplainStageSummary[] = [];
|
|
2055
|
-
const stages = explainResponse?.stages;
|
|
2056
|
-
|
|
2057
|
-
if (Array.isArray(stages)) {
|
|
2058
|
-
stages.forEach((stageEntry, index) => {
|
|
2059
|
-
if (!stageEntry || typeof stageEntry !== 'object') {
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
const stageKey = Object.keys(stageEntry).find(key => key.startsWith('$')) || `stage_${index}`;
|
|
2064
|
-
const stagePayload = (stageEntry as any)[stageKey];
|
|
2065
|
-
const metricsSource = stageKey === '$cursor' && stagePayload?.executionStats
|
|
2066
|
-
? stagePayload.executionStats
|
|
2067
|
-
: stagePayload;
|
|
2068
|
-
const executionStages = metricsSource?.executionStages || stagePayload?.executionStats?.executionStages;
|
|
2069
|
-
const executionTimeMs = SlowQueryVerifier.maxNumber([
|
|
2070
|
-
SlowQueryVerifier.asNonNegativeNumber(metricsSource?.executionTimeMillis),
|
|
2071
|
-
SlowQueryVerifier.asNonNegativeNumber(metricsSource?.executionTimeMillisEstimate),
|
|
2072
|
-
SlowQueryVerifier.asNonNegativeNumber(executionStages?.executionTimeMillis),
|
|
2073
|
-
SlowQueryVerifier.asNonNegativeNumber(executionStages?.executionTimeMillisEstimate)
|
|
2074
|
-
]);
|
|
2075
|
-
const docsExamined = SlowQueryVerifier.maxNumber([
|
|
2076
|
-
SlowQueryVerifier.asNonNegativeNumber(metricsSource?.totalDocsExamined),
|
|
2077
|
-
SlowQueryVerifier.asNonNegativeNumber(metricsSource?.docsExamined),
|
|
2078
|
-
SlowQueryVerifier.asNonNegativeNumber(executionStages?.totalDocsExamined),
|
|
2079
|
-
SlowQueryVerifier.asNonNegativeNumber(executionStages?.docsExamined)
|
|
2080
|
-
]);
|
|
2081
|
-
const keysExamined = SlowQueryVerifier.maxNumber([
|
|
2082
|
-
SlowQueryVerifier.asNonNegativeNumber(metricsSource?.totalKeysExamined),
|
|
2083
|
-
SlowQueryVerifier.asNonNegativeNumber(metricsSource?.keysExamined),
|
|
2084
|
-
SlowQueryVerifier.asNonNegativeNumber(executionStages?.totalKeysExamined),
|
|
2085
|
-
SlowQueryVerifier.asNonNegativeNumber(executionStages?.keysExamined)
|
|
2086
|
-
]);
|
|
2087
|
-
const nReturned = SlowQueryVerifier.maxNumber([
|
|
2088
|
-
SlowQueryVerifier.asNonNegativeNumber(metricsSource?.nReturned),
|
|
2089
|
-
SlowQueryVerifier.asNonNegativeNumber(executionStages?.nReturned)
|
|
2090
|
-
]);
|
|
2091
|
-
|
|
2092
|
-
if (
|
|
2093
|
-
typeof executionTimeMs === 'number'
|
|
2094
|
-
|| typeof docsExamined === 'number'
|
|
2095
|
-
|| typeof keysExamined === 'number'
|
|
2096
|
-
|| typeof nReturned === 'number'
|
|
2097
|
-
) {
|
|
2098
|
-
summaries.push({
|
|
2099
|
-
stage: stageKey,
|
|
2100
|
-
path: `stages[${index}]`,
|
|
2101
|
-
executionTimeMs,
|
|
2102
|
-
docsExamined,
|
|
2103
|
-
keysExamined,
|
|
2104
|
-
nReturned
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
if (executionStages) {
|
|
2109
|
-
SlowQueryVerifier.collectExecutionTreeStages(
|
|
2110
|
-
executionStages,
|
|
2111
|
-
`stages[${index}].${stageKey}.executionStages`,
|
|
2112
|
-
summaries
|
|
2113
|
-
);
|
|
2114
|
-
}
|
|
2115
|
-
});
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
const executionRoot = explainStats?.executionStages
|
|
2119
|
-
|| explainResponse?.executionStats?.executionStages
|
|
2120
|
-
|| explainResponse?.executionStats;
|
|
2121
|
-
if (executionRoot) {
|
|
2122
|
-
SlowQueryVerifier.collectExecutionTreeStages(executionRoot, 'executionStats.executionStages', summaries);
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
const deduped = new Map<string, ExplainStageSummary>();
|
|
2126
|
-
summaries.forEach((summary) => {
|
|
2127
|
-
const key = [
|
|
2128
|
-
summary.path,
|
|
2129
|
-
summary.stage,
|
|
2130
|
-
typeof summary.executionTimeMs === 'number' ? summary.executionTimeMs : '',
|
|
2131
|
-
typeof summary.docsExamined === 'number' ? summary.docsExamined : '',
|
|
2132
|
-
typeof summary.keysExamined === 'number' ? summary.keysExamined : '',
|
|
2133
|
-
typeof summary.nReturned === 'number' ? summary.nReturned : ''
|
|
2134
|
-
].join('|');
|
|
2135
|
-
if (!deduped.has(key)) {
|
|
2136
|
-
deduped.set(key, summary);
|
|
2137
|
-
}
|
|
2138
|
-
});
|
|
2139
|
-
|
|
2140
|
-
return SlowQueryVerifier.sortStageSummaries(Array.from(deduped.values()));
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
private resolveExecutionMetrics(
|
|
2144
|
-
explainStats: Record<string, any>,
|
|
2145
|
-
fallbackDuration?: number,
|
|
2146
|
-
stageSummaries: ExplainStageSummary[] = []
|
|
2147
|
-
): ExecutionMetrics {
|
|
2148
|
-
const docsCandidates: number[] = [];
|
|
2149
|
-
const returnedCandidates: number[] = [];
|
|
2150
|
-
SlowQueryVerifier.collectMetricValues(explainStats || {}, new Set(['totalDocsExamined', 'docsExamined']), docsCandidates);
|
|
2151
|
-
SlowQueryVerifier.collectMetricValues(explainStats || {}, new Set(['nReturned']), returnedCandidates);
|
|
2152
|
-
|
|
2153
|
-
const stageDocsCandidates = stageSummaries
|
|
2154
|
-
.map(stage => stage.docsExamined)
|
|
2155
|
-
.filter((value): value is number => typeof value === 'number');
|
|
2156
|
-
const stageReturnedCandidates = stageSummaries
|
|
2157
|
-
.map(stage => stage.nReturned)
|
|
2158
|
-
.filter((value): value is number => typeof value === 'number');
|
|
2159
|
-
|
|
2160
|
-
const docsExamined = docsCandidates.length
|
|
2161
|
-
? Math.max(...docsCandidates)
|
|
2162
|
-
: (stageDocsCandidates.length ? Math.max(...stageDocsCandidates) : undefined);
|
|
2163
|
-
const nReturned = returnedCandidates.length
|
|
2164
|
-
? Math.max(...returnedCandidates)
|
|
2165
|
-
: (stageReturnedCandidates.length ? Math.max(...stageReturnedCandidates) : undefined);
|
|
2166
|
-
const durationMs = SlowQueryVerifier.isValidDuration(fallbackDuration)
|
|
2167
|
-
? fallbackDuration
|
|
2168
|
-
: undefined;
|
|
2169
|
-
|
|
2170
|
-
return {
|
|
2171
|
-
durationMs,
|
|
2172
|
-
docsExamined,
|
|
2173
|
-
nReturned,
|
|
2174
|
-
topStages: stageSummaries.slice(0, 5)
|
|
2175
|
-
};
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
private evaluateOptimizationOutcome(
|
|
2179
|
-
baseline: ExecutionMetrics,
|
|
2180
|
-
after: ExecutionMetrics,
|
|
2181
|
-
outputEquivalence?: OutputEquivalenceResult
|
|
2182
|
-
): {
|
|
2183
|
-
passed: boolean;
|
|
2184
|
-
reason: string;
|
|
2185
|
-
durationRatio?: number;
|
|
2186
|
-
docsRatio?: number;
|
|
2187
|
-
nReturnedDeltaRatio?: number;
|
|
2188
|
-
outputEquivalence?: OutputEquivalenceResult;
|
|
2189
|
-
} {
|
|
2190
|
-
if (this.config.autoOptimizeRequireExactOutput) {
|
|
2191
|
-
if (!this.config.autoOptimizeOutputCompareEnabled) {
|
|
2192
|
-
return {
|
|
2193
|
-
passed: false,
|
|
2194
|
-
reason: 'Exact output equivalence is required but output comparison is disabled.',
|
|
2195
|
-
outputEquivalence
|
|
2196
|
-
};
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
if (!outputEquivalence) {
|
|
2200
|
-
return {
|
|
2201
|
-
passed: false,
|
|
2202
|
-
reason: 'Output equivalence proof is required but was not generated.',
|
|
2203
|
-
outputEquivalence
|
|
2204
|
-
};
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
if (outputEquivalence && !outputEquivalence.passed) {
|
|
2209
|
-
return {
|
|
2210
|
-
passed: false,
|
|
2211
|
-
reason: `Output equivalence failed: ${outputEquivalence.reason}`,
|
|
2212
|
-
outputEquivalence
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
if (!SlowQueryVerifier.isValidDuration(baseline.durationMs) || !SlowQueryVerifier.isValidDuration(after.durationMs)) {
|
|
2217
|
-
return {
|
|
2218
|
-
passed: false,
|
|
2219
|
-
reason: 'Unable to compare baseline and post-fix duration.',
|
|
2220
|
-
outputEquivalence
|
|
2221
|
-
};
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
const durationRatio = baseline.durationMs > 0 ? (after.durationMs / baseline.durationMs) : 1;
|
|
2225
|
-
if (durationRatio > this.config.autoOptimizeDurationRatioTarget) {
|
|
2226
|
-
return {
|
|
2227
|
-
passed: false,
|
|
2228
|
-
reason: `Duration did not improve enough (${round(durationRatio * 100, 0)}% of baseline).`,
|
|
2229
|
-
durationRatio,
|
|
2230
|
-
outputEquivalence
|
|
2231
|
-
};
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
if (typeof baseline.docsExamined !== 'number' || typeof after.docsExamined !== 'number' || baseline.docsExamined <= 0) {
|
|
2235
|
-
return {
|
|
2236
|
-
passed: false,
|
|
2237
|
-
reason: 'Docs examined metrics are missing for baseline or post-fix explain.',
|
|
2238
|
-
durationRatio,
|
|
2239
|
-
outputEquivalence
|
|
2240
|
-
};
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
const docsRatio = after.docsExamined / baseline.docsExamined;
|
|
2244
|
-
if (docsRatio > this.config.autoOptimizeDocsRatioTarget) {
|
|
2245
|
-
return {
|
|
2246
|
-
passed: false,
|
|
2247
|
-
reason: `Docs examined did not improve enough (${round(docsRatio * 100, 0)}% of baseline).`,
|
|
2248
|
-
durationRatio,
|
|
2249
|
-
docsRatio,
|
|
2250
|
-
outputEquivalence
|
|
2251
|
-
};
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
if (typeof baseline.nReturned === 'number' && typeof after.nReturned === 'number') {
|
|
2255
|
-
const denominator = Math.max(baseline.nReturned, 1);
|
|
2256
|
-
const nReturnedDeltaRatio = Math.abs(after.nReturned - baseline.nReturned) / denominator;
|
|
2257
|
-
if (nReturnedDeltaRatio > this.config.autoOptimizeReturnedDocsTolerance) {
|
|
2258
|
-
return {
|
|
2259
|
-
passed: false,
|
|
2260
|
-
reason: `Returned document count changed too much (${round(nReturnedDeltaRatio * 100, 0)}% delta).`,
|
|
2261
|
-
durationRatio,
|
|
2262
|
-
docsRatio,
|
|
2263
|
-
nReturnedDeltaRatio,
|
|
2264
|
-
outputEquivalence
|
|
2265
|
-
};
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
return {
|
|
2269
|
-
passed: true,
|
|
2270
|
-
reason: 'Query performance improved while keeping returned document count stable.',
|
|
2271
|
-
durationRatio,
|
|
2272
|
-
docsRatio,
|
|
2273
|
-
nReturnedDeltaRatio,
|
|
2274
|
-
outputEquivalence
|
|
2275
|
-
};
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
return {
|
|
2279
|
-
passed: false,
|
|
2280
|
-
reason: 'Returned document metrics are missing for baseline or post-fix explain.',
|
|
2281
|
-
durationRatio,
|
|
2282
|
-
docsRatio,
|
|
2283
|
-
outputEquivalence
|
|
2284
|
-
};
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
private formatStageSummaryForPrompt(stage: ExplainStageSummary, index: number): string {
|
|
2288
|
-
const metrics: string[] = [];
|
|
2289
|
-
if (typeof stage.executionTimeMs === 'number') {
|
|
2290
|
-
metrics.push(`time=${stage.executionTimeMs}ms`);
|
|
2291
|
-
}
|
|
2292
|
-
if (typeof stage.docsExamined === 'number') {
|
|
2293
|
-
metrics.push(`docs=${stage.docsExamined}`);
|
|
2294
|
-
}
|
|
2295
|
-
if (typeof stage.keysExamined === 'number') {
|
|
2296
|
-
metrics.push(`keys=${stage.keysExamined}`);
|
|
2297
|
-
}
|
|
2298
|
-
if (typeof stage.nReturned === 'number') {
|
|
2299
|
-
metrics.push(`returned=${stage.nReturned}`);
|
|
2300
|
-
}
|
|
2301
|
-
return `${index + 1}. ${stage.stage} @ ${stage.path}${metrics.length ? ` (${metrics.join(', ')})` : ''}`;
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
private buildSlowQueryAutoOptimizeDescription(
|
|
2305
|
-
log: SlowQueryLogModel,
|
|
2306
|
-
app: AICoderAppModel,
|
|
2307
|
-
baseline: ExecutionMetrics,
|
|
2308
|
-
repoSlug: string
|
|
2309
|
-
): string {
|
|
2310
|
-
const topStages = Array.isArray(baseline.topStages) ? baseline.topStages : [];
|
|
2311
|
-
const lookupExprInCount = SlowQueryVerifier.countLookupExprInPattern(Array.isArray(log.pipeline) ? log.pipeline : []);
|
|
2312
|
-
const lines: string[] = [
|
|
2313
|
-
'Autonomous slow-query optimization request.',
|
|
2314
|
-
'',
|
|
2315
|
-
'Hard requirements:',
|
|
2316
|
-
'1. Before changing code, query the project MongoDB diagnostics logs for the latest slow-query context.',
|
|
2317
|
-
'2. Query `slow-query-logs` using `_id` and `query_hash` from this task, then use the newest matching records.',
|
|
2318
|
-
'3. If a previous dashboard job id is provided, query `ai-development-jobs` by that `_id` and review recent failure logs before retrying.',
|
|
2319
|
-
'4. Treat `.dashboard-output/build-*.log` as primary build evidence, and `.build-output/build-*.log` as retained history when diagnosing failures.',
|
|
2320
|
-
'5. Locate the source query in app code and optimize it safely (query shape contract must remain compatible).',
|
|
2321
|
-
'6. Add or adjust indexes/code paths so docs examined and processing time drop significantly.',
|
|
2322
|
-
'7. Measure before/after `explain(\"executionStats\")` and identify the slowest stages by execution time/docs examined.',
|
|
2323
|
-
'8. Returned data must be exactly equivalent before and after optimization. Any output difference is a failed run.',
|
|
2324
|
-
'9. In `$lookup`, avoid `$expr` + `$in` when equivalent `localField` / `foreignField` joins are possible and index-friendly.',
|
|
2325
|
-
'10. Run build/lint checks and iterate until green.',
|
|
2326
|
-
'11. Use workspace context `/var/ai-workspace/<id_slow_query>` and inspect transpiled runtime references under `/var/app/current`.',
|
|
2327
|
-
'12. Publish to default branch and deploy artifacts automatically after build success.',
|
|
2328
|
-
'',
|
|
2329
|
-
`App: ${app.name || app._id}`,
|
|
2330
|
-
`Repo: ${repoSlug || app.repo || 'unknown'}`,
|
|
2331
|
-
`Slow Query #: ${log.slow_query_count_string || log._id || ''}`,
|
|
2332
|
-
`Workspace Context Id: ${String(log?._id || '').trim() || 'n/a'}`,
|
|
2333
|
-
`Workspace Path: /var/ai-workspace/${String(log?._id || '').trim() || '<id_slow_query>'}`,
|
|
2334
|
-
`Collection: ${log.collection}`,
|
|
2335
|
-
`Query Hash: ${log.query_hash}`,
|
|
2336
|
-
`Slow Query Log Id: ${String(log?._id || '').trim() || 'n/a'}`,
|
|
2337
|
-
`Previous Dashboard Job Id: ${String((log as any)?.openai_task_id || '').trim() || 'n/a'}`,
|
|
2338
|
-
`Source App: ${log.source_app || 'n/a'}`,
|
|
2339
|
-
`Environment: ${log.environment || 'n/a'}`,
|
|
2340
|
-
`Baseline Duration (ms): ${typeof baseline.durationMs === 'number' ? baseline.durationMs : 'unknown'}`,
|
|
2341
|
-
`Baseline Docs Examined: ${typeof baseline.docsExamined === 'number' ? baseline.docsExamined : 'unknown'}`,
|
|
2342
|
-
`Baseline Returned Docs: ${typeof baseline.nReturned === 'number' ? baseline.nReturned : 'unknown'}`,
|
|
2343
|
-
`Detected $lookup with $expr+$in: ${lookupExprInCount}`,
|
|
2344
|
-
'',
|
|
2345
|
-
'Baseline Hot Stages:',
|
|
2346
|
-
...(topStages.length
|
|
2347
|
-
? topStages.map((stage, index) => this.formatStageSummaryForPrompt(stage, index))
|
|
2348
|
-
: ['No stage-level metrics were captured.']),
|
|
2349
|
-
'',
|
|
2350
|
-
'Filter:',
|
|
2351
|
-
'```json',
|
|
2352
|
-
JSON.stringify(log.filter || {}, null, 2),
|
|
2353
|
-
'```',
|
|
2354
|
-
'',
|
|
2355
|
-
'Pipeline:',
|
|
2356
|
-
'```json',
|
|
2357
|
-
JSON.stringify(Array.isArray(log.pipeline) ? log.pipeline : [], null, 2),
|
|
2358
|
-
'```',
|
|
2359
|
-
'',
|
|
2360
|
-
'Options:',
|
|
2361
|
-
'```json',
|
|
2362
|
-
JSON.stringify(log.options || {}, null, 2),
|
|
2363
|
-
'```'
|
|
2364
|
-
];
|
|
2365
|
-
return lines.join('\n');
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
private resolveAutoOptimizeRepoSlug(app: AICoderAppModel | null): string {
|
|
2369
|
-
const configuredRepo = String(this.config?.autofixGithubRepo || '').trim();
|
|
2370
|
-
const configuredOwner = String(this.config?.autofixGithubOwner || 'resolveio').trim() || 'resolveio';
|
|
2371
|
-
if (configuredRepo) {
|
|
2372
|
-
return `${configuredOwner}/${configuredRepo}`;
|
|
2373
|
-
}
|
|
2374
|
-
return String(app?.repo || '').trim();
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
private resolveAutoOptimizeRepoPath(app: AICoderAppModel | null): string {
|
|
2378
|
-
const configuredPath = String(this.config?.autofixRepoRoot || '').trim();
|
|
2379
|
-
if (configuredPath) {
|
|
2380
|
-
return configuredPath;
|
|
2381
|
-
}
|
|
2382
|
-
return String(app?.git_local_path || '').trim();
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
private static queryHasExplicitSort(pipeline?: any[], findOptions?: Record<string, any>): boolean {
|
|
2386
|
-
const hasFindSort = !!(findOptions?.sort && typeof findOptions.sort === 'object' && Object.keys(findOptions.sort).length);
|
|
2387
|
-
if (hasFindSort) {
|
|
2388
|
-
return true;
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
if (!Array.isArray(pipeline)) {
|
|
2392
|
-
return false;
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
return pipeline.some((stage) => {
|
|
2396
|
-
if (!stage || typeof stage !== 'object') {
|
|
2397
|
-
return false;
|
|
2398
|
-
}
|
|
2399
|
-
const sort = (stage as any).$sort;
|
|
2400
|
-
return !!(sort && typeof sort === 'object' && Object.keys(sort).length);
|
|
2401
|
-
});
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
private static countLookupExprInPattern(pipeline?: any[]): number {
|
|
2405
|
-
if (!Array.isArray(pipeline)) {
|
|
2406
|
-
return 0;
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
let count = 0;
|
|
2410
|
-
pipeline.forEach((stage) => {
|
|
2411
|
-
const lookup = stage && typeof stage === 'object' ? (stage as any).$lookup : undefined;
|
|
2412
|
-
if (!lookup || typeof lookup !== 'object' || !Array.isArray(lookup.pipeline)) {
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
const lookupPipelineJson = JSON.stringify(lookup.pipeline);
|
|
2417
|
-
if (lookupPipelineJson.includes('"$expr"') && lookupPipelineJson.includes('"$in"')) {
|
|
2418
|
-
count += 1;
|
|
2419
|
-
}
|
|
2420
|
-
});
|
|
2421
|
-
return count;
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
private static buildBoundedPipelineForOutputCompare(pipeline: any[], maxDocs: number): any[] {
|
|
2425
|
-
const cloned = SlowQueryVerifier.deepClone(pipeline);
|
|
2426
|
-
if (!Number.isFinite(maxDocs) || maxDocs <= 0) {
|
|
2427
|
-
return cloned;
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
cloned.push({
|
|
2431
|
-
$limit: maxDocs + 1
|
|
2432
|
-
});
|
|
2433
|
-
return cloned;
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
private static buildBoundedFindOptionsForOutputCompare(findOptions: Record<string, any> | undefined, maxDocs: number): Record<string, any> | undefined {
|
|
2437
|
-
const bounded = findOptions ? SlowQueryVerifier.deepClone(findOptions) : {};
|
|
2438
|
-
if (!Number.isFinite(maxDocs) || maxDocs <= 0) {
|
|
2439
|
-
return Object.keys(bounded).length ? bounded : undefined;
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
const compareLimit = maxDocs + 1;
|
|
2443
|
-
if (typeof bounded.limit === 'number' && bounded.limit > 0) {
|
|
2444
|
-
bounded.limit = Math.min(bounded.limit, compareLimit);
|
|
2445
|
-
}
|
|
2446
|
-
else {
|
|
2447
|
-
bounded.limit = compareLimit;
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
return Object.keys(bounded).length ? bounded : undefined;
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
private static normalizeOutputValue(value: any): any {
|
|
2454
|
-
if (value === null) {
|
|
2455
|
-
return null;
|
|
2456
|
-
}
|
|
2457
|
-
if (typeof value === 'undefined') {
|
|
2458
|
-
return null;
|
|
2459
|
-
}
|
|
2460
|
-
if (value instanceof Date) {
|
|
2461
|
-
return {$date: value.toISOString()};
|
|
2462
|
-
}
|
|
2463
|
-
if (Buffer.isBuffer(value)) {
|
|
2464
|
-
return {$binary: value.toString('base64')};
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
const valueType = typeof value;
|
|
2468
|
-
if (valueType === 'string' || valueType === 'boolean') {
|
|
2469
|
-
return value;
|
|
2470
|
-
}
|
|
2471
|
-
if (valueType === 'number') {
|
|
2472
|
-
if (!Number.isFinite(value)) {
|
|
2473
|
-
return '__non_finite_number__';
|
|
2474
|
-
}
|
|
2475
|
-
return value;
|
|
2476
|
-
}
|
|
2477
|
-
if (valueType === 'bigint') {
|
|
2478
|
-
return value.toString();
|
|
2479
|
-
}
|
|
2480
|
-
if (valueType === 'function') {
|
|
2481
|
-
return '__function__';
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
if (Array.isArray(value)) {
|
|
2485
|
-
return value.map(entry => SlowQueryVerifier.normalizeOutputValue(entry));
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
if (valueType === 'object') {
|
|
2489
|
-
if (typeof (value as any).toHexString === 'function') {
|
|
2490
|
-
try {
|
|
2491
|
-
return {$oid: (value as any).toHexString()};
|
|
2492
|
-
}
|
|
2493
|
-
catch {
|
|
2494
|
-
// continue into generic object handling
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
if (typeof (value as any).toJSON === 'function') {
|
|
2499
|
-
try {
|
|
2500
|
-
return SlowQueryVerifier.normalizeOutputValue((value as any).toJSON());
|
|
2501
|
-
}
|
|
2502
|
-
catch {
|
|
2503
|
-
// continue into generic object handling
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
const normalized: Record<string, any> = {};
|
|
2508
|
-
Object.keys(value).sort().forEach((key) => {
|
|
2509
|
-
normalized[key] = SlowQueryVerifier.normalizeOutputValue(value[key]);
|
|
2510
|
-
});
|
|
2511
|
-
return normalized;
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
return `${value}`;
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
private static digestOutputDocument(doc: any): string {
|
|
2518
|
-
const normalized = SlowQueryVerifier.normalizeOutputValue(doc);
|
|
2519
|
-
const serialized = JSON.stringify(normalized);
|
|
2520
|
-
return createHash('sha256').update(serialized, 'utf8').digest('hex');
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
private async captureOutputFingerprint(log: SlowQueryLogModel, overrides?: SlowQueryExecutionOverride): Promise<OutputFingerprint> {
|
|
2524
|
-
const collectionName = log.collection;
|
|
2525
|
-
if (!collectionName) {
|
|
2526
|
-
throw new Error('Slow query missing collection name.');
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
const target = await this.resolveExplainTarget(log);
|
|
2530
|
-
let client: MongoClient | undefined;
|
|
2531
|
-
let db: any;
|
|
2532
|
-
|
|
2533
|
-
try {
|
|
2534
|
-
if (target.type === 'client') {
|
|
2535
|
-
if (!target.uri) {
|
|
2536
|
-
throw new SlowQueryVerifierError('client_db_missing_uri', 'Client DB missing uri.');
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
client = await MongoClient.connect(target.uri, {
|
|
2540
|
-
connectTimeoutMS: 10000,
|
|
2541
|
-
serverSelectionTimeoutMS: 10000,
|
|
2542
|
-
readPreference: 'secondaryPreferred'
|
|
2543
|
-
});
|
|
2544
|
-
db = client.db(target.dbName);
|
|
2545
|
-
}
|
|
2546
|
-
else {
|
|
2547
|
-
db = ResolveIOServer.getMainDB();
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
if (!db) {
|
|
2551
|
-
throw new SlowQueryVerifierError('main_db_unavailable', 'Main server DB is not available.');
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
const maxDocs = Number.isFinite(Number(this.config.autoOptimizeOutputCompareMaxDocs))
|
|
2555
|
-
? Number(this.config.autoOptimizeOutputCompareMaxDocs)
|
|
2556
|
-
: AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS;
|
|
2557
|
-
const effectiveMaxDocs = maxDocs > 0 ? Math.floor(maxDocs) : AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS;
|
|
2558
|
-
const effectiveLog = SlowQueryVerifier.applyQueryOverrides(log, overrides);
|
|
2559
|
-
const pipeline = SlowQueryVerifier.extractPipelineFromLog(effectiveLog);
|
|
2560
|
-
const filter = effectiveLog.filter ?? {};
|
|
2561
|
-
const findOptions = SlowQueryVerifier.extractFindOptions(effectiveLog.options);
|
|
2562
|
-
const aggregateOptions = SlowQueryVerifier.extractAggregateOptions(effectiveLog.options);
|
|
2563
|
-
const explicitSort = SlowQueryVerifier.queryHasExplicitSort(pipeline, findOptions);
|
|
2564
|
-
|
|
2565
|
-
let cursor: any;
|
|
2566
|
-
if (pipeline) {
|
|
2567
|
-
if (SlowQueryVerifier.pipelineHasWriteStage(pipeline as any[])) {
|
|
2568
|
-
throw new SlowQueryVerifierError('aggregate_write_stage', 'Aggregate pipeline includes a write stage; output comparison skipped.');
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
const boundedPipeline = SlowQueryVerifier.buildBoundedPipelineForOutputCompare(
|
|
2572
|
-
pipeline,
|
|
2573
|
-
effectiveMaxDocs
|
|
2574
|
-
);
|
|
2575
|
-
cursor = db.collection(collectionName)
|
|
2576
|
-
.aggregate(boundedPipeline, {
|
|
2577
|
-
...(aggregateOptions || {}),
|
|
2578
|
-
readPreference: 'secondaryPreferred'
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
|
-
else {
|
|
2582
|
-
const boundedFindOptions = SlowQueryVerifier.buildBoundedFindOptionsForOutputCompare(
|
|
2583
|
-
findOptions,
|
|
2584
|
-
effectiveMaxDocs
|
|
2585
|
-
);
|
|
2586
|
-
cursor = SlowQueryVerifier.buildFindCursor(
|
|
2587
|
-
db.collection(collectionName),
|
|
2588
|
-
filter,
|
|
2589
|
-
boundedFindOptions
|
|
2590
|
-
);
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
const orderedHasher = createHash('sha256');
|
|
2594
|
-
let unorderedSum = 0;
|
|
2595
|
-
let unorderedXor = 0;
|
|
2596
|
-
let docsCompared = 0;
|
|
2597
|
-
let truncated = false;
|
|
2598
|
-
let firstDocDigest = '';
|
|
2599
|
-
let lastDocDigest = '';
|
|
2600
|
-
const startedAt = Date.now();
|
|
2601
|
-
|
|
2602
|
-
try {
|
|
2603
|
-
for await (const doc of cursor) {
|
|
2604
|
-
if (docsCompared >= effectiveMaxDocs) {
|
|
2605
|
-
truncated = true;
|
|
2606
|
-
break;
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
const docDigest = SlowQueryVerifier.digestOutputDocument(doc);
|
|
2610
|
-
if (!firstDocDigest) {
|
|
2611
|
-
firstDocDigest = docDigest;
|
|
2612
|
-
}
|
|
2613
|
-
lastDocDigest = docDigest;
|
|
2614
|
-
orderedHasher.update(docDigest, 'utf8');
|
|
2615
|
-
|
|
2616
|
-
const head = (parseInt(docDigest.slice(0, 8), 16) ^ parseInt(docDigest.slice(8, 16), 16)) >>> 0;
|
|
2617
|
-
const tail = (parseInt(docDigest.slice(16, 24), 16) ^ parseInt(docDigest.slice(24, 32), 16)) >>> 0;
|
|
2618
|
-
unorderedSum = (unorderedSum + head + tail) >>> 0;
|
|
2619
|
-
unorderedXor = (unorderedXor ^ head ^ tail) >>> 0;
|
|
2620
|
-
docsCompared += 1;
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
finally {
|
|
2624
|
-
if (cursor && typeof cursor.close === 'function') {
|
|
2625
|
-
await cursor.close();
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
const durationMs = Date.now() - startedAt;
|
|
2630
|
-
return {
|
|
2631
|
-
explicitSort,
|
|
2632
|
-
docsCompared,
|
|
2633
|
-
truncated,
|
|
2634
|
-
orderedDigest: orderedHasher.digest('hex'),
|
|
2635
|
-
unorderedDigest: `${unorderedSum.toString(16).padStart(8, '0')}:${unorderedXor.toString(16).padStart(8, '0')}`,
|
|
2636
|
-
firstDocDigest,
|
|
2637
|
-
lastDocDigest,
|
|
2638
|
-
durationMs: SlowQueryVerifier.isValidDuration(durationMs) ? durationMs : -1,
|
|
2639
|
-
maxDocs: effectiveMaxDocs
|
|
2640
|
-
};
|
|
2641
|
-
}
|
|
2642
|
-
finally {
|
|
2643
|
-
if (client) {
|
|
2644
|
-
await client.close();
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
private compareOutputEquivalence(baseline: OutputFingerprint, after: OutputFingerprint): OutputEquivalenceResult {
|
|
2650
|
-
const mode: 'ordered' | 'unordered' = (baseline.explicitSort || after.explicitSort)
|
|
2651
|
-
? 'ordered'
|
|
2652
|
-
: 'unordered';
|
|
2653
|
-
|
|
2654
|
-
if (baseline.truncated || after.truncated) {
|
|
2655
|
-
return {
|
|
2656
|
-
passed: false,
|
|
2657
|
-
reason: `Result set exceeded output comparison cap (${Math.max(baseline.maxDocs, after.maxDocs)} docs).`,
|
|
2658
|
-
mode,
|
|
2659
|
-
baseline,
|
|
2660
|
-
after
|
|
2661
|
-
};
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
if (baseline.docsCompared !== after.docsCompared) {
|
|
2665
|
-
return {
|
|
2666
|
-
passed: false,
|
|
2667
|
-
reason: `Returned row count changed (${baseline.docsCompared} -> ${after.docsCompared}).`,
|
|
2668
|
-
mode,
|
|
2669
|
-
baseline,
|
|
2670
|
-
after
|
|
2671
|
-
};
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
if (mode === 'ordered') {
|
|
2675
|
-
if (baseline.orderedDigest !== after.orderedDigest) {
|
|
2676
|
-
return {
|
|
2677
|
-
passed: false,
|
|
2678
|
-
reason: 'Ordered output digest changed.',
|
|
2679
|
-
mode,
|
|
2680
|
-
baseline,
|
|
2681
|
-
after
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
else if (baseline.unorderedDigest !== after.unorderedDigest) {
|
|
2686
|
-
return {
|
|
2687
|
-
passed: false,
|
|
2688
|
-
reason: 'Output set digest changed.',
|
|
2689
|
-
mode,
|
|
2690
|
-
baseline,
|
|
2691
|
-
after
|
|
2692
|
-
};
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
return {
|
|
2696
|
-
passed: true,
|
|
2697
|
-
reason: 'Output fingerprints are equivalent.',
|
|
2698
|
-
mode,
|
|
2699
|
-
baseline,
|
|
2700
|
-
after
|
|
2701
|
-
};
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
private async resolveAutoOptimizeApp(log: SlowQueryLogModel): Promise<AICoderAppModel | null> {
|
|
2705
|
-
const exactRepoCandidate = String(log.environment || '').trim();
|
|
2706
|
-
if (exactRepoCandidate && exactRepoCandidate.includes('/')) {
|
|
2707
|
-
const escaped = this.escapeRegex(exactRepoCandidate);
|
|
2708
|
-
const byRepo = await AICoderApps.findOne({
|
|
2709
|
-
repo: {$regex: `^${escaped}$`, $options: 'i'}
|
|
2710
|
-
}, {
|
|
2711
|
-
sort: {
|
|
2712
|
-
updatedAt: -1,
|
|
2713
|
-
createdAt: -1
|
|
2714
|
-
}
|
|
2715
|
-
});
|
|
2716
|
-
if (byRepo) {
|
|
2717
|
-
return byRepo;
|
|
2718
|
-
}
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
const clientCandidates = [
|
|
2722
|
-
String(log.client_slug || '').trim(),
|
|
2723
|
-
String(log.client_name || '').trim(),
|
|
2724
|
-
String(log.source_app || '').trim()
|
|
2725
|
-
].filter(Boolean);
|
|
2726
|
-
|
|
2727
|
-
for (const candidate of clientCandidates) {
|
|
2728
|
-
const escaped = this.escapeRegex(candidate);
|
|
2729
|
-
const byApp = await AICoderApps.findOne({
|
|
2730
|
-
$or: [
|
|
2731
|
-
{slug: {$regex: `^${escaped}$`, $options: 'i'}},
|
|
2732
|
-
{name: {$regex: `^${escaped}$`, $options: 'i'}}
|
|
2733
|
-
]
|
|
2734
|
-
}, {
|
|
2735
|
-
sort: {
|
|
2736
|
-
updatedAt: -1,
|
|
2737
|
-
createdAt: -1
|
|
2738
|
-
}
|
|
2739
|
-
});
|
|
2740
|
-
if (byApp) {
|
|
2741
|
-
return byApp;
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
const clientDoc = await Clients.findOne({
|
|
2745
|
-
$or: [
|
|
2746
|
-
{name: {$regex: `^${escaped}$`, $options: 'i'}},
|
|
2747
|
-
{demo_name: {$regex: `^${escaped}$`, $options: 'i'}},
|
|
2748
|
-
{project: {$regex: `^${escaped}$`, $options: 'i'}},
|
|
2749
|
-
{repo: {$regex: `^${escaped}$`, $options: 'i'}}
|
|
2750
|
-
]
|
|
2751
|
-
});
|
|
2752
|
-
if (!clientDoc?._id) {
|
|
2753
|
-
continue;
|
|
2754
|
-
}
|
|
2755
|
-
const byClient = await AICoderApps.findOne({client_id: clientDoc._id}, {
|
|
2756
|
-
sort: {
|
|
2757
|
-
updatedAt: -1,
|
|
2758
|
-
createdAt: -1
|
|
2759
|
-
}
|
|
2760
|
-
});
|
|
2761
|
-
if (byClient) {
|
|
2762
|
-
return byClient;
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
return null;
|
|
2767
|
-
}
|
|
2768
|
-
|
|
2769
|
-
private async runAutoOptimization(logId: string, force = false): Promise<void> {
|
|
2770
|
-
const autoOptimizeEnabled = await this.resolveAutoOptimizeEnabled();
|
|
2771
|
-
if (!logId || (!autoOptimizeEnabled && !force)) {
|
|
2772
|
-
return;
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
const log = await SlowQueryLogs.findOne({_id: logId});
|
|
2776
|
-
if (!log || !log._id || log.ignored) {
|
|
2777
|
-
return;
|
|
2778
|
-
}
|
|
2779
|
-
if (log.status === 'optimized' && !force) {
|
|
2780
|
-
return;
|
|
2781
|
-
}
|
|
2782
|
-
if (log.auto_fix_status === 'running') {
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
|
-
if (log.auto_fix_status === 'queued' && String(log.openai_task_id || '').trim()) {
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
|
-
if (!force) {
|
|
2790
|
-
const attemptsUsed = Number.isFinite(Number(log.auto_fix_attempt_count))
|
|
2791
|
-
? Number(log.auto_fix_attempt_count)
|
|
2792
|
-
: 0;
|
|
2793
|
-
const maxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerQuery))
|
|
2794
|
-
? Number(this.config.autoOptimizeMaxAttemptsPerQuery)
|
|
2795
|
-
: 0;
|
|
2796
|
-
if (maxAttempts > 0 && attemptsUsed >= maxAttempts) {
|
|
2797
|
-
await this.markAutoOptimizeBudgetExceeded(log, 'Auto optimize skipped');
|
|
2798
|
-
return;
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
const cooldownDeadline = this.resolveCooldownDeadline(log);
|
|
2802
|
-
if (cooldownDeadline && cooldownDeadline.getTime() > Date.now()) {
|
|
2803
|
-
await this.markAutoOptimizeCooldownActive(log, cooldownDeadline);
|
|
2804
|
-
return;
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
const fingerprintMaxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerFingerprint))
|
|
2808
|
-
? Number(this.config.autoOptimizeMaxAttemptsPerFingerprint)
|
|
2809
|
-
: 0;
|
|
2810
|
-
if (fingerprintMaxAttempts > 0) {
|
|
2811
|
-
const windowHours = Number.isFinite(Number(this.config.autoOptimizeFingerprintWindowHours))
|
|
2812
|
-
? Number(this.config.autoOptimizeFingerprintWindowHours)
|
|
2813
|
-
: AUTO_OPTIMIZE_DEFAULT_FINGERPRINT_WINDOW_HOURS;
|
|
2814
|
-
const windowStart = new Date(Date.now() - (windowHours * 60 * 60 * 1000));
|
|
2815
|
-
const fingerprintAttempts = await this.resolveFingerprintAttemptsInWindow(log, windowStart);
|
|
2816
|
-
if (fingerprintAttempts >= fingerprintMaxAttempts) {
|
|
2817
|
-
await this.markAutoOptimizeBudgetExceeded(
|
|
2818
|
-
log,
|
|
2819
|
-
`Auto optimize skipped: fingerprint budget reached (${fingerprintAttempts}/${fingerprintMaxAttempts}) in the last ${windowHours}h.`
|
|
2820
|
-
);
|
|
2821
|
-
return;
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
const app = await this.resolveAutoOptimizeApp(log);
|
|
2827
|
-
const resolvedRepoSlug = this.resolveAutoOptimizeRepoSlug(app);
|
|
2828
|
-
const resolvedRepoPath = this.resolveAutoOptimizeRepoPath(app);
|
|
2829
|
-
if (!app?._id || !resolvedRepoSlug) {
|
|
2830
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
2831
|
-
$set: {
|
|
2832
|
-
status: 'investigating',
|
|
2833
|
-
auto_fix_status: 'failed',
|
|
2834
|
-
verification_notes: 'Auto optimize skipped: unable to map slow query to AI Coder app/repo configuration.',
|
|
2835
|
-
last_triaged_by: 'auto-slow-query',
|
|
2836
|
-
last_triaged_at: new Date()
|
|
2837
|
-
}
|
|
2838
|
-
});
|
|
2839
|
-
return;
|
|
2840
|
-
}
|
|
2841
|
-
const tokenEligibility = await checkAICoderTokenEligibility(
|
|
2842
|
-
app._id,
|
|
2843
|
-
this.config.autoOptimizeRequiredTokens > 0 ? this.config.autoOptimizeRequiredTokens : undefined
|
|
2844
|
-
);
|
|
2845
|
-
if (!tokenEligibility.allowed) {
|
|
2846
|
-
const reason = `${tokenEligibility.message} Available: ${tokenEligibility.summary.available_tokens.toLocaleString()} tokens; required: ${tokenEligibility.required_tokens.toLocaleString()}.`;
|
|
2847
|
-
await this.markAutoOptimizeTokenIneligible(log, reason);
|
|
2848
|
-
return;
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
let baselineExplain: ExplainResult;
|
|
2852
|
-
try {
|
|
2853
|
-
baselineExplain = await this.runExplain(log);
|
|
2854
|
-
}
|
|
2855
|
-
catch (error) {
|
|
2856
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
2857
|
-
$set: {
|
|
2858
|
-
status: 'investigating',
|
|
2859
|
-
auto_fix_status: 'failed',
|
|
2860
|
-
auto_fix_result: {
|
|
2861
|
-
baseline_error: error?.message || 'unknown'
|
|
2862
|
-
},
|
|
2863
|
-
verification_notes: `Auto optimize baseline measurement failed: ${error?.message || 'unknown error'}`,
|
|
2864
|
-
last_triaged_by: 'auto-slow-query',
|
|
2865
|
-
last_triaged_at: new Date()
|
|
2866
|
-
}
|
|
2867
|
-
});
|
|
2868
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize baseline measurement failed');
|
|
2869
|
-
return;
|
|
2870
|
-
}
|
|
2871
|
-
|
|
2872
|
-
const baselineFallbackDuration = this.resolveBaselineDurationMs(log);
|
|
2873
|
-
const baselineDurationMs = SlowQueryVerifier.isValidDuration(baselineExplain.durationMs)
|
|
2874
|
-
? baselineExplain.durationMs
|
|
2875
|
-
: baselineFallbackDuration;
|
|
2876
|
-
const baselineMetrics = this.resolveExecutionMetrics(
|
|
2877
|
-
baselineExplain.explainStats || {},
|
|
2878
|
-
baselineDurationMs,
|
|
2879
|
-
baselineExplain.stageSummaries || []
|
|
2880
|
-
);
|
|
2881
|
-
|
|
2882
|
-
let baselineOutputFingerprint: OutputFingerprint | undefined;
|
|
2883
|
-
if (this.config.autoOptimizeOutputCompareEnabled) {
|
|
2884
|
-
try {
|
|
2885
|
-
baselineOutputFingerprint = await this.captureOutputFingerprint(log);
|
|
2886
|
-
}
|
|
2887
|
-
catch (error) {
|
|
2888
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
2889
|
-
$set: {
|
|
2890
|
-
status: 'investigating',
|
|
2891
|
-
auto_fix_status: 'failed',
|
|
2892
|
-
auto_fix_result: {
|
|
2893
|
-
baseline_error: error?.message || 'unknown'
|
|
2894
|
-
},
|
|
2895
|
-
verification_notes: `Auto optimize baseline output comparison failed: ${error?.message || 'unknown error'}`,
|
|
2896
|
-
last_triaged_by: 'auto-slow-query',
|
|
2897
|
-
last_triaged_at: new Date()
|
|
2898
|
-
}
|
|
2899
|
-
});
|
|
2900
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize baseline output comparison failed');
|
|
2901
|
-
return;
|
|
2902
|
-
}
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
const title = `Optimize slow query ${log.slow_query_count_string || log.collection}`;
|
|
2906
|
-
const description = this.buildSlowQueryAutoOptimizeDescription(log, app, baselineMetrics, resolvedRepoSlug);
|
|
2907
|
-
|
|
2908
|
-
let job: AIDashboardJob;
|
|
2909
|
-
try {
|
|
2910
|
-
job = await this.createDashboardJob({
|
|
2911
|
-
project: app._id,
|
|
2912
|
-
title,
|
|
2913
|
-
description,
|
|
2914
|
-
repo: resolvedRepoSlug,
|
|
2915
|
-
path: resolvedRepoPath || undefined,
|
|
2916
|
-
projectRoot: app.project_root || undefined
|
|
2917
|
-
});
|
|
2918
|
-
}
|
|
2919
|
-
catch (error) {
|
|
2920
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
2921
|
-
$set: {
|
|
2922
|
-
status: 'investigating',
|
|
2923
|
-
auto_fix_status: 'failed',
|
|
2924
|
-
verification_notes: `Auto optimize enqueue failed: ${error?.message || 'unknown error'}`,
|
|
2925
|
-
last_triaged_by: 'auto-slow-query',
|
|
2926
|
-
last_triaged_at: new Date()
|
|
2927
|
-
}
|
|
2928
|
-
});
|
|
2929
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize wait failed');
|
|
2930
|
-
return;
|
|
2931
|
-
}
|
|
2932
|
-
|
|
2933
|
-
const jobId = String(job?._id || '').trim();
|
|
2934
|
-
if (!jobId) {
|
|
2935
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
2936
|
-
$set: {
|
|
2937
|
-
status: 'investigating',
|
|
2938
|
-
auto_fix_status: 'failed',
|
|
2939
|
-
verification_notes: 'Auto optimize enqueue failed: dashboard job id missing.',
|
|
2940
|
-
last_triaged_by: 'auto-slow-query',
|
|
2941
|
-
last_triaged_at: new Date()
|
|
2942
|
-
}
|
|
2943
|
-
});
|
|
2944
|
-
return;
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
const attemptStartedAt = new Date();
|
|
2948
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
2949
|
-
$inc: {
|
|
2950
|
-
auto_fix_attempt_count: 1
|
|
2951
|
-
},
|
|
2952
|
-
$push: {
|
|
2953
|
-
auto_fix_attempt_history: {
|
|
2954
|
-
$each: [attemptStartedAt],
|
|
2955
|
-
$slice: -100
|
|
2956
|
-
}
|
|
2957
|
-
},
|
|
2958
|
-
$set: {
|
|
2959
|
-
status: 'queued',
|
|
2960
|
-
auto_fix_status: 'running',
|
|
2961
|
-
openai_task_id: jobId,
|
|
2962
|
-
auto_fix_last_attempt_at: attemptStartedAt,
|
|
2963
|
-
auto_fix_disabled_reason: '',
|
|
2964
|
-
verification_notes: `Auto optimize job queued (${jobId}).`,
|
|
2965
|
-
last_triaged_by: 'auto-slow-query',
|
|
2966
|
-
last_triaged_at: new Date()
|
|
2967
|
-
}
|
|
2968
|
-
});
|
|
2969
|
-
const queuedLog = (await SlowQueryLogs.findOne({_id: logId})) || log;
|
|
2970
|
-
await this.notifyCustomerSlowQueryStatus('detected_auto_optimize_enabled', queuedLog);
|
|
2971
|
-
|
|
2972
|
-
try {
|
|
2973
|
-
await this.waitForDashboardJobStop(jobId, this.config.autoOptimizeWaitTimeoutMs);
|
|
2974
|
-
}
|
|
2975
|
-
catch (error) {
|
|
2976
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
2977
|
-
$set: {
|
|
2978
|
-
status: 'investigating',
|
|
2979
|
-
auto_fix_status: 'failed',
|
|
2980
|
-
auto_fix_result: {
|
|
2981
|
-
job_id: jobId,
|
|
2982
|
-
error: error?.message || 'timeout'
|
|
2983
|
-
},
|
|
2984
|
-
verification_notes: `Auto optimize wait failed: ${error?.message || 'timeout'}`,
|
|
2985
|
-
last_triaged_by: 'auto-slow-query',
|
|
2986
|
-
last_triaged_at: new Date()
|
|
2987
|
-
}
|
|
2988
|
-
});
|
|
2989
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize job state check failed');
|
|
2990
|
-
return;
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
let isRunning = false;
|
|
2994
|
-
try {
|
|
2995
|
-
isRunning = await this.isDashboardJobRunning(jobId);
|
|
2996
|
-
}
|
|
2997
|
-
catch (error) {
|
|
2998
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
2999
|
-
$set: {
|
|
3000
|
-
status: 'investigating',
|
|
3001
|
-
auto_fix_status: 'failed',
|
|
3002
|
-
auto_fix_result: {
|
|
3003
|
-
job_id: jobId,
|
|
3004
|
-
error: error?.message || 'unknown'
|
|
3005
|
-
},
|
|
3006
|
-
verification_notes: `Unable to confirm dashboard job state: ${error?.message || 'unknown error'}`,
|
|
3007
|
-
last_triaged_by: 'auto-slow-query',
|
|
3008
|
-
last_triaged_at: new Date()
|
|
3009
|
-
}
|
|
3010
|
-
});
|
|
3011
|
-
return;
|
|
3012
|
-
}
|
|
3013
|
-
|
|
3014
|
-
if (isRunning) {
|
|
3015
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
3016
|
-
$set: {
|
|
3017
|
-
status: 'investigating',
|
|
3018
|
-
auto_fix_status: 'failed',
|
|
3019
|
-
auto_fix_result: {
|
|
3020
|
-
job_id: jobId,
|
|
3021
|
-
error: 'still_running_after_timeout'
|
|
3022
|
-
},
|
|
3023
|
-
verification_notes: `Auto optimize timed out while waiting for dashboard job ${jobId}.`,
|
|
3024
|
-
last_triaged_by: 'auto-slow-query',
|
|
3025
|
-
last_triaged_at: new Date()
|
|
3026
|
-
}
|
|
3027
|
-
});
|
|
3028
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize timed out');
|
|
3029
|
-
return;
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
const finalJob = await AIDashboardJobs.findOne({_id: jobId}) as AIDashboardJob | null;
|
|
3033
|
-
if (!finalJob || finalJob.phase !== 'COMPLETE' || finalJob.paused) {
|
|
3034
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
3035
|
-
$set: {
|
|
3036
|
-
status: 'investigating',
|
|
3037
|
-
auto_fix_status: 'failed',
|
|
3038
|
-
auto_fix_result: {
|
|
3039
|
-
job_id: jobId,
|
|
3040
|
-
job_phase: finalJob?.phase || 'missing',
|
|
3041
|
-
job_paused: !!finalJob?.paused
|
|
3042
|
-
},
|
|
3043
|
-
verification_notes: `Auto optimize job ${jobId} did not complete successfully.`,
|
|
3044
|
-
last_triaged_by: 'auto-slow-query',
|
|
3045
|
-
last_triaged_at: new Date()
|
|
3046
|
-
}
|
|
3047
|
-
});
|
|
3048
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize job did not complete');
|
|
3049
|
-
return;
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
|
-
const publishOutcome = this.evaluateDashboardPublishOutcome(finalJob);
|
|
3053
|
-
if (!publishOutcome.success) {
|
|
3054
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
3055
|
-
$set: {
|
|
3056
|
-
status: 'investigating',
|
|
3057
|
-
auto_fix_status: 'failed',
|
|
3058
|
-
auto_fix_result: {
|
|
3059
|
-
job_id: jobId,
|
|
3060
|
-
publish_message: publishOutcome.message,
|
|
3061
|
-
publish_branch: publishOutcome.branchName || ''
|
|
3062
|
-
},
|
|
3063
|
-
verification_notes: `Auto optimize publish/deploy failed: ${publishOutcome.message}`,
|
|
3064
|
-
last_triaged_by: 'auto-slow-query',
|
|
3065
|
-
last_triaged_at: new Date()
|
|
3066
|
-
}
|
|
3067
|
-
});
|
|
3068
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize publish/deploy failed');
|
|
3069
|
-
return;
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
const refreshedLog = (await SlowQueryLogs.findOne({_id: logId})) || log;
|
|
3073
|
-
let afterExplain: ExplainResult;
|
|
3074
|
-
try {
|
|
3075
|
-
afterExplain = await this.runExplain(refreshedLog);
|
|
3076
|
-
}
|
|
3077
|
-
catch (error) {
|
|
3078
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
3079
|
-
$set: {
|
|
3080
|
-
status: 'investigating',
|
|
3081
|
-
auto_fix_status: 'failed',
|
|
3082
|
-
auto_fix_result: {
|
|
3083
|
-
job_id: jobId,
|
|
3084
|
-
publish_message: publishOutcome.message,
|
|
3085
|
-
publish_branch: publishOutcome.branchName || '',
|
|
3086
|
-
validation_error: error?.message || 'unknown'
|
|
3087
|
-
},
|
|
3088
|
-
verification_notes: `Post-deploy validation failed: ${error?.message || 'unknown error'}`,
|
|
3089
|
-
last_triaged_by: 'auto-slow-query',
|
|
3090
|
-
last_triaged_at: new Date()
|
|
3091
|
-
}
|
|
3092
|
-
});
|
|
3093
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize post-deploy validation failed');
|
|
3094
|
-
return;
|
|
3095
|
-
}
|
|
3096
|
-
|
|
3097
|
-
const afterMetrics = this.resolveExecutionMetrics(
|
|
3098
|
-
afterExplain.explainStats || {},
|
|
3099
|
-
afterExplain.durationMs,
|
|
3100
|
-
afterExplain.stageSummaries || []
|
|
3101
|
-
);
|
|
3102
|
-
|
|
3103
|
-
let outputEquivalence: OutputEquivalenceResult | undefined;
|
|
3104
|
-
if (this.config.autoOptimizeOutputCompareEnabled) {
|
|
3105
|
-
if (!baselineOutputFingerprint) {
|
|
3106
|
-
outputEquivalence = {
|
|
3107
|
-
passed: false,
|
|
3108
|
-
reason: 'Baseline output fingerprint missing.',
|
|
3109
|
-
mode: 'unknown'
|
|
3110
|
-
};
|
|
3111
|
-
}
|
|
3112
|
-
else {
|
|
3113
|
-
try {
|
|
3114
|
-
const afterOutputFingerprint = await this.captureOutputFingerprint(refreshedLog);
|
|
3115
|
-
outputEquivalence = this.compareOutputEquivalence(baselineOutputFingerprint, afterOutputFingerprint);
|
|
3116
|
-
}
|
|
3117
|
-
catch (error) {
|
|
3118
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
3119
|
-
$set: {
|
|
3120
|
-
status: 'investigating',
|
|
3121
|
-
auto_fix_status: 'failed',
|
|
3122
|
-
auto_fix_result: {
|
|
3123
|
-
job_id: jobId,
|
|
3124
|
-
publish_message: publishOutcome.message,
|
|
3125
|
-
publish_branch: publishOutcome.branchName || '',
|
|
3126
|
-
baseline: baselineMetrics,
|
|
3127
|
-
after: afterMetrics,
|
|
3128
|
-
output_equivalence_error: error?.message || 'unknown'
|
|
3129
|
-
},
|
|
3130
|
-
verification_notes: `Post-deploy output comparison failed: ${error?.message || 'unknown error'}`,
|
|
3131
|
-
explain_plan: afterExplain.explainPlan,
|
|
3132
|
-
explain_execution_stats: afterExplain.explainStats,
|
|
3133
|
-
explain_generated_at: new Date(),
|
|
3134
|
-
last_triaged_by: 'auto-slow-query',
|
|
3135
|
-
last_triaged_at: new Date()
|
|
3136
|
-
},
|
|
3137
|
-
$push: {
|
|
3138
|
-
verification_runs: {
|
|
3139
|
-
timestamp: new Date(),
|
|
3140
|
-
duration_ms: afterExplain.durationMs
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
|
-
});
|
|
3144
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize output comparison failed');
|
|
3145
|
-
return;
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
const validation = this.evaluateOptimizationOutcome(baselineMetrics, afterMetrics, outputEquivalence);
|
|
3151
|
-
const autoFixResult = {
|
|
3152
|
-
job_id: jobId,
|
|
3153
|
-
publish_message: publishOutcome.message,
|
|
3154
|
-
publish_branch: publishOutcome.branchName || '',
|
|
3155
|
-
baseline: baselineMetrics,
|
|
3156
|
-
after: afterMetrics,
|
|
3157
|
-
output_equivalence: outputEquivalence,
|
|
3158
|
-
validation
|
|
3159
|
-
};
|
|
3160
|
-
|
|
3161
|
-
if (!validation.passed) {
|
|
3162
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
3163
|
-
$set: {
|
|
3164
|
-
status: 'investigating',
|
|
3165
|
-
auto_fix_status: 'failed',
|
|
3166
|
-
auto_fix_result: autoFixResult,
|
|
3167
|
-
verification_notes: `Auto optimize validation failed: ${validation.reason}`,
|
|
3168
|
-
explain_plan: afterExplain.explainPlan,
|
|
3169
|
-
explain_execution_stats: afterExplain.explainStats,
|
|
3170
|
-
explain_generated_at: new Date(),
|
|
3171
|
-
last_triaged_by: 'auto-slow-query',
|
|
3172
|
-
last_triaged_at: new Date()
|
|
3173
|
-
},
|
|
3174
|
-
$push: {
|
|
3175
|
-
verification_runs: {
|
|
3176
|
-
timestamp: new Date(),
|
|
3177
|
-
duration_ms: afterExplain.durationMs
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
});
|
|
3181
|
-
await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize validation failed');
|
|
3182
|
-
return;
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3185
|
-
await SlowQueryLogs.updateOne({_id: logId}, {
|
|
3186
|
-
$set: {
|
|
3187
|
-
status: 'optimized',
|
|
3188
|
-
auto_fix_status: 'completed',
|
|
3189
|
-
auto_fix_result: autoFixResult,
|
|
3190
|
-
verification_notes: `Auto optimize completed: ${validation.reason}`,
|
|
3191
|
-
explain_plan: afterExplain.explainPlan,
|
|
3192
|
-
explain_execution_stats: afterExplain.explainStats,
|
|
3193
|
-
explain_generated_at: new Date(),
|
|
3194
|
-
last_triaged_by: 'auto-slow-query',
|
|
3195
|
-
last_triaged_at: new Date()
|
|
3196
|
-
},
|
|
3197
|
-
$push: {
|
|
3198
|
-
verification_runs: {
|
|
3199
|
-
timestamp: new Date(),
|
|
3200
|
-
duration_ms: afterExplain.durationMs
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
});
|
|
3204
|
-
const optimizedLog = (await SlowQueryLogs.findOne({_id: logId})) || log;
|
|
3205
|
-
await this.notifyCustomerSlowQueryStatus('completed_success', optimizedLog, {notes: validation.reason});
|
|
3206
|
-
}
|
|
3207
|
-
|
|
3208
|
-
private async resolveClientDB(log: SlowQueryLogModel): Promise<ClientDBModel | undefined> {
|
|
3209
|
-
const candidates = [
|
|
3210
|
-
log.client_slug,
|
|
3211
|
-
log.client_name,
|
|
3212
|
-
log.source_app
|
|
3213
|
-
].filter(Boolean);
|
|
3214
|
-
|
|
3215
|
-
if (!candidates.length) {
|
|
3216
|
-
return undefined;
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
const matches = await ClientDBs.find({
|
|
3220
|
-
$or: [
|
|
3221
|
-
{ client: { $in: candidates } },
|
|
3222
|
-
{ name: { $in: candidates } }
|
|
3223
|
-
]
|
|
3224
|
-
}, {
|
|
3225
|
-
limit: 10
|
|
3226
|
-
});
|
|
3227
|
-
|
|
3228
|
-
if (!Array.isArray(matches) || !matches.length) {
|
|
3229
|
-
return undefined;
|
|
3230
|
-
}
|
|
3231
|
-
|
|
3232
|
-
const prodMatch = matches.find(match => match && match.dev_server === false);
|
|
3233
|
-
if (prodMatch) {
|
|
3234
|
-
return prodMatch;
|
|
3235
|
-
}
|
|
3236
|
-
|
|
3237
|
-
const devMatch = matches.find(match => match && match.dev_server === true);
|
|
3238
|
-
if (devMatch) {
|
|
3239
|
-
return devMatch;
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
|
-
return matches[0];
|
|
3243
|
-
}
|
|
3244
|
-
|
|
3245
|
-
private async resolveExplainTarget(log: SlowQueryLogModel): Promise<ExplainTarget> {
|
|
3246
|
-
const clientDB = await this.resolveClientDB(log);
|
|
3247
|
-
|
|
3248
|
-
if (clientDB) {
|
|
3249
|
-
const dbName = clientDB.database || clientDB.name;
|
|
3250
|
-
if (!dbName) {
|
|
3251
|
-
throw new SlowQueryVerifierError('client_db_missing_database', 'Client DB missing database name.');
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
return {
|
|
3255
|
-
type: 'client',
|
|
3256
|
-
dbName,
|
|
3257
|
-
uri: clientDB.uri
|
|
3258
|
-
};
|
|
3259
|
-
}
|
|
3260
|
-
|
|
3261
|
-
if (!this.config.fallbackToMainDB) {
|
|
3262
|
-
throw new SlowQueryVerifierError('client_db_not_found', 'Could not resolve client DB for slow query verification.');
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
|
-
const mainDb = ResolveIOServer.getMainDB();
|
|
3266
|
-
const mainDbName = mainDb?.databaseName;
|
|
3267
|
-
|
|
3268
|
-
if (!mainDb || !mainDbName) {
|
|
3269
|
-
throw new SlowQueryVerifierError('main_db_unavailable', 'Main server DB is not available.');
|
|
3270
|
-
}
|
|
3271
|
-
|
|
3272
|
-
return {
|
|
3273
|
-
type: 'main',
|
|
3274
|
-
dbName: mainDbName
|
|
3275
|
-
};
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
|
-
private static extractPipelineFromLog(log: SlowQueryLogModel): any[] | undefined {
|
|
3279
|
-
if (!log) {
|
|
3280
|
-
return undefined;
|
|
3281
|
-
}
|
|
3282
|
-
|
|
3283
|
-
if (Array.isArray(log.pipeline)) {
|
|
3284
|
-
return log.pipeline;
|
|
3285
|
-
}
|
|
3286
|
-
|
|
3287
|
-
if (log.filter && Array.isArray((log.filter as any).pipeline)) {
|
|
3288
|
-
return (log.filter as any).pipeline;
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
return SlowQueryVerifier.extractPipelineOptions(log.options);
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
private static applyQueryOverrides(log: SlowQueryLogModel, overrides?: SlowQueryExecutionOverride): SlowQueryLogModel {
|
|
3295
|
-
if (!overrides) {
|
|
3296
|
-
return log;
|
|
3297
|
-
}
|
|
3298
|
-
|
|
3299
|
-
const merged: SlowQueryLogModel = {...log};
|
|
3300
|
-
|
|
3301
|
-
if (overrides.filter !== undefined) {
|
|
3302
|
-
merged.filter = overrides.filter;
|
|
3303
|
-
}
|
|
3304
|
-
|
|
3305
|
-
if (overrides.options !== undefined) {
|
|
3306
|
-
merged.options = overrides.options;
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
if (overrides.pipeline !== undefined) {
|
|
3310
|
-
merged.pipeline = overrides.pipeline;
|
|
3311
|
-
}
|
|
3312
|
-
|
|
3313
|
-
return merged;
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
private static extractPipelineOptions(options?: Record<string, any>): any[] | undefined {
|
|
3317
|
-
if (!options) {
|
|
3318
|
-
return undefined;
|
|
3319
|
-
}
|
|
3320
|
-
|
|
3321
|
-
if (Array.isArray(options)) {
|
|
3322
|
-
return options;
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
if (Array.isArray(options.pipeline)) {
|
|
3326
|
-
return options.pipeline;
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
return undefined;
|
|
3330
|
-
}
|
|
3331
|
-
|
|
3332
|
-
private static extractFindOptions(options?: Record<string, any>): Record<string, any> | undefined {
|
|
3333
|
-
if (!options || Array.isArray(options)) {
|
|
3334
|
-
return undefined;
|
|
3335
|
-
}
|
|
3336
|
-
|
|
3337
|
-
const normalized: Record<string, any> = {...options};
|
|
3338
|
-
if (Array.isArray(normalized.pipeline)) {
|
|
3339
|
-
delete normalized.pipeline;
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
if (!Object.keys(normalized).length) {
|
|
3343
|
-
return undefined;
|
|
3344
|
-
}
|
|
3345
|
-
|
|
3346
|
-
return normalized;
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
|
-
private static resolveDurationMs(explainResponse: Record<string, any>): number {
|
|
3350
|
-
const stats = explainResponse?.executionStats;
|
|
3351
|
-
if (!stats) {
|
|
3352
|
-
const stages = explainResponse?.stages;
|
|
3353
|
-
if (Array.isArray(stages)) {
|
|
3354
|
-
for (const stage of stages) {
|
|
3355
|
-
const stageStats = stage?.$cursor?.executionStats;
|
|
3356
|
-
const candidates = [
|
|
3357
|
-
stageStats?.executionTimeMillis,
|
|
3358
|
-
stageStats?.executionTimeMillisEstimate,
|
|
3359
|
-
stageStats?.executionStages?.executionTimeMillis,
|
|
3360
|
-
stageStats?.executionStages?.executionTimeMillisEstimate
|
|
3361
|
-
];
|
|
3362
|
-
|
|
3363
|
-
for (const candidate of candidates) {
|
|
3364
|
-
if (SlowQueryVerifier.isValidDuration(candidate)) {
|
|
3365
|
-
return candidate;
|
|
3366
|
-
}
|
|
3367
|
-
}
|
|
3368
|
-
}
|
|
3369
|
-
}
|
|
3370
|
-
|
|
3371
|
-
return -1;
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
const candidates = [
|
|
3375
|
-
stats.executionTimeMillis,
|
|
3376
|
-
stats.executionTimeMillisEstimate,
|
|
3377
|
-
stats.executionStages?.executionTimeMillis,
|
|
3378
|
-
stats.executionStages?.executionTimeMillisEstimate
|
|
3379
|
-
];
|
|
3380
|
-
|
|
3381
|
-
for (const candidate of candidates) {
|
|
3382
|
-
if (SlowQueryVerifier.isValidDuration(candidate)) {
|
|
3383
|
-
return candidate;
|
|
3384
|
-
}
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
return -1;
|
|
3388
|
-
}
|
|
3389
|
-
|
|
3390
|
-
private static pipelineHasWriteStage(pipeline: any[]): boolean {
|
|
3391
|
-
if (!Array.isArray(pipeline)) {
|
|
3392
|
-
return false;
|
|
3393
|
-
}
|
|
3394
|
-
|
|
3395
|
-
return pipeline.some(stage => {
|
|
3396
|
-
if (!stage || typeof stage !== 'object') {
|
|
3397
|
-
return false;
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
|
-
return typeof (stage as any).$out !== 'undefined' || typeof (stage as any).$merge !== 'undefined';
|
|
3401
|
-
});
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
private static extractAggregateOptions(options?: Record<string, any>): Record<string, any> | undefined {
|
|
3405
|
-
if (!options || Array.isArray(options) || typeof options !== 'object') {
|
|
3406
|
-
return undefined;
|
|
3407
|
-
}
|
|
3408
|
-
|
|
3409
|
-
const allowedKeys = [
|
|
3410
|
-
'allowDiskUse',
|
|
3411
|
-
'bypassDocumentValidation',
|
|
3412
|
-
'collation',
|
|
3413
|
-
'comment',
|
|
3414
|
-
'hint',
|
|
3415
|
-
'let',
|
|
3416
|
-
'maxTimeMS',
|
|
3417
|
-
'maxAwaitTimeMS'
|
|
3418
|
-
];
|
|
3419
|
-
|
|
3420
|
-
const result: Record<string, any> = {};
|
|
3421
|
-
allowedKeys.forEach(key => {
|
|
3422
|
-
if (typeof (options as any)[key] !== 'undefined') {
|
|
3423
|
-
result[key] = (options as any)[key];
|
|
3424
|
-
}
|
|
3425
|
-
});
|
|
3426
|
-
|
|
3427
|
-
return Object.keys(result).length ? result : undefined;
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
private static buildAggregateExplainCommand(
|
|
3431
|
-
collectionName: string,
|
|
3432
|
-
pipeline: any[],
|
|
3433
|
-
options?: Record<string, any>,
|
|
3434
|
-
verbosity: ExplainVerbosity = 'executionStats'
|
|
3435
|
-
): Record<string, any> {
|
|
3436
|
-
const aggregateCommand: Record<string, any> = {
|
|
3437
|
-
aggregate: collectionName,
|
|
3438
|
-
pipeline,
|
|
3439
|
-
cursor: {}
|
|
3440
|
-
};
|
|
3441
|
-
|
|
3442
|
-
if (options && typeof options === 'object') {
|
|
3443
|
-
Object.keys(options).forEach(key => {
|
|
3444
|
-
if (typeof (options as any)[key] !== 'undefined') {
|
|
3445
|
-
aggregateCommand[key] = (options as any)[key];
|
|
3446
|
-
}
|
|
3447
|
-
});
|
|
3448
|
-
}
|
|
3449
|
-
|
|
3450
|
-
return {
|
|
3451
|
-
explain: aggregateCommand,
|
|
3452
|
-
verbosity
|
|
3453
|
-
};
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3456
|
-
private static isAggregateExplainWriteConcernError(err: any): boolean {
|
|
3457
|
-
const message = `${err?.message || ''}`;
|
|
3458
|
-
return message.includes('Option "explain" cannot be used on an aggregate call with writeConcern');
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3461
|
-
private static isBsonObjectTooLargeError(err: any): boolean {
|
|
3462
|
-
const message = `${err?.message || ''}`;
|
|
3463
|
-
return message.includes('BSONObj size:') && message.includes('is invalid');
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
|
-
private static isValidDuration(value: any): value is number {
|
|
3467
|
-
return typeof value === 'number' && !Number.isNaN(value) && value >= 0;
|
|
3468
|
-
}
|
|
3469
|
-
|
|
3470
|
-
private static buildFindCursor(collection: any, filter: Record<string, any>, findOptions?: Record<string, any>) {
|
|
3471
|
-
const cursorOptions: Record<string, any> = {};
|
|
3472
|
-
|
|
3473
|
-
if (findOptions) {
|
|
3474
|
-
Object.keys(findOptions).forEach(key => {
|
|
3475
|
-
if (['sort', 'skip', 'limit', 'projection', 'fields'].includes(key)) {
|
|
3476
|
-
return;
|
|
3477
|
-
}
|
|
3478
|
-
|
|
3479
|
-
cursorOptions[key] = findOptions[key];
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3482
|
-
|
|
3483
|
-
cursorOptions.readPreference = 'secondaryPreferred';
|
|
3484
|
-
|
|
3485
|
-
let cursor = collection.find(filter || {}, cursorOptions);
|
|
3486
|
-
|
|
3487
|
-
const projection = findOptions?.projection ?? findOptions?.fields;
|
|
3488
|
-
if (projection && typeof projection === 'object') {
|
|
3489
|
-
cursor = cursor.project(projection);
|
|
3490
|
-
}
|
|
3491
|
-
|
|
3492
|
-
const sort = findOptions?.sort;
|
|
3493
|
-
if (sort && typeof sort === 'object') {
|
|
3494
|
-
cursor = cursor.sort(sort);
|
|
3495
|
-
}
|
|
3496
|
-
|
|
3497
|
-
const skip = findOptions?.skip;
|
|
3498
|
-
if (typeof skip === 'number') {
|
|
3499
|
-
cursor = cursor.skip(skip);
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3502
|
-
const limit = findOptions?.limit;
|
|
3503
|
-
if (typeof limit === 'number') {
|
|
3504
|
-
cursor = cursor.limit(limit);
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
return cursor;
|
|
3508
|
-
}
|
|
3509
|
-
|
|
3510
|
-
private static async measureExecution(
|
|
3511
|
-
db: any,
|
|
3512
|
-
collectionName: string,
|
|
3513
|
-
pipeline?: any[],
|
|
3514
|
-
filter?: Record<string, any>,
|
|
3515
|
-
findOptions?: Record<string, any>,
|
|
3516
|
-
aggregateOptions?: Record<string, any>
|
|
3517
|
-
): Promise<number> {
|
|
3518
|
-
const start = Date.now();
|
|
3519
|
-
|
|
3520
|
-
try {
|
|
3521
|
-
if (Array.isArray(pipeline)) {
|
|
3522
|
-
await db.collection(collectionName)
|
|
3523
|
-
.aggregate(pipeline, {
|
|
3524
|
-
...(aggregateOptions || {}),
|
|
3525
|
-
readPreference: 'secondaryPreferred'
|
|
3526
|
-
})
|
|
3527
|
-
.toArray();
|
|
3528
|
-
}
|
|
3529
|
-
else {
|
|
3530
|
-
const cursor = SlowQueryVerifier.buildFindCursor(
|
|
3531
|
-
db.collection(collectionName),
|
|
3532
|
-
filter ?? {},
|
|
3533
|
-
findOptions
|
|
3534
|
-
);
|
|
3535
|
-
await cursor.toArray();
|
|
3536
|
-
}
|
|
3537
|
-
}
|
|
3538
|
-
catch (err) {
|
|
3539
|
-
if (!SlowQueryVerifier.isBsonObjectTooLargeError(err)) {
|
|
3540
|
-
console.error('Slow query measurement execution failed for', collectionName, err);
|
|
3541
|
-
}
|
|
3542
|
-
return -1;
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
const duration = Date.now() - start;
|
|
3546
|
-
return SlowQueryVerifier.isValidDuration(duration) ? duration : -1;
|
|
3547
|
-
}
|
|
3548
|
-
|
|
3549
|
-
private static normalizeExplainPayload(input?: any): Record<string, any> {
|
|
3550
|
-
const cloned = SlowQueryVerifier.deepClone(input ?? {});
|
|
3551
|
-
return typeof cloned === 'object' && cloned !== null ? cloned : {};
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
private static sanitizeExplainPayload(payload: Record<string, any>, maxBytes = 2 * 1024 * 1024): Record<string, any> {
|
|
3555
|
-
try {
|
|
3556
|
-
const json = JSON.stringify(payload);
|
|
3557
|
-
const bytes = Buffer.byteLength(json, 'utf8');
|
|
3558
|
-
return bytes <= maxBytes ? payload : {};
|
|
3559
|
-
}
|
|
3560
|
-
catch {
|
|
3561
|
-
return {};
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
|
|
3565
|
-
private static deepClone(value: any): any {
|
|
3566
|
-
if (value === null || typeof value !== 'object') {
|
|
3567
|
-
return value;
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
if (Array.isArray(value)) {
|
|
3571
|
-
return value.map(item => SlowQueryVerifier.deepClone(item));
|
|
3572
|
-
}
|
|
3573
|
-
|
|
3574
|
-
if (typeof value.toJSON === 'function') {
|
|
3575
|
-
try {
|
|
3576
|
-
return SlowQueryVerifier.deepClone(value.toJSON());
|
|
3577
|
-
}
|
|
3578
|
-
catch {
|
|
3579
|
-
// fall through to manual clone
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
|
|
3583
|
-
const result: Record<string, any> = {};
|
|
3584
|
-
Object.keys(value).forEach(key => {
|
|
3585
|
-
result[key] = SlowQueryVerifier.deepClone(value[key]);
|
|
3586
|
-
});
|
|
3587
|
-
|
|
3588
|
-
return result;
|
|
3589
|
-
}
|
|
3590
|
-
}
|