mdbq 4.2.12__tar.gz → 4.2.13__tar.gz

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.

Potentially problematic release.


This version of mdbq might be problematic. Click here for more details.

Files changed (47) hide show
  1. {mdbq-4.2.12 → mdbq-4.2.13}/PKG-INFO +1 -1
  2. mdbq-4.2.13/mdbq/__version__.py +1 -0
  3. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/route/monitor.py +261 -105
  4. mdbq-4.2.13/mdbq/route/monitor_worker.py +152 -0
  5. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq.egg-info/PKG-INFO +1 -1
  6. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq.egg-info/SOURCES.txt +1 -0
  7. mdbq-4.2.12/mdbq/__version__.py +0 -1
  8. {mdbq-4.2.12 → mdbq-4.2.13}/README.txt +0 -0
  9. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/__init__.py +0 -0
  10. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/auth/__init__.py +0 -0
  11. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/auth/auth_backend.py +0 -0
  12. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/auth/crypto.py +0 -0
  13. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/auth/rate_limiter.py +0 -0
  14. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/js/__init__.py +0 -0
  15. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/js/jc.py +0 -0
  16. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/log/__init__.py +0 -0
  17. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/log/mylogger.py +0 -0
  18. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/myconf/__init__.py +0 -0
  19. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/myconf/myconf.py +0 -0
  20. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/mysql/__init__.py +0 -0
  21. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/mysql/deduplicator.py +0 -0
  22. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/mysql/mysql.py +0 -0
  23. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/mysql/s_query.py +0 -0
  24. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/mysql/unique_.py +0 -0
  25. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/mysql/uploader.py +0 -0
  26. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/other/__init__.py +0 -0
  27. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/other/download_sku_picture.py +0 -0
  28. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/other/error_handler.py +0 -0
  29. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/other/otk.py +0 -0
  30. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/other/pov_city.py +0 -0
  31. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/other/ua_sj.py +0 -0
  32. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/pbix/__init__.py +0 -0
  33. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/pbix/pbix_refresh.py +0 -0
  34. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/pbix/refresh_all.py +0 -0
  35. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/redis/__init__.py +0 -0
  36. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/redis/getredis.py +0 -0
  37. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/redis/redis_cache.py +0 -0
  38. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/route/__init__.py +0 -0
  39. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/route/analytics.py +0 -0
  40. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/route/routes.py +0 -0
  41. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/selenium/__init__.py +0 -0
  42. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/selenium/get_driver.py +0 -0
  43. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq/spider/__init__.py +0 -0
  44. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq.egg-info/dependency_links.txt +0 -0
  45. {mdbq-4.2.12 → mdbq-4.2.13}/mdbq.egg-info/top_level.txt +0 -0
  46. {mdbq-4.2.12 → mdbq-4.2.13}/setup.cfg +0 -0
  47. {mdbq-4.2.12 → mdbq-4.2.13}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.2.12
3
+ Version: 4.2.13
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -0,0 +1 @@
1
+ VERSION = '4.2.13'
@@ -15,21 +15,21 @@ import os
15
15
  import json
16
16
  import time
17
17
  import uuid
18
+ import threading
18
19
  import pymysql
19
20
  import functools
21
+ import hashlib
20
22
  from datetime import datetime, timedelta
21
23
  from typing import Dict, Any, Optional
22
24
  from dbutils.pooled_db import PooledDB # type: ignore
23
25
  from flask import request, g
24
26
 
25
27
 
26
-
27
28
  class RouteMonitor:
28
29
  """
29
30
  路由监控核心类
30
31
 
31
32
  负责 API 请求的监控、日志记录和统计分析。
32
- 采用精简设计,专注于核心监控指标,最大程度降低对业务性能的影响。
33
33
 
34
34
  Attributes:
35
35
  database (str): 监控数据库名称,默认为 'api监控系统'
@@ -41,19 +41,50 @@ class RouteMonitor:
41
41
  - cleanup_old_data: 清理历史数据
42
42
  """
