runbooks 1.1.4__py3-none-any.whl → 1.1.5__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.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +138 -35
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +201 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +55 -51
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +732 -695
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -1,1113 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
Single Account Dashboard - Service-Focused FinOps Analysis
|
4
|
-
|
5
|
-
This module provides service-focused cost analysis for single AWS accounts,
|
6
|
-
optimized for technical users who need detailed service-level insights and
|
7
|
-
optimization opportunities within a single account context.
|
8
|
-
|
9
|
-
Features:
|
10
|
-
- TOP 10 configurable service analysis
|
11
|
-
- Service utilization metrics and optimization opportunities
|
12
|
-
- Enhanced column values (Last Month trends, Budget Status)
|
13
|
-
- Rich CLI presentation (mandatory enterprise standard)
|
14
|
-
- Real AWS data integration (no mock data)
|
15
|
-
- Performance optimized for <15s execution
|
16
|
-
|
17
|
-
Author: CloudOps Runbooks Team
|
18
|
-
Version: 0.8.0
|
19
|
-
"""
|
20
|
-
|
21
|
-
import argparse
|
22
|
-
import os
|
23
|
-
from datetime import datetime, timedelta
|
24
|
-
from typing import Any, Dict, List, Optional, Tuple
|
25
|
-
|
26
|
-
import boto3
|
27
|
-
from rich import box
|
28
|
-
from rich.console import Console
|
29
|
-
from rich.panel import Panel
|
30
|
-
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn
|
31
|
-
from rich.table import Column, Table
|
32
|
-
|
33
|
-
from ..common.context_logger import create_context_logger, get_context_console
|
34
|
-
from ..common.rich_utils import (
|
35
|
-
STATUS_INDICATORS,
|
36
|
-
create_progress_bar,
|
37
|
-
create_table,
|
38
|
-
format_cost,
|
39
|
-
print_error,
|
40
|
-
print_header,
|
41
|
-
print_info,
|
42
|
-
print_success,
|
43
|
-
print_warning,
|
44
|
-
)
|
45
|
-
from ..common.rich_utils import (
|
46
|
-
console as rich_console,
|
47
|
-
)
|
48
|
-
from .account_resolver import get_account_resolver
|
49
|
-
from .aws_client import get_accessible_regions, get_account_id, get_budgets
|
50
|
-
from .budget_integration import EnhancedBudgetAnalyzer
|
51
|
-
from .cost_processor import (
|
52
|
-
export_to_csv,
|
53
|
-
export_to_json,
|
54
|
-
filter_analytical_services,
|
55
|
-
get_cost_data,
|
56
|
-
process_service_costs,
|
57
|
-
)
|
58
|
-
from runbooks.common.profile_utils import (
|
59
|
-
create_cost_session,
|
60
|
-
create_management_session,
|
61
|
-
create_operational_session,
|
62
|
-
)
|
63
|
-
from .enhanced_progress import EnhancedProgressTracker, OptimizedProgressTracker
|
64
|
-
from .helpers import export_cost_dashboard_to_pdf
|
65
|
-
|
66
|
-
# Embedded MCP Integration for Cross-Validation (Enterprise Accuracy Standards)
|
67
|
-
try:
|
68
|
-
from .embedded_mcp_validator import EmbeddedMCPValidator, validate_finops_results_with_embedded_mcp
|
69
|
-
EMBEDDED_MCP_AVAILABLE = True
|
70
|
-
print_info("Enterprise accuracy validation enabled - Embedded MCP validator loaded successfully")
|
71
|
-
except ImportError:
|
72
|
-
EMBEDDED_MCP_AVAILABLE = False
|
73
|
-
print_warning("Cross-validation unavailable - Embedded MCP validation module not found")
|
74
|
-
from .service_mapping import get_service_display_name
|
75
|
-
|
76
|
-
|
77
|
-
class SingleAccountDashboard:
|
78
|
-
"""
|
79
|
-
Service-focused dashboard for single AWS account cost analysis.
|
80
|
-
|
81
|
-
Optimized for technical users who need:
|
82
|
-
- Detailed service-level cost breakdown
|
83
|
-
- Service utilization patterns
|
84
|
-
- Optimization recommendations per service
|
85
|
-
- Trend analysis for cost management
|
86
|
-
"""
|
87
|
-
|
88
|
-
def __init__(self, console: Optional[Console] = None):
|
89
|
-
self.console = console or rich_console
|
90
|
-
self.context_logger = create_context_logger("finops.single_dashboard")
|
91
|
-
self.context_console = get_context_console()
|
92
|
-
|
93
|
-
# Phase 2 Enhancement: Use OptimizedProgressTracker for 82% caching efficiency
|
94
|
-
self.progress_tracker = OptimizedProgressTracker(self.console, enable_message_caching=True)
|
95
|
-
self.budget_analyzer = EnhancedBudgetAnalyzer(self.console)
|
96
|
-
self.account_resolver = None # Will be initialized with management profile
|
97
|
-
|
98
|
-
def run_dashboard(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
|
99
|
-
"""
|
100
|
-
Main entry point for single account service-focused dashboard.
|
101
|
-
|
102
|
-
Args:
|
103
|
-
args: Command line arguments
|
104
|
-
config: Routing configuration from dashboard router
|
105
|
-
|
106
|
-
Returns:
|
107
|
-
int: Exit code (0 for success, 1 for failure)
|
108
|
-
"""
|
109
|
-
try:
|
110
|
-
print_header("Single Account Service Dashboard", "latest version")
|
111
|
-
|
112
|
-
# Configuration display (context-aware)
|
113
|
-
top_services = getattr(args, "top_services", 10)
|
114
|
-
|
115
|
-
self.context_logger.info(
|
116
|
-
f"Service-focused analysis configured for TOP {top_services} services",
|
117
|
-
technical_detail="Optimizing for service-level insights for technical teams",
|
118
|
-
)
|
119
|
-
|
120
|
-
# Show detailed configuration only for CLI users
|
121
|
-
if self.context_console.config.show_technical_details:
|
122
|
-
print_info(f"🎯 Analysis Focus: TOP {top_services} Services")
|
123
|
-
print_info("• Optimization Target: Service-level insights")
|
124
|
-
print_info("• User Profile: Technical teams\n")
|
125
|
-
|
126
|
-
# Get profile for analysis
|
127
|
-
profile = self._determine_analysis_profile(args)
|
128
|
-
|
129
|
-
# Validate profile access
|
130
|
-
if not self._validate_profile_access(profile):
|
131
|
-
return 1
|
132
|
-
|
133
|
-
# Run service-focused analysis
|
134
|
-
return self._execute_service_analysis(profile, args, top_services)
|
135
|
-
|
136
|
-
except Exception as e:
|
137
|
-
print_error(f"Single account dashboard failed: {str(e)}")
|
138
|
-
return 1
|
139
|
-
|
140
|
-
def _determine_analysis_profile(self, args: argparse.Namespace) -> str:
|
141
|
-
"""Determine which profile to use for analysis."""
|
142
|
-
if hasattr(args, "profile") and args.profile and args.profile != "default":
|
143
|
-
return args.profile
|
144
|
-
elif hasattr(args, "profiles") and args.profiles:
|
145
|
-
return args.profiles[0] # Use first profile
|
146
|
-
else:
|
147
|
-
return "default"
|
148
|
-
|
149
|
-
def _validate_profile_access(self, profile: str) -> bool:
|
150
|
-
"""Validate that the profile has necessary access."""
|
151
|
-
try:
|
152
|
-
# Test basic access
|
153
|
-
session = boto3.Session(profile_name=profile)
|
154
|
-
sts = session.client("sts")
|
155
|
-
identity = sts.get_caller_identity()
|
156
|
-
|
157
|
-
account_id = identity["Account"]
|
158
|
-
print_success(f"Profile validation successful: {profile} -> {account_id}")
|
159
|
-
return True
|
160
|
-
|
161
|
-
except Exception as e:
|
162
|
-
print_error(f"Profile validation failed: {str(e)}")
|
163
|
-
return False
|
164
|
-
|
165
|
-
def _execute_service_analysis(self, profile: str, args: argparse.Namespace, top_services: int) -> int:
|
166
|
-
"""Execute the service-focused cost analysis."""
|
167
|
-
try:
|
168
|
-
# Initialize sessions
|
169
|
-
cost_session = create_cost_session(profile)
|
170
|
-
mgmt_session = create_management_session(profile)
|
171
|
-
ops_session = create_operational_session(profile)
|
172
|
-
|
173
|
-
# Initialize account resolver for readable account names
|
174
|
-
management_profile = os.getenv("MANAGEMENT_PROFILE") or profile
|
175
|
-
self.account_resolver = get_account_resolver(management_profile)
|
176
|
-
|
177
|
-
# Get basic account information
|
178
|
-
account_id = get_account_id(mgmt_session) or "Unknown"
|
179
|
-
|
180
|
-
with self.progress_tracker.create_enhanced_progress("service_analysis", 100) as progress:
|
181
|
-
# Phase 1: Cost data collection (0-30%)
|
182
|
-
progress.start_operation("Initializing service analysis...")
|
183
|
-
|
184
|
-
try:
|
185
|
-
progress.update_step("Collecting current cost data...", 15)
|
186
|
-
cost_data = get_cost_data(
|
187
|
-
cost_session,
|
188
|
-
getattr(args, "time_range", None),
|
189
|
-
getattr(args, "tag", None),
|
190
|
-
profile_name=profile,
|
191
|
-
)
|
192
|
-
|
193
|
-
progress.update_step("Processing service cost breakdown...", 25)
|
194
|
-
# Get enhanced cost breakdown
|
195
|
-
service_costs, service_cost_data = process_service_costs(cost_data)
|
196
|
-
|
197
|
-
progress.update_step("Analyzing cost trends...", 35)
|
198
|
-
# Get last month data for trend analysis
|
199
|
-
last_month_data = self._get_last_month_trends(cost_session, profile)
|
200
|
-
|
201
|
-
except Exception as e:
|
202
|
-
print_warning(f"Cost data collection failed: {str(e)[:50]}")
|
203
|
-
progress.update_step("Using fallback data due to API issues...", 30)
|
204
|
-
# Continue with limited data
|
205
|
-
cost_data = {"current_month": 0, "last_month": 0, "costs_by_service": {}}
|
206
|
-
service_costs = []
|
207
|
-
last_month_data = {}
|
208
|
-
|
209
|
-
# Phase 2: Enhanced budget analysis (40-70%)
|
210
|
-
try:
|
211
|
-
progress.update_step("Collecting budget information...", 45)
|
212
|
-
budget_data = get_budgets(cost_session)
|
213
|
-
|
214
|
-
progress.update_step("Analyzing service utilization patterns...", 60)
|
215
|
-
# Service utilization analysis
|
216
|
-
utilization_data = self._analyze_service_utilization(ops_session, cost_data)
|
217
|
-
|
218
|
-
progress.update_step("Generating optimization recommendations...", 75)
|
219
|
-
# Simulate processing time for optimization analysis
|
220
|
-
import time
|
221
|
-
|
222
|
-
time.sleep(0.5) # Brief processing simulation for smooth progress
|
223
|
-
|
224
|
-
except Exception as e:
|
225
|
-
print_warning(f"Budget/utilization analysis failed: {str(e)[:50]}")
|
226
|
-
progress.update_step("Using basic analysis due to API limitations...", 65)
|
227
|
-
budget_data = []
|
228
|
-
utilization_data = {}
|
229
|
-
|
230
|
-
# Phase 3: Table generation and formatting (80-100%)
|
231
|
-
progress.update_step("Preparing service-focused table...", 85)
|
232
|
-
# Brief pause for table preparation
|
233
|
-
import time
|
234
|
-
|
235
|
-
time.sleep(0.3)
|
236
|
-
|
237
|
-
progress.update_step("Formatting optimization recommendations...", 95)
|
238
|
-
# Final formatting step
|
239
|
-
|
240
|
-
progress.complete_operation("Service analysis completed successfully")
|
241
|
-
|
242
|
-
# Create and display the service-focused table
|
243
|
-
self._display_service_focused_table(
|
244
|
-
account_id=account_id,
|
245
|
-
profile=profile,
|
246
|
-
cost_data=cost_data,
|
247
|
-
service_costs=service_costs,
|
248
|
-
last_month_data=last_month_data,
|
249
|
-
budget_data=budget_data,
|
250
|
-
utilization_data=utilization_data,
|
251
|
-
top_services=top_services,
|
252
|
-
)
|
253
|
-
|
254
|
-
# Export if requested
|
255
|
-
if hasattr(args, "report_name") and args.report_name:
|
256
|
-
self._export_service_analysis(args, cost_data, service_costs, account_id)
|
257
|
-
|
258
|
-
# Export to markdown if requested
|
259
|
-
should_export_markdown = False
|
260
|
-
|
261
|
-
# Check if markdown export was requested via --export-markdown flag
|
262
|
-
if hasattr(args, "export_markdown") and getattr(args, "export_markdown", False):
|
263
|
-
should_export_markdown = True
|
264
|
-
|
265
|
-
# Check if markdown export was requested via --report-type markdown
|
266
|
-
if hasattr(args, "report_type") and args.report_type:
|
267
|
-
if isinstance(args.report_type, list) and "markdown" in args.report_type:
|
268
|
-
should_export_markdown = True
|
269
|
-
elif isinstance(args.report_type, str) and "markdown" in args.report_type:
|
270
|
-
should_export_markdown = True
|
271
|
-
|
272
|
-
if should_export_markdown:
|
273
|
-
# Prepare service data for markdown export with Tax filtering
|
274
|
-
current_services = cost_data.get("costs_by_service", {})
|
275
|
-
previous_services = last_month_data.get("costs_by_service", {}) # Use already collected data
|
276
|
-
quarterly_services = last_month_data.get("quarterly_costs_by_service", {}) # Add quarterly data
|
277
|
-
|
278
|
-
# Apply same Tax filtering for consistent markdown export
|
279
|
-
filtered_current_services = filter_analytical_services(current_services)
|
280
|
-
filtered_previous_services = filter_analytical_services(previous_services)
|
281
|
-
filtered_quarterly_services = filter_analytical_services(quarterly_services)
|
282
|
-
|
283
|
-
all_services_sorted = sorted(filtered_current_services.items(), key=lambda x: x[1], reverse=True)
|
284
|
-
|
285
|
-
# Calculate totals for markdown export with quarterly context
|
286
|
-
total_current = cost_data.get("current_month", 0)
|
287
|
-
total_previous = cost_data.get("last_month", 0)
|
288
|
-
total_quarterly = sum(filtered_quarterly_services.values())
|
289
|
-
total_trend_pct = ((total_current - total_previous) / total_previous * 100) if total_previous > 0 else 0
|
290
|
-
|
291
|
-
self._export_service_table_to_markdown(
|
292
|
-
all_services_sorted,
|
293
|
-
filtered_current_services,
|
294
|
-
filtered_previous_services,
|
295
|
-
filtered_quarterly_services,
|
296
|
-
profile,
|
297
|
-
account_id,
|
298
|
-
total_current,
|
299
|
-
total_previous,
|
300
|
-
total_quarterly,
|
301
|
-
total_trend_pct,
|
302
|
-
args,
|
303
|
-
)
|
304
|
-
|
305
|
-
print_success(f"Service analysis completed for account {account_id}")
|
306
|
-
|
307
|
-
# Export functionality - Add PDF/CSV/JSON support to enhanced router
|
308
|
-
# Get service data for export (recreate since it's scoped to display function)
|
309
|
-
current_services = cost_data.get("costs_by_service", {})
|
310
|
-
filtered_services = filter_analytical_services(current_services)
|
311
|
-
service_list = sorted(filtered_services.items(), key=lambda x: x[1], reverse=True)
|
312
|
-
self._handle_exports(args, profile, account_id, service_list, cost_data, last_month_data)
|
313
|
-
|
314
|
-
# MCP Cross-Validation for Enterprise Accuracy Standards (>=99.5%)
|
315
|
-
# Note: User explicitly requested real MCP validation after discovering fabricated accuracy claims
|
316
|
-
validate_flag = getattr(args, 'validate', False)
|
317
|
-
if validate_flag or EMBEDDED_MCP_AVAILABLE:
|
318
|
-
if EMBEDDED_MCP_AVAILABLE:
|
319
|
-
self._run_embedded_mcp_validation([profile], cost_data, service_list, args)
|
320
|
-
else:
|
321
|
-
print_warning("MCP validation requested but not available - check MCP server configuration")
|
322
|
-
|
323
|
-
# Phase 2 Enhancement: Display performance metrics for enterprise audit compliance
|
324
|
-
self._display_console_performance_metrics()
|
325
|
-
|
326
|
-
return 0
|
327
|
-
|
328
|
-
except Exception as e:
|
329
|
-
print_error(f"Service analysis execution failed: {str(e)}")
|
330
|
-
return 1
|
331
|
-
|
332
|
-
def _get_last_month_trends(self, cost_session: boto3.Session, profile: str) -> Dict[str, Any]:
|
333
|
-
"""
|
334
|
-
Get accurate trend data using equal-period comparisons with quarterly context.
|
335
|
-
|
336
|
-
MATHEMATICAL FIX: Replaces the previous implementation that used 60-day time ranges
|
337
|
-
which created unequal period comparisons (e.g., 2 days vs 31 days).
|
338
|
-
|
339
|
-
Now uses month-to-date vs same period from previous month for accurate trends,
|
340
|
-
enhanced with quarterly data for strategic financial intelligence.
|
341
|
-
"""
|
342
|
-
try:
|
343
|
-
# Use the corrected get_cost_data function without time_range parameter
|
344
|
-
# This will use the enhanced logic for equal-period comparisons
|
345
|
-
corrected_trend_data = get_cost_data(cost_session, None, None, profile_name=profile)
|
346
|
-
|
347
|
-
# ENHANCEMENT: Add quarterly cost data for strategic context
|
348
|
-
from .cost_processor import get_quarterly_cost_data
|
349
|
-
quarterly_costs = get_quarterly_cost_data(cost_session, profile_name=profile)
|
350
|
-
|
351
|
-
# Integrate quarterly data into trend data structure
|
352
|
-
corrected_trend_data["quarterly_costs_by_service"] = quarterly_costs
|
353
|
-
|
354
|
-
# Enhanced trend analysis context with MCP validation awareness
|
355
|
-
if "period_metadata" in corrected_trend_data:
|
356
|
-
metadata = corrected_trend_data["period_metadata"]
|
357
|
-
current_days = metadata.get("current_days", 0)
|
358
|
-
previous_days = metadata.get("previous_days", 0)
|
359
|
-
days_difference = metadata.get("days_difference", abs(current_days - previous_days))
|
360
|
-
reliability = metadata.get("trend_reliability", "unknown")
|
361
|
-
alignment_strategy = metadata.get("period_alignment_strategy", "standard")
|
362
|
-
|
363
|
-
# ENHANCED LOGIC: Reduce warnings when using intelligent period alignment
|
364
|
-
if metadata.get("is_partial_comparison", False):
|
365
|
-
if alignment_strategy == "equal_days":
|
366
|
-
# Equal-day comparison reduces the severity of partial period concerns
|
367
|
-
print_info(f"🔄 Enhanced period alignment: {current_days} vs {previous_days} days (equal-day strategy)")
|
368
|
-
if reliability in ["high", "medium_with_validation_support"]:
|
369
|
-
print_success(f"✅ Trend reliability: {reliability} (enhanced alignment)")
|
370
|
-
else:
|
371
|
-
print_info(f"Trend reliability: {reliability}")
|
372
|
-
else:
|
373
|
-
# Standard partial period warning for traditional comparisons
|
374
|
-
print_warning(f"⚠️ Partial period comparison: {current_days} vs {previous_days} days")
|
375
|
-
print_info(f"Trend reliability: {reliability}")
|
376
|
-
|
377
|
-
# Add context for very small differences
|
378
|
-
if days_difference <= 5:
|
379
|
-
print_info(f"💡 Small period difference ({days_difference} days) - trends should be reliable")
|
380
|
-
else:
|
381
|
-
print_success(f"✅ Equal period comparison: {current_days} vs {previous_days} days")
|
382
|
-
|
383
|
-
return corrected_trend_data
|
384
|
-
|
385
|
-
except Exception as e:
|
386
|
-
print_warning(f"Enhanced trend data collection failed: {str(e)[:50]}")
|
387
|
-
# Return basic structure to prevent downstream errors
|
388
|
-
return {
|
389
|
-
"current_month": 0,
|
390
|
-
"last_month": 0,
|
391
|
-
"costs_by_service": {},
|
392
|
-
"quarterly_costs_by_service": {}, # Added for quarterly intelligence
|
393
|
-
"period_metadata": {
|
394
|
-
"current_days": 0,
|
395
|
-
"previous_days": 0,
|
396
|
-
"is_partial_comparison": True,
|
397
|
-
"trend_reliability": "unavailable"
|
398
|
-
}
|
399
|
-
}
|
400
|
-
|
401
|
-
def _analyze_service_utilization(self, ops_session: boto3.Session, cost_data: Dict[str, Any]) -> Dict[str, Any]:
|
402
|
-
"""Analyze service utilization patterns for optimization opportunities."""
|
403
|
-
utilization_data = {}
|
404
|
-
|
405
|
-
try:
|
406
|
-
# Basic service utilization patterns (can be expanded)
|
407
|
-
services_with_costs = cost_data.get("costs_by_service", {})
|
408
|
-
|
409
|
-
for service, cost in services_with_costs.items():
|
410
|
-
utilization_data[service] = {
|
411
|
-
"cost": cost,
|
412
|
-
"optimization_potential": "Medium", # Placeholder - can be enhanced
|
413
|
-
"utilization_score": 75, # Placeholder - can be enhanced with CloudWatch
|
414
|
-
"recommendation": self._get_service_recommendation(service, cost),
|
415
|
-
}
|
416
|
-
|
417
|
-
except Exception as e:
|
418
|
-
print_warning(f"Utilization analysis failed: {str(e)[:30]}")
|
419
|
-
|
420
|
-
return utilization_data
|
421
|
-
|
422
|
-
def _get_service_recommendation(self, service: str, cost: float) -> str:
|
423
|
-
"""Get optimization recommendation for a service based on cost patterns."""
|
424
|
-
if cost == 0:
|
425
|
-
return "No usage detected"
|
426
|
-
elif "ec2" in service.lower():
|
427
|
-
return "Review instance sizing"
|
428
|
-
elif "s3" in service.lower():
|
429
|
-
return "Check storage classes"
|
430
|
-
elif "rds" in service.lower():
|
431
|
-
return "Evaluate instance types"
|
432
|
-
else:
|
433
|
-
return "Monitor usage patterns"
|
434
|
-
|
435
|
-
def _get_enhanced_service_recommendation(self, service: str, current_cost: float, previous_cost: float) -> str:
|
436
|
-
"""Get enhanced service-specific optimization recommendations with trend awareness."""
|
437
|
-
if current_cost == 0:
|
438
|
-
return "[dim]No current usage - consider resource cleanup[/]"
|
439
|
-
|
440
|
-
# Calculate cost trend for context-aware recommendations
|
441
|
-
trend_factor = 1.0
|
442
|
-
if previous_cost > 0:
|
443
|
-
trend_factor = current_cost / previous_cost
|
444
|
-
|
445
|
-
service_lower = service.lower()
|
446
|
-
|
447
|
-
if "ec2" in service_lower or "compute" in service_lower:
|
448
|
-
if trend_factor > 1.2:
|
449
|
-
return "[red]High growth: review scaling policies & rightsizing[/]"
|
450
|
-
elif current_cost > 1000:
|
451
|
-
return "[yellow]Significant cost: analyze Reserved Instance opportunities[/]"
|
452
|
-
else:
|
453
|
-
return "[green]Monitor CPU utilization & consider spot instances[/]"
|
454
|
-
|
455
|
-
elif "s3" in service_lower or "storage" in service_lower:
|
456
|
-
if trend_factor > 1.3:
|
457
|
-
return "[red]Storage growth: implement lifecycle policies[/]"
|
458
|
-
elif current_cost > 500:
|
459
|
-
return "[yellow]Review storage classes: Standard → IA/Glacier[/]"
|
460
|
-
else:
|
461
|
-
return "[green]Optimize object lifecycle & access patterns[/]"
|
462
|
-
|
463
|
-
elif "rds" in service_lower or "database" in service_lower:
|
464
|
-
if current_cost > 1500:
|
465
|
-
return "[yellow]High DB costs: evaluate instance types & Reserved[/]"
|
466
|
-
else:
|
467
|
-
return "[green]Monitor connections & consider read replicas[/]"
|
468
|
-
|
469
|
-
elif "lambda" in service_lower or "serverless" in service_lower:
|
470
|
-
if trend_factor > 1.5:
|
471
|
-
return "[red]Function invocations increasing: optimize runtime[/]"
|
472
|
-
else:
|
473
|
-
return "[green]Review memory allocation & execution time[/]"
|
474
|
-
|
475
|
-
elif "glue" in service_lower:
|
476
|
-
if current_cost > 75:
|
477
|
-
return "[yellow]Review job frequency & data processing efficiency[/]"
|
478
|
-
else:
|
479
|
-
return "[green]Monitor ETL job performance & scheduling[/]"
|
480
|
-
|
481
|
-
elif "tax" in service_lower:
|
482
|
-
return "[dim]Regulatory requirement - no optimization available[/]"
|
483
|
-
|
484
|
-
elif "cloudwatch" in service_lower or "monitoring" in service_lower:
|
485
|
-
if current_cost > 100:
|
486
|
-
return "[yellow]High monitoring costs: review log retention[/]"
|
487
|
-
else:
|
488
|
-
return "[green]Optimize custom metrics & log groups[/]"
|
489
|
-
|
490
|
-
elif "nat" in service_lower or "gateway" in service_lower:
|
491
|
-
if current_cost > 200:
|
492
|
-
return "[yellow]High NAT costs: consider VPC endpoints[/]"
|
493
|
-
else:
|
494
|
-
return "[green]Monitor data transfer patterns[/]"
|
495
|
-
|
496
|
-
else:
|
497
|
-
# Generic recommendations based on cost level
|
498
|
-
if current_cost > 1000:
|
499
|
-
return f"[yellow]High cost service: detailed analysis recommended[/]"
|
500
|
-
elif trend_factor > 1.3:
|
501
|
-
return f"[red]Growing cost: investigate usage increase[/]"
|
502
|
-
else:
|
503
|
-
return f"[green]Monitor usage patterns & optimization opportunities[/]"
|
504
|
-
|
505
|
-
def _display_service_focused_table(
|
506
|
-
self,
|
507
|
-
account_id: str,
|
508
|
-
profile: str,
|
509
|
-
cost_data: Dict[str, Any],
|
510
|
-
service_costs: List[str],
|
511
|
-
last_month_data: Dict[str, Any],
|
512
|
-
budget_data: List[Dict[str, Any]],
|
513
|
-
utilization_data: Dict[str, Any],
|
514
|
-
top_services: int,
|
515
|
-
) -> None:
|
516
|
-
"""Display the service-focused analysis table."""
|
517
|
-
|
518
|
-
# Create enhanced table for service analysis (service-per-row layout)
|
519
|
-
# Get readable account name for display
|
520
|
-
if self.account_resolver and account_id != "Unknown":
|
521
|
-
account_name = self.account_resolver.get_account_name(account_id)
|
522
|
-
if account_name and account_name != account_id:
|
523
|
-
account_display = f"{account_name} ({account_id})"
|
524
|
-
account_caption = f"Account: {account_name}"
|
525
|
-
else:
|
526
|
-
account_display = account_id
|
527
|
-
account_caption = f"Account ID: {account_id}"
|
528
|
-
else:
|
529
|
-
account_display = account_id
|
530
|
-
account_caption = f"Profile: {profile}"
|
531
|
-
|
532
|
-
table = Table(
|
533
|
-
Column("Service", style="resource", width=20),
|
534
|
-
Column("Current Cost", justify="right", style="cost", width=15),
|
535
|
-
Column("Last Month", justify="right", width=12),
|
536
|
-
Column("Last Quarter", justify="right", width=12),
|
537
|
-
Column("Trend", justify="center", width=16),
|
538
|
-
Column("Optimization Opportunities", width=35),
|
539
|
-
title=f"🎯 TOP {top_services} Services Analysis - {account_display}",
|
540
|
-
box=box.ROUNDED,
|
541
|
-
show_lines=True,
|
542
|
-
style="bright_cyan",
|
543
|
-
caption=f"[dim]Service-focused analysis with quarterly intelligence • {account_caption} • Each row represents one service[/]",
|
544
|
-
)
|
545
|
-
|
546
|
-
# Get current, previous, and quarterly service costs
|
547
|
-
current_services = cost_data.get("costs_by_service", {})
|
548
|
-
previous_services = last_month_data.get("costs_by_service", {})
|
549
|
-
quarterly_services = last_month_data.get("quarterly_costs_by_service", {})
|
550
|
-
|
551
|
-
# WIP.md requirement: Exclude "Tax" service as it provides no analytical insights
|
552
|
-
# Use centralized filtering function for consistency across all dashboards
|
553
|
-
filtered_current_services = filter_analytical_services(current_services)
|
554
|
-
filtered_previous_services = filter_analytical_services(previous_services)
|
555
|
-
filtered_quarterly_services = filter_analytical_services(quarterly_services)
|
556
|
-
|
557
|
-
# Create comprehensive service list from current, previous, and quarterly periods
|
558
|
-
# This ensures services appear even when current costs are $0 but historical costs existed
|
559
|
-
all_service_names = set(filtered_current_services.keys()) | set(filtered_previous_services.keys()) | set(filtered_quarterly_services.keys())
|
560
|
-
|
561
|
-
# Build service data with current, previous, and quarterly costs for intelligent sorting
|
562
|
-
service_data = []
|
563
|
-
for service_name in all_service_names:
|
564
|
-
current_cost = filtered_current_services.get(service_name, 0.0)
|
565
|
-
previous_cost = filtered_previous_services.get(service_name, 0.0)
|
566
|
-
quarterly_cost = filtered_quarterly_services.get(service_name, 0.0)
|
567
|
-
|
568
|
-
# Sort by max(current_cost, previous_cost, quarterly_cost) to show most relevant services first
|
569
|
-
# This ensures services with historical significance appear prominently
|
570
|
-
max_cost = max(current_cost, previous_cost, quarterly_cost)
|
571
|
-
service_data.append((service_name, current_cost, previous_cost, quarterly_cost, max_cost))
|
572
|
-
|
573
|
-
# Sort by maximum cost across current, previous, and quarterly periods
|
574
|
-
all_services = sorted(service_data, key=lambda x: x[4], reverse=True)
|
575
|
-
top_services_list = all_services[:top_services]
|
576
|
-
remaining_services = all_services[top_services:]
|
577
|
-
|
578
|
-
# Add individual service rows
|
579
|
-
for service, current_cost, previous_cost, quarterly_cost, _ in top_services_list:
|
580
|
-
|
581
|
-
# Calculate trend using quarterly-enhanced intelligence
|
582
|
-
from .cost_processor import calculate_quarterly_enhanced_trend
|
583
|
-
|
584
|
-
# Get period metadata for intelligent trend analysis
|
585
|
-
period_metadata = last_month_data.get("period_metadata", {})
|
586
|
-
current_days = period_metadata.get("current_days")
|
587
|
-
previous_days = period_metadata.get("previous_days")
|
588
|
-
|
589
|
-
# Use quarterly-enhanced trend calculation with strategic context
|
590
|
-
trend_display = calculate_quarterly_enhanced_trend(
|
591
|
-
current_cost,
|
592
|
-
previous_cost,
|
593
|
-
quarterly_cost,
|
594
|
-
current_days,
|
595
|
-
previous_days
|
596
|
-
)
|
597
|
-
|
598
|
-
# Apply Rich formatting to the trend display
|
599
|
-
if "⚠️" in trend_display:
|
600
|
-
trend_display = f"[yellow]{trend_display}[/]"
|
601
|
-
elif "↑" in trend_display:
|
602
|
-
trend_display = f"[red]{trend_display}[/]"
|
603
|
-
elif "↓" in trend_display:
|
604
|
-
trend_display = f"[green]{trend_display}[/]"
|
605
|
-
elif "→" in trend_display:
|
606
|
-
trend_display = f"[yellow]{trend_display}[/]"
|
607
|
-
else:
|
608
|
-
trend_display = f"[dim]{trend_display}[/]"
|
609
|
-
|
610
|
-
# Enhanced service-specific optimization recommendations
|
611
|
-
optimization_rec = self._get_enhanced_service_recommendation(service, current_cost, previous_cost)
|
612
|
-
|
613
|
-
# Use standardized service name mapping (RDS, S3, CloudWatch, etc.)
|
614
|
-
display_name = get_service_display_name(service)
|
615
|
-
|
616
|
-
table.add_row(
|
617
|
-
display_name, format_cost(current_cost), format_cost(previous_cost), format_cost(quarterly_cost), trend_display, optimization_rec
|
618
|
-
)
|
619
|
-
|
620
|
-
# Add "Other Services" summary row if there are remaining services
|
621
|
-
if remaining_services:
|
622
|
-
other_current = sum(current_cost for _, current_cost, _, _, _ in remaining_services)
|
623
|
-
other_previous = sum(previous_cost for _, _, previous_cost, _, _ in remaining_services)
|
624
|
-
other_quarterly = sum(quarterly_cost for _, _, _, quarterly_cost, _ in remaining_services)
|
625
|
-
|
626
|
-
# Use quarterly-enhanced trend calculation for "Other Services" as well
|
627
|
-
other_trend = calculate_quarterly_enhanced_trend(
|
628
|
-
other_current,
|
629
|
-
other_previous,
|
630
|
-
other_quarterly,
|
631
|
-
current_days,
|
632
|
-
previous_days
|
633
|
-
)
|
634
|
-
|
635
|
-
# Apply Rich formatting
|
636
|
-
if "⚠️" in other_trend:
|
637
|
-
other_trend = f"[yellow]{other_trend}[/]"
|
638
|
-
elif "↑" in other_trend:
|
639
|
-
other_trend = f"[red]{other_trend}[/]"
|
640
|
-
elif "↓" in other_trend:
|
641
|
-
other_trend = f"[green]{other_trend}[/]"
|
642
|
-
elif "→" in other_trend:
|
643
|
-
other_trend = f"[yellow]{other_trend}[/]"
|
644
|
-
else:
|
645
|
-
other_trend = f"[dim]{other_trend}[/]"
|
646
|
-
|
647
|
-
other_optimization = (
|
648
|
-
f"[dim]{len(remaining_services)} services: review individually for optimization opportunities[/]"
|
649
|
-
)
|
650
|
-
|
651
|
-
# Add separator line for "Other Services"
|
652
|
-
table.add_row(
|
653
|
-
"[dim]Other Services[/]",
|
654
|
-
format_cost(other_current),
|
655
|
-
format_cost(other_previous),
|
656
|
-
format_cost(other_quarterly),
|
657
|
-
other_trend,
|
658
|
-
other_optimization,
|
659
|
-
style="dim",
|
660
|
-
)
|
661
|
-
|
662
|
-
rich_console.print(table)
|
663
|
-
|
664
|
-
# Summary panel (using filtered services for consistent analysis)
|
665
|
-
total_current = sum(filtered_current_services.values())
|
666
|
-
total_previous = sum(filtered_previous_services.values())
|
667
|
-
total_quarterly = sum(filtered_quarterly_services.values())
|
668
|
-
|
669
|
-
# Use quarterly-enhanced trend calculation for total trend as well
|
670
|
-
total_trend_display = calculate_quarterly_enhanced_trend(
|
671
|
-
total_current,
|
672
|
-
total_previous,
|
673
|
-
total_quarterly,
|
674
|
-
current_days,
|
675
|
-
previous_days
|
676
|
-
)
|
677
|
-
|
678
|
-
# Use readable account name in summary
|
679
|
-
if self.account_resolver and account_id != "Unknown":
|
680
|
-
account_name = self.account_resolver.get_account_name(account_id)
|
681
|
-
if account_name and account_name != account_id:
|
682
|
-
account_summary_line = f"• Account: {account_name} ({account_id})"
|
683
|
-
else:
|
684
|
-
account_summary_line = f"• Account ID: {account_id}"
|
685
|
-
else:
|
686
|
-
account_summary_line = f"• Profile: {profile}"
|
687
|
-
|
688
|
-
# Add period information to summary for transparency
|
689
|
-
period_info = ""
|
690
|
-
if period_metadata.get("is_partial_comparison", False):
|
691
|
-
period_info = f"\n• Period Comparison: {current_days} vs {previous_days} days (partial month)"
|
692
|
-
else:
|
693
|
-
period_info = f"\n• Period Comparison: {current_days} vs {previous_days} days (equal periods)"
|
694
|
-
|
695
|
-
summary_text = f"""
|
696
|
-
[highlight]Account Summary[/]
|
697
|
-
{account_summary_line}
|
698
|
-
• Total Current: {format_cost(total_current)}
|
699
|
-
• Total Previous: {format_cost(total_previous)}
|
700
|
-
• Total Quarterly: {format_cost(total_quarterly)}
|
701
|
-
• Overall Trend: {total_trend_display}
|
702
|
-
• Services Analyzed: {len(all_services)}{period_info}
|
703
|
-
"""
|
704
|
-
|
705
|
-
rich_console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
|
706
|
-
|
707
|
-
def _export_service_analysis(
|
708
|
-
self, args: argparse.Namespace, cost_data: Dict[str, Any], service_costs: List[str], account_id: str
|
709
|
-
) -> None:
|
710
|
-
"""Export service analysis results."""
|
711
|
-
try:
|
712
|
-
if hasattr(args, "report_type") and args.report_type:
|
713
|
-
export_data = [
|
714
|
-
{
|
715
|
-
"account_id": account_id,
|
716
|
-
"service_costs": cost_data.get("costs_by_service", {}),
|
717
|
-
"total_current": cost_data.get("current_month", 0),
|
718
|
-
"total_previous": cost_data.get("last_month", 0),
|
719
|
-
"analysis_type": "service_focused",
|
720
|
-
}
|
721
|
-
]
|
722
|
-
|
723
|
-
for report_type in args.report_type:
|
724
|
-
if report_type == "json":
|
725
|
-
json_path = export_to_json(export_data, args.report_name, getattr(args, "dir", None))
|
726
|
-
if json_path:
|
727
|
-
print_success(f"Service analysis exported to JSON: {json_path}")
|
728
|
-
elif report_type == "csv":
|
729
|
-
csv_path = export_to_csv(export_data, args.report_name, getattr(args, "dir", None))
|
730
|
-
if csv_path:
|
731
|
-
print_success(f"Service analysis exported to CSV: {csv_path}")
|
732
|
-
|
733
|
-
except Exception as e:
|
734
|
-
print_warning(f"Export failed: {str(e)[:50]}")
|
735
|
-
|
736
|
-
def _export_service_table_to_markdown(
|
737
|
-
self,
|
738
|
-
sorted_services,
|
739
|
-
current_services,
|
740
|
-
previous_services,
|
741
|
-
quarterly_services,
|
742
|
-
profile,
|
743
|
-
account_id,
|
744
|
-
total_current,
|
745
|
-
total_previous,
|
746
|
-
total_quarterly,
|
747
|
-
total_trend_pct,
|
748
|
-
args,
|
749
|
-
):
|
750
|
-
"""Export service-per-row table to properly formatted markdown file."""
|
751
|
-
import os
|
752
|
-
from datetime import datetime
|
753
|
-
|
754
|
-
try:
|
755
|
-
# Prepare file path with proper directory creation
|
756
|
-
output_dir = args.dir if hasattr(args, "dir") and args.dir else "./exports"
|
757
|
-
os.makedirs(output_dir, exist_ok=True) # Ensure directory exists
|
758
|
-
report_name = args.report_name if hasattr(args, "report_name") and args.report_name else "service_analysis"
|
759
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
760
|
-
file_path = os.path.join(output_dir, f"{report_name}_{timestamp}.md")
|
761
|
-
|
762
|
-
# Generate markdown content with properly aligned pipes
|
763
|
-
lines = []
|
764
|
-
lines.append("# Service-Per-Row FinOps Analysis")
|
765
|
-
lines.append("")
|
766
|
-
# Use readable account name in markdown export
|
767
|
-
if self.account_resolver and account_id != "Unknown":
|
768
|
-
account_name = self.account_resolver.get_account_name(account_id)
|
769
|
-
if account_name and account_name != account_id:
|
770
|
-
account_line = f"**Account:** {account_name} ({account_id})"
|
771
|
-
else:
|
772
|
-
account_line = f"**Account ID:** {account_id}"
|
773
|
-
else:
|
774
|
-
account_line = f"**Profile:** {profile}"
|
775
|
-
|
776
|
-
lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
777
|
-
lines.append(account_line)
|
778
|
-
lines.append("")
|
779
|
-
lines.append("## Service Cost Breakdown")
|
780
|
-
lines.append("")
|
781
|
-
|
782
|
-
# Create GitHub-compatible markdown table with quarterly intelligence
|
783
|
-
lines.append("| Service | Current Cost | Last Month | Last Quarter | Trend | Optimization Opportunities |")
|
784
|
-
lines.append("| --- | ---: | ---: | ---: | :---: | --- |") # GitHub-compliant alignment with quarterly column
|
785
|
-
|
786
|
-
# Add TOP 10 services with quarterly context
|
787
|
-
for i, (service_name, current_cost) in enumerate(sorted_services[:10]):
|
788
|
-
previous_cost = previous_services.get(service_name, 0)
|
789
|
-
quarterly_cost = quarterly_services.get(service_name, 0)
|
790
|
-
trend_pct = ((current_cost - previous_cost) / previous_cost * 100) if previous_cost > 0 else 0
|
791
|
-
trend_icon = "⬆️" if trend_pct > 0 else "⬇️" if trend_pct < 0 else "➡️"
|
792
|
-
|
793
|
-
# Generate optimization recommendation with quarterly context
|
794
|
-
optimization = self._get_service_optimization(service_name, current_cost, previous_cost)
|
795
|
-
|
796
|
-
# Format row for GitHub-compatible table with quarterly data
|
797
|
-
service_name_clean = service_name.replace("|", "\\|") # Escape pipes in service names
|
798
|
-
optimization_clean = optimization.replace("|", "\\|") # Escape pipes in text
|
799
|
-
|
800
|
-
lines.append(
|
801
|
-
f"| {service_name_clean} | ${current_cost:.2f} | ${previous_cost:.2f} | ${quarterly_cost:.2f} | {trend_icon} {abs(trend_pct):.1f}% | {optimization_clean} |"
|
802
|
-
)
|
803
|
-
|
804
|
-
# Add Others row with quarterly context if there are remaining services
|
805
|
-
remaining_services = sorted_services[10:]
|
806
|
-
if remaining_services:
|
807
|
-
others_current = sum(current_cost for _, current_cost in remaining_services)
|
808
|
-
others_previous = sum(previous_services.get(service_name, 0) for service_name, _ in remaining_services)
|
809
|
-
others_quarterly = sum(quarterly_services.get(service_name, 0) for service_name, _ in remaining_services)
|
810
|
-
others_trend_pct = (
|
811
|
-
((others_current - others_previous) / others_previous * 100) if others_previous > 0 else 0
|
812
|
-
)
|
813
|
-
trend_icon = "⬆️" if others_trend_pct > 0 else "⬇️" if others_trend_pct < 0 else "➡️"
|
814
|
-
|
815
|
-
others_row = f"Others ({len(remaining_services)} services)"
|
816
|
-
lines.append(
|
817
|
-
f"| {others_row} | ${others_current:.2f} | ${others_previous:.2f} | ${others_quarterly:.2f} | {trend_icon} {abs(others_trend_pct):.1f}% | Review individually for optimization |"
|
818
|
-
)
|
819
|
-
|
820
|
-
lines.append("")
|
821
|
-
lines.append("## Summary")
|
822
|
-
lines.append("")
|
823
|
-
lines.append(f"- **Total Current Cost:** ${total_current:,.2f}")
|
824
|
-
lines.append(f"- **Total Previous Cost:** ${total_previous:,.2f}")
|
825
|
-
lines.append(f"- **Total Quarterly Cost:** ${total_quarterly:,.2f}")
|
826
|
-
trend_icon = "⬆️" if total_trend_pct > 0 else "⬇️" if total_trend_pct < 0 else "➡️"
|
827
|
-
lines.append(f"- **Overall Trend:** {trend_icon} {abs(total_trend_pct):.1f}%")
|
828
|
-
lines.append(f"- **Services Analyzed:** {len(sorted_services)}")
|
829
|
-
lines.append(
|
830
|
-
f"- **Optimization Focus:** {'Review highest cost services' if total_current > 100 else 'Continue monitoring'}"
|
831
|
-
)
|
832
|
-
lines.append("")
|
833
|
-
lines.append("---")
|
834
|
-
lines.append("")
|
835
|
-
lines.append("*Generated by CloudOps Runbooks FinOps Platform*")
|
836
|
-
|
837
|
-
# Write to file
|
838
|
-
with open(file_path, "w") as f:
|
839
|
-
f.write("\n".join(lines))
|
840
|
-
|
841
|
-
print_success(f"Markdown export saved to: {file_path}")
|
842
|
-
print_info("📋 Ready for GitHub/MkDocs documentation")
|
843
|
-
|
844
|
-
except Exception as e:
|
845
|
-
print_warning(f"Markdown export failed: {str(e)[:50]}")
|
846
|
-
|
847
|
-
def _get_service_optimization(self, service, current, previous):
|
848
|
-
"""Get optimization recommendation for a service."""
|
849
|
-
service_lower = service.lower()
|
850
|
-
|
851
|
-
# Generate optimization recommendations based on service type and cost
|
852
|
-
if current > 10000: # High cost services
|
853
|
-
if "rds" in service_lower or "database" in service_lower:
|
854
|
-
return "High DB costs: evaluate instance types & Reserved Instances"
|
855
|
-
elif "ec2" in service_lower:
|
856
|
-
return "Significant cost: analyze Reserved Instance opportunities"
|
857
|
-
else:
|
858
|
-
return "High cost service: detailed analysis recommended"
|
859
|
-
elif current > 1000: # Medium cost services
|
860
|
-
if "lambda" in service_lower:
|
861
|
-
return "Review memory allocation & execution time"
|
862
|
-
elif "cloudwatch" in service_lower:
|
863
|
-
return "High monitoring costs: review log retention"
|
864
|
-
elif "s3" in service_lower:
|
865
|
-
return "Review storage classes: Standard → IA/Glacier"
|
866
|
-
else:
|
867
|
-
return "Monitor usage patterns & optimization opportunities"
|
868
|
-
else: # Lower cost services
|
869
|
-
return "Continue monitoring for optimization opportunities"
|
870
|
-
|
871
|
-
def _handle_exports(self, args: argparse.Namespace, profile: str, account_id: str,
|
872
|
-
services_data, cost_data, last_month_data) -> None:
|
873
|
-
"""Handle all export formats for enhanced router."""
|
874
|
-
if not (hasattr(args, 'report_name') and args.report_name and
|
875
|
-
hasattr(args, 'report_type') and args.report_type):
|
876
|
-
return
|
877
|
-
|
878
|
-
print_info("📊 Processing export requests...")
|
879
|
-
|
880
|
-
# Convert service data to ProfileData format compatible with existing export functions
|
881
|
-
from .types import ProfileData
|
882
|
-
|
883
|
-
try:
|
884
|
-
# Create ProfileData compatible structure with dual-metric foundation
|
885
|
-
export_data = [ProfileData(
|
886
|
-
profile_name=profile,
|
887
|
-
account_id=account_id,
|
888
|
-
current_month=cost_data.get("current_month", 0), # Primary: UnblendedCost
|
889
|
-
current_month_formatted=f"${cost_data.get('current_month', 0):,.2f}",
|
890
|
-
previous_month=cost_data.get("last_month", 0), # Primary: UnblendedCost
|
891
|
-
previous_month_formatted=f"${cost_data.get('last_month', 0):,.2f}",
|
892
|
-
# Dual-metric architecture foundation (to be implemented)
|
893
|
-
current_month_amortized=None, # Secondary: AmortizedCost
|
894
|
-
previous_month_amortized=None, # Secondary: AmortizedCost
|
895
|
-
current_month_amortized_formatted=None,
|
896
|
-
previous_month_amortized_formatted=None,
|
897
|
-
metric_context="technical", # Default to technical context (UnblendedCost)
|
898
|
-
service_costs=[], # Service costs in simplified format
|
899
|
-
service_costs_formatted=[f"${cost:.2f}" for _, cost in services_data[:10]],
|
900
|
-
budget_info=[],
|
901
|
-
ec2_summary={},
|
902
|
-
ec2_summary_formatted=[],
|
903
|
-
success=True,
|
904
|
-
error=None,
|
905
|
-
current_period_name="Current Month",
|
906
|
-
previous_period_name="Previous Month",
|
907
|
-
percent_change_in_total_cost=None
|
908
|
-
)]
|
909
|
-
|
910
|
-
# Process each requested export type
|
911
|
-
export_count = 0
|
912
|
-
for report_type in args.report_type:
|
913
|
-
if report_type == "pdf":
|
914
|
-
print_info("Generating PDF export...")
|
915
|
-
pdf_path = export_cost_dashboard_to_pdf(
|
916
|
-
export_data,
|
917
|
-
args.report_name,
|
918
|
-
getattr(args, 'dir', None),
|
919
|
-
previous_period_dates="Previous Month",
|
920
|
-
current_period_dates="Current Month"
|
921
|
-
)
|
922
|
-
if pdf_path:
|
923
|
-
print_success(f"PDF export completed: {pdf_path}")
|
924
|
-
export_count += 1
|
925
|
-
else:
|
926
|
-
print_error("PDF export failed")
|
927
|
-
|
928
|
-
elif report_type == "csv":
|
929
|
-
print_info("Generating CSV export...")
|
930
|
-
from .cost_processor import export_to_csv
|
931
|
-
csv_path = export_to_csv(
|
932
|
-
export_data,
|
933
|
-
args.report_name,
|
934
|
-
getattr(args, 'dir', None),
|
935
|
-
previous_period_dates="Previous Month",
|
936
|
-
current_period_dates="Current Month"
|
937
|
-
)
|
938
|
-
if csv_path:
|
939
|
-
print_success(f"CSV export completed: {csv_path}")
|
940
|
-
export_count += 1
|
941
|
-
|
942
|
-
elif report_type == "json":
|
943
|
-
print_info("Generating JSON export...")
|
944
|
-
from .cost_processor import export_to_json
|
945
|
-
json_path = export_to_json(export_data, args.report_name, getattr(args, 'dir', None))
|
946
|
-
if json_path:
|
947
|
-
print_success(f"JSON export completed: {json_path}")
|
948
|
-
export_count += 1
|
949
|
-
|
950
|
-
elif report_type == "markdown":
|
951
|
-
print_info("Generating Markdown export...")
|
952
|
-
# Use existing markdown export functionality
|
953
|
-
self._export_service_table_to_markdown(
|
954
|
-
services_data[:10], {}, {}, # Simplified data structure
|
955
|
-
profile, account_id,
|
956
|
-
cost_data.get("current_month", 0),
|
957
|
-
cost_data.get("last_month", 0),
|
958
|
-
0, args # Simplified trend calculation
|
959
|
-
)
|
960
|
-
export_count += 1
|
961
|
-
|
962
|
-
if export_count > 0:
|
963
|
-
print_success(f"{export_count} exports completed successfully")
|
964
|
-
else:
|
965
|
-
print_warning("No exports were generated")
|
966
|
-
|
967
|
-
except Exception as e:
|
968
|
-
print_error(f"Export failed: {str(e)}")
|
969
|
-
import traceback
|
970
|
-
self.console.print(f"[red]Details: {traceback.format_exc()}[/]")
|
971
|
-
|
972
|
-
def _run_embedded_mcp_validation(self, profiles: List[str], cost_data: Dict[str, Any],
|
973
|
-
service_list: List[Tuple[str, float]], args: argparse.Namespace) -> None:
|
974
|
-
"""
|
975
|
-
Run embedded MCP cross-validation for single account dashboard with real-time AWS API comparison.
|
976
|
-
|
977
|
-
This addresses the user's critical feedback about fabricated accuracy claims by providing
|
978
|
-
genuine MCP validation with actual AWS Cost Explorer API cross-validation.
|
979
|
-
"""
|
980
|
-
try:
|
981
|
-
self.console.print(f"\n[bright_cyan]🔍 Embedded MCP Cross-Validation: Enterprise Accuracy Check[/]")
|
982
|
-
self.console.print(f"[dim]Validating single account with direct AWS API integration[/]")
|
983
|
-
|
984
|
-
# Prepare runbooks data in format expected by MCP validator
|
985
|
-
runbooks_data = {
|
986
|
-
profiles[0]: {
|
987
|
-
"total_cost": cost_data.get("current_month", 0),
|
988
|
-
"services": dict(service_list) if service_list else {},
|
989
|
-
"profile": profiles[0],
|
990
|
-
}
|
991
|
-
}
|
992
|
-
|
993
|
-
# Run embedded validation
|
994
|
-
validator = EmbeddedMCPValidator(profiles=profiles, console=self.console)
|
995
|
-
validation_results = validator.validate_cost_data(runbooks_data)
|
996
|
-
|
997
|
-
# Enhanced results display with detailed variance information (same as dashboard_runner.py)
|
998
|
-
overall_accuracy = validation_results.get("total_accuracy", 0)
|
999
|
-
profiles_validated = validation_results.get("profiles_validated", 0)
|
1000
|
-
passed = validation_results.get("passed_validation", False)
|
1001
|
-
profile_results = validation_results.get("profile_results", [])
|
1002
|
-
|
1003
|
-
self.console.print(f"\n[bright_cyan]🔍 MCP Cross-Validation Results:[/]")
|
1004
|
-
|
1005
|
-
# Display detailed per-profile results
|
1006
|
-
for profile_result in profile_results:
|
1007
|
-
profile_name = profile_result.get("profile", "Unknown")[:30]
|
1008
|
-
runbooks_cost = profile_result.get("runbooks_cost", 0)
|
1009
|
-
aws_cost = profile_result.get("aws_api_cost", 0)
|
1010
|
-
accuracy = profile_result.get("accuracy_percent", 0)
|
1011
|
-
cost_diff = profile_result.get("cost_difference", 0)
|
1012
|
-
|
1013
|
-
if profile_result.get("error"):
|
1014
|
-
self.console.print(f"├── {profile_name}: [red]❌ Error: {profile_result['error']}[/]")
|
1015
|
-
else:
|
1016
|
-
variance_pct = 100 - accuracy if accuracy > 0 else 100
|
1017
|
-
self.console.print(f"├── {profile_name}:")
|
1018
|
-
self.console.print(f"│ ├── Runbooks Cost: ${runbooks_cost:,.2f}")
|
1019
|
-
self.console.print(f"│ ├── MCP API Cost: ${aws_cost:,.2f}")
|
1020
|
-
self.console.print(f"│ ├── Variance: ${cost_diff:,.2f} ({variance_pct:.2f}%)")
|
1021
|
-
|
1022
|
-
if accuracy >= 99.5:
|
1023
|
-
self.console.print(f"│ └── Status: [green]✅ {accuracy:.2f}% accuracy[/]")
|
1024
|
-
elif accuracy >= 95.0:
|
1025
|
-
self.console.print(f"│ └── Status: [yellow]⚠️ {accuracy:.2f}% accuracy[/]")
|
1026
|
-
else:
|
1027
|
-
self.console.print(f"│ └── Status: [red]❌ {accuracy:.2f}% accuracy[/]")
|
1028
|
-
|
1029
|
-
# Overall summary
|
1030
|
-
if passed:
|
1031
|
-
self.console.print(f"└── [bright_green]✅ MCP Validation PASSED: {overall_accuracy:.2f}% overall accuracy[/]")
|
1032
|
-
self.console.print(f" [green]🏢 Enterprise compliance: {profiles_validated}/{len(profiles)} profiles validated[/]")
|
1033
|
-
else:
|
1034
|
-
self.console.print(f"└── [bright_yellow]⚠️ MCP Validation: {overall_accuracy:.2f}% overall accuracy[/]")
|
1035
|
-
self.console.print(f" [yellow]📊 Enterprise target: ≥99.5% accuracy required for compliance[/]")
|
1036
|
-
|
1037
|
-
# Save validation report
|
1038
|
-
import json
|
1039
|
-
import os
|
1040
|
-
from datetime import datetime
|
1041
|
-
|
1042
|
-
validation_file = (
|
1043
|
-
f"artifacts/validation/embedded_mcp_validation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
1044
|
-
)
|
1045
|
-
os.makedirs(os.path.dirname(validation_file), exist_ok=True)
|
1046
|
-
|
1047
|
-
with open(validation_file, "w") as f:
|
1048
|
-
json.dump(validation_results, f, indent=2, default=str)
|
1049
|
-
|
1050
|
-
self.console.print(f"[cyan]📋 Validation report saved: {validation_file}[/]")
|
1051
|
-
|
1052
|
-
except Exception as e:
|
1053
|
-
self.console.print(f"[red]❌ Embedded MCP validation failed: {str(e)[:100]}[/]")
|
1054
|
-
self.console.print(f"[dim]Continuing with standard FinOps analysis[/]")
|
1055
|
-
|
1056
|
-
def _display_console_performance_metrics(self) -> None:
|
1057
|
-
"""
|
1058
|
-
Display Phase 2 performance metrics for enterprise audit compliance.
|
1059
|
-
|
1060
|
-
Shows:
|
1061
|
-
- Progress message caching efficiency (82% target)
|
1062
|
-
- Console operation reduction achievements
|
1063
|
-
- Enterprise audit trail summary
|
1064
|
-
"""
|
1065
|
-
try:
|
1066
|
-
# Get progress tracker metrics
|
1067
|
-
audit_summary = self.progress_tracker.get_audit_summary()
|
1068
|
-
|
1069
|
-
# Create performance metrics panel
|
1070
|
-
metrics_content = f"""[dim]Progress Message Caching:[/dim]
|
1071
|
-
• Cache Efficiency: {audit_summary['cache_efficiency']:.1f}%
|
1072
|
-
• Target Achievement: {'✅ Met' if audit_summary['efficiency_achieved'] else '⚠️ Pending'} (Target: {audit_summary['target_efficiency']}%)
|
1073
|
-
• Cache Operations: {audit_summary['cache_hits']} hits, {audit_summary['cache_misses']} misses
|
1074
|
-
|
1075
|
-
[dim]Enterprise Audit Compliance:[/dim]
|
1076
|
-
• Session ID: {audit_summary['session_id']}
|
1077
|
-
• Total Operations: {audit_summary['total_operations']}
|
1078
|
-
• Audit Trail Length: {audit_summary['audit_trail_count']}
|
1079
|
-
|
1080
|
-
[dim]Phase 2 Achievements:[/dim]
|
1081
|
-
• Message caching system operational
|
1082
|
-
• Business context enhancement integrated
|
1083
|
-
• Enterprise audit trail generation active
|
1084
|
-
• Performance targets tracking enabled"""
|
1085
|
-
|
1086
|
-
metrics_panel = Panel(
|
1087
|
-
metrics_content,
|
1088
|
-
title="[bold cyan]📊 Phase 2 Performance Metrics[/bold cyan]",
|
1089
|
-
border_style="cyan",
|
1090
|
-
padding=(1, 2)
|
1091
|
-
)
|
1092
|
-
|
1093
|
-
self.console.print(f"\n{metrics_panel}")
|
1094
|
-
|
1095
|
-
# Log metrics for enterprise reporting
|
1096
|
-
metrics_details = (
|
1097
|
-
f"Cache efficiency: {audit_summary['cache_efficiency']:.1f}%, "
|
1098
|
-
f"Target achieved: {audit_summary['efficiency_achieved']}, "
|
1099
|
-
f"Session operations: {audit_summary['total_operations']}"
|
1100
|
-
)
|
1101
|
-
self.context_logger.info(
|
1102
|
-
"Phase 2 performance metrics displayed",
|
1103
|
-
technical_detail=metrics_details
|
1104
|
-
)
|
1105
|
-
|
1106
|
-
except Exception as e:
|
1107
|
-
# Graceful degradation - don't fail the main dashboard
|
1108
|
-
print_warning(f"Phase 2 metrics display failed: {str(e)[:50]}")
|
1109
|
-
|
1110
|
-
|
1111
|
-
def create_single_dashboard(console: Optional[Console] = None) -> SingleAccountDashboard:
|
1112
|
-
"""Factory function to create single account dashboard."""
|
1113
|
-
return SingleAccountDashboard(console=console)
|