@vafast/request-logger 0.4.0 → 0.4.1

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
@@ -17,7 +17,13 @@ server.use(requestLogger({
17
17
  url: 'http://log-server:9005/api/logs/ingest',
18
18
  service: 'my-service',
19
19
  headers: { Authorization: 'Bearer apiKeyId:apiKeySecret' },
20
- onError: (err) => console.error('日志记录失败', err),
20
+ excludePaths: ['/health', '/metrics'],
21
+ onError: (err, { droppedCount }) => {
22
+ console.warn(
23
+ `日志上报失败: ${err.message}`,
24
+ droppedCount > 0 ? `(已忽略 ${droppedCount} 条)` : ''
25
+ )
26
+ },
21
27
  }))
22
28
  ```
23
29
 
@@ -25,6 +31,8 @@ server.use(requestLogger({
25
31
 
26
32
  ## 配置
27
33
 
34
+ ### 基础配置
35
+
28
36
  | 参数 | 类型 | 必填 | 默认值 | 说明 |
29
37
  |------|------|------|--------|------|
30
38
  | `url` | `string` | 是 | - | 日志服务 URL |
@@ -32,10 +40,142 @@ server.use(requestLogger({
32
40
  | `headers` | `Record<string, string>` | 否 | `{}` | 自定义请求头(如认证) |
33
41
  | `timeout` | `number` | 否 | `5000` | 超时时间(毫秒) |
34
42
  | `sanitize` | `SanitizeConfig` | 否 | - | 敏感数据清洗配置 |
35
- | `onError` | `(err) => void` | 否 | `console.error` | 错误回调 |
43
+ | `onError` | `(err, ctx) => void` | 否 | `console.error` | 错误回调,`ctx.droppedCount` 为被节流忽略的错误数 |
36
44
  | `enabled` | `boolean` | 否 | `true` | 是否启用 |
45
+ | `excludePaths` | `(string \| RegExp)[]` | 否 | `[]` | 排除的路径列表,不记录日志 |
46
+
47
+ ### 熔断器配置 (Circuit Breaker)
48
+
49
+ 当日志服务不可用时,避免无谓的超时等待。
50
+
51
+ | 参数 | 类型 | 默认值 | 说明 |
52
+ |------|------|--------|------|
53
+ | `circuitBreaker.failureThreshold` | `number` | `5` | 触发熔断的连续失败次数 |
54
+ | `circuitBreaker.resetTimeout` | `number` | `60000` | 熔断恢复时间(毫秒) |
55
+
56
+ ```typescript
57
+ requestLogger({
58
+ url: '...',
59
+ service: '...',
60
+ circuitBreaker: {
61
+ failureThreshold: 5, // 连续失败 5 次后熔断
62
+ resetTimeout: 60000, // 1 分钟后尝试恢复
63
+ },
64
+ })
65
+ ```
66
+
67
+ **工作原理**:
68
+ 1. 正常状态:每个请求都尝试上报
69
+ 2. 连续失败达到阈值:进入熔断状态,跳过所有上报
70
+ 3. 熔断时间到期:进入半开状态,允许一个请求通过测试
71
+ 4. 测试成功:恢复正常;测试失败:继续熔断
72
+
73
+ ### stdout 双写配置 (Dual Write)
74
+
75
+ 同时输出到 stdout,用于 K8s 日志采集(如 TKE + CLS)。即使 log-server 挂了,运维也能从 CLS 查日志。
76
+
77
+ | 参数 | 类型 | 默认值 | 说明 |
78
+ |------|------|--------|------|
79
+ | `stdout.enabled` | `boolean` | `false` | 是否启用 stdout 输出 |
80
+ | `stdout.format` | `'json' \| 'text'` | `'json'` | 输出格式 |
81
+ | `stdout.includeBody` | `boolean` | `false` | 是否包含请求体 |
82
+ | `stdout.includeResponse` | `boolean` | `false` | 是否包含响应体 |
83
+
84
+ ```typescript
85
+ requestLogger({
86
+ url: 'http://log-server:9005/api/logs/ingest',
87
+ service: 'auth-server',
88
+ stdout: {
89
+ enabled: true, // 启用双写
90
+ format: 'json', // JSON 格式(K8s 友好)
91
+ includeBody: false, // 不含请求体(减小日志量)
92
+ includeResponse: false,
93
+ },
94
+ })
95
+ ```
96
+
97
+ **stdout 输出格式**(精简版,兼容 pino/K8s):
98
+
99
+ ```json
100
+ {"level":30,"time":1706123456789,"service":"auth-server","method":"POST","path":"/api/users","status":200,"duration":50,"msg":"POST /api/users 200 50ms"}
101
+ ```
37
102
 
38
- ## 路由级别控制
103
+ **架构图**:
104
+
105
+ ```
106
+ 请求进来
107
+
108
+
109
+ requestLogger 中间件
110
+
111
+ ├── stdout(JSON)──▶ K8s 采集 ──▶ CLS/Loki(运维备份)
112
+
113
+ └── HTTP 推送 ──▶ log-server ──▶ MongoDB ──▶ ones(用户查询)
114
+ ```
115
+
116
+ ### 错误节流配置 (Error Throttle)
117
+
118
+ 避免相同错误刷屏,在一段时间内只打印一次。
119
+
120
+ | 参数 | 类型 | 默认值 | 说明 |
121
+ |------|------|--------|------|
122
+ | `errorThrottle.interval` | `number` | `60000` | 节流间隔(毫秒) |
123
+
124
+ ```typescript
125
+ requestLogger({
126
+ url: '...',
127
+ service: '...',
128
+ errorThrottle: {
129
+ interval: 60000, // 同类错误 1 分钟内只打 1 条
130
+ },
131
+ onError: (err, { droppedCount }) => {
132
+ // droppedCount: 上次打印到这次之间被忽略的错误数
133
+ logger.warn(
134
+ { errorName: err.name, errorMessage: err.message, droppedCount },
135
+ droppedCount > 0
136
+ ? `日志上报失败 (已忽略 ${droppedCount} 条)`
137
+ : '日志上报失败'
138
+ )
139
+ },
140
+ })
141
+ ```
142
+
143
+ **效果对比**:
144
+
145
+ ```
146
+ # 之前(日志服务挂了)
147
+ 日志上报失败
148
+ 日志上报失败
149
+ 日志上报失败
150
+ ... (每秒好几条,刷屏)
151
+
152
+ # 之后
153
+ 日志上报失败
154
+ (沉默 1 分钟)
155
+ 日志上报失败 (已忽略 120 条)
156
+ (沉默 1 分钟)
157
+ 日志上报失败 (已忽略 118 条)
158
+ ```
159
+
160
+ ## 路径排除
161
+
162
+ ### excludePaths 配置
163
+
164
+ 在中间件配置中排除特定路径:
165
+
166
+ ```typescript
167
+ requestLogger({
168
+ url: '...',
169
+ service: '...',
170
+ excludePaths: [
171
+ '/health', // 精确匹配
172
+ '/internal/', // 前缀匹配(含子路径)
173
+ /^\/metrics/, // 正则匹配
174
+ ],
175
+ })
176
+ ```
177
+
178
+ ### 路由级别控制
39
179
 
40
180
  在路由定义中设置 `log: false` 跳过日志记录:
41
181
 
@@ -69,19 +209,8 @@ server.use(requestLogger({
69
209
  status: 200,
70
210
  duration: 50,
71
211
  service: 'my-service',
72
- userId: 'user123',
73
- appId: 'app456',
74
- authType: 'jwt',
75
- ip: '192.168.1.1',
76
- userAgent: 'Mozilla/5.0...',
77
- traceId: 'trace789',
78
212
  createdAt: '2024-01-01T00:00:00.000Z',
79
- response: {
80
- success: true,
81
- message: 'OK',
82
- code: 0,
83
- },
84
- responseData: { ... },
213
+ response: { success: true, message: 'OK' },
85
214
  }
86
215
  ```
87
216
 
