mm_logger 0.0.0-stage → 1.0.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 ADDED
@@ -0,0 +1,1098 @@
1
+ /**
2
+ * mm_logger 日志模块
3
+ * @module mm_logger
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const log4js = require('log4js');
8
+ const LEVEL = require('./level');
9
+ const { timeFormat } = require('mm_helper').date;
10
+ const color_tools = require('./colors').colors;
11
+
12
+ /**
13
+ * 日志级别到颜色的映射
14
+ * @type {Object}
15
+ * @constant
16
+ */
17
+ const LEVEL_COLOR = {
18
+ info: 'blue',
19
+ success: 'green',
20
+ warn: 'yellow',
21
+ error: 'red',
22
+ fatal: 'magenta'
23
+ };
24
+
25
+ console.debug = console.log;
26
+ console.success = console.info;
27
+ console.fatal = console.error;
28
+
29
+ /**
30
+ * 日志类
31
+ * @class Log
32
+ */
33
+ class Logger {
34
+ /**
35
+ * 日志类默认配置
36
+ * @type {Object}
37
+ * @static
38
+ */
39
+ static config = {
40
+ /**
41
+ * 日志级别
42
+ * @type {string}
43
+ */
44
+ level: 'debug',
45
+ /**
46
+ * 日志目录
47
+ * @type {string}
48
+ */
49
+ dir: './log',
50
+ /**
51
+ * 是否启用控制台日志记录
52
+ * @type {boolean}
53
+ */
54
+ console: true,
55
+ /**
56
+ * 控制台日志记录格式
57
+ * @type {string}
58
+ */
59
+ console_format: '{time} [{level}] {msg}',
60
+ /**
61
+ * 是否启用文件日志记录
62
+ * @type {boolean}
63
+ */
64
+ file: false,
65
+ /**
66
+ * 文件名格式
67
+ * @type {string}
68
+ */
69
+ filename: '{type}.log',
70
+ /**
71
+ * 文件日志记录轮转文件名格式
72
+ * @type {string}
73
+ */
74
+ rotation_filename_format: '{type}_{date}_{index}.log',
75
+ /**
76
+ * 文件日志记录格式
77
+ * @type {string}
78
+ */
79
+ content_format: '{time} [{level}] {msg}',
80
+ /**
81
+ * 文件日志记录最大文件大小
82
+ * @type {number}
83
+ */
84
+ max_size: 1024 * 1024 * 3,
85
+ /**
86
+ * 文件日志记录最大文件数量
87
+ * @type {number}
88
+ */
89
+ max_files: 7
90
+ };
91
+
92
+ /**
93
+ * 构造函数,用于初始化日志类
94
+ * @param {Object} config - 配置对象,包含日志相关配置信息
95
+ */
96
+ constructor(config) {
97
+ /**
98
+ * 日志配置
99
+ * @type {Object}
100
+ */
101
+ this.config = Object.assign({}, Logger.config);
102
+ this.setConfig(config);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 设置配置
108
+ * @param {Object} config - 配置对象
109
+ */
110
+ Logger.prototype.setConfig = function(config = {}) {
111
+ if (typeof config !== 'object' || config === null) {
112
+ throw new Error('配置参数必须是对象类型');
113
+ }
114
+ Object.assign(this.config, config);
115
+ this._preset();
116
+ };
117
+
118
+ /**
119
+ * 初始化
120
+ * @private
121
+ * @returns {Logger} - 日志类实例
122
+ */
123
+ Logger.prototype._preset = function() {
124
+ /**
125
+ * 控制台日志记录器
126
+ * @private
127
+ */
128
+ this._console = console;
129
+
130
+ /**
131
+ * 文件日志记录器对象
132
+ * @private
133
+ */
134
+ this._filer = {};
135
+
136
+ if (this.config.console) {
137
+ this._createConsoleLogger();
138
+ }
139
+
140
+ if (this.config.file) {
141
+ this._initLog4js();
142
+ this._createFileLoggers();
143
+ }
144
+
145
+ return this;
146
+ };
147
+
148
+ /**
149
+ * 创建控制台日志记录器
150
+ * @private
151
+ */
152
+ Logger.prototype._createConsoleLogger = function() {
153
+ try {
154
+ // 自定义日志记录器,更好地控制Windows环境下的中文输出
155
+ const logger = {};
156
+
157
+ // 级别到函数的映射
158
+ const level_funcs = {
159
+ debug: console.debug,
160
+ info: console.info,
161
+ success: console.success,
162
+ warn: console.warn,
163
+ error: console.error,
164
+ fatal: console.fatal
165
+ };
166
+ const console_format = this.config.console_format;
167
+ // 创建各级别日志方法
168
+ Object.keys(level_funcs).forEach(function(level) {
169
+ logger[level] = function() {
170
+ const now = new Date();
171
+ const time_str = timeFormat(now, 'hh:mm:ss');
172
+ const args = Array.from(arguments);
173
+
174
+ // 格式化日志信息
175
+ let prefix = console_format.replace('{time}', time_str)
176
+ .replace('{level}', level.toUpperCase())
177
+ .replace('{msg}', args[0]);
178
+
179
+ // 按级别着色(colors 会根据终端能力自动禁用颜色)
180
+ const color_name = LEVEL_COLOR[level];
181
+ if (color_name) {
182
+ prefix = color_tools[color_name](prefix);
183
+ }
184
+
185
+ level_funcs[level].apply(console, [prefix, ...args.slice(1)]);
186
+ };
187
+ });
188
+
189
+ this._console = logger;
190
+ } catch (error) {
191
+ console.error('创建控制台日志记录器失败:', error);
192
+ }
193
+ };
194
+
195
+ /**
196
+ * 检查是否需要轮转日志
197
+ * @private
198
+ * @param {string} type - 日志类型
199
+ */
200
+ Logger.prototype._checkRotation = function(type) {
201
+ if (this._roller) {
202
+ if (this._roller.check(type)) {
203
+ this._roller.roll(type);
204
+ }
205
+ }
206
+ };
207
+
208
+ /**
209
+ * 清理超过max_files数量的旧日志文件
210
+ * @private
211
+ * @param {string} type - 日志类型
212
+ */
213
+ Logger.prototype._cleanupOldLogs = function(type) {
214
+ // 此方法现在由 LogRoll 内部实现
215
+ // 保留这个方法是为了向后兼容
216
+ this._checkRotation(type);
217
+ };
218
+
219
+ /**
220
+ * 获取当前日期字符串,格式:YYYY-MM-DD
221
+ * @private
222
+ * @returns {string} - 当前日期字符串
223
+ */
224
+ Logger.prototype._getDate = function() {
225
+ const now = new Date();
226
+ const year = now.getFullYear();
227
+ const month = String(now.getMonth() + 1).padStart(2, '0');
228
+ const day = String(now.getDate()).padStart(2, '0');
229
+ return `${year}-${month}-${day}`;
230
+ }
231
+
232
+ /**
233
+ * 创建单个文件日志记录器
234
+ * @private
235
+ * @param {string} type - 日志类型
236
+ */
237
+ Logger.prototype._createFileLogger = function(type) {
238
+ // 创建并保存logger
239
+ const appender_name = `file_${type}`;
240
+ this._filer[type] = log4js.getLogger(appender_name);
241
+ };
242
+
243
+ /**
244
+ * 重命名日志备份文件
245
+ * @private
246
+ */
247
+ Logger.prototype._renameLogBackupFiles = function() {
248
+ if (!this.config.file || !this._dir) return;
249
+
250
+ const types = ['debug', 'error', 'http'];
251
+ let files;
252
+
253
+ try {
254
+ files = fs.readdirSync(this._dir);
255
+ } catch (error) {
256
+ // 忽略错误,继续处理
257
+ return;
258
+ }
259
+
260
+ types.forEach(type => {
261
+ // 获取基础文件名
262
+ const base_filename = this.config.filename.replace('{type}', type);
263
+
264
+ // 查找所有原始格式的备份文件 (如: debug.Logger.1, app-debug.Logger.1)
265
+ const original_backups = files.filter(file =>
266
+ file.startsWith(base_filename) && /\.\d+$/.test(file)
267
+ );
268
+
269
+ // 重命名原始格式的备份文件
270
+ original_backups.forEach(backup_file => {
271
+ // 获取索引
272
+ const index = backup_file.match(/\.(\d+)$/)[1];
273
+
274
+ // 获取文件的创建时间
275
+ const old_path = path.join(this._dir, backup_file);
276
+ let date;
277
+
278
+ try {
279
+ const stats = fs.statSync(old_path);
280
+ // 格式化日期 (YYYY-MM-DD)
281
+ date = new Date(stats.birthtime).toISOString().split('T')[0];
282
+ } catch (error) {
283
+ // 如果获取文件信息失败,使用当前日期
284
+ date = new Date().toISOString().split('T')[0];
285
+ }
286
+
287
+ // 构建新的文件名
288
+ const new_filename = this.config.rotation_filename_format
289
+ .replace('{type}', type)
290
+ .replace('{date}', date)
291
+ .replace('{index}', index);
292
+ const new_path = path.join(this._dir, new_filename);
293
+
294
+ // 检查新文件是否已存在
295
+ if (!fs.existsSync(new_path)) {
296
+ // 重命名文件
297
+ try {
298
+ fs.renameSync(old_path, new_path);
299
+ } catch (error) {
300
+ // 忽略错误,继续处理其他文件
301
+ }
302
+ } else {
303
+ // 如果新文件已存在,删除旧文件
304
+ try {
305
+ fs.unlinkSync(old_path);
306
+ } catch (error) {
307
+ // 忽略错误,继续处理其他文件
308
+ }
309
+ }
310
+ });
311
+ });
312
+ };
313
+
314
+ /**
315
+ * 初始化log4js配置
316
+ * @private
317
+ */
318
+ Logger.prototype._initLog4js = function() {
319
+ let dir = this.config.dir;
320
+ const types = ['debug', 'error', 'http'];
321
+ const appenders = {};
322
+ const categories = {};
323
+
324
+ // 确保日志目录存在
325
+ if (!fs.existsSync(dir)) {
326
+ const absoluteDir = path.isAbsolute(dir) ?
327
+ dir : path.resolve(dir);
328
+ fs.mkdirSync(absoluteDir, {
329
+ recursive: true
330
+ });
331
+ dir = absoluteDir;
332
+ this.config.dir = absoluteDir;
333
+ }
334
+
335
+ // 配置所有appender
336
+ for (const type of types) {
337
+ // 完整的基础文件名(包含扩展名)
338
+ const full_filename = this.config.filename.replace('{type}', type);
339
+ const filename = path.join(dir, full_filename);
340
+ const appender_name = `file_${type}`;
341
+
342
+ // 将自定义格式转换为log4js支持的pattern格式
343
+ let pattern = this.config.content_format
344
+ .replace('{time}', '%d')
345
+ .replace('{level}', '%p')
346
+ .replace('{msg}', '%m');
347
+
348
+ // 使用file appender
349
+ appenders[appender_name] = {
350
+ type: 'file',
351
+ filename: filename,
352
+ maxLogSize: this.config.max_size,
353
+ backups: this.config.max_files,
354
+ encoding: 'utf8',
355
+ compress: false,
356
+ layout: {
357
+ type: 'pattern',
358
+ pattern: pattern
359
+ }
360
+ };
361
+
362
+ categories[appender_name] = {
363
+ appenders: [appender_name],
364
+ level: this.config.level
365
+ };
366
+ }
367
+
368
+ // 保存日志目录路径
369
+ this._dir = dir;
370
+
371
+ // 添加默认类别
372
+ categories.default = {
373
+ appenders: ['file_debug'],
374
+ level: this.config.level
375
+ };
376
+
377
+ // 一次性配置log4js
378
+ log4js.configure({
379
+ appenders: appenders,
380
+ categories: categories
381
+ });
382
+ };
383
+
384
+ /**
385
+ * 创建文件日志记录器
386
+ * @private
387
+ */
388
+ Logger.prototype._createFileLoggers = function() {
389
+ try {
390
+ // 创建多种类型的日志记录器
391
+ const types = ['debug', 'error', 'http'];
392
+ for (let i = 0; i < types.length; i++) {
393
+ const type = types[i];
394
+ this._createFileLogger(type);
395
+ }
396
+ } catch (error) {
397
+ console.error('创建文件日志记录器失败:', error);
398
+ }
399
+ };
400
+
401
+ /**
402
+ * 检查日志级别
403
+ * @param {String} level - 日志级别
404
+ * @returns {Boolean} - 是否应该记录
405
+ * @private
406
+ */
407
+ Logger.prototype._checkLevel = function(level) {
408
+ const currentLevel = LEVEL[this.config.level] || LEVEL.info;
409
+ const targetLevel = LEVEL[level] || LEVEL.info;
410
+ return targetLevel >= currentLevel;
411
+ };
412
+
413
+ /**
414
+ * 更新文件日志记录器(Windows兼容版本)
415
+ * @private
416
+ * @param {string} type - 日志类型
417
+ */
418
+ Logger.prototype._updateFileLogger = function(type) {
419
+ // 如果logger不存在,创建一个
420
+ if (!this._filer[type]) {
421
+ this._createFileLogger(type);
422
+ }
423
+ };
424
+
425
+ /**
426
+ * 通用日志记录方法
427
+ * @param {String} level - 日志级别
428
+ * @param {String} message - 日志消息
429
+ * @param {...*} args - 额外参数
430
+ * @private
431
+ */
432
+ Logger.prototype._log = function(level, message, ...args) {
433
+ try {
434
+ // 校验参数
435
+ if (!LEVEL.hasOwnProperty(level)) {
436
+ throw new Error(`无效的日志级别: ${level}`);
437
+ }
438
+ if (typeof message !== 'string') {
439
+ throw new Error('日志消息必须是字符串类型');
440
+ }
441
+
442
+ // 检查级别
443
+ if (!this._checkLevel(level)) {
444
+ return;
445
+ }
446
+
447
+ // 控制台日志
448
+ if (this._console) {
449
+ this._console[level](message, ...args);
450
+ }
451
+
452
+ // 文件日志
453
+ if (this.config.file) {
454
+ // 在写入前更新所有可能用到的logger
455
+ this._updateFileLogger('debug');
456
+
457
+ // debug日志包含所有级别的日志
458
+ if (this._filer.debug) {
459
+ this._filer.debug[level === 'success' ? 'info' : level](message, ...args);
460
+ }
461
+
462
+ // error日志只包含warn、error、fatal三个级别的日志
463
+ const error_levels = ['warn', 'error', 'fatal'];
464
+ if (error_levels.includes(level)) {
465
+ this._updateFileLogger('error');
466
+ if (this._filer.error) {
467
+ this._filer.error[level === 'success' ? 'info' : level](message, ...args);
468
+ }
469
+ }
470
+
471
+ // 检查并重命名备份文件
472
+ setImmediate(() => {
473
+ this._renameLogBackupFiles();
474
+ });
475
+ }
476
+ } catch (error) {
477
+ console.error(`日志记录错误: ${error.message}`);
478
+ }
479
+ };
480
+
481
+ /**
482
+ * 记录debug级别日志
483
+ * @param {String} message - 日志消息
484
+ * @param {...*} args - 额外参数
485
+ */
486
+ Logger.prototype.debug = function(message, ...args) {
487
+ this._log('debug', message, ...args);
488
+ };
489
+
490
+ /**
491
+ * 记录info级别日志
492
+ * @param {String} message - 日志消息
493
+ * @param {...*} args - 额外参数
494
+ */
495
+ Logger.prototype.info = function(message, ...args) {
496
+ this._log('info', message, ...args);
497
+ };
498
+
499
+ /**
500
+ * 记录success级别日志
501
+ * @param {String} message - 日志消息
502
+ * @param {...*} args - 额外参数
503
+ */
504
+ Logger.prototype.success = function(message, ...args) {
505
+ this._log('success', message, ...args);
506
+ };
507
+
508
+ /**
509
+ * 记录warn级别日志
510
+ * @param {String} message - 日志消息
511
+ * @param {...*} args - 额外参数
512
+ */
513
+ Logger.prototype.warn = function(message, ...args) {
514
+ this._log('warn', message, ...args);
515
+ };
516
+
517
+ /**
518
+ * 记录error级别日志
519
+ * @param {String} message - 日志消息
520
+ * @param {...*} args - 额外参数
521
+ */
522
+ Logger.prototype.error = function(message, ...args) {
523
+ this._log('error', message, ...args);
524
+ };
525
+
526
+ /**
527
+ * 记录fatal级别日志
528
+ * @param {String} message - 日志消息
529
+ * @param {...*} args - 额外参数
530
+ */
531
+ Logger.prototype.fatal = function(message, ...args) {
532
+ this._log('fatal', message, ...args);
533
+ };
534
+
535
+ /**
536
+ * 记录日志
537
+ * @param {String} level - 日志级别
538
+ * @param {String} message - 日志消息
539
+ * @param {...*} args - 额外参数
540
+ */
541
+ Logger.prototype.log = function(level, message, ...args) {
542
+ this._log(level, message, ...args);
543
+ };
544
+
545
+ /**
546
+ * 记录HTTP请求日志
547
+ * @param {String} level - HTTP方法
548
+ * @param {String} message - 日志消息
549
+ * @param {...*} args - 额外参数
550
+ * @returns {Object} - 日志数据
551
+ */
552
+ Logger.prototype.http = function(level, message, ...args) {
553
+ try {
554
+
555
+ // 记录到控制台
556
+ if (this.config.console) {
557
+ const logger = this._console;
558
+ if (logger[level]) {
559
+ logger[level](message, ...args);
560
+ }
561
+ }
562
+
563
+ // 记录到文件
564
+ if (this.config.file) {
565
+ // 更新http logger并记录日志
566
+ this._updateFileLogger('http');
567
+ // 单独输出到http.log
568
+ if (this._filer.http) {
569
+ this._filer.http.info(message, ...args);
570
+ }
571
+
572
+ // 根据级别决定是否输出到error.log
573
+ if (LEVEL[level] >= LEVEL.warn) {
574
+ this._updateFileLogger('error');
575
+ if (this._filer.error) {
576
+ this._filer.error.info(message, ...args);
577
+ }
578
+ }
579
+ }
580
+ } catch (error) {
581
+ console.error(`HTTP日志记录错误: ${error.message}`);
582
+ }
583
+ };
584
+
585
+ /**
586
+ * 解析并规范化分页参数
587
+ * @param {Object} query - 查询条件
588
+ * @param {number} [query.page_now] - 当前页码,从1开始,默认1
589
+ * @param {number} [query.page_size] - 每页条数,默认30
590
+ * @returns {Object} - 规范化后的分页参数 { page_now, page_size }
591
+ * @private
592
+ */
593
+ Logger.prototype._normalizePagination = function(query = {}) {
594
+ const page_now = Math.max(1, Number(query.page_now) || 1);
595
+ const page_size = Math.max(1, Number(query.page_size) || 30);
596
+ return { page_now, page_size };
597
+ };
598
+
599
+ /**
600
+ * 校验查询条件的字段类型
601
+ * @param {Object} query - 查询条件
602
+ * @throws {TypeError} - 当字段类型非法时
603
+ * @private
604
+ */
605
+ Logger.prototype._checkQuery = function(query = {}) {
606
+ if (query.keyword !== undefined && typeof query.keyword !== 'string') {
607
+ throw new TypeError('keyword参数必须是字符串类型');
608
+ }
609
+ if (query.level !== undefined && typeof query.level !== 'string') {
610
+ throw new TypeError('level参数必须是字符串类型');
611
+ }
612
+ if (query.date !== undefined && typeof query.date !== 'string') {
613
+ throw new TypeError('date参数必须是字符串类型');
614
+ }
615
+ };
616
+
617
+ /**
618
+ * 判断日志条目是否匹配关键词
619
+ * @param {Object} log_entry - 日志条目
620
+ * @param {string} keyword - 关键词,空则匹配所有
621
+ * @returns {boolean} - 是否匹配
622
+ * @private
623
+ */
624
+ Logger.prototype._matchKeyword = function(log_entry, keyword) {
625
+ if (!keyword) {
626
+ return true;
627
+ }
628
+ return String(log_entry.message || '').toLowerCase().includes(keyword.toLowerCase());
629
+ };
630
+
631
+ /**
632
+ * 判断日志条目是否匹配级别
633
+ * @param {Object} log_entry - 日志条目
634
+ * @param {string} level - 日志级别,空则匹配所有
635
+ * @returns {boolean} - 是否匹配
636
+ * @private
637
+ */
638
+ Logger.prototype._matchLevel = function(log_entry, level) {
639
+ if (!level) {
640
+ return true;
641
+ }
642
+ return log_entry.level === level.toLowerCase();
643
+ };
644
+
645
+ /**
646
+ * 判断日志条目是否匹配日期
647
+ * @param {Object} log_entry - 日志条目
648
+ * @param {string} date - 日期字符串(YYYY-MM-DD),空则匹配所有
649
+ * @returns {boolean} - 是否匹配
650
+ * @private
651
+ */
652
+ Logger.prototype._matchDate = function(log_entry, date) {
653
+ if (!date) {
654
+ return true;
655
+ }
656
+ return String(log_entry.timestamp || '').startsWith(date);
657
+ };
658
+
659
+ /**
660
+ * 按查询条件过滤日志条目
661
+ * @param {Array} logs - 日志条目数组
662
+ * @param {Object} query - 查询条件
663
+ * @returns {Array} - 过滤后的日志条目数组
664
+ * @private
665
+ */
666
+ Logger.prototype._filterLogs = function(logs, query = {}) {
667
+ const keyword = query.keyword || '';
668
+ const level = query.level || '';
669
+ const date = query.date || '';
670
+ if (!keyword && !level && !date) {
671
+ return logs;
672
+ }
673
+ return logs.filter((log_entry) => {
674
+ return this._matchKeyword(log_entry, keyword)
675
+ && this._matchLevel(log_entry, level)
676
+ && this._matchDate(log_entry, date);
677
+ });
678
+ };
679
+
680
+ /**
681
+ * 查询日志
682
+ * @param {Object} query - 查询条件
683
+ * @param {number} [query.page_now] - 当前页码,从1开始,默认1
684
+ * @param {number} [query.page_size] - 每页条数,默认30
685
+ * @param {string} [query.keyword] - 关键词,匹配消息内容(不区分大小写)
686
+ * @param {string} [query.level] - 日志级别,精确匹配
687
+ * @param {string} [query.date] - 日期前缀,格式 YYYY-MM-DD
688
+ * @returns {Object} - 查询结果,包含 list、count、page_now、page_size、page_total
689
+ */
690
+ Logger.prototype.find = function(query = {}) {
691
+ if (typeof query !== 'object' || query === null) {
692
+ throw new Error('查询参数必须是对象类型');
693
+ }
694
+ this._checkQuery(query);
695
+
696
+ try {
697
+ const { page_now, page_size } = this._normalizePagination(query);
698
+
699
+ const dir = this.config.dir;
700
+ if (!fs.existsSync(dir)) {
701
+ return {
702
+ list: [],
703
+ count: 0,
704
+ page_now,
705
+ page_size,
706
+ page_total: 0
707
+ };
708
+ }
709
+
710
+ // 简单获取所有日志文件
711
+ const files = fs.readdirSync(dir)
712
+ .filter(function(file) {
713
+ return file.endsWith('.log');
714
+ })
715
+ .map(function(file) {
716
+ return path.join(dir, file);
717
+ });
718
+
719
+ let logs = [];
720
+
721
+ // 读取并解析日志文件
722
+ for (let i = 0; i < files.length; i++) {
723
+ const file_path = files[i];
724
+ try {
725
+ const content = fs.readFileSync(file_path, 'utf8');
726
+ const lines = content.trim().split('\n');
727
+
728
+ for (let j = 0; j < lines.length; j++) {
729
+ let line = lines[j];
730
+ // 去除换行符和回车符
731
+ line = line.replace(/[\r\n]/g, '').trim();
732
+ if (line) {
733
+ // 根据配置的content_format动态生成正则表达式
734
+ let content_format = this.config.content_format || '{time} [{level}] {msg}';
735
+
736
+ // 先保存占位符的位置,避免转义
737
+ const placeholders = [];
738
+ let temp_format = content_format.replace(/\{(time|level|msg)\}/g, (match, type) => {
739
+ const placeholder = `__${type.toUpperCase()}__`;
740
+ placeholders.push({
741
+ placeholder,
742
+ type
743
+ });
744
+ return placeholder;
745
+ });
746
+
747
+ // 转义正则表达式特殊字符
748
+ temp_format = temp_format.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
749
+
750
+ // 替换占位符为对应的正则表达式模式
751
+ placeholders.forEach(({
752
+ placeholder,
753
+ type
754
+ }) => {
755
+ let pattern = '';
756
+ switch (type) {
757
+ case 'time':
758
+ pattern = '(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3})';
759
+ break;
760
+ case 'level':
761
+ pattern = '(DEBUG|INFO|SUCCESS|WARN|ERROR|FATAL|TRACE)';
762
+ break;
763
+ case 'msg':
764
+ pattern = '(.+)';
765
+ break;
766
+ }
767
+ temp_format = temp_format.replace(new RegExp(placeholder.replace(
768
+ /[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), pattern);
769
+ });
770
+
771
+ const regex = new RegExp(`^${temp_format}$`);
772
+ const match = line.match(regex);
773
+
774
+ if (match) {
775
+ logs.push({
776
+ timestamp: match[1],
777
+ level: match[2].toLowerCase(),
778
+ message: match[3],
779
+ file: path.basename(file_path)
780
+ });
781
+ } else {
782
+ // 尝试解析JSON格式(兼容旧日志)
783
+ try {
784
+ const log = JSON.parse(line);
785
+ logs.push({
786
+ timestamp: log.time || new Date().toISOString(),
787
+ level: log.level || 'info',
788
+ message: log.msg || log.message || '',
789
+ file: path.basename(file_path)
790
+ });
791
+ } catch (e) {
792
+ // 跳过无法解析的行
793
+ }
794
+ }
795
+ }
796
+ }
797
+ } catch (error) {
798
+ console.error(`读取日志文件失败: ${file_path}`);
799
+ }
800
+ }
801
+
802
+ // 按查询条件过滤
803
+ logs = this._filterLogs(logs, query);
804
+
805
+ // 排序
806
+ logs.sort(function(a, b) {
807
+ return new Date(b.timestamp) - new Date(a.timestamp);
808
+ });
809
+
810
+ // 计算总数
811
+ const count = logs.length;
812
+
813
+ // 计算总页数并截取当前页
814
+ const page_total = Math.ceil(count / page_size);
815
+ const list = logs.slice(page_size * (page_now - 1), page_size * page_now);
816
+
817
+ // 返回标准格式的结果,包含分页信息
818
+ return {
819
+ list,
820
+ count,
821
+ page_now,
822
+ page_size,
823
+ page_total
824
+ };
825
+ } catch (error) {
826
+ console.error('查询日志失败:', error);
827
+ return {
828
+ list: [],
829
+ count: 0,
830
+ page_now: 1,
831
+ page_size: 30,
832
+ page_total: 0
833
+ };
834
+ }
835
+ };
836
+
837
+ /**
838
+ * 删除指定类型的日志文件
839
+ * @param {string} type - 日志类型,可选值:debug、error、http
840
+ * @returns {Object} - 删除结果,包含删除的文件数量
841
+ * @throws {Error} - 当type参数无效时
842
+ */
843
+ Logger.prototype.remove = function(type = 'debug') {
844
+ // 验证参数
845
+ if (!type || typeof type !== 'string') {
846
+ throw new Error('type参数必须是字符串');
847
+ }
848
+
849
+ // 验证日志类型
850
+ const valid_types = ['debug', 'error', 'http'];
851
+ if (!valid_types.includes(type)) {
852
+ throw new Error(`无效的日志类型: ${type},可选值:${valid_types.join(', ')}`);
853
+ }
854
+
855
+ let deleted_count = 0;
856
+
857
+ try {
858
+ // 检查日志目录是否存在
859
+ if (!fs.existsSync(this._dir)) {
860
+ return {
861
+ deleted: deleted_count
862
+ };
863
+ }
864
+
865
+ // 获取日志目录下的所有文件
866
+ const files = fs.readdirSync(this._dir);
867
+
868
+ // 查找并删除指定类型的日志文件
869
+ files.forEach(file => {
870
+ if (file.startsWith(`${type}_`) || file === `${type}.log`) {
871
+ const file_path = path.join(this._dir, file);
872
+ try {
873
+ fs.unlinkSync(file_path);
874
+ deleted_count++;
875
+ } catch (error) {
876
+ console.error(`删除日志文件失败: ${file_path}`, error);
877
+ }
878
+ }
879
+ });
880
+
881
+ // 重新创建日志记录器
882
+ if (this._filer && this._filer[type]) {
883
+ delete this._filer[type];
884
+ this._createFileLogger(type);
885
+ }
886
+ } catch (error) {
887
+ console.error(`删除日志文件失败: ${type}`, error);
888
+ }
889
+
890
+ return {
891
+ deleted: deleted_count
892
+ };
893
+ };
894
+
895
+ /**
896
+ * 清空指定类型的日志(删除日志内容,保留文件)
897
+ * @param {string} type - 日志类型,可选值:debug、error、http
898
+ * @param {string} [keyword] - 关键词,只删除包含该关键词的日志条目
899
+ * @returns {Object} - 清空结果,包含删除的日志条目数
900
+ * @throws {Error} - 当type参数无效时
901
+ */
902
+ Logger.prototype.clear = function(type = 'debug', keyword) {
903
+ // 验证参数
904
+ if (!type || typeof type !== 'string') {
905
+ throw new Error('type参数必须是字符串');
906
+ }
907
+ if (keyword && typeof keyword !== 'string') {
908
+ throw new TypeError('keyword参数必须是字符串类型');
909
+ }
910
+
911
+ // 验证日志类型
912
+ const valid_types = ['debug', 'error', 'http'];
913
+ if (!valid_types.includes(type)) {
914
+ throw new Error(`无效的日志类型: ${type},可选值:${valid_types.join(', ')}`);
915
+ }
916
+
917
+ let deleted_count = 0;
918
+
919
+ try {
920
+ // 检查日志目录是否存在
921
+ if (!fs.existsSync(this._dir)) {
922
+ return {
923
+ deleted: deleted_count
924
+ };
925
+ }
926
+
927
+ // 获取日志目录下的所有文件
928
+ const files = fs.readdirSync(this._dir);
929
+
930
+ // 查找并处理指定类型的日志文件
931
+ files.forEach(file => {
932
+ // 匹配日志文件名格式:type_YYYY-MM-DD_index.log 或 type.log
933
+ if (file.startsWith(`${type}_`) || file === `${type}.log`) {
934
+ const file_path = path.join(this._dir, file);
935
+ try {
936
+ const content = fs.readFileSync(file_path, 'utf8');
937
+ const parsed_logs = this._parseLogContent(content);
938
+ let filtered_logs;
939
+
940
+ if (keyword && typeof keyword === 'string') {
941
+ // 删除包含关键词的日志条目
942
+ filtered_logs = parsed_logs.filter(log_entry => !log_entry.includes(keyword));
943
+ } else {
944
+ // 清空所有日志条目(保留文件)
945
+ filtered_logs = [];
946
+ }
947
+
948
+ const removed_count = parsed_logs.length - filtered_logs.length;
949
+
950
+ if (removed_count > 0) {
951
+ fs.writeFileSync(file_path, filtered_logs.join('\n') + '\n');
952
+ deleted_count += removed_count;
953
+ }
954
+ } catch (error) {
955
+ console.error(`清空日志失败: ${file_path}`, error);
956
+ }
957
+ }
958
+ });
959
+ } catch (error) {
960
+ console.error(`清空日志失败: ${type}`, error);
961
+ }
962
+
963
+ return {
964
+ deleted: deleted_count
965
+ };
966
+ };
967
+
968
+ /**
969
+ * 删除所有类型的日志文件
970
+ * @returns {Object} - 删除结果,包含每种类型删除的文件数量
971
+ */
972
+ Logger.prototype.removeAll = function() {
973
+ const result = {};
974
+ const types = ['debug', 'error', 'http'];
975
+
976
+ types.forEach(type => {
977
+ try {
978
+ result[type] = this.remove(type);
979
+ } catch (error) {
980
+ result[type] = {
981
+ deleted: 0,
982
+ error: error.message
983
+ };
984
+ }
985
+ });
986
+
987
+ return result;
988
+ };
989
+
990
+ /**
991
+ * 解析日志内容,正确处理跨行日志
992
+ * @param {string} content - 日志文件内容
993
+ * @returns {Array} - 日志条目数组
994
+ */
995
+ Logger.prototype._parseLogContent = function(content) {
996
+ if (!content || typeof content !== 'string') {
997
+ return [];
998
+ }
999
+
1000
+ // 日志起始模式:YYYY-MM-DDTHH:mm:ss.SSS [TYPE]
1001
+ const log_start_pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3} \[.*?\]/;
1002
+ const lines = content.split('\n');
1003
+ const logs = [];
1004
+ let current_log = '';
1005
+
1006
+ for (let i = 0; i < lines.length; i++) {
1007
+ const line = lines[i];
1008
+
1009
+ if (log_start_pattern.test(line)) {
1010
+ // 遇到新的日志条目
1011
+ if (current_log) {
1012
+ logs.push(current_log.trim());
1013
+ }
1014
+ current_log = line;
1015
+ } else if (current_log) {
1016
+ // 继续拼接跨行日志
1017
+ current_log += '\n' + line;
1018
+ }
1019
+ }
1020
+
1021
+ // 添加最后一条日志
1022
+ if (current_log) {
1023
+ logs.push(current_log.trim());
1024
+ }
1025
+
1026
+ return logs;
1027
+ };
1028
+
1029
+ /**
1030
+ * 读取日志内容
1031
+ * @param {string} type - 日志类型,可选值:debug、error、http,不传则读取所有类型
1032
+ * @param {string} keyword - 关键词,不传则读取所有内容
1033
+ * @returns {Array} - 日志内容数组
1034
+ */
1035
+ Logger.prototype.read = function(type = 'debug', keyword = '') {
1036
+ // 参数校验
1037
+ if (type && typeof type !== 'string') {
1038
+ throw new TypeError('type参数必须是字符串类型');
1039
+ }
1040
+ if (keyword && typeof keyword !== 'string') {
1041
+ throw new TypeError('keyword参数必须是字符串类型');
1042
+ }
1043
+
1044
+ try {
1045
+ const dir = this.config.dir;
1046
+ if (!fs.existsSync(dir)) {
1047
+ return [];
1048
+ }
1049
+
1050
+ const valid_types = ['debug', 'error', 'http'];
1051
+ let target_types = valid_types;
1052
+
1053
+ // 如果指定了type参数,只读取该类型
1054
+ if (type && valid_types.includes(type)) {
1055
+ target_types = [type];
1056
+ }
1057
+
1058
+ let logs = [];
1059
+
1060
+ // 遍历所有目标类型
1061
+ for (let i = 0; i < target_types.length; i++) {
1062
+ const log_type = target_types[i];
1063
+ const filename = this.config.filename.replace('{type}', log_type);
1064
+ const file_path = path.join(dir, filename);
1065
+
1066
+ if (!fs.existsSync(file_path)) {
1067
+ continue;
1068
+ }
1069
+
1070
+ try {
1071
+ const content = fs.readFileSync(file_path, 'utf8');
1072
+ const parsed_logs = this._parseLogContent(content);
1073
+
1074
+ for (let j = 0; j < parsed_logs.length; j++) {
1075
+ const log_entry = parsed_logs[j];
1076
+
1077
+ if (log_entry) {
1078
+ // 如果指定了keyword,只保留包含关键词的日志
1079
+ if (keyword && !log_entry.includes(keyword)) {
1080
+ continue;
1081
+ }
1082
+ logs.push(log_entry);
1083
+ }
1084
+ }
1085
+ } catch (error) {
1086
+ console.error(`读取日志文件失败: ${file_path}`, error);
1087
+ }
1088
+ }
1089
+
1090
+ return logs;
1091
+ } catch (error) {
1092
+ console.error('读取日志失败:', error);
1093
+ return [];
1094
+ }
1095
+ };
1096
+
1097
+ // 导出模块
1098
+ module.exports = Logger;