Compare commits

..

No commits in common. "46cd1fa0fa872c12f92d76e71aff6733b831de76" and "f8d822ace7feed7a477f39d0c046173361b7067f" have entirely different histories.

4 changed files with 119 additions and 197 deletions

View File

@ -43,14 +43,7 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`; return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`;
}, [metricName, device, source_id]); }, [metricName, device, source_id]);
const formatMetricData = (responseData) => { const formatMetricData = (dataArray) => {
const dataArray = Array.isArray(responseData) ? responseData : responseData.data;
if (!Array.isArray(dataArray)) {
console.error('Expected array but got:', responseData);
return [];
}
return dataArray.map(item => ({ return dataArray.map(item => ({
...item, ...item,
timestamp: item.timestamp, timestamp: item.timestamp,
@ -81,7 +74,6 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
}; };
const step = calculateStep(start, end); const step = calculateStep(start, end);
const data = await metricsService.fetchMetricsRange( const data = await metricsService.fetchMetricsRange(
metricName, metricName,
Math.floor(start.getTime() / 1000), Math.floor(start.getTime() / 1000),
@ -90,8 +82,7 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
extendedFilters extendedFilters
); );
const responseData = Array.isArray(data) ? data : data.data; const formattedData = formatMetricData(data);
const formattedData = formatMetricData(responseData);
setRawData(formattedData); setRawData(formattedData);
if (formattedData.length > 0) { if (formattedData.length > 0) {
@ -117,19 +108,21 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
const end = new Date(); const end = new Date();
const start = new Date(end.getTime() - TIME_WINDOW_MS); const start = new Date(end.getTime() - TIME_WINDOW_MS);
const cutoffTime = Date.now() - TIME_WINDOW_MS;
fetchHistoricalData(start, end).finally(() => setIsLoading(false)); fetchHistoricalData(start, end).finally(() => setIsLoading(false));
return metricsService.subscribeToMetric( return metricsService.subscribeToMetric(
subscriptionKey, subscriptionKey,
(newData) => { (newData) => {
setRawData(prev => { setRawData(prev => {
const actualData = Array.isArray(newData) ? newData : newData.data; const now = Date.now();
const formattedNewData = formatMetricData(actualData) const cutoffTime = now - TIME_WINDOW_MS;
const formattedNewData = formatMetricData(newData)
.filter(point => point.timestamp >= cutoffTime); .filter(point => point.timestamp >= cutoffTime);
const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime); const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime);
// Объединяем данные, удаляем дубликаты
const merged = [...filteredPrev, ...formattedNewData] const merged = [...filteredPrev, ...formattedNewData]
.filter((v, i, a) => .filter((v, i, a) =>
a.findIndex(t => a.findIndex(t =>
@ -141,7 +134,7 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
return merged; return merged;
}); });
}, },
5000, 5000, // Интервал обновления 5 секунд
{ {
...filters, ...filters,
...(device && { device }), ...(device && { device }),
@ -161,6 +154,7 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
} }
}; };
// Обновляем логи статусов
useEffect(() => { useEffect(() => {
if (rawData.length > 0) { if (rawData.length > 0) {
const logs = []; const logs = [];

View File

@ -1,157 +1,90 @@
import { io } from 'socket.io-client';
class MetricsService { class MetricsService {
constructor() { constructor(baseUrl) {
this.baseUrl = '/metrics-ws'; this.baseUrl = baseUrl || window.location.origin;
this.socket = null; this.socket = null;
this.subscriptions = new Map(); this.subscriptions = new Map();
this.pendingRequests = new Map(); this.pendingRequests = new Map();
this.reconnectAttempts = 0; window.addEventListener('beforeunload', this.cleanupAll.bind(this));
this.maxReconnectAttempts = 5; window.addEventListener('pagehide', this.cleanupAll.bind(this));
this.reconnectDelay = 5000;
window.addEventListener('beforeunload', () => this.cleanupAll()); window.addEventListener('beforeunload', () => {
window.addEventListener('pagehide', () => this.cleanupAll()); 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() { connectWebSocket() {
if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) { if (this.socket) {
console.log('WebSocket already exists');
return; return;
} }
console.log('Connecting WebSocket...'); console.log('Connecting WebSocket...');
this.socket = new WebSocket(this.baseUrl); this.socket = io(`${this.baseUrl.replace('http', 'ws')}/api/metrics-ws`, {
transports: ['websocket'],
withCredentials: true,
});
this.socket.addEventListener('open', () => { this.socket.on('connect', () => {
console.log('WebSocket connected'); console.log('WebSocket connected');
this.reconnectAttempts = 0; // Восстанавливаем подписки при переподключении
this.subscriptions.forEach((_, metricKey) => { this.subscriptions.forEach((_, metricKey) => {
const [metric, query] = metricKey.split('?');
const filters = this.parseFiltersFromKey(metricKey); const filters = this.parseFiltersFromKey(metricKey);
const [metric] = metricKey.split('?'); this.socket.emit('subscribe-metric', { metric, filters });
this.sendMessage('subscribe-metric', { metric, filters });
}); });
}); });
this.socket.addEventListener('close', () => { this.socket.on('disconnect', () => {
console.log('WebSocket disconnected'); console.log('WebSocket disconnected');
this.socket = null; this.socket = null;
this.scheduleReconnect();
}); });
this.socket.addEventListener('error', (err) => { this.socket.on('metrics-data', ({ metric, data, requestId }) => {
console.error('WebSocket error:', err); 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('message', (event) => { this.socket.on('metrics-error', ({ error, requestId }) => {
try { if (requestId && this.pendingRequests.has(requestId)) {
const msg = JSON.parse(event.data); const { reject } = this.pendingRequests.get(requestId);
this.handleServerMessage(msg); reject(new Error(error));
} catch (e) { this.pendingRequests.delete(requestId);
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 = {}) { async fetchMetricsRange(metric, start, end, step = 15, filters = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.connectWebSocket(); this.connectWebSocket();
const requestId = `range-${Date.now()}`; const requestId = `range-${Date.now()}`;
this.pendingRequests.set(requestId, { resolve, reject });
// Таймаут для очистки this.socket.emit('get-metrics', {
const timeout = setTimeout(() => { metric,
reject(new Error('Request timeout')); start,
this.pendingRequests.delete(requestId); end,
}, 12000); step,
filters,
isRangeQuery: true,
requestId
});
this.pendingRequests.set(requestId, { setTimeout(() => {
resolve: (responseData) => { if (this.pendingRequests.has(requestId)) {
clearTimeout(timeout); reject(new Error('Request timeout'));
const data = Array.isArray(responseData) ? responseData : this.pendingRequests.delete(requestId);
(responseData?.data || []);
resolve(data);
},
reject: (err) => {
clearTimeout(timeout);
reject(err);
} }
}); }, 30000);
this.sendMessage('get-metrics', {
metric, start, end, step, filters, isRangeQuery: true, requestId
});
}); });
} }
@ -161,13 +94,19 @@ class MetricsService {
if (!this.subscriptions.has(metricKey)) { if (!this.subscriptions.has(metricKey)) {
this.subscriptions.set(metricKey, []); this.subscriptions.set(metricKey, []);
const [metric] = metricKey.split('?'); const [metric] = metricKey.split('?');
this.sendMessage('subscribe-metric', { metric, interval, filters }); this.socket.emit('subscribe-metric', {
metric,
interval,
filters
});
} }
const callbacks = this.subscriptions.get(metricKey); const callbacks = this.subscriptions.get(metricKey);
callbacks.push(callback); callbacks.push(callback);
return () => this.unsubscribeFromMetric(metricKey, callback); return () => {
this.unsubscribeFromMetric(metricKey, callback);
};
} }
unsubscribeFromMetric(metricKey, callback) { unsubscribeFromMetric(metricKey, callback) {
@ -176,8 +115,10 @@ class MetricsService {
if (filtered.length === 0) { if (filtered.length === 0) {
this.subscriptions.delete(metricKey); this.subscriptions.delete(metricKey);
const [metric] = metricKey.split('?'); if (this.socket && this.socket.connected) {
this.sendMessage('unsubscribe-metric', { metric }); const [metric] = metricKey.split('?');
this.socket.emit('unsubscribe-metric', { metric });
}
} else { } else {
this.subscriptions.set(metricKey, filtered); this.subscriptions.set(metricKey, filtered);
} }
@ -186,6 +127,7 @@ class MetricsService {
parseFiltersFromKey(metricKey) { parseFiltersFromKey(metricKey) {
const parts = metricKey.split('?'); const parts = metricKey.split('?');
if (parts.length < 2) return {}; if (parts.length < 2) return {};
return parts[1].split('&').reduce((acc, pair) => { return parts[1].split('&').reduce((acc, pair) => {
const [key, value] = pair.split('='); const [key, value] = pair.split('=');
if (key && value) acc[key] = value; if (key && value) acc[key] = value;
@ -194,19 +136,22 @@ class MetricsService {
} }
cleanupAll() { cleanupAll() {
this.sendMessage('unsubscribe-all', {}); if (this.socket && this.socket.connected) {
this.socket.emit('unsubscribe-all');
}
this.subscriptions.clear(); this.subscriptions.clear();
this.disconnectWebSocket(); this.disconnectWebSocket();
} }
disconnectWebSocket() { disconnectWebSocket() {
if (this.socket) { if (this.socket) {
this.socket.close(); this.socket.disconnect();
this.socket = null; this.socket = null;
} }
} }
} }
const metricsService = new MetricsService(); // Создаем экземпляр сервиса
export default metricsService; const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL);
export default metricsService;

View File

@ -31,7 +31,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
const [showLogs, setShowLogs] = useState(false); const [showLogs, setShowLogs] = useState(false);
const [statusLogs, setStatusLogs] = useState([]); const [statusLogs, setStatusLogs] = useState([]);
const MAX_POINTS = 50; const MAX_POINTS = 50;
const TIME_WINDOW_MS = 3600 * 1000; const TIME_WINDOW_MS = 3600 * 1000; // 1 час в миллисекундах
const getSubscriptionKey = () => { const getSubscriptionKey = () => {
@ -42,28 +42,17 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
}; };
const formatMetricData = (dataArray) => { const formatMetricData = (dataArray) => {
if (!Array.isArray(dataArray)) { return dataArray
console.error('Expected array in formatMetricData, got:', typeof dataArray); .map(item => ({
return [];
}
return dataArray.map(item => {
if (item.timestamp === undefined || item.value === undefined) {
console.warn('Invalid metric item:', item);
return null;
}
return {
...item, ...item,
timestamp: Number(item.timestamp), timestamp: item.timestamp,
value: parseFloat(item.value), value: parseFloat(item.value),
status: parseInt(item.status || '0'),
name: item.__name__ || metricName, name: item.__name__ || metricName,
status: parseInt(item.status) || 0,
device: item.device?.trim() || null, device: item.device?.trim() || null,
source_id: item.source_id || null, source_id: item.source_id || null,
description: item.description || description description: item.description || description
}; }))
}).filter(Boolean)
.sort((a, b) => a.timestamp - b.timestamp); .sort((a, b) => a.timestamp - b.timestamp);
}; };
@ -84,6 +73,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
result.push(sortedData[i]); result.push(sortedData[i]);
} }
// Всегда включаем последнюю точку
if (result.length > 0) { if (result.length > 0) {
const lastOriginalPoint = sortedData[sortedData.length - 1]; const lastOriginalPoint = sortedData[sortedData.length - 1];
if (result[result.length - 1].timestamp !== lastOriginalPoint.timestamp) { if (result[result.length - 1].timestamp !== lastOriginalPoint.timestamp) {
@ -95,6 +85,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
}; };
// Обновляем логи при изменении данных
useEffect(() => { useEffect(() => {
if (chartData.length > 0) { if (chartData.length > 0) {
const newLogs = chartData.reduce((acc, point, index) => { const newLogs = chartData.reduce((acc, point, index) => {
@ -131,6 +122,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
const formattedData = formatMetricData(data) const formattedData = formatMetricData(data)
.sort((a, b) => a.timestamp - b.timestamp); .sort((a, b) => a.timestamp - b.timestamp);
// Применяем ограничение по количеству точек только для исторических данных
const limitedData = formattedData.length > MAX_POINTS const limitedData = formattedData.length > MAX_POINTS
? formattedData.slice(-MAX_POINTS) ? formattedData.slice(-MAX_POINTS)
: formattedData; : formattedData;
@ -160,42 +152,42 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
const end = new Date(); const end = new Date();
const start = new Date(end.getTime() - TIME_WINDOW_MS); const start = new Date(end.getTime() - TIME_WINDOW_MS);
fetchHistoricalData(start, end).finally(() => setIsLoading(false)); fetchHistoricalData(start, end).finally(() => setIsLoading(false));
return metricsService.subscribeToMetric( return metricsService.subscribeToMetric(
getSubscriptionKey(), getSubscriptionKey(),
(newData) => { (newData) => {
console.log('Received WS update:', newData);
if (!Array.isArray(newData)) {
console.error('Expected array in WS update, got:', typeof newData);
return;
}
setChartData(prev => { setChartData(prev => {
const now = Date.now(); const now = Date.now();
const cutoffTime = now - TIME_WINDOW_MS; const cutoffTime = now - TIME_WINDOW_MS;
const formattedNew = formatMetricData(newData) // Фильтруем старые точки (старше TIME_WINDOW_MS)
const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime);
// Добавляем новые точки
const newPoints = formatMetricData(newData)
.filter(point => point.timestamp >= cutoffTime); .filter(point => point.timestamp >= cutoffTime);
const filteredPrev = prev.filter(point => // Объединяем и удаляем дубликаты
point.timestamp >= cutoffTime const mergedData = [...filteredPrev, ...newPoints]
); .filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i)
const merged = [...filteredPrev, ...formattedNew]
.filter((v, i, a) =>
a.findIndex(t => t.timestamp === v.timestamp) === i
)
.sort((a, b) => a.timestamp - b.timestamp); .sort((a, b) => a.timestamp - b.timestamp);
return merged.length > MAX_POINTS // Если точек слишком много, равномерно прореживаем
? merged.slice(-MAX_POINTS) if (mergedData.length > MAX_POINTS) {
: merged; const step = Math.ceil(mergedData.length / MAX_POINTS);
return mergedData.filter((_, index) => index % step === 0);
}
return mergedData;
}); });
}, },
1000, 1000, // Уменьшаем интервал обновления до 1 секунды
{ ...filters, device, source_id } {
...filters,
...(device && { device }),
...(source_id && { source_id })
}
); );
}; };
@ -212,24 +204,20 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
}; };
useEffect(() => { useEffect(() => {
console.log('Metric changed:', { metricName, device, source_id, filters }); console.log('Current metric context:', { device, source_id, metricName });
let unsubscribe; let unsubscribe;
const init = async () => { if (mode === 'realtime') {
if (mode === 'realtime') { unsubscribe = startRealtimeUpdates();
unsubscribe = startRealtimeUpdates(); } else {
} else { stopRealtimeUpdates();
await fetchHistoricalData(startDate, endDate); fetchHistoricalData(startDate, endDate);
} }
};
init();
return () => { return () => {
if (unsubscribe) unsubscribe(); if (unsubscribe) unsubscribe();
stopRealtimeUpdates(); stopRealtimeUpdates();
}; };
}, [mode, metricName, device, source_id, filters]); }, [mode, metricName, device, source_id]);
const metaInfo = [ const metaInfo = [
metricMeta.instance && `Instance: ${metricMeta.instance}`, metricMeta.instance && `Instance: ${metricMeta.instance}`,

View File

@ -13,18 +13,13 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/ai-api/, ''), rewrite: (path) => path.replace(/^\/ai-api/, ''),
}, },
'/metrics-ws': {
target: 'ws://localhost:3001',
ws: true,
changeOrigin: true,
},
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://localhost:3000',
ws: true,
changeOrigin: true, changeOrigin: true,
bypass(req, res, options) { bypass(req, res, options) {
console.log('Proxying request:', req.url); console.log('Proxying request:', req.url);
} }
} }
} }
} }