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:
Philipp Kunz 2025-04-28 15:30:08 +00:00
parent bc19c21949
commit d6c0af35fa
6 changed files with 115 additions and 61 deletions

View File

@ -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

View File

@ -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
View 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
View 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;
}

View File

@ -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);
} }

View File

@ -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();