befly 3.9.38 → 3.9.40

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 (155) hide show
  1. package/README.md +37 -38
  2. package/befly.config.ts +62 -40
  3. package/checks/checkApi.ts +16 -16
  4. package/checks/checkApp.ts +19 -25
  5. package/checks/checkTable.ts +42 -42
  6. package/docs/README.md +42 -35
  7. package/docs/{api.md → api/api.md} +223 -231
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +143 -141
  10. package/docs/{examples.md → guide/examples.md} +181 -181
  11. package/docs/guide/quickstart.md +331 -0
  12. package/docs/hooks/auth.md +38 -0
  13. package/docs/hooks/cors.md +28 -0
  14. package/docs/{hook.md → hooks/hook.md} +140 -57
  15. package/docs/hooks/parser.md +19 -0
  16. package/docs/hooks/rateLimit.md +47 -0
  17. package/docs/{redis.md → infra/redis.md} +84 -93
  18. package/docs/plugins/cipher.md +61 -0
  19. package/docs/plugins/database.md +128 -0
  20. package/docs/{plugin.md → plugins/plugin.md} +83 -81
  21. package/docs/quickstart.md +26 -26
  22. package/docs/{addon.md → reference/addon.md} +46 -46
  23. package/docs/{config.md → reference/config.md} +32 -80
  24. package/docs/{logger.md → reference/logger.md} +52 -52
  25. package/docs/{sync.md → reference/sync.md} +32 -35
  26. package/docs/{table.md → reference/table.md} +1 -1
  27. package/docs/{validator.md → reference/validator.md} +57 -57
  28. package/hooks/auth.ts +8 -4
  29. package/hooks/cors.ts +13 -13
  30. package/hooks/parser.ts +37 -17
  31. package/hooks/permission.ts +26 -14
  32. package/hooks/rateLimit.ts +276 -0
  33. package/hooks/validator.ts +8 -8
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -77
  36. package/lib/cacheKeys.ts +38 -0
  37. package/lib/cipher.ts +30 -30
  38. package/lib/connect.ts +28 -28
  39. package/lib/dbHelper.ts +183 -102
  40. package/lib/jwt.ts +16 -16
  41. package/lib/logger.ts +610 -19
  42. package/lib/redisHelper.ts +185 -44
  43. package/lib/sqlBuilder.ts +90 -91
  44. package/lib/validator.ts +59 -39
  45. package/loader/loadApis.ts +48 -44
  46. package/loader/loadHooks.ts +40 -14
  47. package/loader/loadPlugins.ts +16 -17
  48. package/main.ts +57 -47
  49. package/package.json +47 -45
  50. package/paths.ts +15 -14
  51. package/plugins/cache.ts +5 -4
  52. package/plugins/cipher.ts +3 -3
  53. package/plugins/config.ts +2 -2
  54. package/plugins/db.ts +9 -9
  55. package/plugins/jwt.ts +3 -3
  56. package/plugins/logger.ts +8 -12
  57. package/plugins/redis.ts +8 -8
  58. package/plugins/tool.ts +6 -6
  59. package/router/api.ts +85 -56
  60. package/router/static.ts +12 -12
  61. package/sync/syncAll.ts +12 -12
  62. package/sync/syncApi.ts +55 -52
  63. package/sync/syncDb/apply.ts +20 -19
  64. package/sync/syncDb/constants.ts +25 -23
  65. package/sync/syncDb/ddl.ts +35 -36
  66. package/sync/syncDb/helpers.ts +6 -9
  67. package/sync/syncDb/schema.ts +10 -9
  68. package/sync/syncDb/sqlite.ts +7 -8
  69. package/sync/syncDb/table.ts +37 -35
  70. package/sync/syncDb/tableCreate.ts +21 -20
  71. package/sync/syncDb/types.ts +23 -20
  72. package/sync/syncDb/version.ts +10 -10
  73. package/sync/syncDb.ts +43 -36
  74. package/sync/syncDev.ts +74 -65
  75. package/sync/syncMenu.ts +190 -55
  76. package/tests/api-integration-array-number.test.ts +282 -0
  77. package/tests/befly-config-env.test.ts +78 -0
  78. package/tests/cacheHelper.test.ts +135 -104
  79. package/tests/cacheKeys.test.ts +41 -0
  80. package/tests/cipher.test.ts +90 -89
  81. package/tests/dbHelper-advanced.test.ts +140 -134
  82. package/tests/dbHelper-all-array-types.test.ts +316 -0
  83. package/tests/dbHelper-array-serialization.test.ts +258 -0
  84. package/tests/dbHelper-columns.test.ts +56 -55
  85. package/tests/dbHelper-execute.test.ts +45 -44
  86. package/tests/dbHelper-joins.test.ts +124 -119
  87. package/tests/fields-redis-cache.test.ts +29 -27
  88. package/tests/fields-validate.test.ts +38 -38
  89. package/tests/getClientIp.test.ts +54 -0
  90. package/tests/integration.test.ts +69 -67
  91. package/tests/jwt.test.ts +27 -26
  92. package/tests/logger.test.ts +267 -34
  93. package/tests/rateLimit-hook.test.ts +477 -0
  94. package/tests/redisHelper.test.ts +187 -188
  95. package/tests/redisKeys.test.ts +6 -73
  96. package/tests/scanConfig.test.ts +144 -0
  97. package/tests/sqlBuilder-advanced.test.ts +217 -215
  98. package/tests/sqlBuilder.test.ts +92 -91
  99. package/tests/sync-connection.test.ts +29 -29
  100. package/tests/syncDb-apply.test.ts +97 -96
  101. package/tests/syncDb-array-number.test.ts +160 -0
  102. package/tests/syncDb-constants.test.ts +48 -47
  103. package/tests/syncDb-ddl.test.ts +99 -98
  104. package/tests/syncDb-helpers.test.ts +29 -28
  105. package/tests/syncDb-schema.test.ts +61 -60
  106. package/tests/syncDb-types.test.ts +60 -59
  107. package/tests/syncMenu-paths.test.ts +68 -0
  108. package/tests/util.test.ts +42 -41
  109. package/tests/validator-array-number.test.ts +310 -0
  110. package/tests/validator-default.test.ts +373 -0
  111. package/tests/validator.test.ts +271 -266
  112. package/tsconfig.json +4 -5
  113. package/types/api.d.ts +7 -12
  114. package/types/befly.d.ts +60 -13
  115. package/types/cache.d.ts +8 -4
  116. package/types/common.d.ts +17 -9
  117. package/types/context.d.ts +2 -2
  118. package/types/crypto.d.ts +23 -0
  119. package/types/database.d.ts +19 -19
  120. package/types/hook.d.ts +2 -2
  121. package/types/jwt.d.ts +118 -0
  122. package/types/logger.d.ts +30 -0
  123. package/types/plugin.d.ts +4 -4
  124. package/types/redis.d.ts +7 -3
  125. package/types/roleApisCache.ts +23 -0
  126. package/types/sync.d.ts +10 -10
  127. package/types/table.d.ts +50 -9
  128. package/types/validate.d.ts +69 -0
  129. package/utils/addonHelper.ts +90 -0
  130. package/utils/arrayKeysToCamel.ts +18 -0
  131. package/utils/calcPerfTime.ts +13 -0
  132. package/utils/configTypes.ts +3 -0
  133. package/utils/cors.ts +19 -0
  134. package/utils/fieldClear.ts +75 -0
  135. package/utils/genShortId.ts +12 -0
  136. package/utils/getClientIp.ts +45 -0
  137. package/utils/keysToCamel.ts +22 -0
  138. package/utils/keysToSnake.ts +22 -0
  139. package/utils/modules.ts +98 -0
  140. package/utils/pickFields.ts +19 -0
  141. package/utils/process.ts +56 -0
  142. package/utils/regex.ts +225 -0
  143. package/utils/response.ts +115 -0
  144. package/utils/route.ts +23 -0
  145. package/utils/scanConfig.ts +142 -0
  146. package/utils/scanFiles.ts +48 -0
  147. package/.prettierignore +0 -2
  148. package/.prettierrc +0 -12
  149. package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
  150. package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
  151. package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
  152. package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
  153. package/hooks/requestLogger.ts +0 -84
  154. package/types/index.ts +0 -24
  155. package/util.ts +0 -283