43
43
 
44
- def __init__(self, database: str = 'api监控系统', pool = None):
44
+ def __init__(self, database: str = 'api监控系统', pool = None, redis_client = None, enable_async: bool = True):
45
45
  """
46
46
  初始化监控系统
47
47
 
48
48
  Args:
49
49
  database: 数据库名称,默认为 'api监控系统'
50
50
  pool: 数据库连接池对象,如果不传则使用默认配置创建
51
+ redis_client: Redis 客户端,用于异步队列(可选,如果不传则同步写入)
52
+ enable_async: 是否启用异步模式(需要 redis_client),默认 True
51
53
  """
52
54
  self.database = database
53
55
  self.pool = pool
54
56
  if self.pool is None:
55
57
  self.init_database_pool()
56
58
  self.init_database_tables()
59
+
60
+ # Redis 异步队列配置
61
+ self.redis_client = redis_client
62
+ self.enable_async = enable_async and self.redis_client is not None
63
+ # 队列名ASCII化,避免中文带来的跨系统兼容问题
64
+ try:
65
+ db_hash = hashlib.md5(self.database.encode('utf-8')).hexdigest()[:8]
66
+ self.queue_name = f"api_monitor:{db_hash}:tasks"
67
+ except Exception:
68
+ self.queue_name = "api_monitor:tasks"
69
+
70
+ # 线程锁(用于保护统计数据)
71
+ import threading
72
+ self._stats_lock = threading.Lock()
73
+
74
+ # 统计信息
75
+ self._stats = {
76
+ 'total_requests': 0,
77
+ 'queued_tasks': 0,
78
+ 'sync_writes': 0,
79
+ 'queue_failures': 0,
80
+ }
81
+
82
+ if self.enable_async:
83
+ # 使用 Redis 队列模式(适合 uwsgi 多进程)
84
+ pass # 队列消费者需要单独进程运行
85
+ else:
86
+ # 降级为同步模式
87
+ pass
57
88
 
58
89
  def init_database_pool(self):
