Skip to content

Commit

Permalink
fix: mtr render (#58)
Browse files Browse the repository at this point in the history
* fix: too many decimal points

* fix: acync drops input values

* fix: tests

* fix: prevent NaN/Infinity values
  • Loading branch information
patrykcieszkowski authored Jun 15, 2022
1 parent adc553b commit bac79ad
Show file tree
Hide file tree
Showing 9 changed files with 960 additions and 1,153 deletions.
41 changes: 27 additions & 14 deletions src/command/handlers/mtr/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const getInitialHopState = () => ({
max: 0,
avg: 0,
total: 0,
rcv: 0,
drop: 0,
stDev: 0,
jMin: 0,
Expand All @@ -33,22 +34,30 @@ const withSpacing = (string_: string | number, dSpacing: number, left = false):
return `${string_}${sSpacing}`;
};

const roundNumber = (value: number): number => {
if (!Number.isFinite(value)) {
return 0;
}

return Number.parseFloat(value.toFixed(1));
};

// eslint-disable-next-line @typescript-eslint/naming-convention
export const MtrParser = {
outputBuilder(hops: HopType[]): string {
const rawOutput = [];

const spacings = {
index: String(hops.length).length,
asn: 2 + Math.max(...hops.map(h => String(h?.asn ?? '').length)),
asn: 2 + Math.max(...hops.map(h => String(h?.asn ?? 0).length)),
hostname: (3
+ Math.max(...hops.map(h => String(h?.host ?? '').length))
+ Math.max(...hops.map(h => String(h?.resolvedHost ?? '').length))
+ Math.max(...hops.map(h => String(h?.host ?? 0).length))
+ Math.max(...hops.map(h => String(h?.resolvedHost ?? 0).length))
),
loss: 6,
drop: Math.max(4, ...hops.map(h => String(h?.stats?.drop ?? '').length)),
avg: Math.max(...hops.map(h => String(h?.stats?.avg ?? '').length)),
rcv: 2 + Math.max(...hops.map(h => String(h?.stats?.drop ?? '').length)),
drop: Math.max(4, ...hops.map(h => String(h?.stats?.drop ?? 0).length)),
avg: Math.max(...hops.map(h => String(h?.stats?.avg ?? 0).length)),
rcv: 2 + Math.max(...hops.map(h => String(h?.stats?.drop ?? 0).length)),
stDev: 6,
jAvg: 5,
};
Expand Down Expand Up @@ -84,7 +93,7 @@ export const MtrParser = {
// Stats
const loss = withSpacing(((hop.stats.drop / hop.stats.total) * 100).toFixed(1), spacings.loss, true);
const drop = withSpacing(hop.stats.drop, spacings.drop, true);
const rcv = withSpacing((hop.stats.total - hop.stats.drop), spacings.rcv, true);
const rcv = withSpacing((hop.stats.rcv), spacings.rcv, true);
const avg = withSpacing(hop.stats.avg.toFixed(1), spacings.avg, true);
const stDev = withSpacing(hop.stats.stDev.toFixed(1), spacings.stDev, true);
const jAvg = withSpacing(hop.stats.jAvg.toFixed(1), spacings.jAvg, true);
Expand Down Expand Up @@ -145,8 +154,9 @@ export const MtrParser = {

case 'x': {
const [seq] = value;
const timeEntry = entry.times.find(t => t.seq === seq);

if (!seq) {
if (!seq || timeEntry) {
break;
}

Expand Down Expand Up @@ -190,11 +200,12 @@ export const MtrParser = {
if (timesArray.length > 0) {
stats.min = Math.min(...timesArray);
stats.max = Math.max(...timesArray);
stats.avg = Number.parseFloat((timesArray.reduce((a, b) => a + b, 0) / timesArray.length).toFixed(1));
stats.avg = roundNumber(timesArray.reduce((a, b) => a + b, 0) / timesArray.length);
stats.total = hop.times.length;
stats.stDev = Number.parseFloat((Math.sqrt(timesArray.map(x => (x - stats.avg) ** 2).reduce((a, b) => a + b, 0) / timesArray.length)).toFixed(1));
stats.stDev = roundNumber(Math.sqrt(timesArray.map(x => (x - stats.avg) ** 2).reduce((a, b) => a + b, 0) / timesArray.length));
}

stats.rcv = 0;
stats.drop = 0;

for (let i = 0; i < hop.times.length; i++) {
Expand All @@ -204,7 +215,9 @@ export const MtrParser = {
continue;
}

if (!rtt?.time) {
if (rtt?.time) {
stats.rcv++;
} else {
stats.drop++;
}
}
Expand All @@ -221,9 +234,9 @@ export const MtrParser = {
}

if (jitterArray.length > 0) {
stats.jMin = Math.min(...jitterArray);
stats.jMax = Math.max(...jitterArray);
stats.jAvg = Number.parseFloat((jitterArray.reduce((a, b) => a + b, 0) / jitterArray.length).toFixed(1));
stats.jMin = roundNumber(Math.min(...jitterArray));
stats.jMax = roundNumber(Math.max(...jitterArray));
stats.jAvg = roundNumber(jitterArray.reduce((a, b) => a + b, 0) / jitterArray.length);
}

return stats;
Expand Down
2 changes: 2 additions & 0 deletions src/command/handlers/mtr/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type HopStatsType = {
max: number;
avg: number;
total: number;
rcv: number;
drop: number;
stDev: number;
jMin: number;
Expand All @@ -25,5 +26,6 @@ export type HopType = {

export type ResultType = {
hops: HopType[];
data: string[];
rawOutput: string;
};
93 changes: 74 additions & 19 deletions src/command/mtr-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {isExecaError} from '../helper/execa-error-check.js';
import {InvalidOptionsException} from './exception/invalid-options-exception.js';

import type {HopType, ResultType} from './handlers/mtr/types.js';
import MtrParser from './handlers/mtr/parser.js';
import MtrParser, {NEW_LINE_REG_EXP} from './handlers/mtr/parser.js';

type MtrOptions = {
type: string;
Expand Down Expand Up @@ -69,17 +69,14 @@ export class MtrCommand implements CommandInterface<MtrOptions> {
let isResultPrivate = false;
const result: ResultType = {
hops: [],
data: [],
rawOutput: '',
};

cmd.stdout?.on('data', async (data: Buffer) => {
result.hops = await this.hopsParse(result.hops, data.toString());
result.rawOutput = MtrParser.outputBuilder(result.hops);

const isValid = this.validateResult(result.hops, cmd);
const emitProgress = () => {
isResultPrivate = !this.validateResult(result.hops, cmd);

if (!isValid) {
isResultPrivate = !isValid;
if (isResultPrivate) {
return;
}

Expand All @@ -88,15 +85,33 @@ export class MtrCommand implements CommandInterface<MtrOptions> {
measurementId,
overwrite: true,
result: {
...result,
hops: result.hops,
rawOutput: result.rawOutput,
},
});
};

cmd.stdout?.on('data', async (data: Buffer) => {
for (const line of data.toString().split(NEW_LINE_REG_EXP)) {
if (!line) {
continue;
}

result.data.push(line);
}

const [hops, rawOutput] = await this.parseResult(result.hops, result.data, false);
result.hops = hops;
result.rawOutput = rawOutput;

emitProgress();
});

try {
await cmd;
result.hops = await this.hopsParse(result.hops, '', true);
result.rawOutput = MtrParser.outputBuilder(result.hops);
const [hops, rawOutput] = await this.parseResult(result.hops, result.data, true);
result.hops = hops;
result.rawOutput = rawOutput;
} catch (error: unknown) {
const output = isExecaError(error) ? error.stderr.toString() : '';
result.rawOutput = output;
Expand All @@ -110,28 +125,68 @@ export class MtrCommand implements CommandInterface<MtrOptions> {
socket.emit('probe:measurement:result', {
testId,
measurementId,
result,
result: {
hops: result.hops,
rawOutput: result.rawOutput,
data: result.data,
},
});
}

async parseResult(hops: HopType[], data: string[], isFinalResult = false): Promise<[HopType[], string]> {
let nHops = this.parseData(hops, data.join('\n'), isFinalResult);
const asnList = await this.queryAsn(nHops);
nHops = this.populateAsn(nHops, asnList);
const rawOutput = MtrParser.outputBuilder(nHops);

return [nHops, rawOutput];
}

parseData(hops: HopType[], data: string, isFinalResult?: boolean): HopType[] {
return MtrParser.hopsParse(hops, data.toString(), isFinalResult);
}

populateAsn(hops: HopType[], asnList: string[][]): HopType[] {
return hops.map((hop: HopType) => {
const asn = asnList.find((a: string[]) => hop.host ? a.includes(hop.host) : false);

if (!asn) {
return hop;
}

return {
...hop,
asn: String(asn?.[1]),
};
});
}

async hopsParse(hops: HopType[], data: string, isFinalResult = false): Promise<HopType[]> {
const nHops = MtrParser.hopsParse(hops, data.toString(), isFinalResult);
const dnsResult = await Promise.allSettled(nHops.map(async h => h?.host && !h?.asn && !isIpPrivate(h?.host) ? this.lookupAsn(h?.host) : Promise.reject()));
async queryAsn(hops: HopType[]): Promise<string[][]> {
const dnsResult = await Promise.allSettled(hops.map(async h => (
!h?.asn && h?.host && !isIpPrivate(h?.host)
? this.lookupAsn(h?.host)
: Promise.reject()
)));

const asnList = [];

for (const [index, result] of dnsResult.entries()) {
if (result.status === 'rejected' || !result.value) {
const host = hops[index]?.host;

if (!host || result.status === 'rejected' || !result.value) {
continue;
}

const sDns = result.value.split('|');
nHops[index]!.asn = sDns[0]!.trim();
asnList.push([host, sDns[0]!.trim() ?? '']);
}

return nHops;
return asnList;
}

async lookupAsn(addr: string): Promise<string | undefined> {
const result = await this.dnsResolver(`${addr}.origin.asn.cymru.com`, 'TXT');
const reversedAddr = addr.split('.').reverse().join('.');
const result = await this.dnsResolver(`${reversedAddr}.origin.asn.cymru.com`, 'TXT');

return result.flat()[0];
}
Expand Down
Loading

0 comments on commit bac79ad

Please sign in to comment.