ntlogger 2.10.0 → 4.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 +465 -5
- package/contract/conformance.json +133 -0
- package/contract/redaction.json +115 -0
- package/eslint-rules/prefer-object-first.js +53 -0
- package/eslint.d.ts +3 -0
- package/eslint.js +5 -0
- package/index.d.ts +202 -183
- package/index.js +18 -1
- package/lib/lifecycle.js +53 -0
- package/lib/logObserver.js +55 -0
- package/lib/logger.js +115 -55
- package/lib/pinoHooks.js +446 -0
- package/lib/pinoOtel.js +43 -0
- package/lib/processHandlers.js +305 -0
- package/lib/secretRedaction.js +684 -0
- package/lib/secretScope.js +24 -0
- package/lib/signalHandler.js +31 -48
- package/package.json +79 -26
- package/pino.d.ts +311 -0
- package/pino.js +535 -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 +193 -12
package/plugins/README.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
NightTimeLogger is a powerful and flexible logging utility with various plugins to extend its functionality. This README provides an overview of the available plugins and instructions on how to set them up.
|
|
4
4
|
|
|
5
|
+
## Required Plugin Entry Fields
|
|
6
|
+
|
|
7
|
+
Every entry in the `plugins` array **must** supply all three of the following. The
|
|
8
|
+
loader silently skips a plugin whose `enabled` is missing/falsy, and reports and skips
|
|
9
|
+
a plugin whose `config` is missing or is not an object:
|
|
10
|
+
|
|
11
|
+
| Field | Required | Notes |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `name` | yes | Must be one of `Sentry`, `MySQL`, `Postgres`, `Jest`, `Syslog`, `Discord`, `Teams`, `OpenObserve`, `OpenTelemetry`. Unknown names are reported and skipped. |
|
|
14
|
+
| `enabled` | yes | Must be `true`. A plugin with `enabled: false` (or omitted) is skipped. |
|
|
15
|
+
| `config` | yes | Must be an object, even if empty (`config: {}`). |
|
|
16
|
+
|
|
17
|
+
## Optional Peer Dependencies
|
|
18
|
+
|
|
19
|
+
The heavier plugin backends are **optional peer dependencies**: installing `ntlogger`
|
|
20
|
+
does not pull them in, so projects that do not use those plugins stay lean. Install the
|
|
21
|
+
peer for each plugin you actually enable:
|
|
22
|
+
|
|
23
|
+
| Plugin | Install |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| Sentry | `npm install @sentry/node` |
|
|
26
|
+
| MySQL | `npm install mysql2` |
|
|
27
|
+
| Postgres | `npm install pg` |
|
|
28
|
+
| OpenTelemetry | `npm install @opentelemetry/api @opentelemetry/api-logs @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions` |
|
|
29
|
+
| Discord, Teams, OpenObserve, Syslog, Jest | none - no extra packages required |
|
|
30
|
+
|
|
31
|
+
If the peer is missing, the plugin loader reports an actionable error naming the
|
|
32
|
+
package and the install command, then skips that plugin. Every other plugin in the
|
|
33
|
+
same configuration still loads and the logger keeps working.
|
|
34
|
+
|
|
5
35
|
## Available Plugins
|
|
6
36
|
|
|
7
37
|
### 1. Discord
|
|
@@ -9,10 +39,13 @@ NightTimeLogger is a powerful and flexible logging utility with various plugins
|
|
|
9
39
|
|
|
10
40
|

