ntlogger 2.3.1 → 2.5.1
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/index.js +30 -1
- package/lib/colors.js +25 -0
- package/lib/levels.js +14 -0
- package/lib/logger.js +14 -24
- package/lib/signalHandler.js +53 -0
- package/package.json +15 -8
- package/plugins/discord.js +110 -0
- package/plugins/index.js +78 -0
- package/plugins/jest.js +60 -0
- package/plugins/mysql.js +175 -0
- package/plugins/sentry.js +51 -0
- package/plugins/syslog.js +84 -0
package/index.js
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* ntlogger
|
|
3
|
-
* Copyright(c) 2024
|
|
3
|
+
* Copyright(c) 2024 KR
|
|
4
4
|
* GPL-3.0 Licensed
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* TODO: [Feature] Generic Webhook Plugin for sending logs to any webhook
|
|
9
|
+
* TODO: [Feature] Email Plugin for sending logs to an email address
|
|
10
|
+
* TODO: [Feature] SMS Mail ID Plugin for sending logs to a phone number
|
|
11
|
+
*/
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Change Log:
|
|
9
15
|
*
|
|
16
|
+
* v2.5.1 - 08/10/2024:
|
|
17
|
+
* [Bug] Fixed a bug where Jest plugin was not working due to a missing config parameter. This rule was not originally enforced until v2.5.0.
|
|
18
|
+
*
|
|
19
|
+
* v2.5.0 - 08/10/2024:
|
|
20
|
+
* [NOTE] Syslog plugin can send logs using TLS but it is not tested. Avoid sensitive data.
|
|
21
|
+
* [Feature] Added a plugin for Discord webhook integration, allowing log messages to be sent directly to a specified Discord channel.
|
|
22
|
+
* [Feature] Added a plugin for Syslog server integration, enabling log messages to be sent to a Syslog server using UDP, TCP, or TLS protocols.
|
|
23
|
+
* [Feature] Implemented clean signal handling for graceful shutdowns (SIGINT, SIGTERM).
|
|
24
|
+
*
|
|
25
|
+
* [Update] @sentry/node ^8.17.0 --> ^8.25.0
|
|
26
|
+
* [Update] mysql2 ^3.10.2 --> ^3.11.0
|
|
27
|
+
* [Update] winston ^3.13.1 --> ^3.14.1
|
|
28
|
+
* [Update] @sentry/profiling-node ^8.17.0 --> ^8.25.0
|
|
29
|
+
* [Update] jest ^29.0.0 --> ^29.7.0
|
|
30
|
+
*
|
|
31
|
+
* v2.4.0 - 08/01/2024:
|
|
32
|
+
* [NOTE] Damn its been 4 months.. kinda. forgot to log last changes
|
|
33
|
+
* [CRITICAL] Fixed `includes` in package.json which prevented plugins folder from being pushed to npmjs.com
|
|
34
|
+
* [QA] Added test cases using jest
|
|
35
|
+
* [Feature] Added session ID to logger meta, try `console.log(log.defaultMeta.ID)`
|
|
36
|
+
* [Feature] Created the plugin jest. Added a custom transport for in memory logging and testing
|
|
37
|
+
* [Feature] Created the config var `skipCache` to allow you to create a new logger instance within the same file. (Usual behavior is to return the same instance within the same file)
|
|
38
|
+
*
|
|
10
39
|
* v2.2.3 - 04/01/2024:
|
|
11
40
|
* Reduced GitHub Actions to only NPM publish
|
|
12
41
|
* Fixed export
|
package/lib/colors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /lib/colors.js
|
|
3
|
+
* @description Provides color codes for console output and Discord embeds.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
console: {
|
|
8
|
+
internal: '\x1b[93m', // Bright yellow
|
|
9
|
+
trace: '\x1b[90m', // Light gray
|
|
10
|
+
debug: '\x1b[37m', // White
|
|
11
|
+
info: '\x1b[32m', // Green
|
|
12
|
+
warn: '\x1b[33m', // Yellow
|
|
13
|
+
error: '\x1b[31m', // Red
|
|
14
|
+
fatal: '\x1b[35m', // Magenta
|
|
15
|
+
},
|
|
16
|
+
discord: {
|
|
17
|
+
internal: 0x95a5a6, // Gray
|
|
18
|
+
trace: 0x607d8b, // Blue-gray
|
|
19
|
+
debug: 0x3498db, // Blue
|
|
20
|
+
info: 0x2ecc71, // Green
|
|
21
|
+
warn: 0xf39c12, // Orange
|
|
22
|
+
error: 0xe74c3c, // Red
|
|
23
|
+
fatal: 0x8e44ad, // Purple
|
|
24
|
+
},
|
|
25
|
+
};
|
package/lib/levels.js
ADDED
package/lib/logger.js
CHANGED
|
@@ -6,40 +6,28 @@
|
|
|
6
6
|
* any Node.js project.
|
|
7
7
|
*
|
|
8
8
|
* Author: Kevin R. (Kvrnn#6940, Syntax#5569)
|
|
9
|
-
* Date: 03/31/2024
|
|
10
|
-
* Current Version: 2.1.2
|
|
11
9
|
* License: GPL-3.0
|
|
12
10
|
*/
|
|
11
|
+
|
|
13
12
|
const winston = require('winston');
|
|
14
13
|
const crypto = require('crypto');
|
|
15
14
|
|
|
16
|
-
const {
|
|
15
|
+
const { setupSignalHandlers } = require('./signalHandler');
|
|
16
|
+
const { initPlugins } = require('../plugins/index');
|
|
17
|
+
const colors = require('./colors');
|
|
18
|
+
const levels = require('./levels');
|
|
17
19
|
|
|
18
20
|
// Define a map to hold the logger instances by their location
|
|
19
21
|
const loggerInstances = new Map();
|
|
20
22
|
|
|
23
|
+
// Call the signal handler setup function
|
|
24
|
+
setupSignalHandlers(loggerInstances);
|
|
25
|
+
|
|
21
26
|
const customSettings = {
|
|
22
|
-
levels:
|
|
23
|
-
|
|
24
|
-
trace: 5,
|
|
25
|
-
debug: 4,
|
|
26
|
-
info: 3,
|
|
27
|
-
warn: 2,
|
|
28
|
-
error: 1,
|
|
29
|
-
fatal: 0,
|
|
30
|
-
},
|
|
31
|
-
colors: {
|
|
32
|
-
internal: '\x1b[93m', // Bright yellow
|
|
33
|
-
trace: '\x1b[90m', // Light gray
|
|
34
|
-
debug: '\x1b[37m', // White
|
|
35
|
-
info: '\x1b[32m', // Green
|
|
36
|
-
warn: '\x1b[33m', // Yellow
|
|
37
|
-
error: '\x1b[31m', // Red
|
|
38
|
-
fatal: '\x1b[35m', // Magenta
|
|
39
|
-
},
|
|
27
|
+
levels: levels,
|
|
28
|
+
colors: colors.console,
|
|
40
29
|
};
|
|
41
30
|
|
|
42
|
-
|
|
43
31
|
const randomBrightColor = () => {
|
|
44
32
|
// Configuration variables
|
|
45
33
|
const minLuminanceThreshold = 0.03928;
|
|
@@ -124,7 +112,8 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports) =>
|
|
|
124
112
|
path = './logs',
|
|
125
113
|
maxSize = 1048576,
|
|
126
114
|
maxFiles = 5,
|
|
127
|
-
timestamp = true
|
|
115
|
+
timestamp = true,
|
|
116
|
+
skipCache = false,
|
|
128
117
|
} = config;
|
|
129
118
|
|
|
130
119
|
let logTransport = [].filter(Boolean);
|
|
@@ -162,6 +151,7 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports) =>
|
|
|
162
151
|
transports: logTransport,
|
|
163
152
|
defaultMeta: {
|
|
164
153
|
location,
|
|
154
|
+
ID: sessionId,
|
|
165
155
|
timeCreated: new Date().toLocaleTimeString('en-US', {hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3})
|
|
166
156
|
}
|
|
167
157
|
});
|
|
@@ -198,7 +188,7 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports) =>
|
|
|
198
188
|
*/
|
|
199
189
|
const logger = (location = "Unknown", config = {}) => {
|
|
200
190
|
// Check if a logger for the given location already exists
|
|
201
|
-
if (loggerInstances.has(location)) {
|
|
191
|
+
if (loggerInstances.has(location) && !config.skipCache) {
|
|
202
192
|
// If it does, grab the existing logger instance from the map
|
|
203
193
|
const logger = loggerInstances.get(location);
|
|
204
194
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /lib/signalHandler.js
|
|
3
|
+
* @description Initializes signal handlers for the application.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function setupSignalHandlers(loggerInstances) {
|
|
7
|
+
const cleanup = () => {
|
|
8
|
+
console.log('Cleaning up logger resources...');
|
|
9
|
+
for (const [location, logger] of loggerInstances) {
|
|
10
|
+
logger.end(() => {
|
|
11
|
+
console.log(`Logger for ${location} closed.`);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
for (const transport of logger.transports) {
|
|
15
|
+
if (transport.close) {
|
|
16
|
+
transport.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
process.on('SIGINT', () => {
|
|
23
|
+
console.log('Received SIGINT. Exiting...');
|
|
24
|
+
cleanup();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
process.on('SIGTERM', () => {
|
|
29
|
+
console.log('Received SIGTERM. Exiting...');
|
|
30
|
+
cleanup();
|
|
31
|
+
process.exit(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
process.on('SIGQUIT', () => {
|
|
35
|
+
console.log('Received SIGQUIT. Exiting...');
|
|
36
|
+
cleanup();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
process.on('uncaughtException', (err) => {
|
|
41
|
+
console.error('Uncaught Exception:', err);
|
|
42
|
+
cleanup();
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
47
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
48
|
+
cleanup();
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { setupSignalHandlers };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntlogger",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/NightSquawk/NightTimeLogger.git"
|
|
@@ -18,21 +18,28 @@
|
|
|
18
18
|
"log management",
|
|
19
19
|
"logging utility",
|
|
20
20
|
"sentry compatible",
|
|
21
|
-
"mysql compatible"
|
|
21
|
+
"mysql compatible",
|
|
22
|
+
"syslog compatible",
|
|
23
|
+
"discord compatible"
|
|
22
24
|
],
|
|
23
25
|
"files": [
|
|
24
26
|
"index.js",
|
|
25
|
-
"lib/*.js"
|
|
27
|
+
"lib/*.js",
|
|
28
|
+
"plugins/*.js"
|
|
26
29
|
],
|
|
27
30
|
"license": "GPL-3.0",
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"@sentry/node": "^8.
|
|
30
|
-
"mysql2": "^3.
|
|
31
|
-
"winston": "^3.
|
|
32
|
+
"@sentry/node": "^8.25.0",
|
|
33
|
+
"mysql2": "^3.11.0",
|
|
34
|
+
"winston": "^3.14.1",
|
|
32
35
|
"winston-transport": "^4.7.1"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
|
-
"@sentry/profiling-node": "^8.
|
|
36
|
-
"dotenv": "^16.4.5"
|
|
38
|
+
"@sentry/profiling-node": "^8.25.0",
|
|
39
|
+
"dotenv": "^16.4.5",
|
|
40
|
+
"jest": "^29.7.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "jest"
|
|
37
44
|
}
|
|
38
45
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /plugins/discord.js
|
|
3
|
+
* @description Sends logs to a Discord webhook.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Transport = require('winston-transport');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const { URL } = require('url');
|
|
9
|
+
|
|
10
|
+
const colors = require('../lib/colors');
|
|
11
|
+
const levels = require('../lib/levels'); // Assuming this file contains the log levels mapping
|
|
12
|
+
|
|
13
|
+
class DiscordTransport extends Transport {
|
|
14
|
+
constructor(opts = {}) {
|
|
15
|
+
super(opts);
|
|
16
|
+
|
|
17
|
+
this.name = 'Discord Webhook Transport for NTLogger';
|
|
18
|
+
|
|
19
|
+
this.webhookUrl = opts.webhookUrl;
|
|
20
|
+
this.username = opts.username || 'NTLogger';
|
|
21
|
+
this.avatarUrl = opts.avatarUrl || null;
|
|
22
|
+
this.strict = opts.strict || false;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Set the log level
|
|
26
|
+
this.level = opts.level || 'info';
|
|
27
|
+
if (!(this.level in levels)) {
|
|
28
|
+
throw new Error(`Invalid log level: ${this.level}`);
|
|
29
|
+
}
|
|
30
|
+
this.levelPriority = levels[this.level]; // Get the numerical priority of the log level
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(`Error setting log level: ${error.message}`);
|
|
33
|
+
console.error(error.stack);
|
|
34
|
+
throw error; // Rethrow the error to stop the transport creation if level is invalid
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.levelColors = colors.discord;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async log(info, callback) {
|
|
41
|
+
setImmediate(() => {
|
|
42
|
+
this.emit('logged', info);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const { level, message, ...meta } = info;
|
|
46
|
+
|
|
47
|
+
// Check if the log level matches the configured level (strict mode) or is at or below the configured level
|
|
48
|
+
if (this.strict) {
|
|
49
|
+
if (level !== this.level) {
|
|
50
|
+
callback(); // Skip sending the log if it's not exactly the configured level
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
if (levels[level] > this.levelPriority) {
|
|
55
|
+
callback(); // Skip sending the log if it's above the configured level
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const payload = JSON.stringify({
|
|
61
|
+
username: this.username,
|
|
62
|
+
avatar_url: this.avatarUrl,
|
|
63
|
+
embeds: [
|
|
64
|
+
{
|
|
65
|
+
title: `Log Level: ${level.toUpperCase()}`,
|
|
66
|
+
description: message,
|
|
67
|
+
color: this.levelColors[level] || 0x000000, // Default to black if the level is unknown
|
|
68
|
+
fields: Object.keys(meta).map(key => ({
|
|
69
|
+
name: key,
|
|
70
|
+
value: typeof meta[key] === 'string' ? meta[key] : JSON.stringify(meta[key], null, 2),
|
|
71
|
+
inline: false,
|
|
72
|
+
})),
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const webhookUrl = new URL(this.webhookUrl);
|
|
79
|
+
|
|
80
|
+
const options = {
|
|
81
|
+
hostname: webhookUrl.hostname,
|
|
82
|
+
path: webhookUrl.pathname + webhookUrl.search,
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const req = https.request(options, (res) => {
|
|
91
|
+
res.on('data', (chunk) => {
|
|
92
|
+
console.log(`Response from Discord: ${chunk}`);
|
|
93
|
+
});
|
|
94
|
+
res.on('end', () => {
|
|
95
|
+
callback();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
req.on('error', (e) => {
|
|
100
|
+
console.error(`Failed to send log to Discord: ${e.message}`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
req.write(payload);
|
|
104
|
+
req.end();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
transport: DiscordTransport,
|
|
110
|
+
};
|
package/plugins/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /plugins/index.js
|
|
3
|
+
* @description Initializes any plugins required by the application.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Available plugins
|
|
7
|
+
const plugins = {
|
|
8
|
+
Sentry : require('./sentry'),
|
|
9
|
+
MySQL : require('./mysql'),
|
|
10
|
+
Jest : require('./jest'),
|
|
11
|
+
Syslog : require('./syslog'),
|
|
12
|
+
Discord : require('./discord'),
|
|
13
|
+
// WIP SMSMail : require('./smsMail'),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ------------------------------ DO NOT MODIFY BELOW THIS LINE ------------------------------ //
|
|
17
|
+
|
|
18
|
+
function checkPluginAvailability(pluginName) {
|
|
19
|
+
if (!plugins[pluginName]) {
|
|
20
|
+
throw new Error(`Plugin ${pluginName} is not available\nAvailable plugins: ${Object.keys(plugins).join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getPluginTransport(pluginName) {
|
|
25
|
+
checkPluginAvailability(pluginName);
|
|
26
|
+
return plugins[pluginName].transport;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initializes plugins named and configured in the configuration object.
|
|
31
|
+
* @param {Object} config - The configuration object for the plugins
|
|
32
|
+
* @param {string} config.sentry - The configuration object for Sentry
|
|
33
|
+
* @returns {Array} - An array of plugin components
|
|
34
|
+
*/
|
|
35
|
+
function initPlugins(config = {}) {
|
|
36
|
+
let pluginTransports = [];
|
|
37
|
+
try {
|
|
38
|
+
for (let plugin of config) {
|
|
39
|
+
if (!plugin.name) {
|
|
40
|
+
throw new Error('Plugin name is required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
checkPluginAvailability(plugin.name);
|
|
45
|
+
} catch (availabilityError) {
|
|
46
|
+
console.error(`Plugin ${plugin.name} is not available:`, availabilityError.message);
|
|
47
|
+
console.error(availabilityError.stack);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
let customTransportClass = getPluginTransport(plugin.name);
|
|
53
|
+
|
|
54
|
+
if (typeof customTransportClass !== 'function') {
|
|
55
|
+
throw new Error(`Transport class for plugin ${plugin.name} is not a constructor function`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!plugin.config || typeof plugin.config !== 'object') {
|
|
59
|
+
throw new Error(`Invalid or missing config for plugin ${plugin.name}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pluginTransports.push(new customTransportClass(plugin.config));
|
|
63
|
+
} catch (transportError) {
|
|
64
|
+
console.error(`Failed to initialize plugin ${plugin.name}:`, transportError.message);
|
|
65
|
+
console.error(transportError.stack);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('Error initializing plugins:', err.message);
|
|
70
|
+
console.error(err.stack);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return pluginTransports;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
initPlugins,
|
|
78
|
+
};
|
package/plugins/jest.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /plugins/jest.js
|
|
3
|
+
* @description Stores log messages in memory for Jest testing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Transport = require('winston-transport');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom Winston transport for storing logs in memory for Jest assertions.
|
|
10
|
+
*/
|
|
11
|
+
class JestTransport extends Transport {
|
|
12
|
+
constructor(opts = {}) {
|
|
13
|
+
super(opts);
|
|
14
|
+
|
|
15
|
+
this.name = 'JestTransport';
|
|
16
|
+
this.disc = 'Stores log messages in memory for Jest testing.';
|
|
17
|
+
this.logMessages = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Logs the message and stores it in memory.
|
|
22
|
+
* @param {Object} info - The log information object containing the message, level, and other metadata.
|
|
23
|
+
* @param {Function} callback - Callback function to indicate logging completion.
|
|
24
|
+
*/
|
|
25
|
+
log(info, callback) {
|
|
26
|
+
setImmediate(() => {
|
|
27
|
+
this.emit('logged', info);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { level, message, ...meta } = info;
|
|
31
|
+
|
|
32
|
+
// Store the log message in memory
|
|
33
|
+
this.logMessages.push({ level, message, meta });
|
|
34
|
+
|
|
35
|
+
callback();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Retrieve stored log messages filtered by a specific level, if provided.
|
|
40
|
+
* @param {string} [level] - The log level to filter messages by (e.g., 'info', 'error').
|
|
41
|
+
* @returns {Array} Array of filtered or all log messages.
|
|
42
|
+
*/
|
|
43
|
+
getMessages(level) {
|
|
44
|
+
if (level) {
|
|
45
|
+
return this.logMessages.filter(log => log.level === level);
|
|
46
|
+
}
|
|
47
|
+
return this.logMessages;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear all stored log messages.
|
|
52
|
+
*/
|
|
53
|
+
clearMessages() {
|
|
54
|
+
this.logMessages = [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
transport: JestTransport,
|
|
60
|
+
};
|
package/plugins/mysql.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /plugins/mysql.js
|
|
3
|
+
* @description Stores logs in MySQL database.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Transport = require('winston-transport');
|
|
7
|
+
const mysql = require('mysql2/promise');
|
|
8
|
+
|
|
9
|
+
let connectionPool = null;
|
|
10
|
+
let tableInitialized = false;
|
|
11
|
+
|
|
12
|
+
class MySQLTransport extends Transport {
|
|
13
|
+
constructor(opts = {}) {
|
|
14
|
+
super(opts);
|
|
15
|
+
|
|
16
|
+
this.name = 'MySQL Transport for NTLogger';
|
|
17
|
+
this.database = opts.database || 'test';
|
|
18
|
+
this.table = opts.table || 'logs';
|
|
19
|
+
|
|
20
|
+
this.levels = {
|
|
21
|
+
internal: 6,
|
|
22
|
+
trace: 5,
|
|
23
|
+
debug: 4,
|
|
24
|
+
info: 3,
|
|
25
|
+
warn: 2,
|
|
26
|
+
error: 1,
|
|
27
|
+
fatal: 0,
|
|
28
|
+
};
|
|
29
|
+
this.logLevel = opts.level ? this.levels[opts.level] : this.levels.info;
|
|
30
|
+
|
|
31
|
+
this.poolConfig = {
|
|
32
|
+
host: opts.host || 'localhost',
|
|
33
|
+
port: opts.port || 3306,
|
|
34
|
+
user: opts.user || 'root',
|
|
35
|
+
password: opts.password || '',
|
|
36
|
+
database: this.database,
|
|
37
|
+
waitForConnections: opts.waitForConnections || true,
|
|
38
|
+
connectionLimit: opts.connectionLimit || 10,
|
|
39
|
+
queueLimit: opts.queueLimit || 0,
|
|
40
|
+
maxIdle: opts.maxIdle || 10,
|
|
41
|
+
idleTimeout: opts.idleTimeout || 60000,
|
|
42
|
+
enableKeepAlive: opts.enableKeepAlive || true,
|
|
43
|
+
keepAliveInitialDelay: opts.keepAliveInitialDelay || 0,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.init();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initializes the MySQL connection pool and checks the log table.
|
|
51
|
+
*/
|
|
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
|
+
}
|
|
63
|
+
|
|
64
|
+
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
|
+
|
|
83
|
+
try {
|
|
84
|
+
await this.pool.execute(
|
|
85
|
+
`INSERT INTO ${this.table} (level, message, meta, timestamp) VALUES (?, ?, ?, ?)`,
|
|
86
|
+
[level, message, metaString, timestamp]
|
|
87
|
+
);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`Failed to log message to ${this.name}:`, err);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
callback();
|
|
93
|
+
}
|
|
94
|
+
|
|
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
|
+
formatTimestamp(timeCreated) {
|
|
101
|
+
if (!timeCreated) {
|
|
102
|
+
return new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const date = new Date();
|
|
106
|
+
const [time, modifier] = timeCreated.split(' ');
|
|
107
|
+
let [hours, minutes, seconds] = time.split(':');
|
|
108
|
+
|
|
109
|
+
if (modifier === 'PM' && hours !== '12') {
|
|
110
|
+
hours = parseInt(hours, 10) + 12;
|
|
111
|
+
}
|
|
112
|
+
if (modifier === 'AM' && hours === '12') {
|
|
113
|
+
hours = '00';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${hours}:${minutes}:${seconds}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Checks if the log table exists in the MySQL database. If it does not exist, it will create it.
|
|
121
|
+
*/
|
|
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);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Checks if the MySQL connection pool is active.
|
|
154
|
+
* @returns {boolean} - True if the connection pool is active, false otherwise.
|
|
155
|
+
*/
|
|
156
|
+
isActive() {
|
|
157
|
+
return this.pool && !this.pool.ended;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Closes the MySQL connection pool.
|
|
162
|
+
* @returns {Promise<void>}
|
|
163
|
+
*/
|
|
164
|
+
async close() {
|
|
165
|
+
if (connectionPool) {
|
|
166
|
+
await connectionPool.end();
|
|
167
|
+
connectionPool = null;
|
|
168
|
+
console.log('Closed MySQL connection pool.');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
transport: MySQLTransport,
|
|
175
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /plugins/sentry.js
|
|
3
|
+
* @description Initializes the Sentry SDK for error tracking and defines a custom Winston transport.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Transport = require('winston-transport');
|
|
7
|
+
const Sentry = require("@sentry/node");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Custom Winston transport for sending logs to Sentry.
|
|
11
|
+
*/
|
|
12
|
+
class SentryTransport extends Transport {
|
|
13
|
+
constructor(opts = {}) {
|
|
14
|
+
super(opts);
|
|
15
|
+
|
|
16
|
+
this.name = 'Sentry Transport for NTLogger';
|
|
17
|
+
this.init(opts);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initializes the Sentry SDK with the provided configuration.
|
|
22
|
+
* @param {Object} config - The configuration object for Sentry initialization.
|
|
23
|
+
*/
|
|
24
|
+
init(config = {}) {
|
|
25
|
+
try {
|
|
26
|
+
Sentry.init({...config});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(`Failed to initialize ${this.name}:`, err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
log(info, callback) {
|
|
33
|
+
setImmediate(() => {
|
|
34
|
+
this.emit('logged', info);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const { level, message, ...meta } = info;
|
|
38
|
+
|
|
39
|
+
if (level === 'error') {
|
|
40
|
+
Sentry.captureException(new Error(message), { extra: meta });
|
|
41
|
+
} else {
|
|
42
|
+
Sentry.captureMessage(message, { level, extra: meta });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
callback();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
transport: SentryTransport,
|
|
51
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /plugins/syslog.js
|
|
3
|
+
* @description Sends logs to a Syslog server using custom Syslog client.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Transport = require('winston-transport');
|
|
7
|
+
const SyslogClient = require('./lib/syslogClient');
|
|
8
|
+
|
|
9
|
+
class SyslogTransport extends Transport {
|
|
10
|
+
constructor(opts = {}) {
|
|
11
|
+
super(opts);
|
|
12
|
+
|
|
13
|
+
this.name = 'Syslog Transport for NTLogger';
|
|
14
|
+
this.client = new SyslogClient({
|
|
15
|
+
host: opts.host || 'localhost',
|
|
16
|
+
port: opts.port || 514,
|
|
17
|
+
protocol: opts.protocol || 'UDP', // Options: 'UDP', 'TCP', 'TLS'
|
|
18
|
+
rfc: opts.rfc || 'RFC-5424', // Options: 'RFC-3164', 'RFC-5424'
|
|
19
|
+
facility: opts.facility || 1,
|
|
20
|
+
appName: opts.appName || 'NTLogger',
|
|
21
|
+
hostname: opts.hostname || require('os').hostname(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.levels = {
|
|
25
|
+
internal: 7, // Debug level for internal logs
|
|
26
|
+
trace: 7, // Debug
|
|
27
|
+
debug: 7, // Debug
|
|
28
|
+
info: 6, // Informational
|
|
29
|
+
warn: 4, // Warning
|
|
30
|
+
error: 3, // Error
|
|
31
|
+
fatal: 2, // Critical
|
|
32
|
+
};
|
|
33
|
+
this.logLevel = opts.level ? this.levels[opts.level] : this.levels.info;
|
|
34
|
+
|
|
35
|
+
if (this.client.protocol === 'TCP' || this.client.protocol === 'TLS') {
|
|
36
|
+
this.client.connect().catch(err => {
|
|
37
|
+
console.error(`Failed to connect to Syslog server at ${this.client.host}:${this.client.port}:`, err);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Logs messages to the Syslog server.
|
|
44
|
+
* @param {Object} info - Log information.
|
|
45
|
+
* @param {Function} callback - Callback function.
|
|
46
|
+
*/
|
|
47
|
+
log(info, callback) {
|
|
48
|
+
setImmediate(() => {
|
|
49
|
+
this.emit('logged', info);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const { level, message, ...meta } = info;
|
|
53
|
+
|
|
54
|
+
if (this.levels[level] > this.logLevel) {
|
|
55
|
+
callback();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.client.send(this.levels[level], message, meta);
|
|
60
|
+
|
|
61
|
+
callback();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Checks if the Syslog client is active.
|
|
66
|
+
* @returns {boolean} - True if the client is active, false otherwise.
|
|
67
|
+
*/
|
|
68
|
+
isActive() {
|
|
69
|
+
return this.client && (this.client.protocol === 'UDP' || (this.client.transport && !this.client.transport.destroyed));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Closes the Syslog client connection.
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
async close() {
|
|
77
|
+
this.client.close();
|
|
78
|
+
console.log('Closed Syslog client connection.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
transport: SyslogTransport,
|
|
84
|
+
};
|