@nest-omni/core 4.1.3-26 → 4.1.3-27
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/http-client/config/http-client.config.js +1 -1
- package/http-client/decorators/http-client.decorators.d.ts +1 -1
- package/http-client/decorators/http-client.decorators.js +1 -1
- package/http-client/examples/advanced-usage.example.js +3 -3
- package/http-client/http-client.module.js +2 -2
- package/http-client/interfaces/http-client-config.interface.d.ts +6 -1
- package/http-client/services/api-client-registry.service.js +1 -1
- package/http-client/services/http-client.service.js +1 -1
- package/http-client/services/logging.service.d.ts +1 -17
- package/http-client/services/logging.service.js +35 -124
- package/http-client/utils/curl-generator.util.js +2 -5
- package/http-client/utils/index.d.ts +1 -0
- package/http-client/utils/index.js +1 -0
- package/http-client/utils/retry-recorder.util.d.ts +0 -4
- package/http-client/utils/retry-recorder.util.js +2 -27
- package/http-client/utils/sanitize.util.d.ts +58 -0
- package/http-client/utils/sanitize.util.js +188 -0
- package/interceptors/http-logging-interceptor.service.d.ts +38 -0
- package/interceptors/http-logging-interceptor.service.js +167 -0
- package/interceptors/index.d.ts +1 -0
- package/interceptors/index.js +1 -0
- package/package.json +1 -1
- package/setup/bootstrap.setup.js +1 -1
- package/shared/services/api-config.service.js +3 -18
|
@@ -62,7 +62,7 @@ exports.DEFAULT_HTTP_CLIENT_CONFIG = {
|
|
|
62
62
|
logHeaders: false, // 默认不记录头信息(安全考虑)
|
|
63
63
|
logBody: true,
|
|
64
64
|
maxBodyLength: 1000,
|
|
65
|
-
|
|
65
|
+
sanitize: ['password', 'secret', 'token', 'apiKey', 'authorization', 'cookie'],
|
|
66
66
|
logLevel: 'info',
|
|
67
67
|
databaseLogging: {
|
|
68
68
|
enabled: false, // 默认不启用数据库日志
|
|
@@ -40,7 +40,7 @@ export declare const HttpCircuitBreaker: (options?: {
|
|
|
40
40
|
export declare const HttpLogRequest: (options?: {
|
|
41
41
|
logHeaders?: boolean;
|
|
42
42
|
logBody?: boolean;
|
|
43
|
-
|
|
43
|
+
sanitize?: string[];
|
|
44
44
|
databaseLog?: boolean;
|
|
45
45
|
logLevel?: "debug" | "info" | "warn" | "error";
|
|
46
46
|
serviceClass?: string;
|
|
@@ -125,7 +125,7 @@ const HttpLogRequest = (options = {}) => {
|
|
|
125
125
|
const loggingOptions = {
|
|
126
126
|
logHeaders: (_a = options.logHeaders) !== null && _a !== void 0 ? _a : true,
|
|
127
127
|
logBody: (_b = options.logBody) !== null && _b !== void 0 ? _b : true,
|
|
128
|
-
|
|
128
|
+
sanitize: options.sanitize || [
|
|
129
129
|
'authorization',
|
|
130
130
|
'apikey',
|
|
131
131
|
'password',
|
|
@@ -158,7 +158,7 @@ __decorate([
|
|
|
158
158
|
logHeaders: true,
|
|
159
159
|
logBody: true,
|
|
160
160
|
databaseLog: true,
|
|
161
|
-
|
|
161
|
+
sanitize: ['authorization', 'x-api-key'],
|
|
162
162
|
}),
|
|
163
163
|
__metadata("design:type", Function),
|
|
164
164
|
__metadata("design:paramtypes", [Object, String]),
|
|
@@ -185,7 +185,7 @@ exports.GitHubHttpService = GitHubHttpService = __decorate([
|
|
|
185
185
|
logHeaders: true,
|
|
186
186
|
logBody: true,
|
|
187
187
|
maxBodyLength: 1000,
|
|
188
|
-
|
|
188
|
+
sanitize: ['authorization'],
|
|
189
189
|
logLevel: 'info',
|
|
190
190
|
},
|
|
191
191
|
}),
|
|
@@ -315,7 +315,7 @@ exports.PaymentHttpService = PaymentHttpService = __decorate([
|
|
|
315
315
|
logHeaders: false, // 不记录敏感头信息
|
|
316
316
|
logBody: false, // 不记录支付数据
|
|
317
317
|
maxBodyLength: 1000,
|
|
318
|
-
|
|
318
|
+
sanitize: ['authorization', 'x-api-key', 'x-client-secret'],
|
|
319
319
|
logLevel: 'info',
|
|
320
320
|
},
|
|
321
321
|
}),
|
|
@@ -295,8 +295,8 @@ let HttpClientModule = HttpClientModule_1 = class HttpClientModule {
|
|
|
295
295
|
maxBodyLength: configService.get('HTTP_CLIENT_LOG_MAX_BODY_LENGTH')
|
|
296
296
|
? parseInt(configService.get('HTTP_CLIENT_LOG_MAX_BODY_LENGTH'))
|
|
297
297
|
: undefined,
|
|
298
|
-
|
|
299
|
-
.get('
|
|
298
|
+
sanitize: (_a = configService
|
|
299
|
+
.get('HTTP_CLIENT_LOG_SANITIZE')) === null || _a === void 0 ? void 0 : _a.split(','),
|
|
300
300
|
logLevel: configService.get('HTTP_CLIENT_LOG_LEVEL'),
|
|
301
301
|
databaseLogging: {
|
|
302
302
|
enabled: configService.get('HTTP_CLIENT_DB_LOGGING_ENABLED') ===
|
|
@@ -166,7 +166,12 @@ export interface LoggingConfig {
|
|
|
166
166
|
logHeaders?: boolean;
|
|
167
167
|
logBody?: boolean;
|
|
168
168
|
maxBodyLength?: number;
|
|
169
|
-
|
|
169
|
+
/**
|
|
170
|
+
* 脱敏配置
|
|
171
|
+
* 敏感字段列表,统一适用于 headers、body、query string
|
|
172
|
+
* 例如:['password', 'token', 'secret', 'apiKey']
|
|
173
|
+
*/
|
|
174
|
+
sanitize?: string[];
|
|
170
175
|
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
171
176
|
databaseLogging?: {
|
|
172
177
|
enabled?: boolean;
|
|
@@ -47,7 +47,7 @@ let ApiClientRegistryService = ApiClientRegistryService_1 = class ApiClientRegis
|
|
|
47
47
|
logHeaders: true,
|
|
48
48
|
logBody: true,
|
|
49
49
|
maxBodyLength: 10000,
|
|
50
|
-
|
|
50
|
+
sanitize: ['authorization', 'api-key'],
|
|
51
51
|
logLevel: 'info',
|
|
52
52
|
},
|
|
53
53
|
timeout: 30000,
|
|
@@ -716,7 +716,7 @@ let HttpClientService = HttpClientService_1 = class HttpClientService {
|
|
|
716
716
|
logHeaders: true,
|
|
717
717
|
logBody: true,
|
|
718
718
|
maxBodyLength: 1000,
|
|
719
|
-
|
|
719
|
+
sanitize: ['password', 'secret', 'token', 'apiKey', 'authorization', 'cookie'],
|
|
720
720
|
logLevel: 'info',
|
|
721
721
|
databaseLogging: {
|
|
722
722
|
enabled: true,
|
|
@@ -33,7 +33,7 @@ export declare class HttpLoggingService implements OnModuleDestroy {
|
|
|
33
33
|
private isProcessing;
|
|
34
34
|
constructor();
|
|
35
35
|
onModuleDestroy(): void;
|
|
36
|
-
initRepository(dataSource: string, tableName: string): Repository<HttpLogEntity
|
|
36
|
+
initRepository(dataSource: string, tableName: string): Repository<HttpLogEntity> | null;
|
|
37
37
|
/**
|
|
38
38
|
* 记录请求开始
|
|
39
39
|
*/
|
|
@@ -119,22 +119,6 @@ export declare class HttpLoggingService implements OnModuleDestroy {
|
|
|
119
119
|
* 同步保存日志到数据库(用于异步模式内部)
|
|
120
120
|
*/
|
|
121
121
|
private saveLogToDatabase;
|
|
122
|
-
/**
|
|
123
|
-
* 转换请求体为字符串
|
|
124
|
-
*/
|
|
125
|
-
private sanitizeBodyAsString;
|
|
126
|
-
/**
|
|
127
|
-
* 清理敏感头信息
|
|
128
|
-
*/
|
|
129
|
-
private sanitizeHeaders;
|
|
130
|
-
/**
|
|
131
|
-
* 清理敏感请求体信息
|
|
132
|
-
*/
|
|
133
|
-
private sanitizeBody;
|
|
134
|
-
/**
|
|
135
|
-
* 递归清理对象中的敏感字段
|
|
136
|
-
*/
|
|
137
|
-
private sanitizeObject;
|
|
138
122
|
/**
|
|
139
123
|
* 获取完整URL
|
|
140
124
|
* @param config Axios请求配置
|
|
@@ -25,6 +25,7 @@ const http_log_entity_1 = require("../entities/http-log.entity");
|
|
|
25
25
|
const request_id_util_1 = require("../utils/request-id.util");
|
|
26
26
|
const context_extractor_util_1 = require("../utils/context-extractor.util");
|
|
27
27
|
const call_stack_extractor_util_1 = require("../utils/call-stack-extractor.util");
|
|
28
|
+
const sanitize_util_1 = require("../utils/sanitize.util");
|
|
28
29
|
const transaction_1 = require("@nest-omni/transaction");
|
|
29
30
|
/**
|
|
30
31
|
* HTTP日志服务
|
|
@@ -67,8 +68,9 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
67
68
|
return this.logRepository;
|
|
68
69
|
}
|
|
69
70
|
catch (error) {
|
|
70
|
-
this.logger.
|
|
71
|
-
|
|
71
|
+
this.logger.warn('Database logging not available, continuing without it');
|
|
72
|
+
this.logRepository = null;
|
|
73
|
+
return null;
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
@@ -78,16 +80,16 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
78
80
|
// 从ContextProvider获取上下文信息
|
|
79
81
|
const context = context_extractor_util_1.ContextExtractor.getHttpContext();
|
|
80
82
|
const actualRequestId = requestId || context.requestId || (0, request_id_util_1.generateRequestId)();
|
|
81
|
-
const { logHeaders = true, logBody = true,
|
|
83
|
+
const { logHeaders = true, logBody = true, sanitize = [], } = loggingOptions || {};
|
|
82
84
|
const logData = {
|
|
83
85
|
requestId: actualRequestId,
|
|
84
86
|
userId: context.userId,
|
|
85
|
-
method: config.method ? config.method.toUpperCase() : 'GET',
|
|
86
|
-
url: config.url,
|
|
87
|
-
headers: logHeaders
|
|
88
|
-
?
|
|
87
|
+
method: (config === null || config === void 0 ? void 0 : config.method) ? config.method.toUpperCase() : 'GET',
|
|
88
|
+
url: config ? sanitize_util_1.SanitizeUtil.sanitizeQueryString(config.url, sanitize) : '',
|
|
89
|
+
headers: logHeaders && config
|
|
90
|
+
? sanitize_util_1.SanitizeUtil.sanitizeHeaders(config.headers, sanitize)
|
|
89
91
|
: undefined,
|
|
90
|
-
body: logBody ?
|
|
92
|
+
body: logBody && config ? sanitize_util_1.SanitizeUtil.sanitizeBody(config.data, sanitize) : undefined,
|
|
91
93
|
clientIp: context.clientIp,
|
|
92
94
|
serviceName: context.appId,
|
|
93
95
|
};
|
|
@@ -108,8 +110,8 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
108
110
|
*/
|
|
109
111
|
logRequestSuccess(response_1, startTime_1, requestId_1, loggingOptions_1) {
|
|
110
112
|
return __awaiter(this, arguments, void 0, function* (response, startTime, requestId, loggingOptions, databaseLogging = false, retryRecords, circuitBreakerState, decoratorContext, clientName, callingContext) {
|
|
111
|
-
var _a, _b, _c, _d;
|
|
112
|
-
const { logHeaders = true, logBody = true,
|
|
113
|
+
var _a, _b, _c, _d, _e, _f;
|
|
114
|
+
const { logHeaders = true, logBody = true, sanitize = [], } = loggingOptions || {};
|
|
113
115
|
const responseTime = Date.now() - startTime;
|
|
114
116
|
// 获取当前上下文信息
|
|
115
117
|
const context = context_extractor_util_1.ContextExtractor.getHttpContext();
|
|
@@ -119,9 +121,9 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
119
121
|
statusCode: response.status,
|
|
120
122
|
responseTime,
|
|
121
123
|
headers: logHeaders
|
|
122
|
-
?
|
|
124
|
+
? sanitize_util_1.SanitizeUtil.sanitizeHeaders(response.headers, sanitize)
|
|
123
125
|
: undefined,
|
|
124
|
-
body: logBody ?
|
|
126
|
+
body: logBody ? sanitize_util_1.SanitizeUtil.sanitizeBody(response.data, sanitize) : undefined,
|
|
125
127
|
responseSize: JSON.stringify(response.data).length,
|
|
126
128
|
circuitBreakerState,
|
|
127
129
|
};
|
|
@@ -140,7 +142,7 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
140
142
|
dataSource: (_a = loggingOptions.databaseLogging) === null || _a === void 0 ? void 0 : _a.dataSource,
|
|
141
143
|
tableName: (_b = loggingOptions.databaseLogging) === null || _b === void 0 ? void 0 : _b.tableName,
|
|
142
144
|
});
|
|
143
|
-
this.initRepository(loggingOptions.databaseLogging.dataSource, loggingOptions.databaseLogging.tableName);
|
|
145
|
+
this.initRepository(((_c = loggingOptions.databaseLogging) === null || _c === void 0 ? void 0 : _c.dataSource) || 'default', ((_d = loggingOptions.databaseLogging) === null || _d === void 0 ? void 0 : _d.tableName) || 'http_log');
|
|
144
146
|
}
|
|
145
147
|
// 数据库日志记录
|
|
146
148
|
if (databaseLogging && this.logRepository) {
|
|
@@ -157,25 +159,25 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
157
159
|
this.saveToDatabase({
|
|
158
160
|
requestId,
|
|
159
161
|
userId: context.userId,
|
|
160
|
-
method: ((
|
|
161
|
-
url: this.getFullUrl(response.config)
|
|
162
|
-
headers:
|
|
163
|
-
body:
|
|
164
|
-
params: response.config.params,
|
|
162
|
+
method: ((_e = response.config.method) === null || _e === void 0 ? void 0 : _e.toUpperCase()) || 'UNKNOWN',
|
|
163
|
+
url: sanitize_util_1.SanitizeUtil.sanitizeQueryString(this.getFullUrl(response.config), sanitize),
|
|
164
|
+
headers: sanitize_util_1.SanitizeUtil.sanitizeHeaders(response.config.headers, sanitize),
|
|
165
|
+
body: sanitize_util_1.SanitizeUtil.sanitizeBodyAsString(response.config.data, sanitize),
|
|
166
|
+
params: sanitize_util_1.SanitizeUtil.sanitizeParams(response.config.params, sanitize),
|
|
165
167
|
statusCode: response.status,
|
|
166
168
|
responseTime,
|
|
167
169
|
attemptCount: (retryRecords === null || retryRecords === void 0 ? void 0 : retryRecords.length)
|
|
168
170
|
? Math.max(...retryRecords.map((r) => r.attempt)) + 1
|
|
169
171
|
: 1,
|
|
170
172
|
success: true,
|
|
171
|
-
responseHeaders:
|
|
173
|
+
responseHeaders: sanitize_util_1.SanitizeUtil.sanitizeHeaders(response.headers, sanitize),
|
|
172
174
|
responseBody: logBody
|
|
173
|
-
?
|
|
175
|
+
? sanitize_util_1.SanitizeUtil.sanitizeBodyAsString(response.data, sanitize)
|
|
174
176
|
: undefined,
|
|
175
177
|
serviceName: clientName || context.appId,
|
|
176
178
|
operationName: callInfo.operationName,
|
|
177
179
|
clientIp: context.clientIp,
|
|
178
|
-
source: (
|
|
180
|
+
source: (_f = context.metadata) === null || _f === void 0 ? void 0 : _f.source,
|
|
179
181
|
tags: context.tags,
|
|
180
182
|
metadata: logData,
|
|
181
183
|
retryRecords,
|
|
@@ -188,8 +190,8 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
188
190
|
* 记录请求错误
|
|
189
191
|
*/
|
|
190
192
|
logRequestError(error, startTime, requestId, attemptCount, loggingOptions, databaseLogging = false, retryRecords, circuitBreakerState, decoratorContext, clientName, callingContext) {
|
|
191
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
192
|
-
const { logHeaders = true,
|
|
193
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
194
|
+
const { logHeaders = true, sanitize = [] } = loggingOptions;
|
|
193
195
|
const responseTime = Date.now() - startTime;
|
|
194
196
|
// 获取当前上下文信息
|
|
195
197
|
const context = context_extractor_util_1.ContextExtractor.getHttpContext();
|
|
@@ -203,7 +205,7 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
203
205
|
errorCode: error.code,
|
|
204
206
|
statusCode: (_a = error.response) === null || _a === void 0 ? void 0 : _a.status,
|
|
205
207
|
headers: ((_b = error.config) === null || _b === void 0 ? void 0 : _b.headers) && logHeaders
|
|
206
|
-
?
|
|
208
|
+
? sanitize_util_1.SanitizeUtil.sanitizeHeaders(error.config.headers, sanitize)
|
|
207
209
|
: undefined,
|
|
208
210
|
circuitBreakerState,
|
|
209
211
|
};
|
|
@@ -223,7 +225,7 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
223
225
|
break;
|
|
224
226
|
}
|
|
225
227
|
if (!this.logRepository) {
|
|
226
|
-
this.initRepository(loggingOptions.databaseLogging.dataSource, loggingOptions.databaseLogging.tableName);
|
|
228
|
+
this.initRepository(((_c = loggingOptions.databaseLogging) === null || _c === void 0 ? void 0 : _c.dataSource) || 'default', ((_d = loggingOptions.databaseLogging) === null || _d === void 0 ? void 0 : _d.tableName) || 'http_log');
|
|
227
229
|
}
|
|
228
230
|
// 数据库日志记录
|
|
229
231
|
if (databaseLogging && this.logRepository) {
|
|
@@ -234,14 +236,14 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
234
236
|
this.saveToDatabase({
|
|
235
237
|
requestId,
|
|
236
238
|
userId: context.userId,
|
|
237
|
-
method: ((
|
|
238
|
-
url: this.getFullUrl(error.config)
|
|
239
|
-
headers: ((
|
|
240
|
-
?
|
|
239
|
+
method: ((_f = (_e = error.config) === null || _e === void 0 ? void 0 : _e.method) === null || _f === void 0 ? void 0 : _f.toUpperCase()) || 'UNKNOWN',
|
|
240
|
+
url: sanitize_util_1.SanitizeUtil.sanitizeQueryString(this.getFullUrl(error.config), sanitize),
|
|
241
|
+
headers: ((_g = error.config) === null || _g === void 0 ? void 0 : _g.headers)
|
|
242
|
+
? sanitize_util_1.SanitizeUtil.sanitizeHeaders(error.config.headers, sanitize)
|
|
241
243
|
: {},
|
|
242
|
-
body:
|
|
243
|
-
params: (
|
|
244
|
-
statusCode: (
|
|
244
|
+
body: sanitize_util_1.SanitizeUtil.sanitizeBodyAsString((_h = error.config) === null || _h === void 0 ? void 0 : _h.data, sanitize),
|
|
245
|
+
params: sanitize_util_1.SanitizeUtil.sanitizeParams((_j = error.config) === null || _j === void 0 ? void 0 : _j.params, sanitize),
|
|
246
|
+
statusCode: (_k = error.response) === null || _k === void 0 ? void 0 : _k.status,
|
|
245
247
|
responseTime,
|
|
246
248
|
attemptCount,
|
|
247
249
|
success: false,
|
|
@@ -251,7 +253,7 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
251
253
|
serviceName: clientName || context.appId,
|
|
252
254
|
operationName: callInfo.operationName,
|
|
253
255
|
clientIp: context.clientIp,
|
|
254
|
-
source: (
|
|
256
|
+
source: (_l = context.metadata) === null || _l === void 0 ? void 0 : _l.source,
|
|
255
257
|
tags: context.tags,
|
|
256
258
|
metadata: logData,
|
|
257
259
|
retryRecords,
|
|
@@ -581,97 +583,6 @@ let HttpLoggingService = HttpLoggingService_1 = class HttpLoggingService {
|
|
|
581
583
|
}
|
|
582
584
|
});
|
|
583
585
|
}
|
|
584
|
-
/**
|
|
585
|
-
* 转换请求体为字符串
|
|
586
|
-
*/
|
|
587
|
-
sanitizeBodyAsString(data) {
|
|
588
|
-
if (!data)
|
|
589
|
-
return undefined;
|
|
590
|
-
if (typeof data === 'string') {
|
|
591
|
-
return data.length > 5000 ? data.substring(0, 5000) + '...' : data;
|
|
592
|
-
}
|
|
593
|
-
const jsonString = JSON.stringify(data);
|
|
594
|
-
return jsonString.length > 5000
|
|
595
|
-
? jsonString.substring(0, 5000) + '...'
|
|
596
|
-
: jsonString;
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* 清理敏感头信息
|
|
600
|
-
*/
|
|
601
|
-
sanitizeHeaders(headers, sanitizeHeaders) {
|
|
602
|
-
if (!headers)
|
|
603
|
-
return {};
|
|
604
|
-
const sanitized = {};
|
|
605
|
-
const defaultSensitiveHeaders = [
|
|
606
|
-
'authorization',
|
|
607
|
-
'apikey',
|
|
608
|
-
'password',
|
|
609
|
-
'token',
|
|
610
|
-
'secret',
|
|
611
|
-
'cookie',
|
|
612
|
-
'set-cookie',
|
|
613
|
-
'x-api-key',
|
|
614
|
-
'x-auth-token',
|
|
615
|
-
];
|
|
616
|
-
const headersToSanitize = [...defaultSensitiveHeaders, ...sanitizeHeaders];
|
|
617
|
-
Object.keys(headers).forEach((key) => {
|
|
618
|
-
if (headersToSanitize.some((sensitive) => key.toLowerCase().includes(sensitive.toLowerCase()))) {
|
|
619
|
-
sanitized[key] = '[FILTERED]';
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
sanitized[key] = String(headers[key]);
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
return sanitized;
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* 清理敏感请求体信息
|
|
629
|
-
*/
|
|
630
|
-
sanitizeBody(body) {
|
|
631
|
-
if (!body)
|
|
632
|
-
return undefined;
|
|
633
|
-
if (typeof body === 'string') {
|
|
634
|
-
try {
|
|
635
|
-
body = JSON.parse(body);
|
|
636
|
-
}
|
|
637
|
-
catch (_a) {
|
|
638
|
-
return body.length > 1000 ? body.substring(0, 1000) + '...' : body;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
if (typeof body === 'object') {
|
|
642
|
-
const sanitized = Object.assign({}, body);
|
|
643
|
-
const sensitiveFields = [
|
|
644
|
-
'password',
|
|
645
|
-
'secret',
|
|
646
|
-
'token',
|
|
647
|
-
'key',
|
|
648
|
-
'authorization',
|
|
649
|
-
'credential',
|
|
650
|
-
'private',
|
|
651
|
-
'confidential',
|
|
652
|
-
'ssn',
|
|
653
|
-
'creditCard',
|
|
654
|
-
];
|
|
655
|
-
this.sanitizeObject(sanitized, sensitiveFields);
|
|
656
|
-
return sanitized;
|
|
657
|
-
}
|
|
658
|
-
return body;
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* 递归清理对象中的敏感字段
|
|
662
|
-
*/
|
|
663
|
-
sanitizeObject(obj, sensitiveFields) {
|
|
664
|
-
if (typeof obj !== 'object' || obj === null)
|
|
665
|
-
return;
|
|
666
|
-
for (const key in obj) {
|
|
667
|
-
if (sensitiveFields.some((field) => key.toLowerCase().includes(field.toLowerCase()))) {
|
|
668
|
-
obj[key] = '[FILTERED]';
|
|
669
|
-
}
|
|
670
|
-
else if (typeof obj[key] === 'object') {
|
|
671
|
-
this.sanitizeObject(obj[key], sensitiveFields);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
586
|
/**
|
|
676
587
|
* 获取完整URL
|
|
677
588
|
* @param config Axios请求配置
|
|
@@ -137,13 +137,10 @@ class CurlGenerator {
|
|
|
137
137
|
config.url = urlMatch[1];
|
|
138
138
|
}
|
|
139
139
|
// 提取头信息
|
|
140
|
-
const headerMatches = curlCommand.
|
|
140
|
+
const headerMatches = Array.from(curlCommand.matchAll(/-H\s+'([^:]+):\s*([^']+)'/g));
|
|
141
141
|
config.headers = {};
|
|
142
142
|
headerMatches.forEach((header) => {
|
|
143
|
-
|
|
144
|
-
if (headerMatch) {
|
|
145
|
-
config.headers[headerMatch[1]] = headerMatch[2];
|
|
146
|
-
}
|
|
143
|
+
config.headers[header[1]] = header[2];
|
|
147
144
|
});
|
|
148
145
|
// 提取数据
|
|
149
146
|
const dataMatch = curlCommand.match(/-d\s+'([^']+)'/);
|
|
@@ -19,3 +19,4 @@ __exportStar(require("./retry-recorder.util"), exports);
|
|
|
19
19
|
__exportStar(require("./context-extractor.util"), exports);
|
|
20
20
|
__exportStar(require("./call-stack-extractor.util"), exports);
|
|
21
21
|
__exportStar(require("./proxy-environment.util"), exports);
|
|
22
|
+
__exportStar(require("./sanitize.util"), exports);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RetryRecorder = void 0;
|
|
4
|
+
const sanitize_util_1 = require("./sanitize.util");
|
|
4
5
|
/**
|
|
5
6
|
* 请求重试记录器
|
|
6
7
|
*/
|
|
@@ -22,7 +23,7 @@ class RetryRecorder {
|
|
|
22
23
|
requestConfig: {
|
|
23
24
|
method: ((_a = config.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'UNKNOWN',
|
|
24
25
|
url: config.url || '',
|
|
25
|
-
headers:
|
|
26
|
+
headers: sanitize_util_1.SanitizeUtil.sanitizeHeaders(config.headers || {}),
|
|
26
27
|
},
|
|
27
28
|
};
|
|
28
29
|
}
|
|
@@ -83,32 +84,6 @@ class RetryRecorder {
|
|
|
83
84
|
}
|
|
84
85
|
return `HTTP error: ${status}`;
|
|
85
86
|
}
|
|
86
|
-
/**
|
|
87
|
-
* 过滤敏感头信息
|
|
88
|
-
*/
|
|
89
|
-
static sanitizeHeaders(headers) {
|
|
90
|
-
const sanitized = {};
|
|
91
|
-
const sensitiveKeys = [
|
|
92
|
-
'authorization',
|
|
93
|
-
'apikey',
|
|
94
|
-
'password',
|
|
95
|
-
'secret',
|
|
96
|
-
'token',
|
|
97
|
-
'x-api-key',
|
|
98
|
-
'x-auth-token',
|
|
99
|
-
'cookie',
|
|
100
|
-
'set-cookie',
|
|
101
|
-
];
|
|
102
|
-
Object.entries(headers).forEach(([key, value]) => {
|
|
103
|
-
if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) {
|
|
104
|
-
sanitized[key] = '[FILTERED]';
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
sanitized[key] = value;
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
return sanitized;
|
|
111
|
-
}
|
|
112
87
|
/**
|
|
113
88
|
* 添加重试记录
|
|
114
89
|
*/
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP 请求数据脱敏工具
|
|
3
|
+
* 统一处理 headers、body、query string 的敏感信息过滤
|
|
4
|
+
*/
|
|
5
|
+
export declare class SanitizeUtil {
|
|
6
|
+
/**
|
|
7
|
+
* 默认敏感字段列表
|
|
8
|
+
*/
|
|
9
|
+
private static readonly DEFAULT_SENSITIVE_FIELDS;
|
|
10
|
+
/**
|
|
11
|
+
* 脱敏 headers
|
|
12
|
+
* @param headers - 原始 headers 对象
|
|
13
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
14
|
+
* @returns 脱敏后的 headers
|
|
15
|
+
*/
|
|
16
|
+
static sanitizeHeaders(headers: any, sensitiveFields?: string[]): Record<string, string>;
|
|
17
|
+
/**
|
|
18
|
+
* 脱敏 body
|
|
19
|
+
* @param body - 原始 body 数据
|
|
20
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
21
|
+
* @returns 脱敏后的 body
|
|
22
|
+
*/
|
|
23
|
+
static sanitizeBody(body: any, sensitiveFields?: string[]): any;
|
|
24
|
+
/**
|
|
25
|
+
* 脱敏 URL 中的 query string
|
|
26
|
+
* @param url - 原始 URL
|
|
27
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
28
|
+
* @returns 脱敏后的 URL
|
|
29
|
+
*/
|
|
30
|
+
static sanitizeQueryString(url: string, sensitiveFields?: string[]): string;
|
|
31
|
+
/**
|
|
32
|
+
* 脱敏 params 对象
|
|
33
|
+
* @param params - 原始 params 对象
|
|
34
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
35
|
+
* @returns 脱敏后的 params
|
|
36
|
+
*/
|
|
37
|
+
static sanitizeParams(params: Record<string, any>, sensitiveFields?: string[]): Record<string, any>;
|
|
38
|
+
/**
|
|
39
|
+
* 将 body 转换为字符串并脱敏
|
|
40
|
+
* @param data - 原始数据
|
|
41
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
42
|
+
* @returns 脱敏后的字符串
|
|
43
|
+
*/
|
|
44
|
+
static sanitizeBodyAsString(data: any, sensitiveFields?: string[]): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* 判断字段是否为敏感字段
|
|
47
|
+
* @param key - 字段名
|
|
48
|
+
* @param sensitiveFields - 敏感字段列表
|
|
49
|
+
* @returns 是否为敏感字段
|
|
50
|
+
*/
|
|
51
|
+
private static isSensitiveField;
|
|
52
|
+
/**
|
|
53
|
+
* 递归脱敏对象中的敏感字段
|
|
54
|
+
* @param obj - 目标对象
|
|
55
|
+
* @param sensitiveFields - 敏感字段列表
|
|
56
|
+
*/
|
|
57
|
+
private static sanitizeObject;
|
|
58
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SanitizeUtil = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* HTTP 请求数据脱敏工具
|
|
6
|
+
* 统一处理 headers、body、query string 的敏感信息过滤
|
|
7
|
+
*/
|
|
8
|
+
class SanitizeUtil {
|
|
9
|
+
/**
|
|
10
|
+
* 脱敏 headers
|
|
11
|
+
* @param headers - 原始 headers 对象
|
|
12
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
13
|
+
* @returns 脱敏后的 headers
|
|
14
|
+
*/
|
|
15
|
+
static sanitizeHeaders(headers, sensitiveFields = []) {
|
|
16
|
+
if (!headers)
|
|
17
|
+
return {};
|
|
18
|
+
const sanitized = {};
|
|
19
|
+
const fieldsToSanitize = [
|
|
20
|
+
...this.DEFAULT_SENSITIVE_FIELDS,
|
|
21
|
+
...sensitiveFields,
|
|
22
|
+
];
|
|
23
|
+
Object.keys(headers).forEach((key) => {
|
|
24
|
+
if (this.isSensitiveField(key, fieldsToSanitize)) {
|
|
25
|
+
sanitized[key] = '[FILTERED]';
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
sanitized[key] = String(headers[key]);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return sanitized;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 脱敏 body
|
|
35
|
+
* @param body - 原始 body 数据
|
|
36
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
37
|
+
* @returns 脱敏后的 body
|
|
38
|
+
*/
|
|
39
|
+
static sanitizeBody(body, sensitiveFields = []) {
|
|
40
|
+
if (!body)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (typeof body === 'string') {
|
|
43
|
+
try {
|
|
44
|
+
body = JSON.parse(body);
|
|
45
|
+
}
|
|
46
|
+
catch (_a) {
|
|
47
|
+
// 非 JSON 字符串,不做处理
|
|
48
|
+
return body.length > 1000 ? body.substring(0, 1000) + '...' : body;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (typeof body === 'object') {
|
|
52
|
+
const sanitized = Object.assign({}, body);
|
|
53
|
+
const fieldsToSanitize = [
|
|
54
|
+
...this.DEFAULT_SENSITIVE_FIELDS,
|
|
55
|
+
...sensitiveFields,
|
|
56
|
+
];
|
|
57
|
+
this.sanitizeObject(sanitized, fieldsToSanitize);
|
|
58
|
+
return sanitized;
|
|
59
|
+
}
|
|
60
|
+
return body;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 脱敏 URL 中的 query string
|
|
64
|
+
* @param url - 原始 URL
|
|
65
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
66
|
+
* @returns 脱敏后的 URL
|
|
67
|
+
*/
|
|
68
|
+
static sanitizeQueryString(url, sensitiveFields = []) {
|
|
69
|
+
if (!url)
|
|
70
|
+
return url;
|
|
71
|
+
const queryIndex = url.indexOf('?');
|
|
72
|
+
if (queryIndex === -1) {
|
|
73
|
+
return url; // 没有 query string
|
|
74
|
+
}
|
|
75
|
+
const baseUrl = url.substring(0, queryIndex);
|
|
76
|
+
const queryString = url.substring(queryIndex + 1);
|
|
77
|
+
const params = new URLSearchParams(queryString);
|
|
78
|
+
const fieldsToSanitize = [
|
|
79
|
+
...this.DEFAULT_SENSITIVE_FIELDS,
|
|
80
|
+
...sensitiveFields,
|
|
81
|
+
];
|
|
82
|
+
params.forEach((value, key) => {
|
|
83
|
+
if (this.isSensitiveField(key, fieldsToSanitize)) {
|
|
84
|
+
params.set(key, '[FILTERED]');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const sanitizedQuery = params.toString();
|
|
88
|
+
return sanitizedQuery ? `${baseUrl}?${sanitizedQuery}` : baseUrl;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 脱敏 params 对象
|
|
92
|
+
* @param params - 原始 params 对象
|
|
93
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
94
|
+
* @returns 脱敏后的 params
|
|
95
|
+
*/
|
|
96
|
+
static sanitizeParams(params, sensitiveFields = []) {
|
|
97
|
+
if (!params)
|
|
98
|
+
return {};
|
|
99
|
+
const sanitized = {};
|
|
100
|
+
const fieldsToSanitize = [
|
|
101
|
+
...this.DEFAULT_SENSITIVE_FIELDS,
|
|
102
|
+
...sensitiveFields,
|
|
103
|
+
];
|
|
104
|
+
Object.keys(params).forEach((key) => {
|
|
105
|
+
if (this.isSensitiveField(key, fieldsToSanitize)) {
|
|
106
|
+
sanitized[key] = '[FILTERED]';
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
sanitized[key] = params[key];
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return sanitized;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 将 body 转换为字符串并脱敏
|
|
116
|
+
* @param data - 原始数据
|
|
117
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
118
|
+
* @returns 脱敏后的字符串
|
|
119
|
+
*/
|
|
120
|
+
static sanitizeBodyAsString(data, sensitiveFields = []) {
|
|
121
|
+
const sanitized = this.sanitizeBody(data, sensitiveFields);
|
|
122
|
+
if (!sanitized)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (typeof sanitized === 'string') {
|
|
125
|
+
return sanitized.length > 5000
|
|
126
|
+
? sanitized.substring(0, 5000) + '...'
|
|
127
|
+
: sanitized;
|
|
128
|
+
}
|
|
129
|
+
const jsonString = JSON.stringify(sanitized);
|
|
130
|
+
return jsonString.length > 5000
|
|
131
|
+
? jsonString.substring(0, 5000) + '...'
|
|
132
|
+
: jsonString;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 判断字段是否为敏感字段
|
|
136
|
+
* @param key - 字段名
|
|
137
|
+
* @param sensitiveFields - 敏感字段列表
|
|
138
|
+
* @returns 是否为敏感字段
|
|
139
|
+
*/
|
|
140
|
+
static isSensitiveField(key, sensitiveFields) {
|
|
141
|
+
const lowerKey = key.toLowerCase();
|
|
142
|
+
return sensitiveFields.some((field) => lowerKey.includes(field.toLowerCase()));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 递归脱敏对象中的敏感字段
|
|
146
|
+
* @param obj - 目标对象
|
|
147
|
+
* @param sensitiveFields - 敏感字段列表
|
|
148
|
+
*/
|
|
149
|
+
static sanitizeObject(obj, sensitiveFields) {
|
|
150
|
+
if (typeof obj !== 'object' || obj === null)
|
|
151
|
+
return;
|
|
152
|
+
for (const key in obj) {
|
|
153
|
+
if (this.isSensitiveField(key, sensitiveFields)) {
|
|
154
|
+
obj[key] = '[FILTERED]';
|
|
155
|
+
}
|
|
156
|
+
else if (typeof obj[key] === 'object') {
|
|
157
|
+
this.sanitizeObject(obj[key], sensitiveFields);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.SanitizeUtil = SanitizeUtil;
|
|
163
|
+
/**
|
|
164
|
+
* 默认敏感字段列表
|
|
165
|
+
*/
|
|
166
|
+
SanitizeUtil.DEFAULT_SENSITIVE_FIELDS = [
|
|
167
|
+
// Headers 相关
|
|
168
|
+
'authorization',
|
|
169
|
+
'apikey',
|
|
170
|
+
'x-api-key',
|
|
171
|
+
'x-auth-token',
|
|
172
|
+
'cookie',
|
|
173
|
+
'set-cookie',
|
|
174
|
+
// Body/Query 通用字段
|
|
175
|
+
'password',
|
|
176
|
+
'secret',
|
|
177
|
+
'token',
|
|
178
|
+
'key',
|
|
179
|
+
'credential',
|
|
180
|
+
'private',
|
|
181
|
+
'confidential',
|
|
182
|
+
'ssn',
|
|
183
|
+
'creditCard',
|
|
184
|
+
'accessToken',
|
|
185
|
+
'refreshToken',
|
|
186
|
+
'apiKey',
|
|
187
|
+
'apiSecret',
|
|
188
|
+
];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { ApiConfigService } from '../shared/services/api-config.service';
|
|
4
|
+
/**
|
|
5
|
+
* HTTP 日志拦截器
|
|
6
|
+
* 参考 Tomcat AccessLog 的实现,每个请求只记录一条日志
|
|
7
|
+
* 在请求完成时同时记录请求和响应的完整信息
|
|
8
|
+
*/
|
|
9
|
+
export declare class HttpLoggingInterceptor implements NestInterceptor {
|
|
10
|
+
private readonly configService;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
constructor(configService: ApiConfigService);
|
|
13
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
|
|
14
|
+
/**
|
|
15
|
+
* 生成请求 ID
|
|
16
|
+
*/
|
|
17
|
+
private generateRequestId;
|
|
18
|
+
/**
|
|
19
|
+
* 记录请求和响应(一条日志)
|
|
20
|
+
*/
|
|
21
|
+
private logRequestResponse;
|
|
22
|
+
/**
|
|
23
|
+
* 记录请求和错误(一条日志)
|
|
24
|
+
*/
|
|
25
|
+
private logRequestError;
|
|
26
|
+
/**
|
|
27
|
+
* 清理敏感的 header 信息
|
|
28
|
+
*/
|
|
29
|
+
private sanitizeHeaders;
|
|
30
|
+
/**
|
|
31
|
+
* 清理敏感的 body 信息
|
|
32
|
+
*/
|
|
33
|
+
private sanitizeBody;
|
|
34
|
+
/**
|
|
35
|
+
* 从 headers 提取用户信息
|
|
36
|
+
*/
|
|
37
|
+
private extractUser;
|
|
38
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var HttpLoggingInterceptor_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.HttpLoggingInterceptor = void 0;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const operators_1 = require("rxjs/operators");
|
|
16
|
+
const api_config_service_1 = require("../shared/services/api-config.service");
|
|
17
|
+
/**
|
|
18
|
+
* HTTP 日志拦截器
|
|
19
|
+
* 参考 Tomcat AccessLog 的实现,每个请求只记录一条日志
|
|
20
|
+
* 在请求完成时同时记录请求和响应的完整信息
|
|
21
|
+
*/
|
|
22
|
+
let HttpLoggingInterceptor = HttpLoggingInterceptor_1 = class HttpLoggingInterceptor {
|
|
23
|
+
constructor(configService) {
|
|
24
|
+
this.configService = configService;
|
|
25
|
+
this.logger = new common_1.Logger(HttpLoggingInterceptor_1.name);
|
|
26
|
+
}
|
|
27
|
+
intercept(context, next) {
|
|
28
|
+
const ctx = context.switchToHttp();
|
|
29
|
+
const request = ctx.getRequest();
|
|
30
|
+
const response = ctx.getResponse();
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
const { method, url, headers, body } = request;
|
|
33
|
+
const requestId = String(request.id || headers['x-request-id'] || this.generateRequestId());
|
|
34
|
+
// 监听响应完成事件,一条日志同时记录请求和响应
|
|
35
|
+
response.on('finish', () => {
|
|
36
|
+
const duration = Date.now() - startTime;
|
|
37
|
+
this.logRequestResponse(requestId, request, response, duration);
|
|
38
|
+
});
|
|
39
|
+
return next.handle().pipe((0, operators_1.catchError)((error) => {
|
|
40
|
+
const duration = Date.now() - startTime;
|
|
41
|
+
this.logRequestError(requestId, request, error, duration);
|
|
42
|
+
throw error;
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 生成请求 ID
|
|
47
|
+
*/
|
|
48
|
+
generateRequestId() {
|
|
49
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 记录请求和响应(一条日志)
|
|
53
|
+
*/
|
|
54
|
+
logRequestResponse(requestId, request, response, duration) {
|
|
55
|
+
const statusCode = response.statusCode;
|
|
56
|
+
const logData = {
|
|
57
|
+
requestId,
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
env: this.configService.nodeEnv,
|
|
60
|
+
appName: this.configService.getString('NAME'),
|
|
61
|
+
http: {
|
|
62
|
+
method: request.method,
|
|
63
|
+
url: request.url,
|
|
64
|
+
query: request.query,
|
|
65
|
+
statusCode,
|
|
66
|
+
duration: `${duration}ms`,
|
|
67
|
+
req: {
|
|
68
|
+
headers: this.sanitizeHeaders(request.headers),
|
|
69
|
+
body: this.sanitizeBody(request.body),
|
|
70
|
+
},
|
|
71
|
+
res: {
|
|
72
|
+
headers: this.sanitizeHeaders(response.getHeaders()),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
user: this.extractUser(request.headers),
|
|
76
|
+
};
|
|
77
|
+
const message = `HTTP ${request.method} ${request.url} ${statusCode} (${duration}ms)`;
|
|
78
|
+
if (statusCode >= 500) {
|
|
79
|
+
this.logger.error(message, logData);
|
|
80
|
+
}
|
|
81
|
+
else if (statusCode >= 400) {
|
|
82
|
+
this.logger.warn(message, logData);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.logger.log(message, logData);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 记录请求和错误(一条日志)
|
|
90
|
+
*/
|
|
91
|
+
logRequestError(requestId, request, error, duration) {
|
|
92
|
+
const logData = {
|
|
93
|
+
requestId,
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
env: this.configService.nodeEnv,
|
|
96
|
+
appName: this.configService.getString('NAME'),
|
|
97
|
+
http: {
|
|
98
|
+
method: request.method,
|
|
99
|
+
url: request.url,
|
|
100
|
+
query: request.query,
|
|
101
|
+
statusCode: (error === null || error === void 0 ? void 0 : error.status) || 500,
|
|
102
|
+
duration: `${duration}ms`,
|
|
103
|
+
req: {
|
|
104
|
+
headers: this.sanitizeHeaders(request.headers),
|
|
105
|
+
body: this.sanitizeBody(request.body),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
error: {
|
|
109
|
+
message: error.message,
|
|
110
|
+
stack: error.stack,
|
|
111
|
+
code: error.code,
|
|
112
|
+
},
|
|
113
|
+
user: this.extractUser(request.headers),
|
|
114
|
+
};
|
|
115
|
+
this.logger.error(`HTTP ${request.method} ${request.url} ${(error === null || error === void 0 ? void 0 : error.status) || 500} (${duration}ms) - ${error.message}`, logData);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 清理敏感的 header 信息
|
|
119
|
+
*/
|
|
120
|
+
sanitizeHeaders(headers) {
|
|
121
|
+
if (!headers)
|
|
122
|
+
return {};
|
|
123
|
+
const sanitized = Object.assign({}, headers);
|
|
124
|
+
const sensitiveKeys = ['authorization', 'apikey', 'x-api-key', 'token', 'cookie'];
|
|
125
|
+
for (const key of Object.keys(sanitized)) {
|
|
126
|
+
if (sensitiveKeys.includes(key.toLowerCase())) {
|
|
127
|
+
sanitized[key] = '[REDACTED]';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return sanitized;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 清理敏感的 body 信息
|
|
134
|
+
*/
|
|
135
|
+
sanitizeBody(body) {
|
|
136
|
+
if (!body)
|
|
137
|
+
return body;
|
|
138
|
+
// 如果 body 太大,只记录部分信息
|
|
139
|
+
const bodyStr = JSON.stringify(body);
|
|
140
|
+
if (bodyStr.length > 1000) {
|
|
141
|
+
return { _large: `${bodyStr.length} bytes` };
|
|
142
|
+
}
|
|
143
|
+
return body;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 从 headers 提取用户信息
|
|
147
|
+
*/
|
|
148
|
+
extractUser(headers) {
|
|
149
|
+
const userHeader = headers['user'] || headers['x-user'];
|
|
150
|
+
if (userHeader) {
|
|
151
|
+
try {
|
|
152
|
+
return typeof userHeader === 'string'
|
|
153
|
+
? JSON.parse(userHeader)
|
|
154
|
+
: userHeader;
|
|
155
|
+
}
|
|
156
|
+
catch (_a) {
|
|
157
|
+
return userHeader;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
exports.HttpLoggingInterceptor = HttpLoggingInterceptor;
|
|
164
|
+
exports.HttpLoggingInterceptor = HttpLoggingInterceptor = HttpLoggingInterceptor_1 = __decorate([
|
|
165
|
+
(0, common_1.Injectable)(),
|
|
166
|
+
__metadata("design:paramtypes", [api_config_service_1.ApiConfigService])
|
|
167
|
+
], HttpLoggingInterceptor);
|
package/interceptors/index.d.ts
CHANGED
package/interceptors/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./language-interceptor.service"), exports);
|
|
18
18
|
__exportStar(require("./translation-interceptor.service"), exports);
|
|
19
|
+
__exportStar(require("./http-logging-interceptor.service"), exports);
|
package/package.json
CHANGED
package/setup/bootstrap.setup.js
CHANGED
|
@@ -166,7 +166,7 @@ function bootstrapSetup(AppModule, SetupSwagger) {
|
|
|
166
166
|
const reflector = app.get(core_1.Reflector);
|
|
167
167
|
app.useGlobalFilters(new setup_1.SentryGlobalFilter(), new __1.HttpExceptionFilter(), new __1.QueryFailedFilter(reflector));
|
|
168
168
|
// 全局拦截器
|
|
169
|
-
app.useGlobalInterceptors(new __1.
|
|
169
|
+
app.useGlobalInterceptors(new __1.HttpLoggingInterceptor(configService), new __1.LanguageInterceptor(), new __1.TranslationInterceptor());
|
|
170
170
|
// 全局管道
|
|
171
171
|
app.useGlobalPipes(new nestjs_i18n_1.I18nValidationPipe({
|
|
172
172
|
whitelist: true,
|
|
@@ -214,25 +214,10 @@ let ApiConfigService = ApiConfigService_1 = class ApiConfigService {
|
|
|
214
214
|
genReqId: (req) => req.id,
|
|
215
215
|
transport,
|
|
216
216
|
level: this.isDev ? 'debug' : 'info',
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return 'error';
|
|
220
|
-
if (res.statusCode >= 400)
|
|
221
|
-
return 'warn';
|
|
222
|
-
return 'info';
|
|
223
|
-
},
|
|
224
|
-
wrapSerializers: true,
|
|
225
|
-
customProps: (req, res) => {
|
|
226
|
-
return {
|
|
227
|
-
env: this.nodeEnv,
|
|
228
|
-
appName: this.getString('NAME'),
|
|
229
|
-
user: req === null || req === void 0 ? void 0 : req.user,
|
|
230
|
-
'req.body': req === null || req === void 0 ? void 0 : req.body,
|
|
231
|
-
'res.body': res === null || res === void 0 ? void 0 : res.body,
|
|
232
|
-
};
|
|
233
|
-
},
|
|
217
|
+
// 禁用 pinoHttp 的自动日志,由 HttpLoggingInterceptor 处理
|
|
218
|
+
autoLogging: false,
|
|
234
219
|
redact: {
|
|
235
|
-
paths: ['req.headers.authorization', 'req.headers.apikey'],
|
|
220
|
+
paths: ['req.headers.authorization', 'req.headers.apikey', 'req.headers.cookie'],
|
|
236
221
|
remove: true,
|
|
237
222
|
},
|
|
238
223
|
},
|