|
|
11
41
|
|
|
42
|
+
**Peer dependency:** none.
|
|
43
|
+
|
|
12
44
|
**Setup:**
|
|
13
45
|
```javascript
|
|
14
46
|
{
|
|
15
47
|
name: 'Discord',
|
|
48
|
+
enabled: true,
|
|
16
49
|
config: {
|
|
17
50
|
webhookUrl: process.env.DISCORD_WEBHOOK_URL
|
|
18
51
|
avatarUrl: process.env.DISCORD_AVATAR_URL || 'https://pbs.twimg.com/profile_images/997535493624508416/V7Ed1k2o_400x400.jpg',
|
|
@@ -26,11 +59,15 @@ NightTimeLogger is a powerful and flexible logging utility with various plugins
|
|
|
26
59
|
### 2. Sentry
|
|
27
60
|
Description: Initializes the Sentry SDK for error tracking and defines a custom Winston transport for capturing and sending error logs to Sentry.
|
|
28
61
|
|
|
62
|
+
**Peer dependency:** `npm install @sentry/node` (plus `@sentry/profiling-node` if you use
|
|
63
|
+
`nodeProfilingIntegration()`).
|
|
64
|
+
|
|
29
65
|
Setup:
|
|
30
66
|
|
|
31
67
|
``` javascript
|
|
32
68
|
{
|
|
33
69
|
name: 'Sentry',
|
|
70
|
+
enabled: true,
|
|
34
71
|
config: {
|
|
35
72
|
dsn: process.env.SENTRY_DSN || null,
|
|
36
73
|
release: process.env.SENTRY_RELEASE || null,
|
|
@@ -54,11 +91,14 @@ Setup:
|
|
|
54
91
|
### 3. MySQL
|
|
55
92
|
Description: Stores logs in a MySQL database, allowing you to persist log messages and analyze them using SQL queries.
|
|
56
93
|
|
|
94
|
+
**Peer dependency:** `npm install mysql2`.
|
|
95
|
+
|
|
57
96
|
Setup:
|
|
58
97
|
|
|
59
98
|
``` javascript
|
|
60
99
|
{
|
|
61
100
|
name: 'MySQL',
|
|
101
|
+
enabled: true,
|
|
62
102
|
config: {
|
|
63
103
|
host: process.env.MYSQL_DB_HOST || 'localhost',
|
|
64
104
|
port: process.env.MYSQL_DB_PORT || 3306,
|
|
@@ -73,11 +113,16 @@ Setup:
|
|
|
73
113
|
### 4. Jest
|
|
74
114
|
Description: Stores log messages in memory for testing purposes, particularly useful when running tests with Jest.
|
|
75
115
|
|
|
76
|
-
|
|
116
|
+
**Peer dependency:** none.
|
|
117
|
+
|
|
118
|
+
Setup (`enabled` and an object `config` are both required, even though the Jest
|
|
119
|
+
transport takes no options):
|
|
77
120
|
|
|
78
121
|
``` javascript
|
|
79
122
|
{
|
|
80
123
|
name: 'Jest',
|
|
124
|
+
enabled: true,
|
|
125
|
+
config: {},
|
|
81
126
|
}
|
|
82
127
|
```
|
|
83
128
|
|
|
@@ -87,11 +132,14 @@ Description: Sends logs to a Syslog server using a custom Syslog client. This al
|
|
|
87
132
|

|
|
88
133
|
|
|
89
134
|
|
|
135
|
+
**Peer dependency:** none.
|
|
136
|
+
|
|
90
137
|
Setup:
|
|
91
138
|
|
|
92
139
|
``` javascript
|
|
93
140
|
{
|
|
94
141
|
name: 'Syslog',
|
|
142
|
+
enabled: true,
|
|
95
143
|
config: {
|
|
96
144
|
host: process.env.SYSLOG_HOST || 'localhost',
|
|
97
145
|
port: process.env.SYSLOG_PORT || 514,
|
|
@@ -111,11 +159,14 @@ Setup:
|
|
|
111
159
|

|
|
112
160
|
|
|
113
161
|
|
|
162
|
+
**Peer dependency:** none.
|
|
163
|
+
|
|
114
164
|
**Setup:**
|
|
115
165
|
|
|
116
166
|
``` javascript
|
|
117
167
|
{
|
|
118
168
|
name: 'Teams',
|
|
169
|
+
enabled: true,
|
|
119
170
|
config: {
|
|
120
171
|
webhookUrl: process.env.TEAMS_WEBHOOK_URL || "im not leaking my webhook url again",
|
|
121
172
|
level: process.env.LOG_LEVEL || "info",
|
|
@@ -236,11 +287,14 @@ log.error('Error message',[
|
|
|
236
287
|
### 7. OpenObserve
|
|
237
288
|
**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
289
|
|
|
290
|
+
**Peer dependency:** none.
|
|
291
|
+
|
|
239
292
|
**Setup:**
|
|
240
293
|
|
|
241
294
|
```javascript
|
|
242
295
|
{
|
|
243
296
|
name: 'OpenObserve',
|
|
297
|
+
enabled: true,
|
|
244
298
|
config: {
|
|
245
299
|
host: process.env.OPENOBSERVE_HOST || 'http://localhost:5080',
|
|
246
300
|
organization: process.env.OPENOBSERVE_ORGANIZATION || 'default',
|
|
@@ -261,6 +315,60 @@ log.error('Error message',[
|
|
|
261
315
|
- Clean structured JSON output suitable for OpenObserve ingestion
|
|
262
316
|
- Support for all log levels with configurable filtering
|
|
263
317
|
|
|
318
|
+
### 8. OpenTelemetry
|
|
319
|
+
**Description:** Sends logs to an OpenTelemetry collector as OTLP log records. Maps ntlogger levels to OTel `SeverityNumber`/`SeverityText`, strips ANSI colour codes, flattens metadata into OTel attributes (nested objects are JSON-stringified), and automatically attaches `trace_id`/`span_id` from the active span when `@opentelemetry/api` is installed.
|
|
320
|
+
|
|
321
|
+
**Peer dependency:** the OpenTelemetry packages are optional peers:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
npm install @opentelemetry/api @opentelemetry/api-logs @opentelemetry/sdk-logs \
|
|
325
|
+
@opentelemetry/exporter-logs-otlp-http @opentelemetry/resources \
|
|
326
|
+
@opentelemetry/semantic-conventions
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
If your application already builds its own OTel pipeline, pass an existing
|
|
330
|
+
`loggerProvider` and none of the SDK packages are required by this plugin.
|
|
331
|
+
|
|
332
|
+
**Setup:**
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
{
|
|
336
|
+
name: 'OpenTelemetry',
|
|
337
|
+
enabled: true,
|
|
338
|
+
config: {
|
|
339
|
+
// --- Collector endpoint (used when no `exporter`/`loggerProvider` is given) ---
|
|
340
|
+
url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || 'http://localhost:4318/v1/logs',
|
|
341
|
+
headers: { 'x-api-key': process.env.OTEL_API_KEY },
|
|
342
|
+
concurrencyLimit: 10, // Optional: max in-flight export requests
|
|
343
|
+
timeoutMillis: 10000, // Optional: export request timeout
|
|
344
|
+
|
|
345
|
+
// --- Resource attributes ---
|
|
346
|
+
serviceName: 'my-service', // Optional: sets service.name
|
|
347
|
+
serviceVersion: '1.2.3', // Optional: sets service.version
|
|
348
|
+
resourceAttributes: { 'deployment.environment': process.env.NODE_ENV },
|
|
349
|
+
// resource: myResource, // Optional: a pre-built OTel Resource (wins over the above)
|
|
350
|
+
|
|
351
|
+
// --- Pipeline ---
|
|
352
|
+
// exporter: myExporter, // Optional: bring your own exporter (gRPC/protobuf/etc.)
|
|
353
|
+
// loggerProvider: myProvider, // Optional: reuse an existing LoggerProvider
|
|
354
|
+
useSimpleProcessor: false, // Optional: SimpleLogRecordProcessor instead of Batch
|
|
355
|
+
processorOptions: {}, // Optional: passed to the processor constructor
|
|
356
|
+
|
|
357
|
+
// --- Behaviour ---
|
|
358
|
+
level: 'trace', // Optional: minimum level to export (default: 'trace')
|
|
359
|
+
includeTraceContext: true, // Optional: attach trace_id/span_id (default: true)
|
|
360
|
+
stripAnsi: true, // Optional: strip ANSI codes (default: true)
|
|
361
|
+
instrumentationName: 'ntlogger', // Optional: OTel instrumentation scope name
|
|
362
|
+
instrumentationVersion: '3.0.0', // Optional: OTel instrumentation scope version
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Notes:**
|
|
368
|
+
- `level` uses ntlogger priorities (`fatal` 0 -> `internal` 6). The default `'trace'` exports everything except `internal`; use `'internal'` to export absolutely everything.
|
|
369
|
+
- If neither `loggerProvider` nor `exporter` is supplied and the SDK/exporter packages are not installed, the transport constructor throws a message naming the packages to install - the loader reports it and skips only this plugin.
|
|
370
|
+
- The plugin shuts the provider down on `close()` only when it created the provider itself; a caller-supplied `loggerProvider` is left alone.
|
|
371
|
+
|
|
264
372
|
### Example Logger Configuration
|
|
265
373
|
Here is an example configuration for NightTimeLogger using multiple plugins:
|
|
266
374
|
|
|
@@ -272,6 +380,7 @@ let config = {
|
|
|
272
380
|
plugins: [
|
|
273
381
|
{
|
|
274
382
|
name: 'Discord',
|
|
383
|
+
enabled: true,
|
|
275
384
|
config: {
|
|
276
385
|
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
277
386
|
avatarUrl: process.env.DISCORD_AVATAR_URL,
|
|
@@ -282,6 +391,7 @@ let config = {
|
|
|
282
391
|
},
|
|
283
392
|
{
|
|
284
393
|
name: 'Sentry',
|
|
394
|
+
enabled: true,
|
|
285
395
|
config: {
|
|
286
396
|
dsn: process.env.SENTRY_DSN,
|
|
287
397
|
release: process.env.SENTRY_RELEASE,
|
|
@@ -291,6 +401,7 @@ let config = {
|
|
|
291
401
|
},
|
|
292
402
|
{
|
|
293
403
|
name: 'MySQL',
|
|
404
|
+
enabled: true,
|
|
294
405
|
config: {
|
|
295
406
|
host: process.env.MYSQL_DB_HOST,
|
|
296
407
|
port: process.env.MYSQL_DB_PORT || 3306,
|
|
@@ -302,9 +413,12 @@ let config = {
|
|
|
302
413
|
},
|
|
303
414
|
{
|
|
304
415
|
name: 'Jest',
|
|
416
|
+
enabled: true,
|
|
417
|
+
config: {},
|
|
305
418
|
},
|
|
306
419
|
{
|
|
307
420
|
name: 'Syslog',
|
|
421
|
+
enabled: true,
|
|
308
422
|
config: {
|
|
309
423
|
host: process.env.SYSLOG_HOST || 'localhost',
|
|
310
424
|
port: process.env.SYSLOG_PORT || 514,
|
|
@@ -315,6 +429,7 @@ let config = {
|
|
|
315
429
|
},
|
|
316
430
|
{
|
|
317
431
|
name: 'OpenObserve',
|
|
432
|
+
enabled: true,
|
|
318
433
|
config: {
|
|
319
434
|
host: process.env.OPENOBSERVE_HOST || 'http://localhost:5080',
|
|
320
435
|
organization: process.env.OPENOBSERVE_ORGANIZATION || 'default',
|
|
@@ -326,10 +441,23 @@ let config = {
|
|
|
326
441
|
level: 'info',
|
|
327
442
|
},
|
|
328
443
|
},
|
|
444
|
+
{
|
|
445
|
+
name: 'OpenTelemetry',
|
|
446
|
+
enabled: true,
|
|
447
|
+
config: {
|
|
448
|
+
url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || 'http://localhost:4318/v1/logs',
|
|
449
|
+
serviceName: 'MyAppLogger',
|
|
450
|
+
level: 'info',
|
|
451
|
+
},
|
|
452
|
+
},
|
|
329
453
|
],
|
|
330
454
|
};
|
|
331
455
|
```
|
|
332
456
|
|
|
457
|
+
Every entry above sets `enabled: true` and an object `config` - both are mandatory.
|
|
458
|
+
Plugins whose peer dependency is not installed (`Sentry`, `MySQL`, `Postgres`,
|
|
459
|
+
`OpenTelemetry`) are reported and skipped without affecting the rest of the list.
|
|
460
|
+
|
|
333
461
|
### Conclusion
|
|
334
462
|
The NightTimeLogger is highly customizable and supports various plugins for different logging requirements. By leveraging these plugins, you can easily integrate logging into your existing infrastructure, whether it be storing logs in databases, sending them to monitoring tools like Sentry, or receiving alerts directly in Discord.
|
|
335
463
|
|
package/plugins/discord.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 colors = require('../lib/colors');
|
|
11
10
|
const levels = require('../lib/levels');
|
|
@@ -17,6 +16,7 @@ class DiscordTransport extends Transport {
|
|
|
17
16
|
this.name = 'Discord Webhook Transport for NTLogger';
|
|
18
17
|
|
|
19
18
|
this.webhookUrl = opts.webhookUrl;
|
|
19
|
+
this.delivery = new HttpDelivery(opts.webhookUrl, opts);
|
|
20
20
|
this.username = opts.username || 'NTLogger';
|
|
21
21
|
this.avatarUrl = opts.avatarUrl || null;
|
|
22
22
|
this.strict = opts.strict || false;
|
|
@@ -38,10 +38,6 @@ class DiscordTransport extends Transport {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async log(info, callback) {
|
|
41
|
-
setImmediate(() => {
|
|
42
|
-
this.emit('logged', info);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
41
|
const { level, message, ...meta } = info;
|
|
46
42
|
|
|
47
43
|
// Check if the log level matches the configured level (strict mode) or is at or below the configured level
|
|
@@ -57,52 +53,38 @@ class DiscordTransport extends Transport {
|
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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();
|
|
56
|
+
try {
|
|
57
|
+
const payload = JSON.stringify({
|
|
58
|
+
username: this.username,
|
|
59
|
+
avatar_url: this.avatarUrl,
|
|
60
|
+
embeds: [
|
|
61
|
+
{
|
|
62
|
+
title: `Log Level: ${level.toUpperCase()}`,
|
|
63
|
+
description: message,
|
|
64
|
+
color: this.levelColors[level] || 0x000000, // Default to black if the level is unknown
|
|
65
|
+
fields: Object.keys(meta).map(key => ({
|
|
66
|
+
name: key,
|
|
67
|
+
value: typeof meta[key] === 'string' ? meta[key] : JSON.stringify(meta[key], null, 2),
|
|
68
|
+
inline: false,
|
|
69
|
+
})),
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
}
|
|
72
|
+
]
|
|
96
73
|
});
|
|
97
|
-
});
|
|
98
74
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
75
|
+
if (this.closed) throw new Error('HTTP transport is closed');
|
|
76
|
+
await this.delivery.send(payload);
|
|
77
|
+
this.emit('logged', info);
|
|
78
|
+
callback();
|
|
79
|
+
} catch (error) { callback(error); }
|
|
102
80
|
|
|
103
|
-
req.write(payload);
|
|
104
|
-
req.end();
|
|
105
81
|
}
|
|
82
|
+
flush() { return this.delivery.flush(); }
|
|
83
|
+
close() {
|
|
84
|
+
this.closed = true;
|
|
85
|
+
return this.delivery.flush();
|
|
86
|
+
}
|
|
87
|
+
|
|
106
88
|
}
|
|
107
89
|
|
|
108
90
|
module.exports = {
|
package/plugins/index.js
CHANGED
|
@@ -3,19 +3,28 @@
|
|
|
3
3
|
* @description Initializes any plugins required by the application.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
// Available plugins
|
|
6
|
+
// Available plugins.
|
|
7
|
+
//
|
|
8
|
+
// Each entry is a *thunk*, not a module: the `require()` only runs when the
|
|
9
|
+
// plugin is actually requested by a config entry. Several plugins depend on
|
|
10
|
+
// optional peer dependencies (`@sentry/node`, `mysql2`, `pg`, the
|
|
11
|
+
// OpenTelemetry packages) that consumers are not required to install, so
|
|
12
|
+
// resolving every module at load time would break `require('ntlogger')` for
|
|
13
|
+
// everyone who does not use those backends. Resolution happens inside the
|
|
14
|
+
// per-plugin try/catch in initPlugins(), which means a missing optional
|
|
15
|
+
// dependency disables only that one plugin.
|
|
7
16
|
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'),
|
|
15
|
-
OpenObserve : require('./openobserve'),
|
|
16
|
-
OpenTelemetry : require('./otel'),
|
|
17
|
+
Sentry : () => require('./sentry'),
|
|
18
|
+
MySQL : () => require('./mysql'),
|
|
19
|
+
Postgres : () => require('./postgres'),
|
|
20
|
+
Jest : () => require('./jest'),
|
|
21
|
+
Syslog : () => require('./syslog'),
|
|
22
|
+
Discord : () => require('./discord'),
|
|
23
|
+
Teams : () => require('./teams'),
|
|
24
|
+
OpenObserve : () => require('./openobserve'),
|
|
25
|
+
OpenTelemetry : () => require('./otel'),
|
|
17
26
|
|
|
18
|
-
// WIP SMSMail : require('./smsMail'),
|
|
27
|
+
// WIP SMSMail : () => require('./smsMail'),
|
|
19
28
|
};
|
|
20
29
|
|
|
21
30
|
// ------------------------------ DO NOT MODIFY BELOW THIS LINE ------------------------------ //
|
|
@@ -26,9 +35,15 @@ function checkPluginAvailability(pluginName) {
|
|
|
26
35
|
}
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Lazily resolves a plugin module and returns its transport class.
|
|
40
|
+
* Any error thrown here (including a missing optional peer dependency)
|
|
41
|
+
* is reported and skipped by the caller rather than crashing the logger.
|
|
42
|
+
*/
|
|
29
43
|
function getPluginTransport(pluginName) {
|
|
30
44
|
checkPluginAvailability(pluginName);
|
|
31
|
-
|
|
45
|
+
const pluginModule = plugins[pluginName]();
|
|
46
|
+
return pluginModule && pluginModule.transport;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
/**
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
class HttpDelivery {
|
|
7
|
+
constructor(url, opts = {}) {
|
|
8
|
+
this.url = new URL(url);
|
|
9
|
+
if (!['http:', 'https:'].includes(this.url.protocol)) throw new Error('Expected an HTTP(S) URL');
|
|
10
|
+
this.timeout = opts.timeout ?? 5000;
|
|
11
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
12
|
+
this.retryDelay = opts.retryDelay ?? 1000;
|
|
13
|
+
this.maxPending = opts.maxPending ?? 100;
|
|
14
|
+
for (const [name, value] of Object.entries({ timeout: this.timeout, maxPending: this.maxPending })) {
|
|
15
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`Invalid ${name}`);
|
|
16
|
+
}
|
|
17
|
+
if (!Number.isInteger(this.maxRetries) || this.maxRetries < 0 || !Number.isFinite(this.retryDelay) || this.retryDelay < 0) {
|
|
18
|
+
throw new Error('Invalid retry configuration');
|
|
19
|
+
}
|
|
20
|
+
this.pending = new Set();
|
|
21
|
+
this.failure = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
send(payload, headers = {}) {
|
|
25
|
+
if (this.pending.size >= this.maxPending) {
|
|
26
|
+
const error = new Error('HTTP delivery queue is full');
|
|
27
|
+
this.failure = error;
|
|
28
|
+
return Promise.reject(error);
|
|
29
|
+
}
|
|
30
|
+
const operation = this.deliver(payload, headers);
|
|
31
|
+
this.pending.add(operation);
|
|
32
|
+
operation.then(() => this.pending.delete(operation), error => {
|
|
33
|
+
this.failure = error;
|
|
34
|
+
this.pending.delete(operation);
|
|
35
|
+
});
|
|
36
|
+
return operation;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async deliver(payload, headers) {
|
|
40
|
+
for (let attempt = 0; ; attempt++) {
|
|
41
|
+
try { return await this.request(payload, headers); }
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.retryable === false || attempt >= this.maxRetries) throw error;
|
|
44
|
+
const wait = Math.min(error.retryAfter ?? this.retryDelay * 2 ** attempt, this.timeout);
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, wait));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
request(payload, headers) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const protocol = this.url.protocol === 'https:' ? https : http;
|
|
53
|
+
let timer;
|
|
54
|
+
const req = protocol.request(this.url, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), ...headers },
|
|
57
|
+
}, res => {
|
|
58
|
+
let body = '';
|
|
59
|
+
res.setEncoding('utf8');
|
|
60
|
+
res.on('data', chunk => {
|
|
61
|
+
body += chunk;
|
|
62
|
+
if (body.length > 1048576) req.destroy(new Error('HTTP response exceeds 1 MB'));
|
|
63
|
+
});
|
|
64
|
+
res.on('aborted', () => req.destroy(new Error('HTTP response aborted')));
|
|
65
|
+
res.on('error', error => req.destroy(error));
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
if (res.statusCode >= 200 && res.statusCode < 300 && !body.includes('Microsoft Teams endpoint returned HTTP error 429')) {
|
|
69
|
+
resolve();
|
|
70
|
+
} else {
|
|
71
|
+
const error = new Error(`HTTP log delivery failed (${res.statusCode})`);
|
|
72
|
+
error.retryable = res.statusCode === 429 || res.statusCode >= 500 || body.includes('HTTP error 429');
|
|
73
|
+
const retryAfter = res.headers['retry-after'];
|
|
74
|
+
if (retryAfter) {
|
|
75
|
+
const ms = Number.isFinite(Number(retryAfter)) ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now();
|
|
76
|
+
if (Number.isFinite(ms)) error.retryAfter = Math.max(0, ms);
|
|
77
|
+
}
|
|
78
|
+
reject(error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
timer = setTimeout(() => req.destroy(new Error('HTTP log delivery timed out')), this.timeout);
|
|
83
|
+
req.on('error', error => { clearTimeout(timer); reject(error); });
|
|
84
|
+
req.end(payload);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async flush() {
|
|
89
|
+
await Promise.allSettled([...this.pending]);
|
|
90
|
+
if (this.failure) {
|
|
91
|
+
const error = this.failure;
|
|
92
|
+
this.failure = null;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { HttpDelivery };
|
|
@@ -13,7 +13,7 @@ class SyslogClient {
|
|
|
13
13
|
this.port = opts.port || 514;
|
|
14
14
|
this.protocol = opts.protocol || 'UDP'; // 'UDP', 'TCP', 'TLS'
|
|
15
15
|
this.rfc = opts.rfc || 'RFC-5424'; // 'RFC-3164', 'RFC-5424'
|
|
16
|
-
this.facility = opts.facility
|
|
16
|
+
this.facility = opts.facility ?? 1; // Default to user-level messages
|
|
17
17
|
this.appName = opts.appName || 'NTLogger';
|
|
18
18
|
this.hostname = opts.hostname || require('os').hostname();
|
|
19
19
|
this.transport = null;
|
|
@@ -52,12 +52,13 @@ class SyslogClient {
|
|
|
52
52
|
* @param {string} message - The log message.
|
|
53
53
|
* @param {object} [meta] - Additional metadata for RFC-5424 structured data.
|
|
54
54
|
*/
|
|
55
|
-
send(severity, message, meta = {}) {
|
|
55
|
+
send(severity, message, meta = {}, callback = () => {}) {
|
|
56
56
|
const formattedMessage = this.formatMessage(severity, message, meta);
|
|
57
57
|
if (this.protocol === 'UDP') {
|
|
58
|
-
|
|
58
|
+
const buffer = Buffer.from(formattedMessage);
|
|
59
|
+
this.transport.send(buffer, 0, buffer.length, this.port, this.host, callback);
|
|
59
60
|
} else {
|
|
60
|
-
this.transport.write(formattedMessage + '\n');
|
|
61
|
+
this.transport.write(formattedMessage + '\n', callback);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -101,12 +102,21 @@ class SyslogClient {
|
|
|
101
102
|
* Closes the transport connection.
|
|
102
103
|
*/
|
|
103
104
|
close() {
|
|
104
|
-
if (this.
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
if (!this.closePromise) {
|
|
106
|
+
this.closePromise = new Promise((resolve, reject) => {
|
|
107
|
+
try {
|
|
108
|
+
if (this.protocol === 'UDP') this.transport.close(resolve);
|
|
109
|
+
else if (this.transport.destroyed) resolve();
|
|
110
|
+
else this.transport.end(resolve);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error.code === 'ERR_SOCKET_DGRAM_NOT_RUNNING') resolve();
|
|
113
|
+
else reject(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
108
116
|
}
|
|
117
|
+
return this.closePromise;
|
|
109
118
|
}
|
|
119
|
+
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
module.exports = SyslogClient;
|