ntlogger 2.10.0 → 3.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.
@@ -1,14 +1,36 @@
1
1
  /**
2
2
  * @file /plugins/postgres.js
3
3
  * @description Stores logs in PostgresSQL database.
4
+ *
5
+ * `pg` is an optional peer dependency. Install it when using this plugin:
6
+ * npm install pg
4
7
  */
5
8
 
6
9
  const Transport = require('winston-transport');
7
- const { Pool } = require('pg');
8
10
  const levels = require('../lib/levels');
9
11
 
10
- let connectionPool = null;
11
- let tableInitialized = false;
12
+ /**
13
+ * Requires an optional peer dependency, translating only that package's
14
+ * MODULE_NOT_FOUND into an actionable install message. Any other failure
15
+ * (including a missing module *inside* the package) propagates unchanged.
16
+ * @param {string} moduleId - The module specifier to require.
17
+ * @param {string} packageName - The npm package to install.
18
+ * @returns {*} The resolved module.
19
+ */
20
+ function requirePeer(moduleId, packageName) {
21
+ try {
22
+ return require(moduleId);
23
+ } catch (err) {
24
+ if (err && err.code === 'MODULE_NOT_FOUND' && String(err.message).includes(`'${moduleId}'`)) {
25
+ throw new Error(`ntlogger Postgres plugin requires '${packageName}'. Install it with: npm install ${packageName}`);
26
+ }
27
+ throw err;
28
+ }
29
+ }
30
+
31
+ const { Pool } = requirePeer('pg', 'pg');
32
+
33
+
12
34
 
