@vafast/request-logger 0.2.3 → 0.3.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 +75 -128
- package/dist/index.d.mts +34 -83
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +80 -121
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -12
package/README.md
CHANGED
|
@@ -1,169 +1,116 @@
|
|
|
1
1
|
# @vafast/request-logger
|
|
2
2
|
|
|
3
|
-
API
|
|
3
|
+
API 请求日志中间件,将日志提交到远程日志服务。
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- 📝 Automatic API request/response logging
|
|
8
|
-
- 🔒 Built-in sensitive data sanitization (passwords, tokens, etc.)
|
|
9
|
-
- 🔌 Pluggable storage adapters (MongoDB, Console, Custom)
|
|
10
|
-
- ⚡ Non-blocking async logging
|
|
11
|
-
- 🚫 Path exclusion support
|
|
12
|
-
- 📊 Request duration tracking
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
5
|
+
## 安装
|
|
15
6
|
|
|
16
7
|
```bash
|
|
17
8
|
npm install @vafast/request-logger
|
|
18
|
-
# or
|
|
19
|
-
npm install @vafast/request-logger
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Quick Start
|
|
23
|
-
|
|
24
|
-
### With MongoDB
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
import { Server } from 'vafast'
|
|
28
|
-
import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'
|
|
29
|
-
import { mongoDb } from './mongodb'
|
|
30
|
-
|
|
31
|
-
const server = new Server(routes)
|
|
32
|
-
|
|
33
|
-
// Create request logger middleware
|
|
34
|
-
const requestLogger = createRequestLogger({
|
|
35
|
-
storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),
|
|
36
|
-
excludePaths: ['/health', '/metrics', '/performance/add'],
|
|
37
|
-
getUserId: (req) => {
|
|
38
|
-
const locals = getLocals(req)
|
|
39
|
-
return locals?.userInfo?.id
|
|
40
|
-
},
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
server.use(requestLogger)
|
|
44
9
|
```
|
|
45
10
|
|
|
46
|
-
|
|
11
|
+
## 使用
|
|
47
12
|
|
|
48
13
|
```typescript
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
14
|
+
import { requestLogger } from '@vafast/request-logger'
|
|
15
|
+
|
|
16
|
+
server.use(requestLogger({
|
|
17
|
+
url: 'http://log-server:9005/api/logs/ingest',
|
|
18
|
+
service: 'my-service',
|
|
19
|
+
headers: { Authorization: 'Bearer apiKeyId:apiKeySecret' },
|
|
20
|
+
onError: (err) => console.error('日志记录失败', err),
|
|
21
|
+
}))
|
|
54
22
|
```
|
|
55
23
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
The middleware automatically sanitizes sensitive data before logging:
|
|
24
|
+
业务字段(appId、authType、ip、traceId 等)由日志服务端从 headers 自动解析。
|
|
59
25
|
|
|
60
|
-
|
|
61
|
-
|----------|-----------|
|
|
62
|
-
| `{ password: "abc123" }` | `{ password: "[REDACTED]" }` |
|
|
63
|
-
| `Authorization: "Bearer eyJhbG..."` | `Authorization: "Bearer eyJh****bG..."` |
|
|
64
|
-
| `Cookie: "session=xxx"` | `Cookie: "[REDACTED]"` |
|
|
65
|
-
| `{ apiKey: "sk-1234567890abcdef" }` | `{ apiKey: "sk-1****cdef" }` |
|
|
26
|
+
## 配置
|
|
66
27
|
|
|
67
|
-
|
|
28
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
29
|
+
|------|------|------|--------|------|
|
|
30
|
+
| `url` | `string` | 是 | - | 日志服务 URL |
|
|
31
|
+
| `service` | `string` | 是 | - | 服务标识 |
|
|
32
|
+
| `headers` | `Record<string, string>` | 否 | `{}` | 自定义请求头(如认证) |
|
|
33
|
+
| `timeout` | `number` | 否 | `5000` | 超时时间(毫秒) |
|
|
34
|
+
| `sanitize` | `SanitizeConfig` | 否 | - | 敏感数据清洗配置 |
|
|
35
|
+
| `onError` | `(err) => void` | 否 | `console.error` | 错误回调 |
|
|
36
|
+
| `enabled` | `boolean` | 否 | `true` | 是否启用 |
|
|
68
37
|
|
|
69
|
-
|
|
70
|
-
- `secret`, `secretKey`, `privateKey`
|
|
71
|
-
- `apiSecret`, `clientSecret`
|
|
38
|
+
## 路由级别控制
|
|
72
39
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- `token`, `accessToken`, `refreshToken`
|
|
76
|
-
- `authorization`, `apiKey`, `bearer`
|
|
77
|
-
|
|
78
|
-
### Custom Sanitization
|
|
40
|
+
在路由定义中设置 `log: false` 跳过日志记录:
|
|
79
41
|
|
|
80
42
|
```typescript
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
sanitize: {
|
|
84
|
-
removeFields: ['password', 'ssn', 'creditCard'],
|
|
85
|
-
maskFields: ['token', 'apiKey', 'phoneNumber'],
|
|
86
|
-
placeholder: '***HIDDEN***',
|
|
87
|
-
},
|
|
88
|
-
})
|
|
89
|
-
```
|
|
43
|
+
// 单个路由
|
|
44
|
+
{ method: 'GET', path: '/health', log: false, handler: ... }
|
|
90
45
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
/** Sanitization config */
|
|
100
|
-
sanitize?: SanitizeConfig
|
|
101
|
-
/** Function to get user ID from request */
|
|
102
|
-
getUserId?: (req: Request) => string | undefined
|
|
103
|
-
/** Error callback */
|
|
104
|
-
onError?: (error: Error) => void
|
|
105
|
-
/** Enable/disable logging @default true */
|
|
106
|
-
enabled?: boolean
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Custom Storage Adapter
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
import type { StorageAdapter } from '@vafast/request-logger'
|
|
114
|
-
|
|
115
|
-
const myAdapter: StorageAdapter = {
|
|
116
|
-
async saveRequestLog(log) {
|
|
117
|
-
// Save to your database
|
|
118
|
-
const id = await myDb.insert('request_logs', log)
|
|
119
|
-
return id
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
async saveResponseLog(log) {
|
|
123
|
-
// Save response details
|
|
124
|
-
await myDb.insert('response_logs', log)
|
|
125
|
-
},
|
|
46
|
+
// 父路由设置,子路由继承
|
|
47
|
+
{
|
|
48
|
+
path: '/internal',
|
|
49
|
+
log: false,
|
|
50
|
+
children: [
|
|
51
|
+
{ method: 'GET', path: '/metrics', handler: ... },
|
|
52
|
+
{ method: 'GET', path: '/status', handler: ... },
|
|
53
|
+
]
|
|
126
54
|
}
|
|
127
55
|
```
|
|
128
56
|
|
|
129
|
-
##
|
|
57
|
+
## 日志数据格式
|
|
130
58
|
|
|
131
|
-
|
|
59
|
+
发送到日志服务的数据结构:
|
|
132
60
|
|
|
133
61
|
```typescript
|
|
134
62
|
{
|
|
135
63
|
method: 'POST',
|
|
136
|
-
url: 'http://
|
|
64
|
+
url: 'http://example.com/api/users?page=1',
|
|
137
65
|
path: '/api/users',
|
|
138
|
-
headers: {
|
|
139
|
-
body: {
|
|
140
|
-
query: {},
|
|
66
|
+
headers: { ... },
|
|
67
|
+
body: { ... },
|
|
68
|
+
query: { page: '1' },
|
|
69
|
+
status: 200,
|
|
70
|
+
duration: 50,
|
|
71
|
+
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
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
141
79
|
response: {
|
|
142
80
|
success: true,
|
|
143
81
|
message: 'OK',
|
|
144
82
|
code: 0,
|
|
145
83
|
},
|
|
146
|
-
|
|
147
|
-
duration: 15,
|
|
148
|
-
userId: '123',
|
|
149
|
-
createdAt: Date,
|
|
84
|
+
responseData: { ... },
|
|
150
85
|
}
|
|
151
86
|
```
|
|
152
87
|
|
|
153
|
-
|
|
88
|
+
## 敏感数据脱敏
|
|
89
|
+
|
|
90
|
+
默认自动脱敏以下字段:
|
|
91
|
+
|
|
92
|
+
- `password`, `pwd`, `secret`, `token`
|
|
93
|
+
- `authorization`, `cookie`, `x-api-key`
|
|
94
|
+
- `accessToken`, `refreshToken`, `apiKey`
|
|
95
|
+
|
|
96
|
+
自定义脱敏配置:
|
|
154
97
|
|
|
155
98
|
```typescript
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
99
|
+
requestLogger({
|
|
100
|
+
url: '...',
|
|
101
|
+
service: '...',
|
|
102
|
+
sanitize: {
|
|
103
|
+
fields: ['password', 'creditCard', 'ssn'],
|
|
104
|
+
mask: '******',
|
|
105
|
+
deep: true,
|
|
106
|
+
},
|
|
107
|
+
})
|
|
164
108
|
```
|
|
165
109
|
|
|
166
|
-
##
|
|
167
|
-
|
|
168
|
-
MIT
|
|
110
|
+
## 特性
|
|
169
111
|
|
|
112
|
+
- 异步非阻塞(不影响响应速度)
|
|
113
|
+
- 自动敏感数据脱敏
|
|
114
|
+
- 路由级别日志控制
|
|
115
|
+
- 支持多租户(appId)
|
|
116
|
+
- 支持分布式追踪(traceId)
|
package/dist/index.d.mts
CHANGED
|
@@ -41,122 +41,73 @@ declare function sanitize<T>(data: T, config?: SanitizeConfig, depth?: number):
|
|
|
41
41
|
declare function sanitizeHeaders(headers: Record<string, string>, config?: SanitizeConfig): Record<string, string>;
|
|
42
42
|
//#endregion
|
|
43
43
|
//#region src/index.d.ts
|
|
44
|
-
|
|
44
|
+
/** 请求信息 */
|
|
45
|
+
interface RequestData {
|
|
45
46
|
method: string;
|
|
46
47
|
url: string;
|
|
47
48
|
path: string;
|
|
48
49
|
headers: Record<string, string>;
|
|
49
50
|
body: unknown;
|
|
50
51
|
query: Record<string, string>;
|
|
51
|
-
response: {
|
|
52
|
-
success?: boolean;
|
|
53
|
-
message?: string;
|
|
54
|
-
code?: number;
|
|
55
|
-
};
|
|
56
52
|
status: number;
|
|
57
53
|
duration: number;
|
|
58
54
|
userId?: string;
|
|
59
55
|
appId?: string;
|
|
60
|
-
/** 认证类型(由调用方定义,如 apiKey、jwt 等) */
|
|
61
56
|
authType?: string;
|
|
62
|
-
/** 服务标识(区分不同服务,如 auth-server、ones-server) */
|
|
63
57
|
service?: string;
|
|
58
|
+
ip?: string;
|
|
59
|
+
userAgent?: string;
|
|
60
|
+
traceId?: string;
|
|
64
61
|
createdAt: Date;
|
|
65
62
|
}
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
/** 响应信息 */
|
|
64
|
+
interface ResponseData {
|
|
68
65
|
success?: boolean;
|
|
69
66
|
message?: string;
|
|
70
67
|
code?: number;
|
|
71
68
|
data?: unknown;
|
|
72
|
-
createdAt: Date;
|
|
73
69
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
/** 存储请求日志 */
|
|
79
|
-
saveRequestLog(log: RequestLog): Promise<string>;
|
|
80
|
-
/** 存储响应详情 */
|
|
81
|
-
saveResponseLog(log: ResponseLog): Promise<void>;
|
|
70
|
+
/** 完整日志数据 */
|
|
71
|
+
interface LogData {
|
|
72
|
+
request: RequestData;
|
|
73
|
+
response: ResponseData;
|
|
82
74
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
/** 请求日志配置 */
|
|
76
|
+
interface RequestLoggerOptions {
|
|
77
|
+
/** 日志服务 URL */
|
|
78
|
+
url: string;
|
|
79
|
+
/** 服务标识(如 auth-server、ones-server) */
|
|
80
|
+
service: string;
|
|
81
|
+
/** 自定义请求头(如认证信息) */
|
|
82
|
+
headers?: Record<string, string>;
|
|
83
|
+
/** 超时时间(毫秒),默认 5000 */
|
|
84
|
+
timeout?: number;
|
|
86
85
|
/** 敏感数据清洗配置 */
|
|
87
86
|
sanitize?: SanitizeConfig;
|
|
88
|
-
/** 获取用户 ID 的函数 */
|
|
89
|
-
getUserId?: (req: Request) => string | undefined;
|
|
90
|
-
/** 获取应用 ID 的函数(用于多租户) */
|
|
91
|
-
getAppId?: (req: Request) => string | undefined;
|
|
92
|
-
/** 获取认证类型的函数(如 apiKey、jwt 等) */
|
|
93
|
-
getAuthType?: (req: Request) => string | undefined;
|
|
94
|
-
/** 服务标识(区分不同服务,如 auth-server、ones-server) */
|
|
95
|
-
service?: string;
|
|
96
87
|
/** 错误回调 */
|
|
97
88
|
onError?: (error: Error) => void;
|
|
98
89
|
/** 是否启用 @default true */
|
|
99
90
|
enabled?: boolean;
|
|
91
|
+
/** 排除的路径列表(精确匹配或正则),这些路径不记录日志 */
|
|
92
|
+
excludePaths?: (string | RegExp)[];
|
|
100
93
|
}
|
|
101
94
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* 日志排除:在路由定义中设置 log: false,支持嵌套继承(父路由设置会自动继承给子路由)
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* ```typescript
|
|
108
|
-
* import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'
|
|
109
|
-
* import { mongoDb } from './mongodb'
|
|
110
|
-
*
|
|
111
|
-
* const requestLogger = createRequestLogger({
|
|
112
|
-
* storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),
|
|
113
|
-
* getUserId: (req) => (req as any).__locals?.userInfo?.id,
|
|
114
|
-
* })
|
|
115
|
-
*
|
|
116
|
-
* server.use(requestLogger)
|
|
117
|
-
* ```
|
|
118
|
-
*
|
|
119
|
-
* 在路由定义中使用 log: false(支持嵌套继承):
|
|
120
|
-
* ```typescript
|
|
121
|
-
* // 单个路由
|
|
122
|
-
* { method: 'GET', path: '/health', log: false, handler: ... }
|
|
123
|
-
*
|
|
124
|
-
* // 父路由设置,所有子路由继承
|
|
125
|
-
* {
|
|
126
|
-
* path: '/logs',
|
|
127
|
-
* log: false, // 所有子路由都不记录日志
|
|
128
|
-
* children: [
|
|
129
|
-
* { method: 'POST', path: '/find', handler: ... },
|
|
130
|
-
* { method: 'POST', path: '/search', handler: ... },
|
|
131
|
-
* ]
|
|
132
|
-
* }
|
|
133
|
-
* ```
|
|
134
|
-
*/
|
|
135
|
-
declare function createRequestLogger(config: RequestLoggerConfig): vafast0.TypedMiddleware<object>;
|
|
136
|
-
/**
|
|
137
|
-
* 创建 MongoDB 存储适配器
|
|
95
|
+
* 请求日志中间件
|
|
138
96
|
*
|
|
139
97
|
* @example
|
|
140
98
|
* ```typescript
|
|
141
|
-
* import {
|
|
142
|
-
* import { createMongoAdapter } from '@vafast/request-logger'
|
|
99
|
+
* import { requestLogger } from '@vafast/request-logger'
|
|
143
100
|
*
|
|
144
|
-
*
|
|
101
|
+
* server.use(requestLogger({
|
|
102
|
+
* url: 'http://log-server:9005/api/logs/ingest',
|
|
103
|
+
* service: 'auth-server',
|
|
104
|
+
* auth: { apiKeyId: 'xxx', apiKeySecret: 'yyy' },
|
|
105
|
+
* }))
|
|
145
106
|
* ```
|
|
146
107
|
*/
|
|
147
|
-
declare function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
insertedId: {
|
|
151
|
-
toHexString: () => string;
|
|
152
|
-
};
|
|
153
|
-
}>;
|
|
154
|
-
};
|
|
155
|
-
}, logsCollection?: string, logsResponseCollection?: string): StorageAdapter;
|
|
156
|
-
/**
|
|
157
|
-
* 创建控制台存储适配器(用于开发调试)
|
|
158
|
-
*/
|
|
159
|
-
declare function createConsoleAdapter(): StorageAdapter;
|
|
108
|
+
declare function requestLogger(options: RequestLoggerOptions): vafast0.TypedMiddleware<object>;
|
|
109
|
+
/** @deprecated 使用 requestLogger 代替 */
|
|
110
|
+
declare const createRequestLogger: typeof requestLogger;
|
|
160
111
|
//#endregion
|
|
161
|
-
export {
|
|
112
|
+
export { LogData, RequestData, RequestLoggerOptions, ResponseData, type SanitizeConfig, createRequestLogger, requestLogger as default, requestLogger, sanitize, sanitizeHeaders };
|
|
162
113
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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;;;;
|
|
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;;;;AC/GT;;;;;AAoBA;AAQA;AAMA;AAMY,iBDKI,QCLJ,CAAA,CAAA,CAAA,CAAA,IAAA,EDKsB,CCLtB,EAAA,MAAA,CAAA,EDKkC,cCLlC,EAAA,KAAA,CAAA,EAAA,MAAA,CAAA,EDK8D,CCL9D;;;;;AA8BZ;AAkCA;;;;;iBDIgB,eAAA,UACL,iCACA,iBACR;;;;UC/Gc,WAAA;EDlBA,MAAA,EAAA,MAAA;EA+DD,GAAA,EAAA,MAAQ;EAAU,IAAA,EAAA,MAAA;EAAY,OAAA,ECzCnC,MDyCmC,CAAA,MAAA,EAAA,MAAA,CAAA;EAA4B,IAAA,EAAA,OAAA;EAAC,KAAA,ECvClE,MDuCkE,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;;;;EC/GQ,OAAA,CAAA,EAAA,MAAW;EAIjB,SAAA,EAYE,IAZF;;;AAYM,UAIA,YAAA,CAJA;EAIA,OAAA,CAAA,EAAA,OAAY;EAQZ,OAAA,CAAA,EAAO,MAAA;EAMP,IAAA,CAAA,EAAA,MAAA;EAML,IAAA,CAAA,EAAA,OAAA;;;AAUe,UAtBV,OAAA,CAsBU;EAAM,OAAA,EArBtB,WAqBsB;EAoBjB,QAAA,EAxCJ,YAwCiB;AAkC7B;;UAtEiB,oBAAA;;;;;;YAML;;;;aAIC;;oBAEO;;;;2BAIO;;;;;;;;;;;;;;;;iBAoBX,aAAA,UAAuB,uBAAoB,OAAA,CAAA;;cAkC9C,4BAAmB"}
|
package/dist/index.mjs
CHANGED
|
@@ -105,73 +105,60 @@ function sanitizeHeaders(headers, config) {
|
|
|
105
105
|
//#endregion
|
|
106
106
|
//#region src/index.ts
|
|
107
107
|
/**
|
|
108
|
-
* @vafast/request-logger - API
|
|
108
|
+
* @vafast/request-logger - API 请求日志中间件
|
|
109
109
|
*
|
|
110
|
-
*
|
|
111
|
-
* -
|
|
112
|
-
* -
|
|
113
|
-
* -
|
|
114
|
-
* -
|
|
115
|
-
*
|
|
116
|
-
* Uses vafast RouteRegistry to query route configurations at runtime,
|
|
117
|
-
* similar to @vafast/webhook implementation.
|
|
118
|
-
*/
|
|
119
|
-
/**
|
|
120
|
-
* 创建请求日志中间件
|
|
121
|
-
*
|
|
122
|
-
* 日志排除:在路由定义中设置 log: false,支持嵌套继承(父路由设置会自动继承给子路由)
|
|
110
|
+
* 特性:
|
|
111
|
+
* - 自动敏感数据脱敏
|
|
112
|
+
* - HTTP 远程日志服务
|
|
113
|
+
* - 异步非阻塞记录
|
|
114
|
+
* - 路由级别日志控制(路由定义中设置 log: false)
|
|
123
115
|
*
|
|
124
116
|
* @example
|
|
125
117
|
* ```typescript
|
|
126
|
-
* import {
|
|
127
|
-
* import { mongoDb } from './mongodb'
|
|
128
|
-
*
|
|
129
|
-
* const requestLogger = createRequestLogger({
|
|
130
|
-
* storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),
|
|
131
|
-
* getUserId: (req) => (req as any).__locals?.userInfo?.id,
|
|
132
|
-
* })
|
|
118
|
+
* import { requestLogger } from '@vafast/request-logger'
|
|
133
119
|
*
|
|
134
|
-
* server.use(requestLogger
|
|
120
|
+
* server.use(requestLogger({
|
|
121
|
+
* url: 'http://log-server:9005/api/logs/ingest',
|
|
122
|
+
* service: 'auth-server',
|
|
123
|
+
* getUserId: (req) => req.__locals?.userInfo?.id,
|
|
124
|
+
* }))
|
|
135
125
|
* ```
|
|
126
|
+
*/
|
|
127
|
+
/**
|
|
128
|
+
* 请求日志中间件
|
|
136
129
|
*
|
|
137
|
-
*
|
|
130
|
+
* @example
|
|
138
131
|
* ```typescript
|
|
139
|
-
*
|
|
140
|
-
* { method: 'GET', path: '/health', log: false, handler: ... }
|
|
132
|
+
* import { requestLogger } from '@vafast/request-logger'
|
|
141
133
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* { method: 'POST', path: '/find', handler: ... },
|
|
148
|
-
* { method: 'POST', path: '/search', handler: ... },
|
|
149
|
-
* ]
|
|
150
|
-
* }
|
|
134
|
+
* server.use(requestLogger({
|
|
135
|
+
* url: 'http://log-server:9005/api/logs/ingest',
|
|
136
|
+
* service: 'auth-server',
|
|
137
|
+
* auth: { apiKeyId: 'xxx', apiKeySecret: 'yyy' },
|
|
138
|
+
* }))
|
|
151
139
|
* ```
|
|
152
140
|
*/
|
|
153
|
-
function
|
|
154
|
-
const {
|
|
141
|
+
function requestLogger(options) {
|
|
142
|
+
const { url, service, headers = {}, timeout = 5e3, sanitize: sanitizeConfig, onError = console.error, enabled = true, excludePaths = [] } = options;
|
|
155
143
|
return defineMiddleware(async (req, next) => {
|
|
156
144
|
if (!enabled) return next();
|
|
157
145
|
const startTime = Date.now();
|
|
158
146
|
const response = await next();
|
|
159
147
|
recordLog(req, response, startTime, {
|
|
160
|
-
|
|
161
|
-
sanitizeConfig,
|
|
162
|
-
getUserId,
|
|
163
|
-
getAppId,
|
|
164
|
-
getAuthType,
|
|
148
|
+
url,
|
|
165
149
|
service,
|
|
166
|
-
|
|
150
|
+
headers,
|
|
151
|
+
timeout,
|
|
152
|
+
sanitizeConfig,
|
|
153
|
+
onError,
|
|
154
|
+
excludePaths
|
|
167
155
|
}).catch(onError);
|
|
168
156
|
return response;
|
|
169
157
|
});
|
|
170
158
|
}
|
|
171
|
-
/**
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*/
|
|
159
|
+
/** @deprecated 使用 requestLogger 代替 */
|
|
160
|
+
const createRequestLogger = requestLogger;
|
|
161
|
+
/** 检查路由是否配置了 log: false */
|
|
175
162
|
function shouldSkipLog(method, path) {
|
|
176
163
|
try {
|
|
177
164
|
return getRoute(method, path)?.log === false;
|
|
@@ -179,15 +166,35 @@ function shouldSkipLog(method, path) {
|
|
|
179
166
|
return false;
|
|
180
167
|
}
|
|
181
168
|
}
|
|
169
|
+
/** 检查路径是否在排除列表中 */
|
|
170
|
+
function isPathExcluded(path, excludePaths) {
|
|
171
|
+
return excludePaths.some((pattern) => {
|
|
172
|
+
if (typeof pattern === "string") return path === pattern || path.startsWith(pattern + "/");
|
|
173
|
+
return pattern.test(path);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** 带超时的 fetch */
|
|
177
|
+
async function fetchWithTimeout(targetUrl, options, timeout) {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
180
|
+
try {
|
|
181
|
+
return await fetch(targetUrl, {
|
|
182
|
+
...options,
|
|
183
|
+
signal: controller.signal
|
|
184
|
+
});
|
|
185
|
+
} finally {
|
|
186
|
+
clearTimeout(timeoutId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
182
189
|
async function recordLog(req, response, startTime, options) {
|
|
183
|
-
const {
|
|
184
|
-
const
|
|
185
|
-
const path =
|
|
190
|
+
const { url: logUrl, service, headers: customHeaders, timeout, sanitizeConfig, onError, excludePaths } = options;
|
|
191
|
+
const reqUrl = new URL(req.url);
|
|
192
|
+
const path = reqUrl.pathname;
|
|
193
|
+
if (isPathExcluded(path, excludePaths)) return;
|
|
186
194
|
if (shouldSkipLog(req.method, path)) return;
|
|
187
195
|
let body = null;
|
|
188
196
|
try {
|
|
189
|
-
|
|
190
|
-
if ((req.headers.get("content-type") || "").includes("application/json")) body = await clonedReq.json();
|
|
197
|
+
if ((req.headers.get("content-type") || "").includes("application/json")) body = await req.clone().json();
|
|
191
198
|
} catch {}
|
|
192
199
|
let responseData = {};
|
|
193
200
|
try {
|
|
@@ -200,88 +207,40 @@ async function recordLog(req, response, startTime, options) {
|
|
|
200
207
|
const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig);
|
|
201
208
|
const sanitizedBody = sanitize(body, sanitizeConfig);
|
|
202
209
|
const sanitizedResponseData = sanitize(responseData.data, sanitizeConfig);
|
|
203
|
-
const
|
|
204
|
-
const duration = Date.now() - startTime;
|
|
205
|
-
const userId = getUserId?.(req);
|
|
206
|
-
const appId = getAppId?.(req);
|
|
207
|
-
const authType = getAuthType?.(req);
|
|
208
|
-
const requestLogId = await storage.saveRequestLog({
|
|
210
|
+
const logBody = {
|
|
209
211
|
method: req.method,
|
|
210
212
|
url: req.url,
|
|
211
213
|
path,
|
|
212
214
|
headers: sanitizedHeaders,
|
|
213
215
|
body: sanitizedBody,
|
|
214
|
-
query: Object.fromEntries(
|
|
216
|
+
query: Object.fromEntries(reqUrl.searchParams),
|
|
217
|
+
status: response.status,
|
|
218
|
+
duration: Date.now() - startTime,
|
|
219
|
+
service,
|
|
220
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
215
221
|
response: {
|
|
216
222
|
success: responseData.success,
|
|
217
223
|
message: responseData.message,
|
|
218
224
|
code: responseData.code
|
|
219
225
|
},
|
|
220
|
-
|
|
221
|
-
duration,
|
|
222
|
-
userId,
|
|
223
|
-
appId,
|
|
224
|
-
authType,
|
|
225
|
-
service,
|
|
226
|
-
createdAt: now
|
|
227
|
-
});
|
|
228
|
-
await storage.saveResponseLog({
|
|
229
|
-
requestLogId,
|
|
230
|
-
success: responseData.success,
|
|
231
|
-
message: responseData.message,
|
|
232
|
-
code: responseData.code,
|
|
233
|
-
data: sanitizedResponseData,
|
|
234
|
-
createdAt: now
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* 创建 MongoDB 存储适配器
|
|
239
|
-
*
|
|
240
|
-
* @example
|
|
241
|
-
* ```typescript
|
|
242
|
-
* import { Db } from 'mongodb'
|
|
243
|
-
* import { createMongoAdapter } from '@vafast/request-logger'
|
|
244
|
-
*
|
|
245
|
-
* const adapter = createMongoAdapter(db, 'logs', 'logsResponse')
|
|
246
|
-
* ```
|
|
247
|
-
*/
|
|
248
|
-
function createMongoAdapter(db, logsCollection = "logs", logsResponseCollection = "logsResponse") {
|
|
249
|
-
return {
|
|
250
|
-
async saveRequestLog(log) {
|
|
251
|
-
return (await db.collection(logsCollection).insertOne({
|
|
252
|
-
...log,
|
|
253
|
-
createAt: log.createdAt,
|
|
254
|
-
updateAt: log.createdAt
|
|
255
|
-
})).insertedId.toHexString();
|
|
256
|
-
},
|
|
257
|
-
async saveResponseLog(log) {
|
|
258
|
-
await db.collection(logsResponseCollection).insertOne({
|
|
259
|
-
logsId: log.requestLogId,
|
|
260
|
-
...log,
|
|
261
|
-
createAt: log.createdAt,
|
|
262
|
-
updateAt: log.createdAt
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* 创建控制台存储适配器(用于开发调试)
|
|
269
|
-
*/
|
|
270
|
-
function createConsoleAdapter() {
|
|
271
|
-
let idCounter = 0;
|
|
272
|
-
return {
|
|
273
|
-
async saveRequestLog(log) {
|
|
274
|
-
const id = `log_${++idCounter}`;
|
|
275
|
-
console.log(`[REQUEST] ${log.method} ${log.path} ${log.status} ${log.duration}ms`);
|
|
276
|
-
return id;
|
|
277
|
-
},
|
|
278
|
-
async saveResponseLog(log) {
|
|
279
|
-
if (!log.success) console.log(`[RESPONSE ERROR] ${log.message}`);
|
|
280
|
-
}
|
|
226
|
+
responseData: sanitizedResponseData
|
|
281
227
|
};
|
|
228
|
+
try {
|
|
229
|
+
const res = await fetchWithTimeout(logUrl, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: {
|
|
232
|
+
"Content-Type": "application/json",
|
|
233
|
+
...customHeaders
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify(logBody)
|
|
236
|
+
}, timeout);
|
|
237
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
onError(error);
|
|
240
|
+
}
|
|
282
241
|
}
|
|
283
|
-
var src_default =
|
|
242
|
+
var src_default = requestLogger;
|
|
284
243
|
|
|
285
244
|
//#endregion
|
|
286
|
-
export {
|
|
245
|
+
export { createRequestLogger, src_default as default, requestLogger, sanitize, sanitizeHeaders };
|
|
287
246
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -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 request logging middleware for Vafast\n * \n * Features:\n * - Automatic sensitive data sanitization\n * - Pluggable storage adapters (MongoDB, custom)\n * - Async logging (non-blocking)\n * - Route-level log control (log: false in route definition, supports inheritance)\n * \n * Uses vafast RouteRegistry to query route configurations at runtime,\n * similar to @vafast/webhook implementation.\n */\nimport { defineMiddleware } from 'vafast'\nimport { getRoute } from 'vafast'\nimport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\n\n// ============ Types ============\n\nexport interface RequestLog {\n method: string\n url: string\n path: string\n headers: Record<string, string>\n body: unknown\n query: Record<string, string>\n response: {\n success?: boolean\n message?: string\n code?: number\n }\n status: number\n duration: number\n userId?: string\n appId?: string\n /** 认证类型(由调用方定义,如 apiKey、jwt 等) */\n authType?: string\n /** 服务标识(区分不同服务,如 auth-server、ones-server) */\n service?: string\n createdAt: Date\n}\n\nexport interface ResponseLog {\n requestLogId: string\n success?: boolean\n message?: string\n code?: number\n data?: unknown\n createdAt: Date\n}\n\n/**\n * 存储适配器接口\n */\nexport interface StorageAdapter {\n /** 存储请求日志 */\n saveRequestLog(log: RequestLog): Promise<string>\n /** 存储响应详情 */\n saveResponseLog(log: ResponseLog): Promise<void>\n}\n\nexport interface RequestLoggerConfig {\n /** 存储适配器 */\n storage: StorageAdapter\n /** 敏感数据清洗配置 */\n sanitize?: SanitizeConfig\n /** 获取用户 ID 的函数 */\n getUserId?: (req: Request) => string | undefined\n /** 获取应用 ID 的函数(用于多租户) */\n getAppId?: (req: Request) => string | undefined\n /** 获取认证类型的函数(如 apiKey、jwt 等) */\n getAuthType?: (req: Request) => string | undefined\n /** 服务标识(区分不同服务,如 auth-server、ones-server) */\n service?: string\n /** 错误回调 */\n onError?: (error: Error) => void\n /** 是否启用 @default true */\n enabled?: boolean\n}\n\n// ============ Middleware Factory ============\n\n/**\n * 创建请求日志中间件\n * \n * 日志排除:在路由定义中设置 log: false,支持嵌套继承(父路由设置会自动继承给子路由)\n * \n * @example\n * ```typescript\n * import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'\n * import { mongoDb } from './mongodb'\n * \n * const requestLogger = createRequestLogger({\n * storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),\n * getUserId: (req) => (req as any).__locals?.userInfo?.id,\n * })\n * \n * server.use(requestLogger)\n * ```\n * \n * 在路由定义中使用 log: false(支持嵌套继承):\n * ```typescript\n * // 单个路由\n * { method: 'GET', path: '/health', log: false, handler: ... }\n * \n * // 父路由设置,所有子路由继承\n * {\n * path: '/logs',\n * log: false, // 所有子路由都不记录日志\n * children: [\n * { method: 'POST', path: '/find', handler: ... },\n * { method: 'POST', path: '/search', handler: ... },\n * ]\n * }\n * ```\n */\nexport function createRequestLogger(config: RequestLoggerConfig) {\n const {\n storage,\n sanitize: sanitizeConfig,\n getUserId,\n getAppId,\n getAuthType,\n service,\n onError = console.error,\n enabled = true,\n } = config\n\n return defineMiddleware(async (req, next) => {\n if (!enabled) {\n return next()\n }\n\n const startTime = Date.now()\n const response = await next()\n\n // 异步记录日志,不阻塞响应\n recordLog(req, response, startTime, {\n storage,\n sanitizeConfig,\n getUserId,\n getAppId,\n getAuthType,\n service,\n onError,\n }).catch(onError)\n\n return response\n })\n}\n\n// ============ Internal Functions ============\n\ninterface RecordLogOptions {\n storage: StorageAdapter\n sanitizeConfig?: SanitizeConfig\n getUserId?: (req: Request) => string | undefined\n getAppId?: (req: Request) => string | undefined\n getAuthType?: (req: Request) => string | undefined\n service?: string\n onError: (error: Error) => void\n}\n\n/**\n * 检查路由是否配置了 log: false\n * 使用 vafast RouteRegistry 查询路由配置(支持嵌套继承)\n */\nfunction shouldSkipLog(method: string, path: string): boolean {\n try {\n // RouteRegistry 使用完整路径 (fullPath) 作为 key,直接查询\n const route = getRoute<{ log?: boolean }>(method, path)\n return route?.log === false\n } catch {\n // RouteRegistry 未初始化时忽略错误\n return false\n }\n}\n\nasync function recordLog(\n req: Request,\n response: Response,\n startTime: number,\n options: RecordLogOptions\n) {\n const { storage, sanitizeConfig, getUserId, getAppId, getAuthType, service } = options\n\n const url = new URL(req.url)\n const path = url.pathname\n\n // 检查路由定义中的 log: false(通过 RouteRegistry 查询,支持嵌套继承)\n if (shouldSkipLog(req.method, path)) {\n return\n }\n\n // 解析请求体\n let body: unknown = null\n try {\n const clonedReq = req.clone()\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n body = await clonedReq.json()\n }\n } catch {\n // 忽略解析错误\n }\n\n // 解析响应体\n let responseData: { success?: boolean; message?: string; code?: number; data?: unknown } = {}\n try {\n const clonedRes = response.clone()\n responseData = await clonedRes.json()\n } catch {\n // 忽略解析错误\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.data, sanitizeConfig)\n\n const now = new Date()\n const duration = Date.now() - startTime\n\n // 获取用户 ID、应用 ID 和认证类型\n const userId = getUserId?.(req)\n const appId = getAppId?.(req)\n const authType = getAuthType?.(req)\n\n // 存储请求日志\n const requestLogId = await storage.saveRequestLog({\n method: req.method,\n url: req.url,\n path,\n headers: sanitizedHeaders,\n body: sanitizedBody,\n query: Object.fromEntries(url.searchParams),\n response: {\n success: responseData.success,\n message: responseData.message,\n code: responseData.code,\n },\n status: response.status,\n duration,\n userId,\n appId,\n authType,\n service,\n createdAt: now,\n })\n\n // 存储响应详情\n await storage.saveResponseLog({\n requestLogId,\n success: responseData.success,\n message: responseData.message,\n code: responseData.code,\n data: sanitizedResponseData,\n createdAt: now,\n })\n}\n\n// ============ MongoDB Adapter ============\n\n/**\n * 创建 MongoDB 存储适配器\n * \n * @example\n * ```typescript\n * import { Db } from 'mongodb'\n * import { createMongoAdapter } from '@vafast/request-logger'\n * \n * const adapter = createMongoAdapter(db, 'logs', 'logsResponse')\n * ```\n */\nexport function createMongoAdapter(\n db: { collection: (name: string) => { insertOne: (doc: any) => Promise<{ insertedId: { toHexString: () => string } }> } },\n logsCollection: string = 'logs',\n logsResponseCollection: string = 'logsResponse'\n): StorageAdapter {\n return {\n async saveRequestLog(log: RequestLog): Promise<string> {\n const result = await db.collection(logsCollection).insertOne({\n ...log,\n createAt: log.createdAt,\n updateAt: log.createdAt,\n })\n return result.insertedId.toHexString()\n },\n\n async saveResponseLog(log: ResponseLog): Promise<void> {\n await db.collection(logsResponseCollection).insertOne({\n logsId: log.requestLogId,\n ...log,\n createAt: log.createdAt,\n updateAt: log.createdAt,\n })\n },\n }\n}\n\n// ============ Console Adapter (for development) ============\n\n/**\n * 创建控制台存储适配器(用于开发调试)\n */\nexport function createConsoleAdapter(): StorageAdapter {\n let idCounter = 0\n\n return {\n async saveRequestLog(log: RequestLog): Promise<string> {\n const id = `log_${++idCounter}`\n console.log(`[REQUEST] ${log.method} ${log.path} ${log.status} ${log.duration}ms`)\n return id\n },\n\n async saveResponseLog(log: ResponseLog): Promise<void> {\n if (!log.success) {\n console.log(`[RESPONSE ERROR] ${log.message}`)\n }\n },\n }\n}\n\n// ============ Re-exports ============\n\nexport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\nexport default createRequestLogger\n"],"mappings":"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"}
|
|
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 * @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 interface ResponseData {\n success?: boolean\n message?: string\n code?: number\n data?: unknown\n}\n\n/** 完整日志数据 */\nexport interface LogData {\n request: RequestData\n response: ResponseData\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) => void\n /** 是否启用 @default true */\n enabled?: boolean\n /** 排除的路径列表(精确匹配或正则),这些路径不记录日志 */\n excludePaths?: (string | RegExp)[]\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 } = options\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 }).catch(onError)\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) => void\n excludePaths: (string | RegExp)[]\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(path: string, excludePaths: (string | RegExp)[]): 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 { url: logUrl, service, headers: customHeaders, timeout, sanitizeConfig, onError, excludePaths } = 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 = {}\n try {\n responseData = await response.clone().json()\n } catch {\n // 忽略\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.data, 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: {\n success: responseData.success,\n message: responseData.message,\n code: responseData.code,\n },\n responseData: 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 } catch (error) {\n onError(error as Error)\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC7ET,SAAgB,cAAc,SAA+B;CAC3D,MAAM,EACJ,KACA,SACA,UAAU,EAAE,EACZ,UAAU,KACV,UAAU,gBACV,UAAU,QAAQ,OAClB,UAAU,MACV,eAAe,EAAE,KACf;AAEJ,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;GACD,CAAC,CAAC,MAAM,QAAQ;AAEjB,SAAO;GACP;;;AAIJ,MAAa,sBAAsB;;AAenC,SAAS,cAAc,QAAgB,MAAuB;AAC5D,KAAI;AAEF,SADc,SAA4B,QAAQ,KAAK,EACzC,QAAQ;SAChB;AACN,SAAO;;;;AAKX,SAAS,eAAe,MAAc,cAA4C;AAChF,QAAO,aAAa,MAAK,YAAW;AAClC,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,EAAE,KAAK,QAAQ,SAAS,SAAS,eAAe,SAAS,gBAAgB,SAAS,iBAAiB;CAEzG,MAAM,SAAS,IAAI,IAAI,IAAI,IAAI;CAC/B,MAAM,OAAO,OAAO;AAGpB,KAAI,eAAe,MAAM,aAAa,CAAE;AAGxC,KAAI,cAAc,IAAI,QAAQ,KAAK,CAAE;CAGrC,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,EAAE;AACnC,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,aAAa,MAAM,eAAe;CAGzE,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;GACR,SAAS,aAAa;GACtB,SAAS,aAAa;GACtB,MAAM,aAAa;GACpB;EACD,cAAc;EACf;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;UAEnD,OAAO;AACd,UAAQ,MAAe;;;AAO3B,kBAAe"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vafast/request-logger",
|
|
3
|
-
"version": "0.2
|
|
4
|
-
"description": "API request logging middleware for Vafast
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "API request logging middleware for Vafast",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
7
7
|
"module": "./dist/index.mjs",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"request-logger",
|
|
19
19
|
"api-logs",
|
|
20
20
|
"middleware",
|
|
21
|
-
"mongodb",
|
|
22
21
|
"typescript"
|
|
23
22
|
],
|
|
24
23
|
"scripts": {
|
|
@@ -36,19 +35,12 @@
|
|
|
36
35
|
"dependencies": {},
|
|
37
36
|
"devDependencies": {
|
|
38
37
|
"@types/node": "^22.15.30",
|
|
39
|
-
"mongodb": "^6.16.0",
|
|
40
38
|
"rimraf": "^6.0.1",
|
|
41
39
|
"tsdown": "^0.19.0-beta.4",
|
|
42
40
|
"typescript": "^5.4.5",
|
|
43
41
|
"vafast": "^0.5.1"
|
|
44
42
|
},
|
|
45
43
|
"peerDependencies": {
|
|
46
|
-
"vafast": "^0.5.1"
|
|
47
|
-
"mongodb": ">= 6.0.0"
|
|
48
|
-
},
|
|
49
|
-
"peerDependenciesMeta": {
|
|
50
|
-
"mongodb": {
|
|
51
|
-
"optional": true
|
|
52
|
-
}
|
|
44
|
+
"vafast": "^0.5.1"
|
|
53
45
|
}
|
|
54
|
-
}
|
|
46
|
+
}
|