@jnpt/nest-logging 0.0.0-stage → 1.1.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 +839 -2
- package/dist/fastify/fastify-logging.d.ts +25 -0
- package/dist/fastify/fastify-logging.js +62 -0
- package/dist/httpclient/http-client.d.ts +23 -0
- package/dist/httpclient/http-client.js +62 -0
- package/dist/httpclient/http-client.module.d.ts +10 -0
- package/dist/httpclient/http-client.module.js +40 -0
- package/dist/httpclient/logging-fetch.d.ts +4 -0
- package/dist/httpclient/logging-fetch.js +83 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +43 -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,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;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppLogger = void 0;
|
|
4
|
+
exports.configureDefaults = configureDefaults;
|
|
5
|
+
exports.getDefaultConfig = getDefaultConfig;
|
|
6
|
+
exports.mergeExtras = mergeExtras;
|
|
7
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
8
|
+
const context_1 = require("./context");
|
|
9
|
+
const masking_1 = require("./masking");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
let globalConfig = (0, types_1.defaultConfig)();
|
|
12
|
+
// configureDefaults sets default configuration for all loggers.
|
|
13
|
+
// Accepts a full or partial config (missing fields use defaultConfig()); undefined/null is ignored.
|
|
14
|
+
function configureDefaults(config) {
|
|
15
|
+
if (config)
|
|
16
|
+
globalConfig = (0, types_1.buildConfig)(config);
|
|
17
|
+
}
|
|
18
|
+
// getDefaultConfig returns the default configuration
|
|
19
|
+
function getDefaultConfig() {
|
|
20
|
+
return globalConfig;
|
|
21
|
+
}
|
|
22
|
+
const threadName = node_worker_threads_1.isMainThread ? 'main' : `worker-${node_worker_threads_1.threadId}`;
|
|
23
|
+
// AppLogger is the main structured logger
|
|
24
|
+
class AppLogger {
|
|
25
|
+
name;
|
|
26
|
+
ownConfig;
|
|
27
|
+
output = process.stdout;
|
|
28
|
+
maskingFor;
|
|
29
|
+
maskingUtil;
|
|
30
|
+
// new AppLogger(name) follows the global default config (configureDefaults);
|
|
31
|
+
// new AppLogger(name, config) uses its own config (partial allowed, merged onto defaultConfig()).
|
|
32
|
+
constructor(name, config) {
|
|
33
|
+
this.name = name;
|
|
34
|
+
if (config)
|
|
35
|
+
this.ownConfig = (0, types_1.buildConfig)(config);
|
|
36
|
+
}
|
|
37
|
+
get config() {
|
|
38
|
+
return this.ownConfig ?? getDefaultConfig();
|
|
39
|
+
}
|
|
40
|
+
get masking() {
|
|
41
|
+
const config = this.config;
|
|
42
|
+
if (this.maskingFor !== config) {
|
|
43
|
+
this.maskingUtil = new masking_1.MaskingUtil(config);
|
|
44
|
+
this.maskingFor = config;
|
|
45
|
+
}
|
|
46
|
+
return this.maskingUtil;
|
|
47
|
+
}
|
|
48
|
+
// maskUrl masks sensitive query parameters in a URL using this logger's maskedFields
|
|
49
|
+
maskUrl(url) {
|
|
50
|
+
return this.masking.maskUrl(url);
|
|
51
|
+
}
|
|
52
|
+
// setOutput sets the output writer (default: process.stdout)
|
|
53
|
+
setOutput(w) {
|
|
54
|
+
this.output = w;
|
|
55
|
+
}
|
|
56
|
+
// debug logs a debug message
|
|
57
|
+
debug(message, ...extra) {
|
|
58
|
+
this.logApplicationIfEnabled('DEBUG', message, undefined, extra);
|
|
59
|
+
}
|
|
60
|
+
// info logs an info message
|
|
61
|
+
info(message, ...extra) {
|
|
62
|
+
this.logApplicationIfEnabled('INFO', message, undefined, extra);
|
|
63
|
+
}
|
|
64
|
+
// warn logs a warning message
|
|
65
|
+
warn(message, ...extra) {
|
|
66
|
+
this.logApplicationIfEnabled('WARN', message, undefined, extra);
|
|
67
|
+
}
|
|
68
|
+
// warnError logs a warning message with error
|
|
69
|
+
warnError(message, err, ...extra) {
|
|
70
|
+
this.logApplicationIfEnabled('WARN', message, err, extra);
|
|
71
|
+
}
|
|
72
|
+
// error logs an error message
|
|
73
|
+
error(message, ...extra) {
|
|
74
|
+
this.logApplicationIfEnabled('ERROR', message, undefined, extra);
|
|
75
|
+
}
|
|
76
|
+
// errorWithErr logs an error message with error
|
|
77
|
+
errorWithErr(message, err, ...extra) {
|
|
78
|
+
this.logApplicationIfEnabled('ERROR', message, err, extra);
|
|
79
|
+
}
|
|
80
|
+
// logIncomingHttp logs incoming request and response as a single entry
|
|
81
|
+
logIncomingHttp(method, uri, statusCode, durationMs, request, response, remoteAddr = '', userAgent = '') {
|
|
82
|
+
if (!this.isRequestLoggingEnabled())
|
|
83
|
+
return;
|
|
84
|
+
this.logRequest(getStatusLevel(statusCode), 'Incoming request', method, uri, statusCode, durationMs, remoteAddr, userAgent, request, response);
|
|
85
|
+
}
|
|
86
|
+
// logOutgoingHttp logs outgoing request and response as a single entry
|
|
87
|
+
logOutgoingHttp(method, uri, statusCode, durationMs, request, response) {
|
|
88
|
+
if (!this.isRequestLoggingEnabled())
|
|
89
|
+
return;
|
|
90
|
+
this.logRequest(getStatusLevel(statusCode), 'Outgoing request', method, this.maskUrl(uri), statusCode, durationMs, '', '', request, response, { direction: 'outgoing' });
|
|
91
|
+
}
|
|
92
|
+
logApplicationIfEnabled(level, message, err, extra) {
|
|
93
|
+
if (!this.isApplicationLoggingEnabled() || !this.isLevelEnabled(level))
|
|
94
|
+
return;
|
|
95
|
+
const entry = this.baseEntry(level, message, types_1.LogTypeApplication);
|
|
96
|
+
entry.request_body = {};
|
|
97
|
+
entry.response_body = {};
|
|
98
|
+
if (err !== undefined && err !== null && this.config.applicationLogging.includeStackTrace) {
|
|
99
|
+
entry.error = this.buildErrorInfo(err);
|
|
100
|
+
}
|
|
101
|
+
entry.extra = mergeExtras(extra);
|
|
102
|
+
this.writeLog(entry);
|
|
103
|
+
}
|
|
104
|
+
logRequest(level, message, method, uri, statusCode, durationMs, remoteAddr, userAgent, request, response, extra) {
|
|
105
|
+
const entry = this.baseEntry(level, message, types_1.LogTypeRequest);
|
|
106
|
+
entry.method = method;
|
|
107
|
+
entry.uri = uri;
|
|
108
|
+
entry.status_code = statusCode;
|
|
109
|
+
entry.duration_ms = durationMs;
|
|
110
|
+
entry.remote_address = remoteAddr || undefined;
|
|
111
|
+
entry.user_agent = userAgent || undefined;
|
|
112
|
+
entry.request_body = request ? this.buildRequestBody(request) : {};
|
|
113
|
+
entry.response_body = response ? this.buildResponseBody(response) : {};
|
|
114
|
+
entry.extra = extra;
|
|
115
|
+
this.writeLog(entry);
|
|
116
|
+
}
|
|
117
|
+
baseEntry(level, message, type) {
|
|
118
|
+
// Property order here is the JSON field order.
|
|
119
|
+
return {
|
|
120
|
+
'@timestamp': (0, types_1.formatTimestamp)(new Date()),
|
|
121
|
+
'@version': '1',
|
|
122
|
+
application: this.config.applicationName,
|
|
123
|
+
message,
|
|
124
|
+
logger_name: this.name,
|
|
125
|
+
thread_name: threadName,
|
|
126
|
+
level,
|
|
127
|
+
level_value: types_1.LevelValue[level] ?? 0,
|
|
128
|
+
type,
|
|
129
|
+
correlation_id: (0, context_1.getCorrelationId)(),
|
|
130
|
+
method: '',
|
|
131
|
+
uri: '',
|
|
132
|
+
status_code: undefined,
|
|
133
|
+
duration_ms: undefined,
|
|
134
|
+
remote_address: undefined,
|
|
135
|
+
user_agent: undefined,
|
|
136
|
+
request_body: undefined,
|
|
137
|
+
response_body: undefined,
|
|
138
|
+
error: undefined,
|
|
139
|
+
extra: undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
buildRequestBody(req) {
|
|
143
|
+
const cfg = this.config.requestLogging;
|
|
144
|
+
const body = {};
|
|
145
|
+
if (cfg.logHeaders && req.headers)
|
|
146
|
+
body.headers = this.masking.maskHeaders(req.headers);
|
|
147
|
+
if (req.queryParams)
|
|
148
|
+
body.query_params = this.masking.maskBody(req.queryParams);
|
|
149
|
+
if (req.pathParams)
|
|
150
|
+
body.path_params = req.pathParams;
|
|
151
|
+
if (cfg.logBody && req.body !== undefined && req.body !== null)
|
|
152
|
+
body.body = this.masking.maskBody(req.body);
|
|
153
|
+
if (req.contentType)
|
|
154
|
+
body.content_type = req.contentType;
|
|
155
|
+
if (req.contentLength && req.contentLength > 0)
|
|
156
|
+
body.content_length = req.contentLength;
|
|
157
|
+
return body;
|
|
158
|
+
}
|
|
159
|
+
buildResponseBody(res) {
|
|
160
|
+
const cfg = this.config.requestLogging;
|
|
161
|
+
const body = {};
|
|
162
|
+
if (cfg.logHeaders && res.headers)
|
|
163
|
+
body.headers = this.masking.maskHeaders(res.headers);
|
|
164
|
+
if (cfg.logResponseBody && res.body !== undefined && res.body !== null)
|
|
165
|
+
body.body = this.masking.maskBody(res.body);
|
|
166
|
+
if (res.contentType)
|
|
167
|
+
body.content_type = res.contentType;
|
|
168
|
+
if (res.contentLength && res.contentLength > 0)
|
|
169
|
+
body.content_length = res.contentLength;
|
|
170
|
+
return body;
|
|
171
|
+
}
|
|
172
|
+
buildErrorInfo(err) {
|
|
173
|
+
const info = {
|
|
174
|
+
class: err instanceof Error ? err.constructor?.name || err.name : typeof err,
|
|
175
|
+
message: err instanceof Error ? err.message : String(err),
|
|
176
|
+
};
|
|
177
|
+
if (this.config.applicationLogging.includeStackTrace) {
|
|
178
|
+
const stack = err instanceof Error && err.stack ? err.stack : new Error().stack;
|
|
179
|
+
info.stack_trace = parseStack(stack, this.config.applicationLogging.maxStackTraceDepth);
|
|
180
|
+
}
|
|
181
|
+
return info;
|
|
182
|
+
}
|
|
183
|
+
writeLog(entry) {
|
|
184
|
+
let data;
|
|
185
|
+
try {
|
|
186
|
+
data = JSON.stringify(entry);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
this.output.write(`Failed to marshal log entry: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.output.write(data + '\n');
|
|
193
|
+
}
|
|
194
|
+
isApplicationLoggingEnabled() {
|
|
195
|
+
return this.config.enabled && this.config.applicationLogging.enabled;
|
|
196
|
+
}
|
|
197
|
+
isRequestLoggingEnabled() {
|
|
198
|
+
return this.config.enabled && this.config.requestLogging.enabled;
|
|
199
|
+
}
|
|
200
|
+
// isLevelEnabled checks if the given level should be logged based on minLevel
|
|
201
|
+
isLevelEnabled(level) {
|
|
202
|
+
const minLevelValue = types_1.LevelValue[this.config.minLevel] ?? types_1.LevelValue.DEBUG; // default to DEBUG if invalid
|
|
203
|
+
const levelValue = types_1.LevelValue[level];
|
|
204
|
+
if (levelValue === undefined)
|
|
205
|
+
return true; // log unknown levels
|
|
206
|
+
return levelValue >= minLevelValue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
exports.AppLogger = AppLogger;
|
|
210
|
+
function getStatusLevel(statusCode) {
|
|
211
|
+
if (statusCode >= 500)
|
|
212
|
+
return 'ERROR';
|
|
213
|
+
if (statusCode >= 400)
|
|
214
|
+
return 'WARN';
|
|
215
|
+
return 'INFO';
|
|
216
|
+
}
|
|
217
|
+
// parseStack turns " at fn (file:line:col)" lines into trimmed frames, up to maxDepth
|
|
218
|
+
function parseStack(stack, maxDepth) {
|
|
219
|
+
if (!stack)
|
|
220
|
+
return undefined;
|
|
221
|
+
const frames = stack
|
|
222
|
+
.split('\n')
|
|
223
|
+
.map((line) => line.trim())
|
|
224
|
+
.filter((line) => line.startsWith('at '))
|
|
225
|
+
.map((line) => line.slice(3))
|
|
226
|
+
.slice(0, Math.max(0, maxDepth));
|
|
227
|
+
return frames.length > 0 ? frames : undefined;
|
|
228
|
+
}
|
|
229
|
+
// mergeExtras converts any value (object or class instance) to a plain object and merges them.
|
|
230
|
+
// Values that are not objects after JSON round-trip (arrays, primitives) are stored under "data".
|
|
231
|
+
function mergeExtras(extras) {
|
|
232
|
+
if (extras.length === 0)
|
|
233
|
+
return undefined;
|
|
234
|
+
const result = {};
|
|
235
|
+
for (const extra of extras) {
|
|
236
|
+
if (extra === undefined || extra === null)
|
|
237
|
+
continue;
|
|
238
|
+
if ((0, masking_1.isPlainObject)(extra)) {
|
|
239
|
+
Object.assign(result, extra);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
let parsed;
|
|
243
|
+
try {
|
|
244
|
+
const json = JSON.stringify(extra);
|
|
245
|
+
if (json === undefined)
|
|
246
|
+
continue;
|
|
247
|
+
parsed = JSON.parse(json);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if ((0, masking_1.isPlainObject)(parsed))
|
|
253
|
+
Object.assign(result, parsed);
|
|
254
|
+
else
|
|
255
|
+
result.data = extra;
|
|
256
|
+
}
|
|
257
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
258
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Config } from './types';
|
|
2
|
+
export declare class MaskingUtil {
|
|
3
|
+
private readonly maskedHeaders;
|
|
4
|
+
private readonly maskedFields;
|
|
5
|
+
private readonly maskValue;
|
|
6
|
+
constructor(config: Config);
|
|
7
|
+
maskHeaders(headers?: Record<string, string>): Record<string, string> | undefined;
|
|
8
|
+
maskUrl(url: string): string;
|
|
9
|
+
maskBody(body: unknown): unknown;
|
|
10
|
+
private maskValueRecursive;
|
|
11
|
+
}
|
|
12
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MaskingUtil = void 0;
|
|
4
|
+
exports.isPlainObject = isPlainObject;
|
|
5
|
+
// MaskingUtil provides utilities for masking sensitive data
|
|
6
|
+
class MaskingUtil {
|
|
7
|
+
maskedHeaders;
|
|
8
|
+
maskedFields;
|
|
9
|
+
maskValue;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.maskedHeaders = new Set(config.maskedHeaders.map((h) => h.toLowerCase()));
|
|
12
|
+
this.maskedFields = new Set(config.maskedFields.map((f) => f.toLowerCase()));
|
|
13
|
+
this.maskValue = config.maskValue;
|
|
14
|
+
}
|
|
15
|
+
// maskHeaders masks sensitive headers (case-insensitive)
|
|
16
|
+
maskHeaders(headers) {
|
|
17
|
+
if (!headers)
|
|
18
|
+
return undefined;
|
|
19
|
+
const masked = {};
|
|
20
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
21
|
+
masked[key] = this.maskedHeaders.has(key.toLowerCase()) ? this.maskValue : value;
|
|
22
|
+
}
|
|
23
|
+
return masked;
|
|
24
|
+
}
|
|
25
|
+
// maskUrl masks query parameters whose name is a masked field (case-insensitive).
|
|
26
|
+
// Returns the URL unchanged when nothing matches or it cannot be parsed.
|
|
27
|
+
maskUrl(url) {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = new URL(url);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
let changed = false;
|
|
36
|
+
for (const key of new Set(parsed.searchParams.keys())) {
|
|
37
|
+
if (this.maskedFields.has(key.toLowerCase())) {
|
|
38
|
+
parsed.searchParams.set(key, this.maskValue);
|
|
39
|
+
changed = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return changed ? parsed.toString() : url;
|
|
43
|
+
}
|
|
44
|
+
// maskBody masks sensitive fields in body, recursively through objects and arrays (case-insensitive)
|
|
45
|
+
maskBody(body) {
|
|
46
|
+
if (body === undefined || body === null)
|
|
47
|
+
return body;
|
|
48
|
+
return this.maskValueRecursive(body);
|
|
49
|
+
}
|
|
50
|
+
maskValueRecursive(value) {
|
|
51
|
+
if (Array.isArray(value))
|
|
52
|
+
return value.map((item) => this.maskValueRecursive(item));
|
|
53
|
+
if (isPlainObject(value)) {
|
|
54
|
+
const masked = {};
|
|
55
|
+
for (const [key, v] of Object.entries(value)) {
|
|
56
|
+
masked[key] = this.maskedFields.has(key.toLowerCase()) ? this.maskValue : this.maskValueRecursive(v);
|
|
57
|
+
}
|
|
58
|
+
return masked;
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.MaskingUtil = MaskingUtil;
|
|
64
|
+
function isPlainObject(value) {
|
|
65
|
+
if (value === null || typeof value !== 'object')
|
|
66
|
+
return false;
|
|
67
|
+
const proto = Object.getPrototypeOf(value);
|
|
68
|
+
return proto === Object.prototype || proto === null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type LogType = 'application' | 'request';
|
|
2
|
+
export declare const LogTypeApplication: LogType;
|
|
3
|
+
export declare const LogTypeRequest: LogType;
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
'@timestamp': string;
|
|
6
|
+
'@version': string;
|
|
7
|
+
application: string;
|
|
8
|
+
message: string;
|
|
9
|
+
logger_name: string;
|
|
10
|
+
thread_name: string;
|
|
11
|
+
level: string;
|
|
12
|
+
level_value: number;
|
|
13
|
+
type: LogType;
|
|
14
|
+
correlation_id: string;
|
|
15
|
+
method: string;
|
|
16
|
+
uri: string;
|
|
17
|
+
status_code?: number;
|
|
18
|
+
duration_ms?: number;
|
|
19
|
+
remote_address?: string;
|
|
20
|
+
user_agent?: string;
|
|
21
|
+
request_body: unknown;
|
|
22
|
+
response_body: unknown;
|
|
23
|
+
error?: ErrorInfo;
|
|
24
|
+
extra?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
export interface ErrorInfo {
|
|
27
|
+
class: string;
|
|
28
|
+
message: string;
|
|
29
|
+
stack_trace?: string[];
|
|
30
|
+
root_cause?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RequestInfo {
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
queryParams?: Record<string, string>;
|
|
35
|
+
pathParams?: Record<string, string>;
|
|
36
|
+
body?: unknown;
|
|
37
|
+
contentType?: string;
|
|
38
|
+
contentLength?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ResponseInfo {
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
body?: unknown;
|
|
43
|
+
contentType?: string;
|
|
44
|
+
contentLength?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface Config {
|
|
47
|
+
applicationName: string;
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
minLevel: string;
|
|
50
|
+
requestLogging: RequestLoggingConfig;
|
|
51
|
+
applicationLogging: ApplicationLoggingConfig;
|
|
52
|
+
maskedHeaders: string[];
|
|
53
|
+
maskedFields: string[];
|
|
54
|
+
maskValue: string;
|
|
55
|
+
}
|
|
56
|
+
export interface RequestLoggingConfig {
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
logHeaders: boolean;
|
|
59
|
+
logBody: boolean;
|
|
60
|
+
logResponseBody: boolean;
|
|
61
|
+
maxBodySize: number;
|
|
62
|
+
excludePatterns: string[];
|
|
63
|
+
includePatterns: string[];
|
|
64
|
+
}
|
|
65
|
+
export interface ApplicationLoggingConfig {
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
includeStackTrace: boolean;
|
|
68
|
+
maxStackTraceDepth: number;
|
|
69
|
+
}
|
|
70
|
+
export type ConfigInput = Partial<Omit<Config, 'requestLogging' | 'applicationLogging'>> & {
|
|
71
|
+
requestLogging?: Partial<RequestLoggingConfig>;
|
|
72
|
+
applicationLogging?: Partial<ApplicationLoggingConfig>;
|
|
73
|
+
};
|
|
74
|
+
export declare function defaultConfig(): Config;
|
|
75
|
+
export declare function buildConfig(input?: ConfigInput): Config;
|
|
76
|
+
export declare const LevelValue: Readonly<Record<string, number>>;
|
|
77
|
+
export declare function formatTimestamp(d: Date): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LevelValue = exports.LogTypeRequest = exports.LogTypeApplication = void 0;
|
|
4
|
+
exports.defaultConfig = defaultConfig;
|
|
5
|
+
exports.buildConfig = buildConfig;
|
|
6
|
+
exports.formatTimestamp = formatTimestamp;
|
|
7
|
+
exports.LogTypeApplication = 'application';
|
|
8
|
+
exports.LogTypeRequest = 'request';
|
|
9
|
+
// defaultConfig returns default configuration
|
|
10
|
+
function defaultConfig() {
|
|
11
|
+
return {
|
|
12
|
+
applicationName: 'application',
|
|
13
|
+
enabled: true,
|
|
14
|
+
minLevel: 'DEBUG',
|
|
15
|
+
requestLogging: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
logHeaders: true,
|
|
18
|
+
logBody: true,
|
|
19
|
+
logResponseBody: true,
|
|
20
|
+
maxBodySize: 10240,
|
|
21
|
+
excludePatterns: ['/health', '/metrics', '/favicon.ico'],
|
|
22
|
+
includePatterns: [],
|
|
23
|
+
},
|
|
24
|
+
applicationLogging: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
includeStackTrace: true,
|
|
27
|
+
maxStackTraceDepth: 50,
|
|
28
|
+
},
|
|
29
|
+
maskedHeaders: ['Authorization', 'X-Api-Key', 'Cookie', 'Set-Cookie'],
|
|
30
|
+
maskedFields: ['password', 'secret', 'token', 'creditCard', 'ssn'],
|
|
31
|
+
maskValue: '***MASKED***',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// buildConfig merges a partial config onto defaultConfig()
|
|
35
|
+
function buildConfig(input) {
|
|
36
|
+
const base = defaultConfig();
|
|
37
|
+
if (!input)
|
|
38
|
+
return base;
|
|
39
|
+
return {
|
|
40
|
+
...base,
|
|
41
|
+
...input,
|
|
42
|
+
requestLogging: { ...base.requestLogging, ...input.requestLogging },
|
|
43
|
+
applicationLogging: { ...base.applicationLogging, ...input.applicationLogging },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// LevelValue maps log level to numeric value
|
|
47
|
+
exports.LevelValue = {
|
|
48
|
+
TRACE: 5000,
|
|
49
|
+
DEBUG: 10000,
|
|
50
|
+
INFO: 20000,
|
|
51
|
+
WARN: 30000,
|
|
52
|
+
ERROR: 40000,
|
|
53
|
+
};
|
|
54
|
+
// formatTimestamp formats time to ISO 8601 with local timezone, e.g. 2006-01-02T15:04:05.000+07:00
|
|
55
|
+
function formatTimestamp(d) {
|
|
56
|
+
const pad = (n, w = 2) => String(Math.abs(n)).padStart(w, '0');
|
|
57
|
+
const offset = -d.getTimezoneOffset();
|
|
58
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
59
|
+
return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
|
60
|
+
`T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}` +
|
|
61
|
+
`${sign}${pad(Math.trunc(offset / 60))}:${pad(offset % 60)}`);
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
2
|
+
import type { ConfigInput } from './logger/types';
|
|
3
|
+
interface HttpAdapterHostLike {
|
|
4
|
+
httpAdapter?: {
|
|
5
|
+
getType?(): string;
|
|
6
|
+
getInstance?(): unknown;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare class LoggingModule implements NestModule {
|
|
10
|
+
private readonly adapterHost?;
|
|
11
|
+
constructor(adapterHost?: HttpAdapterHostLike | undefined);
|
|
12
|
+
static forRoot(config?: ConfigInput | null): DynamicModule;
|
|
13
|
+
configure(consumer: MiddlewareConsumer): void;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoggingModule = void 0;
|
|
4
|
+
const fastify_logging_1 = require("./fastify/fastify-logging");
|
|
5
|
+
const logger_1 = require("./logger/logger");
|
|
6
|
+
const logging_middleware_1 = require("./middleware/logging.middleware");
|
|
7
|
+
// LoggingModule.forRoot(config?) sets the global default config and logs every incoming request:
|
|
8
|
+
// Express apps get LoggingMiddleware on all routes, Fastify apps (FastifyAdapter) get Fastify hooks.
|
|
9
|
+
// @nestjs/core is only loaded lazily (no static runtime import), so it works with both
|
|
10
|
+
// CommonJS (Nest <= 11) and ESM (Nest 12+) applications.
|
|
11
|
+
class LoggingModule {
|
|
12
|
+
adapterHost;
|
|
13
|
+
constructor(adapterHost) {
|
|
14
|
+
this.adapterHost = adapterHost;
|
|
15
|
+
}
|
|
16
|
+
static forRoot(config) {
|
|
17
|
+
(0, logger_1.configureDefaults)(config);
|
|
18
|
+
injectHttpAdapterHost();
|
|
19
|
+
return { module: LoggingModule };
|
|
20
|
+
}
|
|
21
|
+
configure(consumer) {
|
|
22
|
+
const adapter = this.adapterHost?.httpAdapter;
|
|
23
|
+
if (adapter?.getType?.() === 'fastify') {
|
|
24
|
+
(0, fastify_logging_1.registerFastifyLogging)(adapter.getInstance(), new logger_1.AppLogger('LoggingMiddleware'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
consumer.apply(logging_middleware_1.LoggingMiddleware).forRoutes('*');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.LoggingModule = LoggingModule;
|
|
31
|
+
let injected = false;
|
|
32
|
+
// Declares the constructor dependency (what @Inject(HttpAdapterHost) would emit).
|
|
33
|
+
function injectHttpAdapterHost() {
|
|
34
|
+
if (injected)
|
|
35
|
+
return;
|
|
36
|
+
injected = true;
|
|
37
|
+
const reflect = Reflect;
|
|
38
|
+
try {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
const { HttpAdapterHost } = require('@nestjs/core');
|
|
41
|
+
reflect.defineMetadata?.('design:paramtypes', [HttpAdapterHost], LoggingModule);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// @nestjs/core not resolvable: fall back to the Express middleware only
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { NestMiddleware } from '@nestjs/common';
|
|
3
|
+
import { AppLogger } from '../logger/logger';
|
|
4
|
+
import { type ConfigInput } from '../logger/types';
|
|
5
|
+
export interface HttpRequest extends IncomingMessage {
|
|
6
|
+
originalUrl?: string;
|
|
7
|
+
ip?: string;
|
|
8
|
+
params?: Record<string, unknown>;
|
|
9
|
+
body?: unknown;
|
|
10
|
+
rawBody?: Buffer;
|
|
11
|
+
}
|
|
12
|
+
export type HttpResponse = ServerResponse;
|
|
13
|
+
export type NextFunction = (err?: unknown) => void;
|
|
14
|
+
export declare function createLoggingMiddleware(log: AppLogger, config?: ConfigInput | null): (req: HttpRequest, res: HttpResponse, next: NextFunction) => void;
|
|
15
|
+
export declare class LoggingMiddleware implements NestMiddleware {
|
|
16
|
+
private readonly handler;
|
|
17
|
+
use(req: HttpRequest, res: HttpResponse, next: NextFunction): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoggingMiddleware = void 0;
|
|
4
|
+
exports.createLoggingMiddleware = createLoggingMiddleware;
|
|
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
|
+
// createLoggingMiddleware returns an Express middleware that logs every incoming
|
|
11
|
+
// request + response as a single "request" entry and sets up the correlation ID.
|
|
12
|
+
// If config is omitted, the global default config (configureDefaults) is used.
|
|
13
|
+
// For Fastify use createFastifyLoggingPlugin (LoggingModule picks the right one automatically).
|
|
14
|
+
function createLoggingMiddleware(log, config) {
|
|
15
|
+
const ownConfig = config ? (0, types_1.buildConfig)(config) : undefined;
|
|
16
|
+
return function loggingMiddleware(req, res, next) {
|
|
17
|
+
const cfg = ownConfig ?? (0, logger_1.getDefaultConfig)();
|
|
18
|
+
const url = req.originalUrl ?? req.url ?? '/';
|
|
19
|
+
// Skip excluded patterns
|
|
20
|
+
if ((0, http_1.shouldSkip)((0, incoming_1.splitUrl)(url).path, cfg)) {
|
|
21
|
+
next();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Setup correlation ID
|
|
25
|
+
const correlationId = (0, incoming_1.firstHeader)(req.headers['x-correlation-id']) || (0, context_1.generateCorrelationId)();
|
|
26
|
+
res.setHeader(context_1.CorrelationIDHeader, correlationId);
|
|
27
|
+
(0, incoming_1.trackIncoming)(log, cfg, correlationId, {
|
|
28
|
+
req,
|
|
29
|
+
res,
|
|
30
|
+
url,
|
|
31
|
+
params: () => req.params,
|
|
32
|
+
body: () => ({ raw: req.rawBody, parsed: req.body }),
|
|
33
|
+
ip: () => req.ip,
|
|
34
|
+
responseHeaders: () => res.getHeaders(),
|
|
35
|
+
});
|
|
36
|
+
// Process request with the correlation ID available to everything downstream
|
|
37
|
+
(0, context_1.withCorrelationId)(correlationId, () => next());
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// LoggingMiddleware is the NestJS (Express) class form; it logs with logger name "LoggingMiddleware"
|
|
41
|
+
// and the global default config. LoggingModule registers it for all routes automatically.
|
|
42
|
+
class LoggingMiddleware {
|
|
43
|
+
handler = createLoggingMiddleware(new logger_1.AppLogger('LoggingMiddleware'));
|
|
44
|
+
use(req, res, next) {
|
|
45
|
+
this.handler(req, res, next);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.LoggingMiddleware = LoggingMiddleware;
|