mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-30 00:22:15 +00:00
feat: Add monitoring page with Prometheus metrics, real-time alerts, and comprehensive system health dashboard
- Add "use client" directive to monitoring page for client-side rendering - Add useState/useEffect hooks for MetricSample/Alert state management with loading/error states - Add loadMonitoringData with Promise.all for api.monitoring.metrics/alerts, handleCustomQuery with PromQL execution - Add 8 predefinedQueries: uptime, CPU/memory/network usage, connections, response time, error/request rates
This commit is contained in:
@@ -0,0 +1,301 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Activity, AlertTriangle, TrendingUp, TrendingDown, Eye, RefreshCw } from "lucide-react";
|
||||||
|
import { api, Alert, MetricSample } from "@/lib/api";
|
||||||
|
import { ErrorAlert } from "@/components/ui/error-alert";
|
||||||
|
import { LoadingState, ActionLoading } from "@/components/ui/loading-state";
|
||||||
|
|
||||||
|
export default function MonitoringPage() {
|
||||||
|
const [metrics, setMetrics] = useState<MetricSample[]>([]);
|
||||||
|
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||||
|
const [selectedQuery, setSelectedQuery] = useState("up");
|
||||||
|
const [customQuery, setCustomQuery] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const predefinedQueries = [
|
||||||
|
{ label: "System Uptime", query: "up" },
|
||||||
|
{ label: "CPU Usage", query: "rate(cpu_usage_total[5m])" },
|
||||||
|
{ label: "Memory Usage", query: "rate(memory_usage_bytes[5m])" },
|
||||||
|
{ label: "Network Traffic", query: "rate(network_traffic_bytes[5m])" },
|
||||||
|
{ label: "Active Connections", query: "active_connections_total" },
|
||||||
|
{ label: "Response Time", query: "http_request_duration_seconds" },
|
||||||
|
{ label: "Error Rate", query: "rate(http_requests_total{status=~\"5..\"}[5m])" },
|
||||||
|
{ label: "Request Rate", query: "rate(http_requests_total[5m])" }
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadMonitoringData();
|
||||||
|
}, [selectedQuery]);
|
||||||
|
|
||||||
|
const loadMonitoringData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const [metricsData, alertsData] = await Promise.all([
|
||||||
|
api.monitoring.metrics(selectedQuery),
|
||||||
|
api.monitoring.alerts()
|
||||||
|
]);
|
||||||
|
|
||||||
|
setMetrics(metricsData.data);
|
||||||
|
setAlerts(alertsData);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Failed to load monitoring data";
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error("Failed to load monitoring data:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomQuery = async () => {
|
||||||
|
if (!customQuery.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const metricsData = await api.monitoring.metrics(customQuery);
|
||||||
|
setMetrics(metricsData.data);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Failed to execute custom query";
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error("Failed to execute custom query:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlertSeverityColor = (severity: string) => {
|
||||||
|
switch (severity.toLowerCase()) {
|
||||||
|
case 'critical': return 'destructive';
|
||||||
|
case 'warning': return 'warning';
|
||||||
|
case 'info': return 'secondary';
|
||||||
|
default: return 'secondary';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlertStatusColor = (state: string) => {
|
||||||
|
switch (state.toLowerCase()) {
|
||||||
|
case 'firing': return 'destructive';
|
||||||
|
case 'resolved': return 'success';
|
||||||
|
default: return 'secondary';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMetricValue = (value: number) => {
|
||||||
|
if (value >= 1000000) return `${(value / 1000000).toFixed(2)}M`;
|
||||||
|
if (value >= 1000) return `${(value / 1000).toFixed(2)}K`;
|
||||||
|
return value.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading && metrics.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="p-8 space-y-6">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="h-8 bg-gray-200 rounded w-48 mb-2"></div>
|
||||||
|
<div className="h-4 bg-gray-200 rounded w-96"></div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="animate-pulse">
|
||||||
|
<div className="h-64 bg-gray-200 rounded"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<h3 className="text-red-800 font-medium">Error loading monitoring data</h3>
|
||||||
|
<p className="text-red-600 text-sm mt-1">{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Activity className="size-8 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Monitoring</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">Advanced metrics and alerting dashboard.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={loadMonitoringData} disabled={loading}>
|
||||||
|
<RefreshCw className={`size-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Query Controls */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Metric Query</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{predefinedQueries.map((pq) => (
|
||||||
|
<Button
|
||||||
|
key={pq.query}
|
||||||
|
variant={selectedQuery === pq.query ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedQuery(pq.query)}
|
||||||
|
>
|
||||||
|
{pq.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter custom PromQL query..."
|
||||||
|
value={customQuery}
|
||||||
|
onChange={(e) => setCustomQuery(e.target.value)}
|
||||||
|
className="flex-1 h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50"
|
||||||
|
/>
|
||||||
|
<Button onClick={handleCustomQuery} disabled={!customQuery.trim() || loading}>
|
||||||
|
Execute
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Current query: <code className="bg-muted px-2 py-1 rounded">{selectedQuery}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Metrics Display */}
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="size-5" />
|
||||||
|
Metric Values
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{metrics.length > 0 ? (
|
||||||
|
metrics.map((metric, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 border rounded">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm">
|
||||||
|
{new Date(metric.timestamp).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{metric.timestamp}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-mono text-lg">
|
||||||
|
{formatMetricValue(metric.value)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{metric.value > 0 ? (
|
||||||
|
<TrendingUp className="inline size-3 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<TrendingDown className="inline size-3 text-red-500" />
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground text-center py-8">No metrics data available</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="size-5" />
|
||||||
|
Active Alerts
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{alerts.length > 0 ? (
|
||||||
|
alerts.map((alert, index) => (
|
||||||
|
<div key={index} className="border rounded p-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="font-medium text-sm">{alert.name}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Badge variant={getAlertSeverityColor(alert.severity)}>
|
||||||
|
{alert.severity}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={getAlertStatusColor(alert.state)}>
|
||||||
|
{alert.state}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">{alert.summary}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{alert.description}</p>
|
||||||
|
<div className="flex items-center justify-between mt-2 text-xs text-muted-foreground">
|
||||||
|
<span>Started: {new Date(alert.startsAt).toLocaleString()}</span>
|
||||||
|
{alert.endsAt && (
|
||||||
|
<span>Ended: {new Date(alert.endsAt).toLocaleString()}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground text-center py-8">No active alerts</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* System Health Overview */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Eye className="size-5" />
|
||||||
|
System Health Overview
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<div className="text-center p-4 border rounded">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{alerts.filter(a => a.state === 'resolved').length}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">Resolved Alerts</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 border rounded">
|
||||||
|
<div className="text-2xl font-bold text-red-600">
|
||||||
|
{alerts.filter(a => a.state === 'firing').length}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">Active Alerts</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 border rounded">
|
||||||
|
<div className="text-2xl font-bold text-orange-600">
|
||||||
|
{alerts.filter(a => a.severity === 'critical').length}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">Critical Alerts</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 border rounded">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{metrics.length}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">Data Points</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user