@valentine-efagene/qshelter-common 2.0.148 → 2.0.149
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.
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
2
|
/**
|
|
3
|
-
* Request logging middleware that logs
|
|
4
|
-
*
|
|
3
|
+
* Request logging middleware that logs:
|
|
4
|
+
* - HTTP method, path, status code, and duration
|
|
5
|
+
* - Request body (with sensitive fields redacted)
|
|
6
|
+
* - Caller info (userId, tenantId, email, roles)
|
|
7
|
+
* - Response body (with sensitive fields redacted)
|
|
8
|
+
* - Unique request ID for correlation
|
|
5
9
|
*
|
|
6
|
-
*
|
|
10
|
+
* Logs in JSON format for easy parsing by CloudWatch/log aggregation tools.
|
|
7
11
|
*/
|
|
8
12
|
export declare function requestLogger(req: Request, res: Response, next: NextFunction): void;
|
|
@@ -8,51 +8,161 @@ catch {
|
|
|
8
8
|
// Package not available
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
|
|
11
|
+
* Fields to redact from request/response logging for security.
|
|
12
|
+
*/
|
|
13
|
+
const SENSITIVE_FIELDS = ['password', 'token', 'accessToken', 'refreshToken', 'secret', 'apiKey', 'credential'];
|
|
14
|
+
/**
|
|
15
|
+
* Redact sensitive fields from an object for safe logging.
|
|
16
|
+
*/
|
|
17
|
+
function redactSensitive(obj) {
|
|
18
|
+
if (!obj || typeof obj !== 'object')
|
|
19
|
+
return obj;
|
|
20
|
+
if (Array.isArray(obj))
|
|
21
|
+
return obj.map(redactSensitive);
|
|
22
|
+
const redacted = {};
|
|
23
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
24
|
+
if (SENSITIVE_FIELDS.some(f => key.toLowerCase().includes(f.toLowerCase()))) {
|
|
25
|
+
redacted[key] = '[REDACTED]';
|
|
26
|
+
}
|
|
27
|
+
else if (typeof value === 'object' && value !== null) {
|
|
28
|
+
redacted[key] = redactSensitive(value);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
redacted[key] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return redacted;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract caller context from the request (authorizer or headers).
|
|
38
|
+
*/
|
|
39
|
+
function extractCaller(req) {
|
|
40
|
+
let authorizer = null;
|
|
41
|
+
// Try getCurrentInvoke first
|
|
42
|
+
if (getCurrentInvoke) {
|
|
43
|
+
const { event } = getCurrentInvoke();
|
|
44
|
+
if (event?.requestContext?.authorizer) {
|
|
45
|
+
const auth = event.requestContext.authorizer;
|
|
46
|
+
authorizer = auth.lambda || auth;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Fallback to req.apiGateway
|
|
50
|
+
if (!authorizer) {
|
|
51
|
+
const lambdaReq = req;
|
|
52
|
+
if (lambdaReq.apiGateway?.event?.requestContext?.authorizer) {
|
|
53
|
+
const auth = lambdaReq.apiGateway.event.requestContext.authorizer;
|
|
54
|
+
authorizer = auth.lambda || auth;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Fallback to headers (for tests/local dev)
|
|
58
|
+
if (!authorizer) {
|
|
59
|
+
return {
|
|
60
|
+
userId: req.headers['x-user-id'],
|
|
61
|
+
tenantId: req.headers['x-tenant-id'],
|
|
62
|
+
email: req.headers['x-user-email'],
|
|
63
|
+
roles: req.headers['x-user-roles'] ? req.headers['x-user-roles'].split(',') : undefined,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
userId: authorizer.userId,
|
|
68
|
+
tenantId: authorizer.tenantId,
|
|
69
|
+
email: authorizer.email,
|
|
70
|
+
roles: authorizer.roles ? (typeof authorizer.roles === 'string' ? authorizer.roles.split(',') : authorizer.roles) : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate a unique request ID for correlation.
|
|
75
|
+
*/
|
|
76
|
+
function generateRequestId() {
|
|
77
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Request logging middleware that logs:
|
|
81
|
+
* - HTTP method, path, status code, and duration
|
|
82
|
+
* - Request body (with sensitive fields redacted)
|
|
83
|
+
* - Caller info (userId, tenantId, email, roles)
|
|
84
|
+
* - Response body (with sensitive fields redacted)
|
|
85
|
+
* - Unique request ID for correlation
|
|
13
86
|
*
|
|
14
|
-
*
|
|
87
|
+
* Logs in JSON format for easy parsing by CloudWatch/log aggregation tools.
|
|
15
88
|
*/
|
|
16
89
|
export function requestLogger(req, res, next) {
|
|
17
90
|
const start = Date.now();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
91
|
+
const requestId = generateRequestId();
|
|
92
|
+
// Attach requestId to request for downstream use
|
|
93
|
+
req.requestId = requestId;
|
|
94
|
+
// Extract caller info
|
|
95
|
+
const caller = extractCaller(req);
|
|
96
|
+
// Capture request body (redacted)
|
|
97
|
+
const requestBody = req.body && Object.keys(req.body).length > 0
|
|
98
|
+
? redactSensitive(req.body)
|
|
99
|
+
: undefined;
|
|
100
|
+
// Log incoming request
|
|
101
|
+
console.log(JSON.stringify({
|
|
102
|
+
type: 'http_request',
|
|
103
|
+
requestId,
|
|
104
|
+
method: req.method,
|
|
105
|
+
path: req.path,
|
|
106
|
+
query: Object.keys(req.query).length > 0 ? req.query : undefined,
|
|
107
|
+
caller: caller.userId ? caller : undefined,
|
|
108
|
+
body: requestBody,
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
}));
|
|
111
|
+
// Capture response body by intercepting res.json and res.send
|
|
112
|
+
let responseBody = undefined;
|
|
113
|
+
const originalJson = res.json.bind(res);
|
|
114
|
+
const originalSend = res.send.bind(res);
|
|
115
|
+
res.json = function (body) {
|
|
116
|
+
responseBody = body;
|
|
117
|
+
return originalJson(body);
|
|
118
|
+
};
|
|
119
|
+
res.send = function (body) {
|
|
120
|
+
// Only capture if it looks like JSON
|
|
121
|
+
if (typeof body === 'object') {
|
|
122
|
+
responseBody = body;
|
|
29
123
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
source = 'req.apiGateway';
|
|
124
|
+
else if (typeof body === 'string') {
|
|
125
|
+
try {
|
|
126
|
+
responseBody = JSON.parse(body);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Not JSON, don't capture
|
|
37
130
|
}
|
|
38
131
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
path: req.path,
|
|
42
|
-
source,
|
|
43
|
-
hasAuthorizer: !!authorizer,
|
|
44
|
-
authorizerKeys: authorizer ? Object.keys(authorizer) : [],
|
|
45
|
-
authorizer: authorizer,
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
132
|
+
return originalSend(body);
|
|
133
|
+
};
|
|
48
134
|
res.on('finish', () => {
|
|
49
135
|
const duration = Date.now() - start;
|
|
50
|
-
|
|
136
|
+
// Determine log level based on status code
|
|
137
|
+
const isError = res.statusCode >= 400;
|
|
138
|
+
// Build response log
|
|
139
|
+
const logEntry = {
|
|
140
|
+
type: 'http_response',
|
|
141
|
+
requestId,
|
|
51
142
|
method: req.method,
|
|
52
143
|
path: req.path,
|
|
53
144
|
statusCode: res.statusCode,
|
|
54
145
|
duration,
|
|
55
|
-
|
|
146
|
+
caller: caller.userId ? { userId: caller.userId, tenantId: caller.tenantId } : undefined,
|
|
147
|
+
};
|
|
148
|
+
// Include response body for errors or when DEBUG_RESPONSE is set
|
|
149
|
+
if (isError || process.env.DEBUG_RESPONSE === 'true') {
|
|
150
|
+
logEntry.response = responseBody ? redactSensitive(responseBody) : undefined;
|
|
151
|
+
// Also include request body for errors to aid debugging
|
|
152
|
+
if (isError) {
|
|
153
|
+
logEntry.request = requestBody;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Log with appropriate level
|
|
157
|
+
if (res.statusCode >= 500) {
|
|
158
|
+
console.error(JSON.stringify(logEntry));
|
|
159
|
+
}
|
|
160
|
+
else if (res.statusCode >= 400) {
|
|
161
|
+
console.warn(JSON.stringify(logEntry));
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.log(JSON.stringify(logEntry));
|
|
165
|
+
}
|
|
56
166
|
});
|
|
57
167
|
next();
|
|
58
168
|
}
|
package/package.json
CHANGED