pytbox 0.0.4__py3-none-any.whl → 0.0.6__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.

Potentially problematic release.


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

@@ -0,0 +1,155 @@
1
+ """
2
+ 输出格式化器 - 支持多种格式和 rich 美化
3
+ """
4
+
5
+ import json
6
+ from typing import Any, Dict, Union
7
+ from ..common.utils import logger
8
+
9
+
10
+ class OutputFormatter:
11
+ """输出格式化器"""
12
+
13
+ @staticmethod
14
+ def format_data(data: Union[Dict[str, Any], list], format_type: str = 'toml') -> str:
15
+ """格式化数据
16
+
17
+ Args:
18
+ data: 要格式化的数据
19
+ format_type: 输出格式 ('toml', 'json', 'yaml')
20
+
21
+ Returns:
22
+ str: 格式化后的字符串
23
+
24
+ Raises:
25
+ ImportError: 缺少必要的依赖
26
+ ValueError: 不支持的格式
27
+ """
28
+ logger.debug(f"格式化数据为 {format_type} 格式")
29
+
30
+ if format_type == 'json':
31
+ return OutputFormatter._format_json(data)
32
+ elif format_type == 'yaml':
33
+ return OutputFormatter._format_yaml(data)
34
+ elif format_type == 'toml':
35
+ return OutputFormatter._format_toml(data)
36
+ else:
37
+ raise ValueError(f"不支持的格式: {format_type}")
38
+
39
+ @staticmethod
40
+ def _format_json(data: Any) -> str:
41
+ """格式化为 JSON"""
42
+ try:
43
+ result = json.dumps(data, indent=2, ensure_ascii=False)
44
+ logger.debug(f"JSON 格式化完成,长度: {len(result)} 字符")
45
+ return result
46
+ except Exception as e:
47
+ logger.error(f"JSON 格式化失败: {e}")
48
+ raise
49
+
50
+ @staticmethod
51
+ def _format_yaml(data: Any) -> str:
52
+ """格式化为 YAML"""
53
+ try:
54
+ import yaml
55
+ result = yaml.dump(
56
+ data,
57
+ default_flow_style=False,
58
+ allow_unicode=True,
59
+ sort_keys=False,
60
+ indent=2
61
+ )
62
+ logger.debug(f"YAML 格式化完成,长度: {len(result)} 字符")
63
+ return result
64
+ except ImportError:
65
+ error_msg = "需要安装 pyyaml: pip install pyyaml"
66
+ logger.error(error_msg)
67
+ raise ImportError(error_msg)
68
+ except Exception as e:
69
+ logger.error(f"YAML 格式化失败: {e}")
70
+ raise
71
+
72
+ @staticmethod
73
+ def _format_toml(data: Any) -> str:
74
+ """格式化为 TOML"""
75
+ try:
76
+ import toml
77
+ result = toml.dumps(data)
78
+ logger.debug(f"TOML 格式化完成,长度: {len(result)} 字符")
79
+ return result
80
+ except ImportError:
81
+ try:
82
+ # Python 3.11+ 的 tomllib 只能读取,不能写入
83
+ import tomllib
84
+ error_msg = "需要安装 toml 库来支持 TOML 输出: pip install toml"
85
+ logger.error(error_msg)
86
+ raise ImportError(error_msg)
87
+ except ImportError:
88
+ error_msg = "需要安装 toml: pip install toml"
89
+ logger.error(error_msg)
90
+ raise ImportError(error_msg)
91
+ except Exception as e:
92
+ logger.error(f"TOML 格式化失败: {e}")
93
+ raise
94
+
95
+ @staticmethod
96
+ def format_template_list(templates: list) -> str:
97
+ """格式化模板列表"""
98
+ if not templates:
99
+ return "未找到模板文件"
100
+
101
+ logger.debug(f"格式化 {len(templates)} 个模板")
102
+
103
+ # 按文件类型分组
104
+ groups = {}
105
+ for template in templates:
106
+ if '.' in template:
107
+ ext = template.split('.')[-1]
108
+ if ext not in groups:
109
+ groups[ext] = []
110
+ groups[ext].append(template)
111
+ else:
112
+ if 'other' not in groups:
113
+ groups['other'] = []
114
+ groups['other'].append(template)
115
+
116
+ result = []
117
+ result.append("可用模板:")
118
+
119
+ for ext, files in sorted(groups.items()):
120
+ result.append(f"\n{ext.upper()} 模板:")
121
+ for template in sorted(files):
122
+ result.append(f" - {template}")
123
+
124
+ return "\n".join(result)
125
+
126
+ @staticmethod
127
+ def format_config_summary(config: Dict[str, Any]) -> str:
128
+ """格式化配置摘要"""
129
+ logger.debug("生成配置摘要")
130
+
131
+ result = []
132
+ result.append("配置摘要:")
133
+
134
+ for service, service_config in config.items():
135
+ result.append(f"\n📊 {service.upper()} 服务:")
136
+
137
+ if isinstance(service_config, dict):
138
+ for key, value in service_config.items():
139
+ if isinstance(value, list):
140
+ result.append(f" {key}: {len(value)} 项")
141
+ # 显示前几个项目
142
+ for i, item in enumerate(value[:3]):
143
+ if isinstance(item, dict):
144
+ item_keys = list(item.keys())[:2] # 只显示前两个键
145
+ result.append(f" - 项目 {i+1}: {item_keys}")
146
+ else:
147
+ result.append(f" - {item}")
148
+ if len(value) > 3:
149
+ result.append(f" ... 还有 {len(value) - 3} 项")
150
+ else:
151
+ result.append(f" {key}: {value}")
152
+ else:
153
+ result.append(f" 值: {service_config}")
154
+
155
+ return "\n".join(result)
pytbox/cli/main.py ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pytbox 主命令行入口
4
+ """
5
+
6
+ import click
7
+ from .categraf import categraf_group
8
+
9
+
10
+ @click.group()
11
+ @click.version_option()
12
+ def main():
13
+ """Pytbox 命令行工具集合"""
14
+ pass
15
+
16
+
17
+ # 注册子命令组
18
+ main.add_command(categraf_group, name='categraf')
19
+
20
+
21
+ if __name__ == "__main__":
22
+ main()
pytbox/cli.py ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pytbox 命令行工具 - 入口文件(保持向后兼容)
4
+ """
5
+
6
+ from pytbox.cli import main
7
+
8
+ if __name__ == "__main__":
9
+ main()
pytbox/database/mongo.py CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  import pymongo
4
-
4
+ from ..utils.timeutils import TimeUtils
5
+ from pytbox.utils import timeutils
5
6
 
6
7
  class Mongo:
7
8
  '''
8
9
  当前主要使用的类
9
10
  '''
10
- def __init__(self, host, port, username, password, auto_source, db_name: str='automate', collection: str=None):
11
+ def __init__(self, host: str=None, port: int=27017, username: str=None, password: str=None, auto_source: str=None, db_name: str='automate', collection: str=None):
11
12
  self.client = self._create_client(host, port, username, password, auto_source)
12
13
  self.collection = self.client[db_name][collection]
13
14
 
@@ -56,3 +57,43 @@ class Mongo:
56
57
  query['event_name'] = event_name
57
58
  return self.collection.find(query)
58
59
 
60
+ def recent_alerts(self, event_content: str) -> str:
61
+ '''
62
+ 获取最近 10 次告警
63
+
64
+ Args:
65
+ alarm_content (str): _description_
66
+
67
+ Returns:
68
+ str: _description_
69
+ '''
70
+
71
+ query = {
72
+ "event_content": event_content,
73
+ 'resolved_time': {
74
+ '$exists': True, # 字段必须存在
75
+ }
76
+ }
77
+ fields = {"_id": 0, 'event_time': 1, 'resolved_time': 1}
78
+ results = self.collection.find(query, fields).sort('event_time', -1)
79
+
80
+ alarm_list = []
81
+ for result in results:
82
+ duration_minute = '持续 ' + str(int((result['resolved_time'] - result['event_time']).total_seconds() / 60)) + ' 分钟'
83
+ alarm_list.append('触发告警: ' + TimeUtils.convert_timeobj_to_str(timeobj=result['event_time']) + ' ' + duration_minute)
84
+
85
+ alarm_str = '\n'.join(alarm_list)
86
+
87
+ alarm_str_display_threshold = 10
88
+
89
+ if len(alarm_list) > alarm_str_display_threshold:
90
+ # 如果告警超过 10 个
91
+ alarm_counter = alarm_str_display_threshold
92
+ alarm_str = '\n'.join(alarm_list[:alarm_str_display_threshold])
93
+ else:
94
+ # 如果不超过 10 个
95
+ alarm_counter = len(alarm_list)
96
+ alarm_str = '\n'.join(alarm_list)
97
+
98
+ return '该告警出现过' + str(len(alarm_list)) + f'次\n最近 {alarm_counter} 次告警如下: \n' + alarm_str
99
+
@@ -86,8 +86,8 @@ class VictoriaMetrics:
86
86
 
