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/README.md +144 -0
- package/index.d.ts +225 -0
- package/lib/logDeduplicator.js +146 -0
- package/lib/logSampler.js +139 -0
- package/lib/logger.js +317 -17
- package/lib/messageNormalizer.js +39 -0
- package/lib/pathReporter.js +146 -0
- package/lib/performanceTracker.js +112 -0
- package/package.json +29 -25
- package/plugins/README.md +41 -0
- package/plugins/index.js +9 -8
- package/plugins/openobserve.js +331 -0
- package/plugins/smsMail.js +91 -0
package/package.json
CHANGED
|
@@ -1,47 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntlogger",
|
|
3
|
-
"version": "2.
|
|
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
|
-
"
|
|
24
|
-
"
|
|
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
|
-
"
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "jest"
|
|
35
|
+
},
|
|
32
36
|
"dependencies": {
|
|
33
|
-
"@sentry/node": "^
|
|
34
|
-
"mysql2": "^3.
|
|
35
|
-
"pg": "^8.
|
|
36
|
-
"winston": "^3.
|
|
37
|
-
"winston-transport": "^4.
|
|
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": "^
|
|
41
|
-
"dotenv": "^
|
|
42
|
-
"jest": "^
|
|
44
|
+
"@sentry/profiling-node": "^10.25.0",
|
|
45
|
+
"dotenv": "^17.2.3",
|
|
46
|
+
"jest": "^30.2.0"
|
|
43
47
|
},
|
|
44
|
-
"
|
|
45
|
-
"
|
|
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
|
|
9
|
-
MySQL
|
|
10
|
-
Postgres: require('./postgres'),
|
|
11
|
-
Jest
|
|
12
|
-
Syslog
|
|
13
|
-
Discord
|
|
14
|
-
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
|
+
};
|