@@ -109,8 +238,53 @@ requestLogger({
109
238
 
110
239
  ## 特性
111
240
 
112
- - 异步非阻塞(不影响响应速度)
113
- - 自动敏感数据脱敏
114
- - 路由级别日志控制
115
- - 支持多租户(appId)
116
- - 支持分布式追踪(traceId)
241
+ - **异步非阻塞**:不影响响应速度
242
+ - **stdout 双写**:同时输出到 stdout,支持 K8s 日志采集
243
+ - **熔断器**:日志服务故障时自动熔断,避免雪崩
244
+ - **错误节流**:相同错误不刷屏,带统计计数
245
+ - **路径排除**:支持精确匹配、前缀匹配、正则匹配
246
+ - **敏感数据脱敏**:自动清洗密码、Token 等敏感字段
247
+ - **路由级别控制**:可在路由定义中禁用日志
248
+ - **支持多租户**:通过 headers 传递 appId
249
+ - **支持分布式追踪**:通过 headers 传递 traceId
250
+
251
+ ## 完整示例
252
+
253
+ ```typescript
254
+ import { requestLogger } from '@vafast/request-logger'
255
+ import { logger } from './logger'
256
+
257
+ server.use(requestLogger({
258
+ url: 'http://log-server:9005/api/logs/ingest',
259
+ service: 'auth-server',
260
+ headers: { Authorization: 'Bearer ak_xxx:sk_xxx' },
261
+ timeout: 5000,
262
+ enabled: true,
263
+ excludePaths: ['/health', '/verifyApiKey'],
264
+ // stdout 双写(K8s 日志采集)
265
+ stdout: {
266
+ enabled: true,
267
+ format: 'json',
268
+ },
269
+ // 熔断器
270
+ circuitBreaker: {
271
+ failureThreshold: 5,
272
+ resetTimeout: 60000,
273
+ },
274
+ // 错误节流
275
+ errorThrottle: {
276
+ interval: 60000,
277
+ },
278
+ onError: (err: Error, { droppedCount }: { droppedCount: number }) =>
279
+ logger.warn(
280
+ {
281
+ errorName: err.name,
282
+ errorMessage: err.message,
283
+ droppedCount,
284
+ },
285
+ droppedCount > 0
286
+ ? `request-logger 上报失败 (已忽略 ${droppedCount} 条相同错误)`
287
+ : 'request-logger 上报失败'
288
+ ),
289
+ }))
290
+ ```
package/dist/index.d.mts CHANGED
@@ -79,6 +79,17 @@ interface ErrorThrottleConfig {
79
79
  /** 同类错误的节流间隔(毫秒),默认 60000(1分钟) */
80
80
  interval?: number;
81
81
  }
82
+ /** stdout 双写配置 */
83
+ interface StdoutConfig {
84
+ /** 是否启用 stdout 输出 @default false */
85
+ enabled?: boolean;
86
+ /** 输出格式 @default 'json' */
87
+ format?: 'json' | 'text';
88
+ /** 是否包含响应体(可能很大)@default false */
89
+ includeResponse?: boolean;
90
+ /** 是否包含请求体 @default false */
91
+ includeBody?: boolean;
92
+ }
82
93
  /** 请求日志配置 */
83
94
  interface RequestLoggerOptions {
84
95
  /** 日志服务 URL */
@@ -103,6 +114,8 @@ interface RequestLoggerOptions {
103
114
  circuitBreaker?: CircuitBreakerConfig;
104
115
  /** 错误节流配置 */
105
116
  errorThrottle?: ErrorThrottleConfig;
117
+ /** stdout 双写配置(用于 K8s 日志采集) */
118
+ stdout?: StdoutConfig;
106
119
  }
107
120
  /**
108
121
  * 请求日志中间件
@@ -122,5 +135,5 @@ declare function requestLogger(options: RequestLoggerOptions): vafast0.TypedMidd
122
135
  /** @deprecated 使用 requestLogger 代替 */
123
136
  declare const createRequestLogger: typeof requestLogger;
124
137
  //#endregion
125
- export { CircuitBreakerConfig, ErrorThrottleConfig, LogData, RequestData, RequestLoggerOptions, ResponseData, type SanitizeConfig, createRequestLogger, requestLogger as default, requestLogger, sanitize, sanitizeHeaders };
138
+ export { CircuitBreakerConfig, ErrorThrottleConfig, LogData, RequestData, RequestLoggerOptions, ResponseData, type SanitizeConfig, StdoutConfig, createRequestLogger, requestLogger as default, requestLogger, sanitize, sanitizeHeaders };
126
139
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/sanitize.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AAQA;AA+DA;AAAkC,UA/DjB,cAAA,CA+DiB;EAAY;EAA4B,YAAA,CAAA,EAAA,MAAA,EAAA;EAAC;EA+D3D,UAAA,CAAA,EAAA,MAAe,EAAA;EACpB;EACA,WAAA,CAAA,EAAA,MAAA;EACR;EAAM,QAAA,CAAA,EAAA,MAAA;;;;AC7GT;;;;;AAoBA;AAGA;AAMA;AAQiB,iBDMD,QCNoB,CAAA,CAAA,CAAA,CAAA,IAAA,EDMF,CCNE,EAAA,MAAA,CAAA,EDMU,cCNV,EAAA,KAAA,CAAA,EAAA,MAAA,CAAA,EDMsC,CCNtC;AAMpC;;;;;;;;AAyHA;AA4CA;iBDtGgB,eAAA,UACL,iCACA,iBACR;;;;UC7Gc,WAAA;EDpBA,MAAA,EAAA,MAAA;EA+DD,GAAA,EAAA,MAAQ;EAAU,IAAA,EAAA,MAAA;EAAY,OAAA,ECvCnC,MDuCmC,CAAA,MAAA,EAAA,MAAA,CAAA;EAA4B,IAAA,EAAA,OAAA;EAAC,KAAA,ECrClE,MDqCkE,CAAA,MAAA,EAAA,MAAA,CAAA;EA+D3D,MAAA,EAAA,MAAA;EACL,QAAA,EAAA,MAAA;EACA,MAAA,CAAA,EAAA,MAAA;EACR,KAAA,CAAA,EAAA,MAAA;EAAM,QAAA,CAAA,EAAA,MAAA;;;;EC7GQ,OAAA,CAAA,EAAA,MAAW;EAIjB,SAAA,EAYE,IAZF;;;AAYM,KAIL,YAAA,GAJK,OAAA;AAIjB;AAGiB,UAAA,OAAA,CACN;EAKM,OAAA,EALN,WAKM;EAQA,QAAA,EAZL,YAYwB;AAMpC;;AAUa,UAxBI,oBAAA,CAwBJ;EAEO;EAIO,gBAAA,CAAA,EAAA,MAAA;EAER;EAED,YAAA,CAAA,EAAA,MAAA;;AAqGlB;AA4Ca,UA3KI,mBAAA,CA2Ke;;;;;UArKf,oBAAA;;;;;;YAML;;;;aAIC;;oBAEO;;;;;;2BAIO;;mBAER;;kBAED;;;;;;;;;;;;;;;;iBAqGF,aAAA,UAAuB,uBAAoB,OAAA,CAAA;;cA4C9C,4BAAmB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/sanitize.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AAQA;AA+DA;AAAkC,UA/DjB,cAAA,CA+DiB;EAAY;EAA4B,YAAA,CAAA,EAAA,MAAA,EAAA;EAAC;EA+D3D,UAAA,CAAA,EAAA,MAAe,EAAA;EACpB;EACA,WAAA,CAAA,EAAA,MAAA;EACR;EAAM,QAAA,CAAA,EAAA,MAAA;;;;AC7GT;;;;;AAoBA;AAGA;AAMA;AAQiB,iBDMD,QCNoB,CAAA,CAAA,CAAA,CAAA,IAAA,EDMF,CCNE,EAAA,MAAA,CAAA,EDMU,cCNV,EAAA,KAAA,CAAA,EAAA,MAAA,CAAA,EDMsC,CCNtC;AAMpC;AAYA;;;;;;;;;AA2HgB,iBDxEA,eAAA,CCwEuB,OAAA,EDvE5B,MCuEgD,CAAA,MAAA,EAAA,MAAA,CAAA,EAAA,MAAA,CAAA,EDtEhD,cCsEgD,CAAA,EDrExD,MCqEwD,CAAA,MAAA,EAAA,MAAA,CAAA;;;;UAlL1C,WAAA;EDpBA,MAAA,EAAA,MAAA;EA+DD,GAAA,EAAA,MAAQ;EAAU,IAAA,EAAA,MAAA;EAAY,OAAA,ECvCnC,MDuCmC,CAAA,MAAA,EAAA,MAAA,CAAA;EAA4B,IAAA,EAAA,OAAA;EAAC,KAAA,ECrClE,MDqCkE,CAAA,MAAA,EAAA,MAAA,CAAA;EA+D3D,MAAA,EAAA,MAAA;EACL,QAAA,EAAA,MAAA;EACA,MAAA,CAAA,EAAA,MAAA;EACR,KAAA,CAAA,EAAA,MAAA;EAAM,QAAA,CAAA,EAAA,MAAA;;;;EC7GQ,OAAA,CAAA,EAAA,MAAW;EAIjB,SAAA,EAYE,IAZF;;;AAYM,KAIL,YAAA,GAJK,OAAA;AAIjB;AAGiB,UAAA,OAAA,CACN;EAKM,OAAA,EALN,WAKM;EAQA,QAAA,EAZL,YAYwB;AAMpC;AAYA;AAMY,UAhCK,oBAAA,CAgCL;EAIC;EAEO,gBAAA,CAAA,EAAA,MAAA;EAIO;EAER,YAAA,CAAA,EAAA,MAAA;;;AAII,UAxCN,mBAAA,CAwCM;EAqGP;EA8CH,QAAA,CAAA,EAAA,MAAA;;;UArLI,YAAA;;;;;;;;;;;UAYA,oBAAA;;;;;;YAML;;;;aAIC;;oBAEO;;;;;;2BAIO;;mBAER;;kBAED;;WAEP;;;;;;;;;;;;;;;;iBAqGK,aAAA,UAAuB,uBAAoB,OAAA,CAAA;;cA8C9C,4BAAmB"}
package/dist/index.mjs CHANGED
@@ -208,7 +208,7 @@ var ErrorThrottle = class {
208
208
  * ```
209
209
  */
210
210
  function requestLogger(options) {
211
- const { url, service, headers = {}, timeout = 5e3, sanitize: sanitizeConfig, onError = console.error, enabled = true, excludePaths = [], circuitBreaker: circuitBreakerConfig, errorThrottle: errorThrottleConfig } = options;
211
+ const { url, service, headers = {}, timeout = 5e3, sanitize: sanitizeConfig, onError = console.error, enabled = true, excludePaths = [], circuitBreaker: circuitBreakerConfig, errorThrottle: errorThrottleConfig, stdout: stdoutConfig } = options;
212
212
  const circuitBreaker = new CircuitBreaker(circuitBreakerConfig);
213
213
  const errorThrottle = new ErrorThrottle(errorThrottleConfig);
214
214
  return defineMiddleware(async (req, next) => {
@@ -224,7 +224,8 @@ function requestLogger(options) {
224
224
  onError,
225
225
  excludePaths,
226
226
  circuitBreaker,
227
- errorThrottle
227
+ errorThrottle,
228
+ stdoutConfig
228
229
  }).catch(() => {});
229
230
  return response;
230
231
  });
@@ -260,12 +261,11 @@ async function fetchWithTimeout(targetUrl, options, timeout) {
260
261
  }
261
262
  }
262
263
  async function recordLog(req, response, startTime, options) {
263
- const { url: logUrl, service, headers: customHeaders, timeout, sanitizeConfig, onError, excludePaths, circuitBreaker, errorThrottle } = options;
264
+ const { url: logUrl, service, headers: customHeaders, timeout, sanitizeConfig, onError, excludePaths, circuitBreaker, errorThrottle, stdoutConfig } = options;
264
265
  const reqUrl = new URL(req.url);
265
266
  const path = reqUrl.pathname;
266
267
  if (isPathExcluded(path, excludePaths)) return;
267
268
  if (shouldSkipLog(req.method, path)) return;
268
- if (!circuitBreaker.canRequest()) return;
269
269
  let body = null;
270
270
  try {
271
271
  if ((req.headers.get("content-type") || "").includes("application/json")) body = await req.clone().json();
@@ -281,6 +281,7 @@ async function recordLog(req, response, startTime, options) {
281
281
  const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig);
282
282
  const sanitizedBody = sanitize(body, sanitizeConfig);
283
283
  const sanitizedResponseData = sanitize(responseData, sanitizeConfig);
284
+ const duration = Date.now() - startTime;
284
285
  const logBody = {
285
286
  method: req.method,
286
287
  url: req.url,
@@ -289,11 +290,13 @@ async function recordLog(req, response, startTime, options) {
289
290
  body: sanitizedBody,
290
291
  query: Object.fromEntries(reqUrl.searchParams),
291
292
  status: response.status,
292
- duration: Date.now() - startTime,
293
+ duration,
293
294
  service,
294
295
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
295
296
  response: sanitizedResponseData
296
297
  };
298
+ if (stdoutConfig?.enabled) writeToStdout(logBody, stdoutConfig);
299
+ if (!circuitBreaker.canRequest()) return;
297
300
  try {
298
301
  const res = await fetchWithTimeout(logUrl, {
299
302
  method: "POST",
@@ -311,6 +314,24 @@ async function recordLog(req, response, startTime, options) {
311
314
  if (shouldLog) onError(error, { droppedCount });
312
315
  }
313
316
  }
317
+ /** 输出到 stdout(用于 K8s 日志采集) */
318
+ function writeToStdout(logBody, config) {
319
+ const { format = "json", includeResponse = false, includeBody = false } = config;
320
+ const stdoutLog = {
321
+ level: 30,
322
+ time: Date.now(),
323
+ service: logBody.service,
324
+ method: logBody.method,
325
+ path: logBody.path,
326
+ status: logBody.status,
327
+ duration: logBody.duration,
328
+ msg: `${logBody.method} ${logBody.path} ${logBody.status} ${logBody.duration}ms`
329
+ };
330
+ if (includeBody && logBody.body) stdoutLog.body = logBody.body;
331
+ if (includeResponse && logBody.response) stdoutLog.response = logBody.response;
332
+ if (format === "json") console.log(JSON.stringify(stdoutLog));
333
+ else console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${logBody.method} ${logBody.path} ${logBody.status} ${logBody.duration}ms`);
334
+ }
314
335
  var src_default = requestLogger;
315
336
 
316
337
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/sanitize.ts","../src/index.ts"],"sourcesContent":["/**\n * 敏感数据清洗工具\n * \n * 用于在记录日志前移除或脱敏敏感信息\n */\n\n// ============ Types ============\n\nexport interface SanitizeConfig {\n /** 需要完全移除的字段(小写) */\n removeFields?: string[]\n /** 需要脱敏的字段(小写,部分匹配) */\n maskFields?: string[]\n /** 脱敏占位符 @default '[REDACTED]' */\n placeholder?: string\n /** 最大递归深度 @default 10 */\n maxDepth?: number\n}\n\n// ============ Default Config ============\n\n/** 默认需要完全移除的敏感字段 */\nconst DEFAULT_REMOVE_FIELDS = [\n 'password',\n 'newpassword',\n 'oldpassword',\n 'confirmpassword',\n 'secret',\n 'secretkey',\n 'privatekey',\n 'apisecret',\n 'clientsecret',\n]\n\n/** 默认需要脱敏的字段(保留部分信息) */\nconst DEFAULT_MASK_FIELDS = [\n 'token',\n 'accesstoken',\n 'refreshtoken',\n 'authorization',\n 'apikey',\n 'api_key',\n 'x-api-key',\n 'idtoken',\n 'sessiontoken',\n 'bearer',\n]\n\nconst DEFAULT_PLACEHOLDER = '[REDACTED]'\nconst DEFAULT_MAX_DEPTH = 10\n\n// ============ Sanitize Functions ============\n\n/**\n * 部分脱敏(保留前4后4位)\n */\nfunction partialMask(value: string, placeholder: string): string {\n if (value.length <= 8) return placeholder\n return value.slice(0, 4) + '****' + value.slice(-4)\n}\n\n/**\n * 深度清洗对象中的敏感数据\n * \n * @example\n * ```typescript\n * const data = { password: '123456', token: 'eyJhbG...' }\n * const sanitized = sanitize(data)\n * // { password: '[REDACTED]', token: 'eyJh****...' }\n * ```\n */\nexport function sanitize<T>(data: T, config?: SanitizeConfig, depth = 0): T {\n const {\n removeFields = DEFAULT_REMOVE_FIELDS,\n maskFields = DEFAULT_MASK_FIELDS,\n placeholder = DEFAULT_PLACEHOLDER,\n maxDepth = DEFAULT_MAX_DEPTH,\n } = config ?? {}\n\n // 防止无限递归\n if (depth > maxDepth) return data\n \n if (data === null || data === undefined) {\n return data\n }\n\n // 处理数组\n if (Array.isArray(data)) {\n return data.map(item => sanitize(item, config, depth + 1)) as T\n }\n\n // 处理对象\n if (typeof data === 'object') {\n const result: Record<string, unknown> = {}\n \n for (const [key, value] of Object.entries(data)) {\n const lowerKey = key.toLowerCase()\n \n // 完全移除的字段\n if (removeFields.some(field => lowerKey === field)) {\n result[key] = placeholder\n continue\n }\n \n // 部分脱敏的字段\n if (maskFields.some(field => lowerKey.includes(field))) {\n if (typeof value === 'string') {\n result[key] = partialMask(value, placeholder)\n } else {\n result[key] = placeholder\n }\n continue\n }\n \n // 递归处理嵌套对象\n result[key] = sanitize(value, config, depth + 1)\n }\n \n return result as T\n }\n\n return data\n}\n\n/**\n * 清洗 HTTP 请求头\n * \n * @example\n * ```typescript\n * const headers = { Authorization: 'Bearer eyJhbG...', Cookie: 'session=xxx' }\n * const sanitized = sanitizeHeaders(headers)\n * // { Authorization: 'Bearer eyJh****...', Cookie: '[REDACTED]' }\n * ```\n */\nexport function sanitizeHeaders(\n headers: Record<string, string>,\n config?: SanitizeConfig\n): Record<string, string> {\n const {\n maskFields = DEFAULT_MASK_FIELDS,\n placeholder = DEFAULT_PLACEHOLDER,\n } = config ?? {}\n\n const result: Record<string, string> = {}\n \n for (const [key, value] of Object.entries(headers)) {\n const lowerKey = key.toLowerCase()\n \n // Authorization 头部分脱敏\n if (lowerKey === 'authorization') {\n if (value.startsWith('Bearer ')) {\n result[key] = 'Bearer ' + partialMask(value.slice(7), placeholder)\n } else {\n result[key] = partialMask(value, placeholder)\n }\n continue\n }\n \n // Cookie 完全脱敏\n if (lowerKey === 'cookie' || lowerKey === 'set-cookie') {\n result[key] = placeholder\n continue\n }\n \n // API Key 相关头脱敏\n if (maskFields.some(field => lowerKey.includes(field))) {\n result[key] = partialMask(value, placeholder)\n continue\n }\n \n result[key] = value\n }\n \n return result\n}\n\n/**\n * 检查值是否为敏感字段\n */\nexport function isSensitiveField(fieldName: string, config?: SanitizeConfig): boolean {\n const {\n removeFields = DEFAULT_REMOVE_FIELDS,\n maskFields = DEFAULT_MASK_FIELDS,\n } = config ?? {}\n\n const lowerName = fieldName.toLowerCase()\n \n return (\n removeFields.some(field => lowerName === field) ||\n maskFields.some(field => lowerName.includes(field))\n )\n}\n\n","/**\n * @vafast/request-logger - API 请求日志中间件\n *\n * 特性:\n * - 自动敏感数据脱敏\n * - HTTP 远程日志服务\n * - 异步非阻塞记录\n * - 路由级别日志控制(路由定义中设置 log: false)\n * - 熔断器:连续失败后暂停上报,避免无谓等待\n * - 错误节流:同类错误在一段时间内只打一次日志\n *\n * @example\n * ```typescript\n * import { requestLogger } from '@vafast/request-logger'\n *\n * server.use(requestLogger({\n * url: 'http://log-server:9005/api/logs/ingest',\n * service: 'auth-server',\n * getUserId: (req) => req.__locals?.userInfo?.id,\n * }))\n * ```\n */\nimport { defineMiddleware, getRoute } from 'vafast'\nimport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\n\n// ============ Types ============\n\n/** 请求信息 */\nexport interface RequestData {\n method: string\n url: string\n path: string\n headers: Record<string, string>\n body: unknown\n query: Record<string, string>\n status: number\n duration: number\n userId?: string\n appId?: string\n authType?: string\n service?: string\n ip?: string\n userAgent?: string\n traceId?: string\n createdAt: Date\n}\n\n/** 响应数据(完整响应体) */\nexport type ResponseData = unknown\n\n/** 完整日志数据 */\nexport interface LogData {\n request: RequestData\n response: ResponseData\n}\n\n/** 熔断器配置 */\nexport interface CircuitBreakerConfig {\n /** 触发熔断的连续失败次数,默认 5 */\n failureThreshold?: number\n /** 熔断恢复时间(毫秒),默认 60000(1分钟) */\n resetTimeout?: number\n}\n\n/** 错误节流配置 */\nexport interface ErrorThrottleConfig {\n /** 同类错误的节流间隔(毫秒),默认 60000(1分钟) */\n interval?: number\n}\n\n/** 请求日志配置 */\nexport interface RequestLoggerOptions {\n /** 日志服务 URL */\n url: string\n /** 服务标识(如 auth-server、ones-server) */\n service: string\n /** 自定义请求头(如认证信息) */\n headers?: Record<string, string>\n /** 超时时间(毫秒),默认 5000 */\n timeout?: number\n /** 敏感数据清洗配置 */\n sanitize?: SanitizeConfig\n /** 错误回调 */\n onError?: (error: Error, context: { droppedCount: number }) => void\n /** 是否启用 @default true */\n enabled?: boolean\n /** 排除的路径列表(精确匹配或正则),这些路径不记录日志 */\n excludePaths?: (string | RegExp)[]\n /** 熔断器配置 */\n circuitBreaker?: CircuitBreakerConfig\n /** 错误节流配置 */\n errorThrottle?: ErrorThrottleConfig\n}\n\n// ============ Circuit Breaker ============\n\ntype CircuitState = 'closed' | 'open' | 'half-open'\n\nclass CircuitBreaker {\n private state: CircuitState = 'closed'\n private failureCount = 0\n private lastFailureTime = 0\n private readonly failureThreshold: number\n private readonly resetTimeout: number\n\n constructor(config: CircuitBreakerConfig = {}) {\n this.failureThreshold = config.failureThreshold ?? 5\n this.resetTimeout = config.resetTimeout ?? 60000\n }\n\n /** 检查是否允许请求 */\n canRequest(): boolean {\n if (this.state === 'closed') return true\n\n if (this.state === 'open') {\n // 检查是否到了恢复时间\n if (Date.now() - this.lastFailureTime >= this.resetTimeout) {\n this.state = 'half-open'\n return true\n }\n return false\n }\n\n // half-open 状态允许一个请求通过测试\n return true\n }\n\n /** 记录成功 */\n recordSuccess(): void {\n this.failureCount = 0\n this.state = 'closed'\n }\n\n /** 记录失败 */\n recordFailure(): void {\n this.failureCount++\n this.lastFailureTime = Date.now()\n\n if (this.failureCount >= this.failureThreshold) {\n this.state = 'open'\n }\n }\n\n /** 获取当前状态信息 */\n getStatus(): { state: CircuitState; failureCount: number } {\n return { state: this.state, failureCount: this.failureCount }\n }\n}\n\n// ============ Error Throttle ============\n\nclass ErrorThrottle {\n private lastErrorTime = 0\n private droppedCount = 0\n private readonly interval: number\n\n constructor(config: ErrorThrottleConfig = {}) {\n this.interval = config.interval ?? 60000\n }\n\n /** 检查是否应该打印错误,返回 { shouldLog, droppedCount } */\n shouldLog(): { shouldLog: boolean; droppedCount: number } {\n const now = Date.now()\n\n if (now - this.lastErrorTime >= this.interval) {\n const dropped = this.droppedCount\n this.lastErrorTime = now\n this.droppedCount = 0\n return { shouldLog: true, droppedCount: dropped }\n }\n\n this.droppedCount++\n return { shouldLog: false, droppedCount: 0 }\n }\n}\n\n// ============ Middleware ============\n\n/**\n * 请求日志中间件\n *\n * @example\n * ```typescript\n * import { requestLogger } from '@vafast/request-logger'\n *\n * server.use(requestLogger({\n * url: 'http://log-server:9005/api/logs/ingest',\n * service: 'auth-server',\n * auth: { apiKeyId: 'xxx', apiKeySecret: 'yyy' },\n * }))\n * ```\n */\nexport function requestLogger(options: RequestLoggerOptions) {\n const {\n url,\n service,\n headers = {},\n timeout = 5000,\n sanitize: sanitizeConfig,\n onError = console.error,\n enabled = true,\n excludePaths = [],\n circuitBreaker: circuitBreakerConfig,\n errorThrottle: errorThrottleConfig,\n } = options\n\n // 创建熔断器和错误节流器实例\n const circuitBreaker = new CircuitBreaker(circuitBreakerConfig)\n const errorThrottle = new ErrorThrottle(errorThrottleConfig)\n\n return defineMiddleware(async (req, next) => {\n if (!enabled) return next()\n\n const startTime = Date.now()\n const response = await next()\n\n // 异步记录日志,不阻塞响应\n recordLog(req, response, startTime, {\n url,\n service,\n headers,\n timeout,\n sanitizeConfig,\n onError,\n excludePaths,\n circuitBreaker,\n errorThrottle,\n }).catch(() => {\n // 错误已在 recordLog 内部处理,这里静默忽略\n })\n\n return response\n })\n}\n\n/** @deprecated 使用 requestLogger 代替 */\nexport const createRequestLogger = requestLogger\n\n// ============ Internal ============\n\ninterface RecordLogOptions {\n url: string\n service: string\n headers: Record<string, string>\n timeout: number\n sanitizeConfig?: SanitizeConfig\n onError: (error: Error, context: { droppedCount: number }) => void\n excludePaths: (string | RegExp)[]\n circuitBreaker: CircuitBreaker\n errorThrottle: ErrorThrottle\n}\n\n/** 检查路由是否配置了 log: false */\nfunction shouldSkipLog(method: string, path: string): boolean {\n try {\n const route = getRoute<{ log?: boolean }>(method, path)\n return route?.log === false\n } catch {\n return false\n }\n}\n\n/** 检查路径是否在排除列表中 */\nfunction isPathExcluded(\n path: string,\n excludePaths: (string | RegExp)[]\n): boolean {\n return excludePaths.some((pattern) => {\n if (typeof pattern === 'string') {\n return path === pattern || path.startsWith(pattern + '/')\n }\n return pattern.test(path)\n })\n}\n\n/** 带超时的 fetch */\nasync function fetchWithTimeout(\n targetUrl: string,\n options: RequestInit,\n timeout: number\n): Promise<Response> {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n return await fetch(targetUrl, { ...options, signal: controller.signal })\n } finally {\n clearTimeout(timeoutId)\n }\n}\n\nasync function recordLog(\n req: Request,\n response: Response,\n startTime: number,\n options: RecordLogOptions\n) {\n const {\n url: logUrl,\n service,\n headers: customHeaders,\n timeout,\n sanitizeConfig,\n onError,\n excludePaths,\n circuitBreaker,\n errorThrottle,\n } = options\n\n const reqUrl = new URL(req.url)\n const path = reqUrl.pathname\n\n // 检查路径是否在排除列表中\n if (isPathExcluded(path, excludePaths)) return\n\n // 检查路由是否禁用日志\n if (shouldSkipLog(req.method, path)) return\n\n // 熔断器检查:如果熔断打开,直接跳过上报\n if (!circuitBreaker.canRequest()) {\n return\n }\n\n // 解析请求体\n let body: unknown = null\n try {\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n body = await req.clone().json()\n }\n } catch {\n // 忽略\n }\n\n // 解析响应体\n let responseData: ResponseData = null\n try {\n responseData = await response.clone().json()\n } catch {\n // 忽略(非 JSON 响应)\n }\n\n // 提取请求头\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n // 清洗敏感数据\n const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig)\n const sanitizedBody = sanitize(body, sanitizeConfig)\n const sanitizedResponseData = sanitize(responseData, sanitizeConfig)\n\n // 构建日志数据(业务字段由 log-server 从 headers 解析)\n const logBody = {\n method: req.method,\n url: req.url,\n path,\n headers: sanitizedHeaders,\n body: sanitizedBody,\n query: Object.fromEntries(reqUrl.searchParams),\n status: response.status,\n duration: Date.now() - startTime,\n service,\n createdAt: new Date().toISOString(),\n response: sanitizedResponseData, // 直接存储完整响应数据\n }\n\n // 发送到日志服务\n try {\n const res = await fetchWithTimeout(\n logUrl,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(logBody),\n },\n timeout\n )\n\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}: ${res.statusText}`)\n }\n\n // 成功:重置熔断器\n circuitBreaker.recordSuccess()\n } catch (error) {\n // 失败:记录到熔断器\n circuitBreaker.recordFailure()\n\n // 错误节流:检查是否应该打印\n const { shouldLog, droppedCount } = errorThrottle.shouldLog()\n if (shouldLog) {\n onError(error as Error, { droppedCount })\n }\n }\n}\n\n// ============ Re-exports ============\n\nexport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\nexport default requestLogger\n"],"mappings":";;;;AAsBA,MAAM,wBAAwB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;AAGD,MAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,sBAAsB;AAC5B,MAAM,oBAAoB;;;;AAO1B,SAAS,YAAY,OAAe,aAA6B;AAC/D,KAAI,MAAM,UAAU,EAAG,QAAO;AAC9B,QAAO,MAAM,MAAM,GAAG,EAAE,GAAG,SAAS,MAAM,MAAM,GAAG;;;;;;;;;;;;AAarD,SAAgB,SAAY,MAAS,QAAyB,QAAQ,GAAM;CAC1E,MAAM,EACJ,eAAe,uBACf,aAAa,qBACb,cAAc,qBACd,WAAW,sBACT,UAAU,EAAE;AAGhB,KAAI,QAAQ,SAAU,QAAO;AAE7B,KAAI,SAAS,QAAQ,SAAS,OAC5B,QAAO;AAIT,KAAI,MAAM,QAAQ,KAAK,CACrB,QAAO,KAAK,KAAI,SAAQ,SAAS,MAAM,QAAQ,QAAQ,EAAE,CAAC;AAI5D,KAAI,OAAO,SAAS,UAAU;EAC5B,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;GAC/C,MAAM,WAAW,IAAI,aAAa;AAGlC,OAAI,aAAa,MAAK,UAAS,aAAa,MAAM,EAAE;AAClD,WAAO,OAAO;AACd;;AAIF,OAAI,WAAW,MAAK,UAAS,SAAS,SAAS,MAAM,CAAC,EAAE;AACtD,QAAI,OAAO,UAAU,SACnB,QAAO,OAAO,YAAY,OAAO,YAAY;QAE7C,QAAO,OAAO;AAEhB;;AAIF,UAAO,OAAO,SAAS,OAAO,QAAQ,QAAQ,EAAE;;AAGlD,SAAO;;AAGT,QAAO;;;;;;;;;;;;AAaT,SAAgB,gBACd,SACA,QACwB;CACxB,MAAM,EACJ,aAAa,qBACb,cAAc,wBACZ,UAAU,EAAE;CAEhB,MAAM,SAAiC,EAAE;AAEzC,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,EAAE;EAClD,MAAM,WAAW,IAAI,aAAa;AAGlC,MAAI,aAAa,iBAAiB;AAChC,OAAI,MAAM,WAAW,UAAU,CAC7B,QAAO,OAAO,YAAY,YAAY,MAAM,MAAM,EAAE,EAAE,YAAY;OAElE,QAAO,OAAO,YAAY,OAAO,YAAY;AAE/C;;AAIF,MAAI,aAAa,YAAY,aAAa,cAAc;AACtD,UAAO,OAAO;AACd;;AAIF,MAAI,WAAW,MAAK,UAAS,SAAS,SAAS,MAAM,CAAC,EAAE;AACtD,UAAO,OAAO,YAAY,OAAO,YAAY;AAC7C;;AAGF,SAAO,OAAO;;AAGhB,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;AC3ET,IAAM,iBAAN,MAAqB;CACnB,AAAQ,QAAsB;CAC9B,AAAQ,eAAe;CACvB,AAAQ,kBAAkB;CAC1B,AAAiB;CACjB,AAAiB;CAEjB,YAAY,SAA+B,EAAE,EAAE;AAC7C,OAAK,mBAAmB,OAAO,oBAAoB;AACnD,OAAK,eAAe,OAAO,gBAAgB;;;CAI7C,aAAsB;AACpB,MAAI,KAAK,UAAU,SAAU,QAAO;AAEpC,MAAI,KAAK,UAAU,QAAQ;AAEzB,OAAI,KAAK,KAAK,GAAG,KAAK,mBAAmB,KAAK,cAAc;AAC1D,SAAK,QAAQ;AACb,WAAO;;AAET,UAAO;;AAIT,SAAO;;;CAIT,gBAAsB;AACpB,OAAK,eAAe;AACpB,OAAK,QAAQ;;;CAIf,gBAAsB;AACpB,OAAK;AACL,OAAK,kBAAkB,KAAK,KAAK;AAEjC,MAAI,KAAK,gBAAgB,KAAK,iBAC5B,MAAK,QAAQ;;;CAKjB,YAA2D;AACzD,SAAO;GAAE,OAAO,KAAK;GAAO,cAAc,KAAK;GAAc;;;AAMjE,IAAM,gBAAN,MAAoB;CAClB,AAAQ,gBAAgB;CACxB,AAAQ,eAAe;CACvB,AAAiB;CAEjB,YAAY,SAA8B,EAAE,EAAE;AAC5C,OAAK,WAAW,OAAO,YAAY;;;CAIrC,YAA0D;EACxD,MAAM,MAAM,KAAK,KAAK;AAEtB,MAAI,MAAM,KAAK,iBAAiB,KAAK,UAAU;GAC7C,MAAM,UAAU,KAAK;AACrB,QAAK,gBAAgB;AACrB,QAAK,eAAe;AACpB,UAAO;IAAE,WAAW;IAAM,cAAc;IAAS;;AAGnD,OAAK;AACL,SAAO;GAAE,WAAW;GAAO,cAAc;GAAG;;;;;;;;;;;;;;;;;AAoBhD,SAAgB,cAAc,SAA+B;CAC3D,MAAM,EACJ,KACA,SACA,UAAU,EAAE,EACZ,UAAU,KACV,UAAU,gBACV,UAAU,QAAQ,OAClB,UAAU,MACV,eAAe,EAAE,EACjB,gBAAgB,sBAChB,eAAe,wBACb;CAGJ,MAAM,iBAAiB,IAAI,eAAe,qBAAqB;CAC/D,MAAM,gBAAgB,IAAI,cAAc,oBAAoB;AAE5D,QAAO,iBAAiB,OAAO,KAAK,SAAS;AAC3C,MAAI,CAAC,QAAS,QAAO,MAAM;EAE3B,MAAM,YAAY,KAAK,KAAK;EAC5B,MAAM,WAAW,MAAM,MAAM;AAG7B,YAAU,KAAK,UAAU,WAAW;GAClC;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACD,CAAC,CAAC,YAAY,GAEb;AAEF,SAAO;GACP;;;AAIJ,MAAa,sBAAsB;;AAiBnC,SAAS,cAAc,QAAgB,MAAuB;AAC5D,KAAI;AAEF,SADc,SAA4B,QAAQ,KAAK,EACzC,QAAQ;SAChB;AACN,SAAO;;;;AAKX,SAAS,eACP,MACA,cACS;AACT,QAAO,aAAa,MAAM,YAAY;AACpC,MAAI,OAAO,YAAY,SACrB,QAAO,SAAS,WAAW,KAAK,WAAW,UAAU,IAAI;AAE3D,SAAO,QAAQ,KAAK,KAAK;GACzB;;;AAIJ,eAAe,iBACb,WACA,SACA,SACmB;CACnB,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,QAAQ;AAE/D,KAAI;AACF,SAAO,MAAM,MAAM,WAAW;GAAE,GAAG;GAAS,QAAQ,WAAW;GAAQ,CAAC;WAChE;AACR,eAAa,UAAU;;;AAI3B,eAAe,UACb,KACA,UACA,WACA,SACA;CACA,MAAM,EACJ,KAAK,QACL,SACA,SAAS,eACT,SACA,gBACA,SACA,cACA,gBACA,kBACE;CAEJ,MAAM,SAAS,IAAI,IAAI,IAAI,IAAI;CAC/B,MAAM,OAAO,OAAO;AAGpB,KAAI,eAAe,MAAM,aAAa,CAAE;AAGxC,KAAI,cAAc,IAAI,QAAQ,KAAK,CAAE;AAGrC,KAAI,CAAC,eAAe,YAAY,CAC9B;CAIF,IAAI,OAAgB;AACpB,KAAI;AAEF,OADoB,IAAI,QAAQ,IAAI,eAAe,IAAI,IACvC,SAAS,mBAAmB,CAC1C,QAAO,MAAM,IAAI,OAAO,CAAC,MAAM;SAE3B;CAKR,IAAI,eAA6B;AACjC,KAAI;AACF,iBAAe,MAAM,SAAS,OAAO,CAAC,MAAM;SACtC;CAKR,MAAM,UAAkC,EAAE;AAC1C,KAAI,QAAQ,SAAS,OAAO,QAAQ;AAClC,UAAQ,OAAO;GACf;CAGF,MAAM,mBAAmB,gBAAgB,SAAS,eAAe;CACjE,MAAM,gBAAgB,SAAS,MAAM,eAAe;CACpD,MAAM,wBAAwB,SAAS,cAAc,eAAe;CAGpE,MAAM,UAAU;EACd,QAAQ,IAAI;EACZ,KAAK,IAAI;EACT;EACA,SAAS;EACT,MAAM;EACN,OAAO,OAAO,YAAY,OAAO,aAAa;EAC9C,QAAQ,SAAS;EACjB,UAAU,KAAK,KAAK,GAAG;EACvB;EACA,4BAAW,IAAI,MAAM,EAAC,aAAa;EACnC,UAAU;EACX;AAGD,KAAI;EACF,MAAM,MAAM,MAAM,iBAChB,QACA;GACE,QAAQ;GACR,SAAS;IAAE,gBAAgB;IAAoB,GAAG;IAAe;GACjE,MAAM,KAAK,UAAU,QAAQ;GAC9B,EACD,QACD;AAED,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,MAAM,QAAQ,IAAI,OAAO,IAAI,IAAI,aAAa;AAI1D,iBAAe,eAAe;UACvB,OAAO;AAEd,iBAAe,eAAe;EAG9B,MAAM,EAAE,WAAW,iBAAiB,cAAc,WAAW;AAC7D,MAAI,UACF,SAAQ,OAAgB,EAAE,cAAc,CAAC;;;AAQ/C,kBAAe"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/sanitize.ts","../src/index.ts"],"sourcesContent":["/**\n * 敏感数据清洗工具\n * \n * 用于在记录日志前移除或脱敏敏感信息\n */\n\n// ============ Types ============\n\nexport interface SanitizeConfig {\n /** 需要完全移除的字段(小写) */\n removeFields?: string[]\n /** 需要脱敏的字段(小写,部分匹配) */\n maskFields?: string[]\n /** 脱敏占位符 @default '[REDACTED]' */\n placeholder?: string\n /** 最大递归深度 @default 10 */\n maxDepth?: number\n}\n\n// ============ Default Config ============\n\n/** 默认需要完全移除的敏感字段 */\nconst DEFAULT_REMOVE_FIELDS = [\n 'password',\n 'newpassword',\n 'oldpassword',\n 'confirmpassword',\n 'secret',\n 'secretkey',\n 'privatekey',\n 'apisecret',\n 'clientsecret',\n]\n\n/** 默认需要脱敏的字段(保留部分信息) */\nconst DEFAULT_MASK_FIELDS = [\n 'token',\n 'accesstoken',\n 'refreshtoken',\n 'authorization',\n 'apikey',\n 'api_key',\n 'x-api-key',\n 'idtoken',\n 'sessiontoken',\n 'bearer',\n]\n\nconst DEFAULT_PLACEHOLDER = '[REDACTED]'\nconst DEFAULT_MAX_DEPTH = 10\n\n// ============ Sanitize Functions ============\n\n/**\n * 部分脱敏(保留前4后4位)\n */\nfunction partialMask(value: string, placeholder: string): string {\n if (value.length <= 8) return placeholder\n return value.slice(0, 4) + '****' + value.slice(-4)\n}\n\n/**\n * 深度清洗对象中的敏感数据\n * \n * @example\n * ```typescript\n * const data = { password: '123456', token: 'eyJhbG...' }\n * const sanitized = sanitize(data)\n * // { password: '[REDACTED]', token: 'eyJh****...' }\n * ```\n */\nexport function sanitize<T>(data: T, config?: SanitizeConfig, depth = 0): T {\n const {\n removeFields = DEFAULT_REMOVE_FIELDS,\n maskFields = DEFAULT_MASK_FIELDS,\n placeholder = DEFAULT_PLACEHOLDER,\n maxDepth = DEFAULT_MAX_DEPTH,\n } = config ?? {}\n\n // 防止无限递归\n if (depth > maxDepth) return data\n \n if (data === null || data === undefined) {\n return data\n }\n\n // 处理数组\n if (Array.isArray(data)) {\n return data.map(item => sanitize(item, config, depth + 1)) as T\n }\n\n // 处理对象\n if (typeof data === 'object') {\n const result: Record<string, unknown> = {}\n \n for (const [key, value] of Object.entries(data)) {\n const lowerKey = key.toLowerCase()\n \n // 完全移除的字段\n if (removeFields.some(field => lowerKey === field)) {\n result[key] = placeholder\n continue\n }\n \n // 部分脱敏的字段\n if (maskFields.some(field => lowerKey.includes(field))) {\n if (typeof value === 'string') {\n result[key] = partialMask(value, placeholder)\n } else {\n result[key] = placeholder\n }\n continue\n }\n \n // 递归处理嵌套对象\n result[key] = sanitize(value, config, depth + 1)\n }\n \n return result as T\n }\n\n return data\n}\n\n/**\n * 清洗 HTTP 请求头\n * \n * @example\n * ```typescript\n * const headers = { Authorization: 'Bearer eyJhbG...', Cookie: 'session=xxx' }\n * const sanitized = sanitizeHeaders(headers)\n * // { Authorization: 'Bearer eyJh****...', Cookie: '[REDACTED]' }\n * ```\n */\nexport function sanitizeHeaders(\n headers: Record<string, string>,\n config?: SanitizeConfig\n): Record<string, string> {\n const {\n maskFields = DEFAULT_MASK_FIELDS,\n placeholder = DEFAULT_PLACEHOLDER,\n } = config ?? {}\n\n const result: Record<string, string> = {}\n \n for (const [key, value] of Object.entries(headers)) {\n const lowerKey = key.toLowerCase()\n \n // Authorization 头部分脱敏\n if (lowerKey === 'authorization') {\n if (value.startsWith('Bearer ')) {\n result[key] = 'Bearer ' + partialMask(value.slice(7), placeholder)\n } else {\n result[key] = partialMask(value, placeholder)\n }\n continue\n }\n \n // Cookie 完全脱敏\n if (lowerKey === 'cookie' || lowerKey === 'set-cookie') {\n result[key] = placeholder\n continue\n }\n \n // API Key 相关头脱敏\n if (maskFields.some(field => lowerKey.includes(field))) {\n result[key] = partialMask(value, placeholder)\n continue\n }\n \n result[key] = value\n }\n \n return result\n}\n\n/**\n * 检查值是否为敏感字段\n */\nexport function isSensitiveField(fieldName: string, config?: SanitizeConfig): boolean {\n const {\n removeFields = DEFAULT_REMOVE_FIELDS,\n maskFields = DEFAULT_MASK_FIELDS,\n } = config ?? {}\n\n const lowerName = fieldName.toLowerCase()\n \n return (\n removeFields.some(field => lowerName === field) ||\n maskFields.some(field => lowerName.includes(field))\n )\n}\n\n","/**\n * @vafast/request-logger - API 请求日志中间件\n *\n * 特性:\n * - 自动敏感数据脱敏\n * - HTTP 远程日志服务\n * - 异步非阻塞记录\n * - 路由级别日志控制(路由定义中设置 log: false)\n * - 熔断器:连续失败后暂停上报,避免无谓等待\n * - 错误节流:同类错误在一段时间内只打一次日志\n *\n * @example\n * ```typescript\n * import { requestLogger } from '@vafast/request-logger'\n *\n * server.use(requestLogger({\n * url: 'http://log-server:9005/api/logs/ingest',\n * service: 'auth-server',\n * getUserId: (req) => req.__locals?.userInfo?.id,\n * }))\n * ```\n */\nimport { defineMiddleware, getRoute } from 'vafast'\nimport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\n\n// ============ Types ============\n\n/** 请求信息 */\nexport interface RequestData {\n method: string\n url: string\n path: string\n headers: Record<string, string>\n body: unknown\n query: Record<string, string>\n status: number\n duration: number\n userId?: string\n appId?: string\n authType?: string\n service?: string\n ip?: string\n userAgent?: string\n traceId?: string\n createdAt: Date\n}\n\n/** 响应数据(完整响应体) */\nexport type ResponseData = unknown\n\n/** 完整日志数据 */\nexport interface LogData {\n request: RequestData\n response: ResponseData\n}\n\n/** 熔断器配置 */\nexport interface CircuitBreakerConfig {\n /** 触发熔断的连续失败次数,默认 5 */\n failureThreshold?: number\n /** 熔断恢复时间(毫秒),默认 60000(1分钟) */\n resetTimeout?: number\n}\n\n/** 错误节流配置 */\nexport interface ErrorThrottleConfig {\n /** 同类错误的节流间隔(毫秒),默认 60000(1分钟) */\n interval?: number\n}\n\n/** stdout 双写配置 */\nexport interface StdoutConfig {\n /** 是否启用 stdout 输出 @default false */\n enabled?: boolean\n /** 输出格式 @default 'json' */\n format?: 'json' | 'text'\n /** 是否包含响应体(可能很大)@default false */\n includeResponse?: boolean\n /** 是否包含请求体 @default false */\n includeBody?: boolean\n}\n\n/** 请求日志配置 */\nexport interface RequestLoggerOptions {\n /** 日志服务 URL */\n url: string\n /** 服务标识(如 auth-server、ones-server) */\n service: string\n /** 自定义请求头(如认证信息) */\n headers?: Record<string, string>\n /** 超时时间(毫秒),默认 5000 */\n timeout?: number\n /** 敏感数据清洗配置 */\n sanitize?: SanitizeConfig\n /** 错误回调 */\n onError?: (error: Error, context: { droppedCount: number }) => void\n /** 是否启用 @default true */\n enabled?: boolean\n /** 排除的路径列表(精确匹配或正则),这些路径不记录日志 */\n excludePaths?: (string | RegExp)[]\n /** 熔断器配置 */\n circuitBreaker?: CircuitBreakerConfig\n /** 错误节流配置 */\n errorThrottle?: ErrorThrottleConfig\n /** stdout 双写配置(用于 K8s 日志采集) */\n stdout?: StdoutConfig\n}\n\n// ============ Circuit Breaker ============\n\ntype CircuitState = 'closed' | 'open' | 'half-open'\n\nclass CircuitBreaker {\n private state: CircuitState = 'closed'\n private failureCount = 0\n private lastFailureTime = 0\n private readonly failureThreshold: number\n private readonly resetTimeout: number\n\n constructor(config: CircuitBreakerConfig = {}) {\n this.failureThreshold = config.failureThreshold ?? 5\n this.resetTimeout = config.resetTimeout ?? 60000\n }\n\n /** 检查是否允许请求 */\n canRequest(): boolean {\n if (this.state === 'closed') return true\n\n if (this.state === 'open') {\n // 检查是否到了恢复时间\n if (Date.now() - this.lastFailureTime >= this.resetTimeout) {\n this.state = 'half-open'\n return true\n }\n return false\n }\n\n // half-open 状态允许一个请求通过测试\n return true\n }\n\n /** 记录成功 */\n recordSuccess(): void {\n this.failureCount = 0\n this.state = 'closed'\n }\n\n /** 记录失败 */\n recordFailure(): void {\n this.failureCount++\n this.lastFailureTime = Date.now()\n\n if (this.failureCount >= this.failureThreshold) {\n this.state = 'open'\n }\n }\n\n /** 获取当前状态信息 */\n getStatus(): { state: CircuitState; failureCount: number } {\n return { state: this.state, failureCount: this.failureCount }\n }\n}\n\n// ============ Error Throttle ============\n\nclass ErrorThrottle {\n private lastErrorTime = 0\n private droppedCount = 0\n private readonly interval: number\n\n constructor(config: ErrorThrottleConfig = {}) {\n this.interval = config.interval ?? 60000\n }\n\n /** 检查是否应该打印错误,返回 { shouldLog, droppedCount } */\n shouldLog(): { shouldLog: boolean; droppedCount: number } {\n const now = Date.now()\n\n if (now - this.lastErrorTime >= this.interval) {\n const dropped = this.droppedCount\n this.lastErrorTime = now\n this.droppedCount = 0\n return { shouldLog: true, droppedCount: dropped }\n }\n\n this.droppedCount++\n return { shouldLog: false, droppedCount: 0 }\n }\n}\n\n// ============ Middleware ============\n\n/**\n * 请求日志中间件\n *\n * @example\n * ```typescript\n * import { requestLogger } from '@vafast/request-logger'\n *\n * server.use(requestLogger({\n * url: 'http://log-server:9005/api/logs/ingest',\n * service: 'auth-server',\n * auth: { apiKeyId: 'xxx', apiKeySecret: 'yyy' },\n * }))\n * ```\n */\nexport function requestLogger(options: RequestLoggerOptions) {\n const {\n url,\n service,\n headers = {},\n timeout = 5000,\n sanitize: sanitizeConfig,\n onError = console.error,\n enabled = true,\n excludePaths = [],\n circuitBreaker: circuitBreakerConfig,\n errorThrottle: errorThrottleConfig,\n stdout: stdoutConfig,\n } = options\n\n // 创建熔断器和错误节流器实例\n const circuitBreaker = new CircuitBreaker(circuitBreakerConfig)\n const errorThrottle = new ErrorThrottle(errorThrottleConfig)\n\n return defineMiddleware(async (req, next) => {\n if (!enabled) return next()\n\n const startTime = Date.now()\n const response = await next()\n\n // 异步记录日志,不阻塞响应\n recordLog(req, response, startTime, {\n url,\n service,\n headers,\n timeout,\n sanitizeConfig,\n onError,\n excludePaths,\n circuitBreaker,\n errorThrottle,\n stdoutConfig,\n }).catch(() => {\n // 错误已在 recordLog 内部处理,这里静默忽略\n })\n\n return response\n })\n}\n\n/** @deprecated 使用 requestLogger 代替 */\nexport const createRequestLogger = requestLogger\n\n// ============ Internal ============\n\ninterface RecordLogOptions {\n url: string\n service: string\n headers: Record<string, string>\n timeout: number\n sanitizeConfig?: SanitizeConfig\n onError: (error: Error, context: { droppedCount: number }) => void\n excludePaths: (string | RegExp)[]\n circuitBreaker: CircuitBreaker\n errorThrottle: ErrorThrottle\n stdoutConfig?: StdoutConfig\n}\n\n/** 检查路由是否配置了 log: false */\nfunction shouldSkipLog(method: string, path: string): boolean {\n try {\n const route = getRoute<{ log?: boolean }>(method, path)\n return route?.log === false\n } catch {\n return false\n }\n}\n\n/** 检查路径是否在排除列表中 */\nfunction isPathExcluded(\n path: string,\n excludePaths: (string | RegExp)[]\n): boolean {\n return excludePaths.some((pattern) => {\n if (typeof pattern === 'string') {\n return path === pattern || path.startsWith(pattern + '/')\n }\n return pattern.test(path)\n })\n}\n\n/** 带超时的 fetch */\nasync function fetchWithTimeout(\n targetUrl: string,\n options: RequestInit,\n timeout: number\n): Promise<Response> {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n return await fetch(targetUrl, { ...options, signal: controller.signal })\n } finally {\n clearTimeout(timeoutId)\n }\n}\n\nasync function recordLog(\n req: Request,\n response: Response,\n startTime: number,\n options: RecordLogOptions\n) {\n const {\n url: logUrl,\n service,\n headers: customHeaders,\n timeout,\n sanitizeConfig,\n onError,\n excludePaths,\n circuitBreaker,\n errorThrottle,\n stdoutConfig,\n } = options\n\n const reqUrl = new URL(req.url)\n const path = reqUrl.pathname\n\n // 检查路径是否在排除列表中\n if (isPathExcluded(path, excludePaths)) return\n\n // 检查路由是否禁用日志\n if (shouldSkipLog(req.method, path)) return\n\n // 解析请求体\n let body: unknown = null\n try {\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n body = await req.clone().json()\n }\n } catch {\n // 忽略\n }\n\n // 解析响应体\n let responseData: ResponseData = null\n try {\n responseData = await response.clone().json()\n } catch {\n // 忽略(非 JSON 响应)\n }\n\n // 提取请求头\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n // 清洗敏感数据\n const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig)\n const sanitizedBody = sanitize(body, sanitizeConfig)\n const sanitizedResponseData = sanitize(responseData, sanitizeConfig)\n\n const duration = Date.now() - startTime\n\n // 构建日志数据(业务字段由 log-server 从 headers 解析)\n const logBody = {\n method: req.method,\n url: req.url,\n path,\n headers: sanitizedHeaders,\n body: sanitizedBody,\n query: Object.fromEntries(reqUrl.searchParams),\n status: response.status,\n duration,\n service,\n createdAt: new Date().toISOString(),\n response: sanitizedResponseData, // 直接存储完整响应数据\n }\n\n // 双写:输出到 stdout(用于 K8s 日志采集)\n if (stdoutConfig?.enabled) {\n writeToStdout(logBody, stdoutConfig)\n }\n\n // 熔断器检查:如果熔断打开,直接跳过 HTTP 上报\n if (!circuitBreaker.canRequest()) {\n return\n }\n\n // 发送到日志服务\n try {\n const res = await fetchWithTimeout(\n logUrl,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(logBody),\n },\n timeout\n )\n\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}: ${res.statusText}`)\n }\n\n // 成功:重置熔断器\n circuitBreaker.recordSuccess()\n } catch (error) {\n // 失败:记录到熔断器\n circuitBreaker.recordFailure()\n\n // 错误节流:检查是否应该打印\n const { shouldLog, droppedCount } = errorThrottle.shouldLog()\n if (shouldLog) {\n onError(error as Error, { droppedCount })\n }\n }\n}\n\n/** 输出到 stdout(用于 K8s 日志采集) */\nfunction writeToStdout(\n logBody: Record<string, unknown>,\n config: StdoutConfig\n): void {\n const { format = 'json', includeResponse = false, includeBody = false } =\n config\n\n // 构建精简版日志(避免 stdout 日志过大)\n const stdoutLog: Record<string, unknown> = {\n level: 30, // INFO level (pino 格式)\n time: Date.now(),\n service: logBody.service,\n method: logBody.method,\n path: logBody.path,\n status: logBody.status,\n duration: logBody.duration,\n msg: `${logBody.method} ${logBody.path} ${logBody.status} ${logBody.duration}ms`,\n }\n\n // 可选:包含请求体\n if (includeBody && logBody.body) {\n stdoutLog.body = logBody.body\n }\n\n // 可选:包含响应体\n if (includeResponse && logBody.response) {\n stdoutLog.response = logBody.response\n }\n\n if (format === 'json') {\n console.log(JSON.stringify(stdoutLog))\n } else {\n // text 格式:更易读\n console.log(\n `[${new Date().toISOString()}] ${logBody.method} ${logBody.path} ${logBody.status} ${logBody.duration}ms`\n )\n }\n}\n\n// ============ Re-exports ============\n\nexport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\nexport default requestLogger\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vafast/request-logger",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "API request logging middleware for Vafast",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -32,15 +32,15 @@
32
32
  "README.md",
33
33
  "LICENSE"
34
34
  ],
35
- "dependencies": {},
36
35
  "devDependencies": {
37
36
  "@types/node": "^22.15.30",
38
37
  "rimraf": "^6.0.1",
39
38
  "tsdown": "^0.19.0-beta.4",
40
39
  "typescript": "^5.4.5",
41
- "vafast": "^0.5.1"
40
+ "vafast": "^0.5.1",
41
+ "vitest": "^4.0.18"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "vafast": "^0.5.1"
45
45
  }
46
- }
46
+ }