befly 3.61.0 → 3.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/logger.js CHANGED
@@ -2,19 +2,18 @@
2
2
  * 日志系统 - Bun 环境自定义实现(替换 pino / pino-roll)
3
3
  */
4
4
 
5
- import { createWriteStream, existsSync, mkdirSync } from "node:fs";
6
- import { stat } from "node:fs/promises";
5
+ import { createWriteStream, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
7
6
  import { join as nodePathJoin, resolve as nodePathResolve } from "node:path";
8
7
 
9
8
  import { formatYmdHms } from "../utils/datetime.js";
10
- import { isFiniteNumber, isNumber, isPlainObject, isString } from "../utils/is.js";
9
+ import { isPlainObject } from "../utils/is.js";
11
10
  import { buildSensitiveKeyMatcher, sanitizeLogObject } from "../utils/loggerUtils.js";
12
11
  import { normalizePositiveInt } from "../utils/util.js";
13
12
 
14
13
  // 注意:Logger 可能在运行时/测试中被 process.chdir() 影响。
15
14
  // 为避免相对路径的 logs 目录随着 cwd 变化,使用模块加载时的初始 cwd 作为锚点。
16
- const INITIAL_CWD = process.cwd();
17
- const RUNTIME_LOG_DIR = nodePathResolve(INITIAL_CWD, "logs");
15
+ const DEFAULT_MAX_SIZE_MB = 20;
16
+ const MAX_FILE_BYTES = DEFAULT_MAX_SIZE_MB * 1024 * 1024;
18
17
 
19
18
  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"];
20
19
 
@@ -32,168 +31,87 @@ let errorFileSink = null;
32
31
 
33
32
  let config = {
34
33
  debug: 0,
35
- dir: RUNTIME_LOG_DIR,
36
- maxSize: 20,
34
+ dir: "",
37
35
  runtimeEnv: "production"
38
36
  };
39
37
 
40
38
  function safeWriteStderr(msg) {
41
- try {
42
- process.stderr.write(`${msg}\n`);
43
- } catch {
44
- // ignore
45
- }
46
- }
47
-
48
- function shiftBatchFromPending(pending, maxBatchBytes) {
49
- const parts = [];
50
- let bytes = 0;
51
-
52
- while (pending.length > 0) {
53
- const next = pending[0];
54
- const nextBytes = Buffer.byteLength(next);
55
- if (parts.length > 0 && bytes + nextBytes > maxBatchBytes) {
56
- break;
57
- }
58
- parts.push(next);
59
- bytes = bytes + nextBytes;
60
- pending.shift();
61
- }
62
-
63
- return { chunk: parts.join(""), bytes: bytes };
39
+ process.stderr.write(`${msg}\n`);
64
40
  }
65
41
 
66
42
  class LogFileSink {
67
- constructor(options) {
68
- this.prefix = options.prefix;
69
- this.maxFileBytes = options.maxFileBytes;
43
+ constructor(prefix) {
44
+ this.prefix = prefix;
70
45
 
71
46
  this.stream = null;
72
47
  this.streamDate = "";
73
48
  this.streamIndex = 0;
74
49
  this.streamSizeBytes = 0;
75
50
  this.disabled = false;
76
-
77
- this.pending = [];
78
- this.pendingBytes = 0;
79
- this.scheduledTimer = null;
80
- this.flushing = false;
81
-
82
- this.maxBufferBytes = 10 * 1024 * 1024;
83
- this.flushDelayMs = 10;
84
- this.maxBatchBytes = 64 * 1024;
85
51
  }
86
52
 
87
- enqueue(line) {
53
+ write(line) {
88
54
  if (this.disabled) return;
89
55
 
90
- const bytes = Buffer.byteLength(line);
91
- if (this.pendingBytes + bytes > this.maxBufferBytes) {
92
- // buffer 满:统一丢弃新日志(不区分 level)
93
- return;
56
+ this.ensureStreamReady(line.length);
57
+ if (this.stream) {
58
+ this.stream.write(line);
59
+ this.streamSizeBytes += line.length;
94
60
  }
95
-
96
- this.pending.push(line);
97
- this.pendingBytes = this.pendingBytes + bytes;
98
-
99
- this.scheduleFlush();
100
61
  }
101
62
 
102
63
  async flushNow() {
103
- this.clearScheduledFlush();
104
- await this.flush();
64
+ return this.closeStream();
105
65
  }
106
66
 
107
67
  async shutdown() {
108
- this.clearScheduledFlush();
109
- await this.flush();
110
- await this.closeStream();
68
+ return this.closeStream();
111
69
  }
112
70
 
113
- scheduleFlush() {
114
- if (this.scheduledTimer) return;
71
+ getFilePath(date, index) {
72
+ if (this.prefix === "debug") {
73
+ return nodePathJoin(config.dir, "dev.log");
74
+ }
115
75
 
116
- this.scheduledTimer = setTimeout(() => {
117
- // timer 触发时先清空句柄,避免 flush 内再次 schedule 时被认为“已安排”。
118
- this.scheduledTimer = null;
119
- this.flush();
120
- }, this.flushDelayMs);
76
+ const suffix = index > 0 ? `.${index}` : "";
77
+ const filename = this.prefix === "app" ? `${date}${suffix}.log` : `${this.prefix}.${date}${suffix}.log`;
78
+ return nodePathJoin(config.dir, filename);
121
79
  }
122
80
 
123
- clearScheduledFlush() {
124
- if (!this.scheduledTimer) return;
125
- clearTimeout(this.scheduledTimer);
126
- this.scheduledTimer = null;
127
- }
81
+ ensureStreamReady(nextChunkBytes) {
82
+ const date = formatYmdHms(Date.now(), "date");
128
83
 
129
- async flush() {
130
- if (this.flushing || this.pending.length === 0) return;
131
- this.flushing = true;
84
+ if (this.stream && this.streamDate && date !== this.streamDate) {
85
+ this.closeStreamSync();
86
+ }
132
87
 
133
- try {
134
- while (this.pending.length > 0) {
135
- const batch = shiftBatchFromPending(this.pending, this.maxBatchBytes);
136
- this.pendingBytes = this.pendingBytes - batch.bytes;
137
-
138
- const ok = await this.writeChunk(batch.chunk, batch.bytes);
139
- if (!ok) {
140
- // writer 已禁用/失败:清空剩余 pending
141
- this.pending = [];
142
- this.pendingBytes = 0;
143
- break;
144
- }
145
- }
146
- } finally {
147
- this.flushing = false;
148
- if (this.pending.length > 0) {
149
- this.scheduleFlush();
150
- }
88
+ if (!this.stream) {
89
+ this.streamDate = date;
90
+ this.streamIndex = 0;
91
+ this.streamSizeBytes = this.getFileSize(this.getFilePath(date, 0));
92
+
93
+ this.openStream(this.getFilePath(date, 0));
151
94
  }
152
- }
153
95
 
154
- async writeChunk(chunk, chunkBytes) {
155
- if (this.disabled) return false;
96
+ if (this.prefix === "debug") {
97
+ return;
98
+ }
156
99
 
157
- try {
158
- await this.ensureStreamReady(chunkBytes);
159
- if (!this.stream) {
160
- // 文件 sink 已被禁用或打开失败
161
- return false;
162
- }
100
+ if (this.stream && this.streamSizeBytes + nextChunkBytes > MAX_FILE_BYTES) {
101
+ this.closeStreamSync();
102
+ this.streamIndex += 1;
103
+ this.streamSizeBytes = 0;
163
104
 
164
- const ok = this.stream.write(chunk);
165
- if (!ok) {
166
- await new Promise((resolve) => {
167
- const stream = this.stream;
168
- if (!stream) {
169
- resolve();
170
- return;
171
- }
172
- stream.once("drain", () => resolve());
173
- });
174
- }
175
- this.streamSizeBytes = this.streamSizeBytes + chunkBytes;
176
- return true;
177
- } catch (error) {
178
- safeWriteStderr(`[Logger] file sink flush error (${this.prefix}): ${error?.message || error}`);
179
- this.disabled = true;
180
- await this.closeStream();
181
- return false;
105
+ this.openStream(this.getFilePath(date, this.streamIndex));
182
106
  }
183
107
  }
184
108
 
185
- async closeStream() {
186
- if (!this.stream) return;
187
- const st = this.stream;
188
- this.stream = null;
189
-
190
- await new Promise((resolve) => {
191
- try {
192
- st.end(() => resolve());
193
- } catch {
194
- resolve();
195
- }
196
- });
109
+ getFileSize(filePath) {
110
+ try {
111
+ return statSync(filePath).size;
112
+ } catch {
113
+ return 0;
114
+ }
197
115
  }
198
116
 
199
117
  openStream(filePath) {
@@ -202,7 +120,7 @@ class LogFileSink {
202
120
  this.stream.on("error", (error) => {
203
121
  safeWriteStderr(`[Logger] file sink error (${this.prefix}): ${error?.message || error}`);
204
122
  this.disabled = true;
205
- this.closeStream();
123
+ this.closeStreamSync();
206
124
  });
207
125
  } catch (error) {
208
126
  safeWriteStderr(`[Logger] createWriteStream failed (${this.prefix}): ${error?.message || error}`);
@@ -211,91 +129,51 @@ class LogFileSink {
211
129
  }
212
130
  }
213
131
 
214
- getFilePath(date, index) {
215
- const suffix = index > 0 ? `.${index}` : "";
216
- const filename = this.prefix === "app" ? `${date}${suffix}.log` : `${this.prefix}.${date}${suffix}.log`;
217
- return nodePathJoin(RUNTIME_LOG_DIR, filename);
218
- }
219
-
220
- async ensureStreamReady(nextChunkBytes) {
221
- const date = formatYmdHms(Date.now(), "date");
222
-
223
- // 日期变化:切新文件
224
- if (this.stream && this.streamDate && date !== this.streamDate) {
225
- await this.closeStream();
226
- this.streamDate = "";
227
- this.streamIndex = 0;
228
- this.streamSizeBytes = 0;
132
+ closeStreamSync() {
133
+ if (this.stream) {
134
+ this.stream.end();
229
135
  }
136
+ this.stream = null;
137
+ this.streamDate = "";
138
+ this.streamIndex = 0;
139
+ this.streamSizeBytes = 0;
140
+ }
230
141
 
231
- // 首次打开
232
- if (!this.stream) {
233
- const filePath = this.getFilePath(date, 0);
234
- let size = 0;
235
- try {
236
- const st = await stat(filePath);
237
- size = isNumber(st.size) ? st.size : 0;
238
- } catch {
239
- size = 0;
142
+ closeStream() {
143
+ return new Promise((resolve) => {
144
+ const st = this.stream;
145
+ if (!st) {
146
+ resolve();
147
+ return;
240
148
  }
241
-
242
- this.streamDate = date;
149
+ this.stream = null;
150
+ this.streamDate = "";
243
151
  this.streamIndex = 0;
244
- this.streamSizeBytes = size;
245
-
246
- this.openStream(filePath);
247
- if (!this.stream) return;
248
- }
249
-
250
- // 大小滚动
251
- if (this.stream && this.maxFileBytes > 0 && this.streamSizeBytes + nextChunkBytes > this.maxFileBytes) {
252
- await this.closeStream();
253
- this.streamIndex = this.streamIndex + 1;
254
- const filePath = this.getFilePath(date, this.streamIndex);
255
- this.streamDate = date;
256
152
  this.streamSizeBytes = 0;
257
-
258
- this.openStream(filePath);
259
- if (!this.stream) return;
260
- }
153
+ st.end(() => resolve());
154
+ });
261
155
  }
262
156
  }
263
157
 
264
158
  export async function flush() {
265
- // 测试场景:mock logger 不需要 flush
266
159
  if (mockInstance) return;
267
160
 
268
161
  for (const sink of [appFileSink, errorFileSink]) {
269
- if (!sink) {
270
- continue;
271
- }
272
-
273
- try {
162
+ if (sink) {
274
163
  await sink.flushNow();
275
- } catch {
276
- // ignore
277
164
  }
278
165
  }
279
166
  }
280
167
 
281
168
  export async function shutdown() {
282
- // 测试场景:mock logger 不需要 shutdown
283
169
  if (mockInstance) return;
284
170
 
285
- // 重要:shutdown 可能与后续 Logger 调用并发。
286
- // 因此这里捕获“当前的旧 sink/instance 快照”,只关闭这些快照,避免把新创建的 sink 一并清掉。
287
171
  const currentAppFileSink = appFileSink;
288
172
  const currentErrorFileSink = errorFileSink;
289
173
 
290
174
  for (const sink of [currentAppFileSink, currentErrorFileSink]) {
291
- if (!sink) {
292
- continue;
293
- }
294
-
295
- try {
175
+ if (sink) {
296
176
  await sink.shutdown();
297
- } catch {
298
- // ignore
299
177
  }
300
178
  }
301
179
 
@@ -305,65 +183,58 @@ export async function shutdown() {
305
183
  if (errorFileSink === currentErrorFileSink) {
306
184
  errorFileSink = null;
307
185
  }
186
+ }
308
187
 
309
- // shutdown 后允许下一次重新初始化时再次校验/创建目录(测试会清理目录,避免 ENOENT)
310
- // 无需缓存状态:确保目录存在是幂等的。
188
+ function getRuntimeLogDir() {
189
+ return nodePathResolve(process.cwd(), "logs");
311
190
  }
312
191
 
313
- function ensureLogDirExists() {
192
+ function prepareLogDir(runtimeEnv, shouldClearDev = false) {
193
+ const logDir = getRuntimeLogDir(runtimeEnv);
194
+ const devLogPath = nodePathResolve(logDir, "dev.log");
195
+
314
196
  try {
315
- if (!existsSync(RUNTIME_LOG_DIR)) {
316
- mkdirSync(RUNTIME_LOG_DIR, { recursive: true });
197
+ if (!existsSync(logDir)) {
198
+ mkdirSync(logDir, { recursive: true });
199
+ }
200
+ if (shouldClearDev && runtimeEnv === "development" && existsSync(devLogPath)) {
201
+ rmSync(devLogPath, { force: true });
317
202
  }
318
203
  } catch (error) {
319
- // 不能在 Logger 初始化前调用 Logger 本身,直接抛错即可
320
- throw new Error(`创建 logs 目录失败: ${RUNTIME_LOG_DIR}. ${error?.message || error}`, {
204
+ throw new Error(`创建日志目录失败: ${logDir}. ${error?.message || error}`, {
321
205
  cause: error,
322
206
  code: "runtime"
323
207
  });
324
208
  }
209
+
210
+ return logDir;
325
211
  }
326
212
 
327
- // 方案B:删除“启动时清理旧日志”功能(减少 I/O 与复杂度)。
213
+ function ensureLogDirExists() {
214
+ config.dir = prepareLogDir(config.runtimeEnv);
215
+ }
328
216
 
329
217
  /**
330
218
  * 配置日志
331
219
  */
332
220
  export function configure(cfg) {
333
- // 旧实例可能仍持有文件句柄;这里异步关闭(不阻塞主流程)
334
221
  shutdown();
335
222
 
336
- // 方案B:每次 configure 都从默认配置重新构建(避免继承上一次配置造成测试/运行时污染)
337
223
  config = Object.assign(
338
224
  {
339
225
  debug: 0,
340
- dir: RUNTIME_LOG_DIR,
341
- maxSize: 20,
226
+ dir: "",
342
227
  runtimeEnv: "production"
343
228
  },
344
229
  cfg
345
230
  );
346
231
 
347
- // 约束:日志目录始终固定在当前项目根目录 logs,不允许外部覆盖。
348
- config.dir = RUNTIME_LOG_DIR;
349
-
350
- if (cfg && !isString(cfg.runtimeEnv) && isString(cfg.runMode)) {
351
- config.runtimeEnv = cfg.runMode;
352
- }
353
-
354
- // maxSize:仅按 MB 计算,且强制范围 10..100
355
- {
356
- const raw = config.maxSize;
357
- let mb = isFiniteNumber(raw) ? raw : 20;
358
- if (mb < 10) mb = 10;
359
- if (mb > 100) mb = 100;
360
- config.maxSize = mb;
361
- }
232
+ config.dir = getRuntimeLogDir(config.runtimeEnv);
233
+ prepareLogDir(config.runtimeEnv, true);
362
234
 
363
235
  appFileSink = null;
364
236
  errorFileSink = null;
365
237
 
366
- // 运行时清洗上限:只保留深度/节点/对象键数量保护。
367
238
  sanitizeOptions = {
368
239
  sanitizeDepth: normalizePositiveInt(config.sanitizeDepth, 5, 1, 10),
369
240
  sanitizeNodes: normalizePositiveInt(config.sanitizeNodes, 5000, 50, 20000),
@@ -381,87 +252,63 @@ export function setMockLogger(mock) {
381
252
  }
382
253
 
383
254
  function buildJsonLine(level, timeMs, record) {
384
- const base = {
255
+ const out = {
385
256
  level: level,
386
257
  time: formatYmdHms(timeMs),
387
258
  pid: process.pid
388
259
  };
389
260
 
390
- // 目标:让 level/time/timeFormat/pid/hostname 在 JSON 行首出现(更易读),同时保持 JSONL 可解析。
391
- // 约束:record 不允许覆盖基础字段。
392
- // 实现:先写入基础字段,再按 record 的 key 顺序追加其它字段。
393
261
  if (record && isPlainObject(record)) {
394
- const out = {};
395
- out["level"] = base["level"];
396
- out["time"] = base["time"];
397
- out["pid"] = base["pid"];
398
-
399
262
  for (const key of Object.keys(record)) {
400
- if (key === "level") continue;
401
- if (key === "time") continue;
402
- if (key === "pid") continue;
263
+ if (key === "level" || key === "time" || key === "pid") {
264
+ continue;
265
+ }
403
266
  out[key] = record[key];
404
267
  }
405
268
 
406
- // msg 允许保留 record.msg(若存在),否则补齐空字符串。
407
- if (record.msg !== undefined) {
408
- out["msg"] = record.msg;
409
- } else if (out["msg"] === undefined) {
410
- out["msg"] = "";
411
- }
412
-
413
- try {
414
- return `${JSON.stringify(out)}\n`;
415
- } catch {
416
- try {
417
- return `${JSON.stringify({ level: out["level"], time: out["time"], pid: out["pid"], msg: "[Unserializable log record]" })}\n`;
418
- } catch {
419
- return '{"msg":"[Unserializable log record]"}\n';
420
- }
269
+ if (out.msg === undefined) {
270
+ out.msg = "";
421
271
  }
422
- }
423
-
424
- if (base["msg"] === undefined) {
425
- base["msg"] = "";
272
+ } else {
273
+ out.msg = "";
426
274
  }
427
275
 
428
276
  try {
429
- return `${JSON.stringify(base)}\n`;
277
+ return `${JSON.stringify(out)}\n`;
430
278
  } catch {
431
- try {
432
- return `${JSON.stringify({ level: base["level"], time: base["time"], pid: base["pid"], msg: "[Unserializable log record]" })}\n`;
433
- } catch {
434
- return '{"msg":"[Unserializable log record]"}\n';
435
- }
279
+ return `${JSON.stringify({ level: out.level, time: out.time, pid: out.pid, msg: "[Unserializable log record]" })}\n`;
436
280
  }
437
281
  }
438
282
 
439
283
  function ensureSinksReady(kind) {
440
284
  ensureLogDirExists();
441
285
 
442
- const maxSizeMb = isFiniteNumber(config.maxSize) ? config.maxSize : 20;
443
- const maxFileBytes = Math.floor(maxSizeMb * 1024 * 1024);
286
+ if (config.runtimeEnv === "development") {
287
+ if (!appFileSink) {
288
+ appFileSink = new LogFileSink("debug");
289
+ }
290
+ return appFileSink;
291
+ }
444
292
 
445
293
  if (kind === "app") {
446
294
  if (!appFileSink) {
447
- appFileSink = new LogFileSink({ prefix: "app", maxFileBytes: maxFileBytes });
295
+ appFileSink = new LogFileSink("app");
448
296
  }
449
- return { fileSink: appFileSink };
297
+ return appFileSink;
450
298
  }
451
299
 
452
300
  if (!errorFileSink) {
453
- errorFileSink = new LogFileSink({ prefix: "error", maxFileBytes: maxFileBytes });
301
+ errorFileSink = new LogFileSink("error");
454
302
  }
455
-
456
- return { fileSink: errorFileSink };
303
+ return errorFileSink;
457
304
  }
458
305
 
459
306
  function writeJsonl(kind, level, record) {
460
- const sinks = ensureSinksReady(kind);
307
+ const sink = ensureSinksReady(kind);
461
308
  const time = Date.now();
462
309
  const sanitizedRecord = sanitizeLogObject(record, sanitizeOptions);
463
310
  const fileLine = buildJsonLine(level, time, sanitizedRecord);
464
- sinks.fileSink.enqueue(fileLine);
311
+ sink.write(fileLine);
465
312
  }
466
313
 
467
314
  // 对象清洗/脱敏逻辑已下沉到 utils/loggerUtils.js(减少 logger.js 复杂度)。
@@ -507,15 +354,7 @@ function logWrite(level, input) {
507
354
  // 测试场景:mock logger 走同步写入,并在入口进行清洗/脱敏控制
508
355
  if (mockInstance) {
509
356
  const sanitized = sanitizeLogObject(record0, sanitizeOptions);
510
- if (level === "info") {
511
- mockInstance.info(sanitized);
512
- } else if (level === "warn") {
513
- mockInstance.warn(sanitized);
514
- } else if (level === "error") {
515
- mockInstance.error(sanitized);
516
- } else {
517
- mockInstance.debug(sanitized);
518
- }
357
+ mockInstance[level](sanitized);
519
358
  return;
520
359
  }
521
360
 
@@ -527,26 +366,23 @@ function logWrite(level, input) {
527
366
  }
528
367
  }
529
368
 
530
- /**
531
- * 日志实例(延迟初始化)
532
- */
533
- export const Logger = {
534
- info: function (msg, data) {
369
+ function buildLogMethod(level) {
370
+ return function (msg, data) {
535
371
  if (isPlainObject(msg)) {
536
- logWrite("info", msg);
372
+ logWrite(level, msg);
537
373
  return;
538
374
  }
539
375
 
540
- logWrite("info", { msg: msg, data: data });
541
- },
542
- warn: function (msg, data) {
543
- if (isPlainObject(msg)) {
544
- logWrite("warn", msg);
545
- return;
546
- }
376
+ logWrite(level, { msg: msg, data: data });
377
+ };
378
+ }
547
379
 
548
- logWrite("warn", { msg: msg, data: data });
549
- },
380
+ /**
381
+ * 日志实例(延迟初始化)
382
+ */
383
+ export const Logger = {
384
+ info: buildLogMethod("info"),
385
+ warn: buildLogMethod("warn"),
550
386
  error: function (msg, err, data) {
551
387
  if (isPlainObject(msg)) {
552
388
  logWrite("error", msg);
@@ -555,14 +391,7 @@ export const Logger = {
555
391
 
556
392
  logWrite("error", { msg: msg, err: err, data: data });
557
393
  },
558
- debug: function (msg, data) {
559
- if (isPlainObject(msg)) {
560
- logWrite("debug", msg);
561
- return;
562
- }
563
-
564
- logWrite("debug", { msg: msg, data: data });
565
- },
394
+ debug: buildLogMethod("debug"),
566
395
  flush: async function () {
567
396
  await flush();
568
397
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.61.0",
3
+ "version": "3.63.0",
4
4
  "gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",