Refactor log rule handling and enhance test session logic

Replaces per-rule application with a unified error-based approach in log processing. Expands and clarifies log rules, introduces deduplication and error counting in TestSession, and updates types to support richer error and result reporting. Improves final result output for better clarity and debugging.
This commit is contained in:
nguyentrungthat 2025-12-19 15:42:02 +07:00
parent b8ab1f0583
commit b9598f9351
3 changed files with 227 additions and 33 deletions

View File

@ -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)
}

View File

@ -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<string>()
applyParsedLog(log: ParsedLog) {
// Detect boot OK
if (/IOS XE Software|System Bootstrap/.test(log.raw)) {
this.bootOk = true
}
public finalize() {
if (!this.boot) return 'FAIL'
if (this.hasHwFail) return 'FAIL'
if (this.licenseWarn) return 'PARTIAL'
return 'PASS'
const matchedErrors = applyRules(log)
matchedErrors.forEach((err) => this.addError(err))
}
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'
}
}
}

View File

@ -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[]
}