From 1e0bb1de24720da4817c1cfda10ed8bd36adabe0 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sat, 4 Apr 2026 16:33:03 +0800 Subject: [PATCH] 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 --- config.template.yaml | 10 ++ package-lock.json | 124 +++++++++++++++---- package.json | 1 + src/main/bootstrap/process-guards.ts | 9 +- src/main/services/config/config-manager.ts | 11 +- src/main/services/logger/audit-logger.ts | 4 + src/main/services/logger/index.ts | 59 +++++++++- src/main/services/logger/seq-transport.ts | 131 +++++++++++++++++++++ src/main/types/audit.types.ts | 2 + src/main/types/config.schema.ts | 16 +++ 10 files changed, 336 insertions(+), 31 deletions(-) create mode 100644 src/main/services/logger/seq-transport.ts diff --git a/config.template.yaml b/config.template.yaml index b17bc62..58af411 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -63,6 +63,16 @@ logging: auditRetention: 30 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: enabled: false # 设置为 true 启用 RustFS 上传 diff --git a/package-lock.json b/package-lock.json index bd67a5b..bea0592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "dependencies": { "@aws-sdk/client-s3": "^3.929.0", + "@datalust/winston-seq": "^3.0.1", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@headlessui/react": "^2.2.9", @@ -970,6 +971,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@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", "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -1222,6 +1225,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1528,6 +1532,19 @@ "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": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -1999,7 +2016,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -2021,7 +2037,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2038,7 +2053,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -2053,7 +2067,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -5002,6 +5015,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5023,6 +5037,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5143,6 +5158,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5582,6 +5598,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5614,6 +5631,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6376,6 +6394,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7171,8 +7190,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -7705,6 +7723,7 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -7919,6 +7938,7 @@ "integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", @@ -8107,7 +8127,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8128,7 +8147,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8150,17 +8168,6 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "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": { "version": "1.4.5", "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==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8527,6 +8535,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9955,7 +9964,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -12978,6 +12987,26 @@ "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": { "version": "11.5.0", "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", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13594,6 +13624,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13617,7 +13648,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -13635,7 +13665,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -13656,6 +13685,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13797,6 +13827,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13818,6 +13849,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13852,7 +13884,8 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -13886,6 +13919,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -14011,7 +14045,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -14540,6 +14575,19 @@ "license": "MIT", "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": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -15405,7 +15453,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -15571,6 +15618,12 @@ "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": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -16441,6 +16494,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16878,6 +16932,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -17424,6 +17479,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -17506,6 +17562,22 @@ "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": { "version": "5.0.0", "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", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -17869,6 +17942,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index cf3ac09..3c93924 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.929.0", + "@datalust/winston-seq": "^3.0.1", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@headlessui/react": "^2.2.9", diff --git a/src/main/bootstrap/process-guards.ts b/src/main/bootstrap/process-guards.ts index d97a5f0..2ece49f 100644 --- a/src/main/bootstrap/process-guards.ts +++ b/src/main/bootstrap/process-guards.ts @@ -1,6 +1,7 @@ import { app } from 'electron' import logger from '../services/logger/index' import { logAudit, closeAuditLogger } from '../services/logger/audit-logger' +import { serializeError } from '../services/logger/error-utils' export function setupProcessGuards(): void { process.on('uncaughtException', (err) => { @@ -16,13 +17,17 @@ export function setupProcessGuards(): void { }) 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', { username: 'system', computerName: process.env.COMPUTERNAME || 'unknown', resource: 'main-process', status: 'failure', - metadata: { reason: String(reason) } + metadata: errorMeta }) }) diff --git a/src/main/services/config/config-manager.ts b/src/main/services/config/config-manager.ts index a0cac1e..1b0f7ec 100644 --- a/src/main/services/config/config-manager.ts +++ b/src/main/services/config/config-manager.ts @@ -102,6 +102,15 @@ const DEFAULT_CONFIG: FullConfig = { auditRetention: 30, appRetention: 14 }, + seq: { + enabled: false, + serverUrl: '', + apiKey: '', + batchPostingLimit: 50, + period: 2000, + queueLimit: 10000, + maxRetries: 3 + }, rustfs: { enabled: false, endpoint: '', @@ -207,7 +216,7 @@ export class ConfigManager { this.config = validated // Apply logging configuration - applyLoggingConfig(validated.logging) + applyLoggingConfig(validated.logging, validated.seq) applyAuditConfig(validated.logging.auditRetention) log.info('Configuration loaded and validated successfully', { diff --git a/src/main/services/logger/audit-logger.ts b/src/main/services/logger/audit-logger.ts index 9500af1..4283658 100644 --- a/src/main/services/logger/audit-logger.ts +++ b/src/main/services/logger/audit-logger.ts @@ -6,6 +6,7 @@ import winston from 'winston' import DailyRotateFile from 'winston-daily-rotate-file' import path from 'path' +import { app } from 'electron' import { getLogDir } from './shared' /** @@ -23,6 +24,8 @@ export interface AuditEntry { username: string /** Computer name from which the action was performed */ computerName: string + /** Application version when the action was performed */ + appVersion: string /** The resource that was affected (e.g., table name, file path) */ resource: string /** Status of the action: 'success' | 'failure' | 'partial' */ @@ -104,6 +107,7 @@ export function logAudit( userId, username: details.username, computerName: details.computerName, + appVersion: app.getVersion(), resource: details.resource, status: details.status, metadata: details.metadata || {} diff --git a/src/main/services/logger/index.ts b/src/main/services/logger/index.ts index 4f001d7..b60394e 100644 --- a/src/main/services/logger/index.ts +++ b/src/main/services/logger/index.ts @@ -11,11 +11,14 @@ import winston from 'winston' import DailyRotateFile from 'winston-daily-rotate-file' import path from 'path' -import { BrowserWindow } from 'electron' +import os from 'os' +import { app, BrowserWindow } from 'electron' import { serializeError, sanitizeError } from './error-utils' import { getLogDir, isProduction, cleanupOldScreenshots } from './shared' import { IPC_CHANNELS } from '../../../shared/ipc-channels' 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 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() const logger = winston.createLogger({ level: 'info', // Default level, can be updated via setLogLevel() - defaultMeta: { service: 'erpauto' }, + defaultMeta: { + service: 'erpauto', + appVersion: app.getVersion(), + computerName: os.hostname() + }, transports: [ // Console transport - always enabled new winston.transports.Console({ @@ -181,10 +188,15 @@ export function setLogLevel(level: string): void { /** * Apply logging configuration from config file * Removes existing DailyRotateFile transports and recreates them with config values + * Also configures Seq transport if enabled * * @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 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 cleanupOldScreenshots(config.appRetention) } diff --git a/src/main/services/logger/seq-transport.ts b/src/main/services/logger/seq-transport.ts new file mode 100644 index 0000000..59bbaf5 --- /dev/null +++ b/src/main/services/logger/seq-transport.ts @@ -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 { + // 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 diff --git a/src/main/types/audit.types.ts b/src/main/types/audit.types.ts index 3fd5518..4d8f7c1 100644 --- a/src/main/types/audit.types.ts +++ b/src/main/types/audit.types.ts @@ -35,6 +35,8 @@ export interface AuditEntry { username: string /** Computer name where action was performed */ computerName: string + /** Application version when action was performed */ + appVersion: string /** Resource affected by the action */ resource?: string /** Status of the action */ diff --git a/src/main/types/config.schema.ts b/src/main/types/config.schema.ts index 744dcbb..81f9779 100644 --- a/src/main/types/config.schema.ts +++ b/src/main/types/config.schema.ts @@ -132,6 +132,21 @@ export const loggingConfigSchema = z.object({ 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 + /** * RustFS 对象存储配置 Schema */ @@ -172,6 +187,7 @@ export const fullConfigSchema = z.object({ cleaner: cleanerConfigSchema, orderResolution: orderResolutionSchema, logging: loggingConfigSchema, + seq: seqConfigSchema.optional(), rustfs: rustfsConfigSchema.optional(), update: updateConfigSchema.optional() })