Add unified health and version reporting

This commit is contained in:
Alexa Amundson
2025-11-19 16:04:41 -06:00
parent 2610c3a07f
commit be339de703
7 changed files with 191 additions and 7 deletions

View File

@@ -36,6 +36,11 @@ class Settings(BaseSettings):
def allowed_origins_list(self) -> List[str]: def allowed_origins_list(self) -> List[str]:
return [origin.strip() for origin in self.ALLOWED_ORIGINS.split(",")] return [origin.strip() for origin in self.ALLOWED_ORIGINS.split(",")]
# Prism / Status page targets
PRISM_CORE_API_URL: str = ""
PRISM_PUBLIC_API_URL: str = ""
PRISM_CONSOLE_URL: str = ""
# AWS S3 # AWS S3
AWS_ACCESS_KEY_ID: str = "" AWS_ACCESS_KEY_ID: str = ""
AWS_SECRET_ACCESS_KEY: str = "" AWS_SECRET_ACCESS_KEY: str = ""

View File

@@ -5,6 +5,7 @@ from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import time import time
from datetime import datetime, timezone
import os import os
from app.config import settings from app.config import settings
@@ -225,8 +226,22 @@ else:
async def health_check(): async def health_check():
"""Health check endpoint""" """Health check endpoint"""
return { return {
"service": "core-api",
"status": "healthy", "status": "healthy",
"timestamp": time.time() "environment": settings.ENVIRONMENT,
"version": settings.APP_VERSION,
"timestamp": datetime.now(timezone.utc).isoformat()
}
@app.get("/version")
async def version():
"""Service version endpoint"""
return {
"service": "core-api",
"version": settings.APP_VERSION,
"environment": settings.ENVIRONMENT,
"timestamp": datetime.now(timezone.utc).isoformat(),
} }

View File

@@ -64,6 +64,15 @@ async def serve_prism_console():
return FileResponse(prism_index) return FileResponse(prism_index)
@router.get("/prism/health")
async def prism_health():
"""Health endpoint for Prism Console assets."""
return {
"service": "prism-console",
"status": "healthy",
}
@router.get("/prism/{file_path:path}") @router.get("/prism/{file_path:path}")
async def serve_prism_static_files(file_path: str): async def serve_prism_static_files(file_path: str):
""" """

View File

@@ -1,7 +1,7 @@
"""System endpoints for core OS operations""" """System endpoints for core OS operations"""
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime from datetime import datetime, timezone
import os import os
from app.config import settings from app.config import settings
@@ -22,7 +22,7 @@ async def get_version():
return { return {
"version": settings.APP_VERSION, "version": settings.APP_VERSION,
"build_time": datetime.utcnow().isoformat(), "build_time": datetime.now(timezone.utc).isoformat(),
"env": settings.ENVIRONMENT, "env": settings.ENVIRONMENT,
"git_sha": git_sha[:8] if len(git_sha) > 8 else git_sha, "git_sha": git_sha[:8] if len(git_sha) > 8 else git_sha,
"app_name": settings.APP_NAME, "app_name": settings.APP_NAME,
@@ -81,3 +81,39 @@ async def get_os_state(db: AsyncSession = Depends(get_db)):
}, },
"note": "This is a stub endpoint. Full OS state tracking coming in Phase 2.", "note": "This is a stub endpoint. Full OS state tracking coming in Phase 2.",
} }
@router.get("/prism/config")
async def prism_config(request: Request):
"""Return Prism Console service configuration for health/status checks."""
def resolve_url(env_url: str, fallback: str) -> str:
return env_url.rstrip("/") if env_url else fallback.rstrip("/")
base_url = str(request.base_url).rstrip("/")
services = [
{
"name": "core-api",
"url": resolve_url(settings.PRISM_CORE_API_URL, base_url),
"health_path": "/health",
"version_path": "/version",
},
{
"name": "public-api",
"url": resolve_url(settings.PRISM_PUBLIC_API_URL, base_url),
"health_path": "/health",
"version_path": "/version",
},
{
"name": "prism-console",
"url": resolve_url(settings.PRISM_CONSOLE_URL, base_url),
"health_path": "/prism/health",
"version_path": "/version",
},
]
return {
"environment": settings.ENVIRONMENT,
"services": services,
}

View File