package/lib/logger.ts CHANGED
@@ -1,27 +1,158 @@
1
- /**
1
+ /**
2
2
  * 日志系统 - 基于 pino 实现
3
3
  */
4
4
 
5
- import pino from 'pino';
6
- import { join } from 'pathe';
5
+ import type { LoggerConfig } from "../types/logger.js";
6
+
7
+ import { readdir, stat, unlink } from "node:fs/promises";
8
+ import { join as nodePathJoin } from "node:path";
9
+
10
+ import { isPlainObject } from "es-toolkit/compat";
11
+ import { escapeRegExp } from "es-toolkit/string";
12
+ import { join } from "pathe";
13
+ import pino from "pino";
14
+
15
+ import { getCtx } from "./asyncContext.js";
16
+
17
+ const MAX_LOG_STRING_LEN = 100;
18
+ const MAX_LOG_ARRAY_ITEMS = 100;
7
19
 
8
- import type { LoggerConfig } from 'befly-shared/types';
20
+ const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
21
+
22
+ 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"];
23
+
24
+ let sensitiveKeySet: Set<string> = new Set();
25
+ let sensitiveSuffixMatchers: string[] = [];
26
+ let sensitivePrefixMatchers: string[] = [];
27
+ let sensitiveContainsMatchers: string[] = [];
28
+ let sensitiveContainsRegex: RegExp | null = null;
9
29
 
