feat: Implement real-time stats and metrics for platform services with WebSocket integration
This commit is contained in:
@@ -21,7 +21,9 @@ export class OneboxDaemon {
|
||||
private smartdaemon: plugins.smartdaemon.SmartDaemon | null = null;
|
||||
private running = false;
|
||||
private monitoringInterval: number | null = null;
|
||||
private metricsInterval = 60000; // 1 minute
|
||||
private statsInterval: number | null = null;
|
||||
private metricsInterval = 60000; // 1 minute (for DB storage)
|
||||
private statsBroadcastInterval = 5000; // 5 seconds (for real-time WebSocket)
|
||||
private pidFilePath: string = PID_FILE_PATH;
|
||||
private lastDomainSync = 0; // Timestamp of last Cloudflare domain sync
|
||||
private domainSyncInterval = 6 * 60 * 60 * 1000; // 6 hours
|
||||
@@ -184,6 +186,11 @@ export class OneboxDaemon {
|
||||
await this.monitoringTick();
|
||||
}, this.metricsInterval);
|
||||
|
||||
// Start stats broadcasting loop (faster for real-time UI)
|
||||
this.statsInterval = setInterval(async () => {
|
||||
await this.broadcastStats();
|
||||
}, this.statsBroadcastInterval);
|
||||
|
||||
// Run first tick immediately
|
||||
this.monitoringTick();
|
||||
}
|
||||
@@ -195,8 +202,12 @@ export class OneboxDaemon {
|
||||
if (this.monitoringInterval !== null) {
|
||||
clearInterval(this.monitoringInterval);
|
||||
this.monitoringInterval = null;
|
||||
logger.debug('Monitoring loop stopped');
|
||||
}
|
||||
if (this.statsInterval !== null) {
|
||||
clearInterval(this.statsInterval);
|
||||
this.statsInterval = null;
|
||||
}
|
||||
logger.debug('Monitoring loops stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,6 +279,30 @@ export class OneboxDaemon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast stats to WebSocket clients (real-time updates)
|
||||
*/
|
||||
private async broadcastStats(): Promise<void> {
|
||||
try {
|
||||
const services = this.oneboxRef.services.listServices();
|
||||
|
||||
for (const service of services) {
|
||||
if (service.status === 'running' && service.containerID) {
|
||||
try {
|
||||
const stats = await this.oneboxRef.docker.getContainerStats(service.containerID);
|
||||
if (stats) {
|
||||
this.oneboxRef.httpServer.broadcastStatsUpdate(service.name, stats);
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore - stats collection can fail transiently
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore broadcast errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check SSL certificate expiration
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import { getErrorMessage } from '../utils/error.ts';
|
||||
import type { Onebox } from './onebox.ts';
|
||||
import type { IApiResponse, ICreateRegistryTokenRequest, IRegistryTokenView, TPlatformServiceType } from '../types.ts';
|
||||
import type { IApiResponse, ICreateRegistryTokenRequest, IRegistryTokenView, TPlatformServiceType, IContainerStats } from '../types.ts';
|
||||
|
||||
export class OneboxHttpServer {
|
||||
private oneboxRef: Onebox;
|
||||
@@ -245,6 +245,13 @@ export class OneboxHttpServer {
|
||||
} else if (path.match(/^\/api\/services\/[^/]+\/logs$/) && method === 'GET') {
|
||||
const name = path.split('/')[3];
|
||||
return await this.handleGetLogsRequest(name);
|
||||
} else if (path.match(/^\/api\/services\/[^/]+\/stats$/) && method === 'GET') {
|
||||
const name = path.split('/')[3];
|
||||
return await this.handleGetServiceStatsRequest(name);
|
||||
} else if (path.match(/^\/api\/services\/[^/]+\/metrics$/) && method === 'GET') {
|
||||
const name = path.split('/')[3];
|
||||
const limit = new URL(req.url).searchParams.get('limit');
|
||||
return await this.handleGetServiceMetricsRequest(name, limit ? parseInt(limit, 10) : 60);
|
||||
} else if (path === '/api/ssl/obtain' && method === 'POST') {
|
||||
return await this.handleObtainCertificateRequest(req);
|
||||
} else if (path === '/api/ssl/list' && method === 'GET') {
|
||||
@@ -293,6 +300,9 @@ export class OneboxHttpServer {
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq|caddy)\/stop$/) && method === 'POST') {
|
||||
const type = path.split('/')[3] as TPlatformServiceType;
|
||||
return await this.handleStopPlatformServiceRequest(type);
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq|caddy)\/stats$/) && method === 'GET') {
|
||||
const type = path.split('/')[3] as TPlatformServiceType;
|
||||
return await this.handleGetPlatformServiceStatsRequest(type);
|
||||
} else if (path.match(/^\/api\/services\/[^/]+\/platform-resources$/) && method === 'GET') {
|
||||
const serviceName = path.split('/')[3];
|
||||
return await this.handleGetServicePlatformResourcesRequest(serviceName);
|
||||
@@ -506,6 +516,51 @@ export class OneboxHttpServer {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetServiceStatsRequest(name: string): Promise<Response> {
|
||||
try {
|
||||
const service = this.oneboxRef.services.getService(name);
|
||||
if (!service) {
|
||||
return this.jsonResponse({ success: false, error: 'Service not found' }, 404);
|
||||
}
|
||||
|
||||
if (!service.containerID) {
|
||||
return this.jsonResponse({ success: false, error: 'Service has no container' }, 400);
|
||||
}
|
||||
|
||||
// Get live container stats
|
||||
const stats = await this.oneboxRef.docker.getContainerStats(service.containerID);
|
||||
if (!stats) {
|
||||
return this.jsonResponse({ success: false, error: 'Could not retrieve container stats' }, 500);
|
||||
}
|
||||
|
||||
return this.jsonResponse({ success: true, data: stats });
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get stats for service ${name}: ${getErrorMessage(error)}`);
|
||||
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to get stats' }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetServiceMetricsRequest(name: string, limit: number): Promise<Response> {
|
||||
try {
|
||||
const service = this.oneboxRef.services.getService(name);
|
||||
if (!service) {
|
||||
return this.jsonResponse({ success: false, error: 'Service not found' }, 404);
|
||||
}
|
||||
|
||||
if (!service.id) {
|
||||
return this.jsonResponse({ success: false, error: 'Service has no ID' }, 400);
|
||||
}
|
||||
|
||||
// Get historical metrics from database
|
||||
const metrics = this.oneboxRef.database.getMetrics(service.id, limit);
|
||||
|
||||
return this.jsonResponse({ success: true, data: metrics });
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get metrics for service ${name}: ${getErrorMessage(error)}`);
|
||||
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to get metrics' }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetSettingsRequest(): Promise<Response> {
|
||||
const settings = this.oneboxRef.database.getAllSettings();
|
||||
return this.jsonResponse({ success: true, data: settings });
|
||||
@@ -1251,6 +1306,18 @@ export class OneboxHttpServer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast stats update for a service
|
||||
*/
|
||||
broadcastStatsUpdate(serviceName: string, stats: IContainerStats): void {
|
||||
this.broadcast({
|
||||
type: 'stats_update',
|
||||
serviceName,
|
||||
stats,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// ============ Platform Services Endpoints ============
|
||||
|
||||
private async handleListPlatformServicesRequest(): Promise<Response> {
|
||||
@@ -1263,11 +1330,19 @@ export class OneboxHttpServer {
|
||||
const service = platformServices.find((s) => s.type === provider.type);
|
||||
// Check if provider has isCore property (like CaddyProvider)
|
||||
const isCore = 'isCore' in provider && (provider as any).isCore === true;
|
||||
|
||||
// For Caddy, check actual runtime status since it starts without a DB record
|
||||
let status = service?.status || 'not-deployed';
|
||||
if (provider.type === 'caddy') {
|
||||
const proxyStatus = this.oneboxRef.reverseProxy.getStatus();
|
||||
status = proxyStatus.http.running ? 'running' : 'stopped';
|
||||
}
|
||||
|
||||
return {
|
||||
type: provider.type,
|
||||
displayName: provider.displayName,
|
||||
resourceTypes: provider.resourceTypes,
|
||||
status: service?.status || 'not-deployed',
|
||||
status,
|
||||
containerId: service?.containerId,
|
||||
isCore,
|
||||
createdAt: service?.createdAt,
|
||||
@@ -1302,13 +1377,20 @@ export class OneboxHttpServer {
|
||||
? this.oneboxRef.database.getPlatformResourcesByPlatformService(service.id)
|
||||
: [];
|
||||
|
||||
// For Caddy, check actual runtime status since it starts without a DB record
|
||||
let status = service?.status || 'not-deployed';
|
||||
if (type === 'caddy') {
|
||||
const proxyStatus = this.oneboxRef.reverseProxy.getStatus();
|
||||
status = proxyStatus.http.running ? 'running' : 'stopped';
|
||||
}
|
||||
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
data: {
|
||||
type: provider.type,
|
||||
displayName: provider.displayName,
|
||||
resourceTypes: provider.resourceTypes,
|
||||
status: service?.status || 'not-deployed',
|
||||
status,
|
||||
containerId: service?.containerId,
|
||||
config: provider.getDefaultConfig(),
|
||||
resourceCount: allResources.length,
|
||||
@@ -1391,6 +1473,52 @@ export class OneboxHttpServer {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetPlatformServiceStatsRequest(type: TPlatformServiceType): Promise<Response> {
|
||||
try {
|
||||
const provider = this.oneboxRef.platformServices.getProvider(type);
|
||||
if (!provider) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: `Unknown platform service type: ${type}`,
|
||||
}, 404);
|
||||
}
|
||||
|
||||
// For Caddy, return proxy stats instead of container stats
|
||||
if (type === 'caddy') {
|
||||
const proxyStatus = this.oneboxRef.reverseProxy.getStatus();
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
data: {
|
||||
type: 'caddy',
|
||||
running: proxyStatus.http.running,
|
||||
httpPort: proxyStatus.http.port,
|
||||
httpsPort: proxyStatus.https.port,
|
||||
routes: proxyStatus.routes,
|
||||
certificates: proxyStatus.https.certificates,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const service = this.oneboxRef.database.getPlatformServiceByType(type);
|
||||
if (!service || !service.containerId) {
|
||||
return this.jsonResponse({ success: false, error: 'Platform service has no container' }, 400);
|
||||
}
|
||||
|
||||
const stats = await this.oneboxRef.docker.getContainerStats(service.containerId);
|
||||
if (!stats) {
|
||||
return this.jsonResponse({ success: false, error: 'Could not retrieve container stats' }, 500);
|
||||
}
|
||||
|
||||
return this.jsonResponse({ success: true, data: stats });
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get stats for platform service ${type}: ${getErrorMessage(error)}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: getErrorMessage(error) || 'Failed to get platform service stats',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetServicePlatformResourcesRequest(serviceName: string): Promise<Response> {
|
||||
try {
|
||||
const service = this.oneboxRef.services.getService(serviceName);
|
||||
|
||||
@@ -237,7 +237,7 @@ export class Onebox {
|
||||
},
|
||||
ssl: {
|
||||
configured: sslConfigured,
|
||||
certbotInstalled: await this.ssl.isCertbotInstalled(),
|
||||
certificateCount: this.ssl.listCertificates().length,
|
||||
},
|
||||
services: {
|
||||
total: totalServices,
|
||||
|
||||
Reference in New Issue
Block a user