@fabbahiense/pulsar-pino-transport 0.3.0 → 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.
@@ -1,49 +1,228 @@
1
+ // ../shared/src/redact.ts
2
+ var DEFAULT_REDACT_KEYS = [
3
+ "password",
4
+ "passwd",
5
+ "password_hash",
6
+ "secret",
7
+ "client_secret",
8
+ "token",
9
+ "access_token",
10
+ "refresh_token",
11
+ "id_token",
12
+ "session_id",
13
+ "api_key",
14
+ "x-api-key",
15
+ "x-auth-token",
16
+ "authorization",
17
+ "proxy-authorization",
18
+ "cookie",
19
+ "set-cookie"
20
+ ];
21
+ function normalizeKey(key) {
22
+ return String(key).toLowerCase().replace(/[-_]/g, "");
23
+ }
24
+ function redactKeyList(extra = []) {
25
+ return [...new Set([...DEFAULT_REDACT_KEYS, ...extra].map(normalizeKey))];
26
+ }
27
+ function isRedactedKey(key, redactKeys) {
28
+ return redactKeys.includes(normalizeKey(key));
29
+ }
30
+
31
+ // ../shared/src/trace.ts
32
+ import { AsyncLocalStorage } from "async_hooks";
33
+ import { createHash, randomBytes } from "crypto";
34
+ var TRACEPARENT_HEADER = "traceparent";
35
+ var TRACESTATE_HEADER = "tracestate";
36
+ var TRACE_ID_HEADER = "x-trace-id";
37
+ var MAX_TRACESTATE_CHARS = 512;
38
+ var ZERO_TRACE_ID = "0".repeat(32);
39
+ var ZERO_SPAN_ID = "0".repeat(16);
40
+ var TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?$/;
41
+ var TRACE_ID_RE = /^[0-9a-f]{32}$/;
42
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
43
+ function newTraceId() {
44
+ for (; ; ) {
45
+ const id = randomBytes(16).toString("hex");
46
+ if (id !== ZERO_TRACE_ID) return id;
47
+ }
48
+ }
49
+ function newSpanId() {
50
+ for (; ; ) {
51
+ const id = randomBytes(8).toString("hex");
52
+ if (id !== ZERO_SPAN_ID) return id;
53
+ }
54
+ }
55
+ function sha256Hex(value) {
56
+ return createHash("sha256").update(value, "utf8").digest("hex");
57
+ }
58
+ function toW3CTraceId(traceId) {
59
+ const lower = traceId.toLowerCase();
60
+ if (TRACE_ID_RE.test(lower) && lower !== ZERO_TRACE_ID) return lower;
61
+ if (UUID_RE.test(lower)) {
62
+ const hex = lower.replace(/-/g, "");
63
+ if (hex !== ZERO_TRACE_ID) return hex;
64
+ }
65
+ return sha256Hex(traceId).slice(0, 32);
66
+ }
67
+ function parseTraceparent(value) {
68
+ if (typeof value !== "string") return null;
69
+ const header = value.trim();
70
+ const match = TRACEPARENT_RE.exec(header);
71
+ if (!match) return null;
72
+ const [, version, traceId, parentId, flagsHex, rest] = match;
73
+ if (version === "ff") return null;
74
+ if (version === "00" && rest !== void 0) return null;
75
+ if (traceId === ZERO_TRACE_ID || parentId === ZERO_SPAN_ID) return null;
76
+ const flags = parseInt(flagsHex, 16);
77
+ return { version, traceId, parentId, flags, sampled: (flags & 1) === 1 };
78
+ }
79
+ var STORE_KEY = /* @__PURE__ */ Symbol.for("@fabbahiense/pulsar.trace-context.v1");
80
+ function sharedStore() {
81
+ const g = globalThis;
82
+ let store = g[STORE_KEY];
83
+ if (!store) {
84
+ store = new AsyncLocalStorage();
85
+ g[STORE_KEY] = store;
86
+ }
87
+ return store;
88
+ }
89
+ var traceStore = sharedStore();
90
+ function text(value) {
91
+ return typeof value === "string" && value.length > 0 ? value : void 0;
92
+ }
93
+ function completeContext(init) {
94
+ const ctx = {
95
+ traceId: text(init?.traceId) ?? newTraceId(),
96
+ spanId: text(init?.spanId) ?? newSpanId(),
97
+ sampled: init?.sampled !== false
98
+ };
99
+ const parentSpanId = text(init?.parentSpanId);
100
+ if (parentSpanId) ctx.parentSpanId = parentSpanId;
101
+ const traceState = text(init?.traceState);
102
+ if (traceState) ctx.traceState = traceState;
103
+ const method = text(init?.method);
104
+ if (method) ctx.method = method;
105
+ const url = text(init?.url);
106
+ if (url) ctx.url = url;
107
+ return ctx;
108
+ }
109
+ function childContext(parent) {
110
+ if (!parent || !text(parent.traceId)) return completeContext(null);
111
+ const local = parent;
112
+ return completeContext({
113
+ traceId: parent.traceId,
114
+ parentSpanId: parent.spanId,
115
+ sampled: parent.sampled,
116
+ traceState: parent.traceState,
117
+ method: local.method,
118
+ url: local.url
119
+ });
120
+ }
121
+ function read(carrier, name) {
122
+ let value;
123
+ if (typeof carrier.get === "function") {
124
+ value = carrier.get(name);
125
+ } else {
126
+ const record = carrier;
127
+ value = record[name];
128
+ if (value === void 0) {
129
+ for (const key of Object.keys(record)) {
130
+ if (key.toLowerCase() === name) {
131
+ value = record[key];
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ if (Array.isArray(value)) value = value.find((v) => typeof v === "string");
138
+ return typeof value === "string" && value.length > 0 ? value : void 0;
139
+ }
140
+ function readTraceState(carrier) {
141
+ let raw;
142
+ if (typeof carrier.get === "function") {
143
+ raw = carrier.get(TRACESTATE_HEADER);
144
+ } else {
145
+ const record = carrier;
146
+ raw = record[TRACESTATE_HEADER];
147
+ if (raw === void 0) {
148
+ const key = Object.keys(record).find((k) => k.toLowerCase() === TRACESTATE_HEADER);
149
+ raw = key === void 0 ? void 0 : record[key];
150
+ }
151
+ }
152
+ const joined = Array.isArray(raw) ? raw.filter((v) => typeof v === "string").join(",") : raw;
153
+ if (typeof joined !== "string") return void 0;
154
+ const value = joined.trim();
155
+ return value.length > 0 && value.length <= MAX_TRACESTATE_CHARS ? value : void 0;
156
+ }
157
+ function extractTrace(carrier) {
158
+ if (typeof carrier !== "object" || carrier === null) return null;
159
+ try {
160
+ const source = carrier;
161
+ const legacy = read(source, TRACE_ID_HEADER);
162
+ const parent = parseTraceparent(read(source, TRACEPARENT_HEADER));
163
+ if (legacy !== void 0) {
164
+ if (parent && parent.traceId === toW3CTraceId(legacy)) {
165
+ return withTraceState({ traceId: legacy, spanId: parent.parentId, sampled: parent.sampled }, source);
166
+ }
167
+ return { traceId: legacy, sampled: true };
168
+ }
169
+ if (parent) return withTraceState({ traceId: parent.traceId, spanId: parent.parentId, sampled: parent.sampled }, source);
170
+ return null;
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+ function withTraceState(trace, carrier) {
176
+ const traceState = readTraceState(carrier);
177
+ if (traceState) trace.traceState = traceState;
178
+ return trace;
179
+ }
180
+
1
181
  // src/middleware.ts
2
182
  var DEFAULT_SKIP_URLS = ["/health", "/metrics", "/favicon.ico"];
3
- var DEFAULT_REDACT = ["authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token"];
4
183
  var DEFAULT_MAX_BODY = 5e4;
5
184
  function pulsarRequestLogger(options) {
6
185
  const logger = options.logger;
7
186
  if (!logger) throw new Error("pulsarRequestLogger: options.logger is required");
8
187
  const skipUrls = options.skipUrls ?? DEFAULT_SKIP_URLS;
9
- const redactKeys = [...DEFAULT_REDACT, ...options.redactKeys ?? []].map((k) => k.toLowerCase());
188
+ const redactKeys = redactKeyList(options.redactKeys ?? []);
10
189
  const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY;
11
190
  const getUserId = options.getUserId ?? ((req) => req.user?.id);
12
191
  const getLevel = options.getLevel ?? defaultLevel;
13
192
  return function pulsarMw(req, res, next) {
14
193
  const url = req.originalUrl || req.url;
15
- if (skipUrls.some((s) => url.includes(s))) return next();
194
+ const ctx = requestContext(req, url);
195
+ if (skipUrls.some((s) => url.includes(s))) return traceStore.run(ctx, next);
16
196
  const startedAt = Date.now();
17
- const chunks = [];
197
+ const capture = new BodyCapture(maxBodySize);
18
198
  const originalWrite = res.write.bind(res);
19
199
  const originalEnd = res.end.bind(res);
20
200
  res.write = function(chunk, ...rest) {
21
- try {
22
- if (chunk) chunks.push(toBuffer(chunk));
23
- } catch {
24
- }
201
+ capture.add(chunk, rest[0]);
25
202
  return originalWrite(chunk, ...rest);
26
203
  };
27
204
  res.end = function(chunk, ...rest) {
28
- try {
29
- if (chunk) chunks.push(toBuffer(chunk));
30
- } catch {
31
- }
205
+ capture.add(chunk, rest[0]);
32
206
  return originalEnd(chunk, ...rest);
33
207
  };
34
- res.on("finish", () => {
208
+ res.on("finish", () => traceStore.run(ctx, () => {
35
209
  try {
36
210
  const responseTime = Date.now() - startedAt;
37
211
  const status = res.statusCode;
38
212
  const level = getLevel(req, res);
39
- const responseBody = readBuffer(chunks, maxBodySize, getResponseContentType(res));
213
+ const responseBody = readBuffer(capture, maxBodySize, getResponseContentType(res), redactKeys);
40
214
  const payload = {
41
- // Top-level → colunas/filtros
215
+ // Top level: becomes columns and filters
42
216
  req: { method: req.method, url, id: req.id },
43
217
  res: { statusCode: status },
44
218
  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
219
+ // A request id set later (pino-http registered after this middleware) still wins, as it always has.
220
+ traceId: req.id || ctx.traceId,
221
+ // The request's span-end entry.
222
+ spanId: ctx.spanId,
223
+ ...ctx.parentSpanId ? { parentSpanId: ctx.parentSpanId } : {},
224
+ spanKind: "server",
225
+ // metadata: the sections of the side panel
47
226
  metadata: trimUndefined({
48
227
  requestBody: redactDeep(req.body, redactKeys),
49
228
  responseBody,
@@ -59,36 +238,75 @@ function pulsarRequestLogger(options) {
59
238
  } catch (err) {
60
239
  if (typeof console !== "undefined") console.error("[pulsarRequestLogger] failed:", err);
61
240
  }
62
- });
63
- next();
241
+ }));
242
+ traceStore.run(ctx, next);
64
243
  };
65
244
  }
245
+ function requestContext(req, url) {
246
+ let parent = extractTrace(req.headers);
247
+ const id = req.id;
248
+ if (id !== void 0 && id !== null && id !== "") {
249
+ const traceId = String(id);
250
+ if (!parent || parent.traceId !== traceId) parent = { traceId, sampled: parent?.sampled ?? true };
251
+ }
252
+ const ctx = childContext(parent);
253
+ ctx.method = req.method;
254
+ ctx.url = url;
255
+ return ctx;
256
+ }
66
257
  function defaultLevel(_req, res) {
67
258
  if (res.statusCode >= 500) return "error";
68
259
  if (res.statusCode >= 400) return "warn";
69
260
  return "info";
70
261
  }
71
- function toBuffer(chunk) {
262
+ function toBuffer(chunk, encoding) {
72
263
  if (Buffer.isBuffer(chunk)) return chunk;
73
- if (typeof chunk === "string") return Buffer.from(chunk);
264
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
265
+ if (typeof chunk === "string") {
266
+ return typeof encoding === "string" && Buffer.isEncoding(encoding) ? Buffer.from(chunk, encoding) : Buffer.from(chunk);
267
+ }
74
268
  try {
75
269
  return Buffer.from(JSON.stringify(chunk));
76
270
  } catch {
77
271
  return Buffer.alloc(0);
78
272
  }
79
273
  }
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`;
274
+ var BodyCapture = class {
275
+ constructor(max) {
276
+ this.max = max;
277
+ }
278
+ max;
279
+ chunks = [];
280
+ kept = 0;
281
+ total = 0;
282
+ add(chunk, encoding) {
283
+ if (!chunk || typeof chunk === "function") return;
284
+ try {
285
+ const buf = toBuffer(chunk, encoding);
286
+ this.total += buf.length;
287
+ const room = this.max + 1 - this.kept;
288
+ if (room <= 0) return;
289
+ const part = Buffer.from(buf.length > room ? buf.subarray(0, room) : buf);
290
+ this.chunks.push(part);
291
+ this.kept += part.length;
292
+ } catch {
293
+ }
294
+ }
295
+ bytes() {
296
+ return Buffer.concat(this.chunks);
297
+ }
298
+ };
299
+ function readBuffer(capture, max, contentType, redactKeys) {
300
+ if (capture.total === 0) return void 0;
301
+ const buf = capture.bytes();
302
+ if (capture.total > max) {
303
+ return `[TRUNCATED ${capture.total}B > ${max}B] ${buf.subarray(0, max).toString("utf8")}\u2026`;
86
304
  }
87
305
  const raw = buf.toString("utf8");
88
306
  const ct = contentType.toLowerCase();
89
307
  if (ct.includes("application/json")) {
90
308
  try {
91
- return JSON.parse(raw);
309
+ return redactDeep(JSON.parse(raw), redactKeys);
92
310
  } catch {
93
311
  return raw;
94
312
  }
@@ -107,19 +325,25 @@ function redactHeaders(headers, redactKeys) {
107
325
  const out = {};
108
326
  for (const [k, v] of Object.entries(headers)) {
109
327
  if (v === void 0 || v === null) continue;
110
- out[k] = redactKeys.includes(k.toLowerCase()) ? "[REDACTED]" : Array.isArray(v) ? v.join(", ") : String(v);
328
+ out[k] = isRedactedKey(k, redactKeys) ? "[REDACTED]" : Array.isArray(v) ? v.join(", ") : String(v);
111
329
  }
112
330
  return out;
113
331
  }
114
- function redactDeep(value, redactKeys) {
332
+ function redactDeep(value, redactKeys, seen = /* @__PURE__ */ new WeakSet()) {
115
333
  if (value === null || value === void 0) return value;
116
334
  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);
335
+ if (seen.has(value)) return "[Circular]";
336
+ seen.add(value);
337
+ try {
338
+ if (Array.isArray(value)) return value.map((v) => redactDeep(v, redactKeys, seen));
339
+ const out = {};
340
+ for (const [k, v] of Object.entries(value)) {
341
+ out[k] = isRedactedKey(k, redactKeys) ? "[REDACTED]" : redactDeep(v, redactKeys, seen);
342
+ }
343
+ return out;
344
+ } finally {
345
+ seen.delete(value);
121
346
  }
122
- return out;
123
347
  }
124
348
  function trimUndefined(obj) {
125
349
  const out = {};
package/package.json CHANGED
@@ -1,33 +1,54 @@
1
1
  {
2
2
  "name": "@fabbahiense/pulsar-pino-transport",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Pino transport for Pulsar observability platform + Express request logger middleware",
5
+ "author": "Fabrício Bahiense Santos",
5
6
  "type": "module",
6
7
  "main": "./dist/index.cjs",
7
8
  "module": "./dist/index.js",
8
9
  "types": "./dist/index.d.ts",
9
10
  "exports": {
10
11
  ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
14
20
  },
15
21
  "./middleware": {
16
- "types": "./dist/middleware.d.ts",
17
- "import": "./dist/middleware.js",
18
- "require": "./dist/middleware.cjs"
22
+ "import": {
23
+ "types": "./dist/middleware.d.ts",
24
+ "default": "./dist/middleware.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/middleware.d.cts",
28
+ "default": "./dist/middleware.cjs"
29
+ }
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "typesVersions": {
34
+ "*": {
35
+ "middleware": ["./dist/middleware.d.ts"]
19
36
  }
20
37
  },
21
- "files": ["dist"],
38
+ "files": ["dist", "CHANGELOG.md"],
39
+ "sideEffects": false,
22
40
  "scripts": {
23
41
  "build": "tsup",
24
42
  "dev": "tsup --watch",
25
- "test": "node --test test/"
43
+ "pretest": "npm run build",
44
+ "test": "vitest run --coverage",
45
+ "prepublishOnly": "npm test"
26
46
  },
27
47
  "peerDependencies": {
28
48
  "pino-abstract-transport": ">=2.0.0"
29
49
  },
30
50
  "devDependencies": {
51
+ "pino": "10.3.1",
31
52
  "pino-abstract-transport": "^3.0.0",
32
53
  "tsup": "^8.0.0",
33
54
  "typescript": "^5.9.0"