10
30
  let instance: pino.Logger | null = null;
31
+ let slowInstance: pino.Logger | null = null;
32
+ let errorInstance: pino.Logger | null = null;
11
33
  let mockInstance: pino.Logger | null = null;
34
+ let didPruneOldLogFiles: boolean = false;
12
35
  let config: LoggerConfig = {
13
36
  debug: 0,
14
- dir: './logs',
37
+ dir: "./logs",
15
38
  console: 1,
16
39
  maxSize: 10
17
40
  };
18
41
 
42
+ async function pruneOldLogFiles(): Promise<void> {
43
+ if (didPruneOldLogFiles) return;
44
+ didPruneOldLogFiles = true;
45
+
46
+ const dir = config.dir || "./logs";
47
+ const now = Date.now();
48
+ const cutoff = now - ONE_YEAR_MS;
49
+
50
+ try {
51
+ const entries = await readdir(dir, { withFileTypes: true });
52
+ for (const entry of entries) {
53
+ if (!entry.isFile()) continue;
54
+
55
+ const name = entry.name;
56
+
57
+ // 只处理本项目的日志文件前缀
58
+ const isTarget = name.startsWith("app.") || name.startsWith("slow.") || name.startsWith("error.");
59
+ if (!isTarget) continue;
60
+
61
+ const fullPath = nodePathJoin(dir, name);
62
+
63
+ let st: any;
64
+ try {
65
+ st = await stat(fullPath);
66
+ } catch {
67
+ continue;
68
+ }
69
+
70
+ const mtimeMs = typeof st.mtimeMs === "number" ? st.mtimeMs : 0;
71
+ if (mtimeMs > 0 && mtimeMs < cutoff) {
72
+ try {
73
+ await unlink(fullPath);
74
+ } catch {
75
+ // 忽略删除失败(权限/占用等),避免影响服务启动
76
+ }
77
+ }
78
+ }
79
+ } catch {
80
+ // 忽略:目录不存在或无权限等
81
+ }
82
+ }
83
+
19
84
  /**
20
85
  * 配置日志
21
86
  */
22
87
  export function configure(cfg: LoggerConfig): void {
23
88
  config = { ...config, ...cfg };
24
89
  instance = null;
90
+ slowInstance = null;
91
+ errorInstance = null;
92
+ didPruneOldLogFiles = false;
93
+
94
+ // 仅支持数组配置:excludeFields?: string[]
95
+ const userPatterns = Array.isArray(config.excludeFields) ? config.excludeFields : [];
96
+ const patterns = [...BUILTIN_SENSITIVE_KEYS, ...userPatterns]
97
+ .map((item) => String(item).trim())
98
+ .filter((item) => item.length > 0)
99
+ .map((item) => item.toLowerCase());
100
+
101
+ const exactSet = new Set<string>();
102
+ const suffixMatchers: string[] = [];
103
+ const prefixMatchers: string[] = [];
104
+ const containsMatchers: string[] = [];
105
+
106
+ for (const pat of patterns) {
107
+ // 支持通配符:
108
+ // - *secret -> 后缀匹配
109
+ // - secret* -> 前缀匹配
110
+ // - *secret* -> 包含匹配
111
+ // - 无 * -> 精确匹配(建议用 *x* 显式开启模糊匹配)
112
+ const hasStar = pat.includes("*");
113
+ if (!hasStar) {
114
+ exactSet.add(pat);
115
+ continue;
116
+ }
117
+
118
+ const trimmed = pat.replace(/\*+/g, "*");
119
+ const startsStar = trimmed.startsWith("*");
120
+ const endsStar = trimmed.endsWith("*");
121
+ const core = trimmed.replace(/^\*+|\*+$/g, "");
122
+ if (!core) {
123
+ continue;
124
+ }
125
+
126
+ if (startsStar && !endsStar) {
127
+ suffixMatchers.push(core);
128
+ continue;
129
+ }
130
+ if (!startsStar && endsStar) {
131
+ prefixMatchers.push(core);
132
+ continue;
133
+ }
134
+
135
+ // *core* 或类似 a*b:都降级为包含匹配
136
+ containsMatchers.push(core);
137
+ }
138
+
139
+ sensitiveKeySet = exactSet;
140
+ sensitiveSuffixMatchers = suffixMatchers;
141
+ sensitivePrefixMatchers = prefixMatchers;
142
+ sensitiveContainsMatchers = containsMatchers;
143
+
144
+ // 预编译包含匹配:减少每次 isSensitiveKey 的循环开销
145
+ // 注意:patterns 已全部 lowerCase,因此 regex 不需要 i 标志
146
+ if (containsMatchers.length > 0) {
147
+ const escaped = containsMatchers.map((item) => escapeRegExp(item)).filter((item) => item.length > 0);
148
+ if (escaped.length > 0) {
149
+ sensitiveContainsRegex = new RegExp(escaped.join("|"));
150
+ } else {
151
+ sensitiveContainsRegex = null;
152
+ }
153
+ } else {
154
+ sensitiveContainsRegex = null;
155
+ }
25
156
  }
