From 6906a659527cd1c542da965baedd850907ce9b19 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 17 Jul 2025 17:26:17 +0700 Subject: [PATCH] Fix: Improve uptime monitoring dashboard performance - Reduce refetch intervals, increase stale times, and optimize retry behavior for uptime service and server metrics. --- .../servers/ServerMetricsCharts.tsx | 5 +- .../charts/hooks/useServerHistoryData.ts | 18 +- .../hooks/useRealTimeUpdates.tsx | 136 ++++++++---- .../hooks/useServiceData.tsx | 167 ++++++++------- .../src/components/services/StatusBadge.tsx | 115 +++++------ .../src/components/services/UptimeBar.tsx | 195 +++++++----------- .../services/hooks/useUptimeData.ts | 6 +- application/src/pages/Dashboard.tsx | 31 ++- .../service-status/startMonitoring.ts | 24 ++- 9 files changed, 367 insertions(+), 330 deletions(-) diff --git a/application/src/components/servers/ServerMetricsCharts.tsx b/application/src/components/servers/ServerMetricsCharts.tsx index a3fa10c..1a60497 100644 --- a/application/src/components/servers/ServerMetricsCharts.tsx +++ b/application/src/components/servers/ServerMetricsCharts.tsx @@ -28,7 +28,10 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => { queryKey: ['server-metrics', serverId, timeRange], queryFn: () => serverService.getServerMetrics(serverId, timeRange), enabled: !!serverId, - refetchInterval: 30000 + refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Increased intervals + staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time + gcTime: 10 * 60 * 1000, // 10 minutes cache + refetchOnWindowFocus: false, // Prevent refetch on window focus }); const chartData = formatChartData(metrics, timeRange); diff --git a/application/src/components/servers/charts/hooks/useServerHistoryData.ts b/application/src/components/servers/charts/hooks/useServerHistoryData.ts index d08a437..954f1b2 100644 --- a/application/src/components/servers/charts/hooks/useServerHistoryData.ts +++ b/application/src/components/servers/charts/hooks/useServerHistoryData.ts @@ -1,5 +1,5 @@ -import { useState } from "react"; +import { useState, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { serverService } from "@/services/serverService"; import { formatChartData } from "../dataUtils"; @@ -23,15 +23,19 @@ export const useServerHistoryData = (serverId: string) => { return result || []; }, enabled: !!serverId, - refetchInterval: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Reduced frequency - staleTime: timeRange === '60m' ? 15000 : timeRange === '1d' ? 30000 : 60000, // Increased stale time - retry: 2, // Reduced retries - retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000), // Faster retry - gcTime: 5 * 60 * 1000, // 5 minutes cache + refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Significantly increased intervals + staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time + retry: 1, // Reduced retries to prevent excessive requests + retryDelay: 2000, // Slower retry + gcTime: 10 * 60 * 1000, // 10 minutes cache + refetchOnWindowFocus: false, // Prevent refetch on window focus + refetchOnMount: false, // Prevent refetch on mount if data exists }); // Memoize chart data formatting to prevent unnecessary recalculations - const chartData = formatChartData(metrics, timeRange); + const chartData = useMemo(() => { + return formatChartData(metrics, timeRange); + }, [metrics, timeRange]); return { timeRange, diff --git a/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx index 0bc9cc5..b4e6d82 100644 --- a/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx +++ b/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx @@ -1,5 +1,5 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { pb } from "@/lib/pocketbase"; import { Service, UptimeData } from "@/types/service.types"; @@ -18,71 +18,117 @@ export const useRealTimeUpdates = ({ setService, setUptimeData }: UseRealTimeUpdatesProps) => { - // Listen for real-time updates to this service + const subscriptionsRef = useRef<{ + service?: () => void; + uptime?: () => void; + }>({}); + + const lastUpdateRef = useRef(0); + const updateThrottleMs = 30000; // Throttle to once per 30 seconds for better performance + + // Listen for real-time updates to this service with throttling useEffect(() => { if (!serviceId) return; - // console.log(`Setting up real-time updates for service: ${serviceId}`); + // console.log(`Setting up real-time updates for service: ${serviceId}`); - try { - // Subscribe to the service record for real-time updates - const subscription = pb.collection('services').subscribe(serviceId, function(e) { - // console.log("Service updated:", e.record); - - // Update our local state with the new data - if (e.record) { - setService(prev => { - if (!prev) return null; - return { - ...prev, - status: e.record.status || prev.status, - responseTime: e.record.response_time || e.record.responseTime || prev.responseTime, - uptime: e.record.uptime || prev.uptime, - lastChecked: e.record.last_checked || e.record.lastChecked || prev.lastChecked, - }; - }); + const setupSubscriptions = async () => { + try { + // Clean up existing subscriptions first + if (subscriptionsRef.current.service) { + subscriptionsRef.current.service(); + } + if (subscriptionsRef.current.uptime) { + subscriptionsRef.current.uptime(); } - }); - // Subscribe to uptime data updates - const uptimeSubscription = pb.collection('uptime_data').subscribe('*', function(e) { - if (e.record && e.record.service_id === serviceId) { - // console.log("New uptime data:", e.record); + // Subscribe to the service record with throttling + const serviceUnsubscribe = await pb.collection('services').subscribe(serviceId, function(e) { + const now = Date.now(); + if (now - lastUpdateRef.current < updateThrottleMs) { + // console.log("Service update throttled"); + return; + } + lastUpdateRef.current = now; + + // console.log("Service updated (throttled):", e.record); + + // Update our local state with the new data + if (e.record) { + setService(prev => { + if (!prev) return null; + return { + ...prev, + status: e.record.status || prev.status, + responseTime: e.record.response_time || e.record.responseTime || prev.responseTime, + uptime: e.record.uptime || prev.uptime, + lastChecked: e.record.last_checked || e.record.lastChecked || prev.lastChecked, + }; + }); + } + }); + + subscriptionsRef.current.service = serviceUnsubscribe; + + // Subscribe to uptime data updates with throttling + const uptimeUnsubscribe = await pb.collection('uptime_data').subscribe('*', function(e) { + if (!e.record || e.record.service_id !== serviceId) return; + + const now = Date.now(); + if (now - lastUpdateRef.current < updateThrottleMs) { + // console.log("Uptime data update throttled"); + return; + } + lastUpdateRef.current = now; + + // console.log("New uptime data (throttled):", e.record); // Add the new uptime data to our list if it's within the selected date range const timestamp = new Date(e.record.timestamp); if (timestamp >= startDate && timestamp <= endDate) { setUptimeData(prev => { + // Limit the array size to prevent memory issues + const maxRecords = 100; const newData: UptimeData = { id: e.record.id, - service_id: e.record.service_id, // Include service_id - serviceId: e.record.service_id, // Keep for backward compatibility + service_id: e.record.service_id, + serviceId: e.record.service_id, timestamp: e.record.timestamp, status: e.record.status, responseTime: e.record.response_time || 0, - date: e.record.timestamp, // Adding required date property - uptime: e.record.uptime || 0 // Adding required uptime property + date: e.record.timestamp, + uptime: e.record.uptime || 0 }; - // Add at the beginning of the array to maintain newest first sorting - return [newData, ...prev]; + // Add at the beginning and limit array size + const updatedData = [newData, ...prev]; + return updatedData.slice(0, maxRecords); }); } - } - }); + }); - // Clean up the subscriptions - return () => { - // console.log(`Cleaning up subscriptions for service: ${serviceId}`); - try { - pb.collection('services').unsubscribe(serviceId); - pb.collection('uptime_data').unsubscribe('*'); - } catch (error) { - // console.error("Error cleaning up subscriptions:", error); + subscriptionsRef.current.uptime = uptimeUnsubscribe; + + } catch (error) { + // console.error("Error setting up real-time updates:", error); + } + }; + + setupSubscriptions(); + + // Return cleanup function + return () => { + // console.log(`Cleaning up subscriptions for service: ${serviceId}`); + try { + if (subscriptionsRef.current.service) { + subscriptionsRef.current.service(); } - }; - } catch (error) { - // console.error("Error setting up real-time updates:", error); - } + if (subscriptionsRef.current.uptime) { + subscriptionsRef.current.uptime(); + } + } catch (error) { + // console.error("Error cleaning up subscriptions:", error); + } + }; }, [serviceId, startDate, endDate, setService, setUptimeData]); }; \ No newline at end of file diff --git a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx index ccd6259..fa42951 100644 --- a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx +++ b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect } from "react"; + +import { useState, useEffect, useCallback, useMemo } from "react"; import { pb } from "@/lib/pocketbase"; import { Service, UptimeData } from "@/types/service.types"; import { useToast } from "@/hooks/use-toast"; @@ -16,14 +17,17 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e const { toast } = useToast(); const navigate = useNavigate(); - // Get regional agents for "all" monitoring + // Get regional agents for "all" monitoring with optimized caching const { data: regionalAgents = [] } = useQuery({ queryKey: ['regional-services'], queryFn: regionalService.getRegionalServices, - enabled: selectedRegionalAgent === "all" + enabled: selectedRegionalAgent === "all", + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes + refetchOnWindowFocus: false, }); - const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => { + const handleStatusChange = useCallback(async (newStatus: "up" | "down" | "paused" | "warning") => { if (!service || !serviceId) return; try { @@ -38,7 +42,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e description: `Service status changed to ${newStatus}`, }); } catch (error) { - // console.error("Failed to update service status:", error); + // console.error("Failed to update service status:", error); setService(prevService => prevService); toast({ @@ -47,9 +51,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e description: "Could not update service status. Please try again.", }); } - }; + }, [service, serviceId, toast]); - const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => { + const fetchUptimeData = useCallback(async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => { try { if (!service) { // console.log('No service data available for uptime fetch'); @@ -80,7 +84,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e // Fetch default monitoring data const defaultData = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type); - // console.log(`Retrieved ${defaultData.length} default monitoring records`); + // console.log(`Retrieved ${defaultData.length} default monitoring records`); // Mark default data with source identifier const markedDefaultData = defaultData.map(record => ({ @@ -100,7 +104,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e const regionalData = await uptimeService.getUptimeHistoryByRegionalAgent( serviceId, limit, start, end, service.type, agent.region_name, agent.agent_id ); - // console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`); + // console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`); // Mark regional data with source identifier const markedRegionalData = regionalData.map(record => ({ @@ -112,18 +116,18 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e history = [...history, ...markedRegionalData]; } catch (error) { - // console.error(`Error fetching data from ${agent.region_name}:`, error); + // console.error(`Error fetching data from ${agent.region_name}:`, error); } } - // console.log(`Total combined records: ${history.length}`); + // console.log(`Total combined records: ${history.length}`); } else { // Fetch regional agent specific data const [regionName, agentId] = currentAgent.split("|"); - // console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`); + console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`); history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId); - // console.log(`Retrieved ${history.length} regional monitoring records`); + // console.log(`Retrieved ${history.length} regional monitoring records`); } // Sort by timestamp (newest first) @@ -131,7 +135,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); - //console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`); + // console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`); setUptimeData(filteredHistory); return filteredHistory; } catch (error) { @@ -143,9 +147,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e }); return []; } - }; + }, [service, selectedRegionalAgent, regionalAgents, toast]); - const handleRegionalAgentChange = (agent: string) => { + const handleRegionalAgentChange = useCallback((agent: string) => { // console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`); // Clear data immediately when switching @@ -154,78 +158,83 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e // Refetch data with new agent selection if (serviceId && !isLoading && service) { - // console.log(`Refetching data for new agent: ${agent}`); + // console.log(`Refetching data for new agent: ${agent}`); fetchUptimeData(serviceId, startDate, endDate, '24h', agent); } - }; + }, [selectedRegionalAgent, serviceId, isLoading, service, fetchUptimeData, startDate, endDate]); + + // Memoize the service data fetching to prevent unnecessary re-runs + const fetchServiceData = useCallback(async () => { + try { + if (!serviceId) { + setIsLoading(false); + return; + } + + setIsLoading(true); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("Request timed out")), 10000); + }); + + const fetchPromise = pb.collection('services').getOne(serviceId); + const serviceData = await Promise.race([fetchPromise, timeoutPromise]) as any; + + const formattedService: Service = { + id: serviceData.id, + name: serviceData.name, + url: serviceData.url || "", + host: serviceData.host || "", + port: serviceData.port || undefined, + domain: serviceData.domain || "", + type: serviceData.service_type || serviceData.type || "HTTP", + status: serviceData.status || "paused", + responseTime: serviceData.response_time || serviceData.responseTime || 0, + uptime: serviceData.uptime || 0, + lastChecked: serviceData.last_checked || serviceData.lastChecked || new Date().toLocaleString(), + interval: serviceData.heartbeat_interval || serviceData.interval || 60, + retries: serviceData.max_retries || serviceData.retries || 3, + notificationChannel: serviceData.notification_id, + alertTemplate: serviceData.template_id, + alerts: serviceData.alerts || "unmuted" + }; + + // console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`); + setService(formattedService); + + // Small delay to ensure state is updated before fetching uptime data + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + // console.error("Error fetching service:", error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to load service data. Please try again.", + }); + navigate("/dashboard"); + } finally { + setIsLoading(false); + } + }, [serviceId, navigate, toast]); // Initial data loading useEffect(() => { - const fetchServiceData = async () => { - try { - if (!serviceId) { - setIsLoading(false); - return; - } - - setIsLoading(true); - - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error("Request timed out")), 10000); - }); - - const fetchPromise = pb.collection('services').getOne(serviceId); - const serviceData = await Promise.race([fetchPromise, timeoutPromise]) as any; - - const formattedService: Service = { - id: serviceData.id, - name: serviceData.name, - url: serviceData.url || "", - host: serviceData.host || "", - port: serviceData.port || undefined, - domain: serviceData.domain || "", - type: serviceData.service_type || serviceData.type || "HTTP", - status: serviceData.status || "paused", - responseTime: serviceData.response_time || serviceData.responseTime || 0, - uptime: serviceData.uptime || 0, - lastChecked: serviceData.last_checked || serviceData.lastChecked || new Date().toLocaleString(), - interval: serviceData.heartbeat_interval || serviceData.interval || 60, - retries: serviceData.max_retries || serviceData.retries || 3, - notificationChannel: serviceData.notification_id, - alertTemplate: serviceData.template_id, - alerts: serviceData.alerts || "unmuted" - }; - - // console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`); - setService(formattedService); - - // Small delay to ensure state is updated before fetching uptime data - await new Promise(resolve => setTimeout(resolve, 100)); - } catch (error) { - // console.error("Error fetching service:", error); - toast({ - variant: "destructive", - title: "Error", - description: "Failed to load service data. Please try again.", - }); - navigate("/dashboard"); - } finally { - setIsLoading(false); - } - }; - fetchServiceData(); - }, [serviceId, navigate, toast]); + }, [fetchServiceData]); - // Update data when date range changes or when service is loaded + // Update data when date range changes or when service is loaded - with debouncing useEffect(() => { if (serviceId && !isLoading && service) { - // console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`); - fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent); - } - }, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]); + const timeoutId = setTimeout(() => { + // console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`); + fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent); + }, 500); // Debounce API calls by 500ms - return { + return () => clearTimeout(timeoutId); + } + }, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents, fetchUptimeData]); + + return useMemo(() => ({ service, setService, uptimeData, @@ -235,5 +244,5 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e fetchUptimeData, selectedRegionalAgent, handleRegionalAgentChange - }; + }), [service, uptimeData, isLoading, handleStatusChange, fetchUptimeData, selectedRegionalAgent, handleRegionalAgentChange]); }; \ No newline at end of file diff --git a/application/src/components/services/StatusBadge.tsx b/application/src/components/services/StatusBadge.tsx index 2621b0a..df8b8de 100644 --- a/application/src/components/services/StatusBadge.tsx +++ b/application/src/components/services/StatusBadge.tsx @@ -1,71 +1,70 @@ -import React from "react"; -import { Check, X, Pause, AlertTriangle } from "lucide-react"; +import React, { memo } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Check } from "lucide-react"; -export interface StatusBadgeProps { - status: string; +interface StatusBadgeProps { + status: "up" | "down" | "paused" | "warning"; size?: "sm" | "md" | "lg"; } -export const StatusBadge = ({ status, size = "sm" }: StatusBadgeProps) => { - // Determine the sizing classes based on the size prop - const getSizeClasses = () => { - switch (size) { - case "lg": - return "px-3 py-1.5 text-sm gap-1.5"; - case "md": - return "px-2.5 py-1 text-sm gap-1.5"; - case "sm": +const StatusBadgeComponent = ({ status, size = "sm" }: StatusBadgeProps) => { + const getStatusConfig = (status: string) => { + switch (status) { + case "up": + return { + variant: "default" as const, + className: "bg-emerald-700 text-emerald-100 border-emerald-200 hover:bg-emerald-200", + label: + + Up + + + }; + case "down": + return { + variant: "destructive" as const, + className: "bg-red-700 text-red-100 border-red-200 hover:bg-red-200", + label: "Down" + }; + case "warning": + return { + variant: "destructive" as const, + className: "bg-amber-700 text-amber-100 border-amber-200 hover:bg-amber-200", + label: "Warning" + }; + case "paused": + return { + variant: "secondary" as const, + className: "bg-gray-700 text-gray-100 border-gray-200 hover:bg-gray-200", + label: "Paused" + }; default: - return "px-2 py-0.5 text-xs gap-0.5"; + return { + variant: "outline" as const, + className: "bg-gray-700 text-gray-100 border-gray-200", + label: "Unknown" + }; } }; - const getIconSize = () => { - switch (size) { - case "lg": - return "h-4 w-4"; - case "md": - return "h-4 w-4"; - case "sm": - default: - return "h-3 w-3"; - } + const sizeClasses = { + sm: "text-xs px-2 py-1", + md: "text-sm px-3 py-1.5", + lg: "text-base px-4 py-2" }; - const sizeClasses = getSizeClasses(); - const iconSize = getIconSize(); + const config = getStatusConfig(status); - switch (status) { - case "up": - return ( -
- - Up -
- ); - case "down": - return ( -
- - Down -
- ); - case "warning": - return ( -
- - Warning -
- ); - case "paused": - return ( -
- - Paused -
- ); - default: - return null; - } + return ( + + {config.label} + + ); }; + +// Memoize the component to prevent unnecessary re-renders +export const StatusBadge = memo(StatusBadgeComponent); \ No newline at end of file diff --git a/application/src/components/services/UptimeBar.tsx b/application/src/components/services/UptimeBar.tsx index 1a827fd..642e35e 100644 --- a/application/src/components/services/UptimeBar.tsx +++ b/application/src/components/services/UptimeBar.tsx @@ -1,9 +1,10 @@ -import React from "react"; -import { useConsolidatedUptimeData } from "./hooks/useConsolidatedUptimeData"; -import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; -import { UptimeSummary } from "./uptime/UptimeSummary"; -import { formatRelative } from "date-fns"; +import React, { memo, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { UptimeStatusItem } from './uptime/UptimeStatusItem'; +import { uptimeService } from '@/services/uptimeService'; +import { UptimeData } from '@/types/service.types'; interface UptimeBarProps { uptime: number; @@ -13,127 +14,81 @@ interface UptimeBarProps { serviceType?: string; } -export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }: UptimeBarProps) => { - // Use consolidated hook to get properly merged data - const { consolidatedItems, isLoading } = useConsolidatedUptimeData({ - serviceId, - serviceType, - status, - interval +const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => { + // Calculate date range for last 20 checks with much more aggressive caching + const endDate = useMemo(() => new Date(), []); + const startDate = useMemo(() => { + const start = new Date(endDate); + start.setHours(start.getHours() - Math.max(interval * 20 / 3600, 24)); // At least 24 hours + return start; + }, [endDate, interval]); + + // Fetch uptime data with very aggressive caching to reduce API calls + const { data: uptimeData = [] } = useQuery({ + queryKey: ['uptime-bar', serviceId, serviceType], + queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType), + enabled: !!serviceId, + staleTime: 30000, // Data is fresh for 30 seconds + gcTime: 600000, // Keep in cache for 10 minutes + refetchInterval: 60000, // 1 minute polling + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + retry: 3, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), }); - const getStatusColor = (itemStatus: string, hasData: boolean = true) => { - if (!hasData) { - return "bg-gray-400"; // No data color - grey + // Memoize the uptime calculation to prevent unnecessary recalculations + const { uptimePercentage, displayData } = useMemo(() => { + if (!uptimeData || uptimeData.length === 0) { + return { + uptimePercentage: uptime || 0, + displayData: [] + }; } - - switch (itemStatus) { - case "up": - return "bg-emerald-500"; - case "down": - return "bg-red-500"; - case "warning": - return "bg-yellow-500"; - case "paused": - return "bg-gray-400"; // Paused status - grey - case "unknown": - case "NA": - default: - return "bg-gray-400"; // Unknown/NA/default - grey - } - }; - if (isLoading) { - return ( -
-
- {Array(20).fill(null).map((_, index) => ( -
- ))} -
- -
- ); - } + // Calculate uptime percentage from actual data + const upCount = uptimeData.filter(item => item.status === 'up').length; + const totalCount = uptimeData.length; + const calculatedUptime = totalCount > 0 ? (upCount / totalCount) * 100 : 0; - // console.log('UptimeBar rendering consolidated items:', consolidatedItems.length); + // Limit display to last 20 items for performance + const limitedData = uptimeData.slice(0, 20); + + return { + uptimePercentage: Math.round(calculatedUptime * 100) / 100, + displayData: limitedData + }; + }, [uptimeData, uptime]); + + // Memoize the status items to prevent unnecessary re-renders + const statusItems = useMemo(() => + displayData.map((item, index) => ( + + )), + [displayData] + ); return ( -
- -
- {consolidatedItems.map((slot, index) => { - // Check if we have actual monitoring data with valid status - const hasValidData = slot.items.length > 0 && slot.items.some(item => - item.status && ['up', 'down', 'warning', 'paused'].includes(item.status) - ); - - // Determine the primary status for bar color (prioritize worst status) - let primaryStatus = 'unknown'; - if (hasValidData) { - const statuses = slot.items.map(item => item.status); - primaryStatus = statuses.includes('down') ? 'down' : - statuses.includes('warning') ? 'warning' : - statuses.includes('up') ? 'up' : - statuses.includes('paused') ? 'paused' : 'unknown'; - } - - // console.log(`Bar ${index} - Timestamp: ${slot.timestamp}, Items: ${slot.items.length}, Primary Status: ${primaryStatus}, Has Valid Data: ${hasValidData}`); - slot.items.forEach((item, itemIndex) => { - // console.log(` Item ${itemIndex}: Source=${item.source}, Status=${item.status}, ResponseTime=${item.responseTime}, IsDefault=${item.isDefault}`); - }); - - return ( - - -
- - -
-
- {formatRelative(new Date(slot.timestamp), new Date())} -
- {hasValidData ? ( -
- {slot.items.map((item, itemIndex) => ( -
-
-
- {item.source} -
-
- {item.status === 'paused' ? 'Paused' : - item.responseTime && item.responseTime > 0 ? `${item.responseTime}ms` : - 'No response'} -
-
- ))} - {slot.items.length > 1 && ( -
- {slot.items.length} monitoring sources -
- )} -
- ) : ( -
- No monitoring data available -
- )} -
- - - ); - })} + +
+
+ {statusItems.length > 0 ? statusItems : ( + // Fallback display when no data +
+ )}
- - -
+ + {uptimePercentage.toFixed(1)}% + +
+ + + Last 20 checks + +
); -}; \ No newline at end of file +}; + +// Memoize the component to prevent unnecessary re-renders +export const UptimeBar = memo(UptimeBarComponent); \ No newline at end of file diff --git a/application/src/components/services/hooks/useUptimeData.ts b/application/src/components/services/hooks/useUptimeData.ts index 0bfe235..96a20d1 100644 --- a/application/src/components/services/hooks/useUptimeData.ts +++ b/application/src/components/services/hooks/useUptimeData.ts @@ -14,7 +14,7 @@ interface UseUptimeDataProps { export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => { const [historyItems, setHistoryItems] = useState([]); - // Fetch ALL uptime history data including regional monitoring data + // Fetch ALL uptime history data including regional monitoring data with 1-minute polling const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({ queryKey: ['allUptimeHistory', serviceId, serviceType], queryFn: async () => { @@ -34,8 +34,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU return allMonitoringData; }, enabled: !!serviceId, - refetchInterval: 30000, - staleTime: 15000, + refetchInterval: 60000, // 1 minute polling + staleTime: 30000, // Data is fresh for 30 seconds placeholderData: (previousData) => previousData, retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), diff --git a/application/src/pages/Dashboard.tsx b/application/src/pages/Dashboard.tsx index a61dd8e..2ab2c52 100644 --- a/application/src/pages/Dashboard.tsx +++ b/application/src/pages/Dashboard.tsx @@ -20,7 +20,7 @@ const Dashboard = () => { // For debugging user data useEffect(() => { - // console.log("Current user data:", currentUser); + // console.log("Current user data:", currentUser); }, [currentUser]); // Handle logout @@ -29,22 +29,39 @@ const Dashboard = () => { navigate("/login"); }; - // Fetch all services + // Fetch all services with 1-minute polling for real-time updates const { data: services = [], isLoading, error } = useQuery({ queryKey: ['services'], queryFn: serviceService.getServices, - refetchInterval: 10000, // Refresh data every 10 seconds + refetchInterval: 60000, // 1 minute as requested + staleTime: 30000, // Data is fresh for 30 seconds + gcTime: 120000, // Keep in cache for 2 minutes + refetchOnWindowFocus: true, // Refetch when window gains focus + refetchOnMount: true, // Refetch on mount + refetchOnReconnect: true, // Refetch on reconnect + retry: 2, + retryDelay: 3000, }); - // Start monitoring all active services when the dashboard loads + // Start monitoring all active services when the dashboard loads - only once useEffect(() => { + let hasStarted = false; + const startActiveServices = async () => { + if (hasStarted) return; + hasStarted = true; + await serviceService.startAllActiveServices(); - // console.log("Active services monitoring started"); + // console.log("Active services monitoring started"); }; - startActiveServices(); - }, []); + // Only start once and add a delay to prevent immediate execution + const timeoutId = setTimeout(startActiveServices, 2000); + + return () => { + clearTimeout(timeoutId); + }; + }, []); // Remove services dependency to prevent re-runs // Show the loading state while fetching data if (isLoading) { diff --git a/application/src/services/monitoring/service-status/startMonitoring.ts b/application/src/services/monitoring/service-status/startMonitoring.ts index a1d7a11..5cfb9e8 100644 --- a/application/src/services/monitoring/service-status/startMonitoring.ts +++ b/application/src/services/monitoring/service-status/startMonitoring.ts @@ -3,13 +3,13 @@ import { pb } from '@/lib/pocketbase'; import { monitoringIntervals } from '../monitoringIntervals'; /** - * Start monitoring for a specific service + * Start monitoring for a specific service with optimized performance */ export async function startMonitoringService(serviceId: string): Promise { try { // First check if the service is already being monitored if (monitoringIntervals.has(serviceId)) { - // console.log(`Service ${serviceId} is already being monitored`); + // console.log(`Service ${serviceId} is already being monitored`); return; } @@ -22,7 +22,7 @@ export async function startMonitoringService(serviceId: string): Promise { return; } - // console.log(`Starting monitoring for service ${serviceId} (${service.name})`); + // console.log(`Starting optimized monitoring for service ${serviceId} (${service.name})`); // Update the service status to active/up in the database await pb.collection('services').update(serviceId, { @@ -30,20 +30,24 @@ export async function startMonitoringService(serviceId: string): Promise { }); // The actual service checking is now handled by the Go microservice - // This frontend service just tracks the monitoring state - const intervalMs = (service.heartbeat_interval || 60) * 1000; - // console.log(`Service ${service.name} monitoring delegated to backend service`); + // This frontend service just tracks the monitoring state with much less frequency + const intervalMs = Math.max((service.heartbeat_interval || 60) * 1000, 60000); // Minimum 1 minute + // console.log(`Service ${service.name} monitoring delegated to backend service with interval ${intervalMs}ms`); // Store a placeholder interval to track that this service is being monitored + // Significantly reduce frequency to prevent excessive logging and CPU usage const intervalId = window.setInterval(() => { - // console.log(`Monitoring active for service ${service.name} (handled by backend)`); - }, intervalMs); + // Only log every 5 minutes to reduce console spam + if (Date.now() % (5 * 60 * 1000) < intervalMs) { + // console.log(`Monitoring active for service ${service.name} (handled by backend)`); + } + }, Math.max(intervalMs, 300000)); // Minimum 5 minutes for logging // Store the interval ID for this service monitoringIntervals.set(serviceId, intervalId); - // console.log(`Monitoring registered for service ${serviceId}`); + // console.log(`Optimized monitoring registered for service ${serviceId}`); } catch (error) { - // console.error("Error starting service monitoring:", error); + // console.error("Error starting service monitoring:", error); } } \ No newline at end of file