@jnpt/nest-logging 0.0.0-stage → 1.0.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 +774 -2
- package/dist/fastify/fastify-logging.d.ts +25 -0
- package/dist/fastify/fastify-logging.js +62 -0
- package/dist/httpclient/logging-fetch.d.ts +4 -0
- package/dist/httpclient/logging-fetch.js +83 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +39 -0
- package/dist/internal/http.d.ts +9 -0
- package/dist/internal/http.js +69 -0
- package/dist/internal/incoming.d.ts +22 -0
- package/dist/internal/incoming.js +123 -0
- package/dist/logger/context.d.ts +5 -0
- package/dist/logger/context.js +30 -0
- package/dist/logger/logger.d.ts +37 -0
- package/dist/logger/logger.js +258 -0
- package/dist/logger/masking.d.ts +12 -0
- package/dist/logger/masking.js +69 -0
- package/dist/logger/types.d.ts +77 -0
- package/dist/logger/types.js +62 -0
- package/dist/logging.module.d.ts +15 -0
- package/dist/logging.module.js +46 -0
- package/dist/middleware/logging.middleware.d.ts +18 -0
- package/dist/middleware/logging.middleware.js +48 -0
- package/package.json +58 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { AppLogger } from '../logger/logger';
|
|
3
|
+
import { type ConfigInput } from '../logger/types';
|
|
4
|
+
import { type HeaderValue } from '../internal/http';
|
|
5
|
+
export interface FastifyRequestLike {
|
|
6
|
+
raw: IncomingMessage;
|
|
7
|
+
url: string;
|
|
8
|
+
ip?: string;
|
|
9
|
+
params?: unknown;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
rawBody?: Buffer | string;
|
|
12
|
+
}
|
|
13
|
+
export interface FastifyReplyLike {
|
|
14
|
+
raw: ServerResponse;
|
|
15
|
+
header(name: string, value: string): unknown;
|
|
16
|
+
getHeaders(): Record<string, HeaderValue>;
|
|
17
|
+
}
|
|
18
|
+
type HookDone = (err?: Error) => void;
|
|
19
|
+
type Hook = (request: FastifyRequestLike, reply: FastifyReplyLike, done: HookDone) => void;
|
|
20
|
+
export interface FastifyInstanceLike {
|
|
21
|
+
addHook(name: 'onRequest' | 'preHandler', hook: Hook): unknown;
|
|
22
|
+
}
|
|
23
|
+
export declare function registerFastifyLogging(fastify: FastifyInstanceLike, log: AppLogger, config?: ConfigInput | null): void;
|
|
24
|
+
export declare function createFastifyLoggingPlugin(log: AppLogger, config?: ConfigInput | null): (fastify: FastifyInstanceLike, _opts: unknown, done: HookDone) => void;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerFastifyLogging = registerFastifyLogging;
|
|
4
|
+
exports.createFastifyLoggingPlugin = createFastifyLoggingPlugin;
|
|
5
|
+
const context_1 = require("../logger/context");
|
|
6
|
+
const logger_1 = require("../logger/logger");
|
|
7
|
+
const types_1 = require("../logger/types");
|
|
8
|
+
const http_1 = require("../internal/http");
|
|
9
|
+
const incoming_1 = require("../internal/incoming");
|
|
10
|
+
const correlationKey = Symbol('nestLoggingCorrelationId');
|
|
11
|
+
// registerFastifyLogging adds hooks to a Fastify instance that log every incoming
|
|
12
|
+
// request + response as a single "request" entry and set up the correlation ID.
|
|
13
|
+
// If config is omitted, the global default config (configureDefaults) is used.
|
|
14
|
+
function registerFastifyLogging(fastify, log, config) {
|
|
15
|
+
const ownConfig = config ? (0, types_1.buildConfig)(config) : undefined;
|
|
16
|
+
fastify.addHook('onRequest', (request, reply, done) => {
|
|
17
|
+
const cfg = ownConfig ?? (0, logger_1.getDefaultConfig)();
|
|
18
|
+
const url = request.raw.url ?? request.url;
|
|
19
|
+
// Skip excluded patterns
|
|
20
|
+
if ((0, http_1.shouldSkip)((0, incoming_1.splitUrl)(url).path, cfg)) {
|
|
21
|
+
done();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Setup correlation ID
|
|
25
|
+
const correlationId = (0, incoming_1.firstHeader)(request.raw.headers['x-correlation-id']) || (0, context_1.generateCorrelationId)();
|
|
26
|
+
request[correlationKey] = correlationId;
|
|
27
|
+
reply.header(context_1.CorrelationIDHeader, correlationId);
|
|
28
|
+
(0, incoming_1.trackIncoming)(log, cfg, correlationId, {
|
|
29
|
+
req: request.raw,
|
|
30
|
+
res: reply.raw,
|
|
31
|
+
url,
|
|
32
|
+
params: () => request.params,
|
|
33
|
+
body: () => ({
|
|
34
|
+
raw: typeof request.rawBody === 'string' ? Buffer.from(request.rawBody) : request.rawBody,
|
|
35
|
+
parsed: request.body,
|
|
36
|
+
}),
|
|
37
|
+
ip: () => request.ip,
|
|
38
|
+
responseHeaders: () => ({ ...reply.raw.getHeaders(), ...reply.getHeaders() }),
|
|
39
|
+
});
|
|
40
|
+
(0, context_1.withCorrelationId)(correlationId, () => done());
|
|
41
|
+
});
|
|
42
|
+
// Body parsing runs on stream events, which lose the async context;
|
|
43
|
+
// re-enter it right before the route handler.
|
|
44
|
+
fastify.addHook('preHandler', (request, _reply, done) => {
|
|
45
|
+
const correlationId = request[correlationKey];
|
|
46
|
+
if (correlationId)
|
|
47
|
+
(0, context_1.withCorrelationId)(correlationId, () => done());
|
|
48
|
+
else
|
|
49
|
+
done();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// createFastifyLoggingPlugin returns a Fastify plugin for fastify.register().
|
|
53
|
+
// It is not encapsulated (same as wrapping with fastify-plugin), so it covers all routes.
|
|
54
|
+
function createFastifyLoggingPlugin(log, config) {
|
|
55
|
+
const plugin = (fastify, _opts, done) => {
|
|
56
|
+
registerFastifyLogging(fastify, log, config);
|
|
57
|
+
done();
|
|
58
|
+
};
|
|
59
|
+
plugin[Symbol.for('skip-override')] = true;
|
|
60
|
+
plugin[Symbol.for('fastify.display-name')] = 'nest-logging';
|
|
61
|
+
return plugin;
|
|
62
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLoggingFetch = createLoggingFetch;
|
|
4
|
+
const context_1 = require("../logger/context");
|
|
5
|
+
const logger_1 = require("../logger/logger");
|
|
6
|
+
const types_1 = require("../logger/types");
|
|
7
|
+
const http_1 = require("../internal/http");
|
|
8
|
+
// createLoggingFetch returns a fetch() that propagates X-Correlation-ID and logs every call
|
|
9
|
+
// (request + response) as a single "request" entry with extra.direction = "outgoing".
|
|
10
|
+
// If config is omitted, the global default config (configureDefaults) is used.
|
|
11
|
+
// baseFetch defaults to the global fetch (Node >= 18).
|
|
12
|
+
function createLoggingFetch(log, config, baseFetch = globalThis.fetch) {
|
|
13
|
+
const ownConfig = config ? (0, types_1.buildConfig)(config) : undefined;
|
|
14
|
+
return async function loggingFetch(input, init) {
|
|
15
|
+
const cfg = ownConfig ?? (0, logger_1.getDefaultConfig)();
|
|
16
|
+
const inputRequest = input instanceof Request ? input : undefined;
|
|
17
|
+
const url = inputRequest ? inputRequest.url : String(input);
|
|
18
|
+
const method = (init?.method ?? inputRequest?.method ?? 'GET').toUpperCase();
|
|
19
|
+
// Ensure correlation ID
|
|
20
|
+
const correlationId = (0, context_1.ensureCorrelationId)();
|
|
21
|
+
const headers = new Headers(init?.headers ?? inputRequest?.headers);
|
|
22
|
+
headers.set(context_1.CorrelationIDHeader, correlationId);
|
|
23
|
+
// Capture request body
|
|
24
|
+
const bodyBytes = await readRequestBody(init, inputRequest);
|
|
25
|
+
const requestInfo = {
|
|
26
|
+
headers: (0, http_1.extractHeaders)(headers, cfg),
|
|
27
|
+
body: cfg.requestLogging.logBody && bodyBytes ? (0, http_1.parseBody)(bodyBytes, cfg.requestLogging.maxBodySize) : undefined,
|
|
28
|
+
contentType: headers.get('content-type') ?? undefined,
|
|
29
|
+
contentLength: bodyBytes && bodyBytes.length > 0 ? bodyBytes.length : undefined,
|
|
30
|
+
};
|
|
31
|
+
// Execute request
|
|
32
|
+
const startTime = performance.now();
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
response = await baseFetch(input, { ...init, headers });
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const duration = Math.floor(performance.now() - startTime);
|
|
39
|
+
(0, context_1.withCorrelationId)(correlationId, () => log.errorWithErr('Outgoing request failed', err, { method, url: log.maskUrl(url), duration_ms: duration }));
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
const duration = Math.floor(performance.now() - startTime);
|
|
43
|
+
// Capture response body (from a clone, so the caller can still read the original)
|
|
44
|
+
let responseBody;
|
|
45
|
+
if (cfg.requestLogging.logResponseBody && response.body) {
|
|
46
|
+
try {
|
|
47
|
+
const buf = Buffer.from(await response.clone().arrayBuffer());
|
|
48
|
+
responseBody = (0, http_1.parseBody)(buf, cfg.requestLogging.maxBodySize);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
responseBody = undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const responseInfo = {
|
|
55
|
+
headers: (0, http_1.extractHeaders)(response.headers, cfg),
|
|
56
|
+
body: responseBody,
|
|
57
|
+
contentType: response.headers.get('content-type') ?? undefined,
|
|
58
|
+
contentLength: (0, http_1.parseContentLength)(response.headers.get('content-length')),
|
|
59
|
+
};
|
|
60
|
+
// Log request + response as single entry
|
|
61
|
+
(0, context_1.withCorrelationId)(correlationId, () => log.logOutgoingHttp(method, url, response.status, duration, requestInfo, responseInfo));
|
|
62
|
+
return response;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function readRequestBody(init, inputRequest) {
|
|
66
|
+
const body = init?.body;
|
|
67
|
+
if (body === undefined || body === null) {
|
|
68
|
+
if (inputRequest?.body)
|
|
69
|
+
return Buffer.from(await inputRequest.clone().arrayBuffer());
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
if (typeof body === 'string')
|
|
73
|
+
return Buffer.from(body);
|
|
74
|
+
if (body instanceof URLSearchParams)
|
|
75
|
+
return Buffer.from(body.toString());
|
|
76
|
+
if (body instanceof ArrayBuffer)
|
|
77
|
+
return Buffer.from(body);
|
|
78
|
+
if (ArrayBuffer.isView(body))
|
|
79
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
80
|
+
if (body instanceof Blob)
|
|
81
|
+
return Buffer.from(await body.arrayBuffer());
|
|
82
|
+
return undefined; // streams / FormData are sent untouched and not logged
|
|
83
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './logger/types';
|
|
2
|
+
export * from './logger/context';
|
|
3
|
+
export { MaskingUtil } from './logger/masking';
|
|
4
|
+
export { AppLogger, configureDefaults, getDefaultConfig, mergeExtras, type Writer } from './logger/logger';
|
|
5
|
+
export { matchPattern, shouldSkip } from './internal/http';
|
|
6
|
+
export { createLoggingMiddleware, LoggingMiddleware, type HttpRequest, type HttpResponse, type NextFunction, } from './middleware/logging.middleware';
|
|
7
|
+
export { createFastifyLoggingPlugin, registerFastifyLogging, type FastifyInstanceLike, type FastifyReplyLike, type FastifyRequestLike, } from './fastify/fastify-logging';
|
|
8
|
+
export { createLoggingFetch, type FetchFn } from './httpclient/logging-fetch';
|
|
9
|
+
export { LoggingModule } from './logging.module';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.LoggingModule = exports.createLoggingFetch = exports.registerFastifyLogging = exports.createFastifyLoggingPlugin = exports.LoggingMiddleware = exports.createLoggingMiddleware = exports.shouldSkip = exports.matchPattern = exports.mergeExtras = exports.getDefaultConfig = exports.configureDefaults = exports.AppLogger = exports.MaskingUtil = void 0;
|
|
18
|
+
__exportStar(require("./logger/types"), exports);
|
|
19
|
+
__exportStar(require("./logger/context"), exports);
|
|
20
|
+
var masking_1 = require("./logger/masking");
|
|
21
|
+
Object.defineProperty(exports, "MaskingUtil", { enumerable: true, get: function () { return masking_1.MaskingUtil; } });
|
|
22
|
+
var logger_1 = require("./logger/logger");
|
|
23
|
+
Object.defineProperty(exports, "AppLogger", { enumerable: true, get: function () { return logger_1.AppLogger; } });
|
|
24
|
+
Object.defineProperty(exports, "configureDefaults", { enumerable: true, get: function () { return logger_1.configureDefaults; } });
|
|
25
|
+
Object.defineProperty(exports, "getDefaultConfig", { enumerable: true, get: function () { return logger_1.getDefaultConfig; } });
|
|
26
|
+
Object.defineProperty(exports, "mergeExtras", { enumerable: true, get: function () { return logger_1.mergeExtras; } });
|
|
27
|
+
var http_1 = require("./internal/http");
|
|
28
|
+
Object.defineProperty(exports, "matchPattern", { enumerable: true, get: function () { return http_1.matchPattern; } });
|
|
29
|
+
Object.defineProperty(exports, "shouldSkip", { enumerable: true, get: function () { return http_1.shouldSkip; } });
|
|
30
|
+
var logging_middleware_1 = require("./middleware/logging.middleware");
|
|
31
|
+
Object.defineProperty(exports, "createLoggingMiddleware", { enumerable: true, get: function () { return logging_middleware_1.createLoggingMiddleware; } });
|
|
32
|
+
Object.defineProperty(exports, "LoggingMiddleware", { enumerable: true, get: function () { return logging_middleware_1.LoggingMiddleware; } });
|
|
33
|
+
var fastify_logging_1 = require("./fastify/fastify-logging");
|
|
34
|
+
Object.defineProperty(exports, "createFastifyLoggingPlugin", { enumerable: true, get: function () { return fastify_logging_1.createFastifyLoggingPlugin; } });
|
|
35
|
+
Object.defineProperty(exports, "registerFastifyLogging", { enumerable: true, get: function () { return fastify_logging_1.registerFastifyLogging; } });
|
|
36
|
+
var logging_fetch_1 = require("./httpclient/logging-fetch");
|
|
37
|
+
Object.defineProperty(exports, "createLoggingFetch", { enumerable: true, get: function () { return logging_fetch_1.createLoggingFetch; } });
|
|
38
|
+
var logging_module_1 = require("./logging.module");
|
|
39
|
+
Object.defineProperty(exports, "LoggingModule", { enumerable: true, get: function () { return logging_module_1.LoggingModule; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Config } from '../logger/types';
|
|
2
|
+
export declare function shouldSkip(path: string, config: Config): boolean;
|
|
3
|
+
export declare function matchPattern(pattern: string, path: string): boolean;
|
|
4
|
+
export declare function canonicalHeaderKey(key: string): string;
|
|
5
|
+
export type HeaderValue = string | number | string[] | undefined;
|
|
6
|
+
export declare function extractHeaders(headers: Record<string, HeaderValue> | Iterable<[string, string]>, config: Config): Record<string, string> | undefined;
|
|
7
|
+
export declare function parseBody(raw: Buffer | string, maxBodySize: number): unknown;
|
|
8
|
+
export declare function truncateBody(buf: Buffer, maxSize: number): string;
|
|
9
|
+
export declare function parseContentLength(value: string | null | undefined): number | undefined;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldSkip = shouldSkip;
|
|
4
|
+
exports.matchPattern = matchPattern;
|
|
5
|
+
exports.canonicalHeaderKey = canonicalHeaderKey;
|
|
6
|
+
exports.extractHeaders = extractHeaders;
|
|
7
|
+
exports.parseBody = parseBody;
|
|
8
|
+
exports.truncateBody = truncateBody;
|
|
9
|
+
exports.parseContentLength = parseContentLength;
|
|
10
|
+
// shouldSkip applies excludePatterns first, then includePatterns (if any are set)
|
|
11
|
+
function shouldSkip(path, config) {
|
|
12
|
+
const { excludePatterns, includePatterns } = config.requestLogging;
|
|
13
|
+
if (excludePatterns.some((p) => matchPattern(p, path)))
|
|
14
|
+
return true;
|
|
15
|
+
if (includePatterns.length > 0)
|
|
16
|
+
return !includePatterns.some((p) => matchPattern(p, path));
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// matchPattern supports exact match, "/prefix/*" and "/prefix/**" (both are prefix matches)
|
|
20
|
+
function matchPattern(pattern, path) {
|
|
21
|
+
if (pattern.endsWith('/**'))
|
|
22
|
+
return path.startsWith(pattern.slice(0, -3));
|
|
23
|
+
if (pattern.endsWith('/*'))
|
|
24
|
+
return path.startsWith(pattern.slice(0, -2));
|
|
25
|
+
return pattern === path;
|
|
26
|
+
}
|
|
27
|
+
// canonicalHeaderKey mirrors Go's http.CanonicalHeaderKey: "x-correlation-id" -> "X-Correlation-Id"
|
|
28
|
+
function canonicalHeaderKey(key) {
|
|
29
|
+
return key.toLowerCase().replace(/(^|-)([a-z])/g, (_, sep, c) => sep + c.toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
// extractHeaders flattens headers to "key: v1, v2" with canonical keys; undefined when logHeaders is off
|
|
32
|
+
function extractHeaders(headers, config) {
|
|
33
|
+
if (!config.requestLogging.logHeaders)
|
|
34
|
+
return undefined;
|
|
35
|
+
const result = {};
|
|
36
|
+
const entries = Symbol.iterator in headers ? headers : Object.entries(headers);
|
|
37
|
+
for (const [key, value] of entries) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
const k = canonicalHeaderKey(key);
|
|
41
|
+
const v = Array.isArray(value) ? value.join(', ') : String(value);
|
|
42
|
+
result[k] = k in result ? `${result[k]}, ${v}` : v;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
// parseBody returns parsed JSON, or the raw text truncated to maxBodySize bytes; undefined when empty
|
|
47
|
+
function parseBody(raw, maxBodySize) {
|
|
48
|
+
const buf = typeof raw === 'string' ? Buffer.from(raw) : raw;
|
|
49
|
+
if (buf.length === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const text = buf.toString('utf8');
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return truncateBody(buf, maxBodySize);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function truncateBody(buf, maxSize) {
|
|
60
|
+
if (buf.length > maxSize)
|
|
61
|
+
return buf.subarray(0, maxSize).toString('utf8') + '... [TRUNCATED]';
|
|
62
|
+
return buf.toString('utf8');
|
|
63
|
+
}
|
|
64
|
+
function parseContentLength(value) {
|
|
65
|
+
if (!value)
|
|
66
|
+
return undefined;
|
|
67
|
+
const n = Number(value);
|
|
68
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
69
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { AppLogger } from '../logger/logger';
|
|
3
|
+
import type { Config } from '../logger/types';
|
|
4
|
+
import { type HeaderValue } from './http';
|
|
5
|
+
export interface IncomingSource {
|
|
6
|
+
req: IncomingMessage;
|
|
7
|
+
res: ServerResponse;
|
|
8
|
+
url: string;
|
|
9
|
+
params(): Record<string, unknown> | undefined;
|
|
10
|
+
body(): {
|
|
11
|
+
raw?: Buffer;
|
|
12
|
+
parsed?: unknown;
|
|
13
|
+
};
|
|
14
|
+
ip(): string | undefined;
|
|
15
|
+
responseHeaders(): Record<string, HeaderValue>;
|
|
16
|
+
}
|
|
17
|
+
export declare function splitUrl(url: string): {
|
|
18
|
+
path: string;
|
|
19
|
+
query: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function trackIncoming(log: AppLogger, cfg: Config, correlationId: string, src: IncomingSource): void;
|
|
22
|
+
export declare function firstHeader(value: HeaderValue): string | undefined;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splitUrl = splitUrl;
|
|
4
|
+
exports.trackIncoming = trackIncoming;
|
|
5
|
+
exports.firstHeader = firstHeader;
|
|
6
|
+
const context_1 = require("../logger/context");
|
|
7
|
+
const masking_1 = require("../logger/masking");
|
|
8
|
+
const http_1 = require("./http");
|
|
9
|
+
function splitUrl(url) {
|
|
10
|
+
const i = url.indexOf('?');
|
|
11
|
+
return i >= 0 ? { path: url.slice(0, i), query: url.slice(i + 1) } : { path: url, query: '' };
|
|
12
|
+
}
|
|
13
|
+
// trackIncoming captures the response body and writes one "Incoming request" entry
|
|
14
|
+
// when the response finishes (or the connection closes).
|
|
15
|
+
function trackIncoming(log, cfg, correlationId, src) {
|
|
16
|
+
const { req, res } = src;
|
|
17
|
+
const { path, query } = splitUrl(src.url);
|
|
18
|
+
// Capture response body (bytes are always counted; content kept only if it will be logged)
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let responseSize = 0;
|
|
21
|
+
const capture = (chunk, encoding) => {
|
|
22
|
+
if (chunk === undefined || chunk === null || typeof chunk === 'function')
|
|
23
|
+
return;
|
|
24
|
+
const buf = Buffer.isBuffer(chunk)
|
|
25
|
+
? chunk
|
|
26
|
+
: chunk instanceof Uint8Array
|
|
27
|
+
? Buffer.from(chunk)
|
|
28
|
+
: Buffer.from(String(chunk), typeof encoding === 'string' ? encoding : 'utf8');
|
|
29
|
+
responseSize += buf.length;
|
|
30
|
+
if (cfg.requestLogging.logResponseBody)
|
|
31
|
+
chunks.push(buf);
|
|
32
|
+
};
|
|
33
|
+
const originalWrite = res.write;
|
|
34
|
+
const originalEnd = res.end;
|
|
35
|
+
res.write = function (chunk, ...args) {
|
|
36
|
+
capture(chunk, args[0]);
|
|
37
|
+
return originalWrite.apply(this, [chunk, ...args]);
|
|
38
|
+
};
|
|
39
|
+
res.end = function (chunk, ...args) {
|
|
40
|
+
capture(chunk, args[0]);
|
|
41
|
+
return originalEnd.apply(this, [chunk, ...args]);
|
|
42
|
+
};
|
|
43
|
+
const startTime = performance.now();
|
|
44
|
+
let logged = false;
|
|
45
|
+
const done = () => {
|
|
46
|
+
if (logged)
|
|
47
|
+
return;
|
|
48
|
+
logged = true;
|
|
49
|
+
const duration = Math.floor(performance.now() - startTime);
|
|
50
|
+
const requestInfo = {
|
|
51
|
+
headers: (0, http_1.extractHeaders)(req.headers, cfg),
|
|
52
|
+
queryParams: extractQueryParams(query),
|
|
53
|
+
pathParams: extractPathParams(src.params()),
|
|
54
|
+
body: cfg.requestLogging.logBody ? extractRequestBody(src, cfg) : undefined,
|
|
55
|
+
contentType: contentTypeWithoutParams(firstHeader(req.headers['content-type'])),
|
|
56
|
+
contentLength: (0, http_1.parseContentLength)(firstHeader(req.headers['content-length'])),
|
|
57
|
+
};
|
|
58
|
+
const responseHeaders = src.responseHeaders();
|
|
59
|
+
const responseInfo = {
|
|
60
|
+
headers: (0, http_1.extractHeaders)(responseHeaders, cfg),
|
|
61
|
+
body: cfg.requestLogging.logResponseBody && responseSize > 0
|
|
62
|
+
? (0, http_1.parseBody)(Buffer.concat(chunks), cfg.requestLogging.maxBodySize)
|
|
63
|
+
: undefined,
|
|
64
|
+
contentType: firstHeader(findHeader(responseHeaders, 'content-type')),
|
|
65
|
+
contentLength: responseSize,
|
|
66
|
+
};
|
|
67
|
+
const ip = src.ip() ?? req.socket?.remoteAddress ?? '';
|
|
68
|
+
// Log request + response as single entry (inside the request's correlation context)
|
|
69
|
+
(0, context_1.withCorrelationId)(correlationId, () => log.logIncomingHttp(req.method ?? '', path, res.statusCode, duration, requestInfo, responseInfo, ip.startsWith('::ffff:') ? ip.slice(7) : ip, firstHeader(req.headers['user-agent'])));
|
|
70
|
+
};
|
|
71
|
+
res.once('finish', done);
|
|
72
|
+
res.once('close', done);
|
|
73
|
+
}
|
|
74
|
+
function extractRequestBody(src, cfg) {
|
|
75
|
+
const max = cfg.requestLogging.maxBodySize;
|
|
76
|
+
const { raw, parsed } = src.body();
|
|
77
|
+
// Raw bytes (NestFactory.create(..., { rawBody: true })) give exact parity with the Go version.
|
|
78
|
+
if (Buffer.isBuffer(raw))
|
|
79
|
+
return (0, http_1.parseBody)(raw, max);
|
|
80
|
+
if (Buffer.isBuffer(parsed) || typeof parsed === 'string')
|
|
81
|
+
return (0, http_1.parseBody)(parsed, max);
|
|
82
|
+
if (parsed === undefined || parsed === null)
|
|
83
|
+
return undefined;
|
|
84
|
+
// Parsed by body-parser: Express sets {} even when no body was sent.
|
|
85
|
+
if ((0, masking_1.isPlainObject)(parsed) && Object.keys(parsed).length === 0 && !hasRequestBody(src.req))
|
|
86
|
+
return undefined;
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
function hasRequestBody(req) {
|
|
90
|
+
return ((0, http_1.parseContentLength)(firstHeader(req.headers['content-length'])) ?? 0) > 0
|
|
91
|
+
|| req.headers['transfer-encoding'] !== undefined;
|
|
92
|
+
}
|
|
93
|
+
function extractQueryParams(query) {
|
|
94
|
+
if (!query)
|
|
95
|
+
return undefined;
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const [key, value] of new URLSearchParams(query)) {
|
|
98
|
+
result[key] = key in result ? `${result[key]}, ${value}` : value;
|
|
99
|
+
}
|
|
100
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
101
|
+
}
|
|
102
|
+
function extractPathParams(params) {
|
|
103
|
+
if (!params || typeof params !== 'object')
|
|
104
|
+
return undefined;
|
|
105
|
+
const entries = Object.entries(params).filter(([, v]) => v !== undefined);
|
|
106
|
+
if (entries.length === 0)
|
|
107
|
+
return undefined;
|
|
108
|
+
return Object.fromEntries(entries.map(([k, v]) => [k, Array.isArray(v) ? v.join('/') : String(v)]));
|
|
109
|
+
}
|
|
110
|
+
function findHeader(headers, name) {
|
|
111
|
+
for (const [k, v] of Object.entries(headers))
|
|
112
|
+
if (k.toLowerCase() === name)
|
|
113
|
+
return v;
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function contentTypeWithoutParams(value) {
|
|
117
|
+
return value?.split(';')[0].trim() || undefined;
|
|
118
|
+
}
|
|
119
|
+
function firstHeader(value) {
|
|
120
|
+
if (value === undefined)
|
|
121
|
+
return undefined;
|
|
122
|
+
return Array.isArray(value) ? value[0] : String(value);
|
|
123
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const CorrelationIDHeader = "X-Correlation-ID";
|
|
2
|
+
export declare function withCorrelationId<T>(correlationId: string, fn: () => T): T;
|
|
3
|
+
export declare function getCorrelationId(): string;
|
|
4
|
+
export declare function generateCorrelationId(): string;
|
|
5
|
+
export declare function ensureCorrelationId(): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CorrelationIDHeader = void 0;
|
|
4
|
+
exports.withCorrelationId = withCorrelationId;
|
|
5
|
+
exports.getCorrelationId = getCorrelationId;
|
|
6
|
+
exports.generateCorrelationId = generateCorrelationId;
|
|
7
|
+
exports.ensureCorrelationId = ensureCorrelationId;
|
|
8
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
// Node equivalent of Go's context.Context value: the correlation ID follows the
|
|
11
|
+
// async call chain (AsyncLocalStorage), so it does not need to be passed by hand.
|
|
12
|
+
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
13
|
+
// CorrelationIDHeader is the HTTP header name for correlation ID
|
|
14
|
+
exports.CorrelationIDHeader = 'X-Correlation-ID';
|
|
15
|
+
// withCorrelationId runs fn with the given correlation ID in the async context
|
|
16
|
+
function withCorrelationId(correlationId, fn) {
|
|
17
|
+
return storage.run({ correlationId }, fn);
|
|
18
|
+
}
|
|
19
|
+
// getCorrelationId retrieves correlation ID from the async context ('' if none)
|
|
20
|
+
function getCorrelationId() {
|
|
21
|
+
return storage.getStore()?.correlationId ?? '';
|
|
22
|
+
}
|
|
23
|
+
// generateCorrelationId generates a new correlation ID (UUID v4)
|
|
24
|
+
function generateCorrelationId() {
|
|
25
|
+
return (0, node_crypto_1.randomUUID)();
|
|
26
|
+
}
|
|
27
|
+
// ensureCorrelationId returns the existing correlation ID or generates a new one
|
|
28
|
+
function ensureCorrelationId() {
|
|
29
|
+
return getCorrelationId() || generateCorrelationId();
|
|
30
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Config, type ConfigInput, type RequestInfo, type ResponseInfo } from './types';
|
|
2
|
+
export interface Writer {
|
|
3
|
+
write(chunk: string): unknown;
|
|
4
|
+
}
|
|
5
|
+
export declare function configureDefaults(config?: ConfigInput | null): void;
|
|
6
|
+
export declare function getDefaultConfig(): Config;
|
|
7
|
+
export declare class AppLogger {
|
|
8
|
+
private readonly name;
|
|
9
|
+
private readonly ownConfig?;
|
|
10
|
+
private output;
|
|
11
|
+
private maskingFor?;
|
|
12
|
+
private maskingUtil?;
|
|
13
|
+
constructor(name: string, config?: ConfigInput | null);
|
|
14
|
+
get config(): Config;
|
|
15
|
+
private get masking();
|
|
16
|
+
maskUrl(url: string): string;
|
|
17
|
+
setOutput(w: Writer): void;
|
|
18
|
+
debug(message: string, ...extra: unknown[]): void;
|
|
19
|
+
info(message: string, ...extra: unknown[]): void;
|
|
20
|
+
warn(message: string, ...extra: unknown[]): void;
|
|
21
|
+
warnError(message: string, err: unknown, ...extra: unknown[]): void;
|
|
22
|
+
error(message: string, ...extra: unknown[]): void;
|
|
23
|
+
errorWithErr(message: string, err: unknown, ...extra: unknown[]): void;
|
|
24
|
+
logIncomingHttp(method: string, uri: string, statusCode: number, durationMs: number, request?: RequestInfo, response?: ResponseInfo, remoteAddr?: string, userAgent?: string): void;
|
|
25
|
+
logOutgoingHttp(method: string, uri: string, statusCode: number, durationMs: number, request?: RequestInfo, response?: ResponseInfo): void;
|
|
26
|
+
private logApplicationIfEnabled;
|
|
27
|
+
private logRequest;
|
|
28
|
+
private baseEntry;
|
|
29
|
+
private buildRequestBody;
|
|
30
|
+
private buildResponseBody;
|
|
31
|
+
private buildErrorInfo;
|
|
32
|
+
private writeLog;
|
|
33
|
+
private isApplicationLoggingEnabled;
|
|
34
|
+
private isRequestLoggingEnabled;
|
|
35
|
+
private isLevelEnabled;
|
|
36
|
+
}
|
|
37
|
+
export declare function mergeExtras(extras: unknown[]): Record<string, unknown> | undefined;
|