runbooks 0.7.9__py3-none-any.whl → 0.9.1__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.
Files changed (122) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/README.md +12 -1
  3. runbooks/cfat/__init__.py +1 -1
  4. runbooks/cfat/assessment/compliance.py +4 -1
  5. runbooks/cfat/assessment/runner.py +42 -34
  6. runbooks/cfat/models.py +1 -1
  7. runbooks/cloudops/__init__.py +123 -0
  8. runbooks/cloudops/base.py +385 -0
  9. runbooks/cloudops/cost_optimizer.py +811 -0
  10. runbooks/cloudops/infrastructure_optimizer.py +29 -0
  11. runbooks/cloudops/interfaces.py +828 -0
  12. runbooks/cloudops/lifecycle_manager.py +29 -0
  13. runbooks/cloudops/mcp_cost_validation.py +678 -0
  14. runbooks/cloudops/models.py +251 -0
  15. runbooks/cloudops/monitoring_automation.py +29 -0
  16. runbooks/cloudops/notebook_framework.py +676 -0
  17. runbooks/cloudops/security_enforcer.py +449 -0
  18. runbooks/common/__init__.py +152 -0
  19. runbooks/common/accuracy_validator.py +1039 -0
  20. runbooks/common/context_logger.py +440 -0
  21. runbooks/common/cross_module_integration.py +594 -0
  22. runbooks/common/enhanced_exception_handler.py +1108 -0
  23. runbooks/common/enterprise_audit_integration.py +634 -0
  24. runbooks/common/mcp_cost_explorer_integration.py +900 -0
  25. runbooks/common/mcp_integration.py +548 -0
  26. runbooks/common/performance_monitor.py +387 -0
  27. runbooks/common/profile_utils.py +216 -0
  28. runbooks/common/rich_utils.py +172 -1
  29. runbooks/feedback/user_feedback_collector.py +440 -0
  30. runbooks/finops/README.md +377 -458
  31. runbooks/finops/__init__.py +4 -21
  32. runbooks/finops/account_resolver.py +279 -0
  33. runbooks/finops/accuracy_cross_validator.py +638 -0
  34. runbooks/finops/aws_client.py +721 -36
  35. runbooks/finops/budget_integration.py +313 -0
  36. runbooks/finops/cli.py +59 -5
  37. runbooks/finops/cost_optimizer.py +1340 -0
  38. runbooks/finops/cost_processor.py +211 -37
  39. runbooks/finops/dashboard_router.py +900 -0
  40. runbooks/finops/dashboard_runner.py +990 -232
  41. runbooks/finops/embedded_mcp_validator.py +288 -0
  42. runbooks/finops/enhanced_dashboard_runner.py +8 -7
  43. runbooks/finops/enhanced_progress.py +327 -0
  44. runbooks/finops/enhanced_trend_visualization.py +423 -0
  45. runbooks/finops/finops_dashboard.py +184 -1829
  46. runbooks/finops/helpers.py +509 -196
  47. runbooks/finops/iam_guidance.py +400 -0
  48. runbooks/finops/markdown_exporter.py +466 -0
  49. runbooks/finops/multi_dashboard.py +1502 -0
  50. runbooks/finops/optimizer.py +15 -15
  51. runbooks/finops/profile_processor.py +2 -2
  52. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  53. runbooks/finops/runbooks.security.report_generator.log +0 -0
  54. runbooks/finops/runbooks.security.run_script.log +0 -0
  55. runbooks/finops/runbooks.security.security_export.log +0 -0
  56. runbooks/finops/schemas.py +589 -0
  57. runbooks/finops/service_mapping.py +195 -0
  58. runbooks/finops/single_dashboard.py +710 -0
  59. runbooks/finops/tests/test_reference_images_validation.py +1 -1
  60. runbooks/inventory/README.md +12 -1
  61. runbooks/inventory/core/collector.py +157 -29
  62. runbooks/inventory/list_ec2_instances.py +9 -6
  63. runbooks/inventory/list_ssm_parameters.py +10 -10
  64. runbooks/inventory/organizations_discovery.py +210 -164
  65. runbooks/inventory/rich_inventory_display.py +74 -107
  66. runbooks/inventory/run_on_multi_accounts.py +13 -13
  67. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  68. runbooks/inventory/runbooks.security.security_export.log +0 -0
  69. runbooks/main.py +1371 -240
  70. runbooks/metrics/dora_metrics_engine.py +711 -17
  71. runbooks/monitoring/performance_monitor.py +433 -0
  72. runbooks/operate/README.md +394 -0
  73. runbooks/operate/base.py +215 -47
  74. runbooks/operate/ec2_operations.py +435 -5
  75. runbooks/operate/iam_operations.py +598 -3
  76. runbooks/operate/privatelink_operations.py +1 -1
  77. runbooks/operate/rds_operations.py +508 -0
  78. runbooks/operate/s3_operations.py +508 -0
  79. runbooks/operate/vpc_endpoints.py +1 -1
  80. runbooks/remediation/README.md +489 -13
  81. runbooks/remediation/base.py +5 -3
  82. runbooks/remediation/commons.py +8 -4
  83. runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
  84. runbooks/security/README.md +12 -1
  85. runbooks/security/__init__.py +265 -33
  86. runbooks/security/cloudops_automation_security_validator.py +1164 -0
  87. runbooks/security/compliance_automation.py +12 -10
  88. runbooks/security/compliance_automation_engine.py +1021 -0
  89. runbooks/security/enterprise_security_framework.py +930 -0
  90. runbooks/security/enterprise_security_policies.json +293 -0
  91. runbooks/security/executive_security_dashboard.py +1247 -0
  92. runbooks/security/integration_test_enterprise_security.py +879 -0
  93. runbooks/security/module_security_integrator.py +641 -0
  94. runbooks/security/multi_account_security_controls.py +2254 -0
  95. runbooks/security/real_time_security_monitor.py +1196 -0
  96. runbooks/security/report_generator.py +1 -1
  97. runbooks/security/run_script.py +4 -8
  98. runbooks/security/security_baseline_tester.py +39 -52
  99. runbooks/security/security_export.py +99 -120
  100. runbooks/sre/README.md +472 -0
  101. runbooks/sre/__init__.py +33 -0
  102. runbooks/sre/mcp_reliability_engine.py +1049 -0
  103. runbooks/sre/performance_optimization_engine.py +1032 -0
  104. runbooks/sre/production_monitoring_framework.py +584 -0
  105. runbooks/sre/reliability_monitoring_framework.py +1011 -0
  106. runbooks/validation/__init__.py +2 -2
  107. runbooks/validation/benchmark.py +154 -149
  108. runbooks/validation/cli.py +159 -147
  109. runbooks/validation/mcp_validator.py +291 -248
  110. runbooks/vpc/README.md +478 -0
  111. runbooks/vpc/__init__.py +2 -2
  112. runbooks/vpc/manager_interface.py +366 -351
  113. runbooks/vpc/networking_wrapper.py +68 -36
  114. runbooks/vpc/rich_formatters.py +22 -8
  115. runbooks-0.9.1.dist-info/METADATA +308 -0
  116. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/RECORD +120 -59
  117. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/entry_points.txt +1 -1
  118. runbooks/finops/cross_validation.py +0 -375
  119. runbooks-0.7.9.dist-info/METADATA +0 -636
  120. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/WHEEL +0 -0
  121. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/licenses/LICENSE +0 -0
  122. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,710 @@
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 .dashboard_runner import (
59
+ _create_cost_session,
60
+ _create_management_session,
61
+ _create_operational_session,
62
+ )
63
+ from .enhanced_progress import EnhancedProgressTracker
64
+ from .helpers import export_cost_dashboard_to_pdf
65
+ from .service_mapping import get_service_display_name
66
+
67
+
68
+ class SingleAccountDashboard:
69
+ """
70
+ Service-focused dashboard for single AWS account cost analysis.
71
+
72
+ Optimized for technical users who need:
73
+ - Detailed service-level cost breakdown
74
+ - Service utilization patterns
75
+ - Optimization recommendations per service
76
+ - Trend analysis for cost management
77
+ """
78
+
79
+ def __init__(self, console: Optional[Console] = None):
80
+ self.console = console or rich_console
81
+ self.context_logger = create_context_logger("finops.single_dashboard")
82
+ self.context_console = get_context_console()
83
+ self.progress_tracker = EnhancedProgressTracker(self.console)
84
+ self.budget_analyzer = EnhancedBudgetAnalyzer(self.console)
85
+ self.account_resolver = None # Will be initialized with management profile
86
+
87
+ def run_dashboard(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
88
+ """
89
+ Main entry point for single account service-focused dashboard.
90
+
91
+ Args:
92
+ args: Command line arguments
93
+ config: Routing configuration from dashboard router
94
+
95
+ Returns:
96
+ int: Exit code (0 for success, 1 for failure)
97
+ """
98
+ try:
99
+ print_header("Single Account Service Dashboard", "0.8.0")
100
+
101
+ # Configuration display (context-aware)
102
+ top_services = getattr(args, "top_services", 10)
103
+
104
+ self.context_logger.info(
105
+ f"Service-focused analysis configured for TOP {top_services} services",
106
+ technical_detail="Optimizing for service-level insights for technical teams",
107
+ )
108
+
109
+ # Show detailed configuration only for CLI users
110
+ if self.context_console.config.show_technical_details:
111
+ self.console.print(f"[info]🎯 Analysis Focus:[/] [highlight]TOP {top_services} Services[/]")
112
+ self.console.print(f"[dim]• Optimization Target: Service-level insights[/]")
113
+ self.console.print(f"[dim]• User Profile: Technical teams[/]\n")
114
+
115
+ # Get profile for analysis
116
+ profile = self._determine_analysis_profile(args)
117
+
118
+ # Validate profile access
119
+ if not self._validate_profile_access(profile):
120
+ return 1
121
+
122
+ # Run service-focused analysis
123
+ return self._execute_service_analysis(profile, args, top_services)
124
+
125
+ except Exception as e:
126
+ print_error(f"Single account dashboard failed: {str(e)}")
127
+ return 1
128
+
129
+ def _determine_analysis_profile(self, args: argparse.Namespace) -> str:
130
+ """Determine which profile to use for analysis."""
131
+ if hasattr(args, "profile") and args.profile and args.profile != "default":
132
+ return args.profile
133
+ elif hasattr(args, "profiles") and args.profiles:
134
+ return args.profiles[0] # Use first profile
135
+ else:
136
+ return "default"
137
+
138
+ def _validate_profile_access(self, profile: str) -> bool:
139
+ """Validate that the profile has necessary access."""
140
+ try:
141
+ # Test basic access
142
+ session = boto3.Session(profile_name=profile)
143
+ sts = session.client("sts")
144
+ identity = sts.get_caller_identity()
145
+
146
+ account_id = identity["Account"]
147
+ print_success(f"Profile validation successful: {profile} -> {account_id}")
148
+ return True
149
+
150
+ except Exception as e:
151
+ print_error(f"Profile validation failed: {str(e)}")
152
+ return False
153
+
154
+ def _execute_service_analysis(self, profile: str, args: argparse.Namespace, top_services: int) -> int:
155
+ """Execute the service-focused cost analysis."""
156
+ try:
157
+ # Initialize sessions
158
+ cost_session = _create_cost_session(profile)
159
+ mgmt_session = _create_management_session(profile)
160
+ ops_session = _create_operational_session(profile)
161
+
162
+ # Initialize account resolver for readable account names
163
+ management_profile = os.getenv("MANAGEMENT_PROFILE") or profile
164
+ self.account_resolver = get_account_resolver(management_profile)
165
+
166
+ # Get basic account information
167
+ account_id = get_account_id(mgmt_session) or "Unknown"
168
+
169
+ with self.progress_tracker.create_enhanced_progress("service_analysis", 100) as progress:
170
+ # Phase 1: Cost data collection (0-30%)
171
+ progress.start_operation("Initializing service analysis...")
172
+
173
+ try:
174
+ progress.update_step("Collecting current cost data...", 15)
175
+ cost_data = get_cost_data(
176
+ cost_session,
177
+ getattr(args, "time_range", None),
178
+ getattr(args, "tag", None),
179
+ profile_name=profile,
180
+ )
181
+
182
+ progress.update_step("Processing service cost breakdown...", 25)
183
+ # Get enhanced cost breakdown
184
+ service_costs, service_cost_data = process_service_costs(cost_data)
185
+
186
+ progress.update_step("Analyzing cost trends...", 35)
187
+ # Get last month data for trend analysis
188
+ last_month_data = self._get_last_month_trends(cost_session, profile)
189
+
190
+ except Exception as e:
191
+ print_warning(f"Cost data collection failed: {str(e)[:50]}")
192
+ progress.update_step("Using fallback data due to API issues...", 30)
193
+ # Continue with limited data
194
+ cost_data = {"current_month": 0, "last_month": 0, "costs_by_service": {}}
195
+ service_costs = []
196
+ last_month_data = {}
197
+
198
+ # Phase 2: Enhanced budget analysis (40-70%)
199
+ try:
200
+ progress.update_step("Collecting budget information...", 45)
201
+ budget_data = get_budgets(cost_session)
202
+
203
+ progress.update_step("Analyzing service utilization patterns...", 60)
204
+ # Service utilization analysis
205
+ utilization_data = self._analyze_service_utilization(ops_session, cost_data)
206
+
207
+ progress.update_step("Generating optimization recommendations...", 75)
208
+ # Simulate processing time for optimization analysis
209
+ import time
210
+
211
+ time.sleep(0.5) # Brief processing simulation for smooth progress
212
+
213
+ except Exception as e:
214
+ print_warning(f"Budget/utilization analysis failed: {str(e)[:50]}")
215
+ progress.update_step("Using basic analysis due to API limitations...", 65)
216
+ budget_data = []
217
+ utilization_data = {}
218
+
219
+ # Phase 3: Table generation and formatting (80-100%)
220
+ progress.update_step("Preparing service-focused table...", 85)
221
+ # Brief pause for table preparation
222
+ import time
223
+
224
+ time.sleep(0.3)
225
+
226
+ progress.update_step("Formatting optimization recommendations...", 95)
227
+ # Final formatting step
228
+
229
+ progress.complete_operation("Service analysis completed successfully")
230
+
231
+ # Create and display the service-focused table
232
+ self._display_service_focused_table(
233
+ account_id=account_id,
234
+ profile=profile,
235
+ cost_data=cost_data,
236
+ service_costs=service_costs,
237
+ last_month_data=last_month_data,
238
+ budget_data=budget_data,
239
+ utilization_data=utilization_data,
240
+ top_services=top_services,
241
+ )
242
+
243
+ # Export if requested
244
+ if hasattr(args, "report_name") and args.report_name:
245
+ self._export_service_analysis(args, cost_data, service_costs, account_id)
246
+
247
+ # Export to markdown if requested
248
+ should_export_markdown = False
249
+
250
+ # Check if markdown export was requested via --export-markdown flag
251
+ if hasattr(args, "export_markdown") and getattr(args, "export_markdown", False):
252
+ should_export_markdown = True
253
+
254
+ # Check if markdown export was requested via --report-type markdown
255
+ if hasattr(args, "report_type") and args.report_type:
256
+ if isinstance(args.report_type, list) and "markdown" in args.report_type:
257
+ should_export_markdown = True
258
+ elif isinstance(args.report_type, str) and "markdown" in args.report_type:
259
+ should_export_markdown = True
260
+
261
+ if should_export_markdown:
262
+ # Prepare service data for markdown export with Tax filtering
263
+ current_services = cost_data.get("costs_by_service", {})
264
+ previous_services = last_month_data.get("costs_by_service", {}) # Use already collected data
265
+
266
+ # Apply same Tax filtering for consistent markdown export
267
+ filtered_current_services = filter_analytical_services(current_services)
268
+ filtered_previous_services = filter_analytical_services(previous_services)
269
+
270
+ all_services_sorted = sorted(filtered_current_services.items(), key=lambda x: x[1], reverse=True)
271
+
272
+ # Calculate totals for markdown export
273
+ total_current = cost_data.get("current_month", 0)
274
+ total_previous = cost_data.get("last_month", 0)
275
+ total_trend_pct = ((total_current - total_previous) / total_previous * 100) if total_previous > 0 else 0
276
+
277
+ self._export_service_table_to_markdown(
278
+ all_services_sorted,
279
+ filtered_current_services,
280
+ filtered_previous_services,
281
+ profile,
282
+ account_id,
283
+ total_current,
284
+ total_previous,
285
+ total_trend_pct,
286
+ args,
287
+ )
288
+
289
+ print_success(f"Service analysis completed for account {account_id}")
290
+ return 0
291
+
292
+ except Exception as e:
293
+ print_error(f"Service analysis execution failed: {str(e)}")
294
+ return 1
295
+
296
+ def _get_last_month_trends(self, cost_session: boto3.Session, profile: str) -> Dict[str, Any]:
297
+ """Get last month cost data for trend analysis."""
298
+ try:
299
+ # Get cost data for previous month
300
+ previous_month_data = get_cost_data(cost_session, 60, None, profile_name=profile) # 60 days for comparison
301
+ return previous_month_data
302
+ except Exception as e:
303
+ print_warning(f"Trend data collection failed: {str(e)[:30]}")
304
+ return {}
305
+
306
+ def _analyze_service_utilization(self, ops_session: boto3.Session, cost_data: Dict[str, Any]) -> Dict[str, Any]:
307
+ """Analyze service utilization patterns for optimization opportunities."""
308
+ utilization_data = {}
309
+
310
+ try:
311
+ # Basic service utilization patterns (can be expanded)
312
+ services_with_costs = cost_data.get("costs_by_service", {})
313
+
314
+ for service, cost in services_with_costs.items():
315
+ utilization_data[service] = {
316
+ "cost": cost,
317
+ "optimization_potential": "Medium", # Placeholder - can be enhanced
318
+ "utilization_score": 75, # Placeholder - can be enhanced with CloudWatch
319
+ "recommendation": self._get_service_recommendation(service, cost),
320
+ }
321
+
322
+ except Exception as e:
323
+ print_warning(f"Utilization analysis failed: {str(e)[:30]}")
324
+
325
+ return utilization_data
326
+
327
+ def _get_service_recommendation(self, service: str, cost: float) -> str:
328
+ """Get optimization recommendation for a service based on cost patterns."""
329
+ if cost == 0:
330
+ return "No usage detected"
331
+ elif "ec2" in service.lower():
332
+ return "Review instance sizing"
333
+ elif "s3" in service.lower():
334
+ return "Check storage classes"
335
+ elif "rds" in service.lower():
336
+ return "Evaluate instance types"
337
+ else:
338
+ return "Monitor usage patterns"
339
+
340
+ def _get_enhanced_service_recommendation(self, service: str, current_cost: float, previous_cost: float) -> str:
341
+ """Get enhanced service-specific optimization recommendations with trend awareness."""
342
+ if current_cost == 0:
343
+ return "[dim]No current usage - consider resource cleanup[/]"
344
+
345
+ # Calculate cost trend for context-aware recommendations
346
+ trend_factor = 1.0
347
+ if previous_cost > 0:
348
+ trend_factor = current_cost / previous_cost
349
+
350
+ service_lower = service.lower()
351
+
352
+ if "ec2" in service_lower or "compute" in service_lower:
353
+ if trend_factor > 1.2:
354
+ return "[red]High growth: review scaling policies & rightsizing[/]"
355
+ elif current_cost > 1000:
356
+ return "[yellow]Significant cost: analyze Reserved Instance opportunities[/]"
357
+ else:
358
+ return "[green]Monitor CPU utilization & consider spot instances[/]"
359
+
360
+ elif "s3" in service_lower or "storage" in service_lower:
361
+ if trend_factor > 1.3:
362
+ return "[red]Storage growth: implement lifecycle policies[/]"
363
+ elif current_cost > 500:
364
+ return "[yellow]Review storage classes: Standard → IA/Glacier[/]"
365
+ else:
366
+ return "[green]Optimize object lifecycle & access patterns[/]"
367
+
368
+ elif "rds" in service_lower or "database" in service_lower:
369
+ if current_cost > 1500:
370
+ return "[yellow]High DB costs: evaluate instance types & Reserved[/]"
371
+ else:
372
+ return "[green]Monitor connections & consider read replicas[/]"
373
+
374
+ elif "lambda" in service_lower or "serverless" in service_lower:
375
+ if trend_factor > 1.5:
376
+ return "[red]Function invocations increasing: optimize runtime[/]"
377
+ else:
378
+ return "[green]Review memory allocation & execution time[/]"
379
+
380
+ elif "glue" in service_lower:
381
+ if current_cost > 75:
382
+ return "[yellow]Review job frequency & data processing efficiency[/]"
383
+ else:
384
+ return "[green]Monitor ETL job performance & scheduling[/]"
385
+
386
+ elif "tax" in service_lower:
387
+ return "[dim]Regulatory requirement - no optimization available[/]"
388
+
389
+ elif "cloudwatch" in service_lower or "monitoring" in service_lower:
390
+ if current_cost > 100:
391
+ return "[yellow]High monitoring costs: review log retention[/]"
392
+ else:
393
+ return "[green]Optimize custom metrics & log groups[/]"
394
+
395
+ elif "nat" in service_lower or "gateway" in service_lower:
396
+ if current_cost > 200:
397
+ return "[yellow]High NAT costs: consider VPC endpoints[/]"
398
+ else:
399
+ return "[green]Monitor data transfer patterns[/]"
400
+
401
+ else:
402
+ # Generic recommendations based on cost level
403
+ if current_cost > 1000:
404
+ return f"[yellow]High cost service: detailed analysis recommended[/]"
405
+ elif trend_factor > 1.3:
406
+ return f"[red]Growing cost: investigate usage increase[/]"
407
+ else:
408
+ return f"[green]Monitor usage patterns & optimization opportunities[/]"
409
+
410
+ def _display_service_focused_table(
411
+ self,
412
+ account_id: str,
413
+ profile: str,
414
+ cost_data: Dict[str, Any],
415
+ service_costs: List[str],
416
+ last_month_data: Dict[str, Any],
417
+ budget_data: List[Dict[str, Any]],
418
+ utilization_data: Dict[str, Any],
419
+ top_services: int,
420
+ ) -> None:
421
+ """Display the service-focused analysis table."""
422
+
423
+ # Create enhanced table for service analysis (service-per-row layout)
424
+ # Get readable account name for display
425
+ if self.account_resolver and account_id != "Unknown":
426
+ account_name = self.account_resolver.get_account_name(account_id)
427
+ if account_name and account_name != account_id:
428
+ account_display = f"{account_name} ({account_id})"
429
+ account_caption = f"Account: {account_name}"
430
+ else:
431
+ account_display = account_id
432
+ account_caption = f"Account ID: {account_id}"
433
+ else:
434
+ account_display = account_id
435
+ account_caption = f"Profile: {profile}"
436
+
437
+ table = Table(
438
+ Column("Service", style="resource", width=20),
439
+ Column("Current Cost", justify="right", style="cost", width=15),
440
+ Column("Last Month", justify="right", width=15),
441
+ Column("Trend", justify="center", width=10),
442
+ Column("Optimization Opportunities", width=35),
443
+ title=f"🎯 TOP {top_services} Services Analysis - {account_display}",
444
+ box=box.ROUNDED,
445
+ show_lines=True,
446
+ style="bright_cyan",
447
+ caption=f"[dim]Service-focused analysis • {account_caption} • Each row represents one service[/]",
448
+ )
449
+
450
+ # Get current and previous service costs
451
+ current_services = cost_data.get("costs_by_service", {})
452
+ previous_services = last_month_data.get("costs_by_service", {})
453
+
454
+ # WIP.md requirement: Exclude "Tax" service as it provides no analytical insights
455
+ # Use centralized filtering function for consistency across all dashboards
456
+ filtered_current_services = filter_analytical_services(current_services)
457
+ filtered_previous_services = filter_analytical_services(previous_services)
458
+
459
+ # Sort services by current cost and take top N, plus "Other Services" summary
460
+ all_services = sorted(filtered_current_services.items(), key=lambda x: x[1], reverse=True)
461
+ top_services_list = all_services[:top_services]
462
+ remaining_services = all_services[top_services:]
463
+
464
+ # Add individual service rows
465
+ for service, current_cost in top_services_list:
466
+ previous_cost = filtered_previous_services.get(service, 0)
467
+
468
+ # Calculate trend
469
+ if previous_cost > 0:
470
+ trend_percent = ((current_cost - previous_cost) / previous_cost) * 100
471
+ if trend_percent > 5:
472
+ trend_display = f"[red]⬆ {trend_percent:.1f}%[/]"
473
+ elif trend_percent < -5:
474
+ trend_display = f"[green]⬇ {abs(trend_percent):.1f}%[/]"
475
+ else:
476
+ trend_display = f"[yellow]➡ {trend_percent:.1f}%[/]"
477
+ else:
478
+ trend_display = "[dim]New[/]"
479
+
480
+ # Enhanced service-specific optimization recommendations
481
+ optimization_rec = self._get_enhanced_service_recommendation(service, current_cost, previous_cost)
482
+
483
+ # Use standardized service name mapping (RDS, S3, CloudWatch, etc.)
484
+ display_name = get_service_display_name(service)
485
+
486
+ table.add_row(
487
+ display_name, format_cost(current_cost), format_cost(previous_cost), trend_display, optimization_rec
488
+ )
489
+
490
+ # Add "Other Services" summary row if there are remaining services
491
+ if remaining_services:
492
+ other_current = sum(cost for _, cost in remaining_services)
493
+ other_previous = sum(filtered_previous_services.get(service, 0) for service, _ in remaining_services)
494
+
495
+ if other_previous > 0:
496
+ other_trend_percent = ((other_current - other_previous) / other_previous) * 100
497
+ if other_trend_percent > 5:
498
+ other_trend = f"[red]⬆ {other_trend_percent:.1f}%[/]"
499
+ elif other_trend_percent < -5:
500
+ other_trend = f"[green]⬇ {abs(other_trend_percent):.1f}%[/]"
501
+ else:
502
+ other_trend = f"[yellow]➡ {other_trend_percent:.1f}%[/]"
503
+ else:
504
+ other_trend = "[dim]Various[/]"
505
+
506
+ other_optimization = (
507
+ f"[dim]{len(remaining_services)} services: review individually for optimization opportunities[/]"
508
+ )
509
+
510
+ # Add separator line for "Other Services"
511
+ table.add_row(
512
+ "[dim]Other Services[/]",
513
+ format_cost(other_current),
514
+ format_cost(other_previous),
515
+ other_trend,
516
+ other_optimization,
517
+ style="dim",
518
+ )
519
+
520
+ self.console.print(table)
521
+
522
+ # Summary panel (using filtered services for consistent analysis)
523
+ total_current = sum(filtered_current_services.values())
524
+ total_previous = sum(filtered_previous_services.values())
525
+ total_trend = ((total_current - total_previous) / total_previous * 100) if total_previous > 0 else 0
526
+
527
+ # Use readable account name in summary
528
+ if self.account_resolver and account_id != "Unknown":
529
+ account_name = self.account_resolver.get_account_name(account_id)
530
+ if account_name and account_name != account_id:
531
+ account_summary_line = f"• Account: {account_name} ({account_id})"
532
+ else:
533
+ account_summary_line = f"• Account ID: {account_id}"
534
+ else:
535
+ account_summary_line = f"• Profile: {profile}"
536
+
537
+ summary_text = f"""
538
+ [highlight]Account Summary[/]
539
+ {account_summary_line}
540
+ • Total Current: {format_cost(total_current)}
541
+ • Total Previous: {format_cost(total_previous)}
542
+ • Overall Trend: {"⬆" if total_trend > 0 else "⬇"} {abs(total_trend):.1f}%
543
+ • Services Analyzed: {len(all_services)}
544
+ """
545
+
546
+ self.console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
547
+
548
+ def _export_service_analysis(
549
+ self, args: argparse.Namespace, cost_data: Dict[str, Any], service_costs: List[str], account_id: str
550
+ ) -> None:
551
+ """Export service analysis results."""
552
+ try:
553
+ if hasattr(args, "report_type") and args.report_type:
554
+ export_data = [
555
+ {
556
+ "account_id": account_id,
557
+ "service_costs": cost_data.get("costs_by_service", {}),
558
+ "total_current": cost_data.get("current_month", 0),
559
+ "total_previous": cost_data.get("last_month", 0),
560
+ "analysis_type": "service_focused",
561
+ }
562
+ ]
563
+
564
+ for report_type in args.report_type:
565
+ if report_type == "json":
566
+ json_path = export_to_json(export_data, args.report_name, getattr(args, "dir", None))
567
+ if json_path:
568
+ print_success(f"Service analysis exported to JSON: {json_path}")
569
+ elif report_type == "csv":
570
+ csv_path = export_to_csv(export_data, args.report_name, getattr(args, "dir", None))
571
+ if csv_path:
572
+ print_success(f"Service analysis exported to CSV: {csv_path}")
573
+
574
+ except Exception as e:
575
+ print_warning(f"Export failed: {str(e)[:50]}")
576
+
577
+ def _export_service_table_to_markdown(
578
+ self,
579
+ sorted_services,
580
+ current_services,
581
+ previous_services,
582
+ profile,
583
+ account_id,
584
+ total_current,
585
+ total_previous,
586
+ total_trend_pct,
587
+ args,
588
+ ):
589
+ """Export service-per-row table to properly formatted markdown file."""
590
+ import os
591
+ from datetime import datetime
592
+
593
+ try:
594
+ # Prepare file path with proper directory creation
595
+ output_dir = args.dir if hasattr(args, "dir") and args.dir else "./exports"
596
+ os.makedirs(output_dir, exist_ok=True) # Ensure directory exists
597
+ report_name = args.report_name if hasattr(args, "report_name") and args.report_name else "service_analysis"
598
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
599
+ file_path = os.path.join(output_dir, f"{report_name}_{timestamp}.md")
600
+
601
+ # Generate markdown content with properly aligned pipes
602
+ lines = []
603
+ lines.append("# Service-Per-Row FinOps Analysis")
604
+ lines.append("")
605
+ # Use readable account name in markdown export
606
+ if self.account_resolver and account_id != "Unknown":
607
+ account_name = self.account_resolver.get_account_name(account_id)
608
+ if account_name and account_name != account_id:
609
+ account_line = f"**Account:** {account_name} ({account_id})"
610
+ else:
611
+ account_line = f"**Account ID:** {account_id}"
612
+ else:
613
+ account_line = f"**Profile:** {profile}"
614
+
615
+ lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
616
+ lines.append(account_line)
617
+ lines.append("")
618
+ lines.append("## Service Cost Breakdown")
619
+ lines.append("")
620
+
621
+ # Create GitHub-compatible markdown table with proper alignment syntax
622
+ lines.append("| Service | Last Month | Current Month | Trend | Optimization Opportunities |")
623
+ lines.append("| --- | ---: | ---: | :---: | --- |") # GitHub-compliant alignment
624
+
625
+ # Add TOP 10 services with proper formatting
626
+ for i, (service_name, current_cost) in enumerate(sorted_services[:10]):
627
+ previous_cost = previous_services.get(service_name, 0)
628
+ trend_pct = ((current_cost - previous_cost) / previous_cost * 100) if previous_cost > 0 else 0
629
+ trend_icon = "⬆️" if trend_pct > 0 else "⬇️" if trend_pct < 0 else "➡️"
630
+
631
+ # Generate optimization recommendation
632
+ optimization = self._get_service_optimization(service_name, current_cost, previous_cost)
633
+
634
+ # Format row for GitHub-compatible table
635
+ service_name_clean = service_name.replace("|", "\\|") # Escape pipes in service names
636
+ optimization_clean = optimization.replace("|", "\\|") # Escape pipes in text
637
+
638
+ lines.append(
639
+ f"| {service_name_clean} | ${previous_cost:.2f} | ${current_cost:.2f} | {trend_icon} {abs(trend_pct):.1f}% | {optimization_clean} |"
640
+ )
641
+
642
+ # Add Others row if there are remaining services
643
+ remaining_services = sorted_services[10:]
644
+ if remaining_services:
645
+ others_current = sum(current_cost for _, current_cost in remaining_services)
646
+ others_previous = sum(previous_services.get(service_name, 0) for service_name, _ in remaining_services)
647
+ others_trend_pct = (
648
+ ((others_current - others_previous) / others_previous * 100) if others_previous > 0 else 0
649
+ )
650
+ trend_icon = "⬆️" if others_trend_pct > 0 else "⬇️" if others_trend_pct < 0 else "➡️"
651
+
652
+ others_row = f"Others ({len(remaining_services)} services)"
653
+ lines.append(
654
+ f"| {others_row} | ${others_previous:.2f} | ${others_current:.2f} | {trend_icon} {abs(others_trend_pct):.1f}% | Review individually for optimization |"
655
+ )
656
+
657
+ lines.append("")
658
+ lines.append("## Summary")
659
+ lines.append("")
660
+ lines.append(f"- **Total Current Cost:** ${total_current:,.2f}")
661
+ lines.append(f"- **Total Previous Cost:** ${total_previous:,.2f}")
662
+ trend_icon = "⬆️" if total_trend_pct > 0 else "⬇️" if total_trend_pct < 0 else "➡️"
663
+ lines.append(f"- **Overall Trend:** {trend_icon} {abs(total_trend_pct):.1f}%")
664
+ lines.append(f"- **Services Analyzed:** {len(sorted_services)}")
665
+ lines.append(
666
+ f"- **Optimization Focus:** {'Review highest cost services' if total_current > 100 else 'Continue monitoring'}"
667
+ )
668
+ lines.append("")
669
+ lines.append("---")
670
+ lines.append("")
671
+ lines.append("*Generated by CloudOps Runbooks FinOps Platform*")
672
+
673
+ # Write to file
674
+ with open(file_path, "w") as f:
675
+ f.write("\n".join(lines))
676
+
677
+ print_success(f"Markdown export saved to: {file_path}")
678
+ self.console.print("[cyan]📋 Ready for GitHub/MkDocs documentation[/]")
679
+
680
+ except Exception as e:
681
+ print_warning(f"Markdown export failed: {str(e)[:50]}")
682
+
683
+ def _get_service_optimization(self, service, current, previous):
684
+ """Get optimization recommendation for a service."""
685
+ service_lower = service.lower()
686
+
687
+ # Generate optimization recommendations based on service type and cost
688
+ if current > 10000: # High cost services
689
+ if "rds" in service_lower or "database" in service_lower:
690
+ return "High DB costs: evaluate instance types & Reserved Instances"
691
+ elif "ec2" in service_lower:
692
+ return "Significant cost: analyze Reserved Instance opportunities"
693
+ else:
694
+ return "High cost service: detailed analysis recommended"
695
+ elif current > 1000: # Medium cost services
696
+ if "lambda" in service_lower:
697
+ return "Review memory allocation & execution time"
698
+ elif "cloudwatch" in service_lower:
699
+ return "High monitoring costs: review log retention"
700
+ elif "s3" in service_lower:
701
+ return "Review storage classes: Standard → IA/Glacier"
702
+ else:
703
+ return "Monitor usage patterns & optimization opportunities"
704
+ else: # Lower cost services
705
+ return "Continue monitoring for optimization opportunities"
706
+
707
+
708
+ def create_single_dashboard(console: Optional[Console] = None) -> SingleAccountDashboard:
709
+ """Factory function to create single account dashboard."""
710
+ return SingleAccountDashboard(console=console)