logixia 1.12.4 → 1.12.6
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 +80 -27
- package/dist/.tsbuildinfo +1 -1
- package/dist/async-context-CSHWoHMB.js +88 -0
- package/dist/async-context-CSHWoHMB.js.map +1 -0
- package/dist/async-context-CwBNMm2E.mjs +68 -0
- package/dist/async-context-CwBNMm2E.mjs.map +1 -0
- package/dist/azure-monitor.transport-CG7ClkYj.d.ts +62 -0
- package/dist/azure-monitor.transport-CG7ClkYj.d.ts.map +1 -0
- package/dist/azure-monitor.transport-CHnjVRF-.mjs +152 -0
- package/dist/azure-monitor.transport-CHnjVRF-.mjs.map +1 -0
- package/dist/azure-monitor.transport-Cm74kXZS.d.mts +62 -0
- package/dist/azure-monitor.transport-Cm74kXZS.d.mts.map +1 -0
- package/dist/azure-monitor.transport-CrvfHd9C.js +157 -0
- package/dist/azure-monitor.transport-CrvfHd9C.js.map +1 -0
- package/dist/azure.d.mts +3 -0
- package/dist/azure.d.ts +3 -0
- package/dist/azure.js +6 -0
- package/dist/azure.mjs +6 -0
- package/dist/batch-buffer-BY-mRWIK.js +170 -0
- package/dist/batch-buffer-BY-mRWIK.js.map +1 -0
- package/dist/batch-buffer-PGvSgbmd.mjs +132 -0
- package/dist/batch-buffer-PGvSgbmd.mjs.map +1 -0
- package/dist/browser.d.mts +427 -0
- package/dist/browser.d.mts.map +1 -0
- package/dist/browser.d.ts +427 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +590 -0
- package/dist/browser.js.map +1 -0
- package/dist/browser.mjs +586 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/chunk-DLSkYcPk.mjs +7 -0
- package/dist/chunk-nOFOJqeH.js +30 -0
- package/dist/cloudwatch.d.mts +3 -0
- package/dist/cloudwatch.d.ts +3 -0
- package/dist/cloudwatch.js +6 -0
- package/dist/cloudwatch.mjs +6 -0
- package/dist/cloudwatch.transport-CGHY4buE.d.mts +81 -0
- package/dist/cloudwatch.transport-CGHY4buE.d.mts.map +1 -0
- package/dist/cloudwatch.transport-DK_8cKd3.d.ts +81 -0
- package/dist/cloudwatch.transport-DK_8cKd3.d.ts.map +1 -0
- package/dist/cloudwatch.transport-pNBNWJLt.js +370 -0
- package/dist/cloudwatch.transport-pNBNWJLt.js.map +1 -0
- package/dist/cloudwatch.transport-yBwm4OeO.mjs +360 -0
- package/dist/cloudwatch.transport-yBwm4OeO.mjs.map +1 -0
- package/dist/coerce.utils-BN980Y9V.js +44 -0
- package/dist/coerce.utils-BN980Y9V.js.map +1 -0
- package/dist/coerce.utils-Dxv4_ikC.mjs +38 -0
- package/dist/coerce.utils-Dxv4_ikC.mjs.map +1 -0
- package/dist/correlation.d.mts +238 -0
- package/dist/correlation.d.mts.map +1 -0
- package/dist/correlation.d.ts +238 -0
- package/dist/correlation.d.ts.map +1 -0
- package/dist/correlation.js +272 -0
- package/dist/correlation.js.map +1 -0
- package/dist/correlation.mjs +261 -0
- package/dist/correlation.mjs.map +1 -0
- package/dist/gcp.d.mts +3 -0
- package/dist/gcp.d.ts +3 -0
- package/dist/gcp.js +6 -0
- package/dist/gcp.mjs +6 -0
- package/dist/gcp.transport-BJuNRFe_.mjs +192 -0
- package/dist/gcp.transport-BJuNRFe_.mjs.map +1 -0
- package/dist/gcp.transport-BOhkVKlI.d.ts +71 -0
- package/dist/gcp.transport-BOhkVKlI.d.ts.map +1 -0
- package/dist/gcp.transport-Bi6CUqZR.d.mts +71 -0
- package/dist/gcp.transport-Bi6CUqZR.d.mts.map +1 -0
- package/dist/gcp.transport-SHdtc1n0.js +198 -0
- package/dist/gcp.transport-SHdtc1n0.js.map +1 -0
- package/dist/{index-CwFww7VC.d.ts → index-BPKwwERo.d.mts} +18 -174
- package/dist/index-BPKwwERo.d.mts.map +1 -0
- package/dist/{index-DnyeaoZU.d.mts → index-C1X-TE5I.d.ts} +18 -174
- package/dist/index-C1X-TE5I.d.ts.map +1 -0
- package/dist/index.d.mts +41 -11
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +40 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +87 -147
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -132
- package/dist/index.mjs.map +1 -1
- package/dist/internal-log-BUJYMZbE.js +70 -0
- package/dist/internal-log-BUJYMZbE.js.map +1 -0
- package/dist/internal-log-Cm8SOWSV.mjs +52 -0
- package/dist/internal-log-Cm8SOWSV.mjs.map +1 -0
- package/dist/{logitron-logger.module-BIGTm2V6.mjs → logitron-logger.module-Bs3XYZgF.mjs} +266 -166
- package/dist/logitron-logger.module-Bs3XYZgF.mjs.map +1 -0
- package/dist/{logitron-logger.module-BTxDpK6u.js → logitron-logger.module-BsDZR5BH.js} +297 -203
- package/dist/logitron-logger.module-BsDZR5BH.js.map +1 -0
- package/dist/{logitron-logger.module-BmT-ewin.d.ts → logitron-logger.module-CxT1F6Cm.d.ts} +23 -10
- package/dist/logitron-logger.module-CxT1F6Cm.d.ts.map +1 -0
- package/dist/{logitron-logger.module-BCq3sF27.d.mts → logitron-logger.module-zDKVd_o0.d.mts} +23 -10
- package/dist/logitron-logger.module-zDKVd_o0.d.mts.map +1 -0
- package/dist/middleware.d.mts +5 -2
- package/dist/middleware.d.mts.map +1 -1
- package/dist/middleware.d.ts +5 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +13 -4
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +14 -4
- package/dist/middleware.mjs.map +1 -1
- package/dist/nest.d.mts +3 -2
- package/dist/nest.d.mts.map +1 -1
- package/dist/nest.d.ts +3 -2
- package/dist/nest.d.ts.map +1 -1
- package/dist/nest.js +6 -2
- package/dist/nest.mjs +6 -2
- package/dist/safe-stringify-DJ66tW5I.mjs +118 -0
- package/dist/safe-stringify-DJ66tW5I.mjs.map +1 -0
- package/dist/safe-stringify-DWo3aZp_.js +142 -0
- package/dist/safe-stringify-DWo3aZp_.js.map +1 -0
- package/dist/search-CFQRISUv.mjs +1596 -0
- package/dist/search-CFQRISUv.mjs.map +1 -0
- package/dist/search-CQOo-Iq6.d.mts +816 -0
- package/dist/search-CQOo-Iq6.d.mts.map +1 -0
- package/dist/search-DujhR94O.d.ts +816 -0
- package/dist/search-DujhR94O.d.ts.map +1 -0
- package/dist/search-njTI2hdo.js +1631 -0
- package/dist/search-njTI2hdo.js.map +1 -0
- package/dist/search.d.mts +4 -1
- package/dist/search.d.ts +4 -1
- package/dist/search.js +9 -1
- package/dist/search.mjs +3 -2
- package/dist/testing.d.mts +2 -1
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.d.ts +2 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/{transport.manager-ciG4r5uz.js → transport.manager-BivyUTtU.js} +497 -437
- package/dist/transport.manager-BivyUTtU.js.map +1 -0
- package/dist/{transport.manager-DETpXuaY.mjs → transport.manager-D6KL3t9V.mjs} +473 -364
- package/dist/transport.manager-D6KL3t9V.mjs.map +1 -0
- package/dist/transport.types-B5vcyv2k.d.mts +187 -0
- package/dist/transport.types-B5vcyv2k.d.mts.map +1 -0
- package/dist/transport.types-BuY84TwO.d.ts +187 -0
- package/dist/transport.types-BuY84TwO.d.ts.map +1 -0
- package/dist/transports.d.mts +90 -29
- package/dist/transports.d.mts.map +1 -1
- package/dist/transports.d.ts +90 -29
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +14 -2
- package/dist/transports.mjs +10 -2
- package/dist/worker-entry.js +1241 -0
- package/dist/worker.d.mts +3 -0
- package/dist/worker.d.ts +3 -0
- package/dist/worker.js +4 -0
- package/dist/worker.mjs +4 -0
- package/dist/worker.transport-BP8Q28Z2.d.mts +76 -0
- package/dist/worker.transport-BP8Q28Z2.d.mts.map +1 -0
- package/dist/worker.transport-BhMyvqQ2.mjs +167 -0
- package/dist/worker.transport-BhMyvqQ2.mjs.map +1 -0
- package/dist/worker.transport-CfH6z37Z.js +168 -0
- package/dist/worker.transport-CfH6z37Z.js.map +1 -0
- package/dist/worker.transport-mKXgNK0L.d.ts +76 -0
- package/dist/worker.transport-mKXgNK0L.d.ts.map +1 -0
- package/package.json +23 -18
- package/dist/index-CwFww7VC.d.ts.map +0 -1
- package/dist/index-DnyeaoZU.d.mts.map +0 -1
- package/dist/logitron-logger.module-BCq3sF27.d.mts.map +0 -1
- package/dist/logitron-logger.module-BIGTm2V6.mjs.map +0 -1
- package/dist/logitron-logger.module-BTxDpK6u.js.map +0 -1
- package/dist/logitron-logger.module-BmT-ewin.d.ts.map +0 -1
- package/dist/search-Cg_OasF-.d.ts +0 -1
- package/dist/search-DN676Dnz.d.mts +0 -1
- package/dist/search-DOvSI-mb.mjs +0 -1
- package/dist/search-DoZF3RZj.js +0 -0
- package/dist/transport.manager-DETpXuaY.mjs.map +0 -1
- package/dist/transport.manager-ciG4r5uz.js.map +0 -1
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_worker_threads = require("node:worker_threads");
|
|
25
|
+
node_worker_threads = __toESM(node_worker_threads);
|
|
26
|
+
let node_fs = require("node:fs");
|
|
27
|
+
node_fs = __toESM(node_fs);
|
|
28
|
+
let node_path = require("node:path");
|
|
29
|
+
node_path = __toESM(node_path);
|
|
30
|
+
let node_stream_promises = require("node:stream/promises");
|
|
31
|
+
node_stream_promises = __toESM(node_stream_promises);
|
|
32
|
+
let node_util = require("node:util");
|
|
33
|
+
node_util = __toESM(node_util);
|
|
34
|
+
let node_zlib = require("node:zlib");
|
|
35
|
+
node_zlib = __toESM(node_zlib);
|
|
36
|
+
|
|
37
|
+
//#region src/utils/coerce.utils.ts
|
|
38
|
+
/**
|
|
39
|
+
* Defensive value coercion helpers for Logixia.
|
|
40
|
+
*
|
|
41
|
+
* Logger inputs come from third-party code (NestJS framework internals,
|
|
42
|
+
* user payloads, error objects) where the runtime type is not always what
|
|
43
|
+
* the TypeScript signature promises. The formatter / transport hot paths
|
|
44
|
+
* call `.replace()` on string fields and crash if the field is anything
|
|
45
|
+
* else. Use these helpers anywhere a downstream consumer assumes a string.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Coerce any value to a string without throwing.
|
|
49
|
+
*
|
|
50
|
+
* - `undefined` / `null` → `''` (so the field is rendered as empty rather
|
|
51
|
+
* than the literal string "undefined")
|
|
52
|
+
* - strings → returned as-is (zero allocation on the hot path)
|
|
53
|
+
* - Errors → `error.message` (matches what users would expect to see)
|
|
54
|
+
* - everything else → `JSON.stringify` if possible, falling back to
|
|
55
|
+
* `String(value)` for circular refs / values without a stringifier.
|
|
56
|
+
*/
|
|
57
|
+
function safeToString(value) {
|
|
58
|
+
var _constructor;
|
|
59
|
+
if (typeof value === "string") return value;
|
|
60
|
+
if (value === void 0 || value === null) return "";
|
|
61
|
+
if (value instanceof Error) return value.message;
|
|
62
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
|
|
63
|
+
if (typeof value === "symbol") return value.toString();
|
|
64
|
+
if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
65
|
+
try {
|
|
66
|
+
const json = JSON.stringify(value);
|
|
67
|
+
if (json !== void 0) return json;
|
|
68
|
+
} catch {}
|
|
69
|
+
return `[${((_constructor = value.constructor) === null || _constructor === void 0 ? void 0 : _constructor.name) ?? "object"}]`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/utils/safe-stringify.ts
|
|
74
|
+
/** Build a JSONPath like `$["a"][0]["b"]` for a decycle pointer. */
|
|
75
|
+
function jsonPath(parts) {
|
|
76
|
+
let path = "$";
|
|
77
|
+
for (const p of parts) path += typeof p === "number" ? `[${p}]` : `[${JSON.stringify(p)}]`;
|
|
78
|
+
return path;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Serialize any value to JSON without throwing on circular references or BigInt.
|
|
82
|
+
* Circular refs become `"[Circular]"` (or `{ $ref }` pointers when `decycle`).
|
|
83
|
+
*/
|
|
84
|
+
function safeStringify(value, options = {}) {
|
|
85
|
+
const { indent, deterministic = false, decycle = false, bigint = "string" } = options;
|
|
86
|
+
if (decycle) return JSON.stringify(decycleValue(value, bigint), void 0, indent);
|
|
87
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
88
|
+
const transform = (val) => {
|
|
89
|
+
if (typeof val === "bigint") return bigint === "number" ? Number(val) : val.toString();
|
|
90
|
+
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
|
|
91
|
+
if (typeof val === "symbol") return val.toString();
|
|
92
|
+
if (val === null || typeof val !== "object") return val;
|
|
93
|
+
if (seen.has(val)) return "[Circular]";
|
|
94
|
+
seen.add(val);
|
|
95
|
+
let out;
|
|
96
|
+
const toJSON = val.toJSON;
|
|
97
|
+
if (Array.isArray(val)) out = val.map((item) => transform(item));
|
|
98
|
+
else if (typeof toJSON === "function") out = transform(toJSON.call(val));
|
|
99
|
+
else {
|
|
100
|
+
const rec = val;
|
|
101
|
+
const keys = deterministic ? Object.keys(rec).sort() : Object.keys(rec);
|
|
102
|
+
const obj = {};
|
|
103
|
+
for (const k of keys) {
|
|
104
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
|
|
105
|
+
obj[k] = transform(rec[k]);
|
|
106
|
+
}
|
|
107
|
+
out = obj;
|
|
108
|
+
}
|
|
109
|
+
seen.delete(val);
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
return JSON.stringify(transform(value), void 0, indent);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Replace repeated object references with round-trippable `{ "$ref": "$..." }`
|
|
116
|
+
* JSONPath pointers (the classic Crockford decycle). Pair with {@link retrocycle}
|
|
117
|
+
* to reconstruct the original shared/circular graph.
|
|
118
|
+
*/
|
|
119
|
+
function decycleValue(value, bigint = "string") {
|
|
120
|
+
const paths = /* @__PURE__ */ new WeakMap();
|
|
121
|
+
const walk = (val, path) => {
|
|
122
|
+
if (typeof val === "bigint") return bigint === "number" ? Number(val) : val.toString();
|
|
123
|
+
if (val === null || typeof val !== "object") return val;
|
|
124
|
+
if (val instanceof Date) return val.toISOString();
|
|
125
|
+
const existing = paths.get(val);
|
|
126
|
+
if (existing !== void 0) return { $ref: existing };
|
|
127
|
+
paths.set(val, jsonPath(path));
|
|
128
|
+
if (Array.isArray(val)) return val.map((item, i) => walk(item, [...path, i]));
|
|
129
|
+
const rec = val;
|
|
130
|
+
const obj = {};
|
|
131
|
+
for (const k of Object.keys(rec)) {
|
|
132
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
|
|
133
|
+
obj[k] = walk(rec[k], [...path, k]);
|
|
134
|
+
}
|
|
135
|
+
return obj;
|
|
136
|
+
};
|
|
137
|
+
return walk(value, []);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* `JSON.stringify` for log output. Takes the plain fast path first and only
|
|
141
|
+
* falls back to {@link safeStringify} when that throws (a cycle, a BigInt, a
|
|
142
|
+
* throwing `toJSON`), so a bad payload costs nothing until it shows up.
|
|
143
|
+
* Never throws: if even the safe walk fails, the value is replaced by a marker.
|
|
144
|
+
*/
|
|
145
|
+
function stringifyForLog(value) {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.stringify(value);
|
|
148
|
+
} catch {
|
|
149
|
+
try {
|
|
150
|
+
return safeStringify(value);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
153
|
+
return JSON.stringify(`[Unserializable: ${reason}]`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/transports/console.transport.ts
|
|
160
|
+
/**
|
|
161
|
+
* Writes log entries to stdout (info/debug/verbose) and stderr (error/warn).
|
|
162
|
+
*
|
|
163
|
+
* Supports pretty-printed colorized output and compact JSON mode.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* transports: { console: { format: 'json' } }
|
|
167
|
+
*/
|
|
168
|
+
var ConsoleTransport = class ConsoleTransport {
|
|
169
|
+
static #_ = this.COLORS = new Map([
|
|
170
|
+
["black", "\x1B[30m"],
|
|
171
|
+
["red", "\x1B[31m"],
|
|
172
|
+
["green", "\x1B[32m"],
|
|
173
|
+
["yellow", "\x1B[33m"],
|
|
174
|
+
["blue", "\x1B[34m"],
|
|
175
|
+
["magenta", "\x1B[35m"],
|
|
176
|
+
["cyan", "\x1B[36m"],
|
|
177
|
+
["white", "\x1B[37m"],
|
|
178
|
+
["gray", "\x1B[90m"],
|
|
179
|
+
["grey", "\x1B[90m"],
|
|
180
|
+
["brightred", "\x1B[91m"],
|
|
181
|
+
["brightgreen", "\x1B[92m"],
|
|
182
|
+
["brightyellow", "\x1B[93m"],
|
|
183
|
+
["brightblue", "\x1B[94m"],
|
|
184
|
+
["brightmagenta", "\x1B[95m"],
|
|
185
|
+
["brightcyan", "\x1B[96m"],
|
|
186
|
+
["brightwhite", "\x1B[97m"],
|
|
187
|
+
["reset", "\x1B[0m"]
|
|
188
|
+
]);
|
|
189
|
+
static #_2 = this.CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g;
|
|
190
|
+
static sanitize(value) {
|
|
191
|
+
return safeToString(value).replace(ConsoleTransport.CONTROL_CHARS_RE, "");
|
|
192
|
+
}
|
|
193
|
+
constructor(config = {}) {
|
|
194
|
+
this.config = config;
|
|
195
|
+
this.name = "console";
|
|
196
|
+
if (config.filter) this.filter = config.filter;
|
|
197
|
+
}
|
|
198
|
+
write(entry) {
|
|
199
|
+
const formatted = this.formatEntry(entry) + "\n";
|
|
200
|
+
const lvl = entry.level.toLowerCase();
|
|
201
|
+
(lvl === "error" || lvl === "warn" ? process.stderr : process.stdout).write(formatted);
|
|
202
|
+
}
|
|
203
|
+
formatEntry(entry) {
|
|
204
|
+
if (this.config.format === "json") return stringifyForLog({
|
|
205
|
+
timestamp: this.config.timestamp !== false ? entry.timestamp.toISOString() : void 0,
|
|
206
|
+
level: entry.level,
|
|
207
|
+
message: entry.message,
|
|
208
|
+
...entry.data || {},
|
|
209
|
+
context: entry.context,
|
|
210
|
+
traceId: entry.traceId,
|
|
211
|
+
appName: entry.appName,
|
|
212
|
+
environment: entry.environment
|
|
213
|
+
});
|
|
214
|
+
const parts = [];
|
|
215
|
+
if (this.config.timestamp !== false) {
|
|
216
|
+
const timestamp = entry.timestamp.toISOString();
|
|
217
|
+
parts.push(this.colorize(timestamp, "gray"));
|
|
218
|
+
}
|
|
219
|
+
const level = ConsoleTransport.sanitize(entry.level).toUpperCase().padEnd(5);
|
|
220
|
+
const coloredLevel = this.colorize(level, this.getLevelColor(entry.level));
|
|
221
|
+
parts.push(coloredLevel);
|
|
222
|
+
if (entry.context) {
|
|
223
|
+
const context = `[${ConsoleTransport.sanitize(entry.context)}]`;
|
|
224
|
+
parts.push(this.colorize(context, "cyan"));
|
|
225
|
+
}
|
|
226
|
+
if (entry.traceId) {
|
|
227
|
+
const traceId = `(${ConsoleTransport.sanitize(entry.traceId)})`;
|
|
228
|
+
parts.push(this.colorize(traceId, "magenta"));
|
|
229
|
+
}
|
|
230
|
+
parts.push(ConsoleTransport.sanitize(entry.message));
|
|
231
|
+
if (entry.data && Object.keys(entry.data).length > 0) {
|
|
232
|
+
const data = stringifyForLog(entry.data);
|
|
233
|
+
parts.push(this.colorize(data, "blue"));
|
|
234
|
+
}
|
|
235
|
+
return parts.join(" ");
|
|
236
|
+
}
|
|
237
|
+
getLevelColor(level) {
|
|
238
|
+
var _this$config$levelCol;
|
|
239
|
+
const lower = level.toLowerCase();
|
|
240
|
+
if ((_this$config$levelCol = this.config.levelColors) === null || _this$config$levelCol === void 0 ? void 0 : _this$config$levelCol[lower]) return this.config.levelColors[lower];
|
|
241
|
+
return {
|
|
242
|
+
error: "red",
|
|
243
|
+
warn: "yellow",
|
|
244
|
+
warning: "yellow",
|
|
245
|
+
info: "green",
|
|
246
|
+
debug: "blue",
|
|
247
|
+
trace: "magenta",
|
|
248
|
+
verbose: "cyan"
|
|
249
|
+
}[lower] || "gray";
|
|
250
|
+
}
|
|
251
|
+
colorize(text, color) {
|
|
252
|
+
if (this.config.colorize === false) return text;
|
|
253
|
+
const reset = ConsoleTransport.COLORS.get("reset");
|
|
254
|
+
return `${ConsoleTransport.COLORS.get(color) ?? ConsoleTransport.COLORS.get("white")}${text}${reset}`;
|
|
255
|
+
}
|
|
256
|
+
close() {
|
|
257
|
+
return Promise.resolve();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/utils/internal-log.ts
|
|
263
|
+
/**
|
|
264
|
+
* Internal logging helpers for use within the logixia library itself.
|
|
265
|
+
*
|
|
266
|
+
* These functions write directly to stderr/stdout without going through
|
|
267
|
+
* LogixiaLogger — this avoids circular dependencies and ensures internal
|
|
268
|
+
* diagnostics still surface even when the logger is misconfigured.
|
|
269
|
+
*
|
|
270
|
+
* Library code must NEVER call console.log/warn/error directly. Use these
|
|
271
|
+
* helpers instead so that internal output can be silenced in tests by setting
|
|
272
|
+
* the LOGIXIA_SILENT_INTERNAL=1 environment variable.
|
|
273
|
+
*/
|
|
274
|
+
/**
|
|
275
|
+
* Read the silence flag on each call rather than caching it at import time, so
|
|
276
|
+
* setting LOGIXIA_SILENT_INTERNAL=1 AFTER the module is first imported (e.g. in
|
|
277
|
+
* a test setup file) still takes effect — the documented test-silencing
|
|
278
|
+
* behavior was previously unreliable because the value was frozen at load.
|
|
279
|
+
*/
|
|
280
|
+
function isSilent() {
|
|
281
|
+
return process.env.LOGIXIA_SILENT_INTERNAL === "1";
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Emit an internal warning to stderr.
|
|
285
|
+
* Use when something is misconfigured but logixia can continue operating.
|
|
286
|
+
*/
|
|
287
|
+
function internalWarn(message) {
|
|
288
|
+
if (!isSilent()) process.stderr.write(`[logixia:warn] ${message}\n`);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Emit an internal error to stderr.
|
|
292
|
+
* Use when a transport write fails or a serious internal error occurs.
|
|
293
|
+
*/
|
|
294
|
+
function internalError(message, error) {
|
|
295
|
+
if (!isSilent()) {
|
|
296
|
+
let errStr = "";
|
|
297
|
+
if (error instanceof Error) errStr = ` — ${error.message}`;
|
|
298
|
+
else if (error != null) errStr = ` — ${String(error)}`;
|
|
299
|
+
process.stderr.write(`[logixia:error] ${message}${errStr}\n`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/transports/database.transport.ts
|
|
305
|
+
/**
|
|
306
|
+
* Validates that a SQL identifier (table/index name) contains only safe characters.
|
|
307
|
+
* Prevents SQL injection via user-supplied table names.
|
|
308
|
+
*/
|
|
309
|
+
const POSTGRES_MAX_PARAMS = 65535;
|
|
310
|
+
const POSTGRES_COLUMNS = 8;
|
|
311
|
+
const POSTGRES_ROWS_PER_INSERT = Math.floor(POSTGRES_MAX_PARAMS / POSTGRES_COLUMNS);
|
|
312
|
+
const MYSQL_ROWS_PER_INSERT = 5e3;
|
|
313
|
+
function chunk(items, size) {
|
|
314
|
+
const chunks = [];
|
|
315
|
+
for (let i = 0; i < items.length; i += size) chunks.push(items.slice(i, i + size));
|
|
316
|
+
return chunks;
|
|
317
|
+
}
|
|
318
|
+
function validateSqlIdentifier(name, label = "table") {
|
|
319
|
+
if (!/^[a-zA-Z_]\w*$/.test(name)) throw new Error(`Invalid ${label} name "${name}". Only letters, digits, and underscores are allowed, and the name must start with a letter or underscore.`);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Persists log entries to a database in batches.
|
|
323
|
+
*
|
|
324
|
+
* Supports MongoDB, PostgreSQL, MySQL, and SQLite. Entries are buffered and
|
|
325
|
+
* flushed on a configurable interval or batch size threshold to minimize
|
|
326
|
+
* round-trips.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* transports: {
|
|
330
|
+
* database: { type: 'postgresql', host: 'localhost', database: 'appdb', table: 'logs' }
|
|
331
|
+
* }
|
|
332
|
+
*/
|
|
333
|
+
var DatabaseTransport = class {
|
|
334
|
+
constructor(config) {
|
|
335
|
+
this.config = config;
|
|
336
|
+
this.name = "database";
|
|
337
|
+
this.batch = [];
|
|
338
|
+
this.isConnected = false;
|
|
339
|
+
this.batchSize = config.batchSize || 100;
|
|
340
|
+
this.flushInterval = config.flushInterval || 5e3;
|
|
341
|
+
if (config.filter) this.filter = config.filter;
|
|
342
|
+
if (config.table) validateSqlIdentifier(config.table, "table");
|
|
343
|
+
this.setupFlushTimer();
|
|
344
|
+
}
|
|
345
|
+
async write(entry) {
|
|
346
|
+
if (!this.isConnected) await this.connect();
|
|
347
|
+
this.addToBatch(entry);
|
|
348
|
+
if (this.batch.length >= this.batchSize) await this.flush();
|
|
349
|
+
}
|
|
350
|
+
addToBatch(entry) {
|
|
351
|
+
this.batch.push(entry);
|
|
352
|
+
}
|
|
353
|
+
async flush() {
|
|
354
|
+
if (!this.flushPromise) this.flushPromise = this.drain().finally(() => {
|
|
355
|
+
this.flushPromise = void 0;
|
|
356
|
+
});
|
|
357
|
+
await this.flushPromise;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Drains the batch to the database one snapshot at a time. The current batch
|
|
361
|
+
* is detached SYNCHRONOUSLY before awaiting the write, so entries appended by
|
|
362
|
+
* concurrent write() calls land in a fresh array and are never written twice.
|
|
363
|
+
* On write failure the snapshot is restored to the front of the batch for the
|
|
364
|
+
* next flush cycle and the loop stops, so a persistently failing DB does not
|
|
365
|
+
* hot-spin retrying the same entries.
|
|
366
|
+
*/
|
|
367
|
+
async drain() {
|
|
368
|
+
while (this.batch.length > 0) {
|
|
369
|
+
const entriesToFlush = this.batch;
|
|
370
|
+
this.batch = [];
|
|
371
|
+
try {
|
|
372
|
+
switch (this.config.type) {
|
|
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
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
internalError("Database flush error", error);
|
|
389
|
+
this.batch.unshift(...entriesToFlush);
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async isReady() {
|
|
395
|
+
try {
|
|
396
|
+
if (!this.isConnected) await this.connect();
|
|
397
|
+
return this.isConnected;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async connect() {
|
|
403
|
+
if (this.connectionPromise) return this.connectionPromise;
|
|
404
|
+
this.connectionPromise = this.establishConnection().catch((error) => {
|
|
405
|
+
this.connectionPromise = void 0;
|
|
406
|
+
throw error;
|
|
407
|
+
});
|
|
408
|
+
return this.connectionPromise;
|
|
409
|
+
}
|
|
410
|
+
async establishConnection() {
|
|
411
|
+
try {
|
|
412
|
+
switch (this.config.type) {
|
|
413
|
+
case "mongodb":
|
|
414
|
+
await this.connectMongoDB();
|
|
415
|
+
break;
|
|
416
|
+
case "postgresql":
|
|
417
|
+
await this.connectPostgreSQL();
|
|
418
|
+
break;
|
|
419
|
+
case "mysql":
|
|
420
|
+
await this.connectMySQL();
|
|
421
|
+
break;
|
|
422
|
+
case "sqlite":
|
|
423
|
+
await this.connectSQLite();
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
this.isConnected = true;
|
|
427
|
+
} catch (error) {
|
|
428
|
+
this.isConnected = false;
|
|
429
|
+
throw new Error(`Database connection failed: ${error}`, { cause: error });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async connectMongoDB() {
|
|
433
|
+
try {
|
|
434
|
+
const { MongoClient } = await import("mongodb").catch(() => {
|
|
435
|
+
throw new Error("MongoDB driver not installed. Run: npm install mongodb");
|
|
436
|
+
});
|
|
437
|
+
this.connection = new MongoClient(this.config.connectionString || `mongodb://${this.config.host}:${this.config.port}/${this.config.database}`);
|
|
438
|
+
await this.connection.connect();
|
|
439
|
+
await this.connection.db(this.config.database).admin().ping();
|
|
440
|
+
} catch (error) {
|
|
441
|
+
throw new Error(`MongoDB connection failed: ${error}`, { cause: error });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async connectPostgreSQL() {
|
|
445
|
+
try {
|
|
446
|
+
const { Client } = await import("pg").catch(() => {
|
|
447
|
+
throw new Error("PostgreSQL driver not installed. Run: npm install pg @types/pg");
|
|
448
|
+
});
|
|
449
|
+
const config = {
|
|
450
|
+
connectionString: this.config.connectionString,
|
|
451
|
+
host: this.config.host,
|
|
452
|
+
port: this.config.port,
|
|
453
|
+
database: this.config.database
|
|
454
|
+
};
|
|
455
|
+
if (this.config.username) config.user = this.config.username;
|
|
456
|
+
if (this.config.password) config.password = this.config.password;
|
|
457
|
+
if (this.config.ssl) config.ssl = typeof this.config.ssl === "boolean" ? {} : this.config.ssl;
|
|
458
|
+
this.connection = new Client(config);
|
|
459
|
+
await this.connection.connect();
|
|
460
|
+
await this.createPostgreSQLTable();
|
|
461
|
+
} catch (error) {
|
|
462
|
+
throw new Error(`PostgreSQL connection failed: ${error}`, { cause: error });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async connectMySQL() {
|
|
466
|
+
try {
|
|
467
|
+
const mysql = await import("mysql2/promise").catch(() => {
|
|
468
|
+
throw new Error("MySQL driver not installed. Run: npm install mysql2");
|
|
469
|
+
});
|
|
470
|
+
const config = {
|
|
471
|
+
host: this.config.host,
|
|
472
|
+
port: this.config.port,
|
|
473
|
+
database: this.config.database
|
|
474
|
+
};
|
|
475
|
+
if (this.config.username) config.user = this.config.username;
|
|
476
|
+
if (this.config.password) config.password = this.config.password;
|
|
477
|
+
if (this.config.ssl) config.ssl = typeof this.config.ssl === "boolean" ? {} : this.config.ssl;
|
|
478
|
+
this.connection = await mysql.createConnection(config);
|
|
479
|
+
await this.createMySQLTable();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
throw new Error(`MySQL connection failed: ${error}`, { cause: error });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async connectSQLite() {
|
|
485
|
+
try {
|
|
486
|
+
const sqlite3ModuleName = "sqlite3";
|
|
487
|
+
const sqliteModuleName = "sqlite";
|
|
488
|
+
const sqlite3 = await import(sqlite3ModuleName).catch(() => {
|
|
489
|
+
throw new Error("SQLite driver not installed. Run: npm install sqlite3 sqlite");
|
|
490
|
+
});
|
|
491
|
+
const { open } = await import(sqliteModuleName).catch(() => {
|
|
492
|
+
throw new Error("SQLite driver not installed. Run: npm install sqlite3 sqlite");
|
|
493
|
+
});
|
|
494
|
+
this.connection = await open({
|
|
495
|
+
filename: this.config.database,
|
|
496
|
+
driver: sqlite3.Database
|
|
497
|
+
});
|
|
498
|
+
await this.createSQLiteTable();
|
|
499
|
+
} catch (error) {
|
|
500
|
+
throw new Error(`SQLite connection failed: ${error}`, { cause: error });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async flushToMongoDB(entries) {
|
|
504
|
+
const collection = this.connection.db(this.config.database).collection(this.config.collection || "logs");
|
|
505
|
+
const documents = entries.map((entry) => ({
|
|
506
|
+
timestamp: entry.timestamp,
|
|
507
|
+
level: entry.level,
|
|
508
|
+
message: entry.message,
|
|
509
|
+
payload: entry.data,
|
|
510
|
+
context: entry.context,
|
|
511
|
+
traceId: entry.traceId,
|
|
512
|
+
appName: entry.appName,
|
|
513
|
+
environment: entry.environment
|
|
514
|
+
}));
|
|
515
|
+
await collection.insertMany(documents);
|
|
516
|
+
}
|
|
517
|
+
async flushToPostgreSQL(entries) {
|
|
518
|
+
const tableName = this.config.table || "logs";
|
|
519
|
+
const chunks = chunk(entries.map((entry) => [
|
|
520
|
+
entry.timestamp,
|
|
521
|
+
entry.level,
|
|
522
|
+
entry.message,
|
|
523
|
+
stringifyForLog(entry.data || {}),
|
|
524
|
+
entry.context,
|
|
525
|
+
entry.traceId,
|
|
526
|
+
entry.appName,
|
|
527
|
+
entry.environment
|
|
528
|
+
]), POSTGRES_ROWS_PER_INSERT);
|
|
529
|
+
const insertChunk = async (rows) => {
|
|
530
|
+
const query = `
|
|
531
|
+
INSERT INTO ${tableName}
|
|
532
|
+
(timestamp, level, message, payload, context, trace_id, app_name, environment)
|
|
533
|
+
VALUES ${rows.map((_, i) => {
|
|
534
|
+
return `(${Array.from({ length: POSTGRES_COLUMNS }, (_unused, c) => `$${i * POSTGRES_COLUMNS + c + 1}`).join(", ")})`;
|
|
535
|
+
}).join(", ")}
|
|
536
|
+
`;
|
|
537
|
+
await this.connection.query(query, rows.flat());
|
|
538
|
+
};
|
|
539
|
+
if (chunks.length === 1) {
|
|
540
|
+
await insertChunk(chunks[0]);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
await this.connection.query("BEGIN");
|
|
544
|
+
try {
|
|
545
|
+
for (const rows of chunks) await insertChunk(rows);
|
|
546
|
+
await this.connection.query("COMMIT");
|
|
547
|
+
} catch (error) {
|
|
548
|
+
await this.connection.query("ROLLBACK").catch(() => {});
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async flushToMySQL(entries) {
|
|
553
|
+
const tableName = this.config.table || "logs";
|
|
554
|
+
const values = entries.map((entry) => [
|
|
555
|
+
entry.timestamp,
|
|
556
|
+
entry.level,
|
|
557
|
+
entry.message,
|
|
558
|
+
stringifyForLog(entry.data || {}),
|
|
559
|
+
entry.context,
|
|
560
|
+
entry.traceId,
|
|
561
|
+
entry.appName,
|
|
562
|
+
entry.environment
|
|
563
|
+
]);
|
|
564
|
+
const query = `
|
|
565
|
+
INSERT INTO ${tableName}
|
|
566
|
+
(timestamp, level, message, payload, context, trace_id, app_name, environment)
|
|
567
|
+
VALUES ?
|
|
568
|
+
`;
|
|
569
|
+
const chunks = chunk(values, MYSQL_ROWS_PER_INSERT);
|
|
570
|
+
if (chunks.length === 1) {
|
|
571
|
+
await this.connection.query(query, [values]);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
await this.connection.beginTransaction();
|
|
575
|
+
try {
|
|
576
|
+
for (const rows of chunks) await this.connection.query(query, [rows]);
|
|
577
|
+
await this.connection.commit();
|
|
578
|
+
} catch (error) {
|
|
579
|
+
await this.connection.rollback().catch(() => {});
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async flushToSQLite(entries) {
|
|
584
|
+
const tableName = this.config.table || "logs";
|
|
585
|
+
const stmt = await this.connection.prepare(`
|
|
586
|
+
INSERT INTO ${tableName}
|
|
587
|
+
(timestamp, level, message, payload, context, trace_id, app_name, environment)
|
|
588
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
589
|
+
`);
|
|
590
|
+
await this.connection.exec("BEGIN");
|
|
591
|
+
try {
|
|
592
|
+
for (const entry of entries) await stmt.run(entry.timestamp instanceof Date ? entry.timestamp.toISOString() : entry.timestamp, entry.level, entry.message, stringifyForLog(entry.data || {}), entry.context, entry.traceId, entry.appName, entry.environment);
|
|
593
|
+
await this.connection.exec("COMMIT");
|
|
594
|
+
} catch (error) {
|
|
595
|
+
await this.connection.exec("ROLLBACK").catch(() => {});
|
|
596
|
+
throw error;
|
|
597
|
+
} finally {
|
|
598
|
+
await stmt.finalize().catch(() => {});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async createPostgreSQLTable() {
|
|
602
|
+
const tableName = this.config.table || "logs";
|
|
603
|
+
const query = `
|
|
604
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
605
|
+
id SERIAL PRIMARY KEY,
|
|
606
|
+
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
607
|
+
level VARCHAR(20) NOT NULL,
|
|
608
|
+
message TEXT NOT NULL,
|
|
609
|
+
payload JSONB,
|
|
610
|
+
context VARCHAR(255),
|
|
611
|
+
trace_id VARCHAR(255),
|
|
612
|
+
app_name VARCHAR(255),
|
|
613
|
+
environment VARCHAR(50),
|
|
614
|
+
time_taken INTEGER,
|
|
615
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
616
|
+
)
|
|
617
|
+
`;
|
|
618
|
+
await this.connection.query(query);
|
|
619
|
+
await this.connection.query(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName} (timestamp)`);
|
|
620
|
+
await this.connection.query(`CREATE INDEX IF NOT EXISTS idx_${tableName}_level ON ${tableName} (level)`);
|
|
621
|
+
await this.connection.query(`CREATE INDEX IF NOT EXISTS idx_${tableName}_trace_id ON ${tableName} (trace_id)`);
|
|
622
|
+
}
|
|
623
|
+
async createMySQLTable() {
|
|
624
|
+
const query = `
|
|
625
|
+
CREATE TABLE IF NOT EXISTS ${this.config.table || "logs"} (
|
|
626
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
627
|
+
timestamp DATETIME NOT NULL,
|
|
628
|
+
level VARCHAR(20) NOT NULL,
|
|
629
|
+
message TEXT NOT NULL,
|
|
630
|
+
payload JSON,
|
|
631
|
+
context VARCHAR(255),
|
|
632
|
+
trace_id VARCHAR(255),
|
|
633
|
+
app_name VARCHAR(255),
|
|
634
|
+
environment VARCHAR(50),
|
|
635
|
+
time_taken INT,
|
|
636
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
637
|
+
INDEX idx_timestamp (timestamp),
|
|
638
|
+
INDEX idx_level (level),
|
|
639
|
+
INDEX idx_trace_id (trace_id)
|
|
640
|
+
)
|
|
641
|
+
`;
|
|
642
|
+
await this.connection.execute(query);
|
|
643
|
+
}
|
|
644
|
+
async createSQLiteTable() {
|
|
645
|
+
const tableName = this.config.table || "logs";
|
|
646
|
+
const query = `
|
|
647
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
648
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
649
|
+
timestamp TEXT NOT NULL,
|
|
650
|
+
level TEXT NOT NULL,
|
|
651
|
+
message TEXT NOT NULL,
|
|
652
|
+
payload TEXT,
|
|
653
|
+
context TEXT,
|
|
654
|
+
trace_id TEXT,
|
|
655
|
+
app_name TEXT,
|
|
656
|
+
environment TEXT,
|
|
657
|
+
time_taken INTEGER,
|
|
658
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
659
|
+
)
|
|
660
|
+
`;
|
|
661
|
+
await this.connection.exec(query);
|
|
662
|
+
await this.connection.exec(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName} (timestamp)`);
|
|
663
|
+
await this.connection.exec(`CREATE INDEX IF NOT EXISTS idx_${tableName}_level ON ${tableName} (level)`);
|
|
664
|
+
await this.connection.exec(`CREATE INDEX IF NOT EXISTS idx_${tableName}_trace_id ON ${tableName} (trace_id)`);
|
|
665
|
+
}
|
|
666
|
+
setupFlushTimer() {
|
|
667
|
+
var _this$flushTimer$unre, _this$flushTimer;
|
|
668
|
+
this.flushTimer = setInterval(() => {
|
|
669
|
+
this.flush().catch((error) => {
|
|
670
|
+
internalError("Scheduled database flush error", error);
|
|
671
|
+
});
|
|
672
|
+
}, this.flushInterval);
|
|
673
|
+
(_this$flushTimer$unre = (_this$flushTimer = this.flushTimer).unref) === null || _this$flushTimer$unre === void 0 || _this$flushTimer$unre.call(_this$flushTimer);
|
|
674
|
+
}
|
|
675
|
+
async close() {
|
|
676
|
+
if (this.flushTimer) {
|
|
677
|
+
clearInterval(this.flushTimer);
|
|
678
|
+
this.flushTimer = void 0;
|
|
679
|
+
}
|
|
680
|
+
const MAX_CLOSE_FLUSH_ATTEMPTS = 3;
|
|
681
|
+
if (this.flushPromise) await this.flushPromise.catch(() => {});
|
|
682
|
+
for (let attempt = 0; attempt < MAX_CLOSE_FLUSH_ATTEMPTS && this.batch.length > 0; attempt += 1) try {
|
|
683
|
+
await this.flush();
|
|
684
|
+
} catch (error) {
|
|
685
|
+
internalError("Database flush during close failed; entries remain buffered", error);
|
|
686
|
+
}
|
|
687
|
+
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`);
|
|
688
|
+
if (this.connection) switch (this.config.type) {
|
|
689
|
+
case "mongodb":
|
|
690
|
+
await this.connection.close();
|
|
691
|
+
break;
|
|
692
|
+
case "postgresql":
|
|
693
|
+
await this.connection.end();
|
|
694
|
+
break;
|
|
695
|
+
case "mysql":
|
|
696
|
+
await this.connection.end();
|
|
697
|
+
break;
|
|
698
|
+
case "sqlite":
|
|
699
|
+
await this.connection.close();
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
this.isConnected = false;
|
|
703
|
+
}
|
|
704
|
+
getBatchInfo() {
|
|
705
|
+
return {
|
|
706
|
+
batchSize: this.batchSize,
|
|
707
|
+
currentBatchLength: this.batch.length,
|
|
708
|
+
flushInterval: this.flushInterval,
|
|
709
|
+
isConnected: this.isConnected
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
async getLogCount() {
|
|
713
|
+
if (!this.isConnected) return 0;
|
|
714
|
+
try {
|
|
715
|
+
switch (this.config.type) {
|
|
716
|
+
case "mongodb": return await this.connection.db(this.config.database).collection(this.config.collection || "logs").countDocuments();
|
|
717
|
+
case "postgresql": {
|
|
718
|
+
var _result$rows;
|
|
719
|
+
const tableName = this.config.table || "logs";
|
|
720
|
+
const result = await this.connection.query(`SELECT COUNT(*) as count FROM ${tableName}`);
|
|
721
|
+
return Number((_result$rows = result.rows) === null || _result$rows === void 0 || (_result$rows = _result$rows[0]) === null || _result$rows === void 0 ? void 0 : _result$rows.count) || 0;
|
|
722
|
+
}
|
|
723
|
+
case "mysql": {
|
|
724
|
+
var _rows$;
|
|
725
|
+
const tableName = this.config.table || "logs";
|
|
726
|
+
const [rows] = await this.connection.query(`SELECT COUNT(*) as count FROM ${tableName}`);
|
|
727
|
+
return Number(rows === null || rows === void 0 || (_rows$ = rows[0]) === null || _rows$ === void 0 ? void 0 : _rows$.count) || 0;
|
|
728
|
+
}
|
|
729
|
+
case "sqlite": {
|
|
730
|
+
const tableName = this.config.table || "logs";
|
|
731
|
+
const row = await this.connection.get(`SELECT COUNT(*) as count FROM ${tableName}`);
|
|
732
|
+
return Number(row === null || row === void 0 ? void 0 : row.count) || 0;
|
|
733
|
+
}
|
|
734
|
+
default: return 0;
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
return 0;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
//#endregion
|
|
743
|
+
//#region src/transports/file.transport.ts
|
|
744
|
+
const appendFile = (0, node_util.promisify)(node_fs.appendFile);
|
|
745
|
+
const rename = (0, node_util.promisify)(node_fs.rename);
|
|
746
|
+
const unlink = (0, node_util.promisify)(node_fs.unlink);
|
|
747
|
+
const readdir = (0, node_util.promisify)(node_fs.readdir);
|
|
748
|
+
const stat = (0, node_util.promisify)(node_fs.stat);
|
|
749
|
+
const DROP_WARN_INTERVAL_MS = 5e3;
|
|
750
|
+
const SIZE_UNITS = {
|
|
751
|
+
"": 1,
|
|
752
|
+
k: 1024,
|
|
753
|
+
m: 1024 ** 2,
|
|
754
|
+
g: 1024 ** 3,
|
|
755
|
+
t: 1024 ** 4
|
|
756
|
+
};
|
|
757
|
+
/**
|
|
758
|
+
* Parses a size like `50MB`, `1.5g`, `512 KiB` or a plain byte count.
|
|
759
|
+
* Units are binary (1KB = 1024 bytes). Returns undefined for anything else.
|
|
760
|
+
*/
|
|
761
|
+
function parseSize(value) {
|
|
762
|
+
if (value === void 0) return void 0;
|
|
763
|
+
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
|
|
764
|
+
const match = /^(\d+(?:\.\d+)?) ?([kmgt]?)(?:i?b)?$/i.exec(value.trim());
|
|
765
|
+
if (!match) return void 0;
|
|
766
|
+
const bytes = Math.floor(Number(match[1]) * (SIZE_UNITS[(match[2] ?? "").toLowerCase()] ?? 1));
|
|
767
|
+
return bytes > 0 ? bytes : void 0;
|
|
768
|
+
}
|
|
769
|
+
/** Formats a date with the YYYY, MM, DD, HH, mm and ss tokens, in local time. */
|
|
770
|
+
function formatDatePattern(date, pattern) {
|
|
771
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
772
|
+
return pattern.replace(/YYYY/g, String(date.getFullYear())).replace(/MM/g, pad(date.getMonth() + 1)).replace(/DD/g, pad(date.getDate())).replace(/HH/g, pad(date.getHours())).replace(/mm/g, pad(date.getMinutes())).replace(/ss/g, pad(date.getSeconds()));
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Appends log entries to a local file with automatic rotation.
|
|
776
|
+
*
|
|
777
|
+
* Rotates by size (`maxSize`), by date (`datePattern`), by interval
|
|
778
|
+
* (`rotation.interval`) or by a custom `rotation.shouldRotate` check. Rotated
|
|
779
|
+
* files can be gzipped (`zippedArchive`) and pruned to the newest `maxFiles`.
|
|
780
|
+
*
|
|
781
|
+
* Without a `rotation` block the live file keeps its name (`app.log`) and
|
|
782
|
+
* rotated files are renamed beside it (`app-2026-10-22T09-30-00-000.log`).
|
|
783
|
+
* With a `rotation` block every file, the live one included, is timestamped.
|
|
784
|
+
*
|
|
785
|
+
* @example
|
|
786
|
+
* transports: { file: { filename: 'app.log', dirname: './logs', maxSize: '50MB' } }
|
|
787
|
+
*/
|
|
788
|
+
var FileTransport = class {
|
|
789
|
+
constructor(config) {
|
|
790
|
+
this.name = "file";
|
|
791
|
+
this.batch = [];
|
|
792
|
+
this.lastRotation = /* @__PURE__ */ new Date();
|
|
793
|
+
this.bytesWritten = 0;
|
|
794
|
+
this.rotateRequested = false;
|
|
795
|
+
this.rotationWaiters = [];
|
|
796
|
+
this.enqueued = 0;
|
|
797
|
+
this.settled = 0;
|
|
798
|
+
this.flushWaiters = [];
|
|
799
|
+
this.spaceWaiters = [];
|
|
800
|
+
this.inFlight = 0;
|
|
801
|
+
this.dropped = 0;
|
|
802
|
+
this.lastDropWarning = 0;
|
|
803
|
+
this.housekeeping = Promise.resolve();
|
|
804
|
+
this.config = {
|
|
805
|
+
format: "json",
|
|
806
|
+
batchSize: 100,
|
|
807
|
+
flushInterval: 5e3,
|
|
808
|
+
...config
|
|
809
|
+
};
|
|
810
|
+
this.level = config.level;
|
|
811
|
+
this.batchSize = this.config.batchSize || 100;
|
|
812
|
+
this.flushInterval = this.config.flushInterval || 5e3;
|
|
813
|
+
if (config.filter) this.filter = config.filter;
|
|
814
|
+
const rotation = config.rotation;
|
|
815
|
+
const rawMaxSize = (rotation === null || rotation === void 0 ? void 0 : rotation.maxSize) ?? config.maxSize;
|
|
816
|
+
this.maxBytes = parseSize(rawMaxSize);
|
|
817
|
+
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
|
+
const maxQueue = config.maxQueue;
|
|
819
|
+
if (maxQueue !== void 0 && !(Number.isInteger(maxQueue) && maxQueue > 0)) internalWarn(`FileTransport: maxQueue ${String(maxQueue)} is not a positive integer; ignored`);
|
|
820
|
+
else this.maxQueue = maxQueue;
|
|
821
|
+
this.queuePolicy = config.queuePolicy ?? "drop-oldest";
|
|
822
|
+
this.maxFiles = (rotation === null || rotation === void 0 ? void 0 : rotation.maxFiles) ?? config.maxFiles;
|
|
823
|
+
this.compress = (rotation === null || rotation === void 0 ? void 0 : rotation.compress) ?? config.zippedArchive ?? false;
|
|
824
|
+
this.intervalMs = (rotation === null || rotation === void 0 ? void 0 : rotation.interval) ? this.parseInterval(rotation.interval) : void 0;
|
|
825
|
+
this.timestampedActive = rotation !== void 0;
|
|
826
|
+
this.dir = this.resolveDir();
|
|
827
|
+
this.ext = node_path.extname(config.filename);
|
|
828
|
+
this.base = node_path.basename(config.filename, this.ext);
|
|
829
|
+
try {
|
|
830
|
+
node_fs.mkdirSync(this.dir, { recursive: true });
|
|
831
|
+
} catch (error) {
|
|
832
|
+
internalError(`FileTransport could not create ${this.dir}`, error);
|
|
833
|
+
}
|
|
834
|
+
this.currentFilePath = this.timestampedActive ? this.uniquePath(this.timestamp(/* @__PURE__ */ new Date())) : node_path.join(this.dir, `${this.base}${this.ext}`);
|
|
835
|
+
let existing;
|
|
836
|
+
try {
|
|
837
|
+
existing = node_fs.statSync(this.currentFilePath);
|
|
838
|
+
} catch {
|
|
839
|
+
existing = void 0;
|
|
840
|
+
}
|
|
841
|
+
this.bytesWritten = (existing === null || existing === void 0 ? void 0 : existing.size) ?? 0;
|
|
842
|
+
if (config.datePattern) this.period = formatDatePattern((existing === null || existing === void 0 ? void 0 : existing.mtime) ?? /* @__PURE__ */ new Date(), config.datePattern);
|
|
843
|
+
}
|
|
844
|
+
async write(entry) {
|
|
845
|
+
try {
|
|
846
|
+
while (this.queuePolicy === "block" && this.queueFull()) await this.roomAvailable();
|
|
847
|
+
if (this.config.batchSize && this.config.batchSize > 1) this.addToBatch(entry);
|
|
848
|
+
else {
|
|
849
|
+
this.enqueue(entry);
|
|
850
|
+
await this.flush();
|
|
851
|
+
}
|
|
852
|
+
} catch (error) {
|
|
853
|
+
throw new Error(`File transport write failed: ${error.message}`, { cause: error });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Resolves once every entry queued before the call is on disk. It does not
|
|
858
|
+
* wait for entries queued afterwards, so under steady traffic it still returns
|
|
859
|
+
* instead of waiting for a quiet moment that never comes.
|
|
860
|
+
*/
|
|
861
|
+
async flush() {
|
|
862
|
+
if (this.batchTimer) {
|
|
863
|
+
clearTimeout(this.batchTimer);
|
|
864
|
+
this.batchTimer = void 0;
|
|
865
|
+
}
|
|
866
|
+
if (this.settled >= this.enqueued) return;
|
|
867
|
+
const done = new Promise((resolve, reject) => {
|
|
868
|
+
this.flushWaiters.push({
|
|
869
|
+
upTo: this.enqueued,
|
|
870
|
+
resolve,
|
|
871
|
+
reject
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
this.startDrain();
|
|
875
|
+
await done;
|
|
876
|
+
}
|
|
877
|
+
/** Writes out anything pending, then starts a new file. */
|
|
878
|
+
async rotate() {
|
|
879
|
+
await this.flush();
|
|
880
|
+
const done = new Promise((resolve) => this.rotationWaiters.push(resolve));
|
|
881
|
+
this.rotateRequested = true;
|
|
882
|
+
this.startDrain();
|
|
883
|
+
await done;
|
|
884
|
+
}
|
|
885
|
+
enqueue(entry) {
|
|
886
|
+
if (this.queueFull()) {
|
|
887
|
+
if (this.queuePolicy === "drop-oldest" && this.batch.length > 0) {
|
|
888
|
+
this.batch.shift();
|
|
889
|
+
this.batch.push(entry);
|
|
890
|
+
}
|
|
891
|
+
this.noteDropped();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
this.batch.push(entry);
|
|
895
|
+
this.enqueued += 1;
|
|
896
|
+
}
|
|
897
|
+
queueFull() {
|
|
898
|
+
return this.maxQueue !== void 0 && this.batch.length + this.inFlight >= this.maxQueue;
|
|
899
|
+
}
|
|
900
|
+
roomAvailable() {
|
|
901
|
+
const room = new Promise((resolve) => this.spaceWaiters.push(resolve));
|
|
902
|
+
this.startDrain();
|
|
903
|
+
return room;
|
|
904
|
+
}
|
|
905
|
+
releaseWaiters() {
|
|
906
|
+
const waiting = this.spaceWaiters;
|
|
907
|
+
this.spaceWaiters = [];
|
|
908
|
+
for (const resolve of waiting) resolve();
|
|
909
|
+
}
|
|
910
|
+
noteDropped() {
|
|
911
|
+
this.dropped += 1;
|
|
912
|
+
this.reportDropped(DROP_WARN_INTERVAL_MS);
|
|
913
|
+
}
|
|
914
|
+
reportDropped(minGapMs) {
|
|
915
|
+
const now = Date.now();
|
|
916
|
+
if (this.dropped === 0 || now - this.lastDropWarning < minGapMs) return;
|
|
917
|
+
internalWarn(`FileTransport: dropped ${this.dropped} log line${this.dropped === 1 ? "" : "s"} (queue full at maxQueue ${this.maxQueue}, policy ${this.queuePolicy})`);
|
|
918
|
+
this.dropped = 0;
|
|
919
|
+
this.lastDropWarning = now;
|
|
920
|
+
}
|
|
921
|
+
startDrain() {
|
|
922
|
+
if (this.draining) return;
|
|
923
|
+
this.draining = this.drain().finally(() => {
|
|
924
|
+
this.draining = void 0;
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Writes the batch one snapshot at a time. Entries queued while a write is in
|
|
929
|
+
* flight are picked up by the next iteration, so one drain empties the batch
|
|
930
|
+
* however much is logged meanwhile.
|
|
931
|
+
*/
|
|
932
|
+
async drain() {
|
|
933
|
+
while (this.batch.length > 0 || this.rotateRequested) {
|
|
934
|
+
if (this.rotateRequested) {
|
|
935
|
+
this.rotateRequested = false;
|
|
936
|
+
const waiting = this.rotationWaiters;
|
|
937
|
+
this.rotationWaiters = [];
|
|
938
|
+
try {
|
|
939
|
+
await this.rotateFile();
|
|
940
|
+
} catch (error) {
|
|
941
|
+
internalError("FileTransport rotation failed", error);
|
|
942
|
+
}
|
|
943
|
+
for (const resolve of waiting) resolve();
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const pending = this.batch;
|
|
947
|
+
this.batch = [];
|
|
948
|
+
this.inFlight = pending.length;
|
|
949
|
+
let failure;
|
|
950
|
+
try {
|
|
951
|
+
await this.writeBatch(pending);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
failure = error;
|
|
954
|
+
}
|
|
955
|
+
this.inFlight = 0;
|
|
956
|
+
this.settled += pending.length;
|
|
957
|
+
this.settle(failure);
|
|
958
|
+
this.releaseWaiters();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Resolves the flush() calls whose entries are now written. A failed batch
|
|
963
|
+
* fails every flush still waiting, since each one covers that batch.
|
|
964
|
+
*/
|
|
965
|
+
settle(failure) {
|
|
966
|
+
if (failure !== void 0) {
|
|
967
|
+
if (this.flushWaiters.length === 0) internalError("FileTransport write failed", failure);
|
|
968
|
+
for (const waiter of this.flushWaiters) waiter.error ??= failure;
|
|
969
|
+
}
|
|
970
|
+
const waiting = [];
|
|
971
|
+
for (const waiter of this.flushWaiters) if (waiter.upTo > this.settled) waiting.push(waiter);
|
|
972
|
+
else if (waiter.error === void 0) waiter.resolve();
|
|
973
|
+
else waiter.reject(waiter.error);
|
|
974
|
+
this.flushWaiters = waiting;
|
|
975
|
+
}
|
|
976
|
+
formatEntry(entry) {
|
|
977
|
+
switch (this.config.format) {
|
|
978
|
+
case "json": return stringifyForLog({
|
|
979
|
+
timestamp: entry.timestamp.toISOString(),
|
|
980
|
+
level: entry.level,
|
|
981
|
+
message: entry.message,
|
|
982
|
+
...entry.data || {}
|
|
983
|
+
});
|
|
984
|
+
case "csv": {
|
|
985
|
+
const safeMessage = safeToString(entry.message);
|
|
986
|
+
return [
|
|
987
|
+
entry.timestamp.toISOString(),
|
|
988
|
+
entry.level,
|
|
989
|
+
`"${safeMessage.replace(/"/g, "\"\"")}"`,
|
|
990
|
+
safeToString(entry.context),
|
|
991
|
+
safeToString(entry.traceId),
|
|
992
|
+
stringifyForLog(entry.data || {})
|
|
993
|
+
].join(",");
|
|
994
|
+
}
|
|
995
|
+
case "text":
|
|
996
|
+
default: return `${entry.timestamp.toISOString()} [${entry.level.toUpperCase()}] ${safeToString(entry.message)}${entry.data ? " " + stringifyForLog(entry.data) : ""}`;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
addToBatch(entry) {
|
|
1000
|
+
this.enqueue(entry);
|
|
1001
|
+
const threshold = Math.min(this.config.batchSize || 100, this.maxQueue ?? Infinity);
|
|
1002
|
+
if (this.batch.length >= threshold && !this.draining) {
|
|
1003
|
+
if (this.batchTimer) {
|
|
1004
|
+
clearTimeout(this.batchTimer);
|
|
1005
|
+
this.batchTimer = void 0;
|
|
1006
|
+
}
|
|
1007
|
+
this.startDrain();
|
|
1008
|
+
} else if (!this.batchTimer && this.config.flushInterval) this.batchTimer = setTimeout(() => {
|
|
1009
|
+
this.flush().catch((err) => internalError("FileTransport interval flush failed", err));
|
|
1010
|
+
}, this.config.flushInterval);
|
|
1011
|
+
}
|
|
1012
|
+
/** Only ever called from drain(), so writes and rotations never overlap. */
|
|
1013
|
+
async writeBatch(entries) {
|
|
1014
|
+
if (entries.length === 0) return;
|
|
1015
|
+
if (this.timeToRotate()) await this.rotateFile();
|
|
1016
|
+
if (this.maxBytes === void 0) {
|
|
1017
|
+
const content = entries.map((entry) => this.formatEntry(entry)).join("\n") + "\n";
|
|
1018
|
+
await this.append(content);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
let chunk$1 = "";
|
|
1022
|
+
let chunkBytes = 0;
|
|
1023
|
+
for (const entry of entries) {
|
|
1024
|
+
const line = this.formatEntry(entry) + "\n";
|
|
1025
|
+
const lineBytes = Buffer.byteLength(line);
|
|
1026
|
+
const used = this.bytesWritten + chunkBytes;
|
|
1027
|
+
if (used > 0 && used + lineBytes > this.maxBytes) {
|
|
1028
|
+
if (chunk$1) await this.append(chunk$1, chunkBytes);
|
|
1029
|
+
chunk$1 = "";
|
|
1030
|
+
chunkBytes = 0;
|
|
1031
|
+
await this.rotateFile();
|
|
1032
|
+
}
|
|
1033
|
+
chunk$1 += line;
|
|
1034
|
+
chunkBytes += lineBytes;
|
|
1035
|
+
}
|
|
1036
|
+
if (chunk$1) await this.append(chunk$1, chunkBytes);
|
|
1037
|
+
}
|
|
1038
|
+
async append(content, bytes = Buffer.byteLength(content)) {
|
|
1039
|
+
await appendFile(this.currentFilePath, content);
|
|
1040
|
+
this.bytesWritten += bytes;
|
|
1041
|
+
}
|
|
1042
|
+
timeToRotate() {
|
|
1043
|
+
var _this$config$rotation;
|
|
1044
|
+
if (this.intervalMs !== void 0 && Date.now() - this.lastRotation.getTime() >= this.intervalMs) return true;
|
|
1045
|
+
if (this.config.datePattern && this.period !== void 0) {
|
|
1046
|
+
if (formatDatePattern(/* @__PURE__ */ new Date(), this.config.datePattern) !== this.period) return true;
|
|
1047
|
+
}
|
|
1048
|
+
const shouldRotate = (_this$config$rotation = this.config.rotation) === null || _this$config$rotation === void 0 ? void 0 : _this$config$rotation.shouldRotate;
|
|
1049
|
+
if (shouldRotate) {
|
|
1050
|
+
const stats = {
|
|
1051
|
+
size: this.bytesWritten,
|
|
1052
|
+
created: this.lastRotation,
|
|
1053
|
+
modified: /* @__PURE__ */ new Date()
|
|
1054
|
+
};
|
|
1055
|
+
try {
|
|
1056
|
+
return shouldRotate(this.currentFilePath, stats);
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
internalError("FileTransport rotation.shouldRotate threw", error);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
parseInterval(interval) {
|
|
1064
|
+
const match = /^(\d{1,6})([hdwmy])$/i.exec(interval.trim());
|
|
1065
|
+
if (!match) return 1440 * 60 * 1e3;
|
|
1066
|
+
const value = Number.parseInt(match[1] || "1", 10);
|
|
1067
|
+
switch ((match[2] || "h").toLowerCase()) {
|
|
1068
|
+
case "h": return value * 60 * 60 * 1e3;
|
|
1069
|
+
case "d": return value * 24 * 60 * 60 * 1e3;
|
|
1070
|
+
case "w": return value * 7 * 24 * 60 * 60 * 1e3;
|
|
1071
|
+
case "m": return value * 30 * 24 * 60 * 60 * 1e3;
|
|
1072
|
+
case "y": return value * 365 * 24 * 60 * 60 * 1e3;
|
|
1073
|
+
default: return value * 60 * 60 * 1e3;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async rotateFile() {
|
|
1077
|
+
const now = /* @__PURE__ */ new Date();
|
|
1078
|
+
let archived;
|
|
1079
|
+
if (this.timestampedActive) {
|
|
1080
|
+
archived = this.currentFilePath;
|
|
1081
|
+
this.currentFilePath = this.uniquePath(this.timestamp(now));
|
|
1082
|
+
} else {
|
|
1083
|
+
archived = this.uniquePath(this.period ?? this.timestamp(now));
|
|
1084
|
+
try {
|
|
1085
|
+
await rename(this.currentFilePath, archived);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
if (error.code !== "ENOENT") {
|
|
1088
|
+
internalError(`FileTransport could not rotate ${this.currentFilePath}`, error);
|
|
1089
|
+
this.bytesWritten = 0;
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
archived = void 0;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
this.bytesWritten = 0;
|
|
1096
|
+
this.lastRotation = now;
|
|
1097
|
+
if (this.config.datePattern) this.period = formatDatePattern(now, this.config.datePattern);
|
|
1098
|
+
if (archived === void 0 || !node_fs.existsSync(archived)) return;
|
|
1099
|
+
const file = archived;
|
|
1100
|
+
this.housekeeping = this.housekeeping.then(async () => {
|
|
1101
|
+
if (this.compress) await this.compressFile(file);
|
|
1102
|
+
if (this.maxFiles) await this.cleanupOldFiles();
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
timestamp(date) {
|
|
1106
|
+
return date.toISOString().replace(/[:.]/g, "-").slice(0, 23);
|
|
1107
|
+
}
|
|
1108
|
+
/** `<base>-<stamp><ext>`, with `.1`, `.2`… added if that name (or its .gz) is taken. */
|
|
1109
|
+
uniquePath(stamp) {
|
|
1110
|
+
const taken = (p) => node_fs.existsSync(p) || node_fs.existsSync(`${p}.gz`);
|
|
1111
|
+
let candidate = node_path.join(this.dir, `${this.base}-${stamp}${this.ext}`);
|
|
1112
|
+
let n = 0;
|
|
1113
|
+
while (taken(candidate) || candidate === this.currentFilePath) {
|
|
1114
|
+
n += 1;
|
|
1115
|
+
candidate = node_path.join(this.dir, `${this.base}-${stamp}.${n}${this.ext}`);
|
|
1116
|
+
}
|
|
1117
|
+
return candidate;
|
|
1118
|
+
}
|
|
1119
|
+
resolveDir() {
|
|
1120
|
+
if (this.config.dirname) return this.config.dirname;
|
|
1121
|
+
const fromFilename = node_path.dirname(this.config.filename);
|
|
1122
|
+
return fromFilename === "." ? process.cwd() : fromFilename;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Gzip the rotated file to `<file>.gz` and remove the original. Streams the
|
|
1126
|
+
* data so large log files don't have to be buffered in memory. Best-effort:
|
|
1127
|
+
* any failure is logged and leaves the original file intact rather than
|
|
1128
|
+
* throwing out of the rotation path.
|
|
1129
|
+
*/
|
|
1130
|
+
async compressFile(filePath) {
|
|
1131
|
+
const gzPath = `${filePath}.gz`;
|
|
1132
|
+
try {
|
|
1133
|
+
await (0, node_stream_promises.pipeline)(node_fs.createReadStream(filePath), (0, node_zlib.createGzip)(), node_fs.createWriteStream(gzPath));
|
|
1134
|
+
await unlink(filePath);
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
internalError("Failed to compress rotated log file", error);
|
|
1137
|
+
try {
|
|
1138
|
+
await unlink(gzPath);
|
|
1139
|
+
} catch {}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* True for this transport's rotated files only: `<base>-<stamp><ext>` where the
|
|
1144
|
+
* stamp is digits and separators, optionally `.gz`. Another transport's
|
|
1145
|
+
* `app-error.log` must not count as one of `app.log`'s rotations.
|
|
1146
|
+
*/
|
|
1147
|
+
isOwnRotatedFile(file) {
|
|
1148
|
+
const name = file.endsWith(".gz") ? file.slice(0, -3) : file;
|
|
1149
|
+
if (!name.startsWith(`${this.base}-`) || !name.endsWith(this.ext)) return false;
|
|
1150
|
+
const stamp = name.slice(this.base.length + 1, name.length - this.ext.length);
|
|
1151
|
+
return /^\d[\d\-_.T]*$/.test(stamp);
|
|
1152
|
+
}
|
|
1153
|
+
async cleanupOldFiles() {
|
|
1154
|
+
if (!this.maxFiles) return;
|
|
1155
|
+
try {
|
|
1156
|
+
const files = await readdir(this.dir);
|
|
1157
|
+
const active = node_path.basename(this.currentFilePath);
|
|
1158
|
+
const newestFirst = (await Promise.all(files.filter((file) => file !== active && this.isOwnRotatedFile(file)).map(async (file) => {
|
|
1159
|
+
const filePath = node_path.join(this.dir, file);
|
|
1160
|
+
return {
|
|
1161
|
+
path: filePath,
|
|
1162
|
+
mtime: (await stat(filePath)).mtimeMs
|
|
1163
|
+
};
|
|
1164
|
+
}))).sort((a, b) => b.mtime - a.mtime || b.path.localeCompare(a.path));
|
|
1165
|
+
for (const file of newestFirst.slice(this.maxFiles)) await unlink(file.path);
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
internalError("Failed to cleanup old log files", error);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
async close() {
|
|
1171
|
+
if (this.batchTimer) {
|
|
1172
|
+
clearTimeout(this.batchTimer);
|
|
1173
|
+
this.batchTimer = void 0;
|
|
1174
|
+
}
|
|
1175
|
+
await this.flush();
|
|
1176
|
+
await this.housekeeping;
|
|
1177
|
+
this.reportDropped(0);
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/transports/worker-entry.ts
|
|
1183
|
+
const port = node_worker_threads.parentPort;
|
|
1184
|
+
if (!port) throw new Error("worker-entry must run inside a worker thread");
|
|
1185
|
+
const { transportType, transportConfig } = node_worker_threads.workerData;
|
|
1186
|
+
function createTransport() {
|
|
1187
|
+
const config = transportConfig;
|
|
1188
|
+
switch (transportType) {
|
|
1189
|
+
case "file": return new FileTransport(config);
|
|
1190
|
+
case "database": return new DatabaseTransport(config);
|
|
1191
|
+
default: return new ConsoleTransport(config);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const errorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
1195
|
+
let transport;
|
|
1196
|
+
try {
|
|
1197
|
+
transport = createTransport();
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
port.postMessage({
|
|
1200
|
+
type: "error",
|
|
1201
|
+
error: errorMessage(err)
|
|
1202
|
+
});
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
const flushTransport = async () => {
|
|
1206
|
+
if (typeof transport.flush === "function") await transport.flush();
|
|
1207
|
+
};
|
|
1208
|
+
async function handle(msg) {
|
|
1209
|
+
if (msg.type === "write") try {
|
|
1210
|
+
await transport.write(msg.entry);
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
port === null || port === void 0 || port.postMessage({
|
|
1213
|
+
type: "write-error",
|
|
1214
|
+
error: errorMessage(err),
|
|
1215
|
+
entry: msg.entry
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
else if (msg.type === "flush") try {
|
|
1219
|
+
await flushTransport();
|
|
1220
|
+
port === null || port === void 0 || port.postMessage({ type: "flushed" });
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
port === null || port === void 0 || port.postMessage({
|
|
1223
|
+
type: "flush-error",
|
|
1224
|
+
error: errorMessage(err)
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
try {
|
|
1229
|
+
await flushTransport();
|
|
1230
|
+
if (typeof transport.close === "function") await transport.close();
|
|
1231
|
+
} catch {}
|
|
1232
|
+
process.exit(0);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
let queue = Promise.resolve();
|
|
1236
|
+
port.on("message", (msg) => {
|
|
1237
|
+
queue = queue.then(() => handle(msg));
|
|
1238
|
+
});
|
|
1239
|
+
port.postMessage({ type: "ready" });
|
|
1240
|
+
|
|
1241
|
+
//#endregion
|