87
87
  return ReturnResponse(code=code, msg=msg, data=data)
88
88
 
89
- def query_interface_rate(self,
90
- direction: Literal['in', 'out'],
89
+ def check_interface_rate(self,
90
+ direction: Literal['in', 'out'],
91
91
  sysName: str,
92
92
  ifName:str,
93
93
  last_minutes: Optional[int] = None
@@ -109,5 +109,5 @@ class VictoriaMetrics:
109
109
  else:
110
110
  query = f'(rate(snmp_interface_ifHCOutOctets{{sysName="{sysName}", ifName="{ifName}"}}[{last_minutes}m])) * 8 / 1000000'
111
111
  r = self.query(query)
112
- rate = r.data['values'][1]
112
+ rate = r.data[0]['value'][1]
113
113
  return int(float(rate))
@@ -930,9 +930,12 @@ class ExtensionsEndpoint(Endpoint):
930
930
  final_data = fields[name][0].get('value')[0]['text']
931
931
 
932
932
  elif isinstance(fields[name], int):
933
- # 将时间戳转换为时间字符串格式
934
- final_data = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(fields[name] / 1000 ))
935
-
933
+ if len(str(fields[name])) >= 12 and fields[name] > 10**11:
934
+ final_data = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(fields[name] / 1000))
935
+ elif len(str(fields[name])) == 10 and 10**9 < fields[name] < 10**11:
936
+ final_data = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(fields[name]))
937
+ else:
938
+ final_data = fields[name]
936
939
  elif isinstance(fields[name], dict):
937
940
  if fields[name].get('type') == 1:
938
941
  final_data = fields[name].get('value')[0]['text']
pytbox/log/logger.py CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  import sys
4
+
4
5
  from loguru import logger
6
+
5
7
  from .victorialog import Victorialog
8
+ from ..database.mongo import Mongo
9
+ from ..feishu.client import Client as FeishuClient
10
+ from ..utils.timeutils import TimeUtils
11
+ from ..dida365 import Dida365
6
12
 
7
13
 
8
14
  logger.remove()
@@ -21,6 +27,9 @@ class AppLogger:
21
27
  stream: str='automation',
22
28
  enable_victorialog: bool=False,
23
29
  victorialog_url: str=None,
30
+ mongo: Mongo=None,
31
+ feishu: FeishuClient=None,
32
+ dida: Dida365=None,
24
33
  enable_sls: bool=False,
25
34
  sls_url: str=None,
26
35
  sls_access_key_id: str=None,
@@ -40,6 +49,9 @@ class AppLogger:
40
49
  self.stream = stream
41
50
  self.victorialog = Victorialog(url=victorialog_url)
42
51
  self.enable_victorialog = enable_victorialog
52
+ self.mongo = mongo
53
+ self.feishu = feishu
54
+ self.dida = dida
43
55
 
44
56
  def _get_caller_info(self) -> tuple[str, int, str]:
