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:
parent
b8ab1f0583
commit
b9598f9351
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue