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

Files changed (55) hide show
  1. pytbox/alert/alert_handler.py +27 -7
  2. pytbox/alert/ping.py +0 -1
  3. pytbox/alicloud/sls.py +9 -6
  4. pytbox/base.py +74 -1
  5. pytbox/categraf/build_config.py +95 -32
  6. pytbox/categraf/instances.toml +39 -0
  7. pytbox/categraf/jinja2/__init__.py +6 -0
  8. pytbox/categraf/jinja2/input.cpu/cpu.toml.j2 +5 -0
  9. pytbox/categraf/jinja2/input.disk/disk.toml.j2 +11 -0
  10. pytbox/categraf/jinja2/input.diskio/diskio.toml.j2 +6 -0
  11. pytbox/categraf/jinja2/input.dns_query/dns_query.toml.j2 +12 -0
  12. pytbox/categraf/jinja2/input.http_response/http_response.toml.j2 +9 -0
  13. pytbox/categraf/jinja2/input.mem/mem.toml.j2 +5 -0
  14. pytbox/categraf/jinja2/input.net/net.toml.j2 +11 -0
  15. pytbox/categraf/jinja2/input.net_response/net_response.toml.j2 +9 -0
  16. pytbox/categraf/jinja2/input.ping/ping.toml.j2 +11 -0
  17. pytbox/categraf/jinja2/input.prometheus/prometheus.toml.j2 +12 -0
  18. pytbox/categraf/jinja2/input.snmp/cisco_interface.toml.j2 +96 -0
  19. pytbox/categraf/jinja2/input.snmp/cisco_system.toml.j2 +41 -0
  20. pytbox/categraf/jinja2/input.snmp/h3c_interface.toml.j2 +96 -0
  21. pytbox/categraf/jinja2/input.snmp/h3c_system.toml.j2 +41 -0
  22. pytbox/categraf/jinja2/input.snmp/huawei_interface.toml.j2 +96 -0
  23. pytbox/categraf/jinja2/input.snmp/huawei_system.toml.j2 +41 -0
  24. pytbox/categraf/jinja2/input.snmp/ruijie_interface.toml.j2 +96 -0
  25. pytbox/categraf/jinja2/input.snmp/ruijie_system.toml.j2 +41 -0
  26. pytbox/categraf/jinja2/input.vsphere/vsphere.toml.j2 +211 -0
  27. pytbox/cli/commands/vm.py +22 -0
  28. pytbox/cli/main.py +2 -0
  29. pytbox/database/mongo.py +1 -1
  30. pytbox/database/victoriametrics.py +331 -40
  31. pytbox/excel.py +64 -0
  32. pytbox/feishu/endpoints.py +6 -6
  33. pytbox/log/logger.py +33 -13
  34. pytbox/mail/alimail.py +142 -0
  35. pytbox/mail/client.py +171 -0
  36. pytbox/mail/mail_detail.py +30 -0
  37. pytbox/mingdao.py +164 -0
  38. pytbox/network/meraki.py +537 -0
  39. pytbox/notion.py +731 -0
  40. pytbox/pyjira.py +612 -0
  41. pytbox/utils/cronjob.py +79 -0
  42. pytbox/utils/env.py +2 -2
  43. pytbox/utils/load_config.py +67 -21
  44. pytbox/utils/load_vm_devfile.py +45 -0
  45. pytbox/utils/richutils.py +11 -1
  46. pytbox/utils/timeutils.py +15 -57
  47. pytbox/vmware.py +120 -0
  48. pytbox/win/ad.py +30 -0
  49. {pytbox-0.0.7.dist-info → pytbox-0.3.2.dist-info}/METADATA +7 -4
  50. pytbox-0.3.2.dist-info/RECORD +72 -0
  51. pytbox/utils/ping_checker.py +0 -1
  52. pytbox-0.0.7.dist-info/RECORD +0 -39
  53. {pytbox-0.0.7.dist-info → pytbox-0.3.2.dist-info}/WHEEL +0 -0
  54. {pytbox-0.0.7.dist-info → pytbox-0.3.2.dist-info}/entry_points.txt +0 -0
  55. {pytbox-0.0.7.dist-info → pytbox-0.3.2.dist-info}/top_level.txt +0 -0