26
157
 
27
158
  /**
@@ -41,26 +172,29 @@ export function getLogger(): pino.Logger {
41
172
 
42
173
  if (instance) return instance;
43
174
 
44
- const level = config.debug === 1 ? 'debug' : 'info';
175
+ // 启动时清理过期日志(异步,不阻塞初始化)
176
+ void pruneOldLogFiles();
177
+
178
+ const level = config.debug === 1 ? "debug" : "info";
45
179
  const targets: pino.TransportTargetOptions[] = [];
46
180
 
47
181
  // 文件输出
48
182
  targets.push({
49
- target: 'pino-roll',
183
+ target: "pino-roll",
50
184
  level: level,
51
185
  options: {
52
- file: join(config.dir || './logs', 'app'),
53
- frequency: 'daily',
186
+ file: join(config.dir || "./logs", "app"),
187
+ frequency: "daily",
54
188
  size: `${config.maxSize || 10}m`,
55
189
  mkdir: true,
56
- dateFormat: 'yyyy-MM-dd'
190
+ dateFormat: "yyyy-MM-dd"
57
191
  }
58
192
  });
59
193
 
60
194
  // 控制台输出
61
195
  if (config.console === 1) {
62
196
  targets.push({
63
- target: 'pino/file',
197
+ target: "pino/file",
64
198
  level: level,
65
199
  options: { destination: 1 }
66
200
  });
@@ -74,21 +208,478 @@ export function getLogger(): pino.Logger {
74
208
  return instance;
75
209
  }
76
210
 
211
+ function getSlowLogger(): pino.Logger {
212
+ if (mockInstance) return mockInstance;
213
+ if (slowInstance) return slowInstance;
214
+
215
+ void pruneOldLogFiles();
216
+
217
+ const level = config.debug === 1 ? "debug" : "info";
218
+ slowInstance = pino({
219
+ level: level,
220
+ transport: {
221
+ targets: [
222
+ {
223
+ target: "pino-roll",
224
+ level: level,
225
+ options: {
226
+ file: join(config.dir || "./logs", "slow"),
227
+ // 只按大小分割(frequency 默认不启用)
228
+ size: `${config.maxSize || 10}m`,
229
+ mkdir: true
230
+ }
231
+ }
232
+ ]
233
+ }
234
+ });
235
+
236
+ return slowInstance;
237
+ }
238
+
239
+ function getErrorLogger(): pino.Logger {
240
+ if (mockInstance) return mockInstance;
241
+ if (errorInstance) return errorInstance;
242
+
243
+ void pruneOldLogFiles();
244
+
245
+ // error 专属文件:只关注 error 及以上
246
+ errorInstance = pino({
247
+ level: "error",
248
+ transport: {
249
+ targets: [
250
+ {
251
+ target: "pino-roll",
252
+ level: "error",
253
+ options: {
254
+ file: join(config.dir || "./logs", "error"),
255
+ // 只按大小分割(frequency 默认不启用)
256
+ size: `${config.maxSize || 10}m`,
257
+ mkdir: true
258
+ }
259
+ }
260
+ ]
261
+ }
262
+ });
263
+
264
+ return errorInstance;
265
+ }
266
+
267
+ function truncateString(val: string, stats: Record<string, number>): string {
268
+ if (val.length <= MAX_LOG_STRING_LEN) return val;
269
+ stats.truncatedStrings = (stats.truncatedStrings || 0) + 1;
270
+ return val.slice(0, MAX_LOG_STRING_LEN);
271
+ }
272
+
273
+ function safeToString(val: any, visited: WeakSet<object>, stats: Record<string, number>): string {
274
+ if (typeof val === "string") return val;
275
+
276
+ if (val instanceof Error) {
277
+ const name = val.name || "Error";
278
+ const message = val.message || "";
279
+ const stack = typeof val.stack === "string" ? val.stack : "";
280
+ const errObj: Record<string, any> = {
281
+ name: name,
282
+ message: message,
283
+ stack: stack
284
+ };
285
+ try {
286
+ return JSON.stringify(errObj);
287
+ } catch {
288
+ return `${name}: ${message}`;
289
+ }
290
+ }
291
+
292
+ if (val && typeof val === "object") {
293
+ if (visited.has(val as object)) {
294
+ stats.circularRefs = (stats.circularRefs || 0) + 1;
295
+ return "[Circular]";
296
+ }
297
+ }
298
+
299
+ try {
300
+ const localVisited = visited;
301
+ const replacer = (_k: string, v: any) => {
302
+ if (v && typeof v === "object") {
303
+ if (localVisited.has(v as object)) {
304
+ stats.circularRefs = (stats.circularRefs || 0) + 1;
305
+ return "[Circular]";
306
+ }
307
+ localVisited.add(v as object);
308
+ }
309
+ return v;
310
+ };
311
+ return JSON.stringify(val, replacer);
312
+ } catch {
313
+ try {
314
+ return String(val);
315
+ } catch {
316
+ return "[Unserializable]";
317
+ }
318
+ }
319
+ }
320
+
321
+ function isSensitiveKey(key: string): boolean {
322
+ const lower = String(key).toLowerCase();
323
+ if (sensitiveKeySet.has(lower)) return true;
324
+
325
+ for (const suffix of sensitiveSuffixMatchers) {
326
+ if (lower.endsWith(suffix)) return true;
327
+ }
328
+ for (const prefix of sensitivePrefixMatchers) {
329
+ if (lower.startsWith(prefix)) return true;
330
+ }
331
+
332
+ if (sensitiveContainsRegex) {
333
+ return sensitiveContainsRegex.test(lower);
334
+ }
335
+
336
+ for (const part of sensitiveContainsMatchers) {
337
+ if (lower.includes(part)) return true;
338
+ }
339
+
340
+ return false;
341
+ }
342
+
343
+ function sanitizeNestedValue(val: any, visited: WeakSet<object>, stats: Record<string, number>): any {
344
+ if (val === null || val === undefined) return val;
345
+ if (typeof val === "string") return truncateString(val, stats);
346
+ if (typeof val === "number") return val;
347
+ if (typeof val === "boolean") return val;
348
+ if (typeof val === "bigint") return val;
349
+
350
+ if (val instanceof Error) {
351
+ const errObj: Record<string, any> = {
352
+ name: val.name || "Error",
353
+ message: truncateString(val.message || "", stats)
354
+ };
355
+ if (typeof val.stack === "string") {
356
+ errObj.stack = truncateString(val.stack, stats);
357
+ }
358
+ return errObj;
359
+ }
360
+
361
+ // 对象/数组:不再深入,转为字符串并截断
362
+ stats.valuesStringified = (stats.valuesStringified || 0) + 1;
363
+ const str = safeToString(val, visited, stats);
364
+ return truncateString(str, stats);
365
+ }
366
+
367
+ function sanitizeObjectFirstLayer(obj: Record<string, any>, visited: WeakSet<object>, stats: Record<string, number>): Record<string, any> {
368
+ if (visited.has(obj)) {
369
+ stats.circularRefs = (stats.circularRefs || 0) + 1;
370
+ return { value: "[Circular]" };
371
+ }
372
+ visited.add(obj);
373
+
374
+ const out: Record<string, any> = {};
375
+ for (const [key, val] of Object.entries(obj)) {
376
+ if (isSensitiveKey(key)) {
377
+ stats.maskedKeys = (stats.maskedKeys || 0) + 1;
378
+ out[key] = "[MASKED]";
379
+ continue;
380
+ }
381
+ out[key] = sanitizeNestedValue(val, visited, stats);
382
+ }
383
+ return out;
384
+ }
385
+
386
+ function sanitizeArray(arr: any[], visited: WeakSet<object>, stats: Record<string, number>): any[] {
387
+ const max = MAX_LOG_ARRAY_ITEMS;
388
+ const len = arr.length;
389
+ const limit = len > max ? max : len;
390
+
391
+ const out: any[] = [];
392
+ for (let i = 0; i < limit; i++) {
393
+ const item = arr[i];
394
+ if (item && typeof item === "object" && !Array.isArray(item) && !(item instanceof Error)) {
395
+ out.push(sanitizeObjectFirstLayer(item as Record<string, any>, visited, stats));
396
+ continue;
397
+ }
398
+ if (item instanceof Error) {
399
+ const errObj: Record<string, any> = {
400
+ name: item.name || "Error",
401
+ message: truncateString(item.message || "", stats)
402
+ };
403
+ if (typeof item.stack === "string") {
404
+ errObj.stack = truncateString(item.stack, stats);
405
+ }
406
+ out.push(errObj);
407
+ continue;
408
+ }
409
+ if (typeof item === "string") {
410
+ out.push(truncateString(item, stats));
411
+ continue;
412
+ }
413
+ if (typeof item === "number" || typeof item === "boolean" || item === null || item === undefined) {
414
+ out.push(item);
415
+ continue;
416
+ }
417
+ if (typeof item === "bigint") {
418
+ out.push(item);
419
+ continue;
420
+ }
421
+
422
+ // 其他类型(包含子数组等)转字符串预览
423
+ stats.valuesStringified = (stats.valuesStringified || 0) + 1;
424
+ const str = safeToString(item, visited, stats);
425
+ out.push(truncateString(str, stats));
426
+ }
427
+
428
+ if (len > max) {
429
+ stats.arraysTruncated = (stats.arraysTruncated || 0) + 1;
430
+ stats.arrayItemsOmitted = (stats.arrayItemsOmitted || 0) + (len - max);
431
+ }
432
+
433
+ return out;
434
+ }
435
+
436
+ function sanitizeTopValue(val: any, visited: WeakSet<object>, stats: Record<string, number>): any {
437
+ if (val === null || val === undefined) return val;
438
+ if (typeof val === "string") return truncateString(val, stats);
439
+ if (typeof val === "number") return val;
440
+ if (typeof val === "boolean") return val;
441
+ if (typeof val === "bigint") return val;
442
+ if (val instanceof Error) {
443
+ const errObj: Record<string, any> = {
444
+ name: val.name || "Error",
445
+ message: truncateString(val.message || "", stats)
446
+ };
447
+ if (typeof val.stack === "string") {
448
+ errObj.stack = truncateString(val.stack, stats);
449
+ }
450
+ return errObj;
451
+ }
452
+ if (Array.isArray(val)) return sanitizeArray(val, visited, stats);
453
+ if (isPlainObject(val)) return sanitizeObjectFirstLayer(val as Record<string, any>, visited, stats);
454
+
455
+ stats.valuesStringified = (stats.valuesStringified || 0) + 1;
456
+ const str = safeToString(val, visited, stats);
457
+ return truncateString(str, stats);
458
+ }
459
+
460
+ function sanitizeLogObject(obj: Record<string, any>): Record<string, any> {
461
+ const visited = new WeakSet<object>();
462
+ const stats: Record<string, number> = {
463
+ maskedKeys: 0,
464
+ truncatedStrings: 0,
465
+ arraysTruncated: 0,
466
+ arrayItemsOmitted: 0,
467
+ valuesStringified: 0,
468
+ circularRefs: 0
469
+ };
470
+
471
+ const out: Record<string, any> = {};
472
+ for (const [key, val] of Object.entries(obj)) {
473
+ if (isSensitiveKey(key)) {
474
+ stats.maskedKeys = stats.maskedKeys + 1;
475
+ out[key] = "[MASKED]";
476
+ continue;
477
+ }
478
+ out[key] = sanitizeTopValue(val, visited, stats);
479
+ }
480
+
481
+ const hasChanges = stats.maskedKeys > 0 || stats.truncatedStrings > 0 || stats.arraysTruncated > 0 || stats.arrayItemsOmitted > 0 || stats.valuesStringified > 0 || stats.circularRefs > 0;
482
+
483
+ if (hasChanges) {
484
+ out.logTrimStats = {
485
+ maskedKeys: stats.maskedKeys,
486
+ truncatedStrings: stats.truncatedStrings,
487
+ arraysTruncated: stats.arraysTruncated,
488
+ arrayItemsOmitted: stats.arrayItemsOmitted,
489
+ valuesStringified: stats.valuesStringified,
490
+ circularRefs: stats.circularRefs
491
+ };
492
+ }
493
+
494
+ return out;
495
+ }
496
+
497
+ function metaToObject(): Record<string, any> | null {
498
+ const meta = getCtx();
499
+ if (!meta) return null;
500
+
501
+ const durationSinceNowMs = Date.now() - meta.now;
502
+
503
+ const obj: Record<string, any> = {
504
+ requestId: meta.requestId,
505
+ method: meta.method,
506
+ route: meta.route,
507
+ ip: meta.ip,
508
+ now: meta.now,
509
+ durationSinceNowMs: durationSinceNowMs
510
+ };
511
+
512
+ // userId / roleCode 默认写入
513
+ obj.userId = meta.userId;
514
+ obj.roleCode = meta.roleCode;
515
+ obj.nickname = (meta as any).nickname;
516
+ obj.roleType = (meta as any).roleType;
517
+
518
+ return obj;
519
+ }
520
+
521
+ function mergeMetaIntoObject(input: Record<string, any>, meta: Record<string, any>): Record<string, any> {
522
+ const merged: Record<string, any> = {};
523
+ for (const [key, value] of Object.entries(input)) {
524
+ merged[key] = value;
525
+ }
526
+
527
+ // 只补齐、不覆盖:允许把 undefined / null / 空字符串写入(由日志底层序列化决定是否展示)
528
+ const keys = ["requestId", "method", "route", "ip", "now", "durationSinceNowMs", "userId", "roleCode", "nickname", "roleType"];
529
+
530
+ for (const key of keys) {
531
+ if (merged[key] === undefined) merged[key] = meta[key];
532
+ }
533
+
534
+ return merged;
535
+ }
536
+
537
+ function withRequestMeta(args: any[]): any[] {
538
+ const meta = metaToObject();
539
+ if (!meta) return args;
540
+ if (args.length === 0) return args;
541
+
542
+ const first = args[0];
543
+ const second = args.length > 1 ? args[1] : undefined;
544
+
545
+ // 兼容:Logger.error("xxx", err)
546
+ if (typeof first === "string" && second instanceof Error) {
547
+ const obj = {
548
+ err: second
549
+ };
550
+ const merged = mergeMetaIntoObject(obj, meta);
551
+ return [merged, first, ...args.slice(2)];
552
+ }
553
+
554
+ // pino 原生:logger.error(err, msg)
555
+ if (first instanceof Error) {
556
+ const msg = typeof second === "string" ? second : undefined;
557
+ const obj = {
558
+ err: first
559
+ };
560
+ const merged = mergeMetaIntoObject(obj, meta);
561
+ if (msg) return [merged, msg, ...args.slice(2)];
562
+ return [merged, ...args.slice(1)];
563
+ }
564
+
565
+ // 纯字符串:Logger.info("msg") -> logger.info(meta, "msg")
566
+ if (typeof first === "string") {
567
+ return [meta, ...args];
568
+ }
569
+
570
+ // 对象:Logger.info(obj, msg?) -> 合并 meta(不覆盖显式字段)
571
+ if (isPlainObject(first)) {
572
+ const merged = mergeMetaIntoObject(first as Record<string, any>, meta);
573
+ return [merged, ...args.slice(1)];
574
+ }
575
+
576
+ return args;
577
+ }
578
+
579
+ function shouldMirrorToSlow(args: any[]): boolean {
580
+ // 测试场景:启用 mock 时不做镜像,避免调用次数翻倍
581
+ if (mockInstance) return false;
582
+ if (!args || args.length === 0) return false;
583
+ const first = args[0];
584
+ if (!isPlainObject(first)) return false;
585
+
586
+ // 优先使用显式标记:event=slow
587
+ const event = (first as any).event;
588
+ if (event === "slow") return true;
589
+
590
+ // 兼容旧写法:仅通过 message emoji 判断(尽量少用)
591
+ const msg = args.length > 1 ? args[1] : undefined;
592
+ if (typeof msg === "string" && msg.includes("🐌")) return true;
593
+
594
+ return false;
595
+ }
596
+
597
+ type LoggerObject = Record<string, any>;
598
+
599
+ // 兼容 pino 常用调用形式 + 本项目的 Logger.error("msg", err)
600
+ type LoggerCallArgs = [] | [msg: string, ...args: unknown[]] | [obj: LoggerObject, msg?: string, ...args: unknown[]] | [err: Error, msg?: string, ...args: unknown[]] | [msg: string, err: Error, ...args: unknown[]];
601
+
602
+ function withRequestMetaTyped(args: LoggerCallArgs): LoggerCallArgs {
603
+ // 复用现有逻辑(保持行为一致),只收敛类型
604
+ return withRequestMeta(args as any[]) as unknown as LoggerCallArgs;
605
+ }
606
+
77
607
  /**
78
608
  * 日志实例(延迟初始化)
79
609
  */
