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.
Files changed (67) hide show
  1. package/dist/befly.config.js +1 -1
  2. package/dist/befly.js +810 -896
  3. package/dist/befly.min.js +15 -18
  4. package/dist/checks/checkApi.js +14 -17
  5. package/dist/checks/checkHook.js +55 -24
  6. package/dist/checks/checkMenu.js +10 -10
  7. package/dist/checks/checkPlugin.js +55 -24
  8. package/dist/checks/checkTable.js +29 -28
  9. package/dist/hooks/auth.d.ts +3 -7
  10. package/dist/hooks/auth.js +2 -1
  11. package/dist/hooks/cors.d.ts +3 -7
  12. package/dist/hooks/cors.js +3 -2
  13. package/dist/hooks/parser.d.ts +3 -7
  14. package/dist/hooks/parser.js +2 -1
  15. package/dist/hooks/permission.d.ts +3 -7
  16. package/dist/hooks/permission.js +5 -3
  17. package/dist/hooks/validator.d.ts +3 -7
  18. package/dist/hooks/validator.js +2 -1
  19. package/dist/index.js +2 -2
  20. package/dist/lib/cacheHelper.js +8 -8
  21. package/dist/lib/connect.js +5 -5
  22. package/dist/lib/dbHelper.js +6 -5
  23. package/dist/lib/logger.d.ts +16 -17
  24. package/dist/lib/logger.js +335 -749
  25. package/dist/lib/redisHelper.js +27 -26
  26. package/dist/loader/loadApis.js +1 -1
  27. package/dist/loader/loadPlugins.js +1 -1
  28. package/dist/plugins/cache.d.ts +3 -9
  29. package/dist/plugins/cache.js +2 -1
  30. package/dist/plugins/cipher.d.ts +3 -8
  31. package/dist/plugins/cipher.js +2 -1
  32. package/dist/plugins/config.d.ts +3 -12
  33. package/dist/plugins/config.js +2 -1
  34. package/dist/plugins/db.d.ts +3 -9
  35. package/dist/plugins/db.js +3 -2
  36. package/dist/plugins/jwt.d.ts +3 -9
  37. package/dist/plugins/jwt.js +2 -1
  38. package/dist/plugins/logger.d.ts +3 -25
  39. package/dist/plugins/logger.js +2 -1
  40. package/dist/plugins/redis.d.ts +3 -9
  41. package/dist/plugins/redis.js +3 -2
  42. package/dist/plugins/tool.d.ts +3 -11
  43. package/dist/plugins/tool.js +2 -1
  44. package/dist/router/api.js +3 -2
  45. package/dist/router/static.js +1 -1
  46. package/dist/sync/syncApi.js +3 -3
  47. package/dist/sync/syncMenu.js +3 -2
  48. package/dist/sync/syncTable.js +2 -2
  49. package/dist/types/hook.d.ts +13 -0
  50. package/dist/types/hook.js +13 -0
  51. package/dist/types/logger.d.ts +20 -6
  52. package/dist/types/plugin.d.ts +12 -1
  53. package/dist/types/plugin.js +12 -1
  54. package/dist/utils/formatYmdHms.d.ts +1 -0
  55. package/dist/utils/formatYmdHms.js +20 -0
  56. package/dist/utils/importDefault.js +1 -1
  57. package/dist/utils/loadMenuConfigs.js +7 -6
  58. package/dist/utils/loggerUtils.d.ts +18 -0
  59. package/dist/utils/loggerUtils.js +167 -0
  60. package/dist/utils/response.js +6 -4
  61. package/dist/utils/scanCoreBuiltins.js +4 -1
  62. package/dist/utils/scanFiles.d.ts +2 -0
  63. package/dist/utils/scanFiles.js +5 -2
  64. package/dist/utils/sortModules.js +8 -7
  65. package/dist/utils/util.d.ts +2 -0
  66. package/dist/utils/util.js +16 -0
  67. package/package.json +2 -2
@@ -2,35 +2,25 @@
2
2
  * 日志系统 - Bun 环境自定义实现(替换 pino / pino-roll)
3
3
  */
4
4
  import { createWriteStream, existsSync, mkdirSync } from "node:fs";
5
- import { readdir, stat, unlink } from "node:fs/promises";
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 { isPlainObject } from "../utils/util";
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 sensitiveKeySet = new Set();
32
- let sensitiveContainsMatchers = [];
33
- let sensitiveContainsRegex = null;
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: 10
42
+ maxSize: 20
65
43
  };
