@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 +48 -0
- package/dist/index.cjs +284 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +284 -0
- package/package.json +45 -0
- package/src/index.ts +378 -0
- package/tsconfig.json +21 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|