@devminister/applog-client 0.0.1 → 0.0.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Application log client for the **Monitoring** platform. Send structured business logs (auth, payments, orders, notifications) with buffered, batched delivery, retry, and duration tracking.
4
4
 
5
- Works as a **standalone Node.js client** or as a **NestJS module**.
5
+ Works as a **standalone Node.js client** or as a **NestJS module** with automatic HTTP request logging.
6
6
 
7
7
  ## Install
8
8
 
@@ -53,7 +53,8 @@ process.on('SIGTERM', async () => {
53
53
 
54
54
  ```typescript
55
55
  // app.module.ts
56
- import { AppLogModule } from '@devminister/applog-client';
56
+ import { AppLogModule, AppLogInterceptor } from '@devminister/applog-client';
57
+ import { APP_INTERCEPTOR } from '@nestjs/core';
57
58
 
58
59
  @Module({
59
60
  imports: [
@@ -61,8 +62,19 @@ import { AppLogModule } from '@devminister/applog-client';
61
62
  apiUrl: process.env.MONITOR_API_URL,
62
63
  clientId: process.env.APPLOG_CLIENT_ID,
63
64
  clientSecret: process.env.APPLOG_CLIENT_SECRET,
65
+ // HTTP interceptor options
66
+ excludePaths: ['/api/health', '/api/health/system'],
67
+ logBodies: true,
68
+ logHeaders: true,
69
+ maxBodySize: 10240,
64
70
  }),
65
71
  ],
72
+ providers: [
73
+ {
74
+ provide: APP_INTERCEPTOR,
75
+ useClass: AppLogInterceptor,
76
+ },
77
+ ],
66
78
  })
67
79
  export class AppModule {}
68
80
  ```
@@ -78,6 +90,9 @@ AppLogModule.registerAsync({
78
90
  clientSecret: config.get('APPLOG_CLIENT_SECRET'),
79
91
  defaultCategory: 'my-service',
80
92
  defaultTags: ['production'],
93
+ excludePaths: ['/api/health'],
94
+ logBodies: true,
95
+ logHeaders: true,
81
96
  }),
82
97
  })
83
98
  ```
@@ -112,24 +127,39 @@ export class PaymentService {
112
127
  }
113
128
  ```
114
129
 
115
- #### Auto-log all HTTP requests (interceptor)
130
+ ## HTTP Interceptor
116
131
 
117
- ```typescript
118
- import { AppLogInterceptor } from '@devminister/applog-client';
132
+ The `AppLogInterceptor` automatically logs every HTTP request with full context:
119
133
 
120
- // app.module.ts
121
- @Module({
122
- providers: [
123
- {
124
- provide: APP_INTERCEPTOR,
125
- useClass: AppLogInterceptor,
126
- },
127
- ],
134
+ - **Method, path, status code, duration**
135
+ - **Request/response bodies** (with configurable size limit)
136
+ - **Request headers** (content-type, user-agent, origin)
137
+ - **Client IP and user-agent**
138
+ - **Query parameters**
139
+ - **Error messages and stack traces** (for failed requests)
140
+ - **User ID** extracted from `request.user.id` or `request.user.sub`
141
+ - **Controller and handler names** as context
142
+
143
+ ### Sensitive Field Redaction
144
+
145
+ Request/response bodies are automatically sanitized. The following fields are redacted:
146
+
147
+ `password`, `token`, `accessToken`, `refreshToken`, `authorization`, `secret`, `creditCard`, `cardNumber`, `cvv`, `ssn`
148
+
149
+ ### Path Exclusion
150
+
151
+ Use `excludePaths` to skip logging for health checks or other noisy endpoints:
152
+
153
+ ```typescript
154
+ AppLogModule.register({
155
+ // ...credentials
156
+ excludePaths: ['/api/health', '/metrics', '/api/docs'],
128
157
  })
