feat(logging): add Seq transport, global meta fields, and improve error handling

- Add Seq centralized logging transport with async ESM import
- Add appVersion and computerName to logger defaultMeta (all app logs)
- Add appVersion to audit log entries for version-level traceability
- Improve unhandledRejection to capture full stack traces for Error instances
- Add Seq config schema and template configuration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-04-04 16:33:03 +08:00
parent fce8dbc37f
commit 1e0bb1de24
10 changed files with 336 additions and 31 deletions

View File

@@ -63,6 +63,16 @@ logging:
auditRetention: 30 auditRetention: 30
appRetention: 14 appRetention: 14
# Seq 日志聚合服务配置(可选,用于集中管理日志)
seq:
enabled: false # 设置为 true 启用 Seq 日志发送
serverUrl: 'http://localhost:5341' # Seq 服务器地址
apiKey: '' # 可选API key 用于认证
batchPostingLimit: 50 # 每批次最大日志条目数
period: 2000 # 发送间隔 (毫秒)
queueLimit: 10000 # 本地队列最大容量
maxRetries: 3 # 失败重试次数
# RustFS 对象存储配置(用于持久化报告) # RustFS 对象存储配置(用于持久化报告)
rustfs: rustfs:
enabled: false # 设置为 true 启用 RustFS 上传 enabled: false # 设置为 true 启用 RustFS 上传

