@nlite/logger-hapi 1.0.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 ADDED
@@ -0,0 +1,48 @@
1
+ # NLite Logger Hapi SDK
2
+
3
+ Hapi plugin for integrating NLite Logger into your Hapi application.
4
+
5
+ ## Features
6
+
7
+ - Seamless integration with Hapi.js
8
+ - Automatic request logging
9
+ - Structured log output
10
+ - Real-time log streaming support
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @nlite/logger-hapi
16
+ ```
17
+
18
+ ## Requirements
19
+
20
+ - @hapi/hapi >= 20.0.0
21
+
22
+ ## Usage
23
+
24
+ ```typescript
25
+ import Hapi from '@hapi/hapi';
26
+ import NliteLogger from '@nlite/logger-hapi';
27
+
28
+ const server = Hapi.server({
29
+ port: 3000,
30
+ });
31
+
32
+ await server.register(NliteLogger);
33
+ ```
34
+
35
+ ## Scripts
36
+
37
+ | Script | Description |
38
+ |--------|-------------|
39
+ | `build` | Compile TypeScript |
40
+ | `dev` | Watch mode for development |
41
+ | `test` | Run tests with Vitest |
42
+ | `test:watch` | Run tests in watch mode |
43
+ | `lint` | Lint source files |
44
+ | `typecheck` | Type check with TypeScript |
45
+
46
+ ## Author
47
+
48
+ Debanjan Dasgupta
package/dist/index.cjs ADDED
@@ -0,0 +1,284 @@
1
+ // @ts-nocheck
2
+ // Hapi.js Middleware for NLite Logger
3
+ import { createLogger, FetchTransport } from '@nlite/logger-core';
4
+ export function createHapiMiddleware(options) {
5
+ const { loggerConfig, captureRequest = true, captureResponse = true, captureError = true, ignorePaths = ['/health', '/ready', '/metrics', '/favicon.ico'], customTags = {}, getUserId, getSessionId, getTraceId, } = options;
6
+ // Create logger instance
7
+ const transport = new FetchTransport(loggerConfig.endpoint || 'http://localhost:3000', loggerConfig.apiKey, loggerConfig.headers, loggerConfig.timeout);
8
+ const logger = createLogger(loggerConfig, transport);
9
+ // Add custom tags
10
+ logger.setTags(customTags);
11
+ return {
12
+ name: 'nlite-logger',
13
+ version: '1.0.0',
14
+ register: async (server) => {
15
+ // Store logger on server for access in routes
16
+ server.decorate('request', 'nLiteLogger', logger);
17
+ // Request logging
18
+ server.ext('onRequest', (request, h) => {
19
+ // Skip ignored paths
20
+ if (ignorePaths.some(path => request.path.startsWith(path))) {
21
+ return h.continue;
22
+ }
23
+ const startTime = Date.now();
24
+ const traceId = getTraceId?.(request) || request.headers['x-trace-id'] || request.headers['trace-id'] || `trace-${startTime}-${Math.random().toString(36).substr(2, 9)}`;
25
+ const spanId = `span-${startTime}-${Math.random().toString(36).substr(2, 9)}`;
26
+ // Store context
27
+ request.nLiteLogContext = { startTime, traceId, spanId };
28
+ // Capture request
29
+ if (captureRequest) {
30
+ const logRequest = {
31
+ method: request.method.toUpperCase(),
32
+ url: `${request.info.host}${request.path}`,
33
+ path: request.path,
34
+ query: request.query,
35
+ headers: sanitizeHeaders(request.headers),
36
+ body: request.payload ? clonePayload(request.payload) : undefined,
37
+ ip: request.info.remoteAddress,
38
+ userAgent: request.headers['user-agent'] || '',
39
+ };
40
+ logger.addBreadcrumb({
41
+ type: 'http',
42
+ category: 'request',
43
+ message: `${request.method.toUpperCase()} ${request.path}`,
44
+ data: { method: request.method, path: request.path },
45
+ level: 'info',
46
+ });
47
+ // Store for response logging
48
+ request._nliteRequest = logRequest;
49
+ }
50
+ // Set user context if available
51
+ const userId = getUserId?.(request);
52
+ if (userId) {
53
+ logger.setUser(userId);
54
+ }
55
+ const sessionId = getSessionId?.(request);
56
+ if (sessionId) {
57
+ logger.setTags({ sessionId });
58
+ }
59
+ return h.continue;
60
+ });
61
+ // Response logging
62
+ if (captureResponse) {
63
+ server.ext('onPreResponse', (request, h) => {
64
+ const response = request.response;
65
+ const context = request.nLiteLogContext;
66
+ const logRequest = request._nliteRequest;
67
+ if (context && logRequest) {
68
+ const durationMs = Date.now() - context.startTime;
69
+ const isBoom = response.isBoom;
70
+ const logResponse = {
71
+ statusCode: isBoom ? response.output.statusCode : response.statusCode || 200,
72
+ statusText: isBoom ? response.output.payload.error : 'OK',
73
+ headers: response.headers || {},
74
+ body: isBoom ? response.output.payload : response.source,
75
+ durationMs,
76
+ };
77
+ const level = logResponse.statusCode >= 500 ? 'error' : logResponse.statusCode >= 400 ? 'warn' : 'info';
78
+ logger.log(level, `${request.method.toUpperCase()} ${request.path} ${logResponse.statusCode}`, {
79
+ request: logRequest,
80
+ response: logResponse,
81
+ durationMs,
82
+ traceId: context.traceId,
83
+ spanId: context.spanId,
84
+ userId: getUserId?.(request),
85
+ sessionId: getSessionId?.(request),
86
+ });
87
+ }
88
+ return h.continue;
89
+ });
90
+ }
91
+ // Error logging
92
+ if (captureError) {
93
+ // Capture request errors using 'request' event with error tags
94
+ server.events.on('request', (request, event, tags) => {
95
+ if (tags.error) {
96
+ const context = request.nLiteLogContext;
97
+ const logRequest = request._nliteRequest;
98
+ const errorMessage = typeof event === 'string' ? event : 'Request error';
99
+ logger.error(errorMessage, new Error(errorMessage), {
100
+ request: logRequest,
101
+ traceId: context?.traceId,
102
+ spanId: context?.spanId,
103
+ userId: getUserId?.(request),
104
+ sessionId: getSessionId?.(request),
105
+ tags: ['hapi', 'request-error'],
106
+ });
107
+ }
108
+ });
109
+ // Capture internal server errors (5xx responses)
110
+ server.ext('onPreResponse', (request, h) => {
111
+ const response = request.response;
112
+ if (response.isBoom && response.output.statusCode >= 500) {
113
+ const context = request.nLiteLogContext;
114
+ const logRequest = request._nliteRequest;
115
+ logger.error(response.message, new Error(response.message), {
116
+ request: logRequest,
117
+ traceId: context?.traceId,
118
+ spanId: context?.spanId,
119
+ userId: getUserId?.(request),
120
+ sessionId: getSessionId?.(request),
121
+ tags: ['hapi', 'server-error'],
122
+ errorCode: response.output.payload.error,
123
+ });
124
+ }
125
+ return h.continue;
126
+ });
127
+ }
128
+ // Additional event handlers for comprehensive logging
129
+ // Log response for all requests (if captureResponse is enabled)
130
+ if (captureResponse) {
131
+ server.ext('onPreResponse', (request, h) => {
132
+ const response = request.response;
133
+ const context = request.nLiteLogContext;
134
+ const logRequest = request._nliteRequest;
135
+ if (context && logRequest) {
136
+ const durationMs = Date.now() - context.startTime;
137
+ const isBoom = response.isBoom;
138
+ const logResponse = {
139
+ statusCode: isBoom ? response.output.statusCode : response.statusCode || 200,
140
+ statusText: isBoom ? response.output.payload.error : 'OK',
141
+ headers: response.headers || {},
142
+ body: isBoom ? response.output.payload : response.source,
143
+ durationMs,
144
+ };
145
+ const level = logResponse.statusCode >= 500 ? 'error' : logResponse.statusCode >= 400 ? 'warn' : 'info';
146
+ logger.log(level, `${request.method.toUpperCase()} ${request.path} ${logResponse.statusCode}`, {
147
+ request: logRequest,
148
+ response: logResponse,
149
+ durationMs,
150
+ traceId: context.traceId,
151
+ spanId: context.spanId,
152
+ userId: getUserId?.(request),
153
+ sessionId: getSessionId?.(request),
154
+ });
155
+ }
156
+ return h.continue;
157
+ });
158
+ }
159
+ // Log route not found (404)
160
+ server.ext('onPreResponse', (request, h) => {
161
+ const response = request.response;
162
+ if (response.isBoom && response.output.statusCode === 404) {
163
+ const context = request.nLiteLogContext;
164
+ const logRequest = request._nliteRequest;
165
+ logger.warn(`Route not found: ${request.method.toUpperCase()} ${request.path}`, {
166
+ request: logRequest,
167
+ traceId: context?.traceId,
168
+ spanId: context?.spanId,
169
+ tags: ['hapi', 'route-not-found'],
170
+ });
171
+ }
172
+ return h.continue;
173
+ });
174
+ // Log validation errors (400)
175
+ server.ext('onPreResponse', (request, h) => {
176
+ const response = request.response;
177
+ if (response.isBoom && response.output.statusCode === 400) {
178
+ const context = request.nLiteLogContext;
179
+ const logRequest = request._nliteRequest;
180
+ logger.warn(`Validation error: ${request.method.toUpperCase()} ${request.path}`, {
181
+ request: logRequest,
182
+ traceId: context?.traceId,
183
+ spanId: context?.spanId,
184
+ tags: ['hapi', 'validation-error'],
185
+ errorDetails: response.output.payload,
186
+ });
187
+ }
188
+ return h.continue;
189
+ });
190
+ // Log authentication errors (401, 403)
191
+ server.ext('onPreResponse', (request, h) => {
192
+ const response = request.response;
193
+ if (response.isBoom && (response.output.statusCode === 401 || response.output.statusCode === 403)) {
194
+ const context = request.nLiteLogContext;
195
+ const logRequest = request._nliteRequest;
196
+ logger.warn(`Auth error ${response.output.statusCode}: ${request.method.toUpperCase()} ${request.path}`, {
197
+ request: logRequest,
198
+ traceId: context?.traceId,
199
+ spanId: context?.spanId,
200
+ tags: ['hapi', 'auth-error'],
201
+ errorCode: response.output.payload.error,
202
+ });
203
+ }
204
+ return h.continue;
205
+ });
206
+ // Log request payload size for large payloads
207
+ server.ext('onRequest', (request, h) => {
208
+ if (request.payload && typeof request.payload === 'object') {
209
+ const payloadSize = JSON.stringify(request.payload).length;
210
+ if (payloadSize > 10000) { // Log if payload > 10KB
211
+ logger.debug('Large request payload', {
212
+ path: request.path,
213
+ method: request.method,
214
+ payloadSize,
215
+ tags: ['hapi', 'large-payload'],
216
+ });
217
+ }
218
+ }
219
+ return h.continue;
220
+ });
221
+ // Graceful shutdown
222
+ server.events.on('stop', async () => {
223
+ await logger.destroy();
224
+ });
225
+ },
226
+ };
227
+ }
228
+ // Helper to access logger in route handlers
229
+ export function getLogger(request) {
230
+ return request.nLiteLogger;
231
+ }
232
+ // Plugin options validation
233
+ export const hapiMiddlewareOptionsSchema = {
234
+ loggerConfig: { required: true },
235
+ captureRequest: { type: 'boolean', default: true },
236
+ captureResponse: { type: 'boolean', default: true },
237
+ captureError: { type: 'boolean', default: true },
238
+ ignorePaths: { type: 'array', items: 'string', default: ['/health', '/ready', '/metrics'] },
239
+ customTags: { type: 'object', default: {} },
240
+ getUserId: { type: 'function' },
241
+ getSessionId: { type: 'function' },
242
+ getTraceId: { type: 'function' },
243
+ };
244
+ // Utility functions
245
+ function sanitizeHeaders(headers) {
246
+ const sanitized = {};
247
+ const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token', 'proxy-authorization'];
248
+ for (const [key, value] of Object.entries(headers)) {
249
+ if (sensitiveHeaders.includes(key.toLowerCase())) {
250
+ sanitized[key] = '[REDACTED]';
251
+ }
252
+ else {
253
+ sanitized[key] = value;
254
+ }
255
+ }
256
+ return sanitized;
257
+ }
258
+ function clonePayload(payload) {
259
+ try {
260
+ if (typeof payload === 'object' && payload !== null) {
261
+ return JSON.parse(JSON.stringify(payload));
262
+ }
263
+ return payload;
264
+ }
265
+ catch {
266
+ return '[UNCLONEABLE]';
267
+ }
268
+ }
269
+ // Default configuration helper
270
+ export function createDefaultHapiConfig(apiKey, appName, options = {}) {
271
+ return {
272
+ loggerConfig: {
273
+ apiKey,
274
+ appName,
275
+ platform: 'backend',
276
+ environment: process.env.NODE_ENV || 'development',
277
+ appVersion: process.env.npm_package_version || '1.0.0',
278
+ endpoint: process.env.NLITE_ENDPOINT || 'http://localhost:3000',
279
+ autoCapture: true,
280
+ ...options.loggerConfig,
281
+ },
282
+ ...options,
283
+ };
284
+ }
@@ -0,0 +1,76 @@
1
+ import { Server, Request } from '@hapi/hapi';
2
+ import { type LoggerSdk, type SdkConfig } from '@nlite/logger-core';
3
+ export interface HapiMiddlewareOptions {
4
+ /** Logger SDK configuration */
5
+ loggerConfig: SdkConfig;
6
+ /** Capture request details (default: true) */
7
+ captureRequest?: boolean;
8
+ /** Capture response details (default: true) */
9
+ captureResponse?: boolean;
10
+ /** Capture errors (default: true) */
11
+ captureError?: boolean;
12
+ /** Paths to ignore (default: ['/health', '/ready', '/metrics']) */
13
+ ignorePaths?: string[];
14
+ /** Custom tags to add to all logs */
15
+ customTags?: Record<string, string>;
16
+ /** Function to extract user ID from request */
17
+ getUserId?: (request: Request) => string | undefined;
18
+ /** Function to extract session ID from request */
19
+ getSessionId?: (request: Request) => string | undefined;
20
+ /** Function to extract trace ID from request */
21
+ getTraceId?: (request: Request) => string | undefined;
22
+ }
23
+ declare module '@hapi/hapi' {
24
+ interface Request {
25
+ nLiteLogger?: LoggerSdk;
26
+ nLiteLogContext?: {
27
+ startTime: number;
28
+ traceId: string;
29
+ spanId: string;
30
+ };
31
+ }
32
+ }
33
+ export declare function createHapiMiddleware(options: HapiMiddlewareOptions): {
34
+ name: string;
35
+ version: string;
36
+ register: (server: Server) => Promise<void>;
37
+ };
38
+ export declare function getLogger(request: Request): LoggerSdk;
39
+ export declare const hapiMiddlewareOptionsSchema: {
40
+ loggerConfig: {
41
+ required: boolean;
42
+ };
43
+ captureRequest: {
44
+ type: string;
45
+ default: boolean;
46
+ };
47
+ captureResponse: {
48
+ type: string;
49
+ default: boolean;
50
+ };
51
+ captureError: {
52
+ type: string;
53
+ default: boolean;
54
+ };
55
+ ignorePaths: {
56
+ type: string;
57
+ items: string;
58
+ default: string[];
59
+ };
60
+ customTags: {
61
+ type: string;
62
+ default: {};
63
+ };
64
+ getUserId: {
65
+ type: string;
66
+ };
67
+ getSessionId: {
68
+ type: string;
69
+ };
70
+ getTraceId: {
71
+ type: string;
72
+ };
73
+ };
74
+ export declare function createDefaultHapiConfig(apiKey: string, appName: string, options?: Partial<HapiMiddlewareOptions>): HapiMiddlewareOptions;
75
+ export type { LoggerSdk, SdkConfig, LogLevel, LogContext, LogRequest, LogResponse } from '@nlite/logger-core';
76
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,OAAO,EAA8B,MAAM,YAAY,CAAC;AACzE,OAAO,EAAgC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAqE,MAAM,oBAAoB,CAAC;AAErK,MAAM,WAAW,qBAAqB;IACpC,+BAA+B;IAC/B,YAAY,EAAE,SAAS,CAAC;IACxB,8CAA8C;IAC9C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,+CAA+C;IAC/C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,qCAAqC;IACrC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,+CAA+C;IAC/C,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD,kDAAkD;IAClD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACxD,gDAAgD;IAChD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;CACvD;AAED,OAAO,QAAQ,YAAY,CAAC;IAC1B,UAAU,OAAO;QACf,WAAW,CAAC,EAAE,SAAS,CAAC;QACxB,eAAe,CAAC,EAAE;YAChB,SAAS,EAAE,MAAM,CAAC;YAClB,OAAO,EAAE,MAAM,CAAC;YAChB,MAAM,EAAE,MAAM,CAAC;SAChB,CAAC;KACH;CACF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB;;;uBAuBtC,MAAM;EAwPlC;AAGD,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAErD;AAGD,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAUvC,CAAC;AA6BF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,OAAO,CAAC,qBAAqB,CAAM,GAC3C,qBAAqB,CAcvB;AAGD,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,284 @@
1
+ // @ts-nocheck
2
+ // Hapi.js Middleware for NLite Logger
3
+ import { createLogger, FetchTransport } from '@nlite/logger-core';
4
+ export function createHapiMiddleware(options) {
5
+ const { loggerConfig, captureRequest = true, captureResponse = true, captureError = true, ignorePaths = ['/health', '/ready', '/metrics', '/favicon.ico'], customTags = {}, getUserId, getSessionId, getTraceId, } = options;
6
+ // Create logger instance
7
+ const transport = new FetchTransport(loggerConfig.endpoint || 'http://localhost:3000', loggerConfig.apiKey, loggerConfig.headers, loggerConfig.timeout);
8
+ const logger = createLogger(loggerConfig, transport);
9
+ // Add custom tags
10
+ logger.setTags(customTags);
11
+ return {
12
+ name: 'nlite-logger',
13
+ version: '1.0.0',
14
+ register: async (server) => {
15
+ // Store logger on server for access in routes
16
+ server.decorate('request', 'nLiteLogger', logger);
17
+ // Request logging
18
+ server.ext('onRequest', (request, h) => {
19
+ // Skip ignored paths
20
+ if (ignorePaths.some(path => request.path.startsWith(path))) {
21
+ return h.continue;
22
+ }
23
+ const startTime = Date.now();
24
+ const traceId = getTraceId?.(request) || request.headers['x-trace-id'] || request.headers['trace-id'] || `trace-${startTime}-${Math.random().toString(36).substr(2, 9)}`;
25
+ const spanId = `span-${startTime}-${Math.random().toString(36).substr(2, 9)}`;
26
+ // Store context
27
+ request.nLiteLogContext = { startTime, traceId, spanId };
28
+ // Capture request
29
+ if (captureRequest) {
30
+ const logRequest = {
31
+ method: request.method.toUpperCase(),
32
+ url: `${request.info.host}${request.path}`,
33
+ path: request.path,
34
+ query: request.query,
35
+ headers: sanitizeHeaders(request.headers),
36
+ body: request.payload ? clonePayload(request.payload) : undefined,
37
+ ip: request.info.remoteAddress,
38
+ userAgent: request.headers['user-agent'] || '',
39
+ };
40
+ logger.addBreadcrumb({
41
+ type: 'http',
42
+ category: 'request',
43
+ message: `${request.method.toUpperCase()} ${request.path}`,
44
+ data: { method: request.method, path: request.path },
45
+ level: 'info',
46
+ });
47
+ // Store for response logging
48
+ request._nliteRequest = logRequest;
49
+ }
50
+ // Set user context if available
51
+ const userId = getUserId?.(request);
52
+ if (userId) {
53
+ logger.setUser(userId);
54
+ }
55
+ const sessionId = getSessionId?.(request);
56
+ if (sessionId) {
57
+ logger.setTags({ sessionId });
58
+ }
59
+ return h.continue;
60
+ });
61
+ // Response logging
62
+ if (captureResponse) {
63
+ server.ext('onPreResponse', (request, h) => {
64
+ const response = request.response;
65
+ const context = request.nLiteLogContext;
66
+ const logRequest = request._nliteRequest;
67
+ if (context && logRequest) {
68
+ const durationMs = Date.now() - context.startTime;
69
+ const isBoom = response.isBoom;
70
+ const logResponse = {
71
+ statusCode: isBoom ? response.output.statusCode : response.statusCode || 200,
72
+ statusText: isBoom ? response.output.payload.error : 'OK',
73
+ headers: response.headers || {},
74
+ body: isBoom ? response.output.payload : response.source,
75
+ durationMs,
76
+ };
77
+ const level = logResponse.statusCode >= 500 ? 'error' : logResponse.statusCode >= 400 ? 'warn' : 'info';
78
+ logger.log(level, `${request.method.toUpperCase()} ${request.path} ${logResponse.statusCode}`, {
79
+ request: logRequest,
80
+ response: logResponse,
81
+ durationMs,
82
+ traceId: context.traceId,
83
+ spanId: context.spanId,
84
+ userId: getUserId?.(request),
85
+ sessionId: getSessionId?.(request),
86
+ });
87
+ }
88
+ return h.continue;
89
+ });
90
+ }
91
+ // Error logging
92
+ if (captureError) {
93
+ // Capture request errors using 'request' event with error tags
94
+ server.events.on('request', (request, event, tags) => {
95
+ if (tags.error) {
96
+ const context = request.nLiteLogContext;
97
+ const logRequest = request._nliteRequest;
98
+ const errorMessage = typeof event === 'string' ? event : 'Request error';
99
+ logger.error(errorMessage, new Error(errorMessage), {
100
+ request: logRequest,
101
+ traceId: context?.traceId,
102
+ spanId: context?.spanId,
103
+ userId: getUserId?.(request),
104
+ sessionId: getSessionId?.(request),
105
+ tags: ['hapi', 'request-error'],
106
+ });
107
+ }
108
+ });
109
+ // Capture internal server errors (5xx responses)
110
+ server.ext('onPreResponse', (request, h) => {
111
+ const response = request.response;
112
+ if (response.isBoom && response.output.statusCode >= 500) {
113
+ const context = request.nLiteLogContext;
114
+ const logRequest = request._nliteRequest;
115
+ logger.error(response.message, new Error(response.message), {
116
+ request: logRequest,
117
+ traceId: context?.traceId,
118
+ spanId: context?.spanId,
119
+ userId: getUserId?.(request),
120
+ sessionId: getSessionId?.(request),
121
+ tags: ['hapi', 'server-error'],
122
+ errorCode: response.output.payload.error,
123
+ });
124
+ }
125
+ return h.continue;
126
+ });
127
+ }
128
+ // Additional event handlers for comprehensive logging
129
+ // Log response for all requests (if captureResponse is enabled)
130
+ if (captureResponse) {
131
+ server.ext('onPreResponse', (request, h) => {
132
+ const response = request.response;
133
+ const context = request.nLiteLogContext;
134
+ const logRequest = request._nliteRequest;
135
+ if (context && logRequest) {
136
+ const durationMs = Date.now() - context.startTime;
137
+ const isBoom = response.isBoom;
138
+ const logResponse = {
139
+ statusCode: isBoom ? response.output.statusCode : response.statusCode || 200,
140
+ statusText: isBoom ? response.output.payload.error : 'OK',
141
+ headers: response.headers || {},
142
+ body: isBoom ? response.output.payload : response.source,
143
+ durationMs,
144
+ };
145
+ const level = logResponse.statusCode >= 500 ? 'error' : logResponse.statusCode >= 400 ? 'warn' : 'info';
146
+ logger.log(level, `${request.method.toUpperCase()} ${request.path} ${logResponse.statusCode}`, {
147
+ request: logRequest,
148
+ response: logResponse,
149
+ durationMs,
150
+ traceId: context.traceId,
151
+ spanId: context.spanId,
152
+ userId: getUserId?.(request),
153
+ sessionId: getSessionId?.(request),
154
+ });
155
+ }
156
+ return h.continue;
157
+ });
158
+ }
159
+ // Log route not found (404)
160
+ server.ext('onPreResponse', (request, h) => {
161
+ const response = request.response;
162
+ if (response.isBoom && response.output.statusCode === 404) {
163
+ const context = request.nLiteLogContext;
164
+ const logRequest = request._nliteRequest;
165
+ logger.warn(`Route not found: ${request.method.toUpperCase()} ${request.path}`, {
166
+ request: logRequest,
167
+ traceId: context?.traceId,
168
+ spanId: context?.spanId,
169
+ tags: ['hapi', 'route-not-found'],
170
+ });
171
+ }
172
+ return h.continue;
173
+ });
174
+ // Log validation errors (400)
175
+ server.ext('onPreResponse', (request, h) => {
176
+ const response = request.response;
177
+ if (response.isBoom && response.output.statusCode === 400) {
178
+ const context = request.nLiteLogContext;
179
+ const logRequest = request._nliteRequest;
180
+ logger.warn(`Validation error: ${request.method.toUpperCase()} ${request.path}`, {
181
+ request: logRequest,
182
+ traceId: context?.traceId,
183
+ spanId: context?.spanId,
184
+ tags: ['hapi', 'validation-error'],
185
+ errorDetails: response.output.payload,
186
+ });
187
+ }
188
+ return h.continue;
189
+ });
190
+ // Log authentication errors (401, 403)
191
+ server.ext('onPreResponse', (request, h) => {
192
+ const response = request.response;
193
+ if (response.isBoom && (response.output.statusCode === 401 || response.output.statusCode === 403)) {
194
+ const context = request.nLiteLogContext;
195
+ const logRequest = request._nliteRequest;
196
+ logger.warn(`Auth error ${response.output.statusCode}: ${request.method.toUpperCase()} ${request.path}`, {
197
+ request: logRequest,
198
+ traceId: context?.traceId,
199
+ spanId: context?.spanId,
200
+ tags: ['hapi', 'auth-error'],
201
+ errorCode: response.output.payload.error,
202
+ });
203
+ }
204
+ return h.continue;
205
+ });
206
+ // Log request payload size for large payloads
207
+ server.ext('onRequest', (request, h) => {
208
+ if (request.payload && typeof request.payload === 'object') {
209
+ const payloadSize = JSON.stringify(request.payload).length;
210
+ if (payloadSize > 10000) { // Log if payload > 10KB
211
+ logger.debug('Large request payload', {
212
+ path: request.path,
213
+ method: request.method,
214
+ payloadSize,
215
+ tags: ['hapi', 'large-payload'],
216
+ });
217
+ }
218
+ }
219
+ return h.continue;
220
+ });
221
+ // Graceful shutdown
222
+ server.events.on('stop', async () => {
223
+ await logger.destroy();
224
+ });
225
+ },
226
+ };
227
+ }
228
+ // Helper to access logger in route handlers
229
+ export function getLogger(request) {
230
+ return request.nLiteLogger;
231
+ }
232
+ // Plugin options validation
233
+ export const hapiMiddlewareOptionsSchema = {
234
+ loggerConfig: { required: true },
235
+ captureRequest: { type: 'boolean', default: true },
236
+ captureResponse: { type: 'boolean', default: true },
237
+ captureError: { type: 'boolean', default: true },
238
+ ignorePaths: { type: 'array', items: 'string', default: ['/health', '/ready', '/metrics'] },
239
+ customTags: { type: 'object', default: {} },
240
+ getUserId: { type: 'function' },
241
+ getSessionId: { type: 'function' },
242
+ getTraceId: { type: 'function' },
243
+ };
244
+ // Utility functions
245
+ function sanitizeHeaders(headers) {
246
+ const sanitized = {};
247
+ const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token', 'proxy-authorization'];
248
+ for (const [key, value] of Object.entries(headers)) {
249
+ if (sensitiveHeaders.includes(key.toLowerCase())) {
250
+ sanitized[key] = '[REDACTED]';
251
+ }
252
+ else {
253
+ sanitized[key] = value;
254
+ }
255
+ }
256
+ return sanitized;
257
+ }
258
+ function clonePayload(payload) {
259
+ try {
260
+ if (typeof payload === 'object' && payload !== null) {
261
+ return JSON.parse(JSON.stringify(payload));
262
+ }
263
+ return payload;
264
+ }
265
+ catch {
266
+ return '[UNCLONEABLE]';
267
+ }
268
+ }
269
+ // Default configuration helper
270
+ export function createDefaultHapiConfig(apiKey, appName, options = {}) {
271
+ return {
272
+ loggerConfig: {
273
+ apiKey,
274
+ appName,
275
+ platform: 'backend',
276
+ environment: process.env.NODE_ENV || 'development',
277
+ appVersion: process.env.npm_package_version || '1.0.0',
278
+ endpoint: process.env.NLITE_ENDPOINT || 'http://localhost:3000',
279
+ autoCapture: true,
280
+ ...options.loggerConfig,
281
+ },
282
+ ...options,
283
+ };
284
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@nlite/logger-hapi",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "keywords": [
6
+ "logging",
7
+ "logger",
8
+ "hapi",
9
+ "nlite",
10
+ "sdk",
11
+ "monitoring",
12
+ "observability"
13
+ ],
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs",
20
+ "types": "./dist/index.d.ts"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "tsc && cp dist/index.js dist/index.cjs",
25
+ "dev": "tsc --watch",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "lint": "eslint src --ext .ts",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "peerDependencies": {
32
+ "@hapi/hapi": ">=20.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@nlite/logger-core": "file:../sdk-core"
36
+ },
37
+ "author": "Debanjan Dasgupta",
38
+ "devDependencies": {
39
+ "@hapi/hapi": "^21.3.2",
40
+ "@types/node": "^20.10.0",
41
+ "eslint": "^8.56.0",
42
+ "typescript": "^5.3.3",
43
+ "vitest": "^1.1.0"
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,378 @@
1
+ // @ts-nocheck
2
+ // Hapi.js Middleware for NLite Logger
3
+
4
+ import { Server, Request, ResponseToolkit, Lifecycle } from '@hapi/hapi';
5
+ import { createLogger, FetchTransport, type LoggerSdk, type SdkConfig, type LogLevel, type LogContext, type LogRequest, type LogResponse } from '@nlite/logger-core';
6
+
7
+ export interface HapiMiddlewareOptions {
8
+ /** Logger SDK configuration */
9
+ loggerConfig: SdkConfig;
10
+ /** Capture request details (default: true) */
11
+ captureRequest?: boolean;
12
+ /** Capture response details (default: true) */
13
+ captureResponse?: boolean;
14
+ /** Capture errors (default: true) */
15
+ captureError?: boolean;
16
+ /** Paths to ignore (default: ['/health', '/ready', '/metrics']) */
17
+ ignorePaths?: string[];
18
+ /** Custom tags to add to all logs */
19
+ customTags?: Record<string, string>;
20
+ /** Function to extract user ID from request */
21
+ getUserId?: (request: Request) => string | undefined;
22
+ /** Function to extract session ID from request */
23
+ getSessionId?: (request: Request) => string | undefined;
24
+ /** Function to extract trace ID from request */
25
+ getTraceId?: (request: Request) => string | undefined;
26
+ }
27
+
28
+ declare module '@hapi/hapi' {
29
+ interface Request {
30
+ nLiteLogger?: LoggerSdk;
31
+ nLiteLogContext?: {
32
+ startTime: number;
33
+ traceId: string;
34
+ spanId: string;
35
+ };
36
+ }
37
+ }
38
+
39
+ export function createHapiMiddleware(options: HapiMiddlewareOptions) {
40
+ const {
41
+ loggerConfig,
42
+ captureRequest = true,
43
+ captureResponse = true,
44
+ captureError = true,
45
+ ignorePaths = ['/health', '/ready', '/metrics', '/favicon.ico'],
46
+ customTags = {},
47
+ getUserId,
48
+ getSessionId,
49
+ getTraceId,
50
+ } = options;
51
+
52
+ // Create logger instance
53
+ const transport = new FetchTransport(loggerConfig.endpoint || 'http://localhost:3000', loggerConfig.apiKey, loggerConfig.headers, loggerConfig.timeout);
54
+ const logger = createLogger(loggerConfig, transport);
55
+
56
+ // Add custom tags
57
+ logger.setTags(customTags);
58
+
59
+ return {
60
+ name: 'nlite-logger',
61
+ version: '1.0.0',
62
+ register: async (server: Server) => {
63
+ // Store logger on server for access in routes
64
+ server.decorate('request', 'nLiteLogger', logger);
65
+
66
+ // Request logging
67
+ server.ext('onRequest', (request: Request, h: ResponseToolkit) => {
68
+ // Skip ignored paths
69
+ if (ignorePaths.some(path => request.path.startsWith(path))) {
70
+ return h.continue;
71
+ }
72
+
73
+ const startTime = Date.now();
74
+ const traceId = getTraceId?.(request) || request.headers['x-trace-id'] || request.headers['trace-id'] || `trace-${startTime}-${Math.random().toString(36).substr(2, 9)}`;
75
+ const spanId = `span-${startTime}-${Math.random().toString(36).substr(2, 9)}`;
76
+
77
+ // Store context
78
+ request.nLiteLogContext = { startTime, traceId, spanId };
79
+
80
+ // Capture request
81
+ if (captureRequest) {
82
+ const logRequest: LogRequest = {
83
+ method: request.method.toUpperCase(),
84
+ url: `${request.info.host}${request.path}`,
85
+ path: request.path,
86
+ query: request.query,
87
+ headers: sanitizeHeaders(request.headers),
88
+ body: request.payload ? clonePayload(request.payload) : undefined,
89
+ ip: request.info.remoteAddress,
90
+ userAgent: request.headers['user-agent'] || '',
91
+ };
92
+
93
+ logger.addBreadcrumb({
94
+ type: 'http',
95
+ category: 'request',
96
+ message: `${request.method.toUpperCase()} ${request.path}`,
97
+ data: { method: request.method, path: request.path },
98
+ level: 'info',
99
+ });
100
+
101
+ // Store for response logging
102
+ (request as any)._nliteRequest = logRequest;
103
+ }
104
+
105
+ // Set user context if available
106
+ const userId = getUserId?.(request);
107
+ if (userId) {
108
+ logger.setUser(userId);
109
+ }
110
+
111
+ const sessionId = getSessionId?.(request);
112
+ if (sessionId) {
113
+ logger.setTags({ sessionId });
114
+ }
115
+
116
+ return h.continue;
117
+ });
118
+
119
+ // Response logging
120
+ if (captureResponse) {
121
+ server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
122
+ const response = request.response;
123
+ const context = request.nLiteLogContext;
124
+ const logRequest = (request as any)._nliteRequest;
125
+
126
+ if (context && logRequest) {
127
+ const durationMs = Date.now() - context.startTime;
128
+ const isBoom = response.isBoom;
129
+
130
+ const logResponse: LogResponse = {
131
+ statusCode: isBoom ? response.output.statusCode : (response as any).statusCode || 200,
132
+ statusText: isBoom ? response.output.payload.error : 'OK',
133
+ headers: (response as any).headers || {},
134
+ body: isBoom ? response.output.payload : (response as any).source,
135
+ durationMs,
136
+ };
137
+
138
+ const level: LogLevel = logResponse.statusCode >= 500 ? 'error' : logResponse.statusCode >= 400 ? 'warn' : 'info';
139
+
140
+ logger.log(level, `${request.method.toUpperCase()} ${request.path} ${logResponse.statusCode}`, {
141
+ request: logRequest,
142
+ response: logResponse,
143
+ durationMs,
144
+ traceId: context.traceId,
145
+ spanId: context.spanId,
146
+ userId: getUserId?.(request),
147
+ sessionId: getSessionId?.(request),
148
+ });
149
+ }
150
+
151
+ return h.continue;
152
+ });
153
+ }
154
+
155
+ // Error logging
156
+ if (captureError) {
157
+ // Capture request errors using 'request' event with error tags
158
+ server.events.on('request', (request: Request, event: string, tags: Record<string, boolean>) => {
159
+ if (tags.error) {
160
+ const context = request.nLiteLogContext;
161
+ const logRequest = (request as any)._nliteRequest;
162
+
163
+ const errorMessage = typeof event === 'string' ? event : 'Request error';
164
+ logger.error(errorMessage, new Error(errorMessage), {
165
+ request: logRequest,
166
+ traceId: context?.traceId,
167
+ spanId: context?.spanId,
168
+ userId: getUserId?.(request),
169
+ sessionId: getSessionId?.(request),
170
+ tags: ['hapi', 'request-error'],
171
+ });
172
+ }
173
+ });
174
+
175
+ // Capture internal server errors (5xx responses)
176
+ server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
177
+ const response = request.response;
178
+
179
+ if (response.isBoom && response.output.statusCode >= 500) {
180
+ const context = request.nLiteLogContext;
181
+ const logRequest = (request as any)._nliteRequest;
182
+
183
+ logger.error(response.message, new Error(response.message), {
184
+ request: logRequest,
185
+ traceId: context?.traceId,
186
+ spanId: context?.spanId,
187
+ userId: getUserId?.(request),
188
+ sessionId: getSessionId?.(request),
189
+ tags: ['hapi', 'server-error'],
190
+ errorCode: response.output.payload.error,
191
+ });
192
+ }
193
+
194
+ return h.continue;
195
+ });
196
+ }
197
+
198
+ // Additional event handlers for comprehensive logging
199
+ // Log response for all requests (if captureResponse is enabled)
200
+ if (captureResponse) {
201
+ server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
202
+ const response = request.response;
203
+ const context = request.nLiteLogContext;
204
+ const logRequest = (request as any)._nliteRequest;
205
+
206
+ if (context && logRequest) {
207
+ const durationMs = Date.now() - context.startTime;
208
+ const isBoom = response.isBoom;
209
+
210
+ const logResponse: LogResponse = {
211
+ statusCode: isBoom ? response.output.statusCode : (response as any).statusCode || 200,
212
+ statusText: isBoom ? response.output.payload.error : 'OK',
213
+ headers: (response as any).headers || {},
214
+ body: isBoom ? response.output.payload : (response as any).source,
215
+ durationMs,
216
+ };
217
+
218
+ const level: LogLevel = logResponse.statusCode >= 500 ? 'error' : logResponse.statusCode >= 400 ? 'warn' : 'info';
219
+
220
+ logger.log(level, `${request.method.toUpperCase()} ${request.path} ${logResponse.statusCode}`, {
221
+ request: logRequest,
222
+ response: logResponse,
223
+ durationMs,
224
+ traceId: context.traceId,
225
+ spanId: context.spanId,
226
+ userId: getUserId?.(request),
227
+ sessionId: getSessionId?.(request),
228
+ });
229
+ }
230
+
231
+ return h.continue;
232
+ });
233
+ }
234
+
235
+ // Log route not found (404)
236
+ server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
237
+ const response = request.response;
238
+ if (response.isBoom && response.output.statusCode === 404) {
239
+ const context = request.nLiteLogContext;
240
+ const logRequest = (request as any)._nliteRequest;
241
+
242
+ logger.warn(`Route not found: ${request.method.toUpperCase()} ${request.path}`, {
243
+ request: logRequest,
244
+ traceId: context?.traceId,
245
+ spanId: context?.spanId,
246
+ tags: ['hapi', 'route-not-found'],
247
+ });
248
+ }
249
+ return h.continue;
250
+ });
251
+
252
+ // Log validation errors (400)
253
+ server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
254
+ const response = request.response;
255
+ if (response.isBoom && response.output.statusCode === 400) {
256
+ const context = request.nLiteLogContext;
257
+ const logRequest = (request as any)._nliteRequest;
258
+
259
+ logger.warn(`Validation error: ${request.method.toUpperCase()} ${request.path}`, {
260
+ request: logRequest,
261
+ traceId: context?.traceId,
262
+ spanId: context?.spanId,
263
+ tags: ['hapi', 'validation-error'],
264
+ errorDetails: response.output.payload,
265
+ });
266
+ }
267
+ return h.continue;
268
+ });
269
+
270
+ // Log authentication errors (401, 403)
271
+ server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
272
+ const response = request.response;
273
+ if (response.isBoom && (response.output.statusCode === 401 || response.output.statusCode === 403)) {
274
+ const context = request.nLiteLogContext;
275
+ const logRequest = (request as any)._nliteRequest;
276
+
277
+ logger.warn(`Auth error ${response.output.statusCode}: ${request.method.toUpperCase()} ${request.path}`, {
278
+ request: logRequest,
279
+ traceId: context?.traceId,
280
+ spanId: context?.spanId,
281
+ tags: ['hapi', 'auth-error'],
282
+ errorCode: response.output.payload.error,
283
+ });
284
+ }
285
+ return h.continue;
286
+ });
287
+
288
+ // Log request payload size for large payloads
289
+ server.ext('onRequest', (request: Request, h: ResponseToolkit) => {
290
+ if (request.payload && typeof request.payload === 'object') {
291
+ const payloadSize = JSON.stringify(request.payload).length;
292
+ if (payloadSize > 10000) { // Log if payload > 10KB
293
+ logger.debug('Large request payload', {
294
+ path: request.path,
295
+ method: request.method,
296
+ payloadSize,
297
+ tags: ['hapi', 'large-payload'],
298
+ });
299
+ }
300
+ }
301
+ return h.continue;
302
+ });
303
+
304
+ // Graceful shutdown
305
+ server.events.on('stop', async () => {
306
+ await logger.destroy();
307
+ });
308
+ },
309
+ };
310
+ }
311
+
312
+ // Helper to access logger in route handlers
313
+ export function getLogger(request: Request): LoggerSdk {
314
+ return request.nLiteLogger!;
315
+ }
316
+
317
+ // Plugin options validation
318
+ export const hapiMiddlewareOptionsSchema = {
319
+ loggerConfig: { required: true },
320
+ captureRequest: { type: 'boolean', default: true },
321
+ captureResponse: { type: 'boolean', default: true },
322
+ captureError: { type: 'boolean', default: true },
323
+ ignorePaths: { type: 'array', items: 'string', default: ['/health', '/ready', '/metrics'] },
324
+ customTags: { type: 'object', default: {} },
325
+ getUserId: { type: 'function' },
326
+ getSessionId: { type: 'function' },
327
+ getTraceId: { type: 'function' },
328
+ };
329
+
330
+ // Utility functions
331
+ function sanitizeHeaders(headers: Record<string, string>): Record<string, string> {
332
+ const sanitized: Record<string, string> = {};
333
+ const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token', 'proxy-authorization'];
334
+
335
+ for (const [key, value] of Object.entries(headers)) {
336
+ if (sensitiveHeaders.includes(key.toLowerCase())) {
337
+ sanitized[key] = '[REDACTED]';
338
+ } else {
339
+ sanitized[key] = value;
340
+ }
341
+ }
342
+ return sanitized;
343
+ }
344
+
345
+ function clonePayload(payload: any): any {
346
+ try {
347
+ if (typeof payload === 'object' && payload !== null) {
348
+ return JSON.parse(JSON.stringify(payload));
349
+ }
350
+ return payload;
351
+ } catch {
352
+ return '[UNCLONEABLE]';
353
+ }
354
+ }
355
+
356
+ // Default configuration helper
357
+ export function createDefaultHapiConfig(
358
+ apiKey: string,
359
+ appName: string,
360
+ options: Partial<HapiMiddlewareOptions> = {}
361
+ ): HapiMiddlewareOptions {
362
+ return {
363
+ loggerConfig: {
364
+ apiKey,
365
+ appName,
366
+ platform: 'backend',
367
+ environment: (process.env.NODE_ENV as any) || 'development',
368
+ appVersion: process.env.npm_package_version || '1.0.0',
369
+ endpoint: process.env.NLITE_ENDPOINT || 'http://localhost:3000',
370
+ autoCapture: true,
371
+ ...options.loggerConfig,
372
+ },
373
+ ...options,
374
+ };
375
+ }
376
+
377
+ // Export types
378
+ export type { LoggerSdk, SdkConfig, LogLevel, LogContext, LogRequest, LogResponse } from '@nlite/logger-core';
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "outDir": "./dist",
17
+ "rootDir": "./src"
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "**/__tests__/**"]
21
+ }