LogAnalyze/server.js

500 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require("express");
const mysql = require("mysql2/promise");
const bodyParser = require("body-parser");
const XLSX = require("xlsx-js-style");
const dotenv = require("dotenv");
dotenv.config();
const stringSimilarity = require("string-similarity");
const inititalWebRoute = require("./route/web");
// Nhận mảng message đầu vào, trả về mảng message đã loại bỏ trùng lặp
function deduplicateErrors(errors, threshold = 0.3) {
const unique = [];
for (const err of errors) {
const isDuplicate = unique.some((u) => {
const similarity = stringSimilarity.compareTwoStrings(
err.error_message,
u.error_message,
);
return similarity >= threshold;
});
if (!isDuplicate) {
unique.push(err);
}
}
return unique;
}
const app = express();
const PORT = 4000;
app.use(bodyParser.json());
app.use(express.static("public"));
app.use(express.urlencoded({ extended: true }));
const db = mysql.createPool({
host: "localhost",
user: "admin",
password: "Work1234",
database: "log_analysis",
});
async function columnExists(table, column) {
const [rows] = await db.query(
`SELECT COUNT(*) AS count
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = ? AND COLUMN_NAME = ? AND TABLE_SCHEMA = DATABASE()`,
[table, column],
);
return rows[0].count > 0;
}
// === Tạo bảng
(async () => {
await db.query(`
CREATE TABLE IF NOT EXISTS devices (
id INT AUTO_INCREMENT PRIMARY KEY,
pid VARCHAR(100) NOT NULL,
version VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_pid_version (pid, version),
INDEX idx_pid (pid),
INDEX idx_version (version),
INDEX idx_created (created_at)
)
`);
if (!(await columnExists("devices", "updated_at"))) {
await db.query(
`ALTER TABLE devices ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`,
);
}
if (await columnExists("devices", "updated_by")) {
await db.query(`ALTER TABLE devices DROP COLUMN updated_by`);
}
if (await columnExists("devices", "updated_user")) {
await db.query(
`ALTER TABLE devices DROP FOREIGN KEY fk_updated_by_user`,
);
await db.query(`ALTER TABLE devices DROP COLUMN updated_user`);
}
if (!(await columnExists("devices", "update_name"))) {
await db.query(`
ALTER TABLE devices
ADD COLUMN update_name VARCHAR(255)
`);
}
if (!(await columnExists("devices", "update_email"))) {
await db.query(`
ALTER TABLE devices
ADD COLUMN update_email VARCHAR(255)
`);
}
if (!(await columnExists("devices", "is_confirmed"))) {
await db.query(
`ALTER TABLE devices ADD COLUMN is_confirmed BOOLEAN DEFAULT FALSE`,
);
}
const createOutputTable = async (command) => {
await db.query(`
CREATE TABLE IF NOT EXISTS ${command}_outputs (
id INT AUTO_INCREMENT PRIMARY KEY,
device_id INT NOT NULL,
filename VARCHAR(255),
output TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_device_created (device_id, created_at),
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE
)
`);
if (await columnExists(`${command}_outputs`, "is_deleted")) {
await db.query(
`ALTER TABLE ${command}_outputs DROP COLUMN is_deleted`,
);
}
};
await createOutputTable("inventory");
await createOutputTable("version");
await createOutputTable("license");
await createOutputTable("logging");
await db.query(`
DROP TABLE IF EXISTS users
`);
})();
// === APIs ===
app.get("/api/pids", async (req, res) => {
const [rows] = await db.query(
"SELECT DISTINCT pid FROM devices ORDER BY pid",
);
res.json(rows.map((r) => r.pid));
});
app.get("/api/pid/:pid/versions", async (req, res) => {
const { pid } = req.params;
// Lấy danh sách các version tương ứng với PID
const [devices] = await db.query(
"SELECT id, version, update_name FROM devices WHERE pid = ?",
[pid],
);
const results = [];
for (const device of devices) {
const deviceId = device.id;
const version = device.version;
const [[inv]] = await db.query(
"SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?",
[deviceId],
);
const [[ver]] = await db.query(
"SELECT COUNT(*) AS c FROM version_outputs WHERE device_id = ?",
[deviceId],
);
const [[lic]] = await db.query(
"SELECT COUNT(*) AS c FROM license_outputs WHERE device_id = ?",
[deviceId],
);
const [[log]] = await db.query(
"SELECT COUNT(*) AS c FROM logging_outputs WHERE device_id = ?",
[deviceId],
);
results.push({
user: device.update_name,
version,
commands: {
inventory: inv.c,
version: ver.c,
license: lic.c,
logging: log.c,
},
});
}
res.json(results);
});
app.get("/api/device/:pid/:version/:command", async (req, res) => {
const { pid, version, command } = req.params;
const [[device]] = await db.query(
`SELECT id FROM devices WHERE pid = ? AND version = ?`,
[pid, version],
);
if (!device) return res.status(404).json({ message: "Not found" });
const [logs] = await db.query(
`SELECT id, filename, output, created_at FROM ${command}_outputs WHERE device_id = ? ORDER BY created_at DESC`,
[device.id],
);
res.json(logs);
});
app.post("/api/confirm-device", async (req, res) => {
const { pid, version, deletedLogs, name, email } = req.body;
if (!pid || !version) {
return res.status(400).json({
success: false,
message: "Missing pid or version",
});
}
try {
// Check if device exists
const [rows] = await db.query(
`SELECT id FROM devices WHERE pid = ? AND version = ?`,
[pid, version],
);
if (rows.length === 0) {
return res.status(404).json({
success: false,
message: "Device not found.",
});
}
if (rows[0].is_confirmed) {
return res.status(400).json({
success: false,
message: "Device is already confirmed.",
});
}
// Update device confirmation
await db.query(
`UPDATE devices
SET is_confirmed = true,
updated_at = CURRENT_TIMESTAMP,
update_name = ?,
update_email = ?
WHERE pid = ? AND version = ?`,
[name, email, pid, version],
);
// Delete Log
for (const item of deletedLogs) {
const { command, id } = item;
if (!command || typeof id !== "number") continue;
const allowedCommands = [
"inventory",
"version",
"license",
"logging",
];
if (!allowedCommands.includes(command)) continue;
const [rows] = await db.query(
`SELECT id FROM \`${command}_outputs\` WHERE id = ?`,
[id],
);
if (rows.length === 0) {
continue;
}
// Delete log
await db.query(`DELETE FROM \`${command}_outputs\` WHERE id = ?`, [
id,
]);
}
res.json({
success: true,
message: "Device confirmed successfully!",
});
} catch (error) {
console.error("Confirm device error:", error);
res.status(500).json({
success: false,
message: "Server error, please try again!",
});
}
});
app.post("/api/confirm-count", async (req, res) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: "Email is required" });
}
const [[result]] = await db.query(
"SELECT COUNT(*) AS c FROM devices WHERE update_email = ?",
[email],
);
res.json({ count: result.c });
} catch (err) {
console.error("Error fetching confirm count:", err);
res.status(500).json({ error: "Internal server error" });
}
});
app.get("/api/device-export-excel", async (req, res) => {
const [rows] = await db.query(
"SELECT pid, version, update_name, update_email, updated_at FROM devices",
);
const worksheetData = [
["No", "PID", "Verion", "Url", "Name", "Email", "Updated At"],
...rows.map((deviceItem, deviceIndex) => {
const url = `${process.env.BASE_URL}?pid=${deviceItem.pid}&version=${deviceItem.version}`;
return [
deviceIndex + 1,
deviceItem.pid,
deviceItem.version,
{ v: url, l: { Target: url } },
deviceItem.update_name,
deviceItem.update_email,
deviceItem.updated_at,
];
}),
];
// Create worksheet
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
// Add styles to header row
const headerStyle = {
font: { bold: true, color: { rgb: "FFFFFF" } },
fill: { fgColor: { rgb: "4F81BD" } },
alignment: { horizontal: "center" },
border: {
top: { style: "thin", color: { rgb: "000000" } },
bottom: { style: "thin", color: { rgb: "000000" } },
left: { style: "thin", color: { rgb: "000000" } },
right: { style: "thin", color: { rgb: "000000" } },
},
};
const commonCellStyle = {
alignment: { horizontal: "center", vertical: "center" },
};
const wrapTextStyle = {
alignment: { horizontal: "left", wrapText: true, vertical: "center" },
};
const dateCellStyle = {
alignment: { horizontal: "center", vertical: "center" },
};
// Set column widths
worksheet["!cols"] = [
{ wch: 6 }, // No
{ wch: 20 }, // PID
{ wch: 10 }, // Verion
{ wch: 60 }, // Url
{ wch: 20 }, // Name
{ wch: 30 }, // Email
{ wch: 10 }, // Updated At
];
["A1", "B1", "C1", "D1", "E1", "F1", "G1"].forEach((cell) => {
worksheet[cell].s = headerStyle;
});
const totalRows = worksheetData.length;
for (let row = 2; row <= totalRows; row++) {
const noCell = worksheet[`A${row}`];
if (noCell) noCell.s = commonCellStyle;
const dateCell = worksheet[`G${row}`];
if (dateCell) {
dateCell.s = dateCellStyle;
}
["B", "C", "D", "E", "F"].forEach((col) => {
const cell = worksheet[`${col}${row}`];
if (cell) cell.s = wrapTextStyle;
});
}
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Devices");
const buffer = XLSX.write(workbook, {
type: "buffer",
bookType: "xlsx",
});
res.setHeader("Content-Disposition", "attachment; filename=devices.xlsx");
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
);
res.send(buffer);
});
// Danh sách regex lọc lỗi
const errorPatterns = [
{
regex: /%\w+-[123]-\w+:/i,
description:
"Lỗi mức độ 13 (critical, error, alert) trong định dạng syslog Cisco",
},
{
regex: /%\w+-4-\w+:/i,
description: "Lỗi mức độ cảnh báo (level 4 - warning)",
},
{
regex: /(fail|error|unexpected|badcfg|invalid|denied|disabled|timeout|panic)/i,
description: "Từ khóa phổ biến của lỗi trong nội dung log",
},
{
regex: /^\d{1,2}:\d{2}:\d{2}:.*?%\w+-[1-4]-\w+:/i,
description: "Dòng log bắt đầu bằng timestamp có mã lỗi Cisco",
},
];
// API: Trả về các dòng lỗi từ bảng logging_outputs theo PID
app.get("/api/errors/:pid", async (req, res) => {
const pid = req.params.pid;
try {
const [rows] = await db.execute(
`
SELECT lo.output, d.pid, lo.filename
FROM logging_outputs lo
JOIN devices d ON lo.device_id = d.id
WHERE d.pid = ?
`,
[pid],
);
const matchedLines = [];
for (const row of rows) {
const lines = row.output.split(/\r?\n/);
for (const line of lines.filter(
(l) => l.includes("*") || l.includes("%"),
)) {
for (const pattern of errorPatterns) {
const regex = new RegExp(pattern.regex.source, "gi");
if (regex.test(line)) {
// Reset regex to start (because `.test()` may advance position in some cases)
regex.lastIndex = 0;
const highlighted = line.replace(
regex,
(match) => `<mark>${match}</mark>`,
);
matchedLines.push({
pid: row.pid,
filename: row.filename,
error_message: line.trim(),
highlighted_message: highlighted.trim(),
regex_pattern: pattern.regex.source,
description: pattern.description,
});
break;
}
}
}
}
const filtered = deduplicateErrors(matchedLines, 0.6);
res.json(filtered);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
});
app.get("/api/test", async (req, res) => {
console.log(req);
res.send("ok");
});
// Web Route
inititalWebRoute(app, db);
app.listen(PORT, () =>
console.log(`🚀 Server running at http://localhost:${PORT}`),
);