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:
parent
744472f3da
commit
0af1cd8747
|
|
@ -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}
|
||||
`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,3 +47,11 @@ export interface TestResult {
|
|||
summary: string
|
||||
errors: TestError[]
|
||||
}
|
||||
|
||||
export interface ErrorRow {
|
||||
level: string
|
||||
rule: string
|
||||
message: string
|
||||
log: string
|
||||
count: number
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue