diff --git a/package-lock.json b/package-lock.json index 5022733..724c4c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@dnd-kit/utilities": "^3.2.2", "antd": "^6.4.3", "exceljs": "^4.4.0", + "log4js": "^6.9.1", "mssql": "^12.5.5", "next": "16.2.9", "react": "19.2.4", @@ -4894,6 +4895,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/dayjs": { "version": "1.11.21", "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", @@ -5952,7 +5962,6 @@ "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, "license": "ISC" }, "node_modules/for-each": { @@ -5977,6 +5986,20 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7079,6 +7102,15 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -7698,6 +7730,22 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8584,6 +8632,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -9054,6 +9108,20 @@ "node": ">= 0.4" } }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", @@ -9693,6 +9761,15 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -9848,6 +9925,7 @@ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/package.json b/package.json index 37b5405..0392145 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@dnd-kit/utilities": "^3.2.2", "antd": "^6.4.3", "exceljs": "^4.4.0", + "log4js": "^6.9.1", "mssql": "^12.5.5", "next": "16.2.9", "react": "19.2.4", diff --git a/src/server/log-format.test.ts b/src/server/log-format.test.ts new file mode 100644 index 0000000..dbc3f1b --- /dev/null +++ b/src/server/log-format.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from "vitest"; +import { formatError } from "./log-format"; + +describe("formatError", () => { + it("returns stack for Error", () => { + const e = new Error("boom"); + expect(formatError(e)).toBe(e.stack); + }); + it("falls back to message when stack missing", () => { + const e = new Error("boom"); + e.stack = undefined as unknown as string; + expect(formatError(e)).toBe("boom"); + }); + it("stringifies non-Error primitives", () => { + expect(formatError("oops")).toBe("oops"); + expect(formatError(42)).toBe("42"); + }); + it("handles null / undefined", () => { + expect(formatError(null)).toBe("Unknown error"); + expect(formatError(undefined)).toBe("Unknown error"); + }); +}); diff --git a/src/server/log-format.ts b/src/server/log-format.ts new file mode 100644 index 0000000..61a1b51 --- /dev/null +++ b/src/server/log-format.ts @@ -0,0 +1,11 @@ +/** + * 把任意错误/值格式化为带堆栈的字符串,用于日志记录。 + * - Error:优先 stack,缺则 message + * - null/undefined:'Unknown error' + * - 其它:String(value) + */ +export function formatError(err: unknown): string { + if (err instanceof Error) return err.stack || err.message; + if (err === null || err === undefined) return "Unknown error"; + return String(err); +} diff --git a/src/server/logger.ts b/src/server/logger.ts new file mode 100644 index 0000000..74f1a1e --- /dev/null +++ b/src/server/logger.ts @@ -0,0 +1,20 @@ +import log4js from "log4js"; + +// fileSync = 同步写:进程 OOM/被杀前日志已落盘,满足崩溃诊断刚需。 +log4js.configure({ + appenders: { + app: { + type: "fileSync", + filename: "logs/app/app.log", + maxLogSize: 10 * 1024 * 1024, // 10MB + backups: 5, + layout: { type: "pattern", pattern: "[%d{ISO8601}] [%p] [%c] %m" }, + }, + }, + categories: { default: { appenders: ["app"], level: "info" } }, +}); + +export { formatError } from "./log-format"; + +export const apiLogger = log4js.getLogger("api"); +export const exportLogger = log4js.getLogger("export");