UNPKG

network-performance-analyzer

Version:

Automated analysis tool for network performance test datasets containing DNS testing results and iperf3 performance measurements

460 lines (399 loc) 17.4 kB
// Data Parser Service Implementation import { DataParser, TestParameters, TestResults, IperfTestResult, DnsTestResult, ParsingError, FileSystemError } from '../models'; import fs from 'fs-extra'; import { DefaultErrorHandler } from '../utils/ErrorHandler'; import { DataValidator } from '../utils/DataValidator'; import { StreamingJsonParser } from '../utils/StreamingJsonParser'; import path from 'path'; /** * Default implementation of the DataParser interface * Parses test parameters and results from JSON files */ export class DefaultDataParser implements DataParser { private errorHandler: DefaultErrorHandler; private validator: DataValidator; private streamingParser: StreamingJsonParser; private parserCache: Map<string, any> = new Map(); private fileSizeThresholdMB: number = 10; // Files larger than this will use streaming parser /** * Creates a new DefaultDataParser instance * @param errorHandler Optional custom error handler */ constructor(errorHandler?: DefaultErrorHandler) { this.errorHandler = errorHandler || new DefaultErrorHandler(); this.validator = new DataValidator(this.errorHandler); this.streamingParser = new StreamingJsonParser(this.errorHandler); } /** * Parse test parameters from a JSON file * @param filePath Path to the parameters JSON file * @returns A promise that resolves to the parsed test parameters * @throws ParsingError if the file cannot be read or parsed */ async parseParameters(filePath: string): Promise<TestParameters> { try { // Read the file const fileContent = await this.readFile(filePath); // Parse JSON const rawData = this.parseJson(fileContent, filePath); // Validate and transform the data const parameters: TestParameters = { backendServer: this.validateString(rawData['backend-server'], 'backend-server'), mtu: this.validateNumber(parseInt(rawData['mtu'], 10), 'mtu'), queryLogging: this.validateLoggingStatus(rawData['query-logging']), timestamp: rawData['timestamp'] || undefined }; // Validate the parameters const validationResult = this.validator.validateTestParameters(parameters); if (!validationResult.isValid) { const errorMessage = `Invalid test parameters: ${validationResult.errors .filter(e => e.severity === 'error') .map(e => `${e.field} - ${e.message}`) .join(', ')}`; throw new Error(errorMessage); } return parameters; } catch (error) { // If it's already a ParsingError, just rethrow it if ((error as ParsingError).filePath) { throw error; } // Create a ParsingError with file information const parsingError = new Error(error instanceof Error ? error.message : String(error)) as ParsingError; parsingError.filePath = filePath; // Add line and column information if available if (error instanceof SyntaxError && 'lineNumber' in error) { parsingError.lineNumber = (error as any).lineNumber; parsingError.columnNumber = (error as any).columnNumber; } // Handle the error const isRecoverable = this.errorHandler.handleParsingError(parsingError); // If the error is not recoverable, throw it if (!isRecoverable) { throw parsingError; } // Return a minimal valid object for recoverable errors return { backendServer: 'unknown', mtu: 1500, // Default MTU queryLogging: 'disabled' }; } } /** * Parse test results from a JSON file * @param filePath Path to the results JSON file * @returns A promise that resolves to the parsed test results * @throws ParsingError if the file cannot be read or parsed */ async parseResults(filePath: string): Promise<TestResults> { try { // Check cache first const cacheKey = this.getCacheKey(filePath); if (this.parserCache.has(cacheKey)) { console.log(`[DataParser] Using cached results for ${path.basename(filePath)}`); return this.parserCache.get(cacheKey); } // Initialize the results object const results: TestResults = { iperfTests: [], dnsResults: [] }; // Check if this is a large file that should use streaming const isLarge = this.streamingParser.isLargeFile(filePath, this.fileSizeThresholdMB); if (isLarge) { console.log(`[DataParser] Using streaming parser for large file: ${path.basename(filePath)}`); // Parse iperf tests using streaming await this.streamingParser.parseJsonStream<any>( { filePath, selector: 'iperf_tests.*', batchSize: 50, errorHandler: this.errorHandler }, async (batch) => { // Process each batch of iperf tests const parsedTests = this.parseIperfTests(batch); results.iperfTests.push(...parsedTests); } ); // Parse DNS results using streaming await this.streamingParser.parseJsonStream<any>( { filePath, selector: 'dns_tests.*', batchSize: 100, errorHandler: this.errorHandler }, async (batch) => { // Process each batch of DNS tests const parsedTests = this.parseDnsTests(batch); results.dnsResults.push(...parsedTests); } ); } else { // For smaller files, use the standard approach // Read the file const fileContent = await this.readFile(filePath); // Parse JSON const rawData = this.parseJson(fileContent, filePath); // Parse iperf tests if available if (rawData.iperf_tests && Array.isArray(rawData.iperf_tests)) { results.iperfTests = this.parseIperfTests(rawData.iperf_tests); } // Parse DNS results if available if (rawData.dns_tests && Array.isArray(rawData.dns_tests)) { results.dnsResults = this.parseDnsTests(rawData.dns_tests); } } // Validate the results const validationResult = this.validator.validateTestResults(results); // If validation passed, cache and return the sanitized data if (validationResult.isValid && validationResult.data) { this.parserCache.set(cacheKey, validationResult.data); return validationResult.data; } // If there are only warnings, cache and return the sanitized data if (validationResult.data && validationResult.errors.every(e => e.severity === 'warning')) { this.parserCache.set(cacheKey, validationResult.data); return validationResult.data; } // If there are errors, throw an exception const errorMessage = `Invalid test results: ${validationResult.errors .filter(e => e.severity === 'error') .map(e => `${e.field} - ${e.message}`) .join(', ')}`; throw new Error(errorMessage); } catch (error) { // If it's already a ParsingError, just rethrow it if ((error as ParsingError).filePath) { throw error; } // Create a ParsingError with file information const parsingError = new Error(error instanceof Error ? error.message : String(error)) as ParsingError; parsingError.filePath = filePath; // Add line and column information if available if (error instanceof SyntaxError && 'lineNumber' in error) { parsingError.lineNumber = (error as any).lineNumber; parsingError.columnNumber = (error as any).columnNumber; } // Handle the error const isRecoverable = this.errorHandler.handleParsingError(parsingError); // If the error is not recoverable, throw it if (!isRecoverable) { throw parsingError; } // Return a minimal valid object for recoverable errors return { iperfTests: [], dnsResults: [] }; } } /** * Read a file with error handling * @param filePath Path to the file to read * @returns The file content as a string * @throws FileSystemError if the file cannot be read */ private async readFile(filePath: string): Promise<string> { try { return await fs.readFile(filePath, 'utf8'); } catch (error) { // Create a FileSystemError with file information const fsError = new Error(error instanceof Error ? error.message : String(error)) as FileSystemError; fsError.code = (error as any).code || 'UNKNOWN'; fsError.path = filePath; // Handle the error const isRecoverable = this.errorHandler.handleFileSystemError(fsError); // If the error is not recoverable, throw it if (!isRecoverable) { throw fsError; } // Return empty string for recoverable errors return '{}'; } } /** * Parse JSON with error handling * @param content JSON content to parse * @param filePath Path to the file (for error reporting) * @returns The parsed JSON object * @throws ParsingError if the JSON cannot be parsed */ private parseJson(content: string, filePath: string): any { try { return JSON.parse(content); } catch (error) { // Create a ParsingError with file information const parsingError = new Error(error instanceof Error ? error.message : String(error)) as ParsingError; parsingError.filePath = filePath; // Add line and column information if available if (error instanceof SyntaxError && 'lineNumber' in error) { parsingError.lineNumber = (error as any).lineNumber; parsingError.columnNumber = (error as any).columnNumber; } // Handle the error const isRecoverable = this.errorHandler.handleParsingError(parsingError); // If the error is not recoverable, throw it if (!isRecoverable) { throw parsingError; } // Return empty object for recoverable errors return {}; } } /** * Parse iperf test results from raw data * @param rawTests Array of raw iperf test data * @returns Array of parsed IperfTestResult objects */ private parseIperfTests(rawTests: any[]): IperfTestResult[] { return rawTests.map(test => { const result: IperfTestResult = { server: this.validateString(test.server, 'server'), scenario: this.validateString(test.scenario, 'scenario'), success: this.validateBoolean(test.success, 'success') }; // Add optional fields if they exist if (test.start_time !== undefined) result.startTime = this.validateNumber(test.start_time, 'start_time'); if (test.end_time !== undefined) result.endTime = this.validateNumber(test.end_time, 'end_time'); if (test.duration !== undefined) result.duration = this.validateNumber(test.duration, 'duration'); if (test.num_streams !== undefined) result.numStreams = this.validateNumber(test.num_streams, 'num_streams'); if (test.cpu_utilization_host !== undefined) result.cpuUtilizationHost = this.validateNumber(test.cpu_utilization_host, 'cpu_utilization_host'); if (test.cpu_utilization_remote !== undefined) result.cpuUtilizationRemote = this.validateNumber(test.cpu_utilization_remote, 'cpu_utilization_remote'); // TCP specific fields if (test.tcp_mss_default !== undefined) result.tcpMssDefault = this.validateNumber(test.tcp_mss_default, 'tcp_mss_default'); if (test.retransmits !== undefined) result.retransmits = this.validateNumber(test.retransmits, 'retransmits'); if (test.snd_cwnd !== undefined) result.sndCwnd = this.validateNumber(test.snd_cwnd, 'snd_cwnd'); // UDP specific fields if (test.jitter_ms !== undefined) result.jitterMs = this.validateNumber(test.jitter_ms, 'jitter_ms'); if (test.packets !== undefined) result.packets = this.validateNumber(test.packets, 'packets'); if (test.lost_packets !== undefined) result.lostPackets = this.validateNumber(test.lost_packets, 'lost_packets'); if (test.packet_loss !== undefined) result.packetLoss = this.validateNumber(test.packet_loss, 'packet_loss'); // Common metrics if (test.blksize !== undefined) result.blksize = this.validateNumber(test.blksize, 'blksize'); if (test.bytes !== undefined) result.bytes = this.validateNumber(test.bytes, 'bytes'); if (test.bits_per_second !== undefined) result.bitsPerSecond = this.validateNumber(test.bits_per_second, 'bits_per_second'); if (test.bandwidth_mbps !== undefined) result.bandwidthMbps = this.validateNumber(test.bandwidth_mbps, 'bandwidth_mbps'); if (test.error !== undefined) result.error = this.validateString(test.error, 'error'); // Store raw data for detailed analysis result.allRawData = test.all_raw_data; return result; }); } /** * Parse DNS test results from raw data * @param rawTests Array of raw DNS test data * @returns Array of parsed DnsTestResult objects */ private parseDnsTests(rawTests: any[]): DnsTestResult[] { return rawTests.map(test => { const result: DnsTestResult = { domain: this.validateString(test.domain, 'domain'), dnsServer: this.validateString(test.dns_server, 'dns_server'), success: this.validateBoolean(test.success, 'success') }; // Add optional fields if they exist if (test.response_time_ms !== undefined) result.responseTimeMs = this.validateNumber(test.response_time_ms, 'response_time_ms'); if (test.query_time_ms !== undefined) result.queryTimeMs = this.validateNumber(test.query_time_ms, 'query_time_ms'); if (test.status !== undefined) result.status = this.validateString(test.status, 'status'); if (test.resolved_ips !== undefined && Array.isArray(test.resolved_ips)) { result.resolvedIps = test.resolved_ips.map((ip: any) => this.validateString(ip, 'resolved_ip')); } if (test.error !== undefined) result.error = this.validateString(test.error, 'error'); return result; }); } /** * Validate that a value is a string * @param value The value to validate * @param fieldName The name of the field being validated * @returns The validated string * @throws Error if the value is not a string */ private validateString(value: any, fieldName: string): string { if (typeof value !== 'string') { throw new Error(`Field '${fieldName}' must be a string, got ${typeof value}`); } return value; } /** * Validate that a value is a number * @param value The value to validate * @param fieldName The name of the field being validated * @returns The validated number * @throws Error if the value is not a number or is NaN */ private validateNumber(value: any, fieldName: string): number { if (typeof value !== 'number' || isNaN(value)) { throw new Error(`Field '${fieldName}' must be a number, got ${typeof value}`); } return value; } /** * Validate that a value is a boolean * @param value The value to validate * @param fieldName The name of the field being validated * @returns The validated boolean * @throws Error if the value is not a boolean */ private validateBoolean(value: any, fieldName: string): boolean { if (typeof value !== 'boolean') { throw new Error(`Field '${fieldName}' must be a boolean, got ${typeof value}`); } return value; } /** * Validate and convert logging status to the expected format * @param value The logging status value to validate * @returns The validated logging status * @throws Error if the value is not 'enabled' or 'disabled' */ private validateLoggingStatus(value: any): 'enabled' | 'disabled' { if (value !== 'enabled' && value !== 'disabled') { throw new Error(`Logging status must be 'enabled' or 'disabled', got '${value}'`); } return value; } /** * Generate a cache key for a file path * @param filePath Path to the file * @returns Cache key string */ private getCacheKey(filePath: string): string { // Use the file path and last modified time as the cache key try { const stats = fs.statSync(filePath); return `${filePath}:${stats.mtimeMs}`; } catch (error) { // If we can't get file stats, just use the path return filePath; } } /** * Clear the parser cache */ public clearCache(): void { this.parserCache.clear(); console.log('[DataParser] Cache cleared'); } /** * Get the current cache size * @returns Number of items in the cache */ public getCacheSize(): number { return this.parserCache.size; } /** * Set the file size threshold for streaming parsing * @param thresholdMB Threshold in megabytes */ public setFileSizeThreshold(thresholdMB: number): void { if (thresholdMB > 0) { this.fileSizeThresholdMB = thresholdMB; console.log(`[DataParser] File size threshold set to ${thresholdMB}MB`); } } } export { DataParser };