@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,1195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MySQL analysis for Railway deployments.
|
|
4
|
+
|
|
5
|
+
Produces a comprehensive report covering:
|
|
6
|
+
- Deployment status & resource metrics (CPU, memory, disk)
|
|
7
|
+
- Connection overview
|
|
8
|
+
- Query throughput & efficiency
|
|
9
|
+
- InnoDB buffer pool & row operations
|
|
10
|
+
- Lock contention
|
|
11
|
+
- Top queries (from performance_schema)
|
|
12
|
+
- Table sizes
|
|
13
|
+
- Active processes
|
|
14
|
+
- Recommendations
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
analyze-mysql.py --service <name>
|
|
18
|
+
analyze-mysql.py --service <name> --json
|
|
19
|
+
analyze-mysql.py --service <name> --step ssh-test
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
29
|
+
from dataclasses import asdict, dataclass, field
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
import dal
|
|
34
|
+
from dal import (
|
|
35
|
+
LOG_LINES_DEFAULT, ProgressTimer, RailwayContext,
|
|
36
|
+
_init_context, progress, run_railway_command, run_ssh_query,
|
|
37
|
+
get_railway_status, get_deployment_status,
|
|
38
|
+
get_all_metrics_from_api, _analyze_window, _build_metrics_history,
|
|
39
|
+
get_recent_logs,
|
|
40
|
+
_safe_int, _safe_float, _format_uptime, _trend_indicator,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Result container
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class MySQLAnalysisResult:
|
|
50
|
+
"""Container for MySQL analysis results."""
|
|
51
|
+
service: str
|
|
52
|
+
db_type: str
|
|
53
|
+
timestamp: str
|
|
54
|
+
deployment_status: str = "UNKNOWN"
|
|
55
|
+
|
|
56
|
+
# Resource metrics from Railway API
|
|
57
|
+
disk_usage: Optional[Dict[str, Any]] = None
|
|
58
|
+
cpu_memory: Optional[Dict[str, Any]] = None
|
|
59
|
+
metrics_history: Optional[Dict[str, Any]] = None
|
|
60
|
+
|
|
61
|
+
# MySQL data
|
|
62
|
+
overview: Optional[Dict[str, Any]] = None
|
|
63
|
+
query_throughput: Optional[Dict[str, Any]] = None
|
|
64
|
+
innodb_row_ops: Optional[Dict[str, Any]] = None
|
|
65
|
+
query_efficiency: Optional[Dict[str, Any]] = None
|
|
66
|
+
innodb_buffer_pool: Optional[Dict[str, Any]] = None
|
|
67
|
+
innodb_io: Optional[Dict[str, Any]] = None
|
|
68
|
+
network: Optional[Dict[str, Any]] = None
|
|
69
|
+
locks: Optional[Dict[str, Any]] = None
|
|
70
|
+
table_cache: Optional[Dict[str, Any]] = None
|
|
71
|
+
top_queries: List[Dict[str, Any]] = field(default_factory=list)
|
|
72
|
+
top_queries_status: Optional[str] = None
|
|
73
|
+
tables: List[Dict[str, Any]] = field(default_factory=list)
|
|
74
|
+
active_processes: List[Dict[str, Any]] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
# Logs
|
|
77
|
+
recent_logs: List[str] = field(default_factory=list)
|
|
78
|
+
recent_errors: List[str] = field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
# Metadata
|
|
81
|
+
collection_status: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
82
|
+
errors: List[str] = field(default_factory=list)
|
|
83
|
+
recommendations: List[Dict[str, str]] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# MySQL-specific helpers
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def run_mysql_query(service: str, query: str, timeout: int = 30) -> Tuple[int, str]:
|
|
91
|
+
"""Run a MySQL query via SSH and return (returncode, output).
|
|
92
|
+
|
|
93
|
+
Uses -B (batch) mode which produces tab-separated output with headers.
|
|
94
|
+
Filters out the mysql CLI password warning.
|
|
95
|
+
"""
|
|
96
|
+
import base64
|
|
97
|
+
query = " ".join(query.split())
|
|
98
|
+
# Base64-encode the query to avoid all shell quoting issues
|
|
99
|
+
# (single quotes in SQL IN clauses break bash -c '...' wrapping)
|
|
100
|
+
encoded = base64.b64encode(query.encode()).decode()
|
|
101
|
+
command = (
|
|
102
|
+
f'''bash +H -c 'echo {encoded} | base64 -d | MYSQL_PWD="$MYSQLPASSWORD" mysql -h localhost -P 3306 '''
|
|
103
|
+
f'''-u "$MYSQLUSER" -D "$MYSQLDATABASE" --default-character-set=utf8mb4 '''
|
|
104
|
+
f'''-B' '''
|
|
105
|
+
)
|
|
106
|
+
code, stdout, stderr = run_ssh_query(service, command, timeout)
|
|
107
|
+
# Filter out the password warning from stdout (mysql sometimes writes it there)
|
|
108
|
+
lines = []
|
|
109
|
+
for line in stdout.split("\n"):
|
|
110
|
+
if "Using a password on the command line" in line:
|
|
111
|
+
continue
|
|
112
|
+
lines.append(line)
|
|
113
|
+
stdout = "\n".join(lines)
|
|
114
|
+
if code != 0:
|
|
115
|
+
# Also filter warning from stderr
|
|
116
|
+
stderr_clean = "\n".join(
|
|
117
|
+
l for l in stderr.split("\n")
|
|
118
|
+
if "Using a password on the command line" not in l
|
|
119
|
+
)
|
|
120
|
+
return code, stderr_clean or stdout
|
|
121
|
+
return 0, stdout
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_mysql_batch(output: str) -> List[Dict[str, str]]:
|
|
125
|
+
"""Parse MySQL -B (batch/tab-separated) output into list of dicts.
|
|
126
|
+
|
|
127
|
+
First line is column headers, subsequent lines are values.
|
|
128
|
+
"""
|
|
129
|
+
lines = [l for l in output.strip().split("\n") if l.strip()]
|
|
130
|
+
if len(lines) < 1:
|
|
131
|
+
return []
|
|
132
|
+
headers = lines[0].split("\t")
|
|
133
|
+
rows = []
|
|
134
|
+
for line in lines[1:]:
|
|
135
|
+
values = line.split("\t")
|
|
136
|
+
if len(values) == len(headers):
|
|
137
|
+
rows.append(dict(zip(headers, values)))
|
|
138
|
+
return rows
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def parse_mysql_kv(output: str) -> Dict[str, str]:
|
|
142
|
+
"""Parse MySQL SHOW output (Variable_name / Value pairs) into a dict."""
|
|
143
|
+
rows = parse_mysql_batch(output)
|
|
144
|
+
result: Dict[str, str] = {}
|
|
145
|
+
for row in rows:
|
|
146
|
+
name = row.get("Variable_name", "")
|
|
147
|
+
value = row.get("Value", "")
|
|
148
|
+
if name:
|
|
149
|
+
result[name] = value
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Railway context / status / metrics helpers
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# MySQL queries
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
QUERY_GLOBAL_STATUS = """SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Threads_running','Max_used_connections','Questions','Slow_queries','Com_select','Com_insert','Com_update','Com_delete','Innodb_buffer_pool_read_requests','Innodb_buffer_pool_reads','Innodb_buffer_pool_pages_data','Innodb_buffer_pool_pages_free','Innodb_buffer_pool_pages_dirty','Innodb_row_lock_waits','Innodb_row_lock_time','Uptime','Bytes_received','Bytes_sent','Connections','Aborted_clients','Aborted_connects','Innodb_rows_read','Innodb_rows_inserted','Innodb_rows_updated','Innodb_rows_deleted','Innodb_data_reads','Innodb_data_writes','Innodb_buffer_pool_bytes_data','Innodb_buffer_pool_bytes_dirty','Created_tmp_disk_tables','Created_tmp_tables','Handler_read_rnd_next','Handler_read_first','Handler_read_key','Select_full_join','Select_range','Sort_merge_passes','Table_locks_waited','Table_locks_immediate','Open_tables','Opened_tables')"""
|
|
162
|
+
|
|
163
|
+
QUERY_VARIABLES = """SHOW VARIABLES WHERE Variable_name IN ('max_connections','innodb_buffer_pool_size','long_query_time','version','table_open_cache','performance_schema')"""
|
|
164
|
+
|
|
165
|
+
QUERY_TABLE_SIZES = """SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, DATA_LENGTH + INDEX_LENGTH AS TOTAL_SIZE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TOTAL_SIZE DESC LIMIT 15"""
|
|
166
|
+
|
|
167
|
+
QUERY_PROCESSLIST = """SHOW PROCESSLIST"""
|
|
168
|
+
|
|
169
|
+
QUERY_TOP_QUERIES = """SELECT DIGEST, LEFT(DIGEST_TEXT, 200) AS DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1000000000, 2) AS TOTAL_LATENCY_MS, ROUND(AVG_TIMER_WAIT/1000000000, 2) AS AVG_LATENCY_MS, SUM_ROWS_EXAMINED, SUM_ROWS_SENT, SUM_CREATED_TMP_DISK_TABLES, SUM_NO_INDEX_USED FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST IS NOT NULL AND COUNT_STAR > 0 ORDER BY SUM_TIMER_WAIT DESC LIMIT 15"""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# MySQL data collection
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
def collect_mysql_data(service: str, timeout: int = 30) -> Dict[str, Any]:
|
|
177
|
+
"""Collect all MySQL metrics via SSH.
|
|
178
|
+
|
|
179
|
+
Batches queries into two SSH calls for efficiency:
|
|
180
|
+
1. SHOW GLOBAL STATUS + SHOW VARIABLES
|
|
181
|
+
2. Table sizes + processlist + top queries (performance_schema)
|
|
182
|
+
|
|
183
|
+
Returns a dict with raw parsed data keyed by section.
|
|
184
|
+
"""
|
|
185
|
+
data: Dict[str, Any] = {
|
|
186
|
+
"global_status": {},
|
|
187
|
+
"variables": {},
|
|
188
|
+
"tables": [],
|
|
189
|
+
"processlist": [],
|
|
190
|
+
"top_queries": [],
|
|
191
|
+
"errors": [],
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# --- Batch 1: SHOW GLOBAL STATUS; SHOW VARIABLES ---
|
|
195
|
+
batch1_query = QUERY_GLOBAL_STATUS + "; " + QUERY_VARIABLES
|
|
196
|
+
code, output = run_mysql_query(service, batch1_query, timeout=timeout)
|
|
197
|
+
if code != 0:
|
|
198
|
+
data["errors"].append(f"Batch 1 (status/variables) failed: {output}")
|
|
199
|
+
else:
|
|
200
|
+
# MySQL concatenates the two result sets; split them by detecting
|
|
201
|
+
# a second header line starting with Variable_name
|
|
202
|
+
sections = _split_mysql_resultsets(output, "Variable_name")
|
|
203
|
+
if len(sections) >= 1:
|
|
204
|
+
data["global_status"] = parse_mysql_kv(sections[0])
|
|
205
|
+
if len(sections) >= 2:
|
|
206
|
+
data["variables"] = parse_mysql_kv(sections[1])
|
|
207
|
+
|
|
208
|
+
# --- Batch 2: tables + processlist + top queries ---
|
|
209
|
+
batch2_query = QUERY_TABLE_SIZES + "; " + QUERY_PROCESSLIST + "; " + QUERY_TOP_QUERIES
|
|
210
|
+
code, output = run_mysql_query(service, batch2_query, timeout=timeout)
|
|
211
|
+
if code != 0:
|
|
212
|
+
# Top queries may fail if performance_schema is off; try without
|
|
213
|
+
batch2_fallback = QUERY_TABLE_SIZES + "; " + QUERY_PROCESSLIST
|
|
214
|
+
code2, output2 = run_mysql_query(service, batch2_fallback, timeout=timeout)
|
|
215
|
+
if code2 != 0:
|
|
216
|
+
data["errors"].append(f"Batch 2 (tables/processlist) failed: {output}")
|
|
217
|
+
else:
|
|
218
|
+
sections = _split_mysql_resultsets_multi(output2, [
|
|
219
|
+
"TABLE_NAME",
|
|
220
|
+
"Id",
|
|
221
|
+
])
|
|
222
|
+
if len(sections) >= 1:
|
|
223
|
+
data["tables"] = parse_mysql_batch(sections[0])
|
|
224
|
+
if len(sections) >= 2:
|
|
225
|
+
data["processlist"] = parse_mysql_batch(sections[1])
|
|
226
|
+
else:
|
|
227
|
+
sections = _split_mysql_resultsets_multi(output, [
|
|
228
|
+
"TABLE_NAME",
|
|
229
|
+
"Id",
|
|
230
|
+
"DIGEST",
|
|
231
|
+
])
|
|
232
|
+
if len(sections) >= 1:
|
|
233
|
+
data["tables"] = parse_mysql_batch(sections[0])
|
|
234
|
+
if len(sections) >= 2:
|
|
235
|
+
data["processlist"] = parse_mysql_batch(sections[1])
|
|
236
|
+
if len(sections) >= 3:
|
|
237
|
+
data["top_queries"] = parse_mysql_batch(sections[2])
|
|
238
|
+
|
|
239
|
+
return data
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _split_mysql_resultsets(output: str, header_key: str) -> List[str]:
|
|
243
|
+
"""Split concatenated MySQL batch output into sections by header line."""
|
|
244
|
+
lines = output.strip().split("\n")
|
|
245
|
+
sections: List[List[str]] = []
|
|
246
|
+
current: List[str] = []
|
|
247
|
+
|
|
248
|
+
for line in lines:
|
|
249
|
+
if line.startswith(header_key + "\t") or line.strip() == header_key:
|
|
250
|
+
if current:
|
|
251
|
+
sections.append("\n".join(current))
|
|
252
|
+
current = [line]
|
|
253
|
+
else:
|
|
254
|
+
current.append(line)
|
|
255
|
+
if current:
|
|
256
|
+
sections.append("\n".join(current))
|
|
257
|
+
|
|
258
|
+
return sections
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _split_mysql_resultsets_multi(output: str, header_keys: List[str]) -> List[str]:
|
|
262
|
+
"""Split concatenated MySQL batch output into sections by multiple different header keys."""
|
|
263
|
+
lines = output.strip().split("\n")
|
|
264
|
+
sections: List[List[str]] = []
|
|
265
|
+
current: List[str] = []
|
|
266
|
+
expected_idx = 0
|
|
267
|
+
|
|
268
|
+
for line in lines:
|
|
269
|
+
# Check if this line starts a new result set
|
|
270
|
+
matched = False
|
|
271
|
+
if expected_idx < len(header_keys):
|
|
272
|
+
key = header_keys[expected_idx]
|
|
273
|
+
if line.startswith(key + "\t") or line.strip() == key:
|
|
274
|
+
if current:
|
|
275
|
+
sections.append("\n".join(current))
|
|
276
|
+
current = [line]
|
|
277
|
+
expected_idx += 1
|
|
278
|
+
matched = True
|
|
279
|
+
# Also check remaining headers in case we skipped one
|
|
280
|
+
if not matched:
|
|
281
|
+
for idx in range(expected_idx, len(header_keys)):
|
|
282
|
+
key = header_keys[idx]
|
|
283
|
+
if line.startswith(key + "\t") or line.strip() == key:
|
|
284
|
+
if current:
|
|
285
|
+
sections.append("\n".join(current))
|
|
286
|
+
current = [line]
|
|
287
|
+
expected_idx = idx + 1
|
|
288
|
+
matched = True
|
|
289
|
+
break
|
|
290
|
+
if not matched:
|
|
291
|
+
current.append(line)
|
|
292
|
+
|
|
293
|
+
if current:
|
|
294
|
+
sections.append("\n".join(current))
|
|
295
|
+
|
|
296
|
+
return sections
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Parse collected data into result
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
def parse_mysql_data(data: Dict[str, Any], result: MySQLAnalysisResult) -> None:
|
|
304
|
+
"""Transform raw MySQL data into structured result sections."""
|
|
305
|
+
gs = data.get("global_status", {})
|
|
306
|
+
vs = data.get("variables", {})
|
|
307
|
+
|
|
308
|
+
# --- Overview ---
|
|
309
|
+
version = vs.get("version", "unknown")
|
|
310
|
+
uptime_sec = _safe_int(gs.get("Uptime"))
|
|
311
|
+
threads_connected = _safe_int(gs.get("Threads_connected"))
|
|
312
|
+
threads_running = _safe_int(gs.get("Threads_running"))
|
|
313
|
+
max_used_connections = _safe_int(gs.get("Max_used_connections"))
|
|
314
|
+
max_connections = _safe_int(vs.get("max_connections"), 1)
|
|
315
|
+
aborted_clients = _safe_int(gs.get("Aborted_clients"))
|
|
316
|
+
aborted_connects = _safe_int(gs.get("Aborted_connects"))
|
|
317
|
+
connection_usage_pct = round(max_used_connections / max_connections * 100, 1) if max_connections > 0 else 0
|
|
318
|
+
|
|
319
|
+
result.overview = {
|
|
320
|
+
"version": version,
|
|
321
|
+
"uptime_seconds": uptime_sec,
|
|
322
|
+
"uptime_human": _format_uptime(uptime_sec),
|
|
323
|
+
"threads_connected": threads_connected,
|
|
324
|
+
"threads_running": threads_running,
|
|
325
|
+
"max_used_connections": max_used_connections,
|
|
326
|
+
"max_connections": max_connections,
|
|
327
|
+
"connection_usage_percent": connection_usage_pct,
|
|
328
|
+
"aborted_clients": aborted_clients,
|
|
329
|
+
"aborted_connects": aborted_connects,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# --- Query Throughput ---
|
|
333
|
+
questions = _safe_int(gs.get("Questions"))
|
|
334
|
+
slow_queries = _safe_int(gs.get("Slow_queries"))
|
|
335
|
+
long_query_time = vs.get("long_query_time", "10")
|
|
336
|
+
com_select = _safe_int(gs.get("Com_select"))
|
|
337
|
+
com_insert = _safe_int(gs.get("Com_insert"))
|
|
338
|
+
com_update = _safe_int(gs.get("Com_update"))
|
|
339
|
+
com_delete = _safe_int(gs.get("Com_delete"))
|
|
340
|
+
|
|
341
|
+
result.query_throughput = {
|
|
342
|
+
"questions": questions,
|
|
343
|
+
"slow_queries": slow_queries,
|
|
344
|
+
"long_query_time": long_query_time,
|
|
345
|
+
"com_select": com_select,
|
|
346
|
+
"com_insert": com_insert,
|
|
347
|
+
"com_update": com_update,
|
|
348
|
+
"com_delete": com_delete,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# --- InnoDB Row Operations ---
|
|
352
|
+
result.innodb_row_ops = {
|
|
353
|
+
"rows_read": _safe_int(gs.get("Innodb_rows_read")),
|
|
354
|
+
"rows_inserted": _safe_int(gs.get("Innodb_rows_inserted")),
|
|
355
|
+
"rows_updated": _safe_int(gs.get("Innodb_rows_updated")),
|
|
356
|
+
"rows_deleted": _safe_int(gs.get("Innodb_rows_deleted")),
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# --- Query Efficiency ---
|
|
360
|
+
created_tmp_disk = _safe_int(gs.get("Created_tmp_disk_tables"))
|
|
361
|
+
created_tmp = _safe_int(gs.get("Created_tmp_tables"))
|
|
362
|
+
tmp_disk_pct = round(created_tmp_disk / created_tmp * 100, 1) if created_tmp > 0 else 0
|
|
363
|
+
handler_rnd_next = _safe_int(gs.get("Handler_read_rnd_next"))
|
|
364
|
+
handler_first = _safe_int(gs.get("Handler_read_first"))
|
|
365
|
+
handler_key = _safe_int(gs.get("Handler_read_key"))
|
|
366
|
+
scan_total = handler_rnd_next + handler_first + handler_key
|
|
367
|
+
table_scan_pct = round((handler_rnd_next + handler_first) / scan_total * 100, 1) if scan_total > 0 else 0
|
|
368
|
+
select_full_join = _safe_int(gs.get("Select_full_join"))
|
|
369
|
+
select_range = _safe_int(gs.get("Select_range"))
|
|
370
|
+
sort_merge_passes = _safe_int(gs.get("Sort_merge_passes"))
|
|
371
|
+
|
|
372
|
+
result.query_efficiency = {
|
|
373
|
+
"created_tmp_disk_tables": created_tmp_disk,
|
|
374
|
+
"created_tmp_tables": created_tmp,
|
|
375
|
+
"tmp_disk_table_percent": tmp_disk_pct,
|
|
376
|
+
"handler_read_rnd_next": handler_rnd_next,
|
|
377
|
+
"handler_read_first": handler_first,
|
|
378
|
+
"handler_read_key": handler_key,
|
|
379
|
+
"table_scan_percent": table_scan_pct,
|
|
380
|
+
"select_full_join": select_full_join,
|
|
381
|
+
"select_range": select_range,
|
|
382
|
+
"sort_merge_passes": sort_merge_passes,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# --- InnoDB Buffer Pool ---
|
|
386
|
+
read_requests = _safe_int(gs.get("Innodb_buffer_pool_read_requests"))
|
|
387
|
+
reads = _safe_int(gs.get("Innodb_buffer_pool_reads"))
|
|
388
|
+
hit_ratio = round((read_requests - reads) / read_requests * 100, 2) if read_requests > 0 else 0
|
|
389
|
+
pool_size = _safe_int(vs.get("innodb_buffer_pool_size"))
|
|
390
|
+
bytes_data = _safe_int(gs.get("Innodb_buffer_pool_bytes_data"))
|
|
391
|
+
bytes_dirty = _safe_int(gs.get("Innodb_buffer_pool_bytes_dirty"))
|
|
392
|
+
usage_pct = round(bytes_data / pool_size * 100, 1) if pool_size > 0 else 0
|
|
393
|
+
pages_free = _safe_int(gs.get("Innodb_buffer_pool_pages_free"))
|
|
394
|
+
pages_data = _safe_int(gs.get("Innodb_buffer_pool_pages_data"))
|
|
395
|
+
pages_dirty = _safe_int(gs.get("Innodb_buffer_pool_pages_dirty"))
|
|
396
|
+
|
|
397
|
+
result.innodb_buffer_pool = {
|
|
398
|
+
"hit_ratio": hit_ratio,
|
|
399
|
+
"read_requests": read_requests,
|
|
400
|
+
"reads": reads,
|
|
401
|
+
"buffer_pool_size": pool_size,
|
|
402
|
+
"bytes_data": bytes_data,
|
|
403
|
+
"bytes_dirty": bytes_dirty,
|
|
404
|
+
"usage_percent": usage_pct,
|
|
405
|
+
"pages_data": pages_data,
|
|
406
|
+
"pages_free": pages_free,
|
|
407
|
+
"pages_dirty": pages_dirty,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
# --- InnoDB I/O ---
|
|
411
|
+
result.innodb_io = {
|
|
412
|
+
"data_reads": _safe_int(gs.get("Innodb_data_reads")),
|
|
413
|
+
"data_writes": _safe_int(gs.get("Innodb_data_writes")),
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# --- Network ---
|
|
417
|
+
result.network = {
|
|
418
|
+
"bytes_received": _safe_int(gs.get("Bytes_received")),
|
|
419
|
+
"bytes_sent": _safe_int(gs.get("Bytes_sent")),
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# --- Locks ---
|
|
423
|
+
row_lock_waits = _safe_int(gs.get("Innodb_row_lock_waits"))
|
|
424
|
+
row_lock_time = _safe_int(gs.get("Innodb_row_lock_time"))
|
|
425
|
+
table_locks_waited = _safe_int(gs.get("Table_locks_waited"))
|
|
426
|
+
table_locks_immediate = _safe_int(gs.get("Table_locks_immediate"))
|
|
427
|
+
lock_total = table_locks_waited + table_locks_immediate
|
|
428
|
+
table_lock_contention = round(table_locks_waited / lock_total * 100, 2) if lock_total > 0 else 0
|
|
429
|
+
|
|
430
|
+
result.locks = {
|
|
431
|
+
"row_lock_waits": row_lock_waits,
|
|
432
|
+
"row_lock_time": row_lock_time,
|
|
433
|
+
"table_locks_waited": table_locks_waited,
|
|
434
|
+
"table_locks_immediate": table_locks_immediate,
|
|
435
|
+
"table_lock_contention": table_lock_contention,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
# --- Table Cache ---
|
|
439
|
+
open_tables = _safe_int(gs.get("Open_tables"))
|
|
440
|
+
opened_tables = _safe_int(gs.get("Opened_tables"))
|
|
441
|
+
table_open_cache = _safe_int(vs.get("table_open_cache"))
|
|
442
|
+
cache_utilization_pct = round(open_tables / table_open_cache * 100, 1) if table_open_cache > 0 else 0
|
|
443
|
+
opens_per_sec = round(opened_tables / uptime_sec, 2) if uptime_sec > 0 else 0
|
|
444
|
+
|
|
445
|
+
result.table_cache = {
|
|
446
|
+
"open_tables": open_tables,
|
|
447
|
+
"opened_tables": opened_tables,
|
|
448
|
+
"table_open_cache": table_open_cache,
|
|
449
|
+
"cache_utilization_percent": cache_utilization_pct,
|
|
450
|
+
"opens_per_second": opens_per_sec,
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# --- Top Queries ---
|
|
454
|
+
for row in data.get("top_queries", []):
|
|
455
|
+
result.top_queries.append({
|
|
456
|
+
"digest": row.get("DIGEST", ""),
|
|
457
|
+
"digest_text": row.get("DIGEST_TEXT", ""),
|
|
458
|
+
"count_star": _safe_int(row.get("COUNT_STAR")),
|
|
459
|
+
"total_latency_ms": _safe_float(row.get("TOTAL_LATENCY_MS")),
|
|
460
|
+
"avg_latency_ms": _safe_float(row.get("AVG_LATENCY_MS")),
|
|
461
|
+
"rows_examined": _safe_int(row.get("SUM_ROWS_EXAMINED")),
|
|
462
|
+
"rows_sent": _safe_int(row.get("SUM_ROWS_SENT")),
|
|
463
|
+
"tmp_disk_tables": _safe_int(row.get("SUM_CREATED_TMP_DISK_TABLES")),
|
|
464
|
+
"no_index_used": _safe_int(row.get("SUM_NO_INDEX_USED")),
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
if result.top_queries:
|
|
468
|
+
result.top_queries_status = "ok"
|
|
469
|
+
else:
|
|
470
|
+
perf_schema = vs.get("performance_schema", "").upper()
|
|
471
|
+
if perf_schema == "OFF":
|
|
472
|
+
result.top_queries_status = "performance_schema_disabled"
|
|
473
|
+
elif perf_schema == "ON":
|
|
474
|
+
result.top_queries_status = "no_queries_recorded"
|
|
475
|
+
else:
|
|
476
|
+
result.top_queries_status = "unknown"
|
|
477
|
+
|
|
478
|
+
# --- Tables ---
|
|
479
|
+
for row in data.get("tables", []):
|
|
480
|
+
result.tables.append({
|
|
481
|
+
"name": row.get("TABLE_NAME", ""),
|
|
482
|
+
"rows": _safe_int(row.get("TABLE_ROWS")),
|
|
483
|
+
"data_length": _safe_int(row.get("DATA_LENGTH")),
|
|
484
|
+
"index_length": _safe_int(row.get("INDEX_LENGTH")),
|
|
485
|
+
"total_size": _safe_int(row.get("TOTAL_SIZE")),
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
# --- Active Processes ---
|
|
489
|
+
for row in data.get("processlist", []):
|
|
490
|
+
result.active_processes.append({
|
|
491
|
+
"id": row.get("Id", ""),
|
|
492
|
+
"user": row.get("User", ""),
|
|
493
|
+
"db": row.get("db", ""),
|
|
494
|
+
"command": row.get("Command", ""),
|
|
495
|
+
"time": _safe_int(row.get("Time")),
|
|
496
|
+
"state": row.get("State", ""),
|
|
497
|
+
"info": row.get("Info", ""),
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ---------------------------------------------------------------------------
|
|
502
|
+
# Formatting helpers
|
|
503
|
+
# ---------------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
def _format_count(n: int) -> str:
|
|
506
|
+
"""Format large numbers with K/M/G suffix."""
|
|
507
|
+
if n >= 1_000_000_000:
|
|
508
|
+
return f"{n / 1_000_000_000:.1f}G"
|
|
509
|
+
if n >= 1_000_000:
|
|
510
|
+
return f"{n / 1_000_000:.1f}M"
|
|
511
|
+
if n >= 1_000:
|
|
512
|
+
return f"{n / 1_000:.1f}K"
|
|
513
|
+
return str(n)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _format_bytes(b: int) -> str:
|
|
517
|
+
"""Format bytes to human-readable."""
|
|
518
|
+
if b >= 1_073_741_824:
|
|
519
|
+
return f"{b / 1_073_741_824:.1f} GB"
|
|
520
|
+
if b >= 1_048_576:
|
|
521
|
+
return f"{b / 1_048_576:.1f} MB"
|
|
522
|
+
if b >= 1_024:
|
|
523
|
+
return f"{b / 1_024:.1f} KB"
|
|
524
|
+
return f"{b} B"
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _status_ok_warn_crit(value: float, warn_threshold: float, crit_threshold: float) -> str:
|
|
528
|
+
if value >= crit_threshold:
|
|
529
|
+
return "CRITICAL"
|
|
530
|
+
if value >= warn_threshold:
|
|
531
|
+
return "WARN"
|
|
532
|
+
return "OK"
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ---------------------------------------------------------------------------
|
|
536
|
+
# Recommendations
|
|
537
|
+
# ---------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
def generate_recommendations(result: MySQLAnalysisResult) -> List[Dict[str, str]]:
|
|
540
|
+
recs: List[Dict[str, str]] = []
|
|
541
|
+
|
|
542
|
+
# Collection failures — surface critical issues when SSH/introspection failed
|
|
543
|
+
if result.collection_status:
|
|
544
|
+
failed = {k: v for k, v in result.collection_status.items()
|
|
545
|
+
if v.get("status") in ("failed", "error")}
|
|
546
|
+
ssh_sources = {"mysql_query"}
|
|
547
|
+
ssh_failed = {k: v for k, v in failed.items() if k in ssh_sources}
|
|
548
|
+
if ssh_failed:
|
|
549
|
+
sources = ", ".join(ssh_failed.keys())
|
|
550
|
+
errors = "; ".join(v.get("error", "unknown") for v in ssh_failed.values())
|
|
551
|
+
recs.append({
|
|
552
|
+
"severity": "critical",
|
|
553
|
+
"category": "collection",
|
|
554
|
+
"message": f"SSH introspection failed — unable to collect {sources}. "
|
|
555
|
+
f"Error: {errors}. "
|
|
556
|
+
f"Analysis is incomplete: InnoDB buffer pool, query throughput, "
|
|
557
|
+
f"locks, and tuning parameters could not be evaluated.",
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
def rec(severity: str, message: str):
|
|
561
|
+
recs.append({"severity": severity, "message": message})
|
|
562
|
+
|
|
563
|
+
ov = result.overview or {}
|
|
564
|
+
qt = result.query_throughput or {}
|
|
565
|
+
qe = result.query_efficiency or {}
|
|
566
|
+
bp = result.innodb_buffer_pool or {}
|
|
567
|
+
lk = result.locks or {}
|
|
568
|
+
tc = result.table_cache or {}
|
|
569
|
+
|
|
570
|
+
# Connection usage
|
|
571
|
+
conn_pct = ov.get("connection_usage_percent", 0)
|
|
572
|
+
max_conn = ov.get("max_connections", 0)
|
|
573
|
+
if conn_pct >= 90:
|
|
574
|
+
rec("critical", f"Connection usage critical at {conn_pct}%. Approaching max_connections ({max_conn}).")
|
|
575
|
+
elif conn_pct >= 70:
|
|
576
|
+
rec("warning", f"Connection usage at {conn_pct}%. Consider increasing max_connections.")
|
|
577
|
+
|
|
578
|
+
# Buffer pool hit ratio
|
|
579
|
+
hit_ratio = bp.get("hit_ratio", 100)
|
|
580
|
+
if hit_ratio < 95:
|
|
581
|
+
rec("critical", f"Buffer pool hit ratio at {hit_ratio}%. Increase innodb_buffer_pool_size.")
|
|
582
|
+
elif hit_ratio < 99:
|
|
583
|
+
rec("warning", f"Buffer pool hit ratio at {hit_ratio}% -- room for improvement with more RAM.")
|
|
584
|
+
|
|
585
|
+
# Buffer pool usage
|
|
586
|
+
bp_usage = bp.get("usage_percent", 0)
|
|
587
|
+
if bp_usage > 95:
|
|
588
|
+
rec("warning", f"Buffer pool {bp_usage}% full. Data pages may be evicted under load.")
|
|
589
|
+
|
|
590
|
+
# Temp tables to disk
|
|
591
|
+
tmp_pct = qe.get("tmp_disk_table_percent", 0)
|
|
592
|
+
created_disk = qe.get("created_tmp_disk_tables", 0)
|
|
593
|
+
created_total = qe.get("created_tmp_tables", 0)
|
|
594
|
+
if tmp_pct > 25:
|
|
595
|
+
rec("warning", f"{tmp_pct}% of temp tables going to disk. Increase tmp_table_size/max_heap_table_size or optimize queries.")
|
|
596
|
+
elif tmp_pct > 10:
|
|
597
|
+
rec("info", f"Temp tables to disk at {tmp_pct}%. Watch for queries creating large temporary results.")
|
|
598
|
+
|
|
599
|
+
# Table scan ratio
|
|
600
|
+
scan_pct = qe.get("table_scan_percent", 0)
|
|
601
|
+
if scan_pct > 75:
|
|
602
|
+
rec("critical", f"Table scan ratio at {scan_pct}%. Most reads are full scans -- add indexes.")
|
|
603
|
+
elif scan_pct > 50:
|
|
604
|
+
rec("warning", f"Table scan ratio at {scan_pct}%. Consider indexing frequently queried columns.")
|
|
605
|
+
|
|
606
|
+
# Full joins
|
|
607
|
+
full_joins = qe.get("select_full_join", 0)
|
|
608
|
+
if full_joins > 100:
|
|
609
|
+
rec("warning", f"{_format_count(full_joins)} full joins detected. These scan entire tables -- add indexes to join columns.")
|
|
610
|
+
|
|
611
|
+
# Sort merge passes
|
|
612
|
+
sort_passes = qe.get("sort_merge_passes", 0)
|
|
613
|
+
if sort_passes > 0:
|
|
614
|
+
rec("info", f"Sort merge passes ({_format_count(sort_passes)}). Increase sort_buffer_size or optimize queries.")
|
|
615
|
+
|
|
616
|
+
# Row lock waits
|
|
617
|
+
row_lock_waits = lk.get("row_lock_waits", 0)
|
|
618
|
+
if row_lock_waits > 1000:
|
|
619
|
+
rec("warning", f"InnoDB row lock waits ({_format_count(row_lock_waits)}). Check for lock contention in concurrent writes.")
|
|
620
|
+
elif row_lock_waits > 0:
|
|
621
|
+
rec("info", f"InnoDB row lock waits ({_format_count(row_lock_waits)}). Check for lock contention in concurrent writes.")
|
|
622
|
+
|
|
623
|
+
# Table lock contention
|
|
624
|
+
tl_contention = lk.get("table_lock_contention", 0)
|
|
625
|
+
if tl_contention > 5:
|
|
626
|
+
rec("warning", f"Table lock contention at {tl_contention}%. May indicate MyISAM tables -- convert to InnoDB.")
|
|
627
|
+
|
|
628
|
+
# Slow queries
|
|
629
|
+
slow = qt.get("slow_queries", 0)
|
|
630
|
+
threshold = qt.get("long_query_time", "10")
|
|
631
|
+
if slow > 0:
|
|
632
|
+
rec("info", f"{_format_count(slow)} slow queries (threshold: {threshold}s). Review with performance_schema or slow query log.")
|
|
633
|
+
|
|
634
|
+
# Aborted clients
|
|
635
|
+
aborted_clients = ov.get("aborted_clients", 0)
|
|
636
|
+
if aborted_clients > 0:
|
|
637
|
+
rec("info", f"{_format_count(aborted_clients)} aborted clients. Applications may not be closing connections properly.")
|
|
638
|
+
|
|
639
|
+
# Aborted connects
|
|
640
|
+
aborted_connects = ov.get("aborted_connects", 0)
|
|
641
|
+
if aborted_connects > 0:
|
|
642
|
+
rec("info", f"{_format_count(aborted_connects)} aborted connection attempts. Check authentication issues or connection limits.")
|
|
643
|
+
|
|
644
|
+
# No index used in top queries
|
|
645
|
+
if result.top_queries:
|
|
646
|
+
no_index_count = sum(1 for q in result.top_queries if q.get("no_index_used", 0) > 0)
|
|
647
|
+
if no_index_count > 0:
|
|
648
|
+
rec("warning", f"Top queries using no index ({no_index_count} of {len(result.top_queries)}). Missing indexes are likely impacting performance.")
|
|
649
|
+
|
|
650
|
+
# Table cache
|
|
651
|
+
tc = result.table_cache or {}
|
|
652
|
+
opens_per_sec = tc.get("opens_per_second", 0)
|
|
653
|
+
cache_util = tc.get("cache_utilization_percent", 0)
|
|
654
|
+
if cache_util >= 95:
|
|
655
|
+
rec("warning", f"Table cache {cache_util}% full ({tc.get('open_tables')}/{tc.get('table_open_cache')}). Increase table_open_cache.")
|
|
656
|
+
if opens_per_sec > 5:
|
|
657
|
+
rec("warning", f"Table opens at {opens_per_sec}/sec — cache may be undersized. Increase table_open_cache.")
|
|
658
|
+
|
|
659
|
+
# Top queries diagnostic
|
|
660
|
+
if not result.top_queries:
|
|
661
|
+
if result.top_queries_status == "performance_schema_disabled":
|
|
662
|
+
pass # Off by default on Railway; overhead (~400MB+) is too high to recommend casually
|
|
663
|
+
elif result.top_queries_status == "no_queries_recorded":
|
|
664
|
+
rec("info", "performance_schema is ON but no queries recorded. Database may be idle or recently restarted.")
|
|
665
|
+
|
|
666
|
+
return recs
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# ---------------------------------------------------------------------------
|
|
670
|
+
# Report formatter
|
|
671
|
+
# ---------------------------------------------------------------------------
|
|
672
|
+
|
|
673
|
+
def format_report(result: MySQLAnalysisResult) -> str:
|
|
674
|
+
lines: List[str] = []
|
|
675
|
+
|
|
676
|
+
def heading(title: str, level: int = 2):
|
|
677
|
+
prefix = "#" * level
|
|
678
|
+
lines.append(f"\n{prefix} {title}")
|
|
679
|
+
|
|
680
|
+
def table_row(*cells: str):
|
|
681
|
+
lines.append("| " + " | ".join(cells) + " |")
|
|
682
|
+
|
|
683
|
+
def table_sep(ncols: int):
|
|
684
|
+
lines.append("| " + " | ".join(["---"] * ncols) + " |")
|
|
685
|
+
|
|
686
|
+
# Title
|
|
687
|
+
lines.append(f"# MySQL Analysis: {result.service}")
|
|
688
|
+
lines.append(f"Timestamp: {result.timestamp}")
|
|
689
|
+
lines.append(f"Deployment: {result.deployment_status}")
|
|
690
|
+
|
|
691
|
+
# --- Resource Overview (from Railway API) ---
|
|
692
|
+
if result.cpu_memory or result.disk_usage:
|
|
693
|
+
heading("Resource Overview")
|
|
694
|
+
table_row("Metric", "Value")
|
|
695
|
+
table_sep(2)
|
|
696
|
+
if result.cpu_memory:
|
|
697
|
+
cm = result.cpu_memory
|
|
698
|
+
if "cpu_percent" in cm:
|
|
699
|
+
trend = _trend_indicator(result.metrics_history, "cpu")
|
|
700
|
+
lines.append(f"| CPU | {cm['cpu_percent']}% vCPU{trend} |")
|
|
701
|
+
if "memory_gb" in cm:
|
|
702
|
+
trend = _trend_indicator(result.metrics_history, "memory")
|
|
703
|
+
mem_str = f"{cm['memory_gb']} GB"
|
|
704
|
+
if "memory_limit_gb" in cm and cm["memory_limit_gb"] > 0:
|
|
705
|
+
pct = round(cm["memory_gb"] / cm["memory_limit_gb"] * 100, 1)
|
|
706
|
+
mem_str += f" / {cm['memory_limit_gb']} GB ({pct}%)"
|
|
707
|
+
lines.append(f"| Memory | {mem_str}{trend} |")
|
|
708
|
+
if result.disk_usage:
|
|
709
|
+
trend = _trend_indicator(result.metrics_history, "disk")
|
|
710
|
+
lines.append(f"| Disk | {result.disk_usage.get('used', 'N/A')}{trend} |")
|
|
711
|
+
|
|
712
|
+
# --- Overview ---
|
|
713
|
+
ov = result.overview
|
|
714
|
+
if ov:
|
|
715
|
+
heading("Overview")
|
|
716
|
+
table_row("Metric", "Value", "Status")
|
|
717
|
+
table_sep(3)
|
|
718
|
+
table_row("Version", str(ov["version"]), "")
|
|
719
|
+
table_row("Uptime", ov["uptime_human"], "")
|
|
720
|
+
conn_status = _status_ok_warn_crit(ov["connection_usage_percent"], 70, 90)
|
|
721
|
+
table_row(
|
|
722
|
+
"Connections",
|
|
723
|
+
f"{ov['connection_usage_percent']}% ({ov['max_used_connections']}/{ov['max_connections']})",
|
|
724
|
+
conn_status,
|
|
725
|
+
)
|
|
726
|
+
table_row("Threads Running", str(ov["threads_running"]), "")
|
|
727
|
+
aborted_c_status = "WARN" if ov["aborted_clients"] > 0 else "OK"
|
|
728
|
+
table_row("Aborted Clients", _format_count(ov["aborted_clients"]), aborted_c_status)
|
|
729
|
+
aborted_conn_status = "WARN" if ov["aborted_connects"] > 0 else "OK"
|
|
730
|
+
table_row("Aborted Connects", _format_count(ov["aborted_connects"]), aborted_conn_status)
|
|
731
|
+
|
|
732
|
+
# --- Query Throughput ---
|
|
733
|
+
qt = result.query_throughput
|
|
734
|
+
if qt:
|
|
735
|
+
heading("Query Throughput")
|
|
736
|
+
table_row("Metric", "Value", "Status")
|
|
737
|
+
table_sep(3)
|
|
738
|
+
table_row("Total Queries", _format_count(qt["questions"]), "")
|
|
739
|
+
slow_status = "WARN" if qt["slow_queries"] > 0 else "OK"
|
|
740
|
+
table_row("Slow Queries", f"{_format_count(qt['slow_queries'])} (> {qt['long_query_time']}s threshold)", slow_status)
|
|
741
|
+
table_row("SELECT", _format_count(qt["com_select"]), "")
|
|
742
|
+
table_row("INSERT", _format_count(qt["com_insert"]), "")
|
|
743
|
+
table_row("UPDATE", _format_count(qt["com_update"]), "")
|
|
744
|
+
table_row("DELETE", _format_count(qt["com_delete"]), "")
|
|
745
|
+
|
|
746
|
+
# --- InnoDB Row Operations ---
|
|
747
|
+
ro = result.innodb_row_ops
|
|
748
|
+
if ro:
|
|
749
|
+
heading("InnoDB Row Operations")
|
|
750
|
+
table_row("Operation", "Count")
|
|
751
|
+
table_sep(2)
|
|
752
|
+
table_row("Rows Read", _format_count(ro["rows_read"]))
|
|
753
|
+
table_row("Rows Inserted", _format_count(ro["rows_inserted"]))
|
|
754
|
+
table_row("Rows Updated", _format_count(ro["rows_updated"]))
|
|
755
|
+
table_row("Rows Deleted", _format_count(ro["rows_deleted"]))
|
|
756
|
+
|
|
757
|
+
# --- Query Efficiency ---
|
|
758
|
+
qe = result.query_efficiency
|
|
759
|
+
if qe:
|
|
760
|
+
heading("Query Efficiency")
|
|
761
|
+
table_row("Metric", "Value", "Status")
|
|
762
|
+
table_sep(3)
|
|
763
|
+
tmp_status = _status_ok_warn_crit(qe["tmp_disk_table_percent"], 10, 25)
|
|
764
|
+
table_row(
|
|
765
|
+
"Temp Tables to Disk",
|
|
766
|
+
f"{qe['tmp_disk_table_percent']}% ({_format_count(qe['created_tmp_disk_tables'])}/{_format_count(qe['created_tmp_tables'])})",
|
|
767
|
+
tmp_status,
|
|
768
|
+
)
|
|
769
|
+
scan_status = _status_ok_warn_crit(qe["table_scan_percent"], 50, 75)
|
|
770
|
+
table_row("Table Scan Ratio", f"{qe['table_scan_percent']}%", scan_status)
|
|
771
|
+
fj_status = "WARN" if qe["select_full_join"] > 100 else "OK"
|
|
772
|
+
table_row("Full Joins", _format_count(qe["select_full_join"]), fj_status)
|
|
773
|
+
table_row("Sort Merge Passes", _format_count(qe["sort_merge_passes"]), "")
|
|
774
|
+
|
|
775
|
+
# --- InnoDB Buffer Pool ---
|
|
776
|
+
bp = result.innodb_buffer_pool
|
|
777
|
+
if bp:
|
|
778
|
+
heading("InnoDB Buffer Pool")
|
|
779
|
+
table_row("Metric", "Value", "Status")
|
|
780
|
+
table_sep(3)
|
|
781
|
+
hr_status = _status_ok_warn_crit(100 - bp["hit_ratio"], 1, 5) # inverted: lower hit = worse
|
|
782
|
+
table_row("Cache Hit Ratio", f"{bp['hit_ratio']}%", hr_status)
|
|
783
|
+
usage_status = _status_ok_warn_crit(bp["usage_percent"], 90, 95)
|
|
784
|
+
table_row(
|
|
785
|
+
"Pool Usage",
|
|
786
|
+
f"{bp['usage_percent']}% ({_format_bytes(bp['bytes_data'])}/{_format_bytes(bp['buffer_pool_size'])})",
|
|
787
|
+
usage_status,
|
|
788
|
+
)
|
|
789
|
+
table_row("Dirty Pages", _format_bytes(bp["bytes_dirty"]), "")
|
|
790
|
+
table_row("Free Pages", _format_count(bp["pages_free"]), "")
|
|
791
|
+
|
|
792
|
+
# --- Network ---
|
|
793
|
+
nw = result.network
|
|
794
|
+
if nw:
|
|
795
|
+
heading("Network")
|
|
796
|
+
table_row("Metric", "Value")
|
|
797
|
+
table_sep(2)
|
|
798
|
+
table_row("Bytes Received", _format_bytes(nw["bytes_received"]))
|
|
799
|
+
table_row("Bytes Sent", _format_bytes(nw["bytes_sent"]))
|
|
800
|
+
|
|
801
|
+
# --- Locks ---
|
|
802
|
+
lk = result.locks
|
|
803
|
+
if lk:
|
|
804
|
+
heading("Locks")
|
|
805
|
+
table_row("Metric", "Value", "Status")
|
|
806
|
+
table_sep(3)
|
|
807
|
+
table_row("Row Lock Waits", _format_count(lk["row_lock_waits"]), "")
|
|
808
|
+
table_row("Row Lock Time (ms)", _format_count(lk["row_lock_time"]), "")
|
|
809
|
+
tl_status = _status_ok_warn_crit(lk["table_lock_contention"], 1, 5)
|
|
810
|
+
table_row("Table Lock Contention", f"{lk['table_lock_contention']}%", tl_status)
|
|
811
|
+
|
|
812
|
+
# --- Table Cache ---
|
|
813
|
+
tc = result.table_cache
|
|
814
|
+
if tc:
|
|
815
|
+
heading("Table Cache")
|
|
816
|
+
table_row("Metric", "Value")
|
|
817
|
+
table_sep(2)
|
|
818
|
+
table_row("Open Tables", f"{_format_count(tc['open_tables'])} / {_format_count(tc.get('table_open_cache', 0))}")
|
|
819
|
+
table_row("Cache Utilization", f"{tc.get('cache_utilization_percent', 0)}%")
|
|
820
|
+
table_row("Table Opens/sec", f"{tc.get('opens_per_second', 0)}")
|
|
821
|
+
|
|
822
|
+
# --- Top Queries ---
|
|
823
|
+
if result.top_queries:
|
|
824
|
+
heading("Top Queries (by total latency)")
|
|
825
|
+
table_row("Query", "Calls", "Avg Latency", "Total Latency", "Rows Examined", "Rows Sent")
|
|
826
|
+
table_sep(6)
|
|
827
|
+
for q in result.top_queries[:15]:
|
|
828
|
+
digest = q["digest_text"][:60] + "..." if len(q["digest_text"]) > 60 else q["digest_text"]
|
|
829
|
+
# Escape pipe chars in query text
|
|
830
|
+
digest = digest.replace("|", "\\|")
|
|
831
|
+
table_row(
|
|
832
|
+
digest,
|
|
833
|
+
_format_count(q["count_star"]),
|
|
834
|
+
f"{q['avg_latency_ms']:.1f}ms",
|
|
835
|
+
f"{q['total_latency_ms']:.1f}ms",
|
|
836
|
+
_format_count(q["rows_examined"]),
|
|
837
|
+
_format_count(q["rows_sent"]),
|
|
838
|
+
)
|
|
839
|
+
else:
|
|
840
|
+
heading("Top Queries (by total latency)")
|
|
841
|
+
if result.top_queries_status == "performance_schema_disabled":
|
|
842
|
+
lines.append("performance_schema is disabled — no query-level data available.")
|
|
843
|
+
lines.append("Note: enabling it requires ~400MB+ additional memory; only advisable on larger instances.")
|
|
844
|
+
elif result.top_queries_status == "no_queries_recorded":
|
|
845
|
+
lines.append("No queries recorded. Database may be idle or recently restarted.")
|
|
846
|
+
else:
|
|
847
|
+
lines.append("No query data available.")
|
|
848
|
+
lines.append("")
|
|
849
|
+
|
|
850
|
+
# --- Tables ---
|
|
851
|
+
if result.tables:
|
|
852
|
+
heading("Tables (by size)")
|
|
853
|
+
table_row("Table", "Rows", "Data", "Indexes", "Total")
|
|
854
|
+
table_sep(5)
|
|
855
|
+
for t in result.tables[:15]:
|
|
856
|
+
table_row(
|
|
857
|
+
t["name"],
|
|
858
|
+
_format_count(t["rows"]),
|
|
859
|
+
_format_bytes(t["data_length"]),
|
|
860
|
+
_format_bytes(t["index_length"]),
|
|
861
|
+
_format_bytes(t["total_size"]),
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# --- Active Queries ---
|
|
865
|
+
if result.active_processes:
|
|
866
|
+
heading("Active Queries")
|
|
867
|
+
# Filter out system processes
|
|
868
|
+
user_procs = [p for p in result.active_processes if p["command"] != "Daemon"]
|
|
869
|
+
if user_procs:
|
|
870
|
+
table_row("User", "Database", "Command", "Time (s)", "Query")
|
|
871
|
+
table_sep(5)
|
|
872
|
+
for p in user_procs[:20]:
|
|
873
|
+
info = (p["info"] or "")[:80]
|
|
874
|
+
info = info.replace("|", "\\|")
|
|
875
|
+
table_row(
|
|
876
|
+
p["user"],
|
|
877
|
+
p["db"] or "",
|
|
878
|
+
p["command"],
|
|
879
|
+
str(p["time"]),
|
|
880
|
+
info,
|
|
881
|
+
)
|
|
882
|
+
else:
|
|
883
|
+
lines.append("\nNo active user queries.")
|
|
884
|
+
|
|
885
|
+
# --- Infrastructure Metrics ---
|
|
886
|
+
if result.metrics_history:
|
|
887
|
+
windows = result.metrics_history.get("windows", {})
|
|
888
|
+
for window_label, window_data in windows.items():
|
|
889
|
+
mh = window_data.get("metrics", {})
|
|
890
|
+
if not mh:
|
|
891
|
+
continue
|
|
892
|
+
lines.append(f"## Infrastructure Metrics ({window_label})")
|
|
893
|
+
lines.append("| Metric | Current | Min | Max | Avg | Trend |")
|
|
894
|
+
lines.append("|--------|---------|-----|-----|-----|-------|")
|
|
895
|
+
for key in ["cpu", "memory", "disk", "network_rx", "network_tx"]:
|
|
896
|
+
if key in mh:
|
|
897
|
+
entry = mh[key]
|
|
898
|
+
trend = entry.get("trend", {})
|
|
899
|
+
trend_str = trend.get("direction", "N/A")
|
|
900
|
+
change = trend.get("change_pct", 0)
|
|
901
|
+
if change != 0:
|
|
902
|
+
trend_str += f" ({change:+.1f}%)"
|
|
903
|
+
lines.append(
|
|
904
|
+
f"| {key.replace('_', ' ').title()} "
|
|
905
|
+
f"| {entry['current']}{entry['unit']} "
|
|
906
|
+
f"| {entry['min']}{entry['unit']} "
|
|
907
|
+
f"| {entry['max']}{entry['unit']} "
|
|
908
|
+
f"| {entry['avg']}{entry['unit']} "
|
|
909
|
+
f"| {trend_str} |"
|
|
910
|
+
)
|
|
911
|
+
lines.append("")
|
|
912
|
+
|
|
913
|
+
# --- Collection Errors ---
|
|
914
|
+
if result.errors:
|
|
915
|
+
heading("Collection Errors")
|
|
916
|
+
for err in result.errors:
|
|
917
|
+
lines.append(f"- {err}")
|
|
918
|
+
|
|
919
|
+
# --- Recommendations ---
|
|
920
|
+
heading("Recommendations")
|
|
921
|
+
if result.recommendations:
|
|
922
|
+
for r in result.recommendations:
|
|
923
|
+
severity = r["severity"].upper()
|
|
924
|
+
lines.append(f"- **[{severity}]** {r['message']}")
|
|
925
|
+
else:
|
|
926
|
+
lines.append("No issues detected.")
|
|
927
|
+
|
|
928
|
+
return "\n".join(lines) + "\n"
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
# ---------------------------------------------------------------------------
|
|
932
|
+
# Main analysis function
|
|
933
|
+
# ---------------------------------------------------------------------------
|
|
934
|
+
|
|
935
|
+
def analyze_mysql(service: str, timeout: int = 60, quiet: bool = False,
|
|
936
|
+
skip_logs: bool = False, metrics_hours: int = 168,
|
|
937
|
+
project_id: Optional[str] = None,
|
|
938
|
+
environment_id: Optional[str] = None,
|
|
939
|
+
service_id: Optional[str] = None) -> MySQLAnalysisResult:
|
|
940
|
+
"""Run complete MySQL analysis."""
|
|
941
|
+
if not quiet:
|
|
942
|
+
print(f"Analyzing mysql database: {service}", file=sys.stderr)
|
|
943
|
+
|
|
944
|
+
result = MySQLAnalysisResult(
|
|
945
|
+
service=service,
|
|
946
|
+
db_type="mysql",
|
|
947
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
# === CONTEXT ===
|
|
951
|
+
if not quiet:
|
|
952
|
+
print(" [0/5] Getting Railway context...", file=sys.stderr, flush=True)
|
|
953
|
+
dal._progress_timer.start()
|
|
954
|
+
|
|
955
|
+
if environment_id and service_id:
|
|
956
|
+
dal._ctx = RailwayContext(project_id=project_id, environment_id=environment_id, service_id=service_id)
|
|
957
|
+
if not quiet:
|
|
958
|
+
print(f" using explicit IDs (env={environment_id[:8]}..., svc={service_id[:8]}...)", file=sys.stderr, flush=True)
|
|
959
|
+
else:
|
|
960
|
+
railway_status = get_railway_status()
|
|
961
|
+
if railway_status:
|
|
962
|
+
dal._ctx = RailwayContext(
|
|
963
|
+
project_id=railway_status.get("projectId"),
|
|
964
|
+
environment_id=railway_status.get("environmentId"),
|
|
965
|
+
service_id=railway_status.get("serviceId"),
|
|
966
|
+
)
|
|
967
|
+
environment_id = dal._ctx.environment_id
|
|
968
|
+
service_id = dal._ctx.service_id
|
|
969
|
+
|
|
970
|
+
# === DEPLOYMENT STATUS ===
|
|
971
|
+
progress(1, 5, "Fetching deployment status...", quiet)
|
|
972
|
+
result.deployment_status = get_deployment_status(service, service_id=service_id)
|
|
973
|
+
|
|
974
|
+
# === SSH PRE-CHECK ===
|
|
975
|
+
progress(2, 5, "Testing SSH connectivity...", quiet)
|
|
976
|
+
ssh_available = False
|
|
977
|
+
ssh_stderr = ""
|
|
978
|
+
ssh_attempts = [30, 60, 90]
|
|
979
|
+
for attempt, attempt_timeout in enumerate(ssh_attempts, 1):
|
|
980
|
+
ssh_code, ssh_stdout, ssh_stderr = run_ssh_query(service, "echo ok", timeout=attempt_timeout)
|
|
981
|
+
if ssh_code == 0 and "ok" in ssh_stdout:
|
|
982
|
+
ssh_available = True
|
|
983
|
+
if not quiet:
|
|
984
|
+
for line in ssh_stderr.splitlines():
|
|
985
|
+
if line.startswith("Using SSH key:"):
|
|
986
|
+
print(f" {line}", file=sys.stderr, flush=True)
|
|
987
|
+
break
|
|
988
|
+
break
|
|
989
|
+
if not quiet:
|
|
990
|
+
remaining = len(ssh_attempts) - attempt
|
|
991
|
+
if remaining > 0:
|
|
992
|
+
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)
|
|
993
|
+
else:
|
|
994
|
+
print(f" SSH attempt {attempt}/{len(ssh_attempts)} failed ({ssh_stderr or 'no response'}), giving up", file=sys.stderr, flush=True)
|
|
995
|
+
|
|
996
|
+
# === PARALLEL EXECUTION ===
|
|
997
|
+
progress(3, 5, "Running analysis (metrics, queries, logs in parallel)...", quiet)
|
|
998
|
+
|
|
999
|
+
def task_metrics():
|
|
1000
|
+
if environment_id and service_id:
|
|
1001
|
+
return get_all_metrics_from_api(environment_id, service_id, hours=metrics_hours)
|
|
1002
|
+
return None
|
|
1003
|
+
|
|
1004
|
+
def task_mysql_queries():
|
|
1005
|
+
if not ssh_available:
|
|
1006
|
+
return {"errors": [f"SSH not available: {ssh_stderr or 'connection failed'}"]}
|
|
1007
|
+
data = collect_mysql_data(service, timeout=timeout)
|
|
1008
|
+
if data.get("errors") and not data.get("global_status"):
|
|
1009
|
+
# Retry once
|
|
1010
|
+
data = collect_mysql_data(service, timeout=timeout)
|
|
1011
|
+
return data
|
|
1012
|
+
|
|
1013
|
+
def task_logs():
|
|
1014
|
+
if skip_logs:
|
|
1015
|
+
return []
|
|
1016
|
+
return get_recent_logs(service, lines=LOG_LINES_DEFAULT,
|
|
1017
|
+
environment_id=environment_id,
|
|
1018
|
+
service_id=service_id)
|
|
1019
|
+
|
|
1020
|
+
with ThreadPoolExecutor(max_workers=3) as executor:
|
|
1021
|
+
future_metrics = executor.submit(task_metrics)
|
|
1022
|
+
future_mysql = executor.submit(task_mysql_queries)
|
|
1023
|
+
future_logs = executor.submit(task_logs)
|
|
1024
|
+
|
|
1025
|
+
metrics_result = future_metrics.result()
|
|
1026
|
+
mysql_data = future_mysql.result()
|
|
1027
|
+
logs_result = future_logs.result()
|
|
1028
|
+
|
|
1029
|
+
# Process metrics
|
|
1030
|
+
if metrics_result:
|
|
1031
|
+
result.disk_usage = metrics_result.get("disk_usage")
|
|
1032
|
+
result.cpu_memory = metrics_result.get("cpu_memory")
|
|
1033
|
+
result.metrics_history = metrics_result.get("metrics_history")
|
|
1034
|
+
result.collection_status["metrics_api"] = {"status": "success"}
|
|
1035
|
+
else:
|
|
1036
|
+
result.collection_status["metrics_api"] = {
|
|
1037
|
+
"status": "error",
|
|
1038
|
+
"error": "Metrics API returned no data",
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
# Process MySQL data
|
|
1042
|
+
progress(4, 5, "Processing results...", quiet)
|
|
1043
|
+
mysql_errors = mysql_data.get("errors", [])
|
|
1044
|
+
if mysql_data.get("global_status"):
|
|
1045
|
+
parse_mysql_data(mysql_data, result)
|
|
1046
|
+
result.collection_status["mysql_query"] = {"status": "success"}
|
|
1047
|
+
if mysql_errors:
|
|
1048
|
+
result.collection_status["mysql_query"]["warnings"] = mysql_errors
|
|
1049
|
+
else:
|
|
1050
|
+
error_msg = "; ".join(mysql_errors) if mysql_errors else "No data returned"
|
|
1051
|
+
if not ssh_available:
|
|
1052
|
+
error_msg = f"SSH failed after {len(ssh_attempts)} attempts: {ssh_stderr or 'connection failed'}"
|
|
1053
|
+
result.errors.append(f"MySQL data collection failed: {error_msg}")
|
|
1054
|
+
result.collection_status["mysql_query"] = {
|
|
1055
|
+
"status": "error",
|
|
1056
|
+
"error": error_msg,
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
# Process logs
|
|
1060
|
+
if skip_logs:
|
|
1061
|
+
result.collection_status["logs_api"] = {"status": "skipped", "reason": "skip_logs flag set"}
|
|
1062
|
+
elif logs_result:
|
|
1063
|
+
result.recent_logs = logs_result
|
|
1064
|
+
result.collection_status["logs_api"] = {"status": "success", "lines": len(logs_result)}
|
|
1065
|
+
result.recent_errors = [
|
|
1066
|
+
line for line in result.recent_logs
|
|
1067
|
+
if "ERROR" in line.upper() or "FATAL" in line.upper()
|
|
1068
|
+
][:100]
|
|
1069
|
+
else:
|
|
1070
|
+
result.recent_logs = []
|
|
1071
|
+
result.collection_status["logs_api"] = {
|
|
1072
|
+
"status": "error",
|
|
1073
|
+
"error": "Logs API returned no data",
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
# === RECOMMENDATIONS ===
|
|
1077
|
+
progress(5, 5, "Generating recommendations...", quiet)
|
|
1078
|
+
result.recommendations = generate_recommendations(result)
|
|
1079
|
+
|
|
1080
|
+
if not quiet:
|
|
1081
|
+
total = dal._progress_timer.total_elapsed()
|
|
1082
|
+
print(f"Done.{total}", file=sys.stderr)
|
|
1083
|
+
|
|
1084
|
+
return result
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
# ---------------------------------------------------------------------------
|
|
1088
|
+
# Single-step debugging
|
|
1089
|
+
# ---------------------------------------------------------------------------
|
|
1090
|
+
|
|
1091
|
+
def run_single_step(args) -> int:
|
|
1092
|
+
"""Run a single collection step for debugging."""
|
|
1093
|
+
service = args.service
|
|
1094
|
+
_init_context(args)
|
|
1095
|
+
environment_id = dal._ctx.environment_id
|
|
1096
|
+
service_id = dal._ctx.service_id
|
|
1097
|
+
|
|
1098
|
+
if args.step == "ssh-test":
|
|
1099
|
+
print(f"Testing SSH to service: {service}", file=sys.stderr)
|
|
1100
|
+
code, stdout, stderr = run_ssh_query(service, "echo ok", timeout=45)
|
|
1101
|
+
print(f"Exit code: {code}")
|
|
1102
|
+
print(f"Stdout: {stdout.strip()}")
|
|
1103
|
+
if stderr:
|
|
1104
|
+
print(f"Stderr: {stderr.strip()}")
|
|
1105
|
+
return 0 if (code == 0 and "ok" in stdout) else 1
|
|
1106
|
+
|
|
1107
|
+
elif args.step == "query":
|
|
1108
|
+
print(f"Running MySQL queries on: {service}", file=sys.stderr)
|
|
1109
|
+
data = collect_mysql_data(service, timeout=args.timeout)
|
|
1110
|
+
print(json.dumps(data, indent=2, default=str))
|
|
1111
|
+
return 0 if data.get("global_status") else 1
|
|
1112
|
+
|
|
1113
|
+
elif args.step == "logs":
|
|
1114
|
+
print(f"Fetching logs for: {service}", file=sys.stderr)
|
|
1115
|
+
logs = get_recent_logs(service, lines=LOG_LINES_DEFAULT,
|
|
1116
|
+
environment_id=environment_id,
|
|
1117
|
+
service_id=service_id)
|
|
1118
|
+
print(f"Lines fetched: {len(logs)}")
|
|
1119
|
+
for line in logs:
|
|
1120
|
+
print(line)
|
|
1121
|
+
return 0
|
|
1122
|
+
|
|
1123
|
+
elif args.step == "metrics":
|
|
1124
|
+
print(f"Fetching metrics for: {service}", file=sys.stderr)
|
|
1125
|
+
if environment_id and service_id:
|
|
1126
|
+
metrics = get_all_metrics_from_api(environment_id, service_id)
|
|
1127
|
+
if metrics:
|
|
1128
|
+
print(json.dumps(metrics, indent=2))
|
|
1129
|
+
else:
|
|
1130
|
+
print("Metrics API returned no data")
|
|
1131
|
+
return 1
|
|
1132
|
+
else:
|
|
1133
|
+
print("Missing environment_id or service_id from railway config")
|
|
1134
|
+
return 1
|
|
1135
|
+
return 0
|
|
1136
|
+
|
|
1137
|
+
return 1
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
# ---------------------------------------------------------------------------
|
|
1141
|
+
# Main entry point
|
|
1142
|
+
# ---------------------------------------------------------------------------
|
|
1143
|
+
|
|
1144
|
+
def main():
|
|
1145
|
+
parser = argparse.ArgumentParser(
|
|
1146
|
+
description="MySQL analysis for Railway services.",
|
|
1147
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
parser.add_argument("--service", required=True, help="Service name")
|
|
1151
|
+
parser.add_argument("--json", action="store_true",
|
|
1152
|
+
help="Output as JSON")
|
|
1153
|
+
parser.add_argument("--timeout", type=int, default=60,
|
|
1154
|
+
help="Timeout in seconds for SSH queries (default: 60)")
|
|
1155
|
+
parser.add_argument("--quiet", "-q", action="store_true",
|
|
1156
|
+
help="Suppress progress messages")
|
|
1157
|
+
parser.add_argument("--skip-logs", action="store_true",
|
|
1158
|
+
help="Skip log fetching for faster analysis")
|
|
1159
|
+
parser.add_argument("--metrics-hours", type=int, default=168,
|
|
1160
|
+
help="Hours of metrics history to fetch (default: 168, max: 168)")
|
|
1161
|
+
parser.add_argument("--step", choices=["ssh-test", "query", "logs", "metrics"],
|
|
1162
|
+
help="Run a single collection step for debugging")
|
|
1163
|
+
parser.add_argument("--project-id", help="Project ID (bypasses railway link)")
|
|
1164
|
+
parser.add_argument("--environment-id", help="Environment ID (bypasses railway link)")
|
|
1165
|
+
parser.add_argument("--service-id", help="Service ID (bypasses railway link)")
|
|
1166
|
+
|
|
1167
|
+
args = parser.parse_args()
|
|
1168
|
+
|
|
1169
|
+
# Single-step debugging mode
|
|
1170
|
+
if args.step:
|
|
1171
|
+
return run_single_step(args)
|
|
1172
|
+
|
|
1173
|
+
# Run analysis
|
|
1174
|
+
result = analyze_mysql(
|
|
1175
|
+
args.service,
|
|
1176
|
+
timeout=args.timeout,
|
|
1177
|
+
quiet=args.quiet,
|
|
1178
|
+
skip_logs=args.skip_logs,
|
|
1179
|
+
metrics_hours=min(args.metrics_hours, 168),
|
|
1180
|
+
project_id=args.project_id,
|
|
1181
|
+
environment_id=args.environment_id,
|
|
1182
|
+
service_id=args.service_id,
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
# Output
|
|
1186
|
+
if args.json:
|
|
1187
|
+
print(json.dumps(asdict(result), indent=2))
|
|
1188
|
+
else:
|
|
1189
|
+
print(format_report(result))
|
|
1190
|
+
|
|
1191
|
+
return 0
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
if __name__ == "__main__":
|
|
1195
|
+
sys.exit(main())
|