1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-04-28 02:36:20 +00:00
thatmattlove-hyperglass/hyperglass/ui/hooks/use-table-to-string.ts
Wilhelm Schonfeldt 0398966062
feat: Add structured traceroute support with comprehensive IP enrichment
MAJOR NEW ARCHITECTURE - STRUCTURED TRACEROUTE:
- Complete rewrite of traceroute data processing with structured output
- Dedicated TracerouteResult and TracerouteHop data models
- Platform-specific parsers with unified output format
- Rich metadata including ASN, organization, country, and prefix information
- AS path visualization with organization names in React Flow charts

SUPPORTED PLATFORMS:
- TraceroutePluginMikrotik: Handles MikroTik's complex multi-table format
  * Progressive statistics parsing with deduplication
  * Timeout hop handling and continuation line processing
  * Loss percentage and RTT statistics extraction
- TraceroutePluginHuawei: Unix-style traceroute format parser
  * Standard hop_number ip_address rtt format support
  * Timeout hop detection with * notation
  * Automatic cleanup of excessive trailing timeouts

COMPREHENSIVE IP ENRICHMENT SYSTEM:
- Offline enrichment using BGP.tools bulk data (1.3M+ CIDR entries)
- PeeringDB integration for IXP detection and ASN organization data
- Ultra-fast pickle cache system with combined data files
- Integer-based bitwise IP matching for maximum performance
- Bulk ASN organization lookup capabilities
- Private/reserved IP handling with AS0 fallbacks
- Country code mapping from ASN database
- Graceful fallbacks for missing enrichment data

FRONTEND ENHANCEMENTS:
- New traceroute table components with consistent formatting
- Enhanced AS path visualization with organization names
- Improved copy-to-clipboard functionality with structured data
- Unified table styling across BGP and traceroute results
- Better error handling and loading states

CONCURRENT PROCESSING INFRASTRUCTURE:
- Thread executor implementation for blocking I/O operations
- Query deduplication system to prevent resource conflicts
- Non-blocking Redis cache operations using asyncio executors
- Event coordination for waiting requests
- Background cleanup for completed operations
- Prevents website hangs during long-running queries

PLUGIN ARCHITECTURE IMPROVEMENTS:
- Platform-aware plugin system with proper execution restrictions
- Enhanced MikroTik garbage output cleaning
- IP enrichment plugins for both BGP routes and traceroute
- Conditional plugin execution based on platform detection
- Proper async/sync plugin method handling

CRITICAL BUG FIXES:
- Fixed double AS prefix bug (ASAS123456 → AS123456)
- Resolved TracerouteHop avg_rtt field/property conflicts
- Corrected Huawei traceroute source field validation
- Fixed plugin platform restriction enforcement
- Eliminated blocking I/O causing UI freezes
- Proper timeout and empty response caching prevention
- Enhanced private IP range detection and handling

PERFORMANCE OPTIMIZATIONS:
- Pickle cache system reduces startup time from seconds to milliseconds
- Bulk processing for ASN organization lookups
- Simplified IXP detection using single PeeringDB API call
- Efficient CIDR network sorting and integer-based lookups
- Reduced external API calls by 90%+
- Optimized memory usage for large datasets

API & ROUTING ENHANCEMENTS:
- Enhanced API routes with proper error handling
- Improved middleware for concurrent request processing
- Better state management and event handling
- Enhanced task processing with thread pool execution

This represents a complete transformation of hyperglass traceroute capabilities,
moving from basic text output to rich, structured data with comprehensive
network intelligence and concurrent processing support.
2025-09-28 13:48:04 +02:00

240 lines
8.5 KiB
TypeScript

