@@ -2,6 +2,7 @@ import { Client } from 'ssh2';
22import db from '../models/database' ;
33import { sshPool } from './sshService' ;
44import { logger } from '../utils/logger' ;
5+ import { v4 as uuidv4 } from 'uuid' ;
56
67interface ServerInfo {
78 id : string ;
@@ -28,6 +29,26 @@ interface ServerInfoResult {
2829 } ;
2930}
3031
32+ interface ServerMetricsResult {
33+ success : boolean ;
34+ error ?: string ;
35+ data ?: {
36+ cpu_usage : number ;
37+ memory_usage : number ;
38+ memory_total_gb : number ;
39+ memory_used_gb : number ;
40+ disk_usage : number ;
41+ disk_total_gb : number ;
42+ disk_used_gb : number ;
43+ network_in_mbps : number ;
44+ network_out_mbps : number ;
45+ load_1min : number ;
46+ load_5min : number ;
47+ load_15min : number ;
48+ uptime_seconds : number ;
49+ } ;
50+ }
51+
3152class ServerInfoCollector {
3253 private static readonly CONNECT_TIMEOUT = 10000 ;
3354
@@ -146,6 +167,224 @@ class ServerInfoCollector {
146167
147168 return { success, failed : errors . length , errors } ;
148169 }
170+
171+ async collectServerMetrics ( serverId : string ) : Promise < ServerMetricsResult > {
172+ const server = db . prepare ( 'SELECT * FROM servers WHERE id = ?' ) . get ( serverId ) as ServerInfo | undefined ;
173+
174+ if ( ! server || ! server . enabled ) {
175+ return { success : false , error : 'Server not found or disabled' } ;
176+ }
177+
178+ let conn : Client | null = null ;
179+
180+ return new Promise ( ( resolve ) => {
181+ sshPool . acquire ( serverId ) . then ( ( connection ) => {
182+ conn = connection ;
183+ } ) . catch ( ( error ) => {
184+ resolve ( { success : false , error : error instanceof Error ? error . message : 'Failed to acquire SSH connection' } ) ;
185+ return ;
186+ } ) . then ( ( ) => {
187+ if ( ! conn ) return ;
188+
189+ let isResolved = false ;
190+
191+ const safeResolve = ( result : ServerMetricsResult ) => {
192+ if ( ! isResolved ) {
193+ isResolved = true ;
194+ if ( conn ) {
195+ sshPool . release ( conn , result . success ) ;
196+ }
197+ resolve ( result ) ;
198+ }
199+ } ;
200+
201+ const commands = {
202+ cpu_usage : `top -bn1 | grep "Cpu(s)" | awk '{print 100 - $8}' || cat /proc/stat | awk '/^cpu / {print ($2+$4)*100/($2+$4+$5)}'` ,
203+ memory : `free -m | awk '/^Mem:/{printf "%.1f %.1f %.1f", $2/1024, $3/1024, $3*100/$2}'` ,
204+ disk : `df -m --output=source,size,used,pcent / 2>/dev/null | tail -1 | awk '{print $2/1024, $3/1024, $4}' || df -BM / | tail -1 | awk '{print $2, $3, $5}'` ,
205+ network : `cat /proc/net/dev 2>/dev/null | grep -v lo: | awk 'NR>2 {rx+=$2; tx+=$10} END {printf "%.2f %.2f", rx/1024/1024, tx/1024/1024}' || echo "0 0"` ,
206+ load : `cat /proc/loadavg 2>/dev/null | awk '{print $1, $2, $3}' || uptime | awk -F'load average:' '{print $2}'` ,
207+ uptime : `cat /proc/uptime 2>/dev/null | awk '{print int($1)}' || echo "0"`
208+ } ;
209+
210+ const results : Record < string , string > = { } ;
211+ let completed = 0 ;
212+ const total = Object . keys ( commands ) . length ;
213+
214+ const checkComplete = ( ) => {
215+ completed ++ ;
216+ if ( completed === total ) {
217+ try {
218+ const data = this . parseMetricsResults ( results ) ;
219+
220+ db . prepare ( `
221+ INSERT INTO server_metrics (
222+ id, server_id, cpu_usage, memory_usage, memory_total_gb, memory_used_gb,
223+ disk_usage, disk_total_gb, disk_used_gb, network_in_mbps, network_out_mbps,
224+ load_1min, load_5min, load_15min, uptime_seconds, collected_at
225+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
226+ ` ) . run (
227+ uuidv4 ( ) ,
228+ serverId ,
229+ data . cpu_usage ,
230+ data . memory_usage ,
231+ data . memory_total_gb ,
232+ data . memory_used_gb ,
233+ data . disk_usage ,
234+ data . disk_total_gb ,
235+ data . disk_used_gb ,
236+ data . network_in_mbps ,
237+ data . network_out_mbps ,
238+ data . load_1min ,
239+ data . load_5min ,
240+ data . load_15min ,
241+ data . uptime_seconds
242+ ) ;
243+
244+ logger . info ( `Server metrics collected for ${ server . name } (${ serverId } ): CPU=${ data . cpu_usage ?. toFixed ( 1 ) } %` ) ;
245+ safeResolve ( { success : true , data } ) ;
246+ } catch ( error ) {
247+ logger . error ( `Failed to save metrics for ${ serverId } :` , error ) ;
248+ safeResolve ( { success : false , error : error instanceof Error ? error . message : 'Failed to save metrics' } ) ;
249+ }
250+ }
251+ } ;
252+
253+ for ( const [ key , cmd ] of Object . entries ( commands ) ) {
254+ conn ! . exec ( cmd , ( err , stream ) => {
255+ if ( err ) {
256+ results [ key ] = '' ;
257+ checkComplete ( ) ;
258+ return ;
259+ }
260+
261+ let output = '' ;
262+ stream . on ( 'data' , ( data : Buffer ) => {
263+ output += data . toString ( 'utf-8' ) ;
264+ } ) ;
265+
266+ stream . on ( 'close' , ( ) => {
267+ results [ key ] = output . trim ( ) ;
268+ checkComplete ( ) ;
269+ } ) ;
270+
271+ stream . stderr . on ( 'data' , ( ) => { /* ignore stderr */ } ) ;
272+ } ) ;
273+ }
274+ } ) ;
275+ } ) ;
276+ }
277+
278+ private parseMetricsResults ( results : Record < string , string > ) : ServerMetricsResult [ 'data' ] {
279+ let cpu_usage = 0 ;
280+ try {
281+ const cpuStr = results . cpu_usage . trim ( ) ;
282+ if ( cpuStr ) {
283+ cpu_usage = parseFloat ( cpuStr ) ;
284+ if ( isNaN ( cpu_usage ) || cpu_usage < 0 || cpu_usage > 100 ) {
285+ cpu_usage = 0 ;
286+ }
287+ }
288+ } catch {
289+ cpu_usage = 0 ;
290+ }
291+
292+ let memory_usage = 0 , memory_total_gb = 0 , memory_used_gb = 0 ;
293+ try {
294+ const memParts = results . memory . trim ( ) . split ( / \s + / ) ;
295+ if ( memParts . length >= 3 ) {
296+ memory_total_gb = parseFloat ( memParts [ 0 ] ) ;
297+ memory_used_gb = parseFloat ( memParts [ 1 ] ) ;
298+ memory_usage = parseFloat ( memParts [ 2 ] ) ;
299+ }
300+ } catch {
301+ // defaults
302+ }
303+
304+ let disk_usage = 0 , disk_total_gb = 0 , disk_used_gb = 0 ;
305+ try {
306+ const diskParts = results . disk . trim ( ) . split ( / \s + / ) ;
307+ if ( diskParts . length >= 3 ) {
308+ const sizeStr = diskParts [ 0 ] . replace ( / [ A - Z a - z ] / g, '' ) ;
309+ const usedStr = diskParts [ 1 ] . replace ( / [ A - Z a - z ] / g, '' ) ;
310+ const pcentStr = diskParts [ 2 ] . replace ( '%' , '' ) ;
311+ disk_total_gb = parseFloat ( sizeStr ) ;
312+ disk_used_gb = parseFloat ( usedStr ) ;
313+ disk_usage = parseFloat ( pcentStr ) ;
314+ }
315+ } catch {
316+ // defaults
317+ }
318+
319+ let network_in_mbps = 0 , network_out_mbps = 0 ;
320+ try {
321+ const netParts = results . network . trim ( ) . split ( / \s + / ) ;
322+ if ( netParts . length >= 2 ) {
323+ network_in_mbps = parseFloat ( netParts [ 0 ] ) ;
324+ network_out_mbps = parseFloat ( netParts [ 1 ] ) ;
325+ }
326+ } catch {
327+ // defaults
328+ }
329+
330+ let load_1min = 0 , load_5min = 0 , load_15min = 0 ;
331+ try {
332+ const loadParts = results . load . trim ( ) . split ( / [ \s , ] + / ) . filter ( s => s ) ;
333+ if ( loadParts . length >= 3 ) {
334+ load_1min = parseFloat ( loadParts [ 0 ] ) ;
335+ load_5min = parseFloat ( loadParts [ 1 ] ) ;
336+ load_15min = parseFloat ( loadParts [ 2 ] ) ;
337+ }
338+ } catch {
339+ // defaults
340+ }
341+
342+ let uptime_seconds = 0 ;
343+ try {
344+ uptime_seconds = parseInt ( results . uptime . trim ( ) , 10 ) || 0 ;
345+ } catch {
346+ // defaults
347+ }
348+
349+ return {
350+ cpu_usage : isNaN ( cpu_usage ) ? 0 : cpu_usage ,
351+ memory_usage : isNaN ( memory_usage ) ? 0 : memory_usage ,
352+ memory_total_gb : isNaN ( memory_total_gb ) ? 0 : memory_total_gb ,
353+ memory_used_gb : isNaN ( memory_used_gb ) ? 0 : memory_used_gb ,
354+ disk_usage : isNaN ( disk_usage ) ? 0 : disk_usage ,
355+ disk_total_gb : isNaN ( disk_total_gb ) ? 0 : disk_total_gb ,
356+ disk_used_gb : isNaN ( disk_used_gb ) ? 0 : disk_used_gb ,
357+ network_in_mbps : isNaN ( network_in_mbps ) ? 0 : network_in_mbps ,
358+ network_out_mbps : isNaN ( network_out_mbps ) ? 0 : network_out_mbps ,
359+ load_1min : isNaN ( load_1min ) ? 0 : load_1min ,
360+ load_5min : isNaN ( load_5min ) ? 0 : load_5min ,
361+ load_15min : isNaN ( load_15min ) ? 0 : load_15min ,
362+ uptime_seconds : uptime_seconds
363+ } ;
364+ }
365+
366+ async collectAllServerMetrics ( ) : Promise < {
367+ success : number ;
368+ failed : number ;
369+ errors : Array < { serverId : string ; serverName : string ; error : string } > ;
370+ } > {
371+ const servers = db . prepare ( 'SELECT id, name FROM servers WHERE enabled = 1' ) . all ( ) as { id : string ; name : string } [ ] ;
372+
373+ const errors : Array < { serverId : string ; serverName : string ; error : string } > = [ ] ;
374+ let success = 0 ;
375+
376+ for ( const server of servers ) {
377+ const result = await this . collectServerMetrics ( server . id ) ;
378+ if ( result . success ) {
379+ success ++ ;
380+ } else {
381+ errors . push ( { serverId : server . id , serverName : server . name , error : result . error || 'Unknown error' } ) ;
382+ }
383+ }
384+
385+ logger . info ( `Metrics collection completed: ${ success } success, ${ errors . length } failed` ) ;
386+ return { success, failed : errors . length , errors } ;
387+ }
149388}
150389
151390export const serverInfoCollector = new ServerInfoCollector ( ) ;
0 commit comments