moai-adk 0.25.4__py3-none-any.whl ā 0.32.8__py3-none-any.whl
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.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/__init__.py +2 -5
- moai_adk/__main__.py +114 -82
- moai_adk/cli/__init__.py +6 -1
- moai_adk/cli/commands/__init__.py +1 -3
- moai_adk/cli/commands/analyze.py +5 -16
- moai_adk/cli/commands/doctor.py +6 -18
- moai_adk/cli/commands/init.py +56 -125
- moai_adk/cli/commands/language.py +14 -35
- moai_adk/cli/commands/status.py +9 -15
- moai_adk/cli/commands/update.py +1555 -190
- moai_adk/cli/prompts/init_prompts.py +112 -56
- moai_adk/cli/spec_status.py +263 -0
- moai_adk/cli/ui/__init__.py +44 -0
- moai_adk/cli/ui/progress.py +422 -0
- moai_adk/cli/ui/prompts.py +389 -0
- moai_adk/cli/ui/theme.py +129 -0
- moai_adk/cli/worktree/__init__.py +27 -0
- moai_adk/cli/worktree/__main__.py +31 -0
- moai_adk/cli/worktree/cli.py +672 -0
- moai_adk/cli/worktree/exceptions.py +89 -0
- moai_adk/cli/worktree/manager.py +490 -0
- moai_adk/cli/worktree/models.py +65 -0
- moai_adk/cli/worktree/registry.py +128 -0
- moai_adk/core/PHASE2_OPTIMIZATIONS.md +467 -0
- moai_adk/core/analysis/session_analyzer.py +17 -56
- moai_adk/core/claude_integration.py +26 -54
- moai_adk/core/command_helpers.py +10 -10
- moai_adk/core/comprehensive_monitoring_system.py +1183 -0
- moai_adk/core/config/auto_spec_config.py +5 -11
- moai_adk/core/config/migration.py +19 -9
- moai_adk/core/config/unified.py +436 -0
- moai_adk/core/context_manager.py +6 -12
- moai_adk/core/enterprise_features.py +1404 -0
- moai_adk/core/error_recovery_system.py +725 -112
- moai_adk/core/event_driven_hook_system.py +1371 -0
- moai_adk/core/git/__init__.py +8 -0
- moai_adk/core/git/branch_manager.py +3 -11
- moai_adk/core/git/checkpoint.py +1 -3
- moai_adk/core/git/conflict_detector.py +413 -0
- moai_adk/core/git/manager.py +91 -1
- moai_adk/core/hooks/post_tool_auto_spec_completion.py +56 -80
- moai_adk/core/input_validation_middleware.py +1006 -0
- moai_adk/core/integration/engine.py +6 -18
- moai_adk/core/integration/integration_tester.py +10 -9
- moai_adk/core/integration/utils.py +1 -1
- moai_adk/core/issue_creator.py +10 -28
- moai_adk/core/jit_context_loader.py +956 -0
- moai_adk/core/jit_enhanced_hook_manager.py +1987 -0
- moai_adk/core/language_config_resolver.py +485 -0
- moai_adk/core/language_validator.py +28 -41
- moai_adk/core/mcp/setup.py +15 -12
- moai_adk/core/merge/__init__.py +9 -0
- moai_adk/core/merge/analyzer.py +481 -0
- moai_adk/core/migration/alfred_to_moai_migrator.py +383 -0
- moai_adk/core/migration/backup_manager.py +78 -9
- moai_adk/core/migration/custom_element_scanner.py +358 -0
- moai_adk/core/migration/file_migrator.py +8 -17
- moai_adk/core/migration/interactive_checkbox_ui.py +488 -0
- moai_adk/core/migration/selective_restorer.py +470 -0
- moai_adk/core/migration/template_utils.py +74 -0
- moai_adk/core/migration/user_selection_ui.py +338 -0
- moai_adk/core/migration/version_detector.py +6 -10
- moai_adk/core/migration/version_migrator.py +3 -3
- moai_adk/core/performance/cache_system.py +8 -10
- moai_adk/core/phase_optimized_hook_scheduler.py +879 -0
- moai_adk/core/project/checker.py +2 -4
- moai_adk/core/project/detector.py +1 -3
- moai_adk/core/project/initializer.py +135 -23
- moai_adk/core/project/phase_executor.py +54 -81
- moai_adk/core/project/validator.py +6 -12
- moai_adk/core/quality/trust_checker.py +9 -27
- moai_adk/core/realtime_monitoring_dashboard.py +1724 -0
- moai_adk/core/robust_json_parser.py +611 -0
- moai_adk/core/rollback_manager.py +73 -148
- moai_adk/core/session_manager.py +10 -26
- moai_adk/core/skill_loading_system.py +579 -0
- moai_adk/core/spec/confidence_scoring.py +31 -100
- moai_adk/core/spec/ears_template_engine.py +351 -286
- moai_adk/core/spec/quality_validator.py +35 -69
- moai_adk/core/spec_status_manager.py +64 -74
- moai_adk/core/template/backup.py +45 -20
- moai_adk/core/template/config.py +112 -39
- moai_adk/core/template/merger.py +11 -19
- moai_adk/core/template/processor.py +253 -149
- moai_adk/core/template_engine.py +73 -40
- moai_adk/core/template_variable_synchronizer.py +417 -0
- moai_adk/core/unified_permission_manager.py +745 -0
- moai_adk/core/user_behavior_analytics.py +851 -0
- moai_adk/core/version_sync.py +429 -0
- moai_adk/foundation/__init__.py +56 -0
- moai_adk/foundation/backend.py +1027 -0
- moai_adk/foundation/database.py +1115 -0
- moai_adk/foundation/devops.py +1585 -0
- moai_adk/foundation/ears.py +431 -0
- moai_adk/foundation/frontend.py +870 -0
- moai_adk/foundation/git/commit_templates.py +4 -12
- moai_adk/foundation/git.py +376 -0
- moai_adk/foundation/langs.py +484 -0
- moai_adk/foundation/ml_ops.py +1162 -0
- moai_adk/foundation/testing.py +1524 -0
- moai_adk/foundation/trust/trust_principles.py +23 -72
- moai_adk/foundation/trust/validation_checklist.py +57 -162
- moai_adk/project/__init__.py +0 -0
- moai_adk/project/configuration.py +1084 -0
- moai_adk/project/documentation.py +566 -0
- moai_adk/project/schema.py +447 -0
- moai_adk/statusline/alfred_detector.py +1 -3
- moai_adk/statusline/config.py +13 -4
- moai_adk/statusline/enhanced_output_style_detector.py +23 -15
- moai_adk/statusline/main.py +51 -15
- moai_adk/statusline/renderer.py +104 -48
- moai_adk/statusline/update_checker.py +3 -9
- moai_adk/statusline/version_reader.py +140 -46
- moai_adk/templates/.claude/agents/moai/ai-nano-banana.md +549 -0
- moai_adk/templates/.claude/agents/moai/builder-agent.md +445 -0
- moai_adk/templates/.claude/agents/moai/builder-command.md +1132 -0
- moai_adk/templates/.claude/agents/moai/builder-skill.md +601 -0
- moai_adk/templates/.claude/agents/moai/expert-backend.md +831 -0
- moai_adk/templates/.claude/agents/moai/expert-database.md +774 -0
- moai_adk/templates/.claude/agents/moai/expert-debug.md +396 -0
- moai_adk/templates/.claude/agents/moai/expert-devops.md +711 -0
- moai_adk/templates/.claude/agents/moai/expert-frontend.md +666 -0
- moai_adk/templates/.claude/agents/moai/expert-security.md +474 -0
- moai_adk/templates/.claude/agents/moai/expert-uiux.md +1038 -0
- moai_adk/templates/.claude/agents/moai/manager-claude-code.md +429 -0
- moai_adk/templates/.claude/agents/moai/manager-docs.md +570 -0
- moai_adk/templates/.claude/agents/moai/manager-git.md +937 -0
- moai_adk/templates/.claude/agents/moai/manager-project.md +891 -0
- moai_adk/templates/.claude/agents/moai/manager-quality.md +598 -0
- moai_adk/templates/.claude/agents/moai/manager-spec.md +713 -0
- moai_adk/templates/.claude/agents/moai/manager-strategy.md +600 -0
- moai_adk/templates/.claude/agents/moai/manager-tdd.md +603 -0
- moai_adk/templates/.claude/agents/moai/mcp-context7.md +369 -0
- moai_adk/templates/.claude/agents/moai/mcp-figma.md +1567 -0
- moai_adk/templates/.claude/agents/moai/mcp-notion.md +749 -0
- moai_adk/templates/.claude/agents/moai/mcp-playwright.md +427 -0
- moai_adk/templates/.claude/agents/moai/mcp-sequential-thinking.md +994 -0
- moai_adk/templates/.claude/commands/moai/0-project.md +1143 -0
- moai_adk/templates/.claude/commands/moai/1-plan.md +1435 -0
- moai_adk/templates/.claude/commands/moai/2-run.md +883 -0
- moai_adk/templates/.claude/commands/moai/3-sync.md +993 -0
- moai_adk/templates/.claude/commands/moai/9-feedback.md +314 -0
- moai_adk/templates/.claude/hooks/__init__.py +8 -0
- moai_adk/templates/.claude/hooks/moai/__init__.py +8 -0
- moai_adk/templates/.claude/hooks/moai/lib/__init__.py +85 -0
- moai_adk/templates/.claude/hooks/moai/lib/checkpoint.py +244 -0
- moai_adk/templates/.claude/hooks/moai/lib/common.py +131 -0
- moai_adk/templates/.claude/hooks/moai/lib/config_manager.py +446 -0
- moai_adk/templates/.claude/hooks/moai/lib/config_validator.py +639 -0
- moai_adk/templates/.claude/hooks/moai/lib/example_config.json +104 -0
- moai_adk/templates/.claude/hooks/moai/lib/git_operations_manager.py +590 -0
- moai_adk/templates/.claude/hooks/moai/lib/language_validator.py +317 -0
- moai_adk/templates/.claude/hooks/moai/lib/models.py +102 -0
- moai_adk/templates/.claude/hooks/moai/lib/path_utils.py +28 -0
- moai_adk/templates/.claude/hooks/moai/lib/project.py +768 -0
- moai_adk/templates/.claude/hooks/moai/lib/test_hooks_improvements.py +443 -0
- moai_adk/templates/.claude/hooks/moai/lib/timeout.py +160 -0
- moai_adk/templates/.claude/hooks/moai/lib/unified_timeout_manager.py +530 -0
- moai_adk/templates/.claude/hooks/moai/session_end__auto_cleanup.py +862 -0
- moai_adk/templates/.claude/hooks/moai/session_start__show_project_info.py +921 -0
- moai_adk/templates/.claude/output-styles/moai/r2d2.md +380 -0
- moai_adk/templates/.claude/output-styles/moai/yoda.md +338 -0
- moai_adk/templates/.claude/settings.json +172 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/SKILL.md +247 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/README.md +44 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/api-documentation.md +130 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/code-documentation.md +152 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/multi-format-output.md +178 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/user-guides.md +147 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +319 -0
- moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +320 -0
- moai_adk/templates/.claude/skills/moai-domain-database/modules/README.md +53 -0
- moai_adk/templates/.claude/skills/moai-domain-database/modules/mongodb.md +231 -0
- moai_adk/templates/.claude/skills/moai-domain-database/modules/postgresql.md +169 -0
- moai_adk/templates/.claude/skills/moai-domain-database/modules/redis.md +262 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +496 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/SKILL.md +453 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/examples.md +560 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/accessibility-wcag.md +260 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/component-architecture.md +228 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/design-system-tokens.md +405 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/icon-libraries.md +401 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/theming-system.md +373 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/reference.md +243 -0
- moai_adk/templates/.claude/skills/moai-formats-data/SKILL.md +491 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/README.md +98 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/SKILL-MODULARIZATION-TEMPLATE.md +278 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/caching-performance.md +459 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/data-validation.md +485 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/json-optimization.md +374 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/toon-encoding.md +308 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/SKILL.md +201 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/best-practices-checklist.md +616 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-custom-slash-commands-official.md +729 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-hooks-official.md +560 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-iam-official.md +635 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-memory-official.md +543 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-settings-official.md +663 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-skills-official.md +113 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-sub-agents-official.md +238 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/complete-configuration-guide.md +175 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/skill-examples.md +1674 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/skill-formatting-guide.md +729 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-examples.md +1513 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-formatting-guide.md +1086 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-integration-patterns.md +1100 -0
- moai_adk/templates/.claude/skills/moai-foundation-context/SKILL.md +438 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/SKILL.md +515 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/README.md +296 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/agents-reference.md +346 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/commands-reference.md +432 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/delegation-patterns.md +757 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/execution-rules.md +687 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/modular-system.md +665 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/progressive-disclosure.md +649 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/spec-first-tdd.md +864 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/token-optimization.md +708 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/trust-5-framework.md +981 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/SKILL.md +362 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/examples.md +1232 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/modules/best-practices.md +261 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/modules/integration-patterns.md +194 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/modules/proactive-analysis.md +229 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/modules/trust5-validation.md +169 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/reference.md +1266 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/scripts/quality-gate.sh +668 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/templates/github-actions-quality.yml +481 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/templates/quality-config.yaml +519 -0
- moai_adk/templates/.claude/skills/moai-integration-mcp/SKILL.md +352 -0
- moai_adk/templates/.claude/skills/moai-integration-mcp/modules/README.md +52 -0
- moai_adk/templates/.claude/skills/moai-integration-mcp/modules/error-handling.md +334 -0
- moai_adk/templates/.claude/skills/moai-integration-mcp/modules/integration-patterns.md +310 -0
- moai_adk/templates/.claude/skills/moai-integration-mcp/modules/security-authentication.md +256 -0
- moai_adk/templates/.claude/skills/moai-integration-mcp/modules/server-architecture.md +253 -0
- moai_adk/templates/.claude/skills/moai-lang-unified/README.md +133 -0
- moai_adk/templates/.claude/skills/moai-lang-unified/SKILL.md +296 -0
- moai_adk/templates/.claude/skills/moai-lang-unified/examples.md +1269 -0
- moai_adk/templates/.claude/skills/moai-lang-unified/reference.md +331 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/SKILL.md +298 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/advanced-patterns.md +465 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/examples.md +270 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/optimization.md +440 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/reference.md +228 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/SKILL.md +316 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/advanced-patterns.md +336 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-deployment-patterns.md +182 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-patterns.md +17 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/configuration.md +57 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/content-architecture-optimization.md +162 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/deployment.md +52 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/framework-core-configuration.md +186 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/i18n-setup.md +55 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/mdx-components.md +52 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/optimization.md +303 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/SKILL.md +370 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/examples.md +575 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/modules/advanced-patterns.md +394 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/modules/optimization.md +278 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/modules/shadcn-components.md +457 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/modules/shadcn-theming.md +373 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/reference.md +74 -0
- moai_adk/templates/.claude/skills/moai-platform-baas/README.md +186 -0
- moai_adk/templates/.claude/skills/moai-platform-baas/SKILL.md +290 -0
- moai_adk/templates/.claude/skills/moai-platform-baas/examples.md +1225 -0
- moai_adk/templates/.claude/skills/moai-platform-baas/reference.md +567 -0
- moai_adk/templates/.claude/skills/moai-platform-baas/scripts/provider-selector.py +323 -0
- moai_adk/templates/.claude/skills/moai-platform-baas/templates/stack-config.yaml +204 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/SKILL.md +446 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/advanced-patterns.md +379 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/optimization.md +286 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/README.md +190 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/SKILL.md +387 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/__init__.py +520 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/complete_workflow_demo_fixed.py +574 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_project_setup.py +317 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_workflow_demo.py +663 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/config-migration-example.json +190 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/question-examples.json +135 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/quick_start.py +196 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/__init__.py +17 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/advanced-patterns.md +158 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/ask_user_integration.py +340 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/batch_questions.py +713 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/config_manager.py +538 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/documentation_manager.py +1336 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/language_initializer.py +730 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/migration_manager.py +608 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/template_optimizer.py +1005 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/schemas/config-schema.json +316 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/schemas/tab_schema.json +1362 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/config-template.json +71 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/product-template.md +44 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/structure-template.md +48 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/tech-template.md +71 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/config-manager-setup.json +109 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/language-initializer.json +228 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/menu-project-config.json +130 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/project-batch-questions.json +97 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/spec-workflow-setup.json +150 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/test_integration_simple.py +436 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/SKILL.md +374 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/modules/code-templates.md +124 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/modules/feedback-templates.md +100 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/modules/template-optimizer.md +138 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/LICENSE.txt +202 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/SKILL.md +453 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/advanced-patterns.md +576 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples/ai-powered-testing.py +294 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples/console_logging.py +35 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples/element_discovery.py +40 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples/static_html_automation.py +34 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/README.md +220 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/ai-debugging.md +845 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review.md +1416 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization.md +1234 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/smart-refactoring.md +1243 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7.md +1260 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/optimization.md +505 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/reference/playwright-best-practices.md +57 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/scripts/with_server.py +218 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/templates/alfred-integration.md +376 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/workflows/enterprise-testing-workflow.py +571 -0
- moai_adk/templates/.claude/skills/moai-worktree/SKILL.md +410 -0
- moai_adk/templates/.claude/skills/moai-worktree/examples.md +606 -0
- moai_adk/templates/.claude/skills/moai-worktree/modules/integration-patterns.md +982 -0
- moai_adk/templates/.claude/skills/moai-worktree/modules/parallel-development.md +778 -0
- moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-commands.md +646 -0
- moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-management.md +782 -0
- moai_adk/templates/.claude/skills/moai-worktree/reference.md +357 -0
- moai_adk/templates/.git-hooks/pre-commit +103 -41
- moai_adk/templates/.git-hooks/pre-push +116 -21
- moai_adk/templates/.github/workflows/ci-universal.yml +513 -0
- moai_adk/templates/.github/workflows/security-secrets-check.yml +179 -0
- moai_adk/templates/.gitignore +184 -44
- moai_adk/templates/.mcp.json +7 -9
- moai_adk/templates/.moai/cache/personalization.json +10 -0
- moai_adk/templates/.moai/config/config.yaml +344 -0
- moai_adk/templates/.moai/config/presets/manual.yaml +28 -0
- moai_adk/templates/.moai/config/presets/personal.yaml +30 -0
- moai_adk/templates/.moai/config/presets/team.yaml +33 -0
- moai_adk/templates/.moai/config/questions/_schema.yaml +79 -0
- moai_adk/templates/.moai/config/questions/tab1-user.yaml +108 -0
- moai_adk/templates/.moai/config/questions/tab2-project.yaml +122 -0
- moai_adk/templates/.moai/config/questions/tab3-git.yaml +542 -0
- moai_adk/templates/.moai/config/questions/tab4-quality.yaml +167 -0
- moai_adk/templates/.moai/config/questions/tab5-system.yaml +152 -0
- moai_adk/templates/.moai/config/sections/git-strategy.yaml +40 -0
- moai_adk/templates/.moai/config/sections/language.yaml +11 -0
- moai_adk/templates/.moai/config/sections/project.yaml +13 -0
- moai_adk/templates/.moai/config/sections/quality.yaml +15 -0
- moai_adk/templates/.moai/config/sections/system.yaml +14 -0
- moai_adk/templates/.moai/config/sections/user.yaml +5 -0
- moai_adk/templates/.moai/config/statusline-config.yaml +86 -0
- moai_adk/templates/.moai/scripts/setup-glm.py +136 -0
- moai_adk/templates/CLAUDE.md +382 -501
- moai_adk/utils/__init__.py +24 -1
- moai_adk/utils/banner.py +7 -10
- moai_adk/utils/common.py +16 -30
- moai_adk/utils/link_validator.py +4 -12
- moai_adk/utils/safe_file_reader.py +2 -6
- moai_adk/utils/timeout.py +160 -0
- moai_adk/utils/toon_utils.py +256 -0
- moai_adk/version.py +22 -0
- moai_adk-0.32.8.dist-info/METADATA +2478 -0
- moai_adk-0.32.8.dist-info/RECORD +396 -0
- {moai_adk-0.25.4.dist-info ā moai_adk-0.32.8.dist-info}/WHEEL +1 -1
- {moai_adk-0.25.4.dist-info ā moai_adk-0.32.8.dist-info}/entry_points.txt +1 -0
- moai_adk/cli/commands/backup.py +0 -82
- moai_adk/cli/commands/improve_user_experience.py +0 -348
- moai_adk/cli/commands/migrate.py +0 -158
- moai_adk/cli/commands/validate_links.py +0 -118
- moai_adk/templates/.github/workflows/moai-gitflow.yml +0 -413
- moai_adk/templates/.github/workflows/moai-release-create.yml +0 -100
- moai_adk/templates/.github/workflows/moai-release-pipeline.yml +0 -188
- moai_adk/utils/user_experience.py +0 -531
- moai_adk-0.25.4.dist-info/METADATA +0 -2279
- moai_adk-0.25.4.dist-info/RECORD +0 -112
- {moai_adk-0.25.4.dist-info ā moai_adk-0.32.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1724 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real-time Monitoring Dashboard
|
|
3
|
+
|
|
4
|
+
Phase 3: Enterprise-grade real-time monitoring dashboard with alerting,
|
|
5
|
+
analytics, and visualization capabilities for the hook system.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Real-time performance monitoring and visualization
|
|
9
|
+
- Automated alerting with configurable thresholds
|
|
10
|
+
- Integration with external monitoring systems (Prometheus, Grafana, DataDog)
|
|
11
|
+
- Visual analytics for system health and performance
|
|
12
|
+
- REST API for monitoring data access
|
|
13
|
+
- WebSocket support for real-time updates
|
|
14
|
+
- Historical data analysis and trend prediction
|
|
15
|
+
- Multi-tenant dashboard support
|
|
16
|
+
- Custom alert rule engine
|
|
17
|
+
- Performance bottleneck detection
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import logging
|
|
22
|
+
import statistics
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
26
|
+
from collections import defaultdict, deque
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime, timedelta
|
|
29
|
+
from enum import Enum
|
|
30
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MetricType(Enum):
|
|
36
|
+
"""Types of metrics collected by the monitoring system"""
|
|
37
|
+
|
|
38
|
+
SYSTEM_PERFORMANCE = "system_performance"
|
|
39
|
+
HOOK_EXECUTION = "hook_execution"
|
|
40
|
+
ERROR_RATE = "error_rate"
|
|
41
|
+
RESPONSE_TIME = "response_time"
|
|
42
|
+
MEMORY_USAGE = "memory_usage"
|
|
43
|
+
CPU_USAGE = "cpu_usage"
|
|
44
|
+
NETWORK_IO = "network_io"
|
|
45
|
+
DISK_IO = "disk_io"
|
|
46
|
+
THROUGHPUT = "throughput"
|
|
47
|
+
AVAILABILITY = "availability"
|
|
48
|
+
CACHE_PERFORMANCE = "cache_performance"
|
|
49
|
+
DATABASE_PERFORMANCE = "database_performance"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AlertSeverity(Enum):
|
|
53
|
+
"""Alert severity levels"""
|
|
54
|
+
|
|
55
|
+
INFO = 1
|
|
56
|
+
WARNING = 2
|
|
57
|
+
ERROR = 3
|
|
58
|
+
CRITICAL = 4
|
|
59
|
+
EMERGENCY = 5
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DashboardType(Enum):
|
|
63
|
+
"""Dashboard types"""
|
|
64
|
+
|
|
65
|
+
SYSTEM_OVERVIEW = "system_overview"
|
|
66
|
+
PERFORMANCE_ANALYTICS = "performance_analytics"
|
|
67
|
+
ERROR_MONITORING = "error_monitoring"
|
|
68
|
+
HOOK_ANALYSIS = "hook_analysis"
|
|
69
|
+
RESOURCE_USAGE = "resource_usage"
|
|
70
|
+
CUSTOM = "custom"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class MetricData:
|
|
75
|
+
"""Single metric data point with enhanced metadata"""
|
|
76
|
+
|
|
77
|
+
timestamp: datetime
|
|
78
|
+
metric_type: MetricType
|
|
79
|
+
value: Union[int, float, str, bool]
|
|
80
|
+
tags: Dict[str, str] = field(default_factory=dict)
|
|
81
|
+
source: str = ""
|
|
82
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
83
|
+
component: str = ""
|
|
84
|
+
environment: str = "production"
|
|
85
|
+
tenant_id: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
88
|
+
"""Convert to dictionary for serialization"""
|
|
89
|
+
return {
|
|
90
|
+
"timestamp": self.timestamp.isoformat(),
|
|
91
|
+
"metric_type": self.metric_type.value,
|
|
92
|
+
"value": self.value,
|
|
93
|
+
"tags": self.tags,
|
|
94
|
+
"source": self.source,
|
|
95
|
+
"metadata": self.metadata,
|
|
96
|
+
"component": self.component,
|
|
97
|
+
"environment": self.environment,
|
|
98
|
+
"tenant_id": self.tenant_id,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class Alert:
|
|
104
|
+
"""Enhanced alert definition with correlation and context"""
|
|
105
|
+
|
|
106
|
+
alert_id: str
|
|
107
|
+
severity: AlertSeverity
|
|
108
|
+
title: str
|
|
109
|
+
description: str
|
|
110
|
+
timestamp: datetime
|
|
111
|
+
metric_type: MetricType
|
|
112
|
+
threshold: float
|
|
113
|
+
current_value: float
|
|
114
|
+
source: str
|
|
115
|
+
component: str
|
|
116
|
+
tags: Dict[str, str] = field(default_factory=dict)
|
|
117
|
+
resolved: bool = False
|
|
118
|
+
resolved_at: Optional[datetime] = None
|
|
119
|
+
acknowledged: bool = False
|
|
120
|
+
acknowledged_at: Optional[datetime] = None
|
|
121
|
+
correlation_id: Optional[str] = None
|
|
122
|
+
context: Dict[str, Any] = field(default_factory=dict)
|
|
123
|
+
affected_services: List[str] = field(default_factory=list)
|
|
124
|
+
recovery_actions: List[str] = field(default_factory=list)
|
|
125
|
+
tenant_id: Optional[str] = None
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
128
|
+
"""Convert to dictionary for serialization"""
|
|
129
|
+
return {
|
|
130
|
+
"alert_id": self.alert_id,
|
|
131
|
+
"severity": self.severity.value,
|
|
132
|
+
"title": self.title,
|
|
133
|
+
"description": self.description,
|
|
134
|
+
"timestamp": self.timestamp.isoformat(),
|
|
135
|
+
"metric_type": self.metric_type.value,
|
|
136
|
+
"threshold": self.threshold,
|
|
137
|
+
"current_value": self.current_value,
|
|
138
|
+
"source": self.source,
|
|
139
|
+
"component": self.component,
|
|
140
|
+
"tags": self.tags,
|
|
141
|
+
"resolved": self.resolved,
|
|
142
|
+
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None,
|
|
143
|
+
"acknowledged": self.acknowledged,
|
|
144
|
+
"acknowledged_at": (self.acknowledged_at.isoformat() if self.acknowledged_at else None),
|
|
145
|
+
"correlation_id": self.correlation_id,
|
|
146
|
+
"context": self.context,
|
|
147
|
+
"affected_services": self.affected_services,
|
|
148
|
+
"recovery_actions": self.recovery_actions,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class DashboardWidget:
|
|
154
|
+
"""Dashboard widget definition"""
|
|
155
|
+
|
|
156
|
+
widget_id: str
|
|
157
|
+
widget_type: str # "chart", "metric", "table", "alert", "heatmap"
|
|
158
|
+
title: str
|
|
159
|
+
position: Dict[str, Any] # {"x": 0, "y": 0, "width": 4, "height": 2}
|
|
160
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
161
|
+
data_source: str = ""
|
|
162
|
+
refresh_interval_seconds: int = 30
|
|
163
|
+
metrics: List[str] = field(default_factory=list)
|
|
164
|
+
filters: Dict[str, Any] = field(default_factory=dict)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class Dashboard:
|
|
169
|
+
"""Dashboard definition with widgets and layout"""
|
|
170
|
+
|
|
171
|
+
dashboard_id: str
|
|
172
|
+
name: str
|
|
173
|
+
description: str
|
|
174
|
+
dashboard_type: DashboardType
|
|
175
|
+
widgets: List[DashboardWidget] = field(default_factory=list)
|
|
176
|
+
layout: Dict[str, Any] = field(default_factory=dict)
|
|
177
|
+
filters: Dict[str, Any] = field(default_factory=dict)
|
|
178
|
+
owner: str = ""
|
|
179
|
+
tenant_id: Optional[str] = None
|
|
180
|
+
is_public: bool = False
|
|
181
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
182
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
183
|
+
|
|
184
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
185
|
+
"""Convert to dictionary for serialization"""
|
|
186
|
+
return {
|
|
187
|
+
"dashboard_id": self.dashboard_id,
|
|
188
|
+
"name": self.name,
|
|
189
|
+
"description": self.description,
|
|
190
|
+
"dashboard_type": self.dashboard_type.value,
|
|
191
|
+
"widgets": [w.__dict__ for w in self.widgets],
|
|
192
|
+
"layout": self.layout,
|
|
193
|
+
"filters": self.filters,
|
|
194
|
+
"owner": self.owner,
|
|
195
|
+
"tenant_id": self.tenant_id,
|
|
196
|
+
"is_public": self.is_public,
|
|
197
|
+
"created_at": self.created_at.isoformat(),
|
|
198
|
+
"updated_at": self.updated_at.isoformat(),
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class MetricsCollector:
|
|
203
|
+
"""Advanced metrics collection with multi-tenant support"""
|
|
204
|
+
|
|
205
|
+
def __init__(self, buffer_size: int = 100000, retention_hours: int = 168): # 7 days
|
|
206
|
+
self.buffer_size = buffer_size
|
|
207
|
+
self.retention_hours = retention_hours
|
|
208
|
+
self.metrics_buffer: Dict[str, deque] = defaultdict(lambda: deque(maxlen=buffer_size))
|
|
209
|
+
self.aggregated_metrics: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
|
210
|
+
self.tenant_metrics: Dict[str, Dict[str, deque]] = defaultdict(lambda: defaultdict(deque))
|
|
211
|
+
self._lock = threading.Lock()
|
|
212
|
+
self._last_cleanup = datetime.now()
|
|
213
|
+
|
|
214
|
+
def add_metric(self, metric: MetricData) -> None:
|
|
215
|
+
"""Add a metric to the collection with tenant support"""
|
|
216
|
+
with self._lock:
|
|
217
|
+
# Global metrics buffer
|
|
218
|
+
key = f"{metric.metric_type.value}:{metric.component}"
|
|
219
|
+
self.metrics_buffer[key].append(metric)
|
|
220
|
+
|
|
221
|
+
# Tenant-specific metrics
|
|
222
|
+
if metric.tenant_id:
|
|
223
|
+
tenant_key = f"{metric.metric_type.value}:{metric.component}:{metric.tenant_id}"
|
|
224
|
+
self.tenant_metrics[metric.tenant_id][tenant_key].append(metric)
|
|
225
|
+
|
|
226
|
+
# Update aggregated statistics
|
|
227
|
+
self._update_aggregated_metrics(metric)
|
|
228
|
+
self._cleanup_old_metrics()
|
|
229
|
+
|
|
230
|
+
def _update_aggregated_metrics(self, metric: MetricData) -> None:
|
|
231
|
+
"""Update aggregated statistics for a metric type"""
|
|
232
|
+
key = f"{metric.metric_type.value}:{metric.component}"
|
|
233
|
+
|
|
234
|
+
if key not in self.aggregated_metrics:
|
|
235
|
+
self.aggregated_metrics[key] = {
|
|
236
|
+
"count": 0,
|
|
237
|
+
"sum": 0,
|
|
238
|
+
"min": float("inf"),
|
|
239
|
+
"max": float("-inf"),
|
|
240
|
+
"values": [],
|
|
241
|
+
"last_updated": datetime.now(),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
agg = self.aggregated_metrics[key]
|
|
245
|
+
|
|
246
|
+
if isinstance(metric.value, (int, float)):
|
|
247
|
+
agg["count"] += 1
|
|
248
|
+
agg["sum"] += metric.value
|
|
249
|
+
agg["min"] = min(agg["min"], metric.value)
|
|
250
|
+
agg["max"] = max(agg["max"], metric.value)
|
|
251
|
+
agg["values"].append(metric.value)
|
|
252
|
+
agg["last_updated"] = datetime.now()
|
|
253
|
+
|
|
254
|
+
# Keep only recent values for statistics
|
|
255
|
+
if len(agg["values"]) > 10000:
|
|
256
|
+
agg["values"] = agg["values"][-10000:]
|
|
257
|
+
|
|
258
|
+
def _cleanup_old_metrics(self) -> None:
|
|
259
|
+
"""Remove metrics older than retention period"""
|
|
260
|
+
now = datetime.now()
|
|
261
|
+
if (now - self._last_cleanup).seconds < 300: # Cleanup every 5 minutes
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
cutoff_time = now - timedelta(hours=self.retention_hours)
|
|
265
|
+
|
|
266
|
+
# Cleanup global metrics
|
|
267
|
+
for key, buffer in self.metrics_buffer.items():
|
|
268
|
+
while buffer and buffer[0].timestamp < cutoff_time:
|
|
269
|
+
buffer.popleft()
|
|
270
|
+
|
|
271
|
+
# Cleanup tenant metrics
|
|
272
|
+
for tenant_id, tenant_buffers in self.tenant_metrics.items():
|
|
273
|
+
for key, buffer in tenant_buffers.items():
|
|
274
|
+
while buffer and buffer[0].timestamp < cutoff_time:
|
|
275
|
+
buffer.popleft()
|
|
276
|
+
|
|
277
|
+
self._last_cleanup = now
|
|
278
|
+
|
|
279
|
+
def get_metrics(
|
|
280
|
+
self,
|
|
281
|
+
metric_type: Optional[MetricType] = None,
|
|
282
|
+
component: Optional[str] = None,
|
|
283
|
+
tenant_id: Optional[str] = None,
|
|
284
|
+
start_time: Optional[datetime] = None,
|
|
285
|
+
end_time: Optional[datetime] = None,
|
|
286
|
+
limit: Optional[int] = None,
|
|
287
|
+
tags: Optional[Dict[str, str]] = None,
|
|
288
|
+
) -> List[MetricData]:
|
|
289
|
+
"""Get metrics with comprehensive filtering"""
|
|
290
|
+
with self._lock:
|
|
291
|
+
# Choose the right metrics source
|
|
292
|
+
source_metrics: List[MetricData] = []
|
|
293
|
+
if tenant_id:
|
|
294
|
+
for buffer in self.tenant_metrics.get(tenant_id, {}).values():
|
|
295
|
+
source_metrics.extend(buffer)
|
|
296
|
+
else:
|
|
297
|
+
for buffer in self.metrics_buffer.values():
|
|
298
|
+
source_metrics.extend(buffer)
|
|
299
|
+
|
|
300
|
+
# Apply filters
|
|
301
|
+
metrics = source_metrics
|
|
302
|
+
|
|
303
|
+
# Filter by metric type
|
|
304
|
+
if metric_type:
|
|
305
|
+
metrics = [m for m in metrics if m.metric_type == metric_type]
|
|
306
|
+
|
|
307
|
+
# Filter by component
|
|
308
|
+
if component:
|
|
309
|
+
metrics = [m for m in metrics if m.component == component]
|
|
310
|
+
|
|
311
|
+
# Filter by time range
|
|
312
|
+
if start_time:
|
|
313
|
+
metrics = [m for m in metrics if m.timestamp >= start_time]
|
|
314
|
+
if end_time:
|
|
315
|
+
metrics = [m for m in metrics if m.timestamp <= end_time]
|
|
316
|
+
|
|
317
|
+
# Filter by tags
|
|
318
|
+
if tags:
|
|
319
|
+
metrics = [m for m in metrics if all(m.tags.get(k) == v for k, v in tags.items())]
|
|
320
|
+
|
|
321
|
+
# Sort by timestamp (newest first)
|
|
322
|
+
metrics.sort(key=lambda m: m.timestamp, reverse=True)
|
|
323
|
+
|
|
324
|
+
# Apply limit
|
|
325
|
+
if limit:
|
|
326
|
+
metrics = metrics[:limit]
|
|
327
|
+
|
|
328
|
+
return metrics
|
|
329
|
+
|
|
330
|
+
def get_statistics(
|
|
331
|
+
self,
|
|
332
|
+
metric_type: MetricType,
|
|
333
|
+
component: Optional[str] = None,
|
|
334
|
+
minutes: int = 60,
|
|
335
|
+
tenant_id: Optional[str] = None,
|
|
336
|
+
) -> Dict[str, Any]:
|
|
337
|
+
"""Get statistical summary for a metric type"""
|
|
338
|
+
with self._lock:
|
|
339
|
+
key = f"{metric_type.value}:{component}" if component else metric_type.value
|
|
340
|
+
|
|
341
|
+
# Choose the right aggregated metrics
|
|
342
|
+
if tenant_id:
|
|
343
|
+
agg: Dict[str, Any] = {}
|
|
344
|
+
# Calculate tenant-specific stats
|
|
345
|
+
tenant_metrics = self.get_metrics(metric_type, component, tenant_id)
|
|
346
|
+
if tenant_metrics:
|
|
347
|
+
values: List[float] = [float(m.value) for m in tenant_metrics if isinstance(m.value, (int, float))]
|
|
348
|
+
if values:
|
|
349
|
+
agg = {
|
|
350
|
+
"count": len(values),
|
|
351
|
+
"average": statistics.mean(values),
|
|
352
|
+
"median": statistics.median(values),
|
|
353
|
+
"min": min(values),
|
|
354
|
+
"max": max(values),
|
|
355
|
+
"std_dev": (statistics.stdev(values) if len(values) > 1 else 0),
|
|
356
|
+
"p95": self._percentile(values, 95),
|
|
357
|
+
"p99": self._percentile(values, 99),
|
|
358
|
+
}
|
|
359
|
+
else:
|
|
360
|
+
agg = self.aggregated_metrics.get(key, {})
|
|
361
|
+
|
|
362
|
+
if not agg or agg.get("count", 0) == 0:
|
|
363
|
+
return {
|
|
364
|
+
"count": 0,
|
|
365
|
+
"average": None,
|
|
366
|
+
"min": None,
|
|
367
|
+
"max": None,
|
|
368
|
+
"median": None,
|
|
369
|
+
"std_dev": None,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
agg_values_raw = agg.get("values", [])
|
|
373
|
+
values_list: List[float] = agg_values_raw if isinstance(agg_values_raw, list) else []
|
|
374
|
+
if not values_list:
|
|
375
|
+
return {
|
|
376
|
+
"count": agg.get("count", 0),
|
|
377
|
+
"average": agg.get("sum", 0) / max(agg.get("count", 1), 1),
|
|
378
|
+
"min": agg.get("min"),
|
|
379
|
+
"max": agg.get("max"),
|
|
380
|
+
"median": None,
|
|
381
|
+
"std_dev": 0,
|
|
382
|
+
"p95": agg.get("max"),
|
|
383
|
+
"p99": agg.get("max"),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
last_updated_raw = agg.get("last_updated", datetime.now())
|
|
388
|
+
if isinstance(last_updated_raw, datetime):
|
|
389
|
+
last_updated_str = last_updated_raw.isoformat()
|
|
390
|
+
else:
|
|
391
|
+
last_updated_str = datetime.now().isoformat()
|
|
392
|
+
return {
|
|
393
|
+
"count": len(values_list),
|
|
394
|
+
"average": statistics.mean(values_list),
|
|
395
|
+
"median": statistics.median(values_list),
|
|
396
|
+
"min": min(values_list),
|
|
397
|
+
"max": max(values_list),
|
|
398
|
+
"std_dev": (statistics.stdev(values_list) if len(values_list) > 1 else 0),
|
|
399
|
+
"p95": self._percentile(values_list, 95),
|
|
400
|
+
"p99": self._percentile(values_list, 99),
|
|
401
|
+
"last_updated": last_updated_str,
|
|
402
|
+
}
|
|
403
|
+
except (statistics.StatisticsError, IndexError):
|
|
404
|
+
return {
|
|
405
|
+
"count": len(values_list),
|
|
406
|
+
"average": statistics.mean(values_list),
|
|
407
|
+
"median": statistics.median(values_list),
|
|
408
|
+
"min": min(values_list),
|
|
409
|
+
"max": max(values_list),
|
|
410
|
+
"std_dev": 0,
|
|
411
|
+
"p95": max(values_list),
|
|
412
|
+
"p99": max(values_list),
|
|
413
|
+
"last_updated": datetime.now().isoformat(),
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def _percentile(self, values: List[float], percentile: int) -> float:
|
|
417
|
+
"""Calculate percentile value"""
|
|
418
|
+
if not values:
|
|
419
|
+
return 0.0
|
|
420
|
+
sorted_values = sorted(values)
|
|
421
|
+
index = (percentile / 100) * (len(sorted_values) - 1)
|
|
422
|
+
if index.is_integer():
|
|
423
|
+
return sorted_values[int(index)]
|
|
424
|
+
else:
|
|
425
|
+
lower = sorted_values[int(index)]
|
|
426
|
+
upper = sorted_values[int(index) + 1]
|
|
427
|
+
return lower + (upper - lower) * (index - int(index))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class AlertManager:
|
|
431
|
+
"""Advanced alert management with correlation and multi-tenant support"""
|
|
432
|
+
|
|
433
|
+
def __init__(self, metrics_collector: MetricsCollector):
|
|
434
|
+
self.metrics_collector = metrics_collector
|
|
435
|
+
self.alert_rules: List[Dict[str, Any]] = []
|
|
436
|
+
self.active_alerts: Dict[str, Alert] = {}
|
|
437
|
+
self.alert_history: List[Alert] = []
|
|
438
|
+
self.alert_callbacks: List[Callable[[Alert], None]] = []
|
|
439
|
+
self.tenant_alerts: Dict[str, Dict[str, Alert]] = defaultdict(dict)
|
|
440
|
+
self._lock = threading.Lock()
|
|
441
|
+
|
|
442
|
+
def add_alert_rule(
|
|
443
|
+
self,
|
|
444
|
+
name: str,
|
|
445
|
+
metric_type: MetricType,
|
|
446
|
+
threshold: float,
|
|
447
|
+
operator: str = "gt", # gt, lt, eq, ne, gte, lte
|
|
448
|
+
severity: AlertSeverity = AlertSeverity.WARNING,
|
|
449
|
+
window_minutes: int = 5,
|
|
450
|
+
consecutive_violations: int = 1,
|
|
451
|
+
tags: Optional[Dict[str, str]] = None,
|
|
452
|
+
description: Optional[str] = None,
|
|
453
|
+
tenant_id: Optional[str] = None,
|
|
454
|
+
component: Optional[str] = None,
|
|
455
|
+
cooldown_minutes: int = 15,
|
|
456
|
+
) -> None:
|
|
457
|
+
"""Add an alert rule with enhanced configuration"""
|
|
458
|
+
rule = {
|
|
459
|
+
"name": name,
|
|
460
|
+
"metric_type": metric_type,
|
|
461
|
+
"threshold": threshold,
|
|
462
|
+
"operator": operator,
|
|
463
|
+
"severity": severity,
|
|
464
|
+
"window_minutes": window_minutes,
|
|
465
|
+
"consecutive_violations": consecutive_violations,
|
|
466
|
+
"tags": tags or {},
|
|
467
|
+
"description": description or f"Alert when {metric_type.value} {operator} {threshold}",
|
|
468
|
+
"violation_count": 0,
|
|
469
|
+
"last_check": None,
|
|
470
|
+
"last_triggered": None,
|
|
471
|
+
"enabled": True,
|
|
472
|
+
"tenant_id": tenant_id,
|
|
473
|
+
"component": component,
|
|
474
|
+
"cooldown_minutes": cooldown_minutes,
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
with self._lock:
|
|
478
|
+
self.alert_rules.append(rule)
|
|
479
|
+
|
|
480
|
+
def check_alerts(self) -> List[Alert]:
|
|
481
|
+
"""Check all alert rules and generate alerts for violations"""
|
|
482
|
+
triggered_alerts = []
|
|
483
|
+
|
|
484
|
+
with self._lock:
|
|
485
|
+
for rule in self.alert_rules:
|
|
486
|
+
if not rule["enabled"]:
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
# Check cooldown period
|
|
490
|
+
if rule["last_triggered"]:
|
|
491
|
+
time_since_last = datetime.now() - rule["last_triggered"]
|
|
492
|
+
if time_since_last.total_seconds() < rule["cooldown_minutes"] * 60:
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
# Get recent metrics for this rule
|
|
496
|
+
recent_metrics = self.metrics_collector.get_metrics(
|
|
497
|
+
metric_type=rule["metric_type"],
|
|
498
|
+
component=rule.get("component"),
|
|
499
|
+
tenant_id=rule.get("tenant_id"),
|
|
500
|
+
start_time=datetime.now() - timedelta(minutes=rule["window_minutes"]),
|
|
501
|
+
tags=rule.get("tags"),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if not recent_metrics:
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
# Check for violations
|
|
508
|
+
violations = 0
|
|
509
|
+
latest_value = None
|
|
510
|
+
|
|
511
|
+
for metric in recent_metrics:
|
|
512
|
+
if isinstance(metric.value, (int, float)):
|
|
513
|
+
if self._evaluate_condition(metric.value, rule["threshold"], rule["operator"]):
|
|
514
|
+
violations += 1
|
|
515
|
+
latest_value = metric.value
|
|
516
|
+
|
|
517
|
+
# Trigger alert if threshold exceeded
|
|
518
|
+
if violations >= rule["consecutive_violations"]:
|
|
519
|
+
alert_id = f"{rule['name']}_{int(time.time())}"
|
|
520
|
+
|
|
521
|
+
alert = Alert(
|
|
522
|
+
alert_id=alert_id,
|
|
523
|
+
severity=rule["severity"],
|
|
524
|
+
title=f"{rule['name']} Alert Triggered",
|
|
525
|
+
description=rule["description"],
|
|
526
|
+
timestamp=datetime.now(),
|
|
527
|
+
metric_type=rule["metric_type"],
|
|
528
|
+
threshold=rule["threshold"],
|
|
529
|
+
current_value=latest_value or 0,
|
|
530
|
+
source="monitoring_system",
|
|
531
|
+
component=rule.get("component", "unknown"),
|
|
532
|
+
tags=rule.get("tags", {}),
|
|
533
|
+
tenant_id=rule.get("tenant_id"),
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Store alert
|
|
537
|
+
self.active_alerts[alert_id] = alert
|
|
538
|
+
self.alert_history.append(alert)
|
|
539
|
+
|
|
540
|
+
if alert.tenant_id:
|
|
541
|
+
self.tenant_alerts[alert.tenant_id][alert_id] = alert
|
|
542
|
+
|
|
543
|
+
triggered_alerts.append(alert)
|
|
544
|
+
|
|
545
|
+
# Update rule state
|
|
546
|
+
rule["violation_count"] = violations
|
|
547
|
+
rule["last_check"] = datetime.now()
|
|
548
|
+
rule["last_triggered"] = datetime.now()
|
|
549
|
+
|
|
550
|
+
# Trigger callbacks
|
|
551
|
+
for callback in self.alert_callbacks:
|
|
552
|
+
try:
|
|
553
|
+
callback(alert)
|
|
554
|
+
except Exception as e:
|
|
555
|
+
logger.error(f"Error in alert callback: {e}")
|
|
556
|
+
|
|
557
|
+
rule["violation_count"] = violations
|
|
558
|
+
rule["last_check"] = datetime.now()
|
|
559
|
+
|
|
560
|
+
# Check for resolved alerts
|
|
561
|
+
self._check_resolved_alerts()
|
|
562
|
+
|
|
563
|
+
return triggered_alerts
|
|
564
|
+
|
|
565
|
+
def _check_resolved_alerts(self) -> None:
|
|
566
|
+
"""Check if any active alerts have been resolved"""
|
|
567
|
+
for alert_id, alert in list(self.active_alerts.items()):
|
|
568
|
+
# Check if alert condition is no longer met
|
|
569
|
+
rule = next((r for r in self.alert_rules if r["name"] in alert_id), None)
|
|
570
|
+
if rule:
|
|
571
|
+
recent_metrics = self.metrics_collector.get_metrics(
|
|
572
|
+
metric_type=alert.metric_type,
|
|
573
|
+
component=alert.component,
|
|
574
|
+
tenant_id=alert.tenant_id,
|
|
575
|
+
start_time=datetime.now() - timedelta(minutes=1), # Check last minute
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if recent_metrics:
|
|
579
|
+
latest_value = None
|
|
580
|
+
for metric in recent_metrics:
|
|
581
|
+
if isinstance(metric.value, (int, float)):
|
|
582
|
+
if not self._evaluate_condition(metric.value, alert.threshold, rule["operator"]):
|
|
583
|
+
latest_value = metric.value
|
|
584
|
+
break
|
|
585
|
+
|
|
586
|
+
if latest_value is not None:
|
|
587
|
+
# Alert is resolved
|
|
588
|
+
alert.resolved = True
|
|
589
|
+
alert.resolved_at = datetime.now()
|
|
590
|
+
self.alert_history.append(alert)
|
|
591
|
+
del self.active_alerts[alert_id]
|
|
592
|
+
|
|
593
|
+
if alert.tenant_id and alert_id in self.tenant_alerts.get(alert.tenant_id, {}):
|
|
594
|
+
del self.tenant_alerts[alert.tenant_id][alert_id]
|
|
595
|
+
|
|
596
|
+
def _evaluate_condition(self, value: float, threshold: float, operator: str) -> bool:
|
|
597
|
+
"""Evaluate alert condition"""
|
|
598
|
+
if operator == "gt":
|
|
599
|
+
return value > threshold
|
|
600
|
+
elif operator == "lt":
|
|
601
|
+
return value < threshold
|
|
602
|
+
elif operator == "eq":
|
|
603
|
+
return value == threshold
|
|
604
|
+
elif operator == "ne":
|
|
605
|
+
return value != threshold
|
|
606
|
+
elif operator == "gte":
|
|
607
|
+
return value >= threshold
|
|
608
|
+
elif operator == "lte":
|
|
609
|
+
return value <= threshold
|
|
610
|
+
else:
|
|
611
|
+
return False
|
|
612
|
+
|
|
613
|
+
def add_alert_callback(self, callback: Callable[[Alert], None]) -> None:
|
|
614
|
+
"""Add a callback function to be triggered when alerts fire"""
|
|
615
|
+
self.alert_callbacks.append(callback)
|
|
616
|
+
|
|
617
|
+
def acknowledge_alert(self, alert_id: str, tenant_id: Optional[str] = None) -> bool:
|
|
618
|
+
"""Acknowledge an alert"""
|
|
619
|
+
with self._lock:
|
|
620
|
+
if tenant_id:
|
|
621
|
+
alerts = self.tenant_alerts.get(tenant_id, {})
|
|
622
|
+
if alert_id in alerts:
|
|
623
|
+
alerts[alert_id].acknowledged = True
|
|
624
|
+
alerts[alert_id].acknowledged_at = datetime.now()
|
|
625
|
+
return True
|
|
626
|
+
else:
|
|
627
|
+
if alert_id in self.active_alerts:
|
|
628
|
+
self.active_alerts[alert_id].acknowledged = True
|
|
629
|
+
self.active_alerts[alert_id].acknowledged_at = datetime.now()
|
|
630
|
+
return True
|
|
631
|
+
return False
|
|
632
|
+
|
|
633
|
+
def get_active_alerts(
|
|
634
|
+
self,
|
|
635
|
+
severity: Optional[AlertSeverity] = None,
|
|
636
|
+
tenant_id: Optional[str] = None,
|
|
637
|
+
component: Optional[str] = None,
|
|
638
|
+
) -> List[Alert]:
|
|
639
|
+
"""Get currently active alerts with filtering"""
|
|
640
|
+
with self._lock:
|
|
641
|
+
if tenant_id:
|
|
642
|
+
alerts = list(self.tenant_alerts.get(tenant_id, {}).values())
|
|
643
|
+
else:
|
|
644
|
+
alerts = list(self.active_alerts.values())
|
|
645
|
+
|
|
646
|
+
# Apply filters
|
|
647
|
+
if severity:
|
|
648
|
+
alerts = [a for a in alerts if a.severity == severity]
|
|
649
|
+
if component:
|
|
650
|
+
alerts = [a for a in alerts if a.component == component]
|
|
651
|
+
|
|
652
|
+
return sorted(alerts, key=lambda a: (a.severity.value, a.timestamp), reverse=True)
|
|
653
|
+
|
|
654
|
+
def get_alert_history(
|
|
655
|
+
self,
|
|
656
|
+
hours: int = 24,
|
|
657
|
+
tenant_id: Optional[str] = None,
|
|
658
|
+
) -> List[Alert]:
|
|
659
|
+
"""Get alert history"""
|
|
660
|
+
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
661
|
+
|
|
662
|
+
if tenant_id:
|
|
663
|
+
tenant_history = []
|
|
664
|
+
for alert in self.alert_history:
|
|
665
|
+
if alert.tenant_id == tenant_id and alert.timestamp >= cutoff_time:
|
|
666
|
+
tenant_history.append(alert)
|
|
667
|
+
return tenant_history
|
|
668
|
+
else:
|
|
669
|
+
return [a for a in self.alert_history if a.timestamp >= cutoff_time]
|
|
670
|
+
|
|
671
|
+
def get_alert_statistics(
|
|
672
|
+
self,
|
|
673
|
+
hours: int = 24,
|
|
674
|
+
tenant_id: Optional[str] = None,
|
|
675
|
+
) -> Dict[str, Any]:
|
|
676
|
+
"""Get alert statistics"""
|
|
677
|
+
alerts = self.get_alert_history(hours, tenant_id)
|
|
678
|
+
|
|
679
|
+
# Count by severity
|
|
680
|
+
by_severity: Dict[str, int] = defaultdict(int)
|
|
681
|
+
by_component: Dict[str, int] = defaultdict(int)
|
|
682
|
+
by_hour: Dict[str, int] = defaultdict(int)
|
|
683
|
+
|
|
684
|
+
for alert in alerts:
|
|
685
|
+
by_severity[str(alert.severity.value)] += 1
|
|
686
|
+
by_component[alert.component] += 1
|
|
687
|
+
hour_key = alert.timestamp.strftime("%Y-%m-%d %H:00")
|
|
688
|
+
by_hour[hour_key] += 1
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
"total_alerts": len(alerts),
|
|
692
|
+
"by_severity": dict(by_severity),
|
|
693
|
+
"by_component": dict(by_component),
|
|
694
|
+
"by_hour": dict(by_hour),
|
|
695
|
+
"resolved_count": sum(1 for a in alerts if a.resolved),
|
|
696
|
+
"acknowledged_count": sum(1 for a in alerts if a.acknowledged),
|
|
697
|
+
"resolution_rate": sum(1 for a in alerts if a.resolved) / max(len(alerts), 1),
|
|
698
|
+
"period_hours": hours,
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class DashboardManager:
|
|
703
|
+
"""Dashboard management with multi-tenant support"""
|
|
704
|
+
|
|
705
|
+
def __init__(self):
|
|
706
|
+
self.dashboards: Dict[str, Dashboard] = {}
|
|
707
|
+
self.tenant_dashboards: Dict[str, Dict[str, Dashboard]] = defaultdict(dict)
|
|
708
|
+
self.default_dashboards: Dict[str, Dashboard] = {}
|
|
709
|
+
self._lock = threading.Lock()
|
|
710
|
+
|
|
711
|
+
# Create default dashboards
|
|
712
|
+
self._create_default_dashboards()
|
|
713
|
+
|
|
714
|
+
def create_dashboard(
|
|
715
|
+
self,
|
|
716
|
+
name: str,
|
|
717
|
+
description: str,
|
|
718
|
+
dashboard_type: DashboardType,
|
|
719
|
+
widgets: List[DashboardWidget],
|
|
720
|
+
owner: str = "",
|
|
721
|
+
tenant_id: Optional[str] = None,
|
|
722
|
+
is_public: bool = False,
|
|
723
|
+
layout: Optional[Dict[str, Any]] = None,
|
|
724
|
+
) -> str:
|
|
725
|
+
"""Create a new dashboard"""
|
|
726
|
+
dashboard_id = str(uuid.uuid4())
|
|
727
|
+
|
|
728
|
+
dashboard = Dashboard(
|
|
729
|
+
dashboard_id=dashboard_id,
|
|
730
|
+
name=name,
|
|
731
|
+
description=description,
|
|
732
|
+
dashboard_type=dashboard_type,
|
|
733
|
+
widgets=widgets,
|
|
734
|
+
layout=layout or {"grid": {"cols": 12, "rows": 8}},
|
|
735
|
+
owner=owner,
|
|
736
|
+
tenant_id=tenant_id,
|
|
737
|
+
is_public=is_public,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
with self._lock:
|
|
741
|
+
if tenant_id:
|
|
742
|
+
self.tenant_dashboards[tenant_id][dashboard_id] = dashboard
|
|
743
|
+
else:
|
|
744
|
+
self.dashboards[dashboard_id] = dashboard
|
|
745
|
+
|
|
746
|
+
logger.info(f"Created dashboard: {name} ({dashboard_id})")
|
|
747
|
+
return dashboard_id
|
|
748
|
+
|
|
749
|
+
def get_dashboard(self, dashboard_id: str, tenant_id: Optional[str] = None) -> Optional[Dashboard]:
|
|
750
|
+
"""Get a dashboard by ID"""
|
|
751
|
+
with self._lock:
|
|
752
|
+
if tenant_id:
|
|
753
|
+
return self.tenant_dashboards.get(tenant_id, {}).get(dashboard_id)
|
|
754
|
+
else:
|
|
755
|
+
return self.dashboards.get(dashboard_id) or self.default_dashboards.get(dashboard_id)
|
|
756
|
+
|
|
757
|
+
def list_dashboards(
|
|
758
|
+
self,
|
|
759
|
+
tenant_id: Optional[str] = None,
|
|
760
|
+
dashboard_type: Optional[DashboardType] = None,
|
|
761
|
+
owner: Optional[str] = None,
|
|
762
|
+
is_public: Optional[bool] = None,
|
|
763
|
+
) -> List[Dashboard]:
|
|
764
|
+
"""List dashboards with filtering"""
|
|
765
|
+
with self._lock:
|
|
766
|
+
dashboards: List[Dashboard] = []
|
|
767
|
+
|
|
768
|
+
# Collect dashboards
|
|
769
|
+
if tenant_id:
|
|
770
|
+
dashboards.extend(self.tenant_dashboards.get(tenant_id, {}).values())
|
|
771
|
+
else:
|
|
772
|
+
dashboards.extend(self.dashboards.values())
|
|
773
|
+
|
|
774
|
+
# Add public dashboards
|
|
775
|
+
dashboards.extend([d for d in self.dashboards.values() if d.is_public])
|
|
776
|
+
|
|
777
|
+
# Apply filters
|
|
778
|
+
if dashboard_type:
|
|
779
|
+
dashboards = [d for d in dashboards if d.dashboard_type == dashboard_type]
|
|
780
|
+
if owner:
|
|
781
|
+
dashboards = [d for d in dashboards if d.owner == owner]
|
|
782
|
+
if is_public is not None:
|
|
783
|
+
dashboards = [d for d in dashboards if d.is_public == is_public]
|
|
784
|
+
|
|
785
|
+
return dashboards
|
|
786
|
+
|
|
787
|
+
def update_dashboard(
|
|
788
|
+
self,
|
|
789
|
+
dashboard_id: str,
|
|
790
|
+
updates: Dict[str, Any],
|
|
791
|
+
tenant_id: Optional[str] = None,
|
|
792
|
+
) -> bool:
|
|
793
|
+
"""Update a dashboard"""
|
|
794
|
+
with self._lock:
|
|
795
|
+
if tenant_id:
|
|
796
|
+
dashboard = self.tenant_dashboards.get(tenant_id, {}).get(dashboard_id)
|
|
797
|
+
else:
|
|
798
|
+
dashboard = self.dashboards.get(dashboard_id)
|
|
799
|
+
|
|
800
|
+
if not dashboard:
|
|
801
|
+
return False
|
|
802
|
+
|
|
803
|
+
# Update fields
|
|
804
|
+
for key, value in updates.items():
|
|
805
|
+
if hasattr(dashboard, key):
|
|
806
|
+
setattr(dashboard, key, value)
|
|
807
|
+
elif key in dashboard.__dict__:
|
|
808
|
+
dashboard.__dict__[key] = value
|
|
809
|
+
|
|
810
|
+
dashboard.updated_at = datetime.now()
|
|
811
|
+
return True
|
|
812
|
+
|
|
813
|
+
def delete_dashboard(self, dashboard_id: str, tenant_id: Optional[str] = None) -> bool:
|
|
814
|
+
"""Delete a dashboard"""
|
|
815
|
+
with self._lock:
|
|
816
|
+
if tenant_id:
|
|
817
|
+
if dashboard_id in self.tenant_dashboards.get(tenant_id, {}):
|
|
818
|
+
del self.tenant_dashboards[tenant_id][dashboard_id]
|
|
819
|
+
return True
|
|
820
|
+
else:
|
|
821
|
+
if dashboard_id in self.dashboards:
|
|
822
|
+
del self.dashboards[dashboard_id]
|
|
823
|
+
return True
|
|
824
|
+
if dashboard_id in self.default_dashboards:
|
|
825
|
+
# Don't delete default dashboards
|
|
826
|
+
return False
|
|
827
|
+
return False
|
|
828
|
+
|
|
829
|
+
def _create_default_dashboards(self):
|
|
830
|
+
"""Create default dashboards"""
|
|
831
|
+
# System Overview Dashboard
|
|
832
|
+
system_overview_widgets = [
|
|
833
|
+
DashboardWidget(
|
|
834
|
+
widget_id="sys_health",
|
|
835
|
+
widget_type="metric",
|
|
836
|
+
title="System Health",
|
|
837
|
+
position={"x": 0, "y": 0, "width": 4, "height": 2},
|
|
838
|
+
config={"metric": "health_score", "format": "percentage"},
|
|
839
|
+
),
|
|
840
|
+
DashboardWidget(
|
|
841
|
+
widget_id="active_alerts",
|
|
842
|
+
widget_type="metric",
|
|
843
|
+
title="Active Alerts",
|
|
844
|
+
position={"x": 4, "y": 0, "width": 4, "height": 2},
|
|
845
|
+
config={"metric": "active_alerts", "format": "number"},
|
|
846
|
+
),
|
|
847
|
+
DashboardWidget(
|
|
848
|
+
widget_id="uptime",
|
|
849
|
+
widget_type="metric",
|
|
850
|
+
title="System Uptime",
|
|
851
|
+
position={"x": 8, "y": 0, "width": 4, "height": 2},
|
|
852
|
+
config={"metric": "uptime", "format": "duration"},
|
|
853
|
+
),
|
|
854
|
+
DashboardWidget(
|
|
855
|
+
widget_id="cpu_chart",
|
|
856
|
+
widget_type="chart",
|
|
857
|
+
title="CPU Usage",
|
|
858
|
+
position={"x": 0, "y": 2, "width": 6, "height": 3},
|
|
859
|
+
config={"chart_type": "line", "time_range": "1h"},
|
|
860
|
+
metrics=["cpu_usage"],
|
|
861
|
+
),
|
|
862
|
+
DashboardWidget(
|
|
863
|
+
widget_id="memory_chart",
|
|
864
|
+
widget_type="chart",
|
|
865
|
+
title="Memory Usage",
|
|
866
|
+
position={"x": 6, "y": 2, "width": 6, "height": 3},
|
|
867
|
+
config={"chart_type": "line", "time_range": "1h"},
|
|
868
|
+
metrics=["memory_usage"],
|
|
869
|
+
),
|
|
870
|
+
DashboardWidget(
|
|
871
|
+
widget_id="alert_table",
|
|
872
|
+
widget_type="table",
|
|
873
|
+
title="Recent Alerts",
|
|
874
|
+
position={"x": 0, "y": 5, "width": 12, "height": 3},
|
|
875
|
+
config={"limit": 10, "sort": "timestamp"},
|
|
876
|
+
),
|
|
877
|
+
]
|
|
878
|
+
|
|
879
|
+
system_dashboard = Dashboard(
|
|
880
|
+
dashboard_id="system_overview",
|
|
881
|
+
name="System Overview",
|
|
882
|
+
description="Overview of system health and performance",
|
|
883
|
+
dashboard_type=DashboardType.SYSTEM_OVERVIEW,
|
|
884
|
+
widgets=system_overview_widgets,
|
|
885
|
+
owner="system",
|
|
886
|
+
is_public=True,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
self.default_dashboards["system_overview"] = system_dashboard
|
|
890
|
+
|
|
891
|
+
# Hook Analysis Dashboard
|
|
892
|
+
hook_analysis_widgets = [
|
|
893
|
+
DashboardWidget(
|
|
894
|
+
widget_id="hook_execution_chart",
|
|
895
|
+
widget_type="chart",
|
|
896
|
+
title="Hook Execution Rate",
|
|
897
|
+
position={"x": 0, "y": 0, "width": 6, "height": 3},
|
|
898
|
+
config={"chart_type": "line", "time_range": "24h"},
|
|
899
|
+
metrics=["hook_execution_rate"],
|
|
900
|
+
),
|
|
901
|
+
DashboardWidget(
|
|
902
|
+
widget_id="hook_success_rate",
|
|
903
|
+
widget_type="metric",
|
|
904
|
+
title="Hook Success Rate",
|
|
905
|
+
position={"x": 6, "y": 0, "width": 6, "height": 3},
|
|
906
|
+
config={"metric": "hook_success_rate", "format": "percentage"},
|
|
907
|
+
),
|
|
908
|
+
DashboardWidget(
|
|
909
|
+
widget_id="slow_hooks",
|
|
910
|
+
widget_type="table",
|
|
911
|
+
title="Slowest Hooks",
|
|
912
|
+
position={"x": 0, "y": 3, "width": 12, "height": 4},
|
|
913
|
+
config={"limit": 15, "sort": "execution_time", "order": "desc"},
|
|
914
|
+
),
|
|
915
|
+
DashboardWidget(
|
|
916
|
+
widget_id="error_by_hook",
|
|
917
|
+
widget_type="chart",
|
|
918
|
+
title="Error Rate by Hook",
|
|
919
|
+
position={"x": 0, "y": 7, "width": 12, "height": 3},
|
|
920
|
+
config={"chart_type": "bar", "time_range": "24h"},
|
|
921
|
+
),
|
|
922
|
+
]
|
|
923
|
+
|
|
924
|
+
hook_dashboard = Dashboard(
|
|
925
|
+
dashboard_id="hook_analysis",
|
|
926
|
+
name="Hook Analysis",
|
|
927
|
+
description="Detailed analysis of hook execution performance",
|
|
928
|
+
dashboard_type=DashboardType.HOOK_ANALYSIS,
|
|
929
|
+
widgets=hook_analysis_widgets,
|
|
930
|
+
owner="system",
|
|
931
|
+
is_public=True,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
self.default_dashboards["hook_analysis"] = hook_dashboard
|
|
935
|
+
|
|
936
|
+
logger.info("Created default dashboards")
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
class RealtimeMonitoringDashboard:
|
|
940
|
+
"""
|
|
941
|
+
Real-time Monitoring Dashboard System
|
|
942
|
+
|
|
943
|
+
Enterprise-grade real-time monitoring dashboard with alerting,
|
|
944
|
+
analytics, and visualization capabilities.
|
|
945
|
+
"""
|
|
946
|
+
|
|
947
|
+
def __init__(
|
|
948
|
+
self,
|
|
949
|
+
metrics_buffer_size: int = 100000,
|
|
950
|
+
retention_hours: int = 168, # 7 days
|
|
951
|
+
alert_check_interval: int = 30, # seconds
|
|
952
|
+
enable_websocket: bool = True,
|
|
953
|
+
enable_external_integration: bool = True,
|
|
954
|
+
):
|
|
955
|
+
"""Initialize Real-time Monitoring Dashboard
|
|
956
|
+
|
|
957
|
+
Args:
|
|
958
|
+
metrics_buffer_size: Size of metrics buffer
|
|
959
|
+
retention_hours: Hours to retain metrics
|
|
960
|
+
alert_check_interval: Interval between alert checks
|
|
961
|
+
enable_websocket: Enable WebSocket support for real-time updates
|
|
962
|
+
enable_external_integration: Enable external monitoring system integration
|
|
963
|
+
"""
|
|
964
|
+
self.metrics_buffer_size = metrics_buffer_size
|
|
965
|
+
self.retention_hours = retention_hours
|
|
966
|
+
self.alert_check_interval = alert_check_interval
|
|
967
|
+
self.enable_websocket = enable_websocket
|
|
968
|
+
self.enable_external_integration = enable_external_integration
|
|
969
|
+
|
|
970
|
+
# Initialize components
|
|
971
|
+
self.metrics_collector = MetricsCollector(metrics_buffer_size, retention_hours)
|
|
972
|
+
self.alert_manager = AlertManager(self.metrics_collector)
|
|
973
|
+
self.dashboard_manager = DashboardManager()
|
|
974
|
+
|
|
975
|
+
# System state
|
|
976
|
+
self._running = False
|
|
977
|
+
self._startup_time = datetime.now()
|
|
978
|
+
|
|
979
|
+
# WebSocket connections
|
|
980
|
+
self.websocket_connections: Set[Any] = set()
|
|
981
|
+
self.websocket_lock = threading.Lock()
|
|
982
|
+
|
|
983
|
+
# Background tasks
|
|
984
|
+
self._monitoring_thread: Optional[threading.Thread] = None
|
|
985
|
+
self._alert_thread: Optional[threading.Thread] = None
|
|
986
|
+
|
|
987
|
+
# External integrations
|
|
988
|
+
self.external_integrations: Dict[str, Any] = {}
|
|
989
|
+
|
|
990
|
+
# Setup default alerts
|
|
991
|
+
self._setup_default_alerts()
|
|
992
|
+
|
|
993
|
+
logger.info("Real-time Monitoring Dashboard initialized")
|
|
994
|
+
|
|
995
|
+
def _setup_default_alerts(self):
|
|
996
|
+
"""Setup default alert rules"""
|
|
997
|
+
# CPU usage alert
|
|
998
|
+
self.alert_manager.add_alert_rule(
|
|
999
|
+
name="High CPU Usage",
|
|
1000
|
+
metric_type=MetricType.CPU_USAGE,
|
|
1001
|
+
threshold=80.0,
|
|
1002
|
+
operator="gt",
|
|
1003
|
+
severity=AlertSeverity.ERROR,
|
|
1004
|
+
window_minutes=5,
|
|
1005
|
+
consecutive_violations=2,
|
|
1006
|
+
tags={"component": "system"},
|
|
1007
|
+
description="CPU usage exceeds 80% for 5 minutes",
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
# Memory usage alert
|
|
1011
|
+
self.alert_manager.add_alert_rule(
|
|
1012
|
+
name="High Memory Usage",
|
|
1013
|
+
metric_type=MetricType.MEMORY_USAGE,
|
|
1014
|
+
threshold=85.0,
|
|
1015
|
+
operator="gt",
|
|
1016
|
+
severity=AlertSeverity.ERROR,
|
|
1017
|
+
window_minutes=5,
|
|
1018
|
+
consecutive_violations=2,
|
|
1019
|
+
tags={"component": "system"},
|
|
1020
|
+
description="Memory usage exceeds 85% for 5 minutes",
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
# Error rate alert
|
|
1024
|
+
self.alert_manager.add_alert_rule(
|
|
1025
|
+
name="High Error Rate",
|
|
1026
|
+
metric_type=MetricType.ERROR_RATE,
|
|
1027
|
+
threshold=5.0,
|
|
1028
|
+
operator="gt",
|
|
1029
|
+
severity=AlertSeverity.CRITICAL,
|
|
1030
|
+
window_minutes=2,
|
|
1031
|
+
consecutive_violations=1,
|
|
1032
|
+
tags={"component": "system"},
|
|
1033
|
+
description="Error rate exceeds 5% in 2 minutes",
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
# Hook execution time alert
|
|
1037
|
+
self.alert_manager.add_alert_rule(
|
|
1038
|
+
name="Slow Hook Execution",
|
|
1039
|
+
metric_type=MetricType.RESPONSE_TIME,
|
|
1040
|
+
threshold=5000.0, # 5 seconds
|
|
1041
|
+
operator="gt",
|
|
1042
|
+
severity=AlertSeverity.WARNING,
|
|
1043
|
+
window_minutes=10,
|
|
1044
|
+
consecutive_violations=3,
|
|
1045
|
+
tags={"component": "hooks"},
|
|
1046
|
+
description="Hook execution time exceeds 5 seconds",
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
logger.info("Default alert rules configured")
|
|
1050
|
+
|
|
1051
|
+
async def start(self) -> None:
|
|
1052
|
+
"""Start the monitoring dashboard system"""
|
|
1053
|
+
if self._running:
|
|
1054
|
+
return
|
|
1055
|
+
|
|
1056
|
+
logger.info("Starting Real-time Monitoring Dashboard...")
|
|
1057
|
+
|
|
1058
|
+
try:
|
|
1059
|
+
# Start background monitoring
|
|
1060
|
+
self._start_background_monitoring()
|
|
1061
|
+
|
|
1062
|
+
# Start alert checking
|
|
1063
|
+
self._start_alert_monitoring()
|
|
1064
|
+
|
|
1065
|
+
# Start WebSocket server if enabled
|
|
1066
|
+
if self.enable_websocket:
|
|
1067
|
+
await self._start_websocket_server()
|
|
1068
|
+
|
|
1069
|
+
# Initialize external integrations
|
|
1070
|
+
if self.enable_external_integration:
|
|
1071
|
+
await self._initialize_external_integrations()
|
|
1072
|
+
|
|
1073
|
+
self._running = True
|
|
1074
|
+
logger.info("Real-time Monitoring Dashboard started successfully")
|
|
1075
|
+
|
|
1076
|
+
except Exception as e:
|
|
1077
|
+
logger.error(f"Error starting monitoring dashboard: {e}")
|
|
1078
|
+
raise
|
|
1079
|
+
|
|
1080
|
+
def stop(self) -> None:
|
|
1081
|
+
"""Stop the monitoring dashboard system"""
|
|
1082
|
+
if not self._running:
|
|
1083
|
+
return
|
|
1084
|
+
|
|
1085
|
+
logger.info("Stopping Real-time Monitoring Dashboard...")
|
|
1086
|
+
|
|
1087
|
+
try:
|
|
1088
|
+
# Stop background threads
|
|
1089
|
+
self._running = False
|
|
1090
|
+
if self._monitoring_thread:
|
|
1091
|
+
self._monitoring_thread.join(timeout=5)
|
|
1092
|
+
if self._alert_thread:
|
|
1093
|
+
self._alert_thread.join(timeout=5)
|
|
1094
|
+
|
|
1095
|
+
# Close WebSocket connections
|
|
1096
|
+
with self.websocket_lock:
|
|
1097
|
+
for conn in self.websocket_connections:
|
|
1098
|
+
try:
|
|
1099
|
+
# Close connection
|
|
1100
|
+
pass # Implementation depends on WebSocket library
|
|
1101
|
+
except Exception:
|
|
1102
|
+
pass
|
|
1103
|
+
self.websocket_connections.clear()
|
|
1104
|
+
|
|
1105
|
+
logger.info("Real-time Monitoring Dashboard stopped")
|
|
1106
|
+
|
|
1107
|
+
except Exception as e:
|
|
1108
|
+
logger.error(f"Error stopping monitoring dashboard: {e}")
|
|
1109
|
+
|
|
1110
|
+
def _start_background_monitoring(self) -> None:
|
|
1111
|
+
"""Start background metrics collection"""
|
|
1112
|
+
|
|
1113
|
+
def monitor_loop():
|
|
1114
|
+
while self._running:
|
|
1115
|
+
try:
|
|
1116
|
+
self._collect_system_metrics()
|
|
1117
|
+
time.sleep(30) # Collect metrics every 30 seconds
|
|
1118
|
+
except Exception as e:
|
|
1119
|
+
logger.error(f"Error in monitoring loop: {e}")
|
|
1120
|
+
time.sleep(30)
|
|
1121
|
+
|
|
1122
|
+
self._monitoring_thread = threading.Thread(target=monitor_loop, daemon=True)
|
|
1123
|
+
self._monitoring_thread.start()
|
|
1124
|
+
|
|
1125
|
+
def _start_alert_monitoring(self) -> None:
|
|
1126
|
+
"""Start alert checking"""
|
|
1127
|
+
|
|
1128
|
+
def alert_loop():
|
|
1129
|
+
while self._running:
|
|
1130
|
+
try:
|
|
1131
|
+
alerts = self.alert_manager.check_alerts()
|
|
1132
|
+
if alerts:
|
|
1133
|
+
for alert in alerts:
|
|
1134
|
+
logger.warning(f"Alert triggered: {alert.title} - {alert.current_value}")
|
|
1135
|
+
# Broadcast to WebSocket clients
|
|
1136
|
+
self._broadcast_alert(alert)
|
|
1137
|
+
|
|
1138
|
+
time.sleep(self.alert_check_interval)
|
|
1139
|
+
except Exception as e:
|
|
1140
|
+
logger.error(f"Error in alert loop: {e}")
|
|
1141
|
+
time.sleep(self.alert_check_interval)
|
|
1142
|
+
|
|
1143
|
+
self._alert_thread = threading.Thread(target=alert_loop, daemon=True)
|
|
1144
|
+
self._alert_thread.start()
|
|
1145
|
+
|
|
1146
|
+
async def _start_websocket_server(self) -> None:
|
|
1147
|
+
"""Start WebSocket server for real-time updates"""
|
|
1148
|
+
# Implementation depends on WebSocket library (websockets, FastAPI WebSocket, etc.)
|
|
1149
|
+
logger.info("WebSocket server support enabled")
|
|
1150
|
+
pass
|
|
1151
|
+
|
|
1152
|
+
async def _initialize_external_integrations(self) -> None:
|
|
1153
|
+
"""Initialize external monitoring system integrations"""
|
|
1154
|
+
# Prometheus integration
|
|
1155
|
+
try:
|
|
1156
|
+
await self._setup_prometheus_integration()
|
|
1157
|
+
except Exception as e:
|
|
1158
|
+
logger.warning(f"Failed to setup Prometheus integration: {e}")
|
|
1159
|
+
|
|
1160
|
+
# DataDog integration
|
|
1161
|
+
try:
|
|
1162
|
+
await self._setup_datadog_integration()
|
|
1163
|
+
except Exception as e:
|
|
1164
|
+
logger.warning(f"Failed to setup DataDog integration: {e}")
|
|
1165
|
+
|
|
1166
|
+
logger.info("External integrations initialized")
|
|
1167
|
+
|
|
1168
|
+
async def _setup_prometheus_integration(self) -> None:
|
|
1169
|
+
"""Setup Prometheus integration"""
|
|
1170
|
+
# Implementation would setup Prometheus metrics endpoint
|
|
1171
|
+
pass
|
|
1172
|
+
|
|
1173
|
+
async def _setup_datadog_integration(self) -> None:
|
|
1174
|
+
"""Setup DataDog integration"""
|
|
1175
|
+
# Implementation would setup DataDog API client
|
|
1176
|
+
pass
|
|
1177
|
+
|
|
1178
|
+
def _collect_system_metrics(self) -> None:
|
|
1179
|
+
"""Collect system performance metrics"""
|
|
1180
|
+
try:
|
|
1181
|
+
import psutil
|
|
1182
|
+
|
|
1183
|
+
# CPU Usage
|
|
1184
|
+
cpu_percent = psutil.cpu_percent(interval=1)
|
|
1185
|
+
self.add_metric(
|
|
1186
|
+
MetricType.CPU_USAGE,
|
|
1187
|
+
cpu_percent,
|
|
1188
|
+
tags={"component": "system"},
|
|
1189
|
+
source="psutil",
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
# Memory Usage
|
|
1193
|
+
memory = psutil.virtual_memory()
|
|
1194
|
+
self.add_metric(
|
|
1195
|
+
MetricType.MEMORY_USAGE,
|
|
1196
|
+
memory.percent,
|
|
1197
|
+
tags={"component": "system"},
|
|
1198
|
+
source="psutil",
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
# Python process memory
|
|
1202
|
+
process = psutil.Process()
|
|
1203
|
+
process_memory = process.memory_info()
|
|
1204
|
+
self.add_metric(
|
|
1205
|
+
MetricType.MEMORY_USAGE,
|
|
1206
|
+
process_memory.rss / (1024**2), # MB
|
|
1207
|
+
tags={"component": "python_process"},
|
|
1208
|
+
source="psutil",
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
# System load
|
|
1212
|
+
try:
|
|
1213
|
+
load_avg = psutil.getloadavg()
|
|
1214
|
+
self.add_metric(
|
|
1215
|
+
MetricType.SYSTEM_PERFORMANCE,
|
|
1216
|
+
load_avg[0], # 1-minute load average
|
|
1217
|
+
tags={"component": "system", "metric": "load_1min"},
|
|
1218
|
+
source="psutil",
|
|
1219
|
+
)
|
|
1220
|
+
except (AttributeError, OSError):
|
|
1221
|
+
pass # Not available on all systems
|
|
1222
|
+
|
|
1223
|
+
except Exception as e:
|
|
1224
|
+
logger.error(f"Error collecting system metrics: {e}")
|
|
1225
|
+
|
|
1226
|
+
def add_metric(
|
|
1227
|
+
self,
|
|
1228
|
+
metric_type: MetricType,
|
|
1229
|
+
value: Union[int, float],
|
|
1230
|
+
tags: Optional[Dict[str, str]] = None,
|
|
1231
|
+
source: str = "custom",
|
|
1232
|
+
component: str = "",
|
|
1233
|
+
tenant_id: Optional[str] = None,
|
|
1234
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1235
|
+
) -> None:
|
|
1236
|
+
"""Add a metric to the monitoring system"""
|
|
1237
|
+
metric = MetricData(
|
|
1238
|
+
timestamp=datetime.now(),
|
|
1239
|
+
metric_type=metric_type,
|
|
1240
|
+
value=value,
|
|
1241
|
+
tags=tags or {},
|
|
1242
|
+
source=source,
|
|
1243
|
+
component=component,
|
|
1244
|
+
tenant_id=tenant_id,
|
|
1245
|
+
metadata=metadata or {},
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
self.metrics_collector.add_metric(metric)
|
|
1249
|
+
|
|
1250
|
+
def get_dashboard_data(
|
|
1251
|
+
self,
|
|
1252
|
+
dashboard_id: str,
|
|
1253
|
+
tenant_id: Optional[str] = None,
|
|
1254
|
+
time_range: Optional[str] = None, # "1h", "24h", "7d"
|
|
1255
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1256
|
+
) -> Dict[str, Any]:
|
|
1257
|
+
"""Get dashboard data for visualization"""
|
|
1258
|
+
try:
|
|
1259
|
+
# Get dashboard
|
|
1260
|
+
dashboard = self.dashboard_manager.get_dashboard(dashboard_id, tenant_id)
|
|
1261
|
+
if not dashboard:
|
|
1262
|
+
return {"error": "Dashboard not found"}
|
|
1263
|
+
|
|
1264
|
+
# Calculate time range
|
|
1265
|
+
end_time = datetime.now()
|
|
1266
|
+
if time_range:
|
|
1267
|
+
if time_range == "1h":
|
|
1268
|
+
start_time = end_time - timedelta(hours=1)
|
|
1269
|
+
elif time_range == "24h":
|
|
1270
|
+
start_time = end_time - timedelta(hours=24)
|
|
1271
|
+
elif time_range == "7d":
|
|
1272
|
+
start_time = end_time - timedelta(days=7)
|
|
1273
|
+
else:
|
|
1274
|
+
start_time = end_time - timedelta(hours=1) # Default
|
|
1275
|
+
else:
|
|
1276
|
+
start_time = end_time - timedelta(hours=1)
|
|
1277
|
+
|
|
1278
|
+
# Collect data for each widget
|
|
1279
|
+
widgets_data = {}
|
|
1280
|
+
for widget in dashboard.widgets:
|
|
1281
|
+
try:
|
|
1282
|
+
widget_data = self._get_widget_data(widget, start_time, end_time, tenant_id, filters)
|
|
1283
|
+
widgets_data[widget.widget_id] = widget_data
|
|
1284
|
+
except Exception as e:
|
|
1285
|
+
logger.error(f"Error getting data for widget {widget.widget_id}: {e}")
|
|
1286
|
+
widgets_data[widget.widget_id] = {"error": str(e)}
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
"dashboard": dashboard.to_dict(),
|
|
1290
|
+
"widgets_data": widgets_data,
|
|
1291
|
+
"time_range": time_range or "1h",
|
|
1292
|
+
"generated_at": datetime.now().isoformat(),
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
except Exception as e:
|
|
1296
|
+
logger.error(f"Error getting dashboard data: {e}")
|
|
1297
|
+
return {"error": str(e), "generated_at": datetime.now().isoformat()}
|
|
1298
|
+
|
|
1299
|
+
def _get_widget_data(
|
|
1300
|
+
self,
|
|
1301
|
+
widget: DashboardWidget,
|
|
1302
|
+
start_time: datetime,
|
|
1303
|
+
end_time: datetime,
|
|
1304
|
+
tenant_id: Optional[str] = None,
|
|
1305
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1306
|
+
) -> Dict[str, Any]:
|
|
1307
|
+
"""Get data for a specific widget"""
|
|
1308
|
+
if widget.widget_type == "metric":
|
|
1309
|
+
return self._get_metric_widget_data(widget, tenant_id)
|
|
1310
|
+
elif widget.widget_type == "chart":
|
|
1311
|
+
return self._get_chart_widget_data(widget, start_time, end_time, tenant_id, filters)
|
|
1312
|
+
elif widget.widget_type == "table":
|
|
1313
|
+
return self._get_table_widget_data(widget, start_time, end_time, tenant_id, filters)
|
|
1314
|
+
elif widget.widget_type == "alert":
|
|
1315
|
+
return self._get_alert_widget_data(widget, tenant_id)
|
|
1316
|
+
else:
|
|
1317
|
+
return {"error": f"Unsupported widget type: {widget.widget_type}"}
|
|
1318
|
+
|
|
1319
|
+
def _get_metric_widget_data(self, widget: DashboardWidget, tenant_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1320
|
+
"""Get data for metric widget"""
|
|
1321
|
+
metric_name = widget.config.get("metric")
|
|
1322
|
+
if not metric_name:
|
|
1323
|
+
return {"error": "No metric specified"}
|
|
1324
|
+
|
|
1325
|
+
# Get latest metrics
|
|
1326
|
+
metric_type = None
|
|
1327
|
+
if metric_name == "cpu_usage":
|
|
1328
|
+
metric_type = MetricType.CPU_USAGE
|
|
1329
|
+
elif metric_name == "memory_usage":
|
|
1330
|
+
metric_type = MetricType.MEMORY_USAGE
|
|
1331
|
+
elif metric_name == "health_score":
|
|
1332
|
+
metric_type = MetricType.AVAILABILITY
|
|
1333
|
+
else:
|
|
1334
|
+
return {"error": f"Unknown metric: {metric_name}"}
|
|
1335
|
+
|
|
1336
|
+
recent_metrics = self.metrics_collector.get_metrics(metric_type=metric_type, tenant_id=tenant_id, limit=1)
|
|
1337
|
+
|
|
1338
|
+
if not recent_metrics:
|
|
1339
|
+
return {"value": 0, "status": "no_data"}
|
|
1340
|
+
|
|
1341
|
+
latest_metric = recent_metrics[0]
|
|
1342
|
+
format_type = widget.config.get("format", "number")
|
|
1343
|
+
|
|
1344
|
+
if format_type == "percentage":
|
|
1345
|
+
value = f"{latest_metric.value:.1f}%"
|
|
1346
|
+
elif format_type == "duration":
|
|
1347
|
+
value = f"{latest_metric.value:.0f}s"
|
|
1348
|
+
else:
|
|
1349
|
+
value = str(latest_metric.value)
|
|
1350
|
+
|
|
1351
|
+
return {
|
|
1352
|
+
"value": latest_metric.value,
|
|
1353
|
+
"formatted_value": value,
|
|
1354
|
+
"timestamp": latest_metric.timestamp.isoformat(),
|
|
1355
|
+
"format": format_type,
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
def _get_chart_widget_data(
|
|
1359
|
+
self,
|
|
1360
|
+
widget: DashboardWidget,
|
|
1361
|
+
start_time: datetime,
|
|
1362
|
+
end_time: datetime,
|
|
1363
|
+
tenant_id: Optional[str] = None,
|
|
1364
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1365
|
+
) -> Dict[str, Any]:
|
|
1366
|
+
"""Get data for chart widget"""
|
|
1367
|
+
chart_type = widget.config.get("chart_type", "line")
|
|
1368
|
+
metrics = widget.metrics or []
|
|
1369
|
+
|
|
1370
|
+
chart_data = {
|
|
1371
|
+
"type": chart_type,
|
|
1372
|
+
"data": [],
|
|
1373
|
+
"labels": [],
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
for metric_name in metrics:
|
|
1377
|
+
# Map metric names to MetricType
|
|
1378
|
+
metric_type = self._map_metric_name(metric_name)
|
|
1379
|
+
if metric_type:
|
|
1380
|
+
metric_data = self.metrics_collector.get_metrics(
|
|
1381
|
+
metric_type=metric_type,
|
|
1382
|
+
tenant_id=tenant_id,
|
|
1383
|
+
start_time=start_time,
|
|
1384
|
+
end_time=end_time,
|
|
1385
|
+
limit=100,
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
series_data = []
|
|
1389
|
+
for metric in metric_data:
|
|
1390
|
+
series_data.append(
|
|
1391
|
+
{
|
|
1392
|
+
"timestamp": metric.timestamp.isoformat(),
|
|
1393
|
+
"value": metric.value,
|
|
1394
|
+
}
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
chart_data["data"].append(
|
|
1398
|
+
{
|
|
1399
|
+
"name": metric_name,
|
|
1400
|
+
"series": series_data,
|
|
1401
|
+
}
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
return chart_data
|
|
1405
|
+
|
|
1406
|
+
def _get_table_widget_data(
|
|
1407
|
+
self,
|
|
1408
|
+
widget: DashboardWidget,
|
|
1409
|
+
start_time: datetime,
|
|
1410
|
+
end_time: datetime,
|
|
1411
|
+
tenant_id: Optional[str] = None,
|
|
1412
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1413
|
+
) -> Dict[str, Any]:
|
|
1414
|
+
"""Get data for table widget"""
|
|
1415
|
+
if widget.widget_id == "alert_table":
|
|
1416
|
+
alerts = self.alert_manager.get_alert_history(hours=24, tenant_id=tenant_id)
|
|
1417
|
+
|
|
1418
|
+
table_data = []
|
|
1419
|
+
for alert in alerts:
|
|
1420
|
+
table_data.append(
|
|
1421
|
+
{
|
|
1422
|
+
"timestamp": alert.timestamp.isoformat(),
|
|
1423
|
+
"severity": alert.severity.value,
|
|
1424
|
+
"title": alert.title,
|
|
1425
|
+
"description": alert.description,
|
|
1426
|
+
"component": alert.component,
|
|
1427
|
+
"resolved": alert.resolved,
|
|
1428
|
+
}
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
return {
|
|
1432
|
+
"columns": [
|
|
1433
|
+
"timestamp",
|
|
1434
|
+
"severity",
|
|
1435
|
+
"title",
|
|
1436
|
+
"description",
|
|
1437
|
+
"component",
|
|
1438
|
+
"resolved",
|
|
1439
|
+
],
|
|
1440
|
+
"data": table_data,
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return {"error": "Unknown table type"}
|
|
1444
|
+
|
|
1445
|
+
def _get_alert_widget_data(self, widget: DashboardWidget, tenant_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1446
|
+
"""Get data for alert widget"""
|
|
1447
|
+
active_alerts = self.alert_manager.get_active_alerts(tenant_id=tenant_id)
|
|
1448
|
+
|
|
1449
|
+
return {
|
|
1450
|
+
"active_count": len(active_alerts),
|
|
1451
|
+
"severity_breakdown": {
|
|
1452
|
+
severity.value: len([a for a in active_alerts if a.severity == severity]) for severity in AlertSeverity
|
|
1453
|
+
},
|
|
1454
|
+
"recent_alerts": [
|
|
1455
|
+
{
|
|
1456
|
+
"id": alert.alert_id,
|
|
1457
|
+
"title": alert.title,
|
|
1458
|
+
"severity": alert.severity.value,
|
|
1459
|
+
"timestamp": alert.timestamp.isoformat(),
|
|
1460
|
+
}
|
|
1461
|
+
for alert in sorted(active_alerts, key=lambda a: a.timestamp, reverse=True)[:10]
|
|
1462
|
+
],
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
def _map_metric_name(self, metric_name: str) -> Optional[MetricType]:
|
|
1466
|
+
"""Map metric name to MetricType enum"""
|
|
1467
|
+
mapping = {
|
|
1468
|
+
"cpu_usage": MetricType.CPU_USAGE,
|
|
1469
|
+
"memory_usage": MetricType.MEMORY_USAGE,
|
|
1470
|
+
"hook_execution_rate": MetricType.THROUGHPUT,
|
|
1471
|
+
"hook_success_rate": MetricType.AVAILABILITY,
|
|
1472
|
+
"response_time": MetricType.RESPONSE_TIME,
|
|
1473
|
+
"error_rate": MetricType.ERROR_RATE,
|
|
1474
|
+
"network_io": MetricType.NETWORK_IO,
|
|
1475
|
+
"disk_io": MetricType.DISK_IO,
|
|
1476
|
+
"cache_performance": MetricType.CACHE_PERFORMANCE,
|
|
1477
|
+
"database_performance": MetricType.DATABASE_PERFORMANCE,
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
return mapping.get(metric_name)
|
|
1481
|
+
|
|
1482
|
+
def _broadcast_alert(self, alert: Alert) -> None:
|
|
1483
|
+
"""Broadcast alert to WebSocket clients"""
|
|
1484
|
+
if not self.enable_websocket:
|
|
1485
|
+
return
|
|
1486
|
+
|
|
1487
|
+
{
|
|
1488
|
+
"type": "alert",
|
|
1489
|
+
"data": alert.to_dict(),
|
|
1490
|
+
"timestamp": datetime.now().isoformat(),
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
# Implementation would send message to all connected WebSocket clients
|
|
1494
|
+
logger.info(f"Broadcasting alert: {alert.title}")
|
|
1495
|
+
|
|
1496
|
+
def get_system_status(self) -> Dict[str, Any]:
|
|
1497
|
+
"""Get comprehensive system status"""
|
|
1498
|
+
return {
|
|
1499
|
+
"status": "running" if self._running else "stopped",
|
|
1500
|
+
"uptime_seconds": (datetime.now() - self._startup_time).total_seconds(),
|
|
1501
|
+
"metrics_collected": len(self.metrics_collector.metrics_buffer),
|
|
1502
|
+
"active_alerts": len(self.alert_manager.active_alerts),
|
|
1503
|
+
"total_dashboards": len(self.dashboard_manager.dashboards) + len(self.dashboard_manager.default_dashboards),
|
|
1504
|
+
"websocket_connections": len(self.websocket_connections),
|
|
1505
|
+
"external_integrations": list(self.external_integrations.keys()),
|
|
1506
|
+
"last_update": datetime.now().isoformat(),
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
def create_custom_dashboard(
|
|
1510
|
+
self,
|
|
1511
|
+
name: str,
|
|
1512
|
+
description: str,
|
|
1513
|
+
widgets: List[Dict[str, Any]],
|
|
1514
|
+
owner: str = "",
|
|
1515
|
+
tenant_id: Optional[str] = None,
|
|
1516
|
+
is_public: bool = False,
|
|
1517
|
+
) -> str:
|
|
1518
|
+
"""Create a custom dashboard from widget definitions"""
|
|
1519
|
+
dashboard_widgets = []
|
|
1520
|
+
|
|
1521
|
+
for widget_def in widgets:
|
|
1522
|
+
widget = DashboardWidget(
|
|
1523
|
+
widget_id=widget_def.get("widget_id", str(uuid.uuid4())),
|
|
1524
|
+
widget_type=widget_def.get("widget_type", "metric"),
|
|
1525
|
+
title=widget_def.get("title", ""),
|
|
1526
|
+
position=widget_def.get("position", {"x": 0, "y": 0, "width": 4, "height": 2}),
|
|
1527
|
+
config=widget_def.get("config", {}),
|
|
1528
|
+
metrics=widget_def.get("metrics", []),
|
|
1529
|
+
)
|
|
1530
|
+
dashboard_widgets.append(widget)
|
|
1531
|
+
|
|
1532
|
+
return self.dashboard_manager.create_dashboard(
|
|
1533
|
+
name=name,
|
|
1534
|
+
description=description,
|
|
1535
|
+
dashboard_type=DashboardType.CUSTOM,
|
|
1536
|
+
widgets=dashboard_widgets,
|
|
1537
|
+
owner=owner,
|
|
1538
|
+
tenant_id=tenant_id,
|
|
1539
|
+
is_public=is_public,
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
# Global instance for easy access
|
|
1544
|
+
_monitoring_dashboard: Optional[RealtimeMonitoringDashboard] = None
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def get_monitoring_dashboard(
|
|
1548
|
+
metrics_buffer_size: int = 100000,
|
|
1549
|
+
retention_hours: int = 168,
|
|
1550
|
+
alert_check_interval: int = 30,
|
|
1551
|
+
enable_websocket: bool = True,
|
|
1552
|
+
enable_external_integration: bool = True,
|
|
1553
|
+
) -> RealtimeMonitoringDashboard:
|
|
1554
|
+
"""Get or create global monitoring dashboard instance"""
|
|
1555
|
+
global _monitoring_dashboard
|
|
1556
|
+
if _monitoring_dashboard is None:
|
|
1557
|
+
_monitoring_dashboard = RealtimeMonitoringDashboard(
|
|
1558
|
+
metrics_buffer_size=metrics_buffer_size,
|
|
1559
|
+
retention_hours=retention_hours,
|
|
1560
|
+
alert_check_interval=alert_check_interval,
|
|
1561
|
+
enable_websocket=enable_websocket,
|
|
1562
|
+
enable_external_integration=enable_external_integration,
|
|
1563
|
+
)
|
|
1564
|
+
return _monitoring_dashboard
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
# Convenience functions
|
|
1568
|
+
async def start_monitoring() -> None:
|
|
1569
|
+
"""Start the monitoring dashboard"""
|
|
1570
|
+
dashboard = get_monitoring_dashboard()
|
|
1571
|
+
await dashboard.start()
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def stop_monitoring() -> None:
|
|
1575
|
+
"""Stop the monitoring dashboard"""
|
|
1576
|
+
dashboard = get_monitoring_dashboard()
|
|
1577
|
+
dashboard.stop()
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
def add_system_metric(
|
|
1581
|
+
metric_type: MetricType,
|
|
1582
|
+
value: Union[int, float],
|
|
1583
|
+
component: str = "",
|
|
1584
|
+
tags: Optional[Dict[str, str]] = None,
|
|
1585
|
+
) -> None:
|
|
1586
|
+
"""Add a system metric"""
|
|
1587
|
+
dashboard = get_monitoring_dashboard()
|
|
1588
|
+
dashboard.add_metric(metric_type, value, tags, component=component)
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
def add_hook_metric(
|
|
1592
|
+
hook_path: str,
|
|
1593
|
+
execution_time_ms: float,
|
|
1594
|
+
success: bool,
|
|
1595
|
+
tenant_id: Optional[str] = None,
|
|
1596
|
+
) -> None:
|
|
1597
|
+
"""Add hook execution metric"""
|
|
1598
|
+
dashboard = get_monitoring_dashboard()
|
|
1599
|
+
|
|
1600
|
+
# Add execution time metric
|
|
1601
|
+
dashboard.add_metric(
|
|
1602
|
+
MetricType.RESPONSE_TIME,
|
|
1603
|
+
execution_time_ms,
|
|
1604
|
+
tags={"hook_path": hook_path, "success": str(success)},
|
|
1605
|
+
component="hooks",
|
|
1606
|
+
tenant_id=tenant_id,
|
|
1607
|
+
)
|
|
1608
|
+
|
|
1609
|
+
# Add success rate metric
|
|
1610
|
+
dashboard.add_metric(
|
|
1611
|
+
MetricType.AVAILABILITY,
|
|
1612
|
+
1.0 if success else 0.0,
|
|
1613
|
+
tags={"hook_path": hook_path},
|
|
1614
|
+
component="hooks",
|
|
1615
|
+
tenant_id=tenant_id,
|
|
1616
|
+
)
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
if __name__ == "__main__":
|
|
1620
|
+
# Example usage
|
|
1621
|
+
async def main():
|
|
1622
|
+
print("š Starting Real-time Monitoring Dashboard...")
|
|
1623
|
+
|
|
1624
|
+
# Initialize monitoring dashboard
|
|
1625
|
+
dashboard = RealtimeMonitoringDashboard(
|
|
1626
|
+
metrics_buffer_size=10000,
|
|
1627
|
+
retention_hours=24,
|
|
1628
|
+
enable_websocket=False, # Disable for demo
|
|
1629
|
+
enable_external_integration=False,
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
try:
|
|
1633
|
+
# Start the system
|
|
1634
|
+
await dashboard.start()
|
|
1635
|
+
|
|
1636
|
+
# Add some test metrics
|
|
1637
|
+
print("\nš Adding test metrics...")
|
|
1638
|
+
for i in range(20):
|
|
1639
|
+
dashboard.add_metric(
|
|
1640
|
+
MetricType.CPU_USAGE,
|
|
1641
|
+
20 + (i % 80), # CPU usage from 20% to 100%
|
|
1642
|
+
tags={"component": "system", "instance": "demo"},
|
|
1643
|
+
source="demo",
|
|
1644
|
+
)
|
|
1645
|
+
|
|
1646
|
+
dashboard.add_metric(
|
|
1647
|
+
MetricType.MEMORY_USAGE,
|
|
1648
|
+
30 + (i % 70), # Memory usage from 30% to 100%
|
|
1649
|
+
tags={"component": "system"},
|
|
1650
|
+
source="demo",
|
|
1651
|
+
)
|
|
1652
|
+
|
|
1653
|
+
dashboard.add_hook_metric(
|
|
1654
|
+
f"test_hook_{i % 5}.py",
|
|
1655
|
+
100 + (i * 50), # Execution time from 100ms to 1000ms
|
|
1656
|
+
i % 4 != 0, # 75% success rate
|
|
1657
|
+
tenant_id="demo_tenant" if i % 2 == 0 else None,
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
await asyncio.sleep(0.1)
|
|
1661
|
+
|
|
1662
|
+
# Let metrics process
|
|
1663
|
+
print("\nā³ Processing metrics and checking alerts...")
|
|
1664
|
+
await asyncio.sleep(5)
|
|
1665
|
+
|
|
1666
|
+
# Get system status
|
|
1667
|
+
status = dashboard.get_system_status()
|
|
1668
|
+
print("\nš System Status:")
|
|
1669
|
+
print(f" Status: {status['status']}")
|
|
1670
|
+
print(f" Uptime: {status['uptime_seconds']:.1f}s")
|
|
1671
|
+
print(f" Metrics collected: {status['metrics_collected']}")
|
|
1672
|
+
print(f" Active alerts: {status['active_alerts']}")
|
|
1673
|
+
print(f" Total dashboards: {status['total_dashboards']}")
|
|
1674
|
+
|
|
1675
|
+
# Get dashboard data
|
|
1676
|
+
dashboard_data = dashboard.get_dashboard_data("system_overview")
|
|
1677
|
+
print("\nš± Dashboard Data:")
|
|
1678
|
+
print(f" Dashboard: {dashboard_data['dashboard']['name']}")
|
|
1679
|
+
print(f" Widgets: {len(dashboard_data.get('widgets_data', {}))}")
|
|
1680
|
+
print(f" Generated at: {dashboard_data.get('generated_at')}")
|
|
1681
|
+
|
|
1682
|
+
# Get metrics statistics
|
|
1683
|
+
cpu_stats = dashboard.metrics_collector.get_statistics(MetricType.CPU_USAGE, minutes=10)
|
|
1684
|
+
memory_stats = dashboard.metrics_collector.get_statistics(MetricType.MEMORY_USAGE, minutes=10)
|
|
1685
|
+
print("\nš Metrics Statistics (last 10 minutes):")
|
|
1686
|
+
cpu_avg = cpu_stats.get("average", 0)
|
|
1687
|
+
cpu_max = cpu_stats.get("max", 0)
|
|
1688
|
+
cpu_count = cpu_stats.get("count", 0)
|
|
1689
|
+
print(f" CPU Usage - Avg: {cpu_avg:.1f}%, Max: {cpu_max:.1f}%, Count: {cpu_count}")
|
|
1690
|
+
mem_avg = memory_stats.get("average", 0)
|
|
1691
|
+
mem_max = memory_stats.get("max", 0)
|
|
1692
|
+
mem_count = memory_stats.get("count", 0)
|
|
1693
|
+
print(f" Memory Usage - Avg: {mem_avg:.1f}%, Max: {mem_max:.1f}%, Count: {mem_count}")
|
|
1694
|
+
|
|
1695
|
+
# Get alert statistics
|
|
1696
|
+
alert_stats = dashboard.alert_manager.get_alert_statistics(hours=1)
|
|
1697
|
+
print("\nšØ Alert Statistics (last 1 hour):")
|
|
1698
|
+
print(f" Total alerts: {alert_stats['total_alerts']}")
|
|
1699
|
+
print(f" Resolved: {alert_stats['resolved_count']}")
|
|
1700
|
+
print(f" Resolution rate: {alert_stats['resolution_rate']:.1%}")
|
|
1701
|
+
print(f" By severity: {alert_stats['by_severity']}")
|
|
1702
|
+
|
|
1703
|
+
# List available dashboards
|
|
1704
|
+
dashboards = dashboard.dashboard_manager.list_dashboards()
|
|
1705
|
+
print("\nš Available Dashboards:")
|
|
1706
|
+
for dashboard_info in dashboards:
|
|
1707
|
+
print(f" - {dashboard_info.name} ({dashboard_info.dashboard_type.value})")
|
|
1708
|
+
|
|
1709
|
+
print("\nā
Real-time Monitoring Dashboard demo completed successfully!")
|
|
1710
|
+
|
|
1711
|
+
except Exception as e:
|
|
1712
|
+
print(f"\nā Demo failed: {str(e)}")
|
|
1713
|
+
import traceback
|
|
1714
|
+
|
|
1715
|
+
traceback.print_exc()
|
|
1716
|
+
|
|
1717
|
+
finally:
|
|
1718
|
+
# Stop the system
|
|
1719
|
+
print("\nš Stopping monitoring dashboard...")
|
|
1720
|
+
dashboard.stop()
|
|
1721
|
+
print("ā
System stopped")
|
|
1722
|
+
|
|
1723
|
+
# Run the demo
|
|
1724
|
+
asyncio.run(main())
|