@@ -3,6 +3,13 @@ const http = require('http')
33const WebSocket = require ( 'ws' )
44const fetch = require ( 'node-fetch' )
55
6+ const fs = require ( 'fs' ) ;
7+ const DATA_FILE = 'latency_data.jsonl' ;
8+
9+ const MAX_STORAGE_TIME_MS = 24 * 60 * 60 * 1000 ; // сутки
10+ let measureCount = 0 ;
11+
12+
613const NAMES = [
714 'scv' ,
815 'admin' ,
@@ -109,31 +116,140 @@ app.get('/', (req, res) => {
109116 });
110117
111118 const socket = new WebSocket('ws://' + location.host);
112- socket.onmessage = (event) => {
113- const msg = JSON.parse(event.data);
114- const points = msg.averages;
115-
116- chart.data.datasets[0].data = points.map(p => ({ x: new Date(p.time), y: p.avg5s, customData: p.reqInfo }));
117- chart.data.datasets[1].data = points.map(p => ({ x: new Date(p.time), y: p.avg1min, customData: p.reqInfo }));
118- chart.data.datasets[2].data = points.map(p => ({ x: new Date(p.time), y: p.avg1h, customData: p.reqInfo }));
119-
120- if (points.length > 0) {
121- const latestTime = new Date(points[points.length - 1].time).getTime();
122- const RANGE = 5 * 60 * 1000; // 5 минут в мс
123- chart.options.scales.x.min = latestTime - RANGE / 2;
124- chart.options.scales.x.max = latestTime + RANGE / 2;
119+ socket.onmessage = (event) => {
120+ const msg = JSON.parse(event.data);
121+ const points = msg.averages;
122+
123+ chart.data.datasets[0].data = points.map(p => ({ x: new Date(p.time), y: p.avg5s, customData: p.reqInfo }));
124+ chart.data.datasets[1].data = points.map(p => ({ x: new Date(p.time), y: p.avg1min, customData: p.reqInfo }));
125+ chart.data.datasets[2].data = points.map(p => ({ x: new Date(p.time), y: p.avg1h, customData: p.reqInfo }));
126+
127+ if (points.length > 0) {
128+ const latestTime = new Date(points[points.length - 1].time).getTime();
129+ const RANGE = 5 * 60 * 1000; // 5 минут в мс
130+ chart.options.scales.x.min = latestTime - RANGE;
131+ chart.options.scales.x.max = latestTime;
132+ }
133+
134+ chart.data.datasets[0].borderColor = msg.colors[0];
135+ chart.data.datasets[1].borderColor = msg.colors[1];
136+ chart.data.datasets[2].borderColor = msg.colors[2];
137+ chart.update();
138+ };
139+ </script>
140+ </body>
141+ </html>` ) ;
142+ } ) ;
143+
144+ app . get ( '/history' , ( req , res ) => {
145+ const timeParam = parseInt ( req . query . time || '60' ) ; // по умолчанию 60 минут
146+ const timeRangeMs = timeParam * 60 * 1000 ;
147+ const now = Date . now ( ) ;
148+ const points = [ ] ;
149+
150+ if ( ! fs . existsSync ( DATA_FILE ) ) {
151+ return res . send ( 'No data available.' ) ;
152+ }
153+
154+ const lines = fs . readFileSync ( DATA_FILE , 'utf8' ) . split ( '\n' ) ;
155+ for ( const line of lines ) {
156+ if ( ! line . trim ( ) ) continue ;
157+ try {
158+ const point = JSON . parse ( line ) ;
159+ const pointTime = new Date ( point . time ) . getTime ( ) ;
160+ if ( now - pointTime <= timeRangeMs ) {
161+ points . push ( point ) ;
125162 }
163+ } catch ( e ) {
164+ console . error ( 'Error parsing history line:' , e . message ) ;
165+ }
166+ }
126167
127- chart.data.datasets[0].borderColor = msg.colors[0];
128- chart.data.datasets[1].borderColor = msg.colors[1];
129- chart.data.datasets[2].borderColor = msg.colors[2];
130- chart.update();
131- };
168+ res . send ( `<!DOCTYPE html>
169+ <html>
170+ <head>
171+ <meta charset="utf-8">
172+ <title>Latency History (Last ${ timeParam } min)</title>
173+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
174+ <script src="https://cdn.jsdelivr.net/npm/luxon@3/build/global/luxon.min.js"></script>
175+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1"></script>
176+ <style>
177+ body { background-color: #1e1e1e; color: #ccc; }
178+ canvas { background-color: #2e2e2e; }
179+ </style>
180+ </head>
181+ <body>
182+ <h2>Latency History - Last ${ timeParam } minutes</h2>
183+ <canvas id="latencyChart" width="800" height="400"></canvas>
184+ <script>
185+ const ctx = document.getElementById('latencyChart').getContext('2d');
186+ const chart = new Chart(ctx, {
187+ type: 'line',
188+ data: {
189+ datasets: [
190+ {
191+ label: 'Latency',
192+ data: [],
193+ borderWidth: 2,
194+ fill: false,
195+ borderColor: 'rgba(0,200,255,1)',
196+ }
197+ ]
198+ },
199+ options: {
200+ animation: false,
201+ plugins: {
202+ legend: {
203+ labels: { color: '#ccc' },
204+ display: true,
205+ position: 'top'
206+ },
207+ tooltip: {
208+ callbacks: {
209+ label: function(context) {
210+ const point = context.raw;
211+ const latency = point.y;
212+ const reqInfo = point.customData || 'n/a';
213+ return 'Latency: ' + latency.toFixed(1) + ' ms | ' + reqInfo;
214+ }
215+ }
216+ }
217+ },
218+ scales: {
219+ x: {
220+ type: 'time',
221+ time: {
222+ unit: 'minute',
223+ displayFormats: { minute: 'HH:mm:ss' }
224+ },
225+ ticks: { color: '#ccc' },
226+ grid: { color: '#444' }
227+ },
228+ y: {
229+ beginAtZero: true,
230+ title: { display: true, text: 'Latency (ms)', color: '#ccc' },
231+ ticks: { color: '#ccc' },
232+ grid: { color: '#444' }
233+ }
234+ }
235+ }
236+ });
237+
238+ const dataPoints = ${ JSON . stringify ( points ) } ;
239+ chart.data.datasets[0].data = dataPoints.map(p => ({ x: new Date(p.time), y: p.latency, customData: p.reqInfo }));
240+ if (dataPoints.length > 0) {
241+ const latestTime = new Date(dataPoints[dataPoints.length - 1].time).getTime();
242+ const RANGE = ${ timeParam } * 60 * 1000;
243+ chart.options.scales.x.min = latestTime - RANGE;
244+ chart.options.scales.x.max = latestTime;
245+ }
246+ chart.update();
132247 </script>
133248</body>
134249</html>` ) ;
135250} ) ;
136251
252+
137253wss . on ( 'connection' , ws => {
138254 console . log ( 'Client connected' ) ;
139255 ws . send ( JSON . stringify ( { averages : calculateAverages ( latencyData ) , colors : [ 'rgba(0,255,0,1)' , 'rgba(0,200,0,1)' , 'rgba(0,150,0,1)' ] } ) ) ;
@@ -155,6 +271,15 @@ function measureLatency() {
155271 latencyData . push ( point ) ;
156272 if ( latencyData . length > MAX_POINTS ) latencyData . shift ( ) ;
157273
274+ measureCount ++ ;
275+ if ( measureCount % 100 === 0 ) {
276+ pruneOldData ( ) ;
277+ }
278+
279+ fs . appendFile ( DATA_FILE , JSON . stringify ( point ) + '\n' , ( err ) => {
280+ if ( err ) console . error ( 'Error writing data file:' , err . message ) ;
281+ } ) ;
282+
158283 const averages = calculateAverages ( latencyData ) ;
159284 const colors = statusToColors ( res . status ) ;
160285 const dataToSend = JSON . stringify ( { averages, colors } ) ;
@@ -208,6 +333,29 @@ function calculateAverages(data) {
208333 } ) ;
209334}
210335
336+ function pruneOldData ( ) {
337+ if ( ! fs . existsSync ( DATA_FILE ) ) return ;
338+ const now = Date . now ( ) ;
339+ try {
340+ const lines = fs . readFileSync ( DATA_FILE , 'utf8' ) . split ( '\n' ) ;
341+ const recentLines = lines . filter ( line => {
342+ if ( ! line . trim ( ) ) return false ;
343+ try {
344+ const point = JSON . parse ( line ) ;
345+ const pointTime = new Date ( point . time ) . getTime ( ) ;
346+ return now - pointTime <= MAX_STORAGE_TIME_MS ;
347+ } catch ( e ) {
348+ return false ;
349+ }
350+ } ) ;
351+ fs . writeFileSync ( DATA_FILE , recentLines . join ( '\n' ) + '\n' ) ;
352+ console . log ( `Pruned old data, remaining points: ${ recentLines . length } ` ) ;
353+ } catch ( e ) {
354+ console . error ( 'Error pruning data file:' , e . message ) ;
355+ }
356+ }
357+
358+
211359function average ( arr ) {
212360 if ( arr . length === 0 ) return 0 ;
213361 return arr . reduce ( ( sum , v ) => sum + v , 0 ) / arr . length ;
0 commit comments