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:
parent
bc19c21949
commit
d6c0af35fa
@ -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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user