ntlogger 2.9.1 → 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 +69 -13
- 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 -11
- 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 +263 -0
- 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/otel.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /plugins/otel.js
|
|
3
|
+
* @description Sends logs to an OpenTelemetry collector via the OTLP Logs SDK.
|
|
4
|
+
*
|
|
5
|
+
* OpenTelemetry packages are optional peer dependencies. Install them when using this plugin:
|
|
6
|
+
* npm install @opentelemetry/api @opentelemetry/api-logs @opentelemetry/sdk-logs \
|
|
7
|
+
* @opentelemetry/exporter-logs-otlp-http @opentelemetry/resources \
|
|
8
|
+
* @opentelemetry/semantic-conventions
|
|
9
|
+
*
|
|
10
|
+
* For gRPC or protobuf transport, install the corresponding exporter package and
|
|
11
|
+
* pass it via the `exporter` option.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const Transport = require('winston-transport');
|
|
15
|
+
const levels = require('../lib/levels');
|
|
16
|
+
|
|
17
|
+
const ANSI_REGEX = /\[[0-9;]*m/g;
|
|
18
|
+
|
|
19
|
+
function stripAnsi(str) {
|
|
20
|
+
if (typeof str !== 'string') return str;
|
|
21
|
+
return str.replace(ANSI_REGEX, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Map ntlogger levels to OpenTelemetry SeverityNumber values.
|
|
26
|
+
* See: https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
|
27
|
+
*/
|
|
28
|
+
const SEVERITY_NUMBER = {
|
|
29
|
+
internal: 1, // TRACE
|
|
30
|
+
trace: 1, // TRACE
|
|
31
|
+
debug: 5, // DEBUG
|
|
32
|
+
info: 9, // INFO
|
|
33
|
+
warn: 13, // WARN
|
|
34
|
+
error: 17, // ERROR
|
|
35
|
+
fatal: 21, // FATAL
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const SEVERITY_TEXT = {
|
|
39
|
+
internal: 'TRACE',
|
|
40
|
+
trace: 'TRACE',
|
|
41
|
+
debug: 'DEBUG',
|
|
42
|
+
info: 'INFO',
|
|
43
|
+
warn: 'WARN',
|
|
44
|
+
error: 'ERROR',
|
|
45
|
+
fatal: 'FATAL',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Lazily require an OTel package, returning null if it's not installed.
|
|
50
|
+
* Lets the plugin loader skip OTel gracefully when deps are missing.
|
|
51
|
+
*/
|
|
52
|
+
function tryRequire(name) {
|
|
53
|
+
try {
|
|
54
|
+
return require(name);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err && err.code === 'MODULE_NOT_FOUND') return null;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class OpenTelemetryTransport extends Transport {
|
|
62
|
+
constructor(opts = {}) {
|
|
63
|
+
super(opts);
|
|
64
|
+
|
|
65
|
+
this.name = 'OpenTelemetry Transport for NTLogger';
|
|
66
|
+
|
|
67
|
+
// Level threshold (default: log everything down to 'trace')
|
|
68
|
+
this.level = opts.level || 'trace';
|
|
69
|
+
if (!(this.level in levels)) {
|
|
70
|
+
throw new Error(`Invalid log level: ${this.level}`);
|
|
71
|
+
}
|
|
72
|
+
this.levelPriority = levels[this.level];
|
|
73
|
+
|
|
74
|
+
this.includeTraceContext = opts.includeTraceContext !== false;
|
|
75
|
+
this.stripAnsi = opts.stripAnsi !== false;
|
|
76
|
+
|
|
77
|
+
// Caller-supplied LoggerProvider takes precedence and lets the plugin
|
|
78
|
+
// run without the OTel SDK packages installed (useful for tests).
|
|
79
|
+
if (opts.loggerProvider) {
|
|
80
|
+
this.loggerProvider = opts.loggerProvider;
|
|
81
|
+
this._ownsProvider = false;
|
|
82
|
+
} else {
|
|
83
|
+
const sdkLogs = tryRequire('@opentelemetry/sdk-logs');
|
|
84
|
+
if (!sdkLogs) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
'OpenTelemetry plugin requires @opentelemetry/sdk-logs (and an exporter) ' +
|
|
87
|
+
'when no `loggerProvider` is supplied. ' +
|
|
88
|
+
'Install with: npm install @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/exporter-logs-otlp-http'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
this.loggerProvider = this._buildLoggerProvider(opts, sdkLogs);
|
|
92
|
+
this._ownsProvider = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const instrumentationName = opts.instrumentationName || 'ntlogger';
|
|
96
|
+
const instrumentationVersion = opts.instrumentationVersion || undefined;
|
|
97
|
+
this.otelLogger = this.loggerProvider.getLogger(instrumentationName, instrumentationVersion);
|
|
98
|
+
|
|
99
|
+
// Optional @opentelemetry/api for trace context correlation
|
|
100
|
+
if (this.includeTraceContext) {
|
|
101
|
+
const api = tryRequire('@opentelemetry/api');
|
|
102
|
+
this._traceApi = api && api.trace ? api.trace : null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build a LoggerProvider from configuration when one isn't supplied.
|
|
108
|
+
*/
|
|
109
|
+
_buildLoggerProvider(opts, sdkLogs) {
|
|
110
|
+
const { LoggerProvider, BatchLogRecordProcessor, SimpleLogRecordProcessor } = sdkLogs;
|
|
111
|
+
|
|
112
|
+
let exporter = opts.exporter;
|
|
113
|
+
if (!exporter) {
|
|
114
|
+
const otlpHttp = tryRequire('@opentelemetry/exporter-logs-otlp-http');
|
|
115
|
+
if (!otlpHttp) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
'OpenTelemetry plugin requires either an `exporter` instance or ' +
|
|
118
|
+
'@opentelemetry/exporter-logs-otlp-http installed. ' +
|
|
119
|
+
'Install with: npm install @opentelemetry/exporter-logs-otlp-http'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
exporter = new otlpHttp.OTLPLogExporter({
|
|
123
|
+
url: opts.url,
|
|
124
|
+
headers: opts.headers,
|
|
125
|
+
concurrencyLimit: opts.concurrencyLimit,
|
|
126
|
+
timeoutMillis: opts.timeoutMillis,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const Processor = opts.useSimpleProcessor ? SimpleLogRecordProcessor : BatchLogRecordProcessor;
|
|
131
|
+
const processor = new Processor({ ...(opts.processorOptions || {}), exporter });
|
|
132
|
+
|
|
133
|
+
let resource;
|
|
134
|
+
const resources = tryRequire('@opentelemetry/resources');
|
|
135
|
+
if (resources && (opts.resource || opts.resourceAttributes || opts.serviceName)) {
|
|
136
|
+
if (opts.resource) {
|
|
137
|
+
resource = opts.resource;
|
|
138
|
+
} else {
|
|
139
|
+
const attrs = { ...(opts.resourceAttributes || {}) };
|
|
140
|
+
if (opts.serviceName) {
|
|
141
|
+
const semconv = tryRequire('@opentelemetry/semantic-conventions');
|
|
142
|
+
const key = (semconv && semconv.ATTR_SERVICE_NAME) || 'service.name';
|
|
143
|
+
attrs[key] = opts.serviceName;
|
|
144
|
+
if (opts.serviceVersion) {
|
|
145
|
+
const vkey = (semconv && semconv.ATTR_SERVICE_VERSION) || 'service.version';
|
|
146
|
+
attrs[vkey] = opts.serviceVersion;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
resource = resources.resourceFromAttributes
|
|
150
|
+
? resources.resourceFromAttributes(attrs)
|
|
151
|
+
: new resources.Resource(attrs);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return new LoggerProvider({
|
|
156
|
+
resource,
|
|
157
|
+
processors: [processor],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Flatten metadata into OTel-compatible primitive/array attributes.
|
|
163
|
+
* Objects are JSON-stringified since OTel attributes can't be nested.
|
|
164
|
+
*/
|
|
165
|
+
_toAttributes(meta) {
|
|
166
|
+
const attrs = {};
|
|
167
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
168
|
+
if (value === null || value === undefined) continue;
|
|
169
|
+
if (key === 'timestamp' || key === 'timeCreated') continue;
|
|
170
|
+
|
|
171
|
+
const t = typeof value;
|
|
172
|
+
if (t === 'string') {
|
|
173
|
+
attrs[key] = this.stripAnsi ? stripAnsi(value) : value;
|
|
174
|
+
} else if (t === 'number' || t === 'boolean') {
|
|
175
|
+
attrs[key] = value;
|
|
176
|
+
} else if (Array.isArray(value)) {
|
|
177
|
+
attrs[key] = value.map(v =>
|
|
178
|
+
typeof v === 'object' && v !== null ? JSON.stringify(v) : v
|
|
179
|
+
);
|
|
180
|
+
} else if (t === 'object') {
|
|
181
|
+
try {
|
|
182
|
+
attrs[key] = JSON.stringify(value);
|
|
183
|
+
} catch {
|
|
184
|
+
attrs[key] = String(value);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
attrs[key] = String(value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return attrs;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
log(info, callback) {
|
|
194
|
+
setImmediate(() => {
|
|
195
|
+
this.emit('logged', info);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const { level, message, ...meta } = info;
|
|
199
|
+
|
|
200
|
+
if (levels[level] === undefined || levels[level] > this.levelPriority) {
|
|
201
|
+
callback();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const severityNumber = SEVERITY_NUMBER[level] ?? SEVERITY_NUMBER.info;
|
|
206
|
+
const severityText = SEVERITY_TEXT[level] || String(level).toUpperCase();
|
|
207
|
+
const body = this.stripAnsi && typeof message === 'string' ? stripAnsi(message) : message;
|
|
208
|
+
|
|
209
|
+
const record = {
|
|
210
|
+
severityNumber,
|
|
211
|
+
severityText,
|
|
212
|
+
body,
|
|
213
|
+
attributes: this._toAttributes(meta),
|
|
214
|
+
...(meta.timestamp ? { timestamp: new Date(meta.timestamp) } : {}),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (this._traceApi) {
|
|
218
|
+
const span = this._traceApi.getActiveSpan();
|
|
219
|
+
const ctx = span && span.spanContext && span.spanContext();
|
|
220
|
+
if (ctx && ctx.traceId && ctx.spanId) {
|
|
221
|
+
record.traceId = ctx.traceId;
|
|
222
|
+
record.spanId = ctx.spanId;
|
|
223
|
+
if (typeof ctx.traceFlags === 'number') {
|
|
224
|
+
record.traceFlags = ctx.traceFlags;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
this.otelLogger.emit(record);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
callback(err);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
callback();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Force-flush pending log records.
|
|
241
|
+
*/
|
|
242
|
+
flush() {
|
|
243
|
+
if (this.loggerProvider && typeof this.loggerProvider.forceFlush === 'function') {
|
|
244
|
+
return this.loggerProvider.forceFlush();
|
|
245
|
+
}
|
|
246
|
+
return Promise.resolve();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Shut down the LoggerProvider (only if this transport created it).
|
|
251
|
+
*/
|
|
252
|
+
async close() {
|
|
253
|
+
if (this._ownsProvider && this.loggerProvider && typeof this.loggerProvider.shutdown === 'function') {
|
|
254
|
+
await this.loggerProvider.shutdown();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
transport: OpenTelemetryTransport,
|
|
261
|
+
SEVERITY_NUMBER,
|
|
262
|
+
SEVERITY_TEXT,
|
|
263
|
+
};
|
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
|
|