ntlogger 2.7.0 → 2.8.2

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/package.json CHANGED
@@ -1,47 +1,51 @@
1
1
  {
2
2
  "name": "ntlogger",
3
- "version": "2.7.0",
4
- "repository": {
5
- "type": "git",
6
- "url": "git+https://github.com/NightSquawk/NightTimeLogger.git"
7
- },
8
- "author": "Kevin R. (Kvrnn#6940, Syntax#5569)",
3
+ "version": "2.8.2",
9
4
  "description": "A Custom Ready-To-Go Logging Wrapper Built on Winston",
10
5
  "keywords": [
6
+ "custom logger",
7
+ "discord compatible",
8
+ "log management",
11
9
  "logger",
12
- "winston wrapper",
13
10
  "logger wrapper",
14
- "wrapper",
15
- "winston logger",
16
- "custom logger",
17
11
  "logging",
18
- "log management",
19
12
  "logging utility",
20
- "sentry compatible",
21
13
  "mysql compatible",
14
+ "sentry compatible",
22
15
  "syslog compatible",
23
- "discord compatible",
24
- "teams compatible"
16
+ "teams compatible",
17
+ "winston logger",
18
+ "winston wrapper",
19
+ "wrapper"
25
20
  ],
21
+ "author": "Kevin R. (Kvrnn#6940, Syntax#5569)",
22
+ "license": "GPL-3.0",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/NightSquawk/NightTimeLogger.git"
26
+ },
26
27
  "files": [
27
28
  "index.js",
29
+ "index.d.ts",
28
30
  "lib/*.js",
29
31
  "plugins/**/*"
30
32
  ],
31
- "license": "GPL-3.0",
33
+ "scripts": {
34
+ "test": "jest"
35
+ },
32
36
  "dependencies": {
33
- "@sentry/node": "^8.26.0",
34
- "mysql2": "^3.11.0",
35
- "pg": "^8.12.0",
36
- "winston": "^3.14.2",
37
- "winston-transport": "^4.7.1"
37
+ "@sentry/node": "^10.25.0",
38
+ "mysql2": "^3.15.3",
39
+ "pg": "^8.16.3",
40
+ "winston": "^3.18.3",
41
+ "winston-transport": "^4.9.0"
38
42
  },
39
43
  "devDependencies": {
40
- "@sentry/profiling-node": "^8.26.0",
41
- "dotenv": "^16.4.5",
42
- "jest": "^29.7.0"
44
+ "@sentry/profiling-node": "^10.25.0",
45
+ "dotenv": "^17.2.3",
46
+ "jest": "^30.2.0"
43
47
  },
44
- "scripts": {
45
- "test": "jest"
48
+ "overrides": {
49
+ "glob": "^11.1.0"
46
50
  }
47
51
  }
package/plugins/README.md CHANGED
@@ -233,6 +233,34 @@ log.error('Error message',[
233
233
  ]);
234
234
  ```
235
235
 
236
+ ### 7. OpenObserve
237
+ **Description:** Sends logs to OpenObserve, an open-source observability platform. This plugin automatically strips ANSI escape codes, removes timestamps, and sends clean structured JSON logs to OpenObserve's HTTP API with batching support for improved performance.
238
+
239
+ **Setup:**
240
+
241
+ ```javascript
242
+ {
243
+ name: 'OpenObserve',
244
+ config: {
245
+ host: process.env.OPENOBSERVE_HOST || 'http://localhost:5080',
246
+ organization: process.env.OPENOBSERVE_ORGANIZATION || 'default',
247
+ stream: process.env.OPENOBSERVE_STREAM || 'logs',
248
+ username: process.env.OPENOBSERVE_USERNAME || 'root@example.com',
249
+ password: process.env.OPENOBSERVE_PASSWORD || 'ComplexPass#123',
250
+ batchSize: 100, // Optional: Number of logs to batch before sending (default: 100)
251
+ timeThreshold: 5000, // Optional: Time in milliseconds before sending batch (default: 5000)
252
+ level: 'info', // Optional: Minimum log level to send (default: 'info')
253
+ },
254
+ }
255
+ ```
256
+
257
+ **Features:**
258
+ - Automatic ANSI code stripping from all log messages and metadata
259
+ - Timestamp removal (OpenObserve adds its own `_timestamp` field)
260
+ - Batching support to reduce HTTP requests and improve performance
261
+ - Clean structured JSON output suitable for OpenObserve ingestion
262
+ - Support for all log levels with configurable filtering
263
+
236
264
  ### Example Logger Configuration
237
265
  Here is an example configuration for NightTimeLogger using multiple plugins:
238
266
 
@@ -285,6 +313,19 @@ let config = {
285
313
  appName: 'MyAppLogger',
286
314
  },
287
315
  },
316
+ {
317
+ name: 'OpenObserve',
318
+ config: {
319
+ host: process.env.OPENOBSERVE_HOST || 'http://localhost:5080',
320
+ organization: process.env.OPENOBSERVE_ORGANIZATION || 'default',
321
+ stream: process.env.OPENOBSERVE_STREAM || 'logs',
322
+ username: process.env.OPENOBSERVE_USERNAME,
323
+ password: process.env.OPENOBSERVE_PASSWORD,
324
+ batchSize: 100,
325
+ timeThreshold: 5000,
326
+ level: 'info',
327
+ },
328
+ },
288
329
  ],
289
330
  };
290
331
  ```