124
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.929.0", "@aws-sdk/client-s3": "^3.929.0",
"@datalust/winston-seq": "^3.0.1",
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.9",
@@ -970,6 +971,7 @@
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
"integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@azure/abort-controller": "^2.1.2", "@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.10.0", "@azure/core-auth": "^1.10.0",
@@ -1031,6 +1033,7 @@
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz",
"integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@azure/abort-controller": "^2.1.2", "@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.10.0", "@azure/core-auth": "^1.10.0",
@@ -1222,6 +1225,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -1528,6 +1532,19 @@
"kuler": "^2.0.0" "kuler": "^2.0.0"
} }
}, },
"node_modules/@datalust/winston-seq": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@datalust/winston-seq/-/winston-seq-3.0.1.tgz",
"integrity": "sha512-jWJd5PKcj/nM5f1T65KJgKaxPJRADWe+GEWtj1yEji1H0ub4RWhBEDLYzIFdwUy365lxtc5njsakenp4Evmv+g==",
"license": "Apache-2.0",
"dependencies": {
"seq-logging": "^3.0.0",
"winston-transport": "^4.9.0"
},
"peerDependencies": {
"winston": "^3.17.0"
}
},
"node_modules/@develar/schema-utils": { "node_modules/@develar/schema-utils": {
"version": "2.6.5", "version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -1999,7 +2016,6 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -2021,7 +2037,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -2038,7 +2053,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -2053,7 +2067,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -5002,6 +5015,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
"integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -5023,6 +5037,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -5143,6 +5158,7 @@
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1", "@typescript-eslint/types": "8.56.1",
@@ -5582,6 +5598,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -5614,6 +5631,7 @@
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -6376,6 +6394,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -7171,8 +7190,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true
"peer": true
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
@@ -7705,6 +7723,7 @@
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.8.1", "app-builder-lib": "26.8.1",
"builder-util": "26.8.1", "builder-util": "26.8.1",
@@ -7919,6 +7938,7 @@
"integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==", "integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^22.7.7", "@types/node": "^22.7.7",
@@ -8107,7 +8127,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -8128,7 +8147,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -8150,17 +8168,6 @@
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -8467,6 +8474,7 @@
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -8527,6 +8535,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@@ -9955,7 +9964,7 @@
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -12978,6 +12987,26 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-gyp": { "node_modules/node-gyp": {
"version": "11.5.0", "version": "11.5.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz",
@@ -13514,6 +13543,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -13594,6 +13624,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -13617,7 +13648,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -13635,7 +13665,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -13656,6 +13685,7 @@
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -13797,6 +13827,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -13818,6 +13849,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -13852,7 +13884,8 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/react-markdown": { "node_modules/react-markdown": {
"version": "10.1.0", "version": "10.1.0",
@@ -13886,6 +13919,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@@ -14011,7 +14045,8 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@@ -14540,6 +14575,19 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/seq-logging": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/seq-logging/-/seq-logging-3.0.0.tgz",
"integrity": "sha512-ys5QV0745vxBCWuZBPSkgoobuLoUMxTSz1g7ZclHqX1tXXKFLyRIIn8V89EPgDnfRiWfoSo4KSxy/E0MtOYYyw==",
"license": "Apache-2.0",
"dependencies": {
"abort-controller": "^3.0.0",
"node-fetch": "^2.7.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/serialize-error": { "node_modules/serialize-error": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
@@ -15405,7 +15453,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -15571,6 +15618,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/traverse": { "node_modules/traverse": {
"version": "0.3.9", "version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
@@ -16441,6 +16494,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -16878,6 +16932,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -17424,6 +17479,7 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vitest/expect": "4.0.18", "@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18", "@vitest/mocker": "4.0.18",
@@ -17506,6 +17562,22 @@
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
@@ -17632,6 +17704,7 @@
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "^1.6.0", "@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8", "@dabh/diagnostics": "^2.0.8",
@@ -17869,6 +17942,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@@ -34,6 +34,7 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.929.0", "@aws-sdk/client-s3": "^3.929.0",
"@datalust/winston-seq": "^3.0.1",
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.9",

View File

@@ -1,6 +1,7 @@
import { app } from 'electron' import { app } from 'electron'
import logger from '../services/logger/index' import logger from '../services/logger/index'
import { logAudit, closeAuditLogger } from '../services/logger/audit-logger' import { logAudit, closeAuditLogger } from '../services/logger/audit-logger'
import { serializeError } from '../services/logger/error-utils'
export function setupProcessGuards(): void { export function setupProcessGuards(): void {
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
@@ -16,13 +17,17 @@ export function setupProcessGuards(): void {
}) })
process.on('unhandledRejection', (reason) => { process.on('unhandledRejection', (reason) => {
logger.error('Unhandled Rejection', { reason: String(reason) }) const errorMeta =
reason instanceof Error
? { error: serializeError(reason) }
: { reason: String(reason) }
logger.error('Unhandled Rejection', errorMeta)
logAudit('SYSTEM_ERROR', 'system', { logAudit('SYSTEM_ERROR', 'system', {
username: 'system', username: 'system',
computerName: process.env.COMPUTERNAME || 'unknown', computerName: process.env.COMPUTERNAME || 'unknown',
resource: 'main-process', resource: 'main-process',
status: 'failure', status: 'failure',
metadata: { reason: String(reason) } metadata: errorMeta
}) })
}) })

View File

@@ -102,6 +102,15 @@ const DEFAULT_CONFIG: FullConfig = {
auditRetention: 30, auditRetention: 30,
appRetention: 14 appRetention: 14
}, },
seq: {
enabled: false,
serverUrl: '',
apiKey: '',
batchPostingLimit: 50,
period: 2000,
queueLimit: 10000,
maxRetries: 3
},
rustfs: { rustfs: {
enabled: false, enabled: false,
endpoint: '', endpoint: '',
@@ -207,7 +216,7 @@ export class ConfigManager {
this.config = validated this.config = validated
// Apply logging configuration // Apply logging configuration
applyLoggingConfig(validated.logging) applyLoggingConfig(validated.logging, validated.seq)
applyAuditConfig(validated.logging.auditRetention) applyAuditConfig(validated.logging.auditRetention)
log.info('Configuration loaded and validated successfully', { log.info('Configuration loaded and validated successfully', {

View File

@@ -6,6 +6,7 @@
import winston from 'winston' import winston from 'winston'
import DailyRotateFile from 'winston-daily-rotate-file' import DailyRotateFile from 'winston-daily-rotate-file'
import path from 'path' import path from 'path'
import { app } from 'electron'
import { getLogDir } from './shared' import { getLogDir } from './shared'
/** /**
@@ -23,6 +24,8 @@ export interface AuditEntry {
username: string username: string
/** Computer name from which the action was performed */ /** Computer name from which the action was performed */
computerName: string computerName: string
/** Application version when the action was performed */
appVersion: string
/** The resource that was affected (e.g., table name, file path) */ /** The resource that was affected (e.g., table name, file path) */
resource: string resource: string
/** Status of the action: 'success' | 'failure' | 'partial' */ /** Status of the action: 'success' | 'failure' | 'partial' */
@@ -104,6 +107,7 @@ export function logAudit(
userId, userId,
username: details.username, username: details.username,
computerName: details.computerName, computerName: details.computerName,
appVersion: app.getVersion(),
resource: details.resource, resource: details.resource,
status: details.status, status: details.status,
metadata: details.metadata || {} metadata: details.metadata || {}

View File

@@ -11,11 +11,14 @@
import winston from 'winston' import winston from 'winston'
import DailyRotateFile from 'winston-daily-rotate-file' import DailyRotateFile from 'winston-daily-rotate-file'
import path from 'path' import path from 'path'
import { BrowserWindow } from 'electron' import os from 'os'
import { app, BrowserWindow } from 'electron'
import { serializeError, sanitizeError } from './error-utils' import { serializeError, sanitizeError } from './error-utils'
import { getLogDir, isProduction, cleanupOldScreenshots } from './shared' import { getLogDir, isProduction, cleanupOldScreenshots } from './shared'
import { IPC_CHANNELS } from '../../../shared/ipc-channels' import { IPC_CHANNELS } from '../../../shared/ipc-channels'
import { getContext, run } from './request-context' import { getContext, run } from './request-context'
import { createSeqTransportSync } from './seq-transport'
import type { SeqConfig } from '../../types/config.schema'
// Cache isProduction() at module load — app.isPackaged never changes at runtime // Cache isProduction() at module load — app.isPackaged never changes at runtime
const IS_PROD = isProduction() const IS_PROD = isProduction()
@@ -154,7 +157,11 @@ const createFileTransport = (level?: string, maxFiles?: string): DailyRotateFile
// File transports are added after config is loaded via applyLoggingConfig() // File transports are added after config is loaded via applyLoggingConfig()
const logger = winston.createLogger({ const logger = winston.createLogger({
level: 'info', // Default level, can be updated via setLogLevel() level: 'info', // Default level, can be updated via setLogLevel()
defaultMeta: { service: 'erpauto' }, defaultMeta: {
service: 'erpauto',
appVersion: app.getVersion(),
computerName: os.hostname()
},
transports: [ transports: [
// Console transport - always enabled // Console transport - always enabled
new winston.transports.Console({ new winston.transports.Console({
@@ -181,10 +188,15 @@ export function setLogLevel(level: string): void {
/** /**
* Apply logging configuration from config file * Apply logging configuration from config file
* Removes existing DailyRotateFile transports and recreates them with config values * Removes existing DailyRotateFile transports and recreates them with config values
* Also configures Seq transport if enabled
* *
* @param config - Logging configuration from config.yaml * @param config - Logging configuration from config.yaml
* @param seqConfig - Optional Seq configuration from config.yaml
*/ */
export function applyLoggingConfig(config: { level: string; appRetention: number }): void { export function applyLoggingConfig(
config: { level: string; appRetention: number },
seqConfig?: SeqConfig
): void {
// Update log level // Update log level
setLogLevel(config.level) setLogLevel(config.level)
@@ -213,6 +225,47 @@ export function applyLoggingConfig(config: { level: string; appRetention: number
) )
} }
// Add Seq transport if configured and enabled
// Note: Uses sync wrapper which initializes asynchronously
if (seqConfig && seqConfig.enabled && seqConfig.serverUrl) {
try {
const seqTransport = createSeqTransportSync(seqConfig)
if (seqTransport) {
logger.add(seqTransport)
logger.info('Seq transport added successfully', {
serverUrl: seqConfig.serverUrl,
batchPostingLimit: seqConfig.batchPostingLimit,
period: seqConfig.period,
context: 'seq-transport'
})
} else {
// Transport not ready yet, will be initialized on next call
logger.debug('Seq transport initialization in progress', {
serverUrl: seqConfig.serverUrl,
context: 'seq-transport'
})
// Initialize and add when ready
import('./seq-transport').then(({ createSeqTransport }) => {
createSeqTransport(seqConfig).then((transport) => {
if (transport) {
logger.add(transport)
logger.info('Seq transport added (async init complete)', {
serverUrl: seqConfig.serverUrl,
context: 'seq-transport'
})
}
})
})
}
} catch (error) {
logger.error('Failed to add Seq transport', {
error: error instanceof Error ? error.message : String(error),
context: 'seq-transport'
})
// Don't throw - allow app to continue without Seq
}
}
// Clean up old screenshot files beyond the retention window // Clean up old screenshot files beyond the retention window
cleanupOldScreenshots(config.appRetention) cleanupOldScreenshots(config.appRetention)
} }

View File

@@ -0,0 +1,131 @@
/**
* Seq logging transport using @datalust/winston-seq
*
* Provides asynchronous batched logging to Seq server
* with configurable parameters for production use.
*
* Features:
* - Non-blocking async batch sending (via dynamic import)
* - Configurable batch size, period, queue limits
* - Automatic retry on failure
* - Graceful shutdown with flush
* - Error suppression (Seq failures don't crash app)
*
* Note: Uses dynamic import because @datalust/winston-seq is an ESM module
*/
import type { SeqConfig } from '../../types/config.schema'
import logger from './index'
/**
* Seq transport constructor type (from dynamic import)
*/
type SeqTransportClass = new (options: {
serverUrl: string
apiKey?: string
batchSizeLimit?: number
maxBatchingTime?: number
maxRetries?: number
onError?: (error: Error) => void
}) => any
/**
* Create and configure Seq transport dynamically
*
* This function handles the ESM module loading and returns
* a configured transport instance compatible with Winston.
*
* @param config - Seq configuration from config.schema
* @returns Promise resolving to transport instance or null if disabled/failed
*/
export async function createSeqTransport(config: SeqConfig): Promise<any> {
// Don't create transport if disabled
if (!config.enabled || !config.serverUrl) {
logger.info('Seq logging is disabled or server URL not provided', {
enabled: config.enabled,
hasServerUrl: !!config.serverUrl,
context: 'seq-transport'
})
return null
}
try {
// Dynamically import the ESM module
const { SeqTransport } = await import('@datalust/winston-seq')
const SeqTransportClass = SeqTransport as SeqTransportClass
// Create transport instance with mapped config
const transport = new SeqTransportClass({
serverUrl: config.serverUrl,
apiKey: config.apiKey || undefined,
batchSizeLimit: config.batchPostingLimit,
maxBatchingTime: config.period,
maxRetries: config.maxRetries,
onError: (error: Error) => {
// Log errors but don't throw - Seq failures should not crash app
logger.error('Seq transport error', {
error: error.message,
stack: error.stack,
context: 'seq-transport'
})
}
})
logger.info('Seq transport created successfully', {
serverUrl: config.serverUrl,
batchPostingLimit: config.batchPostingLimit,
period: config.period,
maxRetries: config.maxRetries,
context: 'seq-transport'
})
return transport
} catch (error) {
logger.error('Failed to create Seq transport', {
error: error instanceof Error ? error.message : String(error),
context: 'seq-transport'
})
// Return null to allow app to continue without Seq
return null
}
}
/**
* Sync factory wrapper for backward compatibility
*
* Note: This creates the transport asynchronously internally.
* The transport is cached and reused.
*/
let cachedTransport: any = null
let isInitializing = false
export function createSeqTransportSync(config: SeqConfig): any {
if (!config.enabled || !config.serverUrl) {
return null
}
if (cachedTransport) {
return cachedTransport
}
if (!isInitializing) {
isInitializing = true
createSeqTransport(config)
.then((transport) => {
cachedTransport = transport
})
.catch((error) => {
logger.error('Seq transport initialization failed', {
error: error instanceof Error ? error.message : String(error)
})
})
.finally(() => {
isInitializing = false
})
}
// Return null on first call, transport will be available on next call
return null
}
export default createSeqTransportSync

View File

@@ -35,6 +35,8 @@ export interface AuditEntry {
username: string username: string
/** Computer name where action was performed */ /** Computer name where action was performed */
computerName: string computerName: string
/** Application version when action was performed */
appVersion: string
/** Resource affected by the action */ /** Resource affected by the action */
resource?: string resource?: string
/** Status of the action */ /** Status of the action */

View File

@@ -132,6 +132,21 @@ export const loggingConfigSchema = z.object({
appRetention: z.number().int().min(1).max(365).default(14) appRetention: z.number().int().min(1).max(365).default(14)
}) })
/**
* Seq 日志聚合服务配置 Schema
*/
export const seqConfigSchema = z.object({
enabled: z.boolean().default(false),
serverUrl: z.string().url('Seq server URL must be a valid URL').optional().default(''),
apiKey: z.string().optional().default(''),
batchPostingLimit: z.number().int().min(1).max(1000).default(50),
period: z.number().int().min(1000).max(30000).default(2000), // milliseconds
queueLimit: z.number().int().min(1).max(10000).default(10000),
maxRetries: z.number().int().min(0).max(10).default(3)
})
export type SeqConfig = z.infer<typeof seqConfigSchema>
/** /**
* RustFS 对象存储配置 Schema * RustFS 对象存储配置 Schema
*/ */
@@ -172,6 +187,7 @@ export const fullConfigSchema = z.object({
cleaner: cleanerConfigSchema, cleaner: cleanerConfigSchema,
orderResolution: orderResolutionSchema, orderResolution: orderResolutionSchema,
logging: loggingConfigSchema, logging: loggingConfigSchema,
seq: seqConfigSchema.optional(),
rustfs: rustfsConfigSchema.optional(), rustfs: rustfsConfigSchema.optional(),
update: updateConfigSchema.optional() update: updateConfigSchema.optional()
}) })