@synapta/skills 0.1.1 → 0.2.0
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/dist/index.js +11 -4
- package/package.json +3 -4
- package/skills/ATTRIBUTION.md +80 -0
- package/skills/accessibility-audit/SKILL.md +325 -0
- package/skills/accessibility-audit/reference/wcag-checklist.md +103 -0
- package/skills/apns-notifier/SKILL.md +86 -0
- package/skills/approval-policy-enforcer/SKILL.md +66 -0
- package/skills/apps-sdk-builder/LICENSE.txt +201 -0
- package/skills/apps-sdk-builder/SKILL.md +328 -0
- package/skills/apps-sdk-builder/agents/openai.yaml +13 -0
- package/skills/apps-sdk-builder/references/app-archetypes.md +132 -0
- package/skills/apps-sdk-builder/references/apps-sdk-docs-workflow.md +135 -0
- package/skills/apps-sdk-builder/references/interactive-state-sync-patterns.md +113 -0
- package/skills/apps-sdk-builder/references/repo-contract-and-validation.md +93 -0
- package/skills/apps-sdk-builder/references/search-fetch-standard.md +67 -0
- package/skills/apps-sdk-builder/references/upstream-example-workflow.md +79 -0
- package/skills/apps-sdk-builder/references/window-openai-patterns.md +79 -0
- package/skills/apps-sdk-builder/scripts/scaffold_node_ext_apps.mjs +606 -0
- package/skills/architecture-selector/SKILL.md +64 -0
- package/skills/backlog-planner/SKILL.md +68 -0
- package/skills/carplay-entitlement-checker/SKILL.md +82 -0
- package/skills/concept-deepener/SKILL.md +86 -0
- package/skills/concept-discovery/SKILL.md +517 -0
- package/skills/concept-discovery/assets/sample-analysis.json +81 -0
- package/skills/concept-discovery/expected_outputs/sample-enum-dictionary.md +25 -0
- package/skills/concept-discovery/expected_outputs/sample-page-user-list.md +83 -0
- package/skills/concept-discovery/expected_outputs/sample-prd-readme.md +43 -0
- package/skills/concept-discovery/references/framework-patterns.md +228 -0
- package/skills/concept-discovery/references/prd-quality-checklist.md +65 -0
- package/skills/concept-discovery/scripts/codebase_analyzer.py +732 -0
- package/skills/concept-discovery/scripts/prd_scaffolder.py +435 -0
- package/skills/dast-zap/SKILL.md +453 -0
- package/skills/dast-zap/assets/.gitkeep +9 -0
- package/skills/dast-zap/assets/github_action.yml +207 -0
- package/skills/dast-zap/assets/gitlab_ci.yml +226 -0
- package/skills/dast-zap/assets/zap_automation.yaml +196 -0
- package/skills/dast-zap/assets/zap_context.xml +192 -0
- package/skills/dast-zap/references/EXAMPLE.md +40 -0
- package/skills/dast-zap/references/api_testing_guide.md +475 -0
- package/skills/dast-zap/references/authentication_guide.md +431 -0
- package/skills/dast-zap/references/false_positive_handling.md +427 -0
- package/skills/dast-zap/references/owasp_mapping.md +255 -0
- package/skills/dep-sbom-scan/SKILL.md +466 -0
- package/skills/deploy-cloudflare/SKILL.md +930 -0
- package/skills/deploy-docker/SKILL.md +55 -0
- package/skills/deploy-fly/SKILL.md +228 -0
- package/skills/deploy-k8s/SKILL.md +108 -0
- package/skills/deploy-k8s/assets/logo.png +0 -0
- package/skills/deploy-k8s/docs/README.md +29 -0
- package/skills/deploy-k8s/docs/SUMMARY.md +56 -0
- package/skills/deploy-k8s/docs/advanced/token-efficiency.md +61 -0
- package/skills/deploy-k8s/docs/architecture/multi-tenancy.md +96 -0
- package/skills/deploy-k8s/docs/architecture/storage-and-state.md +102 -0
- package/skills/deploy-k8s/docs/architecture/workload-patterns.md +87 -0
- package/skills/deploy-k8s/docs/book.json +16 -0
- package/skills/deploy-k8s/docs/community/changelog.md +34 -0
- package/skills/deploy-k8s/docs/community/contributing.md +67 -0
- package/skills/deploy-k8s/docs/core-concepts/failure-modes.md +153 -0
- package/skills/deploy-k8s/docs/core-concepts/philosophy.md +83 -0
- package/skills/deploy-k8s/docs/core-concepts/workflow.md +124 -0
- package/skills/deploy-k8s/docs/examples/bad-patterns.md +47 -0
- package/skills/deploy-k8s/docs/examples/do-dont-checklist.md +37 -0
- package/skills/deploy-k8s/docs/examples/good-patterns.md +49 -0
- package/skills/deploy-k8s/docs/failure-modes/api-drift.md +104 -0
- package/skills/deploy-k8s/docs/failure-modes/fragile-rollouts.md +99 -0
- package/skills/deploy-k8s/docs/failure-modes/insecure-workload-defaults.md +80 -0
- package/skills/deploy-k8s/docs/failure-modes/network-exposure.md +98 -0
- package/skills/deploy-k8s/docs/failure-modes/privilege-sprawl.md +91 -0
- package/skills/deploy-k8s/docs/failure-modes/resource-starvation.md +85 -0
- package/skills/deploy-k8s/docs/getting-started/installation.md +152 -0
- package/skills/deploy-k8s/docs/getting-started/quick-start.md +115 -0
- package/skills/deploy-k8s/docs/guides/helm-patterns.md +71 -0
- package/skills/deploy-k8s/docs/guides/kustomize-patterns.md +65 -0
- package/skills/deploy-k8s/docs/guides/observability.md +67 -0
- package/skills/deploy-k8s/docs/guides/security-hardening.md +59 -0
- package/skills/deploy-k8s/docs/guides/validation-and-policy.md +66 -0
- package/skills/deploy-k8s/docs/integrations/mcp-integration.md +52 -0
- package/skills/deploy-k8s/docs/package-lock.json +2892 -0
- package/skills/deploy-k8s/docs/package.json +13 -0
- package/skills/deploy-k8s/references/api-drift.md +298 -0
- package/skills/deploy-k8s/references/conditional/aks-patterns.md +70 -0
- package/skills/deploy-k8s/references/conditional/eks-patterns.md +79 -0
- package/skills/deploy-k8s/references/conditional/gitops-controllers.md +71 -0
- package/skills/deploy-k8s/references/conditional/gke-patterns.md +74 -0
- package/skills/deploy-k8s/references/conditional/observability-stacks.md +80 -0
- package/skills/deploy-k8s/references/conditional/openshift-patterns.md +67 -0
- package/skills/deploy-k8s/references/daemonset-operator-patterns.md +155 -0
- package/skills/deploy-k8s/references/deployment-patterns.md +146 -0
- package/skills/deploy-k8s/references/do-dont-patterns.md +87 -0
- package/skills/deploy-k8s/references/examples-bad.md +282 -0
- package/skills/deploy-k8s/references/examples-good.md +440 -0
- package/skills/deploy-k8s/references/fragile-rollouts.md +303 -0
- package/skills/deploy-k8s/references/helm-patterns.md +203 -0
- package/skills/deploy-k8s/references/insecure-workload-defaults.md +300 -0
- package/skills/deploy-k8s/references/job-patterns.md +120 -0
- package/skills/deploy-k8s/references/kustomize-patterns.md +239 -0
- package/skills/deploy-k8s/references/multi-tenancy.md +343 -0
- package/skills/deploy-k8s/references/network-exposure.md +481 -0
- package/skills/deploy-k8s/references/observability.md +302 -0
- package/skills/deploy-k8s/references/privilege-sprawl.md +273 -0
- package/skills/deploy-k8s/references/resource-starvation.md +374 -0
- package/skills/deploy-k8s/references/security-hardening.md +209 -0
- package/skills/deploy-k8s/references/stateful-patterns.md +130 -0
- package/skills/deploy-k8s/references/storage-and-state.md +330 -0
- package/skills/deploy-k8s/references/validation-and-policy.md +242 -0
- package/skills/deploy-railway/SKILL.md +235 -0
- package/skills/deploy-railway/references/analyze-db-mongo.md +84 -0
- package/skills/deploy-railway/references/analyze-db-mysql.md +254 -0
- package/skills/deploy-railway/references/analyze-db-postgres.md +479 -0
- package/skills/deploy-railway/references/analyze-db-redis.md +208 -0
- package/skills/deploy-railway/references/analyze-db.md +344 -0
- package/skills/deploy-railway/references/configure.md +309 -0
- package/skills/deploy-railway/references/deploy.md +195 -0
- package/skills/deploy-railway/references/operate.md +214 -0
- package/skills/deploy-railway/references/request.md +248 -0
- package/skills/deploy-railway/references/setup.md +312 -0
- package/skills/deploy-railway/scripts/analyze-mongo.py +1549 -0
- package/skills/deploy-railway/scripts/analyze-mysql.py +1195 -0
- package/skills/deploy-railway/scripts/analyze-postgres.py +3058 -0
- package/skills/deploy-railway/scripts/analyze-redis.py +1090 -0
- package/skills/deploy-railway/scripts/dal.py +671 -0
- package/skills/deploy-railway/scripts/enable-pg-stats.py +170 -0
- package/skills/deploy-railway/scripts/pg-extensions.py +370 -0
- package/skills/deploy-railway/scripts/railway-api.sh +52 -0
- package/skills/deploy-ssh/SKILL.md +91 -0
- package/skills/deploy-vercel/SKILL.md +304 -0
- package/skills/deploy-vercel/resources/deploy-codex.sh +301 -0
- package/skills/deploy-vercel/resources/deploy.sh +301 -0
- package/skills/docs-runbooks/SKILL.md +399 -0
- package/skills/drive-status-renderer/SKILL.md +62 -0
- package/skills/iac-scan/SKILL.md +680 -0
- package/skills/iac-scan/assets/.gitkeep +9 -0
- package/skills/iac-scan/assets/checkov_config.yaml +94 -0
- package/skills/iac-scan/assets/github_actions.yml +199 -0
- package/skills/iac-scan/assets/gitlab_ci.yml +218 -0
- package/skills/iac-scan/assets/pre_commit_config.yaml +92 -0
- package/skills/iac-scan/references/EXAMPLE.md +40 -0
- package/skills/iac-scan/references/compliance_mapping.md +237 -0
- package/skills/iac-scan/references/custom_policies.md +460 -0
- package/skills/iac-scan/references/suppression_guide.md +431 -0
- package/skills/incident-briefing/SKILL.md +66 -0
- package/skills/incident-triage/SKILL.md +481 -0
- package/{LICENSE → skills/mcp-builder/LICENSE.txt} +15 -14
- package/skills/mcp-builder/SKILL.md +244 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/mobile-pairing/SKILL.md +52 -0
- package/skills/ops-sre/SKILL.md +297 -0
- package/skills/playwright-qa/LICENSE.txt +201 -0
- package/skills/playwright-qa/NOTICE.txt +14 -0
- package/skills/playwright-qa/SKILL.md +156 -0
- package/skills/playwright-qa/agents/openai.yaml +6 -0
- package/skills/playwright-qa/assets/playwright-small.svg +3 -0
- package/skills/playwright-qa/assets/playwright.png +0 -0
- package/skills/playwright-qa/references/cli.md +116 -0
- package/skills/playwright-qa/references/workflows.md +95 -0
- package/skills/playwright-qa/scripts/playwright_cli.sh +25 -0
- package/skills/release-publish/SKILL.md +85 -0
- package/skills/repo-bootstrap/SKILL.md +92 -0
- package/skills/repo-bootstrap/assets/example-workflows/validate-agents.yml +89 -0
- package/skills/repo-bootstrap/assets/root-thin.md +141 -0
- package/skills/repo-bootstrap/assets/root-verbose.md +149 -0
- package/skills/repo-bootstrap/assets/scoped/backend-go.md +107 -0
- package/skills/repo-bootstrap/assets/scoped/backend-php.md +94 -0
- package/skills/repo-bootstrap/assets/scoped/backend-python.md +84 -0
- package/skills/repo-bootstrap/assets/scoped/backend-typescript.md +89 -0
- package/skills/repo-bootstrap/assets/scoped/claude-code-skill.md +101 -0
- package/skills/repo-bootstrap/assets/scoped/cli.md +83 -0
- package/skills/repo-bootstrap/assets/scoped/concourse.md +196 -0
- package/skills/repo-bootstrap/assets/scoped/ddev.md +68 -0
- package/skills/repo-bootstrap/assets/scoped/docker.md +160 -0
- package/skills/repo-bootstrap/assets/scoped/documentation.md +98 -0
- package/skills/repo-bootstrap/assets/scoped/examples.md +96 -0
- package/skills/repo-bootstrap/assets/scoped/frontend-typescript.md +88 -0
- package/skills/repo-bootstrap/assets/scoped/github-actions.md +174 -0
- package/skills/repo-bootstrap/assets/scoped/gitlab-ci.md +174 -0
- package/skills/repo-bootstrap/assets/scoped/oro-bundle.md +209 -0
- package/skills/repo-bootstrap/assets/scoped/oro-project.md +170 -0
- package/skills/repo-bootstrap/assets/scoped/python-modern.md +170 -0
- package/skills/repo-bootstrap/assets/scoped/resources.md +96 -0
- package/skills/repo-bootstrap/assets/scoped/skill-repo.md +139 -0
- package/skills/repo-bootstrap/assets/scoped/symfony.md +168 -0
- package/skills/repo-bootstrap/assets/scoped/testing.md +87 -0
- package/skills/repo-bootstrap/assets/scoped/typo3-docs.md +103 -0
- package/skills/repo-bootstrap/assets/scoped/typo3-extension.md +133 -0
- package/skills/repo-bootstrap/assets/scoped/typo3-project.md +137 -0
- package/skills/repo-bootstrap/assets/scoped/typo3-testing.md +80 -0
- package/skills/repo-bootstrap/checkpoints.yaml +279 -0
- package/skills/repo-bootstrap/evals/evals.json +385 -0
- package/skills/repo-bootstrap/references/ai-contribution-guidelines.md +63 -0
- package/skills/repo-bootstrap/references/ai-tool-compatibility.md +223 -0
- package/skills/repo-bootstrap/references/directory-coverage.md +82 -0
- package/skills/repo-bootstrap/references/examples/coding-agent-cli/AGENTS.md +70 -0
- package/skills/repo-bootstrap/references/examples/coding-agent-cli/go.mod +3 -0
- package/skills/repo-bootstrap/references/examples/coding-agent-cli/scripts-AGENTS.md +389 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/.env.example +13 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/AGENTS.md +91 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/package.json +33 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/pnpm-lock.yaml +3 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/AGENTS.md +91 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/config.ts +28 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/controllers/userController.ts +74 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/index.ts +26 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/errorHandler.ts +45 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/requestLogger.ts +18 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/health.ts +18 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/users.ts +13 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/errors.ts +40 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/logger.ts +14 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/tsconfig.json +24 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/.env.example +19 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/AGENTS.md +92 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/pyproject.toml +88 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/AGENTS.md +85 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/__init__.py +3 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/config.py +49 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/main.py +66 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/__init__.py +13 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/item.py +43 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/user.py +40 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/__init__.py +5 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/health.py +20 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/items.py +61 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/users.py +55 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/__init__.py +6 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/item_service.py +77 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/user_service.py +69 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/uv.lock +4 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/.scopes +3 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/AGENTS.md +86 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/package.json +20 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/src/App.tsx +5 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/cmd/api/main.go +7 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/go.mod +2 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/main.go +7 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/.scopes +3 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/AGENTS.md +89 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/go.mod +2 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/AGENTS.md +90 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/package.json +17 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/App.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Button.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Footer.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Header.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Sidebar.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/main.go +7 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package-lock.json +0 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package.json +12 -0
- package/skills/repo-bootstrap/references/examples/ldap-selfservice/AGENTS.md +70 -0
- package/skills/repo-bootstrap/references/examples/ldap-selfservice/go.mod +3 -0
- package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-AGENTS.md +371 -0
- package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-web-AGENTS.md +448 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/.scopes +3 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/AGENTS.md +91 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/composer.json +8 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/package.json +15 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/pnpm-lock.yaml +0 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/src/Controller.php +3 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/AGENTS.md +92 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/package.json +26 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/App.tsx +3 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Button.tsx +10 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Footer.tsx +9 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Header.tsx +9 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/main.tsx +3 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/tsconfig.json +13 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/AGENTS.md +75 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/package.json +7 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/package.json +11 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/src/index.ts +11 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-lock.yaml +42 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-workspace.yaml +2 -0
- package/skills/repo-bootstrap/references/examples/simple-ldap-go/AGENTS.md +70 -0
- package/skills/repo-bootstrap/references/examples/simple-ldap-go/examples-AGENTS.md +45 -0
- package/skills/repo-bootstrap/references/examples/simple-ldap-go/go.mod +3 -0
- package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/AGENTS.md +70 -0
- package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/Classes-AGENTS.md +392 -0
- package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/composer.json +8 -0
- package/skills/repo-bootstrap/references/feedback-memory-schema.md +135 -0
- package/skills/repo-bootstrap/references/git-hooks-setup.md +79 -0
- package/skills/repo-bootstrap/references/output-structure.md +124 -0
- package/skills/repo-bootstrap/references/scripts-guide.md +175 -0
- package/skills/repo-bootstrap/references/verification-guide.md +137 -0
- package/skills/repo-bootstrap/scripts/analyze-git-history.sh +315 -0
- package/skills/repo-bootstrap/scripts/check-freshness.sh +230 -0
- package/skills/repo-bootstrap/scripts/detect-golden-samples.sh +161 -0
- package/skills/repo-bootstrap/scripts/detect-heuristics.sh +93 -0
- package/skills/repo-bootstrap/scripts/detect-project.sh +486 -0
- package/skills/repo-bootstrap/scripts/detect-scopes.sh +330 -0
- package/skills/repo-bootstrap/scripts/detect-utilities.sh +133 -0
- package/skills/repo-bootstrap/scripts/extract-adrs.sh +194 -0
- package/skills/repo-bootstrap/scripts/extract-agent-configs.sh +331 -0
- package/skills/repo-bootstrap/scripts/extract-architecture-rules.sh +522 -0
- package/skills/repo-bootstrap/scripts/extract-ci-commands.sh +385 -0
- package/skills/repo-bootstrap/scripts/extract-ci-rules.sh +384 -0
- package/skills/repo-bootstrap/scripts/extract-commands.sh +358 -0
- package/skills/repo-bootstrap/scripts/extract-documentation.sh +308 -0
- package/skills/repo-bootstrap/scripts/extract-github-rulesets.sh +96 -0
- package/skills/repo-bootstrap/scripts/extract-github-settings.sh +88 -0
- package/skills/repo-bootstrap/scripts/extract-ide-settings.sh +228 -0
- package/skills/repo-bootstrap/scripts/extract-platform-files.sh +290 -0
- package/skills/repo-bootstrap/scripts/extract-quality-configs.sh +442 -0
- package/skills/repo-bootstrap/scripts/generate-agents.sh +2424 -0
- package/skills/repo-bootstrap/scripts/generate-file-map.sh +153 -0
- package/skills/repo-bootstrap/scripts/lib/config-root.sh +211 -0
- package/skills/repo-bootstrap/scripts/lib/summary.sh +244 -0
- package/skills/repo-bootstrap/scripts/lib/template.sh +397 -0
- package/skills/repo-bootstrap/scripts/validate-structure.sh +324 -0
- package/skills/repo-bootstrap/scripts/verify-commands.sh +615 -0
- package/skills/repo-bootstrap/scripts/verify-content.sh +302 -0
- package/skills/schema-api-contracts/SKILL.md +56 -0
- package/skills/secret-hygiene/SKILL.md +511 -0
- package/skills/secret-hygiene/assets/.gitkeep +9 -0
- package/skills/secret-hygiene/assets/config-balanced.toml +81 -0
- package/skills/secret-hygiene/assets/config-custom.toml +178 -0
- package/skills/secret-hygiene/assets/config-strict.toml +48 -0
- package/skills/secret-hygiene/assets/github-action.yml +181 -0
- package/skills/secret-hygiene/assets/gitlab-ci.yml +257 -0
- package/skills/secret-hygiene/assets/precommit-config.yaml +70 -0
- package/skills/secret-hygiene/references/EXAMPLE.md +40 -0
- package/skills/secret-hygiene/references/compliance_mapping.md +538 -0
- package/skills/secret-hygiene/references/detection_rules.md +276 -0
- package/skills/secret-hygiene/references/false_positives.md +598 -0
- package/skills/secret-hygiene/references/remediation_guide.md +530 -0
- package/skills/stack-selector/SKILL.md +56 -0
- package/skills/telegram-control/SKILL.md +110 -0
- package/skills/telegram-control/references/architecture.md +184 -0
- package/skills/telegram-control/references/convex.md +173 -0
- package/skills/telegram-control/references/error_handling.md +212 -0
- package/skills/telegram-control/references/initial_setup.md +165 -0
- package/skills/telegram-control/references/telegram_api.md +156 -0
- package/skills/telegram-control/scripts/cancel_message.ts +53 -0
- package/skills/telegram-control/scripts/list_scheduled.ts +103 -0
- package/skills/telegram-control/scripts/logger.ts +121 -0
- package/skills/telegram-control/scripts/proxy-util.ts +11 -0
- package/skills/telegram-control/scripts/schedule_message.ts +216 -0
- package/skills/telegram-control/scripts/send_message.ts +115 -0
- package/skills/telegram-control/scripts/setup.ts +185 -0
- package/skills/telegram-control/scripts/types.ts +75 -0
- package/skills/telegram-control/scripts/view_history.ts +74 -0
- package/skills/test-strategy/SKILL.md +352 -0
- package/skills/threat-model/SKILL.md +303 -0
- package/skills/threat-model/examples/example-output.md +196 -0
- package/skills/threat-model/template.md +96 -0
- package/skills/ts-lint/SKILL.md +80 -0
- package/skills/ui-flow/SKILL.md +668 -0
- package/skills/voice-command-router/SKILL.md +51 -0
- package/skills/widget-live-activity-sync/SKILL.md +66 -0
|
@@ -0,0 +1,1549 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MongoDB analysis for Railway deployments.
|
|
4
|
+
|
|
5
|
+
Produces a comprehensive report covering:
|
|
6
|
+
- Deployment status & overview
|
|
7
|
+
- Connections & operations
|
|
8
|
+
- Latency (opLatencies)
|
|
9
|
+
- Memory & WiredTiger cache
|
|
10
|
+
- Storage & collection stats
|
|
11
|
+
- Replication / oplog
|
|
12
|
+
- Slow queries & active operations
|
|
13
|
+
- Top collections by activity
|
|
14
|
+
- Recommendations
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
analyze-mongo.py --service <name>
|
|
18
|
+
analyze-mongo.py --service <name> --json
|
|
19
|
+
analyze-mongo.py --service <name> --step ssh-test
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
30
|
+
from dataclasses import dataclass, field, asdict
|
|
31
|
+
|
|
32
|
+
import dal
|
|
33
|
+
from dal import (
|
|
34
|
+
LOG_LINES_DEFAULT, ProgressTimer, RailwayContext,
|
|
35
|
+
_init_context, progress, run_railway_command, run_ssh_query,
|
|
36
|
+
get_railway_status, get_deployment_status,
|
|
37
|
+
get_all_metrics_from_api, _analyze_window, _build_metrics_history,
|
|
38
|
+
get_recent_logs,
|
|
39
|
+
_trend_indicator,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Result container
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class MongoAnalysisResult:
|
|
49
|
+
"""Container for MongoDB analysis results."""
|
|
50
|
+
service: str
|
|
51
|
+
db_type: str
|
|
52
|
+
timestamp: str
|
|
53
|
+
deployment_status: str = "UNKNOWN"
|
|
54
|
+
|
|
55
|
+
# Server overview
|
|
56
|
+
version: Optional[str] = None
|
|
57
|
+
storage_engine: Optional[str] = None
|
|
58
|
+
uptime_seconds: Optional[int] = None
|
|
59
|
+
|
|
60
|
+
# Connections
|
|
61
|
+
connections: Optional[Dict[str, Any]] = None
|
|
62
|
+
|
|
63
|
+
# Operations
|
|
64
|
+
opcounters: Optional[Dict[str, Any]] = None
|
|
65
|
+
opcounters_repl: Optional[Dict[str, Any]] = None
|
|
66
|
+
|
|
67
|
+
# Latency
|
|
68
|
+
op_latencies: Optional[Dict[str, Any]] = None
|
|
69
|
+
|
|
70
|
+
# Memory
|
|
71
|
+
memory: Optional[Dict[str, Any]] = None
|
|
72
|
+
page_faults: Optional[int] = None
|
|
73
|
+
|
|
74
|
+
# Network
|
|
75
|
+
network: Optional[Dict[str, Any]] = None
|
|
76
|
+
|
|
77
|
+
# WiredTiger
|
|
78
|
+
wiredtiger_cache: Optional[Dict[str, Any]] = None
|
|
79
|
+
wiredtiger_checkpoint: Optional[Dict[str, Any]] = None
|
|
80
|
+
wiredtiger_tickets: Optional[Dict[str, Any]] = None
|
|
81
|
+
|
|
82
|
+
# Global lock
|
|
83
|
+
global_lock: Optional[Dict[str, Any]] = None
|
|
84
|
+
|
|
85
|
+
# Document metrics
|
|
86
|
+
document_metrics: Optional[Dict[str, Any]] = None
|
|
87
|
+
|
|
88
|
+
# Query efficiency
|
|
89
|
+
query_executor: Optional[Dict[str, Any]] = None
|
|
90
|
+
|
|
91
|
+
# Plan cache (7.0+)
|
|
92
|
+
plan_cache: Optional[Dict[str, Any]] = None
|
|
93
|
+
|
|
94
|
+
# Sort (7.0+)
|
|
95
|
+
sort_metrics: Optional[Dict[str, Any]] = None
|
|
96
|
+
|
|
97
|
+
# Cursors
|
|
98
|
+
cursors: Optional[Dict[str, Any]] = None
|
|
99
|
+
|
|
100
|
+
# TTL
|
|
101
|
+
ttl_metrics: Optional[Dict[str, Any]] = None
|
|
102
|
+
|
|
103
|
+
# Asserts
|
|
104
|
+
asserts: Optional[Dict[str, Any]] = None
|
|
105
|
+
|
|
106
|
+
# Replication
|
|
107
|
+
replication: Optional[Dict[str, Any]] = None
|
|
108
|
+
oplog: Optional[Dict[str, Any]] = None
|
|
109
|
+
|
|
110
|
+
# Storage (db.stats)
|
|
111
|
+
storage: Optional[Dict[str, Any]] = None
|
|
112
|
+
|
|
113
|
+
# Collection stats
|
|
114
|
+
collection_stats: List[Dict[str, Any]] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
# Top collections
|
|
117
|
+
top_collections: Optional[List[Dict[str, Any]]] = None
|
|
118
|
+
|
|
119
|
+
# Slow queries
|
|
120
|
+
slow_queries: List[Dict[str, Any]] = field(default_factory=list)
|
|
121
|
+
|
|
122
|
+
# Active operations
|
|
123
|
+
active_ops: List[Dict[str, Any]] = field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
# Logs
|
|
126
|
+
recent_logs: List[str] = field(default_factory=list)
|
|
127
|
+
recent_errors: List[str] = field(default_factory=list)
|
|
128
|
+
|
|
129
|
+
# Railway metrics (CPU, memory, disk, network trends)
|
|
130
|
+
cpu_memory: Optional[Dict[str, Any]] = None
|
|
131
|
+
disk_usage: Optional[Dict[str, Any]] = None
|
|
132
|
+
metrics_history: Optional[Dict[str, Any]] = None
|
|
133
|
+
|
|
134
|
+
# Status tracking
|
|
135
|
+
collection_status: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
136
|
+
errors: List[str] = field(default_factory=list)
|
|
137
|
+
recommendations: List[Dict[str, str]] = field(default_factory=list)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# MongoDB-specific helpers
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def run_mongosh_query(service: str, js_expr: str, timeout: int = 30) -> Tuple[int, str, str]:
|
|
145
|
+
"""Run a mongosh query via SSH and return (returncode, stdout, stderr).
|
|
146
|
+
|
|
147
|
+
The query is wrapped in EJSON.stringify and executed through mongosh
|
|
148
|
+
connecting to the local MongoDB instance using container env vars.
|
|
149
|
+
"""
|
|
150
|
+
# Escape single quotes in the JS expression for the shell
|
|
151
|
+
escaped = js_expr.replace("'", "'\\''")
|
|
152
|
+
command = (
|
|
153
|
+
f'''bash +H -c 'mongosh "mongodb://$MONGOUSER:$MONGOPASSWORD@localhost:27017" '''
|
|
154
|
+
f'''--quiet --norc --eval "EJSON.stringify({escaped})"' '''
|
|
155
|
+
)
|
|
156
|
+
return run_ssh_query(service, command, timeout)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# MongoDB queries
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
QUERY_SERVER_STATUS = """(function(){ var s = db.serverStatus(); return { connections: s.connections, opcounters: s.opcounters, opcountersRepl: s.opcountersRepl || null, repl: s.repl || null, mem: s.mem, network: s.network, uptime: s.uptime, opLatencies: s.opLatencies, wiredTiger: s.wiredTiger ? { cache: s.wiredTiger.cache, concurrentTransactions: s.wiredTiger.concurrentTransactions, transaction: s.wiredTiger.transaction || null } : null, globalLock: s.globalLock, metrics: s.metrics ? { document: s.metrics.document, queryExecutor: s.metrics.queryExecutor, cursor: s.metrics.cursor, ttl: s.metrics.ttl || null, query: s.metrics.query || null } : null, extra_info: s.extra_info ? { page_faults: s.extra_info.page_faults } : null, version: s.version, storageEngine: s.storageEngine, asserts: s.asserts }; })()"""
|
|
165
|
+
|
|
166
|
+
QUERY_DB_STATS = """db.stats()"""
|
|
167
|
+
|
|
168
|
+
QUERY_CURRENT_OP = """db.currentOp({ active: true })"""
|
|
169
|
+
|
|
170
|
+
QUERY_COLLECTION_STATS = """db.getCollectionNames().map(function(c) { var s = db.getCollection(c).stats(); return { name: c, count: s.count || 0, size: s.size || 0, storageSize: s.storageSize || 0, indexSize: s.totalIndexSize || 0, nindexes: s.nindexes || 0 }; })"""
|
|
171
|
+
|
|
172
|
+
QUERY_SLOW_QUERIES = """(function(){ try { var logs = db.system.profile.find().sort({ts: -1}).limit(10).toArray(); return logs.map(function(l) { return { op: l.op, ns: l.ns, millis: l.millis, ts: l.ts, command: JSON.stringify(l.command || l.query || {}).substring(0, 200), planSummary: l.planSummary || '' }; }); } catch(e) { return []; } })()"""
|
|
173
|
+
|
|
174
|
+
QUERY_REPL_INFO = """(function(){ try { var info = db.getReplicationInfo(); return { logSizeMB: info.logSizeMB, usedMB: info.usedMB, timeDiffHours: info.timeDiffHours }; } catch(e) { return null; } })()"""
|
|
175
|
+
|
|
176
|
+
QUERY_TOP = """(function(){ try { var t = db.adminCommand({top:1}); var totals = t.totals; var result = []; for (var ns in totals) { if (ns.indexOf('.') > 0 && ns.indexOf('system.') === -1) { var c = totals[ns]; result.push({ ns: ns, reads: c.readLock ? c.readLock.count : 0, readTimeUs: c.readLock ? c.readLock.time : 0, writes: c.writeLock ? c.writeLock.count : 0, writeTimeUs: c.writeLock ? c.writeLock.time : 0 }); } } return result; } catch(e) { return null; } })()"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Parsing helpers
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def _safe_json(raw: str) -> Any:
|
|
184
|
+
"""Parse JSON from mongosh EJSON output, returning None on failure."""
|
|
185
|
+
raw = raw.strip()
|
|
186
|
+
if not raw:
|
|
187
|
+
return None
|
|
188
|
+
# mongosh may emit warnings before the JSON; find the first { or [
|
|
189
|
+
for i, ch in enumerate(raw):
|
|
190
|
+
if ch in ('{', '['):
|
|
191
|
+
raw = raw[i:]
|
|
192
|
+
break
|
|
193
|
+
try:
|
|
194
|
+
return json.loads(raw)
|
|
195
|
+
except json.JSONDecodeError:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _parse_server_status(data: Dict[str, Any], result: MongoAnalysisResult) -> None:
|
|
200
|
+
"""Extract metrics from serverStatus into result."""
|
|
201
|
+
# Overview
|
|
202
|
+
result.version = data.get("version")
|
|
203
|
+
se = data.get("storageEngine")
|
|
204
|
+
if se:
|
|
205
|
+
result.storage_engine = se.get("name")
|
|
206
|
+
result.uptime_seconds = data.get("uptime")
|
|
207
|
+
|
|
208
|
+
# Connections
|
|
209
|
+
conn = data.get("connections")
|
|
210
|
+
if conn:
|
|
211
|
+
result.connections = {
|
|
212
|
+
"current": conn.get("current", 0),
|
|
213
|
+
"available": conn.get("available", 0),
|
|
214
|
+
"totalCreated": conn.get("totalCreated", 0),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# Opcounters
|
|
218
|
+
result.opcounters = data.get("opcounters")
|
|
219
|
+
result.opcounters_repl = data.get("opcountersRepl")
|
|
220
|
+
|
|
221
|
+
# Replication info from serverStatus
|
|
222
|
+
repl = data.get("repl")
|
|
223
|
+
if repl:
|
|
224
|
+
result.replication = {
|
|
225
|
+
"setName": repl.get("setName"),
|
|
226
|
+
"isWritablePrimary": repl.get("isWritablePrimary"),
|
|
227
|
+
"primary": repl.get("primary"),
|
|
228
|
+
"hosts": repl.get("hosts"),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Latency
|
|
232
|
+
lat = data.get("opLatencies")
|
|
233
|
+
if lat:
|
|
234
|
+
result.op_latencies = {}
|
|
235
|
+
for key in ("reads", "writes", "commands"):
|
|
236
|
+
entry = lat.get(key, {})
|
|
237
|
+
ops = entry.get("ops", 0)
|
|
238
|
+
latency = entry.get("latency", 0)
|
|
239
|
+
result.op_latencies[key] = {
|
|
240
|
+
"latency": latency,
|
|
241
|
+
"ops": ops,
|
|
242
|
+
"avg_us": round(latency / ops, 1) if ops > 0 else 0,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Memory
|
|
246
|
+
mem = data.get("mem")
|
|
247
|
+
if mem:
|
|
248
|
+
result.memory = {
|
|
249
|
+
"resident_mb": mem.get("resident", 0),
|
|
250
|
+
"virtual_mb": mem.get("virtual", 0),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
extra = data.get("extra_info")
|
|
254
|
+
if extra:
|
|
255
|
+
result.page_faults = extra.get("page_faults", 0)
|
|
256
|
+
|
|
257
|
+
# Network
|
|
258
|
+
net = data.get("network")
|
|
259
|
+
if net:
|
|
260
|
+
result.network = {
|
|
261
|
+
"bytesIn": net.get("bytesIn", 0),
|
|
262
|
+
"bytesOut": net.get("bytesOut", 0),
|
|
263
|
+
"numRequests": net.get("numRequests", 0),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# WiredTiger
|
|
267
|
+
wt = data.get("wiredTiger")
|
|
268
|
+
if wt:
|
|
269
|
+
cache = wt.get("cache", {})
|
|
270
|
+
result.wiredtiger_cache = {
|
|
271
|
+
"bytes_in_cache": cache.get("bytes currently in the cache", 0),
|
|
272
|
+
"max_bytes": cache.get("maximum bytes configured", 0),
|
|
273
|
+
"dirty_bytes": cache.get("tracked dirty bytes in the cache", 0),
|
|
274
|
+
"pages_read": cache.get("pages read into cache", 0),
|
|
275
|
+
"pages_written": cache.get("pages written from cache", 0),
|
|
276
|
+
"app_evictions": cache.get("pages evicted by application threads", 0),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
txn = wt.get("transaction", {})
|
|
280
|
+
if txn:
|
|
281
|
+
result.wiredtiger_checkpoint = {
|
|
282
|
+
"most_recent_time_ms": txn.get("transaction checkpoint most recent time (msecs)", 0),
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
ct = wt.get("concurrentTransactions", {})
|
|
286
|
+
if ct:
|
|
287
|
+
read_info = ct.get("read", {})
|
|
288
|
+
write_info = ct.get("write", {})
|
|
289
|
+
result.wiredtiger_tickets = {
|
|
290
|
+
"read_available": read_info.get("available", 0),
|
|
291
|
+
"read_total": read_info.get("totalTickets", 0),
|
|
292
|
+
"write_available": write_info.get("available", 0),
|
|
293
|
+
"write_total": write_info.get("totalTickets", 0),
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Global lock
|
|
297
|
+
gl = data.get("globalLock")
|
|
298
|
+
if gl:
|
|
299
|
+
cq = gl.get("currentQueue", {})
|
|
300
|
+
ac = gl.get("activeClients", {})
|
|
301
|
+
result.global_lock = {
|
|
302
|
+
"queue_readers": cq.get("readers", 0),
|
|
303
|
+
"queue_writers": cq.get("writers", 0),
|
|
304
|
+
"active_readers": ac.get("readers", 0),
|
|
305
|
+
"active_writers": ac.get("writers", 0),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Metrics
|
|
309
|
+
metrics = data.get("metrics")
|
|
310
|
+
if metrics:
|
|
311
|
+
doc = metrics.get("document")
|
|
312
|
+
if doc:
|
|
313
|
+
result.document_metrics = {
|
|
314
|
+
"inserted": doc.get("inserted", 0),
|
|
315
|
+
"updated": doc.get("updated", 0),
|
|
316
|
+
"deleted": doc.get("deleted", 0),
|
|
317
|
+
"returned": doc.get("returned", 0),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
qe = metrics.get("queryExecutor")
|
|
321
|
+
if qe:
|
|
322
|
+
result.query_executor = {
|
|
323
|
+
"scanned": qe.get("scanned", 0),
|
|
324
|
+
"scannedObjects": qe.get("scannedObjects", 0),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
cursor = metrics.get("cursor")
|
|
328
|
+
if cursor:
|
|
329
|
+
open_cursors = cursor.get("open", {})
|
|
330
|
+
result.cursors = {
|
|
331
|
+
"open_total": open_cursors.get("total", 0),
|
|
332
|
+
"timed_out": cursor.get("timedOut", 0),
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
ttl = metrics.get("ttl")
|
|
336
|
+
if ttl:
|
|
337
|
+
result.ttl_metrics = {
|
|
338
|
+
"deletedDocuments": ttl.get("deletedDocuments", 0),
|
|
339
|
+
"passes": ttl.get("passes", 0),
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
query_metrics = metrics.get("query")
|
|
343
|
+
if query_metrics:
|
|
344
|
+
pc = query_metrics.get("planCache", {})
|
|
345
|
+
if pc:
|
|
346
|
+
result.plan_cache = {
|
|
347
|
+
"hits": pc.get("hits", 0),
|
|
348
|
+
"misses": pc.get("misses", 0),
|
|
349
|
+
}
|
|
350
|
+
sort = query_metrics.get("sort", {})
|
|
351
|
+
if sort:
|
|
352
|
+
result.sort_metrics = {
|
|
353
|
+
"spillToDisk": sort.get("spillToDisk", 0),
|
|
354
|
+
"totalBytesSorted": sort.get("totalBytesSorted", 0),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Asserts
|
|
358
|
+
result.asserts = data.get("asserts")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _parse_db_stats(data: Dict[str, Any], result: MongoAnalysisResult) -> None:
|
|
362
|
+
"""Extract metrics from db.stats()."""
|
|
363
|
+
result.storage = {
|
|
364
|
+
"dataSize": data.get("dataSize", 0),
|
|
365
|
+
"storageSize": data.get("storageSize", 0),
|
|
366
|
+
"indexSize": data.get("indexSize", 0),
|
|
367
|
+
"objects": data.get("objects", 0),
|
|
368
|
+
"collections": data.get("collections", 0),
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _parse_current_op(data: Any, result: MongoAnalysisResult) -> None:
|
|
373
|
+
"""Extract active operations from currentOp."""
|
|
374
|
+
if not data:
|
|
375
|
+
return
|
|
376
|
+
inprog = data.get("inprog", []) if isinstance(data, dict) else []
|
|
377
|
+
ops = []
|
|
378
|
+
for op in inprog:
|
|
379
|
+
ops.append({
|
|
380
|
+
"opid": op.get("opid"),
|
|
381
|
+
"type": op.get("type", op.get("op", "")),
|
|
382
|
+
"ns": op.get("ns", ""),
|
|
383
|
+
"microsecs_running": op.get("microsecs_running", 0),
|
|
384
|
+
"desc": op.get("desc", ""),
|
|
385
|
+
})
|
|
386
|
+
result.active_ops = ops
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _parse_collection_stats(data: Any, result: MongoAnalysisResult) -> None:
|
|
390
|
+
"""Parse per-collection stats."""
|
|
391
|
+
if not isinstance(data, list):
|
|
392
|
+
return
|
|
393
|
+
result.collection_stats = data
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _parse_slow_queries(data: Any, result: MongoAnalysisResult) -> None:
|
|
397
|
+
"""Parse slow queries from profiler."""
|
|
398
|
+
if not isinstance(data, list):
|
|
399
|
+
return
|
|
400
|
+
result.slow_queries = data
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _parse_repl_info(data: Any, result: MongoAnalysisResult) -> None:
|
|
404
|
+
"""Parse oplog replication info."""
|
|
405
|
+
if not data or not isinstance(data, dict):
|
|
406
|
+
return
|
|
407
|
+
result.oplog = {
|
|
408
|
+
"logSizeMB": data.get("logSizeMB", 0),
|
|
409
|
+
"usedMB": data.get("usedMB", 0),
|
|
410
|
+
"timeDiffHours": data.get("timeDiffHours", 0),
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _parse_top(data: Any, result: MongoAnalysisResult) -> None:
|
|
415
|
+
"""Parse top collection activity."""
|
|
416
|
+
if not isinstance(data, list):
|
|
417
|
+
return
|
|
418
|
+
result.top_collections = data
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
# Formatting helpers
|
|
423
|
+
# ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
def _fmt_bytes(b: int) -> str:
|
|
426
|
+
"""Format bytes as human-readable."""
|
|
427
|
+
if b >= 1024 * 1024 * 1024:
|
|
428
|
+
return f"{b / 1024 / 1024 / 1024:.1f} GB"
|
|
429
|
+
elif b >= 1024 * 1024:
|
|
430
|
+
return f"{b / 1024 / 1024:.1f} MB"
|
|
431
|
+
elif b >= 1024:
|
|
432
|
+
return f"{b / 1024:.1f} KB"
|
|
433
|
+
return f"{b} B"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _fmt_count(n: int) -> str:
|
|
437
|
+
"""Format large numbers with K/M suffix."""
|
|
438
|
+
if n >= 1_000_000_000:
|
|
439
|
+
return f"{n / 1_000_000_000:.1f}B"
|
|
440
|
+
elif n >= 1_000_000:
|
|
441
|
+
return f"{n / 1_000_000:.1f}M"
|
|
442
|
+
elif n >= 1_000:
|
|
443
|
+
return f"{n / 1_000:.1f}K"
|
|
444
|
+
return str(n)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _fmt_uptime(seconds: int) -> str:
|
|
448
|
+
"""Format seconds as Xd Yh."""
|
|
449
|
+
days = seconds // 86400
|
|
450
|
+
hours = (seconds % 86400) // 3600
|
|
451
|
+
if days > 0:
|
|
452
|
+
return f"{days}d {hours}h"
|
|
453
|
+
elif hours > 0:
|
|
454
|
+
return f"{hours}h {(seconds % 3600) // 60}m"
|
|
455
|
+
return f"{seconds // 60}m"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _fmt_us(microseconds: float) -> str:
|
|
459
|
+
"""Format microseconds as human-readable latency."""
|
|
460
|
+
if microseconds >= 1_000_000:
|
|
461
|
+
return f"{microseconds / 1_000_000:.1f}s"
|
|
462
|
+
elif microseconds >= 1_000:
|
|
463
|
+
return f"{microseconds / 1_000:.1f}ms"
|
|
464
|
+
return f"{microseconds:.0f}us"
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# ---------------------------------------------------------------------------
|
|
468
|
+
# Main analysis
|
|
469
|
+
# ---------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
def analyze_mongo(service: str, timeout: int = 300, quiet: bool = False,
|
|
472
|
+
skip_logs: bool = False, metrics_hours: int = 168,
|
|
473
|
+
project_id: Optional[str] = None,
|
|
474
|
+
environment_id: Optional[str] = None,
|
|
475
|
+
service_id: Optional[str] = None) -> MongoAnalysisResult:
|
|
476
|
+
"""Run complete MongoDB analysis."""
|
|
477
|
+
if not quiet:
|
|
478
|
+
print(f"Analyzing MongoDB database: {service}", file=sys.stderr)
|
|
479
|
+
|
|
480
|
+
result = MongoAnalysisResult(
|
|
481
|
+
service=service,
|
|
482
|
+
db_type="mongo",
|
|
483
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# === CONTEXT ===
|
|
487
|
+
if not quiet:
|
|
488
|
+
print(" [0/5] Getting Railway context...", file=sys.stderr, flush=True)
|
|
489
|
+
dal._progress_timer.start()
|
|
490
|
+
|
|
491
|
+
if environment_id and service_id:
|
|
492
|
+
dal._ctx = RailwayContext(project_id=project_id, environment_id=environment_id, service_id=service_id)
|
|
493
|
+
if not quiet:
|
|
494
|
+
print(f" using explicit IDs (env={environment_id[:8]}..., svc={service_id[:8]}...)", file=sys.stderr, flush=True)
|
|
495
|
+
else:
|
|
496
|
+
railway_status = get_railway_status()
|
|
497
|
+
if railway_status:
|
|
498
|
+
dal._ctx = RailwayContext(
|
|
499
|
+
project_id=railway_status.get("projectId"),
|
|
500
|
+
environment_id=railway_status.get("environmentId"),
|
|
501
|
+
service_id=railway_status.get("serviceId"),
|
|
502
|
+
)
|
|
503
|
+
environment_id = dal._ctx.environment_id
|
|
504
|
+
service_id = dal._ctx.service_id
|
|
505
|
+
|
|
506
|
+
# === DEPLOYMENT STATUS ===
|
|
507
|
+
progress(1, 5, "Fetching deployment status...", quiet)
|
|
508
|
+
result.deployment_status = get_deployment_status(service, service_id=service_id)
|
|
509
|
+
|
|
510
|
+
# === SSH PRE-CHECK ===
|
|
511
|
+
progress(2, 5, "Testing SSH connectivity...", quiet)
|
|
512
|
+
ssh_available = False
|
|
513
|
+
ssh_stderr = ""
|
|
514
|
+
ssh_attempts = [30, 60, 90]
|
|
515
|
+
for attempt, attempt_timeout in enumerate(ssh_attempts, 1):
|
|
516
|
+
ssh_code, ssh_stdout, ssh_stderr = run_ssh_query(service, "echo ok", timeout=attempt_timeout)
|
|
517
|
+
if ssh_code == 0 and "ok" in ssh_stdout:
|
|
518
|
+
ssh_available = True
|
|
519
|
+
if not quiet:
|
|
520
|
+
for line in ssh_stderr.splitlines():
|
|
521
|
+
if line.startswith("Using SSH key:"):
|
|
522
|
+
print(f" {line}", file=sys.stderr, flush=True)
|
|
523
|
+
break
|
|
524
|
+
break
|
|
525
|
+
if not quiet:
|
|
526
|
+
remaining = len(ssh_attempts) - attempt
|
|
527
|
+
if remaining > 0:
|
|
528
|
+
print(f" SSH attempt {attempt}/{len(ssh_attempts)} failed ({ssh_stderr or 'no response'}), retrying with {ssh_attempts[attempt]}s timeout...", file=sys.stderr, flush=True)
|
|
529
|
+
else:
|
|
530
|
+
print(f" SSH attempt {attempt}/{len(ssh_attempts)} failed ({ssh_stderr or 'no response'}), giving up", file=sys.stderr, flush=True)
|
|
531
|
+
|
|
532
|
+
# === PARALLEL DATA COLLECTION ===
|
|
533
|
+
progress(3, 5, "Running analysis (metrics, mongo queries, logs in parallel)...", quiet)
|
|
534
|
+
|
|
535
|
+
def task_metrics():
|
|
536
|
+
if environment_id and service_id:
|
|
537
|
+
return get_all_metrics_from_api(environment_id, service_id, hours=metrics_hours)
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
def task_mongo_batch1():
|
|
541
|
+
"""serverStatus + dbStats + collectionStats."""
|
|
542
|
+
if not ssh_available:
|
|
543
|
+
return ("error", f"SSH not available: {ssh_stderr or 'connection failed'}")
|
|
544
|
+
results = {}
|
|
545
|
+
# serverStatus
|
|
546
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_SERVER_STATUS, timeout=30)
|
|
547
|
+
if code == 0:
|
|
548
|
+
results["serverStatus"] = _safe_json(stdout)
|
|
549
|
+
else:
|
|
550
|
+
results["serverStatus_error"] = stderr or stdout or "unknown"
|
|
551
|
+
# dbStats
|
|
552
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_DB_STATS, timeout=30)
|
|
553
|
+
if code == 0:
|
|
554
|
+
results["dbStats"] = _safe_json(stdout)
|
|
555
|
+
else:
|
|
556
|
+
results["dbStats_error"] = stderr or stdout or "unknown"
|
|
557
|
+
# collectionStats
|
|
558
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_COLLECTION_STATS, timeout=30)
|
|
559
|
+
if code == 0:
|
|
560
|
+
results["collStats"] = _safe_json(stdout)
|
|
561
|
+
else:
|
|
562
|
+
results["collStats_error"] = stderr or stdout or "unknown"
|
|
563
|
+
return ("ok", results)
|
|
564
|
+
|
|
565
|
+
def task_mongo_batch2():
|
|
566
|
+
"""slowQueries + currentOp + replInfo + top."""
|
|
567
|
+
if not ssh_available:
|
|
568
|
+
return ("error", f"SSH not available: {ssh_stderr or 'connection failed'}")
|
|
569
|
+
results = {}
|
|
570
|
+
# slow queries
|
|
571
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_SLOW_QUERIES, timeout=30)
|
|
572
|
+
if code == 0:
|
|
573
|
+
results["slowQueries"] = _safe_json(stdout)
|
|
574
|
+
else:
|
|
575
|
+
results["slowQueries_error"] = stderr or stdout or "unknown"
|
|
576
|
+
# currentOp
|
|
577
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_CURRENT_OP, timeout=30)
|
|
578
|
+
if code == 0:
|
|
579
|
+
results["currentOp"] = _safe_json(stdout)
|
|
580
|
+
else:
|
|
581
|
+
results["currentOp_error"] = stderr or stdout or "unknown"
|
|
582
|
+
# replication info
|
|
583
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_REPL_INFO, timeout=30)
|
|
584
|
+
if code == 0:
|
|
585
|
+
results["replInfo"] = _safe_json(stdout)
|
|
586
|
+
else:
|
|
587
|
+
results["replInfo_error"] = stderr or stdout or "unknown"
|
|
588
|
+
# top
|
|
589
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_TOP, timeout=30)
|
|
590
|
+
if code == 0:
|
|
591
|
+
results["top"] = _safe_json(stdout)
|
|
592
|
+
else:
|
|
593
|
+
results["top_error"] = stderr or stdout or "unknown"
|
|
594
|
+
return ("ok", results)
|
|
595
|
+
|
|
596
|
+
def task_logs():
|
|
597
|
+
if skip_logs:
|
|
598
|
+
return []
|
|
599
|
+
return get_recent_logs(service, lines=LOG_LINES_DEFAULT,
|
|
600
|
+
environment_id=environment_id,
|
|
601
|
+
service_id=service_id)
|
|
602
|
+
|
|
603
|
+
with ThreadPoolExecutor(max_workers=4) as executor:
|
|
604
|
+
future_metrics = executor.submit(task_metrics)
|
|
605
|
+
future_batch1 = executor.submit(task_mongo_batch1)
|
|
606
|
+
future_batch2 = executor.submit(task_mongo_batch2)
|
|
607
|
+
future_logs = executor.submit(task_logs)
|
|
608
|
+
|
|
609
|
+
metrics_result = future_metrics.result()
|
|
610
|
+
batch1_result = future_batch1.result()
|
|
611
|
+
batch2_result = future_batch2.result()
|
|
612
|
+
logs_result = future_logs.result()
|
|
613
|
+
|
|
614
|
+
# === PROCESS METRICS ===
|
|
615
|
+
if metrics_result:
|
|
616
|
+
result.disk_usage = metrics_result.get("disk_usage")
|
|
617
|
+
result.cpu_memory = metrics_result.get("cpu_memory")
|
|
618
|
+
result.metrics_history = metrics_result.get("metrics_history")
|
|
619
|
+
result.collection_status["metrics_api"] = {"status": "success"}
|
|
620
|
+
else:
|
|
621
|
+
result.collection_status["metrics_api"] = {
|
|
622
|
+
"status": "error",
|
|
623
|
+
"error": "Metrics API returned no data"
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
# === PROCESS BATCH 1 (serverStatus, dbStats, collStats) ===
|
|
627
|
+
if batch1_result[0] == "ok":
|
|
628
|
+
b1 = batch1_result[1]
|
|
629
|
+
ss = b1.get("serverStatus")
|
|
630
|
+
if ss:
|
|
631
|
+
_parse_server_status(ss, result)
|
|
632
|
+
result.collection_status["server_status"] = {"status": "success"}
|
|
633
|
+
else:
|
|
634
|
+
err = b1.get("serverStatus_error", "no data")
|
|
635
|
+
result.errors.append(f"serverStatus failed: {err}")
|
|
636
|
+
result.collection_status["server_status"] = {"status": "error", "error": err}
|
|
637
|
+
|
|
638
|
+
dbs = b1.get("dbStats")
|
|
639
|
+
if dbs:
|
|
640
|
+
_parse_db_stats(dbs, result)
|
|
641
|
+
result.collection_status["db_stats"] = {"status": "success"}
|
|
642
|
+
else:
|
|
643
|
+
err = b1.get("dbStats_error", "no data")
|
|
644
|
+
result.collection_status["db_stats"] = {"status": "error", "error": err}
|
|
645
|
+
|
|
646
|
+
cs = b1.get("collStats")
|
|
647
|
+
if cs:
|
|
648
|
+
_parse_collection_stats(cs, result)
|
|
649
|
+
result.collection_status["collection_stats"] = {"status": "success"}
|
|
650
|
+
else:
|
|
651
|
+
err = b1.get("collStats_error", "no data")
|
|
652
|
+
result.collection_status["collection_stats"] = {"status": "error", "error": err}
|
|
653
|
+
else:
|
|
654
|
+
error_msg = batch1_result[1] if len(batch1_result) > 1 else "unknown"
|
|
655
|
+
result.errors.append(f"Batch 1 (serverStatus/dbStats/collStats) failed: {error_msg}")
|
|
656
|
+
for src in ("server_status", "db_stats", "collection_stats"):
|
|
657
|
+
result.collection_status[src] = {"status": "error", "error": error_msg}
|
|
658
|
+
|
|
659
|
+
# === PROCESS BATCH 2 (slowQueries, currentOp, replInfo, top) ===
|
|
660
|
+
if batch2_result[0] == "ok":
|
|
661
|
+
b2 = batch2_result[1]
|
|
662
|
+
|
|
663
|
+
sq = b2.get("slowQueries")
|
|
664
|
+
if sq is not None:
|
|
665
|
+
_parse_slow_queries(sq, result)
|
|
666
|
+
result.collection_status["slow_queries"] = {"status": "success"}
|
|
667
|
+
else:
|
|
668
|
+
err = b2.get("slowQueries_error", "no data")
|
|
669
|
+
result.collection_status["slow_queries"] = {"status": "error", "error": err}
|
|
670
|
+
|
|
671
|
+
co = b2.get("currentOp")
|
|
672
|
+
if co is not None:
|
|
673
|
+
_parse_current_op(co, result)
|
|
674
|
+
result.collection_status["current_op"] = {"status": "success"}
|
|
675
|
+
else:
|
|
676
|
+
err = b2.get("currentOp_error", "no data")
|
|
677
|
+
result.collection_status["current_op"] = {"status": "error", "error": err}
|
|
678
|
+
|
|
679
|
+
ri = b2.get("replInfo")
|
|
680
|
+
if ri is not None:
|
|
681
|
+
_parse_repl_info(ri, result)
|
|
682
|
+
result.collection_status["repl_info"] = {"status": "success"}
|
|
683
|
+
else:
|
|
684
|
+
err = b2.get("replInfo_error", "no data or not a replica set")
|
|
685
|
+
result.collection_status["repl_info"] = {"status": "skipped", "reason": err}
|
|
686
|
+
|
|
687
|
+
top = b2.get("top")
|
|
688
|
+
if top is not None:
|
|
689
|
+
_parse_top(top, result)
|
|
690
|
+
result.collection_status["top"] = {"status": "success"}
|
|
691
|
+
else:
|
|
692
|
+
err = b2.get("top_error", "no data or insufficient privileges")
|
|
693
|
+
result.collection_status["top"] = {"status": "skipped", "reason": err}
|
|
694
|
+
else:
|
|
695
|
+
error_msg = batch2_result[1] if len(batch2_result) > 1 else "unknown"
|
|
696
|
+
result.errors.append(f"Batch 2 (slowQueries/currentOp/replInfo/top) failed: {error_msg}")
|
|
697
|
+
for src in ("slow_queries", "current_op", "repl_info", "top"):
|
|
698
|
+
result.collection_status[src] = {"status": "error", "error": error_msg}
|
|
699
|
+
|
|
700
|
+
# === PROCESS LOGS ===
|
|
701
|
+
progress(4, 5, "Processing logs...", quiet)
|
|
702
|
+
if skip_logs:
|
|
703
|
+
result.collection_status["logs_api"] = {"status": "skipped", "reason": "skip_logs flag set"}
|
|
704
|
+
elif logs_result:
|
|
705
|
+
result.recent_logs = logs_result
|
|
706
|
+
result.collection_status["logs_api"] = {"status": "success", "lines": len(logs_result)}
|
|
707
|
+
result.recent_errors = [
|
|
708
|
+
line for line in result.recent_logs
|
|
709
|
+
if 'ERROR' in line.upper() or 'FATAL' in line.upper() or 'PANIC' in line.upper()
|
|
710
|
+
][:100]
|
|
711
|
+
else:
|
|
712
|
+
result.recent_logs = []
|
|
713
|
+
result.collection_status["logs_api"] = {"status": "error", "error": "Logs API returned no data"}
|
|
714
|
+
|
|
715
|
+
# === RECOMMENDATIONS ===
|
|
716
|
+
progress(5, 5, "Generating recommendations...", quiet)
|
|
717
|
+
result.recommendations = generate_recommendations(result)
|
|
718
|
+
|
|
719
|
+
if not quiet:
|
|
720
|
+
total = dal._progress_timer.total_elapsed()
|
|
721
|
+
print(f"Done.{total}", file=sys.stderr)
|
|
722
|
+
|
|
723
|
+
return result
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# ---------------------------------------------------------------------------
|
|
727
|
+
# Recommendations engine
|
|
728
|
+
# ---------------------------------------------------------------------------
|
|
729
|
+
|
|
730
|
+
def generate_recommendations(result: MongoAnalysisResult) -> List[Dict[str, str]]:
|
|
731
|
+
"""Generate recommendations based on analysis results."""
|
|
732
|
+
recs: List[Dict[str, str]] = []
|
|
733
|
+
|
|
734
|
+
# Collection failures — surface critical issues when SSH/introspection failed
|
|
735
|
+
if result.collection_status:
|
|
736
|
+
failed = {k: v for k, v in result.collection_status.items()
|
|
737
|
+
if v.get("status") in ("failed", "error")}
|
|
738
|
+
ssh_sources = {"server_status", "db_stats", "collection_stats",
|
|
739
|
+
"slow_queries", "current_op", "repl_info", "top"}
|
|
740
|
+
ssh_failed = {k: v for k, v in failed.items() if k in ssh_sources}
|
|
741
|
+
if ssh_failed:
|
|
742
|
+
sources = ", ".join(ssh_failed.keys())
|
|
743
|
+
errors = "; ".join(v.get("error", "unknown") for v in ssh_failed.values())
|
|
744
|
+
recs.append({
|
|
745
|
+
"severity": "critical",
|
|
746
|
+
"category": "collection",
|
|
747
|
+
"message": f"SSH introspection failed — unable to collect {sources}. "
|
|
748
|
+
f"Error: {errors}. "
|
|
749
|
+
f"Analysis is incomplete: WiredTiger cache, connections, "
|
|
750
|
+
f"collection stats, and replication health could not be evaluated.",
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
# --- WiredTiger cache usage ---
|
|
754
|
+
wt = result.wiredtiger_cache
|
|
755
|
+
if wt:
|
|
756
|
+
max_bytes = wt.get("max_bytes", 0)
|
|
757
|
+
used_bytes = wt.get("bytes_in_cache", 0)
|
|
758
|
+
dirty_bytes = wt.get("dirty_bytes", 0)
|
|
759
|
+
app_evictions = wt.get("app_evictions", 0)
|
|
760
|
+
|
|
761
|
+
if max_bytes > 0:
|
|
762
|
+
usage_pct = round(100.0 * used_bytes / max_bytes, 1)
|
|
763
|
+
if usage_pct > 80:
|
|
764
|
+
recs.append({
|
|
765
|
+
"priority": "immediate",
|
|
766
|
+
"issue": f"WiredTiger cache is {usage_pct}% full ({_fmt_bytes(used_bytes)} of {_fmt_bytes(max_bytes)})",
|
|
767
|
+
"action": "Consider increasing service RAM. WiredTiger cache defaults to 50% of RAM minus 1 GB.",
|
|
768
|
+
"explanation": "When the WiredTiger cache is nearly full, MongoDB must evict pages more aggressively, "
|
|
769
|
+
"increasing latency for reads and writes. Increasing RAM gives WiredTiger more room to cache data.",
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
if used_bytes > 0:
|
|
773
|
+
dirty_pct = round(100.0 * dirty_bytes / max_bytes, 1)
|
|
774
|
+
if dirty_pct > 20:
|
|
775
|
+
recs.append({
|
|
776
|
+
"priority": "short-term",
|
|
777
|
+
"issue": f"High dirty cache ({dirty_pct}% of total cache). Checkpoint may be falling behind.",
|
|
778
|
+
"action": "Monitor checkpoint duration and consider increasing RAM or reducing write throughput.",
|
|
779
|
+
"explanation": "Dirty pages must be written to disk during checkpoints. A high dirty ratio means "
|
|
780
|
+
"checkpoints have more work, potentially causing latency spikes.",
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
if app_evictions and app_evictions > 0:
|
|
784
|
+
recs.append({
|
|
785
|
+
"priority": "immediate",
|
|
786
|
+
"issue": f"Application threads performing evictions ({app_evictions:,} pages). WiredTiger cache under pressure.",
|
|
787
|
+
"action": "Increase RAM to give WiredTiger more cache space.",
|
|
788
|
+
"explanation": "Normally the WiredTiger eviction threads handle cache pressure. When application threads "
|
|
789
|
+
"must evict pages themselves, queries stall waiting for cache space. This directly increases latency.",
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
# --- Connection usage ---
|
|
793
|
+
conn = result.connections
|
|
794
|
+
if conn:
|
|
795
|
+
current = conn.get("current", 0)
|
|
796
|
+
available = conn.get("available", 0)
|
|
797
|
+
total = current + available
|
|
798
|
+
if total > 0:
|
|
799
|
+
pct = round(100.0 * current / total, 1)
|
|
800
|
+
if pct > 80:
|
|
801
|
+
recs.append({
|
|
802
|
+
"priority": "immediate" if pct > 90 else "short-term",
|
|
803
|
+
"issue": f"Connection usage at {pct}% ({current} of {total}). Approaching connection limit.",
|
|
804
|
+
"action": "Review application connection pooling. Consider using a connection pooler or increasing maxIncomingConnections.",
|
|
805
|
+
"explanation": "Running out of connections will cause new client connections to be refused. "
|
|
806
|
+
"Most applications should use connection pooling to limit concurrent connections.",
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
# --- Page faults ---
|
|
810
|
+
if result.page_faults and result.page_faults > 10000:
|
|
811
|
+
recs.append({
|
|
812
|
+
"priority": "short-term",
|
|
813
|
+
"issue": f"Significant page faults ({result.page_faults:,}). Working set may exceed available RAM.",
|
|
814
|
+
"action": "Increase service RAM or optimize queries to reduce working set size.",
|
|
815
|
+
"explanation": "Page faults occur when MongoDB accesses data not in memory, requiring disk reads. "
|
|
816
|
+
"High page faults indicate the working set is larger than available RAM.",
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
# --- Queued operations ---
|
|
820
|
+
gl = result.global_lock
|
|
821
|
+
if gl:
|
|
822
|
+
qr = gl.get("queue_readers", 0)
|
|
823
|
+
qw = gl.get("queue_writers", 0)
|
|
824
|
+
if qr > 0 or qw > 0:
|
|
825
|
+
recs.append({
|
|
826
|
+
"priority": "immediate" if (qr + qw) > 10 else "short-term",
|
|
827
|
+
"issue": f"Operations queuing detected (readers: {qr}, writers: {qw}). Database may be under resource pressure.",
|
|
828
|
+
"action": "Investigate slow operations and consider increasing RAM or CPU.",
|
|
829
|
+
"explanation": "Queued operations mean requests are waiting for a lock. This can be caused by slow queries, "
|
|
830
|
+
"write-heavy workloads, or insufficient resources.",
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
# --- Query efficiency ---
|
|
834
|
+
qe = result.query_executor
|
|
835
|
+
dm = result.document_metrics
|
|
836
|
+
if qe and dm:
|
|
837
|
+
scanned = qe.get("scannedObjects", 0)
|
|
838
|
+
returned = dm.get("returned", 0)
|
|
839
|
+
if returned > 0 and scanned > returned * 10:
|
|
840
|
+
ratio = round(scanned / returned, 1)
|
|
841
|
+
recs.append({
|
|
842
|
+
"priority": "immediate" if ratio > 100 else "short-term",
|
|
843
|
+
"issue": f"Query efficiency concern: {_fmt_count(scanned)} objects scanned vs {_fmt_count(returned)} returned (ratio: {ratio}x).",
|
|
844
|
+
"action": "Create indexes for frequently queried fields. Review slow query log for full collection scans.",
|
|
845
|
+
"explanation": "A high scan-to-return ratio means MongoDB is examining many documents to satisfy queries. "
|
|
846
|
+
"Adding appropriate indexes dramatically reduces the number of documents examined.",
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
# --- Plan cache ---
|
|
850
|
+
pc = result.plan_cache
|
|
851
|
+
if pc:
|
|
852
|
+
hits = pc.get("hits", 0)
|
|
853
|
+
misses = pc.get("misses", 0)
|
|
854
|
+
total = hits + misses
|
|
855
|
+
if total > 100 and misses > hits:
|
|
856
|
+
recs.append({
|
|
857
|
+
"priority": "short-term",
|
|
858
|
+
"issue": f"High plan cache miss ratio ({misses:,} misses vs {hits:,} hits). Queries may not be using optimal plans.",
|
|
859
|
+
"action": "Consider creating indexes for frequent query patterns to stabilize query plans.",
|
|
860
|
+
"explanation": "Plan cache misses mean MongoDB must re-evaluate query plans. Stable indexes help the planner "
|
|
861
|
+
"pick consistent, efficient plans.",
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
# --- Sort spill to disk ---
|
|
865
|
+
sm = result.sort_metrics
|
|
866
|
+
if sm:
|
|
867
|
+
spill = sm.get("spillToDisk", 0)
|
|
868
|
+
if spill > 0:
|
|
869
|
+
recs.append({
|
|
870
|
+
"priority": "short-term",
|
|
871
|
+
"issue": f"Sorts spilling to disk ({spill:,} times). Queries performing in-memory sorts exceeding limit.",
|
|
872
|
+
"action": "Add indexes to support sort operations, or increase RAM.",
|
|
873
|
+
"explanation": "When a sort operation exceeds the memory limit (100 MB by default), MongoDB spills to disk. "
|
|
874
|
+
"Creating an index that matches the sort key avoids the in-memory sort entirely.",
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
# --- Cursor timeouts ---
|
|
878
|
+
cur = result.cursors
|
|
879
|
+
if cur:
|
|
880
|
+
timed_out = cur.get("timed_out", 0)
|
|
881
|
+
if timed_out > 0:
|
|
882
|
+
recs.append({
|
|
883
|
+
"priority": "short-term",
|
|
884
|
+
"issue": f"Cursor timeouts detected ({timed_out:,}). Long-running queries may need optimization.",
|
|
885
|
+
"action": "Review application code for unbounded queries or missing pagination.",
|
|
886
|
+
"explanation": "Cursors time out after 10 minutes of inactivity by default. Frequent timeouts suggest "
|
|
887
|
+
"clients are not consuming results quickly enough or queries are returning too much data.",
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
# --- Asserts ---
|
|
891
|
+
asserts = result.asserts
|
|
892
|
+
if asserts:
|
|
893
|
+
regular = asserts.get("regular", 0)
|
|
894
|
+
warning = asserts.get("warning", 0)
|
|
895
|
+
user = asserts.get("user", 0)
|
|
896
|
+
msg = asserts.get("msg", 0)
|
|
897
|
+
if regular > 0 or warning > 0 or user > 0:
|
|
898
|
+
recs.append({
|
|
899
|
+
"priority": "short-term",
|
|
900
|
+
"issue": f"Database asserts detected (regular: {regular}, warning: {warning}, user: {user}, msg: {msg}). Investigate error conditions.",
|
|
901
|
+
"action": "Check MongoDB logs for assert details. User asserts often indicate client errors; regular/warning asserts may signal server issues.",
|
|
902
|
+
"explanation": "Asserts are internal consistency checks. Regular and warning asserts may indicate bugs or data issues. "
|
|
903
|
+
"User asserts are typically client-side errors (e.g., duplicate key violations).",
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
# --- Oplog usage ---
|
|
907
|
+
oplog = result.oplog
|
|
908
|
+
if oplog:
|
|
909
|
+
log_size = oplog.get("logSizeMB", 0)
|
|
910
|
+
used = oplog.get("usedMB", 0)
|
|
911
|
+
if log_size > 0:
|
|
912
|
+
oplog_pct = round(100.0 * used / log_size, 1)
|
|
913
|
+
if oplog_pct > 80:
|
|
914
|
+
recs.append({
|
|
915
|
+
"priority": "short-term",
|
|
916
|
+
"issue": f"Oplog is {oplog_pct}% full ({used:.0f} MB of {log_size:.0f} MB). May impact replication if oplog window is too small.",
|
|
917
|
+
"action": "Consider increasing the oplog size to maintain a larger replication window.",
|
|
918
|
+
"explanation": "The oplog stores recent write operations for replication. If it fills up and wraps around too quickly, "
|
|
919
|
+
"replica set members that fall behind may need a full resync instead of incremental replication.",
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
return recs
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
# ---------------------------------------------------------------------------
|
|
926
|
+
# Report formatting
|
|
927
|
+
# ---------------------------------------------------------------------------
|
|
928
|
+
|
|
929
|
+
def format_report(result: MongoAnalysisResult) -> str:
|
|
930
|
+
"""Format analysis result as human-readable markdown report."""
|
|
931
|
+
lines: List[str] = []
|
|
932
|
+
lines.append("=" * 60)
|
|
933
|
+
lines.append(f"# MongoDB Analysis: {result.service}")
|
|
934
|
+
lines.append("=" * 60)
|
|
935
|
+
lines.append(f"Timestamp: {result.timestamp}")
|
|
936
|
+
lines.append(f"Status: {result.deployment_status}")
|
|
937
|
+
lines.append("")
|
|
938
|
+
|
|
939
|
+
# --- Data Collection Status ---
|
|
940
|
+
if result.collection_status:
|
|
941
|
+
lines.append("## Data Collection Status")
|
|
942
|
+
lines.append("")
|
|
943
|
+
lines.append("| Source | Status | Details |")
|
|
944
|
+
lines.append("|--------|--------|---------|")
|
|
945
|
+
source_labels = {
|
|
946
|
+
"server_status": "Server Status (SSH)",
|
|
947
|
+
"db_stats": "Database Stats (SSH)",
|
|
948
|
+
"collection_stats": "Collection Stats (SSH)",
|
|
949
|
+
"slow_queries": "Slow Queries (SSH)",
|
|
950
|
+
"current_op": "Current Operations (SSH)",
|
|
951
|
+
"repl_info": "Replication Info (SSH)",
|
|
952
|
+
"top": "Top Collections (SSH)",
|
|
953
|
+
"metrics_api": "Metrics API",
|
|
954
|
+
"logs_api": "Logs API",
|
|
955
|
+
}
|
|
956
|
+
for source in ["server_status", "db_stats", "collection_stats",
|
|
957
|
+
"slow_queries", "current_op", "repl_info", "top",
|
|
958
|
+
"metrics_api", "logs_api"]:
|
|
959
|
+
if source in result.collection_status:
|
|
960
|
+
info = result.collection_status[source]
|
|
961
|
+
status = info["status"].upper()
|
|
962
|
+
details = ""
|
|
963
|
+
if info.get("error"):
|
|
964
|
+
details = info["error"]
|
|
965
|
+
elif info.get("reason"):
|
|
966
|
+
details = info["reason"]
|
|
967
|
+
elif info.get("lines"):
|
|
968
|
+
details = f"{info['lines']} lines collected"
|
|
969
|
+
elif status == "SUCCESS":
|
|
970
|
+
details = "OK"
|
|
971
|
+
label = source_labels.get(source, source)
|
|
972
|
+
lines.append(f"| {label} | {status} | {details} |")
|
|
973
|
+
lines.append("")
|
|
974
|
+
|
|
975
|
+
# --- Overview ---
|
|
976
|
+
lines.append("## Overview")
|
|
977
|
+
lines.append("")
|
|
978
|
+
lines.append("| Metric | Value |")
|
|
979
|
+
lines.append("|--------|-------|")
|
|
980
|
+
if result.version:
|
|
981
|
+
lines.append(f"| Version | {result.version} |")
|
|
982
|
+
if result.storage_engine:
|
|
983
|
+
lines.append(f"| Storage Engine | {result.storage_engine} |")
|
|
984
|
+
if result.uptime_seconds is not None:
|
|
985
|
+
lines.append(f"| Uptime | {_fmt_uptime(result.uptime_seconds)} |")
|
|
986
|
+
status_icon = "Healthy" if result.deployment_status == "SUCCESS" else "Warning"
|
|
987
|
+
lines.append(f"| Deployment | {result.deployment_status} | {status_icon} |")
|
|
988
|
+
lines.append("")
|
|
989
|
+
|
|
990
|
+
# --- Connections ---
|
|
991
|
+
if result.connections:
|
|
992
|
+
lines.append("## Connections")
|
|
993
|
+
lines.append("")
|
|
994
|
+
lines.append("| Metric | Value | Status |")
|
|
995
|
+
lines.append("|--------|-------|--------|")
|
|
996
|
+
c = result.connections
|
|
997
|
+
current = c.get("current", 0)
|
|
998
|
+
available = c.get("available", 0)
|
|
999
|
+
total = current + available
|
|
1000
|
+
pct = round(100.0 * current / total, 1) if total > 0 else 0
|
|
1001
|
+
status = "Critical" if pct > 90 else "Warning" if pct > 80 else ""
|
|
1002
|
+
lines.append(f"| Current | {current:,} | {status} |")
|
|
1003
|
+
lines.append(f"| Available | {available:,} | |")
|
|
1004
|
+
lines.append(f"| Total Created | {c.get('totalCreated', 0):,} | |")
|
|
1005
|
+
lines.append("")
|
|
1006
|
+
|
|
1007
|
+
# --- Operations (since startup) ---
|
|
1008
|
+
if result.opcounters:
|
|
1009
|
+
lines.append("## Operations (since startup)")
|
|
1010
|
+
lines.append("")
|
|
1011
|
+
lines.append("| Operation | Count |")
|
|
1012
|
+
lines.append("|-----------|-------|")
|
|
1013
|
+
for op in ("insert", "query", "update", "delete", "getmore", "command"):
|
|
1014
|
+
val = result.opcounters.get(op, 0)
|
|
1015
|
+
lines.append(f"| {op} | {_fmt_count(val)} |")
|
|
1016
|
+
lines.append("")
|
|
1017
|
+
|
|
1018
|
+
# --- Replication opcounters ---
|
|
1019
|
+
if result.opcounters_repl:
|
|
1020
|
+
any_repl = any(v > 0 for v in result.opcounters_repl.values() if isinstance(v, (int, float)))
|
|
1021
|
+
if any_repl:
|
|
1022
|
+
lines.append("## Replication Operations")
|
|
1023
|
+
lines.append("")
|
|
1024
|
+
lines.append("| Operation | Count |")
|
|
1025
|
+
lines.append("|-----------|-------|")
|
|
1026
|
+
for op in ("insert", "query", "update", "delete", "getmore", "command"):
|
|
1027
|
+
val = result.opcounters_repl.get(op, 0)
|
|
1028
|
+
lines.append(f"| {op} | {_fmt_count(val)} |")
|
|
1029
|
+
lines.append("")
|
|
1030
|
+
|
|
1031
|
+
# --- Latency ---
|
|
1032
|
+
if result.op_latencies:
|
|
1033
|
+
lines.append("## Latency")
|
|
1034
|
+
lines.append("")
|
|
1035
|
+
lines.append("| Operation | Avg Latency | Total Ops |")
|
|
1036
|
+
lines.append("|-----------|-------------|-----------|")
|
|
1037
|
+
for key, label in [("reads", "Reads"), ("writes", "Writes"), ("commands", "Commands")]:
|
|
1038
|
+
entry = result.op_latencies.get(key, {})
|
|
1039
|
+
avg_us = entry.get("avg_us", 0)
|
|
1040
|
+
ops = entry.get("ops", 0)
|
|
1041
|
+
lines.append(f"| {label} | {_fmt_us(avg_us)} | {_fmt_count(ops)} |")
|
|
1042
|
+
lines.append("")
|
|
1043
|
+
|
|
1044
|
+
# --- Memory ---
|
|
1045
|
+
if result.memory:
|
|
1046
|
+
lines.append("## Memory")
|
|
1047
|
+
lines.append("")
|
|
1048
|
+
lines.append("| Metric | Value |")
|
|
1049
|
+
lines.append("|--------|-------|")
|
|
1050
|
+
lines.append(f"| Resident | {result.memory.get('resident_mb', 0):,} MB |")
|
|
1051
|
+
lines.append(f"| Virtual | {result.memory.get('virtual_mb', 0):,} MB |")
|
|
1052
|
+
if result.page_faults is not None:
|
|
1053
|
+
lines.append(f"| Page Faults | {result.page_faults:,} |")
|
|
1054
|
+
lines.append("")
|
|
1055
|
+
|
|
1056
|
+
# --- WiredTiger Cache ---
|
|
1057
|
+
wt = result.wiredtiger_cache
|
|
1058
|
+
if wt:
|
|
1059
|
+
lines.append("## WiredTiger Cache")
|
|
1060
|
+
lines.append("")
|
|
1061
|
+
lines.append("| Metric | Value | Status |")
|
|
1062
|
+
lines.append("|--------|-------|--------|")
|
|
1063
|
+
used = wt.get("bytes_in_cache", 0)
|
|
1064
|
+
max_b = wt.get("max_bytes", 0)
|
|
1065
|
+
dirty = wt.get("dirty_bytes", 0)
|
|
1066
|
+
app_evict = wt.get("app_evictions", 0)
|
|
1067
|
+
lines.append(f"| Used | {_fmt_bytes(used)} | |")
|
|
1068
|
+
lines.append(f"| Maximum | {_fmt_bytes(max_b)} | |")
|
|
1069
|
+
if max_b > 0:
|
|
1070
|
+
usage_pct = round(100.0 * used / max_b, 1)
|
|
1071
|
+
cache_status = "Critical" if usage_pct > 90 else "Warning" if usage_pct > 80 else "OK"
|
|
1072
|
+
lines.append(f"| Usage | {usage_pct}% | {cache_status} |")
|
|
1073
|
+
lines.append(f"| Dirty | {_fmt_bytes(dirty)} | |")
|
|
1074
|
+
evict_status = "Warning" if app_evict > 0 else "OK"
|
|
1075
|
+
lines.append(f"| App Thread Evictions | {app_evict:,} | {evict_status} |")
|
|
1076
|
+
lines.append(f"| Pages Read Into Cache | {wt.get('pages_read', 0):,} | |")
|
|
1077
|
+
lines.append(f"| Pages Written From Cache | {wt.get('pages_written', 0):,} | |")
|
|
1078
|
+
lines.append("")
|
|
1079
|
+
|
|
1080
|
+
# --- WiredTiger Checkpoint ---
|
|
1081
|
+
cp = result.wiredtiger_checkpoint
|
|
1082
|
+
if cp:
|
|
1083
|
+
ms = cp.get("most_recent_time_ms", 0)
|
|
1084
|
+
lines.append("## WiredTiger Checkpoint")
|
|
1085
|
+
lines.append("")
|
|
1086
|
+
lines.append(f"| Most Recent Checkpoint Time | {ms:,} ms |")
|
|
1087
|
+
lines.append("")
|
|
1088
|
+
|
|
1089
|
+
# --- WiredTiger Tickets ---
|
|
1090
|
+
tk = result.wiredtiger_tickets
|
|
1091
|
+
if tk:
|
|
1092
|
+
lines.append("## WiredTiger Tickets")
|
|
1093
|
+
lines.append("")
|
|
1094
|
+
lines.append("| Metric | Available | Total |")
|
|
1095
|
+
lines.append("|--------|-----------|-------|")
|
|
1096
|
+
lines.append(f"| Read | {tk.get('read_available', 0)} | {tk.get('read_total', 0)} |")
|
|
1097
|
+
lines.append(f"| Write | {tk.get('write_available', 0)} | {tk.get('write_total', 0)} |")
|
|
1098
|
+
lines.append("")
|
|
1099
|
+
|
|
1100
|
+
# --- Global Lock ---
|
|
1101
|
+
gl = result.global_lock
|
|
1102
|
+
if gl:
|
|
1103
|
+
lines.append("## Global Lock")
|
|
1104
|
+
lines.append("")
|
|
1105
|
+
lines.append("| Metric | Readers | Writers |")
|
|
1106
|
+
lines.append("|--------|---------|---------|")
|
|
1107
|
+
lines.append(f"| Queue | {gl.get('queue_readers', 0)} | {gl.get('queue_writers', 0)} |")
|
|
1108
|
+
lines.append(f"| Active | {gl.get('active_readers', 0)} | {gl.get('active_writers', 0)} |")
|
|
1109
|
+
lines.append("")
|
|
1110
|
+
|
|
1111
|
+
# --- Network ---
|
|
1112
|
+
if result.network:
|
|
1113
|
+
lines.append("## Network")
|
|
1114
|
+
lines.append("")
|
|
1115
|
+
lines.append("| Metric | Value |")
|
|
1116
|
+
lines.append("|--------|-------|")
|
|
1117
|
+
lines.append(f"| Bytes In | {_fmt_bytes(result.network.get('bytesIn', 0))} |")
|
|
1118
|
+
lines.append(f"| Bytes Out | {_fmt_bytes(result.network.get('bytesOut', 0))} |")
|
|
1119
|
+
lines.append(f"| Requests | {_fmt_count(result.network.get('numRequests', 0))} |")
|
|
1120
|
+
lines.append("")
|
|
1121
|
+
|
|
1122
|
+
# --- Document Metrics ---
|
|
1123
|
+
dm = result.document_metrics
|
|
1124
|
+
if dm:
|
|
1125
|
+
lines.append("## Documents")
|
|
1126
|
+
lines.append("")
|
|
1127
|
+
lines.append("| Operation | Count |")
|
|
1128
|
+
lines.append("|-----------|-------|")
|
|
1129
|
+
for key in ("inserted", "updated", "deleted", "returned"):
|
|
1130
|
+
lines.append(f"| {key} | {_fmt_count(dm.get(key, 0))} |")
|
|
1131
|
+
lines.append("")
|
|
1132
|
+
|
|
1133
|
+
# --- Query Efficiency ---
|
|
1134
|
+
qe = result.query_executor
|
|
1135
|
+
if qe:
|
|
1136
|
+
lines.append("## Query Efficiency")
|
|
1137
|
+
lines.append("")
|
|
1138
|
+
lines.append("| Metric | Value |")
|
|
1139
|
+
lines.append("|--------|-------|")
|
|
1140
|
+
lines.append(f"| Scanned Objects | {_fmt_count(qe.get('scannedObjects', 0))} |")
|
|
1141
|
+
lines.append(f"| Scanned Keys | {_fmt_count(qe.get('scanned', 0))} |")
|
|
1142
|
+
if dm:
|
|
1143
|
+
returned = dm.get("returned", 0)
|
|
1144
|
+
scanned = qe.get("scannedObjects", 0)
|
|
1145
|
+
if returned > 0:
|
|
1146
|
+
ratio = round(scanned / returned, 1)
|
|
1147
|
+
status = "Warning" if ratio > 10 else "OK"
|
|
1148
|
+
lines.append(f"| Scan-to-Return Ratio | {ratio}x | {status} |")
|
|
1149
|
+
lines.append("")
|
|
1150
|
+
|
|
1151
|
+
# --- Plan Cache ---
|
|
1152
|
+
pc = result.plan_cache
|
|
1153
|
+
if pc:
|
|
1154
|
+
lines.append("## Plan Cache (7.0+)")
|
|
1155
|
+
lines.append("")
|
|
1156
|
+
lines.append("| Metric | Value |")
|
|
1157
|
+
lines.append("|--------|-------|")
|
|
1158
|
+
lines.append(f"| Hits | {_fmt_count(pc.get('hits', 0))} |")
|
|
1159
|
+
lines.append(f"| Misses | {_fmt_count(pc.get('misses', 0))} |")
|
|
1160
|
+
lines.append("")
|
|
1161
|
+
|
|
1162
|
+
# --- Sort Metrics ---
|
|
1163
|
+
sm = result.sort_metrics
|
|
1164
|
+
if sm:
|
|
1165
|
+
lines.append("## Sort (7.0+)")
|
|
1166
|
+
lines.append("")
|
|
1167
|
+
lines.append("| Metric | Value |")
|
|
1168
|
+
lines.append("|--------|-------|")
|
|
1169
|
+
lines.append(f"| Spill to Disk | {sm.get('spillToDisk', 0):,} |")
|
|
1170
|
+
lines.append(f"| Total Bytes Sorted | {_fmt_bytes(sm.get('totalBytesSorted', 0))} |")
|
|
1171
|
+
lines.append("")
|
|
1172
|
+
|
|
1173
|
+
# --- Cursors ---
|
|
1174
|
+
cur = result.cursors
|
|
1175
|
+
if cur:
|
|
1176
|
+
lines.append("## Cursors")
|
|
1177
|
+
lines.append("")
|
|
1178
|
+
lines.append("| Metric | Value |")
|
|
1179
|
+
lines.append("|--------|-------|")
|
|
1180
|
+
lines.append(f"| Open Total | {cur.get('open_total', 0):,} |")
|
|
1181
|
+
timed = cur.get("timed_out", 0)
|
|
1182
|
+
status = "Warning" if timed > 0 else ""
|
|
1183
|
+
lines.append(f"| Timed Out | {timed:,} | {status} |")
|
|
1184
|
+
lines.append("")
|
|
1185
|
+
|
|
1186
|
+
# --- TTL ---
|
|
1187
|
+
ttl = result.ttl_metrics
|
|
1188
|
+
if ttl:
|
|
1189
|
+
lines.append("## TTL")
|
|
1190
|
+
lines.append("")
|
|
1191
|
+
lines.append("| Metric | Value |")
|
|
1192
|
+
lines.append("|--------|-------|")
|
|
1193
|
+
lines.append(f"| Deleted Documents | {_fmt_count(ttl.get('deletedDocuments', 0))} |")
|
|
1194
|
+
lines.append(f"| Passes | {ttl.get('passes', 0):,} |")
|
|
1195
|
+
lines.append("")
|
|
1196
|
+
|
|
1197
|
+
# --- Asserts ---
|
|
1198
|
+
asserts = result.asserts
|
|
1199
|
+
if asserts:
|
|
1200
|
+
any_assert = any(asserts.get(k, 0) > 0 for k in ("regular", "warning", "msg", "user"))
|
|
1201
|
+
if any_assert:
|
|
1202
|
+
lines.append("## Asserts")
|
|
1203
|
+
lines.append("")
|
|
1204
|
+
lines.append("| Type | Count |")
|
|
1205
|
+
lines.append("|------|-------|")
|
|
1206
|
+
for key in ("regular", "warning", "msg", "user", "rollovers"):
|
|
1207
|
+
lines.append(f"| {key} | {asserts.get(key, 0):,} |")
|
|
1208
|
+
lines.append("")
|
|
1209
|
+
|
|
1210
|
+
# --- Storage ---
|
|
1211
|
+
st = result.storage
|
|
1212
|
+
if st:
|
|
1213
|
+
lines.append("## Storage")
|
|
1214
|
+
lines.append("")
|
|
1215
|
+
lines.append("| Metric | Value |")
|
|
1216
|
+
lines.append("|--------|-------|")
|
|
1217
|
+
lines.append(f"| Data Size | {_fmt_bytes(st.get('dataSize', 0))} |")
|
|
1218
|
+
lines.append(f"| Storage Size | {_fmt_bytes(st.get('storageSize', 0))} |")
|
|
1219
|
+
lines.append(f"| Index Size | {_fmt_bytes(st.get('indexSize', 0))} |")
|
|
1220
|
+
lines.append(f"| Objects | {_fmt_count(st.get('objects', 0))} |")
|
|
1221
|
+
lines.append(f"| Collections | {st.get('collections', 0)} |")
|
|
1222
|
+
lines.append("")
|
|
1223
|
+
|
|
1224
|
+
# --- Collections ---
|
|
1225
|
+
if result.collection_stats:
|
|
1226
|
+
lines.append("## Collections")
|
|
1227
|
+
lines.append("")
|
|
1228
|
+
lines.append("| Collection | Documents | Data Size | Storage | Indexes |")
|
|
1229
|
+
lines.append("|------------|-----------|-----------|---------|---------|")
|
|
1230
|
+
# Sort by size descending
|
|
1231
|
+
sorted_colls = sorted(result.collection_stats, key=lambda c: c.get("size", 0), reverse=True)
|
|
1232
|
+
for c in sorted_colls:
|
|
1233
|
+
name = c.get("name", "?")
|
|
1234
|
+
count = _fmt_count(c.get("count", 0))
|
|
1235
|
+
size = _fmt_bytes(c.get("size", 0))
|
|
1236
|
+
storage = _fmt_bytes(c.get("storageSize", 0))
|
|
1237
|
+
nidx = c.get("nindexes", 0)
|
|
1238
|
+
lines.append(f"| {name} | {count} | {size} | {storage} | {nidx} |")
|
|
1239
|
+
lines.append("")
|
|
1240
|
+
|
|
1241
|
+
# --- Top Collections by Activity ---
|
|
1242
|
+
if result.top_collections:
|
|
1243
|
+
lines.append("## Top Collections by Activity")
|
|
1244
|
+
lines.append("")
|
|
1245
|
+
lines.append("| Namespace | Reads | Read Time | Writes | Write Time |")
|
|
1246
|
+
lines.append("|-----------|-------|-----------|--------|------------|")
|
|
1247
|
+
# Sort by total activity
|
|
1248
|
+
sorted_top = sorted(result.top_collections,
|
|
1249
|
+
key=lambda t: t.get("reads", 0) + t.get("writes", 0),
|
|
1250
|
+
reverse=True)
|
|
1251
|
+
for t in sorted_top[:20]:
|
|
1252
|
+
ns = t.get("ns", "?")
|
|
1253
|
+
reads = _fmt_count(t.get("reads", 0))
|
|
1254
|
+
read_time = _fmt_us(t.get("readTimeUs", 0))
|
|
1255
|
+
writes = _fmt_count(t.get("writes", 0))
|
|
1256
|
+
write_time = _fmt_us(t.get("writeTimeUs", 0))
|
|
1257
|
+
lines.append(f"| {ns} | {reads} | {read_time} | {writes} | {write_time} |")
|
|
1258
|
+
lines.append("")
|
|
1259
|
+
|
|
1260
|
+
# --- Replication ---
|
|
1261
|
+
if result.replication:
|
|
1262
|
+
lines.append("## Replication")
|
|
1263
|
+
lines.append("")
|
|
1264
|
+
lines.append("| Metric | Value |")
|
|
1265
|
+
lines.append("|--------|-------|")
|
|
1266
|
+
r = result.replication
|
|
1267
|
+
if r.get("setName"):
|
|
1268
|
+
lines.append(f"| Replica Set | {r['setName']} |")
|
|
1269
|
+
lines.append(f"| Is Writable Primary | {r.get('isWritablePrimary', 'N/A')} |")
|
|
1270
|
+
if r.get("primary"):
|
|
1271
|
+
lines.append(f"| Primary | {r['primary']} |")
|
|
1272
|
+
if r.get("hosts"):
|
|
1273
|
+
lines.append(f"| Hosts | {', '.join(r['hosts'])} |")
|
|
1274
|
+
lines.append("")
|
|
1275
|
+
|
|
1276
|
+
# --- Oplog ---
|
|
1277
|
+
if result.oplog:
|
|
1278
|
+
lines.append("## Oplog")
|
|
1279
|
+
lines.append("")
|
|
1280
|
+
lines.append("| Metric | Value |")
|
|
1281
|
+
lines.append("|--------|-------|")
|
|
1282
|
+
ol = result.oplog
|
|
1283
|
+
log_size = ol.get("logSizeMB", 0)
|
|
1284
|
+
used = ol.get("usedMB", 0)
|
|
1285
|
+
lines.append(f"| Log Size | {log_size:.0f} MB |")
|
|
1286
|
+
lines.append(f"| Used | {used:.0f} MB |")
|
|
1287
|
+
if log_size > 0:
|
|
1288
|
+
lines.append(f"| Usage | {round(100.0 * used / log_size, 1)}% |")
|
|
1289
|
+
hours = ol.get("timeDiffHours", 0)
|
|
1290
|
+
lines.append(f"| Time Window | {hours:.1f} hours |")
|
|
1291
|
+
lines.append("")
|
|
1292
|
+
|
|
1293
|
+
# --- Slow Queries ---
|
|
1294
|
+
if result.slow_queries:
|
|
1295
|
+
lines.append("## Slow Queries")
|
|
1296
|
+
lines.append("")
|
|
1297
|
+
lines.append("| Op | Namespace | Duration | Plan |")
|
|
1298
|
+
lines.append("|----|-----------|----------|------|")
|
|
1299
|
+
for q in result.slow_queries:
|
|
1300
|
+
op = q.get("op", "?")
|
|
1301
|
+
ns = q.get("ns", "?")
|
|
1302
|
+
millis = q.get("millis", 0)
|
|
1303
|
+
plan = q.get("planSummary", "")
|
|
1304
|
+
lines.append(f"| {op} | {ns} | {millis}ms | {plan} |")
|
|
1305
|
+
lines.append("")
|
|
1306
|
+
|
|
1307
|
+
# --- Active Operations ---
|
|
1308
|
+
if result.active_ops:
|
|
1309
|
+
lines.append("## Active Operations")
|
|
1310
|
+
lines.append("")
|
|
1311
|
+
lines.append("| OpID | Type | Namespace | Duration |")
|
|
1312
|
+
lines.append("|------|------|-----------|----------|")
|
|
1313
|
+
sorted_ops = sorted(result.active_ops, key=lambda o: o.get("microsecs_running", 0), reverse=True)
|
|
1314
|
+
for op in sorted_ops[:20]:
|
|
1315
|
+
opid = op.get("opid", "?")
|
|
1316
|
+
op_type = op.get("type", "?")
|
|
1317
|
+
ns = op.get("ns", "")
|
|
1318
|
+
us = op.get("microsecs_running", 0)
|
|
1319
|
+
lines.append(f"| {opid} | {op_type} | {ns} | {_fmt_us(us)} |")
|
|
1320
|
+
lines.append("")
|
|
1321
|
+
|
|
1322
|
+
# --- Infrastructure Trends ---
|
|
1323
|
+
if result.metrics_history and result.metrics_history.get("windows"):
|
|
1324
|
+
windows = result.metrics_history.get("windows", {})
|
|
1325
|
+
for window_label, window_data in windows.items():
|
|
1326
|
+
mh = window_data.get("metrics", {})
|
|
1327
|
+
if not mh:
|
|
1328
|
+
continue
|
|
1329
|
+
lines.append(f"## Infrastructure Trends ({window_label})")
|
|
1330
|
+
lines.append("")
|
|
1331
|
+
lines.append("| Metric | Current | Min | Max | Avg | Trend | Change |")
|
|
1332
|
+
lines.append("|--------|---------|-----|-----|-----|-------|--------|")
|
|
1333
|
+
display_order = [
|
|
1334
|
+
("cpu", "CPU"),
|
|
1335
|
+
("memory", "Memory"),
|
|
1336
|
+
("disk", "Disk"),
|
|
1337
|
+
("network_rx", "Network RX"),
|
|
1338
|
+
("network_tx", "Network TX"),
|
|
1339
|
+
]
|
|
1340
|
+
for key, label in display_order:
|
|
1341
|
+
if key in mh:
|
|
1342
|
+
m = mh[key]
|
|
1343
|
+
unit = m["unit"]
|
|
1344
|
+
trend = m.get("trend", {})
|
|
1345
|
+
direction = trend.get("direction", "?")
|
|
1346
|
+
change = trend.get("change_pct", 0)
|
|
1347
|
+
arrow = {"increasing": "^", "decreasing": "v", "stable": "~"}.get(direction, "?")
|
|
1348
|
+
spike_note = ""
|
|
1349
|
+
if m.get("spikes"):
|
|
1350
|
+
spike_note = f" ({m['spikes']['count']} spikes)"
|
|
1351
|
+
lines.append(
|
|
1352
|
+
f"| {label} | {m['current']} {unit} | {m['min']} | {m['max']} | "
|
|
1353
|
+
f"{m['avg']} | {arrow} {direction} | {change:+.1f}%{spike_note} |"
|
|
1354
|
+
)
|
|
1355
|
+
lines.append("")
|
|
1356
|
+
|
|
1357
|
+
# --- CPU / Memory summary ---
|
|
1358
|
+
if result.cpu_memory:
|
|
1359
|
+
lines.append("## Resource Usage")
|
|
1360
|
+
lines.append("")
|
|
1361
|
+
lines.append("| Metric | Value | Status |")
|
|
1362
|
+
lines.append("|--------|-------|--------|")
|
|
1363
|
+
cm = result.cpu_memory
|
|
1364
|
+
if "cpu_percent" in cm:
|
|
1365
|
+
cpu = cm["cpu_percent"]
|
|
1366
|
+
status = "Critical" if cpu > 85 else "Warning" if cpu > 70 else "Healthy"
|
|
1367
|
+
trend_str = _trend_indicator(result.metrics_history, "cpu")
|
|
1368
|
+
lines.append(f"| CPU Usage | {cpu} vCPU{trend_str} | {status} |")
|
|
1369
|
+
if cm.get("cpu_limit"):
|
|
1370
|
+
lines.append(f"| CPU Limit | {cm['cpu_limit']} vCPU | - |")
|
|
1371
|
+
if "memory_gb" in cm:
|
|
1372
|
+
mem_val = cm["memory_gb"]
|
|
1373
|
+
trend_str = _trend_indicator(result.metrics_history, "memory")
|
|
1374
|
+
utilization = ""
|
|
1375
|
+
if cm.get("memory_limit_gb"):
|
|
1376
|
+
pct = round((mem_val / cm["memory_limit_gb"]) * 100, 1)
|
|
1377
|
+
status = "Critical" if pct > 90 else "Warning" if pct > 80 else "Healthy"
|
|
1378
|
+
utilization = f" ({pct}% of {cm['memory_limit_gb']} GB)"
|
|
1379
|
+
else:
|
|
1380
|
+
status = "-"
|
|
1381
|
+
lines.append(f"| Memory Usage | {mem_val} GB{utilization}{trend_str} | {status} |")
|
|
1382
|
+
if result.disk_usage:
|
|
1383
|
+
lines.append(f"| Disk Usage | {result.disk_usage.get('used', 'N/A')} | - |")
|
|
1384
|
+
lines.append("")
|
|
1385
|
+
|
|
1386
|
+
# --- Recent Errors ---
|
|
1387
|
+
if result.recent_errors:
|
|
1388
|
+
lines.append("## Recent Errors")
|
|
1389
|
+
lines.append("")
|
|
1390
|
+
for error in result.recent_errors[:10]:
|
|
1391
|
+
lines.append(f"- {error[:150]}...")
|
|
1392
|
+
lines.append("")
|
|
1393
|
+
|
|
1394
|
+
# --- Recommendations ---
|
|
1395
|
+
if result.recommendations:
|
|
1396
|
+
lines.append("## Recommendations")
|
|
1397
|
+
lines.append("")
|
|
1398
|
+
for i, rec in enumerate(result.recommendations, 1):
|
|
1399
|
+
priority = rec["priority"].upper()
|
|
1400
|
+
lines.append(f"{i}. **[{priority}]** {rec['issue']}")
|
|
1401
|
+
lines.append(f" **Action:** {rec['action']}")
|
|
1402
|
+
if rec.get("explanation"):
|
|
1403
|
+
lines.append(f" **Why:** {rec['explanation']}")
|
|
1404
|
+
lines.append("")
|
|
1405
|
+
|
|
1406
|
+
# --- Errors ---
|
|
1407
|
+
if result.errors:
|
|
1408
|
+
lines.append("## Errors")
|
|
1409
|
+
lines.append("")
|
|
1410
|
+
for error in result.errors:
|
|
1411
|
+
lines.append(f"- {error}")
|
|
1412
|
+
lines.append("")
|
|
1413
|
+
|
|
1414
|
+
lines.append("=" * 60)
|
|
1415
|
+
lines.append("END OF REPORT")
|
|
1416
|
+
lines.append("=" * 60)
|
|
1417
|
+
|
|
1418
|
+
return "\n".join(lines)
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
# ---------------------------------------------------------------------------
|
|
1422
|
+
# Single-step debugging
|
|
1423
|
+
# ---------------------------------------------------------------------------
|
|
1424
|
+
|
|
1425
|
+
def run_single_step(args) -> int:
|
|
1426
|
+
"""Run a single collection step for debugging."""
|
|
1427
|
+
service = args.service
|
|
1428
|
+
_init_context(args)
|
|
1429
|
+
environment_id = dal._ctx.environment_id
|
|
1430
|
+
service_id = dal._ctx.service_id
|
|
1431
|
+
|
|
1432
|
+
if args.step == "ssh-test":
|
|
1433
|
+
print(f"Testing SSH to service: {service}", file=sys.stderr)
|
|
1434
|
+
code, stdout, stderr = run_ssh_query(service, "echo ok", timeout=45)
|
|
1435
|
+
print(f"Exit code: {code}")
|
|
1436
|
+
print(f"Stdout: {stdout.strip()}")
|
|
1437
|
+
if stderr:
|
|
1438
|
+
print(f"Stderr: {stderr.strip()}")
|
|
1439
|
+
return 0 if (code == 0 and "ok" in stdout) else 1
|
|
1440
|
+
|
|
1441
|
+
elif args.step == "server-status":
|
|
1442
|
+
print(f"Running serverStatus on: {service}", file=sys.stderr)
|
|
1443
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_SERVER_STATUS, timeout=30)
|
|
1444
|
+
print(f"Exit code: {code}")
|
|
1445
|
+
if code == 0 and stdout:
|
|
1446
|
+
data = _safe_json(stdout)
|
|
1447
|
+
if data:
|
|
1448
|
+
print(json.dumps(data, indent=2))
|
|
1449
|
+
else:
|
|
1450
|
+
print(f"Raw output:\n{stdout}")
|
|
1451
|
+
else:
|
|
1452
|
+
print(f"Error: {stderr or stdout}")
|
|
1453
|
+
return code
|
|
1454
|
+
|
|
1455
|
+
elif args.step == "db-stats":
|
|
1456
|
+
print(f"Running db.stats() on: {service}", file=sys.stderr)
|
|
1457
|
+
code, stdout, stderr = run_mongosh_query(service, QUERY_DB_STATS, timeout=30)
|
|
1458
|
+
print(f"Exit code: {code}")
|
|
1459
|
+
if code == 0 and stdout:
|
|
1460
|
+
data = _safe_json(stdout)
|
|
1461
|
+
if data:
|
|
1462
|
+
print(json.dumps(data, indent=2))
|
|
1463
|
+
else:
|
|
1464
|
+
print(f"Raw output:\n{stdout}")
|
|
1465
|
+
else:
|
|
1466
|
+
print(f"Error: {stderr or stdout}")
|
|
1467
|
+
return code
|
|
1468
|
+
|
|
1469
|
+
elif args.step == "logs":
|
|
1470
|
+
print(f"Fetching logs for: {service}", file=sys.stderr)
|
|
1471
|
+
logs = get_recent_logs(service, lines=LOG_LINES_DEFAULT,
|
|
1472
|
+
environment_id=environment_id,
|
|
1473
|
+
service_id=service_id)
|
|
1474
|
+
print(f"Lines fetched: {len(logs)}")
|
|
1475
|
+
for line in logs:
|
|
1476
|
+
print(line)
|
|
1477
|
+
return 0
|
|
1478
|
+
|
|
1479
|
+
elif args.step == "metrics":
|
|
1480
|
+
print(f"Fetching metrics for: {service}", file=sys.stderr)
|
|
1481
|
+
if environment_id and service_id:
|
|
1482
|
+
metrics = get_all_metrics_from_api(environment_id, service_id)
|
|
1483
|
+
if metrics:
|
|
1484
|
+
print(json.dumps(metrics, indent=2))
|
|
1485
|
+
else:
|
|
1486
|
+
print("Metrics API returned no data")
|
|
1487
|
+
return 1
|
|
1488
|
+
else:
|
|
1489
|
+
print("Missing environment_id or service_id from railway config")
|
|
1490
|
+
return 1
|
|
1491
|
+
return 0
|
|
1492
|
+
|
|
1493
|
+
return 1
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
# ---------------------------------------------------------------------------
|
|
1497
|
+
# CLI entry point
|
|
1498
|
+
# ---------------------------------------------------------------------------
|
|
1499
|
+
|
|
1500
|
+
def main():
|
|
1501
|
+
parser = argparse.ArgumentParser(
|
|
1502
|
+
description="MongoDB analysis for Railway services.",
|
|
1503
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1504
|
+
)
|
|
1505
|
+
|
|
1506
|
+
parser.add_argument("--service", required=True, help="Service name")
|
|
1507
|
+
parser.add_argument("--json", action="store_true",
|
|
1508
|
+
help="Output as JSON")
|
|
1509
|
+
parser.add_argument("--timeout", type=int, default=300,
|
|
1510
|
+
help="Timeout in seconds (default: 300)")
|
|
1511
|
+
parser.add_argument("--quiet", "-q", action="store_true",
|
|
1512
|
+
help="Suppress progress messages")
|
|
1513
|
+
parser.add_argument("--skip-logs", action="store_true",
|
|
1514
|
+
help="Skip log fetching for faster analysis")
|
|
1515
|
+
parser.add_argument("--metrics-hours", type=int, default=168,
|
|
1516
|
+
help="Hours of metrics history to fetch (default: 168, max: 168)")
|
|
1517
|
+
parser.add_argument("--step",
|
|
1518
|
+
choices=["ssh-test", "server-status", "db-stats", "logs", "metrics"],
|
|
1519
|
+
help="Run a single collection step for debugging")
|
|
1520
|
+
parser.add_argument("--project-id", help="Project ID (bypasses railway link)")
|
|
1521
|
+
parser.add_argument("--environment-id", help="Environment ID (bypasses railway link)")
|
|
1522
|
+
parser.add_argument("--service-id", help="Service ID (bypasses railway link)")
|
|
1523
|
+
|
|
1524
|
+
args = parser.parse_args()
|
|
1525
|
+
|
|
1526
|
+
if args.step:
|
|
1527
|
+
return run_single_step(args)
|
|
1528
|
+
|
|
1529
|
+
result = analyze_mongo(
|
|
1530
|
+
args.service,
|
|
1531
|
+
timeout=args.timeout,
|
|
1532
|
+
quiet=args.quiet,
|
|
1533
|
+
skip_logs=args.skip_logs,
|
|
1534
|
+
metrics_hours=min(args.metrics_hours, 168),
|
|
1535
|
+
project_id=args.project_id,
|
|
1536
|
+
environment_id=args.environment_id,
|
|
1537
|
+
service_id=args.service_id,
|
|
1538
|
+
)
|
|
1539
|
+
|
|
1540
|
+
if args.json:
|
|
1541
|
+
print(json.dumps(asdict(result), indent=2))
|
|
1542
|
+
else:
|
|
1543
|
+
print(format_report(result))
|
|
1544
|
+
|
|
1545
|
+
return 0
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
if __name__ == "__main__":
|
|
1549
|
+
sys.exit(main())
|