ntlogger 2.9.0 → 2.10.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/package.json +40 -10
- package/plugins/index.js +1 -0
- package/plugins/otel.js +265 -0
- package/transports/pino.js +9 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntlogger",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "A Custom Ready-To-Go Logging Wrapper Built on Winston",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"custom logger",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"logging",
|
|
13
13
|
"logging utility",
|
|
14
14
|
"mysql compatible",
|
|
15
|
+
"opentelemetry",
|
|
16
|
+
"otel",
|
|
17
|
+
"otlp",
|
|
15
18
|
"pino transport",
|
|
16
19
|
"sentry compatible",
|
|
17
20
|
"syslog compatible",
|
|
@@ -42,27 +45,54 @@
|
|
|
42
45
|
"test": "jest"
|
|
43
46
|
},
|
|
44
47
|
"dependencies": {
|
|
45
|
-
"@sentry/node": "^10.
|
|
46
|
-
"mysql2": "^3.
|
|
47
|
-
"pg": "^8.
|
|
48
|
-
"winston": "^3.
|
|
48
|
+
"@sentry/node": "^10.48.0",
|
|
49
|
+
"mysql2": "^3.22.0",
|
|
50
|
+
"pg": "^8.17.1",
|
|
51
|
+
"winston": "^3.19.0",
|
|
49
52
|
"winston-transport": "^4.9.0"
|
|
50
53
|
},
|
|
51
54
|
"optionalDependencies": {
|
|
52
|
-
"
|
|
55
|
+
"split2": "^4.2.0"
|
|
53
56
|
},
|
|
54
57
|
"peerDependencies": {
|
|
55
|
-
"pino": ">=8.0.0"
|
|
58
|
+
"pino": ">=8.0.0",
|
|
59
|
+
"@opentelemetry/api": ">=1.9.0",
|
|
60
|
+
"@opentelemetry/api-logs": ">=0.50.0",
|
|
61
|
+
"@opentelemetry/sdk-logs": ">=0.50.0",
|
|
62
|
+
"@opentelemetry/exporter-logs-otlp-http": ">=0.50.0",
|
|
63
|
+
"@opentelemetry/resources": ">=1.20.0",
|
|
64
|
+
"@opentelemetry/semantic-conventions": ">=1.20.0"
|
|
56
65
|
},
|
|
57
66
|
"peerDependenciesMeta": {
|
|
58
67
|
"pino": {
|
|
59
68
|
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@opentelemetry/api": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@opentelemetry/api-logs": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@opentelemetry/sdk-logs": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@opentelemetry/exporter-logs-otlp-http": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@opentelemetry/resources": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"@opentelemetry/semantic-conventions": {
|
|
86
|
+
"optional": true
|
|
60
87
|
}
|
|
61
88
|
},
|
|
62
89
|
"devDependencies": {
|
|
63
|
-
"@sentry/profiling-node": "^10.
|
|
64
|
-
"
|
|
65
|
-
"
|
|
90
|
+
"@sentry/profiling-node": "^10.52.0",
|
|
91
|
+
"@testcontainers/mysql": "^11.14.0",
|
|
92
|
+
"@testcontainers/postgresql": "^11.14.0",
|
|
93
|
+
"dotenv": "^17.4.2",
|
|
94
|
+
"jest": "^30.4.2",
|
|
95
|
+
"testcontainers": "^11.14.0"
|
|
66
96
|
},
|
|
67
97
|
"overrides": {
|
|
68
98
|
"glob": "^11.1.0"
|
package/plugins/index.js
CHANGED
package/plugins/otel.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
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(exporter, opts.processorOptions || {});
|
|
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
|
+
};
|
|
215
|
+
|
|
216
|
+
if (this._traceApi) {
|
|
217
|
+
const span = this._traceApi.getActiveSpan();
|
|
218
|
+
const ctx = span && span.spanContext && span.spanContext();
|
|
219
|
+
if (ctx && ctx.traceId && ctx.spanId) {
|
|
220
|
+
record.traceId = ctx.traceId;
|
|
221
|
+
record.spanId = ctx.spanId;
|
|
222
|
+
if (typeof ctx.traceFlags === 'number') {
|
|
223
|
+
record.traceFlags = ctx.traceFlags;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
this.otelLogger.emit(record);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`OpenTelemetry transport emit failed: ${err.message}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
callback();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Force-flush pending log records.
|
|
239
|
+
*/
|
|
240
|
+
flush() {
|
|
241
|
+
if (this.loggerProvider && typeof this.loggerProvider.forceFlush === 'function') {
|
|
242
|
+
return this.loggerProvider.forceFlush();
|
|
243
|
+
}
|
|
244
|
+
return Promise.resolve();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Shut down the LoggerProvider (only if this transport created it).
|
|
249
|
+
*/
|
|
250
|
+
async close() {
|
|
251
|
+
if (this._ownsProvider && this.loggerProvider && typeof this.loggerProvider.shutdown === 'function') {
|
|
252
|
+
try {
|
|
253
|
+
await this.loggerProvider.shutdown();
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(`OpenTelemetry transport shutdown failed: ${err.message}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
transport: OpenTelemetryTransport,
|
|
263
|
+
SEVERITY_NUMBER,
|
|
264
|
+
SEVERITY_TEXT,
|
|
265
|
+
};
|
package/transports/pino.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const split = require('split2');
|
|
4
4
|
const colors = require('../lib/colors');
|
|
5
5
|
|
|
6
6
|
const levelColors = colors.console;
|
|
@@ -153,13 +153,16 @@ function formatLine(obj, opts = {}) {
|
|
|
153
153
|
* @param {Object} opts - Transport options
|
|
154
154
|
*/
|
|
155
155
|
module.exports = function (opts = {}) {
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
return split(function (line) {
|
|
157
|
+
let obj;
|
|
158
|
+
try {
|
|
159
|
+
obj = JSON.parse(line);
|
|
160
|
+
} catch {
|
|
159
161
|
process.stdout.write(line + '\n');
|
|
162
|
+
return;
|
|
160
163
|
}
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
const formatted = formatLine(obj, opts);
|
|
165
|
+
process.stdout.write(formatted + '\n');
|
|
163
166
|
});
|
|
164
167
|
};
|
|
165
168
|
|