package/plugins/index.js CHANGED
@@ -5,13 +5,14 @@
5
5
 
6
6
  // Available plugins
7
7
  const plugins = {
8
- Sentry : require('./sentry'),
9
- MySQL : require('./mysql'),
10
- Postgres: require('./postgres'),
11
- Jest : require('./jest'),
12
- Syslog : require('./syslog'),
13
- Discord : require('./discord'),
14
- Teams : require('./teams'),
8
+ Sentry : require('./sentry'),
9
+ MySQL : require('./mysql'),
10
+ Postgres : require('./postgres'),
11
+ Jest : require('./jest'),
12
+ Syslog : require('./syslog'),
13
+ Discord : require('./discord'),
14
+ Teams : require('./teams'),
15
+ OpenObserve : require('./openobserve'),
15
16
 
16
17
  // WIP SMSMail : require('./smsMail'),
17
18
  };
@@ -51,7 +52,7 @@ function initPlugins(config = {}) {
51
52
 
52
53
  try {
53
54
  if (!plugin.enabled) {
54
- console.log(`Plugin ${plugin.name} is disabled`);
55
+ if (config.alerts === true) {console.log(`Plugin ${plugin.name} is disabled`);}
55
56
  continue;
56
57
  }
57
58
  } catch (enabledError) {
@@ -0,0 +1,331 @@
1
+ /**
2
+ * @file /plugins/openobserve.js
3
+ * @description Sends logs to OpenObserve using HTTP API with batching support.
4
+ */
5
+
6
+ const Transport = require('winston-transport');
7
+ const https = require('https');
8
+ const http = require('http');
9
+ const { URL } = require('url');
10
+ const levels = require('../lib/levels');
11
+
12
+ /**
13
+ * Strips ANSI escape codes from a string
14
+ * @param {string} str - String to clean
15
+ * @returns {string} - Cleaned string
16
+ */
17
+ function stripAnsiCodes(str) {
18
+ if (typeof str !== 'string') {
19
+ return str;
20
+ }
21
+ // Remove ANSI escape codes: \u001b[ followed by numbers, semicolons, and ending with 'm'
22
+ return str.replace(/\u001b\[[0-9;]*m/g, '');
23
+ }
24
+
25
+ /**
26
+ * Recursively strips ANSI codes from all string values in an object
27
+ * @param {any} obj - Object or value to clean
28
+ * @returns {any} - Cleaned object or value
29
+ */
30
+ function cleanObject(obj) {
31
+ if (obj === null || obj === undefined) {
32
+ return obj;
33
+ }
34
+
35
+ if (typeof obj === 'string') {
36
+ return stripAnsiCodes(obj);
37
+ }
38
+
39
+ if (Array.isArray(obj)) {
40
+ return obj.map(item => cleanObject(item));
41
+ }
42
+
43
+ if (typeof obj === 'object') {
44
+ const cleaned = {};
45
+ for (const [key, value] of Object.entries(obj)) {
46
+ // Skip timestamp fields
47
+ if (key === 'timestamp' || key === 'timeCreated') {
48
+ continue;
49
+ }
50
+ cleaned[key] = cleanObject(value);
51
+ }
52
+ return cleaned;
53
+ }
54
+
55
+ return obj;
56
+ }
57
+
58
+ class OpenObserveTransport extends Transport {
59
+ constructor(opts = {}) {
60
+ super(opts);
61
+
62
+ this.name = 'OpenObserve Transport for NTLogger';
63
+
64
+ // Required configuration
65
+ if (!opts.host) {
66
+ throw new Error('OpenObserve host is required');
67
+ }
68
+ if (!opts.organization) {
69
+ throw new Error('OpenObserve organization is required');
70
+ }
71
+ if (!opts.stream) {
72
+ throw new Error('OpenObserve stream is required');
73
+ }
74
+ if (!opts.username) {
75
+ throw new Error('OpenObserve username is required');
76
+ }
77
+ if (!opts.password) {
78
+ throw new Error('OpenObserve password is required');
79
+ }
80
+
81
+ this.host = opts.host;
82
+ this.organization = opts.organization;
83
+ this.stream = opts.stream;
84
+ this.username = opts.username;
85
+ this.password = opts.password;
86
+
87
+ // Optional configuration
88
+ this.batchSize = opts.batchSize || 100;
89
+ this.timeThreshold = opts.timeThreshold || 5000;
90
+
91
+ // Set log level
92
+ try {
93
+ this.level = opts.level || 'info';
94
+ if (!(this.level in levels)) {
95
+ throw new Error(`Invalid log level: ${this.level}`);
96
+ }
97
+ this.levelPriority = levels[this.level];
98
+ } catch (error) {
99
+ console.error(`Error setting log level: ${error.message}`);
100
+ throw error;
101
+ }
102
+
103
+ // Batching queue
104
+ this.logQueue = [];
105
+ this.flushTimer = null;
106
+
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
+ }
118
+
119
+ // Create Basic Auth header
120
+ const auth = Buffer.from(`${this.username}:${this.password}`).toString('base64');
121
+ this.authHeader = `Basic ${auth}`;
122
+
123
+ // Build API endpoint path
124
+ 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);
204
+ }
205
+
206
+ 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;
217
+ }
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
+ callback();
235
+ }
236
+
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
+ _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
+ });
311
+
312
+ req.write(payload);
313
+ req.end();
314
+ }
315
+
316
+ /**
317
+ * Closes the transport and flushes any remaining logs
318
+ */
319
+ close() {
320
+ if (this.flushTimer) {
321
+ clearTimeout(this.flushTimer);
322
+ this.flushTimer = null;
323
+ }
324
+ this._flush();
325
+ }
326
+ }
327
+
328
+ module.exports = {
329
+ transport: OpenObserveTransport,
330
+ };
331
+
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @file /plugins/smsMail.js
3
+ * @description Sends logs via SMS using an email-to-SMS gateway.
4
+ */
5
+
6
+ const Transport = require('winston-transport');
7
+ const { spawn } = require('child_process');
8
+ const os = require('os');
9
+
10
+ const levels = require('../lib/levels');
11
+
12
+ class SMSTransport extends Transport {
13
+ constructor(opts = {}) {
14
+ super(opts);
15
+
16
+ this.name = 'SMS Transport for NTLogger';
17
+
18
+ // Required options for the SMTP transport
19
+ this.smsMailId = opts.smsMailId; // SMS email gateway address (e.g., 1234567890@carrier.com)
20
+ this.from = opts.from || `noreply@${os.hostname()}`; // Default from address
21
+ this.subjectPrefix = opts.subjectPrefix || 'Log Notification'; // Subject prefix for emails
22
+ this.smtpCommand = opts.smtpCommand || 'msmtp'; // Default to msmtp if no command provided
23
+
24
+ this.strict = opts.strict || false;
25
+
26
+ try {
27
+ this.level = opts.level || 'info';
28
+ if (!(this.level in levels)) {
29
+ throw new Error(`Invalid log level: ${this.level}`);
30
+ }
31
+ this.levelPriority = levels[this.level];
32
+ } catch (error) {
33
+ console.error(`Error setting log level: ${error.message}`);
34
+ console.error(error.stack);
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ async log(info, callback) {
40
+ setImmediate(() => {
41
+ this.emit('logged', info);
42
+ });
43
+
44
+ const { level, message, ...meta } = info;
45
+
46
+ // Check if the log level matches the configured level (strict mode) or is at or below the configured level
47
+ if (this.strict) {
48
+ if (level !== this.level) {
49
+ callback(); // Skip sending the log if it's not exactly the configured level
50
+ return;
51
+ }
52
+ } else {
53
+ if (levels[level] > this.levelPriority) {
54
+ callback(); // Skip sending the log if it's above the configured level
55
+ return;
56
+ }
57
+ }
58
+
59
+ // Construct the email content
60
+ const emailSubject = `${this.subjectPrefix}: ${level.toUpperCase()}`;
61
+ const emailText = `${message}\n\n${JSON.stringify(meta, null, 2)}`;
62
+
63
+ // Use the configured SMTP command to send the email
64
+ const smtpProcess = spawn(this.smtpCommand, ['-t']);
65
+
66
+ // Write the email content
67
+ smtpProcess.stdin.write(`To: ${this.smsMailId}\n`);
68
+ smtpProcess.stdin.write(`From: ${this.from}\n`);
69
+ smtpProcess.stdin.write(`Subject: ${emailSubject}\n`);
70
+ smtpProcess.stdin.write('\n'); // End of headers
71
+ smtpProcess.stdin.write(`${emailText}\n`);
72
+ smtpProcess.stdin.end();
73
+
74
+ smtpProcess.on('error', (err) => {
75
+ console.error(`Failed to send log via SMS: ${err.message}`);
76
+ console.error(`Please provide information for a valid SMTP server to use.`);
77
+ });
78
+
79
+ smtpProcess.on('close', (code) => {
80
+ if (code !== 0) {
81
+ console.error(`SMTP process exited with code ${code}`);
82
+ console.error(`Ensure that the SMTP server information is correct.`);
83
+ }
84
+ callback();
85
+ });
86
+ }
87
+ }
88
+
89
+ module.exports = {
90
+ transport: SMSTransport,
91
+ };