diff --git a/src/Charts/SystemChart.jsx b/src/Charts/SystemChart.jsx index 8ce0eca..478d7cb 100644 --- a/src/Charts/SystemChart.jsx +++ b/src/Charts/SystemChart.jsx @@ -43,7 +43,14 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`; }, [metricName, device, source_id]); - const formatMetricData = (dataArray) => { + const formatMetricData = (responseData) => { + const dataArray = Array.isArray(responseData) ? responseData : responseData.data; + + if (!Array.isArray(dataArray)) { + console.error('Expected array but got:', responseData); + return []; + } + return dataArray.map(item => ({ ...item, timestamp: item.timestamp, @@ -74,6 +81,7 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { }; const step = calculateStep(start, end); + const data = await metricsService.fetchMetricsRange( metricName, Math.floor(start.getTime() / 1000), @@ -82,7 +90,8 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { extendedFilters ); - const formattedData = formatMetricData(data); + const responseData = Array.isArray(data) ? data : data.data; + const formattedData = formatMetricData(responseData); setRawData(formattedData); if (formattedData.length > 0) { @@ -108,21 +117,19 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { const end = new Date(); const start = new Date(end.getTime() - TIME_WINDOW_MS); + const cutoffTime = Date.now() - TIME_WINDOW_MS; fetchHistoricalData(start, end).finally(() => setIsLoading(false)); return metricsService.subscribeToMetric( subscriptionKey, (newData) => { setRawData(prev => { - const now = Date.now(); - const cutoffTime = now - TIME_WINDOW_MS; - - const formattedNewData = formatMetricData(newData) + const actualData = Array.isArray(newData) ? newData : newData.data; + const formattedNewData = formatMetricData(actualData) .filter(point => point.timestamp >= cutoffTime); const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime); - // Объединяем данные, удаляем дубликаты const merged = [...filteredPrev, ...formattedNewData] .filter((v, i, a) => a.findIndex(t => @@ -134,7 +141,7 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { return merged; }); }, - 5000, // Интервал обновления 5 секунд + 5000, { ...filters, ...(device && { device }), @@ -154,7 +161,6 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { } }; - // Обновляем логи статусов useEffect(() => { if (rawData.length > 0) { const logs = []; diff --git a/src/Charts2/Components/metricsService.jsx b/src/Charts2/Components/metricsService.jsx index 7cf4d81..90da3ed 100644 --- a/src/Charts2/Components/metricsService.jsx +++ b/src/Charts2/Components/metricsService.jsx @@ -1,90 +1,157 @@ -import { io } from 'socket.io-client'; - class MetricsService { - constructor(baseUrl) { - this.baseUrl = baseUrl || window.location.origin; + constructor() { + this.baseUrl = 'ws://localhost:3001/api/metrics-ws'; this.socket = null; this.subscriptions = new Map(); this.pendingRequests = new Map(); - window.addEventListener('beforeunload', this.cleanupAll.bind(this)); - window.addEventListener('pagehide', this.cleanupAll.bind(this)); + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + this.reconnectDelay = 5000; - window.addEventListener('beforeunload', () => { - this.cleanupAll(); - }); + window.addEventListener('beforeunload', () => this.cleanupAll()); + window.addEventListener('pagehide', () => this.cleanupAll()); + } + + handleServerMessage(msg) { + try { + if (!msg || typeof msg !== 'object') { + console.error('Invalid message format', msg); + return; + } + + const { event, data, requestId } = msg; + + switch (event) { + case 'metrics-data': + if (requestId && this.pendingRequests.has(requestId)) { + const { resolve } = this.pendingRequests.get(requestId); + resolve(data); + this.pendingRequests.delete(requestId); + } else { + const metricKey = data.metric; + const callbacks = this.subscriptions.get(metricKey) || []; + callbacks.forEach(cb => cb(data)); + } + + break; + + case 'metrics-error': + if (requestId && this.pendingRequests.has(requestId)) { + const { reject } = this.pendingRequests.get(requestId); + reject(new Error(data.error)); + this.pendingRequests.delete(requestId); + } + break; + + default: + console.warn('Unknown message type:', event); + } + } catch (error) { + console.error('Error processing message:', error, msg); + } } connectWebSocket() { - if (this.socket) { - console.log('WebSocket already exists'); + if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) { return; } console.log('Connecting WebSocket...'); - this.socket = io(`${this.baseUrl.replace('http', 'ws')}/api/metrics-ws`, { - transports: ['websocket'], - withCredentials: true, - }); + this.socket = new WebSocket(this.baseUrl); - this.socket.on('connect', () => { + this.socket.addEventListener('open', () => { console.log('WebSocket connected'); - // Восстанавливаем подписки при переподключении + this.reconnectAttempts = 0; this.subscriptions.forEach((_, metricKey) => { - const [metric, query] = metricKey.split('?'); const filters = this.parseFiltersFromKey(metricKey); - this.socket.emit('subscribe-metric', { metric, filters }); + const [metric] = metricKey.split('?'); + this.sendMessage('subscribe-metric', { metric, filters }); }); }); - this.socket.on('disconnect', () => { + this.socket.addEventListener('close', () => { console.log('WebSocket disconnected'); this.socket = null; + this.scheduleReconnect(); }); - this.socket.on('metrics-data', ({ metric, data, requestId }) => { - if (requestId && this.pendingRequests.has(requestId)) { - const { resolve } = this.pendingRequests.get(requestId); - resolve(data); - this.pendingRequests.delete(requestId); - return; - } - - const callbacks = this.subscriptions.get(metric) || []; - callbacks.forEach(cb => cb(data)); + this.socket.addEventListener('error', (err) => { + console.error('WebSocket error:', err); }); - this.socket.on('metrics-error', ({ error, requestId }) => { - if (requestId && this.pendingRequests.has(requestId)) { - const { reject } = this.pendingRequests.get(requestId); - reject(new Error(error)); - this.pendingRequests.delete(requestId); + this.socket.addEventListener('message', (event) => { + try { + const msg = JSON.parse(event.data); + this.handleServerMessage(msg); + } catch (e) { + console.error('Error parsing WS message:', e); } }); } + scheduleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.warn('Max reconnect attempts reached'); + return; + } + + this.reconnectAttempts++; + const delay = this.reconnectDelay * this.reconnectAttempts; + console.log(`Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`); + + setTimeout(() => { + this.connectWebSocket(); + }, delay); + } + + sendMessage(event, data) { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + if (this.socket && this.socket.readyState === WebSocket.CONNECTING) { + const waitForOpen = () => { + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify({ event, data })); + } else if (this.socket.readyState === WebSocket.CONNECTING) { + setTimeout(waitForOpen, 100); + } + }; + waitForOpen(); + } else { + console.warn('WebSocket not connected, cannot send:', event); + this.connectWebSocket(); + } + return; + } + + this.socket.send(JSON.stringify({ event, data })); + } + async fetchMetricsRange(metric, start, end, step = 15, filters = {}) { return new Promise((resolve, reject) => { this.connectWebSocket(); - const requestId = `range-${Date.now()}`; - this.pendingRequests.set(requestId, { resolve, reject }); - this.socket.emit('get-metrics', { - metric, - start, - end, - step, - filters, - isRangeQuery: true, - requestId + // Таймаут для очистки + const timeout = setTimeout(() => { + reject(new Error('Request timeout')); + this.pendingRequests.delete(requestId); + }, 12000); + + this.pendingRequests.set(requestId, { + resolve: (responseData) => { + clearTimeout(timeout); + const data = Array.isArray(responseData) ? responseData : + (responseData?.data || []); + resolve(data); + }, + reject: (err) => { + clearTimeout(timeout); + reject(err); + } }); - setTimeout(() => { - if (this.pendingRequests.has(requestId)) { - reject(new Error('Request timeout')); - this.pendingRequests.delete(requestId); - } - }, 30000); + this.sendMessage('get-metrics', { + metric, start, end, step, filters, isRangeQuery: true, requestId + }); }); } @@ -94,19 +161,13 @@ class MetricsService { if (!this.subscriptions.has(metricKey)) { this.subscriptions.set(metricKey, []); const [metric] = metricKey.split('?'); - this.socket.emit('subscribe-metric', { - metric, - interval, - filters - }); + this.sendMessage('subscribe-metric', { metric, interval, filters }); } const callbacks = this.subscriptions.get(metricKey); callbacks.push(callback); - return () => { - this.unsubscribeFromMetric(metricKey, callback); - }; + return () => this.unsubscribeFromMetric(metricKey, callback); } unsubscribeFromMetric(metricKey, callback) { @@ -115,10 +176,8 @@ class MetricsService { if (filtered.length === 0) { this.subscriptions.delete(metricKey); - if (this.socket && this.socket.connected) { - const [metric] = metricKey.split('?'); - this.socket.emit('unsubscribe-metric', { metric }); - } + const [metric] = metricKey.split('?'); + this.sendMessage('unsubscribe-metric', { metric }); } else { this.subscriptions.set(metricKey, filtered); } @@ -127,7 +186,6 @@ class MetricsService { parseFiltersFromKey(metricKey) { const parts = metricKey.split('?'); if (parts.length < 2) return {}; - return parts[1].split('&').reduce((acc, pair) => { const [key, value] = pair.split('='); if (key && value) acc[key] = value; @@ -136,22 +194,19 @@ class MetricsService { } cleanupAll() { - if (this.socket && this.socket.connected) { - this.socket.emit('unsubscribe-all'); - } + this.sendMessage('unsubscribe-all', {}); this.subscriptions.clear(); this.disconnectWebSocket(); } disconnectWebSocket() { if (this.socket) { - this.socket.disconnect(); + this.socket.close(); this.socket = null; } } } -// Создаем экземпляр сервиса -const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL); +const metricsService = new MetricsService(); +export default metricsService; -export default metricsService; \ No newline at end of file diff --git a/src/Charts2/PrometheusChart.jsx b/src/Charts2/PrometheusChart.jsx index 8466c14..f3ea659 100644 --- a/src/Charts2/PrometheusChart.jsx +++ b/src/Charts2/PrometheusChart.jsx @@ -31,7 +31,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { const [showLogs, setShowLogs] = useState(false); const [statusLogs, setStatusLogs] = useState([]); const MAX_POINTS = 50; - const TIME_WINDOW_MS = 3600 * 1000; // 1 час в миллисекундах + const TIME_WINDOW_MS = 3600 * 1000; const getSubscriptionKey = () => { @@ -42,17 +42,28 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { }; const formatMetricData = (dataArray) => { - return dataArray - .map(item => ({ + if (!Array.isArray(dataArray)) { + console.error('Expected array in formatMetricData, got:', typeof dataArray); + return []; + } + + return dataArray.map(item => { + if (item.timestamp === undefined || item.value === undefined) { + console.warn('Invalid metric item:', item); + return null; + } + + return { ...item, - timestamp: item.timestamp, + timestamp: Number(item.timestamp), value: parseFloat(item.value), + status: parseInt(item.status || '0'), name: item.__name__ || metricName, - status: parseInt(item.status) || 0, device: item.device?.trim() || null, source_id: item.source_id || null, description: item.description || description - })) + }; + }).filter(Boolean) .sort((a, b) => a.timestamp - b.timestamp); }; @@ -73,7 +84,6 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { result.push(sortedData[i]); } - // Всегда включаем последнюю точку if (result.length > 0) { const lastOriginalPoint = sortedData[sortedData.length - 1]; if (result[result.length - 1].timestamp !== lastOriginalPoint.timestamp) { @@ -85,7 +95,6 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { }; - // Обновляем логи при изменении данных useEffect(() => { if (chartData.length > 0) { const newLogs = chartData.reduce((acc, point, index) => { @@ -122,7 +131,6 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { const formattedData = formatMetricData(data) .sort((a, b) => a.timestamp - b.timestamp); - // Применяем ограничение по количеству точек только для исторических данных const limitedData = formattedData.length > MAX_POINTS ? formattedData.slice(-MAX_POINTS) : formattedData; @@ -152,42 +160,42 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { const end = new Date(); const start = new Date(end.getTime() - TIME_WINDOW_MS); + fetchHistoricalData(start, end).finally(() => setIsLoading(false)); return metricsService.subscribeToMetric( getSubscriptionKey(), (newData) => { + console.log('Received WS update:', newData); + if (!Array.isArray(newData)) { + console.error('Expected array in WS update, got:', typeof newData); + return; + } + setChartData(prev => { const now = Date.now(); const cutoffTime = now - TIME_WINDOW_MS; - // Фильтруем старые точки (старше TIME_WINDOW_MS) - const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime); - - // Добавляем новые точки - const newPoints = formatMetricData(newData) + const formattedNew = formatMetricData(newData) .filter(point => point.timestamp >= cutoffTime); - // Объединяем и удаляем дубликаты - const mergedData = [...filteredPrev, ...newPoints] - .filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i) + const filteredPrev = prev.filter(point => + point.timestamp >= cutoffTime + ); + + const merged = [...filteredPrev, ...formattedNew] + .filter((v, i, a) => + a.findIndex(t => t.timestamp === v.timestamp) === i + ) .sort((a, b) => a.timestamp - b.timestamp); - // Если точек слишком много, равномерно прореживаем - if (mergedData.length > MAX_POINTS) { - const step = Math.ceil(mergedData.length / MAX_POINTS); - return mergedData.filter((_, index) => index % step === 0); - } - - return mergedData; + return merged.length > MAX_POINTS + ? merged.slice(-MAX_POINTS) + : merged; }); }, - 1000, // Уменьшаем интервал обновления до 1 секунды - { - ...filters, - ...(device && { device }), - ...(source_id && { source_id }) - } + 1000, + { ...filters, device, source_id } ); }; @@ -204,20 +212,24 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { }; useEffect(() => { - console.log('Current metric context:', { device, source_id, metricName }); + console.log('Metric changed:', { metricName, device, source_id, filters }); + let unsubscribe; - if (mode === 'realtime') { - unsubscribe = startRealtimeUpdates(); - } else { - stopRealtimeUpdates(); - fetchHistoricalData(startDate, endDate); - } + const init = async () => { + if (mode === 'realtime') { + unsubscribe = startRealtimeUpdates(); + } else { + await fetchHistoricalData(startDate, endDate); + } + }; + + init(); return () => { if (unsubscribe) unsubscribe(); stopRealtimeUpdates(); }; - }, [mode, metricName, device, source_id]); + }, [mode, metricName, device, source_id, filters]); const metaInfo = [ metricMeta.instance && `Instance: ${metricMeta.instance}`, diff --git a/vite.config.js b/vite.config.js index ceb9230..6ad91d5 100755 --- a/vite.config.js +++ b/vite.config.js @@ -15,6 +15,7 @@ export default defineConfig({ }, '/api': { target: 'http://localhost:3000', + ws: true, changeOrigin: true, bypass(req, res, options) { console.log('Proxying request:', req.url);