@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
|
-
|
|
130
|
+
## HTTP Interceptor
|
|
116
131
|
|
|
117
|
-
|
|
118
|
-
import { AppLogInterceptor } from '@devminister/applog-client';
|
|
132
|
+
The `AppLogInterceptor` automatically logs every HTTP request with full context:
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
| `message`
|
|
151
|
-
| `metadata`
|
|
152
|
-
| `userId`
|
|
153
|
-
| `duration`
|
|
154
|
-
| `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
|
|
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:
|
|
8
|
-
*
|
|
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:
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/applog.service.d.ts
CHANGED
|
@@ -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.
|
|
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": {
|
|
35
|
-
|
|
36
|
-
|
|
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",
|