@nlite/logger-hapi
v1.0.2
Published
> 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.
Maintainers
Readme
@nlite/logger-hapi
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.
Built on top of @nlite/logger-core and intended to be used with the @nlite/logger-server ingestion API.
Table of Contents
- Highlights
- Installation
- Quick Start
- Plugin Options
- Programmatic API
- Workflow & Request Lifecycle
- Architecture Diagrams
- Advanced Examples
- Environment Variables
- Scripts
- Compatibility
- License & Author
Highlights
- Zero-config request/response/error capture via Hapi lifecycle extensions.
- Trace IDs auto-generated or pulled from
x-trace-id/trace-idheaders. - Header sanitization —
Authorization,Cookie,x-api-key, etc. are redacted automatically. - Hooks for everything — pass
getUserId,getSessionId,getTraceIdto integrate with your auth/session middleware. - Configurable ignore paths so
/health,/ready,/metricsdon't flood your dashboard. - Graceful shutdown — final flush on
server.events.on('stop').
Installation
# npm
npm install @nlite/logger-hapi
# pnpm
pnpm add @nlite/logger-hapi
# yarn
yarn add @nlite/logger-hapiRequirements
| Tool | Version |
|------|---------|
| @hapi/hapi | >=20.0.0 (peer) |
| @nlite/logger-core | installed automatically (workspace dep) |
| Node.js | >=18.0.0 |
Quick Start
import Hapi from '@hapi/hapi';
import { createHapiMiddleware, getLogger } from '@nlite/logger-hapi';
async function bootstrap() {
const server = Hapi.server({ port: 3000 });
await server.register({
plugin: createHapiMiddleware({
loggerConfig: {
apiKey: process.env.NLITE_API_KEY!,
appName: 'orders-api',
platform: 'backend',
environment: (process.env.NODE_ENV as any) ?? 'development',
endpoint: process.env.NLITE_ENDPOINT ?? 'http://localhost:3000',
},
// Capture only what you need (all default to true)
captureRequest: true,
captureResponse: true,
captureError: true,
ignorePaths: ['/health', '/ready', '/metrics', '/favicon.ico'],
// Optional context extractors
getUserId: (req) => (req.auth.credentials?.user?.id as string | undefined),
getSessionId: (req) => req.headers['x-session-id'],
getTraceId: (req) => req.headers['x-trace-id'],
}),
});
server.route({
method: 'GET',
path: '/orders/{id}',
handler: async (request, h) => {
// Access the logger from inside any handler
const logger = getLogger(request);
logger?.info('Loading order', { orderId: request.params.id });
return { id: request.params.id };
},
});
await server.start();
}
bootstrap();Plugin Options
createHapiMiddleware(options: HapiMiddlewareOptions) accepts:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| loggerConfig | SdkConfig | — | Required. Forwarded to @nlite/logger-core. |
| captureRequest | boolean | true | Log incoming requests + add navigation breadcrumbs. |
| captureResponse | boolean | true | Log outgoing responses with status, duration, headers. |
| captureError | boolean | true | Log internal errors (5xx, thrown exceptions, request errors). |
| ignorePaths | string[] | ['/health', '/ready', '/metrics', '/favicon.ico'] | Prefix-matched paths that skip request/response logging. |
| customTags | Record<string,string> | {} | Tags merged into every log emitted by this plugin. |
| getUserId | (req) => string \| undefined | — | Extract the authenticated user id. |
| getSessionId | (req) => string \| undefined | — | Extract a session id. |
| getTraceId | (req) => string \| undefined | — | Extract / generate a trace id. |
A ready-made helper is also exported:
import { createDefaultHapiConfig } from '@nlite/logger-hapi';
const options = createDefaultHapiConfig('API_KEY', 'orders-api', {
captureResponse: false,
ignorePaths: ['/_internal/*'],
});Programmatic API
createHapiMiddleware(options)
Returns a Hapi plugin object. Register it with server.register({ plugin }).
getLogger(request)
Inside any route handler:
server.route({
method: 'POST',
path: '/checkout',
handler: async (request) => {
const logger = getLogger(request)!;
logger.setUser('user_42', { plan: 'pro' });
logger.info('Checkout started');
// ...
},
});The plugin decorates each request with:
request.nLiteLogger // LoggerSdk
request.nLiteLogContext // { startTime, traceId, spanId }hapiMiddlewareOptionsSchema
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.
Workflow & Request Lifecycle
Every request that is not on an ignore path flows through these stages:
┌────────────────────────────────────────────────────┐
│ Hapi request arrives │
└─────────────────────┬──────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ onRequest │
│ - record startTime, traceId, spanId │
│ - store context on request.nLiteLogContext │
│ - build sanitized LogRequest (headers redacted) │
│ - add 'http' breadcrumb │
│ - call getUserId / getSessionId, attach user/session │
│ - if payload > 10KB → log 'large payload' debug │
└──────────────────────────┬───────────────────────────────┘
│
▼
route handler
│
▼
┌──────────────────────────────────────────────────────────┐
│ onPreResponse (response branch) │
│ - compute durationMs │
│ - build LogResponse │
│ - emit single log with level by status (5xx=error, │
│ 4xx=warn, else=info) │
└──────────────────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ onPreResponse (error branch — captureError: true) │
│ - 404 → warn 'Route not found' │
│ - 400 → warn 'Validation error' │
│ - 401/403 → warn 'Auth error' │
│ - 5xx → error with thrown Boom │
└──────────────────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ request event tagged error │
│ - emits 'Request error' with thrown error context │
└──────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Enqueue → @nlite/logger-core │
│ (batched, retried, persisted) │
└─────────────────────────────────────┘Level mapping
| HTTP status | Log level |
|-------------|-----------|
| 5xx | error |
| 4xx | warn |
| 2xx/3xx | info |
Architecture Diagrams
Component view
┌──────────────────────────┐
│ Hapi Server │
└────────────┬─────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ createHapiMiddleware() │
│ │
│ onRequest ─► build LogRequest (sanitize headers) │
│ store request.nLiteLogContext │
│ add 'http' breadcrumb │
│ │
│ onPreResponse ─► build LogResponse │
│ emit level-mapped log │
│ handle 4xx/5xx branches │
│ │
│ request 'error' event ─► logger.error() │
│ │
│ server 'stop' ─► logger.destroy() (flush) │
└────────────────────────────┬──────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ @nlite/logger-core │
│ queue → batch → retry → transport │
└────────────────────┬───────────────────┘
│
▼
POST {endpoint}/api/logs/batch
@nlite/logger-server → SQLite / RedisSequence diagram
Client Hapi HapiLoggerPlugin CoreLogger Transport Server
| | | | | |
| POST / | | | | |
|-------->| | | | |
| | onRequest| | | |
| |--------->| | | |
| | | breadcrumb | | |
| | |----------------->| | |
| | handler | | | |
| |--------->| | | |
| | | info('hi') | | |
| | |----------------->| | |
| | onPreResponse | | |
| |<---------| log('POST / 200')| | |
| | |----------------->| | |
| | | | batch POST | |
| | | |------------>| |
| | | | | 200 OK |
| | | | |<-------->|
| 200 | | | | |
|<--------| | | | |Advanced Examples
Selective capture (ignore noisy paths)
createHapiMiddleware({
loggerConfig: { /* ... */ },
captureResponse: true,
captureError: true,
captureRequest: true,
ignorePaths: ['/health', '/ready', '/metrics', '/static', '/_next'],
});Per-route user binding
const server = Hapi.server({ port: 3000 });
await server.register({
plugin: createHapiMiddleware({
loggerConfig: { /* ... */ },
getUserId: (req) => (req.auth.isAuthenticated ? req.auth.credentials.user.id : undefined),
}),
});
server.route({
method: 'GET',
path: '/me',
options: { auth: 'jwt' },
handler: (request) => {
const logger = getLogger(request)!;
logger.setUser(request.auth.credentials.user.id, request.auth.credentials.user);
logger.info('Profile viewed');
return request.auth.credentials.user;
},
});Custom tags via setTags
createHapiMiddleware({
loggerConfig: { /* ... */ },
customTags: { service: 'orders', team: 'checkout' },
});Tracing integration
createHapiMiddleware({
loggerConfig: { /* ... */ },
getTraceId: (req) => req.headers['x-b3-traceid'] as string | undefined,
});Environment Variables
| Variable | Description |
|----------|-------------|
| NLITE_ENDPOINT | Override the ingestion endpoint. Defaults to http://localhost:3000. |
| NLITE_API_KEY | API key used by createDefaultHapiConfig. |
| NODE_ENV | Mapped to environment when using the default helper. |
| npm_package_version | Used as appVersion by the default helper. |
Scripts
| Script | Description |
|--------|-------------|
| npm run build | tsc + copy dist/index.js to dist/index.cjs. |
| npm run dev | Watch-mode build. |
| npm test | Run Vitest. |
| npm run test:watch | Vitest watch. |
| npm run lint | ESLint over src. |
| npm run typecheck | tsc --noEmit. |
Compatibility
- Hapi 20.x and 21.x.
- Node.js 18, 20, 22.
- TypeScript 5.3+.
- Works alongside other Hapi plugins (e.g.
@hapi/auth-jwt2,@hapi/inert).
License & Author
MIT — © Debanjan Dasgupta. See the root README.
