@synapta/skills 0.1.0 → 0.1.2

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.
Files changed (353) hide show
  1. package/dist/index.js +11 -4
  2. package/package.json +3 -4
  3. package/skills/ATTRIBUTION.md +80 -0
  4. package/skills/accessibility-audit/SKILL.md +325 -0
  5. package/skills/accessibility-audit/reference/wcag-checklist.md +103 -0
  6. package/skills/apns-notifier/SKILL.md +86 -0
  7. package/skills/approval-policy-enforcer/SKILL.md +66 -0
  8. package/skills/apps-sdk-builder/LICENSE.txt +201 -0
  9. package/skills/apps-sdk-builder/SKILL.md +328 -0
  10. package/skills/apps-sdk-builder/agents/openai.yaml +13 -0
  11. package/skills/apps-sdk-builder/references/app-archetypes.md +132 -0
  12. package/skills/apps-sdk-builder/references/apps-sdk-docs-workflow.md +135 -0
  13. package/skills/apps-sdk-builder/references/interactive-state-sync-patterns.md +113 -0
  14. package/skills/apps-sdk-builder/references/repo-contract-and-validation.md +93 -0
  15. package/skills/apps-sdk-builder/references/search-fetch-standard.md +67 -0
  16. package/skills/apps-sdk-builder/references/upstream-example-workflow.md +79 -0
  17. package/skills/apps-sdk-builder/references/window-openai-patterns.md +79 -0
  18. package/skills/apps-sdk-builder/scripts/scaffold_node_ext_apps.mjs +606 -0
  19. package/skills/architecture-selector/SKILL.md +64 -0
  20. package/skills/backlog-planner/SKILL.md +68 -0
  21. package/skills/carplay-entitlement-checker/SKILL.md +82 -0
  22. package/skills/concept-discovery/SKILL.md +517 -0
  23. package/skills/concept-discovery/assets/sample-analysis.json +81 -0
  24. package/skills/concept-discovery/expected_outputs/sample-enum-dictionary.md +25 -0
  25. package/skills/concept-discovery/expected_outputs/sample-page-user-list.md +83 -0
  26. package/skills/concept-discovery/expected_outputs/sample-prd-readme.md +43 -0
  27. package/skills/concept-discovery/references/framework-patterns.md +228 -0
  28. package/skills/concept-discovery/references/prd-quality-checklist.md +65 -0
  29. package/skills/concept-discovery/scripts/codebase_analyzer.py +732 -0
  30. package/skills/concept-discovery/scripts/prd_scaffolder.py +435 -0
  31. package/skills/dast-zap/SKILL.md +453 -0
  32. package/skills/dast-zap/assets/.gitkeep +9 -0
  33. package/skills/dast-zap/assets/github_action.yml +207 -0
  34. package/skills/dast-zap/assets/gitlab_ci.yml +226 -0
  35. package/skills/dast-zap/assets/zap_automation.yaml +196 -0
  36. package/skills/dast-zap/assets/zap_context.xml +192 -0
  37. package/skills/dast-zap/references/EXAMPLE.md +40 -0
  38. package/skills/dast-zap/references/api_testing_guide.md +475 -0
  39. package/skills/dast-zap/references/authentication_guide.md +431 -0
  40. package/skills/dast-zap/references/false_positive_handling.md +427 -0
  41. package/skills/dast-zap/references/owasp_mapping.md +255 -0
  42. package/skills/dep-sbom-scan/SKILL.md +466 -0
  43. package/skills/deploy-cloudflare/SKILL.md +930 -0
  44. package/skills/deploy-docker/SKILL.md +55 -0
  45. package/skills/deploy-fly/SKILL.md +228 -0
  46. package/skills/deploy-k8s/SKILL.md +108 -0
  47. package/skills/deploy-k8s/assets/logo.png +0 -0
  48. package/skills/deploy-k8s/docs/README.md +29 -0
  49. package/skills/deploy-k8s/docs/SUMMARY.md +56 -0
  50. package/skills/deploy-k8s/docs/advanced/token-efficiency.md +61 -0
  51. package/skills/deploy-k8s/docs/architecture/multi-tenancy.md +96 -0
  52. package/skills/deploy-k8s/docs/architecture/storage-and-state.md +102 -0
  53. package/skills/deploy-k8s/docs/architecture/workload-patterns.md +87 -0
  54. package/skills/deploy-k8s/docs/book.json +16 -0
  55. package/skills/deploy-k8s/docs/community/changelog.md +34 -0
  56. package/skills/deploy-k8s/docs/community/contributing.md +67 -0
  57. package/skills/deploy-k8s/docs/core-concepts/failure-modes.md +153 -0
  58. package/skills/deploy-k8s/docs/core-concepts/philosophy.md +83 -0
  59. package/skills/deploy-k8s/docs/core-concepts/workflow.md +124 -0
  60. package/skills/deploy-k8s/docs/examples/bad-patterns.md +47 -0
  61. package/skills/deploy-k8s/docs/examples/do-dont-checklist.md +37 -0
  62. package/skills/deploy-k8s/docs/examples/good-patterns.md +49 -0
  63. package/skills/deploy-k8s/docs/failure-modes/api-drift.md +104 -0
  64. package/skills/deploy-k8s/docs/failure-modes/fragile-rollouts.md +99 -0
  65. package/skills/deploy-k8s/docs/failure-modes/insecure-workload-defaults.md +80 -0
  66. package/skills/deploy-k8s/docs/failure-modes/network-exposure.md +98 -0
  67. package/skills/deploy-k8s/docs/failure-modes/privilege-sprawl.md +91 -0
  68. package/skills/deploy-k8s/docs/failure-modes/resource-starvation.md +85 -0
  69. package/skills/deploy-k8s/docs/getting-started/installation.md +152 -0
  70. package/skills/deploy-k8s/docs/getting-started/quick-start.md +115 -0
  71. package/skills/deploy-k8s/docs/guides/helm-patterns.md +71 -0
  72. package/skills/deploy-k8s/docs/guides/kustomize-patterns.md +65 -0
  73. package/skills/deploy-k8s/docs/guides/observability.md +67 -0
  74. package/skills/deploy-k8s/docs/guides/security-hardening.md +59 -0
  75. package/skills/deploy-k8s/docs/guides/validation-and-policy.md +66 -0
  76. package/skills/deploy-k8s/docs/integrations/mcp-integration.md +52 -0
  77. package/skills/deploy-k8s/docs/package-lock.json +2892 -0
  78. package/skills/deploy-k8s/docs/package.json +13 -0
  79. package/skills/deploy-k8s/references/api-drift.md +298 -0
  80. package/skills/deploy-k8s/references/conditional/aks-patterns.md +70 -0
  81. package/skills/deploy-k8s/references/conditional/eks-patterns.md +79 -0
  82. package/skills/deploy-k8s/references/conditional/gitops-controllers.md +71 -0
  83. package/skills/deploy-k8s/references/conditional/gke-patterns.md +74 -0
  84. package/skills/deploy-k8s/references/conditional/observability-stacks.md +80 -0
  85. package/skills/deploy-k8s/references/conditional/openshift-patterns.md +67 -0
  86. package/skills/deploy-k8s/references/daemonset-operator-patterns.md +155 -0
  87. package/skills/deploy-k8s/references/deployment-patterns.md +146 -0
  88. package/skills/deploy-k8s/references/do-dont-patterns.md +87 -0
  89. package/skills/deploy-k8s/references/examples-bad.md +282 -0
  90. package/skills/deploy-k8s/references/examples-good.md +440 -0
  91. package/skills/deploy-k8s/references/fragile-rollouts.md +303 -0
  92. package/skills/deploy-k8s/references/helm-patterns.md +203 -0
  93. package/skills/deploy-k8s/references/insecure-workload-defaults.md +300 -0
  94. package/skills/deploy-k8s/references/job-patterns.md +120 -0
  95. package/skills/deploy-k8s/references/kustomize-patterns.md +239 -0
  96. package/skills/deploy-k8s/references/multi-tenancy.md +343 -0
  97. package/skills/deploy-k8s/references/network-exposure.md +481 -0
  98. package/skills/deploy-k8s/references/observability.md +302 -0
  99. package/skills/deploy-k8s/references/privilege-sprawl.md +273 -0
  100. package/skills/deploy-k8s/references/resource-starvation.md +374 -0
  101. package/skills/deploy-k8s/references/security-hardening.md +209 -0
  102. package/skills/deploy-k8s/references/stateful-patterns.md +130 -0
  103. package/skills/deploy-k8s/references/storage-and-state.md +330 -0
  104. package/skills/deploy-k8s/references/validation-and-policy.md +242 -0
  105. package/skills/deploy-railway/SKILL.md +235 -0
  106. package/skills/deploy-railway/references/analyze-db-mongo.md +84 -0
  107. package/skills/deploy-railway/references/analyze-db-mysql.md +254 -0
  108. package/skills/deploy-railway/references/analyze-db-postgres.md +479 -0
  109. package/skills/deploy-railway/references/analyze-db-redis.md +208 -0
  110. package/skills/deploy-railway/references/analyze-db.md +344 -0
  111. package/skills/deploy-railway/references/configure.md +309 -0
  112. package/skills/deploy-railway/references/deploy.md +195 -0
  113. package/skills/deploy-railway/references/operate.md +214 -0
  114. package/skills/deploy-railway/references/request.md +248 -0
  115. package/skills/deploy-railway/references/setup.md +312 -0
  116. package/skills/deploy-railway/scripts/analyze-mongo.py +1549 -0
  117. package/skills/deploy-railway/scripts/analyze-mysql.py +1195 -0
  118. package/skills/deploy-railway/scripts/analyze-postgres.py +3058 -0
  119. package/skills/deploy-railway/scripts/analyze-redis.py +1090 -0
  120. package/skills/deploy-railway/scripts/dal.py +671 -0
  121. package/skills/deploy-railway/scripts/enable-pg-stats.py +170 -0
  122. package/skills/deploy-railway/scripts/pg-extensions.py +370 -0
  123. package/skills/deploy-railway/scripts/railway-api.sh +52 -0
  124. package/skills/deploy-ssh/SKILL.md +91 -0
  125. package/skills/deploy-vercel/SKILL.md +304 -0
  126. package/skills/deploy-vercel/resources/deploy-codex.sh +301 -0
  127. package/skills/deploy-vercel/resources/deploy.sh +301 -0
  128. package/skills/docs-runbooks/SKILL.md +399 -0
  129. package/skills/drive-status-renderer/SKILL.md +62 -0
  130. package/skills/iac-scan/SKILL.md +680 -0
  131. package/skills/iac-scan/assets/.gitkeep +9 -0
  132. package/skills/iac-scan/assets/checkov_config.yaml +94 -0
  133. package/skills/iac-scan/assets/github_actions.yml +199 -0
  134. package/skills/iac-scan/assets/gitlab_ci.yml +218 -0
  135. package/skills/iac-scan/assets/pre_commit_config.yaml +92 -0
  136. package/skills/iac-scan/references/EXAMPLE.md +40 -0
  137. package/skills/iac-scan/references/compliance_mapping.md +237 -0
  138. package/skills/iac-scan/references/custom_policies.md +460 -0
  139. package/skills/iac-scan/references/suppression_guide.md +431 -0
  140. package/skills/incident-briefing/SKILL.md +66 -0
  141. package/skills/incident-triage/SKILL.md +481 -0
  142. package/{LICENSE → skills/mcp-builder/LICENSE.txt} +15 -14
  143. package/skills/mcp-builder/SKILL.md +244 -0
  144. package/skills/mcp-builder/reference/evaluation.md +602 -0
  145. package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  146. package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  147. package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  148. package/skills/mcp-builder/scripts/connections.py +151 -0
  149. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  150. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  151. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  152. package/skills/mobile-pairing/SKILL.md +52 -0
  153. package/skills/ops-sre/SKILL.md +297 -0
  154. package/skills/playwright-qa/LICENSE.txt +201 -0
  155. package/skills/playwright-qa/NOTICE.txt +14 -0
  156. package/skills/playwright-qa/SKILL.md +156 -0
  157. package/skills/playwright-qa/agents/openai.yaml +6 -0
  158. package/skills/playwright-qa/assets/playwright-small.svg +3 -0
  159. package/skills/playwright-qa/assets/playwright.png +0 -0
  160. package/skills/playwright-qa/references/cli.md +116 -0
  161. package/skills/playwright-qa/references/workflows.md +95 -0
  162. package/skills/playwright-qa/scripts/playwright_cli.sh +25 -0
  163. package/skills/release-publish/SKILL.md +85 -0
  164. package/skills/repo-bootstrap/SKILL.md +92 -0
  165. package/skills/repo-bootstrap/assets/example-workflows/validate-agents.yml +89 -0
  166. package/skills/repo-bootstrap/assets/root-thin.md +141 -0
  167. package/skills/repo-bootstrap/assets/root-verbose.md +149 -0
  168. package/skills/repo-bootstrap/assets/scoped/backend-go.md +107 -0
  169. package/skills/repo-bootstrap/assets/scoped/backend-php.md +94 -0
  170. package/skills/repo-bootstrap/assets/scoped/backend-python.md +84 -0
  171. package/skills/repo-bootstrap/assets/scoped/backend-typescript.md +89 -0
  172. package/skills/repo-bootstrap/assets/scoped/claude-code-skill.md +101 -0
  173. package/skills/repo-bootstrap/assets/scoped/cli.md +83 -0
  174. package/skills/repo-bootstrap/assets/scoped/concourse.md +196 -0
  175. package/skills/repo-bootstrap/assets/scoped/ddev.md +68 -0
  176. package/skills/repo-bootstrap/assets/scoped/docker.md +160 -0
  177. package/skills/repo-bootstrap/assets/scoped/documentation.md +98 -0
  178. package/skills/repo-bootstrap/assets/scoped/examples.md +96 -0
  179. package/skills/repo-bootstrap/assets/scoped/frontend-typescript.md +88 -0
  180. package/skills/repo-bootstrap/assets/scoped/github-actions.md +174 -0
  181. package/skills/repo-bootstrap/assets/scoped/gitlab-ci.md +174 -0
  182. package/skills/repo-bootstrap/assets/scoped/oro-bundle.md +209 -0
  183. package/skills/repo-bootstrap/assets/scoped/oro-project.md +170 -0
  184. package/skills/repo-bootstrap/assets/scoped/python-modern.md +170 -0
  185. package/skills/repo-bootstrap/assets/scoped/resources.md +96 -0
  186. package/skills/repo-bootstrap/assets/scoped/skill-repo.md +139 -0
  187. package/skills/repo-bootstrap/assets/scoped/symfony.md +168 -0
  188. package/skills/repo-bootstrap/assets/scoped/testing.md +87 -0
  189. package/skills/repo-bootstrap/assets/scoped/typo3-docs.md +103 -0
  190. package/skills/repo-bootstrap/assets/scoped/typo3-extension.md +133 -0
  191. package/skills/repo-bootstrap/assets/scoped/typo3-project.md +137 -0
  192. package/skills/repo-bootstrap/assets/scoped/typo3-testing.md +80 -0
  193. package/skills/repo-bootstrap/checkpoints.yaml +279 -0
  194. package/skills/repo-bootstrap/evals/evals.json +385 -0
  195. package/skills/repo-bootstrap/references/ai-contribution-guidelines.md +63 -0
  196. package/skills/repo-bootstrap/references/ai-tool-compatibility.md +223 -0
  197. package/skills/repo-bootstrap/references/directory-coverage.md +82 -0
  198. package/skills/repo-bootstrap/references/examples/coding-agent-cli/AGENTS.md +70 -0
  199. package/skills/repo-bootstrap/references/examples/coding-agent-cli/go.mod +3 -0
  200. package/skills/repo-bootstrap/references/examples/coding-agent-cli/scripts-AGENTS.md +389 -0
  201. package/skills/repo-bootstrap/references/examples/express-api-ts/.env.example +13 -0
  202. package/skills/repo-bootstrap/references/examples/express-api-ts/AGENTS.md +91 -0
  203. package/skills/repo-bootstrap/references/examples/express-api-ts/package.json +33 -0
  204. package/skills/repo-bootstrap/references/examples/express-api-ts/pnpm-lock.yaml +3 -0
  205. package/skills/repo-bootstrap/references/examples/express-api-ts/src/AGENTS.md +91 -0
  206. package/skills/repo-bootstrap/references/examples/express-api-ts/src/config.ts +28 -0
  207. package/skills/repo-bootstrap/references/examples/express-api-ts/src/controllers/userController.ts +74 -0
  208. package/skills/repo-bootstrap/references/examples/express-api-ts/src/index.ts +26 -0
  209. package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/errorHandler.ts +45 -0
  210. package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/requestLogger.ts +18 -0
  211. package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/health.ts +18 -0
  212. package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/users.ts +13 -0
  213. package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/errors.ts +40 -0
  214. package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/logger.ts +14 -0
  215. package/skills/repo-bootstrap/references/examples/express-api-ts/tsconfig.json +24 -0
  216. package/skills/repo-bootstrap/references/examples/fastapi-app/.env.example +19 -0
  217. package/skills/repo-bootstrap/references/examples/fastapi-app/AGENTS.md +92 -0
  218. package/skills/repo-bootstrap/references/examples/fastapi-app/pyproject.toml +88 -0
  219. package/skills/repo-bootstrap/references/examples/fastapi-app/src/AGENTS.md +85 -0
  220. package/skills/repo-bootstrap/references/examples/fastapi-app/src/__init__.py +3 -0
  221. package/skills/repo-bootstrap/references/examples/fastapi-app/src/config.py +49 -0
  222. package/skills/repo-bootstrap/references/examples/fastapi-app/src/main.py +66 -0
  223. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/__init__.py +13 -0
  224. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/item.py +43 -0
  225. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/user.py +40 -0
  226. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/__init__.py +5 -0
  227. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/health.py +20 -0
  228. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/items.py +61 -0
  229. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/users.py +55 -0
  230. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/__init__.py +6 -0
  231. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/item_service.py +77 -0
  232. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/user_service.py +69 -0
  233. package/skills/repo-bootstrap/references/examples/fastapi-app/uv.lock +4 -0
  234. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/.scopes +3 -0
  235. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/AGENTS.md +86 -0
  236. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/package.json +20 -0
  237. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/src/App.tsx +5 -0
  238. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/cmd/api/main.go +7 -0
  239. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/go.mod +2 -0
  240. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/main.go +7 -0
  241. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/.scopes +3 -0
  242. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/AGENTS.md +89 -0
  243. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/go.mod +2 -0
  244. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/AGENTS.md +90 -0
  245. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/package.json +17 -0
  246. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/App.tsx +1 -0
  247. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Button.tsx +1 -0
  248. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Footer.tsx +1 -0
  249. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Header.tsx +1 -0
  250. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Sidebar.tsx +1 -0
  251. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/main.go +7 -0
  252. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package-lock.json +0 -0
  253. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package.json +12 -0
  254. package/skills/repo-bootstrap/references/examples/ldap-selfservice/AGENTS.md +70 -0
  255. package/skills/repo-bootstrap/references/examples/ldap-selfservice/go.mod +3 -0
  256. package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-AGENTS.md +371 -0
  257. package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-web-AGENTS.md +448 -0
  258. package/skills/repo-bootstrap/references/examples/php-with-frontend/.scopes +3 -0
  259. package/skills/repo-bootstrap/references/examples/php-with-frontend/AGENTS.md +91 -0
  260. package/skills/repo-bootstrap/references/examples/php-with-frontend/composer.json +8 -0
  261. package/skills/repo-bootstrap/references/examples/php-with-frontend/package.json +15 -0
  262. package/skills/repo-bootstrap/references/examples/php-with-frontend/pnpm-lock.yaml +0 -0
  263. package/skills/repo-bootstrap/references/examples/php-with-frontend/src/Controller.php +3 -0
  264. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/AGENTS.md +92 -0
  265. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/package.json +26 -0
  266. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/App.tsx +3 -0
  267. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Button.tsx +10 -0
  268. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Footer.tsx +9 -0
  269. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Header.tsx +9 -0
  270. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/main.tsx +3 -0
  271. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/tsconfig.json +13 -0
  272. package/skills/repo-bootstrap/references/examples/pnpm-workspace/AGENTS.md +75 -0
  273. package/skills/repo-bootstrap/references/examples/pnpm-workspace/package.json +7 -0
  274. package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/package.json +11 -0
  275. package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/src/index.ts +11 -0
  276. package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-lock.yaml +42 -0
  277. package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-workspace.yaml +2 -0
  278. package/skills/repo-bootstrap/references/examples/simple-ldap-go/AGENTS.md +70 -0
  279. package/skills/repo-bootstrap/references/examples/simple-ldap-go/examples-AGENTS.md +45 -0
  280. package/skills/repo-bootstrap/references/examples/simple-ldap-go/go.mod +3 -0
  281. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/AGENTS.md +70 -0
  282. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/Classes-AGENTS.md +392 -0
  283. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/composer.json +8 -0
  284. package/skills/repo-bootstrap/references/feedback-memory-schema.md +135 -0
  285. package/skills/repo-bootstrap/references/git-hooks-setup.md +79 -0
  286. package/skills/repo-bootstrap/references/output-structure.md +124 -0
  287. package/skills/repo-bootstrap/references/scripts-guide.md +175 -0
  288. package/skills/repo-bootstrap/references/verification-guide.md +137 -0
  289. package/skills/repo-bootstrap/scripts/analyze-git-history.sh +315 -0
  290. package/skills/repo-bootstrap/scripts/check-freshness.sh +230 -0
  291. package/skills/repo-bootstrap/scripts/detect-golden-samples.sh +161 -0
  292. package/skills/repo-bootstrap/scripts/detect-heuristics.sh +93 -0
  293. package/skills/repo-bootstrap/scripts/detect-project.sh +486 -0
  294. package/skills/repo-bootstrap/scripts/detect-scopes.sh +330 -0
  295. package/skills/repo-bootstrap/scripts/detect-utilities.sh +133 -0
  296. package/skills/repo-bootstrap/scripts/extract-adrs.sh +194 -0
  297. package/skills/repo-bootstrap/scripts/extract-agent-configs.sh +331 -0
  298. package/skills/repo-bootstrap/scripts/extract-architecture-rules.sh +522 -0
  299. package/skills/repo-bootstrap/scripts/extract-ci-commands.sh +385 -0
  300. package/skills/repo-bootstrap/scripts/extract-ci-rules.sh +384 -0
  301. package/skills/repo-bootstrap/scripts/extract-commands.sh +358 -0
  302. package/skills/repo-bootstrap/scripts/extract-documentation.sh +308 -0
  303. package/skills/repo-bootstrap/scripts/extract-github-rulesets.sh +96 -0
  304. package/skills/repo-bootstrap/scripts/extract-github-settings.sh +88 -0
  305. package/skills/repo-bootstrap/scripts/extract-ide-settings.sh +228 -0
  306. package/skills/repo-bootstrap/scripts/extract-platform-files.sh +290 -0
  307. package/skills/repo-bootstrap/scripts/extract-quality-configs.sh +442 -0
  308. package/skills/repo-bootstrap/scripts/generate-agents.sh +2424 -0
  309. package/skills/repo-bootstrap/scripts/generate-file-map.sh +153 -0
  310. package/skills/repo-bootstrap/scripts/lib/config-root.sh +211 -0
  311. package/skills/repo-bootstrap/scripts/lib/summary.sh +244 -0
  312. package/skills/repo-bootstrap/scripts/lib/template.sh +397 -0
  313. package/skills/repo-bootstrap/scripts/validate-structure.sh +324 -0
  314. package/skills/repo-bootstrap/scripts/verify-commands.sh +615 -0
  315. package/skills/repo-bootstrap/scripts/verify-content.sh +302 -0
  316. package/skills/schema-api-contracts/SKILL.md +56 -0
  317. package/skills/secret-hygiene/SKILL.md +511 -0
  318. package/skills/secret-hygiene/assets/.gitkeep +9 -0
  319. package/skills/secret-hygiene/assets/config-balanced.toml +81 -0
  320. package/skills/secret-hygiene/assets/config-custom.toml +178 -0
  321. package/skills/secret-hygiene/assets/config-strict.toml +48 -0
  322. package/skills/secret-hygiene/assets/github-action.yml +181 -0
  323. package/skills/secret-hygiene/assets/gitlab-ci.yml +257 -0
  324. package/skills/secret-hygiene/assets/precommit-config.yaml +70 -0
  325. package/skills/secret-hygiene/references/EXAMPLE.md +40 -0
  326. package/skills/secret-hygiene/references/compliance_mapping.md +538 -0
  327. package/skills/secret-hygiene/references/detection_rules.md +276 -0
  328. package/skills/secret-hygiene/references/false_positives.md +598 -0
  329. package/skills/secret-hygiene/references/remediation_guide.md +530 -0
  330. package/skills/stack-selector/SKILL.md +56 -0
  331. package/skills/telegram-control/SKILL.md +110 -0
  332. package/skills/telegram-control/references/architecture.md +184 -0
  333. package/skills/telegram-control/references/convex.md +173 -0
  334. package/skills/telegram-control/references/error_handling.md +212 -0
  335. package/skills/telegram-control/references/initial_setup.md +165 -0
  336. package/skills/telegram-control/references/telegram_api.md +156 -0
  337. package/skills/telegram-control/scripts/cancel_message.ts +53 -0
  338. package/skills/telegram-control/scripts/list_scheduled.ts +103 -0
  339. package/skills/telegram-control/scripts/logger.ts +121 -0
  340. package/skills/telegram-control/scripts/proxy-util.ts +11 -0
  341. package/skills/telegram-control/scripts/schedule_message.ts +216 -0
  342. package/skills/telegram-control/scripts/send_message.ts +115 -0
  343. package/skills/telegram-control/scripts/setup.ts +185 -0
  344. package/skills/telegram-control/scripts/types.ts +75 -0
  345. package/skills/telegram-control/scripts/view_history.ts +74 -0
  346. package/skills/test-strategy/SKILL.md +352 -0
  347. package/skills/threat-model/SKILL.md +303 -0
  348. package/skills/threat-model/examples/example-output.md +196 -0
  349. package/skills/threat-model/template.md +96 -0
  350. package/skills/ts-lint/SKILL.md +80 -0
  351. package/skills/ui-flow/SKILL.md +668 -0
  352. package/skills/voice-command-router/SKILL.md +51 -0
  353. 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())