@munlib/common 1.0.248 → 1.1.2
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 +27 -5
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -1
- package/build/logs/logger.d.ts +37 -0
- package/build/logs/logger.js +228 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
|
+
# @munlib/common
|
|
1
2
|
|
|
3
|
+
## HTTP logs in MongoDB
|
|
4
|
+
|
|
5
|
+
`createHttpLogger` captures completed Express requests and responses. The consuming service owns the MongoDB connection and collection; a logging failure is swallowed and never changes the HTTP response.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import mongoose from "mongoose";
|
|
9
|
+
import { createHttpLogger } from "@munlib/common";
|
|
10
|
+
|
|
11
|
+
app.use(createHttpLogger({
|
|
12
|
+
serviceName: "payment",
|
|
13
|
+
environment: process.env.ENVIRONMENT,
|
|
14
|
+
write: (document) => mongoose.connection
|
|
15
|
+
.collection(process.env.HTTP_LOG_COLLECTION || "http_logs")
|
|
16
|
+
.insertOne(document)
|
|
17
|
+
}));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Register the middleware after the JSON parser and before authentication or routes. It automatically redacts credentials and payment-sensitive fields, limits request and response payloads to 16 KB, adds `x-request-id`, and only persists complete JSON response bodies. Other response types are represented by safe metadata.
|
|
21
|
+
|
|
22
|
+
Use `redactFields`, `maxPayloadBytes`, `shouldLog`, and `onWriteError` to customize the behavior.
|
|
23
|
+
|
|
24
|
+
## Publishing
|
|
25
|
+
|
|
26
|
+
```sh
|
|
2
27
|
npm run build
|
|
3
28
|
npm publish --dry-run
|
|
4
|
-
npm run pub -- --otp=token
|
|
5
|
-
|
|
6
|
-
luego
|
|
7
|
-
npm whoami
|
|
8
|
-
npm publish
|
|
29
|
+
npm run pub -- --otp=token-de-npm
|
|
30
|
+
```
|
package/build/index.d.ts
CHANGED
package/build/index.js
CHANGED
|
@@ -29,6 +29,6 @@ __exportStar(require("./middlewares/validate-request"), exports);
|
|
|
29
29
|
__exportStar(require("./events/subjects"), exports);
|
|
30
30
|
__exportStar(require("./events/types/order-status"), exports);
|
|
31
31
|
__exportStar(require("./events/types/instituciones-pagos"), exports);
|
|
32
|
-
|
|
32
|
+
__exportStar(require("./logs/logger"), exports);
|
|
33
33
|
__exportStar(require("./errors/erroresStatus"), exports);
|
|
34
34
|
//export * from './middlewares/apm';
|
package/build/logs/logger.d.ts
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Request, RequestHandler } from "express";
|
|
2
|
+
export interface HttpLogDocument {
|
|
3
|
+
timestamp: Date;
|
|
4
|
+
serviceName: string;
|
|
5
|
+
environment?: string;
|
|
6
|
+
requestId: string;
|
|
7
|
+
method: string;
|
|
8
|
+
path: string;
|
|
9
|
+
clientIp?: string;
|
|
10
|
+
userAgent?: string;
|
|
11
|
+
request: {
|
|
12
|
+
headers: {
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
query: unknown;
|
|
16
|
+
params: unknown;
|
|
17
|
+
body: unknown;
|
|
18
|
+
};
|
|
19
|
+
response: {
|
|
20
|
+
statusCode: number;
|
|
21
|
+
headers: {
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
body: unknown;
|
|
25
|
+
};
|
|
26
|
+
durationMs: number;
|
|
27
|
+
}
|
|
28
|
+
export interface HttpLoggerOptions {
|
|
29
|
+
serviceName: string;
|
|
30
|
+
write: (document: HttpLogDocument) => Promise<unknown> | unknown;
|
|
31
|
+
environment?: string;
|
|
32
|
+
redactFields?: string[];
|
|
33
|
+
maxPayloadBytes?: number;
|
|
34
|
+
shouldLog?: (request: Request) => boolean;
|
|
35
|
+
onWriteError?: (error: unknown) => void;
|
|
36
|
+
}
|
|
37
|
+
export declare function createHttpLogger(options: HttpLoggerOptions): RequestHandler;
|
package/build/logs/logger.js
CHANGED
|
@@ -1,10 +1,229 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHttpLogger = createHttpLogger;
|
|
4
|
+
var DEFAULT_REDACTED_FIELDS = [
|
|
5
|
+
"authorization",
|
|
6
|
+
"cookie",
|
|
7
|
+
"set-cookie",
|
|
8
|
+
"x-api-key",
|
|
9
|
+
"api-key",
|
|
10
|
+
"password",
|
|
11
|
+
"token",
|
|
12
|
+
"access-token",
|
|
13
|
+
"refresh-token",
|
|
14
|
+
"secret",
|
|
15
|
+
"cvv",
|
|
16
|
+
"card-number"
|
|
17
|
+
];
|
|
18
|
+
function normalizeFieldName(value) {
|
|
19
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
20
|
+
}
|
|
21
|
+
function isSensitiveField(fieldName, redactedFields) {
|
|
22
|
+
var normalized = normalizeFieldName(fieldName);
|
|
23
|
+
return redactedFields.indexOf(normalized) !== -1
|
|
24
|
+
|| normalized.indexOf("password") !== -1
|
|
25
|
+
|| normalized.indexOf("token") !== -1
|
|
26
|
+
|| normalized.indexOf("secret") !== -1
|
|
27
|
+
|| normalized.indexOf("authorization") !== -1
|
|
28
|
+
|| normalized.indexOf("cookie") !== -1
|
|
29
|
+
|| normalized.indexOf("apikey") !== -1
|
|
30
|
+
|| normalized.indexOf("cardnumber") !== -1
|
|
31
|
+
|| normalized === "cvv";
|
|
32
|
+
}
|
|
33
|
+
function sanitize(value, redactedFields, ancestors, depth) {
|
|
34
|
+
if (ancestors === void 0) { ancestors = []; }
|
|
35
|
+
if (depth === void 0) { depth = 0; }
|
|
36
|
+
if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (depth >= 8) {
|
|
40
|
+
return "[Truncated: maximum depth]";
|
|
41
|
+
}
|
|
42
|
+
if (typeof value !== "object") {
|
|
43
|
+
return String(value);
|
|
44
|
+
}
|
|
45
|
+
if (ancestors.indexOf(value) !== -1) {
|
|
46
|
+
return "[Circular]";
|
|
47
|
+
}
|
|
48
|
+
var nextAncestors = ancestors.concat([value]);
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
return value.map(function (item) { return sanitize(item, redactedFields, nextAncestors, depth + 1); });
|
|
51
|
+
}
|
|
52
|
+
var result = {};
|
|
53
|
+
Object.keys(value).forEach(function (key) {
|
|
54
|
+
var record = value;
|
|
55
|
+
result[key] = isSensitiveField(key, redactedFields)
|
|
56
|
+
? "[REDACTED]"
|
|
57
|
+
: sanitize(record[key], redactedFields, nextAncestors, depth + 1);
|
|
58
|
+
});
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function byteLength(value) {
|
|
62
|
+
return Buffer.byteLength(value, "utf8");
|
|
63
|
+
}
|
|
64
|
+
function truncateText(value, maxBytes) {
|
|
65
|
+
if (byteLength(value) <= maxBytes) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
var result = "";
|
|
69
|
+
for (var index = 0; index < value.length; index += 1) {
|
|
70
|
+
var next = result + value[index];
|
|
71
|
+
if (byteLength(next) > maxBytes) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
result = next;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function limitPayload(value, maxBytes) {
|
|
79
|
+
var serialized;
|
|
80
|
+
try {
|
|
81
|
+
serialized = JSON.stringify(value);
|
|
82
|
+
}
|
|
83
|
+
catch (_error) {
|
|
84
|
+
serialized = String(value);
|
|
85
|
+
}
|
|
86
|
+
if (byteLength(serialized) <= maxBytes) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
truncated: true,
|
|
91
|
+
sizeBytes: byteLength(serialized),
|
|
92
|
+
preview: truncateText(serialized, maxBytes)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function responseBody(content, contentType, wasTruncated, maxBytes, redactedFields) {
|
|
96
|
+
var normalizedContentType = contentType ? contentType.toLowerCase() : "";
|
|
97
|
+
var jsonContent = normalizedContentType.indexOf("application/json") !== -1;
|
|
98
|
+
if (!jsonContent) {
|
|
99
|
+
return { omitted: true, reason: "non-json-response" };
|
|
100
|
+
}
|
|
101
|
+
if (wasTruncated) {
|
|
102
|
+
return { truncated: true, capturedBytes: byteLength(content) };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return limitPayload(sanitize(JSON.parse(content), redactedFields), maxBytes);
|
|
106
|
+
}
|
|
107
|
+
catch (_error) {
|
|
108
|
+
return { omitted: true, reason: "invalid-json-response" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function createRequestId() {
|
|
112
|
+
return Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
|
|
113
|
+
}
|
|
114
|
+
function reportWriteError(error, onWriteError) {
|
|
115
|
+
if (!onWriteError) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
onWriteError(error);
|
|
120
|
+
}
|
|
121
|
+
catch (_callbackError) {
|
|
122
|
+
// Logging failures must never affect the request lifecycle.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function createHttpLogger(options) {
|
|
126
|
+
var maxPayloadBytes = options.maxPayloadBytes || 16 * 1024;
|
|
127
|
+
var redactedFields = DEFAULT_REDACTED_FIELDS
|
|
128
|
+
.concat(options.redactFields || [])
|
|
129
|
+
.map(normalizeFieldName);
|
|
130
|
+
return function (req, res, next) {
|
|
131
|
+
try {
|
|
132
|
+
if (options.shouldLog && !options.shouldLog(req)) {
|
|
133
|
+
next();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
var startedAt_1 = Date.now();
|
|
137
|
+
var suppliedRequestId = req.header("x-request-id");
|
|
138
|
+
var requestId_1 = suppliedRequestId && suppliedRequestId.length <= 128
|
|
139
|
+
? suppliedRequestId
|
|
140
|
+
: createRequestId();
|
|
141
|
+
var responseContent_1 = "";
|
|
142
|
+
var responseTruncated_1 = false;
|
|
143
|
+
var originalWrite_1 = res.write.bind(res);
|
|
144
|
+
var originalEnd_1 = res.end.bind(res);
|
|
145
|
+
res.setHeader("x-request-id", requestId_1);
|
|
146
|
+
var captureResponseChunk_1 = function (chunk) {
|
|
147
|
+
try {
|
|
148
|
+
if (chunk === undefined || chunk === null || responseTruncated_1) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
var text = typeof chunk === "string"
|
|
152
|
+
? chunk
|
|
153
|
+
: typeof chunk.toString === "function"
|
|
154
|
+
? chunk.toString("utf8")
|
|
155
|
+
: undefined;
|
|
156
|
+
if (text === undefined) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
var availableBytes = maxPayloadBytes - byteLength(responseContent_1);
|
|
160
|
+
if (availableBytes <= 0) {
|
|
161
|
+
responseTruncated_1 = true;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (byteLength(text) > availableBytes) {
|
|
165
|
+
responseContent_1 += truncateText(text, availableBytes);
|
|
166
|
+
responseTruncated_1 = true;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
responseContent_1 += text;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
reportWriteError(error, options.onWriteError);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
res.write = function () {
|
|
176
|
+
var args = [];
|
|
177
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
178
|
+
args[_i] = arguments[_i];
|
|
179
|
+
}
|
|
180
|
+
captureResponseChunk_1(args[0]);
|
|
181
|
+
return originalWrite_1.apply(void 0, args);
|
|
182
|
+
};
|
|
183
|
+
res.end = function () {
|
|
184
|
+
var args = [];
|
|
185
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
186
|
+
args[_i] = arguments[_i];
|
|
187
|
+
}
|
|
188
|
+
captureResponseChunk_1(args[0]);
|
|
189
|
+
return originalEnd_1.apply(void 0, args);
|
|
190
|
+
};
|
|
191
|
+
res.once("finish", function () {
|
|
192
|
+
try {
|
|
193
|
+
var document = {
|
|
194
|
+
timestamp: new Date(),
|
|
195
|
+
serviceName: options.serviceName,
|
|
196
|
+
environment: options.environment,
|
|
197
|
+
requestId: requestId_1,
|
|
198
|
+
method: req.method,
|
|
199
|
+
path: req.path,
|
|
200
|
+
clientIp: req.ip,
|
|
201
|
+
userAgent: req.get("user-agent"),
|
|
202
|
+
request: {
|
|
203
|
+
headers: sanitize(req.headers, redactedFields),
|
|
204
|
+
query: limitPayload(sanitize(req.query, redactedFields), maxPayloadBytes),
|
|
205
|
+
params: limitPayload(sanitize(req.params, redactedFields), maxPayloadBytes),
|
|
206
|
+
body: limitPayload(sanitize(req.body, redactedFields), maxPayloadBytes)
|
|
207
|
+
},
|
|
208
|
+
response: {
|
|
209
|
+
statusCode: res.statusCode,
|
|
210
|
+
headers: sanitize(res.getHeaders(), redactedFields),
|
|
211
|
+
body: responseBody(responseContent_1, res.getHeader("content-type"), responseTruncated_1, maxPayloadBytes, redactedFields)
|
|
212
|
+
},
|
|
213
|
+
durationMs: Date.now() - startedAt_1
|
|
214
|
+
};
|
|
215
|
+
Promise.resolve(options.write(document)).catch(function (error) {
|
|
216
|
+
reportWriteError(error, options.onWriteError);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
reportWriteError(error, options.onWriteError);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
reportWriteError(error, options.onWriteError);
|
|
226
|
+
}
|
|
227
|
+
next();
|
|
228
|
+
};
|
|
229
|
+
}
|