@rawnodes/logger 1.7.0 → 1.8.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/dist/index.js CHANGED
@@ -4,11 +4,14 @@ var winston = require('winston');
4
4
  var async_hooks = require('async_hooks');
5
5
  var DailyRotateFile = require('winston-daily-rotate-file');
6
6
  var util = require('util');
7
+ var TransportStream = require('winston-transport');
8
+ var clientCloudwatchLogs = require('@aws-sdk/client-cloudwatch-logs');
7
9
  var crypto = require('crypto');
8
10
 
9
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
12
 
11
13
  var DailyRotateFile__default = /*#__PURE__*/_interopDefault(DailyRotateFile);
14
+ var TransportStream__default = /*#__PURE__*/_interopDefault(TransportStream);
12
15
 
13
16
  // src/state.ts
14
17
  var LoggerStore = class {
@@ -21,6 +24,26 @@ var LoggerStore = class {
21
24
  }
22
25
  };
23
26
 
27
+ // src/types.ts
28
+ var LOG_LEVELS = {
29
+ off: -1,
30
+ error: 0,
31
+ warn: 1,
32
+ info: 2,
33
+ http: 3,
34
+ verbose: 4,
35
+ debug: 5,
36
+ silly: 6
37
+ };
38
+ function isValidLogLevel(level) {
39
+ return level in LOG_LEVELS;
40
+ }
41
+ function assertLogLevel(level) {
42
+ if (!isValidLogLevel(level)) {
43
+ throw new Error(`Invalid log level: "${level}". Valid levels: ${Object.keys(LOG_LEVELS).join(", ")}`);
44
+ }
45
+ }
46
+
24
47
  // src/utils/mask-secrets.ts
25
48
  var DEFAULT_SECRET_PATTERNS = [
26
49
  "password",
@@ -175,6 +198,20 @@ function maskSecretsFormat(options) {
175
198
  return maskSecrets(info, options);
176
199
  })();
177
200
  }