59
90
  """
@@ -100,6 +131,7 @@ class RouteMonitor:
100
131
  connection.close()
101
132
 
102
133
  except Exception as e:
134
+ # 保持原有行为:抛出异常由上层处理
103
135
  raise
104
136
 
105
137
  def ensure_database_context(self, cursor):
@@ -178,7 +210,7 @@ class RouteMonitor:
178
210
  `失败次数` INT UNSIGNED DEFAULT 0 COMMENT '失败响应次数(状态码 >= 400)',
179
211
  `平均耗时` DECIMAL(10,3) DEFAULT 0 COMMENT '平均响应耗时(毫秒)',
180
212
  `最大耗时` DECIMAL(10,3) DEFAULT 0 COMMENT '最大响应耗时(毫秒)',
181
- `最小耗时` DECIMAL(10,3) DEFAULT 0 COMMENT '最小响应耗时(毫秒)',
213
+ `最小耗时` DECIMAL(10,3) DEFAULT NULL COMMENT '最小响应耗时(毫秒)',
182
214
  `独立ip数` INT UNSIGNED DEFAULT 0 COMMENT '访问的独立 ip 数量',
183
215
  `创建时间` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
184
216
  `更新时间` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
@@ -223,9 +255,88 @@ class RouteMonitor:
223
255
  connection.close()
224
256
 
225
257
  except Exception as e:
226
- # 静默处理初始化错误,避免影响主应用启动
258
+ # 保持静默降级行为,不影响主应用启动
227
259
  pass
228
260
 
261
+ # ==================== Redis 队列方法 ====================
262
+
263
+ @staticmethod
264
+ def _datetime_converter(obj):
265
+ """
266
+ JSON 序列化时的 datetime 转换器
267
+
268
+ 将 datetime 对象转换为特殊格式的字典,便于反序列化时还原
269
+ """
270
+ if isinstance(obj, datetime):
271
+ return {'__datetime__': True, 'value': obj.isoformat()}
272
+ return str(obj)
273
+
274
+ @staticmethod
275
+ def _datetime_decoder(dct):
276
+ """
277
+ JSON 反序列化时的 datetime 解码器
278
+
279
+ 将特殊格式的字典还原为 datetime 对象
280
+ """
281
+ if isinstance(dct, dict) and '__datetime__' in dct:
282
+ return datetime.fromisoformat(dct['value'])
283
+ return dct
284
+
285
+ def _push_to_queue(self, task_data: Dict[str, Any]) -> bool:
286
+ """
287
+ 将监控任务推入 Redis 队列(非阻塞,极快)
288
+
289
+ Args:
290
+ task_data: 任务数据字典
291
+
292
+ Returns:
293
+ bool: 是否成功推入队列
294
+ """
295
+ if not self.enable_async:
296
+ return False
297
+
298
+ try:
299
+ # 将任务数据序列化为 JSON(使用 datetime 转换器)
300
+ task_json = json.dumps(task_data, default=self._datetime_converter, ensure_ascii=False)
301
+
302
+ # 推入 Redis 列表(LPUSH,从左侧推入)
303
+ self.redis_client.lpush(self.queue_name, task_json)
304
+
305
+ with self._stats_lock:
306
+ self._stats['queued_tasks'] += 1
307
+ return True
308
+
309
+ except Exception as e:
310
+ # 静默处理错误
311
+ with self._stats_lock:
312
+ self._stats['queue_failures'] += 1
313
+ return False
314
+
315
+ def get_monitor_stats(self) -> Dict[str, Any]:
316
+ """
317
+ 获取监控系统统计信息
318
+
319
+ Returns:
320
+ dict: 统计信息字典
321
+ """
322
+ with self._stats_lock:
323
+ stats = self._stats.copy()
324
+
325
+ # 添加队列信息
326
+ if self.enable_async:
327
+ try:
328
+ queue_length = self.redis_client.llen(self.queue_name)
329
+ stats['queue_length'] = queue_length
330
+ stats['mode'] = 'async_redis'
331
+ except:
332
+ stats['queue_length'] = -1
333
+ stats['mode'] = 'async_redis_error'
334
+ else:
335
+ stats['queue_length'] = 0
336
+ stats['mode'] = 'sync'
337
+
338
+ return stats
339
+
229
340
  # ==================== 辅助方法 ====================
230
341
 
231
342
  def generate_request_id(self) -> str:
@@ -345,10 +456,31 @@ class RouteMonitor:
345
456
  elif hasattr(g, 'user_id'):
346
457
  user_id = str(g.user_id)
347
458
 
348
- # 收集请求参数(可选,仅在需要时记录)
459
+ # 收集请求参数(GET 参数 + POST 数据)
349
460
  request_params = None
461
+ params_dict = {}
462
+
463
+ # 1. 收集 URL 参数(GET)
350
464
  if request.args:
351
- request_params = self.sanitize_params(dict(request.args))
465
+ params_dict.update(dict(request.args))
466
+
467
+ # 2. 收集 POST/PUT/PATCH 请求体数据
468
+ if request.method in ['POST', 'PUT', 'PATCH']:
469
+ try:
470
+ if request.is_json:
471
+ # JSON 数据
472
+ json_data = request.get_json(silent=True)
473
+ if json_data:
474
+ params_dict.update(json_data)
475
+ elif request.form:
476
+ # 表单数据
477
+ params_dict.update(dict(request.form))
478
+ except Exception:
479
+ pass
480
+
481
+ # 3. 脱敏处理
482
+ if params_dict:
483
+ request_params = self.sanitize_params(params_dict)
352
484
 
353
485
  # 构建请求数据字典
354
486
  request_data = {
@@ -436,9 +568,11 @@ class RouteMonitor:
436
568
  with connection.cursor() as cursor:
437
569
  self.ensure_database_context(cursor)
438
570
 
439
- now = datetime.now()
440
- date = now.date()
441
- hour = now.hour
571
+ # 使用请求时间而不是当前时间,避免统计时间不一致
572
+ request_time = request_data.get('请求时间', datetime.now())
573
+ date = request_time.date()
574
+ hour = request_time.hour
575
+ now = datetime.now() # 用于IP统计的最后访问时间
442
576
 
443
577
  # 判断是否成功(状态码 < 400)
444
578
  status_code = request_data.get('响应状态码', 500)
@@ -455,14 +589,19 @@ class RouteMonitor:
455
589
  ) VALUES (
456
590
  %s, %s, %s, %s, 1, %s, %s, %s, %s, %s
457
591
  ) ON DUPLICATE KEY UPDATE
458
- `请求总数` = `请求总数` + 1,
459
- `成功次数` = `成功次数` + %s,
460
- `失败次数` = `失败次数` + %s,
461
- `平均耗时` = (
462
- (`平均耗时` * (`请求总数` - 1) + %s) / `请求总数`
463
- ),
464
- `最大耗时` = GREATEST(`最大耗时`, %s),
465
- `最小耗时` = LEAST(`最小耗时`, %s)
592
+ `请求总数` = `请求总数` + 1,
593
+ `成功次数` = `成功次数` + %s,
594
+ `失败次数` = `失败次数` + %s,
595
+ `平均耗时` = (
596
+ (`平均耗时` * `请求总数` + %s) / (`请求总数` + 1)
597
+ ),
598
+ `最大耗时` = GREATEST(`最大耗时`, %s),
599
+ `最小耗时` = (
600
+ CASE
601
+ WHEN `最小耗时` IS NULL OR `最小耗时` = 0 THEN %s
602
+ ELSE LEAST(`最小耗时`, %s)
603
+ END
604
+ )
466
605
  """, (
467
606
  date, hour,
468
607
  request_data.get('接口路径', ''),
@@ -470,9 +609,10 @@ class RouteMonitor:
470
609
  is_success, is_error,
471
610
  response_time, response_time, response_time,
472
611
  is_success, is_error,
473
- response_time,
474
- response_time,
475
- response_time
612
+ response_time,
613
+ response_time,
614
+ response_time,
615
+ response_time
476
616
  ))
