Add email reporting for test errors and AI log analysis

Introduces functions to format test errors and AI log analysis as HTML tables and send them via email. Adds helper utilities for error mapping and HTML escaping, and updates types to support structured error rows. Also increases the log check interval and refactors buffer management for improved clarity.
This commit is contained in:
nguyentrungthat 2025-12-22 16:54:02 +07:00
parent 744472f3da
commit 0af1cd8747
3 changed files with 144 additions and 26 deletions

View File

@ -7,9 +7,12 @@ import {
classifyLog,
cleanData,
detectScenarioByModel,
escapeHtml,
isValidJson,
LogStreamBuffer,
mapErrorsToRows,
mapToLineFormat,
sendMessageToMail,
sleep,
TestSession,
} from '../ultils/helper.js'
@ -18,6 +21,7 @@ import path from 'node:path'
import axios from 'axios'
import redis from '@adonisjs/redis/services/main'
import Line from '#models/line'
import { ErrorRow, TestResult } from '../ultils/types.js'
type Inventory = {
pid: string
@ -424,15 +428,6 @@ export default class LineConnection {
lineId: this.config.id,
title: '',
})
this.outputBuffer = ''
this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n`
appendLog(
`\n---end-scenarios---${now}---${userName}---\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
const logScenarios = this.outputScenario
const data = textfsmResults(logScenarios, '')
@ -469,7 +464,7 @@ export default class LineConnection {
script.title.includes('DPELP')
) {
this.listScenarios.push(scenario.id)
this.outputScenario = ''
// this.outputScenario = ''
this.runScript(scenario, userName)
// this.socketIO.emit('confirm_scenario', {
// scenario: scenario,
@ -507,8 +502,16 @@ export default class LineConnection {
} catch (error) {
console.log(error)
}
this.outputBuffer = ''
this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n`
appendLog(
`\n---end-scenarios---${now}---${userName}---\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
this.listScenarios = []
this.outputScenario = ''
resolve(true)
return
}
@ -807,18 +810,106 @@ export default class LineConnection {
}
const result = this.session.finalize()
console.log('===== TEST RESULT =====')
console.log('STATUS:', result.status)
console.log('SUMMARY:', result.summary)
if (result.errors.length === 0) return
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)
})
// 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)
// })
const detectLog = await this.detectLogWithAI(this.bufferLog.buffer)
// console.log(detectLog)
const tableHTML = this.buildEmailContent(result, detectLog)
await sendMessageToMail(
'andrew.ng@apactech.io',
`[${result.status}] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Cisco Device Log Result`,
tableHTML
// ,
// ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io']
)
this.session.clear()
this.bufferLog.clear()
} catch (err: any) {
console.error('Error checking log:', err)
}
}, 30000)
}, 300000)
}
renderErrorTable(rows: ErrorRow[]): string {
if (!rows.length) {
return `<p style="color: green;">No errors detected</p>`
}
const header = `
<tr>
<th style="padding:6px;">Level</th>
<th style="padding:6px;">Rule</th>
<th style="padding:6px; text-align:center;">Message</th>
<th style="padding:6px; width:1000px;">Log Evidence</th>
</tr>
`
const body = rows
.map(
(r) => `
<tr>
<td style="padding:6px;">
<span style="color:${r.level === 'FAIL' ? 'red' : 'orange'};">${r.level}</span>
</td>
<td style="padding:6px;">${r.rule}</td>
<td style="padding:6px; text-align:center;">${r.message}</td>
<td style="padding:6px;font-family:monospace;">
${escapeHtml(r.log)}
</td>
</tr>
`
)
.join('')
return `
<table border="1" cellpadding="6" style="border-collapse: collapse; width:100%;">
${header}
${body}
</table>
`
}
renderAIDetectTable(row: any): string {
return `
<table border="1" cellpadding="6" style="border-collapse: collapse; width:100%;">
<tr>
<th style="padding:6px;">Summary</th>
<th style="padding:6px;">Issues</th>
</tr>
<tr>
<td style="text-wrap: wrap;">${row.summary || ''}</td>
<td style="width:1000px; text-wrap: wrap;">${row.issues?.length ? `- ` + row.issues.join(`<br>- `) : '- No issues detected.'}</td>
</tr>
</table>
`
}
buildEmailContent(result: TestResult, value: any): string {
const rows = mapErrorsToRows(result.errors)
const table = this.renderErrorTable(rows)
const tableAI = this.renderAIDetectTable(value)
return `
<h3>Cisco Device Log Result</h3>
<p>Line: <b>${this.config.lineNumber}</b> - Station: <b>${this.config.stationName}</b></p>
<p><b>Status:</b> ${result.status}</p>
<p><b>Summary:</b> ${result.summary}</p>
<hr />
${table}
<br />
<hr />
<p>AI Detected:</p>
${tableAI}
`
}
}

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, TestError, TestResult } from './types.js'
import { ErrorRow, LogRule, ParsedLog, TestError, TestResult } from './types.js'
type DetectAI = {
status: string[]
@ -510,9 +510,6 @@ export class TestSession {
bootOk = false
errors: TestError[] = []
// 👉 dùng Set để deduplicate
private errorFingerprints = new Set<string>()
applyParsedLog(log: ParsedLog) {
// Detect boot OK
if (/IOS XE Software|System Bootstrap/.test(log.raw)) {
@ -562,6 +559,10 @@ export class TestSession {
}
}
clear() {
this.errors = []
}
private buildSummary(status: TestResult['status']): string {
switch (status) {
case 'PASS':
@ -575,10 +576,10 @@ export class TestSession {
}
export class LogStreamBuffer {
private buffer = ''
public buffer = ''
public push(chunk: Buffer): string[] {
this.buffer += chunk.toString('utf8')
this.buffer += chunk.toString('utf8').replace('--More--', '').trim()
const lines = this.buffer.split(/\r?\n/)
this.buffer = lines.pop() || ''
@ -592,4 +593,22 @@ export class LogStreamBuffer {
this.buffer = ''
return last
}
public clear() {
this.buffer = ''
}
}
export function mapErrorsToRows(errors: TestError[]): ErrorRow[] {
return errors.map((e) => ({
level: e.level,
rule: e.ruleId,
message: e.message,
log: e.evidence.raw,
count: e.evidence.count ?? 1,
}))
}
export function escapeHtml(str: string): string {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

View File

@ -47,3 +47,11 @@ export interface TestResult {
summary: string
errors: TestError[]
}
export interface ErrorRow {
level: string
rule: string
message: string
log: string
count: number
}