logixia 1.12.6 → 1.13.3
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 +19 -4
- package/dist/{async-context-CwBNMm2E.mjs → async-context-BzJrbfQs.mjs} +8 -4
- package/dist/async-context-BzJrbfQs.mjs.map +1 -0
- package/dist/{async-context-CSHWoHMB.js → async-context-DyQQYq3I.js} +8 -4
- package/dist/async-context-DyQQYq3I.js.map +1 -0
- package/dist/{azure-monitor.transport-CG7ClkYj.d.ts → azure-monitor.transport-BDFO4F-i.d.ts} +2 -2
- package/dist/{azure-monitor.transport-CG7ClkYj.d.ts.map → azure-monitor.transport-BDFO4F-i.d.ts.map} +1 -1
- package/dist/{azure-monitor.transport-Cm74kXZS.d.mts → azure-monitor.transport-D5wQEH04.d.mts} +2 -2
- package/dist/{azure-monitor.transport-Cm74kXZS.d.mts.map → azure-monitor.transport-D5wQEH04.d.mts.map} +1 -1
- package/dist/azure.d.mts +2 -2
- package/dist/azure.d.ts +2 -2
- package/dist/cli/index.js +98 -52
- package/dist/cloudwatch.d.mts +2 -2
- package/dist/cloudwatch.d.ts +2 -2
- package/dist/cloudwatch.js +2 -1
- package/dist/cloudwatch.mjs +2 -1
- package/dist/{cloudwatch.transport-CGHY4buE.d.mts → cloudwatch.transport-B00st6Xv.d.ts} +9 -2
- package/dist/cloudwatch.transport-B00st6Xv.d.ts.map +1 -0
- package/dist/{cloudwatch.transport-pNBNWJLt.js → cloudwatch.transport-BO1ahxw-.js} +26 -19
- package/dist/cloudwatch.transport-BO1ahxw-.js.map +1 -0
- package/dist/{cloudwatch.transport-DK_8cKd3.d.ts → cloudwatch.transport-Blc7Gtxt.d.mts} +9 -2
- package/dist/cloudwatch.transport-Blc7Gtxt.d.mts.map +1 -0
- package/dist/{cloudwatch.transport-yBwm4OeO.mjs → cloudwatch.transport-DZ6ies7g.mjs} +26 -19
- package/dist/cloudwatch.transport-DZ6ies7g.mjs.map +1 -0
- package/dist/{coerce.utils-BN980Y9V.js → coerce.utils-B-LilukN.js} +1 -1
- package/dist/{coerce.utils-BN980Y9V.js.map → coerce.utils-B-LilukN.js.map} +1 -1
- package/dist/{coerce.utils-Dxv4_ikC.mjs → coerce.utils-Cu6IUTgX.mjs} +1 -1
- package/dist/{coerce.utils-Dxv4_ikC.mjs.map → coerce.utils-Cu6IUTgX.mjs.map} +1 -1
- package/dist/correlation.d.mts +31 -6
- package/dist/correlation.d.mts.map +1 -1
- package/dist/correlation.d.ts +31 -6
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +45 -20
- package/dist/correlation.js.map +1 -1
- package/dist/correlation.mjs +45 -20
- package/dist/correlation.mjs.map +1 -1
- package/dist/entry-fields-BJkuPnIg.mjs +30 -0
- package/dist/entry-fields-BJkuPnIg.mjs.map +1 -0
- package/dist/entry-fields-BvOLFlJ4.js +36 -0
- package/dist/entry-fields-BvOLFlJ4.js.map +1 -0
- package/dist/gcp.d.mts +2 -2
- package/dist/gcp.d.ts +2 -2
- package/dist/gcp.js +2 -1
- package/dist/gcp.mjs +2 -1
- package/dist/{gcp.transport-SHdtc1n0.js → gcp.transport-CHV8MGgz.js} +5 -5
- package/dist/gcp.transport-CHV8MGgz.js.map +1 -0
- package/dist/{gcp.transport-BJuNRFe_.mjs → gcp.transport-DBX5TPIy.mjs} +5 -5
- package/dist/gcp.transport-DBX5TPIy.mjs.map +1 -0
- package/dist/{gcp.transport-BOhkVKlI.d.ts → gcp.transport-m4fVw0b1.d.ts} +2 -2
- package/dist/gcp.transport-m4fVw0b1.d.ts.map +1 -0
- package/dist/{gcp.transport-Bi6CUqZR.d.mts → gcp.transport-zzNyIoih.d.mts} +2 -2
- package/dist/gcp.transport-zzNyIoih.d.mts.map +1 -0
- package/dist/{index-BPKwwERo.d.mts → index-BPwEisy4.d.mts} +2 -2
- package/dist/{index-BPKwwERo.d.mts.map → index-BPwEisy4.d.mts.map} +1 -1
- package/dist/{index-C1X-TE5I.d.ts → index-nZEeCr3X.d.ts} +2 -2
- package/dist/{index-C1X-TE5I.d.ts.map → index-nZEeCr3X.d.ts.map} +1 -1
- package/dist/index.d.mts +63 -98
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +63 -98
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +167 -443
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +98 -377
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.d.mts +11 -4
- package/dist/middleware.d.mts.map +1 -1
- package/dist/middleware.d.ts +11 -4
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +148 -24
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +148 -24
- package/dist/middleware.mjs.map +1 -1
- package/dist/nest.d.mts +4 -4
- package/dist/nest.d.ts +4 -4
- package/dist/nest.js +20 -11
- package/dist/nest.mjs +7 -5
- package/dist/{logitron-logger.module-BsDZR5BH.js → nestjs-extras-DnJ6vrIC.js} +562 -444
- package/dist/nestjs-extras-DnJ6vrIC.js.map +1 -0
- package/dist/{logitron-logger.module-Bs3XYZgF.mjs → nestjs-extras-ImbuDhs1.mjs} +505 -349
- package/dist/nestjs-extras-ImbuDhs1.mjs.map +1 -0
- package/dist/{logitron-logger.module-zDKVd_o0.d.mts → nestjs-extras-OkC796py.d.mts} +113 -6
- package/dist/nestjs-extras-OkC796py.d.mts.map +1 -0
- package/dist/{logitron-logger.module-CxT1F6Cm.d.ts → nestjs-extras-mKrHE9S6.d.ts} +113 -6
- package/dist/nestjs-extras-mKrHE9S6.d.ts.map +1 -0
- package/dist/{search-njTI2hdo.js → search-B0MH1SJJ.js} +23 -10
- package/dist/search-B0MH1SJJ.js.map +1 -0
- package/dist/{search-DujhR94O.d.ts → search-BfBzDIT9.d.mts} +2 -3
- package/dist/search-BfBzDIT9.d.mts.map +1 -0
- package/dist/{search-CQOo-Iq6.d.mts → search-CHUZPl0h.d.ts} +2 -3
- package/dist/search-CHUZPl0h.d.ts.map +1 -0
- package/dist/{search-CFQRISUv.mjs → search-DjXSn6v5.mjs} +23 -10
- package/dist/search-DjXSn6v5.mjs.map +1 -0
- package/dist/search.d.mts +3 -3
- package/dist/search.d.ts +3 -3
- package/dist/search.js +2 -2
- package/dist/search.mjs +2 -2
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/trace.utils-CvXgaiQT.mjs +278 -0
- package/dist/trace.utils-CvXgaiQT.mjs.map +1 -0
- package/dist/trace.utils-DpBOLDtq.js +358 -0
- package/dist/trace.utils-DpBOLDtq.js.map +1 -0
- package/dist/{transport.manager-D6KL3t9V.mjs → transport.manager-CpdVHEGP.mjs} +365 -86
- package/dist/transport.manager-CpdVHEGP.mjs.map +1 -0
- package/dist/{transport.manager-BivyUTtU.js → transport.manager-Crau3Rle.js} +370 -85
- package/dist/transport.manager-Crau3Rle.js.map +1 -0
- package/dist/{transport.types-B5vcyv2k.d.mts → transport.types-CtNr3D7S.d.mts} +19 -2
- package/dist/transport.types-CtNr3D7S.d.mts.map +1 -0
- package/dist/{transport.types-BuY84TwO.d.ts → transport.types-DqVLZTyf.d.ts} +19 -2
- package/dist/transport.types-DqVLZTyf.d.ts.map +1 -0
- package/dist/transports.d.mts +38 -13
- package/dist/transports.d.mts.map +1 -1
- package/dist/transports.d.ts +38 -13
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +6 -5
- package/dist/transports.mjs +6 -5
- package/dist/worker-entry.js +307 -55
- package/dist/worker.d.mts +2 -2
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +1 -1
- package/dist/worker.mjs +1 -1
- package/dist/{worker.transport-BP8Q28Z2.d.mts → worker.transport-Bgv9heDI.d.mts} +2 -2
- package/dist/{worker.transport-BP8Q28Z2.d.mts.map → worker.transport-Bgv9heDI.d.mts.map} +1 -1
- package/dist/{worker.transport-mKXgNK0L.d.ts → worker.transport-Bj-i0P9V.d.ts} +2 -2
- package/dist/{worker.transport-mKXgNK0L.d.ts.map → worker.transport-Bj-i0P9V.d.ts.map} +1 -1
- package/dist/{worker.transport-CfH6z37Z.js → worker.transport-JP8YHBuU.js} +1 -1
- package/dist/{worker.transport-CfH6z37Z.js.map → worker.transport-JP8YHBuU.js.map} +1 -1
- package/dist/{worker.transport-BhMyvqQ2.mjs → worker.transport-otwhJ_Et.mjs} +1 -1
- package/dist/{worker.transport-BhMyvqQ2.mjs.map → worker.transport-otwhJ_Et.mjs.map} +1 -1
- package/package.json +10 -6
- package/dist/.tsbuildinfo +0 -1
- package/dist/async-context-CSHWoHMB.js.map +0 -1
- package/dist/async-context-CwBNMm2E.mjs.map +0 -1
- package/dist/cloudwatch.transport-CGHY4buE.d.mts.map +0 -1
- package/dist/cloudwatch.transport-DK_8cKd3.d.ts.map +0 -1
- package/dist/cloudwatch.transport-pNBNWJLt.js.map +0 -1
- package/dist/cloudwatch.transport-yBwm4OeO.mjs.map +0 -1
- package/dist/gcp.transport-BJuNRFe_.mjs.map +0 -1
- package/dist/gcp.transport-BOhkVKlI.d.ts.map +0 -1
- package/dist/gcp.transport-Bi6CUqZR.d.mts.map +0 -1
- package/dist/gcp.transport-SHdtc1n0.js.map +0 -1
- package/dist/logitron-logger.module-Bs3XYZgF.mjs.map +0 -1
- package/dist/logitron-logger.module-BsDZR5BH.js.map +0 -1
- package/dist/logitron-logger.module-CxT1F6Cm.d.ts.map +0 -1
- package/dist/logitron-logger.module-zDKVd_o0.d.mts.map +0 -1
- package/dist/search-CFQRISUv.mjs.map +0 -1
- package/dist/search-CQOo-Iq6.d.mts.map +0 -1
- package/dist/search-DujhR94O.d.ts.map +0 -1
- package/dist/search-njTI2hdo.js.map +0 -1
- package/dist/transport.manager-BivyUTtU.js.map +0 -1
- package/dist/transport.manager-D6KL3t9V.mjs.map +0 -1
- package/dist/transport.types-B5vcyv2k.d.mts.map +0 -1
- package/dist/transport.types-BuY84TwO.d.ts.map +0 -1
- package/scripts/postinstall.js +0 -61
package/dist/worker-entry.js
CHANGED
|
@@ -155,6 +155,52 @@ function stringifyForLog(value) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/utils/sanitize.utils.ts
|
|
160
|
+
/**
|
|
161
|
+
* Control-character stripping for text-mode log output.
|
|
162
|
+
*
|
|
163
|
+
* Line breaks become the visible escapes `\\n` / `\\r`; other C0 characters
|
|
164
|
+
* (except tab), DEL and C1 characters are removed, so user-supplied text cannot
|
|
165
|
+
* inject ANSI escapes, overwrite lines, or forge extra log entries.
|
|
166
|
+
*/
|
|
167
|
+
const CONTROL_CHARS_RE = /[\x00-\x08\x0A-\x1F\x7F-\x9F]/g;
|
|
168
|
+
const VISIBLE_ESCAPES = {
|
|
169
|
+
"\n": "\\n",
|
|
170
|
+
"\r": "\\r"
|
|
171
|
+
};
|
|
172
|
+
function stripControlChars(value) {
|
|
173
|
+
return value.replace(CONTROL_CHARS_RE, (ch) => VISIBLE_ESCAPES[ch] ?? "");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/transports/entry-fields.ts
|
|
178
|
+
const PAYLOAD_PREFIX = "data_";
|
|
179
|
+
const UNSAFE_KEYS = new Set(["__proto__"]);
|
|
180
|
+
/**
|
|
181
|
+
* Copies `core` and then the user payload into one object. The core fields stay
|
|
182
|
+
* authoritative: a payload key that matches a core key (even one whose value is
|
|
183
|
+
* undefined) or a reserved name is kept under a `data_` prefix instead. The
|
|
184
|
+
* prefix is repeated until the name is free of core keys and of every other
|
|
185
|
+
* payload key, so the result does not depend on key order and a payload's own
|
|
186
|
+
* `data_level` stays where it is.
|
|
187
|
+
*/
|
|
188
|
+
function mergeCoreFields(core, data, extraReserved = []) {
|
|
189
|
+
const merged = { ...core };
|
|
190
|
+
if (!data) return merged;
|
|
191
|
+
const reserved = new Set([...Object.keys(core), ...extraReserved]);
|
|
192
|
+
const payloadKeys = new Set(Object.keys(data));
|
|
193
|
+
for (const key of payloadKeys) {
|
|
194
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
195
|
+
let target = key;
|
|
196
|
+
if (reserved.has(target)) do
|
|
197
|
+
target = `${PAYLOAD_PREFIX}${target}`;
|
|
198
|
+
while (reserved.has(target) || payloadKeys.has(target));
|
|
199
|
+
merged[target] = data[key];
|
|
200
|
+
}
|
|
201
|
+
return merged;
|
|
202
|
+
}
|
|
203
|
+
|
|
158
204
|
//#endregion
|
|
159
205
|
//#region src/transports/console.transport.ts
|
|
160
206
|
/**
|
|
@@ -186,9 +232,8 @@ var ConsoleTransport = class ConsoleTransport {
|
|
|
186
232
|
["brightwhite", "\x1B[97m"],
|
|
187
233
|
["reset", "\x1B[0m"]
|
|
188
234
|
]);
|
|
189
|
-
static #_2 = this.CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g;
|
|
190
235
|
static sanitize(value) {
|
|
191
|
-
return safeToString(value)
|
|
236
|
+
return stripControlChars(safeToString(value));
|
|
192
237
|
}
|
|
193
238
|
constructor(config = {}) {
|
|
194
239
|
this.config = config;
|
|
@@ -201,16 +246,15 @@ var ConsoleTransport = class ConsoleTransport {
|
|
|
201
246
|
(lvl === "error" || lvl === "warn" ? process.stderr : process.stdout).write(formatted);
|
|
202
247
|
}
|
|
203
248
|
formatEntry(entry) {
|
|
204
|
-
if (this.config.format === "json") return stringifyForLog({
|
|
249
|
+
if (this.config.format === "json") return stringifyForLog(mergeCoreFields({
|
|
205
250
|
timestamp: this.config.timestamp !== false ? entry.timestamp.toISOString() : void 0,
|
|
206
251
|
level: entry.level,
|
|
207
252
|
message: entry.message,
|
|
208
|
-
...entry.data || {},
|
|
209
253
|
context: entry.context,
|
|
210
254
|
traceId: entry.traceId,
|
|
211
255
|
appName: entry.appName,
|
|
212
256
|
environment: entry.environment
|
|
213
|
-
});
|
|
257
|
+
}, entry.data));
|
|
214
258
|
const parts = [];
|
|
215
259
|
if (this.config.timestamp !== false) {
|
|
216
260
|
const timestamp = entry.timestamp.toISOString();
|
|
@@ -300,6 +344,47 @@ function internalError(message, error) {
|
|
|
300
344
|
}
|
|
301
345
|
}
|
|
302
346
|
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/transports/buffer-cap.ts
|
|
349
|
+
const DEFAULT_BUFFER_CAP = 1e4;
|
|
350
|
+
const DROP_WARN_INTERVAL_MS$1 = 5e3;
|
|
351
|
+
/**
|
|
352
|
+
* Keeps an in-memory retry buffer bounded. Past `max` the oldest items are
|
|
353
|
+
* dropped, counted, and reported through internalWarn at most every few seconds.
|
|
354
|
+
*
|
|
355
|
+
* `requested` must be an integer of at least 1, otherwise the default is used.
|
|
356
|
+
* A cap below `batchSize` is raised to `batchSize` so one batch always fits.
|
|
357
|
+
*/
|
|
358
|
+
var BufferCap = class {
|
|
359
|
+
constructor(name, requested, batchSize, fallback = DEFAULT_BUFFER_CAP) {
|
|
360
|
+
this.name = name;
|
|
361
|
+
this.dropped = 0;
|
|
362
|
+
this.lastWarning = 0;
|
|
363
|
+
let max = fallback;
|
|
364
|
+
if (requested !== void 0) if (Number.isInteger(requested) && requested >= 1) max = requested;
|
|
365
|
+
else internalWarn(`${name}: maxBufferSize ${String(requested)} is not a positive integer; using ${fallback}`);
|
|
366
|
+
this.max = Math.max(max, batchSize);
|
|
367
|
+
}
|
|
368
|
+
/** Drops the oldest items beyond the cap, in place. */
|
|
369
|
+
enforce(items) {
|
|
370
|
+
const excess = items.length - this.max;
|
|
371
|
+
if (excess <= 0) return;
|
|
372
|
+
items.splice(0, excess);
|
|
373
|
+
this.note(excess);
|
|
374
|
+
}
|
|
375
|
+
note(count) {
|
|
376
|
+
this.dropped += count;
|
|
377
|
+
this.report(DROP_WARN_INTERVAL_MS$1);
|
|
378
|
+
}
|
|
379
|
+
report(minGapMs) {
|
|
380
|
+
const now = Date.now();
|
|
381
|
+
if (this.dropped === 0 || now - this.lastWarning < minGapMs) return;
|
|
382
|
+
internalWarn(`${this.name}: buffer full (${this.max}), dropped ${this.dropped} oldest entr${this.dropped === 1 ? "y" : "ies"}`);
|
|
383
|
+
this.dropped = 0;
|
|
384
|
+
this.lastWarning = now;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
303
388
|
//#endregion
|
|
304
389
|
//#region src/transports/database.transport.ts
|
|
305
390
|
/**
|
|
@@ -310,6 +395,80 @@ const POSTGRES_MAX_PARAMS = 65535;
|
|
|
310
395
|
const POSTGRES_COLUMNS = 8;
|
|
311
396
|
const POSTGRES_ROWS_PER_INSERT = Math.floor(POSTGRES_MAX_PARAMS / POSTGRES_COLUMNS);
|
|
312
397
|
const MYSQL_ROWS_PER_INSERT = 5e3;
|
|
398
|
+
/** Postgres rejects NUL in TEXT and JSONB. */
|
|
399
|
+
function stripNul(value) {
|
|
400
|
+
return typeof value === "string" ? value.split("\0").join("") : value;
|
|
401
|
+
}
|
|
402
|
+
/** JSONB also rejects the \u0000 escape, which JSON.stringify writes for a NUL. */
|
|
403
|
+
function stripJsonNulEscape(value) {
|
|
404
|
+
if (typeof value !== "string") return value;
|
|
405
|
+
return value.replace(/(?<!\\)((?:\\\\)*)\\u0000/g, "$1");
|
|
406
|
+
}
|
|
407
|
+
const PG_DATA_CLASSES = [
|
|
408
|
+
"22",
|
|
409
|
+
"23",
|
|
410
|
+
"54"
|
|
411
|
+
];
|
|
412
|
+
const MYSQL_DATA_CODES = new Set([
|
|
413
|
+
"ER_DATA_TOO_LONG",
|
|
414
|
+
"ER_TRUNCATED_WRONG_VALUE",
|
|
415
|
+
"ER_TRUNCATED_WRONG_VALUE_FOR_FIELD",
|
|
416
|
+
"ER_BAD_NULL_ERROR",
|
|
417
|
+
"ER_WARN_DATA_OUT_OF_RANGE",
|
|
418
|
+
"ER_DUP_ENTRY",
|
|
419
|
+
"ER_INVALID_JSON_TEXT",
|
|
420
|
+
"ER_INVALID_CHARACTER_STRING"
|
|
421
|
+
]);
|
|
422
|
+
const MYSQL_DATA_ERRNOS = new Set([
|
|
423
|
+
1048,
|
|
424
|
+
1062,
|
|
425
|
+
1264,
|
|
426
|
+
1292,
|
|
427
|
+
1366,
|
|
428
|
+
1406,
|
|
429
|
+
3140
|
|
430
|
+
]);
|
|
431
|
+
const SQLITE_DATA_CODES = new Set([
|
|
432
|
+
"SQLITE_CONSTRAINT",
|
|
433
|
+
"SQLITE_TOOBIG",
|
|
434
|
+
"SQLITE_MISMATCH"
|
|
435
|
+
]);
|
|
436
|
+
const MONGO_DATA_CODES = new Set([
|
|
437
|
+
2,
|
|
438
|
+
121,
|
|
439
|
+
10334,
|
|
440
|
+
11e3
|
|
441
|
+
]);
|
|
442
|
+
function isDataErrorLayer(type, e) {
|
|
443
|
+
const code = e.code;
|
|
444
|
+
switch (type) {
|
|
445
|
+
case "postgresql": return typeof code === "string" && PG_DATA_CLASSES.includes(code.slice(0, 2));
|
|
446
|
+
case "mysql": return typeof code === "string" && MYSQL_DATA_CODES.has(code) || typeof e.errno === "number" && MYSQL_DATA_ERRNOS.has(e.errno);
|
|
447
|
+
case "sqlite": return typeof code === "string" && (SQLITE_DATA_CODES.has(code) || code.startsWith("SQLITE_CONSTRAINT_"));
|
|
448
|
+
case "mongodb": return e.name === "BSONObjectTooLarge" || typeof code === "number" && MONGO_DATA_CODES.has(code);
|
|
449
|
+
default: return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* True when the database refused the data itself (bad value, constraint, size),
|
|
454
|
+
* which retrying cannot fix. Connection, timeout, auth and unknown errors are
|
|
455
|
+
* not data errors and must be retried, never dropped.
|
|
456
|
+
*/
|
|
457
|
+
function isDataError(type, error) {
|
|
458
|
+
let current = error;
|
|
459
|
+
for (let depth = 0; depth < 3 && current && typeof current === "object"; depth += 1) {
|
|
460
|
+
if (isDataErrorLayer(type, current)) return true;
|
|
461
|
+
current = current.cause;
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
/** A write that stored part of a batch: only `remaining` still needs to be written. */
|
|
466
|
+
var PartialWriteError = class extends Error {
|
|
467
|
+
constructor(message, remaining, cause) {
|
|
468
|
+
super(message, { cause });
|
|
469
|
+
this.remaining = remaining;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
313
472
|
function chunk(items, size) {
|
|
314
473
|
const chunks = [];
|
|
315
474
|
for (let i = 0; i < items.length; i += size) chunks.push(items.slice(i, i + size));
|
|
@@ -338,6 +497,7 @@ var DatabaseTransport = class {
|
|
|
338
497
|
this.isConnected = false;
|
|
339
498
|
this.batchSize = config.batchSize || 100;
|
|
340
499
|
this.flushInterval = config.flushInterval || 5e3;
|
|
500
|
+
this.cap = new BufferCap("DatabaseTransport", config.maxBufferSize, this.batchSize);
|
|
341
501
|
if (config.filter) this.filter = config.filter;
|
|
342
502
|
if (config.table) validateSqlIdentifier(config.table, "table");
|
|
343
503
|
this.setupFlushTimer();
|
|
@@ -349,6 +509,7 @@ var DatabaseTransport = class {
|
|
|
349
509
|
}
|
|
350
510
|
addToBatch(entry) {
|
|
351
511
|
this.batch.push(entry);
|
|
512
|
+
this.cap.enforce(this.batch);
|
|
352
513
|
}
|
|
353
514
|
async flush() {
|
|
354
515
|
if (!this.flushPromise) this.flushPromise = this.drain().finally(() => {
|
|
@@ -357,37 +518,97 @@ var DatabaseTransport = class {
|
|
|
357
518
|
await this.flushPromise;
|
|
358
519
|
}
|
|
359
520
|
/**
|
|
360
|
-
* Drains the
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
521
|
+
* Drains the buffer to the database one batch at a time. A batch is detached
|
|
522
|
+
* SYNCHRONOUSLY before awaiting the write, so entries appended by concurrent
|
|
523
|
+
* write() calls land in a fresh array and are never written twice.
|
|
524
|
+
*
|
|
525
|
+
* A failure that is not a data error (connection, timeout, auth, unknown) puts
|
|
526
|
+
* the batch back at the front of the buffer, bounded by `maxBufferSize`, and
|
|
527
|
+
* stops the loop so a down database is not hammered. A data error means the
|
|
528
|
+
* database refused some entry itself, so that batch alone is split to find and
|
|
529
|
+
* drop the refused entries; see isolate().
|
|
366
530
|
*/
|
|
367
531
|
async drain() {
|
|
368
532
|
while (this.batch.length > 0) {
|
|
369
|
-
const
|
|
370
|
-
this.batch = [];
|
|
533
|
+
const entries = this.batch.splice(0, this.batchSize);
|
|
371
534
|
try {
|
|
372
|
-
|
|
373
|
-
case "mongodb":
|
|
374
|
-
await this.flushToMongoDB(entriesToFlush);
|
|
375
|
-
break;
|
|
376
|
-
case "postgresql":
|
|
377
|
-
await this.flushToPostgreSQL(entriesToFlush);
|
|
378
|
-
break;
|
|
379
|
-
case "mysql":
|
|
380
|
-
await this.flushToMySQL(entriesToFlush);
|
|
381
|
-
break;
|
|
382
|
-
case "sqlite":
|
|
383
|
-
await this.flushToSQLite(entriesToFlush);
|
|
384
|
-
break;
|
|
385
|
-
default: throw new Error(`Unsupported database type: ${this.config.type}`);
|
|
386
|
-
}
|
|
535
|
+
await this.writeEntries(entries);
|
|
387
536
|
} catch (error) {
|
|
388
537
|
internalError("Database flush error", error);
|
|
389
|
-
|
|
390
|
-
|
|
538
|
+
if (error instanceof PartialWriteError) {
|
|
539
|
+
this.requeue(error.remaining);
|
|
540
|
+
throw error.cause;
|
|
541
|
+
}
|
|
542
|
+
if (!isDataError(this.config.type, error)) {
|
|
543
|
+
this.requeue(entries);
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
const state = {
|
|
547
|
+
budget: 2 * this.batchSize,
|
|
548
|
+
rejected: 0,
|
|
549
|
+
requeue: [],
|
|
550
|
+
stopped: false
|
|
551
|
+
};
|
|
552
|
+
await this.isolate(entries, state);
|
|
553
|
+
if (state.rejected > 0) internalWarn(`DatabaseTransport: dropped ${state.rejected} log entr${state.rejected === 1 ? "y" : "ies"} the database rejected`);
|
|
554
|
+
this.requeue(state.requeue);
|
|
555
|
+
if (state.stopped) throw state.error;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
requeue(entries) {
|
|
560
|
+
if (entries.length === 0) return;
|
|
561
|
+
this.batch = entries.concat(this.batch);
|
|
562
|
+
this.cap.enforce(this.batch);
|
|
563
|
+
}
|
|
564
|
+
async writeEntries(entries) {
|
|
565
|
+
switch (this.config.type) {
|
|
566
|
+
case "mongodb":
|
|
567
|
+
await this.flushToMongoDB(entries);
|
|
568
|
+
break;
|
|
569
|
+
case "postgresql":
|
|
570
|
+
await this.flushToPostgreSQL(entries);
|
|
571
|
+
break;
|
|
572
|
+
case "mysql":
|
|
573
|
+
await this.flushToMySQL(entries);
|
|
574
|
+
break;
|
|
575
|
+
case "sqlite":
|
|
576
|
+
await this.flushToSQLite(entries);
|
|
577
|
+
break;
|
|
578
|
+
default: throw new Error(`Unsupported database type: ${this.config.type}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* `entries` failed with a data error. Splits them in half until the refused
|
|
583
|
+
* entries are alone and drops those. A write that fails with anything but a
|
|
584
|
+
* data error, or running out of the per-flush write budget, stops the search:
|
|
585
|
+
* the entries not yet written go back to the buffer.
|
|
586
|
+
*/
|
|
587
|
+
async isolate(entries, state) {
|
|
588
|
+
if (entries.length === 1) {
|
|
589
|
+
state.rejected += 1;
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const mid = Math.ceil(entries.length / 2);
|
|
593
|
+
for (const half of [entries.slice(0, mid), entries.slice(mid)]) {
|
|
594
|
+
if (state.stopped || state.budget <= 0) {
|
|
595
|
+
state.requeue.push(...half);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
state.budget -= 1;
|
|
599
|
+
try {
|
|
600
|
+
await this.writeEntries(half);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (error instanceof PartialWriteError) {
|
|
603
|
+
state.requeue.push(...error.remaining);
|
|
604
|
+
state.stopped = true;
|
|
605
|
+
state.error = error.cause;
|
|
606
|
+
} else if (isDataError(this.config.type, error)) await this.isolate(half, state);
|
|
607
|
+
else {
|
|
608
|
+
state.requeue.push(...half);
|
|
609
|
+
state.stopped = true;
|
|
610
|
+
state.error = error;
|
|
611
|
+
}
|
|
391
612
|
}
|
|
392
613
|
}
|
|
393
614
|
}
|
|
@@ -512,7 +733,26 @@ var DatabaseTransport = class {
|
|
|
512
733
|
appName: entry.appName,
|
|
513
734
|
environment: entry.environment
|
|
514
735
|
}));
|
|
515
|
-
|
|
736
|
+
try {
|
|
737
|
+
await collection.insertMany(documents, { ordered: false });
|
|
738
|
+
} catch (error) {
|
|
739
|
+
const raw = error.writeErrors;
|
|
740
|
+
let list = [];
|
|
741
|
+
if (Array.isArray(raw)) list = raw;
|
|
742
|
+
else if (raw) list = [raw];
|
|
743
|
+
const writeErrors = list;
|
|
744
|
+
if (writeErrors.length === 0) throw error;
|
|
745
|
+
const remaining = [];
|
|
746
|
+
let rejected = 0;
|
|
747
|
+
for (const writeError of writeErrors) {
|
|
748
|
+
const entry = typeof writeError.index === "number" ? entries[writeError.index] : void 0;
|
|
749
|
+
if (!entry) throw error;
|
|
750
|
+
if (isDataError("mongodb", writeError)) rejected += 1;
|
|
751
|
+
else remaining.push(entry);
|
|
752
|
+
}
|
|
753
|
+
if (rejected > 0) internalWarn(`DatabaseTransport: dropped ${rejected} log entr${rejected === 1 ? "y" : "ies"} the database rejected`);
|
|
754
|
+
if (remaining.length > 0) throw new PartialWriteError("MongoDB insert partially failed", remaining, error);
|
|
755
|
+
}
|
|
516
756
|
}
|
|
517
757
|
async flushToPostgreSQL(entries) {
|
|
518
758
|
const tableName = this.config.table || "logs";
|
|
@@ -525,7 +765,7 @@ var DatabaseTransport = class {
|
|
|
525
765
|
entry.traceId,
|
|
526
766
|
entry.appName,
|
|
527
767
|
entry.environment
|
|
528
|
-
]), POSTGRES_ROWS_PER_INSERT);
|
|
768
|
+
].map((value, column) => stripNul(column === 3 ? stripJsonNulEscape(value) : value))), POSTGRES_ROWS_PER_INSERT);
|
|
529
769
|
const insertChunk = async (rows) => {
|
|
530
770
|
const query = `
|
|
531
771
|
INSERT INTO ${tableName}
|
|
@@ -685,6 +925,7 @@ var DatabaseTransport = class {
|
|
|
685
925
|
internalError("Database flush during close failed; entries remain buffered", error);
|
|
686
926
|
}
|
|
687
927
|
if (this.batch.length > 0) internalError(`Database transport closing with ${this.batch.length} unflushed log entr${this.batch.length === 1 ? "y" : "ies"} after ${MAX_CLOSE_FLUSH_ATTEMPTS} attempts`);
|
|
928
|
+
this.cap.report(0);
|
|
688
929
|
if (this.connection) switch (this.config.type) {
|
|
689
930
|
case "mongodb":
|
|
690
931
|
await this.connection.close();
|
|
@@ -747,6 +988,12 @@ const unlink = (0, node_util.promisify)(node_fs.unlink);
|
|
|
747
988
|
const readdir = (0, node_util.promisify)(node_fs.readdir);
|
|
748
989
|
const stat = (0, node_util.promisify)(node_fs.stat);
|
|
749
990
|
const DROP_WARN_INTERVAL_MS = 5e3;
|
|
991
|
+
const DEFAULT_MAX_QUEUE = 1e4;
|
|
992
|
+
const DEFAULT_FILE_MODE = 384;
|
|
993
|
+
const DEFAULT_DIR_MODE = 448;
|
|
994
|
+
function csvField(value) {
|
|
995
|
+
return `"${stripControlChars(safeToString(value)).replace(/"/g, "\"\"")}"`;
|
|
996
|
+
}
|
|
750
997
|
const SIZE_UNITS = {
|
|
751
998
|
"": 1,
|
|
752
999
|
k: 1024,
|
|
@@ -782,6 +1029,10 @@ function formatDatePattern(date, pattern) {
|
|
|
782
1029
|
* rotated files are renamed beside it (`app-2026-10-22T09-30-00-000.log`).
|
|
783
1030
|
* With a `rotation` block every file, the live one included, is timestamped.
|
|
784
1031
|
*
|
|
1032
|
+
* Files are created with mode 0o600 and directories with 0o700; override with
|
|
1033
|
+
* `fileMode` and `dirMode`. At most `maxQueue` (default 10000) entries wait in
|
|
1034
|
+
* memory; the rest are dropped per `queuePolicy` and counted in a warning.
|
|
1035
|
+
*
|
|
785
1036
|
* @example
|
|
786
1037
|
* transports: { file: { filename: 'app.log', dirname: './logs', maxSize: '50MB' } }
|
|
787
1038
|
*/
|
|
@@ -816,8 +1067,10 @@ var FileTransport = class {
|
|
|
816
1067
|
this.maxBytes = parseSize(rawMaxSize);
|
|
817
1068
|
if (rawMaxSize !== void 0 && this.maxBytes === void 0) internalWarn(`FileTransport: maxSize ${JSON.stringify(rawMaxSize)} is not a valid size (try '50MB'); size rotation is off`);
|
|
818
1069
|
const maxQueue = config.maxQueue;
|
|
819
|
-
if (maxQueue !== void 0 && !(Number.isInteger(maxQueue) && maxQueue > 0)) internalWarn(`FileTransport: maxQueue ${String(maxQueue)} is not a positive integer;
|
|
820
|
-
|
|
1070
|
+
if (maxQueue !== void 0 && !(Number.isInteger(maxQueue) && maxQueue > 0)) internalWarn(`FileTransport: maxQueue ${String(maxQueue)} is not a positive integer; using ${DEFAULT_MAX_QUEUE}`);
|
|
1071
|
+
this.maxQueue = maxQueue !== void 0 && Number.isInteger(maxQueue) && maxQueue > 0 ? maxQueue : DEFAULT_MAX_QUEUE;
|
|
1072
|
+
this.fileMode = config.fileMode ?? DEFAULT_FILE_MODE;
|
|
1073
|
+
this.dirMode = config.dirMode ?? DEFAULT_DIR_MODE;
|
|
821
1074
|
this.queuePolicy = config.queuePolicy ?? "drop-oldest";
|
|
822
1075
|
this.maxFiles = (rotation === null || rotation === void 0 ? void 0 : rotation.maxFiles) ?? config.maxFiles;
|
|
823
1076
|
this.compress = (rotation === null || rotation === void 0 ? void 0 : rotation.compress) ?? config.zippedArchive ?? false;
|
|
@@ -827,7 +1080,10 @@ var FileTransport = class {
|
|
|
827
1080
|
this.ext = node_path.extname(config.filename);
|
|
828
1081
|
this.base = node_path.basename(config.filename, this.ext);
|
|
829
1082
|
try {
|
|
830
|
-
node_fs.mkdirSync(this.dir, {
|
|
1083
|
+
node_fs.mkdirSync(this.dir, {
|
|
1084
|
+
recursive: true,
|
|
1085
|
+
mode: this.dirMode
|
|
1086
|
+
});
|
|
831
1087
|
} catch (error) {
|
|
832
1088
|
internalError(`FileTransport could not create ${this.dir}`, error);
|
|
833
1089
|
}
|
|
@@ -895,7 +1151,7 @@ var FileTransport = class {
|
|
|
895
1151
|
this.enqueued += 1;
|
|
896
1152
|
}
|
|
897
1153
|
queueFull() {
|
|
898
|
-
return this.
|
|
1154
|
+
return this.batch.length + this.inFlight >= this.maxQueue;
|
|
899
1155
|
}
|
|
900
1156
|
roomAvailable() {
|
|
901
1157
|
const room = new Promise((resolve) => this.spaceWaiters.push(resolve));
|
|
@@ -975,30 +1231,26 @@ var FileTransport = class {
|
|
|
975
1231
|
}
|
|
976
1232
|
formatEntry(entry) {
|
|
977
1233
|
switch (this.config.format) {
|
|
978
|
-
case "json": return stringifyForLog({
|
|
1234
|
+
case "json": return stringifyForLog(mergeCoreFields({
|
|
979
1235
|
timestamp: entry.timestamp.toISOString(),
|
|
980
1236
|
level: entry.level,
|
|
981
|
-
message: entry.message
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
safeToString(entry.traceId),
|
|
992
|
-
stringifyForLog(entry.data || {})
|
|
993
|
-
].join(",");
|
|
994
|
-
}
|
|
1237
|
+
message: entry.message
|
|
1238
|
+
}, entry.data));
|
|
1239
|
+
case "csv": return [
|
|
1240
|
+
entry.timestamp.toISOString(),
|
|
1241
|
+
csvField(entry.level),
|
|
1242
|
+
csvField(entry.message),
|
|
1243
|
+
csvField(entry.context),
|
|
1244
|
+
csvField(entry.traceId),
|
|
1245
|
+
csvField(stringifyForLog(entry.data || {}))
|
|
1246
|
+
].join(",");
|
|
995
1247
|
case "text":
|
|
996
|
-
default: return `${entry.timestamp.toISOString()} [${entry.level.toUpperCase()}] ${safeToString(entry.message)}${entry.data ? " " + stringifyForLog(entry.data) : ""}`;
|
|
1248
|
+
default: return `${entry.timestamp.toISOString()} [${stripControlChars(safeToString(entry.level)).toUpperCase()}] ${stripControlChars(safeToString(entry.message))}${entry.data ? " " + stringifyForLog(entry.data) : ""}`;
|
|
997
1249
|
}
|
|
998
1250
|
}
|
|
999
1251
|
addToBatch(entry) {
|
|
1000
1252
|
this.enqueue(entry);
|
|
1001
|
-
const threshold = Math.min(this.config.batchSize || 100, this.maxQueue
|
|
1253
|
+
const threshold = Math.min(this.config.batchSize || 100, this.maxQueue);
|
|
1002
1254
|
if (this.batch.length >= threshold && !this.draining) {
|
|
1003
1255
|
if (this.batchTimer) {
|
|
1004
1256
|
clearTimeout(this.batchTimer);
|
|
@@ -1036,7 +1288,7 @@ var FileTransport = class {
|
|
|
1036
1288
|
if (chunk$1) await this.append(chunk$1, chunkBytes);
|
|
1037
1289
|
}
|
|
1038
1290
|
async append(content, bytes = Buffer.byteLength(content)) {
|
|
1039
|
-
await appendFile(this.currentFilePath, content);
|
|
1291
|
+
await appendFile(this.currentFilePath, content, { mode: this.fileMode });
|
|
1040
1292
|
this.bytesWritten += bytes;
|
|
1041
1293
|
}
|
|
1042
1294
|
timeToRotate() {
|
|
@@ -1130,7 +1382,7 @@ var FileTransport = class {
|
|
|
1130
1382
|
async compressFile(filePath) {
|
|
1131
1383
|
const gzPath = `${filePath}.gz`;
|
|
1132
1384
|
try {
|
|
1133
|
-
await (0, node_stream_promises.pipeline)(node_fs.createReadStream(filePath), (0, node_zlib.createGzip)(), node_fs.createWriteStream(gzPath));
|
|
1385
|
+
await (0, node_stream_promises.pipeline)(node_fs.createReadStream(filePath), (0, node_zlib.createGzip)(), node_fs.createWriteStream(gzPath, { mode: this.fileMode }));
|
|
1134
1386
|
await unlink(filePath);
|
|
1135
1387
|
} catch (error) {
|
|
1136
1388
|
internalError("Failed to compress rotated log file", error);
|
package/dist/worker.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./transport.types-
|
|
2
|
-
import { n as WorkerTransportConfig, t as WorkerTransport } from "./worker.transport-
|
|
1
|
+
import "./transport.types-CtNr3D7S.mjs";
|
|
2
|
+
import { n as WorkerTransportConfig, t as WorkerTransport } from "./worker.transport-Bgv9heDI.mjs";
|
|
3
3
|
export { WorkerTransport, WorkerTransportConfig };
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./transport.types-
|
|
2
|
-
import { n as WorkerTransportConfig, t as WorkerTransport } from "./worker.transport-
|
|
1
|
+
import "./transport.types-DqVLZTyf.js";
|
|
2
|
+
import { n as WorkerTransportConfig, t as WorkerTransport } from "./worker.transport-Bj-i0P9V.js";
|
|
3
3
|
export { WorkerTransport, WorkerTransportConfig };
|
package/dist/worker.js
CHANGED
package/dist/worker.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as ITransport, p as TransportLogEntry } from "./transport.types-
|
|
1
|
+
import { l as ITransport, p as TransportLogEntry } from "./transport.types-CtNr3D7S.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/transports/worker.transport.d.ts
|
|
4
4
|
|
|
@@ -73,4 +73,4 @@ declare class WorkerTransport implements ITransport {
|
|
|
73
73
|
}
|
|
74
74
|
//#endregion
|
|
75
75
|
export { WorkerTransportConfig as n, WorkerTransport as t };
|
|
76
|
-
//# sourceMappingURL=worker.transport-
|
|
76
|
+
//# sourceMappingURL=worker.transport-Bgv9heDI.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.transport-
|
|
1
|
+
{"version":3,"file":"worker.transport-Bgv9heDI.d.mts","names":[],"sources":["../src/transports/worker.transport.ts"],"sourcesContent":[],"mappings":";;;;UAuCiB,qBAAA;;;;;;;mBAOE;;;;;;;;;;;;;;;;;;;;;;cAuBN,eAAA,YAA2B;;;;;;;;;;;;;;;;;;;;sBAsBlB;;;;;;eAiFP;WAYE;;;;;;;;WAiCA;;cAkCG"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as ITransport, p as TransportLogEntry } from "./transport.types-
|
|
1
|
+
import { l as ITransport, p as TransportLogEntry } from "./transport.types-DqVLZTyf.js";
|
|
2
2
|
|
|
3
3
|
//#region src/transports/worker.transport.d.ts
|
|
4
4
|
|
|
@@ -73,4 +73,4 @@ declare class WorkerTransport implements ITransport {
|
|
|
73
73
|
}
|
|
74
74
|
//#endregion
|
|
75
75
|
export { WorkerTransportConfig as n, WorkerTransport as t };
|
|
76
|
-
//# sourceMappingURL=worker.transport-
|
|
76
|
+
//# sourceMappingURL=worker.transport-Bj-i0P9V.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.transport-
|
|
1
|
+
{"version":3,"file":"worker.transport-Bj-i0P9V.d.ts","names":[],"sources":["../src/transports/worker.transport.ts"],"sourcesContent":[],"mappings":";;;;UAuCiB,qBAAA;;;;;;;mBAOE;;;;;;;;;;;;;;;;;;;;;;cAuBN,eAAA,YAA2B;;;;;;;;;;;;;;;;;;;;sBAsBlB;;;;;;eAiFP;WAYE;;;;;;;;WAiCA;;cAkCG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.transport-CfH6z37Z.js","names":["Worker","path"],"sources":["../src/transports/worker.transport.ts"],"sourcesContent":["/**\n * logixia — Worker Thread Transport\n *\n * Offloads JSON serialization and all I/O to a Node.js worker thread,\n * completely eliminating any event-loop impact from logging. The main thread\n * only posts a raw message to the worker — it never calls JSON.stringify or\n * any I/O syscalls on the hot path.\n *\n * Architecture:\n * Main thread → MessageChannel → Worker thread → Wrapped transport\n *\n * The worker runs an ordinary logixia transport (file, database, console…)\n * inside its own event loop. If the worker crashes it is restarted automatically\n * (up to `maxRestarts` times) and buffered messages are replayed.\n *\n * @example\n * ```ts\n * import { WorkerTransport } from 'logixia/worker';\n *\n * const logger = createLogger({\n * transports: {\n * // Run file transport inside a worker thread — zero event-loop impact\n * worker: new WorkerTransport({\n * transportType: 'file',\n * transportConfig: { filename: 'app.log', dirname: './logs' },\n * bufferSize: 500,\n * maxRestarts: 3,\n * }),\n * },\n * });\n * ```\n */\n\nimport path from 'node:path';\nimport { Worker } from 'node:worker_threads';\n\nimport type { ITransport, TransportLogEntry } from '../types/transport.types';\nimport { internalError, internalWarn } from '../utils/internal-log';\n\nexport interface WorkerTransportConfig {\n /**\n * Which built-in logixia transport to run inside the worker thread.\n * The worker resolves the correct module automatically.\n */\n transportType: 'file' | 'database' | 'console';\n /** Config forwarded verbatim to the wrapped transport constructor. */\n transportConfig: Record<string, unknown>;\n /**\n * Max log entries to buffer in the main thread while the worker is starting\n * or restarting. Entries beyond this limit are dropped with a warning.\n * @default 1000\n */\n bufferSize?: number;\n /**\n * How many times to restart the worker if it crashes unexpectedly.\n * @default 3\n */\n maxRestarts?: number;\n /**\n * Log level filter — only entries at or above this level are forwarded.\n * Useful for routing only errors to an expensive transport in a worker.\n */\n level?: string;\n}\n\n/**\n * Transport that runs a wrapped transport inside a dedicated Node.js worker\n * thread, completely removing JSON serialization and I/O from the main event loop.\n */\nexport class WorkerTransport implements ITransport {\n public readonly name = 'worker';\n public readonly level: string | undefined;\n public readonly batchSize = 1;\n public readonly flushInterval = 0;\n\n private worker: Worker | null = null;\n private ready = false;\n private readonly buffer: TransportLogEntry[] = [];\n private readonly bufferSize: number;\n private restarts = 0;\n private readonly maxRestarts: number;\n private readonly config: WorkerTransportConfig;\n /** Pending restart-backoff timer, so close() can cancel a queued restart. */\n private restartTimer: NodeJS.Timeout | null = null;\n /** Set once close() begins, so a worker 'exit' no longer triggers a restart. */\n private closing = false;\n /** Max time to wait for the worker's 'flushed' ack before giving up. */\n private readonly FLUSH_TIMEOUT_MS = 5000;\n /** Max time to wait for a graceful worker exit before force-terminating. */\n private readonly SHUTDOWN_TIMEOUT_MS = 5000;\n\n constructor(config: WorkerTransportConfig) {\n this.config = config;\n this.bufferSize = config.bufferSize ?? 1000;\n this.maxRestarts = config.maxRestarts ?? 3;\n this.level = config.level;\n this.startWorker();\n }\n\n private startWorker(): void {\n try {\n this.worker = new Worker(this.workerFile(), {\n workerData: {\n transportType: this.config.transportType,\n transportConfig: this.config.transportConfig,\n },\n });\n\n this.worker.on('message', (msg: { type: string; error?: string }) => {\n if (msg.type === 'ready') {\n this.ready = true;\n this.drainBuffer();\n } else if (msg.type === 'error' || msg.type === 'write-error') {\n internalError(\n `WorkerTransport [${this.config.transportType}]: ${msg.error ?? 'unknown'}`\n );\n }\n });\n\n this.worker.on('error', (err) => {\n internalError('WorkerTransport worker error', err);\n this.handleWorkerExit();\n });\n\n this.worker.on('exit', (code) => {\n if (code !== 0) {\n internalWarn(`WorkerTransport worker exited with code ${code}`);\n this.handleWorkerExit();\n }\n });\n } catch (err) {\n internalError('WorkerTransport failed to start', err);\n }\n }\n\n /** The worker entry is always built as CommonJS, next to this module's output. */\n protected workerFile(): string {\n return path.join(__dirname, 'worker-entry.js');\n }\n\n private handleWorkerExit(): void {\n this.ready = false;\n // Don't resurrect a worker we're intentionally shutting down — close() sets\n // `closing` and nulls the worker, so a late 'exit'/'error' event must not\n // queue a restart (which would spawn a fresh leaked thread post-shutdown).\n if (this.closing) return;\n if (this.restarts < this.maxRestarts) {\n this.restarts++;\n internalWarn(`WorkerTransport restarting (attempt ${this.restarts}/${this.maxRestarts})`);\n // Small backoff before restart. unref() so a perpetually-failing worker's\n // pending restart never keeps the process alive on its own.\n this.restartTimer = setTimeout(() => {\n this.restartTimer = null;\n this.startWorker();\n }, 500 * this.restarts);\n if (this.restartTimer.unref) this.restartTimer.unref();\n } else {\n internalError(\n `WorkerTransport exhausted ${this.maxRestarts} restart attempts — transport disabled`\n );\n }\n }\n\n private drainBuffer(): void {\n while (this.buffer.length > 0 && this.ready && this.worker) {\n const entry = this.buffer.shift();\n if (entry) {\n this.worker.postMessage({ type: 'write', entry });\n }\n }\n }\n\n write(entry: TransportLogEntry): void {\n if (this.ready && this.worker) {\n this.worker.postMessage({ type: 'write', entry });\n } else {\n if (this.buffer.length >= this.bufferSize) {\n internalWarn('WorkerTransport buffer full — dropping oldest entry');\n this.buffer.shift();\n }\n this.buffer.push(entry);\n }\n }\n\n async flush(): Promise<void> {\n if (!this.ready || !this.worker) return;\n const worker = this.worker;\n return new Promise<void>((resolve) => {\n // Time-box the flush: if the worker dies or restarts before answering,\n // the 'flushed' message would never arrive and this promise would hang\n // forever, blocking shutdown until the force-exit timer kills the process\n // (losing everything). Resolve on timeout so close() can still terminate.\n const timer = setTimeout(() => {\n worker.off('message', handler);\n resolve();\n }, this.FLUSH_TIMEOUT_MS);\n if (timer.unref) timer.unref();\n\n const handler = (msg: { type: string }) => {\n if (msg.type === 'flushed') {\n clearTimeout(timer);\n worker.off('message', handler);\n resolve();\n }\n };\n worker.on('message', handler);\n worker.postMessage({ type: 'flush' });\n });\n }\n\n /**\n * Flush the worker's buffer and terminate the thread on shutdown.\n *\n * This MUST be named close() — TransportManager.close() invokes\n * transport.close(), so a method named only shutdown() was never called on\n * graceful exit, leaking the worker thread and dropping its buffered logs.\n */\n async close(): Promise<void> {\n // Stop the restart loop first: cancel any queued backoff restart and flag\n // that further worker exits are expected (so handleWorkerExit no-ops).\n this.closing = true;\n if (this.restartTimer) {\n clearTimeout(this.restartTimer);\n this.restartTimer = null;\n }\n\n if (!this.worker) return;\n const worker = this.worker;\n this.worker = null;\n\n // Push any locally-buffered (pre-ready) entries first: the worker's port\n // queues them until its listener attaches, so they are handled before the\n // shutdown message, which flushes the wrapped transport and exits.\n this.drainBufferTo(worker);\n worker.postMessage({ type: 'shutdown' });\n\n await new Promise<void>((resolve) => {\n // Don't wait forever for a wedged worker — terminate() it after a timeout.\n const timer = setTimeout(() => {\n worker.terminate().finally(() => resolve());\n }, this.SHUTDOWN_TIMEOUT_MS);\n if (timer.unref) timer.unref();\n\n worker.once('exit', () => {\n clearTimeout(timer);\n resolve();\n });\n });\n }\n\n /** Back-compat alias — prefer close(). */\n async shutdown(): Promise<void> {\n await this.close();\n }\n\n private drainBufferTo(worker: Worker): void {\n while (this.buffer.length > 0) {\n const entry = this.buffer.shift();\n if (entry) worker.postMessage({ type: 'write', entry });\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"worker.transport-JP8YHBuU.js","names":["Worker","path"],"sources":["../src/transports/worker.transport.ts"],"sourcesContent":["/**\n * logixia — Worker Thread Transport\n *\n * Offloads JSON serialization and all I/O to a Node.js worker thread,\n * completely eliminating any event-loop impact from logging. The main thread\n * only posts a raw message to the worker — it never calls JSON.stringify or\n * any I/O syscalls on the hot path.\n *\n * Architecture:\n * Main thread → MessageChannel → Worker thread → Wrapped transport\n *\n * The worker runs an ordinary logixia transport (file, database, console…)\n * inside its own event loop. If the worker crashes it is restarted automatically\n * (up to `maxRestarts` times) and buffered messages are replayed.\n *\n * @example\n * ```ts\n * import { WorkerTransport } from 'logixia/worker';\n *\n * const logger = createLogger({\n * transports: {\n * // Run file transport inside a worker thread — zero event-loop impact\n * worker: new WorkerTransport({\n * transportType: 'file',\n * transportConfig: { filename: 'app.log', dirname: './logs' },\n * bufferSize: 500,\n * maxRestarts: 3,\n * }),\n * },\n * });\n * ```\n */\n\nimport path from 'node:path';\nimport { Worker } from 'node:worker_threads';\n\nimport type { ITransport, TransportLogEntry } from '../types/transport.types';\nimport { internalError, internalWarn } from '../utils/internal-log';\n\nexport interface WorkerTransportConfig {\n /**\n * Which built-in logixia transport to run inside the worker thread.\n * The worker resolves the correct module automatically.\n */\n transportType: 'file' | 'database' | 'console';\n /** Config forwarded verbatim to the wrapped transport constructor. */\n transportConfig: Record<string, unknown>;\n /**\n * Max log entries to buffer in the main thread while the worker is starting\n * or restarting. Entries beyond this limit are dropped with a warning.\n * @default 1000\n */\n bufferSize?: number;\n /**\n * How many times to restart the worker if it crashes unexpectedly.\n * @default 3\n */\n maxRestarts?: number;\n /**\n * Log level filter — only entries at or above this level are forwarded.\n * Useful for routing only errors to an expensive transport in a worker.\n */\n level?: string;\n}\n\n/**\n * Transport that runs a wrapped transport inside a dedicated Node.js worker\n * thread, completely removing JSON serialization and I/O from the main event loop.\n */\nexport class WorkerTransport implements ITransport {\n public readonly name = 'worker';\n public readonly level: string | undefined;\n public readonly batchSize = 1;\n public readonly flushInterval = 0;\n\n private worker: Worker | null = null;\n private ready = false;\n private readonly buffer: TransportLogEntry[] = [];\n private readonly bufferSize: number;\n private restarts = 0;\n private readonly maxRestarts: number;\n private readonly config: WorkerTransportConfig;\n /** Pending restart-backoff timer, so close() can cancel a queued restart. */\n private restartTimer: NodeJS.Timeout | null = null;\n /** Set once close() begins, so a worker 'exit' no longer triggers a restart. */\n private closing = false;\n /** Max time to wait for the worker's 'flushed' ack before giving up. */\n private readonly FLUSH_TIMEOUT_MS = 5000;\n /** Max time to wait for a graceful worker exit before force-terminating. */\n private readonly SHUTDOWN_TIMEOUT_MS = 5000;\n\n constructor(config: WorkerTransportConfig) {\n this.config = config;\n this.bufferSize = config.bufferSize ?? 1000;\n this.maxRestarts = config.maxRestarts ?? 3;\n this.level = config.level;\n this.startWorker();\n }\n\n private startWorker(): void {\n try {\n this.worker = new Worker(this.workerFile(), {\n workerData: {\n transportType: this.config.transportType,\n transportConfig: this.config.transportConfig,\n },\n });\n\n this.worker.on('message', (msg: { type: string; error?: string }) => {\n if (msg.type === 'ready') {\n this.ready = true;\n this.drainBuffer();\n } else if (msg.type === 'error' || msg.type === 'write-error') {\n internalError(\n `WorkerTransport [${this.config.transportType}]: ${msg.error ?? 'unknown'}`\n );\n }\n });\n\n this.worker.on('error', (err) => {\n internalError('WorkerTransport worker error', err);\n this.handleWorkerExit();\n });\n\n this.worker.on('exit', (code) => {\n if (code !== 0) {\n internalWarn(`WorkerTransport worker exited with code ${code}`);\n this.handleWorkerExit();\n }\n });\n } catch (err) {\n internalError('WorkerTransport failed to start', err);\n }\n }\n\n /** The worker entry is always built as CommonJS, next to this module's output. */\n protected workerFile(): string {\n return path.join(__dirname, 'worker-entry.js');\n }\n\n private handleWorkerExit(): void {\n this.ready = false;\n // Don't resurrect a worker we're intentionally shutting down — close() sets\n // `closing` and nulls the worker, so a late 'exit'/'error' event must not\n // queue a restart (which would spawn a fresh leaked thread post-shutdown).\n if (this.closing) return;\n if (this.restarts < this.maxRestarts) {\n this.restarts++;\n internalWarn(`WorkerTransport restarting (attempt ${this.restarts}/${this.maxRestarts})`);\n // Small backoff before restart. unref() so a perpetually-failing worker's\n // pending restart never keeps the process alive on its own.\n this.restartTimer = setTimeout(() => {\n this.restartTimer = null;\n this.startWorker();\n }, 500 * this.restarts);\n if (this.restartTimer.unref) this.restartTimer.unref();\n } else {\n internalError(\n `WorkerTransport exhausted ${this.maxRestarts} restart attempts — transport disabled`\n );\n }\n }\n\n private drainBuffer(): void {\n while (this.buffer.length > 0 && this.ready && this.worker) {\n const entry = this.buffer.shift();\n if (entry) {\n this.worker.postMessage({ type: 'write', entry });\n }\n }\n }\n\n write(entry: TransportLogEntry): void {\n if (this.ready && this.worker) {\n this.worker.postMessage({ type: 'write', entry });\n } else {\n if (this.buffer.length >= this.bufferSize) {\n internalWarn('WorkerTransport buffer full — dropping oldest entry');\n this.buffer.shift();\n }\n this.buffer.push(entry);\n }\n }\n\n async flush(): Promise<void> {\n if (!this.ready || !this.worker) return;\n const worker = this.worker;\n return new Promise<void>((resolve) => {\n // Time-box the flush: if the worker dies or restarts before answering,\n // the 'flushed' message would never arrive and this promise would hang\n // forever, blocking shutdown until the force-exit timer kills the process\n // (losing everything). Resolve on timeout so close() can still terminate.\n const timer = setTimeout(() => {\n worker.off('message', handler);\n resolve();\n }, this.FLUSH_TIMEOUT_MS);\n if (timer.unref) timer.unref();\n\n const handler = (msg: { type: string }) => {\n if (msg.type === 'flushed') {\n clearTimeout(timer);\n worker.off('message', handler);\n resolve();\n }\n };\n worker.on('message', handler);\n worker.postMessage({ type: 'flush' });\n });\n }\n\n /**\n * Flush the worker's buffer and terminate the thread on shutdown.\n *\n * This MUST be named close() — TransportManager.close() invokes\n * transport.close(), so a method named only shutdown() was never called on\n * graceful exit, leaking the worker thread and dropping its buffered logs.\n */\n async close(): Promise<void> {\n // Stop the restart loop first: cancel any queued backoff restart and flag\n // that further worker exits are expected (so handleWorkerExit no-ops).\n this.closing = true;\n if (this.restartTimer) {\n clearTimeout(this.restartTimer);\n this.restartTimer = null;\n }\n\n if (!this.worker) return;\n const worker = this.worker;\n this.worker = null;\n\n // Push any locally-buffered (pre-ready) entries first: the worker's port\n // queues them until its listener attaches, so they are handled before the\n // shutdown message, which flushes the wrapped transport and exits.\n this.drainBufferTo(worker);\n worker.postMessage({ type: 'shutdown' });\n\n await new Promise<void>((resolve) => {\n // Don't wait forever for a wedged worker — terminate() it after a timeout.\n const timer = setTimeout(() => {\n worker.terminate().finally(() => resolve());\n }, this.SHUTDOWN_TIMEOUT_MS);\n if (timer.unref) timer.unref();\n\n worker.once('exit', () => {\n clearTimeout(timer);\n resolve();\n });\n });\n }\n\n /** Back-compat alias — prefer close(). */\n async shutdown(): Promise<void> {\n await this.close();\n }\n\n private drainBufferTo(worker: Worker): void {\n while (this.buffer.length > 0) {\n const entry = this.buffer.shift();\n if (entry) worker.postMessage({ type: 'write', entry });\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAqEA,IAAa,kBAAb,MAAmD;CAsBjD,YAAY,QAA+B;cArBpB;mBAEK;uBACI;gBAEA;eAChB;gBAC+B,EAAE;kBAE9B;sBAI2B;iBAE5B;0BAEkB;6BAEG;AAGrC,OAAK,SAAS;AACd,OAAK,aAAa,OAAO,cAAc;AACvC,OAAK,cAAc,OAAO,eAAe;AACzC,OAAK,QAAQ,OAAO;AACpB,OAAK,aAAa;;CAGpB,AAAQ,cAAoB;AAC1B,MAAI;AACF,QAAK,SAAS,IAAIA,2BAAO,KAAK,YAAY,EAAE,EAC1C,YAAY;IACV,eAAe,KAAK,OAAO;IAC3B,iBAAiB,KAAK,OAAO;IAC9B,EACF,CAAC;AAEF,QAAK,OAAO,GAAG,YAAY,QAA0C;AACnE,QAAI,IAAI,SAAS,SAAS;AACxB,UAAK,QAAQ;AACb,UAAK,aAAa;eACT,IAAI,SAAS,WAAW,IAAI,SAAS,cAC9C,oCACE,oBAAoB,KAAK,OAAO,cAAc,KAAK,IAAI,SAAS,YACjE;KAEH;AAEF,QAAK,OAAO,GAAG,UAAU,QAAQ;AAC/B,uCAAc,gCAAgC,IAAI;AAClD,SAAK,kBAAkB;KACvB;AAEF,QAAK,OAAO,GAAG,SAAS,SAAS;AAC/B,QAAI,SAAS,GAAG;AACd,uCAAa,2CAA2C,OAAO;AAC/D,UAAK,kBAAkB;;KAEzB;WACK,KAAK;AACZ,sCAAc,mCAAmC,IAAI;;;;CAKzD,AAAU,aAAqB;AAC7B,SAAOC,kBAAK,KAAK,WAAW,kBAAkB;;CAGhD,AAAQ,mBAAyB;AAC/B,OAAK,QAAQ;AAIb,MAAI,KAAK,QAAS;AAClB,MAAI,KAAK,WAAW,KAAK,aAAa;AACpC,QAAK;AACL,qCAAa,uCAAuC,KAAK,SAAS,GAAG,KAAK,YAAY,GAAG;AAGzF,QAAK,eAAe,iBAAiB;AACnC,SAAK,eAAe;AACpB,SAAK,aAAa;MACjB,MAAM,KAAK,SAAS;AACvB,OAAI,KAAK,aAAa,MAAO,MAAK,aAAa,OAAO;QAEtD,oCACE,6BAA6B,KAAK,YAAY,wCAC/C;;CAIL,AAAQ,cAAoB;AAC1B,SAAO,KAAK,OAAO,SAAS,KAAK,KAAK,SAAS,KAAK,QAAQ;GAC1D,MAAM,QAAQ,KAAK,OAAO,OAAO;AACjC,OAAI,MACF,MAAK,OAAO,YAAY;IAAE,MAAM;IAAS;IAAO,CAAC;;;CAKvD,MAAM,OAAgC;AACpC,MAAI,KAAK,SAAS,KAAK,OACrB,MAAK,OAAO,YAAY;GAAE,MAAM;GAAS;GAAO,CAAC;OAC5C;AACL,OAAI,KAAK,OAAO,UAAU,KAAK,YAAY;AACzC,sCAAa,sDAAsD;AACnE,SAAK,OAAO,OAAO;;AAErB,QAAK,OAAO,KAAK,MAAM;;;CAI3B,MAAM,QAAuB;AAC3B,MAAI,CAAC,KAAK,SAAS,CAAC,KAAK,OAAQ;EACjC,MAAM,SAAS,KAAK;AACpB,SAAO,IAAI,SAAe,YAAY;GAKpC,MAAM,QAAQ,iBAAiB;AAC7B,WAAO,IAAI,WAAW,QAAQ;AAC9B,aAAS;MACR,KAAK,iBAAiB;AACzB,OAAI,MAAM,MAAO,OAAM,OAAO;GAE9B,MAAM,WAAW,QAA0B;AACzC,QAAI,IAAI,SAAS,WAAW;AAC1B,kBAAa,MAAM;AACnB,YAAO,IAAI,WAAW,QAAQ;AAC9B,cAAS;;;AAGb,UAAO,GAAG,WAAW,QAAQ;AAC7B,UAAO,YAAY,EAAE,MAAM,SAAS,CAAC;IACrC;;;;;;;;;CAUJ,MAAM,QAAuB;AAG3B,OAAK,UAAU;AACf,MAAI,KAAK,cAAc;AACrB,gBAAa,KAAK,aAAa;AAC/B,QAAK,eAAe;;AAGtB,MAAI,CAAC,KAAK,OAAQ;EAClB,MAAM,SAAS,KAAK;AACpB,OAAK,SAAS;AAKd,OAAK,cAAc,OAAO;AAC1B,SAAO,YAAY,EAAE,MAAM,YAAY,CAAC;AAExC,QAAM,IAAI,SAAe,YAAY;GAEnC,MAAM,QAAQ,iBAAiB;AAC7B,WAAO,WAAW,CAAC,cAAc,SAAS,CAAC;MAC1C,KAAK,oBAAoB;AAC5B,OAAI,MAAM,MAAO,OAAM,OAAO;AAE9B,UAAO,KAAK,cAAc;AACxB,iBAAa,MAAM;AACnB,aAAS;KACT;IACF;;;CAIJ,MAAM,WAA0B;AAC9B,QAAM,KAAK,OAAO;;CAGpB,AAAQ,cAAc,QAAsB;AAC1C,SAAO,KAAK,OAAO,SAAS,GAAG;GAC7B,MAAM,QAAQ,KAAK,OAAO,OAAO;AACjC,OAAI,MAAO,QAAO,YAAY;IAAE,MAAM;IAAS;IAAO,CAAC"}
|