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
## 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)
Improve CI workflows, update project configuration, and clean up code formatting

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
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.'
}

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 { getLogger } from './logging.js';
import { NetworkError, TimeoutError } from './errors.js';
import * as stats from './helpers/stats.js';
export class CloudflareSpeed {
@ -49,7 +51,7 @@ export class CloudflareSpeed {
measurements.push(response[4] - response[0] - response[6]);
},
(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));
},
(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));
},
(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 }> {
const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations'));
return res.reduce((data: any, optionsArg: { iata: string; city: string }) => {
// Bypass prettier "no-assign-param" rules
const data1 = data;
data1[optionsArg.iata] = optionsArg.city;
return data1;
}, {});
const res = JSON.parse(
await this.get('speed.cloudflare.com', '/locations'),
) as Array<{ iata: string; city: string }>;
return res.reduce(
(data: Record<string, string>, optionsArg) => {
data[optionsArg.iata] = optionsArg.city;
return data;
},
{} as Record<string, string>,
);
}
public async get(hostname: string, path: string): Promise<string> {
@ -122,6 +125,8 @@ export class CloudflareSpeed {
hostname,
path,
method: 'GET',
// disable connection pooling to avoid listener accumulation
agent: false,
},
(res) => {
const body: Array<Buffer> = [];
@ -135,8 +140,8 @@ export class CloudflareSpeed {
reject(e);
}
});
req.on('error', (err) => {
reject(err);
req.on('error', (err: Error & { code?: string }) => {
reject(new NetworkError(err.message, err.code));
});
},
);
@ -179,7 +184,9 @@ export class CloudflareSpeed {
return new Promise((resolve, reject) => {
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', () => {
ttfb = plugins.perfHooks.performance.now();
});
@ -193,19 +200,20 @@ export class CloudflareSpeed {
sslHandshake,
ttfb,
ended,
parseFloat(res.headers['server-timing'].slice(22) as any),
parseFloat((res.headers['server-timing'] as string).slice(22)),
]);
});
});
req.on('socket', (socket) => {
socket.on('lookup', () => {
// Listen for timing events once per new socket
req.once('socket', (socket) => {
socket.once('lookup', () => {
dnsLookup = plugins.perfHooks.performance.now();
});
socket.on('connect', () => {
socket.once('connect', () => {
tcpHandshake = plugins.perfHooks.performance.now();
});
socket.on('secureConnect', () => {
socket.once('secureConnect', () => {
sslHandshake = plugins.perfHooks.performance.now();
});
});
@ -238,20 +246,14 @@ export class CloudflareSpeed {
text
.split('\n')
.map((i) => {
const j = i.split('=');
return [j[0], j[1]];
const parts = i.split('=');
return [parts[0], parts[1]];
})
.reduce((data: any, [k, v]) => {
.reduce((data: Record<string, string>, [k, v]) => {
if (v === undefined) return data;
// Bypass prettier "no-assign-param" rules
const data1 = data;
// Object.fromEntries is only supported by Node.js 12 or newer
data1[k] = v;
return data1;
}, {});
data[k] = v;
return data;
}, {} as Record<string, string>);
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 { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
import { getLogger } from './logging.js';
/**
* SmartNetwork simplifies actions within the network
@ -37,11 +38,7 @@ export class SmartNetwork {
// test IPv4 space
const ipv4Test = net.createServer();
ipv4Test.once('error', (err: any) => {
if (err.code !== 'EADDRINUSE') {
doneIpV4.resolve(false);
return;
}
ipv4Test.once('error', () => {
doneIpV4.resolve(false);
});
ipv4Test.once('listening', () => {
@ -56,11 +53,7 @@ export class SmartNetwork {
// test IPv6 space
const ipv6Test = net.createServer();
ipv6Test.once('error', function (err: any) {
if (err.code !== 'EADDRINUSE') {
doneIpV6.resolve(false);
return;
}
ipv6Test.once('error', () => {
doneIpV6.resolve(false);
});
ipv6Test.once('listening', () => {
@ -87,14 +80,15 @@ export class SmartNetwork {
const domainPart = domainArg.split(':')[0];
const port = portArg ? portArg : parseInt(domainArg.split(':')[1], 10);
plugins.isopen(domainPart, port, (response: any) => {
console.log(response);
if (response[port.toString()].isOpen) {
done.resolve(true);
} else {
done.resolve(false);
}
});
plugins.isopen(
domainPart,
port,
(response: Record<string, { isOpen: boolean }>) => {
getLogger().debug(response);
const portInfo = response[port.toString()];
done.resolve(Boolean(portInfo?.isOpen));
},
);
const result = await done.promise;
return result;
}
@ -110,7 +104,7 @@ export class SmartNetwork {
}> {
const defaultGatewayName = await plugins.systeminformation.networkInterfaceDefault();
if (!defaultGatewayName) {
console.log('Cannot determine default gateway');
getLogger().warn?.('Cannot determine default gateway');
return null;
}
const gateways = await this.getGateways();