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/README.md +237 -2
- package/index.d.ts +192 -183
- package/lib/lifecycle.js +53 -0
- package/lib/logger.js +100 -57
- package/lib/pinoHooks.js +446 -0
- package/lib/processHandlers.js +305 -0
- package/lib/secretRedaction.js +675 -0
- package/lib/signalHandler.js +31 -48
- package/package.json +44 -18
- package/pino.d.ts +287 -0
- package/pino.js +434 -13
- package/plugins/README.md +129 -1
- package/plugins/discord.js +30 -48
- package/plugins/index.js +27 -12
- package/plugins/lib/httpDelivery.js +98 -0
- package/plugins/lib/syslogClient.js +18 -8
- package/plugins/mysql.js +73 -83
- package/plugins/openobserve.js +33 -207
- package/plugins/otel.js +5 -7
- package/plugins/postgres.js +69 -67
- package/plugins/sentry.js +36 -3
- package/plugins/syslog.js +6 -9
- package/plugins/teams.js +16 -67
- package/transports/pino.js +188 -12
package/plugins/postgres.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
79
|
+
[level, message, JSON.stringify(meta), timestamp]
|
|
72
80
|
);
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
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
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
54
|
+
if (this.severities[level] > this.logLevel) {
|
|
55
55
|
callback();
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
this.client.send(this.
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 = {
|