pytbox/cli/main.py CHANGED
@@ -5,6 +5,7 @@ Pytbox 主命令行入口
5
5
 
6
6
  import click
7
7
  from .categraf import categraf_group
8
+ from .commands.vm import vm_group
8
9
 
9
10
 
10
11
  @click.group()
@@ -16,6 +17,7 @@ def main():
16
17
 
17
18
  # 注册子命令组
18
19
  main.add_command(categraf_group, name='categraf')
20
+ main.add_command(vm_group, name='vm')
19
21
 
20
22
 
21
23
  if __name__ == "__main__":
pytbox/database/mongo.py CHANGED
@@ -80,7 +80,7 @@ class Mongo:
80
80
  alarm_list = []
81
81
  for result in results:
82
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)
83
+ alarm_list.append('触发时间: ' + TimeUtils.convert_timeobj_to_str(timeobj=result['event_time']) + ' ' + duration_minute)
84
84
 
85
85
  alarm_str = '\n'.join(alarm_list)
86
86
 
@@ -1,13 +1,16 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- from typing import Literal, Optional
3
+ import time
4
+ import json
5
+ from typing import Literal, Optional, Dict, List
4
6
  import requests
5
7
  from ..utils.response import ReturnResponse
8
+ from ..utils.load_vm_devfile import load_dev_file
6
9
 
7
10
 
8
11
  class VictoriaMetrics:
9
12
 
10
- def __init__(self, url: str='', timeout: int=3) -> None:
13
+ def __init__(self, url: str='', timeout: int=3, env: str='prod') -> None:
11
14
  self.url = url
12
15
  self.timeout = timeout
13
16
  self.session = requests.Session()
@@ -15,14 +18,51 @@ class VictoriaMetrics:
15
18
  'Content-Type': 'application/json',
16
19
  'Accept': 'application/json'
17
20
  })
21
+ self.env = env
22
+
23
+ def insert(self, metric_name: str = '', labels: Dict[str, str] = None,
24
+ value: List[float] = None, timestamp: int = None) -> ReturnResponse:
25
+ """插入指标数据。
18
26
 
19
- def query(self, query: str) -> ReturnResponse:
27
+ Args:
28
+ metric_name: 指标名称
29
+ labels: 标签字典
30
+ value: 值列表
31
+ timestamp: 时间戳(毫秒),默认为当前时间
32
+
33
+ Raises:
34
+ requests.RequestException: 当请求失败时抛出
35
+ """
36
+ if labels is None:
37
+ labels = {}
38
+ if value is None:
39
+ value = 1
40
+ if timestamp is None:
41
+ timestamp = int(time.time() * 1000)
42
+
43
+ url = f"{self.url}/api/v1/import"
44
+ data = {
45
+ "metric": {
46
+ "__name__": metric_name,
47
+ **labels
48
+ },
49
+ "values": [value],
50
+ "timestamps": [timestamp]
51
+ }
52
+
53
+ try:
54
+ response = requests.post(url, json=data, timeout=self.timeout)
55
+ return ReturnResponse(code=0, msg=f"数据插入成功,状态码: {response.status_code}, metric_name: {metric_name}, labels: {labels}, value: {value}, timestamp: {timestamp}")
56
+ except requests.RequestException as e:
57
+ return ReturnResponse(code=1, msg=f"数据插入失败: {e}")
58
+
59
+ def query(self, query: str=None, output_format: Literal['json']=None) -> ReturnResponse:
20
60
  '''
21
61
  查询指标数据
22
-
62
+
23
63
  Args:
24
64
  query (str): 查询语句
25
-
65
+
26
66
  Returns:
27
67
  dict: 查询结果
28
68
  '''
@@ -32,14 +72,79 @@ class VictoriaMetrics:
32
72
  timeout=self.timeout,
33
73
  params={"query": query}
34
74
  )