477
617
 
478
618
  # 2. 更新 IP 统计表
@@ -486,9 +626,9 @@ class RouteMonitor:
486
626
  `请求总数` = `请求总数` + 1,
487
627
  `成功次数` = `成功次数` + %s,
488
628
  `失败次数` = `失败次数` + %s,
489
- `平均耗时` = (
490
- (`平均耗时` * (`请求总数` - 1) + %s) / `请求总数`
491
- ),
629
+ `平均耗时` = (
630
+ (`平均耗时` * `请求总数` + %s) / (`请求总数` + 1)
631
+ ),
492
632
  `最后访问` = %s
493
633
  """, (
494
634
  date,
@@ -547,6 +687,10 @@ class RouteMonitor:
547
687
  request_data = self.collect_request_data(request)
548
688
  request_id = request_data.get('请求id', 'unknown')
549
689
 
690
+ # 统计总请求数(线程安全)
691
+ with self._stats_lock:
692
+ self._stats['total_requests'] += 1
693
+
550
694
  try:
551
695
  # 执行原函数
552
696
  response = func(*args, **kwargs)
@@ -562,6 +706,12 @@ class RouteMonitor:
562
706
  elif isinstance(response, tuple) and len(response) > 1:
563
707
  # 处理 (data, status_code) 格式的返回
564
708
  response_status = response[1]
709
+ # 健壮化:支持 '200 OK' 等字符串状态
710
+ if isinstance(response_status, str):
711
+ try:
712
+ response_status = int(str(response_status).split()[0])
713
+ except Exception:
714
+ response_status = 200
565
715
 
566
716
  # 更新响应数据
567
717
  response_data = {
@@ -569,12 +719,22 @@ class RouteMonitor:
569
719
  '响应耗时': process_time,
570
720
  }
571
721
 
572
- # 保存日志
573
- self.save_request_log(request_data, response_data)
574
-
575
- # 更新统计
722
+ # 合并数据
576
723
  request_data.update(response_data)
577
- self.update_statistics(request_data)
724
+
725
+ # 【改进】优先使用 Redis 队列(非阻塞)
726
+ queued = self._push_to_queue({
727
+ 'type': 'request_log',
728
+ 'data': request_data,
729
+ 'timestamp': time.time()
730
+ })
731
+
732
+ # 如果队列失败,降级为同步写入
733
+ if not queued:
734
+ with self._stats_lock:
735
+ self._stats['sync_writes'] += 1
736
+ self.save_request_log(request_data, None)
737
+ self.update_statistics(request_data)
578
738
 
579
739
  return response
580
740
 
@@ -590,12 +750,22 @@ class RouteMonitor:
590
750
  '错误信息': f"{type(e).__name__}: {str(e)}"
591
751
  }
592
752
 
593
- # 保存错误日志
594
- self.save_request_log(request_data, error_data)
595
-
596
- # 更新统计
753
+ # 合并数据
597
754
  request_data.update(error_data)
598
- self.update_statistics(request_data)
755
+
756
+ # 【改进】优先使用 Redis 队列(非阻塞)
757
+ queued = self._push_to_queue({
758
+ 'type': 'request_log',
759
+ 'data': request_data,
760
+ 'timestamp': time.time()
761
+ })
762
+
763
+ # 如果队列失败,降级为同步写入
764
+ if not queued:
765
+ with self._stats_lock:
766
+ self._stats['sync_writes'] += 1
767
+ self.save_request_log(request_data, None)
768
+ self.update_statistics(request_data)
599
769
 
600
770
  # 重新抛出异常,不影响原有错误处理逻辑
601
771
  raise
@@ -754,84 +924,69 @@ class RouteMonitor:
754
924
 
755
925
  except Exception as e:
756
926
  return {'错误': str(e)}
757
-
758
-
759
- # ==================== 全局实例与导出 ====================
760
-
761
- # 创建全局监控实例
762
- route_monitor = RouteMonitor()
763
-
764
- # 导出核心装饰器(推荐使用此方式)
765
- api_monitor = route_monitor.api_monitor
766
-
767
-
768
- # ==================== 便捷工具函数 ====================
769
-
770
- def get_request_id() -> Optional[str]:
771
- """
772
- 获取当前请求的唯一 ID
773
-
774
- 可在被 @api_monitor 装饰的函数内调用,用于日志关联和问题追踪。
775
927
 