import { useCallback } from 'react';
import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc';
import { useConfig } from '~/context';
import { isStructuredOutput, isBGPStructuredOutput, isTracerouteStructuredOutput } from '~/types';
type TableToStringFormatter =
| ((v: string) => string)
| ((v: number) => string)
| ((v: number[]) => string)
| ((v: string[]) => string)
| ((v: boolean) => string);
interface TableToStringFormatted {
age: (v: number) => string;
active: (v: boolean) => string;
as_path: (v: number[]) => string;
communities: (v: string[]) => string;
rpki_state: (v: number, n: RPKIState) => string;
}
dayjs.extend(relativeTimePlugin);
dayjs.extend(utcPlugin);
function formatAsPath(path: number[]): string {
return path.join(' → ');
}
function formatCommunities(comms: string[]): string {
const commsStr = comms.map(c => ` - ${c}`);
return `\n ${commsStr.join('\n')}`;
}
function formatBool(val: boolean): string {
let fmt = '';
if (val === true) {
fmt = 'yes';
} else if (val === false) {
fmt = 'no';
}
return fmt;
}
function formatTime(val: number): string {
const now = dayjs.utc();
const then = now.subtract(val, 'second');
const timestamp = then.toString().replace('GMT', 'UTC');
const relative = now.to(then, true);
return `${relative} (${timestamp})`;
}
/**
* Get a function to convert table data to string, for use in the copy button component.
*/
export function useTableToString(
target: string[],
data: QueryResponse | undefined,
...deps: unknown[]
): () => string {
const { web, parsedDataFields, messages } = useConfig();
function formatRpkiState(val: number): string {
const rpkiStates = [
web.text.rpkiInvalid,
web.text.rpkiValid,
web.text.rpkiUnknown,
web.text.rpkiUnverified,
];
return rpkiStates[val];
}
const tableFormatMap = {
age: formatTime,
active: formatBool,
as_path: formatAsPath,
communities: formatCommunities,
rpki_state: formatRpkiState,
};
function isFormatted(key: string): key is keyof TableToStringFormatted {
return key in tableFormatMap;
}
function getFmtFunc(accessor: keyof Route): TableToStringFormatter {
if (isFormatted(accessor)) {
return tableFormatMap[accessor];
}
return String;
}
function doFormat(target: string[], data: QueryResponse | undefined): string {
let result = messages.noOutput;
try {
if (typeof data !== 'undefined' && isStructuredOutput(data)) {
// Handle BGP data
if (isBGPStructuredOutput(data)) {
// Check if this is BGP data with routes
if (!('routes' in data.output) || !Array.isArray(data.output.routes)) {
return messages.noOutput; // Not BGP data, return early
}
const tableStringParts = [
`Routes For: ${target.join(', ')}`,
`Timestamp: ${data.timestamp} UTC`,
];
for (const route of data.output.routes) {
for (const field of parsedDataFields) {
const [header, accessor, align] = field;
if (align !== null) {
let value = route[accessor];
// Handle fields that should be hidden when empty/not available
if ((accessor === 'source_rid' || accessor === 'age') &&
(value === null || value === undefined ||
(typeof value === 'string' && value.trim() === '') ||
(accessor === 'age' && value === -1))) {
continue; // Skip this field entirely
}
const fmtFunc = getFmtFunc(accessor) as (v: typeof value) => string;
value = fmtFunc(value);
if (accessor === 'prefix') {
tableStringParts.push(` - ${header}: ${value}`);
} else {
tableStringParts.push(` - ${header}: ${value}`);
}
}
}
}
result = tableStringParts.join('\n');
}
// Handle Traceroute data
else if (isTracerouteStructuredOutput(data)) {
if (!('hops' in data.output) || !Array.isArray(data.output.hops)) {
return messages.noOutput; // Not traceroute data, return early
}
const formatRTT = (rtt: number | null | undefined): string => {
if (rtt === null || rtt === undefined) return '*';
return `${rtt.toFixed(1)}ms`;
};
const formatIP = (hop: any): string => {
if (hop.display_ip) return hop.display_ip; // For truncated IPv6
if (hop.ip_address) return hop.ip_address;
return '*';
};
const formatASN = (hop: any): string => {
if (hop.asn) return `AS${hop.asn}`;
return '*';
};
const formatHostname = (hop: any): string => {
if (hop.hostname && hop.hostname !== 'None' && hop.hostname !== 'null') return hop.hostname;
return '*';
};
// Create a nicely formatted text table with proper column alignment
const header = `Traceroute to ${data.output.target} from ${data.output.source}`;
const timestamp = `Timestamp: ${data.timestamp} UTC`;
const separator = '=' .repeat(header.length);
// Calculate optimal column widths by examining all data
const columnWidths = {
hop: Math.max(3, ...data.output.hops.map(h => h.hop_number.toString().length)),
ip: Math.max(10, ...data.output.hops.map(h => formatIP(h).length)),
hostname: Math.max(8, ...data.output.hops.map(h => formatHostname(h).length)),
asn: Math.max(3, ...data.output.hops.map(h => formatASN(h).length)),
loss: 4, // "100%" is max
sent: Math.max(4, ...data.output.hops.map(h => (h.sent_count || 0).toString().length)),
last: Math.max(4, ...data.output.hops.map(h => formatRTT(h.last_rtt).length)),
avg: Math.max(3, ...data.output.hops.map(h => formatRTT(h.avg_rtt).length)),
best: Math.max(4, ...data.output.hops.map(h => formatRTT(h.best_rtt).length)),
worst: Math.max(5, ...data.output.hops.map(h => formatRTT(h.worst_rtt).length)),
};
// Create header row with proper spacing
const headerRow = [
'Hop'.padEnd(columnWidths.hop),
'IP Address'.padEnd(columnWidths.ip),
'Hostname'.padEnd(columnWidths.hostname),
'ASN'.padEnd(columnWidths.asn),
'Loss'.padEnd(columnWidths.loss),
'Sent'.padEnd(columnWidths.sent),
'Last'.padEnd(columnWidths.last),
'AVG'.padEnd(columnWidths.avg),
'Best'.padEnd(columnWidths.best),
'Worst'
].join(' ');
const totalWidth = headerRow.length;
const tableLines = [
header,
timestamp,
separator,
'',
headerRow,
'-'.repeat(totalWidth),
];
// Format data rows with consistent column widths
for (const hop of data.output.hops) {
const row = [
hop.hop_number.toString().padEnd(columnWidths.hop),
formatIP(hop).padEnd(columnWidths.ip),
formatHostname(hop).padEnd(columnWidths.hostname),
formatASN(hop).padEnd(columnWidths.asn),
`${hop.loss_pct || 0}%`.padEnd(columnWidths.loss),
(hop.sent_count || 0).toString().padEnd(columnWidths.sent),
formatRTT(hop.last_rtt).padEnd(columnWidths.last),
formatRTT(hop.avg_rtt).padEnd(columnWidths.avg),
formatRTT(hop.best_rtt).padEnd(columnWidths.best),
formatRTT(hop.worst_rtt)
].join(' ');
tableLines.push(row);
}
result = tableLines.join('\n');
}
}
return result;
} catch (err) {
console.error(err);
let error = String(err);
if (err instanceof Error) {
error = err.message;
}
return `An error occurred while parsing the output: '${error}'`;
}
}
const formatCallback = useCallback(doFormat, [target, data, doFormat]);
// eslint-disable-next-line react-hooks/exhaustive-deps
return useCallback(() => formatCallback(target, data), [target, data, formatCallback, ...deps]);
}