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.
package/plugins/mysql.js CHANGED
@@ -1,13 +1,35 @@
1
1
  /**
2
2
  * @file /plugins/mysql.js
3
3
  * @description Stores logs in MySQL database.
4
+ *
5
+ * `mysql2` is an optional peer dependency. Install it when using this plugin:
6
+ * npm install mysql2
4
7
  */
5
8
 
6
9
  const Transport = require('winston-transport');
7
- const mysql = require('mysql2/promise');
8
10
 
9
- let connectionPool = null;
10
- let tableInitialized = false;
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 MySQL plugin requires '${packageName}'. Install it with: npm install ${packageName}`);
25
+ }
26
+ throw err;
27
+ }
28
+ }
29
+
30
+ const mysql = requirePeer('mysql2/promise', 'mysql2');
31
+
32
+
11
33
 
12
34
  class MySQLTransport extends Transport {
13
35
  constructor(opts = {}) {
@@ -16,6 +38,11 @@ class MySQLTransport extends Transport {
16
38
  this.name = 'MySQL Transport for NTLogger';
17
39
  this.database = opts.database || 'test';
18
40
  this.table = opts.table || 'logs';
41
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(this.table)) {
42
+ throw new Error('Log table must be a simple SQL identifier');
43
+ }
44
+ this.tableInitialized = false;
45
+ this.closed = false;
19
46
 
20
47
  this.levels = {
21
48
  internal: 6,
@@ -34,70 +61,52 @@ class MySQLTransport extends Transport {
34
61
  user: opts.user || 'root',
35
62
  password: opts.password || '',
36
63
  database: this.database,
37
- waitForConnections: opts.waitForConnections || true,
64
+ ssl: opts.ssl,
65
+ timezone: "+00:00",
66
+ waitForConnections: opts.waitForConnections ?? true,
38
67
  connectionLimit: opts.connectionLimit || 10,
39
68
  queueLimit: opts.queueLimit || 0,
40
- maxIdle: opts.maxIdle || 10,
69
+ maxIdle: opts.maxIdle ?? 10,
41
70
  idleTimeout: opts.idleTimeout || 60000,
42
- enableKeepAlive: opts.enableKeepAlive || true,
71
+ enableKeepAlive: opts.enableKeepAlive ?? true,
43
72
  keepAliveInitialDelay: opts.keepAliveInitialDelay || 0,
44
73
  };
45
74
 
46
- this.init();
75
+ this.pool = mysql.createPool(this.poolConfig);
76
+ this.pool.on('error', error => { this.lastError = error; });
77
+ this.ready = this.checkLogTable();
78
+ this.ready.catch(error => { this.lastError = error; });
47
79
  }
48
80
 
49
81
  /**
50
82
  * Initializes the MySQL connection pool and checks the log table.
51
83
  */
52
- async init() {
53
- try {
54
- if (!connectionPool) {
55
- connectionPool = mysql.createPool(this.poolConfig);
56
- }
57
- this.pool = connectionPool;
58
- await this.checkLogTable();
59
- } catch (err) {
60
- console.error(`Failed to initialize ${this.name}:`, err);
61
- }
62
- }
84
+ async init() { return this.ready; }
63
85
 
64
86
  async log(info, callback) {
65
- setImmediate(() => {
66
- this.emit('logged', info);
67
- });
68
-
69
- if (!tableInitialized) {
70
- await this.checkLogTable();
71
- }
72
-
73
- const { level, message, ...meta } = info;
74
-
75
- if (this.levels[level] > this.logLevel) {
76
- callback();
77
- return;
78
- }
79
-
80
- const metaString = JSON.stringify(meta);
81
- const timestamp = this.formatTimestamp(meta.timeCreated);
82
-
87
+ if (this.levels[info.level] > this.logLevel) { callback(); return; }
83
88
  try {
89
+ if (this.closed) throw new Error('Database transport is closed');
90
+ await this.ready;
91
+ const { level, message, ...meta } = info;
92
+ const timestamp = this.formatTimestamp(meta.timestamp);
84
93
  await this.pool.execute(
85
94
  `INSERT INTO ${this.table} (level, message, meta, timestamp) VALUES (?, ?, ?, ?)`,
86
- [level, message, metaString, timestamp]
95
+ [level, message, JSON.stringify(meta), timestamp]
87
96
  );
88
- } catch (err) {
89
- console.error(`Failed to log message to ${this.name}:`, err);
97
+ this.emit('logged', info);
98
+ callback();
99
+ } catch (error) {
100
+ this.lastError = error;
101
+ callback(error);
90
102
  }
91
-
92
- callback();
93
103
  }
94
104
 
95
- /**
96
- * Formats the timestamp for the MySQL database.
97
- * @param {string} timeCreated - The time created string.
98
- * @returns {string} - The formatted timestamp.
99
- */
100
105
  formatTimestamp(timeCreated) {
106
+ if (timeCreated && /^\d{4}-/.test(timeCreated)) {
107
+ const date = new Date(timeCreated);
108
+ return date.toISOString().slice(0, 23).replace('T', ' ');
109
+ }
101
110
  if (!timeCreated) {
102
111
  return new Date().toISOString().slice(0, 19).replace('T', ' ');
103
112
  }
@@ -119,55 +128,36 @@ class MySQLTransport extends Transport {
119
128
  /**
120
129
  * Checks if the log table exists in the MySQL database. If it does not exist, it will create it.
121
130
  */
122
- async checkLogTable() {
123
- if (tableInitialized) return;
124
-
125
- try {
126
- const [rows] = await this.pool.execute(
127
- `SELECT COUNT(*) AS count
128
- FROM information_schema.tables
129
- WHERE table_schema = ?
130
- AND table_name = ?`,
131
- [this.database, this.table]
132
- );
133
-
134
- if (rows[0].count === 0) {
135
- await this.pool.execute(
136
- `CREATE TABLE IF NOT EXISTS ${this.table} (
137
- id INT AUTO_INCREMENT PRIMARY KEY,
138
- level VARCHAR(255) NOT NULL,
139
- message TEXT NOT NULL,
140
- meta TEXT,
141
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
142
- )`
143
- );
144
- }
145
-
146
- tableInitialized = true;
147
- } catch (err) {
148
- console.error(`Failed to check or create table ${this.table}:`, err);
131
+ checkLogTable() {
132
+ if (!this.tableReady) {
133
+ this.tableReady = this.pool.execute(`CREATE TABLE IF NOT EXISTS ${this.table} (id INT AUTO_INCREMENT PRIMARY KEY, level VARCHAR(255) NOT NULL, message TEXT NOT NULL, meta TEXT, timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3))`)
134
+ .then(() => { this.tableInitialized = true; })
135
+ .catch(error => { this.tableReady = null; throw error; });
149
136
  }
137
+ return this.tableReady;
150
138
  }
151
139
 
152
- /**
153
- * Checks if the MySQL connection pool is active.
154
- * @returns {boolean} - True if the connection pool is active, false otherwise.
155
- */
156
140
  isActive() {
157
- return this.pool && !this.pool.ended;
141
+ return this.pool && !this.closed;
158
142
  }
159
143
 
160
144
  /**
161
145
  * Closes the MySQL connection pool.
162
146
  * @returns {Promise<void>}
163
147
  */
164
- async close() {
165
- if (connectionPool) {
166
- await connectionPool.end();
167
- connectionPool = null;
168
- console.log('Closed MySQL connection pool.');
148
+ async flush() {
149
+ await this.ready;
150
+ if (this.lastError) throw this.lastError;
151
+ }
152
+
153
+ close() {
154
+ if (!this.closePromise) {
155
+ this.closed = true;
156
+ this.closePromise = this.ready.catch(() => {}).then(() => this.pool.end());
169
157
  }
158
+ return this.closePromise;
170
159
  }
160
+
171
161
  }
172
162
 
173
163
  module.exports = {
@@ -4,10 +4,9 @@
4
4
  */
5
5
 
6
6
  const Transport = require('winston-transport');
7
- const https = require('https');
8
- const http = require('http');
9
7
  const { URL } = require('url');
10
8
  const levels = require('../lib/levels');
9
+ const { HttpDelivery } = require('./lib/httpDelivery');
11
10
 
12
11
  /**
13
12
  * Strips ANSI escape codes from a string
@@ -44,7 +43,7 @@ function cleanObject(obj) {
44
43
  const cleaned = {};
45
44
  for (const [key, value] of Object.entries(obj)) {
46
45
  // Skip timestamp fields
47
- if (key === 'timestamp' || key === 'timeCreated') {
46
+ if (key === 'timeCreated') {
48
47
  continue;
49
48
  }
50
49
  cleaned[key] = cleanObject(value);
@@ -86,7 +85,10 @@ class OpenObserveTransport extends Transport {
86
85
 
87
86
  // Optional configuration
88
87
  this.batchSize = opts.batchSize || 100;
89
- this.timeThreshold = opts.timeThreshold || 5000;
88
+ this.timeThreshold = opts.timeThreshold ?? 5000;
89
+ if (!Number.isInteger(this.batchSize) || this.batchSize < 1 || !Number.isFinite(this.timeThreshold) || this.timeThreshold < 0) {
90
+ throw new Error('Invalid batching configuration');
91
+ }
90
92
 
91
93
  // Set log level
92
94
  try {
@@ -104,17 +106,8 @@ class OpenObserveTransport extends Transport {
104
106
  this.logQueue = [];
105
107
  this.flushTimer = null;
106
108
 
107
- // Parse URL to determine protocol
108
- try {
109
- const url = new URL(this.host);
110
- this.protocol = url.protocol === 'https:' ? https : http;
111
- this.hostname = url.hostname;
112
- this.port = url.port || (url.protocol === 'https:' ? 443 : 80);
113
- // Normalize pathname: remove trailing slash if present, keep empty if no path
114
- this.pathname = url.pathname.replace(/\/$/, '') || '';
115
- } catch (error) {
116
- throw new Error(`Invalid host URL: ${error.message}`);
117
- }
109
+ const url = new URL(this.host);
110
+ this.pathname = url.pathname.replace(/\/$/, '') || '';
118
111
 
119
112
  // Create Basic Auth header
120
113
  const auth = Buffer.from(`${this.username}:${this.password}`).toString('base64');
@@ -122,210 +115,43 @@ class OpenObserveTransport extends Transport {
122
115
 
123
116
  // Build API endpoint path
124
117
  this.apiPath = `${this.pathname}/api/${encodeURIComponent(this.organization)}/${encodeURIComponent(this.stream)}/_json`;
125
- }
126
-
127
- /**
128
- * Flushes the log queue to OpenObserve
129
- */
130
- flush() {
131
- if (this.logQueue.length === 0) {
132
- return;
133
- }
134
-
135
- const logsToSend = [...this.logQueue];
136
- this.logQueue = [];
137
-
138
- // Clear the timer since we're flushing now
139
- if (this.flushTimer) {
140
- clearTimeout(this.flushTimer);
141
- this.flushTimer = null;
142
- }
143
-
144
- // Clean all logs: strip ANSI codes and remove timestamps
145
- const cleanedLogs = logsToSend.map(log => {
146
- const cleaned = cleanObject(log);
147
- // Ensure we have the essential fields
148
- // Note: filePath (from reportPath feature) is preserved in metadata as a separate JSON field
149
- // It's not in the formatted message string, ensuring clean structured data for OpenObserve
150
- return {
151
- level: cleaned.level,
152
- message: cleaned.message,
153
- ...cleaned // Includes all metadata fields like filePath, location, ID, etc.
154
- };
155
- });
156
-
157
- const payload = JSON.stringify(cleanedLogs);
158
-
159
- const options = {
160
- hostname: this.hostname,
161
- port: this.port,
162
- path: this.apiPath,
163
- method: 'POST',
164
- headers: {
165
- 'Content-Type': 'application/json',
166
- 'Content-Length': Buffer.byteLength(payload),
167
- 'Authorization': this.authHeader,
168
- },
169
- };
170
-
171
- const req = this.protocol.request(options, (res) => {
172
- let responseData = '';
173
- res.on('data', (chunk) => {
174
- responseData += chunk;
175
- });
176
- res.on('end', () => {
177
- if (res.statusCode >= 200 && res.statusCode < 300) {
178
- // Success
179
- } else {
180
- console.error(`OpenObserve API error: ${res.statusCode} - ${responseData}`);
181
- }
182
- });
183
- });
184
-
185
- req.on('error', (e) => {
186
- console.error(`Failed to send logs to OpenObserve: ${e.message}`);
187
- });
188
-
189
- req.write(payload);
190
- req.end();
191
- }
192
-
193
- /**
194
- * Schedules a flush after timeThreshold milliseconds
195
- */
196
- scheduleFlush() {
197
- if (this.flushTimer) {
198
- return; // Already scheduled
199
- }
200
-
201
- this.flushTimer = setTimeout(() => {
202
- this._flush();
203
- }, this.timeThreshold);
118
+ const endpoint = new URL(this.host);
119
+ endpoint.pathname = this.apiPath;
120
+ this.delivery = new HttpDelivery(endpoint, opts);
204
121
  }
205
122
 
206
123
  log(info, callback) {
207
- setImmediate(() => {
208
- this.emit('logged', info);
209
- });
210
-
211
- const { level, message, ...meta } = info;
212
-
213
- // Check if log level is at or below the configured level
214
- if (levels[level] > this.levelPriority) {
215
- callback();
216
- return;
124
+ if (levels[info.level] > this.levelPriority) { callback(); return; }
125
+ if (this.closed) { callback(new Error('OpenObserve transport is closed')); return; }
126
+ this.logQueue.push(info);
127
+ if (this.logQueue.length >= this.batchSize) this._flush();
128
+ else if (!this.flushTimer) {
129
+ this.flushTimer = setTimeout(() => this._flush(), this.timeThreshold);
130
+ this.flushTimer.unref();
217
131
  }
218
-
219
- // Add log to queue
220
- this.logQueue.push({
221
- level,
222
- message,
223
- ...meta
224
- });
225
-
226
- // Flush if batch size is reached
227
- if (this.logQueue.length >= this.batchSize) {
228
- this._flush();
229
- } else {
230
- // Schedule a flush after timeThreshold
231
- this.scheduleFlush();
232
- }
233
-
234
132
  callback();
235
133
  }
236
134
 
237
- /**
238
- * Flushes the log queue (can be called manually)
239
- * @returns {Promise} - Promise that resolves when flush is complete
240
- */
241
- flush() {
242
- return new Promise((resolve) => {
243
- setImmediate(() => {
244
- this._flush();
245
- resolve();
246
- });
247
- });
248
- }
249
-
250
- /**
251
- * Internal flush method (synchronous)
252
- */
253
135
  _flush() {
254
- if (this.logQueue.length === 0) {
255
- return;
256
- }
257
-
258
- const logsToSend = [...this.logQueue];
259
- this.logQueue = [];
260
-
261
- // Clear the timer since we're flushing now
262
- if (this.flushTimer) {
263
- clearTimeout(this.flushTimer);
264
- this.flushTimer = null;
265
- }
266
-
267
- // Clean all logs: strip ANSI codes and remove timestamps
268
- const cleanedLogs = logsToSend.map(log => {
269
- const cleaned = cleanObject(log);
270
- // Ensure we have the essential fields
271
- // Note: filePath (from reportPath feature) is preserved in metadata as a separate JSON field
272
- // It's not in the formatted message string, ensuring clean structured data for OpenObserve
273
- return {
274
- level: cleaned.level,
275
- message: cleaned.message,
276
- ...cleaned // Includes all metadata fields like filePath, location, ID, etc.
277
- };
278
- });
279
-
280
- const payload = JSON.stringify(cleanedLogs);
281
-
282
- const options = {
283
- hostname: this.hostname,
284
- port: this.port,
285
- path: this.apiPath,
286
- method: 'POST',
287
- headers: {
288
- 'Content-Type': 'application/json',
289
- 'Content-Length': Buffer.byteLength(payload),
290
- 'Authorization': this.authHeader,
291
- },
292
- };
293
-
294
- const req = this.protocol.request(options, (res) => {
295
- let responseData = '';
296
- res.on('data', (chunk) => {
297
- responseData += chunk;
298
- });
299
- res.on('end', () => {
300
- if (res.statusCode >= 200 && res.statusCode < 300) {
301
- // Success
302
- } else {
303
- console.error(`OpenObserve API error: ${res.statusCode} - ${responseData}`);
304
- }
305
- });
306
- });
307
-
308
- req.on('error', (e) => {
309
- console.error(`Failed to send logs to OpenObserve: ${e.message}`);
310
- });
136
+ clearTimeout(this.flushTimer);
137
+ this.flushTimer = null;
138
+ if (!this.logQueue.length) return;
139
+ const batch = this.logQueue.splice(0);
140
+ try {
141
+ const payload = JSON.stringify(batch.map(log => cleanObject(log)));
142
+ this.delivery.send(payload, { Authorization: this.authHeader }).catch(() => {});
143
+ } catch (error) { this.delivery.failure = error; }
144
+ }
311
145
 
312
- req.write(payload);
313
- req.end();
146
+ async flush() {
147
+ this._flush();
148
+ await this.delivery.flush();
314
149
  }
315
150
 
316
- /**
317
- * Closes the transport and flushes any remaining logs
318
- */
319
151
  close() {
320
- if (this.flushTimer) {
321
- clearTimeout(this.flushTimer);
322
- this.flushTimer = null;
323
- }
324
- this._flush();
152
+ this.closed = true;
153
+ return this.flush();
325
154
  }
326
155
  }
327
156
 
328
- module.exports = {
329
- transport: OpenObserveTransport,
330
- };
331
-
157
+ module.exports = { transport: OpenObserveTransport };
package/plugins/otel.js CHANGED
@@ -128,7 +128,7 @@ class OpenTelemetryTransport extends Transport {
128
128
  }
129
129
 
130
130
  const Processor = opts.useSimpleProcessor ? SimpleLogRecordProcessor : BatchLogRecordProcessor;
131
- const processor = new Processor(exporter, opts.processorOptions || {});
131
+ const processor = new Processor({ ...(opts.processorOptions || {}), exporter });
132
132
 
133
133
  let resource;
134
134
  const resources = tryRequire('@opentelemetry/resources');
@@ -211,6 +211,7 @@ class OpenTelemetryTransport extends Transport {
211
211
  severityText,
212
212
  body,
213
213
  attributes: this._toAttributes(meta),
214
+ ...(meta.timestamp ? { timestamp: new Date(meta.timestamp) } : {}),
214
215
  };
215
216
 
216
217
  if (this._traceApi) {
@@ -228,7 +229,8 @@ class OpenTelemetryTransport extends Transport {
228
229
  try {
229
230
  this.otelLogger.emit(record);
230
231
  } catch (err) {
231
- console.error(`OpenTelemetry transport emit failed: ${err.message}`);
232
+ callback(err);
233
+ return;
232
234
  }
233
235
 
234
236
  callback();
@@ -249,11 +251,7 @@ class OpenTelemetryTransport extends Transport {
249
251
  */
250
252
  async close() {
251
253
  if (this._ownsProvider && this.loggerProvider && typeof this.loggerProvider.shutdown === 'function') {
252
- try {
253
- await this.loggerProvider.shutdown();
254
- } catch (err) {
255
- console.error(`OpenTelemetry transport shutdown failed: ${err.message}`);
256
- }
254
+ await this.loggerProvider.shutdown();
257
255
  }
258
256
  }
259
257
  }