"use strict"; const fs = require("fs"); const path = require("path"); const { obfuscate } = require("./obfuscate"); const C = { reset: "\x1b[0m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", white: "\x1b[37m" }; function createLogger({ logDir, retentionDays }) { if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } cleanupOldLogs(logDir, retentionDays); const cleanupTimer = setInterval(() => cleanupOldLogs(logDir, retentionDays), 3600000); cleanupTimer.unref(); function getLogPath() { const date = new Date().toISOString().slice(0, 10); return path.join(logDir, `service.${date}.log`); } function ts() { const d = new Date(); return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0") + " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0") + ":" + String(d.getSeconds()).padStart(2, "0") + "." + String(d.getMilliseconds()).padStart(3, "0"); } function ipPad(ip) { return (ip || "-").padEnd(17); } // 写文件日志(纯文本格式,异步避免阻塞事件循环) function writeFile(line) { const filePath = getLogPath(); const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {} } fs.appendFile(filePath, line + "\n", "utf-8", (err) => { if (err) console.error(`[logger] 写日志失败: ${filePath} - ${err.message}`); }); } // 写控制台 + 文件 function log(ip, color, icon, msg) { const time = ts(); // 控制台:彩色 console.log(`${C.dim}${time.slice(-12)}${C.reset} ${color}${icon}${C.reset} ${C.white}${ipPad(ip)}${C.reset} ${msg}`); // 文件:纯文本 writeFile(`${time} ${icon} [${ip || "-"}] ${msg}`); } return { // ── 系统事件 ── sys(msg) { const time = ts(); console.log(`${C.dim}${time.slice(-12)}${C.reset} ${C.dim}▸${C.reset} ${C.dim}${msg}${C.reset}`); writeFile(`${time} ▸ [system] ${msg}`); }, // ── TCP 握手 ── connecting(ip) { log(ip, C.cyan, "⏳", `TCP 握手 - 正在连接 ${ip}`); }, connected(ip, port) { log(ip, C.green, "✓", `TCP 握手成功 - 已连接 ${ip}:${port}`); }, disconnected(ip, reason) { log(ip, C.yellow, "✂", `TCP 连接断开 - ${reason}`); }, reconnecting(ip, delaySec) { log(ip, C.cyan, "↻", `计划重连 - ${delaySec.toFixed(1)}s 后重试`); }, // ── 数据收发 ── sendK(ip) { log(ip, C.yellow, "→", `发送轮询请求`); }, recvK(ip, rawFrame, fieldCount, statusCode) { log(ip, C.green, "←", `${obfuscate(rawFrame)}`); }, // ── 异常 ── warn(ip, msg) { log(ip, C.yellow, "!", msg); }, error(ip, msg, extra) { const line = extra ? `${msg} ${JSON.stringify(extra)}` : msg; log(ip, C.red, "✗", line); }, // ── 上传事件 ── upload(channel, stage, deviceNo, msg) { const color = channel === "mqtt" ? C.cyan : channel === "aliyun" ? C.green : C.dim; const icon = stage === "publish" || stage === "postProps" ? "↑" : stage === "tuple" ? "⚙" : stage === "connect" ? "✓" : stage === "error" ? "✗" : "·"; log(deviceNo, color, icon, `[${channel}/${stage}] ${msg}`); }, close() { clearInterval(cleanupTimer); }, }; } function cleanupOldLogs(logDir, retentionDays) { const now = Date.now(); const maxAge = retentionDays * 86400000; let files; try { files = fs.readdirSync(logDir); } catch (_) { return; } for (const file of files) { if (!file.endsWith(".log")) continue; const filePath = path.join(logDir, file); let stat; try { stat = fs.statSync(filePath); } catch (_) { continue; } if (now - stat.mtimeMs > maxAge) { try { fs.unlinkSync(filePath); } catch (_) { /* ignore */ } } } } module.exports = { createLogger };