import electron from "electron"; import * as date from "date-and-time"; import path from "node:path"; import {AppEnv} from "../env"; import fs from "node:fs"; import dayjs from "dayjs"; import FileIndex from "../file"; let fileName = null; let fileStream = null; let appFileNames = {}; let appFileStreams = {}; const stringDatetime = () => { return date.format(new Date(), "YYYYMMDD"); }; const jsonStringifyLogData = (data: any) => { return JSON.stringify(data, (key, value) => { if (typeof value === "string" && value.length > 200) { if (value.startsWith("data:") || value.substring(0, 190).match(/^[a-zA-Z0-9+/=]+\s*$/)) { return value.substring(0, 100) + "...(length=" + value.length + ")"; } } return value; }) } const logsDir = () => { return path.join(AppEnv.userData, "logs"); }; const appLogsDir = () => { return path.join(AppEnv.dataRoot, "logs"); }; const root = () => { return logsDir(); }; const file = () => { return path.join(logsDir(), "log_" + stringDatetime() + ".log"); }; const appFile = (name: string) => { return path.join(appLogsDir(), name + "_" + stringDatetime() + ".log"); }; const cleanOldLogs = (keepDays: number) => { const logDirs = [ // 系统日志 logsDir(), // 应用日志 appLogsDir(), ]; for (const logDir of logDirs) { if (!fs.existsSync(logDir)) { return; } const files = fs.readdirSync(logDir); const now = new Date(); // console.log('cleanOldLogs', logDir, files) for (let file of files) { const filePath = path.join(logDir, file); let date = null; for (let s of file.split(/[_\\.]/)) { // 匹配 YYYYMMDD if (s.match(/^\d{8}$/)) { date = s; break; } } if (!date) { continue; } const fileDate = new Date( parseInt(date.substring(0, 4)), parseInt(date.substring(4, 6)) - 1, parseInt(date.substring(6, 8)) ); const diff = Math.abs(now.getTime() - fileDate.getTime()); const diffDays = Math.ceil(diff / (1000 * 3600 * 24)); // console.log('fileDate', file, fileDate, diffDays) if (diffDays > keepDays) { fs.unlinkSync(filePath); } } } }; const log = (level: "INFO" | "ERROR", label: string, data: any = null) => { if (fileName !== file()) { fileName = file(); const logDir = logsDir(); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir); } if (fileStream) { fileStream.end(); } fileStream = fs.createWriteStream(fileName, {flags: "a"}); cleanOldLogs(14); } let line = []; line.push(date.format(new Date(), "YYYY-MM-DD HH:mm:ss")); line.push(level); line.push(label); if (data) { if (!["number", "string"].includes(typeof data)) { data = jsonStringifyLogData(data); } line.push(data); } console.log(line.join(" - ")); fileStream.write(line.join(" - ") + "\n"); }; const info = (label: string, data: any = null) => { return log("INFO", label, data); }; const error = (label: string, data: any = null) => { return log("ERROR", label, data); }; const debug = (label: string, data: any = null) => { // debug 级别等同于 info,但在开发环境中可能显示更多细节 return log("INFO", label, data); }; const appLog = (name: string, level: "INFO" | "ERROR", label: string, data: any = null) => { let fileChanged = false; if (appFileNames[name] !== appFile(name)) { appFileNames[name] = appFile(name); fileChanged = true; } if (fileChanged || !appFileStreams[name]) { if (appFileStreams[name]) { appFileStreams[name].end(); } const logDir = appLogsDir(); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir); } appFileStreams[name] = fs.createWriteStream(appFileNames[name], {flags: "a"}); } let line = []; line.push(date.format(new Date(), "YYYY-MM-DD HH:mm:ss")); line.push(level); line.push(label); if (data) { if (!["number", "string"].includes(typeof data)) { data = JSON.stringify(data); } line.push(data); } console.log(`[APP:${name}] - ` + line.join(" - ")); appFileStreams[name].write(line.join(" - ") + "\n"); }; const appPath = (name: string) => { if (!appFileNames[name]) { appFileNames[name] = appFile(name); } return appFileNames[name]; }; const appInfo = (name: string, label: string, data: any = null) => { return appLog(name, "INFO", label, data); }; const appError = (name: string, label: string, data: any = null) => { return appLog(name, "ERROR", label, data); }; const infoRenderOrMain = (label: string, data: any = null) => { if (electron.ipcRenderer) { console.log("Log.info", label, data); return electron.ipcRenderer.invoke("log:info", label, data); } else { return info(label, data); } }; const errorRenderOrMain = (label: string, data: any = null) => { if (electron.ipcRenderer) { console.error("Log.error", label, data); return electron.ipcRenderer.invoke("log:error", label, data); } else { return error(label, data); } }; const debugRenderOrMain = (label: string, data: any = null) => { if (electron.ipcRenderer) { console.log("Log.debug", label, data); return electron.ipcRenderer.invoke("log:debug", label, data); } else { return debug(label, data); } }; const appInfoRenderOrMain = (name: string, label: string, data: any = null) => { if (electron.ipcRenderer) { console.log("Log.appInfo", name, label, data); return electron.ipcRenderer.invoke("log:appInfo", name, label, data); } else { return appInfo(name, label, data); } }; const appErrorRenderOrMain = (name: string, label: string, data: any = null) => { if (electron.ipcRenderer) { console.error("Log.appError", name, label, data); return electron.ipcRenderer.invoke("log:appError", name, label, data); } else { return appError(name, label, data); } }; const collectRenderOrMain = async (option?: { startTime?: string; endTime?: string; limit?: number }) => { option = Object.assign( { startTime: dayjs().subtract(1, "day").format("YYYY-MM-DD HH:mm:ss"), endTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), limit: 10 * 10000, }, option ); let startMs = dayjs(option.startTime).valueOf(); let endMs = dayjs(option.endTime).valueOf(); let startDayMs = dayjs(option.startTime).startOf("day").valueOf(); let endDayMs = dayjs(option.endTime).endOf("day").valueOf(); let resultLines = []; let logFiles = []; logFiles = logFiles.concat(await FileIndex.list(logsDir(), {isDataPath: false})); logFiles = logFiles.concat(await FileIndex.list(appLogsDir(), {isDataPath: false})); // console.log('logFiles', logFiles) logFiles = logFiles.filter(logFile => { if (logFile.isDirectory) { return false; } let date = null; for (let s of logFile.name.split(/[_\\.]/)) { // 匹配 YYYYMMDD if (s.match(/^\d{8}$/)) { date = s; break; } } if (!date) { return false; } const fileDate = new Date( parseInt(date.substring(0, 4)), parseInt(date.substring(4, 6)) - 1, parseInt(date.substring(6, 8)) ); if (fileDate.getTime() < startDayMs || fileDate.getTime() > endDayMs) { return false; } return true; }); // console.log('collectRenderOrMain', { // ...option, // logFiles, startMs, endMs, startDayMs, endDayMs // }) for (const logFile of logFiles) { await FileIndex.readLine( logFile.pathname, line => { const lineParts = line.split(" - "); const lineTime = dayjs(lineParts[0]); // console.log('lineTime', lineParts[0], lineTime.isBefore(startMs) || lineTime.isAfter(endMs)) if (lineTime.isBefore(startMs) || lineTime.isAfter(endMs)) { return; } resultLines.push(line); }, {isDataPath: false} ); } return { startTime: option.startTime, endTime: option.endTime, logs: resultLines.join("\n"), }; }; export default { root, info, error, debug, infoRenderOrMain, errorRenderOrMain, debugRenderOrMain, appPath, appInfo, appError, appInfoRenderOrMain, appErrorRenderOrMain, collectRenderOrMain, jsonStringifyLogData, }; export const Log = { jsonStringifyLogData, info: infoRenderOrMain, error: errorRenderOrMain, debug: debugRenderOrMain, warn: infoRenderOrMain, // warn 使用 info 实现 appPath, appInfo: appInfoRenderOrMain, appError: appErrorRenderOrMain, };