35
- if r.json().get("status") == "success":
36
- if r.json()['data']['result']:
37
- return ReturnResponse(code=0, msg=f"[{query}] 查询成功!", data=r.json()['data']['result'])
75
+ res_json = r.json()
76
+ status = res_json.get("status")
77
+ result = res_json.get("data", {}).get("result", [])
78
+ is_json = output_format == 'json'
79
+
80
+ if status == "success":
81
+ if result:
82
+ code = 0
83
+ msg = f"[{query}] 查询成功!"
84
+ data = result
38
85
  else:
39
- return ReturnResponse(code=2, msg=f"[{query}] 没有查询到结果", data=r.json())
86
+ code = 2
87
+ msg = f"[{query}] 没有查询到结果"
88
+ data = res_json
89
+ else:
90
+ code = 1
91
+ msg = f"[{query}] 查询失败: {res_json.get('error')}"
92
+ data = res_json
93
+
94
+ resp = ReturnResponse(code=code, msg=msg, data=data)
95
+
96
+ if is_json:
97
+ json_result = json.dumps(resp.__dict__, ensure_ascii=False)
98
+ return json_result
40
99
  else:
41
- return ReturnResponse(code=1, msg=f"[{query}] 查询失败: {r.json().get('error')}", data=r.json())
100
+ return resp
101
+
102
+ def query_range(self, query):
103
+ '''
104
+ 查询指标数据
42
105
 
106
+ Args:
107
+ query (str): 查询语句
108
+
109
+ Returns:
110
+ dict: 查询结果
111
+ '''
112
+ url = f"{self.url}/prometheus/api/v1/query_range"
113
+
114
+ data = {
115
+ 'query': query,
116
+ 'start': '-1d',
117
+ 'step': '1h'
118
+ }
119
+
120
+ r = requests.post(url, data=data, timeout=self.timeout)
121
+ res_json = r.json()
122
+ print(res_json)
123
+ # status = res_json.get("status")
124
+ # result = res_json.get("data", {}).get("result", [])
125
+ # is_json = output_format == 'json'
126
+
127
+ # if status == "success":
128
+ # if result:
129
+ # code = 0
130
+ # msg = f"[{query}] 查询成功!"
131
+ # data = result
132
+ # else:
133
+ # code = 2
134
+ # msg = f"[{query}] 没有查询到结果"
135
+ # data = res_json
136
+ # else:
137
+ # code = 1
138
+ # msg = f"[{query}] 查询失败: {res_json.get('error')}"
139
+ # data = res_json
140
+
141
+ # resp = ReturnResponse(code=code, msg=msg, data=data)
142
+
143
+ # if is_json:
144
+ # json_result = json.dumps(resp.__dict__, ensure_ascii=False)
145
+ # return json_result
146
+ # else:
147
+ # return resp
43
148
  def get_labels(self, metric_name: str) -> ReturnResponse:
44
149
  url = f"{self.url}/api/v1/series?match[]={metric_name}"
45
150
  response = requests.get(url, timeout=self.timeout)
@@ -49,42 +154,56 @@ class VictoriaMetrics:
49
154
  else:
50
155
  return ReturnResponse(code=1, msg=f"metric name: {metric_name} 查询失败")
51
156
 
52
- def check_ping_result(self, target: str, last_minute: int=10) -> ReturnResponse:
157
+ def check_ping_result(self, target: str, last_minute: int=10, env: str='prod', dev_file: str='') -> ReturnResponse:
53
158
  '''
54
159
  检查ping结果
160
+
161
+ Args:
162
+ target (str): 目标地址
163
+ last_minute (int, optional): 最近多少分钟. Defaults to 10.
164
+ env (str, optional): 环境. Defaults to 'prod'.
165
+ dev_file (str, optional): 开发文件. Defaults to ''.
166
+
167
+ Returns:
168
+ ReturnResponse:
169
+ code = 0 正常, code = 1 异常, code = 2 没有查询到数据, 建议将其判断为正常
55
170
  '''
56
- if target:
57
- # 这里需要在字符串中保留 {},同时插入 target,可以用双大括号转义
58
- query = f"ping_result_code{{target='{target}'}}"
171
+ query = f'min_over_time(ping_result_code{{target="{target}"}}[{last_minute}m])'
172
+ # query = f'avg_over_time((ping_result_code{{target="{target}"}})[{last_minute}m])'
173
+ if self.env == 'dev':
174
+ r = load_dev_file(dev_file)
59
175
  else:
