@fabbahiense/pulsar-pino-transport 0.1.0 → 0.2.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/README.md +105 -3
- package/dist/index.cjs +56 -5
- package/dist/index.js +56 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# @pulsar
|
|
1
|
+
# @fabbahiense/pulsar-pino-transport
|
|
2
2
|
|
|
3
3
|
Pino transport for [Pulsar](https://github.com/your-org/pulsar) — send your application logs to Pulsar in real-time.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @pulsar
|
|
8
|
+
npm install @fabbahiense/pulsar-pino-transport
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
@@ -15,7 +15,7 @@ import pino from 'pino'
|
|
|
15
15
|
|
|
16
16
|
const logger = pino({
|
|
17
17
|
transport: {
|
|
18
|
-
target: '@pulsar
|
|
18
|
+
target: '@fabbahiense/pulsar-pino-transport',
|
|
19
19
|
options: {
|
|
20
20
|
url: 'https://your-pulsar-instance.com',
|
|
21
21
|
apiKey: 'psk_your_api_key',
|
|
@@ -62,6 +62,108 @@ The transport automatically picks up trace IDs from these fields:
|
|
|
62
62
|
logger.info({ traceId: 'my-trace-id' }, 'Processing request')
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## Capturing request/response bodies (with `pino-http`)
|
|
66
|
+
|
|
67
|
+
The transport itself doesn't intercept HTTP — pino is a logger, not a middleware. To get rich request/response logs in Pulsar, pair it with [`pino-http`](https://github.com/pinojs/pino-http):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install pino-http
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import express from 'express'
|
|
75
|
+
import pino from 'pino'
|
|
76
|
+
import pinoHttp from 'pino-http'
|
|
77
|
+
|
|
78
|
+
const logger = pino({
|
|
79
|
+
transport: {
|
|
80
|
+
target: '@fabbahiense/pulsar-pino-transport',
|
|
81
|
+
options: { url: process.env.PULSAR_URL, apiKey: process.env.PULSAR_API_KEY },
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const app = express()
|
|
86
|
+
app.use(express.json())
|
|
87
|
+
|
|
88
|
+
app.use(pinoHttp({
|
|
89
|
+
logger,
|
|
90
|
+
// Serialize request body
|
|
91
|
+
serializers: {
|
|
92
|
+
req(req) {
|
|
93
|
+
return {
|
|
94
|
+
method: req.method,
|
|
95
|
+
url: req.url,
|
|
96
|
+
headers: redact(req.headers),
|
|
97
|
+
requestBody: req.raw?.body, // Pulsar UI recognizes this key
|
|
98
|
+
query: req.query,
|
|
99
|
+
params: req.params,
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
res(res) {
|
|
103
|
+
return {
|
|
104
|
+
statusCode: res.statusCode,
|
|
105
|
+
headers: redact(res.getHeaders?.() ?? {}),
|
|
106
|
+
// responseBody requires intercepting res.write/res.end — see below
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
customLogLevel(_req, res, err) {
|
|
111
|
+
if (err || res.statusCode >= 500) return 'error'
|
|
112
|
+
if (res.statusCode >= 400) return 'warn'
|
|
113
|
+
return 'info'
|
|
114
|
+
},
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
function redact(headers: Record<string, unknown>): Record<string, unknown> {
|
|
118
|
+
const out: Record<string, unknown> = {}
|
|
119
|
+
const drop = new Set(['authorization', 'cookie', 'set-cookie', 'x-api-key'])
|
|
120
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
121
|
+
out[k] = drop.has(k.toLowerCase()) ? '[REDACTED]' : v
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**No painel lateral do Pulsar** isso aparece em seções dedicadas: Request body, Headers, Query, Path params, status, duration.
|
|
128
|
+
|
|
129
|
+
### Capturando response body (avançado)
|
|
130
|
+
|
|
131
|
+
Pino-http não captura response body nativamente. Para capturar, intercepte `res.write`/`res.end`:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
app.use((req, res, next) => {
|
|
135
|
+
const chunks: Buffer[] = []
|
|
136
|
+
const originalWrite = res.write.bind(res)
|
|
137
|
+
const originalEnd = res.end.bind(res)
|
|
138
|
+
res.write = function (chunk: any) { if (chunk) chunks.push(Buffer.from(chunk)); return originalWrite(chunk) }
|
|
139
|
+
res.end = function (chunk: any) { if (chunk) chunks.push(Buffer.from(chunk)); return originalEnd(chunk) }
|
|
140
|
+
res.on('finish', () => {
|
|
141
|
+
try {
|
|
142
|
+
const body = Buffer.concat(chunks).toString('utf8').slice(0, 10240)
|
|
143
|
+
logger.info({ responseBody: tryParseJson(body), statusCode: res.statusCode, traceId: req.id }, `${req.method} ${req.url} ${res.statusCode}`)
|
|
144
|
+
} catch {}
|
|
145
|
+
})
|
|
146
|
+
next()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
function tryParseJson(s: string) { try { return JSON.parse(s) } catch { return s } }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
> **Mais simples?** Se você puder, considere `@fabbahiense/pulsar-node` que já vem com `Pulsar.middleware({ captureResponseBody: true })` configurado.
|
|
153
|
+
|
|
154
|
+
## Recognized metadata keys
|
|
155
|
+
|
|
156
|
+
Pulsar UI auto-detects these key names and renders dedicated sections:
|
|
157
|
+
|
|
158
|
+
| Section | Recognized keys |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Request body | `body`, `requestBody`, `reqBody`, `payload`, `input` |
|
|
161
|
+
| Response body | `response`, `responseBody`, `output`, `result`, `data` |
|
|
162
|
+
| Request headers | `headers`, `requestHeaders`, `reqHeaders` |
|
|
163
|
+
| Response headers | `responseHeaders`, `resHeaders` |
|
|
164
|
+
| Query | `query`, `queryParams`, `qs` |
|
|
165
|
+
| Path params | `params`, `pathParams` |
|
|
166
|
+
|
|
65
167
|
## License
|
|
66
168
|
|
|
67
169
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,45 @@ __export(index_exports, {
|
|
|
34
34
|
default: () => index_default
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var GENERIC_HTTP_MSGS = /* @__PURE__ */ new Set([
|
|
38
|
+
"request completed",
|
|
39
|
+
"request errored",
|
|
40
|
+
"request aborted",
|
|
41
|
+
"response sent",
|
|
42
|
+
"incoming request",
|
|
43
|
+
"http request"
|
|
44
|
+
]);
|
|
45
|
+
function pickFirst(obj, keys) {
|
|
46
|
+
for (const k of keys) {
|
|
47
|
+
if (obj[k] !== void 0 && obj[k] !== null) return obj[k];
|
|
48
|
+
}
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
function isObj(v) {
|
|
52
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
function extractPayloads(obj) {
|
|
55
|
+
const out = {};
|
|
56
|
+
const req = obj.req;
|
|
57
|
+
if (isObj(req)) {
|
|
58
|
+
const body = pickFirst(req, ["body", "requestBody", "reqBody", "payload", "input"]);
|
|
59
|
+
if (body !== void 0) out.requestBody = body;
|
|
60
|
+
const headers = pickFirst(req, ["headers", "requestHeaders", "reqHeaders"]);
|
|
61
|
+
if (isObj(headers)) out.requestHeaders = headers;
|
|
62
|
+
const query = pickFirst(req, ["query", "queryParams", "qs"]);
|
|
63
|
+
if (isObj(query)) out.query = query;
|
|
64
|
+
const params = pickFirst(req, ["params", "pathParams", "routeParams"]);
|
|
65
|
+
if (isObj(params)) out.params = params;
|
|
66
|
+
}
|
|
67
|
+
const res = obj.res;
|
|
68
|
+
if (isObj(res)) {
|
|
69
|
+
const body = pickFirst(res, ["body", "responseBody", "resBody", "response", "output", "result", "data"]);
|
|
70
|
+
if (body !== void 0) out.responseBody = body;
|
|
71
|
+
const headers = pickFirst(res, ["headers", "responseHeaders", "resHeaders"]);
|
|
72
|
+
if (isObj(headers)) out.responseHeaders = headers;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
37
76
|
var LEVEL_MAP = {
|
|
38
77
|
10: "trace",
|
|
39
78
|
20: "debug",
|
|
@@ -115,19 +154,31 @@ async function index_default(options) {
|
|
|
115
154
|
}
|
|
116
155
|
return build(async function(stream) {
|
|
117
156
|
for await (const obj of stream) {
|
|
157
|
+
const method = obj.req?.method || void 0;
|
|
158
|
+
const url2 = obj.req?.url || void 0;
|
|
159
|
+
const statusCode = obj.res?.statusCode || void 0;
|
|
160
|
+
let msg = obj.msg ?? "";
|
|
161
|
+
const lower = msg.toLowerCase().trim();
|
|
162
|
+
if (GENERIC_HTTP_MSGS.has(lower) && method && url2) {
|
|
163
|
+
msg = `${method.toUpperCase()} ${url2}${statusCode ? ` ${statusCode}` : ""}`;
|
|
164
|
+
}
|
|
165
|
+
const extracted = extractPayloads(obj);
|
|
166
|
+
const baseMeta = isObj(obj.metadata) ? obj.metadata : {};
|
|
167
|
+
const merged = { ...baseMeta, ...extracted };
|
|
168
|
+
const metadata = Object.keys(merged).length > 0 ? merged : null;
|
|
118
169
|
buffer.push({
|
|
119
170
|
level: LEVEL_MAP[obj.level] || "info",
|
|
120
|
-
msg
|
|
171
|
+
msg,
|
|
121
172
|
timestamp: parseTimestamp(obj.time),
|
|
122
173
|
serviceName: resolvedSource,
|
|
123
174
|
traceId: obj.traceId || obj.trace_id || obj.requestId || obj.req?.id || null,
|
|
124
|
-
method
|
|
125
|
-
url:
|
|
126
|
-
statusCode
|
|
175
|
+
method,
|
|
176
|
+
url: url2,
|
|
177
|
+
statusCode,
|
|
127
178
|
responseTime: obj.responseTime,
|
|
128
179
|
errorMessage: obj.err?.message,
|
|
129
180
|
errorStack: obj.err?.stack,
|
|
130
|
-
metadata
|
|
181
|
+
metadata
|
|
131
182
|
});
|
|
132
183
|
if (buffer.length >= batchSize) {
|
|
133
184
|
await flush();
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,43 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
var GENERIC_HTTP_MSGS = /* @__PURE__ */ new Set([
|
|
3
|
+
"request completed",
|
|
4
|
+
"request errored",
|
|
5
|
+
"request aborted",
|
|
6
|
+
"response sent",
|
|
7
|
+
"incoming request",
|
|
8
|
+
"http request"
|
|
9
|
+
]);
|
|
10
|
+
function pickFirst(obj, keys) {
|
|
11
|
+
for (const k of keys) {
|
|
12
|
+
if (obj[k] !== void 0 && obj[k] !== null) return obj[k];
|
|
13
|
+
}
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
function isObj(v) {
|
|
17
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
18
|
+
}
|
|
19
|
+
function extractPayloads(obj) {
|
|
20
|
+
const out = {};
|
|
21
|
+
const req = obj.req;
|
|
22
|
+
if (isObj(req)) {
|
|
23
|
+
const body = pickFirst(req, ["body", "requestBody", "reqBody", "payload", "input"]);
|
|
24
|
+
if (body !== void 0) out.requestBody = body;
|
|
25
|
+
const headers = pickFirst(req, ["headers", "requestHeaders", "reqHeaders"]);
|
|
26
|
+
if (isObj(headers)) out.requestHeaders = headers;
|
|
27
|
+
const query = pickFirst(req, ["query", "queryParams", "qs"]);
|
|
28
|
+
if (isObj(query)) out.query = query;
|
|
29
|
+
const params = pickFirst(req, ["params", "pathParams", "routeParams"]);
|
|
30
|
+
if (isObj(params)) out.params = params;
|
|
31
|
+
}
|
|
32
|
+
const res = obj.res;
|
|
33
|
+
if (isObj(res)) {
|
|
34
|
+
const body = pickFirst(res, ["body", "responseBody", "resBody", "response", "output", "result", "data"]);
|
|
35
|
+
if (body !== void 0) out.responseBody = body;
|
|
36
|
+
const headers = pickFirst(res, ["headers", "responseHeaders", "resHeaders"]);
|
|
37
|
+
if (isObj(headers)) out.responseHeaders = headers;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
2
41
|
var LEVEL_MAP = {
|
|
3
42
|
10: "trace",
|
|
4
43
|
20: "debug",
|
|
@@ -80,19 +119,31 @@ async function index_default(options) {
|
|
|
80
119
|
}
|
|
81
120
|
return build(async function(stream) {
|
|
82
121
|
for await (const obj of stream) {
|
|
122
|
+
const method = obj.req?.method || void 0;
|
|
123
|
+
const url2 = obj.req?.url || void 0;
|
|
124
|
+
const statusCode = obj.res?.statusCode || void 0;
|
|
125
|
+
let msg = obj.msg ?? "";
|
|
126
|
+
const lower = msg.toLowerCase().trim();
|
|
127
|
+
if (GENERIC_HTTP_MSGS.has(lower) && method && url2) {
|
|
128
|
+
msg = `${method.toUpperCase()} ${url2}${statusCode ? ` ${statusCode}` : ""}`;
|
|
129
|
+
}
|
|
130
|
+
const extracted = extractPayloads(obj);
|
|
131
|
+
const baseMeta = isObj(obj.metadata) ? obj.metadata : {};
|
|
132
|
+
const merged = { ...baseMeta, ...extracted };
|
|
133
|
+
const metadata = Object.keys(merged).length > 0 ? merged : null;
|
|
83
134
|
buffer.push({
|
|
84
135
|
level: LEVEL_MAP[obj.level] || "info",
|
|
85
|
-
msg
|
|
136
|
+
msg,
|
|
86
137
|
timestamp: parseTimestamp(obj.time),
|
|
87
138
|
serviceName: resolvedSource,
|
|
88
139
|
traceId: obj.traceId || obj.trace_id || obj.requestId || obj.req?.id || null,
|
|
89
|
-
method
|
|
90
|
-
url:
|
|
91
|
-
statusCode
|
|
140
|
+
method,
|
|
141
|
+
url: url2,
|
|
142
|
+
statusCode,
|
|
92
143
|
responseTime: obj.responseTime,
|
|
93
144
|
errorMessage: obj.err?.message,
|
|
94
145
|
errorStack: obj.err?.stack,
|
|
95
|
-
metadata
|
|
146
|
+
metadata
|
|
96
147
|
});
|
|
97
148
|
if (buffer.length >= batchSize) {
|
|
98
149
|
await flush();
|