befly 3.10.19 → 3.11.1
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.d.ts +1 -1
- package/dist/befly.config.js +3 -5
- package/dist/befly.js +15621 -0
- package/dist/befly.min.js +21 -0
- package/dist/checks/checkApi.js +2 -3
- package/dist/checks/checkHook.js +50 -3
- package/dist/checks/checkMenu.d.ts +2 -2
- package/dist/checks/checkMenu.js +3 -3
- package/dist/checks/checkPlugin.js +50 -3
- package/dist/checks/checkTable.d.ts +1 -1
- package/dist/checks/checkTable.js +1 -1
- package/dist/hooks/auth.d.ts +3 -1
- package/dist/hooks/auth.js +3 -1
- package/dist/hooks/cors.d.ts +3 -1
- package/dist/hooks/cors.js +3 -1
- package/dist/hooks/parser.d.ts +3 -1
- package/dist/hooks/parser.js +4 -3
- package/dist/hooks/permission.d.ts +3 -1
- package/dist/hooks/permission.js +5 -3
- package/dist/hooks/validator.d.ts +3 -1
- package/dist/hooks/validator.js +4 -2
- package/dist/{main.d.ts → index.d.ts} +1 -1
- package/dist/{main.js → index.js} +24 -24
- package/dist/lib/cacheHelper.js +2 -2
- package/dist/lib/cipher.d.ts +1 -1
- package/dist/lib/connect.d.ts +1 -1
- package/dist/lib/connect.js +1 -1
- package/dist/lib/dbHelper.d.ts +3 -3
- package/dist/lib/dbHelper.js +9 -11
- package/dist/lib/dbUtils.d.ts +1 -1
- package/dist/lib/dbUtils.js +2 -3
- package/dist/lib/jwt.d.ts +2 -2
- package/dist/lib/logger.d.ts +17 -7
- package/dist/lib/logger.js +620 -210
- package/dist/lib/redisHelper.js +2 -2
- package/dist/lib/sqlBuilder.d.ts +1 -1
- package/dist/lib/sqlBuilder.js +1 -1
- package/dist/lib/validator.d.ts +1 -1
- package/dist/lib/validator.js +1 -1
- package/dist/loader/loadApis.d.ts +2 -2
- package/dist/loader/loadApis.js +3 -3
- package/dist/loader/loadHooks.d.ts +3 -4
- package/dist/loader/loadHooks.js +6 -8
- package/dist/loader/loadPlugins.d.ts +4 -4
- package/dist/loader/loadPlugins.js +7 -7
- package/dist/plugins/cache.d.ts +4 -2
- package/dist/plugins/cache.js +3 -1
- package/dist/plugins/cipher.d.ts +3 -1
- package/dist/plugins/cipher.js +3 -1
- package/dist/plugins/config.d.ts +4 -2
- package/dist/plugins/config.js +2 -0
- package/dist/plugins/db.d.ts +4 -2
- package/dist/plugins/db.js +6 -4
- package/dist/plugins/jwt.d.ts +4 -2
- package/dist/plugins/jwt.js +3 -1
- package/dist/plugins/logger.d.ts +7 -3
- package/dist/plugins/logger.js +3 -1
- package/dist/plugins/redis.d.ts +4 -2
- package/dist/plugins/redis.js +5 -3
- package/dist/plugins/tool.d.ts +3 -1
- package/dist/plugins/tool.js +2 -0
- package/dist/router/api.d.ts +3 -3
- package/dist/router/api.js +5 -5
- package/dist/router/static.d.ts +1 -1
- package/dist/router/static.js +3 -3
- package/dist/scripts/ensureDist.js +218 -2
- package/dist/sync/syncApi.d.ts +2 -2
- package/dist/sync/syncApi.js +2 -2
- package/dist/sync/syncCache.d.ts +1 -1
- package/dist/sync/syncDev.d.ts +1 -1
- package/dist/sync/syncDev.js +2 -2
- package/dist/sync/syncMenu.d.ts +2 -2
- package/dist/sync/syncMenu.js +4 -4
- package/dist/sync/syncTable.d.ts +6 -6
- package/dist/sync/syncTable.js +4 -4
- package/dist/types/api.d.ts +4 -4
- package/dist/types/befly.d.ts +8 -12
- package/dist/types/cache.d.ts +2 -2
- package/dist/types/cipher.d.ts +1 -1
- package/dist/types/context.d.ts +1 -1
- package/dist/types/database.d.ts +1 -1
- package/dist/types/hook.d.ts +4 -2
- package/dist/types/logger.d.ts +14 -2
- package/dist/types/plugin.d.ts +3 -1
- package/dist/types/sync.d.ts +2 -2
- package/dist/utils/cors.d.ts +1 -1
- package/dist/utils/importDefault.js +1 -1
- package/dist/utils/loadMenuConfigs.d.ts +26 -2
- package/dist/utils/loadMenuConfigs.js +44 -3
- package/dist/utils/mergeAndConcat.d.ts +7 -0
- package/dist/utils/mergeAndConcat.js +72 -0
- package/dist/utils/processAtSymbol.d.ts +4 -0
- package/dist/utils/{processFields.js → processAtSymbol.js} +2 -2
- package/dist/utils/response.d.ts +1 -1
- package/dist/utils/response.js +1 -1
- package/dist/utils/scanAddons.js +3 -3
- package/dist/utils/scanConfig.js +6 -7
- package/dist/utils/scanCoreBuiltins.d.ts +3 -0
- package/dist/utils/scanCoreBuiltins.js +65 -0
- package/dist/utils/scanFiles.js +19 -6
- package/dist/utils/scanSources.d.ts +2 -2
- package/dist/utils/scanSources.js +16 -11
- package/dist/utils/sortModules.js +2 -2
- package/dist/utils/util.d.ts +84 -0
- package/dist/utils/util.js +262 -0
- package/package.json +20 -14
- package/dist/utils/arrayKeysToCamel.d.ts +0 -13
- package/dist/utils/arrayKeysToCamel.js +0 -18
- package/dist/utils/configTypes.d.ts +0 -1
- package/dist/utils/configTypes.js +0 -1
- package/dist/utils/genShortId.d.ts +0 -10
- package/dist/utils/genShortId.js +0 -12
- package/dist/utils/keysToCamel.d.ts +0 -10
- package/dist/utils/keysToCamel.js +0 -21
- package/dist/utils/keysToSnake.d.ts +0 -10
- package/dist/utils/keysToSnake.js +0 -21
- package/dist/utils/pickFields.d.ts +0 -4
- package/dist/utils/pickFields.js +0 -16
- package/dist/utils/processFields.d.ts +0 -4
- package/dist/utils/regex.d.ts +0 -145
- package/dist/utils/regex.js +0 -202
- package/dist/utils/sqlLog.d.ts +0 -14
- package/dist/utils/sqlLog.js +0 -25
- /package/dist/utils/{process.d.ts → processInfo.d.ts} +0 -0
- /package/dist/utils/{process.js → processInfo.js} +0 -0
package/dist/lib/logger.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 日志系统 -
|
|
2
|
+
* 日志系统 - Bun 环境自定义实现(替换 pino / pino-roll)
|
|
3
3
|
*/
|
|
4
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { createWriteStream, existsSync, mkdirSync } from "node:fs";
|
|
5
5
|
import { readdir, stat, unlink } from "node:fs/promises";
|
|
6
|
+
import { hostname as osHostname } from "node:os";
|
|
6
7
|
import { isAbsolute as nodePathIsAbsolute, join as nodePathJoin, resolve as nodePathResolve } from "node:path";
|
|
7
|
-
import { isPlainObject } from "
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import { isPlainObject } from "../utils/util";
|
|
9
|
+
import { getCtx } from "./asyncContext";
|
|
10
|
+
const REGEXP_SPECIAL = /[\\^$.*+?()[\]{}|]/g;
|
|
11
|
+
export function escapeRegExp(input) {
|
|
12
|
+
return String(input).replace(REGEXP_SPECIAL, "\\$&");
|
|
13
|
+
}
|
|
12
14
|
// 注意:Logger 可能在运行时/测试中被 process.chdir() 影响。
|
|
13
15
|
// 为避免相对路径的 logs 目录随着 cwd 变化,使用模块加载时的初始 cwd 作为锚点。
|
|
14
16
|
const INITIAL_CWD = process.cwd();
|
|
@@ -27,22 +29,445 @@ let sanitizeNodesLimit = DEFAULT_LOG_SANITIZE_NODES;
|
|
|
27
29
|
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
|
28
30
|
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"];
|
|
29
31
|
let sensitiveKeySet = new Set();
|
|
30
|
-
let sensitiveSuffixMatchers = [];
|
|
31
|
-
let sensitivePrefixMatchers = [];
|
|
32
32
|
let sensitiveContainsMatchers = [];
|
|
33
33
|
let sensitiveContainsRegex = null;
|
|
34
|
+
const HOSTNAME = (() => {
|
|
35
|
+
try {
|
|
36
|
+
return osHostname();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
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;
|
|
34
51
|
let instance = null;
|
|
35
|
-
let slowInstance = null;
|
|
36
52
|
let errorInstance = null;
|
|
37
53
|
let mockInstance = null;
|
|
54
|
+
let appFileSink = null;
|
|
55
|
+
let errorFileSink = null;
|
|
56
|
+
let appConsoleSink = null;
|
|
57
|
+
let didWarnIoError = false;
|
|
38
58
|
let didPruneOldLogFiles = false;
|
|
39
59
|
let didEnsureLogDir = false;
|
|
60
|
+
let didInstallGracefulExitHooks = false;
|
|
40
61
|
let config = {
|
|
41
62
|
debug: 0,
|
|
42
63
|
dir: "./logs",
|
|
43
64
|
console: 1,
|
|
44
65
|
maxSize: 10
|
|
45
66
|
};
|
|
67
|
+
function installGracefulExitHooks() {
|
|
68
|
+
if (didInstallGracefulExitHooks)
|
|
69
|
+
return;
|
|
70
|
+
didInstallGracefulExitHooks = true;
|
|
71
|
+
const install = (signal, exitCode) => {
|
|
72
|
+
// 若业务侧已经注册了 handler,避免我们抢占默认行为
|
|
73
|
+
try {
|
|
74
|
+
if (typeof process.listenerCount === "function" && process.listenerCount(signal) > 0) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
process.once(signal, () => {
|
|
83
|
+
let exited = false;
|
|
84
|
+
// 给 flush 一个机会;超过上限则强制退出
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
if (exited)
|
|
87
|
+
return;
|
|
88
|
+
exited = true;
|
|
89
|
+
try {
|
|
90
|
+
process.exit(exitCode);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
}, 2000);
|
|
96
|
+
void shutdown()
|
|
97
|
+
.catch(() => undefined)
|
|
98
|
+
.finally(() => {
|
|
99
|
+
if (exited)
|
|
100
|
+
return;
|
|
101
|
+
exited = true;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
try {
|
|
104
|
+
process.exit(exitCode);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// ignore
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// ignore
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
install("SIGINT", 130);
|
|
117
|
+
install("SIGTERM", 143);
|
|
118
|
+
}
|
|
119
|
+
function formatLocalDate(date) {
|
|
120
|
+
const y = date.getFullYear();
|
|
121
|
+
const m = date.getMonth() + 1;
|
|
122
|
+
const d = date.getDate();
|
|
123
|
+
const mm = m < 10 ? `0${m}` : String(m);
|
|
124
|
+
const dd = d < 10 ? `0${d}` : String(d);
|
|
125
|
+
return `${y}-${mm}-${dd}`;
|
|
126
|
+
}
|
|
127
|
+
function normalizeLogLevelName() {
|
|
128
|
+
// 与旧行为保持一致:debug=1 -> debug,否则 -> info
|
|
129
|
+
return config.debug === 1 ? "debug" : "info";
|
|
130
|
+
}
|
|
131
|
+
function shouldAccept(minLevel, level) {
|
|
132
|
+
return LOG_LEVEL_NUM[level] >= LOG_LEVEL_NUM[minLevel];
|
|
133
|
+
}
|
|
134
|
+
function safeWriteStderrOnce(msg) {
|
|
135
|
+
if (didWarnIoError)
|
|
136
|
+
return;
|
|
137
|
+
didWarnIoError = true;
|
|
138
|
+
try {
|
|
139
|
+
process.stderr.write(`${msg}\n`);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function shiftBatchFromPending(pending, maxBatchBytes) {
|
|
146
|
+
const parts = [];
|
|
147
|
+
let bytes = 0;
|
|
148
|
+
while (pending.length > 0) {
|
|
149
|
+
const next = pending[0];
|
|
150
|
+
const nextBytes = Buffer.byteLength(next);
|
|
151
|
+
if (parts.length > 0 && bytes + nextBytes > maxBatchBytes) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
parts.push(next);
|
|
155
|
+
bytes += nextBytes;
|
|
156
|
+
pending.shift();
|
|
157
|
+
}
|
|
158
|
+
return { chunk: parts.join(""), bytes: bytes };
|
|
159
|
+
}
|
|
160
|
+
class LogStreamSink {
|
|
161
|
+
kind;
|
|
162
|
+
minLevel;
|
|
163
|
+
pending;
|
|
164
|
+
pendingBytes;
|
|
165
|
+
scheduled;
|
|
166
|
+
flushing;
|
|
167
|
+
maxBufferBytes;
|
|
168
|
+
flushDelayMs;
|
|
169
|
+
maxBatchBytes;
|
|
170
|
+
constructor(options) {
|
|
171
|
+
this.kind = options.kind;
|
|
172
|
+
this.minLevel = options.minLevel;
|
|
173
|
+
this.pending = [];
|
|
174
|
+
this.pendingBytes = 0;
|
|
175
|
+
this.scheduled = false;
|
|
176
|
+
this.flushing = false;
|
|
177
|
+
this.maxBufferBytes = DEFAULT_MAX_BUFFER_BYTES;
|
|
178
|
+
this.flushDelayMs = DEFAULT_FLUSH_DELAY_MS;
|
|
179
|
+
this.maxBatchBytes = DEFAULT_MAX_BATCH_BYTES;
|
|
180
|
+
}
|
|
181
|
+
enqueue(level, line) {
|
|
182
|
+
if (!shouldAccept(this.minLevel, level))
|
|
183
|
+
return;
|
|
184
|
+
const bytes = Buffer.byteLength(line);
|
|
185
|
+
if (this.pendingBytes + bytes > this.maxBufferBytes) {
|
|
186
|
+
// stream sink:优先丢 debug/info,保留 warn/error
|
|
187
|
+
if (LOG_LEVEL_NUM[level] < LOG_LEVEL_NUM.warn) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.pending.push(line);
|
|
192
|
+
this.pendingBytes += bytes;
|
|
193
|
+
this.scheduleFlush();
|
|
194
|
+
}
|
|
195
|
+
async flushNow() {
|
|
196
|
+
await this.flush();
|
|
197
|
+
}
|
|
198
|
+
async shutdown() {
|
|
199
|
+
await this.flush();
|
|
200
|
+
}
|
|
201
|
+
scheduleFlush() {
|
|
202
|
+
if (this.scheduled)
|
|
203
|
+
return;
|
|
204
|
+
this.scheduled = true;
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
void this.flush();
|
|
207
|
+
}, this.flushDelayMs);
|
|
208
|
+
}
|
|
209
|
+
getStream() {
|
|
210
|
+
return this.kind === "stderr" ? process.stderr : process.stdout;
|
|
211
|
+
}
|
|
212
|
+
async flush() {
|
|
213
|
+
if (this.flushing)
|
|
214
|
+
return;
|
|
215
|
+
this.scheduled = false;
|
|
216
|
+
this.flushing = true;
|
|
217
|
+
try {
|
|
218
|
+
const stream = this.getStream();
|
|
219
|
+
while (this.pending.length > 0) {
|
|
220
|
+
const batch = shiftBatchFromPending(this.pending, this.maxBatchBytes);
|
|
221
|
+
const chunk = batch.chunk;
|
|
222
|
+
const chunkBytes = Buffer.byteLength(chunk);
|
|
223
|
+
this.pendingBytes = this.pendingBytes - chunkBytes;
|
|
224
|
+
const ok = stream.write(chunk);
|
|
225
|
+
if (!ok) {
|
|
226
|
+
await new Promise((resolve) => {
|
|
227
|
+
stream.once("drain", () => resolve());
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
safeWriteStderrOnce(`[Logger] stream sink error: ${error?.message || error}`);
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
this.flushing = false;
|
|
237
|
+
if (this.pending.length > 0) {
|
|
238
|
+
this.scheduleFlush();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
class LogFileSink {
|
|
244
|
+
prefix;
|
|
245
|
+
minLevel;
|
|
246
|
+
maxFileBytes;
|
|
247
|
+
pending;
|
|
248
|
+
pendingBytes;
|
|
249
|
+
scheduled;
|
|
250
|
+
flushing;
|
|
251
|
+
maxBufferBytes;
|
|
252
|
+
flushDelayMs;
|
|
253
|
+
maxBatchBytes;
|
|
254
|
+
stream;
|
|
255
|
+
streamDate;
|
|
256
|
+
streamIndex;
|
|
257
|
+
streamSizeBytes;
|
|
258
|
+
disabled;
|
|
259
|
+
constructor(options) {
|
|
260
|
+
this.prefix = options.prefix;
|
|
261
|
+
this.minLevel = options.minLevel;
|
|
262
|
+
this.maxFileBytes = options.maxFileBytes;
|
|
263
|
+
this.pending = [];
|
|
264
|
+
this.pendingBytes = 0;
|
|
265
|
+
this.scheduled = false;
|
|
266
|
+
this.flushing = false;
|
|
267
|
+
this.maxBufferBytes = DEFAULT_MAX_BUFFER_BYTES;
|
|
268
|
+
this.flushDelayMs = DEFAULT_FLUSH_DELAY_MS;
|
|
269
|
+
this.maxBatchBytes = DEFAULT_MAX_BATCH_BYTES;
|
|
270
|
+
this.stream = null;
|
|
271
|
+
this.streamDate = "";
|
|
272
|
+
this.streamIndex = 0;
|
|
273
|
+
this.streamSizeBytes = 0;
|
|
274
|
+
this.disabled = false;
|
|
275
|
+
}
|
|
276
|
+
enqueue(level, line) {
|
|
277
|
+
if (this.disabled)
|
|
278
|
+
return;
|
|
279
|
+
if (!shouldAccept(this.minLevel, level))
|
|
280
|
+
return;
|
|
281
|
+
const bytes = Buffer.byteLength(line);
|
|
282
|
+
if (this.pendingBytes + bytes > this.maxBufferBytes) {
|
|
283
|
+
// 文件 sink:优先丢 debug/info,保留 warn/error
|
|
284
|
+
if (LOG_LEVEL_NUM[level] < LOG_LEVEL_NUM.warn) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.pending.push(line);
|
|
289
|
+
this.pendingBytes += bytes;
|
|
290
|
+
this.scheduleFlush();
|
|
291
|
+
}
|
|
292
|
+
async flushNow() {
|
|
293
|
+
await this.flush();
|
|
294
|
+
}
|
|
295
|
+
async shutdown() {
|
|
296
|
+
await this.flush();
|
|
297
|
+
await this.closeStream();
|
|
298
|
+
}
|
|
299
|
+
scheduleFlush() {
|
|
300
|
+
if (this.scheduled)
|
|
301
|
+
return;
|
|
302
|
+
this.scheduled = true;
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
void this.flush();
|
|
305
|
+
}, this.flushDelayMs);
|
|
306
|
+
}
|
|
307
|
+
async closeStream() {
|
|
308
|
+
if (!this.stream)
|
|
309
|
+
return;
|
|
310
|
+
const st = this.stream;
|
|
311
|
+
this.stream = null;
|
|
312
|
+
await new Promise((resolve) => {
|
|
313
|
+
try {
|
|
314
|
+
st.end(() => resolve());
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
resolve();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
getFilePath(date, index) {
|
|
322
|
+
const suffix = index > 0 ? `.${index}` : "";
|
|
323
|
+
const filename = `${this.prefix}.${date}${suffix}.log`;
|
|
324
|
+
return nodePathJoin(resolveLogDir(), filename);
|
|
325
|
+
}
|
|
326
|
+
async ensureStreamReady(nextChunkBytes) {
|
|
327
|
+
const date = formatLocalDate(new Date());
|
|
328
|
+
// 日期变化:切新文件
|
|
329
|
+
if (this.stream && this.streamDate && date !== this.streamDate) {
|
|
330
|
+
await this.closeStream();
|
|
331
|
+
this.streamDate = "";
|
|
332
|
+
this.streamIndex = 0;
|
|
333
|
+
this.streamSizeBytes = 0;
|
|
334
|
+
}
|
|
335
|
+
// 首次打开
|
|
336
|
+
if (!this.stream) {
|
|
337
|
+
const filePath = this.getFilePath(date, 0);
|
|
338
|
+
let size = 0;
|
|
339
|
+
try {
|
|
340
|
+
const st = await stat(filePath);
|
|
341
|
+
size = typeof st.size === "number" ? st.size : 0;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
size = 0;
|
|
345
|
+
}
|
|
346
|
+
this.streamDate = date;
|
|
347
|
+
this.streamIndex = 0;
|
|
348
|
+
this.streamSizeBytes = size;
|
|
349
|
+
try {
|
|
350
|
+
this.stream = createWriteStream(filePath, { flags: "a" });
|
|
351
|
+
this.stream.on("error", (error) => {
|
|
352
|
+
safeWriteStderrOnce(`[Logger] file sink error (${this.prefix}): ${error?.message || error}`);
|
|
353
|
+
this.disabled = true;
|
|
354
|
+
void this.closeStream();
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
safeWriteStderrOnce(`[Logger] createWriteStream failed (${this.prefix}): ${error?.message || error}`);
|
|
359
|
+
this.disabled = true;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// 大小滚动
|
|
364
|
+
if (this.stream && this.maxFileBytes > 0 && this.streamSizeBytes + nextChunkBytes > this.maxFileBytes) {
|
|
365
|
+
await this.closeStream();
|
|
366
|
+
this.streamIndex = this.streamIndex + 1;
|
|
367
|
+
const filePath = this.getFilePath(date, this.streamIndex);
|
|
368
|
+
this.streamDate = date;
|
|
369
|
+
this.streamSizeBytes = 0;
|
|
370
|
+
try {
|
|
371
|
+
this.stream = createWriteStream(filePath, { flags: "a" });
|
|
372
|
+
this.stream.on("error", (error) => {
|
|
373
|
+
safeWriteStderrOnce(`[Logger] file sink error (${this.prefix}): ${error?.message || error}`);
|
|
374
|
+
this.disabled = true;
|
|
375
|
+
void this.closeStream();
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
safeWriteStderrOnce(`[Logger] createWriteStream failed (${this.prefix}): ${error?.message || error}`);
|
|
380
|
+
this.disabled = true;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async flush() {
|
|
386
|
+
if (this.disabled) {
|
|
387
|
+
this.pending = [];
|
|
388
|
+
this.pendingBytes = 0;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (this.flushing)
|
|
392
|
+
return;
|
|
393
|
+
this.scheduled = false;
|
|
394
|
+
this.flushing = true;
|
|
395
|
+
try {
|
|
396
|
+
while (this.pending.length > 0 && !this.disabled) {
|
|
397
|
+
const batch = shiftBatchFromPending(this.pending, this.maxBatchBytes);
|
|
398
|
+
const chunk = batch.chunk;
|
|
399
|
+
const chunkBytes = Buffer.byteLength(chunk);
|
|
400
|
+
this.pendingBytes = this.pendingBytes - chunkBytes;
|
|
401
|
+
await this.ensureStreamReady(chunkBytes);
|
|
402
|
+
if (!this.stream) {
|
|
403
|
+
// 文件 sink 已被禁用或打开失败
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
const ok = this.stream.write(chunk);
|
|
407
|
+
if (!ok) {
|
|
408
|
+
await new Promise((resolve) => {
|
|
409
|
+
this.stream.once("drain", () => resolve());
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
this.streamSizeBytes = this.streamSizeBytes + chunkBytes;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
safeWriteStderrOnce(`[Logger] file sink flush error (${this.prefix}): ${error?.message || error}`);
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
this.flushing = false;
|
|
420
|
+
if (this.pending.length > 0 && !this.disabled) {
|
|
421
|
+
this.scheduleFlush();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
export async function flush() {
|
|
427
|
+
// 测试场景:mock logger 不需要 flush
|
|
428
|
+
if (mockInstance)
|
|
429
|
+
return;
|
|
430
|
+
const sinks = [];
|
|
431
|
+
if (appFileSink)
|
|
432
|
+
sinks.push({ flush: () => (appFileSink ? appFileSink.flushNow() : Promise.resolve()) });
|
|
433
|
+
if (errorFileSink)
|
|
434
|
+
sinks.push({ flush: () => (errorFileSink ? errorFileSink.flushNow() : Promise.resolve()) });
|
|
435
|
+
if (appConsoleSink)
|
|
436
|
+
sinks.push({ flush: () => (appConsoleSink ? appConsoleSink.flushNow() : Promise.resolve()) });
|
|
437
|
+
for (const item of sinks) {
|
|
438
|
+
try {
|
|
439
|
+
await item.flush();
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// ignore
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
export async function shutdown() {
|
|
447
|
+
// 测试场景:mock logger 不需要 shutdown
|
|
448
|
+
if (mockInstance)
|
|
449
|
+
return;
|
|
450
|
+
const sinks = [];
|
|
451
|
+
if (appFileSink)
|
|
452
|
+
sinks.push({ shutdown: () => (appFileSink ? appFileSink.shutdown() : Promise.resolve()) });
|
|
453
|
+
if (errorFileSink)
|
|
454
|
+
sinks.push({ shutdown: () => (errorFileSink ? errorFileSink.shutdown() : Promise.resolve()) });
|
|
455
|
+
if (appConsoleSink)
|
|
456
|
+
sinks.push({ shutdown: () => (appConsoleSink ? appConsoleSink.shutdown() : Promise.resolve()) });
|
|
457
|
+
for (const item of sinks) {
|
|
458
|
+
try {
|
|
459
|
+
await item.shutdown();
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// ignore
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
appFileSink = null;
|
|
466
|
+
errorFileSink = null;
|
|
467
|
+
appConsoleSink = null;
|
|
468
|
+
instance = null;
|
|
469
|
+
errorInstance = null;
|
|
470
|
+
}
|
|
46
471
|
function normalizePositiveInt(value, fallback, min, max) {
|
|
47
472
|
if (typeof value !== "number")
|
|
48
473
|
return fallback;
|
|
@@ -91,7 +516,7 @@ async function pruneOldLogFiles() {
|
|
|
91
516
|
continue;
|
|
92
517
|
const name = entry.name;
|
|
93
518
|
// 只处理本项目的日志文件前缀
|
|
94
|
-
const isTarget = name.startsWith("app.") || name.startsWith("
|
|
519
|
+
const isTarget = name.startsWith("app.") || name.startsWith("error.");
|
|
95
520
|
if (!isTarget)
|
|
96
521
|
continue;
|
|
97
522
|
const fullPath = nodePathJoin(dir, name);
|
|
@@ -121,12 +546,17 @@ async function pruneOldLogFiles() {
|
|
|
121
546
|
* 配置日志
|
|
122
547
|
*/
|
|
123
548
|
export function configure(cfg) {
|
|
124
|
-
|
|
549
|
+
// 旧实例可能仍持有文件句柄;这里异步关闭(不阻塞主流程)
|
|
550
|
+
void shutdown();
|
|
551
|
+
config = Object.assign({}, config, cfg);
|
|
125
552
|
instance = null;
|
|
126
|
-
slowInstance = null;
|
|
127
553
|
errorInstance = null;
|
|
128
554
|
didPruneOldLogFiles = false;
|
|
129
555
|
didEnsureLogDir = false;
|
|
556
|
+
didWarnIoError = false;
|
|
557
|
+
appFileSink = null;
|
|
558
|
+
errorFileSink = null;
|
|
559
|
+
appConsoleSink = null;
|
|
130
560
|
// 运行时清洗上限(可配置)
|
|
131
561
|
sanitizeDepthLimit = normalizePositiveInt(config.sanitizeDepth, DEFAULT_LOG_SANITIZE_DEPTH, 1, 10);
|
|
132
562
|
sanitizeNodesLimit = normalizePositiveInt(config.sanitizeNodes, DEFAULT_LOG_SANITIZE_NODES, 50, 20000);
|
|
@@ -136,46 +566,34 @@ export function configure(cfg) {
|
|
|
136
566
|
maxLogArrayItems = normalizePositiveInt(config.maxArrayItems, DEFAULT_LOG_ARRAY_ITEMS, 10, 5000);
|
|
137
567
|
// 仅支持数组配置:excludeFields?: string[]
|
|
138
568
|
const userPatterns = Array.isArray(config.excludeFields) ? config.excludeFields : [];
|
|
139
|
-
const patterns = [
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.
|
|
569
|
+
const patterns = [];
|
|
570
|
+
for (const item of BUILTIN_SENSITIVE_KEYS) {
|
|
571
|
+
const trimmed = String(item).trim();
|
|
572
|
+
if (trimmed.length > 0)
|
|
573
|
+
patterns.push(trimmed.toLowerCase());
|
|
574
|
+
}
|
|
575
|
+
for (const item of userPatterns) {
|
|
576
|
+
const trimmed = String(item).trim();
|
|
577
|
+
if (trimmed.length > 0)
|
|
578
|
+
patterns.push(trimmed.toLowerCase());
|
|
579
|
+
}
|
|
143
580
|
const exactSet = new Set();
|
|
144
|
-
const suffixMatchers = [];
|
|
145
|
-
const prefixMatchers = [];
|
|
146
581
|
const containsMatchers = [];
|
|
147
582
|
for (const pat of patterns) {
|
|
148
|
-
//
|
|
149
|
-
// -
|
|
150
|
-
// -
|
|
151
|
-
|
|
152
|
-
// - 无 * -> 精确匹配(建议用 *x* 显式开启模糊匹配)
|
|
153
|
-
const hasStar = pat.includes("*");
|
|
154
|
-
if (!hasStar) {
|
|
583
|
+
// 精简策略:
|
|
584
|
+
// - 无 *:精确匹配
|
|
585
|
+
// - 有 *:统一按“包含匹配”处理(*x*、x*、*x、a*b 都视为包含 core)
|
|
586
|
+
if (!pat.includes("*")) {
|
|
155
587
|
exactSet.add(pat);
|
|
156
588
|
continue;
|
|
157
589
|
}
|
|
158
|
-
const
|
|
159
|
-
const startsStar = trimmed.startsWith("*");
|
|
160
|
-
const endsStar = trimmed.endsWith("*");
|
|
161
|
-
const core = trimmed.replace(/^\*+|\*+$/g, "");
|
|
590
|
+
const core = pat.replace(/\*+/g, "").trim();
|
|
162
591
|
if (!core) {
|
|
163
592
|
continue;
|
|
164
593
|
}
|
|
165
|
-
if (startsStar && !endsStar) {
|
|
166
|
-
suffixMatchers.push(core);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (!startsStar && endsStar) {
|
|
170
|
-
prefixMatchers.push(core);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
// *core* 或类似 a*b:都降级为包含匹配
|
|
174
594
|
containsMatchers.push(core);
|
|
175
595
|
}
|
|
176
596
|
sensitiveKeySet = exactSet;
|
|
177
|
-
sensitiveSuffixMatchers = suffixMatchers;
|
|
178
|
-
sensitivePrefixMatchers = prefixMatchers;
|
|
179
597
|
sensitiveContainsMatchers = containsMatchers;
|
|
180
598
|
// 预编译包含匹配:减少每次 isSensitiveKey 的循环开销
|
|
181
599
|
// 注意:patterns 已全部 lowerCase,因此 regex 不需要 i 标志
|
|
@@ -194,13 +612,13 @@ export function configure(cfg) {
|
|
|
194
612
|
}
|
|
195
613
|
/**
|
|
196
614
|
* 设置 Mock Logger(用于测试)
|
|
197
|
-
* @param mock - Mock
|
|
615
|
+
* @param mock - Mock Logger 实例(形如 {info/warn/error/debug}),传 null 清除 mock
|
|
198
616
|
*/
|
|
199
617
|
export function setMockLogger(mock) {
|
|
200
618
|
mockInstance = mock;
|
|
201
619
|
}
|
|
202
620
|
/**
|
|
203
|
-
* 获取
|
|
621
|
+
* 获取 Logger 实例(延迟初始化)
|
|
204
622
|
*/
|
|
205
623
|
export function getLogger() {
|
|
206
624
|
// 优先返回 mock 实例(用于测试)
|
|
@@ -209,108 +627,178 @@ export function getLogger() {
|
|
|
209
627
|
if (instance)
|
|
210
628
|
return instance;
|
|
211
629
|
ensureLogDirExists();
|
|
630
|
+
installGracefulExitHooks();
|
|
212
631
|
// 启动时清理过期日志(异步,不阻塞初始化)
|
|
213
632
|
void pruneOldLogFiles();
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
file: join(resolveLogDir(), "app"),
|
|
222
|
-
frequency: "daily",
|
|
223
|
-
size: `${config.maxSize || 10}m`,
|
|
224
|
-
mkdir: true,
|
|
225
|
-
dateFormat: "yyyy-MM-dd"
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
// 控制台输出
|
|
229
|
-
if (config.console === 1) {
|
|
230
|
-
targets.push({
|
|
231
|
-
target: "pino/file",
|
|
232
|
-
level: level,
|
|
233
|
-
options: { destination: 1 }
|
|
234
|
-
});
|
|
633
|
+
const minLevel = normalizeLogLevelName();
|
|
634
|
+
const maxFileBytes = (typeof config.maxSize === "number" && config.maxSize > 0 ? config.maxSize : 10) * 1024 * 1024;
|
|
635
|
+
if (!appFileSink) {
|
|
636
|
+
appFileSink = new LogFileSink({ prefix: "app", minLevel: minLevel, maxFileBytes: maxFileBytes });
|
|
637
|
+
}
|
|
638
|
+
if (config.console === 1 && !appConsoleSink) {
|
|
639
|
+
appConsoleSink = new LogStreamSink({ kind: "stdout", minLevel: minLevel });
|
|
235
640
|
}
|
|
236
|
-
instance =
|
|
237
|
-
level: level,
|
|
238
|
-
transport: { targets: targets }
|
|
239
|
-
});
|
|
641
|
+
instance = createSinkLogger({ kind: "app", minLevel: minLevel, fileSink: appFileSink, consoleSink: config.console === 1 ? appConsoleSink : null });
|
|
240
642
|
return instance;
|
|
241
643
|
}
|
|
242
|
-
function getSlowLogger() {
|
|
243
|
-
if (mockInstance)
|
|
244
|
-
return mockInstance;
|
|
245
|
-
if (slowInstance)
|
|
246
|
-
return slowInstance;
|
|
247
|
-
ensureLogDirExists();
|
|
248
|
-
void pruneOldLogFiles();
|
|
249
|
-
const level = config.debug === 1 ? "debug" : "info";
|
|
250
|
-
slowInstance = pino({
|
|
251
|
-
level: level,
|
|
252
|
-
transport: {
|
|
253
|
-
targets: [
|
|
254
|
-
{
|
|
255
|
-
target: "pino-roll",
|
|
256
|
-
level: level,
|
|
257
|
-
options: {
|
|
258
|
-
file: join(resolveLogDir(), "slow"),
|
|
259
|
-
// 只按大小分割(frequency 默认不启用)
|
|
260
|
-
size: `${config.maxSize || 10}m`,
|
|
261
|
-
mkdir: true
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
]
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
return slowInstance;
|
|
268
|
-
}
|
|
269
644
|
function getErrorLogger() {
|
|
270
645
|
if (mockInstance)
|
|
271
646
|
return mockInstance;
|
|
272
647
|
if (errorInstance)
|
|
273
648
|
return errorInstance;
|
|
274
649
|
ensureLogDirExists();
|
|
650
|
+
installGracefulExitHooks();
|
|
275
651
|
void pruneOldLogFiles();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
target: "pino-roll",
|
|
283
|
-
level: "error",
|
|
284
|
-
options: {
|
|
285
|
-
file: join(resolveLogDir(), "error"),
|
|
286
|
-
// 只按大小分割(frequency 默认不启用)
|
|
287
|
-
size: `${config.maxSize || 10}m`,
|
|
288
|
-
mkdir: true
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
]
|
|
292
|
-
}
|
|
293
|
-
});
|
|
652
|
+
const maxFileBytes = (typeof config.maxSize === "number" && config.maxSize > 0 ? config.maxSize : 10) * 1024 * 1024;
|
|
653
|
+
if (!errorFileSink) {
|
|
654
|
+
// error logger:固定 minLevel=error
|
|
655
|
+
errorFileSink = new LogFileSink({ prefix: "error", minLevel: "error", maxFileBytes: maxFileBytes });
|
|
656
|
+
}
|
|
657
|
+
errorInstance = createSinkLogger({ kind: "error", minLevel: "error", fileSink: errorFileSink, consoleSink: null });
|
|
294
658
|
return errorInstance;
|
|
295
659
|
}
|
|
660
|
+
function formatExtrasToMsg(extras) {
|
|
661
|
+
if (!extras || extras.length === 0)
|
|
662
|
+
return "";
|
|
663
|
+
const parts = [];
|
|
664
|
+
for (const item of extras) {
|
|
665
|
+
if (item === null) {
|
|
666
|
+
parts.push("null");
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (item === undefined) {
|
|
670
|
+
parts.push("undefined");
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (typeof item === "string") {
|
|
674
|
+
parts.push(item);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (typeof item === "number" || typeof item === "boolean" || typeof item === "bigint") {
|
|
678
|
+
parts.push(String(item));
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (item instanceof Error) {
|
|
682
|
+
parts.push(item.stack || item.message || item.name);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (isPlainObject(item) || Array.isArray(item)) {
|
|
686
|
+
try {
|
|
687
|
+
parts.push(JSON.stringify(item));
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
parts.push("[Unserializable]");
|
|
691
|
+
}
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
parts.push(String(item));
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
parts.push("[Unknown]");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return parts.join(" ");
|
|
702
|
+
}
|
|
703
|
+
function buildLogLine(level, args) {
|
|
704
|
+
const time = Date.now();
|
|
705
|
+
const base = {
|
|
706
|
+
level: LOG_LEVEL_NUM[level],
|
|
707
|
+
time: time,
|
|
708
|
+
pid: process.pid,
|
|
709
|
+
hostname: HOSTNAME
|
|
710
|
+
};
|
|
711
|
+
if (!args || args.length === 0) {
|
|
712
|
+
base.msg = "";
|
|
713
|
+
return `${JSON.stringify(base)}\n`;
|
|
714
|
+
}
|
|
715
|
+
const first = args[0];
|
|
716
|
+
const second = args.length > 1 ? args[1] : undefined;
|
|
717
|
+
// 兼容:logger.(level)(obj, msg?, ...)
|
|
718
|
+
if (isPlainObject(first)) {
|
|
719
|
+
for (const [k, v] of Object.entries(first)) {
|
|
720
|
+
base[k] = v;
|
|
721
|
+
}
|
|
722
|
+
if (typeof second === "string") {
|
|
723
|
+
const extraMsg = formatExtrasToMsg(args.slice(2));
|
|
724
|
+
base.msg = extraMsg ? `${second} ${extraMsg}` : second;
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
const extraMsg = formatExtrasToMsg(args.slice(1));
|
|
728
|
+
base.msg = extraMsg;
|
|
729
|
+
}
|
|
730
|
+
return `${safeJsonStringify(base)}\n`;
|
|
731
|
+
}
|
|
732
|
+
// 兼容:logger.(level)(err, msg?)
|
|
733
|
+
if (first instanceof Error) {
|
|
734
|
+
base.err = sanitizeErrorValue(first, {});
|
|
735
|
+
if (typeof second === "string") {
|
|
736
|
+
const extraMsg = formatExtrasToMsg(args.slice(2));
|
|
737
|
+
base.msg = extraMsg ? `${second} ${extraMsg}` : second;
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
base.msg = formatExtrasToMsg(args.slice(1));
|
|
741
|
+
}
|
|
742
|
+
return `${safeJsonStringify(base)}\n`;
|
|
743
|
+
}
|
|
744
|
+
// 兼容:logger.(level)(msg, ...)
|
|
745
|
+
if (typeof first === "string") {
|
|
746
|
+
const extraMsg = formatExtrasToMsg(args.slice(1));
|
|
747
|
+
base.msg = extraMsg ? `${first} ${extraMsg}` : first;
|
|
748
|
+
return `${safeJsonStringify(base)}\n`;
|
|
749
|
+
}
|
|
750
|
+
// 兜底
|
|
751
|
+
base.msg = formatExtrasToMsg(args);
|
|
752
|
+
return `${safeJsonStringify(base)}\n`;
|
|
753
|
+
}
|
|
754
|
+
function safeJsonStringify(obj) {
|
|
755
|
+
try {
|
|
756
|
+
return JSON.stringify(obj);
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
try {
|
|
760
|
+
return JSON.stringify({ level: obj.level, time: obj.time, pid: obj.pid, hostname: obj.hostname, msg: "[Unserializable log record]" });
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return '{"msg":"[Unserializable log record]"}';
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function createSinkLogger(options) {
|
|
768
|
+
const minLevel = options.minLevel;
|
|
769
|
+
const write = (level, args) => {
|
|
770
|
+
if (!shouldAccept(minLevel, level))
|
|
771
|
+
return;
|
|
772
|
+
const line = buildLogLine(level, args);
|
|
773
|
+
options.fileSink.enqueue(level, line);
|
|
774
|
+
if (options.consoleSink) {
|
|
775
|
+
options.consoleSink.enqueue(level, line);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
return {
|
|
779
|
+
info(...args) {
|
|
780
|
+
write("info", args);
|
|
781
|
+
},
|
|
782
|
+
warn(...args) {
|
|
783
|
+
write("warn", args);
|
|
784
|
+
},
|
|
785
|
+
error(...args) {
|
|
786
|
+
write("error", args);
|
|
787
|
+
},
|
|
788
|
+
debug(...args) {
|
|
789
|
+
write("debug", args);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
296
793
|
function truncateString(val, stats) {
|
|
297
794
|
if (val.length <= maxLogStringLen)
|
|
298
795
|
return val;
|
|
299
|
-
stats.truncatedStrings = (stats.truncatedStrings || 0) + 1;
|
|
300
796
|
return val.slice(0, maxLogStringLen);
|
|
301
797
|
}
|
|
302
798
|
function isSensitiveKey(key) {
|
|
303
799
|
const lower = String(key).toLowerCase();
|
|
304
800
|
if (sensitiveKeySet.has(lower))
|
|
305
801
|
return true;
|
|
306
|
-
for (const suffix of sensitiveSuffixMatchers) {
|
|
307
|
-
if (lower.endsWith(suffix))
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
for (const prefix of sensitivePrefixMatchers) {
|
|
311
|
-
if (lower.startsWith(prefix))
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
802
|
if (sensitiveContainsRegex) {
|
|
315
803
|
return sensitiveContainsRegex.test(lower);
|
|
316
804
|
}
|
|
@@ -341,7 +829,6 @@ function safeToStringMasked(val, visited, stats) {
|
|
|
341
829
|
}
|
|
342
830
|
if (val && typeof val === "object") {
|
|
343
831
|
if (visited.has(val)) {
|
|
344
|
-
stats.circularRefs = (stats.circularRefs || 0) + 1;
|
|
345
832
|
return "[Circular]";
|
|
346
833
|
}
|
|
347
834
|
}
|
|
@@ -350,12 +837,10 @@ function safeToStringMasked(val, visited, stats) {
|
|
|
350
837
|
const replacer = (k, v) => {
|
|
351
838
|
// JSON.stringify 的根节点 key 为空字符串
|
|
352
839
|
if (k && isSensitiveKey(k)) {
|
|
353
|
-
stats.maskedKeys = (stats.maskedKeys || 0) + 1;
|
|
354
840
|
return "[MASKED]";
|
|
355
841
|
}
|
|
356
842
|
if (v && typeof v === "object") {
|
|
357
843
|
if (localVisited.has(v)) {
|
|
358
|
-
stats.circularRefs = (stats.circularRefs || 0) + 1;
|
|
359
844
|
return "[Circular]";
|
|
360
845
|
}
|
|
361
846
|
localVisited.add(v);
|
|
@@ -384,7 +869,6 @@ function sanitizeErrorValue(err, stats) {
|
|
|
384
869
|
return errObj;
|
|
385
870
|
}
|
|
386
871
|
function stringifyPreview(val, visited, stats) {
|
|
387
|
-
stats.valuesStringified = (stats.valuesStringified || 0) + 1;
|
|
388
872
|
const str = safeToStringMasked(val, visited, stats);
|
|
389
873
|
return truncateString(str, stats);
|
|
390
874
|
}
|
|
@@ -410,7 +894,6 @@ function sanitizeValueLimited(val, visited, stats) {
|
|
|
410
894
|
}
|
|
411
895
|
// 防环(根节点)
|
|
412
896
|
if (visited.has(val)) {
|
|
413
|
-
stats.circularRefs = (stats.circularRefs || 0) + 1;
|
|
414
897
|
return "[Circular]";
|
|
415
898
|
}
|
|
416
899
|
visited.add(val);
|
|
@@ -442,18 +925,15 @@ function sanitizeValueLimited(val, visited, stats) {
|
|
|
442
925
|
}
|
|
443
926
|
// 深度/节点数上限:超出则降级为字符串预览
|
|
444
927
|
if (depth >= sanitizeDepthLimit) {
|
|
445
|
-
stats.depthLimited = (stats.depthLimited || 0) + 1;
|
|
446
928
|
dst[key] = stringifyPreview(child, visited, stats);
|
|
447
929
|
return;
|
|
448
930
|
}
|
|
449
931
|
if (nodes >= sanitizeNodesLimit) {
|
|
450
|
-
stats.nodesLimited = (stats.nodesLimited || 0) + 1;
|
|
451
932
|
dst[key] = stringifyPreview(child, visited, stats);
|
|
452
933
|
return;
|
|
453
934
|
}
|
|
454
935
|
// 防环
|
|
455
936
|
if (visited.has(child)) {
|
|
456
|
-
stats.circularRefs = (stats.circularRefs || 0) + 1;
|
|
457
937
|
dst[key] = "[Circular]";
|
|
458
938
|
return;
|
|
459
939
|
}
|
|
@@ -467,7 +947,6 @@ function sanitizeValueLimited(val, visited, stats) {
|
|
|
467
947
|
nodes = nodes + 1;
|
|
468
948
|
if (nodes > sanitizeNodesLimit) {
|
|
469
949
|
// 超出节点上限:不再深入(已入栈的节点会被忽略,留空结构由上层兜底预览)。
|
|
470
|
-
stats.nodesLimited = (stats.nodesLimited || 0) + 1;
|
|
471
950
|
break;
|
|
472
951
|
}
|
|
473
952
|
if (Array.isArray(frame.src)) {
|
|
@@ -478,8 +957,7 @@ function sanitizeValueLimited(val, visited, stats) {
|
|
|
478
957
|
tryAssign(frame.dst, i, arr[i], frame.depth);
|
|
479
958
|
}
|
|
480
959
|
if (len > maxLogArrayItems) {
|
|
481
|
-
|
|
482
|
-
stats.arrayItemsOmitted = (stats.arrayItemsOmitted || 0) + (len - maxLogArrayItems);
|
|
960
|
+
// ignore omitted items
|
|
483
961
|
}
|
|
484
962
|
continue;
|
|
485
963
|
}
|
|
@@ -491,15 +969,13 @@ function sanitizeValueLimited(val, visited, stats) {
|
|
|
491
969
|
const key = entries[i][0];
|
|
492
970
|
const child = entries[i][1];
|
|
493
971
|
if (isSensitiveKey(key)) {
|
|
494
|
-
stats.maskedKeys = (stats.maskedKeys || 0) + 1;
|
|
495
972
|
frame.dst[key] = "[MASKED]";
|
|
496
973
|
continue;
|
|
497
974
|
}
|
|
498
975
|
tryAssign(frame.dst, key, child, frame.depth);
|
|
499
976
|
}
|
|
500
977
|
if (len > sanitizeObjectKeysLimit) {
|
|
501
|
-
|
|
502
|
-
stats.objectKeysOmitted = (stats.objectKeysOmitted || 0) + (len - sanitizeObjectKeysLimit);
|
|
978
|
+
// ignore omitted keys
|
|
503
979
|
}
|
|
504
980
|
continue;
|
|
505
981
|
}
|
|
@@ -507,46 +983,18 @@ function sanitizeValueLimited(val, visited, stats) {
|
|
|
507
983
|
}
|
|
508
984
|
return rootOut;
|
|
509
985
|
}
|
|
510
|
-
function sanitizeTopValue(val, visited
|
|
511
|
-
return sanitizeValueLimited(val, visited,
|
|
986
|
+
function sanitizeTopValue(val, visited) {
|
|
987
|
+
return sanitizeValueLimited(val, visited, {});
|
|
512
988
|
}
|
|
513
989
|
function sanitizeLogObject(obj) {
|
|
514
990
|
const visited = new WeakSet();
|
|
515
|
-
const stats = {
|
|
516
|
-
maskedKeys: 0,
|
|
517
|
-
truncatedStrings: 0,
|
|
518
|
-
arraysTruncated: 0,
|
|
519
|
-
arrayItemsOmitted: 0,
|
|
520
|
-
valuesStringified: 0,
|
|
521
|
-
circularRefs: 0,
|
|
522
|
-
depthLimited: 0,
|
|
523
|
-
nodesLimited: 0,
|
|
524
|
-
objectKeysLimited: 0,
|
|
525
|
-
objectKeysOmitted: 0
|
|
526
|
-
};
|
|
527
991
|
const out = {};
|
|
528
992
|
for (const [key, val] of Object.entries(obj)) {
|
|
529
993
|
if (isSensitiveKey(key)) {
|
|
530
|
-
stats.maskedKeys = stats.maskedKeys + 1;
|
|
531
994
|
out[key] = "[MASKED]";
|
|
532
995
|
continue;
|
|
533
996
|
}
|
|
534
|
-
out[key] = sanitizeTopValue(val, visited
|
|
535
|
-
}
|
|
536
|
-
const hasChanges = stats.maskedKeys > 0 || stats.truncatedStrings > 0 || stats.arraysTruncated > 0 || stats.arrayItemsOmitted > 0 || stats.valuesStringified > 0 || stats.circularRefs > 0 || stats.depthLimited > 0 || stats.nodesLimited > 0 || stats.objectKeysLimited > 0 || stats.objectKeysOmitted > 0;
|
|
537
|
-
if (hasChanges) {
|
|
538
|
-
out.logTrimStats = {
|
|
539
|
-
maskedKeys: stats.maskedKeys,
|
|
540
|
-
truncatedStrings: stats.truncatedStrings,
|
|
541
|
-
arraysTruncated: stats.arraysTruncated,
|
|
542
|
-
arrayItemsOmitted: stats.arrayItemsOmitted,
|
|
543
|
-
valuesStringified: stats.valuesStringified,
|
|
544
|
-
circularRefs: stats.circularRefs,
|
|
545
|
-
depthLimited: stats.depthLimited,
|
|
546
|
-
nodesLimited: stats.nodesLimited,
|
|
547
|
-
objectKeysLimited: stats.objectKeysLimited,
|
|
548
|
-
objectKeysOmitted: stats.objectKeysOmitted
|
|
549
|
-
};
|
|
997
|
+
out[key] = sanitizeTopValue(val, visited);
|
|
550
998
|
}
|
|
551
999
|
return out;
|
|
552
1000
|
}
|
|
@@ -621,25 +1069,6 @@ function withRequestMeta(args) {
|
|
|
621
1069
|
}
|
|
622
1070
|
return args;
|
|
623
1071
|
}
|
|
624
|
-
function shouldMirrorToSlow(args) {
|
|
625
|
-
// 测试场景:启用 mock 时不做镜像,避免调用次数翻倍
|
|
626
|
-
if (mockInstance)
|
|
627
|
-
return false;
|
|
628
|
-
if (!args || args.length === 0)
|
|
629
|
-
return false;
|
|
630
|
-
const first = args[0];
|
|
631
|
-
if (!isPlainObject(first))
|
|
632
|
-
return false;
|
|
633
|
-
// 优先使用显式标记:event=slow
|
|
634
|
-
const event = first.event;
|
|
635
|
-
if (event === "slow")
|
|
636
|
-
return true;
|
|
637
|
-
// 兼容旧写法:仅通过 message emoji 判断(尽量少用)
|
|
638
|
-
const msg = args.length > 1 ? args[1] : undefined;
|
|
639
|
-
if (typeof msg === "string" && msg.includes("🐌"))
|
|
640
|
-
return true;
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
643
1072
|
function withRequestMetaTyped(args) {
|
|
644
1073
|
// 复用现有逻辑(保持行为一致),只收敛类型
|
|
645
1074
|
return withRequestMeta(args);
|
|
@@ -659,12 +1088,6 @@ export const Logger = {
|
|
|
659
1088
|
finalArgs[0] = sanitizeLogObject(finalArgs[0]);
|
|
660
1089
|
}
|
|
661
1090
|
const ret = logger.info.apply(logger, finalArgs);
|
|
662
|
-
if (mockInstance)
|
|
663
|
-
return ret;
|
|
664
|
-
if (shouldMirrorToSlow(finalArgs)) {
|
|
665
|
-
const slowLogger = getSlowLogger();
|
|
666
|
-
slowLogger.info.apply(slowLogger, finalArgs);
|
|
667
|
-
}
|
|
668
1091
|
return ret;
|
|
669
1092
|
},
|
|
670
1093
|
warn(...args) {
|
|
@@ -678,12 +1101,6 @@ export const Logger = {
|
|
|
678
1101
|
finalArgs[0] = sanitizeLogObject(finalArgs[0]);
|
|
679
1102
|
}
|
|
680
1103
|
const ret = logger.warn.apply(logger, finalArgs);
|
|
681
|
-
if (mockInstance)
|
|
682
|
-
return ret;
|
|
683
|
-
if (shouldMirrorToSlow(finalArgs)) {
|
|
684
|
-
const slowLogger = getSlowLogger();
|
|
685
|
-
slowLogger.warn.apply(slowLogger, finalArgs);
|
|
686
|
-
}
|
|
687
1104
|
return ret;
|
|
688
1105
|
},
|
|
689
1106
|
error(...args) {
|
|
@@ -703,11 +1120,6 @@ export const Logger = {
|
|
|
703
1120
|
// error 专属文件:始终镜像一份
|
|
704
1121
|
const errorLogger = getErrorLogger();
|
|
705
1122
|
errorLogger.error.apply(errorLogger, finalArgs);
|
|
706
|
-
// error 同时也属于 slow?一般不会,但允许显式 event=slow
|
|
707
|
-
if (shouldMirrorToSlow(finalArgs)) {
|
|
708
|
-
const slowLogger = getSlowLogger();
|
|
709
|
-
slowLogger.error.apply(slowLogger, finalArgs);
|
|
710
|
-
}
|
|
711
1123
|
return ret;
|
|
712
1124
|
},
|
|
713
1125
|
debug(...args) {
|
|
@@ -721,14 +1133,12 @@ export const Logger = {
|
|
|
721
1133
|
finalArgs[0] = sanitizeLogObject(finalArgs[0]);
|
|
722
1134
|
}
|
|
723
1135
|
const ret = logger.debug.apply(logger, finalArgs);
|
|
724
|
-
if (mockInstance)
|
|
725
|
-
return ret;
|
|
726
|
-
if (shouldMirrorToSlow(finalArgs)) {
|
|
727
|
-
const slowLogger = getSlowLogger();
|
|
728
|
-
slowLogger.debug.apply(slowLogger, finalArgs);
|
|
729
|
-
}
|
|
730
1136
|
return ret;
|
|
731
1137
|
},
|
|
1138
|
+
async flush() {
|
|
1139
|
+
await flush();
|
|
1140
|
+
},
|
|
732
1141
|
configure: configure,
|
|
733
|
-
setMock: setMockLogger
|
|
1142
|
+
setMock: setMockLogger,
|
|
1143
|
+
shutdown: shutdown
|
|
734
1144
|
};
|