60
- query = "ping_result_code"
61
-
62
- if last_minute:
63
- query = query + f"[{last_minute}m]"
64
-
65
- r = self.query(query=query)
176
+ r = self.query(query=query)
66
177
  if r.code == 0:
67
- values = r.data[0]['values']
68
- if len(values) == 2 and values[1] == "0":
69
- code = 0
70
- msg = f"已检查 {target} 最近 {last_minute} 分钟是正常的!"
178
+ value = r.data[0]['value'][1]
179
+ if value == '0':
180
+ return ReturnResponse(code=0, msg=f"已检查 {target} 最近 {last_minute} 分钟是正常的!", data=r.data)
71
181
  else:
72
- if all(str(item[1]) == "1" for item in values):
73
- code = 1
74
- msg = f"已检查 {target} 最近 {last_minute} 分钟是异常的!"
75
- else:
76
- code = 0
77
- msg = f"已检查 {target} 最近 {last_minute} 分钟是正常的!"
78
- elif r.code == 2:
79
- code = 2
80
- msg = f"没有查询到 {target} 最近 {last_minute} 分钟的ping结果!"
81
-
82
- try:
83
- data = r.data[0]
84
- except KeyError:
85
- data = r.data
182
+ return ReturnResponse(code=1, msg=f"已检查 {target} 最近 {last_minute} 分钟是异常的!", data=r.data)
183
+ else:
184
+ return r
185
+
186
+ def check_unreachable_ping_result(self, dev_file: str='') -> ReturnResponse:
187
+ '''
188
+ 检查ping结果
189
+
190
+ Args:
191
+ target (str): 目标地址
192
+ last_minute (int, optional): 最近多少分钟. Defaults to 10.
193
+ env (str, optional): 环境. Defaults to 'prod'.
194
+ dev_file (str, optional): 开发文件. Defaults to ''.
195
+
196
+ Returns:
197
+ ReturnResponse:
198
+ code = 0 正常, code = 1 异常, code = 2 没有查询到数据, 建议将其判断为正常
199
+ '''
200
+ query = "ping_result_code == 1"
86
201
 
87
- return ReturnResponse(code=code, msg=msg, data=data)
202
+ if self.env == 'dev':
203
+ r = load_dev_file(dev_file)
204
+ else:
205
+ r = self.query(query=query)
206
+ return r
88
207
 
