"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 };
|