fix(core): Improve logging and error handling by introducing custom error classes and a global logging interface while refactoring network diagnostics methods.
This commit is contained in:
		| @@ -1,5 +1,13 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-04-28 - 3.0.5 - fix(core) | ||||||
|  | Improve logging and error handling by introducing custom error classes and a global logging interface while refactoring network diagnostics methods. | ||||||
|  |  | ||||||
|  | - Added custom error classes (NetworkError, TimeoutError) for network operations. | ||||||
|  | - Introduced a global logging interface to replace direct console logging. | ||||||
|  | - Updated CloudflareSpeed and SmartNetwork classes to use getLogger for improved error reporting. | ||||||
|  | - Disabled connection pooling in HTTP requests to prevent listener accumulation. | ||||||
|  |  | ||||||
| ## 2025-04-28 - 3.0.4 - fix(ci/config) | ## 2025-04-28 - 3.0.4 - fix(ci/config) | ||||||
| Improve CI workflows, update project configuration, and clean up code formatting | Improve CI workflows, update project configuration, and clean up code formatting | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartnetwork', |   name: '@push.rocks/smartnetwork', | ||||||
|   version: '3.0.4', |   version: '3.0.5', | ||||||
|   description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.' |   description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.' | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								ts/errors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								ts/errors.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | /** | ||||||
|  |  * Custom error classes for network operations | ||||||
|  |  */ | ||||||
|  | export class NetworkError extends Error { | ||||||
|  |   public code?: string; | ||||||
|  |   constructor(message?: string, code?: string) { | ||||||
|  |     super(message); | ||||||
|  |     this.name = 'NetworkError'; | ||||||
|  |     this.code = code; | ||||||
|  |     Object.setPrototypeOf(this, new.target.prototype); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class TimeoutError extends NetworkError { | ||||||
|  |   constructor(message?: string) { | ||||||
|  |     super(message, 'ETIMEOUT'); | ||||||
|  |     this.name = 'TimeoutError'; | ||||||
|  |     Object.setPrototypeOf(this, new.target.prototype); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								ts/logging.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								ts/logging.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | /** | ||||||
|  |  * Injectable logging interface and global logger | ||||||
|  |  */ | ||||||
|  | export interface Logger { | ||||||
|  |   /** Debug-level messages */ | ||||||
|  |   debug?(...args: unknown[]): void; | ||||||
|  |   /** Informational messages */ | ||||||
|  |   info(...args: unknown[]): void; | ||||||
|  |   /** Warning messages */ | ||||||
|  |   warn?(...args: unknown[]): void; | ||||||
|  |   /** Error messages */ | ||||||
|  |   error(...args: unknown[]): void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let globalLogger: Logger = console; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Replace the global logger implementation | ||||||
|  |  * @param logger Custom logger adhering to Logger interface | ||||||
|  |  */ | ||||||
|  | export function setLogger(logger: Logger): void { | ||||||
|  |   globalLogger = logger; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Retrieve the current global logger | ||||||
|  |  */ | ||||||
|  | export function getLogger(): Logger { | ||||||
|  |   return globalLogger; | ||||||
|  | } | ||||||
| @@ -1,4 +1,6 @@ | |||||||
| import * as plugins from './smartnetwork.plugins.js'; | import * as plugins from './smartnetwork.plugins.js'; | ||||||
|  | import { getLogger } from './logging.js'; | ||||||
|  | import { NetworkError, TimeoutError } from './errors.js'; | ||||||
| import * as stats from './helpers/stats.js'; | import * as stats from './helpers/stats.js'; | ||||||
|  |  | ||||||
| export class CloudflareSpeed { | export class CloudflareSpeed { | ||||||
| @@ -49,7 +51,7 @@ export class CloudflareSpeed { | |||||||
|           measurements.push(response[4] - response[0] - response[6]); |           measurements.push(response[4] - response[0] - response[6]); | ||||||
|         }, |         }, | ||||||
|         (error) => { |         (error) => { | ||||||
|           console.log(`Error: ${error}`); |           getLogger().error('Error measuring latency:', error); | ||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @@ -73,7 +75,7 @@ export class CloudflareSpeed { | |||||||
|           measurements.push(await this.measureSpeed(bytes, transferTime)); |           measurements.push(await this.measureSpeed(bytes, transferTime)); | ||||||
|         }, |         }, | ||||||
|         (error) => { |         (error) => { | ||||||
|           console.log(`Error: ${error}`); |           getLogger().error('Error measuring download chunk:', error); | ||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @@ -91,7 +93,7 @@ export class CloudflareSpeed { | |||||||
|           measurements.push(await this.measureSpeed(bytes, transferTime)); |           measurements.push(await this.measureSpeed(bytes, transferTime)); | ||||||
|         }, |         }, | ||||||
|         (error) => { |         (error) => { | ||||||
|           console.log(`Error: ${error}`); |           getLogger().error('Error measuring upload chunk:', error); | ||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @@ -104,15 +106,16 @@ export class CloudflareSpeed { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async fetchServerLocations(): Promise<{ [key: string]: string }> { |   public async fetchServerLocations(): Promise<{ [key: string]: string }> { | ||||||
|     const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations')); |     const res = JSON.parse( | ||||||
|  |       await this.get('speed.cloudflare.com', '/locations'), | ||||||
|     return res.reduce((data: any, optionsArg: { iata: string; city: string }) => { |     ) as Array<{ iata: string; city: string }>; | ||||||
|       // Bypass prettier "no-assign-param" rules |     return res.reduce( | ||||||
|       const data1 = data; |       (data: Record<string, string>, optionsArg) => { | ||||||
|  |         data[optionsArg.iata] = optionsArg.city; | ||||||
|       data1[optionsArg.iata] = optionsArg.city; |         return data; | ||||||
|       return data1; |       }, | ||||||
|     }, {}); |       {} as Record<string, string>, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async get(hostname: string, path: string): Promise<string> { |   public async get(hostname: string, path: string): Promise<string> { | ||||||
| @@ -122,6 +125,8 @@ export class CloudflareSpeed { | |||||||
|           hostname, |           hostname, | ||||||
|           path, |           path, | ||||||
|           method: 'GET', |           method: 'GET', | ||||||
|  |           // disable connection pooling to avoid listener accumulation | ||||||
|  |           agent: false, | ||||||
|         }, |         }, | ||||||
|         (res) => { |         (res) => { | ||||||
|           const body: Array<Buffer> = []; |           const body: Array<Buffer> = []; | ||||||
| @@ -135,9 +140,9 @@ export class CloudflareSpeed { | |||||||
|               reject(e); |               reject(e); | ||||||
|             } |             } | ||||||
|           }); |           }); | ||||||
|           req.on('error', (err) => { |       req.on('error', (err: Error & { code?: string }) => { | ||||||
|             reject(err); |         reject(new NetworkError(err.message, err.code)); | ||||||
|           }); |       }); | ||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
| @@ -179,33 +184,36 @@ export class CloudflareSpeed { | |||||||
|  |  | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       started = plugins.perfHooks.performance.now(); |       started = plugins.perfHooks.performance.now(); | ||||||
|       const req = plugins.https.request(options, (res) => { |       // disable connection pooling to avoid listener accumulation across requests | ||||||
|  |       const reqOptions = { ...options, agent: false }; | ||||||
|  |       const req = plugins.https.request(reqOptions, (res) => { | ||||||
|         res.once('readable', () => { |         res.once('readable', () => { | ||||||
|           ttfb = plugins.perfHooks.performance.now(); |           ttfb = plugins.perfHooks.performance.now(); | ||||||
|         }); |         }); | ||||||
|         res.on('data', () => {}); |         res.on('data', () => {}); | ||||||
|         res.on('end', () => { |         res.on('end', () => { | ||||||
|           ended = plugins.perfHooks.performance.now(); |           ended = plugins.perfHooks.performance.now(); | ||||||
|           resolve([ |             resolve([ | ||||||
|             started, |               started, | ||||||
|             dnsLookup, |               dnsLookup, | ||||||
|             tcpHandshake, |               tcpHandshake, | ||||||
|             sslHandshake, |               sslHandshake, | ||||||
|             ttfb, |               ttfb, | ||||||
|             ended, |               ended, | ||||||
|             parseFloat(res.headers['server-timing'].slice(22) as any), |               parseFloat((res.headers['server-timing'] as string).slice(22)), | ||||||
|           ]); |             ]); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       req.on('socket', (socket) => { |       // Listen for timing events once per new socket | ||||||
|         socket.on('lookup', () => { |       req.once('socket', (socket) => { | ||||||
|  |         socket.once('lookup', () => { | ||||||
|           dnsLookup = plugins.perfHooks.performance.now(); |           dnsLookup = plugins.perfHooks.performance.now(); | ||||||
|         }); |         }); | ||||||
|         socket.on('connect', () => { |         socket.once('connect', () => { | ||||||
|           tcpHandshake = plugins.perfHooks.performance.now(); |           tcpHandshake = plugins.perfHooks.performance.now(); | ||||||
|         }); |         }); | ||||||
|         socket.on('secureConnect', () => { |         socket.once('secureConnect', () => { | ||||||
|           sslHandshake = plugins.perfHooks.performance.now(); |           sslHandshake = plugins.perfHooks.performance.now(); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| @@ -238,20 +246,14 @@ export class CloudflareSpeed { | |||||||
|       text |       text | ||||||
|         .split('\n') |         .split('\n') | ||||||
|         .map((i) => { |         .map((i) => { | ||||||
|           const j = i.split('='); |           const parts = i.split('='); | ||||||
|  |           return [parts[0], parts[1]]; | ||||||
|           return [j[0], j[1]]; |  | ||||||
|         }) |         }) | ||||||
|         .reduce((data: any, [k, v]) => { |         .reduce((data: Record<string, string>, [k, v]) => { | ||||||
|           if (v === undefined) return data; |           if (v === undefined) return data; | ||||||
|  |           data[k] = v; | ||||||
|           // Bypass prettier "no-assign-param" rules |           return data; | ||||||
|           const data1 = data; |         }, {} as Record<string, string>); | ||||||
|           // Object.fromEntries is only supported by Node.js 12 or newer |  | ||||||
|           data1[k] = v; |  | ||||||
|  |  | ||||||
|           return data1; |  | ||||||
|         }, {}); |  | ||||||
|  |  | ||||||
|     return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace); |     return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as plugins from './smartnetwork.plugins.js'; | import * as plugins from './smartnetwork.plugins.js'; | ||||||
|  |  | ||||||
| import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js'; | import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js'; | ||||||
|  | import { getLogger } from './logging.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * SmartNetwork simplifies actions within the network |  * SmartNetwork simplifies actions within the network | ||||||
| @@ -37,11 +38,7 @@ export class SmartNetwork { | |||||||
|  |  | ||||||
|     // test IPv4 space |     // test IPv4 space | ||||||
|     const ipv4Test = net.createServer(); |     const ipv4Test = net.createServer(); | ||||||
|     ipv4Test.once('error', (err: any) => { |     ipv4Test.once('error', () => { | ||||||
|       if (err.code !== 'EADDRINUSE') { |  | ||||||
|         doneIpV4.resolve(false); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       doneIpV4.resolve(false); |       doneIpV4.resolve(false); | ||||||
|     }); |     }); | ||||||
|     ipv4Test.once('listening', () => { |     ipv4Test.once('listening', () => { | ||||||
| @@ -56,11 +53,7 @@ export class SmartNetwork { | |||||||
|  |  | ||||||
|     // test IPv6 space |     // test IPv6 space | ||||||
|     const ipv6Test = net.createServer(); |     const ipv6Test = net.createServer(); | ||||||
|     ipv6Test.once('error', function (err: any) { |     ipv6Test.once('error', () => { | ||||||
|       if (err.code !== 'EADDRINUSE') { |  | ||||||
|         doneIpV6.resolve(false); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       doneIpV6.resolve(false); |       doneIpV6.resolve(false); | ||||||
|     }); |     }); | ||||||
|     ipv6Test.once('listening', () => { |     ipv6Test.once('listening', () => { | ||||||
| @@ -87,14 +80,15 @@ export class SmartNetwork { | |||||||
|     const domainPart = domainArg.split(':')[0]; |     const domainPart = domainArg.split(':')[0]; | ||||||
|     const port = portArg ? portArg : parseInt(domainArg.split(':')[1], 10); |     const port = portArg ? portArg : parseInt(domainArg.split(':')[1], 10); | ||||||
|  |  | ||||||
|     plugins.isopen(domainPart, port, (response: any) => { |     plugins.isopen( | ||||||
|       console.log(response); |       domainPart, | ||||||
|       if (response[port.toString()].isOpen) { |       port, | ||||||
|         done.resolve(true); |       (response: Record<string, { isOpen: boolean }>) => { | ||||||
|       } else { |         getLogger().debug(response); | ||||||
|         done.resolve(false); |         const portInfo = response[port.toString()]; | ||||||
|       } |         done.resolve(Boolean(portInfo?.isOpen)); | ||||||
|     }); |       }, | ||||||
|  |     ); | ||||||
|     const result = await done.promise; |     const result = await done.promise; | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
| @@ -110,7 +104,7 @@ export class SmartNetwork { | |||||||
|   }> { |   }> { | ||||||
|     const defaultGatewayName = await plugins.systeminformation.networkInterfaceDefault(); |     const defaultGatewayName = await plugins.systeminformation.networkInterfaceDefault(); | ||||||
|     if (!defaultGatewayName) { |     if (!defaultGatewayName) { | ||||||
|       console.log('Cannot determine default gateway'); |       getLogger().warn?.('Cannot determine default gateway'); | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|     const gateways = await this.getGateways(); |     const gateways = await this.getGateways(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user