89
208
  def check_interface_rate(self,
90
209
  direction: Literal['in', 'out'],
@@ -110,4 +229,176 @@ class VictoriaMetrics:
110
229
  query = f'(rate(snmp_interface_ifHCOutOctets{{sysName="{sysName}", ifName="{ifName}"}}[{last_minutes}m])) * 8 / 1000000'
111
230
  r = self.query(query)
112
231
  rate = r.data[0]['value'][1]
113
- return int(float(rate))
232
+ return int(float(rate))
233
+
234
+ def check_interface_avg_rate(self,
235
+ direction: Literal['in', 'out'],
236
+ sysname: str,
237
+ ifname:str,
238
+ last_hours: Optional[int] = 24,
239
+ last_minutes: Optional[int] = 5,
240
+ ) -> ReturnResponse:
241
+ '''
242
+ _summary_
243
+
244
+ Args:
245
+ direction (Literal['in', 'out']): _description_
246
+ sysname (str): _description_
247
+ ifname (str): _description_
248
+ last_hours (Optional[int], optional): _description_. Defaults to 24.
249
+ last_minutes (Optional[int], optional): _description_. Defaults to 5.
250
+
251
+ Returns:
252
+ ReturnResponse: _description_
253
+ '''
254
+ if direction == 'in':
255
+ query = f'avg_over_time((rate(snmp_interface_ifHCInOctets{{sysName="{sysname}", ifName="{ifname}"}}[{last_minutes}m]) * 8) [{last_hours}h:]) / 1e6'
256
+ else:
257
+ query = f'avg_over_time((rate(snmp_interface_ifHCOutOctets{{sysName="{sysname}", ifName="{ifname}"}}[{last_minutes}m]) * 8) [{last_hours}h:]) / 1e6'
258
+ r = self.query(query)
259
+ try:
260
+ rate = r.data[0]['value'][1]
261
+ return ReturnResponse(code=0, msg=f"查询 {sysname} {ifname} 最近 {last_hours} 小时平均速率为 {round(float(rate), 2)} Mbit/s", data=round(float(rate), 2))
262
+ except KeyError:
263
+ return ReturnResponse(code=1, msg=f"查询 {sysname} {ifname} 最近 {last_hours} 小时平均速率为 0 Mbit/s")
264
+
265
+ def check_interface_max_rate(self,
266
+ direction: Literal['in', 'out'],
267
+ sysname: str,
268
+ ifname:str,
269
+ last_hours: Optional[int] = 24,
270
+ last_minutes: Optional[int] = 5,
271
+ ) -> ReturnResponse:
272
+ '''
273
+ _summary_
274
+
275
+ Args:
276
+ direction (Literal['in', 'out']): _description_
277
+ sysname (str): _description_
278
+ ifname (str): _description_
279
+ last_hours (Optional[int], optional): _description_. Defaults to 24.
280
+ last_minutes (Optional[int], optional): _description_. Defaults to 5.
281
+
282
+ Returns:
283
+ ReturnResponse: _description_
284
+ '''
285
+ if direction == 'in':
286
+ query = f'max_over_time((rate(snmp_interface_ifHCInOctets{{sysName="{sysname}", ifName="{ifname}"}}[{last_minutes}m]) * 8) [{last_hours}h:]) / 1e6'
287
+ else:
288
+ query = f'max_over_time((rate(snmp_interface_ifHCOutOctets{{sysName="{sysname}", ifName="{ifname}"}}[{last_minutes}m]) * 8) [{last_hours}h:]) / 1e6'
289
+ r = self.query(query)
290
+ try:
291
+ rate = r.data[0]['value'][1]
292
+ return ReturnResponse(code=0, msg=f"查询 {sysname} {ifname} 最近 {last_hours} 小时最大速率为 {round(float(rate), 2)} Mbit/s", data=round(float(rate), 2))
293
+ except KeyError:
294
+ return ReturnResponse(code=1, msg=f"查询 {sysname} {ifname} 最近 {last_hours} 小时最大速率为 0 Mbit/s")
295
+
296
+ def check_snmp_port_status(self, sysname: str=None, if_name: str=None, last_minute: int=5, dev_file: str=None) -> ReturnResponse:
297
+ '''
298
+ 查询端口状态
299
+ status code 可参考 SNMP 文件 https://mibbrowser.online/mibdb_search.php?mib=IF-MIB
300
+
301
+ Args:
302
+ sysname (_type_): 设备名称
303
+ if_name (_type_): _description_
304
+ last_minute (_type_): _description_
305
+
306
+ Returns:
307
+ ReturnResponse:
308
+ code: 0, msg: , data: up,down
309
+ '''
310
+ q = f"""avg_over_time(snmp_interface_ifOperStatus{{sysName="{sysname}", ifName="{if_name}"}}[{last_minute}m])"""
311
+ if self.env == 'dev':
312
+ r = load_dev_file(dev_file)
313
+ else:
314
+ r = self.query(query=q)
315
+ if r.code == 0:
316
+ status_code = int(r.data[0]['value'][1])
317
+ if status_code == 1:
318
+ status = 'up'
319
+ else:
320
+ status = 'down'
321
+ return ReturnResponse(code=0, msg=f"{sysname} {if_name} 最近 {last_minute} 分钟端口状态为 {status}", data=status)
322
+ else:
323
+ return r
324
+
325
+ def insert_cronjob_run_status(self,
326
+ app_type: Literal['alert', 'meraki', 'other']='other',
327
+ app: str='',
328
+ status_code: Literal[0, 1]=1,
329
+ comment: str=None,
330
+ schedule_interval: str=None,
331
+ schedule_cron: str=None
332
+ ) -> ReturnResponse:
333
+ labels = {
334
+ "app": app,
335
+ "env": self.env,
336
+ }
337
+ if app_type:
338
+ labels['app_type'] = app_type
339
+ if comment:
340
+ labels['comment'] = comment
341
+
342
+ if schedule_interval:
343
+ labels['schedule_type'] = 'interval'
344
+ labels['schedule_interval'] = schedule_interval
345
+
346
+ if schedule_cron:
347
+ labels['schedule_type'] = 'cron'
348
+ labels['schedule_cron'] = schedule_cron
349
+
350
+ r = self.insert(metric_name="cronjob_run_status", labels=labels, value=status_code)
351
+ return r
352
+
353
+ def insert_cronjob_duration_seconds(self,
354
+ app_type: Literal['alert', 'meraki', 'other']='other',
355
+ app: str='',
356
+ duration_seconds: float=None,
357
+ comment: str=None,
358
+ schedule_interval: str=None,
359
+ schedule_cron: str=None
360
+ ) -> ReturnResponse:
361
+ labels = {
362
+ "app": app,
363
+ "env": self.env
364
+ }
365
+ if app_type:
366
+ labels['app_type'] = app_type
367
+ if comment:
368
+ labels['comment'] = comment
369
+
370
+ if schedule_interval:
371
+ labels['schedule_type'] = 'interval'
372
+ labels['schedule_interval'] = schedule_interval
373
+
374
+ if schedule_cron:
375
+ labels['schedule_type'] = 'cron'
376
+ labels['schedule_cron'] = schedule_cron
377
+ r = self.insert(metric_name="cronjob_run_duration_seconds", labels=labels, value=duration_seconds)
378
+ return r
379
+
380
+ def get_vmware_esxhostnames(self, vcenter: str=None) -> list:
381
+ '''
382
+ _summary_
383
+ '''
384
+ esxhostnames = []
385
+ query = f'vsphere_host_sys_uptime_latest{{vcenter="{vcenter}"}}'
386
+ metrics = self.query(query=query).data
387
+ for metric in metrics:
388
+ esxhostname = metric['metric']['esxhostname']
389
+ esxhostnames.append(esxhostname)
390
+ return esxhostnames
391
+
392
+ def get_vmware_cpu_usage(self, vcenter: str=None, esxhostname: str=None) -> float:
393
+ '''
394
+ _summary_
395
+ '''
396
+ query = f'vsphere_host_cpu_usage_average{{vcenter="{vcenter}", esxhostname="{esxhostname}"}}'
397
+ return self.query(query=query).data[0]['value'][1]
398
+
399
+ def get_vmware_memory_usage(self, vcenter: str=None, esxhostname: str=None) -> float:
400
+ '''
401
+ _summary_
402
+ '''
403
+ query = f'vsphere_host_mem_usage_average{{vcenter="{vcenter}", esxhostname="{esxhostname}"}}'
404
+ return self.query(query=query).data[0]['value'][1]
pytbox/excel.py ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from openpyxl.styles import Alignment
4
+ from openpyxl.styles import PatternFill
5
+ from openpyxl.styles.differential import DifferentialStyle
6
+ from openpyxl.styles import Font
7
+
8
+
9
+ class ExcelFormat:
10
+ def __init__(self, ws):
11
+ self.ws = ws
12
+
13
+ def set_column(self):
14
+ for column in self.ws.columns:
15
+ max_length = 0
16
+ column = [cell for cell in column]
17
+ for cell in column:
18
+ try:
19
+ if len(str(cell.value)) > max_length:
20
+ max_length = len(cell.value)
21
+ except:
22
+ pass
23
+ adjusted_width = (max_length + 10)
24
+ self.ws.column_dimensions[column[0].column_letter].width = adjusted_width
25
+
26
+ def set_rows_center(self):
27
+ # 将所有单元格的文字居中
28
+ for row in self.ws.iter_rows():
29
+
30
+ for cell in row:
31
+ cell.alignment = Alignment(horizontal='center', vertical='center')
32
+ for row in self.ws.iter_rows():
33
+ self.ws.row_dimensions[row[0].row].height = 24
34
+
35
+ def set_freeze_first_row(self):
36
+ """设置首行锁定/冻结首行"""
37
+ # 冻结首行,从第二行开始滚动
38
+ self.ws.freeze_panes = 'A2'
39
+
40
+ def set_freeze_first_column(self):
41
+ """设置首列锁定/冻结首列"""
42
+ # 冻结首列,从第二列开始滚动
43
+ self.ws.freeze_panes = 'B1'
44
+
45
+ def set_first_row_bold_color(self, font_color='FF0000FF'):
46
+ """设置首行字体加粗并改变颜色
47
+
48
+ Args:
49
+ font_color (str): 字体颜色的十六进制代码,默认为蓝色(FF0000FF)
50
+ 格式:AARRGGBB 或 RRGGBB
51
+ 例如:'FF0000FF'(蓝色), 'FFFF0000'(红色), 'FF008000'(绿色)
52
+ """
53
+ # 遍历首行的所有单元格
54
+ for cell in self.ws[1]:
55
+ if cell.value is not None: # 只对有内容的单元格设置样式
56
+ # 设置字体为粗体并改变颜色
57
+ cell.font = Font(bold=True, color=font_color)
58
+
59
+ def set_freeze_first_row_and_column(self):
60
+ """同时冻结首行和首列"""
61
+ # 冻结首行首列,从第二行第二列开始滚动
62
+ self.ws.freeze_panes = 'B2'
63
+
64
+
@@ -254,9 +254,9 @@ class MessageEndpoint(Endpoint):
254
254
  body=payload
255
255
  )