@@ -57,6 +57,25 @@
</div> </div>
</div> </div>
<div class="section">
<h3>Service Health</h3>
<table class="data-table">
<thead>
<tr>
<th>Service</th>
<th>Status</th>
<th>Version</th>
<th>Endpoint</th>
</tr>
</thead>
<tbody id="service-health-body">
<tr>
<td colspan="4" class="empty-state">Waiting for health checks...</td>
</tr>
</tbody>
</table>
</div>
<div class="section"> <div class="section">
<h3>Quick Actions</h3> <h3>Quick Actions</h3>
<div class="button-group"> <div class="button-group">

View File

@@ -216,6 +216,28 @@ body {
border-bottom: 1px solid var(--prism-border); border-bottom: 1px solid var(--prism-border);
} }
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.6rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
}
.status-badge.healthy {
background: rgba(16, 185, 129, 0.12);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.35);
}
.status-badge.unhealthy {
background: rgba(239, 68, 68, 0.12);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.35);
}
.empty-state { .empty-state {
text-align: center; text-align: center;
color: var(--prism-text-muted); color: var(--prism-text-muted);

View File

@@ -6,6 +6,7 @@
class PrismConsole { class PrismConsole {
constructor() { constructor() {
this.apiBase = window.location.origin; this.apiBase = window.location.origin;
this.services = [];
this.init(); this.init();
} }
@@ -15,7 +16,8 @@ class PrismConsole {
// Setup tab navigation // Setup tab navigation
this.setupTabs(); this.setupTabs();
// Load initial data // Load configuration and initial data
await this.loadConfig();
await this.loadDashboard(); await this.loadDashboard();
// Setup auto-refresh // Setup auto-refresh
@@ -46,6 +48,20 @@ class PrismConsole {
}); });
} }
async loadConfig() {
try {
const config = await this.fetchAPI('/api/system/prism/config');
this.services = config.services || [];
if (config.environment) {
document.getElementById('environment-badge').textContent = config.environment;
}
} catch (error) {
console.error('Failed to load Prism configuration', error);
this.services = [];
}
}
async loadDashboard() { async loadDashboard() {
try { try {
// Get system version // Get system version
@@ -54,8 +70,7 @@ class PrismConsole {
document.getElementById('environment-badge').textContent = version.env; document.getElementById('environment-badge').textContent = version.env;
// Get system status // Get system status
document.getElementById('system-status').textContent = 'Healthy ✓'; await this.loadServiceHealth();
document.getElementById('health-status').style.color = '#10b981';
// Update last updated time // Update last updated time
const now = new Date().toLocaleTimeString(); const now = new Date().toLocaleTimeString();
@@ -120,6 +135,61 @@ class PrismConsole {
} }
} }
async loadServiceHealth() {
const tbody = document.getElementById('service-health-body');
if (!tbody || !this.services.length) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No services configured</td></tr>';
return;
}
const results = await Promise.all(
this.services.map(async (service) => {
const healthUrl = `${service.url}${service.health_path || '/health'}`;
const versionUrl = `${service.url}${service.version_path || '/version'}`;
try {
const [health, version] = await Promise.all([
this.fetchExternal(healthUrl),
this.fetchExternal(versionUrl),
]);
return {
name: service.name,
status: (health.status || 'unknown').toLowerCase(),
version: version.version || 'unknown',
endpoint: healthUrl,
};
} catch (error) {
console.error(`Health check failed for ${service.name}:`, error);
return {
name: service.name,
status: 'error',
version: 'n/a',
endpoint: healthUrl,
};
}
})
);
const unhealthy = results.some((result) => result.status !== 'healthy');
document.getElementById('system-status').textContent = unhealthy ? 'Issues Detected' : 'Healthy ✓';
document.getElementById('health-status').style.color = unhealthy ? '#ef4444' : '#10b981';
tbody.innerHTML = results
.map(
(result) => `
<tr>
<td>${result.name}</td>
<td><span class="status-badge ${result.status === 'healthy' ? 'healthy' : 'unhealthy'}">${result.status}</span></td>
<td>${result.version}</td>
<td><a href="${result.endpoint}" target="_blank" rel="noopener noreferrer">${result.endpoint}</a></td>
</tr>
`
)
.join('');
}
async fetchAPI(endpoint) { async fetchAPI(endpoint) {
const response = await fetch(`${this.apiBase}${endpoint}`); const response = await fetch(`${this.apiBase}${endpoint}`);
if (!response.ok) { if (!response.ok) {
@@ -127,6 +197,14 @@ class PrismConsole {
} }
return response.json(); return response.json();
} }
async fetchExternal(url) {
const response = await fetch(url, { headers: { Accept: 'application/json' } });
if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
} }
// Global functions for HTML onclick // Global functions for HTML onclick