logixia 1.13.0 → 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 +9 -0
- 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 +24 -6
- package/dist/correlation.d.mts.map +1 -1
- package/dist/correlation.d.ts +24 -6
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +41 -16
- package/dist/correlation.js.map +1 -1
- package/dist/correlation.mjs +41 -16
- 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-CbXy2gfW.d.ts → nestjs-extras-OkC796py.d.mts} +111 -4
- package/dist/nestjs-extras-OkC796py.d.mts.map +1 -0
- package/dist/{logitron-logger.module-BdG1Jnpk.d.mts → nestjs-extras-mKrHE9S6.d.ts} +111 -4
- 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-BdG1Jnpk.d.mts.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-CbXy2gfW.d.ts.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/middleware.js
CHANGED
|
@@ -1,12 +1,60 @@
|
|
|
1
|
+
const require_internal_log = require('./internal-log-BUJYMZbE.js');
|
|
1
2
|
const require_safe_stringify = require('./safe-stringify-DWo3aZp_.js');
|
|
3
|
+
const require_trace_utils = require('./trace.utils-DpBOLDtq.js');
|
|
2
4
|
|
|
3
5
|
//#region src/middleware/http-logger.ts
|
|
4
|
-
const DEFAULT_REDACT_HEADERS =
|
|
6
|
+
const DEFAULT_REDACT_HEADERS = [
|
|
5
7
|
"authorization",
|
|
6
8
|
"cookie",
|
|
7
9
|
"set-cookie",
|
|
8
|
-
"x-api-key"
|
|
9
|
-
|
|
10
|
+
"x-api-key",
|
|
11
|
+
"proxy-authorization",
|
|
12
|
+
"x-auth-token",
|
|
13
|
+
"x-access-token",
|
|
14
|
+
"x-csrf-token",
|
|
15
|
+
"x-amz-security-token"
|
|
16
|
+
];
|
|
17
|
+
const DEFAULT_REDACT_QUERY_PARAMS = [
|
|
18
|
+
"token",
|
|
19
|
+
"access_token",
|
|
20
|
+
"refresh_token",
|
|
21
|
+
"code",
|
|
22
|
+
"password",
|
|
23
|
+
"secret",
|
|
24
|
+
"api_key",
|
|
25
|
+
"apikey",
|
|
26
|
+
"key",
|
|
27
|
+
"signature",
|
|
28
|
+
"sig",
|
|
29
|
+
"auth"
|
|
30
|
+
];
|
|
31
|
+
function buildNameSet(defaults, extra) {
|
|
32
|
+
return new Set([...defaults, ...extra ?? []].map((n) => n.toLowerCase()));
|
|
33
|
+
}
|
|
34
|
+
function safeDecode(value) {
|
|
35
|
+
try {
|
|
36
|
+
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
37
|
+
} catch {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Replace the values of sensitive query parameters, leaving the rest verbatim. */
|
|
42
|
+
function sanitizeUrl(url, redactParams) {
|
|
43
|
+
const q = url.indexOf("?");
|
|
44
|
+
if (q === -1) return url;
|
|
45
|
+
const hashAt = url.indexOf("#", q);
|
|
46
|
+
const query = url.slice(q + 1, hashAt === -1 ? void 0 : hashAt);
|
|
47
|
+
const hash = hashAt === -1 ? "" : url.slice(hashAt);
|
|
48
|
+
const cleaned = query.split("&").map((pair) => {
|
|
49
|
+
const eq = pair.indexOf("=");
|
|
50
|
+
const rawKey = eq === -1 ? pair : pair.slice(0, eq);
|
|
51
|
+
const decoded = safeDecode(rawKey);
|
|
52
|
+
const bracket = decoded.indexOf("[");
|
|
53
|
+
const name = (bracket === -1 ? decoded : decoded.slice(0, bracket)).toLowerCase();
|
|
54
|
+
return redactParams.has(name) && eq !== -1 ? `${rawKey}=[REDACTED]` : pair;
|
|
55
|
+
}).join("&");
|
|
56
|
+
return `${url.slice(0, q)}?${cleaned}${hash}`;
|
|
57
|
+
}
|
|
10
58
|
function sanitizeHeaders(headers, redactSet) {
|
|
11
59
|
if (!headers) return {};
|
|
12
60
|
const out = {};
|
|
@@ -16,16 +64,25 @@ function sanitizeHeaders(headers, redactSet) {
|
|
|
16
64
|
}
|
|
17
65
|
return out;
|
|
18
66
|
}
|
|
67
|
+
/** Fire-and-forget a logger call; a failing log must never become an unhandled rejection. */
|
|
68
|
+
function fire(result) {
|
|
69
|
+
if (result && typeof result.catch === "function") result.catch((error) => require_internal_log.internalError("http-logger: log call failed", error));
|
|
70
|
+
}
|
|
19
71
|
function shortId() {
|
|
20
72
|
return Math.random().toString(36).slice(2, 10);
|
|
21
73
|
}
|
|
22
|
-
function
|
|
74
|
+
function resolveNameSets(options) {
|
|
75
|
+
return {
|
|
76
|
+
headers: buildNameSet(DEFAULT_REDACT_HEADERS, options.redactHeaders),
|
|
77
|
+
queryParams: buildNameSet(DEFAULT_REDACT_QUERY_PARAMS, options.redactQueryParams)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function buildBaseFields(req, traceId, options, { headers: redactSet, queryParams: paramSet }) {
|
|
23
81
|
var _req$method, _req$socket;
|
|
24
|
-
const redactSet = options.redactHeaders ? new Set(options.redactHeaders.map((h) => h.toLowerCase())) : DEFAULT_REDACT_HEADERS;
|
|
25
82
|
const fields = {
|
|
26
83
|
traceId,
|
|
27
84
|
method: ((_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase()) ?? "UNKNOWN",
|
|
28
|
-
url: req.url ?? "/",
|
|
85
|
+
url: sanitizeUrl(req.url ?? "/", paramSet),
|
|
29
86
|
ip: req.ip ?? ((_req$socket = req.socket) === null || _req$socket === void 0 ? void 0 : _req$socket.remoteAddress) ?? "unknown",
|
|
30
87
|
headers: sanitizeHeaders(req.headers, redactSet)
|
|
31
88
|
};
|
|
@@ -37,19 +94,21 @@ function buildBaseFields(req, traceId, options) {
|
|
|
37
94
|
*/
|
|
38
95
|
function createExpressMiddleware(logger, options = {}) {
|
|
39
96
|
const { skip, logBody, bodyMaxBytes, traceIdHeader = "x-trace-id", requestLevel = "debug", responseLevel = "info", errorLevel = "error", slowRequestThresholdMs = 1e3 } = options;
|
|
97
|
+
const nameSets = resolveNameSets(options);
|
|
98
|
+
const traceHeaderName = traceIdHeader.toLowerCase();
|
|
40
99
|
return function logixiaHttpMiddleware(req, res, next) {
|
|
41
100
|
var _req$headers, _res$once, _res$once2;
|
|
42
101
|
if (skip === null || skip === void 0 ? void 0 : skip(req)) {
|
|
43
102
|
next();
|
|
44
103
|
return;
|
|
45
104
|
}
|
|
46
|
-
const traceId = ((_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : _req$headers[
|
|
105
|
+
const traceId = require_trace_utils.sanitizeIncomingId((_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : _req$headers[traceHeaderName]) ?? shortId();
|
|
47
106
|
const startMs = Date.now();
|
|
48
|
-
const baseFields = buildBaseFields(req, traceId, options);
|
|
49
|
-
if (requestLevel !== "silent") logger.logLevel(requestLevel, "request started", {
|
|
107
|
+
const baseFields = buildBaseFields(req, traceId, options, nameSets);
|
|
108
|
+
if (requestLevel !== "silent") fire(logger.logLevel(requestLevel, "request started", {
|
|
50
109
|
...baseFields,
|
|
51
110
|
...logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}
|
|
52
|
-
});
|
|
111
|
+
}));
|
|
53
112
|
let completed = false;
|
|
54
113
|
const onFinish = () => {
|
|
55
114
|
if (completed) return;
|
|
@@ -57,17 +116,17 @@ function createExpressMiddleware(logger, options = {}) {
|
|
|
57
116
|
const duration = Date.now() - startMs;
|
|
58
117
|
const status = res.statusCode ?? 0;
|
|
59
118
|
const level = status >= 500 ? errorLevel : responseLevel;
|
|
60
|
-
logger.logLevel(level, "request completed", {
|
|
119
|
+
fire(logger.logLevel(level, "request completed", {
|
|
61
120
|
...baseFields,
|
|
62
121
|
statusCode: status,
|
|
63
122
|
duration
|
|
64
|
-
});
|
|
65
|
-
if (duration > slowRequestThresholdMs) logger.warn("slow request detected", {
|
|
123
|
+
}));
|
|
124
|
+
if (duration > slowRequestThresholdMs) fire(logger.warn("slow request detected", {
|
|
66
125
|
...baseFields,
|
|
67
126
|
statusCode: status,
|
|
68
127
|
duration,
|
|
69
128
|
threshold: slowRequestThresholdMs
|
|
70
|
-
});
|
|
129
|
+
}));
|
|
71
130
|
};
|
|
72
131
|
(_res$once = res.once) === null || _res$once === void 0 || _res$once.call(res, "finish", onFinish);
|
|
73
132
|
(_res$once2 = res.once) === null || _res$once2 === void 0 || _res$once2.call(res, "close", onFinish);
|
|
@@ -84,6 +143,8 @@ function createExpressMiddleware(logger, options = {}) {
|
|
|
84
143
|
*/
|
|
85
144
|
function createFastifyPlugin(logger, options = {}) {
|
|
86
145
|
const { skip, traceIdHeader = "x-trace-id", requestLevel = "debug", responseLevel = "info", errorLevel = "error", slowRequestThresholdMs = 1e3 } = options;
|
|
146
|
+
const nameSets = resolveNameSets(options);
|
|
147
|
+
const traceHeaderName = traceIdHeader.toLowerCase();
|
|
87
148
|
function logixiaFastifyPlugin(fastify, _opts, done) {
|
|
88
149
|
fastify.addHook("onRequest", (request, _reply, hookDone) => {
|
|
89
150
|
var _req$headers2;
|
|
@@ -92,10 +153,10 @@ function createFastifyPlugin(logger, options = {}) {
|
|
|
92
153
|
hookDone();
|
|
93
154
|
return;
|
|
94
155
|
}
|
|
95
|
-
const traceId = ((_req$headers2 = req.headers) === null || _req$headers2 === void 0 ? void 0 : _req$headers2[
|
|
156
|
+
const traceId = require_trace_utils.sanitizeIncomingId((_req$headers2 = req.headers) === null || _req$headers2 === void 0 ? void 0 : _req$headers2[traceHeaderName]) ?? shortId();
|
|
96
157
|
req._logixiaStart = Date.now();
|
|
97
158
|
req._logixiaId = traceId;
|
|
98
|
-
if (requestLevel !== "silent") logger.logLevel(requestLevel, "request started", buildBaseFields(req, traceId, options));
|
|
159
|
+
if (requestLevel !== "silent") fire(logger.logLevel(requestLevel, "request started", buildBaseFields(req, traceId, options, nameSets)));
|
|
99
160
|
hookDone();
|
|
100
161
|
});
|
|
101
162
|
fastify.addHook("onResponse", (request, reply, hookDone) => {
|
|
@@ -109,18 +170,19 @@ function createFastifyPlugin(logger, options = {}) {
|
|
|
109
170
|
const status = rep.statusCode ?? 0;
|
|
110
171
|
const traceId = req._logixiaId ?? shortId();
|
|
111
172
|
const level = status >= 500 ? errorLevel : responseLevel;
|
|
112
|
-
|
|
113
|
-
|
|
173
|
+
const baseFields = buildBaseFields(req, traceId, options, nameSets);
|
|
174
|
+
fire(logger.logLevel(level, "request completed", {
|
|
175
|
+
...baseFields,
|
|
114
176
|
statusCode: status,
|
|
115
177
|
duration
|
|
116
|
-
});
|
|
117
|
-
if (duration > slowRequestThresholdMs) logger.warn("slow request detected", {
|
|
178
|
+
}));
|
|
179
|
+
if (duration > slowRequestThresholdMs) fire(logger.warn("slow request detected", {
|
|
118
180
|
traceId,
|
|
119
|
-
url:
|
|
181
|
+
url: baseFields.url,
|
|
120
182
|
method: req.method,
|
|
121
183
|
duration,
|
|
122
184
|
threshold: slowRequestThresholdMs
|
|
123
|
-
});
|
|
185
|
+
}));
|
|
124
186
|
hookDone();
|
|
125
187
|
});
|
|
126
188
|
done();
|
|
@@ -130,14 +192,76 @@ function createFastifyPlugin(logger, options = {}) {
|
|
|
130
192
|
[Symbol.for("fastify.display-name")]: "logixia"
|
|
131
193
|
});
|
|
132
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Bound the captured body without turning it into a string, so the logger's
|
|
197
|
+
* path-based redaction still sees the original keys. Oversized structures are
|
|
198
|
+
* pruned (extra keys/items dropped, long strings cut) rather than serialized.
|
|
199
|
+
*/
|
|
133
200
|
function truncateBody(body, maxBytes = 4096) {
|
|
134
201
|
if (typeof body === "string") return body.length > maxBytes ? body.slice(0, maxBytes) + "…[truncated]" : body;
|
|
135
202
|
if (body && typeof body === "object") {
|
|
136
|
-
|
|
137
|
-
|
|
203
|
+
if (require_safe_stringify.stringifyForLog(body).length <= maxBytes) return body;
|
|
204
|
+
return pruneValue(body, { left: maxBytes }, 0, /* @__PURE__ */ new Set());
|
|
138
205
|
}
|
|
139
206
|
return body;
|
|
140
207
|
}
|
|
208
|
+
const TRUNCATED = "…[truncated]";
|
|
209
|
+
const MAX_PRUNE_DEPTH = 8;
|
|
210
|
+
function pruneValue(value, budget, depth, ancestors) {
|
|
211
|
+
if (typeof value === "string") {
|
|
212
|
+
const cut = value.slice(0, Math.max(0, budget.left));
|
|
213
|
+
budget.left -= cut.length;
|
|
214
|
+
return cut.length < value.length ? cut + TRUNCATED : cut;
|
|
215
|
+
}
|
|
216
|
+
if (value === null || typeof value !== "object") {
|
|
217
|
+
budget.left -= 8;
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
if (value instanceof Date) {
|
|
221
|
+
budget.left -= 24;
|
|
222
|
+
return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
223
|
+
}
|
|
224
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
225
|
+
budget.left -= 16;
|
|
226
|
+
return `[Buffer ${value.byteLength} bytes]`;
|
|
227
|
+
}
|
|
228
|
+
if (value instanceof Error) return value;
|
|
229
|
+
if (ancestors.has(value) || depth >= MAX_PRUNE_DEPTH) return TRUNCATED;
|
|
230
|
+
ancestors.add(value);
|
|
231
|
+
try {
|
|
232
|
+
if (Array.isArray(value) || value instanceof Set || value instanceof Map) {
|
|
233
|
+
const items = [];
|
|
234
|
+
for (const item of value instanceof Map ? value.entries() : value) {
|
|
235
|
+
if (budget.left <= 0) {
|
|
236
|
+
items.push(TRUNCATED);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
items.push(pruneValue(item, budget, depth + 1, ancestors));
|
|
240
|
+
}
|
|
241
|
+
return items;
|
|
242
|
+
}
|
|
243
|
+
const toJSON = value.toJSON;
|
|
244
|
+
if (typeof toJSON === "function") {
|
|
245
|
+
const json = toJSON.call(value);
|
|
246
|
+
if (json !== value) return pruneValue(json, budget, depth + 1, ancestors);
|
|
247
|
+
}
|
|
248
|
+
const record = {};
|
|
249
|
+
for (const [k, v] of Object.entries(value)) {
|
|
250
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
|
|
251
|
+
if (budget.left <= 0) {
|
|
252
|
+
record["…"] = TRUNCATED;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
budget.left -= k.length;
|
|
256
|
+
record[k] = pruneValue(v, budget, depth + 1, ancestors);
|
|
257
|
+
}
|
|
258
|
+
return record;
|
|
259
|
+
} catch {
|
|
260
|
+
return TRUNCATED;
|
|
261
|
+
} finally {
|
|
262
|
+
ancestors.delete(value);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
141
265
|
|
|
142
266
|
//#endregion
|
|
143
267
|
exports.createExpressMiddleware = createExpressMiddleware;
|
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","names":["out: Record<string, unknown>","fields: Record<string, unknown>","stringifyForLog"],"sources":["../src/middleware/http-logger.ts"],"sourcesContent":["/**\n * HTTP request/response logging middleware — Morgan replacement.\n *\n * Fixes every documented Morgan bug:\n * - statusCode always captured correctly, even for requests > 20 s\n * - Logs request START (with traceId) and response FINISH (with duration)\n * - Captures errors before and after response\n * - Auto-redacts Authorization / Cookie / Set-Cookie headers\n * - Slow-request warnings\n * - Skip predicates for health-check routes / static assets\n *\n * @example Express\n * ```ts\n * import { createExpressMiddleware } from 'logixia/middleware';\n * app.use(createExpressMiddleware(logger));\n * ```\n *\n * @example Fastify\n * ```ts\n * import { createFastifyPlugin } from 'logixia/middleware';\n * await fastify.register(createFastifyPlugin(logger));\n * ```\n */\n\nimport type { IBaseLogger } from '../types';\nimport { stringifyForLog } from '../utils/safe-stringify';\n\n// ── Shared types ─────────────────────────────────────────────────────────────\n\nexport interface HttpLoggerOptions {\n /**\n * Skip logging for a request. Called before any I/O.\n * @example `skip: (req) => req.url === '/health'`\n */\n skip?: (req: IncomingRequest) => boolean;\n /**\n * Log request body (POST/PUT/PATCH). Capped at `bodyMaxBytes` (default: 4096).\n * Redaction still applies to the captured body.\n * Default: false.\n */\n logBody?: boolean;\n /** Max bytes of body to capture. Default: 4096. */\n bodyMaxBytes?: number;\n /**\n * Emit a WARN log when a request duration exceeds this threshold (ms).\n * Default: 1000.\n */\n slowRequestThresholdMs?: number;\n /**\n * Additional fields to include in every log entry.\n * @example `extraFields: (req) => ({ tenantId: req.headers['x-tenant-id'] })`\n */\n extraFields?: (req: IncomingRequest) => Record<string, unknown>;\n /**\n * Trace ID header. Default: 'x-trace-id'.\n * If the header is absent, a short random ID is generated.\n */\n traceIdHeader?: string;\n /**\n * Headers to redact from logged output.\n * Default: ['authorization', 'cookie', 'set-cookie', 'x-api-key'].\n */\n redactHeaders?: string[];\n /**\n * Log level for request-start entries. Default: 'debug'.\n * Set to 'silent' to suppress request-start logs entirely.\n */\n requestLevel?: string;\n /** Log level for successful response entries. Default: 'info'. */\n responseLevel?: string;\n /** Log level for error responses (status ≥ 500). Default: 'error'. */\n errorLevel?: string;\n}\n\n// Minimal structural types so we don't need @types/express / fastify in core\nexport interface IncomingRequest {\n method?: string;\n url?: string;\n headers?: Record<string, string | string[] | undefined>;\n body?: unknown;\n socket?: { remoteAddress?: string };\n ip?: string;\n}\n\nexport interface OutgoingResponse {\n statusCode?: number;\n on?: (event: string, cb: () => void) => void;\n once?: (event: string, cb: () => void) => void;\n getHeader?: (name: string) => string | number | string[] | undefined;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst DEFAULT_REDACT_HEADERS = new Set(['authorization', 'cookie', 'set-cookie', 'x-api-key']);\n\nfunction sanitizeHeaders(\n headers: Record<string, string | string[] | undefined> | undefined,\n redactSet: Set<string>\n): Record<string, unknown> {\n if (!headers) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(headers)) {\n if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;\n out[k] = redactSet.has(k.toLowerCase()) ? '[REDACTED]' : v;\n }\n return out;\n}\n\nfunction shortId(): string {\n // eslint-disable-next-line sonarjs/pseudo-random -- non-security request ID\n return Math.random().toString(36).slice(2, 10);\n}\n\nfunction buildBaseFields(\n req: IncomingRequest,\n traceId: string,\n options: HttpLoggerOptions\n): Record<string, unknown> {\n const redactSet = options.redactHeaders\n ? new Set(options.redactHeaders.map((h) => h.toLowerCase()))\n : DEFAULT_REDACT_HEADERS;\n\n const fields: Record<string, unknown> = {\n traceId,\n method: req.method?.toUpperCase() ?? 'UNKNOWN',\n url: req.url ?? '/',\n ip: req.ip ?? req.socket?.remoteAddress ?? 'unknown',\n headers: sanitizeHeaders(req.headers, redactSet),\n };\n\n if (options.extraFields) {\n Object.assign(fields, options.extraFields(req));\n }\n\n return fields;\n}\n\n// ── Express middleware ────────────────────────────────────────────────────────\n\n/**\n * Create an Express / Connect compatible middleware that replaces Morgan.\n */\nexport function createExpressMiddleware(\n logger: IBaseLogger,\n options: HttpLoggerOptions = {}\n): (req: IncomingRequest, res: OutgoingResponse, next: () => void) => void {\n const {\n skip,\n logBody,\n bodyMaxBytes,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n return function logixiaHttpMiddleware(\n req: IncomingRequest,\n res: OutgoingResponse,\n next: () => void\n ): void {\n if (skip?.(req)) {\n next();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n const startMs = Date.now();\n const baseFields = buildBaseFields(req, traceId, options);\n\n // Log request start\n if (requestLevel !== 'silent') {\n void logger.logLevel(requestLevel, 'request started', {\n ...baseFields,\n ...(logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}),\n });\n }\n\n // Hook into the response 'finish' event — fires after headers + body are sent.\n // This is what Morgan gets wrong for slow requests (it uses 'close' which may\n // fire before the status code is set on some Node versions).\n //\n // BOTH 'finish' and 'close' fire on a normal response, so guard against\n // logging the completion twice (which would duplicate the completed entry and\n // the slow-request warning).\n let completed = false;\n const onFinish = (): void => {\n if (completed) return;\n completed = true;\n\n const duration = Date.now() - startMs;\n const status = res.statusCode ?? 0;\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...baseFields,\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n ...baseFields,\n statusCode: status,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n };\n\n res.once?.('finish', onFinish);\n // Fallback: also covers a client that disconnects before 'finish' fires. The\n // `completed` guard ensures a normal finish+close pair logs only once.\n res.once?.('close', onFinish);\n\n next();\n };\n}\n\n// ── Fastify plugin ─────────────────────────────────────────────────────────────\n\nexport interface FastifyInstance {\n addHook: (name: string, fn: (req: unknown, reply: unknown, done: () => void) => void) => void;\n}\n\n/**\n * Create a Fastify plugin (a function you pass to `fastify.register()`).\n *\n * @example\n * ```ts\n * await fastify.register(createFastifyPlugin(logger, { slowRequestThresholdMs: 500 }));\n * ```\n */\nexport function createFastifyPlugin(logger: IBaseLogger, options: HttpLoggerOptions = {}) {\n const {\n skip,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n\n function logixiaFastifyPlugin(fastify: FastifyInstance, _opts: unknown, done: () => void): void {\n fastify.addHook('onRequest', (request: unknown, _reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n if (skip?.(req)) {\n hookDone();\n return;\n }\n\n const traceId = (req.headers?.[traceIdHeader] as string | undefined) ?? shortId();\n req._logixiaStart = Date.now();\n req._logixiaId = traceId;\n\n if (requestLevel !== 'silent') {\n void logger.logLevel(\n requestLevel,\n 'request started',\n buildBaseFields(req, traceId, options)\n );\n }\n hookDone();\n });\n\n fastify.addHook('onResponse', (request: unknown, reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n if (req._logixiaStart === undefined) {\n hookDone();\n return;\n }\n\n const rep = reply as { statusCode?: number };\n const duration = Date.now() - req._logixiaStart;\n const status = rep.statusCode ?? 0;\n const traceId = req._logixiaId ?? shortId();\n const level = status >= 500 ? errorLevel : responseLevel;\n\n void logger.logLevel(level, 'request completed', {\n ...buildBaseFields(req, traceId, options),\n statusCode: status,\n duration,\n });\n\n if (duration > slowRequestThresholdMs) {\n void logger.warn('slow request detected', {\n traceId,\n url: req.url,\n method: req.method,\n duration,\n threshold: slowRequestThresholdMs,\n });\n }\n\n hookDone();\n });\n\n done();\n }\n\n return Object.assign(logixiaFastifyPlugin, {\n [Symbol.for('skip-override')]: true,\n [Symbol.for('fastify.display-name')]: 'logixia',\n });\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\nfunction truncateBody(body: unknown, maxBytes = 4096): unknown {\n if (typeof body === 'string') {\n return body.length > maxBytes ? body.slice(0, maxBytes) + '…[truncated]' : body;\n }\n if (body && typeof body === 'object') {\n const str = stringifyForLog(body);\n if (str.length > maxBytes) {\n return str.slice(0, maxBytes) + '…[truncated]';\n }\n }\n return body;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"middleware.js","names":["out: Record<string, unknown>","internalError","fields: Record<string, unknown>","sanitizeIncomingId","stringifyForLog","items: unknown[]","json: unknown","record: Record<string, unknown>"],"sources":["../src/middleware/http-logger.ts"],"sourcesContent":["/**\n * HTTP request/response logging middleware — Morgan replacement.\n *\n * Fixes every documented Morgan bug:\n * - statusCode always captured correctly, even for requests > 20 s\n * - Logs request START (with traceId) and response FINISH (with duration)\n * - Captures errors before and after response\n * - Auto-redacts Authorization / Cookie / Set-Cookie headers\n * - Slow-request warnings\n * - Skip predicates for health-check routes / static assets\n *\n * @example Express\n * ```ts\n * import { createExpressMiddleware } from 'logixia/middleware';\n * app.use(createExpressMiddleware(logger));\n * ```\n *\n * @example Fastify\n * ```ts\n * import { createFastifyPlugin } from 'logixia/middleware';\n * await fastify.register(createFastifyPlugin(logger));\n * ```\n */\n\nimport type { IBaseLogger } from '../types';\nimport { internalError } from '../utils/internal-log';\nimport { stringifyForLog } from '../utils/safe-stringify';\nimport { sanitizeIncomingId } from '../utils/trace.utils';\n\n// ── Shared types ─────────────────────────────────────────────────────────────\n\nexport interface HttpLoggerOptions {\n /**\n * Skip logging for a request. Called before any I/O.\n * @example `skip: (req) => req.url === '/health'`\n */\n skip?: (req: IncomingRequest) => boolean;\n /**\n * Log request body (POST/PUT/PATCH). Capped at `bodyMaxBytes` (default: 4096).\n * Redaction still applies to the captured body.\n * Default: false.\n */\n logBody?: boolean;\n /** Max bytes of body to capture. Default: 4096. */\n bodyMaxBytes?: number;\n /**\n * Emit a WARN log when a request duration exceeds this threshold (ms).\n * Default: 1000.\n */\n slowRequestThresholdMs?: number;\n /**\n * Additional fields to include in every log entry.\n * @example `extraFields: (req) => ({ tenantId: req.headers['x-tenant-id'] })`\n */\n extraFields?: (req: IncomingRequest) => Record<string, unknown>;\n /**\n * Trace ID header. Default: 'x-trace-id'.\n * If the header is absent, a short random ID is generated.\n */\n traceIdHeader?: string;\n /**\n * Additional headers to redact from logged output. Merged with the defaults\n * (authorization, cookie, set-cookie, x-api-key, proxy-authorization,\n * x-auth-token, x-access-token, x-csrf-token, x-amz-security-token).\n */\n redactHeaders?: string[];\n /**\n * Additional query-string parameter names whose values are redacted from the\n * logged URL. Merged with the defaults (token, access_token, refresh_token,\n * code, password, secret, api_key, apikey, key, signature, sig, auth).\n */\n redactQueryParams?: string[];\n /**\n * Log level for request-start entries. Default: 'debug'.\n * Set to 'silent' to suppress request-start logs entirely.\n */\n requestLevel?: string;\n /** Log level for successful response entries. Default: 'info'. */\n responseLevel?: string;\n /** Log level for error responses (status ≥ 500). Default: 'error'. */\n errorLevel?: string;\n}\n\n// Minimal structural types so we don't need @types/express / fastify in core\nexport interface IncomingRequest {\n method?: string;\n url?: string;\n headers?: Record<string, string | string[] | undefined>;\n body?: unknown;\n socket?: { remoteAddress?: string };\n ip?: string;\n}\n\nexport interface OutgoingResponse {\n statusCode?: number;\n on?: (event: string, cb: () => void) => void;\n once?: (event: string, cb: () => void) => void;\n getHeader?: (name: string) => string | number | string[] | undefined;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst DEFAULT_REDACT_HEADERS = [\n 'authorization',\n 'cookie',\n 'set-cookie',\n 'x-api-key',\n 'proxy-authorization',\n 'x-auth-token',\n 'x-access-token',\n 'x-csrf-token',\n 'x-amz-security-token',\n];\n\nconst DEFAULT_REDACT_QUERY_PARAMS = [\n 'token',\n 'access_token',\n 'refresh_token',\n 'code',\n 'password',\n 'secret',\n 'api_key',\n 'apikey',\n 'key',\n 'signature',\n 'sig',\n 'auth',\n];\n\nfunction buildNameSet(defaults: readonly string[], extra: readonly string[] | undefined) {\n return new Set([...defaults, ...(extra ?? [])].map((n) => n.toLowerCase()));\n}\n\nfunction safeDecode(value: string): string {\n try {\n return decodeURIComponent(value.replace(/\\+/g, ' '));\n } catch {\n return value;\n }\n}\n\n/** Replace the values of sensitive query parameters, leaving the rest verbatim. */\nfunction sanitizeUrl(url: string, redactParams: Set<string>): string {\n const q = url.indexOf('?');\n if (q === -1) return url;\n const hashAt = url.indexOf('#', q);\n const query = url.slice(q + 1, hashAt === -1 ? undefined : hashAt);\n const hash = hashAt === -1 ? '' : url.slice(hashAt);\n const cleaned = query\n .split('&')\n .map((pair) => {\n const eq = pair.indexOf('=');\n const rawKey = eq === -1 ? pair : pair.slice(0, eq);\n const decoded = safeDecode(rawKey);\n const bracket = decoded.indexOf('[');\n const name = (bracket === -1 ? decoded : decoded.slice(0, bracket)).toLowerCase();\n return redactParams.has(name) && eq !== -1 ? `${rawKey}=[REDACTED]` : pair;\n })\n .join('&');\n return `${url.slice(0, q)}?${cleaned}${hash}`;\n}\n\nfunction sanitizeHeaders(\n headers: Record<string, string | string[] | undefined> | undefined,\n redactSet: Set<string>\n): Record<string, unknown> {\n if (!headers) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(headers)) {\n if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;\n out[k] = redactSet.has(k.toLowerCase()) ? '[REDACTED]' : v;\n }\n return out;\n}\n\n/** Fire-and-forget a logger call; a failing log must never become an unhandled rejection. */\nfunction fire(result: unknown): void {\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\n (result as Promise<unknown>).catch((error: unknown) =>\n internalError('http-logger: log call failed', error)\n );\n }\n}\n\nfunction shortId(): string {\n // eslint-disable-next-line sonarjs/pseudo-random -- non-security request ID\n return Math.random().toString(36).slice(2, 10);\n}\n\ninterface NameSets {\n headers: Set<string>;\n queryParams: Set<string>;\n}\n\nfunction resolveNameSets(options: HttpLoggerOptions): NameSets {\n return {\n headers: buildNameSet(DEFAULT_REDACT_HEADERS, options.redactHeaders),\n queryParams: buildNameSet(DEFAULT_REDACT_QUERY_PARAMS, options.redactQueryParams),\n };\n}\n\nfunction buildBaseFields(\n req: IncomingRequest,\n traceId: string,\n options: HttpLoggerOptions,\n { headers: redactSet, queryParams: paramSet }: NameSets\n): Record<string, unknown> {\n const fields: Record<string, unknown> = {\n traceId,\n method: req.method?.toUpperCase() ?? 'UNKNOWN',\n url: sanitizeUrl(req.url ?? '/', paramSet),\n ip: req.ip ?? req.socket?.remoteAddress ?? 'unknown',\n headers: sanitizeHeaders(req.headers, redactSet),\n };\n\n if (options.extraFields) {\n Object.assign(fields, options.extraFields(req));\n }\n\n return fields;\n}\n\n// ── Express middleware ────────────────────────────────────────────────────────\n\n/**\n * Create an Express / Connect compatible middleware that replaces Morgan.\n */\nexport function createExpressMiddleware(\n logger: IBaseLogger,\n options: HttpLoggerOptions = {}\n): (req: IncomingRequest, res: OutgoingResponse, next: () => void) => void {\n const {\n skip,\n logBody,\n bodyMaxBytes,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n const nameSets = resolveNameSets(options);\n const traceHeaderName = traceIdHeader.toLowerCase();\n\n return function logixiaHttpMiddleware(\n req: IncomingRequest,\n res: OutgoingResponse,\n next: () => void\n ): void {\n if (skip?.(req)) {\n next();\n return;\n }\n\n const traceId = sanitizeIncomingId(req.headers?.[traceHeaderName]) ?? shortId();\n const startMs = Date.now();\n const baseFields = buildBaseFields(req, traceId, options, nameSets);\n\n // Log request start\n if (requestLevel !== 'silent') {\n fire(\n logger.logLevel(requestLevel, 'request started', {\n ...baseFields,\n ...(logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}),\n })\n );\n }\n\n // Hook into the response 'finish' event — fires after headers + body are sent.\n // This is what Morgan gets wrong for slow requests (it uses 'close' which may\n // fire before the status code is set on some Node versions).\n //\n // BOTH 'finish' and 'close' fire on a normal response, so guard against\n // logging the completion twice (which would duplicate the completed entry and\n // the slow-request warning).\n let completed = false;\n const onFinish = (): void => {\n if (completed) return;\n completed = true;\n\n const duration = Date.now() - startMs;\n const status = res.statusCode ?? 0;\n const level = status >= 500 ? errorLevel : responseLevel;\n\n fire(\n logger.logLevel(level, 'request completed', {\n ...baseFields,\n statusCode: status,\n duration,\n })\n );\n\n if (duration > slowRequestThresholdMs) {\n fire(\n logger.warn('slow request detected', {\n ...baseFields,\n statusCode: status,\n duration,\n threshold: slowRequestThresholdMs,\n })\n );\n }\n };\n\n res.once?.('finish', onFinish);\n // Fallback: also covers a client that disconnects before 'finish' fires. The\n // `completed` guard ensures a normal finish+close pair logs only once.\n res.once?.('close', onFinish);\n\n next();\n };\n}\n\n// ── Fastify plugin ─────────────────────────────────────────────────────────────\n\nexport interface FastifyInstance {\n addHook: (name: string, fn: (req: unknown, reply: unknown, done: () => void) => void) => void;\n}\n\n/**\n * Create a Fastify plugin (a function you pass to `fastify.register()`).\n *\n * @example\n * ```ts\n * await fastify.register(createFastifyPlugin(logger, { slowRequestThresholdMs: 500 }));\n * ```\n */\nexport function createFastifyPlugin(logger: IBaseLogger, options: HttpLoggerOptions = {}) {\n const {\n skip,\n traceIdHeader = 'x-trace-id',\n requestLevel = 'debug',\n responseLevel = 'info',\n errorLevel = 'error',\n slowRequestThresholdMs = 1000,\n } = options;\n const nameSets = resolveNameSets(options);\n const traceHeaderName = traceIdHeader.toLowerCase();\n\n function logixiaFastifyPlugin(fastify: FastifyInstance, _opts: unknown, done: () => void): void {\n fastify.addHook('onRequest', (request: unknown, _reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n if (skip?.(req)) {\n hookDone();\n return;\n }\n\n const traceId = sanitizeIncomingId(req.headers?.[traceHeaderName]) ?? shortId();\n req._logixiaStart = Date.now();\n req._logixiaId = traceId;\n\n if (requestLevel !== 'silent') {\n fire(\n logger.logLevel(\n requestLevel,\n 'request started',\n buildBaseFields(req, traceId, options, nameSets)\n )\n );\n }\n hookDone();\n });\n\n fastify.addHook('onResponse', (request: unknown, reply: unknown, hookDone: () => void) => {\n const req = request as IncomingRequest & { _logixiaStart?: number; _logixiaId?: string };\n if (req._logixiaStart === undefined) {\n hookDone();\n return;\n }\n\n const rep = reply as { statusCode?: number };\n const duration = Date.now() - req._logixiaStart;\n const status = rep.statusCode ?? 0;\n const traceId = req._logixiaId ?? shortId();\n const level = status >= 500 ? errorLevel : responseLevel;\n const baseFields = buildBaseFields(req, traceId, options, nameSets);\n\n fire(\n logger.logLevel(level, 'request completed', {\n ...baseFields,\n statusCode: status,\n duration,\n })\n );\n\n if (duration > slowRequestThresholdMs) {\n fire(\n logger.warn('slow request detected', {\n traceId,\n url: baseFields.url,\n method: req.method,\n duration,\n threshold: slowRequestThresholdMs,\n })\n );\n }\n\n hookDone();\n });\n\n done();\n }\n\n return Object.assign(logixiaFastifyPlugin, {\n [Symbol.for('skip-override')]: true,\n [Symbol.for('fastify.display-name')]: 'logixia',\n });\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\n/**\n * Bound the captured body without turning it into a string, so the logger's\n * path-based redaction still sees the original keys. Oversized structures are\n * pruned (extra keys/items dropped, long strings cut) rather than serialized.\n */\nfunction truncateBody(body: unknown, maxBytes = 4096): unknown {\n if (typeof body === 'string') {\n return body.length > maxBytes ? body.slice(0, maxBytes) + '…[truncated]' : body;\n }\n if (body && typeof body === 'object') {\n const str = stringifyForLog(body);\n if (str.length <= maxBytes) return body;\n return pruneValue(body, { left: maxBytes }, 0, new Set());\n }\n return body;\n}\n\nconst TRUNCATED = '…[truncated]';\nconst MAX_PRUNE_DEPTH = 8;\n\nfunction pruneValue(\n value: unknown,\n budget: { left: number },\n depth: number,\n ancestors: Set<object>\n): unknown {\n if (typeof value === 'string') {\n const cut = value.slice(0, Math.max(0, budget.left));\n budget.left -= cut.length;\n return cut.length < value.length ? cut + TRUNCATED : cut;\n }\n if (value === null || typeof value !== 'object') {\n budget.left -= 8;\n return value;\n }\n if (value instanceof Date) {\n budget.left -= 24;\n return Number.isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString();\n }\n if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {\n budget.left -= 16;\n return `[Buffer ${value.byteLength} bytes]`;\n }\n if (value instanceof Error) return value;\n if (ancestors.has(value) || depth >= MAX_PRUNE_DEPTH) return TRUNCATED;\n ancestors.add(value);\n try {\n if (Array.isArray(value) || value instanceof Set || value instanceof Map) {\n const items: unknown[] = [];\n for (const item of value instanceof Map ? value.entries() : value) {\n if (budget.left <= 0) {\n items.push(TRUNCATED);\n break;\n }\n items.push(pruneValue(item, budget, depth + 1, ancestors));\n }\n return items;\n }\n const toJSON = (value as { toJSON?: unknown }).toJSON;\n if (typeof toJSON === 'function') {\n const json: unknown = toJSON.call(value);\n if (json !== value) return pruneValue(json, budget, depth + 1, ancestors);\n }\n const record: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;\n if (budget.left <= 0) {\n record['…'] = TRUNCATED;\n break;\n }\n budget.left -= k.length;\n record[k] = pruneValue(v, budget, depth + 1, ancestors);\n }\n return record;\n } catch {\n return TRUNCATED;\n } finally {\n ancestors.delete(value);\n }\n}\n"],"mappings":";;;;;AAsGA,MAAM,yBAAyB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,8BAA8B;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,SAAS,aAAa,UAA6B,OAAsC;AACvF,QAAO,IAAI,IAAI,CAAC,GAAG,UAAU,GAAI,SAAS,EAAE,CAAE,CAAC,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC;;AAG7E,SAAS,WAAW,OAAuB;AACzC,KAAI;AACF,SAAO,mBAAmB,MAAM,QAAQ,OAAO,IAAI,CAAC;SAC9C;AACN,SAAO;;;;AAKX,SAAS,YAAY,KAAa,cAAmC;CACnE,MAAM,IAAI,IAAI,QAAQ,IAAI;AAC1B,KAAI,MAAM,GAAI,QAAO;CACrB,MAAM,SAAS,IAAI,QAAQ,KAAK,EAAE;CAClC,MAAM,QAAQ,IAAI,MAAM,IAAI,GAAG,WAAW,KAAK,SAAY,OAAO;CAClE,MAAM,OAAO,WAAW,KAAK,KAAK,IAAI,MAAM,OAAO;CACnD,MAAM,UAAU,MACb,MAAM,IAAI,CACV,KAAK,SAAS;EACb,MAAM,KAAK,KAAK,QAAQ,IAAI;EAC5B,MAAM,SAAS,OAAO,KAAK,OAAO,KAAK,MAAM,GAAG,GAAG;EACnD,MAAM,UAAU,WAAW,OAAO;EAClC,MAAM,UAAU,QAAQ,QAAQ,IAAI;EACpC,MAAM,QAAQ,YAAY,KAAK,UAAU,QAAQ,MAAM,GAAG,QAAQ,EAAE,aAAa;AACjF,SAAO,aAAa,IAAI,KAAK,IAAI,OAAO,KAAK,GAAG,OAAO,eAAe;GACtE,CACD,KAAK,IAAI;AACZ,QAAO,GAAG,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,UAAU;;AAGzC,SAAS,gBACP,SACA,WACyB;AACzB,KAAI,CAAC,QAAS,QAAO,EAAE;CACvB,MAAMA,MAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,QAAQ,EAAE;AAC5C,MAAI,MAAM,eAAe,MAAM,iBAAiB,MAAM,YAAa;AACnE,MAAI,KAAK,UAAU,IAAI,EAAE,aAAa,CAAC,GAAG,eAAe;;AAE3D,QAAO;;;AAIT,SAAS,KAAK,QAAuB;AACnC,KAAI,UAAU,OAAQ,OAA4B,UAAU,WAC1D,CAAC,OAA4B,OAAO,UAClCC,mCAAc,gCAAgC,MAAM,CACrD;;AAIL,SAAS,UAAkB;AAEzB,QAAO,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,GAAG,GAAG;;AAQhD,SAAS,gBAAgB,SAAsC;AAC7D,QAAO;EACL,SAAS,aAAa,wBAAwB,QAAQ,cAAc;EACpE,aAAa,aAAa,6BAA6B,QAAQ,kBAAkB;EAClF;;AAGH,SAAS,gBACP,KACA,SACA,SACA,EAAE,SAAS,WAAW,aAAa,YACV;;CACzB,MAAMC,SAAkC;EACtC;EACA,wBAAQ,IAAI,kEAAQ,aAAa,KAAI;EACrC,KAAK,YAAY,IAAI,OAAO,KAAK,SAAS;EAC1C,IAAI,IAAI,sBAAM,IAAI,kEAAQ,kBAAiB;EAC3C,SAAS,gBAAgB,IAAI,SAAS,UAAU;EACjD;AAED,KAAI,QAAQ,YACV,QAAO,OAAO,QAAQ,QAAQ,YAAY,IAAI,CAAC;AAGjD,QAAO;;;;;AAQT,SAAgB,wBACd,QACA,UAA6B,EAAE,EAC0C;CACzE,MAAM,EACJ,MACA,SACA,cACA,gBAAgB,cAChB,eAAe,SACf,gBAAgB,QAChB,aAAa,SACb,yBAAyB,QACvB;CACJ,MAAM,WAAW,gBAAgB,QAAQ;CACzC,MAAM,kBAAkB,cAAc,aAAa;AAEnD,QAAO,SAAS,sBACd,KACA,KACA,MACM;;AACN,kDAAI,KAAO,IAAI,EAAE;AACf,SAAM;AACN;;EAGF,MAAM,UAAUC,uDAAmB,IAAI,qEAAU,iBAAiB,IAAI,SAAS;EAC/E,MAAM,UAAU,KAAK,KAAK;EAC1B,MAAM,aAAa,gBAAgB,KAAK,SAAS,SAAS,SAAS;AAGnE,MAAI,iBAAiB,SACnB,MACE,OAAO,SAAS,cAAc,mBAAmB;GAC/C,GAAG;GACH,GAAI,WAAW,IAAI,OAAO,EAAE,MAAM,aAAa,IAAI,MAAM,aAAa,EAAE,GAAG,EAAE;GAC9E,CAAC,CACH;EAUH,IAAI,YAAY;EAChB,MAAM,iBAAuB;AAC3B,OAAI,UAAW;AACf,eAAY;GAEZ,MAAM,WAAW,KAAK,KAAK,GAAG;GAC9B,MAAM,SAAS,IAAI,cAAc;GACjC,MAAM,QAAQ,UAAU,MAAM,aAAa;AAE3C,QACE,OAAO,SAAS,OAAO,qBAAqB;IAC1C,GAAG;IACH,YAAY;IACZ;IACD,CAAC,CACH;AAED,OAAI,WAAW,uBACb,MACE,OAAO,KAAK,yBAAyB;IACnC,GAAG;IACH,YAAY;IACZ;IACA,WAAW;IACZ,CAAC,CACH;;AAIL,mBAAI,8DAAO,UAAU,SAAS;AAG9B,oBAAI,gEAAO,SAAS,SAAS;AAE7B,QAAM;;;;;;;;;;;AAkBV,SAAgB,oBAAoB,QAAqB,UAA6B,EAAE,EAAE;CACxF,MAAM,EACJ,MACA,gBAAgB,cAChB,eAAe,SACf,gBAAgB,QAChB,aAAa,SACb,yBAAyB,QACvB;CACJ,MAAM,WAAW,gBAAgB,QAAQ;CACzC,MAAM,kBAAkB,cAAc,aAAa;CAEnD,SAAS,qBAAqB,SAA0B,OAAgB,MAAwB;AAC9F,UAAQ,QAAQ,cAAc,SAAkB,QAAiB,aAAyB;;GACxF,MAAM,MAAM;AACZ,mDAAI,KAAO,IAAI,EAAE;AACf,cAAU;AACV;;GAGF,MAAM,UAAUA,wDAAmB,IAAI,uEAAU,iBAAiB,IAAI,SAAS;AAC/E,OAAI,gBAAgB,KAAK,KAAK;AAC9B,OAAI,aAAa;AAEjB,OAAI,iBAAiB,SACnB,MACE,OAAO,SACL,cACA,mBACA,gBAAgB,KAAK,SAAS,SAAS,SAAS,CACjD,CACF;AAEH,aAAU;IACV;AAEF,UAAQ,QAAQ,eAAe,SAAkB,OAAgB,aAAyB;GACxF,MAAM,MAAM;AACZ,OAAI,IAAI,kBAAkB,QAAW;AACnC,cAAU;AACV;;GAGF,MAAM,MAAM;GACZ,MAAM,WAAW,KAAK,KAAK,GAAG,IAAI;GAClC,MAAM,SAAS,IAAI,cAAc;GACjC,MAAM,UAAU,IAAI,cAAc,SAAS;GAC3C,MAAM,QAAQ,UAAU,MAAM,aAAa;GAC3C,MAAM,aAAa,gBAAgB,KAAK,SAAS,SAAS,SAAS;AAEnE,QACE,OAAO,SAAS,OAAO,qBAAqB;IAC1C,GAAG;IACH,YAAY;IACZ;IACD,CAAC,CACH;AAED,OAAI,WAAW,uBACb,MACE,OAAO,KAAK,yBAAyB;IACnC;IACA,KAAK,WAAW;IAChB,QAAQ,IAAI;IACZ;IACA,WAAW;IACZ,CAAC,CACH;AAGH,aAAU;IACV;AAEF,QAAM;;AAGR,QAAO,OAAO,OAAO,sBAAsB;GACxC,OAAO,IAAI,gBAAgB,GAAG;GAC9B,OAAO,IAAI,uBAAuB,GAAG;EACvC,CAAC;;;;;;;AAUJ,SAAS,aAAa,MAAe,WAAW,MAAe;AAC7D,KAAI,OAAO,SAAS,SAClB,QAAO,KAAK,SAAS,WAAW,KAAK,MAAM,GAAG,SAAS,GAAG,iBAAiB;AAE7E,KAAI,QAAQ,OAAO,SAAS,UAAU;AAEpC,MADYC,uCAAgB,KAAK,CACzB,UAAU,SAAU,QAAO;AACnC,SAAO,WAAW,MAAM,EAAE,MAAM,UAAU,EAAE,mBAAG,IAAI,KAAK,CAAC;;AAE3D,QAAO;;AAGT,MAAM,YAAY;AAClB,MAAM,kBAAkB;AAExB,SAAS,WACP,OACA,QACA,OACA,WACS;AACT,KAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,MAAM,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,OAAO,KAAK,CAAC;AACpD,SAAO,QAAQ,IAAI;AACnB,SAAO,IAAI,SAAS,MAAM,SAAS,MAAM,YAAY;;AAEvD,KAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,SAAO,QAAQ;AACf,SAAO;;AAET,KAAI,iBAAiB,MAAM;AACzB,SAAO,QAAQ;AACf,SAAO,OAAO,MAAM,MAAM,SAAS,CAAC,GAAG,iBAAiB,MAAM,aAAa;;AAE7E,KAAI,iBAAiB,eAAe,YAAY,OAAO,MAAM,EAAE;AAC7D,SAAO,QAAQ;AACf,SAAO,WAAW,MAAM,WAAW;;AAErC,KAAI,iBAAiB,MAAO,QAAO;AACnC,KAAI,UAAU,IAAI,MAAM,IAAI,SAAS,gBAAiB,QAAO;AAC7D,WAAU,IAAI,MAAM;AACpB,KAAI;AACF,MAAI,MAAM,QAAQ,MAAM,IAAI,iBAAiB,OAAO,iBAAiB,KAAK;GACxE,MAAMC,QAAmB,EAAE;AAC3B,QAAK,MAAM,QAAQ,iBAAiB,MAAM,MAAM,SAAS,GAAG,OAAO;AACjE,QAAI,OAAO,QAAQ,GAAG;AACpB,WAAM,KAAK,UAAU;AACrB;;AAEF,UAAM,KAAK,WAAW,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC;;AAE5D,UAAO;;EAET,MAAM,SAAU,MAA+B;AAC/C,MAAI,OAAO,WAAW,YAAY;GAChC,MAAMC,OAAgB,OAAO,KAAK,MAAM;AACxC,OAAI,SAAS,MAAO,QAAO,WAAW,MAAM,QAAQ,QAAQ,GAAG,UAAU;;EAE3E,MAAMC,SAAkC,EAAE;AAC1C,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,EAAE;AAC1C,OAAI,MAAM,eAAe,MAAM,iBAAiB,MAAM,YAAa;AACnE,OAAI,OAAO,QAAQ,GAAG;AACpB,WAAO,OAAO;AACd;;AAEF,UAAO,QAAQ,EAAE;AACjB,UAAO,KAAK,WAAW,GAAG,QAAQ,QAAQ,GAAG,UAAU;;AAEzD,SAAO;SACD;AACN,SAAO;WACC;AACR,YAAU,OAAO,MAAM"}
|
package/dist/middleware.mjs
CHANGED
|
@@ -1,12 +1,60 @@
|
|
|
1
|
+
import { t as internalError } from "./internal-log-Cm8SOWSV.mjs";
|
|
1
2
|
import { i as stringifyForLog } from "./safe-stringify-DJ66tW5I.mjs";
|
|
3
|
+
import { d as sanitizeIncomingId } from "./trace.utils-CvXgaiQT.mjs";
|
|
2
4
|
|
|
3
5
|
//#region src/middleware/http-logger.ts
|
|
4
|
-
const DEFAULT_REDACT_HEADERS =
|
|
6
|
+
const DEFAULT_REDACT_HEADERS = [
|
|
5
7
|
"authorization",
|
|
6
8
|
"cookie",
|
|
7
9
|
"set-cookie",
|
|
8
|
-
"x-api-key"
|
|
9
|
-
|
|
10
|
+
"x-api-key",
|
|
11
|
+
"proxy-authorization",
|
|
12
|
+
"x-auth-token",
|
|
13
|
+
"x-access-token",
|
|
14
|
+
"x-csrf-token",
|
|
15
|
+
"x-amz-security-token"
|
|
16
|
+
];
|
|
17
|
+
const DEFAULT_REDACT_QUERY_PARAMS = [
|
|
18
|
+
"token",
|
|
19
|
+
"access_token",
|
|
20
|
+
"refresh_token",
|
|
21
|
+
"code",
|
|
22
|
+
"password",
|
|
23
|
+
"secret",
|
|
24
|
+
"api_key",
|
|
25
|
+
"apikey",
|
|
26
|
+
"key",
|
|
27
|
+
"signature",
|
|
28
|
+
"sig",
|
|
29
|
+
"auth"
|
|
30
|
+
];
|
|
31
|
+
function buildNameSet(defaults, extra) {
|
|
32
|
+
return new Set([...defaults, ...extra ?? []].map((n) => n.toLowerCase()));
|
|
33
|
+
}
|
|
34
|
+
function safeDecode(value) {
|
|
35
|
+
try {
|
|
36
|
+
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
37
|
+
} catch {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Replace the values of sensitive query parameters, leaving the rest verbatim. */
|
|
42
|
+
function sanitizeUrl(url, redactParams) {
|
|
43
|
+
const q = url.indexOf("?");
|
|
44
|
+
if (q === -1) return url;
|
|
45
|
+
const hashAt = url.indexOf("#", q);
|
|
46
|
+
const query = url.slice(q + 1, hashAt === -1 ? void 0 : hashAt);
|
|
47
|
+
const hash = hashAt === -1 ? "" : url.slice(hashAt);
|
|
48
|
+
const cleaned = query.split("&").map((pair) => {
|
|
49
|
+
const eq = pair.indexOf("=");
|
|
50
|
+
const rawKey = eq === -1 ? pair : pair.slice(0, eq);
|
|
51
|
+
const decoded = safeDecode(rawKey);
|
|
52
|
+
const bracket = decoded.indexOf("[");
|
|
53
|
+
const name = (bracket === -1 ? decoded : decoded.slice(0, bracket)).toLowerCase();
|
|
54
|
+
return redactParams.has(name) && eq !== -1 ? `${rawKey}=[REDACTED]` : pair;
|
|
55
|
+
}).join("&");
|
|
56
|
+
return `${url.slice(0, q)}?${cleaned}${hash}`;
|
|
57
|
+
}
|
|
10
58
|
function sanitizeHeaders(headers, redactSet) {
|
|
11
59
|
if (!headers) return {};
|
|
12
60
|
const out = {};
|
|
@@ -16,16 +64,25 @@ function sanitizeHeaders(headers, redactSet) {
|
|
|
16
64
|
}
|
|
17
65
|
return out;
|
|
18
66
|
}
|
|
67
|
+
/** Fire-and-forget a logger call; a failing log must never become an unhandled rejection. */
|
|
68
|
+
function fire(result) {
|
|
69
|
+
if (result && typeof result.catch === "function") result.catch((error) => internalError("http-logger: log call failed", error));
|
|
70
|
+
}
|
|
19
71
|
function shortId() {
|
|
20
72
|
return Math.random().toString(36).slice(2, 10);
|
|
21
73
|
}
|
|
22
|
-
function
|
|
74
|
+
function resolveNameSets(options) {
|
|
75
|
+
return {
|
|
76
|
+
headers: buildNameSet(DEFAULT_REDACT_HEADERS, options.redactHeaders),
|
|
77
|
+
queryParams: buildNameSet(DEFAULT_REDACT_QUERY_PARAMS, options.redactQueryParams)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function buildBaseFields(req, traceId, options, { headers: redactSet, queryParams: paramSet }) {
|
|
23
81
|
var _req$method, _req$socket;
|
|
24
|
-
const redactSet = options.redactHeaders ? new Set(options.redactHeaders.map((h) => h.toLowerCase())) : DEFAULT_REDACT_HEADERS;
|
|
25
82
|
const fields = {
|
|
26
83
|
traceId,
|
|
27
84
|
method: ((_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase()) ?? "UNKNOWN",
|
|
28
|
-
url: req.url ?? "/",
|
|
85
|
+
url: sanitizeUrl(req.url ?? "/", paramSet),
|
|
29
86
|
ip: req.ip ?? ((_req$socket = req.socket) === null || _req$socket === void 0 ? void 0 : _req$socket.remoteAddress) ?? "unknown",
|
|
30
87
|
headers: sanitizeHeaders(req.headers, redactSet)
|
|
31
88
|
};
|
|
@@ -37,19 +94,21 @@ function buildBaseFields(req, traceId, options) {
|
|
|
37
94
|
*/
|
|
38
95
|
function createExpressMiddleware(logger, options = {}) {
|
|
39
96
|
const { skip, logBody, bodyMaxBytes, traceIdHeader = "x-trace-id", requestLevel = "debug", responseLevel = "info", errorLevel = "error", slowRequestThresholdMs = 1e3 } = options;
|
|
97
|
+
const nameSets = resolveNameSets(options);
|
|
98
|
+
const traceHeaderName = traceIdHeader.toLowerCase();
|
|
40
99
|
return function logixiaHttpMiddleware(req, res, next) {
|
|
41
100
|
var _req$headers, _res$once, _res$once2;
|
|
42
101
|
if (skip === null || skip === void 0 ? void 0 : skip(req)) {
|
|
43
102
|
next();
|
|
44
103
|
return;
|
|
45
104
|
}
|
|
46
|
-
const traceId = ((_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : _req$headers[
|
|
105
|
+
const traceId = sanitizeIncomingId((_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : _req$headers[traceHeaderName]) ?? shortId();
|
|
47
106
|
const startMs = Date.now();
|
|
48
|
-
const baseFields = buildBaseFields(req, traceId, options);
|
|
49
|
-
if (requestLevel !== "silent") logger.logLevel(requestLevel, "request started", {
|
|
107
|
+
const baseFields = buildBaseFields(req, traceId, options, nameSets);
|
|
108
|
+
if (requestLevel !== "silent") fire(logger.logLevel(requestLevel, "request started", {
|
|
50
109
|
...baseFields,
|
|
51
110
|
...logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}
|
|
52
|
-
});
|
|
111
|
+
}));
|
|
53
112
|
let completed = false;
|
|
54
113
|
const onFinish = () => {
|
|
55
114
|
if (completed) return;
|
|
@@ -57,17 +116,17 @@ function createExpressMiddleware(logger, options = {}) {
|
|
|
57
116
|
const duration = Date.now() - startMs;
|
|
58
117
|
const status = res.statusCode ?? 0;
|
|
59
118
|
const level = status >= 500 ? errorLevel : responseLevel;
|
|
60
|
-
logger.logLevel(level, "request completed", {
|
|
119
|
+
fire(logger.logLevel(level, "request completed", {
|
|
61
120
|
...baseFields,
|
|
62
121
|
statusCode: status,
|
|
63
122
|
duration
|
|
64
|
-
});
|
|
65
|
-
if (duration > slowRequestThresholdMs) logger.warn("slow request detected", {
|
|
123
|
+
}));
|
|
124
|
+
if (duration > slowRequestThresholdMs) fire(logger.warn("slow request detected", {
|
|
66
125
|
...baseFields,
|
|
67
126
|
statusCode: status,
|
|
68
127
|
duration,
|
|
69
128
|
threshold: slowRequestThresholdMs
|
|
70
|
-
});
|
|
129
|
+
}));
|
|
71
130
|
};
|
|
72
131
|
(_res$once = res.once) === null || _res$once === void 0 || _res$once.call(res, "finish", onFinish);
|
|
73
132
|
(_res$once2 = res.once) === null || _res$once2 === void 0 || _res$once2.call(res, "close", onFinish);
|
|
@@ -84,6 +143,8 @@ function createExpressMiddleware(logger, options = {}) {
|
|
|
84
143
|
*/
|
|
85
144
|
function createFastifyPlugin(logger, options = {}) {
|
|
86
145
|
const { skip, traceIdHeader = "x-trace-id", requestLevel = "debug", responseLevel = "info", errorLevel = "error", slowRequestThresholdMs = 1e3 } = options;
|
|
146
|
+
const nameSets = resolveNameSets(options);
|
|
147
|
+
const traceHeaderName = traceIdHeader.toLowerCase();
|
|
87
148
|
function logixiaFastifyPlugin(fastify, _opts, done) {
|
|
88
149
|
fastify.addHook("onRequest", (request, _reply, hookDone) => {
|
|
89
150
|
var _req$headers2;
|
|
@@ -92,10 +153,10 @@ function createFastifyPlugin(logger, options = {}) {
|
|
|
92
153
|
hookDone();
|
|
93
154
|
return;
|
|
94
155
|
}
|
|
95
|
-
const traceId = ((_req$headers2 = req.headers) === null || _req$headers2 === void 0 ? void 0 : _req$headers2[
|
|
156
|
+
const traceId = sanitizeIncomingId((_req$headers2 = req.headers) === null || _req$headers2 === void 0 ? void 0 : _req$headers2[traceHeaderName]) ?? shortId();
|
|
96
157
|
req._logixiaStart = Date.now();
|
|
97
158
|
req._logixiaId = traceId;
|
|
98
|
-
if (requestLevel !== "silent") logger.logLevel(requestLevel, "request started", buildBaseFields(req, traceId, options));
|
|
159
|
+
if (requestLevel !== "silent") fire(logger.logLevel(requestLevel, "request started", buildBaseFields(req, traceId, options, nameSets)));
|
|
99
160
|
hookDone();
|
|
100
161
|
});
|
|
101
162
|
fastify.addHook("onResponse", (request, reply, hookDone) => {
|
|
@@ -109,18 +170,19 @@ function createFastifyPlugin(logger, options = {}) {
|
|
|
109
170
|
const status = rep.statusCode ?? 0;
|
|
110
171
|
const traceId = req._logixiaId ?? shortId();
|
|
111
172
|
const level = status >= 500 ? errorLevel : responseLevel;
|
|
112
|
-
|
|
113
|
-
|
|
173
|
+
const baseFields = buildBaseFields(req, traceId, options, nameSets);
|
|
174
|
+
fire(logger.logLevel(level, "request completed", {
|
|
175
|
+
...baseFields,
|
|
114
176
|
statusCode: status,
|
|
115
177
|
duration
|
|
116
|
-
});
|
|
117
|
-
if (duration > slowRequestThresholdMs) logger.warn("slow request detected", {
|
|
178
|
+
}));
|
|
179
|
+
if (duration > slowRequestThresholdMs) fire(logger.warn("slow request detected", {
|
|
118
180
|
traceId,
|
|
119
|
-
url:
|
|
181
|
+
url: baseFields.url,
|
|
120
182
|
method: req.method,
|
|
121
183
|
duration,
|
|
122
184
|
threshold: slowRequestThresholdMs
|
|
123
|
-
});
|
|
185
|
+
}));
|
|
124
186
|
hookDone();
|
|
125
187
|
});
|
|
126
188
|
done();
|
|
@@ -130,14 +192,76 @@ function createFastifyPlugin(logger, options = {}) {
|
|
|
130
192
|
[Symbol.for("fastify.display-name")]: "logixia"
|
|
131
193
|
});
|
|
132
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Bound the captured body without turning it into a string, so the logger's
|
|
197
|
+
* path-based redaction still sees the original keys. Oversized structures are
|
|
198
|
+
* pruned (extra keys/items dropped, long strings cut) rather than serialized.
|
|
199
|
+
*/
|
|
133
200
|
function truncateBody(body, maxBytes = 4096) {
|
|
134
201
|
if (typeof body === "string") return body.length > maxBytes ? body.slice(0, maxBytes) + "…[truncated]" : body;
|
|
135
202
|
if (body && typeof body === "object") {
|
|
136
|
-
|
|
137
|
-
|
|
203
|
+
if (stringifyForLog(body).length <= maxBytes) return body;
|
|
204
|
+
return pruneValue(body, { left: maxBytes }, 0, /* @__PURE__ */ new Set());
|
|
138
205
|
}
|
|
139
206
|
return body;
|
|
140
207
|
}
|
|
208
|
+
const TRUNCATED = "…[truncated]";
|
|
209
|
+
const MAX_PRUNE_DEPTH = 8;
|
|
210
|
+
function pruneValue(value, budget, depth, ancestors) {
|
|
211
|
+
if (typeof value === "string") {
|
|
212
|
+
const cut = value.slice(0, Math.max(0, budget.left));
|
|
213
|
+
budget.left -= cut.length;
|
|
214
|
+
return cut.length < value.length ? cut + TRUNCATED : cut;
|
|
215
|
+
}
|
|
216
|
+
if (value === null || typeof value !== "object") {
|
|
217
|
+
budget.left -= 8;
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
if (value instanceof Date) {
|
|
221
|
+
budget.left -= 24;
|
|
222
|
+
return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
223
|
+
}
|
|
224
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
225
|
+
budget.left -= 16;
|
|
226
|
+
return `[Buffer ${value.byteLength} bytes]`;
|
|
227
|
+
}
|
|
228
|
+
if (value instanceof Error) return value;
|
|
229
|
+
if (ancestors.has(value) || depth >= MAX_PRUNE_DEPTH) return TRUNCATED;
|
|
230
|
+
ancestors.add(value);
|
|
231
|
+
try {
|
|
232
|
+
if (Array.isArray(value) || value instanceof Set || value instanceof Map) {
|
|
233
|
+
const items = [];
|
|
234
|
+
for (const item of value instanceof Map ? value.entries() : value) {
|
|
235
|
+
if (budget.left <= 0) {
|
|
236
|
+
items.push(TRUNCATED);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
items.push(pruneValue(item, budget, depth + 1, ancestors));
|
|
240
|
+
}
|
|
241
|
+
return items;
|
|
242
|
+
}
|
|
243
|
+
const toJSON = value.toJSON;
|
|
244
|
+
if (typeof toJSON === "function") {
|
|
245
|
+
const json = toJSON.call(value);
|
|
246
|
+
if (json !== value) return pruneValue(json, budget, depth + 1, ancestors);
|
|
247
|
+
}
|
|
248
|
+
const record = {};
|
|
249
|
+
for (const [k, v] of Object.entries(value)) {
|
|
250
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
|
|
251
|
+
if (budget.left <= 0) {
|
|
252
|
+
record["…"] = TRUNCATED;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
budget.left -= k.length;
|
|
256
|
+
record[k] = pruneValue(v, budget, depth + 1, ancestors);
|
|
257
|
+
}
|
|
258
|
+
return record;
|
|
259
|
+
} catch {
|
|
260
|
+
return TRUNCATED;
|
|
261
|
+
} finally {
|
|
262
|
+
ancestors.delete(value);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
141
265
|
|
|
142
266
|
//#endregion
|
|
143
267
|
export { createExpressMiddleware, createFastifyPlugin };
|