256
256
  if r.code == 0:
257
- return ReturnResponse(code=0, message=f"{message_id} 回复 emoji [{emoji_type}] 成功")
257
+ return ReturnResponse(code=0, msg=f"{message_id} 回复 emoji [{emoji_type}] 成功")
258
258
  else:
259
- return ReturnResponse(code=1, message=f"{message_id} 回复 emoji [{emoji_type}] 失败")
259
+ return ReturnResponse(code=1, msg=f"{message_id} 回复 emoji [{emoji_type}] 失败")
260
260
 
261
261
  class BitableEndpoint(Endpoint):
262
262
 
@@ -432,10 +432,10 @@ class BitableEndpoint(Endpoint):
432
432
  resp = self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}',
433
433
  method='PUT',
434
434
  body=payload)
435
- return ReturnResponse(code=resp.code, message=f"记录已存在, 进行更新", data=resp.data)
435
+ return ReturnResponse(code=resp.code, msg=f"记录已存在, 进行更新", data=resp.data)
436
436
  else:
437
437
  resp = self.add_record(app_token, table_id, fields)
438
- return ReturnResponse(code=resp.code, message=f"记录不存在, 进行创建", data=resp.data)
438
+ return ReturnResponse(code=resp.code, msg=f"记录不存在, 进行创建", data=resp.data)
439
439
 
