logixia 1.10.2 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/{index-Co47qPnq.d.mts → index-CSFeEGLb.d.ts} +32 -2
- package/dist/index-CSFeEGLb.d.ts.map +1 -0
- package/dist/{index-F-A7hg1u.d.ts → index-Cw-sN_0_.d.mts} +32 -2
- package/dist/index-Cw-sN_0_.d.mts.map +1 -0
- package/dist/index.d.mts +256 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +256 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +569 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +557 -34
- package/dist/index.mjs.map +1 -1
- package/dist/{logitron-logger.module-BLT1y5Iq.d.ts → logitron-logger.module-DGwNfjBX.d.mts} +21 -2
- package/dist/logitron-logger.module-DGwNfjBX.d.mts.map +1 -0
- package/dist/{logitron-logger.module-bJ1hGhaL.js → logitron-logger.module-DHFampon.js} +186 -38
- package/dist/logitron-logger.module-DHFampon.js.map +1 -0
- package/dist/{logitron-logger.module-B8NklSC4.d.mts → logitron-logger.module-DfyBsT_K.d.ts} +21 -2
- package/dist/logitron-logger.module-DfyBsT_K.d.ts.map +1 -0
- package/dist/{logitron-logger.module-Bt_Jei1V.mjs → logitron-logger.module-QYBy_Kkq.mjs} +186 -38
- package/dist/logitron-logger.module-QYBy_Kkq.mjs.map +1 -0
- package/dist/middleware.d.mts +1 -1
- package/dist/middleware.d.mts.map +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +4 -3
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +4 -3
- package/dist/middleware.mjs.map +1 -1
- package/dist/nest.d.mts +2 -2
- package/dist/nest.d.mts.map +1 -1
- package/dist/nest.d.ts +2 -2
- package/dist/nest.d.ts.map +1 -1
- package/dist/nest.js +2 -2
- package/dist/nest.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{transport.manager-zgEZCJhR.js → transport.manager-B9LF9uDd.js} +130 -56
- package/dist/transport.manager-B9LF9uDd.js.map +1 -0
- package/dist/{transport.manager-CaL4XuLD.mjs → transport.manager-Cij_sA-b.mjs} +128 -56
- package/dist/transport.manager-Cij_sA-b.mjs.map +1 -0
- package/dist/transports.d.mts +42 -3
- package/dist/transports.d.mts.map +1 -1
- package/dist/transports.d.ts +42 -3
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +1 -1
- package/dist/transports.mjs +1 -1
- package/package.json +1 -1
- package/dist/index-Co47qPnq.d.mts.map +0 -1
- package/dist/index-F-A7hg1u.d.ts.map +0 -1
- package/dist/logitron-logger.module-B8NklSC4.d.mts.map +0 -1
- package/dist/logitron-logger.module-BLT1y5Iq.d.ts.map +0 -1
- package/dist/logitron-logger.module-Bt_Jei1V.mjs.map +0 -1
- package/dist/logitron-logger.module-bJ1hGhaL.js.map +0 -1
- package/dist/transport.manager-CaL4XuLD.mjs.map +0 -1
- package/dist/transport.manager-zgEZCJhR.js.map +0 -1
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","names":["out: Record<string, unknown>","fields: 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\n/* eslint-disable sonarjs/void-use -- intentional fire-and-forget in sync middleware callbacks */\nimport type { IBaseLogger } from '../types';\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 const onFinish = (): void => {\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 listen to 'close' (client disconnected before response finished)\n res.once?.('close', () => {\n if ((res.statusCode ?? 0) === 0) onFinish();\n });\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 return function logixiaFastifyPlugin(\n fastify: FastifyInstance,\n _opts: unknown,\n done: () => void\n ): 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 const rep = reply as { statusCode?: number };\n const duration = Date.now() - (req._logixiaStart ?? Date.now());\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\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 = JSON.stringify(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>","fields: 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\n/* eslint-disable sonarjs/void-use -- intentional fire-and-forget in sync middleware callbacks */\nimport type { IBaseLogger } from '../types';\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 return function logixiaFastifyPlugin(\n fastify: FastifyInstance,\n _opts: unknown,\n done: () => void\n ): 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 const rep = reply as { statusCode?: number };\n const duration = Date.now() - (req._logixiaStart ?? Date.now());\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\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 = JSON.stringify(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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"}
|
package/dist/middleware.mjs
CHANGED
|
@@ -48,7 +48,10 @@ function createExpressMiddleware(logger, options = {}) {
|
|
|
48
48
|
...baseFields,
|
|
49
49
|
...logBody && req.body ? { body: truncateBody(req.body, bodyMaxBytes) } : {}
|
|
50
50
|
});
|
|
51
|
+
let completed = false;
|
|
51
52
|
const onFinish = () => {
|
|
53
|
+
if (completed) return;
|
|
54
|
+
completed = true;
|
|
52
55
|
const duration = Date.now() - startMs;
|
|
53
56
|
const status = res.statusCode ?? 0;
|
|
54
57
|
const level = status >= 500 ? errorLevel : responseLevel;
|
|
@@ -65,9 +68,7 @@ function createExpressMiddleware(logger, options = {}) {
|
|
|
65
68
|
});
|
|
66
69
|
};
|
|
67
70
|
(_res$once = res.once) === null || _res$once === void 0 || _res$once.call(res, "finish", onFinish);
|
|
68
|
-
(_res$once2 = res.once) === null || _res$once2 === void 0 || _res$once2.call(res, "close",
|
|
69
|
-
if ((res.statusCode ?? 0) === 0) onFinish();
|
|
70
|
-
});
|
|
71
|
+
(_res$once2 = res.once) === null || _res$once2 === void 0 || _res$once2.call(res, "close", onFinish);
|
|
71
72
|
next();
|
|
72
73
|
};
|
|
73
74
|
}
|
package/dist/middleware.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.mjs","names":["out: Record<string, unknown>","fields: 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\n/* eslint-disable sonarjs/void-use -- intentional fire-and-forget in sync middleware callbacks */\nimport type { IBaseLogger } from '../types';\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 const onFinish = (): void => {\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 listen to 'close' (client disconnected before response finished)\n res.once?.('close', () => {\n if ((res.statusCode ?? 0) === 0) onFinish();\n });\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 return function logixiaFastifyPlugin(\n fastify: FastifyInstance,\n _opts: unknown,\n done: () => void\n ): 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 const rep = reply as { statusCode?: number };\n const duration = Date.now() - (req._logixiaStart ?? Date.now());\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\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 = JSON.stringify(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.mjs","names":["out: Record<string, unknown>","fields: 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\n/* eslint-disable sonarjs/void-use -- intentional fire-and-forget in sync middleware callbacks */\nimport type { IBaseLogger } from '../types';\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 return function logixiaFastifyPlugin(\n fastify: FastifyInstance,\n _opts: unknown,\n done: () => void\n ): 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 const rep = reply as { statusCode?: number };\n const duration = Date.now() - (req._logixiaStart ?? Date.now());\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\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 = JSON.stringify(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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"}
|
package/dist/nest.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as LogixiaOptionsFactory, c as LogixiaLoggerService, i as LogixiaLoggerModule, l as LogixiaServiceWith, n as LOGIXIA_LOGGER_PREFIX, o as WebSocketTraceInterceptor, r as LogixiaAsyncOptions, s as KafkaTraceInterceptor, t as LOGIXIA_LOGGER_CONFIG, u as LogixiaServiceWithLevels } from "./logitron-logger.module-
|
|
1
|
+
import { E as TraceIdConfig } from "./index-Cw-sN_0_.mjs";
|
|
2
|
+
import { a as LogixiaOptionsFactory, c as LogixiaLoggerService, i as LogixiaLoggerModule, l as LogixiaServiceWith, n as LOGIXIA_LOGGER_PREFIX, o as WebSocketTraceInterceptor, r as LogixiaAsyncOptions, s as KafkaTraceInterceptor, t as LOGIXIA_LOGGER_CONFIG, u as LogixiaServiceWithLevels } from "./logitron-logger.module-DGwNfjBX.mjs";
|
|
3
3
|
import { NestMiddleware } from "@nestjs/common";
|
|
4
4
|
import { NextFunction, Request, Response } from "express";
|
|
5
5
|
|
package/dist/nest.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nest.d.mts","names":[],"sources":["../src/core/trace.middleware.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"nest.d.mts","names":[],"sources":["../src/core/trace.middleware.ts"],"sourcesContent":[],"mappings":";;;;;;;QAkFyC,MAAA,CAAA;EAxBD,UAAA,OAAA,CAAA;IAAc,UAAA,OAAA,CAAA;;;;;cAAzC,eAAA,YAA2B;;;uBAGY;WAqBzC,cAAc,gBAAgB"}
|
package/dist/nest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as LogixiaOptionsFactory, c as LogixiaLoggerService, i as LogixiaLoggerModule, l as LogixiaServiceWith, n as LOGIXIA_LOGGER_PREFIX, o as WebSocketTraceInterceptor, r as LogixiaAsyncOptions, s as KafkaTraceInterceptor, t as LOGIXIA_LOGGER_CONFIG, u as LogixiaServiceWithLevels } from "./logitron-logger.module-
|
|
1
|
+
import { E as TraceIdConfig } from "./index-CSFeEGLb.js";
|
|
2
|
+
import { a as LogixiaOptionsFactory, c as LogixiaLoggerService, i as LogixiaLoggerModule, l as LogixiaServiceWith, n as LOGIXIA_LOGGER_PREFIX, o as WebSocketTraceInterceptor, r as LogixiaAsyncOptions, s as KafkaTraceInterceptor, t as LOGIXIA_LOGGER_CONFIG, u as LogixiaServiceWithLevels } from "./logitron-logger.module-DfyBsT_K.js";
|
|
3
3
|
import { NestMiddleware } from "@nestjs/common";
|
|
4
4
|
import { NextFunction, Request, Response } from "express";
|
|
5
5
|
|
package/dist/nest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nest.d.ts","names":[],"sources":["../src/core/trace.middleware.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"nest.d.ts","names":[],"sources":["../src/core/trace.middleware.ts"],"sourcesContent":[],"mappings":";;;;;;;QAkFyC,MAAA,CAAA;EAxBD,UAAA,OAAA,CAAA;IAAc,UAAA,OAAA,CAAA;;;;;cAAzC,eAAA,YAA2B;;;uBAGY;WAqBzC,cAAc,gBAAgB"}
|
package/dist/nest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require('./transport.manager-
|
|
2
|
-
const require_logitron_logger_module = require('./logitron-logger.module-
|
|
1
|
+
require('./transport.manager-B9LF9uDd.js');
|
|
2
|
+
const require_logitron_logger_module = require('./logitron-logger.module-DHFampon.js');
|
|
3
3
|
|
|
4
4
|
Object.defineProperty(exports, 'KafkaTraceInterceptor', {
|
|
5
5
|
enumerable: true,
|
package/dist/nest.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./transport.manager-
|
|
2
|
-
import { a as TraceMiddleware, c as KafkaTraceInterceptor, i as WebSocketTraceInterceptor, l as LogixiaLoggerService, n as LOGIXIA_LOGGER_PREFIX, r as LogixiaLoggerModule, t as LOGIXIA_LOGGER_CONFIG } from "./logitron-logger.module-
|
|
1
|
+
import "./transport.manager-Cij_sA-b.mjs";
|
|
2
|
+
import { a as TraceMiddleware, c as KafkaTraceInterceptor, i as WebSocketTraceInterceptor, l as LogixiaLoggerService, n as LOGIXIA_LOGGER_PREFIX, r as LogixiaLoggerModule, t as LOGIXIA_LOGGER_CONFIG } from "./logitron-logger.module-QYBy_Kkq.mjs";
|
|
3
3
|
|
|
4
4
|
export { KafkaTraceInterceptor, LOGIXIA_LOGGER_CONFIG, LOGIXIA_LOGGER_PREFIX, LogixiaLoggerModule, LogixiaLoggerService, TraceMiddleware, WebSocketTraceInterceptor };
|
package/dist/testing.d.mts
CHANGED
package/dist/testing.d.ts
CHANGED
|
@@ -29,8 +29,12 @@ let node_fs = require("node:fs");
|
|
|
29
29
|
node_fs = __toESM(node_fs);
|
|
30
30
|
let node_path = require("node:path");
|
|
31
31
|
node_path = __toESM(node_path);
|
|
32
|
+
let node_stream_promises = require("node:stream/promises");
|
|
33
|
+
node_stream_promises = __toESM(node_stream_promises);
|
|
32
34
|
let node_util = require("node:util");
|
|
33
35
|
node_util = __toESM(node_util);
|
|
36
|
+
let node_zlib = require("node:zlib");
|
|
37
|
+
node_zlib = __toESM(node_zlib);
|
|
34
38
|
|
|
35
39
|
//#region src/utils/internal-log.ts
|
|
36
40
|
/**
|
|
@@ -44,28 +48,36 @@ node_util = __toESM(node_util);
|
|
|
44
48
|
* helpers instead so that internal output can be silenced in tests by setting
|
|
45
49
|
* the LOGIXIA_SILENT_INTERNAL=1 environment variable.
|
|
46
50
|
*/
|
|
47
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Read the silence flag on each call rather than caching it at import time, so
|
|
53
|
+
* setting LOGIXIA_SILENT_INTERNAL=1 AFTER the module is first imported (e.g. in
|
|
54
|
+
* a test setup file) still takes effect — the documented test-silencing
|
|
55
|
+
* behavior was previously unreliable because the value was frozen at load.
|
|
56
|
+
*/
|
|
57
|
+
function isSilent() {
|
|
58
|
+
return process.env.LOGIXIA_SILENT_INTERNAL === "1";
|
|
59
|
+
}
|
|
48
60
|
/**
|
|
49
61
|
* Emit an internal debug/info message to stderr.
|
|
50
62
|
* Use for field-enable/disable notifications that a developer might want to see
|
|
51
63
|
* during development but should not appear in production log streams.
|
|
52
64
|
*/
|
|
53
65
|
function internalLog(message) {
|
|
54
|
-
if (!
|
|
66
|
+
if (!isSilent()) process.stderr.write(`[logixia] ${message}\n`);
|
|
55
67
|
}
|
|
56
68
|
/**
|
|
57
69
|
* Emit an internal warning to stderr.
|
|
58
70
|
* Use when something is misconfigured but logixia can continue operating.
|
|
59
71
|
*/
|
|
60
72
|
function internalWarn(message) {
|
|
61
|
-
if (!
|
|
73
|
+
if (!isSilent()) process.stderr.write(`[logixia:warn] ${message}\n`);
|
|
62
74
|
}
|
|
63
75
|
/**
|
|
64
76
|
* Emit an internal error to stderr.
|
|
65
77
|
* Use when a transport write fails or a serious internal error occurs.
|
|
66
78
|
*/
|
|
67
79
|
function internalError(message, error) {
|
|
68
|
-
if (!
|
|
80
|
+
if (!isSilent()) {
|
|
69
81
|
let errStr = "";
|
|
70
82
|
if (error instanceof Error) errStr = ` — ${error.message}`;
|
|
71
83
|
else if (error != null) errStr = ` — ${String(error)}`;
|
|
@@ -95,6 +107,7 @@ function internalError(message, error) {
|
|
|
95
107
|
* `String(value)` for circular refs / values without a stringifier.
|
|
96
108
|
*/
|
|
97
109
|
function safeToString(value) {
|
|
110
|
+
var _constructor;
|
|
98
111
|
if (typeof value === "string") return value;
|
|
99
112
|
if (value === void 0 || value === null) return "";
|
|
100
113
|
if (value instanceof Error) return value.message;
|
|
@@ -102,11 +115,10 @@ function safeToString(value) {
|
|
|
102
115
|
if (typeof value === "symbol") return value.toString();
|
|
103
116
|
if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
104
117
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
118
|
+
const json = JSON.stringify(value);
|
|
119
|
+
if (json !== void 0) return json;
|
|
120
|
+
} catch {}
|
|
121
|
+
return `[${((_constructor = value.constructor) === null || _constructor === void 0 ? void 0 : _constructor.name) ?? "object"}]`;
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
//#endregion
|
|
@@ -239,7 +251,6 @@ var DatabaseTransport = class {
|
|
|
239
251
|
this.config = config;
|
|
240
252
|
this.name = "database";
|
|
241
253
|
this.batch = [];
|
|
242
|
-
this.isFlushing = false;
|
|
243
254
|
this.isConnected = false;
|
|
244
255
|
this.batchSize = config.batchSize || 100;
|
|
245
256
|
this.flushInterval = config.flushInterval || 5e3;
|
|
@@ -256,33 +267,44 @@ var DatabaseTransport = class {
|
|
|
256
267
|
this.batch.push(entry);
|
|
257
268
|
}
|
|
258
269
|
async flush() {
|
|
259
|
-
if (this.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
270
|
+
if (!this.flushPromise) this.flushPromise = this.drain().finally(() => {
|
|
271
|
+
this.flushPromise = void 0;
|
|
272
|
+
});
|
|
273
|
+
await this.flushPromise;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Drains the batch to the database one snapshot at a time. The current batch
|
|
277
|
+
* is detached SYNCHRONOUSLY before awaiting the write, so entries appended by
|
|
278
|
+
* concurrent write() calls land in a fresh array and are never written twice.
|
|
279
|
+
* On write failure the snapshot is restored to the front of the batch for the
|
|
280
|
+
* next flush cycle and the loop stops, so a persistently failing DB does not
|
|
281
|
+
* hot-spin retrying the same entries.
|
|
282
|
+
*/
|
|
283
|
+
async drain() {
|
|
284
|
+
while (this.batch.length > 0) {
|
|
285
|
+
const entriesToFlush = this.batch;
|
|
286
|
+
this.batch = [];
|
|
287
|
+
try {
|
|
288
|
+
switch (this.config.type) {
|
|
289
|
+
case "mongodb":
|
|
290
|
+
await this.flushToMongoDB(entriesToFlush);
|
|
291
|
+
break;
|
|
292
|
+
case "postgresql":
|
|
293
|
+
await this.flushToPostgreSQL(entriesToFlush);
|
|
294
|
+
break;
|
|
295
|
+
case "mysql":
|
|
296
|
+
await this.flushToMySQL(entriesToFlush);
|
|
297
|
+
break;
|
|
298
|
+
case "sqlite":
|
|
299
|
+
await this.flushToSQLite(entriesToFlush);
|
|
300
|
+
break;
|
|
301
|
+
default: throw new Error(`Unsupported database type: ${this.config.type}`);
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
internalError("Database flush error", error);
|
|
305
|
+
this.batch.unshift(...entriesToFlush);
|
|
306
|
+
throw error;
|
|
279
307
|
}
|
|
280
|
-
} catch (error) {
|
|
281
|
-
internalError("Database flush error", error);
|
|
282
|
-
this.batch.unshift(...entriesToFlush);
|
|
283
|
-
throw error;
|
|
284
|
-
} finally {
|
|
285
|
-
this.isFlushing = false;
|
|
286
308
|
}
|
|
287
309
|
}
|
|
288
310
|
async isReady() {
|
|
@@ -295,7 +317,10 @@ var DatabaseTransport = class {
|
|
|
295
317
|
}
|
|
296
318
|
async connect() {
|
|
297
319
|
if (this.connectionPromise) return this.connectionPromise;
|
|
298
|
-
this.connectionPromise = this.establishConnection()
|
|
320
|
+
this.connectionPromise = this.establishConnection().catch((error) => {
|
|
321
|
+
this.connectionPromise = void 0;
|
|
322
|
+
throw error;
|
|
323
|
+
});
|
|
299
324
|
return this.connectionPromise;
|
|
300
325
|
}
|
|
301
326
|
async establishConnection() {
|
|
@@ -526,8 +551,17 @@ var DatabaseTransport = class {
|
|
|
526
551
|
}, this.flushInterval);
|
|
527
552
|
}
|
|
528
553
|
async close() {
|
|
529
|
-
if (this.flushTimer)
|
|
530
|
-
|
|
554
|
+
if (this.flushTimer) {
|
|
555
|
+
clearInterval(this.flushTimer);
|
|
556
|
+
this.flushTimer = void 0;
|
|
557
|
+
}
|
|
558
|
+
const MAX_CLOSE_FLUSH_ATTEMPTS = 3;
|
|
559
|
+
for (let attempt = 0; attempt < MAX_CLOSE_FLUSH_ATTEMPTS && this.batch.length > 0; attempt += 1) try {
|
|
560
|
+
await this.flush();
|
|
561
|
+
} catch (error) {
|
|
562
|
+
internalError("Database flush during close failed; entries remain buffered", error);
|
|
563
|
+
}
|
|
564
|
+
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`);
|
|
531
565
|
if (this.connection) switch (this.config.type) {
|
|
532
566
|
case "mongodb":
|
|
533
567
|
await this.connection.close();
|
|
@@ -563,7 +597,8 @@ var DatabaseTransport = class {
|
|
|
563
597
|
var _result$rows, _result$;
|
|
564
598
|
const tableName = this.config.table || "logs";
|
|
565
599
|
const result = await this.connection.query(`SELECT COUNT(*) as count FROM ${tableName}`);
|
|
566
|
-
|
|
600
|
+
const rawCount = ((_result$rows = result.rows) === null || _result$rows === void 0 || (_result$rows = _result$rows[0]) === null || _result$rows === void 0 ? void 0 : _result$rows.count) ?? ((_result$ = result[0]) === null || _result$ === void 0 ? void 0 : _result$.count) ?? 0;
|
|
601
|
+
return Number(rawCount) || 0;
|
|
567
602
|
}
|
|
568
603
|
default: return 0;
|
|
569
604
|
}
|
|
@@ -606,23 +641,43 @@ var AnalyticsTransport = class {
|
|
|
606
641
|
}, this.config.flushInterval);
|
|
607
642
|
}
|
|
608
643
|
async flush() {
|
|
609
|
-
if (this.batch.length === 0) return;
|
|
610
|
-
const entriesToSend = [...this.batch];
|
|
611
|
-
this.batch = [];
|
|
612
644
|
if (this.batchTimer) {
|
|
613
645
|
clearTimeout(this.batchTimer);
|
|
614
|
-
|
|
646
|
+
this.batchTimer = void 0;
|
|
615
647
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
648
|
+
if (!this.flushPromise) this.flushPromise = this.drain().finally(() => {
|
|
649
|
+
this.flushPromise = void 0;
|
|
650
|
+
});
|
|
651
|
+
await this.flushPromise;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Drains the batch to the provider one snapshot at a time. The batch is
|
|
655
|
+
* detached SYNCHRONOUSLY before awaiting sendBatch(), so entries appended by
|
|
656
|
+
* concurrent writes land in a fresh array and are never sent twice. On failure
|
|
657
|
+
* the snapshot is restored to the front of the batch for the next flush and
|
|
658
|
+
* the loop stops, so a failing provider does not hot-spin.
|
|
659
|
+
*/
|
|
660
|
+
async drain() {
|
|
661
|
+
while (this.batch.length > 0) {
|
|
662
|
+
const entriesToSend = this.batch;
|
|
663
|
+
this.batch = [];
|
|
664
|
+
try {
|
|
665
|
+
await this.sendBatch(entriesToSend);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
internalError(`Analytics transport ${this.name} flush failed`, error);
|
|
668
|
+
this.batch.unshift(...entriesToSend);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
621
671
|
}
|
|
622
672
|
}
|
|
623
673
|
async close() {
|
|
624
|
-
|
|
625
|
-
|
|
674
|
+
if (this.batchTimer) {
|
|
675
|
+
clearTimeout(this.batchTimer);
|
|
676
|
+
this.batchTimer = void 0;
|
|
677
|
+
}
|
|
678
|
+
const MAX_CLOSE_FLUSH_ATTEMPTS = 3;
|
|
679
|
+
for (let attempt = 0; attempt < MAX_CLOSE_FLUSH_ATTEMPTS && this.batch.length > 0; attempt += 1) await this.flush();
|
|
680
|
+
if (this.batch.length > 0) internalError(`Analytics transport ${this.name} closing with ${this.batch.length} unflushed entr${this.batch.length === 1 ? "y" : "ies"} after ${MAX_CLOSE_FLUSH_ATTEMPTS} attempts`);
|
|
626
681
|
await this.cleanup();
|
|
627
682
|
}
|
|
628
683
|
shouldSkipEntry(entry) {
|
|
@@ -1048,15 +1103,34 @@ var FileTransport = class {
|
|
|
1048
1103
|
await mkdir(dir, { recursive: true });
|
|
1049
1104
|
} catch {}
|
|
1050
1105
|
}
|
|
1051
|
-
|
|
1106
|
+
/**
|
|
1107
|
+
* Gzip the rotated file to `<file>.gz` and remove the original. Streams the
|
|
1108
|
+
* data so large log files don't have to be buffered in memory. Best-effort:
|
|
1109
|
+
* any failure is logged and leaves the original file intact rather than
|
|
1110
|
+
* throwing out of the rotation path.
|
|
1111
|
+
*/
|
|
1112
|
+
async compressFile(filePath) {
|
|
1113
|
+
const gzPath = `${filePath}.gz`;
|
|
1114
|
+
try {
|
|
1115
|
+
await (0, node_stream_promises.pipeline)(node_fs.createReadStream(filePath), (0, node_zlib.createGzip)(), node_fs.createWriteStream(gzPath));
|
|
1116
|
+
await unlink(filePath);
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
internalError("Failed to compress rotated log file", error);
|
|
1119
|
+
try {
|
|
1120
|
+
await unlink(gzPath);
|
|
1121
|
+
} catch {}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1052
1124
|
async cleanupOldFiles() {
|
|
1053
1125
|
var _this$config$rotation4;
|
|
1054
1126
|
if (!((_this$config$rotation4 = this.config.rotation) === null || _this$config$rotation4 === void 0 ? void 0 : _this$config$rotation4.maxFiles)) return;
|
|
1055
1127
|
try {
|
|
1056
1128
|
const dir = this.resolveDir();
|
|
1057
1129
|
const files = await readdir(dir);
|
|
1058
|
-
const
|
|
1059
|
-
const
|
|
1130
|
+
const ext = node_path.extname(this.config.filename);
|
|
1131
|
+
const base = node_path.basename(this.config.filename, ext);
|
|
1132
|
+
const isOwnRotatedFile = (file) => file.startsWith(`${base}-`) && (file.endsWith(ext) || file.endsWith(`${ext}.gz`));
|
|
1133
|
+
const logFiles = files.filter(isOwnRotatedFile).map(async (file) => {
|
|
1060
1134
|
const filePath = node_path.join(dir, file);
|
|
1061
1135
|
return {
|
|
1062
1136
|
path: filePath,
|
|
@@ -1857,7 +1931,7 @@ var TransportManager = class extends node_events.EventEmitter {
|
|
|
1857
1931
|
const writeTime = Date.now() - startTime;
|
|
1858
1932
|
metrics.logsWritten++;
|
|
1859
1933
|
metrics.lastWrite = new Date(startTime);
|
|
1860
|
-
metrics.averageWriteTime
|
|
1934
|
+
metrics.averageWriteTime += (writeTime - metrics.averageWriteTime) / metrics.logsWritten;
|
|
1861
1935
|
this.emit("log", entry);
|
|
1862
1936
|
} catch (error) {
|
|
1863
1937
|
metrics.errors++;
|
|
@@ -2168,4 +2242,4 @@ Object.defineProperty(exports, 'safeToString', {
|
|
|
2168
2242
|
return safeToString;
|
|
2169
2243
|
}
|
|
2170
2244
|
});
|
|
2171
|
-
//# sourceMappingURL=transport.manager-
|
|
2245
|
+
//# sourceMappingURL=transport.manager-B9LF9uDd.js.map
|