From 0af1cd87477649e0da853c699b61485be3acf0bf Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:54:02 +0700 Subject: [PATCH] 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. --- BACKEND/app/services/line_connection.ts | 131 ++++++++++++++++++++---- BACKEND/app/ultils/helper.ts | 31 ++++-- BACKEND/app/ultils/types.ts | 8 ++ 3 files changed, 144 insertions(+), 26 deletions(-) diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index f67da06..0a07cf6 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -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 `

No errors detected

` + } + + const header = ` + + Level + Rule + Message + Log Evidence + + ` + + const body = rows + .map( + (r) => ` + + + ${r.level} + + ${r.rule} + ${r.message} + + ${escapeHtml(r.log)} + + + ` + ) + .join('') + + return ` + + ${header} + ${body} +
+ ` + } + + renderAIDetectTable(row: any): string { + return ` + + + + + + + + + +
SummaryIssues
${row.summary || ''}${row.issues?.length ? `- ` + row.issues.join(`
- `) : '- No issues detected.'}
+ ` + } + + buildEmailContent(result: TestResult, value: any): string { + const rows = mapErrorsToRows(result.errors) + const table = this.renderErrorTable(rows) + const tableAI = this.renderAIDetectTable(value) + + return ` +

Cisco Device Log Result

+

Line: ${this.config.lineNumber} - Station: ${this.config.stationName}

+

Status: ${result.status}

+

Summary: ${result.summary}

+
+ ${table} +
+
+

AI Detected:

+ ${tableAI} + ` } } diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index afe3388..d57c8cb 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, 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() - 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, '>') } diff --git a/BACKEND/app/ultils/types.ts b/BACKEND/app/ultils/types.ts index ceda333..734cd25 100644 --- a/BACKEND/app/ultils/types.ts +++ b/BACKEND/app/ultils/types.ts @@ -47,3 +47,11 @@ export interface TestResult { summary: string errors: TestError[] } + +export interface ErrorRow { + level: string + rule: string + message: string + log: string + count: number +}