@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.
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 [Pulsar](https://github.com/your-org/pulsar) — send your application logs to Pulsar in real-time.
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
- ## Usage
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
- ```typescript
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 logger = pino({
24
+ export const pinoLogger = pino({
25
+ level: process.env.LOG_LEVEL || 'info',
17
26
  transport: {
18
- target: '@fabbahiense/pulsar-pino-transport',
19
- options: {
20
- url: 'https://your-pulsar-instance.com',
21
- apiKey: 'psk_your_api_key',
22
- source: 'my-api', // optional, default: 'default'
23
- batchSize: 50, // optional, default: 50
24
- flushInterval: 2000, // optional, default: 2000ms
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
- logger.info('Server started')
30
- logger.error({ traceId: 'abc-123' }, 'Something went wrong')
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
- ## Features
50
+ ### 2. Bootstrap with the middleware
34
51
 
35
- - Batched log delivery (configurable batch size)
36
- - Automatic retry with exponential backoff (3 attempts)
37
- - In-memory queue — no logs lost on temporary network failures
38
- - Zero dependencies (uses native `fetch`)
39
- - TypeScript types included
40
- - Dual CJS/ESM package
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
- ## Options
61
+ async function bootstrap() {
62
+ const app = await NestFactory.create<NestExpressApplication>(AppModule, { bufferLogs: true })
63
+ app.useLogger(app.get(Logger))
43
64
 
44
- | Option | Type | Default | Description |
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
- ## Trace ID
53
-
54
- The transport automatically picks up trace IDs from these fields:
67
+ await app.listen(process.env.PORT ?? 3000)
68
+ }
69
+ bootstrap()
70
+ ```
55
71
 
56
- - `traceId`
57
- - `trace_id`
58
- - `requestId`
59
- - `req.id`
72
+ ### 3. Use the logger normally
60
73
 
61
- ```typescript
62
- logger.info({ traceId: 'my-trace-id' }, 'Processing request')
63
- ```
74
+ ```ts
75
+ import { Logger } from '@nestjs/common'
64
76
 
65
- ## Capturing request/response bodies (with `pino-http`)
77
+ @Injectable()
78
+ export class CampaignService {
79
+ private readonly logger = new Logger(CampaignService.name)
66
80
 
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):
81
+ async tokensUsed(campaignId: string, tokensUsed: number) {
82
+ // Plain log — appears as a normal line
83
+ this.logger.log('Campaign processed')
68
84
 
69
- ```bash
70
- npm install pino-http
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
- ```typescript
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 pinoHttp from 'pino-http'
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: { url: process.env.PULSAR_URL, apiKey: process.env.PULSAR_API_KEY },
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.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
- }))
115
+ app.post('/api/users', (req, res) => {
116
+ res.status(201).json({ ok: true })
117
+ })
116
118
 
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
- }
119
+ app.listen(3000)
125
120
  ```
126
121
 
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()
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
- function tryParseJson(s: string) { try { return JSON.parse(s) } catch { return s } }
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
- > **Mais simples?** Se você puder, considere `@fabbahiense/pulsar-node` que já vem com `Pulsar.middleware({ captureResponseBody: true })` configurado.
178
+ Or let the helper build that object:
153
179
 
154
- ## Recognized metadata keys
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
- Pulsar UI auto-detects these key names and renders dedicated sections:
187
+ ## Delivery guarantees
157
188
 
158
- | Section | Recognized keys |
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