@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@fabbahiense/pulsar-pino-transport`. Versions before 0.4.0 were not documented here.
|
|
4
|
+
|
|
5
|
+
## 0.4.0 (not yet published)
|
|
6
|
+
|
|
7
|
+
Every 0.3.0 export and option keeps working. Since this is a 0.x minor, a `^0.3.0` range does not pick it up: update the range to `^0.4.0`.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`createPulsarTransport()` was unusable.** It pointed pino at `@pulsar/pino-transport`, a name that was never published, so pino could not resolve the target. It now uses `@fabbahiense/pulsar-pino-transport`.
|
|
12
|
+
- **Logs buffered at shutdown were lost.** The final flush ran after the stream had already emitted `close`, and pino's worker exits on `close`. The flush now happens in the transport's `close` hook, which pino-abstract-transport awaits (bounded by `shutdownTimeout`).
|
|
13
|
+
- **A slow or down Pulsar stalled pino.** The stream loop awaited each HTTP request (with retries) when a batch filled, so the worker stopped reading. Records are now buffered as they arrive and sent in the background.
|
|
14
|
+
- **A 429 dropped logs.** Every 4xx was treated as permanent. `429`, `408`, `5xx`, network errors and timeouts are now retried with exponential backoff and jitter, honouring `Retry-After`, from a bounded buffer (`maxBufferSize`, drop oldest, counted). Requests time out, one is in flight at a time, `batchSize` is clamped to the server's 1000, a `413` splits the batch, and entries are capped at 256 KiB.
|
|
15
|
+
- A non-string `msg` or `req.method` threw and ended the transport, losing every later log. Values are coerced; a custom `message` key is used as the message.
|
|
16
|
+
- Level labels (`formatters.level`) were all sent as `info`; custom numeric levels too. Both are mapped now.
|
|
17
|
+
- Epoch-seconds times (`stdTimeFunctions.unixTime`) became dates in 1970; they are converted.
|
|
18
|
+
- `pulsarRequestLogger` kept every response chunk in memory (it captures every response); it now keeps at most `maxBodySize + 1` bytes. `Uint8Array` chunks, string encodings and circular request bodies are handled.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Transport options: `maxBufferSize`, `maxRetries`, `requestTimeout`, `shutdownTimeout`, `warnOnDrop`. `createPulsarTransport()` passes them on.
|
|
23
|
+
- Dropped logs (by reason: `overflow`, `rejected`, `retriesExhausted`, `shutdown`) produce one stderr line at most once a minute, unless `warnOnDrop: false`.
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
- `pulsarRequestLogger` captures request bodies on every request but only redacted header names, so a login body's `password` was logged in plain text; JSON response bodies were not redacted at all. The default list now also covers `password`, `passwd`, `password_hash`, `secret`, `client_secret`, `token`, `access_token`, `refresh_token`, `id_token`, `session_id`, `api_key` (any spelling) and `proxy-authorization`, and JSON response bodies are redacted too.
|
|
28
|
+
- Redaction keys are now matched ignoring case, `-` and `_`: `access_token`, `access-token` and `accessToken` are the same key, for the defaults and for your `redactKeys`. Before, only the exact snake/kebab spelling matched, so camelCase fields such as a login response's `accessToken` were logged. Only whole keys match: `tokensUsed` or `passwordHint` are kept unless listed.
|
|
29
|
+
|
|
30
|
+
### Packaging
|
|
31
|
+
|
|
32
|
+
- **CommonJS TypeScript consumers** (`module: node16`/`nodenext`) got TS1479 because both conditions pointed at the ESM declarations. Each condition now has its own types (`.d.ts` / `.d.cts`). `/middleware` resolves under the legacy `node10` resolution too (`typesVersions`). `./package.json` is exported, `sideEffects` is `false`, and `prepublishOnly` builds and tests.
|
|
33
|
+
- The middleware's JSDoc is now in English.
|
|
34
|
+
|
|
35
|
+
### Distributed tracing
|
|
36
|
+
|
|
37
|
+
Additive and opt-in: existing trace id fields keep working as before, nothing is patched, and no runtime dependency was added.
|
|
38
|
+
|
|
39
|
+
- `pulsarMixin()`: a pino `mixin` that adds `traceId`, `spanId` and `parentSpanId` from the current trace context to every record, in the app's thread, so they reach the transport worker. One line: `pino({ mixin: pulsarMixin(), transport })`.
|
|
40
|
+
- **The transport** sends `spanId`, `parentSpanId` and `spanKind` as top-level fields when they are non-empty strings (the server stores them as `metadata.spanId`, `metadata.parentSpanId` and `metadata.spanKind`, where they ended up before too). Other values stay in metadata.
|
|
41
|
+
- **`pulsarRequestLogger()` runs each request in a span** and logs the request entry as its span-end entry (`spanId`, `parentSpanId`, `spanKind: "server"`). The trace is `req.id` when already set (as before), else the `x-trace-id` header, else a valid W3C `traceparent` (whose span becomes the parent), else a **new trace id**: requests without a trace id used to get none. A `req.id` set later (pino-http registered after it) still wins on the request entry. Skipped URLs are still not logged, but run in a span.
|
|
42
|
+
- New exports from the root entry: `startSpan(name, fn, { logger })`, `instrumentFetch(fetch?, { logger? })`, `instrumentAxios(instance, { logger? })`, `traceHeaders`, `injectTrace`, `extractTrace`, `runWithTrace`, `getTraceContext`, `parseTraceparent`, `formatTraceparent`. The trace context store is shared with `@fabbahiense/pulsar-node` and `@fabbahiense/pulsar-next` in the same process.
|
|
43
|
+
- `pino` 10.3.1 is a dev dependency, for the tests only.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabrício Bahiense Santos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,168 +1,263 @@
|
|
|
1
1
|
# @fabbahiense/pulsar-pino-transport
|
|
2
2
|
|
|
3
|
-
Pino transport for
|
|
3
|
+
Pino transport + drop-in Express middleware for Pulsar observability. Send rich logs (method, URL, status, duration, request/response body, headers, query, params, custom context) with one line of middleware.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @fabbahiense/pulsar-pino-transport
|
|
8
|
+
npm install @fabbahiense/pulsar-pino-transport pino-abstract-transport
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Requires Node.js 18 or later. `pino-abstract-transport` (v2 or later) is a peer dependency. Ships ESM and CommonJS builds with TypeScript types for both.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## Quick Start — NestJS
|
|
14
|
+
|
|
15
|
+
This is the recommended setup. Works with `nestjs-pino`.
|
|
16
|
+
|
|
17
|
+
### 1. Logger config
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// server/src/infra/logger.config.ts
|
|
14
21
|
import pino from 'pino'
|
|
22
|
+
import type { Params } from 'nestjs-pino'
|
|
15
23
|
|
|
16
|
-
const
|
|
24
|
+
export const pinoLogger = pino({
|
|
25
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
17
26
|
transport: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
targets: [
|
|
28
|
+
{
|
|
29
|
+
target: '@fabbahiense/pulsar-pino-transport',
|
|
30
|
+
level: 'info',
|
|
31
|
+
options: {
|
|
32
|
+
url: process.env.PULSAR_URL,
|
|
33
|
+
apiKey: process.env.PULSAR_API_KEY,
|
|
34
|
+
source: process.env.PULSAR_SOURCE,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
// keep your other transports (pino-pretty in dev, pino/file in prod) here
|
|
38
|
+
],
|
|
26
39
|
},
|
|
27
40
|
})
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
export const loggerConfig: Params = {
|
|
43
|
+
pinoHttp: {
|
|
44
|
+
logger: pinoLogger,
|
|
45
|
+
autoLogging: false, // disabled — pulsarRequestLogger handles request auto-logs
|
|
46
|
+
},
|
|
47
|
+
}
|
|
31
48
|
```
|
|
32
49
|
|
|
33
|
-
|
|
50
|
+
### 2. Bootstrap with the middleware
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
```ts
|
|
53
|
+
// server/src/main.ts
|
|
54
|
+
import { NestFactory } from '@nestjs/core'
|
|
55
|
+
import { NestExpressApplication } from '@nestjs/platform-express'
|
|
56
|
+
import { Logger } from 'nestjs-pino'
|
|
57
|
+
import { pulsarRequestLogger } from '@fabbahiense/pulsar-pino-transport/middleware'
|
|
58
|
+
import { AppModule } from './app.module'
|
|
59
|
+
import { pinoLogger } from './infra/logger.config'
|
|
41
60
|
|
|
42
|
-
|
|
61
|
+
async function bootstrap() {
|
|
62
|
+
const app = await NestFactory.create<NestExpressApplication>(AppModule, { bufferLogs: true })
|
|
63
|
+
app.useLogger(app.get(Logger))
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|--------|------|---------|-------------|
|
|
46
|
-
| `url` | `string` | — | Pulsar server URL (required) |
|
|
47
|
-
| `apiKey` | `string` | — | API key for authentication (required) |
|
|
48
|
-
| `source` | `string` | `'default'` | Service name in Pulsar |
|
|
49
|
-
| `batchSize` | `number` | `50` | Logs buffered before flush |
|
|
50
|
-
| `flushInterval` | `number` | `2000` | Flush interval in ms |
|
|
65
|
+
app.use(pulsarRequestLogger({ logger: pinoLogger }))
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
await app.listen(process.env.PORT ?? 3000)
|
|
68
|
+
}
|
|
69
|
+
bootstrap()
|
|
70
|
+
```
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
- `trace_id`
|
|
58
|
-
- `requestId`
|
|
59
|
-
- `req.id`
|
|
72
|
+
### 3. Use the logger normally
|
|
60
73
|
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
```
|
|
74
|
+
```ts
|
|
75
|
+
import { Logger } from '@nestjs/common'
|
|
64
76
|
|
|
65
|
-
|
|
77
|
+
@Injectable()
|
|
78
|
+
export class CampaignService {
|
|
79
|
+
private readonly logger = new Logger(CampaignService.name)
|
|
66
80
|
|
|
67
|
-
|
|
81
|
+
async tokensUsed(campaignId: string, tokensUsed: number) {
|
|
82
|
+
// Plain log — appears as a normal line
|
|
83
|
+
this.logger.log('Campaign processed')
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
// Rich log — appears with Context section in Pulsar's side panel
|
|
86
|
+
this.logger.log({ metadata: { campaignId, tokensUsed } }, 'Campaign analytics tokens used')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
71
89
|
```
|
|
72
90
|
|
|
73
|
-
|
|
91
|
+
**Rule of thumb:** anything you want to see in Pulsar's side panel goes inside `metadata: {...}` as the first argument.
|
|
92
|
+
|
|
93
|
+
## Quick Start — Express (no NestJS)
|
|
94
|
+
|
|
95
|
+
```ts
|
|
74
96
|
import express from 'express'
|
|
75
97
|
import pino from 'pino'
|
|
76
|
-
import
|
|
98
|
+
import { pulsarRequestLogger } from '@fabbahiense/pulsar-pino-transport/middleware'
|
|
77
99
|
|
|
78
100
|
const logger = pino({
|
|
79
101
|
transport: {
|
|
80
102
|
target: '@fabbahiense/pulsar-pino-transport',
|
|
81
|
-
options: {
|
|
103
|
+
options: {
|
|
104
|
+
url: process.env.PULSAR_URL,
|
|
105
|
+
apiKey: process.env.PULSAR_API_KEY,
|
|
106
|
+
source: 'my-api',
|
|
107
|
+
},
|
|
82
108
|
},
|
|
83
109
|
})
|
|
84
110
|
|
|
85
111
|
const app = express()
|
|
86
112
|
app.use(express.json())
|
|
113
|
+
app.use(pulsarRequestLogger({ logger }))
|
|
87
114
|
|
|
88
|
-
app.
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
}))
|
|
115
|
+
app.post('/api/users', (req, res) => {
|
|
116
|
+
res.status(201).json({ ok: true })
|
|
117
|
+
})
|
|
116
118
|
|
|
117
|
-
|
|
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
|
-
}
|
|
119
|
+
app.listen(3000)
|
|
125
120
|
```
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
122
|
+
## What appears in Pulsar
|
|
123
|
+
|
|
124
|
+
For each request, **one** log entry shows up with:
|
|
125
|
+
|
|
126
|
+
- **Chips**: status code (color-coded by 2xx/4xx/5xx), response time, trace id, user id
|
|
127
|
+
- **Context section** (top): any custom fields you logged
|
|
128
|
+
- **Endpoint**: `METHOD url`
|
|
129
|
+
- **Tabs**:
|
|
130
|
+
- **Request**: query params, path params, request headers, request body
|
|
131
|
+
- **Response**: response headers, response body
|
|
132
|
+
- **Stack trace**: if there was an error
|
|
133
|
+
|
|
134
|
+
All sections have syntax-highlighted JSON, byte-size badge and copy button.
|
|
135
|
+
|
|
136
|
+
## Middleware options
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
pulsarRequestLogger({
|
|
140
|
+
logger, // required: your pino instance
|
|
141
|
+
skipUrls: ['/health', '/metrics'], // default — URLs to skip
|
|
142
|
+
redactKeys: ['cpf', 'cardNumber'], // extra keys to redact (case-insensitive)
|
|
143
|
+
maxBodySize: 50_000, // bytes — bigger bodies are truncated
|
|
144
|
+
getUserId: req => req.user?.id, // how to extract userId from req
|
|
145
|
+
getLevel: (req, res) => // how to choose log level
|
|
146
|
+
res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info',
|
|
147
147
|
})
|
|
148
|
+
```
|
|
148
149
|
|
|
149
|
-
|
|
150
|
+
**Default redacted keys** (always applied, in headers and in request and JSON response bodies):
|
|
151
|
+
`password`, `passwd`, `password_hash`, `secret`, `client_secret`, `token`, `access_token`, `refresh_token`, `id_token`, `session_id`, `api_key`, `x-api-key`, `x-auth-token`, `authorization`, `proxy-authorization`, `cookie`, `set-cookie`.
|
|
152
|
+
|
|
153
|
+
Keys are compared ignoring case, `-` and `_`, so `access_token`, `access-token` and `accessToken` are all redacted (the same applies to your `redactKeys`). Only whole keys match: `tokensUsed` or `passwordHint` are kept unless you list them.
|
|
154
|
+
|
|
155
|
+
Redaction is recursive — it walks nested objects in the bodies and replaces matching keys with `[REDACTED]`. At most `maxBodySize` bytes of a response are kept in memory, however large the response is.
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
## Transport options
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
{
|
|
162
|
+
target: '@fabbahiense/pulsar-pino-transport',
|
|
163
|
+
options: {
|
|
164
|
+
url: 'https://your-pulsar.com', // required
|
|
165
|
+
apiKey: 'psk_xxx', // required
|
|
166
|
+
source: 'my-service', // optional — appears as serviceName (alias: serviceName)
|
|
167
|
+
batchSize: 50, // optional — buffered logs that trigger an immediate send (max 1000)
|
|
168
|
+
flushInterval: 2000, // optional — longest a log waits before it is sent, in ms
|
|
169
|
+
maxBufferSize: 5000, // optional — most logs held in memory while Pulsar is slow or down
|
|
170
|
+
maxRetries: 10, // optional — retries before a batch is dropped (at most maxRetries + 1 sends)
|
|
171
|
+
requestTimeout: 10000, // optional — timeout per HTTP request, in ms
|
|
172
|
+
shutdownTimeout: 3000, // optional — longest the final flush waits when pino closes, in ms
|
|
173
|
+
warnOnDrop: true, // optional — one stderr line (at most once a minute) when logs are dropped
|
|
174
|
+
},
|
|
175
|
+
}
|
|
150
176
|
```
|
|
151
177
|
|
|
152
|
-
|
|
178
|
+
Or let the helper build that object:
|
|
153
179
|
|
|
154
|
-
|
|
180
|
+
```ts
|
|
181
|
+
import pino from 'pino'
|
|
182
|
+
import { createPulsarTransport } from '@fabbahiense/pulsar-pino-transport'
|
|
183
|
+
|
|
184
|
+
const logger = pino(createPulsarTransport({ url: process.env.PULSAR_URL!, apiKey: process.env.PULSAR_API_KEY!, source: 'my-api' }))
|
|
185
|
+
```
|
|
155
186
|
|
|
156
|
-
|
|
187
|
+
## Delivery guarantees
|
|
157
188
|
|
|
158
|
-
|
|
189
|
+
- **Never blocks pino.** Records are buffered as they arrive and sent in the background, so a slow or unreachable Pulsar never stalls pino's worker (or, with `sync: true`, your app's thread).
|
|
190
|
+
- **Batched.** Logs are sent in one request at a time to `/api/ingest/batch`, when `batchSize` logs are buffered or `flushInterval` passes. A backlog drains in requests of up to 1,000 logs / 512 KiB. Entries are capped at 256 KiB.
|
|
191
|
+
- **Retried.** `429`, `408`, `5xx`, network errors and timeouts are retried with exponential backoff (1 s, doubling, capped at 30 s, with jitter), honouring `Retry-After`. A retry resends the same logs with the same ids, so the server deduplicates them. When the last of `maxRetries` retries also fails, that is after `maxRetries + 1` failed requests in a row, the batch is dropped. Other `4xx` responses are permanent (a `413` first splits the batch).
|
|
192
|
+
- **Bounded memory.** At most `maxBufferSize` logs (and 16 MiB) are buffered. When full, the **oldest** logs are dropped and counted.
|
|
193
|
+
- **Delivered on shutdown.** When pino closes the transport (it does as the process exits), what is left is sent before the stream closes, within `shutdownTimeout`. After an explicit `process.exit()` pino cannot wait for network I/O, so logs still buffered then may be lost.
|
|
194
|
+
- **Reported.** Dropped logs (by reason: `overflow`, `rejected`, `retriesExhausted`, `shutdown`) are summarized on stderr at most once a minute, unless `warnOnDrop: false`.
|
|
195
|
+
|
|
196
|
+
## How it works
|
|
197
|
+
|
|
198
|
+
1. The transport runs in pino's worker thread and forwards pino records to Pulsar's `/api/ingest/batch`.
|
|
199
|
+
2. `pulsarRequestLogger` intercepts `res.write`/`res.end` to capture the response body (up to `maxBodySize`), then on `res.on('finish')` emits one structured log via your pino logger.
|
|
200
|
+
3. The log is shaped so the Pulsar UI recognizes the key names and renders dedicated sections:
|
|
201
|
+
|
|
202
|
+
| Pulsar UI section | Recognized metadata keys |
|
|
159
203
|
|---|---|
|
|
160
204
|
| Request body | `body`, `requestBody`, `reqBody`, `payload`, `input` |
|
|
161
205
|
| Response body | `response`, `responseBody`, `output`, `result`, `data` |
|
|
162
206
|
| Request headers | `headers`, `requestHeaders`, `reqHeaders` |
|
|
163
207
|
| Response headers | `responseHeaders`, `resHeaders` |
|
|
164
|
-
| Query | `query`, `queryParams`, `qs` |
|
|
165
|
-
| Path params | `params`, `pathParams` |
|
|
208
|
+
| Query params | `query`, `queryParams`, `qs` |
|
|
209
|
+
| Path params | `params`, `pathParams`, `routeParams` |
|
|
210
|
+
| Entity chip | `userId`, `accountId`, `tenantId`, `email`, `workspaceId` |
|
|
211
|
+
| Context tree | everything else |
|
|
212
|
+
|
|
213
|
+
Levels: pino's numeric levels, level labels (`formatters.level`) and custom numeric levels (mapped to the nearest standard level below them) are all understood. Times can be epoch milliseconds, epoch seconds (`stdTimeFunctions.unixTime`) or ISO strings.
|
|
214
|
+
|
|
215
|
+
## Features
|
|
216
|
+
|
|
217
|
+
- One-line drop-in Express middleware
|
|
218
|
+
- Captures request body, response body, headers, query, params
|
|
219
|
+
- Recursive redaction of sensitive keys in bodies and headers
|
|
220
|
+
- Body size limits with truncation (not drop)
|
|
221
|
+
- Skip URL list for noisy endpoints
|
|
222
|
+
- Automatic level inference: 5xx=error, 4xx=warn, else=info
|
|
223
|
+
- Custom message synthesis when pino-http emits generic strings
|
|
224
|
+
- Batched delivery with retries, backoff and `Retry-After`
|
|
225
|
+
- Bounded in-memory buffer with an explicit, counted overflow policy
|
|
226
|
+
- TypeScript types included
|
|
227
|
+
- Dual CJS/ESM package
|
|
228
|
+
- End-to-end tested
|
|
229
|
+
|
|
230
|
+
## Trace ID
|
|
231
|
+
|
|
232
|
+
The transport picks up trace IDs from these fields (first match wins):
|
|
233
|
+
`traceId`, `trace_id`, `requestId`, `req.id`
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
logger.info({ traceId: 'my-trace-id' }, 'Processing request')
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
With the middleware, the request entry gets it automatically: `req.id` (if a middleware such as pino-http set it), else the `x-trace-id` header, else a W3C `traceparent`, else a new trace id.
|
|
240
|
+
|
|
241
|
+
## Distributed tracing
|
|
242
|
+
|
|
243
|
+
pino runs this transport in a worker thread, where the app's `AsyncLocalStorage` is out of reach, so a mixin writes the trace ids into each record in the app's thread. One line:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import pino from 'pino'
|
|
247
|
+
import { pulsarMixin, instrumentFetch, startSpan } from '@fabbahiense/pulsar-pino-transport'
|
|
248
|
+
|
|
249
|
+
const logger = pino({
|
|
250
|
+
mixin: pulsarMixin(), // adds traceId, spanId, parentSpanId to every record logged inside a trace
|
|
251
|
+
transport: { target: '@fabbahiense/pulsar-pino-transport', options: { url, apiKey, source: 'checkout' } },
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
app.use(pulsarRequestLogger({ logger })) // each request runs in its own span
|
|
255
|
+
|
|
256
|
+
const tracedFetch = instrumentFetch({ logger }) // sends traceparent + x-trace-id, logs a client span
|
|
257
|
+
await startSpan('db.orders.insert', () => db.insert(order), { logger })
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Also exported from the root entry: `getTraceContext`, `runWithTrace`, `instrumentAxios`, `traceHeaders`, `injectTrace` / `extractTrace` (queues), `parseTraceparent`, `formatTraceparent`. With pino-http, register `pulsarRequestLogger()` first and set pino-http's `genReqId: () => getTraceContext()?.traceId ?? randomUUID()`, so both agree on the trace. Details: [docs/sdks.md, Distributed tracing](https://github.com/FabbSantos/pulsar/blob/main/docs/sdks.md#distributed-tracing).
|
|
166
261
|
|
|
167
262
|
## License
|
|
168
263
|
|