@fabbahiense/pulsar-pino-transport 0.2.0 → 0.3.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/dist/index.cjs +24 -1
- package/dist/index.js +24 -1
- package/dist/middleware.cjs +161 -0
- package/dist/middleware.d.cts +63 -0
- package/dist/middleware.d.ts +63 -0
- package/dist/middleware.js +136 -0
- package/package.json +12 -6
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,23 @@ var GENERIC_HTTP_MSGS = /* @__PURE__ */ new Set([
|
|
|
42
42
|
"incoming request",
|
|
43
43
|
"http request"
|
|
44
44
|
]);
|
|
45
|
+
var PINO_INTERNAL_FIELDS = /* @__PURE__ */ new Set([
|
|
46
|
+
"level",
|
|
47
|
+
"msg",
|
|
48
|
+
"time",
|
|
49
|
+
"v",
|
|
50
|
+
"pid",
|
|
51
|
+
"hostname",
|
|
52
|
+
"name",
|
|
53
|
+
"traceId",
|
|
54
|
+
"trace_id",
|
|
55
|
+
"requestId",
|
|
56
|
+
"req",
|
|
57
|
+
"res",
|
|
58
|
+
"responseTime",
|
|
59
|
+
"err",
|
|
60
|
+
"metadata"
|
|
61
|
+
]);
|
|
45
62
|
function pickFirst(obj, keys) {
|
|
46
63
|
for (const k of keys) {
|
|
47
64
|
if (obj[k] !== void 0 && obj[k] !== null) return obj[k];
|
|
@@ -164,7 +181,13 @@ async function index_default(options) {
|
|
|
164
181
|
}
|
|
165
182
|
const extracted = extractPayloads(obj);
|
|
166
183
|
const baseMeta = isObj(obj.metadata) ? obj.metadata : {};
|
|
167
|
-
const
|
|
184
|
+
const customFields = {};
|
|
185
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
186
|
+
if (!PINO_INTERNAL_FIELDS.has(key) && value !== void 0 && value !== null) {
|
|
187
|
+
customFields[key] = value;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const merged = { ...baseMeta, ...customFields, ...extracted };
|
|
168
191
|
const metadata = Object.keys(merged).length > 0 ? merged : null;
|
|
169
192
|
buffer.push({
|
|
170
193
|
level: LEVEL_MAP[obj.level] || "info",
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,23 @@ var GENERIC_HTTP_MSGS = /* @__PURE__ */ new Set([
|
|
|
7
7
|
"incoming request",
|
|
8
8
|
"http request"
|
|
9
9
|
]);
|
|
10
|
+
var PINO_INTERNAL_FIELDS = /* @__PURE__ */ new Set([
|
|
11
|
+
"level",
|
|
12
|
+
"msg",
|
|
13
|
+
"time",
|
|
14
|
+
"v",
|
|
15
|
+
"pid",
|
|
16
|
+
"hostname",
|
|
17
|
+
"name",
|
|
18
|
+
"traceId",
|
|
19
|
+
"trace_id",
|
|
20
|
+
"requestId",
|
|
21
|
+
"req",
|
|
22
|
+
"res",
|
|
23
|
+
"responseTime",
|
|
24
|
+
"err",
|
|
25
|
+
"metadata"
|
|
26
|
+
]);
|
|
10
27
|
function pickFirst(obj, keys) {
|
|
11
28
|
for (const k of keys) {
|
|
12
29
|
if (obj[k] !== void 0 && obj[k] !== null) return obj[k];
|
|
@@ -129,7 +146,13 @@ async function index_default(options) {
|
|
|
129
146
|
}
|
|
130
147
|
const extracted = extractPayloads(obj);
|
|
131
148
|
const baseMeta = isObj(obj.metadata) ? obj.metadata : {};
|
|
132
|
-
const
|
|
149
|
+
const customFields = {};
|
|
150
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
151
|
+
if (!PINO_INTERNAL_FIELDS.has(key) && value !== void 0 && value !== null) {
|
|
152
|
+
customFields[key] = value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const merged = { ...baseMeta, ...customFields, ...extracted };
|
|
133
156
|
const metadata = Object.keys(merged).length > 0 ? merged : null;
|
|
134
157
|
buffer.push({
|
|
135
158
|
level: LEVEL_MAP[obj.level] || "info",
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
var DEFAULT_SKIP_URLS = ["/health", "/metrics", "/favicon.ico"];
|
|
27
|
+
var DEFAULT_REDACT = ["authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token"];
|
|
28
|
+
var DEFAULT_MAX_BODY = 5e4;
|
|
29
|
+
function pulsarRequestLogger(options) {
|
|
30
|
+
const logger = options.logger;
|
|
31
|
+
if (!logger) throw new Error("pulsarRequestLogger: options.logger is required");
|
|
32
|
+
const skipUrls = options.skipUrls ?? DEFAULT_SKIP_URLS;
|
|
33
|
+
const redactKeys = [...DEFAULT_REDACT, ...options.redactKeys ?? []].map((k) => k.toLowerCase());
|
|
34
|
+
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY;
|
|
35
|
+
const getUserId = options.getUserId ?? ((req) => req.user?.id);
|
|
36
|
+
const getLevel = options.getLevel ?? defaultLevel;
|
|
37
|
+
return function pulsarMw(req, res, next) {
|
|
38
|
+
const url = req.originalUrl || req.url;
|
|
39
|
+
if (skipUrls.some((s) => url.includes(s))) return next();
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
const chunks = [];
|
|
42
|
+
const originalWrite = res.write.bind(res);
|
|
43
|
+
const originalEnd = res.end.bind(res);
|
|
44
|
+
res.write = function(chunk, ...rest) {
|
|
45
|
+
try {
|
|
46
|
+
if (chunk) chunks.push(toBuffer(chunk));
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
return originalWrite(chunk, ...rest);
|
|
50
|
+
};
|
|
51
|
+
res.end = function(chunk, ...rest) {
|
|
52
|
+
try {
|
|
53
|
+
if (chunk) chunks.push(toBuffer(chunk));
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
return originalEnd(chunk, ...rest);
|
|
57
|
+
};
|
|
58
|
+
res.on("finish", () => {
|
|
59
|
+
try {
|
|
60
|
+
const responseTime = Date.now() - startedAt;
|
|
61
|
+
const status = res.statusCode;
|
|
62
|
+
const level = getLevel(req, res);
|
|
63
|
+
const responseBody = readBuffer(chunks, maxBodySize, getResponseContentType(res));
|
|
64
|
+
const payload = {
|
|
65
|
+
// Top-level → colunas/filtros
|
|
66
|
+
req: { method: req.method, url, id: req.id },
|
|
67
|
+
res: { statusCode: status },
|
|
68
|
+
responseTime,
|
|
69
|
+
traceId: req.id || (typeof req.headers["x-trace-id"] === "string" ? req.headers["x-trace-id"] : void 0),
|
|
70
|
+
// metadata → seções coloridas no painel lateral
|
|
71
|
+
metadata: trimUndefined({
|
|
72
|
+
requestBody: redactDeep(req.body, redactKeys),
|
|
73
|
+
responseBody,
|
|
74
|
+
requestHeaders: redactHeaders(req.headers, redactKeys),
|
|
75
|
+
responseHeaders: redactHeaders(res.getHeaders ? res.getHeaders() : {}, redactKeys),
|
|
76
|
+
query: isPopulatedObject(req.query) ? req.query : void 0,
|
|
77
|
+
params: isPopulatedObject(req.params) ? req.params : void 0,
|
|
78
|
+
userId: getUserId(req)
|
|
79
|
+
})
|
|
80
|
+
};
|
|
81
|
+
const msg = `${req.method} ${url} ${status}`;
|
|
82
|
+
logger[level](payload, msg);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (typeof console !== "undefined") console.error("[pulsarRequestLogger] failed:", err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
next();
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function defaultLevel(_req, res) {
|
|
91
|
+
if (res.statusCode >= 500) return "error";
|
|
92
|
+
if (res.statusCode >= 400) return "warn";
|
|
93
|
+
return "info";
|
|
94
|
+
}
|
|
95
|
+
function toBuffer(chunk) {
|
|
96
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
97
|
+
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
98
|
+
try {
|
|
99
|
+
return Buffer.from(JSON.stringify(chunk));
|
|
100
|
+
} catch {
|
|
101
|
+
return Buffer.alloc(0);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function readBuffer(chunks, max, contentType) {
|
|
105
|
+
if (chunks.length === 0) return void 0;
|
|
106
|
+
const buf = Buffer.concat(chunks);
|
|
107
|
+
if (buf.length === 0) return void 0;
|
|
108
|
+
if (buf.length > max) {
|
|
109
|
+
return `[TRUNCATED ${buf.length}B > ${max}B] ${buf.subarray(0, max).toString("utf8")}\u2026`;
|
|
110
|
+
}
|
|
111
|
+
const raw = buf.toString("utf8");
|
|
112
|
+
const ct = contentType.toLowerCase();
|
|
113
|
+
if (ct.includes("application/json")) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(raw);
|
|
116
|
+
} catch {
|
|
117
|
+
return raw;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return raw;
|
|
121
|
+
}
|
|
122
|
+
function getResponseContentType(res) {
|
|
123
|
+
if (!res.getHeaders) return "";
|
|
124
|
+
const headers = res.getHeaders();
|
|
125
|
+
const v = headers["content-type"] ?? headers["Content-Type"];
|
|
126
|
+
if (typeof v === "string") return v;
|
|
127
|
+
if (Array.isArray(v)) return v.join(", ");
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
function redactHeaders(headers, redactKeys) {
|
|
131
|
+
const out = {};
|
|
132
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
133
|
+
if (v === void 0 || v === null) continue;
|
|
134
|
+
out[k] = redactKeys.includes(k.toLowerCase()) ? "[REDACTED]" : Array.isArray(v) ? v.join(", ") : String(v);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function redactDeep(value, redactKeys) {
|
|
139
|
+
if (value === null || value === void 0) return value;
|
|
140
|
+
if (typeof value !== "object") return value;
|
|
141
|
+
if (Array.isArray(value)) return value.map((v) => redactDeep(v, redactKeys));
|
|
142
|
+
const out = {};
|
|
143
|
+
for (const [k, v] of Object.entries(value)) {
|
|
144
|
+
out[k] = redactKeys.includes(k.toLowerCase()) ? "[REDACTED]" : redactDeep(v, redactKeys);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function trimUndefined(obj) {
|
|
149
|
+
const out = {};
|
|
150
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
151
|
+
if (v !== void 0) out[k] = v;
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function isPopulatedObject(v) {
|
|
156
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length > 0;
|
|
157
|
+
}
|
|
158
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
159
|
+
0 && (module.exports = {
|
|
160
|
+
pulsarRequestLogger
|
|
161
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware que loga cada request com payload completo no Pulsar.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import pino from 'pino'
|
|
7
|
+
* import { pulsarRequestLogger } from '@fabbahiense/pulsar-pino-transport/middleware'
|
|
8
|
+
*
|
|
9
|
+
* const logger = pino({
|
|
10
|
+
* transport: {
|
|
11
|
+
* target: '@fabbahiense/pulsar-pino-transport',
|
|
12
|
+
* options: { url: process.env.PULSAR_URL, apiKey: process.env.PULSAR_API_KEY },
|
|
13
|
+
* },
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* const app = express()
|
|
17
|
+
* app.use(express.json())
|
|
18
|
+
* app.use(pulsarRequestLogger({ logger }))
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
interface PinoLikeLogger {
|
|
22
|
+
info: (obj: unknown, msg?: string) => void;
|
|
23
|
+
warn: (obj: unknown, msg?: string) => void;
|
|
24
|
+
error: (obj: unknown, msg?: string) => void;
|
|
25
|
+
}
|
|
26
|
+
interface PulsarRequestLoggerOptions {
|
|
27
|
+
/** Your pino logger (com o transport do Pulsar configurado). */
|
|
28
|
+
logger: PinoLikeLogger;
|
|
29
|
+
/** URLs (substring match) que NÃO devem gerar log. Default: /health, /metrics, /favicon */
|
|
30
|
+
skipUrls?: string[];
|
|
31
|
+
/** Chaves a redactar em headers e bodies (case-insensitive). */
|
|
32
|
+
redactKeys?: string[];
|
|
33
|
+
/** Tamanho máximo em bytes pra request/response body. Bigger = truncado. Default: 50000 (50KB) */
|
|
34
|
+
maxBodySize?: number;
|
|
35
|
+
/** Extrai o id do user do req pra mandar como `metadata.userId`. */
|
|
36
|
+
getUserId?: (req: ExpressLikeRequest) => string | undefined;
|
|
37
|
+
/** Sobrescreve o cálculo de level (default: 5xx=error, 4xx=warn, resto=info). */
|
|
38
|
+
getLevel?: (req: ExpressLikeRequest, res: ExpressLikeResponse) => 'info' | 'warn' | 'error';
|
|
39
|
+
}
|
|
40
|
+
interface ExpressLikeRequest {
|
|
41
|
+
method: string;
|
|
42
|
+
url: string;
|
|
43
|
+
originalUrl?: string;
|
|
44
|
+
headers: Record<string, string | string[] | undefined>;
|
|
45
|
+
body?: unknown;
|
|
46
|
+
query?: Record<string, unknown>;
|
|
47
|
+
params?: Record<string, unknown>;
|
|
48
|
+
id?: string;
|
|
49
|
+
user?: {
|
|
50
|
+
id?: string;
|
|
51
|
+
};
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
interface ExpressLikeResponse {
|
|
55
|
+
statusCode: number;
|
|
56
|
+
write: (...args: unknown[]) => boolean;
|
|
57
|
+
end: (...args: unknown[]) => unknown;
|
|
58
|
+
getHeaders?: () => Record<string, string | string[] | number | undefined>;
|
|
59
|
+
on: (event: string, handler: () => void) => void;
|
|
60
|
+
}
|
|
61
|
+
declare function pulsarRequestLogger(options: PulsarRequestLoggerOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void;
|
|
62
|
+
|
|
63
|
+
export { type ExpressLikeRequest, type ExpressLikeResponse, type PinoLikeLogger, type PulsarRequestLoggerOptions, pulsarRequestLogger };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware que loga cada request com payload completo no Pulsar.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import pino from 'pino'
|
|
7
|
+
* import { pulsarRequestLogger } from '@fabbahiense/pulsar-pino-transport/middleware'
|
|
8
|
+
*
|
|
9
|
+
* const logger = pino({
|
|
10
|
+
* transport: {
|
|
11
|
+
* target: '@fabbahiense/pulsar-pino-transport',
|
|
12
|
+
* options: { url: process.env.PULSAR_URL, apiKey: process.env.PULSAR_API_KEY },
|
|
13
|
+
* },
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* const app = express()
|
|
17
|
+
* app.use(express.json())
|
|
18
|
+
* app.use(pulsarRequestLogger({ logger }))
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
interface PinoLikeLogger {
|
|
22
|
+
info: (obj: unknown, msg?: string) => void;
|
|
23
|
+
warn: (obj: unknown, msg?: string) => void;
|
|
24
|
+
error: (obj: unknown, msg?: string) => void;
|
|
25
|
+
}
|
|
26
|
+
interface PulsarRequestLoggerOptions {
|
|
27
|
+
/** Your pino logger (com o transport do Pulsar configurado). */
|
|
28
|
+
logger: PinoLikeLogger;
|
|
29
|
+
/** URLs (substring match) que NÃO devem gerar log. Default: /health, /metrics, /favicon */
|
|
30
|
+
skipUrls?: string[];
|
|
31
|
+
/** Chaves a redactar em headers e bodies (case-insensitive). */
|
|
32
|
+
redactKeys?: string[];
|
|
33
|
+
/** Tamanho máximo em bytes pra request/response body. Bigger = truncado. Default: 50000 (50KB) */
|
|
34
|
+
maxBodySize?: number;
|
|
35
|
+
/** Extrai o id do user do req pra mandar como `metadata.userId`. */
|
|
36
|
+
getUserId?: (req: ExpressLikeRequest) => string | undefined;
|
|
37
|
+
/** Sobrescreve o cálculo de level (default: 5xx=error, 4xx=warn, resto=info). */
|
|
38
|
+
getLevel?: (req: ExpressLikeRequest, res: ExpressLikeResponse) => 'info' | 'warn' | 'error';
|
|
39
|
+
}
|
|
40
|
+
interface ExpressLikeRequest {
|
|
41
|
+
method: string;
|
|
42
|
+
url: string;
|
|
43
|
+
originalUrl?: string;
|
|
44
|
+
headers: Record<string, string | string[] | undefined>;
|
|
45
|
+
body?: unknown;
|
|
46
|
+
query?: Record<string, unknown>;
|
|
47
|
+
params?: Record<string, unknown>;
|
|
48
|
+
id?: string;
|
|
49
|
+
user?: {
|
|
50
|
+
id?: string;
|
|
51
|
+
};
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
interface ExpressLikeResponse {
|
|
55
|
+
statusCode: number;
|
|
56
|
+
write: (...args: unknown[]) => boolean;
|
|
57
|
+
end: (...args: unknown[]) => unknown;
|
|
58
|
+
getHeaders?: () => Record<string, string | string[] | number | undefined>;
|
|
59
|
+
on: (event: string, handler: () => void) => void;
|
|
60
|
+
}
|
|
61
|
+
declare function pulsarRequestLogger(options: PulsarRequestLoggerOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void;
|
|
62
|
+
|
|
63
|
+
export { type ExpressLikeRequest, type ExpressLikeResponse, type PinoLikeLogger, type PulsarRequestLoggerOptions, pulsarRequestLogger };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
var DEFAULT_SKIP_URLS = ["/health", "/metrics", "/favicon.ico"];
|
|
3
|
+
var DEFAULT_REDACT = ["authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token"];
|
|
4
|
+
var DEFAULT_MAX_BODY = 5e4;
|
|
5
|
+
function pulsarRequestLogger(options) {
|
|
6
|
+
const logger = options.logger;
|
|
7
|
+
if (!logger) throw new Error("pulsarRequestLogger: options.logger is required");
|
|
8
|
+
const skipUrls = options.skipUrls ?? DEFAULT_SKIP_URLS;
|
|
9
|
+
const redactKeys = [...DEFAULT_REDACT, ...options.redactKeys ?? []].map((k) => k.toLowerCase());
|
|
10
|
+
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY;
|
|
11
|
+
const getUserId = options.getUserId ?? ((req) => req.user?.id);
|
|
12
|
+
const getLevel = options.getLevel ?? defaultLevel;
|
|
13
|
+
return function pulsarMw(req, res, next) {
|
|
14
|
+
const url = req.originalUrl || req.url;
|
|
15
|
+
if (skipUrls.some((s) => url.includes(s))) return next();
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
const chunks = [];
|
|
18
|
+
const originalWrite = res.write.bind(res);
|
|
19
|
+
const originalEnd = res.end.bind(res);
|
|
20
|
+
res.write = function(chunk, ...rest) {
|
|
21
|
+
try {
|
|
22
|
+
if (chunk) chunks.push(toBuffer(chunk));
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
return originalWrite(chunk, ...rest);
|
|
26
|
+
};
|
|
27
|
+
res.end = function(chunk, ...rest) {
|
|
28
|
+
try {
|
|
29
|
+
if (chunk) chunks.push(toBuffer(chunk));
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
return originalEnd(chunk, ...rest);
|
|
33
|
+
};
|
|
34
|
+
res.on("finish", () => {
|
|
35
|
+
try {
|
|
36
|
+
const responseTime = Date.now() - startedAt;
|
|
37
|
+
const status = res.statusCode;
|
|
38
|
+
const level = getLevel(req, res);
|
|
39
|
+
const responseBody = readBuffer(chunks, maxBodySize, getResponseContentType(res));
|
|
40
|
+
const payload = {
|
|
41
|
+
// Top-level → colunas/filtros
|
|
42
|
+
req: { method: req.method, url, id: req.id },
|
|
43
|
+
res: { statusCode: status },
|
|
44
|
+
responseTime,
|
|
45
|
+
traceId: req.id || (typeof req.headers["x-trace-id"] === "string" ? req.headers["x-trace-id"] : void 0),
|
|
46
|
+
// metadata → seções coloridas no painel lateral
|
|
47
|
+
metadata: trimUndefined({
|
|
48
|
+
requestBody: redactDeep(req.body, redactKeys),
|
|
49
|
+
responseBody,
|
|
50
|
+
requestHeaders: redactHeaders(req.headers, redactKeys),
|
|
51
|
+
responseHeaders: redactHeaders(res.getHeaders ? res.getHeaders() : {}, redactKeys),
|
|
52
|
+
query: isPopulatedObject(req.query) ? req.query : void 0,
|
|
53
|
+
params: isPopulatedObject(req.params) ? req.params : void 0,
|
|
54
|
+
userId: getUserId(req)
|
|
55
|
+
})
|
|
56
|
+
};
|
|
57
|
+
const msg = `${req.method} ${url} ${status}`;
|
|
58
|
+
logger[level](payload, msg);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (typeof console !== "undefined") console.error("[pulsarRequestLogger] failed:", err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
next();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function defaultLevel(_req, res) {
|
|
67
|
+
if (res.statusCode >= 500) return "error";
|
|
68
|
+
if (res.statusCode >= 400) return "warn";
|
|
69
|
+
return "info";
|
|
70
|
+
}
|
|
71
|
+
function toBuffer(chunk) {
|
|
72
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
73
|
+
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
74
|
+
try {
|
|
75
|
+
return Buffer.from(JSON.stringify(chunk));
|
|
76
|
+
} catch {
|
|
77
|
+
return Buffer.alloc(0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function readBuffer(chunks, max, contentType) {
|
|
81
|
+
if (chunks.length === 0) return void 0;
|
|
82
|
+
const buf = Buffer.concat(chunks);
|
|
83
|
+
if (buf.length === 0) return void 0;
|
|
84
|
+
if (buf.length > max) {
|
|
85
|
+
return `[TRUNCATED ${buf.length}B > ${max}B] ${buf.subarray(0, max).toString("utf8")}\u2026`;
|
|
86
|
+
}
|
|
87
|
+
const raw = buf.toString("utf8");
|
|
88
|
+
const ct = contentType.toLowerCase();
|
|
89
|
+
if (ct.includes("application/json")) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(raw);
|
|
92
|
+
} catch {
|
|
93
|
+
return raw;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return raw;
|
|
97
|
+
}
|
|
98
|
+
function getResponseContentType(res) {
|
|
99
|
+
if (!res.getHeaders) return "";
|
|
100
|
+
const headers = res.getHeaders();
|
|
101
|
+
const v = headers["content-type"] ?? headers["Content-Type"];
|
|
102
|
+
if (typeof v === "string") return v;
|
|
103
|
+
if (Array.isArray(v)) return v.join(", ");
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
function redactHeaders(headers, redactKeys) {
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
109
|
+
if (v === void 0 || v === null) continue;
|
|
110
|
+
out[k] = redactKeys.includes(k.toLowerCase()) ? "[REDACTED]" : Array.isArray(v) ? v.join(", ") : String(v);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function redactDeep(value, redactKeys) {
|
|
115
|
+
if (value === null || value === void 0) return value;
|
|
116
|
+
if (typeof value !== "object") return value;
|
|
117
|
+
if (Array.isArray(value)) return value.map((v) => redactDeep(v, redactKeys));
|
|
118
|
+
const out = {};
|
|
119
|
+
for (const [k, v] of Object.entries(value)) {
|
|
120
|
+
out[k] = redactKeys.includes(k.toLowerCase()) ? "[REDACTED]" : redactDeep(v, redactKeys);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function trimUndefined(obj) {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
127
|
+
if (v !== void 0) out[k] = v;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function isPopulatedObject(v) {
|
|
132
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length > 0;
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
pulsarRequestLogger
|
|
136
|
+
};
|
package/package.json
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabbahiense/pulsar-pino-transport",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pino transport for Pulsar observability platform",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Pino transport for Pulsar observability platform + Express request logger middleware",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
11
12
|
"import": "./dist/index.js",
|
|
12
|
-
"require": "./dist/index.cjs"
|
|
13
|
-
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./middleware": {
|
|
16
|
+
"types": "./dist/middleware.d.ts",
|
|
17
|
+
"import": "./dist/middleware.js",
|
|
18
|
+
"require": "./dist/middleware.cjs"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"files": ["dist"],
|
|
17
22
|
"scripts": {
|
|
18
23
|
"build": "tsup",
|
|
19
|
-
"dev": "tsup --watch"
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"test": "node --test test/"
|
|
20
26
|
},
|
|
21
27
|
"peerDependencies": {
|
|
22
28
|
"pino-abstract-transport": ">=2.0.0"
|
|
@@ -26,7 +32,7 @@
|
|
|
26
32
|
"tsup": "^8.0.0",
|
|
27
33
|
"typescript": "^5.9.0"
|
|
28
34
|
},
|
|
29
|
-
"keywords": ["pino", "transport", "pulsar", "observability", "logging"],
|
|
35
|
+
"keywords": ["pino", "transport", "pulsar", "observability", "logging", "express", "middleware"],
|
|
30
36
|
"license": "MIT",
|
|
31
37
|
"engines": {
|
|
32
38
|
"node": ">=18.0.0"
|