45
57
  """
@@ -73,7 +85,6 @@ class AppLogger:
73
85
  logger.info(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
74
86
  if self.enable_victorialog:
75
87
  r = self.victorialog.send_program_log(stream=self.stream, level="INFO", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
76
- print(r)
77
88
  if feishu_notify:
78
89
  self.feishu(message)
79
90
 
@@ -90,10 +101,48 @@ class AppLogger:
90
101
  logger.error(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
91
102
  if self.enable_victorialog:
92
103
  self.victorialog.send_program_log(stream=self.stream, level="ERROR", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
93
- from src.library.monitor.insert_program import insert_error_message
94
- # 传递清理后的消息给监控系统
95
- message.replace('#', '')
96
- insert_error_message(message, self.app_name, caller_filename, caller_lineno, caller_function)
104
+
105
+ if self.feishu:
106
+ existing_message = self.mongo.collection.find_one({"message": message}, sort=[("time", -1)])
107
+ current_time = TimeUtils.get_now_time_mongo()
108
+
109
+ if not existing_message or TimeUtils.get_time_diff_hours(existing_message["time"], current_time) > 36:
110
+ self.mongo.collection.insert_one({
111
+ "message": message,
112
+ "time": current_time,
113
+ "file_name": caller_filename,
114
+ "line_number": caller_lineno,
115
+ "function_name": caller_function
116
+ })
117
+
118
+
119
+ content_list = [
120
+ f"{self.feishu.extensions.format_rich_text(text='app:', color='blue', bold=True)} {self.app_name}",
121
+ f"{self.feishu.extensions.format_rich_text(text='message:', color='blue', bold=True)} {message}",
122
+ f"{self.feishu.extensions.format_rich_text(text='file_name:', color='blue', bold=True)} {caller_filename}",
123
+ f"{self.feishu.extensions.format_rich_text(text='line_number:', color='blue', bold=True)} {caller_lineno}",
124
+ f"{self.feishu.extensions.format_rich_text(text='function_name:', color='blue', bold=True)} {caller_function}"
125
+ ]
126
+
127
+ self.feishu.extensions.send_message_notify(
128
+ title=f"自动化脚本告警: {self.app_name}",
129
+ content="\n".join(content_list)
130
+ )
131
+
132
+ dida_content_list = [
133
+ f"**app**: {self.app_name}",
134
+ f"**message**: {message}",
135
+ f"**file_name**: {caller_filename}",
136
+ f"**line_number**: {caller_lineno}",
137
+ f"**function_name**: {caller_function}"
138
+ ]
139
+
140
+ self.dida.task_create(
141
+ project_id="65e87d2b3e73517c2cdd9d63",
142
+ title=f"自动化脚本告警: {self.app_name}",
143
+ content="\n".join(dida_content_list),
144
+ tags=['L-程序告警', 't-问题处理']
145
+ )
97
146
 
98
147
  def critical(self, message: str):
99
148
  """记录严重错误级别日志"""
@@ -103,24 +152,6 @@ class AppLogger:
103
152
  self.victorialog.send_program_log(stream=self.stream, level="CRITICAL", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
104
153
 
105
154
 
106
- def get_logger(app_name: str, enable_) -> AppLogger:
107
- """
108
- 获取应用日志记录器实例
109
-
110
- Args:
111
- app_name: 应用名称
112
- log_level: 日志级别
113
- enable_influx: 是否启用InfluxDB记录
114
-
115
- Returns:
116
- AppLogger: 日志记录器实例
117
- """
118
- return AppLogger(app_name)
119
-
120
-
121
155
  # 使用示例
122
156
  if __name__ == "__main__":
123
- log = get_logger(app_name='test')
124
- log.info("That's it, beautiful and simple logging!")
125
- log.warning("That's it, beautiful and simple logging!")
126
- log.error("That's it, beautiful and simple logging!11")
157
+ pass
pytbox/log/victorialog.py CHANGED
@@ -3,8 +3,8 @@
3
3
  from typing import Literal
4
4
  import requests
5
5
  import time
6
- from .utils.response import ReturnResponse
7
- from .utils.timeutils import TimeUtils
6
+ from ..utils.response import ReturnResponse
7
+ from ..utils.timeutils import TimeUtils
8
8
 
9
9
 
10
10
 
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from typing import Literal
5
+ from rich.console import Console
6
+ from rich.theme import Theme
7
+
8
+
9
+ class RichUtils:
10
+
11
+ def __init__(self):
12
+ self.theme = Theme({
13
+ "info": "bold blue",
14
+ "warning": "bold yellow",
15
+ "danger": "bold red",
16
+ })
17
+ self.console = Console(theme=self.theme)
18
+
19
+
20
+ def print(self, msg: str, style: Literal['info', 'warning', 'danger']='info'):
21
+ self.console.print(msg, style=style)