Skip to content

Commit 7fde038

Browse files
authored
Merge pull request #174 from lbedner/services-health-check
Services Health Check Initial Implementation
2 parents da2ac2e + 51b8da0 commit 7fde038

22 files changed

Lines changed: 1191 additions & 95 deletions

File tree

aegis/cli/interactive.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,27 @@ def interactive_project_selection() -> tuple[list[str], str, list[str]]:
171171
for service_name, service_spec in auth_services.items():
172172
prompt = f" Add {service_spec.description.lower()}?"
173173
if typer.confirm(prompt):
174-
selected_services.append(service_name)
174+
# Auth service requires database - provide explicit confirmation
175+
typer.echo("\n🗃️ Database Required:")
176+
typer.echo(" Authentication requires a database for user storage")
177+
typer.echo(" (user accounts, sessions, JWT tokens)")
178+
179+
# Check if database is already selected
180+
database_already_selected = any(
181+
"database" in comp for comp in selected
182+
)
183+
184+
if database_already_selected:
185+
typer.echo("✅ Database component already selected")
186+
selected_services.append(service_name)
187+
else:
188+
auth_confirm_prompt = " Continue and add database component?"
189+
if typer.confirm(auth_confirm_prompt, default=True):
190+
selected_services.append(service_name)
191+
# Note: Database will be auto-added by service resolution in init.py
192+
typer.echo("✅ Authentication + Database configured")
193+
else:
194+
typer.echo("⏹️ Authentication service cancelled")
175195

176196
# Future service types can be added here as they become available
177197
# payment_services = get_services_by_type(ServiceType.PAYMENT)