201
+ function createFilterFormat(defaultLevel, rules, store) {
202
+ if (!rules?.length && defaultLevel === void 0) {
203
+ return winston.format((info) => info)();
204
+ }
205
+ return winston.format((info) => {
206
+ const logLevel = info.level;
207
+ const context = info.context;
208
+ const storeContext = store.getStore();
209
+ const matchingRule = rules?.find((rule) => matchesContext(storeContext, context, rule.match));
210
+ const effectiveLevel = matchingRule?.level ?? defaultLevel ?? "silly";
211
+ if (effectiveLevel === "off") return false;
212
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[effectiveLevel] ? info : false;
213
+ })();
214
+ }
178
215
  function createPlainFormat(store) {
179
216
  return winston.format.combine(
180
217
  winston.format.errors({ stack: true }),
@@ -239,17 +276,457 @@ function createFormat(logFormat, store) {
239
276
  }
240
277
  }
241
278
 
279
+ // src/transports/buffer.ts
280
+ var MessageBuffer = class {
281
+ constructor(options) {
282
+ this.options = options;
283
+ }
284
+ queue = [];
285
+ timer = null;
286
+ flushing = false;
287
+ closed = false;
288
+ add(message) {
289
+ if (this.closed) return;
290
+ this.queue.push(message);
291
+ if (this.queue.length >= this.options.batchSize) {
292
+ void this.flush();
293
+ } else {
294
+ this.scheduleFlush();
295
+ }
296
+ }
297
+ async flush() {
298
+ if (this.flushing || this.queue.length === 0) return;
299
+ this.flushing = true;
300
+ this.clearTimer();
301
+ const messages = this.queue.splice(0, this.options.batchSize);
302
+ try {
303
+ await this.sendWithRetry(messages);
304
+ } catch (error) {
305
+ this.options.onError?.(error, messages);
306
+ } finally {
307
+ this.flushing = false;
308
+ if (this.queue.length > 0 && !this.closed) {
309
+ void this.flush();
310
+ }
311
+ }
312
+ }
313
+ async close() {
314
+ this.closed = true;
315
+ this.clearTimer();
316
+ while (this.queue.length > 0) {
317
+ await this.flush();
318
+ }
319
+ }
320
+ scheduleFlush() {
321
+ if (this.timer || this.closed) return;
322
+ this.timer = setTimeout(() => {
323
+ this.timer = null;
324
+ void this.flush();
325
+ }, this.options.flushInterval);
326
+ }
327
+ clearTimer() {
328
+ if (this.timer) {
329
+ clearTimeout(this.timer);
330
+ this.timer = null;
331
+ }
332
+ }
333
+ async sendWithRetry(messages) {
334
+ let lastError = null;
335
+ for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {
336
+ try {
337
+ await this.options.onFlush(messages);
338
+ return;
339
+ } catch (error) {
340
+ lastError = error;
341
+ if (attempt < this.options.maxRetries - 1) {
342
+ await this.delay(this.options.retryDelay * Math.pow(2, attempt));
343
+ }
344
+ }
345
+ }
346
+ throw lastError;
347
+ }
348
+ delay(ms) {
349
+ return new Promise((resolve) => setTimeout(resolve, ms));
350
+ }
351
+ };
352
+
353
+ // src/transports/base-http.ts
354
+ var DEFAULT_OPTIONS = {
355
+ batchSize: 10,
356
+ flushInterval: 5e3,
357
+ maxRetries: 3,
358
+ retryDelay: 1e3
359
+ };
360
+ var BaseHttpTransport = class extends TransportStream__default.default {
361
+ buffer;
362
+ constructor(opts = {}) {
363
+ super();
364
+ this.buffer = new MessageBuffer({
365
+ batchSize: opts.batchSize ?? DEFAULT_OPTIONS.batchSize,
366
+ flushInterval: opts.flushInterval ?? DEFAULT_OPTIONS.flushInterval,
367
+ maxRetries: opts.maxRetries ?? DEFAULT_OPTIONS.maxRetries,
368
+ retryDelay: opts.retryDelay ?? DEFAULT_OPTIONS.retryDelay,
369
+ onFlush: this.sendBatch.bind(this),
370
+ onError: this.handleError.bind(this)
371
+ });
372
+ }
373
+ log(info, callback) {
374
+ const message = this.transformMessage(info);
375
+ this.buffer.add(message);
376
+ callback();
377
+ }
378
+ close() {
379
+ return this.buffer.close();
380
+ }
381
+ transformMessage(info) {
382
+ const { level, message, timestamp, context, ...meta } = info;
383
+ return {
384
+ level,
385
+ message: String(message),
386
+ timestamp: timestamp ? new Date(String(timestamp)) : /* @__PURE__ */ new Date(),
387
+ context,
388
+ meta: Object.keys(meta).length > 0 ? meta : void 0
389
+ };
390
+ }
391
+ handleError(error, messages) {
392
+ console.error(
393
+ `[${this.constructor.name}] Failed to send ${messages.length} messages:`,
394
+ error.message
395
+ );
396
+ this.emit("error", error);
397
+ }
398
+ };
399
+
400
+ // src/transports/discord.ts
401
+ var DEFAULT_EMBED_COLORS = {
402
+ off: 0,
403
+ error: 15548997,
404
+ warn: 16705372,
405
+ info: 5763719,
406
+ http: 5793266,
407
+ verbose: 10181046,
408
+ debug: 3447003,
409
+ silly: 9807270
410
+ };
411
+ var DiscordTransport = class extends BaseHttpTransport {
412
+ config;
413
+ constructor(config) {
414
+ super({
415
+ batchSize: config.batchSize ?? 10,
416
+ flushInterval: config.flushInterval ?? 2e3,
417
+ maxRetries: config.maxRetries,
418
+ retryDelay: config.retryDelay
419
+ });
420
+ this.config = config;
421
+ }
422
+ async sendBatch(messages) {
423
+ const chunks = this.chunkArray(messages, 10);
424
+ for (const chunk of chunks) {
425
+ const payload = {
426
+ username: this.config.username,
427
+ avatar_url: this.config.avatarUrl,
428
+ embeds: chunk.map((msg) => this.createEmbed(msg))
429
+ };
430
+ await this.sendWebhook(payload);
431
+ }
432
+ }
433
+ createEmbed(msg) {
434
+ const color = this.config.embedColors?.[msg.level] ?? DEFAULT_EMBED_COLORS[msg.level];
435
+ const embed = {
436
+ title: `[${msg.level.toUpperCase()}] ${msg.context || "APP"}`,
437
+ description: msg.message.slice(0, 4096),
438
+ // Discord limit
439
+ color
440
+ };
441
+ if (this.config.includeTimestamp !== false) {
442
+ embed.timestamp = msg.timestamp.toISOString();
443
+ }
444
+ if (this.config.includeMeta !== false && msg.meta) {
445
+ embed.fields = this.metaToFields(msg.meta);
446
+ }
447
+ return embed;
448
+ }
449
+ metaToFields(meta) {
450
+ const maxFields = this.config.maxEmbedFields ?? 25;
451
+ const fields = [];
452
+ for (const [key, value] of Object.entries(meta)) {
453
+ if (fields.length >= maxFields) break;
454
+ let strValue;
455
+ if (typeof value === "object") {
456
+ strValue = "```json\n" + JSON.stringify(value, null, 2).slice(0, 1e3) + "\n```";
457
+ } else {
458
+ strValue = String(value).slice(0, 1024);
459
+ }
460
+ fields.push({
461
+ name: key.slice(0, 256),
462
+ value: strValue,
463
+ inline: typeof value !== "object" && String(value).length < 50
464
+ });
465
+ }
466
+ return fields;
467
+ }
468
+ async sendWebhook(payload) {
469
+ const response = await fetch(this.config.webhookUrl, {
470
+ method: "POST",
471
+ headers: { "Content-Type": "application/json" },
472
+ body: JSON.stringify(payload)
473
+ });
474
+ if (!response.ok) {
475
+ const text = await response.text();
476
+ throw new Error(`Discord webhook failed: ${response.status} ${text}`);
477
+ }
478
+ }
479
+ chunkArray(array, size) {
480
+ const chunks = [];
481
+ for (let i = 0; i < array.length; i += size) {
482
+ chunks.push(array.slice(i, i + size));
483
+ }
484
+ return chunks;
485
+ }
486
+ };
487
+
488
+ // src/transports/telegram.ts
489
+ var LEVEL_EMOJI = {
490
+ off: "",
491
+ error: "\u{1F534}",
492
+ warn: "\u{1F7E1}",
493
+ info: "\u{1F7E2}",
494
+ http: "\u{1F535}",
495
+ verbose: "\u{1F7E3}",
496
+ debug: "\u26AA",
497
+ silly: "\u26AB"
498
+ };
499
+ var TelegramTransport = class extends BaseHttpTransport {
500
+ config;
501
+ apiUrl;
502
+ constructor(config) {
503
+ super({
504
+ batchSize: config.batchSize ?? 20,
505
+ flushInterval: config.flushInterval ?? 1e3,
506
+ maxRetries: config.maxRetries,
507
+ retryDelay: config.retryDelay
508
+ });
509
+ this.config = config;
510
+ this.apiUrl = `https://api.telegram.org/bot${config.botToken}`;
511
+ }
512
+ async sendBatch(messages) {
513
+ const text = this.formatBatchMessage(messages);
514
+ await this.sendMessage(text, messages);
515
+ }
516
+ formatBatchMessage(messages) {
517
+ const parseMode = this.config.parseMode ?? "Markdown";
518
+ return messages.map((msg) => {
519
+ if (parseMode === "HTML") {
520
+ return this.formatHtml(msg);
521
+ }
522
+ return this.formatMarkdown(msg, parseMode === "MarkdownV2");
523
+ }).join("\n\n---\n\n");
524
+ }
525
+ formatMarkdown(msg, v2) {
526
+ const emoji = LEVEL_EMOJI[msg.level];
527
+ const escape = v2 ? this.escapeMarkdownV2.bind(this) : (s) => s;
528
+ let text = `${emoji} *${msg.level.toUpperCase()}* \\[${escape(msg.context || "APP")}\\]
529
+ `;
530
+ text += escape(msg.message);
531
+ if (msg.meta && Object.keys(msg.meta).length > 0) {
532
+ const metaStr = JSON.stringify(msg.meta, null, 2);
533
+ text += "\n```json\n" + metaStr + "\n```";
534
+ }
535
+ return text;
536
+ }
537
+ formatHtml(msg) {
538
+ const emoji = LEVEL_EMOJI[msg.level];
539
+ let text = `${emoji} <b>${msg.level.toUpperCase()}</b> [${this.escapeHtml(msg.context || "APP")}]
540
+ `;
541
+ text += this.escapeHtml(msg.message);
542
+ if (msg.meta && Object.keys(msg.meta).length > 0) {
543
+ const metaStr = JSON.stringify(msg.meta, null, 2);
544
+ text += "\n<pre>" + this.escapeHtml(metaStr) + "</pre>";
545
+ }
546
+ return text;
547
+ }
548
+ shouldMute(messages) {
549
+ if (this.config.disableNotification !== void 0) {
550
+ return this.config.disableNotification;
551
+ }
552
+ return !messages.some((m) => m.level === "error");
553
+ }
554
+ async sendMessage(text, messages) {
555
+ const body = {
556
+ chat_id: this.config.chatId,
557
+ text,
558
+ parse_mode: this.config.parseMode ?? "Markdown",
559
+ disable_notification: this.shouldMute(messages)
560
+ };
561
+ if (this.config.threadId) {
562
+ body.message_thread_id = this.config.threadId;
563
+ }
564
+ if (this.config.replyToMessageId) {
565
+ body.reply_to_message_id = this.config.replyToMessageId;
566
+ }
567
+ const response = await fetch(`${this.apiUrl}/sendMessage`, {
568
+ method: "POST",
569
+ headers: { "Content-Type": "application/json" },
570
+ body: JSON.stringify(body)
571
+ });
572
+ if (!response.ok) {
573
+ const result = await response.json();
574
+ throw new Error(`Telegram API failed: ${response.status} ${JSON.stringify(result)}`);
575
+ }
576
+ }
577
+ escapeMarkdownV2(text) {
578
+ return text.replace(/[_*[\]()~`>#+\-=|{}.!]/g, "\\$&");
579
+ }
580
+ escapeHtml(text) {
581
+ const entities = {
582
+ "&": "&amp;",
583
+ "<": "&lt;",
584
+ ">": "&gt;",
585
+ '"': "&quot;",
586
+ "'": "&#39;"
587
+ };
588
+ return text.replace(/[&<>"']/g, (c) => entities[c] || c);
589
+ }
590
+ };
591
+ var CloudWatchTransport = class extends BaseHttpTransport {
592
+ config;
593
+ client;
594
+ sequenceToken;
595
+ initialized = false;
596
+ initPromise = null;
597
+ constructor(config) {
598
+ super({
599
+ batchSize: config.batchSize ?? 100,
600
+ flushInterval: config.flushInterval ?? 1e3,
601
+ maxRetries: config.maxRetries,
602
+ retryDelay: config.retryDelay
603
+ });
604
+ this.config = config;
605
+ this.client = new clientCloudwatchLogs.CloudWatchLogsClient({
606
+ region: config.region,
607
+ credentials: {
608
+ accessKeyId: config.accessKeyId,
609
+ secretAccessKey: config.secretAccessKey
610
+ }
611
+ });
612
+ }
613
+ async sendBatch(messages) {
614
+ await this.ensureInitialized();
615
+ const logEvents = messages.map((msg) => ({
616
+ timestamp: msg.timestamp.getTime(),
617
+ message: JSON.stringify({
618
+ level: msg.level,
619
+ message: msg.message,
620
+ context: msg.context,
621
+ ...msg.meta
622
+ })
623
+ }));
624
+ logEvents.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
625
+ const command = new clientCloudwatchLogs.PutLogEventsCommand({
626
+ logGroupName: this.config.logGroupName,
627
+ logStreamName: this.config.logStreamName,
628
+ logEvents,
629
+ sequenceToken: this.sequenceToken
630
+ });
631
+ try {
632
+ const response = await this.client.send(command);
633
+ this.sequenceToken = response.nextSequenceToken;
634
+ } catch (error) {
635
+ if (this.isInvalidSequenceTokenError(error)) {
636
+ await this.fetchSequenceToken();
637
+ const retryCommand = new clientCloudwatchLogs.PutLogEventsCommand({
638
+ logGroupName: this.config.logGroupName,
639
+ logStreamName: this.config.logStreamName,
640
+ logEvents,
641
+ sequenceToken: this.sequenceToken
642
+ });
643
+ const response = await this.client.send(retryCommand);
644
+ this.sequenceToken = response.nextSequenceToken;
645
+ } else {
646
+ throw error;
647
+ }
648
+ }
649
+ }
650
+ async ensureInitialized() {
651
+ if (this.initialized) return;
652
+ if (!this.initPromise) {
653
+ this.initPromise = this.initialize();
654
+ }
655
+ await this.initPromise;
656
+ }
657
+ async initialize() {
658
+ if (this.config.createLogGroup) {
659
+ await this.createLogGroupIfNotExists();
660
+ }
661
+ if (this.config.createLogStream !== false) {
662
+ await this.createLogStreamIfNotExists();
663
+ }
664
+ await this.fetchSequenceToken();
665
+ this.initialized = true;
666
+ }
667
+ async createLogGroupIfNotExists() {
668
+ try {
669
+ await this.client.send(
670
+ new clientCloudwatchLogs.CreateLogGroupCommand({
671
+ logGroupName: this.config.logGroupName
672
+ })
673
+ );
674
+ } catch (error) {
675
+ if (!this.isResourceAlreadyExistsError(error)) {
676
+ throw error;
677
+ }
678
+ }
679
+ }
680
+ async createLogStreamIfNotExists() {
681
+ try {
682
+ await this.client.send(
683
+ new clientCloudwatchLogs.CreateLogStreamCommand({
684
+ logGroupName: this.config.logGroupName,
685
+ logStreamName: this.config.logStreamName
686
+ })
687
+ );
688
+ } catch (error) {
689
+ if (!this.isResourceAlreadyExistsError(error)) {
690
+ throw error;
691
+ }
692
+ }
693
+ }
694
+ async fetchSequenceToken() {
695
+ const response = await this.client.send(
696
+ new clientCloudwatchLogs.DescribeLogStreamsCommand({
697
+ logGroupName: this.config.logGroupName,
698
+ logStreamNamePrefix: this.config.logStreamName,
699
+ limit: 1
700
+ })
701
+ );
702
+ const stream = response.logStreams?.find((s) => s.logStreamName === this.config.logStreamName);
703
+ this.sequenceToken = stream?.uploadSequenceToken;
704
+ }
705
+ isResourceAlreadyExistsError(error) {
706
+ return typeof error === "object" && error !== null && "name" in error && error.name === "ResourceAlreadyExistsException";
707
+ }
708
+ isInvalidSequenceTokenError(error) {
709
+ return typeof error === "object" && error !== null && "name" in error && error.name === "InvalidSequenceTokenException";
710
+ }
711
+ };
712
+
242
713
  // src/transports.ts
243
714
  function createTransports(config, store) {
244
715
  const result = [
245
716
  new winston.transports.Console({
246
- format: createFormat(config.console.format, store)
717
+ format: winston.format.combine(
718
+ createFilterFormat(config.console.level, config.console.rules, store),
719
+ createFormat(config.console.format, store)
720
+ )
247
721
  })
248
722
  ];
249
723
  if (config.file) {
250
724
  result.push(
251
725
  new DailyRotateFile__default.default({
252
- format: createFormat(config.file.format, store),
726
+ format: winston.format.combine(
727
+ createFilterFormat(config.file.level, config.file.rules, store),
728
+ createFormat(config.file.format, store)
729
+ ),
253
730
  dirname: config.file.dirname,
254
731
  filename: config.file.filename,
255
732
  datePattern: config.file.datePattern ?? "YYYY-MM-DD",
@@ -259,6 +736,30 @@ function createTransports(config, store) {
259
736
  })
260
737
  );
261
738
  }
739
+ if (config.discord) {
740
+ const discord = new DiscordTransport(config.discord);
741
+ discord.format = winston.format.combine(
742
+ createFilterFormat(config.discord.level, config.discord.rules, store),
743
+ winston.format.timestamp()
744
+ );
745
+ result.push(discord);
746
+ }
747
+ if (config.telegram) {
748
+ const telegram = new TelegramTransport(config.telegram);
749
+ telegram.format = winston.format.combine(
750
+ createFilterFormat(config.telegram.level, config.telegram.rules, store),
751
+ winston.format.timestamp()
752
+ );
753
+ result.push(telegram);
754
+ }
755
+ if (config.cloudwatch) {
756
+ const cloudwatch = new CloudWatchTransport(config.cloudwatch);
757
+ cloudwatch.format = winston.format.combine(
758
+ createFilterFormat(config.cloudwatch.level, config.cloudwatch.rules, store),
759
+ winston.format.timestamp()
760
+ );
761
+ result.push(cloudwatch);
762
+ }
262
763
  return result;
263
764
  }
264
765
  function createExceptionHandlers(config, store) {
@@ -283,25 +784,6 @@ function createExceptionHandlers(config, store) {
283
784
  return result;
284
785
  }
285
786
 
286
- // src/types.ts
287
- var LOG_LEVELS = {
288
- error: 0,
289
- warn: 1,
290
- info: 2,
291
- http: 3,
292
- verbose: 4,
293
- debug: 5,
294
- silly: 6
295
- };
296
- function isValidLogLevel(level) {
297
- return level in LOG_LEVELS;
298
- }
299
- function assertLogLevel(level) {
300
- if (!isValidLogLevel(level)) {
301
- throw new Error(`Invalid log level: "${level}". Valid levels: ${Object.keys(LOG_LEVELS).join(", ")}`);
302
- }
303
- }
304
-
305
787
  // src/state.ts
306
788
  function parseLevelConfig(level) {
307
789
  if (typeof level === "string") {
@@ -544,9 +1026,14 @@ function getOrGenerateRequestId(headers, options = {}) {
544
1026
  return extractRequestId(headers) ?? generateRequestId(options);
545
1027
  }
546
1028
 
1029
+ exports.BaseHttpTransport = BaseHttpTransport;
1030
+ exports.CloudWatchTransport = CloudWatchTransport;
1031
+ exports.DiscordTransport = DiscordTransport;
547
1032
  exports.LOG_LEVELS = LOG_LEVELS;
548
1033
  exports.Logger = Logger;
549
1034
  exports.LoggerStore = LoggerStore;
1035
+ exports.MessageBuffer = MessageBuffer;
1036
+ exports.TelegramTransport = TelegramTransport;
550
1037
  exports.assertLogLevel = assertLogLevel;
551
1038
  exports.createMasker = createMasker;
552
1039
  exports.createSingletonLogger = createSingletonLogger;
@@ -558,6 +1045,7 @@ exports.generateRequestId = generateRequestId;
558
1045
  exports.getOrGenerateRequestId = getOrGenerateRequestId;
559
1046
  exports.isValidLogLevel = isValidLogLevel;
560
1047
  exports.maskSecrets = maskSecrets;
1048
+ exports.matchesContext = matchesContext;
561
1049
  exports.measureAsync = measureAsync;
562
1050
  exports.measureSync = measureSync;
563
1051
  //# sourceMappingURL=index.js.map