440
440
  def query_name_by_record_id(self, app_token: str=None, table_id: str=None, field_names: list=None, record_id: str='', name: str=''):
441
441
  response = self.query_record(app_token=app_token, table_id=table_id, field_names=field_names)
@@ -967,9 +967,9 @@ class ExtensionsEndpoint(Endpoint):
967
967
  body=payload)
968
968
  if response.code == 0:
969
969
  if get == 'open_id':
970
- return ReturnResponse(code=0, message=f'根据用户输入的 {user_input}, 获取用户信息成功', data=response.data['user_list'][0]['user_id'])
970
+ return ReturnResponse(code=0, msg=f'根据用户输入的 {user_input}, 获取用户信息成功', data=response.data['user_list'][0]['user_id'])
971
971
  else:
972
- return ReturnResponse(code=response.code, message=f"获取时失败, 报错请见 data 字段", data=response.data)
972
+ return ReturnResponse(code=response.code, msg=f"获取时失败, 报错请见 data 字段", data=response.data)
973
973
 
974
974
  def format_rich_text(self, text: str, color: Literal['red', 'green', 'yellow', 'blue'], bold: bool=False):
975
975
  if bold:
pytbox/log/logger.py CHANGED
@@ -9,7 +9,7 @@ from ..database.mongo import Mongo
9
9
  from ..feishu.client import Client as FeishuClient
10
10
  from ..utils.timeutils import TimeUtils
11
11
  from ..dida365 import Dida365
