befly 3.10.18 → 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/README.md +83 -307
- package/dist/befly.config.d.ts +7 -0
- package/{befly.config.ts → dist/befly.config.js} +11 -36
- package/dist/befly.js +15621 -0
- package/dist/befly.min.js +21 -0
- package/dist/checks/checkApi.d.ts +1 -0
- package/{checks/checkApi.ts → dist/checks/checkApi.js} +12 -30
- package/dist/checks/checkHook.d.ts +1 -0
- package/dist/checks/checkHook.js +86 -0
- package/dist/checks/checkMenu.d.ts +7 -0
- package/{checks/checkMenu.ts → dist/checks/checkMenu.js} +18 -53
- package/dist/checks/checkPlugin.d.ts +1 -0
- package/dist/checks/checkPlugin.js +86 -0
- package/dist/checks/checkTable.d.ts +6 -0
- package/{checks/checkTable.ts → dist/checks/checkTable.js} +17 -41
- package/dist/configs/presetFields.d.ts +4 -0
- package/{configs/presetFields.ts → dist/configs/presetFields.js} +1 -1
- package/dist/configs/presetRegexp.d.ts +145 -0
- package/{utils/regex.ts → dist/configs/presetRegexp.js} +8 -31
- package/dist/hooks/auth.d.ts +7 -0
- package/{hooks/auth.ts → dist/hooks/auth.js} +8 -10
- package/dist/hooks/cors.d.ts +11 -0
- package/{hooks/cors.ts → dist/hooks/cors.js} +5 -13
- package/dist/hooks/parser.d.ts +14 -0
- package/{hooks/parser.ts → dist/hooks/parser.js} +31 -45
- package/dist/hooks/permission.d.ts +14 -0
- package/{hooks/permission.ts → dist/hooks/permission.js} +16 -25
- package/dist/hooks/validator.d.ts +11 -0
- package/{hooks/validator.ts → dist/hooks/validator.js} +9 -14
- package/dist/index.d.ts +26 -0
- package/{main.ts → dist/index.js} +61 -100
- package/dist/lib/asyncContext.d.ts +21 -0
- package/dist/lib/asyncContext.js +27 -0
- package/dist/lib/cacheHelper.d.ts +95 -0
- package/{lib/cacheHelper.ts → dist/lib/cacheHelper.js} +45 -105
- package/dist/lib/cacheKeys.d.ts +23 -0
- package/{lib/cacheKeys.ts → dist/lib/cacheKeys.js} +5 -10
- package/dist/lib/cipher.d.ts +153 -0
- package/{lib/cipher.ts → dist/lib/cipher.js} +23 -44
- package/dist/lib/connect.d.ts +91 -0
- package/{lib/connect.ts → dist/lib/connect.js} +47 -88
- package/dist/lib/dbDialect.d.ts +87 -0
- package/{lib/dbDialect.ts → dist/lib/dbDialect.js} +32 -112
- package/dist/lib/dbHelper.d.ts +204 -0
- package/{lib/dbHelper.ts → dist/lib/dbHelper.js} +82 -241
- package/dist/lib/dbUtils.d.ts +68 -0
- package/{lib/dbUtils.ts → dist/lib/dbUtils.js} +51 -126
- package/dist/lib/jwt.d.ts +13 -0
- package/{lib/jwt.ts → dist/lib/jwt.js} +11 -32
- package/dist/lib/logger.d.ts +42 -0
- package/dist/lib/logger.js +1144 -0
- package/dist/lib/redisHelper.d.ts +185 -0
- package/{lib/redisHelper.ts → dist/lib/redisHelper.js} +97 -141
- package/dist/lib/sqlBuilder.d.ts +160 -0
- package/{lib/sqlBuilder.ts → dist/lib/sqlBuilder.js} +132 -278
- package/dist/lib/sqlCheck.d.ts +23 -0
- package/{lib/sqlCheck.ts → dist/lib/sqlCheck.js} +24 -41
- package/dist/lib/validator.d.ts +45 -0
- package/{lib/validator.ts → dist/lib/validator.js} +44 -61
- package/dist/loader/loadApis.d.ts +12 -0
- package/{loader/loadApis.ts → dist/loader/loadApis.js} +10 -20
- package/dist/loader/loadHooks.d.ts +7 -0
- package/dist/loader/loadHooks.js +35 -0
- package/dist/loader/loadPlugins.d.ts +8 -0
- package/{loader/loadPlugins.ts → dist/loader/loadPlugins.js} +14 -26
- package/dist/paths.d.ts +93 -0
- package/{paths.ts → dist/paths.js} +6 -19
- package/dist/plugins/cache.d.ts +16 -0
- package/{plugins/cache.ts → dist/plugins/cache.js} +7 -12
- package/dist/plugins/cipher.d.ts +12 -0
- package/{plugins/cipher.ts → dist/plugins/cipher.js} +4 -6
- package/dist/plugins/config.d.ts +12 -0
- package/dist/plugins/config.js +8 -0
- package/dist/plugins/db.d.ts +16 -0
- package/{plugins/db.ts → dist/plugins/db.js} +11 -17
- package/dist/plugins/jwt.d.ts +12 -0
- package/dist/plugins/jwt.js +12 -0
- package/dist/plugins/logger.d.ts +32 -0
- package/{plugins/logger.ts → dist/plugins/logger.js} +5 -8
- package/dist/plugins/redis.d.ts +16 -0
- package/{plugins/redis.ts → dist/plugins/redis.js} +9 -12
- package/dist/plugins/tool.d.ts +81 -0
- package/{plugins/tool.ts → dist/plugins/tool.js} +9 -30
- package/dist/router/api.d.ts +14 -0
- package/dist/router/api.js +107 -0
- package/dist/router/static.d.ts +9 -0
- package/{router/static.ts → dist/router/static.js} +20 -34
- package/dist/scripts/ensureDist.d.ts +1 -0
- package/dist/scripts/ensureDist.js +296 -0
- package/dist/sync/syncApi.d.ts +3 -0
- package/{sync/syncApi.ts → dist/sync/syncApi.js} +35 -55
- package/dist/sync/syncCache.d.ts +2 -0
- package/{sync/syncCache.ts → dist/sync/syncCache.js} +1 -6
- package/dist/sync/syncDev.d.ts +6 -0
- package/{sync/syncDev.ts → dist/sync/syncDev.js} +29 -62
- package/dist/sync/syncMenu.d.ts +14 -0
- package/{sync/syncMenu.ts → dist/sync/syncMenu.js} +65 -125
- package/dist/sync/syncTable.d.ts +151 -0
- package/{sync/syncTable.ts → dist/sync/syncTable.js} +172 -379
- package/{types → dist/types}/api.d.ts +12 -51
- package/dist/types/api.js +4 -0
- package/{types → dist/types}/befly.d.ts +32 -227
- package/dist/types/befly.js +4 -0
- package/{types → dist/types}/cache.d.ts +7 -15
- package/dist/types/cache.js +4 -0
- package/dist/types/cipher.d.ts +27 -0
- package/dist/types/cipher.js +7 -0
- package/{types → dist/types}/common.d.ts +8 -33
- package/dist/types/common.js +5 -0
- package/{types → dist/types}/context.d.ts +3 -5
- package/dist/types/context.js +4 -0
- package/{types → dist/types}/crypto.d.ts +0 -3
- package/dist/types/crypto.js +4 -0
- package/dist/types/database.d.ts +138 -0
- package/dist/types/database.js +4 -0
- package/dist/types/hook.d.ts +17 -0
- package/dist/types/hook.js +6 -0
- package/dist/types/jwt.d.ts +75 -0
- package/dist/types/jwt.js +4 -0
- package/dist/types/logger.d.ts +59 -0
- package/dist/types/logger.js +6 -0
- package/dist/types/plugin.d.ts +16 -0
- package/dist/types/plugin.js +6 -0
- package/dist/types/redis.d.ts +71 -0
- package/dist/types/redis.js +4 -0
- package/{types/roleApisCache.ts → dist/types/roleApisCache.d.ts} +0 -2
- package/dist/types/roleApisCache.js +8 -0
- package/dist/types/sync.d.ts +92 -0
- package/dist/types/sync.js +4 -0
- package/dist/types/table.d.ts +34 -0
- package/dist/types/table.js +4 -0
- package/dist/types/validate.d.ts +67 -0
- package/dist/types/validate.js +4 -0
- package/dist/utils/calcPerfTime.d.ts +4 -0
- package/{utils/calcPerfTime.ts → dist/utils/calcPerfTime.js} +3 -3
- package/dist/utils/convertBigIntFields.d.ts +11 -0
- package/{utils/convertBigIntFields.ts → dist/utils/convertBigIntFields.js} +5 -9
- package/dist/utils/cors.d.ts +8 -0
- package/{utils/cors.ts → dist/utils/cors.js} +1 -3
- package/dist/utils/disableMenusGlob.d.ts +13 -0
- package/{utils/disableMenusGlob.ts → dist/utils/disableMenusGlob.js} +9 -29
- package/dist/utils/fieldClear.d.ts +11 -0
- package/{utils/fieldClear.ts → dist/utils/fieldClear.js} +15 -33
- package/dist/utils/getClientIp.d.ts +6 -0
- package/{utils/getClientIp.ts → dist/utils/getClientIp.js} +1 -7
- package/dist/utils/importDefault.d.ts +1 -0
- package/dist/utils/importDefault.js +29 -0
- package/dist/utils/isDirentDirectory.d.ts +2 -0
- package/{utils/isDirentDirectory.ts → dist/utils/isDirentDirectory.js} +3 -8
- package/dist/utils/loadMenuConfigs.d.ts +29 -0
- package/{utils/loadMenuConfigs.ts → dist/utils/loadMenuConfigs.js} +66 -52
- package/dist/utils/mergeAndConcat.d.ts +7 -0
- package/dist/utils/mergeAndConcat.js +72 -0
- package/dist/utils/processAtSymbol.d.ts +4 -0
- package/{utils/processFields.ts → dist/utils/processAtSymbol.js} +5 -9
- package/dist/utils/processInfo.d.ts +24 -0
- package/{utils/process.ts → dist/utils/processInfo.js} +2 -18
- package/dist/utils/response.d.ts +20 -0
- package/{utils/response.ts → dist/utils/response.js} +28 -49
- package/dist/utils/scanAddons.d.ts +17 -0
- package/{utils/scanAddons.ts → dist/utils/scanAddons.js} +7 -41
- package/dist/utils/scanConfig.d.ts +26 -0
- package/{utils/scanConfig.ts → dist/utils/scanConfig.js} +28 -66
- package/dist/utils/scanCoreBuiltins.d.ts +3 -0
- package/dist/utils/scanCoreBuiltins.js +65 -0
- package/dist/utils/scanFiles.d.ts +30 -0
- package/{utils/scanFiles.ts → dist/utils/scanFiles.js} +44 -71
- package/dist/utils/scanSources.d.ts +10 -0
- package/dist/utils/scanSources.js +46 -0
- package/dist/utils/sortModules.d.ts +28 -0
- package/{utils/sortModules.ts → dist/utils/sortModules.js} +26 -66
- package/dist/utils/util.d.ts +84 -0
- package/dist/utils/util.js +262 -0
- package/package.json +26 -34
- package/.gitignore +0 -0
- package/bunfig.toml +0 -3
- package/checks/checkHook.ts +0 -48
- package/checks/checkPlugin.ts +0 -48
- package/configs/presetRegexp.ts +0 -225
- package/docs/README.md +0 -98
- package/docs/api/api.md +0 -1921
- package/docs/guide/examples.md +0 -926
- package/docs/guide/quickstart.md +0 -354
- package/docs/hooks/auth.md +0 -38
- package/docs/hooks/cors.md +0 -28
- package/docs/hooks/hook.md +0 -838
- package/docs/hooks/parser.md +0 -19
- package/docs/hooks/rateLimit.md +0 -47
- package/docs/infra/redis.md +0 -628
- package/docs/plugins/cipher.md +0 -61
- package/docs/plugins/database.md +0 -189
- package/docs/plugins/plugin.md +0 -986
- package/docs/reference/addon.md +0 -510
- package/docs/reference/config.md +0 -573
- package/docs/reference/logger.md +0 -495
- package/docs/reference/sync.md +0 -478
- package/docs/reference/table.md +0 -763
- package/docs/reference/validator.md +0 -620
- package/lib/asyncContext.ts +0 -43
- package/lib/logger.ts +0 -811
- package/loader/loadHooks.ts +0 -51
- package/plugins/config.ts +0 -13
- package/plugins/jwt.ts +0 -15
- package/router/api.ts +0 -130
- package/tsconfig.json +0 -8
- package/types/database.d.ts +0 -541
- package/types/hook.d.ts +0 -25
- package/types/jwt.d.ts +0 -118
- package/types/logger.d.ts +0 -65
- package/types/plugin.d.ts +0 -19
- package/types/redis.d.ts +0 -83
- package/types/sync.d.ts +0 -398
- package/types/table.d.ts +0 -216
- package/types/validate.d.ts +0 -69
- package/utils/arrayKeysToCamel.ts +0 -18
- package/utils/configTypes.ts +0 -3
- package/utils/genShortId.ts +0 -12
- package/utils/importDefault.ts +0 -21
- package/utils/keysToCamel.ts +0 -22
- package/utils/keysToSnake.ts +0 -22
- package/utils/pickFields.ts +0 -19
- package/utils/scanSources.ts +0 -64
- package/utils/sqlLog.ts +0 -37
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志系统 - Bun 环境自定义实现(替换 pino / pino-roll)
|
|
3
|
+
*/
|
|
4
|
+
import { createWriteStream, existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { readdir, stat, unlink } from "node:fs/promises";
|
|
6
|
+
import { hostname as osHostname } from "node:os";
|
|
7
|
+
import { isAbsolute as nodePathIsAbsolute, join as nodePathJoin, resolve as nodePathResolve } from "node:path";
|
|
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
|
+
}
|
|
14
|
+
// 注意:Logger 可能在运行时/测试中被 process.chdir() 影响。
|
|
15
|
+
// 为避免相对路径的 logs 目录随着 cwd 变化,使用模块加载时的初始 cwd 作为锚点。
|
|
16
|
+
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
|
+
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;
|
|
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;
|
|
51
|
+
let instance = null;
|
|
52
|
+
let errorInstance = null;
|
|
53
|
+
let mockInstance = null;
|
|
54
|
+
let appFileSink = null;
|
|
55
|
+
let errorFileSink = null;
|
|
56
|
+
let appConsoleSink = null;
|
|
57
|
+
let didWarnIoError = false;
|
|
58
|
+
let didPruneOldLogFiles = false;
|
|
59
|
+
let didEnsureLogDir = false;
|
|
60
|
+
let didInstallGracefulExitHooks = false;
|
|
61
|
+
let config = {
|
|
62
|
+
debug: 0,
|
|
63
|
+
dir: "./logs",
|
|
64
|
+
console: 1,
|
|
65
|
+
maxSize: 10
|
|
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
|
+
}
|
|
471
|
+
function normalizePositiveInt(value, fallback, min, max) {
|
|
472
|
+
if (typeof value !== "number")
|
|
473
|
+
return fallback;
|
|
474
|
+
if (!Number.isFinite(value))
|
|
475
|
+
return fallback;
|
|
476
|
+
const v = Math.floor(value);
|
|
477
|
+
if (v < min)
|
|
478
|
+
return fallback;
|
|
479
|
+
if (v > max)
|
|
480
|
+
return max;
|
|
481
|
+
return v;
|
|
482
|
+
}
|
|
483
|
+
function resolveLogDir() {
|
|
484
|
+
const rawDir = config.dir || "./logs";
|
|
485
|
+
if (nodePathIsAbsolute(rawDir)) {
|
|
486
|
+
return rawDir;
|
|
487
|
+
}
|
|
488
|
+
return nodePathResolve(INITIAL_CWD, rawDir);
|
|
489
|
+
}
|
|
490
|
+
function ensureLogDirExists() {
|
|
491
|
+
if (didEnsureLogDir)
|
|
492
|
+
return;
|
|
493
|
+
didEnsureLogDir = true;
|
|
494
|
+
const dir = resolveLogDir();
|
|
495
|
+
try {
|
|
496
|
+
if (!existsSync(dir)) {
|
|
497
|
+
mkdirSync(dir, { recursive: true });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
// 不能在 Logger 初始化前调用 Logger 本身,直接抛错即可
|
|
502
|
+
throw new Error(`创建 logs 目录失败: ${dir}. ${error?.message || error}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function pruneOldLogFiles() {
|
|
506
|
+
if (didPruneOldLogFiles)
|
|
507
|
+
return;
|
|
508
|
+
didPruneOldLogFiles = true;
|
|
509
|
+
const dir = resolveLogDir();
|
|
510
|
+
const now = Date.now();
|
|
511
|
+
const cutoff = now - ONE_YEAR_MS;
|
|
512
|
+
try {
|
|
513
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
514
|
+
for (const entry of entries) {
|
|
515
|
+
if (!entry.isFile())
|
|
516
|
+
continue;
|
|
517
|
+
const name = entry.name;
|
|
518
|
+
// 只处理本项目的日志文件前缀
|
|
519
|
+
const isTarget = name.startsWith("app.") || name.startsWith("error.");
|
|
520
|
+
if (!isTarget)
|
|
521
|
+
continue;
|
|
522
|
+
const fullPath = nodePathJoin(dir, name);
|
|
523
|
+
let st;
|
|
524
|
+
try {
|
|
525
|
+
st = await stat(fullPath);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const mtimeMs = typeof st.mtimeMs === "number" ? st.mtimeMs : 0;
|
|
531
|
+
if (mtimeMs > 0 && mtimeMs < cutoff) {
|
|
532
|
+
try {
|
|
533
|
+
await unlink(fullPath);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
// 忽略删除失败(权限/占用等),避免影响服务启动
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// 忽略:目录不存在或无权限等
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* 配置日志
|
|
547
|
+
*/
|
|
548
|
+
export function configure(cfg) {
|
|
549
|
+
// 旧实例可能仍持有文件句柄;这里异步关闭(不阻塞主流程)
|
|
550
|
+
void shutdown();
|
|
551
|
+
config = Object.assign({}, config, cfg);
|
|
552
|
+
instance = null;
|
|
553
|
+
errorInstance = null;
|
|
554
|
+
didPruneOldLogFiles = false;
|
|
555
|
+
didEnsureLogDir = false;
|
|
556
|
+
didWarnIoError = false;
|
|
557
|
+
appFileSink = null;
|
|
558
|
+
errorFileSink = null;
|
|
559
|
+
appConsoleSink = null;
|
|
560
|
+
// 运行时清洗上限(可配置)
|
|
561
|
+
sanitizeDepthLimit = normalizePositiveInt(config.sanitizeDepth, DEFAULT_LOG_SANITIZE_DEPTH, 1, 10);
|
|
562
|
+
sanitizeNodesLimit = normalizePositiveInt(config.sanitizeNodes, DEFAULT_LOG_SANITIZE_NODES, 50, 20000);
|
|
563
|
+
sanitizeObjectKeysLimit = normalizePositiveInt(config.sanitizeObjectKeys, DEFAULT_LOG_OBJECT_KEYS, 10, 5000);
|
|
564
|
+
// 运行时截断上限(可配置)
|
|
565
|
+
maxLogStringLen = normalizePositiveInt(config.maxStringLen, DEFAULT_LOG_STRING_LEN, 20, 200000);
|
|
566
|
+
maxLogArrayItems = normalizePositiveInt(config.maxArrayItems, DEFAULT_LOG_ARRAY_ITEMS, 10, 5000);
|
|
567
|
+
// 仅支持数组配置:excludeFields?: string[]
|
|
568
|
+
const userPatterns = Array.isArray(config.excludeFields) ? config.excludeFields : [];
|
|
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
|
+
}
|
|
580
|
+
const exactSet = new Set();
|
|
581
|
+
const containsMatchers = [];
|
|
582
|
+
for (const pat of patterns) {
|
|
583
|
+
// 精简策略:
|
|
584
|
+
// - 无 *:精确匹配
|
|
585
|
+
// - 有 *:统一按“包含匹配”处理(*x*、x*、*x、a*b 都视为包含 core)
|
|
586
|
+
if (!pat.includes("*")) {
|
|
587
|
+
exactSet.add(pat);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const core = pat.replace(/\*+/g, "").trim();
|
|
591
|
+
if (!core) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
containsMatchers.push(core);
|
|
595
|
+
}
|
|
596
|
+
sensitiveKeySet = exactSet;
|
|
597
|
+
sensitiveContainsMatchers = containsMatchers;
|
|
598
|
+
// 预编译包含匹配:减少每次 isSensitiveKey 的循环开销
|
|
599
|
+
// 注意:patterns 已全部 lowerCase,因此 regex 不需要 i 标志
|
|
600
|
+
if (containsMatchers.length > 0) {
|
|
601
|
+
const escaped = containsMatchers.map((item) => escapeRegExp(item)).filter((item) => item.length > 0);
|
|
602
|
+
if (escaped.length > 0) {
|
|
603
|
+
sensitiveContainsRegex = new RegExp(escaped.join("|"));
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
sensitiveContainsRegex = null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
sensitiveContainsRegex = null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* 设置 Mock Logger(用于测试)
|
|
615
|
+
* @param mock - Mock Logger 实例(形如 {info/warn/error/debug}),传 null 清除 mock
|
|
616
|
+
*/
|
|
617
|
+
export function setMockLogger(mock) {
|
|
618
|
+
mockInstance = mock;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* 获取 Logger 实例(延迟初始化)
|
|
622
|
+
*/
|
|
623
|
+
export function getLogger() {
|
|
624
|
+
// 优先返回 mock 实例(用于测试)
|
|
625
|
+
if (mockInstance)
|
|
626
|
+
return mockInstance;
|
|
627
|
+
if (instance)
|
|
628
|
+
return instance;
|
|
629
|
+
ensureLogDirExists();
|
|
630
|
+
installGracefulExitHooks();
|
|
631
|
+
// 启动时清理过期日志(异步,不阻塞初始化)
|
|
632
|
+
void pruneOldLogFiles();
|
|
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 });
|
|
640
|
+
}
|
|
641
|
+
instance = createSinkLogger({ kind: "app", minLevel: minLevel, fileSink: appFileSink, consoleSink: config.console === 1 ? appConsoleSink : null });
|
|
642
|
+
return instance;
|
|
643
|
+
}
|
|
644
|
+
function getErrorLogger() {
|
|
645
|
+
if (mockInstance)
|
|
646
|
+
return mockInstance;
|
|
647
|
+
if (errorInstance)
|
|
648
|
+
return errorInstance;
|
|
649
|
+
ensureLogDirExists();
|
|
650
|
+
installGracefulExitHooks();
|
|
651
|
+
void pruneOldLogFiles();
|
|
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 });
|
|
658
|
+
return errorInstance;
|
|
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
|
+
}
|
|
793
|
+
function truncateString(val, stats) {
|
|
794
|
+
if (val.length <= maxLogStringLen)
|
|
795
|
+
return val;
|
|
796
|
+
return val.slice(0, maxLogStringLen);
|
|
797
|
+
}
|
|
798
|
+
function isSensitiveKey(key) {
|
|
799
|
+
const lower = String(key).toLowerCase();
|
|
800
|
+
if (sensitiveKeySet.has(lower))
|
|
801
|
+
return true;
|
|
802
|
+
if (sensitiveContainsRegex) {
|
|
803
|
+
return sensitiveContainsRegex.test(lower);
|
|
804
|
+
}
|
|
805
|
+
for (const part of sensitiveContainsMatchers) {
|
|
806
|
+
if (lower.includes(part))
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
function safeToStringMasked(val, visited, stats) {
|
|
812
|
+
if (typeof val === "string")
|
|
813
|
+
return val;
|
|
814
|
+
if (val instanceof Error) {
|
|
815
|
+
const name = val.name || "Error";
|
|
816
|
+
const message = val.message || "";
|
|
817
|
+
const stack = typeof val.stack === "string" ? val.stack : "";
|
|
818
|
+
const errObj = {
|
|
819
|
+
name: name,
|
|
820
|
+
message: message,
|
|
821
|
+
stack: stack
|
|
822
|
+
};
|
|
823
|
+
try {
|
|
824
|
+
return JSON.stringify(errObj);
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
return `${name}: ${message}`;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (val && typeof val === "object") {
|
|
831
|
+
if (visited.has(val)) {
|
|
832
|
+
return "[Circular]";
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
const localVisited = visited;
|
|
837
|
+
const replacer = (k, v) => {
|
|
838
|
+
// JSON.stringify 的根节点 key 为空字符串
|
|
839
|
+
if (k && isSensitiveKey(k)) {
|
|
840
|
+
return "[MASKED]";
|
|
841
|
+
}
|
|
842
|
+
if (v && typeof v === "object") {
|
|
843
|
+
if (localVisited.has(v)) {
|
|
844
|
+
return "[Circular]";
|
|
845
|
+
}
|
|
846
|
+
localVisited.add(v);
|
|
847
|
+
}
|
|
848
|
+
return v;
|
|
849
|
+
};
|
|
850
|
+
return JSON.stringify(val, replacer);
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
try {
|
|
854
|
+
return String(val);
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
return "[Unserializable]";
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function sanitizeErrorValue(err, stats) {
|
|
862
|
+
const errObj = {
|
|
863
|
+
name: err.name || "Error",
|
|
864
|
+
message: truncateString(err.message || "", stats)
|
|
865
|
+
};
|
|
866
|
+
if (typeof err.stack === "string") {
|
|
867
|
+
errObj.stack = truncateString(err.stack, stats);
|
|
868
|
+
}
|
|
869
|
+
return errObj;
|
|
870
|
+
}
|
|
871
|
+
function stringifyPreview(val, visited, stats) {
|
|
872
|
+
const str = safeToStringMasked(val, visited, stats);
|
|
873
|
+
return truncateString(str, stats);
|
|
874
|
+
}
|
|
875
|
+
function sanitizeValueLimited(val, visited, stats) {
|
|
876
|
+
if (val === null || val === undefined)
|
|
877
|
+
return val;
|
|
878
|
+
if (typeof val === "string")
|
|
879
|
+
return truncateString(val, stats);
|
|
880
|
+
if (typeof val === "number")
|
|
881
|
+
return val;
|
|
882
|
+
if (typeof val === "boolean")
|
|
883
|
+
return val;
|
|
884
|
+
if (typeof val === "bigint")
|
|
885
|
+
return val;
|
|
886
|
+
if (val instanceof Error) {
|
|
887
|
+
return sanitizeErrorValue(val, stats);
|
|
888
|
+
}
|
|
889
|
+
// 仅支持数组 + plain object 的结构化清洗,其余类型走字符串预览。
|
|
890
|
+
const isArr = Array.isArray(val);
|
|
891
|
+
const isObj = isPlainObject(val);
|
|
892
|
+
if (!isArr && !isObj) {
|
|
893
|
+
return stringifyPreview(val, visited, stats);
|
|
894
|
+
}
|
|
895
|
+
// 防环(根节点)
|
|
896
|
+
if (visited.has(val)) {
|
|
897
|
+
return "[Circular]";
|
|
898
|
+
}
|
|
899
|
+
visited.add(val);
|
|
900
|
+
const rootOut = isArr ? [] : {};
|
|
901
|
+
const stack = [{ src: val, dst: rootOut, depth: 1 }];
|
|
902
|
+
let nodes = 0;
|
|
903
|
+
const tryAssign = (dst, key, child, depth) => {
|
|
904
|
+
if (child === null || child === undefined) {
|
|
905
|
+
dst[key] = child;
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (typeof child === "string") {
|
|
909
|
+
dst[key] = truncateString(child, stats);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (typeof child === "number" || typeof child === "boolean" || typeof child === "bigint") {
|
|
913
|
+
dst[key] = child;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (child instanceof Error) {
|
|
917
|
+
dst[key] = sanitizeErrorValue(child, stats);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const childIsArr = Array.isArray(child);
|
|
921
|
+
const childIsObj = isPlainObject(child);
|
|
922
|
+
if (!childIsArr && !childIsObj) {
|
|
923
|
+
dst[key] = stringifyPreview(child, visited, stats);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
// 深度/节点数上限:超出则降级为字符串预览
|
|
927
|
+
if (depth >= sanitizeDepthLimit) {
|
|
928
|
+
dst[key] = stringifyPreview(child, visited, stats);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (nodes >= sanitizeNodesLimit) {
|
|
932
|
+
dst[key] = stringifyPreview(child, visited, stats);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
// 防环
|
|
936
|
+
if (visited.has(child)) {
|
|
937
|
+
dst[key] = "[Circular]";
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
visited.add(child);
|
|
941
|
+
const childOut = childIsArr ? [] : {};
|
|
942
|
+
dst[key] = childOut;
|
|
943
|
+
stack.push({ src: child, dst: childOut, depth: depth + 1 });
|
|
944
|
+
};
|
|
945
|
+
while (stack.length > 0) {
|
|
946
|
+
const frame = stack.pop();
|
|
947
|
+
nodes = nodes + 1;
|
|
948
|
+
if (nodes > sanitizeNodesLimit) {
|
|
949
|
+
// 超出节点上限:不再深入(已入栈的节点会被忽略,留空结构由上层兜底预览)。
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
if (Array.isArray(frame.src)) {
|
|
953
|
+
const arr = frame.src;
|
|
954
|
+
const len = arr.length;
|
|
955
|
+
const limit = len > maxLogArrayItems ? maxLogArrayItems : len;
|
|
956
|
+
for (let i = 0; i < limit; i++) {
|
|
957
|
+
tryAssign(frame.dst, i, arr[i], frame.depth);
|
|
958
|
+
}
|
|
959
|
+
if (len > maxLogArrayItems) {
|
|
960
|
+
// ignore omitted items
|
|
961
|
+
}
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (isPlainObject(frame.src)) {
|
|
965
|
+
const entries = Object.entries(frame.src);
|
|
966
|
+
const len = entries.length;
|
|
967
|
+
const limit = len > sanitizeObjectKeysLimit ? sanitizeObjectKeysLimit : len;
|
|
968
|
+
for (let i = 0; i < limit; i++) {
|
|
969
|
+
const key = entries[i][0];
|
|
970
|
+
const child = entries[i][1];
|
|
971
|
+
if (isSensitiveKey(key)) {
|
|
972
|
+
frame.dst[key] = "[MASKED]";
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
tryAssign(frame.dst, key, child, frame.depth);
|
|
976
|
+
}
|
|
977
|
+
if (len > sanitizeObjectKeysLimit) {
|
|
978
|
+
// ignore omitted keys
|
|
979
|
+
}
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
// 兜底:理论上不会到这里(frame 只会压入 array/plain object)
|
|
983
|
+
}
|
|
984
|
+
return rootOut;
|
|
985
|
+
}
|
|
986
|
+
function sanitizeTopValue(val, visited) {
|
|
987
|
+
return sanitizeValueLimited(val, visited, {});
|
|
988
|
+
}
|
|
989
|
+
function sanitizeLogObject(obj) {
|
|
990
|
+
const visited = new WeakSet();
|
|
991
|
+
const out = {};
|
|
992
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
993
|
+
if (isSensitiveKey(key)) {
|
|
994
|
+
out[key] = "[MASKED]";
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
out[key] = sanitizeTopValue(val, visited);
|
|
998
|
+
}
|
|
999
|
+
return out;
|
|
1000
|
+
}
|
|
1001
|
+
function metaToObject() {
|
|
1002
|
+
const meta = getCtx();
|
|
1003
|
+
if (!meta)
|
|
1004
|
+
return null;
|
|
1005
|
+
const durationSinceNowMs = Date.now() - meta.now;
|
|
1006
|
+
const obj = {
|
|
1007
|
+
requestId: meta.requestId,
|
|
1008
|
+
method: meta.method,
|
|
1009
|
+
route: meta.route,
|
|
1010
|
+
ip: meta.ip,
|
|
1011
|
+
now: meta.now,
|
|
1012
|
+
durationSinceNowMs: durationSinceNowMs
|
|
1013
|
+
};
|
|
1014
|
+
// userId / roleCode 默认写入
|
|
1015
|
+
obj.userId = meta.userId;
|
|
1016
|
+
obj.roleCode = meta.roleCode;
|
|
1017
|
+
obj.nickname = meta.nickname;
|
|
1018
|
+
obj.roleType = meta.roleType;
|
|
1019
|
+
return obj;
|
|
1020
|
+
}
|
|
1021
|
+
function mergeMetaIntoObject(input, meta) {
|
|
1022
|
+
const merged = {};
|
|
1023
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1024
|
+
merged[key] = value;
|
|
1025
|
+
}
|
|
1026
|
+
// 只补齐、不覆盖:允许把 undefined / null / 空字符串写入(由日志底层序列化决定是否展示)
|
|
1027
|
+
const keys = ["requestId", "method", "route", "ip", "now", "durationSinceNowMs", "userId", "roleCode", "nickname", "roleType"];
|
|
1028
|
+
for (const key of keys) {
|
|
1029
|
+
if (merged[key] === undefined)
|
|
1030
|
+
merged[key] = meta[key];
|
|
1031
|
+
}
|
|
1032
|
+
return merged;
|
|
1033
|
+
}
|
|
1034
|
+
function withRequestMeta(args) {
|
|
1035
|
+
const meta = metaToObject();
|
|
1036
|
+
if (!meta)
|
|
1037
|
+
return args;
|
|
1038
|
+
if (args.length === 0)
|
|
1039
|
+
return args;
|
|
1040
|
+
const first = args[0];
|
|
1041
|
+
const second = args.length > 1 ? args[1] : undefined;
|
|
1042
|
+
// 兼容:Logger.error("xxx", err)
|
|
1043
|
+
if (typeof first === "string" && second instanceof Error) {
|
|
1044
|
+
const obj = {
|
|
1045
|
+
err: second
|
|
1046
|
+
};
|
|
1047
|
+
const merged = mergeMetaIntoObject(obj, meta);
|
|
1048
|
+
return [merged, first, ...args.slice(2)];
|
|
1049
|
+
}
|
|
1050
|
+
// pino 原生:logger.error(err, msg)
|
|
1051
|
+
if (first instanceof Error) {
|
|
1052
|
+
const msg = typeof second === "string" ? second : undefined;
|
|
1053
|
+
const obj = {
|
|
1054
|
+
err: first
|
|
1055
|
+
};
|
|
1056
|
+
const merged = mergeMetaIntoObject(obj, meta);
|
|
1057
|
+
if (msg)
|
|
1058
|
+
return [merged, msg, ...args.slice(2)];
|
|
1059
|
+
return [merged, ...args.slice(1)];
|
|
1060
|
+
}
|
|
1061
|
+
// 纯字符串:Logger.info("msg") -> logger.info(meta, "msg")
|
|
1062
|
+
if (typeof first === "string") {
|
|
1063
|
+
return [meta, ...args];
|
|
1064
|
+
}
|
|
1065
|
+
// 对象:Logger.info(obj, msg?) -> 合并 meta(不覆盖显式字段)
|
|
1066
|
+
if (isPlainObject(first)) {
|
|
1067
|
+
const merged = mergeMetaIntoObject(first, meta);
|
|
1068
|
+
return [merged, ...args.slice(1)];
|
|
1069
|
+
}
|
|
1070
|
+
return args;
|
|
1071
|
+
}
|
|
1072
|
+
function withRequestMetaTyped(args) {
|
|
1073
|
+
// 复用现有逻辑(保持行为一致),只收敛类型
|
|
1074
|
+
return withRequestMeta(args);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* 日志实例(延迟初始化)
|
|
1078
|
+
*/
|
|
1079
|
+
export const Logger = {
|
|
1080
|
+
info(...args) {
|
|
1081
|
+
if (args.length === 0)
|
|
1082
|
+
return;
|
|
1083
|
+
const logger = getLogger();
|
|
1084
|
+
const finalArgs = withRequestMetaTyped(args);
|
|
1085
|
+
if (finalArgs.length === 0)
|
|
1086
|
+
return;
|
|
1087
|
+
if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
|
|
1088
|
+
finalArgs[0] = sanitizeLogObject(finalArgs[0]);
|
|
1089
|
+
}
|
|
1090
|
+
const ret = logger.info.apply(logger, finalArgs);
|
|
1091
|
+
return ret;
|
|
1092
|
+
},
|
|
1093
|
+
warn(...args) {
|
|
1094
|
+
if (args.length === 0)
|
|
1095
|
+
return;
|
|
1096
|
+
const logger = getLogger();
|
|
1097
|
+
const finalArgs = withRequestMetaTyped(args);
|
|
1098
|
+
if (finalArgs.length === 0)
|
|
1099
|
+
return;
|
|
1100
|
+
if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
|
|
1101
|
+
finalArgs[0] = sanitizeLogObject(finalArgs[0]);
|
|
1102
|
+
}
|
|
1103
|
+
const ret = logger.warn.apply(logger, finalArgs);
|
|
1104
|
+
return ret;
|
|
1105
|
+
},
|
|
1106
|
+
error(...args) {
|
|
1107
|
+
if (args.length === 0)
|
|
1108
|
+
return;
|
|
1109
|
+
const logger = getLogger();
|
|
1110
|
+
const finalArgs = withRequestMetaTyped(args);
|
|
1111
|
+
if (finalArgs.length === 0)
|
|
1112
|
+
return;
|
|
1113
|
+
if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
|
|
1114
|
+
finalArgs[0] = sanitizeLogObject(finalArgs[0]);
|
|
1115
|
+
}
|
|
1116
|
+
const ret = logger.error.apply(logger, finalArgs);
|
|
1117
|
+
// 测试场景:启用 mock 时不做镜像,避免调用次数翻倍
|
|
1118
|
+
if (mockInstance)
|
|
1119
|
+
return ret;
|
|
1120
|
+
// error 专属文件:始终镜像一份
|
|
1121
|
+
const errorLogger = getErrorLogger();
|
|
1122
|
+
errorLogger.error.apply(errorLogger, finalArgs);
|
|
1123
|
+
return ret;
|
|
1124
|
+
},
|
|
1125
|
+
debug(...args) {
|
|
1126
|
+
if (args.length === 0)
|
|
1127
|
+
return;
|
|
1128
|
+
const logger = getLogger();
|
|
1129
|
+
const finalArgs = withRequestMetaTyped(args);
|
|
1130
|
+
if (finalArgs.length === 0)
|
|
1131
|
+
return;
|
|
1132
|
+
if (finalArgs.length > 0 && isPlainObject(finalArgs[0])) {
|
|
1133
|
+
finalArgs[0] = sanitizeLogObject(finalArgs[0]);
|
|
1134
|
+
}
|
|
1135
|
+
const ret = logger.debug.apply(logger, finalArgs);
|
|
1136
|
+
return ret;
|
|
1137
|
+
},
|
|
1138
|
+
async flush() {
|
|
1139
|
+
await flush();
|
|
1140
|
+
},
|
|
1141
|
+
configure: configure,
|
|
1142
|
+
setMock: setMockLogger,
|
|
1143
|
+
shutdown: shutdown
|
|
1144
|
+
};
|