@nlite/logger-hapi 1.0.0 → 1.0.1
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 +372 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,48 +1,397 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nlite/logger-hapi
|
|
2
2
|
|
|
3
|
-
Hapi plugin
|
|
3
|
+
> Drop-in Hapi.js plugin that turns your server into a fully observable NLite logger client. Captures every request, response, error, 404, validation failure, and auth challenge with zero boilerplate.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Built on top of [`@nlite/logger-core`](../sdk-core/README.md) and intended to be used with the [`@nlite/logger-server`](../server/README.md) ingestion API.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Highlights](#highlights)
|
|
12
|
+
2. [Installation](#installation)
|
|
13
|
+
3. [Quick Start](#quick-start)
|
|
14
|
+
4. [Plugin Options](#plugin-options)
|
|
15
|
+
5. [Programmatic API](#programmatic-api)
|
|
16
|
+
6. [Workflow & Request Lifecycle](#workflow--request-lifecycle)
|
|
17
|
+
7. [Architecture Diagrams](#architecture-diagrams)
|
|
18
|
+
8. [Advanced Examples](#advanced-examples)
|
|
19
|
+
9. [Environment Variables](#environment-variables)
|
|
20
|
+
10. [Scripts](#scripts)
|
|
21
|
+
11. [Compatibility](#compatibility)
|
|
22
|
+
12. [License & Author](#license--author)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Highlights
|
|
27
|
+
|
|
28
|
+
- **Zero-config request/response/error capture** via Hapi lifecycle extensions.
|
|
29
|
+
- **Trace IDs** auto-generated or pulled from `x-trace-id` / `trace-id` headers.
|
|
30
|
+
- **Header sanitization** — `Authorization`, `Cookie`, `x-api-key`, etc. are redacted automatically.
|
|
31
|
+
- **Hooks for everything** — pass `getUserId`, `getSessionId`, `getTraceId` to integrate with your auth/session middleware.
|
|
32
|
+
- **Configurable ignore paths** so `/health`, `/ready`, `/metrics` don't flood your dashboard.
|
|
33
|
+
- **Graceful shutdown** — final flush on `server.events.on('stop')`.
|
|
34
|
+
|
|
35
|
+
---
|
|
11
36
|
|
|
12
37
|
## Installation
|
|
13
38
|
|
|
14
39
|
```bash
|
|
40
|
+
# npm
|
|
15
41
|
npm install @nlite/logger-hapi
|
|
42
|
+
|
|
43
|
+
# pnpm
|
|
44
|
+
pnpm add @nlite/logger-hapi
|
|
45
|
+
|
|
46
|
+
# yarn
|
|
47
|
+
yarn add @nlite/logger-hapi
|
|
16
48
|
```
|
|
17
49
|
|
|
18
|
-
|
|
50
|
+
**Requirements**
|
|
51
|
+
|
|
52
|
+
| Tool | Version |
|
|
53
|
+
|------|---------|
|
|
54
|
+
| `@hapi/hapi` | `>=20.0.0` (peer) |
|
|
55
|
+
| `@nlite/logger-core` | installed automatically (workspace dep) |
|
|
56
|
+
| Node.js | `>=18.0.0` |
|
|
19
57
|
|
|
20
|
-
|
|
58
|
+
---
|
|
21
59
|
|
|
22
|
-
##
|
|
60
|
+
## Quick Start
|
|
23
61
|
|
|
24
|
-
```
|
|
62
|
+
```ts
|
|
25
63
|
import Hapi from '@hapi/hapi';
|
|
26
|
-
import
|
|
64
|
+
import { createHapiMiddleware, getLogger } from '@nlite/logger-hapi';
|
|
65
|
+
|
|
66
|
+
async function bootstrap() {
|
|
67
|
+
const server = Hapi.server({ port: 3000 });
|
|
68
|
+
|
|
69
|
+
await server.register({
|
|
70
|
+
plugin: createHapiMiddleware({
|
|
71
|
+
loggerConfig: {
|
|
72
|
+
apiKey: process.env.NLITE_API_KEY!,
|
|
73
|
+
appName: 'orders-api',
|
|
74
|
+
platform: 'backend',
|
|
75
|
+
environment: (process.env.NODE_ENV as any) ?? 'development',
|
|
76
|
+
endpoint: process.env.NLITE_ENDPOINT ?? 'http://localhost:3000',
|
|
77
|
+
},
|
|
78
|
+
// Capture only what you need (all default to true)
|
|
79
|
+
captureRequest: true,
|
|
80
|
+
captureResponse: true,
|
|
81
|
+
captureError: true,
|
|
82
|
+
ignorePaths: ['/health', '/ready', '/metrics', '/favicon.ico'],
|
|
83
|
+
// Optional context extractors
|
|
84
|
+
getUserId: (req) => (req.auth.credentials?.user?.id as string | undefined),
|
|
85
|
+
getSessionId: (req) => req.headers['x-session-id'],
|
|
86
|
+
getTraceId: (req) => req.headers['x-trace-id'],
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
server.route({
|
|
91
|
+
method: 'GET',
|
|
92
|
+
path: '/orders/{id}',
|
|
93
|
+
handler: async (request, h) => {
|
|
94
|
+
// Access the logger from inside any handler
|
|
95
|
+
const logger = getLogger(request);
|
|
96
|
+
logger?.info('Loading order', { orderId: request.params.id });
|
|
97
|
+
return { id: request.params.id };
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await server.start();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bootstrap();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Plugin Options
|
|
110
|
+
|
|
111
|
+
`createHapiMiddleware(options: HapiMiddlewareOptions)` accepts:
|
|
112
|
+
|
|
113
|
+
| Option | Type | Default | Description |
|
|
114
|
+
|--------|------|---------|-------------|
|
|
115
|
+
| `loggerConfig` | `SdkConfig` | — | **Required.** Forwarded to `@nlite/logger-core`. |
|
|
116
|
+
| `captureRequest` | `boolean` | `true` | Log incoming requests + add navigation breadcrumbs. |
|
|
117
|
+
| `captureResponse` | `boolean` | `true` | Log outgoing responses with status, duration, headers. |
|
|
118
|
+
| `captureError` | `boolean` | `true` | Log internal errors (5xx, thrown exceptions, request errors). |
|
|
119
|
+
| `ignorePaths` | `string[]` | `['/health', '/ready', '/metrics', '/favicon.ico']` | Prefix-matched paths that skip request/response logging. |
|
|
120
|
+
| `customTags` | `Record<string,string>` | `{}` | Tags merged into every log emitted by this plugin. |
|
|
121
|
+
| `getUserId` | `(req) => string \| undefined` | — | Extract the authenticated user id. |
|
|
122
|
+
| `getSessionId` | `(req) => string \| undefined` | — | Extract a session id. |
|
|
123
|
+
| `getTraceId` | `(req) => string \| undefined` | — | Extract / generate a trace id. |
|
|
124
|
+
|
|
125
|
+
A ready-made helper is also exported:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { createDefaultHapiConfig } from '@nlite/logger-hapi';
|
|
27
129
|
|
|
28
|
-
const
|
|
29
|
-
|
|
130
|
+
const options = createDefaultHapiConfig('API_KEY', 'orders-api', {
|
|
131
|
+
captureResponse: false,
|
|
132
|
+
ignorePaths: ['/_internal/*'],
|
|
30
133
|
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Programmatic API
|
|
139
|
+
|
|
140
|
+
### `createHapiMiddleware(options)`
|
|
141
|
+
|
|
142
|
+
Returns a Hapi plugin object. Register it with `server.register({ plugin })`.
|
|
143
|
+
|
|
144
|
+
### `getLogger(request)`
|
|
145
|
+
|
|
146
|
+
Inside any route handler:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
server.route({
|
|
150
|
+
method: 'POST',
|
|
151
|
+
path: '/checkout',
|
|
152
|
+
handler: async (request) => {
|
|
153
|
+
const logger = getLogger(request)!;
|
|
154
|
+
logger.setUser('user_42', { plan: 'pro' });
|
|
155
|
+
logger.info('Checkout started');
|
|
156
|
+
// ...
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The plugin decorates each request with:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
request.nLiteLogger // LoggerSdk
|
|
165
|
+
request.nLiteLogContext // { startTime, traceId, spanId }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `hapiMiddlewareOptionsSchema`
|
|
169
|
+
|
|
170
|
+
Static schema description (useful for Hapi's `server.register({ options, plugin })` validation pattern). Note: this is a documentation object, not a Joi schema — wire it through your own validator if you need strict runtime checks.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Workflow & Request Lifecycle
|
|
175
|
+
|
|
176
|
+
Every request that is **not** on an ignore path flows through these stages:
|
|
31
177
|
|
|
32
|
-
await server.register(NliteLogger);
|
|
33
178
|
```
|
|
179
|
+
┌────────────────────────────────────────────────────┐
|
|
180
|
+
│ Hapi request arrives │
|
|
181
|
+
└─────────────────────┬──────────────────────────────┘
|
|
182
|
+
│
|
|
183
|
+
▼
|
|
184
|
+
┌──────────────────────────────────────────────────────────┐
|
|
185
|
+
│ onRequest │
|
|
186
|
+
│ - record startTime, traceId, spanId │
|
|
187
|
+
│ - store context on request.nLiteLogContext │
|
|
188
|
+
│ - build sanitized LogRequest (headers redacted) │
|
|
189
|
+
│ - add 'http' breadcrumb │
|
|
190
|
+
│ - call getUserId / getSessionId, attach user/session │
|
|
191
|
+
│ - if payload > 10KB → log 'large payload' debug │
|
|
192
|
+
└──────────────────────────┬───────────────────────────────┘
|
|
193
|
+
│
|
|
194
|
+
▼
|
|
195
|
+
route handler
|
|
196
|
+
│
|
|
197
|
+
▼
|
|
198
|
+
┌──────────────────────────────────────────────────────────┐
|
|
199
|
+
│ onPreResponse (response branch) │
|
|
200
|
+
│ - compute durationMs │
|
|
201
|
+
│ - build LogResponse │
|
|
202
|
+
│ - emit single log with level by status (5xx=error, │
|
|
203
|
+
│ 4xx=warn, else=info) │
|
|
204
|
+
└──────────────────────────┬───────────────────────────────┘
|
|
205
|
+
│
|
|
206
|
+
▼
|
|
207
|
+
┌──────────────────────────────────────────────────────────┐
|
|
208
|
+
│ onPreResponse (error branch — captureError: true) │
|
|
209
|
+
│ - 404 → warn 'Route not found' │
|
|
210
|
+
│ - 400 → warn 'Validation error' │
|
|
211
|
+
│ - 401/403 → warn 'Auth error' │
|
|
212
|
+
│ - 5xx → error with thrown Boom │
|
|
213
|
+
└──────────────────────────┬───────────────────────────────┘
|
|
214
|
+
│
|
|
215
|
+
▼
|
|
216
|
+
┌──────────────────────────────────────────────────────────┐
|
|
217
|
+
│ request event tagged error │
|
|
218
|
+
│ - emits 'Request error' with thrown error context │
|
|
219
|
+
└──────────────────────────┬───────────────────────────────┘
|
|
220
|
+
│
|
|
221
|
+
▼
|
|
222
|
+
┌─────────────────────────────────────┐
|
|
223
|
+
│ Enqueue → @nlite/logger-core │
|
|
224
|
+
│ (batched, retried, persisted) │
|
|
225
|
+
└─────────────────────────────────────┘
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Level mapping
|
|
229
|
+
|
|
230
|
+
| HTTP status | Log level |
|
|
231
|
+
|-------------|-----------|
|
|
232
|
+
| `5xx` | `error` |
|
|
233
|
+
| `4xx` | `warn` |
|
|
234
|
+
| `2xx/3xx` | `info` |
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Architecture Diagrams
|
|
239
|
+
|
|
240
|
+
### Component view
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
┌──────────────────────────┐
|
|
244
|
+
│ Hapi Server │
|
|
245
|
+
└────────────┬─────────────┘
|
|
246
|
+
│
|
|
247
|
+
▼
|
|
248
|
+
┌─────────────────────────────────────────────────────────┐
|
|
249
|
+
│ createHapiMiddleware() │
|
|
250
|
+
│ │
|
|
251
|
+
│ onRequest ─► build LogRequest (sanitize headers) │
|
|
252
|
+
│ store request.nLiteLogContext │
|
|
253
|
+
│ add 'http' breadcrumb │
|
|
254
|
+
│ │
|
|
255
|
+
│ onPreResponse ─► build LogResponse │
|
|
256
|
+
│ emit level-mapped log │
|
|
257
|
+
│ handle 4xx/5xx branches │
|
|
258
|
+
│ │
|
|
259
|
+
│ request 'error' event ─► logger.error() │
|
|
260
|
+
│ │
|
|
261
|
+
│ server 'stop' ─► logger.destroy() (flush) │
|
|
262
|
+
└────────────────────────────┬──────────────────────────┘
|
|
263
|
+
│
|
|
264
|
+
▼
|
|
265
|
+
┌────────────────────────────────────────┐
|
|
266
|
+
│ @nlite/logger-core │
|
|
267
|
+
│ queue → batch → retry → transport │
|
|
268
|
+
└────────────────────┬───────────────────┘
|
|
269
|
+
│
|
|
270
|
+
▼
|
|
271
|
+
POST {endpoint}/api/logs/batch
|
|
272
|
+
@nlite/logger-server → SQLite / Redis
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Sequence diagram
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
Client Hapi HapiLoggerPlugin CoreLogger Transport Server
|
|
279
|
+
| | | | | |
|
|
280
|
+
| POST / | | | | |
|
|
281
|
+
|-------->| | | | |
|
|
282
|
+
| | onRequest| | | |
|
|
283
|
+
| |--------->| | | |
|
|
284
|
+
| | | breadcrumb | | |
|
|
285
|
+
| | |----------------->| | |
|
|
286
|
+
| | handler | | | |
|
|
287
|
+
| |--------->| | | |
|
|
288
|
+
| | | info('hi') | | |
|
|
289
|
+
| | |----------------->| | |
|
|
290
|
+
| | onPreResponse | | |
|
|
291
|
+
| |<---------| log('POST / 200')| | |
|
|
292
|
+
| | |----------------->| | |
|
|
293
|
+
| | | | batch POST | |
|
|
294
|
+
| | | |------------>| |
|
|
295
|
+
| | | | | 200 OK |
|
|
296
|
+
| | | | |<-------->|
|
|
297
|
+
| 200 | | | | |
|
|
298
|
+
|<--------| | | | |
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Advanced Examples
|
|
304
|
+
|
|
305
|
+
### Selective capture (ignore noisy paths)
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
createHapiMiddleware({
|
|
309
|
+
loggerConfig: { /* ... */ },
|
|
310
|
+
captureResponse: true,
|
|
311
|
+
captureError: true,
|
|
312
|
+
captureRequest: true,
|
|
313
|
+
ignorePaths: ['/health', '/ready', '/metrics', '/static', '/_next'],
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Per-route user binding
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const server = Hapi.server({ port: 3000 });
|
|
321
|
+
|
|
322
|
+
await server.register({
|
|
323
|
+
plugin: createHapiMiddleware({
|
|
324
|
+
loggerConfig: { /* ... */ },
|
|
325
|
+
getUserId: (req) => (req.auth.isAuthenticated ? req.auth.credentials.user.id : undefined),
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
server.route({
|
|
330
|
+
method: 'GET',
|
|
331
|
+
path: '/me',
|
|
332
|
+
options: { auth: 'jwt' },
|
|
333
|
+
handler: (request) => {
|
|
334
|
+
const logger = getLogger(request)!;
|
|
335
|
+
logger.setUser(request.auth.credentials.user.id, request.auth.credentials.user);
|
|
336
|
+
logger.info('Profile viewed');
|
|
337
|
+
return request.auth.credentials.user;
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Custom tags via `setTags`
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
createHapiMiddleware({
|
|
346
|
+
loggerConfig: { /* ... */ },
|
|
347
|
+
customTags: { service: 'orders', team: 'checkout' },
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Tracing integration
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
createHapiMiddleware({
|
|
355
|
+
loggerConfig: { /* ... */ },
|
|
356
|
+
getTraceId: (req) => req.headers['x-b3-traceid'] as string | undefined,
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Environment Variables
|
|
363
|
+
|
|
364
|
+
| Variable | Description |
|
|
365
|
+
|----------|-------------|
|
|
366
|
+
| `NLITE_ENDPOINT` | Override the ingestion endpoint. Defaults to `http://localhost:3000`. |
|
|
367
|
+
| `NLITE_API_KEY` | API key used by `createDefaultHapiConfig`. |
|
|
368
|
+
| `NODE_ENV` | Mapped to `environment` when using the default helper. |
|
|
369
|
+
| `npm_package_version` | Used as `appVersion` by the default helper. |
|
|
370
|
+
|
|
371
|
+
---
|
|
34
372
|
|
|
35
373
|
## Scripts
|
|
36
374
|
|
|
37
375
|
| Script | Description |
|
|
38
376
|
|--------|-------------|
|
|
39
|
-
| `build` |
|
|
40
|
-
| `dev` | Watch
|
|
41
|
-
| `test` | Run
|
|
42
|
-
| `test:watch` |
|
|
43
|
-
| `lint` |
|
|
44
|
-
| `typecheck` |
|
|
377
|
+
| `npm run build` | `tsc` + copy `dist/index.js` to `dist/index.cjs`. |
|
|
378
|
+
| `npm run dev` | Watch-mode build. |
|
|
379
|
+
| `npm test` | Run Vitest. |
|
|
380
|
+
| `npm run test:watch` | Vitest watch. |
|
|
381
|
+
| `npm run lint` | ESLint over `src`. |
|
|
382
|
+
| `npm run typecheck` | `tsc --noEmit`. |
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Compatibility
|
|
387
|
+
|
|
388
|
+
- **Hapi** 20.x and 21.x.
|
|
389
|
+
- **Node.js** 18, 20, 22.
|
|
390
|
+
- TypeScript 5.3+.
|
|
391
|
+
- Works alongside other Hapi plugins (e.g. `@hapi/auth-jwt2`, `@hapi/inert`).
|
|
392
|
+
|
|
393
|
+
---
|
|
45
394
|
|
|
46
|
-
## Author
|
|
395
|
+
## License & Author
|
|
47
396
|
|
|
48
|
-
Debanjan Dasgupta
|
|
397
|
+
MIT — © Debanjan Dasgupta. See the [root README](../../README.md).
|