12
-
12
+ from ..alicloud.sls import AliCloudSls
13
13
 
14
14
  logger.remove()
15
15
  logger.add(sys.stdout, colorize=True, format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>")
@@ -31,7 +31,6 @@ class AppLogger:
31
31
  feishu: FeishuClient=None,
32
32
  dida: Dida365=None,
33
33
  enable_sls: bool=False,
34
- sls_url: str=None,
35
34
  sls_access_key_id: str=None,
36
35
  sls_access_key_secret: str=None,
37
36
  sls_project: str=None,
@@ -49,10 +48,17 @@ class AppLogger:
49
48
  self.stream = stream
50
49
  self.victorialog = Victorialog(url=victorialog_url)
51
50
  self.enable_victorialog = enable_victorialog
51
+ self.enable_sls = enable_sls
52
52
  self.mongo = mongo
53
53
  self.feishu = feishu
54
54
  self.dida = dida
55
-
55
+ self.sls = AliCloudSls(
56
+ access_key_id=sls_access_key_id,
57
+ access_key_secret=sls_access_key_secret,
58
+ project=sls_project,
59
+ logstore=sls_logstore
60
+ )
61
+
56
62
  def _get_caller_info(self) -> tuple[str, int, str]:
57
63
  """
58
64
  获取调用者信息
@@ -78,6 +84,9 @@ class AppLogger:
78
84
  logger.debug(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
79
85
  if self.enable_victorialog:
80
86
  self.victorialog.send_program_log(stream=self.stream, level="DEBUG", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
87
+ if self.enable_sls:
88
+ self.sls.put_logs(level="DEBUG", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
89
+
81
90
 
82
91
  def info(self, message: str='', feishu_notify: bool=False):
83
92
  """记录信息级别日志"""
@@ -85,22 +94,31 @@ class AppLogger:
85
94
  logger.info(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
86
95
  if self.enable_victorialog:
87
96
  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)
97
+ if self.enable_sls:
98
+ self.sls.put_logs(level="INFO", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
88
99
  if feishu_notify:
89
- self.feishu(message)
90
-
100
+ self.feishu.extensions.send_message_notify(
101
+ title=f"自动化脚本告警: {self.app_name}",
102
+ content=message
103
+ )
104
+
91
105
  def warning(self, message: str):
92
106
  """记录警告级别日志"""
93
107
  caller_filename, caller_lineno, caller_function, call_full_filename = self._get_caller_info()
94
108
  logger.warning(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
95
109
  if self.enable_victorialog:
96
110
  self.victorialog.send_program_log(stream=self.stream, level="WARN", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
97
-
111
+ if self.enable_sls:
112
+ self.sls.put_logs(level="WARN", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
113
+
98
114
  def error(self, message: str):
99
115
  """记录错误级别日志"""
100
116
  caller_filename, caller_lineno, caller_function, call_full_filename = self._get_caller_info()
101
117
  logger.error(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
102
118
  if self.enable_victorialog:
103
119
  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)
120
+ if self.enable_sls:
121
+ self.sls.put_logs(level="ERROR", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
104
122
 
105
123
  if self.feishu:
106
124
  existing_message = self.mongo.collection.find_one({"message": message}, sort=[("time", -1)])
@@ -137,12 +155,13 @@ class AppLogger:
137
155
  f"**function_name**: {caller_function}"
138
156
  ]
139
157
 
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
- )
158
+ if self.dida:
159
+ self.dida.task_create(
160
+ project_id="65e87d2b3e73517c2cdd9d63",
161
+ title=f"自动化脚本告警: {self.app_name}",
162
+ content="\n".join(dida_content_list),
163
+ tags=['L-程序告警', 't-问题处理']
164
+ )
146
165
 
147
166
  def critical(self, message: str):
148
167
  """记录严重错误级别日志"""
@@ -150,7 +169,8 @@ class AppLogger:
150
169
  logger.critical(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
151
170
  if self.enable_victorialog:
152
171
  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)
153
-
172
+ if self.enable_sls:
173
+ self.sls.put_logs(level="CRITICAL", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
154
174
 
155
175
  # 使用示例
156
176
  if __name__ == "__main__":