diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 78f9bf5..09214e4 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -788,12 +788,7 @@ export default class LineConnection { handleLogLine = (line: string) => { try { const parsed = classifyLog(line) - const matchedRules = applyRules(parsed) - - matchedRules.forEach((rule) => { - // console.log(rule) - this.session.applyRule(rule) - }) + this.session.applyParsedLog(parsed) } catch (error) { console.log('handleLogLine', error) } @@ -807,7 +802,16 @@ export default class LineConnection { return } const result = this.session.finalize() - console.log('FINAL RESULT:', this.config.apcName, this.config.lineNumber, result) + + console.log('===== TEST RESULT =====') + console.log('STATUS:', result.status) + console.log('SUMMARY:', result.summary) + + result.errors.forEach((err, idx) => { + console.log(`\n[${idx + 1}] ${err.level} - ${err.ruleId}`) + console.log('Message:', err.message) + console.log('Log:', err.evidence.raw) + }) } catch (err: any) { console.error('Error checking log:', err) } diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 373e1f5..afe3388 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -3,7 +3,7 @@ import fs from 'node:fs' import path from 'node:path' import nodeMailer from 'nodemailer' import zulip from 'zulip-js' -import { LogRule, ParsedLog } from './types.js' +import { LogRule, ParsedLog, TestError, TestResult } from './types.js' type DetectAI = { status: string[] @@ -351,58 +351,226 @@ export function classifyLog(line: string): ParsedLog { } export const RULES: LogRule[] = [ + // BOOT { id: 'BOOT_OK', category: 'BOOT', - match: /IOS XE Software|System Bootstrap/, - result: 'PASS', + match: /IOS XE Software|System Bootstrap|Boot successful/i, + level: 'PASS', message: 'Boot successful', }, { id: 'BOOT_LOOP', category: 'BOOT', match: /boot loop|reloading|restart/i, - result: 'FAIL', + level: 'FAIL', message: 'Boot loop detected', }, + { + id: 'BOOT_CRASH', + category: 'BOOT', + match: /crashinfo|Traceback|Kernel panic/i, + level: 'FAIL', + message: 'System crash detected during boot', + }, + { + id: 'BOOT_SLOW', + category: 'BOOT', + match: /Booting.*takes longer than expected/i, + level: 'WARN', + message: 'Boot time abnormal', + }, + // LICENSE + { + id: 'LICENSE_OK', + category: 'LICENSE', + match: /License State:\s*ACTIVE|Smart Licensing Status:\s*AUTHORIZED/i, + level: 'PASS', + message: 'License active', + }, { id: 'LICENSE_EXPIRED', category: 'LICENSE', match: /Evaluation.*expired|license expired/i, - result: 'WARN', + level: 'WARN', message: 'License expired', }, { - id: 'HW_FAIL', + id: 'LICENSE_NOT_REGISTERED', + category: 'LICENSE', + match: /NOT REGISTERED|Registration failed/i, + level: 'WARN', + message: 'License not registered', + }, + { + id: 'LICENSE_DISABLED', + category: 'LICENSE', + match: /Feature.*disabled due to license/i, + level: 'FAIL', + message: 'Critical features disabled by license', + }, + // INTERFACE + { + id: 'INTERFACE_UP', + category: 'INTERFACE', + match: /LINK-3-UPDOWN: Interface .* up/i, + level: 'PASS', + message: 'Interface up', + }, + { + id: 'INTERFACE_FLAP', + category: 'INTERFACE', + match: /LINK-3-UPDOWN: Interface .* down/i, + level: 'WARN', + message: 'Interface flapping detected', + }, + { + id: 'INTERFACE_ERROR', + category: 'INTERFACE', + match: /input errors|CRC|frame error/i, + level: 'WARN', + message: 'Interface errors detected', + }, + // HARDWARE + { + id: 'FAN_FAIL', category: 'HARDWARE', - match: /(FAN|PSU).*(FAIL|CRITICAL)/i, - result: 'FAIL', - message: 'Hardware failure', + match: /FAN.*(FAIL|CRITICAL|NOT PRESENT)/i, + level: 'FAIL', + message: 'Fan failure', + }, + { + id: 'PSU_FAIL', + category: 'HARDWARE', + match: /PSU.*(FAIL|CRITICAL|NOT PRESENT)/i, + level: 'FAIL', + message: 'Power supply failure', + }, + { + id: 'TEMP_HIGH', + category: 'HARDWARE', + match: /TEMP.*(HIGH|CRITICAL)/i, + level: 'FAIL', + message: 'Over temperature detected', + }, + { + id: 'HW_WARNING', + category: 'HARDWARE', + match: /ENVIRONMENT WARNING/i, + level: 'WARN', + message: 'Hardware environment warning', + }, + // ERROR + { + id: 'MEMORY_ERROR', + category: 'ERROR', + match: /malloc|out of memory|memory corruption/i, + level: 'FAIL', + message: 'Memory error detected', + }, + { + id: 'FLASH_ERROR', + category: 'ERROR', + match: /flash.*(error|corrupt|fail)/i, + level: 'FAIL', + message: 'Flash storage error', + }, + { + id: 'CONFIG_MISSING', + category: 'ERROR', + match: /startup-config is missing|No configuration found/i, + level: 'WARN', + message: 'Startup configuration missing', + }, + { + id: 'SECURE_BOOT_FAIL', + category: 'ERROR', + match: /Secure Boot.*(FAIL|ERROR)/i, + level: 'FAIL', + message: 'Secure boot failed', }, ] -export function applyRules(log: ParsedLog): LogRule[] { - return RULES.filter((rule) => rule.category === log.category && rule.match.test(log.raw)) +export function applyRules(log: ParsedLog): TestError[] { + return RULES.filter( + (rule): rule is LogRule & { level: 'FAIL' | 'WARN' } => + rule.category === log.category && rule.match.test(log.raw) && rule.level !== 'PASS' + ).map((rule) => ({ + ruleId: rule.id, + level: rule.level, // ✅ giờ TS biết chắc chỉ FAIL | WARN + message: rule.message, + evidence: { + raw: log.raw, + timestamp: log.timestamp, + }, + })) } export class TestSession { - boot = false - hasHwFail = false - licenseWarn = false - issues: string[] = [] + bootOk = false + errors: TestError[] = [] - public applyRule(rule: LogRule) { - if (rule.id === 'BOOT_OK') this.boot = true - if (rule.result === 'FAIL') this.hasHwFail = true - if (rule.result === 'WARN') this.licenseWarn = true - this.issues.push(rule.message) + // 👉 dùng Set để deduplicate + private errorFingerprints = new Set() + + applyParsedLog(log: ParsedLog) { + // Detect boot OK + if (/IOS XE Software|System Bootstrap/.test(log.raw)) { + this.bootOk = true + } + + const matchedErrors = applyRules(log) + matchedErrors.forEach((err) => this.addError(err)) } - public finalize() { - if (!this.boot) return 'FAIL' - if (this.hasHwFail) return 'FAIL' - if (this.licenseWarn) return 'PARTIAL' - return 'PASS' + private addError(err: TestError) { + const fingerprint = `${err.ruleId}|${this.normalize(err.evidence.raw)}` + + const existing = this.errors.find( + (e) => `${e.ruleId}|${this.normalize(e.evidence.raw)}` === fingerprint + ) + + if (existing) { + existing.evidence.count = (existing.evidence.count ?? 1) + 1 + return + } + + err.evidence.count = 1 + this.errors.push(err) + } + + private normalize(raw: string): string { + return raw + .toLowerCase() + .replace(/\d+/g, '#') // thay số (PSU 1, PSU 2 → PSU #) + .replace(/\s+/g, ' ') + .trim() + } + + finalize(): TestResult { + const hasFail = this.errors.some((e) => e.level === 'FAIL') + const hasWarn = this.errors.some((e) => e.level === 'WARN') + + let status: TestResult['status'] = 'PASS' + if (!this.bootOk || hasFail) status = 'FAIL' + else if (hasWarn) status = 'PARTIAL' + + return { + status, + summary: this.buildSummary(status), + errors: this.errors, + } + } + + private buildSummary(status: TestResult['status']): string { + switch (status) { + case 'PASS': + return 'All tests passed' + case 'FAIL': + return 'Critical errors detected' + case 'PARTIAL': + return 'Warnings detected during test' + } } } diff --git a/BACKEND/app/ultils/types.ts b/BACKEND/app/ultils/types.ts index 4a66cb3..ceda333 100644 --- a/BACKEND/app/ultils/types.ts +++ b/BACKEND/app/ultils/types.ts @@ -22,6 +22,28 @@ export interface LogRule { id: string category: LogCategory match: RegExp - result: RuleResult + level: RuleResult message: string } + +export interface LogEvidence { + raw: string + timestamp?: Date +} + +export interface TestError { + ruleId: string + level: 'FAIL' | 'WARN' + message: string + evidence: { + raw: string + timestamp?: Date + count?: number + } +} + +export interface TestResult { + status: 'PASS' | 'FAIL' | 'PARTIAL' + summary: string + errors: TestError[] +}