66
- function formatLocalDate(date) {
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 += nextBytes;
62
+ bytes = bytes + nextBytes;
103
63
  pending.shift();
104
64
  }
105
65
  return { chunk: parts.join(""), bytes: bytes };
106
66
  }
107
- class LogStreamSink {
108
- kind;
109
- minLevel;
67
+ class BufferedSink {
110
68
  pending;
111
69
  pendingBytes;
112
- scheduled;
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.scheduled = false;
80
+ this.scheduledTimer = null;
123
81
  this.flushing = false;
124
- this.maxBufferBytes = DEFAULT_MAX_BUFFER_BYTES;
125
- this.flushDelayMs = DEFAULT_FLUSH_DELAY_MS;
126
- this.maxBatchBytes = DEFAULT_MAX_BATCH_BYTES;
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
- enqueue(level, line) {
129
- if (!shouldAccept(this.minLevel, level))
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
- // stream sink:优先丢 debug/info,保留 warn/error
134
- if (LOG_LEVEL_NUM[level] < LOG_LEVEL_NUM.warn) {
135
- return;
136
- }
106
+ // buffer 满:统一丢弃新日志(不区分 level)
107
+ return;
137
108
  }
138
109
  this.pending.push(line);
139
- this.pendingBytes += bytes;
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
- scheduleFlush() {
149
- if (this.scheduled)
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 = stream.write(chunk);
135
+ const ok = await this.writeChunk(chunk, chunkBytes);
172
136
  if (!ok) {
173
- await new Promise((resolve) => {
174
- stream.once("drain", () => resolve());
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
- pending;
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(level, line) {
240
+ enqueue(line) {
224
241
  if (this.disabled)
225
242
  return;
226
- if (!shouldAccept(this.minLevel, level))
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.flush();
246
+ await this.buffer.flushNow();
241
247
  }
242
248
  async shutdown() {
243
- await this.flush();
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 = formatLocalDate(new 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
- try {
297
- this.stream = createWriteStream(filePath, { flags: "a" });
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
- try {
318
- this.stream = createWriteStream(filePath, { flags: "a" });
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 (appFileSink)
399
- sinks.push({ shutdown: () => (appFileSink ? appFileSink.shutdown() : Promise.resolve()) });
400
- if (errorFileSink)
401
- sinks.push({ shutdown: () => (errorFileSink ? errorFileSink.shutdown() : Promise.resolve()) });
402
- if (appConsoleSink)
403
- sinks.push({ shutdown: () => (appConsoleSink ? appConsoleSink.shutdown() : Promise.resolve()) });
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 = null;
413
- errorFileSink = null;
414
- appConsoleSink = null;
415
- instance = null;
416
- errorInstance = null;
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
- didEnsureLogDir = false;
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
- config = Object.assign({}, config, cfg);
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
- sanitizeDepthLimit = normalizePositiveInt(config.sanitizeDepth, DEFAULT_LOG_SANITIZE_DEPTH, 1, 10);
509
- sanitizeNodesLimit = normalizePositiveInt(config.sanitizeNodes, DEFAULT_LOG_SANITIZE_NODES, 50, 20000);
510
- sanitizeObjectKeysLimit = normalizePositiveInt(config.sanitizeObjectKeys, DEFAULT_LOG_OBJECT_KEYS, 10, 5000);
511
- // 运行时截断上限(可配置)
512
- maxLogStringLen = normalizePositiveInt(config.maxStringLen, DEFAULT_LOG_STRING_LEN, 20, 200000);
513
- maxLogArrayItems = normalizePositiveInt(config.maxArrayItems, DEFAULT_LOG_ARRAY_ITEMS, 10, 5000);
514
- // 仅支持数组配置:excludeFields?: string[]
515
- const userPatterns = Array.isArray(config.excludeFields) ? config.excludeFields : [];
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 (instance)
575
- return instance;
576
- ensureLogDirExists();
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
- if (config.console === 1 && !appConsoleSink) {
585
- appConsoleSink = new LogStreamSink({ kind: "stdout", minLevel: minLevel });
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
- void pruneOldLogFiles();
597
- const maxFileBytes = (typeof config.maxSize === "number" && config.maxSize > 0 ? config.maxSize : 10) * 1024 * 1024;
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
- // error logger:固定 minLevel=error
600
- errorFileSink = new LogFileSink({ prefix: "error", minLevel: "error", maxFileBytes: maxFileBytes });
480
+ errorFileSink = new LogFileSink({ prefix: "error", maxFileBytes: maxFileBytes });
601
481
  }
602
- errorInstance = createSinkLogger({ kind: "error", minLevel: "error", fileSink: errorFileSink, consoleSink: null });
482
+ errorInstance = createSinkLogger({ fileSink: errorFileSink, consoleSink: null });
603
483
  return errorInstance;
604
484
  }
605
- function formatExtrasToMsg(extras) {
606
- if (!extras || extras.length === 0)
607
- return "";
608
- const parts = [];
609
- for (const item of extras) {
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, args) {
491
+ function buildLogLine(level, record) {
649
492
  const time = Date.now();
650
493
  const base = {
651
- level: LOG_LEVEL_NUM[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
- if (!args || args.length === 0) {
657
- base.msg = "";
658
- return `${JSON.stringify(base)}\n`;
659
- }
660
- const first = args[0];
661
- const second = args.length > 1 ? args[1] : undefined;
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 (typeof second === "string") {
668
- const extraMsg = formatExtrasToMsg(args.slice(2));
669
- base.msg = extraMsg ? `${second} ${extraMsg}` : second;
507
+ else if (out.msg === undefined) {
508
+ out.msg = "";
670
509
  }
671
- else {
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
- // 兼容:logger.(level)(err, msg?)
678
- if (first instanceof Error) {
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 minLevel = options.minLevel;
714
- const write = (level, args) => {
715
- if (!shouldAccept(minLevel, level))
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 line = buildLogLine(level, args);
718
- options.fileSink.enqueue(level, line);
719
- if (options.consoleSink) {
720
- options.consoleSink.enqueue(level, line);
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(...args) {
725
- write("info", args);
543
+ info(record) {
544
+ write("info", record);
726
545
  },
727
- warn(...args) {
728
- write("warn", args);
546
+ warn(record) {
547
+ write("warn", record);
729
548
  },
730
- error(...args) {
731
- write("error", args);
549
+ error(record) {
550
+ write("error", record);
732
551
  },
733
- debug(...args) {
734
- write("debug", args);
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
- function withRequestMeta(args) {
980
- const meta = metaToObject();
981
- if (!meta)
982
- return args;
983
- if (args.length === 0)
984
- return args;
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
- // pino 原生:logger.error(err, msg)
996
- if (first instanceof Error) {
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
- // 纯字符串:Logger.info("msg") -> logger.info(meta, "msg")
1007
- if (typeof first === "string") {
1008
- return [meta, ...args];
601
+ if (input === undefined) {
602
+ return { msg: "" };
1009
603
  }
1010
- // 对象:Logger.info(obj, msg?) -> 合并 meta(不覆盖显式字段)
1011
- if (isPlainObject(first)) {
1012
- const merged = mergeMetaIntoObject(first, meta);
1013
- return [merged, ...args.slice(1)];
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
- export const Logger = {
1025
- info(...args) {
1026
- if (args.length === 0)
1027
- return;
1028
- const logger = getLogger();
1029
- const finalArgs = withRequestMetaTyped(args);
1030
- if (finalArgs.length === 0)
1031
- return;
1032
- if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
1033
- finalArgs[0] = sanitizeLogObject(finalArgs[0]);
1034
- }
1035
- const ret = logger.info.apply(logger, finalArgs);
1036
- return ret;
1037
- },
1038
- warn(...args) {
1039
- if (args.length === 0)
1040
- return;
1041
- const logger = getLogger();
1042
- const finalArgs = withRequestMetaTyped(args);
1043
- if (finalArgs.length === 0)
1044
- return;
1045
- if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
1046
- finalArgs[0] = sanitizeLogObject(finalArgs[0]);
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
- const errorLogger = getErrorLogger();
1067
- errorLogger.error.apply(errorLogger, finalArgs);
648
+ getSink("error").error(record);
1068
649
  return ret;
1069
- },
1070
- debug(...args) {
1071
- if (args.length === 0)
1072
- return;
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
- if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
1078
- finalArgs[0] = sanitizeLogObject(finalArgs[0]);
1079
- }
1080
- const ret = logger.debug.apply(logger, finalArgs);
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: configure,
1087
- setMock: setMockLogger,
1088
- shutdown: shutdown
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();