80
610
  export const Logger = {
81
- get info() {
82
- return getLogger().info.bind(getLogger());
611
+ info(...args: LoggerCallArgs) {
612
+ if (args.length === 0) return;
613
+ const logger = getLogger();
614
+ const finalArgs = withRequestMetaTyped(args);
615
+ if (finalArgs.length === 0) return;
616
+ if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
617
+ finalArgs[0] = sanitizeLogObject(finalArgs[0] as Record<string, any>);
618
+ }
619
+ const ret = (logger.info as any).apply(logger, finalArgs);
620
+ if (mockInstance) return ret;
621
+ if (shouldMirrorToSlow(finalArgs as any[])) {
622
+ const slowLogger = getSlowLogger();
623
+ (slowLogger.info as any).apply(slowLogger, finalArgs);
624
+ }
625
+ return ret;
83
626
  },
84
- get warn() {
85
- return getLogger().warn.bind(getLogger());
627
+ warn(...args: LoggerCallArgs) {
628
+ if (args.length === 0) return;
629
+ const logger = getLogger();
630
+ const finalArgs = withRequestMetaTyped(args);
631
+ if (finalArgs.length === 0) return;
632
+ if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
633
+ finalArgs[0] = sanitizeLogObject(finalArgs[0] as Record<string, any>);
634
+ }
635
+ const ret = (logger.warn as any).apply(logger, finalArgs);
636
+ if (mockInstance) return ret;
637
+ if (shouldMirrorToSlow(finalArgs as any[])) {
638
+ const slowLogger = getSlowLogger();
639
+ (slowLogger.warn as any).apply(slowLogger, finalArgs);
640
+ }
641
+ return ret;
86
642
  },
87
- get error() {
88
- return getLogger().error.bind(getLogger());
643
+ error(...args: LoggerCallArgs) {
644
+ if (args.length === 0) return;
645
+ const logger = getLogger();
646
+ const finalArgs = withRequestMetaTyped(args);
647
+ if (finalArgs.length === 0) return;
648
+ if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
649
+ finalArgs[0] = sanitizeLogObject(finalArgs[0] as Record<string, any>);
650
+ }
651
+ const ret = (logger.error as any).apply(logger, finalArgs);
652
+
653
+ // 测试场景:启用 mock 时不做镜像,避免调用次数翻倍
654
+ if (mockInstance) return ret;
655
+
656
+ // error 专属文件:始终镜像一份
657
+ const errorLogger = getErrorLogger();
658
+ (errorLogger.error as any).apply(errorLogger, finalArgs);
659
+
660
+ // error 同时也属于 slow?一般不会,但允许显式 event=slow
661
+ if (shouldMirrorToSlow(finalArgs as any[])) {
662
+ const slowLogger = getSlowLogger();
663
+ (slowLogger.error as any).apply(slowLogger, finalArgs);
664
+ }
665
+
666
+ return ret;
89
667
  },
90
- get debug() {
91
- return getLogger().debug.bind(getLogger());
668
+ debug(...args: LoggerCallArgs) {
669
+ if (args.length === 0) return;
670
+ const logger = getLogger();
671
+ const finalArgs = withRequestMetaTyped(args);
672
+ if (finalArgs.length === 0) return;
673
+ if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
674
+ finalArgs[0] = sanitizeLogObject(finalArgs[0] as Record<string, any>);
675
+ }
676
+ const ret = (logger.debug as any).apply(logger, finalArgs);
677
+ if (mockInstance) return ret;
678
+ if (shouldMirrorToSlow(finalArgs as any[])) {
679
+ const slowLogger = getSlowLogger();
680
+ (slowLogger.debug as any).apply(slowLogger, finalArgs);
681
+ }
682
+ return ret;
92
683
  },
93
684
  configure: configure,
94
685
  setMock: setMockLogger