mdbq 4.0.80__py3-none-any.whl → 4.0.82__py3-none-any.whl

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.
mdbq/route/monitor.py ADDED
@@ -0,0 +1,940 @@
1
+ """
2
+ 路由监控系统
3
+ 专业的API接口访问监控、记录和统计系统
4
+
5
+ 主要功能:
6
+ 1. 监控所有路由接口的访问请求
7
+ 2. 记录详细的请求信息(IP、设备、请求头、请求体等)
8
+ 3. 提供统计分析功能
9
+ 4. 异常处理和数据清理
10
+
11
+ """
12
+
13
+ import os
14
+ import json
15
+ import time
16
+ import uuid
17
+ import pymysql
18
+ import hashlib
19
+ import functools
20
+ from datetime import datetime, timedelta
21
+ from typing import Dict, Any, Optional, List
22
+ from urllib.parse import urlparse, parse_qs
23
+ from dbutils.pooled_db import PooledDB
24
+ from mdbq.myconf import myconf
25
+ from mdbq.log import mylogger
26
+ from flask import request, g
27
+ import re
28
+ import ipaddress
29
+
30
+ parser = myconf.ConfigParser()
31
+ host, port, username, password = parser.get_section_values(
32
+ file_path=os.path.join(os.path.expanduser("~"), 'spd.txt'),
33
+ section='mysql',
34
+ keys=['host', 'port', 'username', 'password'],
35
+ )
36
+
37
+ logger = mylogger.MyLogger(
38
+ logging_mode='file',
39
+ log_level='info',
40
+ log_format='json',
41
+ max_log_size=50,
42
+ backup_count=5,
43
+ enable_async=False, # 是否启用异步日志
44
+ sample_rate=1, # 采样DEBUG/INFO日志
45
+ sensitive_fields=[], # 敏感字段过滤
46
+ enable_metrics=False, # 是否启用性能指标
47
+ )
48
+
49
+
50
+ class RouteMonitor:
51
+ """路由监控核心类"""
52
+
53
+ def __init__(self, pool=None):
54
+ """初始化监控系统"""
55
+ if pool is not None:
56
+ self.pool = pool
57
+ else:
58
+ self.init_database_pool()
59
+
60
+ self.init_database_tables()
61
+ logger.debug("✨ 路由监控系统初始化完成", {
62
+ "数据库表": "已创建/验证",
63
+ "系统状态": "就绪"
64
+ })
65
+
66
+ def init_database_pool(self):
67
+ """初始化数据库连接池"""
68
+ try:
69
+ logger.debug("📊 初始化数据库连接池", {
70
+ "主机": host,
71
+ "端口": port,
72
+ "用户": username,
73
+ "最大连接数": 3
74
+ })
75
+
76
+ self.pool = PooledDB(
77
+ creator=pymysql,
78
+ maxconnections=3, # 最大连接数
79
+ mincached=1, # 初始化空闲连接数
80
+ maxcached=3, # 空闲连接最大缓存数
81
+ blocking=True,
82
+ host=host,
83
+ port=int(port),
84
+ user=username,
85
+ password=password,
86
+ ping=1,
87
+ charset='utf8mb4',
88
+ cursorclass=pymysql.cursors.DictCursor
89
+ )
90
+
91
+ logger.info("✅ 数据库连接池创建成功", {
92
+ "连接池状态": "已初始化",
93
+ "字符集": "utf8mb4"
94
+ })
95
+
96
+ except Exception as e:
97
+ logger.error("❌ 数据库连接池初始化失败", {
98
+ "错误信息": str(e),
99
+ "主机": host,
100
+ "端口": port
101
+ })
102
+ raise
103
+
104
+ def init_database_tables(self):
105
+ """初始化数据库表结构"""
106
+ try:
107
+ logger.debug("🗄️ 开始创建/验证数据库表结构", {
108
+ "操作": "表结构初始化",
109
+ "预期表数": 4
110
+ })
111
+
112
+ connection = self.pool.connection()
113
+ try:
114
+ with connection.cursor() as cursor:
115
+ # 创建详细请求记录表 - 修复MySQL 8.4+兼容性
116
+ cursor.execute("""
117
+ CREATE TABLE IF NOT EXISTS `api_request_logs` (
118
+ `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
119
+ `request_id` VARCHAR(64) NOT NULL COMMENT '请求唯一标识',
120
+ `timestamp` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '请求时间(精确到毫秒)',
121
+ `method` VARCHAR(10) NOT NULL COMMENT 'HTTP方法',
122
+ `endpoint` VARCHAR(500) NOT NULL COMMENT '请求端点',
123
+ `full_url` TEXT COMMENT '完整URL',
124
+ `client_ip` VARCHAR(45) NOT NULL COMMENT '客户端IP地址',
125
+ `real_ip` VARCHAR(45) COMMENT '真实IP地址',
126
+ `forwarded_ips` TEXT COMMENT '转发IP链',
127
+ `user_agent` TEXT COMMENT '用户代理',
128
+ `referer` VARCHAR(1000) COMMENT '来源页面',
129
+ `host` VARCHAR(255) COMMENT '请求主机',
130
+ `scheme` VARCHAR(10) COMMENT '协议类型',
131
+ `port` INT COMMENT '端口号',
132
+ `request_headers` JSON COMMENT '请求头信息',
133
+ `request_params` JSON COMMENT '请求参数',
134
+ `request_body` LONGTEXT COMMENT '请求体内容',
135
+ `request_size` INT DEFAULT 0 COMMENT '请求大小(字节)',
136
+ `response_status` INT COMMENT '响应状态码',
137
+ `response_size` INT COMMENT '响应大小(字节)',
138
+ `process_time` DECIMAL(10,3) COMMENT '处理时间(毫秒)',
139
+ `session_id` VARCHAR(128) COMMENT '会话ID',
140
+ `user_id` VARCHAR(64) COMMENT '用户ID',
141
+ `auth_token` VARCHAR(255) COMMENT '认证令牌(脱敏)',
142
+ `device_fingerprint` VARCHAR(128) COMMENT '设备指纹',
143
+ `device_info` JSON COMMENT '设备信息',
144
+ `geo_country` VARCHAR(50) COMMENT '地理位置-国家',
145
+ `geo_region` VARCHAR(100) COMMENT '地理位置-地区',
146
+ `geo_city` VARCHAR(100) COMMENT '地理位置-城市',
147
+ `is_bot` BOOLEAN DEFAULT FALSE COMMENT '是否为机器人',
148
+ `is_mobile` BOOLEAN DEFAULT FALSE COMMENT '是否为移动设备',
149
+ `browser_name` VARCHAR(50) COMMENT '浏览器名称',
150
+ `browser_version` VARCHAR(20) COMMENT '浏览器版本',
151
+ `os_name` VARCHAR(50) COMMENT '操作系统名称',
152
+ `os_version` VARCHAR(20) COMMENT '操作系统版本',
153
+ `error_message` TEXT COMMENT '错误信息',
154
+ `business_data` JSON COMMENT '业务数据',
155
+ `tags` JSON COMMENT '标签信息',
156
+ UNIQUE KEY `uk_request_id` (`request_id`),
157
+ INDEX `idx_timestamp` (`timestamp`),
158
+ INDEX `idx_endpoint` (`endpoint`(191)),
159
+ INDEX `idx_client_ip` (`client_ip`),
160
+ INDEX `idx_user_id` (`user_id`),
161
+ INDEX `idx_status` (`response_status`),
162
+ INDEX `idx_method_endpoint` (`method`, `endpoint`(191)),
163
+ INDEX `idx_timestamp_endpoint` (`timestamp`, `endpoint`(191))
164
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
165
+ COMMENT='API请求详细日志表';
166
+ """)
167
+
168
+ logger.debug("✅ api_request_logs 表创建/验证成功", {
169
+ "表名": "api_request_logs",
170
+ "用途": "API请求详细日志"
171
+ })
172
+
173
+ # 创建访问统计汇总表
174
+ cursor.execute("""
175
+ CREATE TABLE IF NOT EXISTS `api_access_statistics` (
176
+ `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
177
+ `date` DATE NOT NULL COMMENT '统计日期',
178
+ `hour` TINYINT NOT NULL DEFAULT 0 COMMENT '小时(0-23)',
179
+ `endpoint` VARCHAR(500) NOT NULL COMMENT '端点',
180
+ `method` VARCHAR(10) NOT NULL COMMENT 'HTTP方法',
181
+ `total_requests` INT UNSIGNED DEFAULT 0 COMMENT '总请求数',
182
+ `success_requests` INT UNSIGNED DEFAULT 0 COMMENT '成功请求数',
183
+ `error_requests` INT UNSIGNED DEFAULT 0 COMMENT '错误请求数',
184
+ `unique_ips` INT UNSIGNED DEFAULT 0 COMMENT '唯一IP数',
185
+ `unique_users` INT UNSIGNED DEFAULT 0 COMMENT '唯一用户数',
186
+ `avg_response_time` DECIMAL(10,3) DEFAULT 0 COMMENT '平均响应时间(毫秒)',
187
+ `max_response_time` DECIMAL(10,3) DEFAULT 0 COMMENT '最大响应时间(毫秒)',
188
+ `min_response_time` DECIMAL(10,3) DEFAULT 0 COMMENT '最小响应时间(毫秒)',
189
+ `total_request_size` BIGINT UNSIGNED DEFAULT 0 COMMENT '总请求大小(字节)',
190
+ `total_response_size` BIGINT UNSIGNED DEFAULT 0 COMMENT '总响应大小(字节)',
191
+ `bot_requests` INT UNSIGNED DEFAULT 0 COMMENT '机器人请求数',
192
+ `mobile_requests` INT UNSIGNED DEFAULT 0 COMMENT '移动端请求数',
193
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
194
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
195
+ UNIQUE KEY `uk_date_hour_endpoint_method` (`date`, `hour`, `endpoint`(191), `method`),
196
+ INDEX `idx_date` (`date`),
197
+ INDEX `idx_endpoint` (`endpoint`(191)),
198
+ INDEX `idx_date_endpoint` (`date`, `endpoint`(191))
199
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
200
+ COMMENT='API访问统计汇总表';
201
+ """)
202
+
203
+ logger.debug("✅ api_access_statistics 表创建/验证成功", {
204
+ "表名": "api_access_statistics",
205
+ "用途": "API访问统计汇总"
206
+ })
207
+
208
+ # 创建IP访问统计表
209
+ cursor.execute("""
210
+ CREATE TABLE IF NOT EXISTS `ip_access_statistics` (
211
+ `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
212
+ `date` DATE NOT NULL COMMENT '统计日期',
213
+ `ip_address` VARCHAR(45) NOT NULL COMMENT 'IP地址',
214
+ `total_requests` INT UNSIGNED DEFAULT 0 COMMENT '总请求数',
215
+ `unique_endpoints` INT UNSIGNED DEFAULT 0 COMMENT '访问的唯一端点数',
216
+ `success_rate` DECIMAL(5,2) DEFAULT 0 COMMENT '成功率(%)',
217
+ `avg_response_time` DECIMAL(10,3) DEFAULT 0 COMMENT '平均响应时间(毫秒)',
218
+ `first_access` DATETIME COMMENT '首次访问时间',
219
+ `last_access` DATETIME COMMENT '最后访问时间',
220
+ `user_agent_hash` VARCHAR(64) COMMENT '用户代理哈希',
221
+ `is_suspicious` BOOLEAN DEFAULT FALSE COMMENT '是否可疑',
222
+ `risk_score` TINYINT UNSIGNED DEFAULT 0 COMMENT '风险评分(0-100)',
223
+ `geo_country` VARCHAR(50) COMMENT '地理位置-国家',
224
+ `geo_region` VARCHAR(100) COMMENT '地理位置-地区',
225
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
226
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
227
+ UNIQUE KEY `uk_date_ip` (`date`, `ip_address`),
228
+ INDEX `idx_date` (`date`),
229
+ INDEX `idx_ip` (`ip_address`),
230
+ INDEX `idx_suspicious` (`is_suspicious`),
231
+ INDEX `idx_risk_score` (`risk_score`)
232
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
233
+ COMMENT='IP访问统计表';
234
+ """)
235
+
236
+ logger.debug("✅ ip_access_statistics 表创建/验证成功", {
237
+ "表名": "ip_access_statistics",
238
+ "用途": "IP访问统计"
239
+ })
240
+
241
+ # 创建系统性能统计表
242
+ cursor.execute("""
243
+ CREATE TABLE IF NOT EXISTS `system_performance_stats` (
244
+ `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
245
+ `timestamp` DATETIME NOT NULL COMMENT '统计时间',
246
+ `total_requests_per_minute` INT UNSIGNED DEFAULT 0 COMMENT '每分钟总请求数',
247
+ `avg_response_time` DECIMAL(10,3) DEFAULT 0 COMMENT '平均响应时间(毫秒)',
248
+ `error_rate` DECIMAL(5,2) DEFAULT 0 COMMENT '错误率(%)',
249
+ `active_ips` INT UNSIGNED DEFAULT 0 COMMENT '活跃IP数',
250
+ `peak_concurrent_requests` INT UNSIGNED DEFAULT 0 COMMENT '峰值并发请求数',
251
+ `slowest_endpoint` VARCHAR(500) COMMENT '最慢端点',
252
+ `slowest_response_time` DECIMAL(10,3) DEFAULT 0 COMMENT '最慢响应时间(毫秒)',
253
+ `most_accessed_endpoint` VARCHAR(500) COMMENT '最热门端点',
254
+ `most_accessed_count` INT UNSIGNED DEFAULT 0 COMMENT '最热门端点访问次数',
255
+ INDEX `idx_timestamp` (`timestamp`)
256
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
257
+ COMMENT='系统性能统计表';
258
+ """)
259
+
260
+ logger.debug("✅ system_performance_stats 表创建/验证成功", {
261
+ "表名": "system_performance_stats",
262
+ "用途": "系统性能统计"
263
+ })
264
+
265
+ connection.commit()
266
+ logger.info("🎯 所有数据库表结构初始化完成", {
267
+ "创建表数": 4,
268
+ "操作状态": "成功",
269
+ "数据库引擎": "InnoDB"
270
+ })
271
+
272
+ finally:
273
+ connection.close()
274
+
275
+ except Exception as e:
276
+ logger.error("❌ 数据库表结构初始化失败", {
277
+ "错误信息": str(e),
278
+ "错误类型": type(e).__name__,
279
+ "影响": "监控系统可能无法正常工作"
280
+ })
281
+ # 静默处理初始化错误,避免影响主应用
282
+ pass
283
+
284
+ def generate_request_id(self) -> str:
285
+ """生成唯一的请求ID"""
286
+ timestamp = str(int(time.time() * 1000)) # 毫秒时间戳
287
+ random_part = uuid.uuid4().hex[:8]
288
+ return f"req_{timestamp}_{random_part}"
289
+
290
+ def extract_device_info(self, user_agent: str) -> Dict[str, Any]:
291
+ """提取设备信息"""
292
+ device_info = {
293
+ 'is_mobile': False,
294
+ 'is_bot': False,
295
+ 'browser_name': 'Unknown',
296
+ 'browser_version': 'Unknown',
297
+ 'os_name': 'Unknown',
298
+ 'os_version': 'Unknown'
299
+ }
300
+
301
+ if not user_agent:
302
+ return device_info
303
+
304
+ user_agent_lower = user_agent.lower()
305
+
306
+ # 检测移动设备
307
+ mobile_keywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'windows phone']
308
+ device_info['is_mobile'] = any(keyword in user_agent_lower for keyword in mobile_keywords)
309
+
310
+ # 检测机器人
311
+ bot_keywords = ['bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python-requests']
312
+ device_info['is_bot'] = any(keyword in user_agent_lower for keyword in bot_keywords)
313
+
314
+ # 浏览器检测
315
+ browsers = [
316
+ ('chrome', r'chrome/(\d+)'),
317
+ ('firefox', r'firefox/(\d+)'),
318
+ ('safari', r'safari/(\d+)'),
319
+ ('edge', r'edge/(\d+)'),
320
+ ('opera', r'opera/(\d+)')
321
+ ]
322
+
323
+ for browser, pattern in browsers:
324
+ match = re.search(pattern, user_agent_lower)
325
+ if match:
326
+ device_info['browser_name'] = browser.title()
327
+ device_info['browser_version'] = match.group(1)
328
+ break
329
+
330
+ # 操作系统检测
331
+ os_patterns = [
332
+ ('Windows', r'windows nt (\d+\.\d+)'),
333
+ ('macOS', r'mac os x (\d+_\d+)'),
334
+ ('Linux', r'linux'),
335
+ ('Android', r'android (\d+)'),
336
+ ('iOS', r'os (\d+_\d+)')
337
+ ]
338
+
339
+ for os_name, pattern in os_patterns:
340
+ match = re.search(pattern, user_agent_lower)
341
+ if match:
342
+ device_info['os_name'] = os_name
343
+ if len(match.groups()) > 0:
344
+ device_info['os_version'] = match.group(1).replace('_', '.')
345
+ break
346
+
347
+ return device_info
348
+
349
+ def generate_device_fingerprint(self, request_data: Dict) -> str:
350
+ """生成设备指纹"""
351
+ fingerprint_data = {
352
+ 'user_agent': request_data.get('user_agent', ''),
353
+ 'accept_language': request_data.get('request_headers', {}).get('Accept-Language', ''),
354
+ 'accept_encoding': request_data.get('request_headers', {}).get('Accept-Encoding', ''),
355
+ 'connection': request_data.get('request_headers', {}).get('Connection', ''),
356
+ }
357
+
358
+ fingerprint_str = json.dumps(fingerprint_data, sort_keys=True)
359
+ return hashlib.md5(fingerprint_str.encode()).hexdigest()
360
+
361
+ def sanitize_data(self, data: Any, max_length: int = 10000) -> Any:
362
+ """数据清理和截断"""
363
+ if data is None:
364
+ return None
365
+
366
+ if isinstance(data, str):
367
+ # 移除敏感信息
368
+ sensitive_patterns = [
369
+ (r'password["\']?\s*[:=]\s*["\']?[^"\'&\s]+', 'password: [REDACTED]'),
370
+ (r'token["\']?\s*[:=]\s*["\']?[^"\'&\s]+', 'token: [REDACTED]'),
371
+ (r'key["\']?\s*[:=]\s*["\']?[^"\'&\s]+', 'key: [REDACTED]'),
372
+ ]
373
+
374
+ sanitized = data
375
+ for pattern, replacement in sensitive_patterns:
376
+ sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE)
377
+
378
+ # 截断过长的内容
379
+ if len(sanitized) > max_length:
380
+ sanitized = sanitized[:max_length] + '...[TRUNCATED]'
381
+
382
+ return sanitized
383
+
384
+ elif isinstance(data, dict):
385
+ sanitized = {}
386
+ for key, value in data.items():
387
+ if key.lower() in ['password', 'token', 'key', 'secret']:
388
+ sanitized[key] = '[REDACTED]'
389
+ else:
390
+ sanitized[key] = self.sanitize_data(value, max_length)
391
+ return sanitized
392
+
393
+ elif isinstance(data, list):
394
+ return [self.sanitize_data(item, max_length) for item in data[:100]] # 限制列表长度
395
+
396
+ return data
397
+
398
+ def get_real_ip(self, request) -> tuple:
399
+ """获取真实IP地址"""
400
+ # IP地址优先级顺序
401
+ ip_headers = [
402
+ 'X-Forwarded-For',
403
+ 'X-Real-IP',
404
+ 'CF-Connecting-IP', # Cloudflare
405
+ 'X-Client-IP',
406
+ 'X-Forwarded',
407
+ 'Forwarded-For',
408
+ 'Forwarded'
409
+ ]
410
+
411
+ forwarded_ips = []
412
+ real_ip = request.remote_addr
413
+
414
+ for header in ip_headers:
415
+ header_value = request.headers.get(header)
416
+ if header_value:
417
+ # 处理多个IP的情况(用逗号分隔)
418
+ ips = [ip.strip() for ip in header_value.split(',')]
419
+ forwarded_ips.extend(ips)
420
+
421
+ # 取第一个有效的IP作为真实IP
422
+ for ip in ips:
423
+ if self.is_valid_ip(ip) and not self.is_private_ip(ip):
424
+ real_ip = ip
425
+ break
426
+
427
+ if real_ip != request.remote_addr:
428
+ break
429
+
430
+ return real_ip, forwarded_ips
431
+
432
+ def is_valid_ip(self, ip: str) -> bool:
433
+ """验证IP地址格式"""
434
+ try:
435
+ ipaddress.ip_address(ip)
436
+ return True
437
+ except ValueError:
438
+ return False
439
+
440
+ def is_private_ip(self, ip: str) -> bool:
441
+ """检查是否为私有IP"""
442
+ try:
443
+ return ipaddress.ip_address(ip).is_private
444
+ except ValueError:
445
+ return False
446
+
447
+ def collect_request_data(self, request) -> Dict[str, Any]:
448
+ """收集请求数据"""
449
+ start_time = getattr(g, 'request_start_time', time.time())
450
+ request_id = self.generate_request_id()
451
+
452
+ # 设置请求ID到全局变量中,供后续使用
453
+ g.request_id = request_id
454
+
455
+ logger.debug("📋 开始收集请求数据", {
456
+ "请求ID": request_id,
457
+ "请求方法": request.method,
458
+ "端点": request.endpoint or request.path,
459
+ "客户端IP": request.remote_addr
460
+ })
461
+
462
+ # 获取真实IP
463
+ real_ip, forwarded_ips = self.get_real_ip(request)
464
+
465
+ if real_ip != request.remote_addr:
466
+ logger.debug("🔍 检测到IP转发", {
467
+ "请求ID": request_id,
468
+ "原始IP": request.remote_addr,
469
+ "真实IP": real_ip,
470
+ "转发链长度": len(forwarded_ips) if forwarded_ips else 0
471
+ })
472
+
473
+ # 获取请求头信息
474
+ headers = dict(request.headers)
475
+ sanitized_headers = self.sanitize_data(headers)
476
+
477
+ # 获取请求参数
478
+ request_params = {}
479
+ if request.args:
480
+ request_params.update(dict(request.args))
481
+
482
+ # 获取请求体
483
+ request_body = None
484
+ request_size = 0
485
+
486
+ try:
487
+ if request.method in ['POST', 'PUT', 'PATCH']:
488
+ if request.is_json:
489
+ request_body = request.get_json()
490
+ elif request.form:
491
+ request_body = dict(request.form)
492
+ else:
493
+ body_data = request.get_data()
494
+ if body_data:
495
+ try:
496
+ request_body = body_data.decode('utf-8')
497
+ except UnicodeDecodeError:
498
+ request_body = f"[BINARY_DATA:{len(body_data)}_bytes]"
499
+ logger.debug("📁 检测到二进制数据", {
500
+ "请求ID": request_id,
501
+ "数据大小": len(body_data),
502
+ "处理方式": "标记为二进制"
503
+ })
504
+
505
+ if request_body:
506
+ request_size = len(str(request_body).encode('utf-8'))
507
+ logger.debug("📊 请求体信息", {
508
+ "请求ID": request_id,
509
+ "数据类型": "JSON" if request.is_json else "表单" if request.form else "文本",
510
+ "大小": f"{request_size} bytes"
511
+ })
512
+ except Exception as e:
513
+ request_body = "[ERROR_READING_BODY]"
514
+ logger.warning("⚠️ 读取请求体失败", {
515
+ "请求ID": request_id,
516
+ "错误": str(e)
517
+ })
518
+
519
+ # 清理敏感数据
520
+ sanitized_body = self.sanitize_data(request_body)
521
+ sanitized_params = self.sanitize_data(request_params)
522
+
523
+ # 设备信息提取
524
+ user_agent = request.headers.get('User-Agent', '')
525
+ device_info = self.extract_device_info(user_agent)
526
+
527
+ if device_info['is_bot']:
528
+ logger.debug("🤖 检测到机器人请求", {
529
+ "请求ID": request_id,
530
+ "用户代理": user_agent[:100] + "..." if len(user_agent) > 100 else user_agent,
531
+ "IP": real_ip
532
+ })
533
+
534
+ if device_info['is_mobile']:
535
+ logger.debug("📱 检测到移动设备请求", {
536
+ "请求ID": request_id,
537
+ "操作系统": device_info['os_name'],
538
+ "浏览器": device_info['browser_name']
539
+ })
540
+
541
+ # URL解析
542
+ parsed_url = urlparse(request.url)
543
+
544
+ # 构建请求数据
545
+ request_data = {
546
+ 'request_id': request_id,
547
+ 'timestamp': datetime.now(),
548
+ 'method': request.method,
549
+ 'endpoint': request.endpoint or request.path,
550
+ 'full_url': request.url,
551
+ 'client_ip': request.remote_addr,
552
+ 'real_ip': real_ip,
553
+ 'forwarded_ips': json.dumps(forwarded_ips) if forwarded_ips else None,
554
+ 'user_agent': user_agent,
555
+ 'referer': request.headers.get('Referer'),
556
+ 'host': request.headers.get('Host'),
557
+ 'scheme': parsed_url.scheme,
558
+ 'port': parsed_url.port,
559
+ 'request_headers': json.dumps(sanitized_headers),
560
+ 'request_params': json.dumps(sanitized_params) if sanitized_params else None,
561
+ 'request_body': json.dumps(sanitized_body) if sanitized_body else None,
562
+ 'request_size': request_size,
563
+ 'session_id': request.cookies.get('session_id'),
564
+ 'user_id': getattr(request, 'current_user', {}).get('id') if hasattr(request, 'current_user') else None,
565
+ 'auth_token': self.mask_token(request.headers.get('Authorization')),
566
+ 'device_fingerprint': self.generate_device_fingerprint({
567
+ 'user_agent': user_agent,
568
+ 'request_headers': sanitized_headers
569
+ }),
570
+ 'device_info': json.dumps(device_info),
571
+ 'is_bot': device_info['is_bot'],
572
+ 'is_mobile': device_info['is_mobile'],
573
+ 'browser_name': device_info['browser_name'],
574
+ 'browser_version': device_info['browser_version'],
575
+ 'os_name': device_info['os_name'],
576
+ 'os_version': device_info['os_version'],
577
+ }
578
+
579
+ logger.debug("✅ 请求数据收集完成", {
580
+ "请求ID": request_id,
581
+ "数据字段数": len(request_data),
582
+ "请求大小": f"{request_size} bytes",
583
+ "设备类型": "移动" if device_info['is_mobile'] else "桌面",
584
+ "是否机器人": device_info['is_bot']
585
+ })
586
+
587
+ return request_data
588
+
589
+ def mask_token(self, token: str) -> str:
590
+ """脱敏处理令牌"""
591
+ if not token:
592
+ return None
593
+
594
+ if len(token) <= 8:
595
+ return '*' * len(token)
596
+
597
+ return token[:4] + '*' * (len(token) - 8) + token[-4:]
598
+
599
+ def save_request_log(self, request_data: Dict[str, Any], response_data: Dict[str, Any] = None):
600
+ """保存请求日志到数据库"""
601
+ request_id = request_data.get('request_id', 'unknown')
602
+
603
+ try:
604
+ # 合并响应数据
605
+ if response_data:
606
+ request_data.update(response_data)
607
+
608
+ logger.debug("💾 保存请求日志到数据库", {
609
+ "请求ID": request_id,
610
+ "端点": request_data.get('endpoint', ''),
611
+ "状态码": request_data.get('response_status', '未知'),
612
+ "处理时间": f"{request_data.get('process_time', 0)}ms"
613
+ })
614
+
615
+ connection = self.pool.connection()
616
+ try:
617
+ with connection.cursor() as cursor:
618
+ # 插入请求日志
619
+ columns = ', '.join([f"`{key}`" for key in request_data.keys()])
620
+ placeholders = ', '.join(['%s'] * len(request_data))
621
+
622
+ sql = f"""
623
+ INSERT INTO `api_request_logs` ({columns})
624
+ VALUES ({placeholders})
625
+ """
626
+
627
+ cursor.execute(sql, list(request_data.values()))
628
+ connection.commit()
629
+
630
+ logger.debug("✅ 请求日志保存成功", {
631
+ "请求ID": request_id,
632
+ "写入状态": "成功",
633
+ "数据库表": "api_request_logs"
634
+ })
635
+
636
+ finally:
637
+ connection.close()
638
+
639
+ except Exception as e:
640
+ logger.error("❌ 保存请求日志失败", {
641
+ "请求ID": request_id,
642
+ "错误信息": str(e),
643
+ "错误类型": type(e).__name__,
644
+ "影响": "日志丢失,但不影响主业务"
645
+ })
646
+ # 静默处理错误,不影响主业务
647
+ pass
648
+
649
+ def update_statistics(self, request_data: Dict[str, Any]):
650
+ """更新统计数据"""
651
+ request_id = request_data.get('request_id', 'unknown')
652
+ endpoint = request_data.get('endpoint', '')
653
+ status_code = request_data.get('response_status', 500)
654
+
655
+ try:
656
+ logger.debug("📈 更新统计数据", {
657
+ "请求ID": request_id,
658
+ "端点": endpoint,
659
+ "状态码": status_code,
660
+ "统计类型": "API访问统计和IP统计"
661
+ })
662
+
663
+ connection = self.pool.connection()
664
+ try:
665
+ with connection.cursor() as cursor:
666
+ now = datetime.now()
667
+ date = now.date()
668
+ hour = now.hour
669
+
670
+ # 更新API访问统计
671
+ cursor.execute("""
672
+ INSERT INTO `api_access_statistics`
673
+ (`date`, `hour`, `endpoint`, `method`, `total_requests`,
674
+ `success_requests`, `error_requests`, `avg_response_time`)
675
+ VALUES (%s, %s, %s, %s, 1, %s, %s, %s)
676
+ ON DUPLICATE KEY UPDATE
677
+ `total_requests` = `total_requests` + 1,
678
+ `success_requests` = `success_requests` + %s,
679
+ `error_requests` = `error_requests` + %s,
680
+ `avg_response_time` = (
681
+ (`avg_response_time` * (`total_requests` - 1) + %s) / `total_requests`
682
+ ),
683
+ `updated_at` = CURRENT_TIMESTAMP
684
+ """, (
685
+ date, hour,
686
+ request_data.get('endpoint', ''),
687
+ request_data.get('method', ''),
688
+ 1 if (request_data.get('response_status', 500) < 400) else 0,
689
+ 1 if (request_data.get('response_status', 500) >= 400) else 0,
690
+ request_data.get('process_time', 0),
691
+ 1 if (request_data.get('response_status', 500) < 400) else 0,
692
+ 1 if (request_data.get('response_status', 500) >= 400) else 0,
693
+ request_data.get('process_time', 0)
694
+ ))
695
+
696
+ # 更新IP访问统计
697
+ cursor.execute("""
698
+ INSERT INTO `ip_access_statistics`
699
+ (`date`, `ip_address`, `total_requests`, `first_access`, `last_access`,
700
+ `user_agent_hash`)
701
+ VALUES (%s, %s, 1, %s, %s, %s)
702
+ ON DUPLICATE KEY UPDATE
703
+ `total_requests` = `total_requests` + 1,
704
+ `last_access` = %s,
705
+ `updated_at` = CURRENT_TIMESTAMP
706
+ """, (
707
+ date,
708
+ request_data.get('real_ip', request_data.get('client_ip')),
709
+ now, now,
710
+ hashlib.md5((request_data.get('user_agent', '')).encode()).hexdigest(),
711
+ now
712
+ ))
713
+
714
+ connection.commit()
715
+
716
+ logger.debug("✅ 统计数据更新成功", {
717
+ "请求ID": request_id,
718
+ "更新表": "api_access_statistics, ip_access_statistics",
719
+ "日期": str(date),
720
+ "小时": hour
721
+ })
722
+
723
+ finally:
724
+ connection.close()
725
+
726
+ except Exception as e:
727
+ logger.error("❌ 更新统计数据失败", {
728
+ "请求ID": request_id,
729
+ "错误信息": str(e),
730
+ "错误类型": type(e).__name__,
731
+ "影响": "统计数据缺失,但不影响主业务"
732
+ })
733
+ # 静默处理错误
734
+ pass
735
+
736
+ def monitor_request(self, func):
737
+ """请求监控装饰器"""
738
+ @functools.wraps(func)
739
+ def wrapper(*args, **kwargs):
740
+ # 记录开始时间
741
+ start_time = time.time()
742
+ g.request_start_time = start_time
743
+
744
+ # 收集请求数据
745
+ request_data = self.collect_request_data(request)
746
+ request_id = request_data.get('request_id', 'unknown')
747
+
748
+ logger.debug("🎯 开始监控请求", {
749
+ "请求ID": request_id,
750
+ "函数名": func.__name__,
751
+ "端点": request_data.get('endpoint', ''),
752
+ "方法": request_data.get('method', ''),
753
+ "来源IP": request_data.get('real_ip', request_data.get('client_ip'))
754
+ })
755
+
756
+ try:
757
+ # 执行原函数
758
+ response = func(*args, **kwargs)
759
+
760
+ # 记录响应信息
761
+ end_time = time.time()
762
+ process_time = round((end_time - start_time) * 1000, 3)
763
+
764
+ response_status = getattr(response, 'status_code', 200) if hasattr(response, 'status_code') else 200
765
+ response_size = len(str(response.get_data() if hasattr(response, 'get_data') else ''))
766
+
767
+ response_data = {
768
+ 'response_status': response_status,
769
+ 'process_time': process_time,
770
+ 'response_size': response_size
771
+ }
772
+
773
+ logger.debug("✅ 请求处理完成", {
774
+ "请求ID": request_id,
775
+ "函数名": func.__name__,
776
+ "状态码": response_status,
777
+ "处理时间": f"{process_time}ms",
778
+ "响应大小": f"{response_size} bytes",
779
+ "结果": "成功"
780
+ })
781
+
782
+ # 保存日志
783
+ self.save_request_log(request_data, response_data)
784
+
785
+ # 更新统计
786
+ request_data.update(response_data)
787
+ self.update_statistics(request_data)
788
+
789
+ return response
790
+
791
+ except Exception as e:
792
+ # 记录错误信息
793
+ end_time = time.time()
794
+ process_time = round((end_time - start_time) * 1000, 3)
795
+
796
+ error_data = {
797
+ 'response_status': 500,
798
+ 'process_time': process_time,
799
+ 'error_message': str(e),
800
+ 'response_size': 0
801
+ }
802
+
803
+ logger.error("❌ 请求处理异常", {
804
+ "请求ID": request_id,
805
+ "函数名": func.__name__,
806
+ "错误信息": str(e),
807
+ "错误类型": type(e).__name__,
808
+ "处理时间": f"{process_time}ms",
809
+ "结果": "异常"
810
+ })
811
+
812
+ # 保存错误日志
813
+ self.save_request_log(request_data, error_data)
814
+
815
+ # 更新统计
816
+ request_data.update(error_data)
817
+ self.update_statistics(request_data)
818
+
819
+ # 重新抛出异常
820
+ raise e
821
+
822
+ return wrapper
823
+
824
+ def get_statistics_summary(self, days: int = 7) -> Dict[str, Any]:
825
+ """获取统计摘要"""
826
+ try:
827
+ logger.debug("📊 开始获取统计摘要", {
828
+ "查询天数": days,
829
+ "操作": "统计数据查询"
830
+ })
831
+
832
+ connection = self.pool.connection()
833
+ try:
834
+ with connection.cursor() as cursor:
835
+ end_date = datetime.now().date()
836
+ start_date = end_date - timedelta(days=days)
837
+
838
+ logger.debug("📅 统计查询时间范围", {
839
+ "开始日期": str(start_date),
840
+ "结束日期": str(end_date),
841
+ "查询天数": days
842
+ })
843
+
844
+ # 总体统计
845
+ cursor.execute("""
846
+ SELECT
847
+ SUM(total_requests) as total_requests,
848
+ SUM(success_requests) as success_requests,
849
+ SUM(error_requests) as error_requests,
850
+ AVG(avg_response_time) as avg_response_time,
851
+ COUNT(DISTINCT endpoint) as unique_endpoints
852
+ FROM api_access_statistics
853
+ WHERE date BETWEEN %s AND %s
854
+ """, (start_date, end_date))
855
+
856
+ summary = cursor.fetchone() or {}
857
+
858
+ logger.debug("📈 总体统计查询完成", {
859
+ "总请求数": summary.get('total_requests', 0),
860
+ "成功请求数": summary.get('success_requests', 0),
861
+ "错误请求数": summary.get('error_requests', 0),
862
+ "平均响应时间": f"{summary.get('avg_response_time', 0):.2f}ms",
863
+ "唯一端点数": summary.get('unique_endpoints', 0)
864
+ })
865
+
866
+ # 热门端点
867
+ cursor.execute("""
868
+ SELECT endpoint, SUM(total_requests) as requests
869
+ FROM api_access_statistics
870
+ WHERE date BETWEEN %s AND %s
871
+ GROUP BY endpoint
872
+ ORDER BY requests DESC
873
+ LIMIT 10
874
+ """, (start_date, end_date))
875
+
876
+ top_endpoints = cursor.fetchall()
877
+
878
+ logger.debug("🔥 热门端点查询完成", {
879
+ "查询结果数": len(top_endpoints),
880
+ "最热门端点": top_endpoints[0]['endpoint'] if top_endpoints else "无数据",
881
+ "最高请求数": top_endpoints[0]['requests'] if top_endpoints else 0
882
+ })
883
+
884
+ # 活跃IP统计
885
+ cursor.execute("""
886
+ SELECT COUNT(DISTINCT ip_address) as unique_ips,
887
+ SUM(total_requests) as total_ip_requests
888
+ FROM ip_access_statistics
889
+ WHERE date BETWEEN %s AND %s
890
+ """, (start_date, end_date))
891
+
892
+ ip_stats = cursor.fetchone() or {}
893
+
894
+ logger.debug("🌐 IP统计查询完成", {
895
+ "唯一IP数": ip_stats.get('unique_ips', 0),
896
+ "IP总请求数": ip_stats.get('total_ip_requests', 0)
897
+ })
898
+
899
+ result = {
900
+ 'period': f'{start_date} to {end_date}',
901
+ 'summary': summary,
902
+ 'top_endpoints': top_endpoints,
903
+ 'ip_statistics': ip_stats
904
+ }
905
+
906
+ logger.debug("✅ 统计摘要获取成功", {
907
+ "查询天数": days,
908
+ "数据完整性": "完整",
909
+ "结果状态": "成功"
910
+ })
911
+
912
+ return result
913
+
914
+ finally:
915
+ connection.close()
916
+
917
+ except Exception as e:
918
+ logger.error("❌ 获取统计摘要失败", {
919
+ "查询天数": days,
920
+ "错误信息": str(e),
921
+ "错误类型": type(e).__name__,
922
+ "影响": "统计摘要不可用"
923
+ })
924
+ return {'error': str(e)}
925
+
926
+
927
+ # 全局监控实例
928
+ route_monitor = RouteMonitor()
929
+
930
+ # 导出监控装饰器
931
+ monitor_request = route_monitor.monitor_request
932
+
933
+ # 导出其他有用的函数
934
+ def get_request_id():
935
+ """获取当前请求ID"""
936
+ return getattr(g, 'request_id', None)
937
+
938
+ def get_statistics_summary(days: int = 7):
939
+ """获取统计摘要"""
940
+ return route_monitor.get_statistics_summary(days)