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.
- pytbox/alert/alert_handler.py +3 -3
- pytbox/base.py +26 -19
- pytbox/categraf/build_config.py +53 -0
- pytbox/cli/__init__.py +7 -0
- pytbox/cli/categraf/__init__.py +7 -0
- pytbox/cli/categraf/commands.py +55 -0
- pytbox/cli/common/__init__.py +6 -0
- pytbox/cli/common/options.py +42 -0
- pytbox/cli/common/utils.py +269 -0
- pytbox/cli/formatters/__init__.py +7 -0
- pytbox/cli/formatters/output.py +155 -0
- pytbox/cli/main.py +22 -0
- pytbox/cli.py +9 -0
- pytbox/database/mongo.py +43 -2
- pytbox/database/victoriametrics.py +3 -3
- pytbox/feishu/endpoints.py +6 -3
- pytbox/log/logger.py +55 -24
- pytbox/log/victorialog.py +2 -2
- pytbox/utils/richutils.py +21 -0
- pytbox/utils/timeutils.py +513 -19
- {pytbox-0.0.4.dist-info → pytbox-0.0.6.dist-info}/METADATA +9 -2
- pytbox-0.0.6.dist-info/RECORD +39 -0
- pytbox-0.0.6.dist-info/entry_points.txt +2 -0
- pytbox/common/base.py +0 -0
- pytbox-0.0.4.dist-info/RECORD +0 -27
- {pytbox-0.0.4.dist-info → pytbox-0.0.6.dist-info}/WHEEL +0 -0
- {pytbox-0.0.4.dist-info → pytbox-0.0.6.dist-info}/top_level.txt +0 -0
|
@@ -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
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
|
|
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['
|
|
112
|
+
rate = r.data[0]['value'][1]
|
|
113
113
|
return int(float(rate))
|
pytbox/feishu/endpoints.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
@@ -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)
|