776
- Returns:
777
- str: 请求 ID,如果不在请求上下文中则返回 None
928
+ def consume_queue_tasks(self, batch_size: int = 100, timeout: float = 1.0):
929
+ """
930
+ 从 Redis 队列中消费任务并写入数据库(用于单独的消费者进程)
778
931
 
779
- 示例:
780
- ```python
781
- @api_monitor
782
- def my_api():
783
- req_id = get_request_id()
784
- logger.info(f"处理请求: {req_id}")
785
- return {'status': 'ok'}
786
- ```
787
- """
788
- return getattr(g, 'request_id', None)
789
-
790
-
791
- def get_statistics_summary(days: int = 7) -> Dict[str, Any]:
792
- """
793
- 获取统计摘要数据
794
-
795
- Args:
796
- days: 统计天数,默认 7 天
932
+ 这个方法应该在单独的进程中循环调用,不要在 uwsgi worker 中调用!
797
933
 
798
- Returns:
799
- dict: 统计摘要数据
934
+ Args:
935
+ batch_size: 每次处理的最大任务数
936
+ timeout: 从队列获取任务的超时时间(秒)
937
+
938
+ Returns:
939
+ int: 处理的任务数量
940
+ """
941
+ if not self.enable_async:
942
+ return 0
800
943
 
