runbooks 0.7.9__py3-none-any.whl → 0.9.0__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 +1 -1
- runbooks/cfat/README.md +12 -1
- runbooks/cfat/__init__.py +1 -1
- runbooks/cfat/assessment/runner.py +42 -34
- runbooks/cfat/models.py +1 -1
- runbooks/common/__init__.py +152 -0
- runbooks/common/accuracy_validator.py +1039 -0
- runbooks/common/context_logger.py +440 -0
- runbooks/common/cross_module_integration.py +594 -0
- runbooks/common/enhanced_exception_handler.py +1108 -0
- runbooks/common/enterprise_audit_integration.py +634 -0
- runbooks/common/mcp_integration.py +539 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +171 -0
- runbooks/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +339 -451
- runbooks/finops/__init__.py +4 -21
- runbooks/finops/account_resolver.py +279 -0
- runbooks/finops/accuracy_cross_validator.py +638 -0
- runbooks/finops/aws_client.py +721 -36
- runbooks/finops/budget_integration.py +313 -0
- runbooks/finops/cli.py +59 -5
- runbooks/finops/cost_processor.py +211 -37
- runbooks/finops/dashboard_router.py +900 -0
- runbooks/finops/dashboard_runner.py +990 -232
- runbooks/finops/embedded_mcp_validator.py +288 -0
- runbooks/finops/enhanced_dashboard_runner.py +8 -7
- runbooks/finops/enhanced_progress.py +327 -0
- runbooks/finops/enhanced_trend_visualization.py +423 -0
- runbooks/finops/finops_dashboard.py +29 -1880
- runbooks/finops/helpers.py +509 -196
- runbooks/finops/iam_guidance.py +400 -0
- runbooks/finops/markdown_exporter.py +466 -0
- runbooks/finops/multi_dashboard.py +1502 -0
- runbooks/finops/optimizer.py +15 -15
- runbooks/finops/profile_processor.py +2 -2
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/service_mapping.py +195 -0
- runbooks/finops/single_dashboard.py +710 -0
- runbooks/finops/tests/test_reference_images_validation.py +1 -1
- runbooks/inventory/README.md +12 -1
- runbooks/inventory/core/collector.py +157 -29
- runbooks/inventory/list_ec2_instances.py +9 -6
- runbooks/inventory/list_ssm_parameters.py +10 -10
- runbooks/inventory/organizations_discovery.py +210 -164
- runbooks/inventory/rich_inventory_display.py +74 -107
- runbooks/inventory/run_on_multi_accounts.py +13 -13
- runbooks/main.py +740 -134
- runbooks/metrics/dora_metrics_engine.py +711 -17
- runbooks/monitoring/performance_monitor.py +433 -0
- runbooks/operate/README.md +394 -0
- runbooks/operate/base.py +215 -47
- runbooks/operate/ec2_operations.py +7 -5
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/remediation/README.md +489 -13
- runbooks/remediation/commons.py +8 -4
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +164 -33
- runbooks/security/compliance_automation.py +12 -10
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +931 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/report_generator.py +1 -1
- runbooks/security/run_script.py +4 -8
- runbooks/security/security_baseline_tester.py +36 -49
- runbooks/security/security_export.py +99 -120
- runbooks/sre/README.md +472 -0
- runbooks/sre/__init__.py +33 -0
- runbooks/sre/mcp_reliability_engine.py +1049 -0
- runbooks/sre/performance_optimization_engine.py +1032 -0
- runbooks/sre/reliability_monitoring_framework.py +1011 -0
- runbooks/validation/__init__.py +2 -2
- runbooks/validation/benchmark.py +154 -149
- runbooks/validation/cli.py +159 -147
- runbooks/validation/mcp_validator.py +265 -236
- runbooks/vpc/README.md +478 -0
- runbooks/vpc/__init__.py +2 -2
- runbooks/vpc/manager_interface.py +366 -351
- runbooks/vpc/networking_wrapper.py +62 -33
- runbooks/vpc/rich_formatters.py +22 -8
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/METADATA +136 -54
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/RECORD +94 -55
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/entry_points.txt +1 -1
- runbooks/finops/cross_validation.py +0 -375
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/WHEEL +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.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)
|