129
- export class AppModule {}
130
158
  ```
131
159
 
132
- This will automatically log every request with controller name, handler, method, path, status code, duration, and userId.
160
+ ### Disabled Mode
161
+
162
+ When `disabled: true`, the interceptor still logs to the NestJS console (method, path, status, duration) but does not send logs to the monitoring API.
133
163
 
134
164
  ## API
135
165
 
@@ -145,13 +175,18 @@ appLog.fatal(category, action, options?)
145
175
 
146
176
  ### Options
147
177
 
148
- | Field | Type | Description |
149
- |------------|----------|------------------------------------------|
150
- | `message` | string | Human-readable log message |
151
- | `metadata` | object | Any structured data (JSON) |
152
- | `userId` | string | User identifier |
153
- | `duration` | number | Duration in milliseconds |
154
- | `tags` | string[] | Searchable tags |
178
+ | Field | Type | Description |
179
+ |--------------|----------|------------------------------------------|
180
+ | `message` | string | Human-readable log message |
181
+ | `metadata` | object | Any structured data (JSON) |
182
+ | `userId` | string | User identifier |
183
+ | `duration` | number | Duration in milliseconds |
184
+ | `tags` | string[] | Searchable tags |
185
+ | `method` | string | HTTP method (GET, POST, etc.) |
186
+ | `path` | string | Request path / URL |
187
+ | `statusCode` | number | HTTP status code |
188
+ | `context` | string | Log context (e.g. controller name) |
189
+ | `pid` | number | Process ID |
155
190
 
156
191
  ### Timer
157
192
 
@@ -184,13 +219,18 @@ appLog.bufferSize // Current buffer length
184
219
  | `maxRetries` | number | `3` | Retry attempts with exponential backoff |
185
220
  | `defaultCategory` | string | — | Default category when none specified |
186
221
  | `defaultTags` | string[] | `[]` | Tags added to every log |
187
- | `disabled` | boolean | `false` | Disable all logging |
222
+ | `disabled` | boolean | `false` | Disable API logging (console logging still works)|
188
223
  | `logger` | object | console | Custom logger `{ debug, warn, error }` |
224
+ | `excludePaths` | string[] | `[]` | Paths to skip in the HTTP interceptor |
225
+ | `logBodies` | boolean | `true` | Capture request/response bodies |
226
+ | `logHeaders` | boolean | `true` | Capture request headers |
227
+ | `maxBodySize` | number | `10240` | Max body size to capture (characters) |
189
228
 
190
229
  ## Category & Action Conventions
191
230
 
192
231
  | Category | Example Actions |
193
232
  |----------------|--------------------------------------------------------------|
233
+ | `http` | `Controller.handler` (auto-logged by interceptor) |
194
234
  | `auth` | `user.login`, `user.logout`, `user.register`, `token.refresh` |
195
235
  | `payment` | `payment.charge`, `payment.refund`, `subscription.create` |
196
236
  | `order` | `order.create`, `order.update`, `order.cancel`, `order.ship` |
@@ -4,21 +4,17 @@ import { AppLogNestService } from './applog-nestjs.service';
4
4
  /**
5
5
  * NestJS interceptor that automatically logs every HTTP request as an app log.
6
6
  *
7
- * Captures: controller name, handler name, method, path, status, duration, userId.
8
- * Errors are logged at level 'error', successful requests at 'info'.
7
+ * Captures: method, path, status, duration, headers, body, IP, user-agent,
8
+ * error stack, query params. Supports path exclusion, sensitive field redaction,
9
+ * and body truncation.
9
10
  *
10
11
  * @example
11
12
  * // Global interceptor
12
- * app.useGlobalInterceptors(app.get(AppLogInterceptor));
13
- *
14
- * // Or via module providers
15
13
  * { provide: APP_INTERCEPTOR, useClass: AppLogInterceptor }
16
- *
17
- * // Or per-controller
18
- * @UseInterceptors(AppLogInterceptor)
19
14
  */
20
15
  export declare class AppLogInterceptor implements NestInterceptor {
21
16
  private readonly appLog;
17
+ private readonly logger;
22
18
  constructor(appLog: AppLogNestService);
23
19
  intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
24
20
  }
@@ -14,61 +14,166 @@ const common_1 = require("@nestjs/common");
14
14
  const rxjs_1 = require("rxjs");
15
15
  const operators_1 = require("rxjs/operators");
16
16
  const applog_nestjs_service_1 = require("./applog-nestjs.service");
17
+ // ─── Sensitive Field Redaction ──────────────────────────
18
+ const SENSITIVE_FIELDS = new Set([
19
+ 'password',
20
+ 'token',
21
+ 'accesstoken',
22
+ 'refreshtoken',
23
+ 'authorization',
24
+ 'secret',
25
+ 'creditcard',
26
+ 'cardnumber',
27
+ 'cvv',
28
+ 'ssn',
29
+ ]);
30
+ function sanitize(obj, depth = 0) {
31
+ if (!obj || typeof obj !== 'object' || depth > 5)
32
+ return obj;
33
+ if (Array.isArray(obj))
34
+ return obj.map((item) => sanitize(item, depth + 1));
35
+ const cleaned = {};
36
+ for (const [key, value] of Object.entries(obj)) {
37
+ if (SENSITIVE_FIELDS.has(key.toLowerCase())) {
38
+ cleaned[key] = '[REDACTED]';
39
+ }
40
+ else if (typeof value === 'object' && value !== null) {
41
+ cleaned[key] = sanitize(value, depth + 1);
42
+ }
43
+ else {
44
+ cleaned[key] = value;
45
+ }
46
+ }
47
+ return cleaned;
48
+ }
49
+ function truncate(obj, maxSize) {
50
+ if (!obj)
51
+ return obj;
52
+ const str = typeof obj === 'string' ? obj : JSON.stringify(obj);
53
+ if (str.length <= maxSize)
54
+ return obj;
55
+ return typeof obj === 'string'
56
+ ? str.slice(0, maxSize) + '...[truncated]'
57
+ : undefined;
58
+ }
17
59
  /**
18
60
  * NestJS interceptor that automatically logs every HTTP request as an app log.
19
61
  *
20
- * Captures: controller name, handler name, method, path, status, duration, userId.
21
- * Errors are logged at level 'error', successful requests at 'info'.
62
+ * Captures: method, path, status, duration, headers, body, IP, user-agent,
63
+ * error stack, query params. Supports path exclusion, sensitive field redaction,
64
+ * and body truncation.
22
65
  *
23
66
  * @example
24
67
  * // Global interceptor
25
- * app.useGlobalInterceptors(app.get(AppLogInterceptor));
26
- *
27
- * // Or via module providers
28
68
  * { provide: APP_INTERCEPTOR, useClass: AppLogInterceptor }
29
- *
30
- * // Or per-controller
31
- * @UseInterceptors(AppLogInterceptor)
32
69
  */
33
70
  let AppLogInterceptor = class AppLogInterceptor {
34
71
  constructor(appLog) {
35
72
  this.appLog = appLog;
73
+ this.logger = new common_1.Logger('HTTP');
36
74
  }
37
75
  intercept(context, next) {
38
- if (this.appLog.getConfig().disabled)
39
- return next.handle();
76
+ const config = this.appLog.getConfig();
40
77
  const ctx = context.switchToHttp();
41
78
  const request = ctx.getRequest();
79
+ const startTime = Date.now();
80
+ // When disabled, still log to console
81
+ if (config.disabled) {
82
+ return next.handle().pipe((0, operators_1.tap)(() => {
83
+ const response = ctx.getResponse();
84
+ this.logger.log(`${request.method} ${request.path} ${response.statusCode} - ${Date.now() - startTime}ms`);
85
+ }), (0, operators_1.catchError)((error) => {
86
+ const statusCode = error instanceof common_1.HttpException
87
+ ? error.getStatus()
88
+ : common_1.HttpStatus.INTERNAL_SERVER_ERROR;
89
+ const msg = `${request.method} ${request.path} ${statusCode} - ${Date.now() - startTime}ms`;
90
+ statusCode >= 500 ? this.logger.error(msg) : this.logger.warn(msg);
91
+ return (0, rxjs_1.throwError)(() => error);
92
+ }));
93
+ }
94
+ // Path exclusion
95
+ const excludePaths = config.excludePaths ?? [];
96
+ const isExcluded = excludePaths.some((p) => request.path?.startsWith(p));
97
+ // Config defaults
98
+ const maxBodySize = config.maxBodySize ?? 10240;
99
+ const logBodies = config.logBodies !== false;
100
+ const logHeaders = config.logHeaders !== false;
101
+ // Extract context
42
102
  const controller = context.getClass().name;
43
103
  const handler = context.getHandler().name;
44
- const start = Date.now();
45
104
  const userId = request.user?.id || request.user?.sub || undefined;
46
- return next.handle().pipe((0, operators_1.tap)(() => {
105
+ // Extract HTTP details
106
+ const ip = (request.headers?.['x-forwarded-for']?.toString().split(',')[0] ||
107
+ request.ip ||
108
+ request.socket?.remoteAddress ||
109
+ '').substring(0, 45);
110
+ const userAgent = (request.headers?.['user-agent'] || '').substring(0, 500);
111
+ const query = request.query && Object.keys(request.query).length > 0 ? request.query : undefined;
112
+ const requestHeaders = logHeaders
113
+ ? sanitize({
114
+ 'content-type': request.headers?.['content-type'],
115
+ 'user-agent': request.headers?.['user-agent'],
116
+ origin: request.headers?.['origin'],
117
+ })
118
+ : undefined;
119
+ const requestBody = logBodies
120
+ ? truncate(sanitize(request.body), maxBodySize)
121
+ : undefined;
122
+ return next.handle().pipe((0, operators_1.tap)((responseBody) => {
47
123
  const response = ctx.getResponse();
48
- this.appLog.info('api', `${controller}.${handler}`, {
49
- userId,
50
- duration: Date.now() - start,
51
- metadata: {
124
+ const duration = Date.now() - startTime;
125
+ const statusCode = response.statusCode;
126
+ // Console log
127
+ this.logger.log(`${request.method} ${request.path} ${statusCode} - ${duration}ms`);
128
+ // Push to app log (skip excluded paths)
129
+ if (!isExcluded) {
130
+ this.appLog.info('http', `${controller}.${handler}`, {
131
+ userId,
132
+ duration,
52
133
  method: request.method,
53
134
  path: request.url,
54
- statusCode: response.statusCode,
55
- },
56
- });
135
+ statusCode,
136
+ context: controller,
137
+ metadata: {
138
+ ip,
139
+ userAgent,
140
+ ...(query ? { query } : {}),
141
+ ...(requestHeaders ? { requestHeaders } : {}),
142
+ ...(requestBody ? { requestBody } : {}),
143
+ ...(logBodies && responseBody
144
+ ? { responseBody: truncate(sanitize(responseBody), maxBodySize) }
145
+ : {}),
146
+ },
147
+ });
148
+ }
57
149
  }), (0, operators_1.catchError)((error) => {
150
+ const duration = Date.now() - startTime;
58
151
  const statusCode = error instanceof common_1.HttpException
59
152
  ? error.getStatus()
60
153
  : common_1.HttpStatus.INTERNAL_SERVER_ERROR;
61
- this.appLog.error('api', `${controller}.${handler}`, {
62
- message: error.message,
63
- userId,
64
- duration: Date.now() - start,
65
- metadata: {
154
+ const msg = `${request.method} ${request.path} ${statusCode} - ${duration}ms`;
155
+ statusCode >= 500 ? this.logger.error(msg) : this.logger.warn(msg);
156
+ // Push to app log (skip excluded paths)
157
+ if (!isExcluded) {
158
+ this.appLog.error('http', `${controller}.${handler}`, {
159
+ message: error.message,
160
+ userId,
161
+ duration,
66
162
  method: request.method,
67
163
  path: request.url,
68
164
  statusCode,
69
- error: error.message,
70
- },
71
- });
165
+ context: controller,
166
+ metadata: {
167
+ ip,
168
+ userAgent,
169
+ ...(query ? { query } : {}),
170
+ ...(requestHeaders ? { requestHeaders } : {}),
171
+ ...(requestBody ? { requestBody } : {}),
172
+ errorMessage: error.message,
173
+ errorStack: error.stack,
174
+ },
175
+ });
176
+ }
72
177
  return (0, rxjs_1.throwError)(() => error);
73
178
  }));
74
179
  }
@@ -10,6 +10,11 @@ export interface AppLogPayload {
10
10
  duration?: number;
11
11
  tags?: string[];
12
12
  createdAt?: string;
13
+ method?: string;
14
+ path?: string;
15
+ statusCode?: number;
16
+ context?: string;
17
+ pid?: number;
13
18
  }
14
19
  export interface AppLogOptions {
15
20
  message?: string;
@@ -17,6 +22,10 @@ export interface AppLogOptions {
17
22
  userId?: string;
18
23
  duration?: number;
19
24
  tags?: string[];
25
+ method?: string;
26
+ path?: string;
27
+ statusCode?: number;
28
+ context?: string;
20
29
  }
21
30
  export declare class AppLogClientService {
22
31
  private readonly config;
@@ -27,5 +27,13 @@ export interface AppLogClientConfig {
27
27
  warn?: (msg: string) => void;
28
28
  error?: (msg: string) => void;
29
29
  };
30
+ /** Paths to exclude from HTTP request logging (e.g. ['/health', '/api/health']) */
31
+ excludePaths?: string[];
32
+ /** Whether to log request/response bodies (default: true) */
33
+ logBodies?: boolean;
34
+ /** Whether to log request headers (default: true) */
35
+ logHeaders?: boolean;
36
+ /** Max body size to capture in characters (default: 10240) */
37
+ maxBodySize?: number;
30
38
  }
31
39
  export declare const APPLOG_CLIENT_CONFIG = "APPLOG_CLIENT_CONFIG";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devminister/applog-client",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Application log client for the Monitoring platform. Buffered, batched delivery with retry. Works standalone or as a NestJS module.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,9 +31,15 @@
31
31
  "rxjs": "^7.0.0"
32
32
  },
33
33
  "peerDependenciesMeta": {
34
- "@nestjs/common": { "optional": true },
35
- "@nestjs/core": { "optional": true },
36
- "rxjs": { "optional": true }
34
+ "@nestjs/common": {
35
+ "optional": true
36
+ },
37
+ "@nestjs/core": {
38
+ "optional": true
39
+ },
40
+ "rxjs": {
41
+ "optional": true
42
+ }
37
43
  },
38
44
  "devDependencies": {
39
45
  "@nestjs/common": "^11.1.3",