aegis/commands/init.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,13 @@ def init_command(
102102
selected_services = cast(list[str], services) if services else []
103103
scheduler_backend = "memory" # Default to in-memory scheduler
104104

105-
# Resolve services to components if services were provided
106-
if selected_services:
107-
# If user provided explicit components, validate compatibility first
108-
if components is not None: # User provided explicit --components
105+
# Resolve services to components if services were provided (non-interactive mode only)
106+
if selected_services and not interactive:
107+
# Check if --components was explicitly provided
108+
components_explicitly_provided = components is not None
109+
110+
if components_explicitly_provided:
111+
# In non-interactive mode with explicit --components, validate compatibility
109112
# Include core components (always present) for validation
110113
components_for_validation = list(set(selected_components + CORE_COMPONENTS))
111114
errors = ServiceResolver.validate_service_component_compatibility(
@@ -124,15 +127,30 @@ def init_command(
124127
)
125128
if missing_components:
126129
typer.echo(
127-
f"💡 Suggestion: Add missing components: --components {','.join(list(dict.fromkeys(selected_components + missing_components)))}",
130+
f"💡 Suggestion: Add missing components --components {','.join(sorted(set(selected_components + missing_components)))}",
131+
err=True,
132+
)
133+
typer.echo(
134+
" Or remove --components to let services auto-add dependencies.",
128135
err=True,
129136
)
130137
typer.echo(
131-
" Alternatively, remove --components to let services auto-add their dependencies.",
138+
" Alternatively, use interactive mode to auto-add service dependencies.",
132139
err=True,
133140
)
134141
raise typer.Exit(1)
142+
else:
143+
# No --components provided, auto-add required components for services
144+
service_components, _ = ServiceResolver.resolve_service_dependencies(
145+
selected_services
146+
)
147+
if service_components:
148+
typer.echo(
149+
f"📦 Services require components: {', '.join(sorted(service_components))}"
150+
)
151+
selected_components = service_components
135152

153+
# Resolve service dependencies and merge with any explicitly selected components
136154
service_components, _ = ServiceResolver.resolve_service_dependencies(
137155
selected_services
138156
)
@@ -178,6 +196,43 @@ def init_command(
178196
# Merge interactively selected services with any already selected services
179197
selected_services = list(set(selected_services + interactive_services))
180198

199+
# Handle service dependencies for interactively selected services
200+
if interactive_services:
201+
# Track originally selected components before service resolution
202+
originally_selected_components = selected_components.copy()
203+
204+
service_components, _ = ServiceResolver.resolve_service_dependencies(
205+
interactive_services
206+
)
207+
# Merge service-required components with selected components
208+
all_components = list(set(selected_components + service_components))
209+
selected_components = all_components
210+
211+
# Show which components were auto-added by services
212+
service_added_components = [
213+
comp
214+
for comp in service_components
215+
if comp not in originally_selected_components
216+
and comp not in CORE_COMPONENTS
217+
]
218+
if service_added_components:
219+
# Create a mapping of which services require which components
220+
service_component_map = {}
221+
for service_name in interactive_services:
222+
service_deps = ServiceResolver.resolve_service_dependencies(
223+
[service_name]
224+
)[0]
225+
for comp in service_deps:
226+
if comp in service_added_components:
227+
if comp not in service_component_map:
228+
service_component_map[comp] = []
229+
service_component_map[comp].append(service_name)
230+
231+
typer.echo("\n📦 Auto-added by services:")
232+
for comp, requiring_services in service_component_map.items():
233+
services_str = ", ".join(requiring_services)
234+
typer.echo(f" • {comp} (required by {services_str})")
235+
181236
# Create template generator with scheduler backend context
182237
template_gen = TemplateGenerator(
183238
project_name, list(selected_components), scheduler_backend, selected_services

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py.j2

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,15 +338,35 @@ def _display_health_status(
338338
f"Health Percentage: [bold]"
339339
f"{health_percentage:.{CLI.HEALTH_PERCENTAGE_DECIMALS}f}%[/bold]",
340340
f"Components: {healthy_count}/" + f"{total_count} healthy",
341-
f"Timestamp: {timestamp}",
342341
]
343342

343+
# Add service information if available (DetailedHealthResponse)
344+
if hasattr(health_data, "has_services") and health_data.has_services:
345+
healthy_services_count = len(health_data.healthy_services)
346+
total_services_count = len(health_data.service_names)
347+
if healthy_services_count == total_services_count:
348+
service_status_color = "green"
349+
elif healthy_services_count > 0:
350+
service_status_color = "yellow"
351+
else:
352+
service_status_color = "red"
353+
354+
panel_content.append(
355+
f"Services: [bold {service_status_color}]{healthy_services_count}/"
356+
f"{total_services_count} healthy[/bold {service_status_color}]"
357+
)
358+
359+
panel_content.append(f"Timestamp: {timestamp}")
360+
344361
console.print(
345362
Panel("\n".join(panel_content), title=title, border_style=overall_color)
346363
)
347364

348-
# Component Tree Display
349-
console.print("\n[bold magenta]Component Tree:[/bold magenta]")
365+
# Component and Service Tree Display
366+
tree_title = "Component Tree"
367+
if hasattr(health_data, "has_services") and health_data.has_services:
368+
tree_title = "Component & Service Tree"
369+
console.print(f"\n[bold magenta]{tree_title}:[/bold magenta]")
350370

351371
# Sort components: unhealthy first, then by name
352372
sorted_components = sorted(components.items(), key=lambda x: (x[1].healthy, x[0]))

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py.j2

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ def run_load_test(
115115
)
116116
else:
117117
rprint("\n💡 [yellow]Use this command to check results later:[/yellow]")
118-
rprint(f" [bold]{{ cookiecutter.project_slug }} load-test results {task_id}[/bold]")
118+
rprint(
119+
f" [bold]{{ cookiecutter.project_slug }} load-test results {task_id}[/bold]"
120+
)
119121

120122
except Exception as e:
121123
rprint(f"❌ [red]Failed to start load test:[/red] {e}")

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ async def detailed_health() -> DetailedHealthResponse:
5959
healthy_components=system_status.healthy_components,
6060
unhealthy_components=system_status.unhealthy_components,
6161
health_percentage=system_status.health_percentage,
62+
# Service-specific information
63+
has_services=system_status.has_services,
64+
service_names=system_status.service_names,
65+
healthy_services=system_status.healthy_services,
66+
unhealthy_services=system_status.unhealthy_services,
6267
)
6368

6469
except HTTPException:
@@ -119,6 +124,31 @@ async def system_dashboard() -> dict[str, Any]:
119124
"healthy_components": len(system_status.healthy_components),
120125
"unhealthy_components": len(system_status.unhealthy_components),
121126
"recent_alerts": recent_alerts,
127+
# Service summary information
128+
"has_services": system_status.has_services,
129+
"total_services": len(system_status.service_names),
130+
"healthy_services": len(system_status.healthy_services),
131+
"unhealthy_services": len(system_status.unhealthy_services),
132+
},
133+
# Services section for frontend consumption
134+
"services": {
135+
"enabled": system_status.has_services,
136+
"services": {
137+
name: {
138+
"name": name,
139+
"healthy": service.healthy,
140+
"message": service.message,
141+
"response_time_ms": service.response_time_ms,
142+
"metadata": service.metadata,
143+
}
144+
for name, service in (
145+
system_status.services_status.sub_components.items()
146+
if system_status.services_status
147+
else {}
148+
).items()
149+
}
150+
if system_status.has_services
151+
else {},
122152
},
123153
"system_info": system_status.system_info,
124154
"timestamp": system_status.timestamp.isoformat(),

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,4 +386,26 @@ async def startup_hook() -> None:
386386
logger.info("Database component health check registered")
387387
{%- endif %}
388388

389-
logger.info("✅ Component health detection complete")
389+
logger.info("✅ Component health detection complete")
390+
391+
# ==========================================
392+
# Service Health Checks Registration
393+
# ==========================================
394+
395+
from app.services.system.health import register_service_health_check
396+
397+
logger.info("🔧 Registering service health checks...")
398+
399+
{%- if cookiecutter.include_auth == "yes" %}
400+
# Register auth service health check
401+
from app.services.auth.health import check_auth_service_health
402+
register_service_health_check("auth", check_auth_service_health)
403+
logger.info("Auth service health check registered")
404+
{%- endif %}
405+
406+
# Future services will be registered here:
407+
# if cookiecutter.include_payment == "yes":
408+
# from app.services.payment.health import check_payment_service_health
409+
# register_service_health_check("payment", check_payment_service_health)
410+
411+
logger.info("✅ Service health detection complete")

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/__init__.py.j2

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Dashboard component cards."""
22

3+
from .ai_card import AICard
4+
from .auth_card import AuthCard
35
from .fastapi_card import FastAPICard
46
from .flet_card import FletCard
7+
from .payment_card import PaymentCard
8+
from .services_card import ServicesCard
59
{%- if cookiecutter.include_database == "yes" %}
610
from .database_card import DatabaseCard
711
{%- endif %}
@@ -16,8 +20,12 @@ from .worker_card import WorkerCard
1620
{%- endif %}
1721

1822
__all__ = [
23+
"AICard",
24+
"AuthCard",
1925
"FastAPICard",
2026
"FletCard",
27+
"PaymentCard",
28+
"ServicesCard",
2129
{%- if cookiecutter.include_database == "yes" %}
2230
"DatabaseCard",
2331
{%- endif %}

0 commit comments

Comments
 (0)