@fabbahiense/pulsar-pino-transport 0.2.1 → 0.4.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/CHANGELOG.md +43 -0
- package/LICENSE +21 -0
- package/README.md +205 -110
- package/dist/index.cjs +1438 -86
- package/dist/index.d.cts +278 -4
- package/dist/index.d.ts +278 -4
- package/dist/index.js +1423 -85
- package/dist/middleware.cjs +387 -0
- package/dist/middleware.d.cts +70 -0
- package/dist/middleware.d.ts +70 -0
- package/dist/middleware.js +360 -0
- package/package.json +35 -8
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/middleware.ts
|
|
21
|
+
var middleware_exports = {};
|
|
22
|
+
__export(middleware_exports, {
|
|
23
|
+
pulsarRequestLogger: () => pulsarRequestLogger
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(middleware_exports);
|
|
26
|
+
|
|
27
|
+
// ../shared/src/redact.ts
|
|
28
|
+
var DEFAULT_REDACT_KEYS = [
|
|
29
|
+
"password",
|
|
30
|
+
"passwd",
|
|
31
|
+
"password_hash",
|
|
32
|
+
"secret",
|
|
33
|
+
"client_secret",
|
|
34
|
+
"token",
|
|
35
|
+
"access_token",
|
|
36
|
+
"refresh_token",
|
|
37
|
+
"id_token",
|
|
38
|
+
"session_id",
|
|
39
|
+
"api_key",
|
|
40
|
+
"x-api-key",
|
|
41
|
+
"x-auth-token",
|
|
42
|
+
"authorization",
|
|
43
|
+
"proxy-authorization",
|
|
44
|
+
"cookie",
|
|
45
|
+
"set-cookie"
|
|
46
|
+
];
|
|
47
|
+
function normalizeKey(key) {
|
|
48
|
+
return String(key).toLowerCase().replace(/[-_]/g, "");
|
|
49
|
+
}
|
|
50
|
+
function redactKeyList(extra = []) {
|
|
51
|
+
return [...new Set([...DEFAULT_REDACT_KEYS, ...extra].map(normalizeKey))];
|
|
52
|
+
}
|
|
53
|
+
function isRedactedKey(key, redactKeys) {
|
|
54
|
+
return redactKeys.includes(normalizeKey(key));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ../shared/src/trace.ts
|
|
58
|
+
var import_node_async_hooks = require("async_hooks");
|
|
59
|
+
var import_node_crypto = require("crypto");
|
|
60
|
+
var TRACEPARENT_HEADER = "traceparent";
|
|
61
|
+
var TRACESTATE_HEADER = "tracestate";
|
|
62
|
+
var TRACE_ID_HEADER = "x-trace-id";
|
|
63
|
+
var MAX_TRACESTATE_CHARS = 512;
|
|
64
|
+
var ZERO_TRACE_ID = "0".repeat(32);
|
|
65
|
+
var ZERO_SPAN_ID = "0".repeat(16);
|
|
66
|
+
var TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?$/;
|
|
67
|
+
var TRACE_ID_RE = /^[0-9a-f]{32}$/;
|
|
68
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
69
|
+
function newTraceId() {
|
|
70
|
+
for (; ; ) {
|
|
71
|
+
const id = (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
72
|
+
if (id !== ZERO_TRACE_ID) return id;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function newSpanId() {
|
|
76
|
+
for (; ; ) {
|
|
77
|
+
const id = (0, import_node_crypto.randomBytes)(8).toString("hex");
|
|
78
|
+
if (id !== ZERO_SPAN_ID) return id;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function sha256Hex(value) {
|
|
82
|
+
return (0, import_node_crypto.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
83
|
+
}
|
|
84
|
+
function toW3CTraceId(traceId) {
|
|
85
|
+
const lower = traceId.toLowerCase();
|
|
86
|
+
if (TRACE_ID_RE.test(lower) && lower !== ZERO_TRACE_ID) return lower;
|
|
87
|
+
if (UUID_RE.test(lower)) {
|
|
88
|
+
const hex = lower.replace(/-/g, "");
|
|
89
|
+
if (hex !== ZERO_TRACE_ID) return hex;
|
|
90
|
+
}
|
|
91
|
+
return sha256Hex(traceId).slice(0, 32);
|
|
92
|
+
}
|
|
93
|
+
function parseTraceparent(value) {
|
|
94
|
+
if (typeof value !== "string") return null;
|
|
95
|
+
const header = value.trim();
|
|
96
|
+
const match = TRACEPARENT_RE.exec(header);
|
|
97
|
+
if (!match) return null;
|
|
98
|
+
const [, version, traceId, parentId, flagsHex, rest] = match;
|
|
99
|
+
if (version === "ff") return null;
|
|
100
|
+
if (version === "00" && rest !== void 0) return null;
|
|
101
|
+
if (traceId === ZERO_TRACE_ID || parentId === ZERO_SPAN_ID) return null;
|
|
102
|
+
const flags = parseInt(flagsHex, 16);
|
|
103
|
+
return { version, traceId, parentId, flags, sampled: (flags & 1) === 1 };
|
|
104
|
+
}
|
|
105
|
+
var STORE_KEY = /* @__PURE__ */ Symbol.for("@fabbahiense/pulsar.trace-context.v1");
|
|
106
|
+
function sharedStore() {
|
|
107
|
+
const g = globalThis;
|
|
108
|
+
let store = g[STORE_KEY];
|
|
109
|
+
if (!store) {
|
|
110
|
+
store = new import_node_async_hooks.AsyncLocalStorage();
|
|
111
|
+
g[STORE_KEY] = store;
|
|
112
|
+
}
|
|
113
|
+
return store;
|
|
114
|
+
}
|
|
115
|
+
var traceStore = sharedStore();
|
|
116
|
+
function text(value) {
|
|
117
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
118
|
+
}
|
|
119
|
+
function completeContext(init) {
|
|
120
|
+
const ctx = {
|
|
121
|
+
traceId: text(init?.traceId) ?? newTraceId(),
|
|
122
|
+
spanId: text(init?.spanId) ?? newSpanId(),
|
|
123
|
+
sampled: init?.sampled !== false
|
|
124
|
+
};
|
|
125
|
+
const parentSpanId = text(init?.parentSpanId);
|
|
126
|
+
if (parentSpanId) ctx.parentSpanId = parentSpanId;
|
|
127
|
+
const traceState = text(init?.traceState);
|
|
128
|
+
if (traceState) ctx.traceState = traceState;
|
|
129
|
+
const method = text(init?.method);
|
|
130
|
+
if (method) ctx.method = method;
|
|
131
|
+
const url = text(init?.url);
|
|
132
|
+
if (url) ctx.url = url;
|
|
133
|
+
return ctx;
|
|
134
|
+
}
|
|
135
|
+
function childContext(parent) {
|
|
136
|
+
if (!parent || !text(parent.traceId)) return completeContext(null);
|
|
137
|
+
const local = parent;
|
|
138
|
+
return completeContext({
|
|
139
|
+
traceId: parent.traceId,
|
|
140
|
+
parentSpanId: parent.spanId,
|
|
141
|
+
sampled: parent.sampled,
|
|
142
|
+
traceState: parent.traceState,
|
|
143
|
+
method: local.method,
|
|
144
|
+
url: local.url
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function read(carrier, name) {
|
|
148
|
+
let value;
|
|
149
|
+
if (typeof carrier.get === "function") {
|
|
150
|
+
value = carrier.get(name);
|
|
151
|
+
} else {
|
|
152
|
+
const record = carrier;
|
|
153
|
+
value = record[name];
|
|
154
|
+
if (value === void 0) {
|
|
155
|
+
for (const key of Object.keys(record)) {
|
|
156
|
+
if (key.toLowerCase() === name) {
|
|
157
|
+
value = record[key];
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (Array.isArray(value)) value = value.find((v) => typeof v === "string");
|
|
164
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
165
|
+
}
|
|
166
|
+
function readTraceState(carrier) {
|
|
167
|
+
let raw;
|
|
168
|
+
if (typeof carrier.get === "function") {
|
|
169
|
+
raw = carrier.get(TRACESTATE_HEADER);
|
|
170
|
+
} else {
|
|
171
|
+
const record = carrier;
|
|
172
|
+
raw = record[TRACESTATE_HEADER];
|
|
173
|
+
if (raw === void 0) {
|
|
174
|
+
const key = Object.keys(record).find((k) => k.toLowerCase() === TRACESTATE_HEADER);
|
|
175
|
+
raw = key === void 0 ? void 0 : record[key];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const joined = Array.isArray(raw) ? raw.filter((v) => typeof v === "string").join(",") : raw;
|
|
179
|
+
if (typeof joined !== "string") return void 0;
|
|
180
|
+
const value = joined.trim();
|
|
181
|
+
return value.length > 0 && value.length <= MAX_TRACESTATE_CHARS ? value : void 0;
|
|
182
|
+
}
|
|
183
|
+
function extractTrace(carrier) {
|
|
184
|
+
if (typeof carrier !== "object" || carrier === null) return null;
|
|
185
|
+
try {
|
|
186
|
+
const source = carrier;
|
|
187
|
+
const legacy = read(source, TRACE_ID_HEADER);
|
|
188
|
+
const parent = parseTraceparent(read(source, TRACEPARENT_HEADER));
|
|
189
|
+
if (legacy !== void 0) {
|
|
190
|
+
if (parent && parent.traceId === toW3CTraceId(legacy)) {
|
|
191
|
+
return withTraceState({ traceId: legacy, spanId: parent.parentId, sampled: parent.sampled }, source);
|
|
192
|
+
}
|
|
193
|
+
return { traceId: legacy, sampled: true };
|
|
194
|
+
}
|
|
195
|
+
if (parent) return withTraceState({ traceId: parent.traceId, spanId: parent.parentId, sampled: parent.sampled }, source);
|
|
196
|
+
return null;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function withTraceState(trace, carrier) {
|
|
202
|
+
const traceState = readTraceState(carrier);
|
|
203
|
+
if (traceState) trace.traceState = traceState;
|
|
204
|
+
return trace;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/middleware.ts
|
|
208
|
+
var DEFAULT_SKIP_URLS = ["/health", "/metrics", "/favicon.ico"];
|
|
209
|
+
var DEFAULT_MAX_BODY = 5e4;
|
|
210
|
+
function pulsarRequestLogger(options) {
|
|
211
|
+
const logger = options.logger;
|
|
212
|
+
if (!logger) throw new Error("pulsarRequestLogger: options.logger is required");
|
|
213
|
+
const skipUrls = options.skipUrls ?? DEFAULT_SKIP_URLS;
|
|
214
|
+
const redactKeys = redactKeyList(options.redactKeys ?? []);
|
|
215
|
+
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY;
|
|
216
|
+
const getUserId = options.getUserId ?? ((req) => req.user?.id);
|
|
217
|
+
const getLevel = options.getLevel ?? defaultLevel;
|
|
218
|
+
return function pulsarMw(req, res, next) {
|
|
219
|
+
const url = req.originalUrl || req.url;
|
|
220
|
+
const ctx = requestContext(req, url);
|
|
221
|
+
if (skipUrls.some((s) => url.includes(s))) return traceStore.run(ctx, next);
|
|
222
|
+
const startedAt = Date.now();
|
|
223
|
+
const capture = new BodyCapture(maxBodySize);
|
|
224
|
+
const originalWrite = res.write.bind(res);
|
|
225
|
+
const originalEnd = res.end.bind(res);
|
|
226
|
+
res.write = function(chunk, ...rest) {
|
|
227
|
+
capture.add(chunk, rest[0]);
|
|
228
|
+
return originalWrite(chunk, ...rest);
|
|
229
|
+
};
|
|
230
|
+
res.end = function(chunk, ...rest) {
|
|
231
|
+
capture.add(chunk, rest[0]);
|
|
232
|
+
return originalEnd(chunk, ...rest);
|
|
233
|
+
};
|
|
234
|
+
res.on("finish", () => traceStore.run(ctx, () => {
|
|
235
|
+
try {
|
|
236
|
+
const responseTime = Date.now() - startedAt;
|
|
237
|
+
const status = res.statusCode;
|
|
238
|
+
const level = getLevel(req, res);
|
|
239
|
+
const responseBody = readBuffer(capture, maxBodySize, getResponseContentType(res), redactKeys);
|
|
240
|
+
const payload = {
|
|
241
|
+
// Top level: becomes columns and filters
|
|
242
|
+
req: { method: req.method, url, id: req.id },
|
|
243
|
+
res: { statusCode: status },
|
|
244
|
+
responseTime,
|
|
245
|
+
// A request id set later (pino-http registered after this middleware) still wins, as it always has.
|
|
246
|
+
traceId: req.id || ctx.traceId,
|
|
247
|
+
// The request's span-end entry.
|
|
248
|
+
spanId: ctx.spanId,
|
|
249
|
+
...ctx.parentSpanId ? { parentSpanId: ctx.parentSpanId } : {},
|
|
250
|
+
spanKind: "server",
|
|
251
|
+
// metadata: the sections of the side panel
|
|
252
|
+
metadata: trimUndefined({
|
|
253
|
+
requestBody: redactDeep(req.body, redactKeys),
|
|
254
|
+
responseBody,
|
|
255
|
+
requestHeaders: redactHeaders(req.headers, redactKeys),
|
|
256
|
+
responseHeaders: redactHeaders(res.getHeaders ? res.getHeaders() : {}, redactKeys),
|
|
257
|
+
query: isPopulatedObject(req.query) ? req.query : void 0,
|
|
258
|
+
params: isPopulatedObject(req.params) ? req.params : void 0,
|
|
259
|
+
userId: getUserId(req)
|
|
260
|
+
})
|
|
261
|
+
};
|
|
262
|
+
const msg = `${req.method} ${url} ${status}`;
|
|
263
|
+
logger[level](payload, msg);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (typeof console !== "undefined") console.error("[pulsarRequestLogger] failed:", err);
|
|
266
|
+
}
|
|
267
|
+
}));
|
|
268
|
+
traceStore.run(ctx, next);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function requestContext(req, url) {
|
|
272
|
+
let parent = extractTrace(req.headers);
|
|
273
|
+
const id = req.id;
|
|
274
|
+
if (id !== void 0 && id !== null && id !== "") {
|
|
275
|
+
const traceId = String(id);
|
|
276
|
+
if (!parent || parent.traceId !== traceId) parent = { traceId, sampled: parent?.sampled ?? true };
|
|
277
|
+
}
|
|
278
|
+
const ctx = childContext(parent);
|
|
279
|
+
ctx.method = req.method;
|
|
280
|
+
ctx.url = url;
|
|
281
|
+
return ctx;
|
|
282
|
+
}
|
|
283
|
+
function defaultLevel(_req, res) {
|
|
284
|
+
if (res.statusCode >= 500) return "error";
|
|
285
|
+
if (res.statusCode >= 400) return "warn";
|
|
286
|
+
return "info";
|
|
287
|
+
}
|
|
288
|
+
function toBuffer(chunk, encoding) {
|
|
289
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
290
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
291
|
+
if (typeof chunk === "string") {
|
|
292
|
+
return typeof encoding === "string" && Buffer.isEncoding(encoding) ? Buffer.from(chunk, encoding) : Buffer.from(chunk);
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
return Buffer.from(JSON.stringify(chunk));
|
|
296
|
+
} catch {
|
|
297
|
+
return Buffer.alloc(0);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
var BodyCapture = class {
|
|
301
|
+
constructor(max) {
|
|
302
|
+
this.max = max;
|
|
303
|
+
}
|
|
304
|
+
max;
|
|
305
|
+
chunks = [];
|
|
306
|
+
kept = 0;
|
|
307
|
+
total = 0;
|
|
308
|
+
add(chunk, encoding) {
|
|
309
|
+
if (!chunk || typeof chunk === "function") return;
|
|
310
|
+
try {
|
|
311
|
+
const buf = toBuffer(chunk, encoding);
|
|
312
|
+
this.total += buf.length;
|
|
313
|
+
const room = this.max + 1 - this.kept;
|
|
314
|
+
if (room <= 0) return;
|
|
315
|
+
const part = Buffer.from(buf.length > room ? buf.subarray(0, room) : buf);
|
|
316
|
+
this.chunks.push(part);
|
|
317
|
+
this.kept += part.length;
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
bytes() {
|
|
322
|
+
return Buffer.concat(this.chunks);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function readBuffer(capture, max, contentType, redactKeys) {
|
|
326
|
+
if (capture.total === 0) return void 0;
|
|
327
|
+
const buf = capture.bytes();
|
|
328
|
+
if (capture.total > max) {
|
|
329
|
+
return `[TRUNCATED ${capture.total}B > ${max}B] ${buf.subarray(0, max).toString("utf8")}\u2026`;
|
|
330
|
+
}
|
|
331
|
+
const raw = buf.toString("utf8");
|
|
332
|
+
const ct = contentType.toLowerCase();
|
|
333
|
+
if (ct.includes("application/json")) {
|
|
334
|
+
try {
|
|
335
|
+
return redactDeep(JSON.parse(raw), redactKeys);
|
|
336
|
+
} catch {
|
|
337
|
+
return raw;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return raw;
|
|
341
|
+
}
|
|
342
|
+
function getResponseContentType(res) {
|
|
343
|
+
if (!res.getHeaders) return "";
|
|
344
|
+
const headers = res.getHeaders();
|
|
345
|
+
const v = headers["content-type"] ?? headers["Content-Type"];
|
|
346
|
+
if (typeof v === "string") return v;
|
|
347
|
+
if (Array.isArray(v)) return v.join(", ");
|
|
348
|
+
return "";
|
|
349
|
+
}
|
|
350
|
+
function redactHeaders(headers, redactKeys) {
|
|
351
|
+
const out = {};
|
|
352
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
353
|
+
if (v === void 0 || v === null) continue;
|
|
354
|
+
out[k] = isRedactedKey(k, redactKeys) ? "[REDACTED]" : Array.isArray(v) ? v.join(", ") : String(v);
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
function redactDeep(value, redactKeys, seen = /* @__PURE__ */ new WeakSet()) {
|
|
359
|
+
if (value === null || value === void 0) return value;
|
|
360
|
+
if (typeof value !== "object") return value;
|
|
361
|
+
if (seen.has(value)) return "[Circular]";
|
|
362
|
+
seen.add(value);
|
|
363
|
+
try {
|
|
364
|
+
if (Array.isArray(value)) return value.map((v) => redactDeep(v, redactKeys, seen));
|
|
365
|
+
const out = {};
|
|
366
|
+
for (const [k, v] of Object.entries(value)) {
|
|
367
|
+
out[k] = isRedactedKey(k, redactKeys) ? "[REDACTED]" : redactDeep(v, redactKeys, seen);
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
} finally {
|
|
371
|
+
seen.delete(value);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function trimUndefined(obj) {
|
|
375
|
+
const out = {};
|
|
376
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
377
|
+
if (v !== void 0) out[k] = v;
|
|
378
|
+
}
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
function isPopulatedObject(v) {
|
|
382
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length > 0;
|
|
383
|
+
}
|
|
384
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
385
|
+
0 && (module.exports = {
|
|
386
|
+
pulsarRequestLogger
|
|
387
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware that logs every request to Pulsar with its full payload:
|
|
3
|
+
* one log per request, with method, URL, status, duration, headers, bodies,
|
|
4
|
+
* query and params (redacted and size-capped). Each request runs in its own
|
|
5
|
+
* span, continuing an inbound `x-trace-id` or W3C `traceparent`; with
|
|
6
|
+
* `pulsarMixin()` on the logger, every record logged during the request
|
|
7
|
+
* carries the span's ids.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import pino from 'pino'
|
|
12
|
+
* import { pulsarMixin } from '@fabbahiense/pulsar-pino-transport'
|
|
13
|
+
* import { pulsarRequestLogger } from '@fabbahiense/pulsar-pino-transport/middleware'
|
|
14
|
+
*
|
|
15
|
+
* const logger = pino({
|
|
16
|
+
* mixin: pulsarMixin(),
|
|
17
|
+
* transport: {
|
|
18
|
+
* target: '@fabbahiense/pulsar-pino-transport',
|
|
19
|
+
* options: { url: process.env.PULSAR_URL, apiKey: process.env.PULSAR_API_KEY },
|
|
20
|
+
* },
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* const app = express()
|
|
24
|
+
* app.use(express.json())
|
|
25
|
+
* app.use(pulsarRequestLogger({ logger }))
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
interface PinoLikeLogger {
|
|
29
|
+
info: (obj: unknown, msg?: string) => void;
|
|
30
|
+
warn: (obj: unknown, msg?: string) => void;
|
|
31
|
+
error: (obj: unknown, msg?: string) => void;
|
|
32
|
+
}
|
|
33
|
+
interface PulsarRequestLoggerOptions {
|
|
34
|
+
/** Your pino logger (with the Pulsar transport configured). */
|
|
35
|
+
logger: PinoLikeLogger;
|
|
36
|
+
/** URL substrings that must NOT produce a log. Default: /health, /metrics, /favicon.ico */
|
|
37
|
+
skipUrls?: string[];
|
|
38
|
+
/** Extra keys to redact in headers and bodies, on top of the defaults (passwords, tokens, secrets, API keys, cookies, auth headers). Matching ignores case, `-` and `_`, so `apiKey` also matches `api_key` and `api-key`. */
|
|
39
|
+
redactKeys?: string[];
|
|
40
|
+
/** Max bytes kept per request/response body; bigger bodies are truncated. Default: 50000 (50 KB) */
|
|
41
|
+
maxBodySize?: number;
|
|
42
|
+
/** Extracts the user id from the request, sent as `metadata.userId`. */
|
|
43
|
+
getUserId?: (req: ExpressLikeRequest) => string | undefined;
|
|
44
|
+
/** Overrides the level (default: 5xx=error, 4xx=warn, otherwise info). */
|
|
45
|
+
getLevel?: (req: ExpressLikeRequest, res: ExpressLikeResponse) => 'info' | 'warn' | 'error';
|
|
46
|
+
}
|
|
47
|
+
interface ExpressLikeRequest {
|
|
48
|
+
method: string;
|
|
49
|
+
url: string;
|
|
50
|
+
originalUrl?: string;
|
|
51
|
+
headers: Record<string, string | string[] | undefined>;
|
|
52
|
+
body?: unknown;
|
|
53
|
+
query?: Record<string, unknown>;
|
|
54
|
+
params?: Record<string, unknown>;
|
|
55
|
+
id?: string;
|
|
56
|
+
user?: {
|
|
57
|
+
id?: string;
|
|
58
|
+
};
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
interface ExpressLikeResponse {
|
|
62
|
+
statusCode: number;
|
|
63
|
+
write: (...args: unknown[]) => boolean;
|
|
64
|
+
end: (...args: unknown[]) => unknown;
|
|
65
|
+
getHeaders?: () => Record<string, string | string[] | number | undefined>;
|
|
66
|
+
on: (event: string, handler: () => void) => void;
|
|
67
|
+
}
|
|
68
|
+
declare function pulsarRequestLogger(options: PulsarRequestLoggerOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void;
|
|
69
|
+
|
|
70
|
+
export { type ExpressLikeRequest, type ExpressLikeResponse, type PinoLikeLogger, type PulsarRequestLoggerOptions, pulsarRequestLogger };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware that logs every request to Pulsar with its full payload:
|
|
3
|
+
* one log per request, with method, URL, status, duration, headers, bodies,
|
|
4
|
+
* query and params (redacted and size-capped). Each request runs in its own
|
|
5
|
+
* span, continuing an inbound `x-trace-id` or W3C `traceparent`; with
|
|
6
|
+
* `pulsarMixin()` on the logger, every record logged during the request
|
|
7
|
+
* carries the span's ids.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import pino from 'pino'
|
|
12
|
+
* import { pulsarMixin } from '@fabbahiense/pulsar-pino-transport'
|
|
13
|
+
* import { pulsarRequestLogger } from '@fabbahiense/pulsar-pino-transport/middleware'
|
|
14
|
+
*
|
|
15
|
+
* const logger = pino({
|
|
16
|
+
* mixin: pulsarMixin(),
|
|
17
|
+
* transport: {
|
|
18
|
+
* target: '@fabbahiense/pulsar-pino-transport',
|
|
19
|
+
* options: { url: process.env.PULSAR_URL, apiKey: process.env.PULSAR_API_KEY },
|
|
20
|
+
* },
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* const app = express()
|
|
24
|
+
* app.use(express.json())
|
|
25
|
+
* app.use(pulsarRequestLogger({ logger }))
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
interface PinoLikeLogger {
|
|
29
|
+
info: (obj: unknown, msg?: string) => void;
|
|
30
|
+
warn: (obj: unknown, msg?: string) => void;
|
|
31
|
+
error: (obj: unknown, msg?: string) => void;
|
|
32
|
+
}
|
|
33
|
+
interface PulsarRequestLoggerOptions {
|
|
34
|
+
/** Your pino logger (with the Pulsar transport configured). */
|
|
35
|
+
logger: PinoLikeLogger;
|
|
36
|
+
/** URL substrings that must NOT produce a log. Default: /health, /metrics, /favicon.ico */
|
|
37
|
+
skipUrls?: string[];
|
|
38
|
+
/** Extra keys to redact in headers and bodies, on top of the defaults (passwords, tokens, secrets, API keys, cookies, auth headers). Matching ignores case, `-` and `_`, so `apiKey` also matches `api_key` and `api-key`. */
|
|
39
|
+
redactKeys?: string[];
|
|
40
|
+
/** Max bytes kept per request/response body; bigger bodies are truncated. Default: 50000 (50 KB) */
|
|
41
|
+
maxBodySize?: number;
|
|
42
|
+
/** Extracts the user id from the request, sent as `metadata.userId`. */
|
|
43
|
+
getUserId?: (req: ExpressLikeRequest) => string | undefined;
|
|
44
|
+
/** Overrides the level (default: 5xx=error, 4xx=warn, otherwise info). */
|
|
45
|
+
getLevel?: (req: ExpressLikeRequest, res: ExpressLikeResponse) => 'info' | 'warn' | 'error';
|
|
46
|
+
}
|
|
47
|
+
interface ExpressLikeRequest {
|
|
48
|
+
method: string;
|
|
49
|
+
url: string;
|
|
50
|
+
originalUrl?: string;
|
|
51
|
+
headers: Record<string, string | string[] | undefined>;
|
|
52
|
+
body?: unknown;
|
|
53
|
+
query?: Record<string, unknown>;
|
|
54
|
+
params?: Record<string, unknown>;
|
|
55
|
+
id?: string;
|
|
56
|
+
user?: {
|
|
57
|
+
id?: string;
|
|
58
|
+
};
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
interface ExpressLikeResponse {
|
|
62
|
+
statusCode: number;
|
|
63
|
+
write: (...args: unknown[]) => boolean;
|
|
64
|
+
end: (...args: unknown[]) => unknown;
|
|
65
|
+
getHeaders?: () => Record<string, string | string[] | number | undefined>;
|
|
66
|
+
on: (event: string, handler: () => void) => void;
|
|
67
|
+
}
|
|
68
|
+
declare function pulsarRequestLogger(options: PulsarRequestLoggerOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void;
|
|
69
|
+
|
|
70
|
+
export { type ExpressLikeRequest, type ExpressLikeResponse, type PinoLikeLogger, type PulsarRequestLoggerOptions, pulsarRequestLogger };
|