13
35
  class PostgreSQLTransport extends Transport {
14
36
  constructor(opts = {}) {
@@ -17,6 +39,11 @@ class PostgreSQLTransport extends Transport {
17
39
  this.name = 'PostgreSQL Transport for NTLogger';
18
40
  this.database = opts.database || 'test';
19
41
  this.table = opts.table || 'logs';
42
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(this.table)) {
43
+ throw new Error('Log table must be a simple SQL identifier');
44
+ }
45
+ this.tableInitialized = false;
46
+ this.closed = false;
20
47
 
21
48
  this.logLevel = opts.level ? levels[opts.level] : levels.info;
22
49
 
@@ -26,58 +53,44 @@ class PostgreSQLTransport extends Transport {
26
53
  user: opts.user || 'postgres',
27
54
  password: opts.password || '',
28
55
  database: this.database,
56
+ ssl: opts.ssl,
29
57
  max: opts.connectionLimit || 10,
30
58
  idleTimeoutMillis: opts.idleTimeout || 60000,
31
59
  connectionTimeoutMillis: opts.connectionTimeout || 2000,
32
60
  };
33
61
 
34
- this.init();
62
+ this.pool = new Pool(this.poolConfig);
63
+ this.pool.on('error', error => { this.lastError = error; });
64
+ this.ready = this.checkLogTable();
65
+ this.ready.catch(error => { this.lastError = error; });
35
66
  }
36
67
 
37
- async init() {
38
- try {
39
- if (!connectionPool) {
40
- connectionPool = new Pool(this.poolConfig);
41
- }
42
- this.pool = connectionPool;
43
- await this.checkLogTable();
44
- } catch (err) {
45
- console.error(`Failed to initialize ${this.name}:`, err);
46
- }
47
- }
68
+ async init() { return this.ready; }
48
69
 
49
70
  async log(info, callback) {
50
- setImmediate(() => {
51
- this.emit('logged', info);
52
- });
53
-
54
- if (!tableInitialized) {
55
- await this.checkLogTable();
56
- }
57
-
58
- const { level, message, ...meta } = info;
59
-
60
- if (levels[level] > this.logLevel) {
61
- callback();
62
- return;
63
- }
64
-
65
- const metaString = JSON.stringify(meta);
66
- const timestamp = this.formatTimestamp(meta.timeCreated);
67
-
71
+ if (levels[info.level] > this.logLevel) { callback(); return; }
68
72
  try {
73
+ if (this.closed) throw new Error('Database transport is closed');
74
+ await this.ready;
75
+ const { level, message, ...meta } = info;
76
+ const timestamp = this.formatTimestamp(meta.timestamp);
69
77
  await this.pool.query(
70
78
  `INSERT INTO ${this.table} (level, message, meta, timestamp) VALUES ($1, $2, $3, $4)`,
71
- [level, message, metaString, timestamp]
79
+ [level, message, JSON.stringify(meta), timestamp]
72
80
  );
73
- } catch (err) {
74
- console.error(`Failed to log message to ${this.name}:`, err);
81
+ this.emit('logged', info);
82
+ callback();
83
+ } catch (error) {
84
+ this.lastError = error;
85
+ callback(error);
75
86
  }
76
-
77
- callback();
78
87
  }
79
88
 
80
89
  formatTimestamp(timeCreated) {
90
+ if (timeCreated && /^\d{4}-/.test(timeCreated)) {
91
+ const date = new Date(timeCreated);
92
+ return date.toISOString();
93
+ }
81
94
  if (!timeCreated) {
82
95
  return new Date().toISOString().slice(0, 19).replace('T', ' ');
83
96
  }
@@ -96,43 +109,32 @@ class PostgreSQLTransport extends Transport {
96
109
  return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${hours}:${minutes}:${seconds}`;
97
110
  }
98
111
 
99
- async checkLogTable() {
100
- if (tableInitialized) return;
101
-
102
- try {
103
- const result = await this.pool.query(
104
- `SELECT to_regclass('${this.table}') as tablename`
105
- );
106
-
107
- if (!result.rows[0].tablename) {
108
- await this.pool.query(
109
- `CREATE TABLE IF NOT EXISTS ${this.table} (
110
- id SERIAL PRIMARY KEY,
111
- level VARCHAR(255) NOT NULL,
112
- message TEXT NOT NULL,
113
- meta JSONB,
114
- timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
115
- )`
116
- );
117
- }
118
-
119
- tableInitialized = true;
120
- } catch (err) {
121
- console.error(`Failed to check or create table ${this.table}:`, err);
112
+ checkLogTable() {
113
+ if (!this.tableReady) {
114
+ this.tableReady = this.pool.query(`CREATE TABLE IF NOT EXISTS ${this.table} (id SERIAL PRIMARY KEY, level VARCHAR(255) NOT NULL, message TEXT NOT NULL, meta JSONB, timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP)`)
115
+ .then(() => { this.tableInitialized = true; })
116
+ .catch(error => { this.tableReady = null; throw error; });
122
117
  }
118
+ return this.tableReady;
123
119
  }
124
120
 
125
121
  isActive() {
126
- return this.pool && !this.pool.ended;
122
+ return this.pool && !this.closed;
123
+ }
124
+
125
+ async flush() {
126
+ await this.ready;
127
+ if (this.lastError) throw this.lastError;
127
128
  }
128
129
 
129
- async close() {
130
- if (connectionPool) {
131
- await connectionPool.end();
132
- connectionPool = null;
133
- console.log('Closed PostgreSQL connection pool.');
130
+ close() {
131
+ if (!this.closePromise) {
132
+ this.closed = true;
133
+ this.closePromise = this.ready.catch(() => {}).then(() => this.pool.end());
134
134
  }
135
+ return this.closePromise;
135
136
  }
137
+
136
138
  }
137
139
 
138
140
  module.exports = {
package/plugins/sentry.js CHANGED
@@ -1,10 +1,33 @@
1
1
  /**
2
2
  * @file /plugins/sentry.js
3
3
  * @description Initializes the Sentry SDK for error tracking and defines a custom Winston transport.
4
+ *
5
+ * `@sentry/node` is an optional peer dependency. Install it when using this plugin:
6
+ * npm install @sentry/node
4
7
  */
5
8
 
6
9
  const Transport = require('winston-transport');
7
- const Sentry = require("@sentry/node");
10
+
11
+ /**
12
+ * Requires an optional peer dependency, translating only that package's
13
+ * MODULE_NOT_FOUND into an actionable install message. Any other failure
14
+ * (including a missing module *inside* the package) propagates unchanged.
15
+ * @param {string} moduleId - The module specifier to require.
16
+ * @param {string} packageName - The npm package to install.
17
+ * @returns {*} The resolved module.
18
+ */
19
+ function requirePeer(moduleId, packageName) {
20
+ try {
21
+ return require(moduleId);
22
+ } catch (err) {
23
+ if (err && err.code === 'MODULE_NOT_FOUND' && String(err.message).includes(`'${moduleId}'`)) {
24
+ throw new Error(`ntlogger Sentry plugin requires '${packageName}'. Install it with: npm install ${packageName}`);
25
+ }
26
+ throw err;
27
+ }
28
+ }
29
+
30
+ const Sentry = requirePeer('@sentry/node', '@sentry/node');
8
31
 
9
32
  /**
10
33
  * Custom Winston transport for sending logs to Sentry.
@@ -14,6 +37,7 @@ class SentryTransport extends Transport {
14
37
  super(opts);
15
38
 
16
39
  this.name = 'Sentry Transport for NTLogger';
40
+ this.flushTimeout = opts.flushTimeout ?? 5000;
17
41
  this.init(opts);
18
42
  }
19
43
 
@@ -37,13 +61,22 @@ class SentryTransport extends Transport {
37
61
  const { level, message, ...meta } = info;
38
62
 
39
63
  if (level === 'error') {
40
- Sentry.captureException(new Error(message), { extra: meta });
64
+ const error = new Error(message);
65
+ if (meta.stack) error.stack = meta.stack;
66
+ Sentry.captureException(error, { extra: meta });
41
67
  } else {
42
- Sentry.captureMessage(message, { level, extra: meta });
68
+ Sentry.captureMessage(message, { level: ({ warn: 'warning', trace: 'debug', internal: 'debug' })[level] || level, extra: meta });
43
69
  }
44
70
 
45
71
  callback();
46
72
  }
73
+ async flush() {
74
+ if (!await Sentry.flush(this.flushTimeout)) throw new Error('Sentry flush timed out');
75
+ }
76
+
77
+ // Flush without disabling the SDK shared with the host application.
78
+ close() { return this.flush(); }
79
+
47
80
  }
48
81
 
49
82
  module.exports = {
package/plugins/syslog.js CHANGED
@@ -16,12 +16,12 @@ class SyslogTransport extends Transport {
16
16
  port: opts.port || 514,
17
17
  protocol: opts.protocol || 'UDP', // Options: 'UDP', 'TCP', 'TLS'
18
18
  rfc: opts.rfc || 'RFC-5424', // Options: 'RFC-3164', 'RFC-5424'
19
- facility: opts.facility || 1,
19
+ facility: opts.facility ?? 1,
20
20
  appName: opts.appName || 'NTLogger',
21
21
  hostname: opts.hostname || require('os').hostname(),
22
22
  });
23
23
 
24
- this.levels = {
24
+ this.severities = {
25
25
  internal: 7, // Debug level for internal logs
26
26
  trace: 7, // Debug
27
27
  debug: 7, // Debug
@@ -30,7 +30,7 @@ class SyslogTransport extends Transport {
30
30
  error: 3, // Error
31
31
  fatal: 2, // Critical
32
32
  };
33
- this.logLevel = opts.level ? this.levels[opts.level] : this.levels.info;
33
+ this.logLevel = opts.level ? this.severities[opts.level] : this.severities.info;
34
34
 
35
35
  if (this.client.protocol === 'TCP' || this.client.protocol === 'TLS') {
36
36
  this.client.connect().catch(err => {
@@ -51,14 +51,12 @@ class SyslogTransport extends Transport {
51
51
 
52
52
  const { level, message, ...meta } = info;
53
53
 
54
- if (this.levels[level] > this.logLevel) {
54
+ if (this.severities[level] > this.logLevel) {
55
55
  callback();
56
56
  return;
57
57
  }
58
58
 
59
- this.client.send(this.levels[level], message, meta);
60
-
61
- callback();
59
+ this.client.send(this.severities[level], message, meta, callback);
62
60
  }
63
61
 
64
62
  /**
@@ -74,8 +72,7 @@ class SyslogTransport extends Transport {
74
72
  * @returns {Promise<void>}
75
73
  */
76
74
  async close() {
77
- this.client.close();
78
- console.log('Closed Syslog client connection.');
75
+ await this.client.close();
79
76
  }
80
77
  }
81
78
 
package/plugins/teams.js CHANGED
@@ -4,8 +4,7 @@
4
4
  */
5
5
 
6
6
  const Transport = require('winston-transport');
7
- const https = require('https');
8
- const { URL } = require('url');
7
+ const { HttpDelivery } = require('./lib/httpDelivery');
9
8
 
10
9
  const levels = require('../lib/levels');
11
10
 
@@ -16,13 +15,9 @@ class TeamsTransport extends Transport {
16
15
  this.name = 'Teams Webhook Transport for NTLogger';
17
16
 
18
17
  this.webhookUrl = opts.webhookUrl;
18
+ this.delivery = new HttpDelivery(opts.webhookUrl, opts);
19
19
  this.strict = opts.strict || false;
20
20
 
21
- // Retry logic with exponential back-off
22
- // https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL%2Ctext1#rate-limiting-for-connectors
23
- this.maxRetries = opts.maxRetries || 3;
24
- this.retryDelay = opts.retryDelay || 1000; // Initial delay for exponential back-off (in ms)
25
-
26
21
  try {
27
22
  // Set the log level
28
23
  this.level = opts.level || 'info';
@@ -38,10 +33,6 @@ class TeamsTransport extends Transport {
38
33
  }
39
34
 
40
35
  async log(info, callback) {
41
- setImmediate(() => {
42
- this.emit('logged', info);
43
- });
44
-
45
36
  const { level, message, ...meta } = info;
46
37
 
47
38
  // Check if the log level matches the configured level (strict mode) or is at or below the configured level
@@ -57,62 +48,14 @@ class TeamsTransport extends Transport {
57
48
  }
58
49
  }
59
50
 
60
- const payload = this.createPayload(level, message, meta);
61
- const webhookUrl = new URL(this.webhookUrl);
62
-
63
- const options = {
64
- hostname: webhookUrl.hostname,
65
- path: webhookUrl.pathname + webhookUrl.search,
66
- method: 'POST',
67
- headers: {
68
- 'Content-Type': 'application/json',
69
- 'Content-Length': Buffer.byteLength(payload),
70
- },
71
- };
72
-
73
- const sendLog = (retryCount = 0) => {
74
- const req = https.request(options, (res) => {
75
- let responseContent = '';
76
-
77
- res.on('data', (chunk) => {
78
- responseContent += chunk;
79
- });
80
-
81
- res.on('end', () => {
82
- if (responseContent.includes("Microsoft Teams endpoint returned HTTP error 429")) {
83
- console.error('Rate limit hit, retrying with exponential back-off...');
84
- if (retryCount < this.maxRetries) {
85
- setTimeout(() => {
86
- sendLog(retryCount + 1);
87
- }, this.retryDelay * Math.pow(2, retryCount)); // Exponential back-off
88
- } else {
89
- console.error('Max retries reached, log sending failed.');
90
- callback();
91
- }
92
- } else {
93
- callback();
94
- }
95
- });
96
- });
97
-
98
- req.on('error', (e) => {
99
- console.error(`Failed to send log to Teams: ${e.message}`);
100
- if (retryCount < this.maxRetries) {
101
- console.log(`Retrying... (${retryCount + 1}/${this.maxRetries})`);
102
- setTimeout(() => {
103
- sendLog(retryCount + 1);
104
- }, this.retryDelay * Math.pow(2, retryCount));
105
- } else {
106
- console.error('Max retries reached, log sending failed.');
107
- callback();
108
- }
109
- });
110
-
111
- req.write(payload);
112
- req.end();
113
- };
51
+ try {
52
+ const payload = this.createPayload(level, message, meta);
53
+ if (this.closed) throw new Error('HTTP transport is closed');
54
+ await this.delivery.send(payload);
55
+ this.emit('logged', info);
56
+ callback();
57
+ } catch (error) { callback(error); }
114
58
 
115
- sendLog();
116
59
  }
117
60
 
118
61
  createPayload(level, message, meta) {
@@ -132,7 +75,7 @@ class TeamsTransport extends Transport {
132
75
  const metaArray = Array.isArray(meta) ? meta : Object.values(meta);
133
76
 
134
77
  metaArray.forEach(item => {
135
- if (typeof item !== 'object' || !item.type) {
78
+ if (!item || typeof item !== 'object' || !item.type) {
136
79
  return;
137
80
  }
138
81
 
@@ -304,6 +247,12 @@ class TeamsTransport extends Transport {
304
247
  ]
305
248
  });
306
249
  }
250
+ flush() { return this.delivery.flush(); }
251
+ close() {
252
+ this.closed = true;
253
+ return this.delivery.flush();
254
+ }
255
+
307
256
  }
308
257
 
309
258
  module.exports = {