moved most api stuff to web-api and built out a better monitoring solution for web-app
This commit is contained in:
parent
fbff428e90
commit
da1c52a841
45 changed files with 2986 additions and 312 deletions
104
apps/stock/web-app/src/features/monitoring/MonitoringPage.tsx
Normal file
104
apps/stock/web-app/src/features/monitoring/MonitoringPage.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* System Monitoring Page
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useSystemHealth, useCacheStats, useQueueStats, useDatabaseStats } from './hooks';
|
||||
import { SystemHealthCard, CacheStatsCard, QueueStatsTable, DatabaseStatsGrid } from './components';
|
||||
|
||||
export function MonitoringPage() {
|
||||
const [refreshInterval, setRefreshInterval] = useState(5000); // 5 seconds default
|
||||
|
||||
const { data: health, loading: healthLoading, error: healthError } = useSystemHealth(refreshInterval);
|
||||
const { data: cache, loading: cacheLoading, error: cacheError } = useCacheStats(refreshInterval);
|
||||
const { data: queues, loading: queuesLoading, error: queuesError } = useQueueStats(refreshInterval);
|
||||
const { data: databases, loading: dbLoading, error: dbError } = useDatabaseStats(refreshInterval);
|
||||
|
||||
const handleRefreshIntervalChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setRefreshInterval(Number(e.target.value));
|
||||
};
|
||||
|
||||
if (healthLoading || cacheLoading || queuesLoading || dbLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading monitoring data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasErrors = healthError || cacheError || queuesError || dbError;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">System Monitoring</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<label htmlFor="refresh-interval" className="text-sm text-gray-600">
|
||||
Refresh interval:
|
||||
</label>
|
||||
<select
|
||||
id="refresh-interval"
|
||||
value={refreshInterval}
|
||||
onChange={handleRefreshIntervalChange}
|
||||
className="text-sm border rounded px-2 py-1"
|
||||
>
|
||||
<option value={0}>Manual</option>
|
||||
<option value={5000}>5 seconds</option>
|
||||
<option value={10000}>10 seconds</option>
|
||||
<option value={30000}>30 seconds</option>
|
||||
<option value={60000}>1 minute</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasErrors && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<h3 className="font-semibold text-red-800 mb-2">Errors occurred while fetching data:</h3>
|
||||
<ul className="list-disc list-inside text-sm text-red-700 space-y-1">
|
||||
{healthError && <li>System Health: {healthError}</li>}
|
||||
{cacheError && <li>Cache Stats: {cacheError}</li>}
|
||||
{queuesError && <li>Queue Stats: {queuesError}</li>}
|
||||
{dbError && <li>Database Stats: {dbError}</li>}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* System Health */}
|
||||
{health && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-1">
|
||||
<SystemHealthCard health={health} />
|
||||
</div>
|
||||
<div className="lg:col-span-2">
|
||||
{cache && <CacheStatsCard stats={cache} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Database Stats */}
|
||||
{databases && databases.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Database Connections</h2>
|
||||
<DatabaseStatsGrid databases={databases} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Queue Stats */}
|
||||
{queues && queues.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Queue Status</h2>
|
||||
<QueueStatsTable queues={queues} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Cache Statistics Card Component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '../../../components/ui/Card';
|
||||
import type { CacheStats } from '../types';
|
||||
|
||||
interface CacheStatsCardProps {
|
||||
stats: CacheStats;
|
||||
}
|
||||
|
||||
export function CacheStatsCard({ stats }: CacheStatsCardProps) {
|
||||
const formatBytes = (bytes: number) => {
|
||||
const mb = bytes / 1024 / 1024;
|
||||
return mb.toFixed(2) + ' MB';
|
||||
};
|
||||
|
||||
const hitRate = stats.stats && (stats.stats.hits + stats.stats.misses) > 0
|
||||
? (stats.stats.hits / (stats.stats.hits + stats.stats.misses) * 100).toFixed(1)
|
||||
: '0';
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">Cache (Dragonfly)</h3>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
stats.connected ? 'text-green-600 bg-green-100' : 'text-red-600 bg-red-100'
|
||||
}`}>
|
||||
{stats.connected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{stats.connected ? (
|
||||
<div className="space-y-4">
|
||||
{stats.memoryUsage && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">Memory Used</div>
|
||||
<div className="text-xl font-semibold">{formatBytes(stats.memoryUsage.used)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">Peak Memory</div>
|
||||
<div className="text-xl font-semibold">{formatBytes(stats.memoryUsage.peak)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats.stats && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">Hit Rate</div>
|
||||
<div className="text-xl font-semibold">{hitRate}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">Total Keys</div>
|
||||
<div className="text-xl font-semibold">{stats.stats.keys.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">Hits:</span> {stats.stats.hits.toLocaleString()}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Misses:</span> {stats.stats.misses.toLocaleString()}
|
||||
</div>
|
||||
{stats.stats.evictedKeys !== undefined && (
|
||||
<div>
|
||||
<span className="text-gray-600">Evicted:</span> {stats.stats.evictedKeys.toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
{stats.stats.expiredKeys !== undefined && (
|
||||
<div>
|
||||
<span className="text-gray-600">Expired:</span> {stats.stats.expiredKeys.toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{stats.uptime && (
|
||||
<div className="text-sm text-gray-600">
|
||||
Uptime: {Math.floor(stats.uptime / 3600)}h {Math.floor((stats.uptime % 3600) / 60)}m
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Cache service is not available
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* Database Statistics Grid Component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '../../../components/ui/Card';
|
||||
import type { DatabaseStats } from '../types';
|
||||
|
||||
interface DatabaseStatsGridProps {
|
||||
databases: DatabaseStats[];
|
||||
}
|
||||
|
||||
export function DatabaseStatsGrid({ databases }: DatabaseStatsGridProps) {
|
||||
const getDbIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'postgres':
|
||||
return '🐘';
|
||||
case 'mongodb':
|
||||
return '🍃';
|
||||
case 'questdb':
|
||||
return '⚡';
|
||||
default:
|
||||
return '💾';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{databases.map((db) => (
|
||||
<Card key={db.type} className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-2xl">{getDbIcon(db.type)}</span>
|
||||
<h4 className="font-semibold">{db.name}</h4>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
db.connected ? 'text-green-600 bg-green-100' : 'text-red-600 bg-red-100'
|
||||
}`}>
|
||||
{db.connected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{db.connected ? (
|
||||
<div className="space-y-3">
|
||||
{db.latency !== undefined && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">Latency</div>
|
||||
<div className="text-lg font-semibold">{db.latency}ms</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{db.pool && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">Connection Pool</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">Active:</span> {db.pool.active}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Idle:</span> {db.pool.idle}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Size:</span> {db.pool.size}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Max:</span> {db.pool.max}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{db.pool.max > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${(db.pool.size / db.pool.max) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{((db.pool.size / db.pool.max) * 100).toFixed(0)}% utilized
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{db.type === 'mongodb' && db.stats && (
|
||||
<div className="text-xs text-gray-600">
|
||||
<div>Version: {db.stats.version}</div>
|
||||
{db.stats.connections && (
|
||||
<div>Connections: {db.stats.connections.current}/{db.stats.connections.available}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-gray-500">
|
||||
Database is not available
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Queue Statistics Table Component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '../../../components/ui/Card';
|
||||
import type { QueueStats } from '../types';
|
||||
|
||||
interface QueueStatsTableProps {
|
||||
queues: QueueStats[];
|
||||
}
|
||||
|
||||
export function QueueStatsTable({ queues }: QueueStatsTableProps) {
|
||||
const totalJobs = (queue: QueueStats) => {
|
||||
const { jobs } = queue;
|
||||
return jobs.waiting + jobs.active + jobs.completed + jobs.failed + jobs.delayed + jobs.paused;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Queue Statistics</h3>
|
||||
|
||||
{queues.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 px-3">Queue</th>
|
||||
<th className="text-center py-2 px-3">Status</th>
|
||||
<th className="text-right py-2 px-3">Waiting</th>
|
||||
<th className="text-right py-2 px-3">Active</th>
|
||||
<th className="text-right py-2 px-3">Completed</th>
|
||||
<th className="text-right py-2 px-3">Failed</th>
|
||||
<th className="text-right py-2 px-3">Delayed</th>
|
||||
<th className="text-right py-2 px-3">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{queues.map((queue) => (
|
||||
<tr key={queue.name} className="border-b hover:bg-gray-50">
|
||||
<td className="py-2 px-3 font-medium">{queue.name}</td>
|
||||
<td className="py-2 px-3 text-center">
|
||||
<span className={`inline-block w-2 h-2 rounded-full ${
|
||||
queue.connected ? 'bg-green-500' : 'bg-red-500'
|
||||
}`} />
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">{queue.jobs.waiting.toLocaleString()}</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
{queue.jobs.active > 0 ? (
|
||||
<span className="text-blue-600 font-medium">{queue.jobs.active}</span>
|
||||
) : (
|
||||
queue.jobs.active
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">{queue.jobs.completed.toLocaleString()}</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
{queue.jobs.failed > 0 ? (
|
||||
<span className="text-red-600 font-medium">{queue.jobs.failed}</span>
|
||||
) : (
|
||||
queue.jobs.failed
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">{queue.jobs.delayed.toLocaleString()}</td>
|
||||
<td className="py-2 px-3 text-right font-medium">{totalJobs(queue).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No queue data available
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* System Health Card Component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '../../../components/ui/Card';
|
||||
import type { SystemHealth } from '../types';
|
||||
|
||||
interface SystemHealthCardProps {
|
||||
health: SystemHealth;
|
||||
}
|
||||
|
||||
export function SystemHealthCard({ health }: SystemHealthCardProps) {
|
||||
const statusColor = {
|
||||
healthy: 'text-green-600 bg-green-100',
|
||||
degraded: 'text-yellow-600 bg-yellow-100',
|
||||
unhealthy: 'text-red-600 bg-red-100',
|
||||
}[health.status];
|
||||
|
||||
const formatUptime = (ms: number) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days}d ${hours % 24}h`;
|
||||
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
||||
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
||||
return `${seconds}s`;
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
const gb = bytes / 1024 / 1024 / 1024;
|
||||
return gb.toFixed(2) + ' GB';
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">System Health</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${statusColor}`}>
|
||||
{health.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">Uptime</div>
|
||||
<div className="text-2xl font-semibold">{formatUptime(health.uptime)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">Memory Usage</div>
|
||||
<div className="mt-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{formatBytes(health.memory.used)} / {formatBytes(health.memory.total)}</span>
|
||||
<span>{health.memory.percentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="mt-1 w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${health.memory.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{health.errors && health.errors.length > 0 && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-2">Issues</div>
|
||||
<ul className="space-y-1">
|
||||
{health.errors.map((error, index) => (
|
||||
<li key={index} className="text-sm text-red-600">
|
||||
• {error}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
Last updated: {new Date(health.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Monitoring components exports
|
||||
*/
|
||||
|
||||
export { SystemHealthCard } from './SystemHealthCard';
|
||||
export { CacheStatsCard } from './CacheStatsCard';
|
||||
export { QueueStatsTable } from './QueueStatsTable';
|
||||
export { DatabaseStatsGrid } from './DatabaseStatsGrid';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* Monitoring hooks exports
|
||||
*/
|
||||
|
||||
export * from './useMonitoring';
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* Custom hook for monitoring data
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { monitoringApi } from '../services/monitoringApi';
|
||||
import type { SystemHealth, CacheStats, QueueStats, DatabaseStats } from '../types';
|
||||
|
||||
export function useSystemHealth(refreshInterval: number = 5000) {
|
||||
const [data, setData] = useState<SystemHealth | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const health = await monitoringApi.getSystemHealth();
|
||||
setData(health);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch system health');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
const interval = setInterval(fetchData, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [fetchData, refreshInterval]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
export function useCacheStats(refreshInterval: number = 5000) {
|
||||
const [data, setData] = useState<CacheStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const stats = await monitoringApi.getCacheStats();
|
||||
setData(stats);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch cache stats');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
const interval = setInterval(fetchData, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [fetchData, refreshInterval]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
export function useQueueStats(refreshInterval: number = 5000) {
|
||||
const [data, setData] = useState<QueueStats[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const result = await monitoringApi.getQueueStats();
|
||||
setData(result.queues);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch queue stats');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
const interval = setInterval(fetchData, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [fetchData, refreshInterval]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
export function useDatabaseStats(refreshInterval: number = 5000) {
|
||||
const [data, setData] = useState<DatabaseStats[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const result = await monitoringApi.getDatabaseStats();
|
||||
setData(result.databases);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch database stats');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
const interval = setInterval(fetchData, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [fetchData, refreshInterval]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
8
apps/stock/web-app/src/features/monitoring/index.ts
Normal file
8
apps/stock/web-app/src/features/monitoring/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Monitoring feature exports
|
||||
*/
|
||||
|
||||
export { MonitoringPage } from './MonitoringPage';
|
||||
export * from './types';
|
||||
export * from './hooks/useMonitoring';
|
||||
export * from './services/monitoringApi';
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* Monitoring API Service
|
||||
*/
|
||||
|
||||
import type { SystemHealth, CacheStats, QueueStats, DatabaseStats } from '../types';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:2003';
|
||||
const MONITORING_BASE = `${API_BASE_URL}/api/system/monitoring`;
|
||||
|
||||
export const monitoringApi = {
|
||||
/**
|
||||
* Get overall system health
|
||||
*/
|
||||
async getSystemHealth(): Promise<SystemHealth> {
|
||||
const response = await fetch(MONITORING_BASE);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch system health: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
async getCacheStats(): Promise<CacheStats> {
|
||||
const response = await fetch(`${MONITORING_BASE}/cache`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch cache stats: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get queue statistics
|
||||
*/
|
||||
async getQueueStats(): Promise<{ queues: QueueStats[] }> {
|
||||
const response = await fetch(`${MONITORING_BASE}/queues`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch queue stats: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get specific queue statistics
|
||||
*/
|
||||
async getQueueStatsByName(name: string): Promise<QueueStats> {
|
||||
const response = await fetch(`${MONITORING_BASE}/queues/${name}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch queue ${name} stats: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
*/
|
||||
async getDatabaseStats(): Promise<{ databases: DatabaseStats[] }> {
|
||||
const response = await fetch(`${MONITORING_BASE}/databases`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch database stats: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get specific database statistics
|
||||
*/
|
||||
async getDatabaseStatsByType(type: 'postgres' | 'mongodb' | 'questdb'): Promise<DatabaseStats> {
|
||||
const response = await fetch(`${MONITORING_BASE}/databases/${type}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${type} stats: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get detailed cache info
|
||||
*/
|
||||
async getCacheInfo(): Promise<{ parsed: CacheStats; raw: string }> {
|
||||
const response = await fetch(`${MONITORING_BASE}/cache/info`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch cache info: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
};
|
||||
80
apps/stock/web-app/src/features/monitoring/types/index.ts
Normal file
80
apps/stock/web-app/src/features/monitoring/types/index.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Monitoring types for system health and metrics
|
||||
*/
|
||||
|
||||
export interface CacheStats {
|
||||
provider: string;
|
||||
connected: boolean;
|
||||
uptime?: number;
|
||||
memoryUsage?: {
|
||||
used: number;
|
||||
peak: number;
|
||||
total?: number;
|
||||
};
|
||||
stats?: {
|
||||
hits: number;
|
||||
misses: number;
|
||||
keys: number;
|
||||
evictedKeys?: number;
|
||||
expiredKeys?: number;
|
||||
};
|
||||
info?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface QueueStats {
|
||||
name: string;
|
||||
connected: boolean;
|
||||
jobs: {
|
||||
waiting: number;
|
||||
active: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
delayed: number;
|
||||
paused: number;
|
||||
};
|
||||
workers?: {
|
||||
count: number;
|
||||
concurrency: number;
|
||||
};
|
||||
throughput?: {
|
||||
processed: number;
|
||||
failed: number;
|
||||
avgProcessingTime?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DatabaseStats {
|
||||
type: 'postgres' | 'mongodb' | 'questdb';
|
||||
name: string;
|
||||
connected: boolean;
|
||||
latency?: number;
|
||||
pool?: {
|
||||
size: number;
|
||||
active: number;
|
||||
idle: number;
|
||||
waiting?: number;
|
||||
max: number;
|
||||
};
|
||||
stats?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SystemHealth {
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
timestamp: string;
|
||||
uptime: number;
|
||||
memory: {
|
||||
used: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
};
|
||||
cpu?: {
|
||||
usage: number;
|
||||
loadAverage?: number[];
|
||||
};
|
||||
services: {
|
||||
cache: CacheStats;
|
||||
queues: QueueStats[];
|
||||
databases: DatabaseStats[];
|
||||
};
|
||||
errors?: string[];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue