befly 3.12.3 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/befly.config.js +1 -1
- package/dist/befly.js +810 -896
- package/dist/befly.min.js +15 -18
- package/dist/checks/checkApi.js +14 -17
- package/dist/checks/checkHook.js +55 -24
- package/dist/checks/checkMenu.js +10 -10
- package/dist/checks/checkPlugin.js +55 -24
- package/dist/checks/checkTable.js +29 -28
- package/dist/hooks/auth.d.ts +3 -7
- package/dist/hooks/auth.js +2 -1
- package/dist/hooks/cors.d.ts +3 -7
- package/dist/hooks/cors.js +3 -2
- package/dist/hooks/parser.d.ts +3 -7
- package/dist/hooks/parser.js +2 -1
- package/dist/hooks/permission.d.ts +3 -7
- package/dist/hooks/permission.js +5 -3
- package/dist/hooks/validator.d.ts +3 -7
- package/dist/hooks/validator.js +2 -1
- package/dist/index.js +2 -2
- package/dist/lib/cacheHelper.js +8 -8
- package/dist/lib/connect.js +5 -5
- package/dist/lib/dbHelper.js +6 -5
- package/dist/lib/logger.d.ts +16 -17
- package/dist/lib/logger.js +335 -749
- package/dist/lib/redisHelper.js +27 -26
- package/dist/loader/loadApis.js +1 -1
- package/dist/loader/loadPlugins.js +1 -1
- package/dist/plugins/cache.d.ts +3 -9
- package/dist/plugins/cache.js +2 -1
- package/dist/plugins/cipher.d.ts +3 -8
- package/dist/plugins/cipher.js +2 -1
- package/dist/plugins/config.d.ts +3 -12
- package/dist/plugins/config.js +2 -1
- package/dist/plugins/db.d.ts +3 -9
- package/dist/plugins/db.js +3 -2
- package/dist/plugins/jwt.d.ts +3 -9
- package/dist/plugins/jwt.js +2 -1
- package/dist/plugins/logger.d.ts +3 -25
- package/dist/plugins/logger.js +2 -1
- package/dist/plugins/redis.d.ts +3 -9
- package/dist/plugins/redis.js +3 -2
- package/dist/plugins/tool.d.ts +3 -11
- package/dist/plugins/tool.js +2 -1
- package/dist/router/api.js +3 -2
- package/dist/router/static.js +1 -1
- package/dist/sync/syncApi.js +3 -3
- package/dist/sync/syncMenu.js +3 -2
- package/dist/sync/syncTable.js +2 -2
- package/dist/types/hook.d.ts +13 -0
- package/dist/types/hook.js +13 -0
- package/dist/types/logger.d.ts +20 -6
- package/dist/types/plugin.d.ts +12 -1
- package/dist/types/plugin.js +12 -1
- package/dist/utils/formatYmdHms.d.ts +1 -0
- package/dist/utils/formatYmdHms.js +20 -0
- package/dist/utils/importDefault.js +1 -1
- package/dist/utils/loadMenuConfigs.js +7 -6
- package/dist/utils/loggerUtils.d.ts +18 -0
- package/dist/utils/loggerUtils.js +167 -0
- package/dist/utils/response.js +6 -4
- package/dist/utils/scanCoreBuiltins.js +4 -1
- package/dist/utils/scanFiles.d.ts +2 -0
- package/dist/utils/scanFiles.js +5 -2
- package/dist/utils/sortModules.js +8 -7
- package/dist/utils/util.d.ts +2 -0
- package/dist/utils/util.js +16 -0
- package/package.json +2 -2
package/dist/lib/logger.js
CHANGED
|
@@ -2,35 +2,25 @@
|
|
|
2
2
|
* 日志系统 - Bun 环境自定义实现(替换 pino / pino-roll)
|
|
3
3
|
*/
|
|
4
4
|
import { createWriteStream, existsSync, mkdirSync } from "node:fs";
|
|
5
|
-
import {
|
|
5
|
+
import { stat } from "node:fs/promises";
|
|
6
6
|
import { hostname as osHostname } from "node:os";
|
|
7
7
|
import { isAbsolute as nodePathIsAbsolute, join as nodePathJoin, resolve as nodePathResolve } from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import { formatYmdHms } from "../utils/formatYmdHms";
|
|
9
|
+
import { buildSensitiveKeyMatcher, sanitizeLogObject } from "../utils/loggerUtils";
|
|
10
|
+
import { isPlainObject, normalizePositiveInt } from "../utils/util";
|
|
9
11
|
import { getCtx } from "./asyncContext";
|
|
10
|
-
const REGEXP_SPECIAL = /[\\^$.*+?()[\]{}|]/g;
|
|
11
|
-
export function escapeRegExp(input) {
|
|
12
|
-
return String(input).replace(REGEXP_SPECIAL, "\\$&");
|
|
13
|
-
}
|
|
14
12
|
// 注意:Logger 可能在运行时/测试中被 process.chdir() 影响。
|
|
15
13
|
// 为避免相对路径的 logs 目录随着 cwd 变化,使用模块加载时的初始 cwd 作为锚点。
|
|
16
14
|
const INITIAL_CWD = process.cwd();
|
|
17
|
-
const DEFAULT_LOG_STRING_LEN = 100;
|
|
18
|
-
const DEFAULT_LOG_ARRAY_ITEMS = 100;
|
|
19
|
-
let maxLogStringLen = DEFAULT_LOG_STRING_LEN;
|
|
20
|
-
let maxLogArrayItems = DEFAULT_LOG_ARRAY_ITEMS;
|
|
21
|
-
// 为避免递归导致栈溢出/性能抖动:使用非递归遍历,并对深度/节点数做硬限制。
|
|
22
|
-
// 说明:这不是业务数据结构的“真实深度”,而是日志清洗的最大深入层级(越大越重)。
|
|
23
|
-
const DEFAULT_LOG_SANITIZE_DEPTH = 3;
|
|
24
|
-
const DEFAULT_LOG_OBJECT_KEYS = 100;
|
|
25
|
-
const DEFAULT_LOG_SANITIZE_NODES = 500;
|
|
26
|
-
let sanitizeDepthLimit = DEFAULT_LOG_SANITIZE_DEPTH;
|
|
27
|
-
let sanitizeObjectKeysLimit = DEFAULT_LOG_OBJECT_KEYS;
|
|
28
|
-
let sanitizeNodesLimit = DEFAULT_LOG_SANITIZE_NODES;
|
|
29
|
-
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
|
30
15
|
const BUILTIN_SENSITIVE_KEYS = ["*password*", "pass", "pwd", "*token*", "access_token", "refresh_token", "accessToken", "refreshToken", "authorization", "cookie", "set-cookie", "*secret*", "apiKey", "api_key", "privateKey", "private_key"];
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
16
|
+
let sanitizeOptions = {
|
|
17
|
+
maxStringLen: 100,
|
|
18
|
+
maxArrayItems: 100,
|
|
19
|
+
sanitizeDepth: 3,
|
|
20
|
+
sanitizeNodes: 500,
|
|
21
|
+
sanitizeObjectKeys: 100,
|
|
22
|
+
sensitiveKeyMatcher: buildSensitiveKeyMatcher({ builtinPatterns: BUILTIN_SENSITIVE_KEYS, userPatterns: [] })
|
|
23
|
+
};
|
|
34
24
|
const HOSTNAME = (() => {
|
|
35
25
|
try {
|
|
36
26
|
return osHostname();
|
|
@@ -39,49 +29,19 @@ const HOSTNAME = (() => {
|
|
|
39
29
|
return "unknown";
|
|
40
30
|
}
|
|
41
31
|
})();
|
|
42
|
-
const LOG_LEVEL_NUM = {
|
|
43
|
-
debug: 20,
|
|
44
|
-
info: 30,
|
|
45
|
-
warn: 40,
|
|
46
|
-
error: 50
|
|
47
|
-
};
|
|
48
|
-
const DEFAULT_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
49
|
-
const DEFAULT_FLUSH_DELAY_MS = 10;
|
|
50
|
-
const DEFAULT_MAX_BATCH_BYTES = 64 * 1024;
|
|
51
32
|
let instance = null;
|
|
52
33
|
let errorInstance = null;
|
|
53
34
|
let mockInstance = null;
|
|
54
35
|
let appFileSink = null;
|
|
55
36
|
let errorFileSink = null;
|
|
56
37
|
let appConsoleSink = null;
|
|
57
|
-
let didWarnIoError = false;
|
|
58
|
-
let didPruneOldLogFiles = false;
|
|
59
|
-
let didEnsureLogDir = false;
|
|
60
38
|
let config = {
|
|
61
39
|
debug: 0,
|
|
62
40
|
dir: "./logs",
|
|
63
41
|
console: 1,
|
|
64
|
-
maxSize:
|
|
42
|
+
maxSize: 20
|
|
65
43
|
};
|
|
66
|
-
function
|
|
67
|
-
const y = date.getFullYear();
|
|
68
|
-
const m = date.getMonth() + 1;
|
|
69
|
-
const d = date.getDate();
|
|
70
|
-
const mm = m < 10 ? `0${m}` : String(m);
|
|
71
|
-
const dd = d < 10 ? `0${d}` : String(d);
|
|
72
|
-
return `${y}-${mm}-${dd}`;
|
|
73
|
-
}
|
|
74
|
-
function normalizeLogLevelName() {
|
|
75
|
-
// 与旧行为保持一致:debug=1 -> debug,否则 -> info
|
|
76
|
-
return config.debug === 1 ? "debug" : "info";
|
|
77
|
-
}
|
|
78
|
-
function shouldAccept(minLevel, level) {
|
|
79
|
-
return LOG_LEVEL_NUM[level] >= LOG_LEVEL_NUM[minLevel];
|
|
80
|
-
}
|
|
81
|
-
function safeWriteStderrOnce(msg) {
|
|
82
|
-
if (didWarnIoError)
|
|
83
|
-
return;
|
|
84
|
-
didWarnIoError = true;
|
|
44
|
+
function safeWriteStderr(msg) {
|
|
85
45
|
try {
|
|
86
46
|
process.stderr.write(`${msg}\n`);
|
|
87
47
|
}
|
|
@@ -99,86 +59,88 @@ function shiftBatchFromPending(pending, maxBatchBytes) {
|
|
|
99
59
|
break;
|
|
100
60
|
}
|
|
101
61
|
parts.push(next);
|
|
102
|
-
bytes
|
|
62
|
+
bytes = bytes + nextBytes;
|
|
103
63
|
pending.shift();
|
|
104
64
|
}
|
|
105
65
|
return { chunk: parts.join(""), bytes: bytes };
|
|
106
66
|
}
|
|
107
|
-
class
|
|
108
|
-
kind;
|
|
109
|
-
minLevel;
|
|
67
|
+
class BufferedSink {
|
|
110
68
|
pending;
|
|
111
69
|
pendingBytes;
|
|
112
|
-
|
|
70
|
+
scheduledTimer;
|
|
113
71
|
flushing;
|
|
114
72
|
maxBufferBytes;
|
|
115
73
|
flushDelayMs;
|
|
116
74
|
maxBatchBytes;
|
|
75
|
+
writeChunk;
|
|
76
|
+
onShutdown;
|
|
117
77
|
constructor(options) {
|
|
118
|
-
this.kind = options.kind;
|
|
119
|
-
this.minLevel = options.minLevel;
|
|
120
78
|
this.pending = [];
|
|
121
79
|
this.pendingBytes = 0;
|
|
122
|
-
this.
|
|
80
|
+
this.scheduledTimer = null;
|
|
123
81
|
this.flushing = false;
|
|
124
|
-
this.maxBufferBytes =
|
|
125
|
-
this.flushDelayMs =
|
|
126
|
-
this.maxBatchBytes =
|
|
82
|
+
this.maxBufferBytes = options.maxBufferBytes;
|
|
83
|
+
this.flushDelayMs = options.flushDelayMs;
|
|
84
|
+
this.maxBatchBytes = options.maxBatchBytes;
|
|
85
|
+
this.writeChunk = options.writeChunk;
|
|
86
|
+
this.onShutdown = options.onShutdown ? options.onShutdown : null;
|
|
87
|
+
}
|
|
88
|
+
scheduleFlush() {
|
|
89
|
+
if (this.scheduledTimer)
|
|
90
|
+
return;
|
|
91
|
+
this.scheduledTimer = setTimeout(() => {
|
|
92
|
+
// timer 触发时先清空句柄,避免 flush 内再次 schedule 时被认为“已安排”。
|
|
93
|
+
this.scheduledTimer = null;
|
|
94
|
+
void this.flush();
|
|
95
|
+
}, this.flushDelayMs);
|
|
127
96
|
}
|
|
128
|
-
|
|
129
|
-
if (!
|
|
97
|
+
cancelScheduledFlush() {
|
|
98
|
+
if (!this.scheduledTimer)
|
|
130
99
|
return;
|
|
100
|
+
clearTimeout(this.scheduledTimer);
|
|
101
|
+
this.scheduledTimer = null;
|
|
102
|
+
}
|
|
103
|
+
enqueue(line) {
|
|
131
104
|
const bytes = Buffer.byteLength(line);
|
|
132
105
|
if (this.pendingBytes + bytes > this.maxBufferBytes) {
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
106
|
+
// buffer 满:统一丢弃新日志(不区分 level)
|
|
107
|
+
return;
|
|
137
108
|
}
|
|
138
109
|
this.pending.push(line);
|
|
139
|
-
this.pendingBytes
|
|
110
|
+
this.pendingBytes = this.pendingBytes + bytes;
|
|
140
111
|
this.scheduleFlush();
|
|
141
112
|
}
|
|
142
113
|
async flushNow() {
|
|
114
|
+
// 若已安排了定时 flush,flushNow 会覆盖它,避免出现多个 timer 并存。
|
|
115
|
+
this.cancelScheduledFlush();
|
|
143
116
|
await this.flush();
|
|
144
117
|
}
|
|
145
118
|
async shutdown() {
|
|
119
|
+
this.cancelScheduledFlush();
|
|
146
120
|
await this.flush();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return;
|
|
151
|
-
this.scheduled = true;
|
|
152
|
-
setTimeout(() => {
|
|
153
|
-
void this.flush();
|
|
154
|
-
}, this.flushDelayMs);
|
|
155
|
-
}
|
|
156
|
-
getStream() {
|
|
157
|
-
return this.kind === "stderr" ? process.stderr : process.stdout;
|
|
121
|
+
if (this.onShutdown) {
|
|
122
|
+
await this.onShutdown();
|
|
123
|
+
}
|
|
158
124
|
}
|
|
159
125
|
async flush() {
|
|
160
126
|
if (this.flushing)
|
|
161
127
|
return;
|
|
162
|
-
this.scheduled = false;
|
|
163
128
|
this.flushing = true;
|
|
164
129
|
try {
|
|
165
|
-
const stream = this.getStream();
|
|
166
130
|
while (this.pending.length > 0) {
|
|
167
131
|
const batch = shiftBatchFromPending(this.pending, this.maxBatchBytes);
|
|
168
132
|
const chunk = batch.chunk;
|
|
169
133
|
const chunkBytes = Buffer.byteLength(chunk);
|
|
170
134
|
this.pendingBytes = this.pendingBytes - chunkBytes;
|
|
171
|
-
const ok =
|
|
135
|
+
const ok = await this.writeChunk(chunk, chunkBytes);
|
|
172
136
|
if (!ok) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
137
|
+
// writer 已禁用/失败:清空剩余 pending
|
|
138
|
+
this.pending = [];
|
|
139
|
+
this.pendingBytes = 0;
|
|
140
|
+
break;
|
|
176
141
|
}
|
|
177
142
|
}
|
|
178
143
|
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
safeWriteStderrOnce(`[Logger] stream sink error: ${error?.message || error}`);
|
|
181
|
-
}
|
|
182
144
|
finally {
|
|
183
145
|
this.flushing = false;
|
|
184
146
|
if (this.pending.length > 0) {
|
|
@@ -187,17 +149,47 @@ class LogStreamSink {
|
|
|
187
149
|
}
|
|
188
150
|
}
|
|
189
151
|
}
|
|
152
|
+
function createStreamSink(kind) {
|
|
153
|
+
const getStream = () => {
|
|
154
|
+
return kind === "stderr" ? process.stderr : process.stdout;
|
|
155
|
+
};
|
|
156
|
+
const buffer = new BufferedSink({
|
|
157
|
+
maxBufferBytes: 10 * 1024 * 1024,
|
|
158
|
+
flushDelayMs: 10,
|
|
159
|
+
maxBatchBytes: 64 * 1024,
|
|
160
|
+
writeChunk: async (chunk) => {
|
|
161
|
+
try {
|
|
162
|
+
const stream = getStream();
|
|
163
|
+
const ok = stream.write(chunk);
|
|
164
|
+
if (!ok) {
|
|
165
|
+
await new Promise((resolve) => {
|
|
166
|
+
stream.once("drain", () => resolve());
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
safeWriteStderr(`[Logger] stream sink error: ${error?.message || error}`);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
enqueue(line) {
|
|
179
|
+
buffer.enqueue(line);
|
|
180
|
+
},
|
|
181
|
+
async flushNow() {
|
|
182
|
+
await buffer.flushNow();
|
|
183
|
+
},
|
|
184
|
+
async shutdown() {
|
|
185
|
+
await buffer.shutdown();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
190
189
|
class LogFileSink {
|
|
191
190
|
prefix;
|
|
192
|
-
minLevel;
|
|
193
191
|
maxFileBytes;
|
|
194
|
-
|
|
195
|
-
pendingBytes;
|
|
196
|
-
scheduled;
|
|
197
|
-
flushing;
|
|
198
|
-
maxBufferBytes;
|
|
199
|
-
flushDelayMs;
|
|
200
|
-
maxBatchBytes;
|
|
192
|
+
buffer;
|
|
201
193
|
stream;
|
|
202
194
|
streamDate;
|
|
203
195
|
streamIndex;
|
|
@@ -205,51 +197,56 @@ class LogFileSink {
|
|
|
205
197
|
disabled;
|
|
206
198
|
constructor(options) {
|
|
207
199
|
this.prefix = options.prefix;
|
|
208
|
-
this.minLevel = options.minLevel;
|
|
209
200
|
this.maxFileBytes = options.maxFileBytes;
|
|
210
|
-
this.pending = [];
|
|
211
|
-
this.pendingBytes = 0;
|
|
212
|
-
this.scheduled = false;
|
|
213
|
-
this.flushing = false;
|
|
214
|
-
this.maxBufferBytes = DEFAULT_MAX_BUFFER_BYTES;
|
|
215
|
-
this.flushDelayMs = DEFAULT_FLUSH_DELAY_MS;
|
|
216
|
-
this.maxBatchBytes = DEFAULT_MAX_BATCH_BYTES;
|
|
217
201
|
this.stream = null;
|
|
218
202
|
this.streamDate = "";
|
|
219
203
|
this.streamIndex = 0;
|
|
220
204
|
this.streamSizeBytes = 0;
|
|
221
205
|
this.disabled = false;
|
|
206
|
+
this.buffer = new BufferedSink({
|
|
207
|
+
maxBufferBytes: 10 * 1024 * 1024,
|
|
208
|
+
flushDelayMs: 10,
|
|
209
|
+
maxBatchBytes: 64 * 1024,
|
|
210
|
+
writeChunk: async (chunk, chunkBytes) => {
|
|
211
|
+
if (this.disabled)
|
|
212
|
+
return false;
|
|
213
|
+
try {
|
|
214
|
+
await this.ensureStreamReady(chunkBytes);
|
|
215
|
+
if (!this.stream) {
|
|
216
|
+
// 文件 sink 已被禁用或打开失败
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const ok = this.stream.write(chunk);
|
|
220
|
+
if (!ok) {
|
|
221
|
+
await new Promise((resolve) => {
|
|
222
|
+
this.stream.once("drain", () => resolve());
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
this.streamSizeBytes = this.streamSizeBytes + chunkBytes;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
safeWriteStderr(`[Logger] file sink flush error (${this.prefix}): ${error?.message || error}`);
|
|
230
|
+
this.disabled = true;
|
|
231
|
+
await this.closeStream();
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
onShutdown: async () => {
|
|
236
|
+
await this.closeStream();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
222
239
|
}
|
|
223
|
-
enqueue(
|
|
240
|
+
enqueue(line) {
|
|
224
241
|
if (this.disabled)
|
|
225
242
|
return;
|
|
226
|
-
|
|
227
|
-
return;
|
|
228
|
-
const bytes = Buffer.byteLength(line);
|
|
229
|
-
if (this.pendingBytes + bytes > this.maxBufferBytes) {
|
|
230
|
-
// 文件 sink:优先丢 debug/info,保留 warn/error
|
|
231
|
-
if (LOG_LEVEL_NUM[level] < LOG_LEVEL_NUM.warn) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
this.pending.push(line);
|
|
236
|
-
this.pendingBytes += bytes;
|
|
237
|
-
this.scheduleFlush();
|
|
243
|
+
this.buffer.enqueue(line);
|
|
238
244
|
}
|
|
239
245
|
async flushNow() {
|
|
240
|
-
await this.
|
|
246
|
+
await this.buffer.flushNow();
|
|
241
247
|
}
|
|
242
248
|
async shutdown() {
|
|
243
|
-
await this.
|
|
244
|
-
await this.closeStream();
|
|
245
|
-
}
|
|
246
|
-
scheduleFlush() {
|
|
247
|
-
if (this.scheduled)
|
|
248
|
-
return;
|
|
249
|
-
this.scheduled = true;
|
|
250
|
-
setTimeout(() => {
|
|
251
|
-
void this.flush();
|
|
252
|
-
}, this.flushDelayMs);
|
|
249
|
+
await this.buffer.shutdown();
|
|
253
250
|
}
|
|
254
251
|
async closeStream() {
|
|
255
252
|
if (!this.stream)
|
|
@@ -265,13 +262,28 @@ class LogFileSink {
|
|
|
265
262
|
}
|
|
266
263
|
});
|
|
267
264
|
}
|
|
265
|
+
openStream(filePath) {
|
|
266
|
+
try {
|
|
267
|
+
this.stream = createWriteStream(filePath, { flags: "a" });
|
|
268
|
+
this.stream.on("error", (error) => {
|
|
269
|
+
safeWriteStderr(`[Logger] file sink error (${this.prefix}): ${error?.message || error}`);
|
|
270
|
+
this.disabled = true;
|
|
271
|
+
void this.closeStream();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
safeWriteStderr(`[Logger] createWriteStream failed (${this.prefix}): ${error?.message || error}`);
|
|
276
|
+
this.disabled = true;
|
|
277
|
+
this.stream = null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
268
280
|
getFilePath(date, index) {
|
|
269
281
|
const suffix = index > 0 ? `.${index}` : "";
|
|
270
282
|
const filename = `${this.prefix}.${date}${suffix}.log`;
|
|
271
283
|
return nodePathJoin(resolveLogDir(), filename);
|
|
272
284
|
}
|
|
273
285
|
async ensureStreamReady(nextChunkBytes) {
|
|
274
|
-
const date =
|
|
286
|
+
const date = formatYmdHms(new Date(), "date");
|
|
275
287
|
// 日期变化:切新文件
|
|
276
288
|
if (this.stream && this.streamDate && date !== this.streamDate) {
|
|
277
289
|
await this.closeStream();
|
|
@@ -293,19 +305,9 @@ class LogFileSink {
|
|
|
293
305
|
this.streamDate = date;
|
|
294
306
|
this.streamIndex = 0;
|
|
295
307
|
this.streamSizeBytes = size;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.stream.on("error", (error) => {
|
|
299
|
-
safeWriteStderrOnce(`[Logger] file sink error (${this.prefix}): ${error?.message || error}`);
|
|
300
|
-
this.disabled = true;
|
|
301
|
-
void this.closeStream();
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
catch (error) {
|
|
305
|
-
safeWriteStderrOnce(`[Logger] createWriteStream failed (${this.prefix}): ${error?.message || error}`);
|
|
306
|
-
this.disabled = true;
|
|
308
|
+
this.openStream(filePath);
|
|
309
|
+
if (!this.stream)
|
|
307
310
|
return;
|
|
308
|
-
}
|
|
309
311
|
}
|
|
310
312
|
// 大小滚动
|
|
311
313
|
if (this.stream && this.maxFileBytes > 0 && this.streamSizeBytes + nextChunkBytes > this.maxFileBytes) {
|
|
@@ -314,59 +316,9 @@ class LogFileSink {
|
|
|
314
316
|
const filePath = this.getFilePath(date, this.streamIndex);
|
|
315
317
|
this.streamDate = date;
|
|
316
318
|
this.streamSizeBytes = 0;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
this.stream.on("error", (error) => {
|
|
320
|
-
safeWriteStderrOnce(`[Logger] file sink error (${this.prefix}): ${error?.message || error}`);
|
|
321
|
-
this.disabled = true;
|
|
322
|
-
void this.closeStream();
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
safeWriteStderrOnce(`[Logger] createWriteStream failed (${this.prefix}): ${error?.message || error}`);
|
|
327
|
-
this.disabled = true;
|
|
319
|
+
this.openStream(filePath);
|
|
320
|
+
if (!this.stream)
|
|
328
321
|
return;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
async flush() {
|
|
333
|
-
if (this.disabled) {
|
|
334
|
-
this.pending = [];
|
|
335
|
-
this.pendingBytes = 0;
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (this.flushing)
|
|
339
|
-
return;
|
|
340
|
-
this.scheduled = false;
|
|
341
|
-
this.flushing = true;
|
|
342
|
-
try {
|
|
343
|
-
while (this.pending.length > 0 && !this.disabled) {
|
|
344
|
-
const batch = shiftBatchFromPending(this.pending, this.maxBatchBytes);
|
|
345
|
-
const chunk = batch.chunk;
|
|
346
|
-
const chunkBytes = Buffer.byteLength(chunk);
|
|
347
|
-
this.pendingBytes = this.pendingBytes - chunkBytes;
|
|
348
|
-
await this.ensureStreamReady(chunkBytes);
|
|
349
|
-
if (!this.stream) {
|
|
350
|
-
// 文件 sink 已被禁用或打开失败
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
const ok = this.stream.write(chunk);
|
|
354
|
-
if (!ok) {
|
|
355
|
-
await new Promise((resolve) => {
|
|
356
|
-
this.stream.once("drain", () => resolve());
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
this.streamSizeBytes = this.streamSizeBytes + chunkBytes;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
safeWriteStderrOnce(`[Logger] file sink flush error (${this.prefix}): ${error?.message || error}`);
|
|
364
|
-
}
|
|
365
|
-
finally {
|
|
366
|
-
this.flushing = false;
|
|
367
|
-
if (this.pending.length > 0 && !this.disabled) {
|
|
368
|
-
this.scheduleFlush();
|
|
369
|
-
}
|
|
370
322
|
}
|
|
371
323
|
}
|
|
372
324
|
}
|
|
@@ -394,13 +346,20 @@ export async function shutdown() {
|
|
|
394
346
|
// 测试场景:mock logger 不需要 shutdown
|
|
395
347
|
if (mockInstance)
|
|
396
348
|
return;
|
|
349
|
+
// 重要:shutdown 可能与后续 Logger.getLogger() 并发。
|
|
350
|
+
// 因此这里捕获“当前的旧 sink/instance 快照”,只关闭这些快照,避免把新创建的 sink 一并清掉。
|
|
351
|
+
const currentInstance = instance;
|
|
352
|
+
const currentErrorInstance = errorInstance;
|
|
353
|
+
const currentAppFileSink = appFileSink;
|
|
354
|
+
const currentErrorFileSink = errorFileSink;
|
|
355
|
+
const currentAppConsoleSink = appConsoleSink;
|
|
397
356
|
const sinks = [];
|
|
398
|
-
if (
|
|
399
|
-
sinks.push({ shutdown: () =>
|
|
400
|
-
if (
|
|
401
|
-
sinks.push({ shutdown: () =>
|
|
402
|
-
if (
|
|
403
|
-
sinks.push({ shutdown: () =>
|
|
357
|
+
if (currentAppFileSink)
|
|
358
|
+
sinks.push({ shutdown: () => currentAppFileSink.shutdown() });
|
|
359
|
+
if (currentErrorFileSink)
|
|
360
|
+
sinks.push({ shutdown: () => currentErrorFileSink.shutdown() });
|
|
361
|
+
if (currentAppConsoleSink)
|
|
362
|
+
sinks.push({ shutdown: () => currentAppConsoleSink.shutdown() });
|
|
404
363
|
for (const item of sinks) {
|
|
405
364
|
try {
|
|
406
365
|
await item.shutdown();
|
|
@@ -409,25 +368,23 @@ export async function shutdown() {
|
|
|
409
368
|
// ignore
|
|
410
369
|
}
|
|
411
370
|
}
|
|
412
|
-
appFileSink
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
371
|
+
if (appFileSink === currentAppFileSink) {
|
|
372
|
+
appFileSink = null;
|
|
373
|
+
}
|
|
374
|
+
if (errorFileSink === currentErrorFileSink) {
|
|
375
|
+
errorFileSink = null;
|
|
376
|
+
}
|
|
377
|
+
if (appConsoleSink === currentAppConsoleSink) {
|
|
378
|
+
appConsoleSink = null;
|
|
379
|
+
}
|
|
380
|
+
if (instance === currentInstance) {
|
|
381
|
+
instance = null;
|
|
382
|
+
}
|
|
383
|
+
if (errorInstance === currentErrorInstance) {
|
|
384
|
+
errorInstance = null;
|
|
385
|
+
}
|
|
417
386
|
// shutdown 后允许下一次重新初始化时再次校验/创建目录(测试会清理目录,避免 ENOENT)
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
function normalizePositiveInt(value, fallback, min, max) {
|
|
421
|
-
if (typeof value !== "number")
|
|
422
|
-
return fallback;
|
|
423
|
-
if (!Number.isFinite(value))
|
|
424
|
-
return fallback;
|
|
425
|
-
const v = Math.floor(value);
|
|
426
|
-
if (v < min)
|
|
427
|
-
return fallback;
|
|
428
|
-
if (v > max)
|
|
429
|
-
return max;
|
|
430
|
-
return v;
|
|
387
|
+
// 无需缓存状态:确保目录存在是幂等的。
|
|
431
388
|
}
|
|
432
389
|
function resolveLogDir() {
|
|
433
390
|
const rawDir = config.dir || "./logs";
|
|
@@ -447,115 +404,45 @@ function ensureLogDirExists() {
|
|
|
447
404
|
// 不能在 Logger 初始化前调用 Logger 本身,直接抛错即可
|
|
448
405
|
throw new Error(`创建 logs 目录失败: ${dir}. ${error?.message || error}`);
|
|
449
406
|
}
|
|
450
|
-
didEnsureLogDir = true;
|
|
451
|
-
}
|
|
452
|
-
async function pruneOldLogFiles() {
|
|
453
|
-
if (didPruneOldLogFiles)
|
|
454
|
-
return;
|
|
455
|
-
didPruneOldLogFiles = true;
|
|
456
|
-
const dir = resolveLogDir();
|
|
457
|
-
const now = Date.now();
|
|
458
|
-
const cutoff = now - ONE_YEAR_MS;
|
|
459
|
-
try {
|
|
460
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
461
|
-
for (const entry of entries) {
|
|
462
|
-
if (!entry.isFile())
|
|
463
|
-
continue;
|
|
464
|
-
const name = entry.name;
|
|
465
|
-
// 只处理本项目的日志文件前缀
|
|
466
|
-
const isTarget = name.startsWith("app.") || name.startsWith("error.");
|
|
467
|
-
if (!isTarget)
|
|
468
|
-
continue;
|
|
469
|
-
const fullPath = nodePathJoin(dir, name);
|
|
470
|
-
let st;
|
|
471
|
-
try {
|
|
472
|
-
st = await stat(fullPath);
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
const mtimeMs = typeof st.mtimeMs === "number" ? st.mtimeMs : 0;
|
|
478
|
-
if (mtimeMs > 0 && mtimeMs < cutoff) {
|
|
479
|
-
try {
|
|
480
|
-
await unlink(fullPath);
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
// 忽略删除失败(权限/占用等),避免影响服务启动
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
catch {
|
|
489
|
-
// 忽略:目录不存在或无权限等
|
|
490
|
-
}
|
|
491
407
|
}
|
|
408
|
+
// 方案B:删除“启动时清理旧日志”功能(减少 I/O 与复杂度)。
|
|
492
409
|
/**
|
|
493
410
|
* 配置日志
|
|
494
411
|
*/
|
|
495
412
|
export function configure(cfg) {
|
|
496
413
|
// 旧实例可能仍持有文件句柄;这里异步关闭(不阻塞主流程)
|
|
497
414
|
void shutdown();
|
|
498
|
-
|
|
415
|
+
// 方案B:每次 configure 都从默认配置重新构建(避免继承上一次配置造成测试/运行时污染)
|
|
416
|
+
config = Object.assign({
|
|
417
|
+
debug: 0,
|
|
418
|
+
dir: "./logs",
|
|
419
|
+
console: 1,
|
|
420
|
+
maxSize: 20
|
|
421
|
+
}, cfg);
|
|
422
|
+
// maxSize:仅按 MB 计算,且强制范围 10..100
|
|
423
|
+
{
|
|
424
|
+
const raw = config.maxSize;
|
|
425
|
+
let mb = typeof raw === "number" && Number.isFinite(raw) ? raw : 20;
|
|
426
|
+
if (mb < 10)
|
|
427
|
+
mb = 10;
|
|
428
|
+
if (mb > 100)
|
|
429
|
+
mb = 100;
|
|
430
|
+
config.maxSize = mb;
|
|
431
|
+
}
|
|
499
432
|
instance = null;
|
|
500
433
|
errorInstance = null;
|
|
501
|
-
didPruneOldLogFiles = false;
|
|
502
|
-
didEnsureLogDir = false;
|
|
503
|
-
didWarnIoError = false;
|
|
504
434
|
appFileSink = null;
|
|
505
435
|
errorFileSink = null;
|
|
506
436
|
appConsoleSink = null;
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const patterns = [];
|
|
517
|
-
for (const item of BUILTIN_SENSITIVE_KEYS) {
|
|
518
|
-
const trimmed = String(item).trim();
|
|
519
|
-
if (trimmed.length > 0)
|
|
520
|
-
patterns.push(trimmed.toLowerCase());
|
|
521
|
-
}
|
|
522
|
-
for (const item of userPatterns) {
|
|
523
|
-
const trimmed = String(item).trim();
|
|
524
|
-
if (trimmed.length > 0)
|
|
525
|
-
patterns.push(trimmed.toLowerCase());
|
|
526
|
-
}
|
|
527
|
-
const exactSet = new Set();
|
|
528
|
-
const containsMatchers = [];
|
|
529
|
-
for (const pat of patterns) {
|
|
530
|
-
// 精简策略:
|
|
531
|
-
// - 无 *:精确匹配
|
|
532
|
-
// - 有 *:统一按“包含匹配”处理(*x*、x*、*x、a*b 都视为包含 core)
|
|
533
|
-
if (!pat.includes("*")) {
|
|
534
|
-
exactSet.add(pat);
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
const core = pat.replace(/\*+/g, "").trim();
|
|
538
|
-
if (!core) {
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
containsMatchers.push(core);
|
|
542
|
-
}
|
|
543
|
-
sensitiveKeySet = exactSet;
|
|
544
|
-
sensitiveContainsMatchers = containsMatchers;
|
|
545
|
-
// 预编译包含匹配:减少每次 isSensitiveKey 的循环开销
|
|
546
|
-
// 注意:patterns 已全部 lowerCase,因此 regex 不需要 i 标志
|
|
547
|
-
if (containsMatchers.length > 0) {
|
|
548
|
-
const escaped = containsMatchers.map((item) => escapeRegExp(item)).filter((item) => item.length > 0);
|
|
549
|
-
if (escaped.length > 0) {
|
|
550
|
-
sensitiveContainsRegex = new RegExp(escaped.join("|"));
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
sensitiveContainsRegex = null;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
sensitiveContainsRegex = null;
|
|
558
|
-
}
|
|
437
|
+
// 运行时清洗/截断上限(可配置)
|
|
438
|
+
sanitizeOptions = {
|
|
439
|
+
maxStringLen: normalizePositiveInt(config.maxStringLen, 100, 20, 200000),
|
|
440
|
+
maxArrayItems: normalizePositiveInt(config.maxArrayItems, 100, 10, 5000),
|
|
441
|
+
sanitizeDepth: normalizePositiveInt(config.sanitizeDepth, 3, 1, 10),
|
|
442
|
+
sanitizeNodes: normalizePositiveInt(config.sanitizeNodes, 500, 50, 20000),
|
|
443
|
+
sanitizeObjectKeys: normalizePositiveInt(config.sanitizeObjectKeys, 100, 10, 5000),
|
|
444
|
+
sensitiveKeyMatcher: buildSensitiveKeyMatcher({ builtinPatterns: BUILTIN_SENSITIVE_KEYS, userPatterns: config.excludeFields })
|
|
445
|
+
};
|
|
559
446
|
}
|
|
560
447
|
/**
|
|
561
448
|
* 设置 Mock Logger(用于测试)
|
|
@@ -564,136 +451,67 @@ export function configure(cfg) {
|
|
|
564
451
|
export function setMockLogger(mock) {
|
|
565
452
|
mockInstance = mock;
|
|
566
453
|
}
|
|
567
|
-
|
|
568
|
-
* 获取 Logger 实例(延迟初始化)
|
|
569
|
-
*/
|
|
570
|
-
export function getLogger() {
|
|
454
|
+
function getSink(kind) {
|
|
571
455
|
// 优先返回 mock 实例(用于测试)
|
|
572
456
|
if (mockInstance)
|
|
573
457
|
return mockInstance;
|
|
574
|
-
if (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
// 启动时清理过期日志(异步,不阻塞初始化)
|
|
578
|
-
void pruneOldLogFiles();
|
|
579
|
-
const minLevel = normalizeLogLevelName();
|
|
580
|
-
const maxFileBytes = (typeof config.maxSize === "number" && config.maxSize > 0 ? config.maxSize : 10) * 1024 * 1024;
|
|
581
|
-
if (!appFileSink) {
|
|
582
|
-
appFileSink = new LogFileSink({ prefix: "app", minLevel: minLevel, maxFileBytes: maxFileBytes });
|
|
458
|
+
if (kind === "app") {
|
|
459
|
+
if (instance)
|
|
460
|
+
return instance;
|
|
583
461
|
}
|
|
584
|
-
|
|
585
|
-
|
|
462
|
+
else {
|
|
463
|
+
if (errorInstance)
|
|
464
|
+
return errorInstance;
|
|
586
465
|
}
|
|
587
|
-
instance = createSinkLogger({ kind: "app", minLevel: minLevel, fileSink: appFileSink, consoleSink: config.console === 1 ? appConsoleSink : null });
|
|
588
|
-
return instance;
|
|
589
|
-
}
|
|
590
|
-
function getErrorLogger() {
|
|
591
|
-
if (mockInstance)
|
|
592
|
-
return mockInstance;
|
|
593
|
-
if (errorInstance)
|
|
594
|
-
return errorInstance;
|
|
595
466
|
ensureLogDirExists();
|
|
596
|
-
|
|
597
|
-
const maxFileBytes = (
|
|
467
|
+
const maxSizeMb = typeof config.maxSize === "number" ? config.maxSize : 20;
|
|
468
|
+
const maxFileBytes = Math.floor(maxSizeMb * 1024 * 1024);
|
|
469
|
+
if (kind === "app") {
|
|
470
|
+
if (!appFileSink) {
|
|
471
|
+
appFileSink = new LogFileSink({ prefix: "app", maxFileBytes: maxFileBytes });
|
|
472
|
+
}
|
|
473
|
+
if (config.console === 1 && !appConsoleSink) {
|
|
474
|
+
appConsoleSink = createStreamSink("stdout");
|
|
475
|
+
}
|
|
476
|
+
instance = createSinkLogger({ fileSink: appFileSink, consoleSink: config.console === 1 ? appConsoleSink : null });
|
|
477
|
+
return instance;
|
|
478
|
+
}
|
|
598
479
|
if (!errorFileSink) {
|
|
599
|
-
|
|
600
|
-
errorFileSink = new LogFileSink({ prefix: "error", minLevel: "error", maxFileBytes: maxFileBytes });
|
|
480
|
+
errorFileSink = new LogFileSink({ prefix: "error", maxFileBytes: maxFileBytes });
|
|
601
481
|
}
|
|
602
|
-
errorInstance = createSinkLogger({
|
|
482
|
+
errorInstance = createSinkLogger({ fileSink: errorFileSink, consoleSink: null });
|
|
603
483
|
return errorInstance;
|
|
604
484
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if (item === null) {
|
|
611
|
-
parts.push("null");
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
if (item === undefined) {
|
|
615
|
-
parts.push("undefined");
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
if (typeof item === "string") {
|
|
619
|
-
parts.push(item);
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
if (typeof item === "number" || typeof item === "boolean" || typeof item === "bigint") {
|
|
623
|
-
parts.push(String(item));
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
if (item instanceof Error) {
|
|
627
|
-
parts.push(item.stack || item.message || item.name);
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
|
-
if (isPlainObject(item) || Array.isArray(item)) {
|
|
631
|
-
try {
|
|
632
|
-
parts.push(JSON.stringify(item));
|
|
633
|
-
}
|
|
634
|
-
catch {
|
|
635
|
-
parts.push("[Unserializable]");
|
|
636
|
-
}
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
try {
|
|
640
|
-
parts.push(String(item));
|
|
641
|
-
}
|
|
642
|
-
catch {
|
|
643
|
-
parts.push("[Unknown]");
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return parts.join(" ");
|
|
485
|
+
/**
|
|
486
|
+
* 获取 Logger 实例(延迟初始化)
|
|
487
|
+
*/
|
|
488
|
+
export function getLogger() {
|
|
489
|
+
return getSink("app");
|
|
647
490
|
}
|
|
648
|
-
function buildLogLine(level,
|
|
491
|
+
function buildLogLine(level, record) {
|
|
649
492
|
const time = Date.now();
|
|
650
493
|
const base = {
|
|
651
|
-
level:
|
|
494
|
+
level: level,
|
|
652
495
|
time: time,
|
|
496
|
+
timeFormat: formatYmdHms(new Date(time)),
|
|
653
497
|
pid: process.pid,
|
|
654
498
|
hostname: HOSTNAME
|
|
655
499
|
};
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
// 兼容:logger.(level)(obj, msg?, ...)
|
|
663
|
-
if (isPlainObject(first)) {
|
|
664
|
-
for (const [k, v] of Object.entries(first)) {
|
|
665
|
-
base[k] = v;
|
|
500
|
+
// record 先写入,再用 base 覆盖基础字段(避免 record 覆盖 level/time/pid/hostname 等)
|
|
501
|
+
// msg 允许保留 record.msg(若存在),否则补齐空字符串。
|
|
502
|
+
if (record && isPlainObject(record)) {
|
|
503
|
+
const out = Object.assign({}, record, base);
|
|
504
|
+
if (record.msg !== undefined) {
|
|
505
|
+
out.msg = record.msg;
|
|
666
506
|
}
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
base.msg = extraMsg ? `${second} ${extraMsg}` : second;
|
|
507
|
+
else if (out.msg === undefined) {
|
|
508
|
+
out.msg = "";
|
|
670
509
|
}
|
|
671
|
-
|
|
672
|
-
const extraMsg = formatExtrasToMsg(args.slice(1));
|
|
673
|
-
base.msg = extraMsg;
|
|
674
|
-
}
|
|
675
|
-
return `${safeJsonStringify(base)}\n`;
|
|
510
|
+
return `${safeJsonStringify(out)}\n`;
|
|
676
511
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
base.err = sanitizeErrorValue(first);
|
|
680
|
-
if (typeof second === "string") {
|
|
681
|
-
const extraMsg = formatExtrasToMsg(args.slice(2));
|
|
682
|
-
base.msg = extraMsg ? `${second} ${extraMsg}` : second;
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
base.msg = formatExtrasToMsg(args.slice(1));
|
|
686
|
-
}
|
|
687
|
-
return `${safeJsonStringify(base)}\n`;
|
|
688
|
-
}
|
|
689
|
-
// 兼容:logger.(level)(msg, ...)
|
|
690
|
-
if (typeof first === "string") {
|
|
691
|
-
const extraMsg = formatExtrasToMsg(args.slice(1));
|
|
692
|
-
base.msg = extraMsg ? `${first} ${extraMsg}` : first;
|
|
693
|
-
return `${safeJsonStringify(base)}\n`;
|
|
512
|
+
if (base.msg === undefined) {
|
|
513
|
+
base.msg = "";
|
|
694
514
|
}
|
|
695
|
-
// 兜底
|
|
696
|
-
base.msg = formatExtrasToMsg(args);
|
|
697
515
|
return `${safeJsonStringify(base)}\n`;
|
|
698
516
|
}
|
|
699
517
|
function safeJsonStringify(obj) {
|
|
@@ -710,239 +528,33 @@ function safeJsonStringify(obj) {
|
|
|
710
528
|
}
|
|
711
529
|
}
|
|
712
530
|
function createSinkLogger(options) {
|
|
713
|
-
const
|
|
714
|
-
const
|
|
715
|
-
|
|
531
|
+
const fileSink = options.fileSink;
|
|
532
|
+
const consoleSink = options.consoleSink;
|
|
533
|
+
const write = (level, record) => {
|
|
534
|
+
if (level === "debug" && config.debug !== 1)
|
|
716
535
|
return;
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
536
|
+
const sanitizedRecord = sanitizeLogObject(record, sanitizeOptions);
|
|
537
|
+
const line = buildLogLine(level, sanitizedRecord);
|
|
538
|
+
fileSink.enqueue(line);
|
|
539
|
+
if (consoleSink)
|
|
540
|
+
consoleSink.enqueue(line);
|
|
722
541
|
};
|
|
723
542
|
return {
|
|
724
|
-
info(
|
|
725
|
-
write("info",
|
|
543
|
+
info(record) {
|
|
544
|
+
write("info", record);
|
|
726
545
|
},
|
|
727
|
-
warn(
|
|
728
|
-
write("warn",
|
|
546
|
+
warn(record) {
|
|
547
|
+
write("warn", record);
|
|
729
548
|
},
|
|
730
|
-
error(
|
|
731
|
-
write("error",
|
|
549
|
+
error(record) {
|
|
550
|
+
write("error", record);
|
|
732
551
|
},
|
|
733
|
-
debug(
|
|
734
|
-
write("debug",
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
function truncateString(val) {
|
|
739
|
-
if (val.length <= maxLogStringLen)
|
|
740
|
-
return val;
|
|
741
|
-
return val.slice(0, maxLogStringLen);
|
|
742
|
-
}
|
|
743
|
-
function isSensitiveKey(key) {
|
|
744
|
-
const lower = String(key).toLowerCase();
|
|
745
|
-
if (sensitiveKeySet.has(lower))
|
|
746
|
-
return true;
|
|
747
|
-
if (sensitiveContainsRegex) {
|
|
748
|
-
return sensitiveContainsRegex.test(lower);
|
|
749
|
-
}
|
|
750
|
-
for (const part of sensitiveContainsMatchers) {
|
|
751
|
-
if (lower.includes(part))
|
|
752
|
-
return true;
|
|
753
|
-
}
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
function safeToStringMasked(val, visited) {
|
|
757
|
-
if (typeof val === "string")
|
|
758
|
-
return val;
|
|
759
|
-
if (val instanceof Error) {
|
|
760
|
-
const name = val.name || "Error";
|
|
761
|
-
const message = val.message || "";
|
|
762
|
-
const stack = typeof val.stack === "string" ? val.stack : "";
|
|
763
|
-
const errObj = {
|
|
764
|
-
name: name,
|
|
765
|
-
message: message,
|
|
766
|
-
stack: stack
|
|
767
|
-
};
|
|
768
|
-
try {
|
|
769
|
-
return JSON.stringify(errObj);
|
|
770
|
-
}
|
|
771
|
-
catch {
|
|
772
|
-
return `${name}: ${message}`;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
if (val && typeof val === "object") {
|
|
776
|
-
if (visited.has(val)) {
|
|
777
|
-
return "[Circular]";
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
try {
|
|
781
|
-
const localVisited = visited;
|
|
782
|
-
const replacer = (k, v) => {
|
|
783
|
-
// JSON.stringify 的根节点 key 为空字符串
|
|
784
|
-
if (k && isSensitiveKey(k)) {
|
|
785
|
-
return "[MASKED]";
|
|
786
|
-
}
|
|
787
|
-
if (v && typeof v === "object") {
|
|
788
|
-
if (localVisited.has(v)) {
|
|
789
|
-
return "[Circular]";
|
|
790
|
-
}
|
|
791
|
-
localVisited.add(v);
|
|
792
|
-
}
|
|
793
|
-
return v;
|
|
794
|
-
};
|
|
795
|
-
return JSON.stringify(val, replacer);
|
|
796
|
-
}
|
|
797
|
-
catch {
|
|
798
|
-
try {
|
|
799
|
-
return String(val);
|
|
800
|
-
}
|
|
801
|
-
catch {
|
|
802
|
-
return "[Unserializable]";
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
function sanitizeErrorValue(err) {
|
|
807
|
-
const errObj = {
|
|
808
|
-
name: err.name || "Error",
|
|
809
|
-
message: truncateString(err.message || "")
|
|
810
|
-
};
|
|
811
|
-
if (typeof err.stack === "string") {
|
|
812
|
-
errObj.stack = truncateString(err.stack);
|
|
813
|
-
}
|
|
814
|
-
return errObj;
|
|
815
|
-
}
|
|
816
|
-
function stringifyPreview(val, visited) {
|
|
817
|
-
const str = safeToStringMasked(val, visited);
|
|
818
|
-
return truncateString(str);
|
|
819
|
-
}
|
|
820
|
-
function sanitizeValueLimited(val, visited) {
|
|
821
|
-
if (val === null || val === undefined)
|
|
822
|
-
return val;
|
|
823
|
-
if (typeof val === "string")
|
|
824
|
-
return truncateString(val);
|
|
825
|
-
if (typeof val === "number")
|
|
826
|
-
return val;
|
|
827
|
-
if (typeof val === "boolean")
|
|
828
|
-
return val;
|
|
829
|
-
if (typeof val === "bigint")
|
|
830
|
-
return val;
|
|
831
|
-
if (val instanceof Error) {
|
|
832
|
-
return sanitizeErrorValue(val);
|
|
833
|
-
}
|
|
834
|
-
// 仅支持数组 + plain object 的结构化清洗,其余类型走字符串预览。
|
|
835
|
-
const isArr = Array.isArray(val);
|
|
836
|
-
const isObj = isPlainObject(val);
|
|
837
|
-
if (!isArr && !isObj) {
|
|
838
|
-
return stringifyPreview(val, visited);
|
|
839
|
-
}
|
|
840
|
-
// 防环(根节点)
|
|
841
|
-
if (visited.has(val)) {
|
|
842
|
-
return "[Circular]";
|
|
843
|
-
}
|
|
844
|
-
visited.add(val);
|
|
845
|
-
const rootOut = isArr ? [] : {};
|
|
846
|
-
const stack = [{ src: val, dst: rootOut, depth: 1 }];
|
|
847
|
-
let nodes = 0;
|
|
848
|
-
const tryAssign = (dst, key, child, depth) => {
|
|
849
|
-
if (child === null || child === undefined) {
|
|
850
|
-
dst[key] = child;
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
if (typeof child === "string") {
|
|
854
|
-
dst[key] = truncateString(child);
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
if (typeof child === "number" || typeof child === "boolean" || typeof child === "bigint") {
|
|
858
|
-
dst[key] = child;
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
if (child instanceof Error) {
|
|
862
|
-
dst[key] = sanitizeErrorValue(child);
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
const childIsArr = Array.isArray(child);
|
|
866
|
-
const childIsObj = isPlainObject(child);
|
|
867
|
-
if (!childIsArr && !childIsObj) {
|
|
868
|
-
dst[key] = stringifyPreview(child, visited);
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
// 深度/节点数上限:超出则降级为字符串预览
|
|
872
|
-
if (depth >= sanitizeDepthLimit) {
|
|
873
|
-
dst[key] = stringifyPreview(child, visited);
|
|
874
|
-
return;
|
|
552
|
+
debug(record) {
|
|
553
|
+
write("debug", record);
|
|
875
554
|
}
|
|
876
|
-
if (nodes >= sanitizeNodesLimit) {
|
|
877
|
-
dst[key] = stringifyPreview(child, visited);
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
// 防环
|
|
881
|
-
if (visited.has(child)) {
|
|
882
|
-
dst[key] = "[Circular]";
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
visited.add(child);
|
|
886
|
-
const childOut = childIsArr ? [] : {};
|
|
887
|
-
dst[key] = childOut;
|
|
888
|
-
stack.push({ src: child, dst: childOut, depth: depth + 1 });
|
|
889
555
|
};
|
|
890
|
-
while (stack.length > 0) {
|
|
891
|
-
const frame = stack.pop();
|
|
892
|
-
nodes = nodes + 1;
|
|
893
|
-
if (nodes > sanitizeNodesLimit) {
|
|
894
|
-
// 超出节点上限:不再深入(已入栈的节点会被忽略,留空结构由上层兜底预览)。
|
|
895
|
-
break;
|
|
896
|
-
}
|
|
897
|
-
if (Array.isArray(frame.src)) {
|
|
898
|
-
const arr = frame.src;
|
|
899
|
-
const len = arr.length;
|
|
900
|
-
const limit = len > maxLogArrayItems ? maxLogArrayItems : len;
|
|
901
|
-
for (let i = 0; i < limit; i++) {
|
|
902
|
-
tryAssign(frame.dst, i, arr[i], frame.depth);
|
|
903
|
-
}
|
|
904
|
-
if (len > maxLogArrayItems) {
|
|
905
|
-
// ignore omitted items
|
|
906
|
-
}
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
if (isPlainObject(frame.src)) {
|
|
910
|
-
const entries = Object.entries(frame.src);
|
|
911
|
-
const len = entries.length;
|
|
912
|
-
const limit = len > sanitizeObjectKeysLimit ? sanitizeObjectKeysLimit : len;
|
|
913
|
-
for (let i = 0; i < limit; i++) {
|
|
914
|
-
const key = entries[i][0];
|
|
915
|
-
const child = entries[i][1];
|
|
916
|
-
if (isSensitiveKey(key)) {
|
|
917
|
-
frame.dst[key] = "[MASKED]";
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
tryAssign(frame.dst, key, child, frame.depth);
|
|
921
|
-
}
|
|
922
|
-
if (len > sanitizeObjectKeysLimit) {
|
|
923
|
-
// ignore omitted keys
|
|
924
|
-
}
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
// 兜底:理论上不会到这里(frame 只会压入 array/plain object)
|
|
928
|
-
}
|
|
929
|
-
return rootOut;
|
|
930
|
-
}
|
|
931
|
-
function sanitizeTopValue(val, visited) {
|
|
932
|
-
return sanitizeValueLimited(val, visited);
|
|
933
|
-
}
|
|
934
|
-
function sanitizeLogObject(obj) {
|
|
935
|
-
const visited = new WeakSet();
|
|
936
|
-
const out = {};
|
|
937
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
938
|
-
if (isSensitiveKey(key)) {
|
|
939
|
-
out[key] = "[MASKED]";
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
out[key] = sanitizeTopValue(val, visited);
|
|
943
|
-
}
|
|
944
|
-
return out;
|
|
945
556
|
}
|
|
557
|
+
// 对象清洗/脱敏/截断逻辑已下沉到 utils/loggerUtils.ts(减少 logger.ts 复杂度)。
|
|
946
558
|
function metaToObject() {
|
|
947
559
|
const meta = getCtx();
|
|
948
560
|
if (!meta)
|
|
@@ -976,114 +588,88 @@ function mergeMetaIntoObject(input, meta) {
|
|
|
976
588
|
}
|
|
977
589
|
return merged;
|
|
978
590
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
if (
|
|
984
|
-
return
|
|
985
|
-
const first = args[0];
|
|
986
|
-
const second = args.length > 1 ? args[1] : undefined;
|
|
987
|
-
// 兼容:Logger.error("xxx", err)
|
|
988
|
-
if (typeof first === "string" && second instanceof Error) {
|
|
989
|
-
const obj = {
|
|
990
|
-
err: second
|
|
991
|
-
};
|
|
992
|
-
const merged = mergeMetaIntoObject(obj, meta);
|
|
993
|
-
return [merged, first, ...args.slice(2)];
|
|
591
|
+
// 日志仅接受 1 个入参(任意类型)。
|
|
592
|
+
// - plain object({})直接作为 record
|
|
593
|
+
// - 其他任何类型:包装成对象再写入(避免 sink 层依赖入参类型)
|
|
594
|
+
function toRecord(input) {
|
|
595
|
+
if (isPlainObject(input)) {
|
|
596
|
+
return input;
|
|
994
597
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const msg = typeof second === "string" ? second : undefined;
|
|
998
|
-
const obj = {
|
|
999
|
-
err: first
|
|
1000
|
-
};
|
|
1001
|
-
const merged = mergeMetaIntoObject(obj, meta);
|
|
1002
|
-
if (msg)
|
|
1003
|
-
return [merged, msg, ...args.slice(2)];
|
|
1004
|
-
return [merged, ...args.slice(1)];
|
|
598
|
+
if (input instanceof Error) {
|
|
599
|
+
return { err: input, msg: input.message || input.name || "Error" };
|
|
1005
600
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
return [meta, ...args];
|
|
601
|
+
if (input === undefined) {
|
|
602
|
+
return { msg: "" };
|
|
1009
603
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
604
|
+
if (input === null) {
|
|
605
|
+
return { msg: "null" };
|
|
606
|
+
}
|
|
607
|
+
// 非 plain object(数组/Date/Map/...)也走 value 包装,由 sanitize 负责做安全预览/截断。
|
|
608
|
+
if (typeof input === "object") {
|
|
609
|
+
return { value: input };
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
return { msg: String(input) };
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return { msg: "[Unserializable]" };
|
|
1014
616
|
}
|
|
1015
|
-
return args;
|
|
1016
|
-
}
|
|
1017
|
-
function withRequestMetaTyped(args) {
|
|
1018
|
-
// 复用现有逻辑(保持行为一致),只收敛类型
|
|
1019
|
-
return withRequestMeta(args);
|
|
1020
617
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
const
|
|
1036
|
-
return
|
|
1037
|
-
}
|
|
1038
|
-
warn(
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
const ret = logger.warn.apply(logger, finalArgs);
|
|
1049
|
-
return ret;
|
|
1050
|
-
},
|
|
1051
|
-
error(...args) {
|
|
1052
|
-
if (args.length === 0)
|
|
1053
|
-
return;
|
|
1054
|
-
const logger = getLogger();
|
|
1055
|
-
const finalArgs = withRequestMetaTyped(args);
|
|
1056
|
-
if (finalArgs.length === 0)
|
|
1057
|
-
return;
|
|
1058
|
-
if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
|
|
1059
|
-
finalArgs[0] = sanitizeLogObject(finalArgs[0]);
|
|
1060
|
-
}
|
|
1061
|
-
const ret = logger.error.apply(logger, finalArgs);
|
|
618
|
+
function withRequestMetaRecord(record) {
|
|
619
|
+
const meta = metaToObject();
|
|
620
|
+
if (!meta)
|
|
621
|
+
return record;
|
|
622
|
+
return mergeMetaIntoObject(record, meta);
|
|
623
|
+
}
|
|
624
|
+
class LoggerFacade {
|
|
625
|
+
maybeSanitizeForMock(record) {
|
|
626
|
+
if (!mockInstance)
|
|
627
|
+
return record;
|
|
628
|
+
return sanitizeLogObject(record, sanitizeOptions);
|
|
629
|
+
}
|
|
630
|
+
info(input) {
|
|
631
|
+
const record0 = withRequestMetaRecord(toRecord(input));
|
|
632
|
+
const record = this.maybeSanitizeForMock(record0);
|
|
633
|
+
return getSink("app").info(record);
|
|
634
|
+
}
|
|
635
|
+
warn(input) {
|
|
636
|
+
const record0 = withRequestMetaRecord(toRecord(input));
|
|
637
|
+
const record = this.maybeSanitizeForMock(record0);
|
|
638
|
+
return getSink("app").warn(record);
|
|
639
|
+
}
|
|
640
|
+
error(input) {
|
|
641
|
+
const record0 = withRequestMetaRecord(toRecord(input));
|
|
642
|
+
const record = this.maybeSanitizeForMock(record0);
|
|
643
|
+
const ret = getSink("app").error(record);
|
|
1062
644
|
// 测试场景:启用 mock 时不做镜像,避免调用次数翻倍
|
|
1063
645
|
if (mockInstance)
|
|
1064
646
|
return ret;
|
|
1065
647
|
// error 专属文件:始终镜像一份
|
|
1066
|
-
|
|
1067
|
-
errorLogger.error.apply(errorLogger, finalArgs);
|
|
648
|
+
getSink("error").error(record);
|
|
1068
649
|
return ret;
|
|
1069
|
-
}
|
|
1070
|
-
debug(
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const logger = getLogger();
|
|
1074
|
-
const finalArgs = withRequestMetaTyped(args);
|
|
1075
|
-
if (finalArgs.length === 0)
|
|
650
|
+
}
|
|
651
|
+
debug(input) {
|
|
652
|
+
// debug!=1 则完全不记录 debug 日志(包括文件与控制台)
|
|
653
|
+
if (config.debug !== 1)
|
|
1076
654
|
return;
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
return ret;
|
|
1082
|
-
},
|
|
655
|
+
const record0 = withRequestMetaRecord(toRecord(input));
|
|
656
|
+
const record = this.maybeSanitizeForMock(record0);
|
|
657
|
+
return getSink("app").debug(record);
|
|
658
|
+
}
|
|
1083
659
|
async flush() {
|
|
1084
660
|
await flush();
|
|
1085
|
-
}
|
|
1086
|
-
configure
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
661
|
+
}
|
|
662
|
+
configure(cfg) {
|
|
663
|
+
configure(cfg);
|
|
664
|
+
}
|
|
665
|
+
setMock(mock) {
|
|
666
|
+
setMockLogger(mock);
|
|
667
|
+
}
|
|
668
|
+
async shutdown() {
|
|
669
|
+
await shutdown();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* 日志实例(延迟初始化)
|
|
674
|
+
*/
|
|
675
|
+
export const Logger = new LoggerFacade();
|