@vafast/request-logger 0.1.0

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 ADDED
@@ -0,0 +1,169 @@
1
+ # @vafast/request-logger
2
+
3
+ API request logging middleware for Vafast with automatic sensitive data sanitization.
4
+
5
+ ## Features
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
15
+
16
+ ```bash
17
+ npm install @vafast/request-logger
18
+ # or
19
+ bun add @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
+ ```
45
+
46
+ ### Development (Console)
47
+
48
+ ```typescript
49
+ import { createRequestLogger, createConsoleAdapter } from '@vafast/request-logger'
50
+
51
+ const requestLogger = createRequestLogger({
52
+ storage: createConsoleAdapter(),
53
+ })
54
+ ```
55
+
56
+ ## Sensitive Data Sanitization
57
+
58
+ The middleware automatically sanitizes sensitive data before logging:
59
+
60
+ | Original | Sanitized |
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" }` |
66
+
67
+ ### Default Removed Fields
68
+
69
+ - `password`, `newPassword`, `oldPassword`
70
+ - `secret`, `secretKey`, `privateKey`
71
+ - `apiSecret`, `clientSecret`
72
+
73
+ ### Default Masked Fields (partial)
74
+
75
+ - `token`, `accessToken`, `refreshToken`
76
+ - `authorization`, `apiKey`, `bearer`
77
+
78
+ ### Custom Sanitization
79
+
80
+ ```typescript
81
+ const requestLogger = createRequestLogger({
82
+ storage: adapter,
83
+ sanitize: {
84
+ removeFields: ['password', 'ssn', 'creditCard'],
85
+ maskFields: ['token', 'apiKey', 'phoneNumber'],
86
+ placeholder: '***HIDDEN***',
87
+ },
88
+ })
89
+ ```
90
+
91
+ ## Configuration
92
+
93
+ ```typescript
94
+ interface RequestLoggerConfig {
95
+ /** Storage adapter (required) */
96
+ storage: StorageAdapter
97
+ /** Paths to exclude from logging */
98
+ excludePaths?: (string | RegExp)[]
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
+ },
126
+ }
127
+ ```
128
+
129
+ ## Log Structure
130
+
131
+ ### Request Log
132
+
133
+ ```typescript
134
+ {
135
+ method: 'POST',
136
+ url: 'http://localhost:3000/api/users',
137
+ path: '/api/users',
138
+ headers: { /* sanitized */ },
139
+ body: { /* sanitized */ },
140
+ query: {},
141
+ response: {
142
+ success: true,
143
+ message: 'OK',
144
+ code: 0,
145
+ },
146
+ status: 200,
147
+ duration: 15,
148
+ userId: '123',
149
+ createdAt: Date,
150
+ }
151
+ ```
152
+
153
+ ### Response Log
154
+
155
+ ```typescript
156
+ {
157
+ requestLogId: 'abc123',
158
+ success: true,
159
+ message: 'OK',
160
+ code: 0,
161
+ data: { /* sanitized response data */ },
162
+ createdAt: Date,
163
+ }
164
+ ```
165
+
166
+ ## License
167
+
168
+ MIT
169
+
@@ -0,0 +1,142 @@
1
+ import { Middleware } from 'vafast';
2
+
3
+ /**
4
+ * 敏感数据清洗工具
5
+ *
6
+ * 用于在记录日志前移除或脱敏敏感信息
7
+ */
8
+ interface SanitizeConfig {
9
+ /** 需要完全移除的字段(小写) */
10
+ removeFields?: string[];
11
+ /** 需要脱敏的字段(小写,部分匹配) */
12
+ maskFields?: string[];
13
+ /** 脱敏占位符 @default '[REDACTED]' */
14
+ placeholder?: string;
15
+ /** 最大递归深度 @default 10 */
16
+ maxDepth?: number;
17
+ }
18
+ /**
19
+ * 深度清洗对象中的敏感数据
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const data = { password: '123456', token: 'eyJhbG...' }
24
+ * const sanitized = sanitize(data)
25
+ * // { password: '[REDACTED]', token: 'eyJh****...' }
26
+ * ```
27
+ */
28
+ declare function sanitize<T>(data: T, config?: SanitizeConfig, depth?: number): T;
29
+ /**
30
+ * 清洗 HTTP 请求头
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const headers = { Authorization: 'Bearer eyJhbG...', Cookie: 'session=xxx' }
35
+ * const sanitized = sanitizeHeaders(headers)
36
+ * // { Authorization: 'Bearer eyJh****...', Cookie: '[REDACTED]' }
37
+ * ```
38
+ */
39
+ declare function sanitizeHeaders(headers: Record<string, string>, config?: SanitizeConfig): Record<string, string>;
40
+
41
+ /**
42
+ * @vafast/request-logger - API request logging middleware for Vafast
43
+ *
44
+ * Features:
45
+ * - Automatic sensitive data sanitization
46
+ * - Pluggable storage adapters (MongoDB, custom)
47
+ * - Async logging (non-blocking)
48
+ * - Path exclusion support
49
+ */
50
+
51
+ interface RequestLog {
52
+ method: string;
53
+ url: string;
54
+ path: string;
55
+ headers: Record<string, string>;
56
+ body: unknown;
57
+ query: Record<string, string>;
58
+ response: {
59
+ success?: boolean;
60
+ message?: string;
61
+ code?: number;
62
+ };
63
+ status: number;
64
+ duration: number;
65
+ userId?: string;
66
+ createdAt: Date;
67
+ }
68
+ interface ResponseLog {
69
+ requestLogId: string;
70
+ success?: boolean;
71
+ message?: string;
72
+ code?: number;
73
+ data?: unknown;
74
+ createdAt: Date;
75
+ }
76
+ /**
77
+ * 存储适配器接口
78
+ */
79
+ interface StorageAdapter {
80
+ /** 存储请求日志 */
81
+ saveRequestLog(log: RequestLog): Promise<string>;
82
+ /** 存储响应详情 */
83
+ saveResponseLog(log: ResponseLog): Promise<void>;
84
+ }
85
+ interface RequestLoggerConfig {
86
+ /** 存储适配器 */
87
+ storage: StorageAdapter;
88
+ /** 排除的路径(支持字符串或正则) */
89
+ excludePaths?: (string | RegExp)[];
90
+ /** 敏感数据清洗配置 */
91
+ sanitize?: SanitizeConfig;
92
+ /** 获取用户 ID 的函数 */
93
+ getUserId?: (req: Request) => string | undefined;
94
+ /** 错误回调 */
95
+ onError?: (error: Error) => void;
96
+ /** 是否启用 @default true */
97
+ enabled?: boolean;
98
+ }
99
+ /**
100
+ * 创建请求日志中间件
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'
105
+ * import { mongoDb } from './mongodb'
106
+ *
107
+ * const requestLogger = createRequestLogger({
108
+ * storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),
109
+ * excludePaths: ['/health', '/metrics'],
110
+ * getUserId: (req) => getLocals(req)?.userInfo?.id,
111
+ * })
112
+ *
113
+ * server.use(requestLogger)
114
+ * ```
115
+ */
116
+ declare function createRequestLogger(config: RequestLoggerConfig): Middleware;
117
+ /**
118
+ * 创建 MongoDB 存储适配器
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * import { Db } from 'mongodb'
123
+ * import { createMongoAdapter } from '@vafast/request-logger'
124
+ *
125
+ * const adapter = createMongoAdapter(db, 'logs', 'logsResponse')
126
+ * ```
127
+ */
128
+ declare function createMongoAdapter(db: {
129
+ collection: (name: string) => {
130
+ insertOne: (doc: any) => Promise<{
131
+ insertedId: {
132
+ toHexString: () => string;
133
+ };
134
+ }>;
135
+ };
136
+ }, logsCollection?: string, logsResponseCollection?: string): StorageAdapter;
137
+ /**
138
+ * 创建控制台存储适配器(用于开发调试)
139
+ */
140
+ declare function createConsoleAdapter(): StorageAdapter;
141
+
142
+ export { type RequestLog, type RequestLoggerConfig, type ResponseLog, type SanitizeConfig, type StorageAdapter, createConsoleAdapter, createMongoAdapter, createRequestLogger, createRequestLogger as default, sanitize, sanitizeHeaders };
package/dist/index.js ADDED
@@ -0,0 +1,230 @@
1
+ // src/sanitize.ts
2
+ var DEFAULT_REMOVE_FIELDS = [
3
+ "password",
4
+ "newpassword",
5
+ "oldpassword",
6
+ "confirmpassword",
7
+ "secret",
8
+ "secretkey",
9
+ "privatekey",
10
+ "apisecret",
11
+ "clientsecret"
12
+ ];
13
+ var DEFAULT_MASK_FIELDS = [
14
+ "token",
15
+ "accesstoken",
16
+ "refreshtoken",
17
+ "authorization",
18
+ "apikey",
19
+ "api_key",
20
+ "x-api-key",
21
+ "idtoken",
22
+ "sessiontoken",
23
+ "bearer"
24
+ ];
25
+ var DEFAULT_PLACEHOLDER = "[REDACTED]";
26
+ var DEFAULT_MAX_DEPTH = 10;
27
+ function partialMask(value, placeholder) {
28
+ if (value.length <= 8) return placeholder;
29
+ return value.slice(0, 4) + "****" + value.slice(-4);
30
+ }
31
+ function sanitize(data, config, depth = 0) {
32
+ const {
33
+ removeFields = DEFAULT_REMOVE_FIELDS,
34
+ maskFields = DEFAULT_MASK_FIELDS,
35
+ placeholder = DEFAULT_PLACEHOLDER,
36
+ maxDepth = DEFAULT_MAX_DEPTH
37
+ } = config ?? {};
38
+ if (depth > maxDepth) return data;
39
+ if (data === null || data === void 0) {
40
+ return data;
41
+ }
42
+ if (Array.isArray(data)) {
43
+ return data.map((item) => sanitize(item, config, depth + 1));
44
+ }
45
+ if (typeof data === "object") {
46
+ const result = {};
47
+ for (const [key, value] of Object.entries(data)) {
48
+ const lowerKey = key.toLowerCase();
49
+ if (removeFields.some((field) => lowerKey === field)) {
50
+ result[key] = placeholder;
51
+ continue;
52
+ }
53
+ if (maskFields.some((field) => lowerKey.includes(field))) {
54
+ if (typeof value === "string") {
55
+ result[key] = partialMask(value, placeholder);
56
+ } else {
57
+ result[key] = placeholder;
58
+ }
59
+ continue;
60
+ }
61
+ result[key] = sanitize(value, config, depth + 1);
62
+ }
63
+ return result;
64
+ }
65
+ return data;
66
+ }
67
+ function sanitizeHeaders(headers, config) {
68
+ const {
69
+ maskFields = DEFAULT_MASK_FIELDS,
70
+ placeholder = DEFAULT_PLACEHOLDER
71
+ } = config ?? {};
72
+ const result = {};
73
+ for (const [key, value] of Object.entries(headers)) {
74
+ const lowerKey = key.toLowerCase();
75
+ if (lowerKey === "authorization") {
76
+ if (value.startsWith("Bearer ")) {
77
+ result[key] = "Bearer " + partialMask(value.slice(7), placeholder);
78
+ } else {
79
+ result[key] = partialMask(value, placeholder);
80
+ }
81
+ continue;
82
+ }
83
+ if (lowerKey === "cookie" || lowerKey === "set-cookie") {
84
+ result[key] = placeholder;
85
+ continue;
86
+ }
87
+ if (maskFields.some((field) => lowerKey.includes(field))) {
88
+ result[key] = partialMask(value, placeholder);
89
+ continue;
90
+ }
91
+ result[key] = value;
92
+ }
93
+ return result;
94
+ }
95
+
96
+ // src/index.ts
97
+ function createRequestLogger(config) {
98
+ const {
99
+ storage,
100
+ excludePaths = [],
101
+ sanitize: sanitizeConfig,
102
+ getUserId,
103
+ onError = console.error,
104
+ enabled = true
105
+ } = config;
106
+ return async (req, next) => {
107
+ if (!enabled) {
108
+ return next();
109
+ }
110
+ const startTime = Date.now();
111
+ const response = await next();
112
+ recordLog(req, response, startTime, {
113
+ storage,
114
+ excludePaths,
115
+ sanitizeConfig,
116
+ getUserId,
117
+ onError
118
+ }).catch(onError);
119
+ return response;
120
+ };
121
+ }
122
+ async function recordLog(req, response, startTime, options) {
123
+ const { storage, excludePaths, sanitizeConfig, getUserId } = options;
124
+ const url = new URL(req.url);
125
+ const path = url.pathname;
126
+ const shouldExclude = excludePaths.some((pattern) => {
127
+ if (typeof pattern === "string") {
128
+ return path.includes(pattern);
129
+ }
130
+ return pattern.test(path);
131
+ });
132
+ if (shouldExclude) {
133
+ return;
134
+ }
135
+ let body = null;
136
+ try {
137
+ const clonedReq = req.clone();
138
+ const contentType = req.headers.get("content-type") || "";
139
+ if (contentType.includes("application/json")) {
140
+ body = await clonedReq.json();
141
+ }
142
+ } catch {
143
+ }
144
+ let responseData = {};
145
+ try {
146
+ const clonedRes = response.clone();
147
+ responseData = await clonedRes.json();
148
+ } catch {
149
+ }
150
+ const headers = {};
151
+ req.headers.forEach((value, key) => {
152
+ headers[key] = value;
153
+ });
154
+ const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig);
155
+ const sanitizedBody = sanitize(body, sanitizeConfig);
156
+ const sanitizedResponseData = sanitize(responseData.data, sanitizeConfig);
157
+ const now = /* @__PURE__ */ new Date();
158
+ const duration = Date.now() - startTime;
159
+ const userId = getUserId?.(req);
160
+ const requestLogId = await storage.saveRequestLog({
161
+ method: req.method,
162
+ url: req.url,
163
+ path,
164
+ headers: sanitizedHeaders,
165
+ body: sanitizedBody,
166
+ query: Object.fromEntries(url.searchParams),
167
+ response: {
168
+ success: responseData.success,
169
+ message: responseData.message,
170
+ code: responseData.code
171
+ },
172
+ status: response.status,
173
+ duration,
174
+ userId,
175
+ createdAt: now
176
+ });
177
+ await storage.saveResponseLog({
178
+ requestLogId,
179
+ success: responseData.success,
180
+ message: responseData.message,
181
+ code: responseData.code,
182
+ data: sanitizedResponseData,
183
+ createdAt: now
184
+ });
185
+ }
186
+ function createMongoAdapter(db, logsCollection = "logs", logsResponseCollection = "logsResponse") {
187
+ return {
188
+ async saveRequestLog(log) {
189
+ const result = await db.collection(logsCollection).insertOne({
190
+ ...log,
191
+ createAt: log.createdAt,
192
+ updateAt: log.createdAt
193
+ });
194
+ return result.insertedId.toHexString();
195
+ },
196
+ async saveResponseLog(log) {
197
+ await db.collection(logsResponseCollection).insertOne({
198
+ logsId: log.requestLogId,
199
+ ...log,
200
+ createAt: log.createdAt,
201
+ updateAt: log.createdAt
202
+ });
203
+ }
204
+ };
205
+ }
206
+ function createConsoleAdapter() {
207
+ let idCounter = 0;
208
+ return {
209
+ async saveRequestLog(log) {
210
+ const id = `log_${++idCounter}`;
211
+ console.log(`[REQUEST] ${log.method} ${log.path} ${log.status} ${log.duration}ms`);
212
+ return id;
213
+ },
214
+ async saveResponseLog(log) {
215
+ if (!log.success) {
216
+ console.log(`[RESPONSE ERROR] ${log.message}`);
217
+ }
218
+ }
219
+ };
220
+ }
221
+ var index_default = createRequestLogger;
222
+ export {
223
+ createConsoleAdapter,
224
+ createMongoAdapter,
225
+ createRequestLogger,
226
+ index_default as default,
227
+ sanitize,
228
+ sanitizeHeaders
229
+ };
230
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"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 * - Path exclusion support\n */\nimport type { Middleware } 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 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 excludePaths?: (string | RegExp)[]\n /** 敏感数据清洗配置 */\n sanitize?: SanitizeConfig\n /** 获取用户 ID 的函数 */\n getUserId?: (req: Request) => string | undefined\n /** 错误回调 */\n onError?: (error: Error) => void\n /** 是否启用 @default true */\n enabled?: boolean\n}\n\n// ============ Middleware Factory ============\n\n/**\n * 创建请求日志中间件\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 * excludePaths: ['/health', '/metrics'],\n * getUserId: (req) => getLocals(req)?.userInfo?.id,\n * })\n * \n * server.use(requestLogger)\n * ```\n */\nexport function createRequestLogger(config: RequestLoggerConfig): Middleware {\n const {\n storage,\n excludePaths = [],\n sanitize: sanitizeConfig,\n getUserId,\n onError = console.error,\n enabled = true,\n } = config\n\n return async (req: Request, next: () => Promise<Response>) => {\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 excludePaths,\n sanitizeConfig,\n getUserId,\n onError,\n }).catch(onError)\n\n return response\n }\n}\n\n// ============ Internal Functions ============\n\ninterface RecordLogOptions {\n storage: StorageAdapter\n excludePaths: (string | RegExp)[]\n sanitizeConfig?: SanitizeConfig\n getUserId?: (req: Request) => string | undefined\n onError: (error: Error) => void\n}\n\nasync function recordLog(\n req: Request,\n response: Response,\n startTime: number,\n options: RecordLogOptions\n) {\n const { storage, excludePaths, sanitizeConfig, getUserId } = options\n\n const url = new URL(req.url)\n const path = url.pathname\n\n // 检查是否需要排除\n const shouldExclude = excludePaths.some(pattern => {\n if (typeof pattern === 'string') {\n return path.includes(pattern)\n }\n return pattern.test(path)\n })\n\n if (shouldExclude) {\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\n const userId = getUserId?.(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 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\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@vafast/request-logger",
3
+ "version": "0.1.0",
4
+ "description": "API request logging middleware for Vafast with sensitive data sanitization",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "keywords": [
17
+ "vafast",
18
+ "request-logger",
19
+ "api-logs",
20
+ "middleware",
21
+ "mongodb",
22
+ "typescript"
23
+ ],
24
+ "scripts": {
25
+ "clean": "rimraf dist",
26
+ "build": "tsup",
27
+ "dev": "bun run --hot example/index.ts",
28
+ "test": "bun test",
29
+ "test:types": "tsc --noEmit",
30
+ "prepublishOnly": "npm run build",
31
+ "release": "npm run build && npm publish --access=public"
32
+ },
33
+ "license": "MIT",
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "dependencies": {},
40
+ "devDependencies": {
41
+ "@types/node": "^22.15.30",
42
+ "mongodb": "^6.16.0",
43
+ "rimraf": "^6.0.1",
44
+ "tsup": "^8.5.1",
45
+ "typescript": "^5.4.5",
46
+ "vafast": "^0.3.9"
47
+ },
48
+ "peerDependencies": {
49
+ "vafast": ">= 0.3.0",
50
+ "mongodb": ">= 6.0.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "mongodb": {
54
+ "optional": true
55
+ }
56
+ }
57
+ }
58
+