801
- 示例:
802
- ```python
803
- # 获取最近 7 天的统计
804
- stats = get_statistics_summary(7)
805
- print(stats['总体统计'])
944
+ processed = 0
806
945
 
807
- # 获取最近 30 天的统计
808
- stats = get_statistics_summary(30)
809
- ```
810
- """
811
- return route_monitor.get_statistics_summary(days)
946
+ try:
947
+ # 批量从队列中取出任务(BRPOP,从右侧阻塞弹出)
948
+ for _ in range(batch_size):
949
+ result = self.redis_client.brpop(self.queue_name, timeout=timeout)
950
+ if not result:
951
+ break # 队列为空
952
+
953
+ _, task_json = result
954
+
955
+ try:
956
+ # (Redis 可能返回 bytes)
957
+ if isinstance(task_json, bytes):
958
+ task_json = task_json.decode('utf-8')
959
+
960
+ # 解析任务数据,还原 datetime 对象(使用 datetime 解码器)
961
+ task = json.loads(task_json, object_hook=self._datetime_decoder)
962
+ task_type = task.get('type')
963
+ task_data = task.get('data', {})
964
+
965
+ # 处理不同类型的任务
966
+ if task_type == 'request_log':
967
+ # 写入日志和统计
968
+ self.save_request_log(task_data, None)
969
+ self.update_statistics(task_data)
970
+ processed += 1
971
+
972
+ except Exception as e:
973
+ # 单个任务失败不影响其他任务
974
+ pass
975
+
976
+ return processed
977
+
978
+ except Exception as e:
979
+ # 静默处理错误
980
+ return processed
812
981
 
813
982
 
814
- def cleanup_old_data(days_to_keep: int = 30) -> Dict[str, int]:
815
- """
816
- 清理历史数据
817
-
818
- 删除指定天数之前的详细日志,保留统计数据。
819
- 建议通过定时任务定期执行。
820
-
821
- Args:
822
- days_to_keep: 保留最近多少天的数据,默认 30 天
823
-
824
- Returns:
825
- dict: 清理结果统计
826
-
827
- 示例:
828
- ```python
829
- # 保留最近 30 天的数据,删除更早的
830
- result = cleanup_old_data(30)
831
- print(f"清理完成: {result}")
832
- ```
833
- """
834
- return route_monitor.cleanup_old_data(days_to_keep)
983
+ # # ==================== 全局实例与导出 ====================
984
+
985
+ # # 创建全局监控实例
986
+ # route_monitor = RouteMonitor()
987
+
988
+ # # 导出核心装饰器(推荐使用此方式)
989
+ # api_monitor = route_monitor.api_monitor
835
990
 
836
991
 
837
992
  # ==================== 模块导出列表 ====================
@@ -841,4 +996,5 @@ __all__ = [
841
996
  'get_request_id',
842
997
  'get_statistics_summary',
843
998
  'cleanup_old_data',
999
+ 'get_async_stats',
844
1000
  ]
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ API 监控系统 - 队列消费者进程
5
+
6
+ 这是一个单独运行的守护进程,从 Redis 队列中消费监控任务并写入数据库。
7
+ 在 uwsgi 多进程环境下,只需运行一个或多个此进程即可处理所有 worker 的监控数据。
8
+
9
+ 使用方法:
10
+ python -m mdbq.route.monitor_worker
11
+
12
+ 或通过 systemd/supervisor 管理:
13
+ supervisorctl start api_monitor_worker
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import time
19
+ import signal
20
+ from typing import Optional
21
+
22
+
23
+ class MonitorWorker:
24
+ """监控任务队列消费者"""
25
+
26
+ def __init__(self, redis_client, database: str = 'api监控系统', pool=None):
27
+ """
28
+ 初始化消费者
29
+
30
+ Args:
31
+ redis_client: Redis 客户端实例
32
+ database: 监控数据库名称
33
+ pool: 数据库连接池(可选)
34
+ """
35
+ from mdbq.route.monitor import RouteMonitor
36
+
37
+ self.monitor = RouteMonitor(
38
+ database=database,
39
+ pool=pool,
40
+ redis_client=redis_client,
41
+ enable_async=True
42
+ )
43
+ self.running = True
44
+ self.processed_total = 0
45
+
46
+ # 注册信号处理
47
+ signal.signal(signal.SIGINT, self._handle_signal)
48
+ signal.signal(signal.SIGTERM, self._handle_signal)
49
+
50
+ def _handle_signal(self, signum, frame):
51
+ """处理关闭信号"""
52
+ self.running = False
53
+
54
+ def run(self, batch_size: int = 100, sleep_interval: float = 0.1):
55
+ """
56
+ 运行消费者主循环
57
+
58
+ Args:
59
+ batch_size: 每次处理的最大任务数
60
+ sleep_interval: 队列为空时的休眠间隔(秒)
61
+ """
62
+ last_log_time = time.time()
63
+ log_interval = 60 # 每60秒输出一次统计
64
+
65
+ try:
66
+ while self.running:
67
+ # 消费一批任务
68
+ processed = self.monitor.consume_queue_tasks(
69
+ batch_size=batch_size,
70
+ timeout=1.0
71
+ )
72
+
73
+ self.processed_total += processed
74
+
75
+ # 定期输出统计信息
76
+ current_time = time.time()
77
+ if current_time - last_log_time >= log_interval:
78
+ last_log_time = current_time
79
+
80
+ # 如果队列为空,短暂休眠
81
+ if processed == 0:
82
+ time.sleep(sleep_interval)
83
+
84
+ except Exception as e:
85
+ pass
86
+
87
+
88
+ def create_worker_from_config(config_file: Optional[str] = None):
89
+ """
90
+ 从配置文件创建消费者实例
91
+
92
+ Args:
93
+ config_file: 配置文件路径(可选,默认 ~/spd.txt)
94
+
95
+ Returns:
96
+ MonitorWorker: 消费者实例
97
+ """
98
+ try:
99
+ import redis
100
+ from mdbq.myconf import myconf
101
+
102
+ # 读取配置
103
+ if config_file is None:
104
+ config_file = os.path.join(os.path.expanduser("~"), 'spd.txt')
105
+
106
+ parser = myconf.ConfigParser()
107
+ redis_password = parser.get_value(
108
+ file_path=config_file,
109
+ section='redis',
110
+ key='password',
111
+ value_type=str
112
+ )
113
+
114
+ # 创建 Redis 客户端
115
+ redis_client = redis.Redis(
116
+ host='127.0.0.1',
117
+ port=6379,
118
+ db=0,
119
+ password=redis_password,
120
+ decode_responses=False,
121
+ socket_timeout=5,
122
+ socket_connect_timeout=3
123
+ )
124
+
125
+ # 创建消费者
126
+ worker = MonitorWorker(
127
+ redis_client=redis_client,
128
+ database='api监控系统'
129
+ )
130
+
131
+ return worker
132
+
133
+ except Exception as e:
134
+ sys.exit(1)
135
+
136
+
137
+ def main():
138
+ """主函数"""
139
+
140
+ # 创建消费者
141
+ worker = create_worker_from_config()
142
+
143
+ # 运行消费者
144
+ worker.run(
145
+ batch_size=100, # 每次处理100个任务
146
+ sleep_interval=0.5 # 队列空时休眠0.5秒
147
+ )
148
+
149
+
150
+ if __name__ == '__main__':
151
+ main()
152
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.2.12
3
+ Version: 4.2.13
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -37,6 +37,7 @@ mdbq/redis/redis_cache.py
37
37
  mdbq/route/__init__.py
38
38
  mdbq/route/analytics.py
39
39
  mdbq/route/monitor.py
40
+ mdbq/route/monitor_worker.py
40
41
  mdbq/route/routes.py
41
42
  mdbq/selenium/__init__.py
42
43
  mdbq/selenium/get_driver.py
@@ -1 +0,0 @@
1
- VERSION = '4.2.12'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes