pylogging-ext 0.1.0__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.
- pylogging_ext-0.1.0/LICENSE +21 -0
- pylogging_ext-0.1.0/MANIFEST.in +9 -0
- pylogging_ext-0.1.0/PKG-INFO +166 -0
- pylogging_ext-0.1.0/README.md +119 -0
- pylogging_ext-0.1.0/pylogging_ext/__init__.py +6 -0
- pylogging_ext-0.1.0/pylogging_ext/base.py +199 -0
- pylogging_ext-0.1.0/pylogging_ext/cls.py +93 -0
- pylogging_ext-0.1.0/pylogging_ext/dingtalk.py +5 -0
- pylogging_ext-0.1.0/pylogging_ext/telegram.py +111 -0
- pylogging_ext-0.1.0/pylogging_ext/wechat.py +100 -0
- pylogging_ext-0.1.0/pylogging_ext.egg-info/PKG-INFO +166 -0
- pylogging_ext-0.1.0/pylogging_ext.egg-info/SOURCES.txt +16 -0
- pylogging_ext-0.1.0/pylogging_ext.egg-info/dependency_links.txt +1 -0
- pylogging_ext-0.1.0/pylogging_ext.egg-info/requires.txt +5 -0
- pylogging_ext-0.1.0/pylogging_ext.egg-info/top_level.txt +1 -0
- pylogging_ext-0.1.0/requirements.txt +2 -0
- pylogging_ext-0.1.0/setup.cfg +4 -0
- pylogging_ext-0.1.0/setup.py +57 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2022] [QASP]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pylogging-ext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 一个支持 异步、批量、重试、多通道通知 的 logging 扩展库。
|
|
5
|
+
Home-page: https://github.com/cnmax/pylogging-ext
|
|
6
|
+
Author: chao
|
|
7
|
+
Author-email: mr.qchao@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/cnmax/pylogging-ext
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/cnmax/pylogging-ext/issues
|
|
11
|
+
Project-URL: Source Code, https://github.com/cnmax/pylogging-ext
|
|
12
|
+
Keywords: logging,async logging,handler,cls,tencent cloud,wechat,telegram,notification,monitoring
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Topic :: System :: Logging
|
|
26
|
+
Classifier: Topic :: System :: Monitoring
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: requests
|
|
31
|
+
Requires-Dist: tencentcloud-cls-sdk-python
|
|
32
|
+
Provides-Extra: socks
|
|
33
|
+
Requires-Dist: requests[socks]; extra == "socks"
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: author-email
|
|
36
|
+
Dynamic: classifier
|
|
37
|
+
Dynamic: description
|
|
38
|
+
Dynamic: description-content-type
|
|
39
|
+
Dynamic: home-page
|
|
40
|
+
Dynamic: keywords
|
|
41
|
+
Dynamic: license
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
Dynamic: project-url
|
|
44
|
+
Dynamic: provides-extra
|
|
45
|
+
Dynamic: requires-dist
|
|
46
|
+
Dynamic: summary
|
|
47
|
+
|
|
48
|
+
# pylogging-ext
|
|
49
|
+
|
|
50
|
+
[](https://pypi.python.org/pypi/pylogging-ext)
|
|
51
|
+

|
|
52
|
+

|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
一个支持 异步、批量、重试、多通道通知 的 logging 扩展库。
|
|
56
|
+
|
|
57
|
+
> 不只是 logging handler,更是通知基础设施
|
|
58
|
+
|
|
59
|
+
## 安装
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install pylogging-ext
|
|
63
|
+
```
|
|
64
|
+
可选(支持 socks 代理):
|
|
65
|
+
```bash
|
|
66
|
+
pip install pylogging-ext[socks]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 使用
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import logging
|
|
73
|
+
from pylogging_ext import TelegramHandler
|
|
74
|
+
|
|
75
|
+
logger = logging.getLogger('demo')
|
|
76
|
+
logger.setLevel(logging.INFO)
|
|
77
|
+
|
|
78
|
+
handler = TelegramHandler(
|
|
79
|
+
token='your-bot-token',
|
|
80
|
+
chat_id='your-chat-id',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
logger.addHandler(handler)
|
|
84
|
+
|
|
85
|
+
logger.error('发生错误了')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## API
|
|
89
|
+
|
|
90
|
+
### Class: BaseHandler
|
|
91
|
+
|
|
92
|
+
通用异步 Handler 基类,所有通知 Handler 均继承自此类。
|
|
93
|
+
|
|
94
|
+
#### BaseHandler(...)
|
|
95
|
+
|
|
96
|
+
- `batch_size` 批量发送大小
|
|
97
|
+
- `flush_interval` 刷新间隔 (秒)
|
|
98
|
+
- `queue_size` 队列大小
|
|
99
|
+
- `retry` 重试次数
|
|
100
|
+
|
|
101
|
+
#### 特性:
|
|
102
|
+
- 异步发送(非阻塞)
|
|
103
|
+
- 批量处理
|
|
104
|
+
- 指数退避重试
|
|
105
|
+
- 优雅关闭
|
|
106
|
+
|
|
107
|
+
## 腾讯云 CLS
|
|
108
|
+
### Class: TencentCloudCLSHandler
|
|
109
|
+
|
|
110
|
+
```pycon
|
|
111
|
+
from pylogging_ext import TencentCloudCLSHandler
|
|
112
|
+
|
|
113
|
+
handler = TencentCloudCLSHandler(
|
|
114
|
+
endpoint='ap-shanghai.cls.tencentcs.com',
|
|
115
|
+
secret_id='your-secret-id',
|
|
116
|
+
secret_key='your-secret-key',
|
|
117
|
+
topic_id='your-topic-id',
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
### 参数
|
|
121
|
+
- `endpoint` CLS接入地址
|
|
122
|
+
- `secret_id` 腾讯云密钥
|
|
123
|
+
- `secret_key` 腾讯云密钥
|
|
124
|
+
- `topic_id` 日志主题 ID
|
|
125
|
+
|
|
126
|
+
## 微信机器人
|
|
127
|
+
### Class: WeChatHandler
|
|
128
|
+
|
|
129
|
+
```pycon
|
|
130
|
+
from pylogging_ext import WeChatHandler
|
|
131
|
+
|
|
132
|
+
handler = WeChatHandler(
|
|
133
|
+
webhook='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx'
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 参数
|
|
138
|
+
- `webhook` 企业微信机器人地址
|
|
139
|
+
|
|
140
|
+
## Telegram
|
|
141
|
+
### Class: TelegramHandler
|
|
142
|
+
|
|
143
|
+
```pycon
|
|
144
|
+
from pylogging_ext import TelegramHandler
|
|
145
|
+
|
|
146
|
+
handler = TelegramHandler(
|
|
147
|
+
token='your-bot-token',
|
|
148
|
+
chat_id='your-chat-id',
|
|
149
|
+
proxies={
|
|
150
|
+
'http': 'http://127.0.0.1:7890',
|
|
151
|
+
'https': 'http://127.0.0.1:7890',
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
### 参数
|
|
156
|
+
- `token` Telegram Bot Token
|
|
157
|
+
- `chat_id` 聊天 ID
|
|
158
|
+
- `proxies` 代理配置(可选)
|
|
159
|
+
- `ssl_verify` SSL校验 (默认 True)
|
|
160
|
+
|
|
161
|
+
## 支持的通知渠道
|
|
162
|
+
- 腾讯云 CLS
|
|
163
|
+
- 企业微信
|
|
164
|
+
- Telegram
|
|
165
|
+
|
|
166
|
+
持续扩展中...
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# pylogging-ext
|
|
2
|
+
|
|
3
|
+
[](https://pypi.python.org/pypi/pylogging-ext)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
一个支持 异步、批量、重试、多通道通知 的 logging 扩展库。
|
|
9
|
+
|
|
10
|
+
> 不只是 logging handler,更是通知基础设施
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install pylogging-ext
|
|
16
|
+
```
|
|
17
|
+
可选(支持 socks 代理):
|
|
18
|
+
```bash
|
|
19
|
+
pip install pylogging-ext[socks]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 使用
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import logging
|
|
26
|
+
from pylogging_ext import TelegramHandler
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger('demo')
|
|
29
|
+
logger.setLevel(logging.INFO)
|
|
30
|
+
|
|
31
|
+
handler = TelegramHandler(
|
|
32
|
+
token='your-bot-token',
|
|
33
|
+
chat_id='your-chat-id',
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger.addHandler(handler)
|
|
37
|
+
|
|
38
|
+
logger.error('发生错误了')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### Class: BaseHandler
|
|
44
|
+
|
|
45
|
+
通用异步 Handler 基类,所有通知 Handler 均继承自此类。
|
|
46
|
+
|
|
47
|
+
#### BaseHandler(...)
|
|
48
|
+
|
|
49
|
+
- `batch_size` 批量发送大小
|
|
50
|
+
- `flush_interval` 刷新间隔 (秒)
|
|
51
|
+
- `queue_size` 队列大小
|
|
52
|
+
- `retry` 重试次数
|
|
53
|
+
|
|
54
|
+
#### 特性:
|
|
55
|
+
- 异步发送(非阻塞)
|
|
56
|
+
- 批量处理
|
|
57
|
+
- 指数退避重试
|
|
58
|
+
- 优雅关闭
|
|
59
|
+
|
|
60
|
+
## 腾讯云 CLS
|
|
61
|
+
### Class: TencentCloudCLSHandler
|
|
62
|
+
|
|
63
|
+
```pycon
|
|
64
|
+
from pylogging_ext import TencentCloudCLSHandler
|
|
65
|
+
|
|
66
|
+
handler = TencentCloudCLSHandler(
|
|
67
|
+
endpoint='ap-shanghai.cls.tencentcs.com',
|
|
68
|
+
secret_id='your-secret-id',
|
|
69
|
+
secret_key='your-secret-key',
|
|
70
|
+
topic_id='your-topic-id',
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
### 参数
|
|
74
|
+
- `endpoint` CLS接入地址
|
|
75
|
+
- `secret_id` 腾讯云密钥
|
|
76
|
+
- `secret_key` 腾讯云密钥
|
|
77
|
+
- `topic_id` 日志主题 ID
|
|
78
|
+
|
|
79
|
+
## 微信机器人
|
|
80
|
+
### Class: WeChatHandler
|
|
81
|
+
|
|
82
|
+
```pycon
|
|
83
|
+
from pylogging_ext import WeChatHandler
|
|
84
|
+
|
|
85
|
+
handler = WeChatHandler(
|
|
86
|
+
webhook='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx'
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 参数
|
|
91
|
+
- `webhook` 企业微信机器人地址
|
|
92
|
+
|
|
93
|
+
## Telegram
|
|
94
|
+
### Class: TelegramHandler
|
|
95
|
+
|
|
96
|
+
```pycon
|
|
97
|
+
from pylogging_ext import TelegramHandler
|
|
98
|
+
|
|
99
|
+
handler = TelegramHandler(
|
|
100
|
+
token='your-bot-token',
|
|
101
|
+
chat_id='your-chat-id',
|
|
102
|
+
proxies={
|
|
103
|
+
'http': 'http://127.0.0.1:7890',
|
|
104
|
+
'https': 'http://127.0.0.1:7890',
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
### 参数
|
|
109
|
+
- `token` Telegram Bot Token
|
|
110
|
+
- `chat_id` 聊天 ID
|
|
111
|
+
- `proxies` 代理配置(可选)
|
|
112
|
+
- `ssl_verify` SSL校验 (默认 True)
|
|
113
|
+
|
|
114
|
+
## 支持的通知渠道
|
|
115
|
+
- 腾讯云 CLS
|
|
116
|
+
- 企业微信
|
|
117
|
+
- Telegram
|
|
118
|
+
|
|
119
|
+
持续扩展中...
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import queue
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import traceback
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseHandler(logging.Handler):
|
|
10
|
+
"""
|
|
11
|
+
通用异步通知 Handler 基类
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
level=logging.NOTSET,
|
|
17
|
+
batch_size=10,
|
|
18
|
+
flush_interval=3.0,
|
|
19
|
+
queue_size=1000,
|
|
20
|
+
retry=1,
|
|
21
|
+
):
|
|
22
|
+
super().__init__(level)
|
|
23
|
+
|
|
24
|
+
# 内部日志
|
|
25
|
+
self._internal_logger = logging.getLogger('pylogging_ext.internal')
|
|
26
|
+
self._internal_logger.propagate = False
|
|
27
|
+
|
|
28
|
+
if not self._internal_logger.handlers:
|
|
29
|
+
handler = logging.StreamHandler()
|
|
30
|
+
formatter = logging.Formatter(
|
|
31
|
+
'%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
|
32
|
+
)
|
|
33
|
+
handler.setFormatter(formatter)
|
|
34
|
+
self._internal_logger.addHandler(handler)
|
|
35
|
+
|
|
36
|
+
# 批量参数
|
|
37
|
+
self.batch_size = batch_size
|
|
38
|
+
self.flush_interval = flush_interval
|
|
39
|
+
|
|
40
|
+
# 重试次数
|
|
41
|
+
self.retry = retry
|
|
42
|
+
|
|
43
|
+
# 丢弃统计
|
|
44
|
+
self._drop_count = 0
|
|
45
|
+
|
|
46
|
+
# 队列
|
|
47
|
+
self.queue = queue.Queue(maxsize=queue_size)
|
|
48
|
+
|
|
49
|
+
# 停止信号
|
|
50
|
+
self._stop_event = threading.Event()
|
|
51
|
+
|
|
52
|
+
# worker 线程
|
|
53
|
+
self.worker = threading.Thread(
|
|
54
|
+
target=self._worker,
|
|
55
|
+
name='BaseHandlerWorker',
|
|
56
|
+
daemon=True,
|
|
57
|
+
)
|
|
58
|
+
self.worker.start()
|
|
59
|
+
|
|
60
|
+
def emit(self, record):
|
|
61
|
+
"""logging入口"""
|
|
62
|
+
try:
|
|
63
|
+
payload = self.build_payload(record)
|
|
64
|
+
self.queue.put_nowait(payload)
|
|
65
|
+
|
|
66
|
+
except queue.Full:
|
|
67
|
+
self._drop_count += 1
|
|
68
|
+
|
|
69
|
+
if self._drop_count % 100 == 0:
|
|
70
|
+
self._internal_logger.warning(
|
|
71
|
+
'log dropped count=%s', self._drop_count
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
except Exception:
|
|
75
|
+
self.handleError(record)
|
|
76
|
+
|
|
77
|
+
def _worker(self):
|
|
78
|
+
"""worker主循环"""
|
|
79
|
+
buffer = []
|
|
80
|
+
last_flush = time.monotonic()
|
|
81
|
+
|
|
82
|
+
while not self._stop_event.is_set() or not self.queue.empty():
|
|
83
|
+
try:
|
|
84
|
+
log = self.queue.get(timeout=1)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
if log is None:
|
|
88
|
+
# 收到退出信号
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
buffer.append(log)
|
|
92
|
+
|
|
93
|
+
finally:
|
|
94
|
+
self.queue.task_done()
|
|
95
|
+
|
|
96
|
+
except queue.Empty:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
now = time.monotonic()
|
|
100
|
+
|
|
101
|
+
# flush条件:
|
|
102
|
+
# 1. 达到 batch_size
|
|
103
|
+
# 2. 超过 flush_interval
|
|
104
|
+
if (
|
|
105
|
+
len(buffer) >= self.batch_size
|
|
106
|
+
or (buffer and now - last_flush >= self.flush_interval)
|
|
107
|
+
):
|
|
108
|
+
self._flush(buffer[:])
|
|
109
|
+
buffer.clear()
|
|
110
|
+
last_flush = now
|
|
111
|
+
|
|
112
|
+
# 兜底flush
|
|
113
|
+
if buffer:
|
|
114
|
+
self._flush(buffer)
|
|
115
|
+
|
|
116
|
+
def _flush(self, logs):
|
|
117
|
+
"""
|
|
118
|
+
带重试的发送逻辑(指数退避)
|
|
119
|
+
"""
|
|
120
|
+
for i in range(self.retry + 1):
|
|
121
|
+
attempt = i + 1
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
self.send(logs)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self._handle_send_error(e, attempt=attempt)
|
|
129
|
+
|
|
130
|
+
if i < self.retry:
|
|
131
|
+
if self._stop_event.wait(2 ** i):
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
else:
|
|
135
|
+
self._handle_final_failure(e)
|
|
136
|
+
|
|
137
|
+
def _handle_send_error(self, error, attempt):
|
|
138
|
+
self._internal_logger.warning(
|
|
139
|
+
'send failed attempt=%s error=%s',
|
|
140
|
+
attempt,
|
|
141
|
+
error,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _handle_final_failure(self, error):
|
|
145
|
+
self._internal_logger.error(
|
|
146
|
+
'FINAL send failure error=%s',
|
|
147
|
+
error,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def build_payload(self, record):
|
|
151
|
+
"""构建日志结构"""
|
|
152
|
+
payload = {
|
|
153
|
+
'level': record.levelname,
|
|
154
|
+
'location': f'{record.module}.{record.funcName}({record.pathname}:{record.lineno})',
|
|
155
|
+
'log': self.format(record),
|
|
156
|
+
'message': record.getMessage(),
|
|
157
|
+
'thread': record.threadName,
|
|
158
|
+
'time': datetime.fromtimestamp(
|
|
159
|
+
record.created,
|
|
160
|
+
tz=timezone.utc,
|
|
161
|
+
).strftime('%Y-%m-%dT%H:%M:%S%z'),
|
|
162
|
+
'timestamp': record.created,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if record.exc_info:
|
|
166
|
+
payload['exc_info'] = ''.join(
|
|
167
|
+
traceback.format_exception(*record.exc_info)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return payload
|
|
171
|
+
|
|
172
|
+
def send(self, logs):
|
|
173
|
+
"""
|
|
174
|
+
子类必须实现
|
|
175
|
+
:param logs: List[dict]
|
|
176
|
+
"""
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
|
|
179
|
+
def close(self):
|
|
180
|
+
"""优雅关闭"""
|
|
181
|
+
# 停止接收新任务
|
|
182
|
+
self._stop_event.set()
|
|
183
|
+
|
|
184
|
+
# 发送退出信号
|
|
185
|
+
while True:
|
|
186
|
+
try:
|
|
187
|
+
self.queue.put(None, timeout=1)
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
except queue.Full:
|
|
191
|
+
time.sleep(0.1)
|
|
192
|
+
|
|
193
|
+
# 等待队列消费完成
|
|
194
|
+
self.queue.join()
|
|
195
|
+
|
|
196
|
+
# 等待线程退出
|
|
197
|
+
self.worker.join(timeout=5)
|
|
198
|
+
|
|
199
|
+
super().close()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from tencentcloud.log.cls_pb2 import LogGroupList, Log
|
|
4
|
+
from tencentcloud.log.logclient import LogClient
|
|
5
|
+
from tencentcloud.log.logexception import LogException
|
|
6
|
+
|
|
7
|
+
from pylogging_ext import BaseHandler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TencentCloudCLSHandler(BaseHandler):
|
|
11
|
+
"""腾讯云 CLS Handler"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
endpoint,
|
|
16
|
+
secret_id,
|
|
17
|
+
secret_key,
|
|
18
|
+
topic_id,
|
|
19
|
+
security_token=None,
|
|
20
|
+
source=None,
|
|
21
|
+
region='',
|
|
22
|
+
is_https=False,
|
|
23
|
+
timeout=3,
|
|
24
|
+
batch_size=10,
|
|
25
|
+
flush_interval=3,
|
|
26
|
+
level=logging.NOTSET,
|
|
27
|
+
queue_size=1000,
|
|
28
|
+
retry=2,
|
|
29
|
+
):
|
|
30
|
+
super().__init__(
|
|
31
|
+
level=level,
|
|
32
|
+
batch_size=batch_size,
|
|
33
|
+
flush_interval=flush_interval,
|
|
34
|
+
queue_size=queue_size,
|
|
35
|
+
retry=retry,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.client = LogClient(
|
|
39
|
+
endpoint,
|
|
40
|
+
secret_id,
|
|
41
|
+
secret_key,
|
|
42
|
+
securityToken=security_token,
|
|
43
|
+
source=source,
|
|
44
|
+
region=region,
|
|
45
|
+
is_https=is_https,
|
|
46
|
+
)
|
|
47
|
+
self.client.timeout = timeout
|
|
48
|
+
|
|
49
|
+
self.topic_id = topic_id
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def make_log(cls, log):
|
|
53
|
+
pb_log = Log()
|
|
54
|
+
pb_log.time = int(log.get('timestamp') * 1000000)
|
|
55
|
+
|
|
56
|
+
for key in [
|
|
57
|
+
'level',
|
|
58
|
+
'location',
|
|
59
|
+
'log',
|
|
60
|
+
'message',
|
|
61
|
+
'thread',
|
|
62
|
+
'time',
|
|
63
|
+
]:
|
|
64
|
+
content = pb_log.contents.add()
|
|
65
|
+
content.key = key
|
|
66
|
+
content.value = str(log.get(key, ''))
|
|
67
|
+
|
|
68
|
+
if log.get('exc_info'):
|
|
69
|
+
content = pb_log.contents.add()
|
|
70
|
+
content.key = 'exc_info'
|
|
71
|
+
content.value = str(log.get('exc_info'))
|
|
72
|
+
|
|
73
|
+
return pb_log
|
|
74
|
+
|
|
75
|
+
def send(self, logs):
|
|
76
|
+
"""批量发送到 CLS"""
|
|
77
|
+
log_group_list = LogGroupList()
|
|
78
|
+
log_group = log_group_list.logGroupList.add()
|
|
79
|
+
|
|
80
|
+
# metadata
|
|
81
|
+
log_group.filename = 'pylogging_ext.log'
|
|
82
|
+
log_group.source = getattr(self.client, '_source', '')
|
|
83
|
+
|
|
84
|
+
for log in logs:
|
|
85
|
+
log_group.logs.append(self.make_log(log))
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
self.client.put_log_raw(self.topic_id, log_group_list)
|
|
89
|
+
|
|
90
|
+
except LogException as e:
|
|
91
|
+
raise Exception(
|
|
92
|
+
f'CLS API error [{e.get_error_code()}]: {e.get_error_message()}'
|
|
93
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from requests import Session
|
|
4
|
+
|
|
5
|
+
from pylogging_ext import BaseHandler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _escape_html(text: str) -> str:
|
|
9
|
+
if not text:
|
|
10
|
+
return ''
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
str(text).strip()
|
|
14
|
+
.replace('&', '&')
|
|
15
|
+
.replace('<', '<')
|
|
16
|
+
.replace('>', '>')
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TelegramHandler(BaseHandler):
|
|
21
|
+
"""Telegram机器人 Handler"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
token,
|
|
26
|
+
chat_id,
|
|
27
|
+
timeout=3,
|
|
28
|
+
level=logging.NOTSET,
|
|
29
|
+
queue_size=1000,
|
|
30
|
+
retry=2,
|
|
31
|
+
proxies=None,
|
|
32
|
+
ssl_verify=True,
|
|
33
|
+
endpoint='https://api.telegram.org',
|
|
34
|
+
):
|
|
35
|
+
super().__init__(
|
|
36
|
+
level=level,
|
|
37
|
+
batch_size=1,
|
|
38
|
+
queue_size=queue_size,
|
|
39
|
+
retry=retry,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self.chat_id = chat_id
|
|
43
|
+
self.api_url = f'{endpoint}/bot{token}/sendMessage'
|
|
44
|
+
|
|
45
|
+
self.session = Session()
|
|
46
|
+
|
|
47
|
+
if proxies:
|
|
48
|
+
self.session.proxies.update(proxies)
|
|
49
|
+
|
|
50
|
+
self.session.verify = ssl_verify
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
|
|
53
|
+
def make_html_message(self, log):
|
|
54
|
+
"""
|
|
55
|
+
构建 Telegram HTML 消息
|
|
56
|
+
|
|
57
|
+
- 标题:level + message
|
|
58
|
+
- 正文:log / exc_info
|
|
59
|
+
- 附加信息:thread / location
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
level = log.get('level', 'INFO')
|
|
63
|
+
message = _escape_html(log.get('message'))
|
|
64
|
+
|
|
65
|
+
# 标题
|
|
66
|
+
content = f'<b>{level}: {message}</b>\n\n'
|
|
67
|
+
|
|
68
|
+
# 优先展示异常
|
|
69
|
+
if log.get('exc_info'):
|
|
70
|
+
content += (
|
|
71
|
+
f'<pre>'
|
|
72
|
+
f'{_escape_html(log["exc_info"])}'
|
|
73
|
+
f'</pre>'
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
content += (
|
|
77
|
+
f'<pre>'
|
|
78
|
+
f'{_escape_html(log["log"])}'
|
|
79
|
+
f'</pre>'
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 附加信息
|
|
83
|
+
content += (
|
|
84
|
+
f'\n '
|
|
85
|
+
f'\n <i>Thread: {log["thread"]}</i>'
|
|
86
|
+
f'\n <i>Location: {log["location"]}</i>'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
'chat_id': self.chat_id,
|
|
91
|
+
'text': content,
|
|
92
|
+
'parse_mode': 'HTML',
|
|
93
|
+
'disable_web_page_preview': True,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def send(self, logs):
|
|
97
|
+
"""发送到 Telegram"""
|
|
98
|
+
resp = self.session.post(
|
|
99
|
+
self.api_url,
|
|
100
|
+
timeout=self.timeout,
|
|
101
|
+
json=self.make_html_message(logs[0]),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# HTTP 错误
|
|
105
|
+
resp.raise_for_status()
|
|
106
|
+
|
|
107
|
+
result = resp.json()
|
|
108
|
+
|
|
109
|
+
# Telegram 返回错误
|
|
110
|
+
if not result.get('ok', False):
|
|
111
|
+
raise Exception('Telegram API error [{error_code}]: {description}'.format_map(result))
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from pylogging_ext import BaseHandler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _safe_text(text):
|
|
9
|
+
if not text:
|
|
10
|
+
return ''
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
str(text).strip()
|
|
14
|
+
.replace('\\', '\\\\')
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WeChatHandler(BaseHandler):
|
|
19
|
+
"""企业微信机器人 Handler"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
webhook,
|
|
24
|
+
timeout=3,
|
|
25
|
+
level=logging.NOTSET,
|
|
26
|
+
queue_size=1000,
|
|
27
|
+
retry=2,
|
|
28
|
+
):
|
|
29
|
+
super().__init__(
|
|
30
|
+
level=level,
|
|
31
|
+
batch_size=1,
|
|
32
|
+
queue_size=queue_size,
|
|
33
|
+
retry=retry,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.webhook = webhook
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def make_markdown_message(cls, log):
|
|
41
|
+
"""
|
|
42
|
+
构建企业微信 Markdown 消息
|
|
43
|
+
|
|
44
|
+
- 标题:level + message
|
|
45
|
+
- 正文:log / exc_info
|
|
46
|
+
- 附加信息:thread / location
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
level = log.get('level', 'INFO')
|
|
50
|
+
message = _safe_text(log.get('message'))
|
|
51
|
+
|
|
52
|
+
# 标题
|
|
53
|
+
content = f'**{level}: {message}**\n\n'
|
|
54
|
+
|
|
55
|
+
# 优先展示异常
|
|
56
|
+
if log.get('exc_info'):
|
|
57
|
+
content += (
|
|
58
|
+
f'\n> '
|
|
59
|
+
f'<font color=\"info\">'
|
|
60
|
+
f'{_safe_text(log["exc_info"])}'
|
|
61
|
+
f'</font>'
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
content += (
|
|
65
|
+
f'\n> '
|
|
66
|
+
f'<font color=\"info\">'
|
|
67
|
+
f'{_safe_text(log["log"])}'
|
|
68
|
+
f'</font>'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# 附加信息
|
|
72
|
+
content += (
|
|
73
|
+
f'\n> '
|
|
74
|
+
f'\n> <font color=\"comment\">Thread: {log["thread"]}</font>'
|
|
75
|
+
f'\n> <font color=\"warning\">Location: {_safe_text(log["location"])}</font>'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
'msgtype': 'markdown',
|
|
80
|
+
'markdown': {
|
|
81
|
+
'content': content
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def send(self, logs):
|
|
86
|
+
"""发送到企业微信"""
|
|
87
|
+
resp = requests.post(
|
|
88
|
+
self.webhook,
|
|
89
|
+
timeout=self.timeout,
|
|
90
|
+
json=self.make_markdown_message(logs[0]),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# HTTP 错误
|
|
94
|
+
resp.raise_for_status()
|
|
95
|
+
|
|
96
|
+
result = resp.json()
|
|
97
|
+
|
|
98
|
+
# 企业微信返回错误
|
|
99
|
+
if result.get('errcode') != 0:
|
|
100
|
+
raise Exception('WeChat API error [{errcode}]: {errmsg}'.format_map(result))
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pylogging-ext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 一个支持 异步、批量、重试、多通道通知 的 logging 扩展库。
|
|
5
|
+
Home-page: https://github.com/cnmax/pylogging-ext
|
|
6
|
+
Author: chao
|
|
7
|
+
Author-email: mr.qchao@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/cnmax/pylogging-ext
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/cnmax/pylogging-ext/issues
|
|
11
|
+
Project-URL: Source Code, https://github.com/cnmax/pylogging-ext
|
|
12
|
+
Keywords: logging,async logging,handler,cls,tencent cloud,wechat,telegram,notification,monitoring
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Topic :: System :: Logging
|
|
26
|
+
Classifier: Topic :: System :: Monitoring
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: requests
|
|
31
|
+
Requires-Dist: tencentcloud-cls-sdk-python
|
|
32
|
+
Provides-Extra: socks
|
|
33
|
+
Requires-Dist: requests[socks]; extra == "socks"
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: author-email
|
|
36
|
+
Dynamic: classifier
|
|
37
|
+
Dynamic: description
|
|
38
|
+
Dynamic: description-content-type
|
|
39
|
+
Dynamic: home-page
|
|
40
|
+
Dynamic: keywords
|
|
41
|
+
Dynamic: license
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
Dynamic: project-url
|
|
44
|
+
Dynamic: provides-extra
|
|
45
|
+
Dynamic: requires-dist
|
|
46
|
+
Dynamic: summary
|
|
47
|
+
|
|
48
|
+
# pylogging-ext
|
|
49
|
+
|
|
50
|
+
[](https://pypi.python.org/pypi/pylogging-ext)
|
|
51
|
+

|
|
52
|
+

|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
一个支持 异步、批量、重试、多通道通知 的 logging 扩展库。
|
|
56
|
+
|
|
57
|
+
> 不只是 logging handler,更是通知基础设施
|
|
58
|
+
|
|
59
|
+
## 安装
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install pylogging-ext
|
|
63
|
+
```
|
|
64
|
+
可选(支持 socks 代理):
|
|
65
|
+
```bash
|
|
66
|
+
pip install pylogging-ext[socks]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 使用
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import logging
|
|
73
|
+
from pylogging_ext import TelegramHandler
|
|
74
|
+
|
|
75
|
+
logger = logging.getLogger('demo')
|
|
76
|
+
logger.setLevel(logging.INFO)
|
|
77
|
+
|
|
78
|
+
handler = TelegramHandler(
|
|
79
|
+
token='your-bot-token',
|
|
80
|
+
chat_id='your-chat-id',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
logger.addHandler(handler)
|
|
84
|
+
|
|
85
|
+
logger.error('发生错误了')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## API
|
|
89
|
+
|
|
90
|
+
### Class: BaseHandler
|
|
91
|
+
|
|
92
|
+
通用异步 Handler 基类,所有通知 Handler 均继承自此类。
|
|
93
|
+
|
|
94
|
+
#### BaseHandler(...)
|
|
95
|
+
|
|
96
|
+
- `batch_size` 批量发送大小
|
|
97
|
+
- `flush_interval` 刷新间隔 (秒)
|
|
98
|
+
- `queue_size` 队列大小
|
|
99
|
+
- `retry` 重试次数
|
|
100
|
+
|
|
101
|
+
#### 特性:
|
|
102
|
+
- 异步发送(非阻塞)
|
|
103
|
+
- 批量处理
|
|
104
|
+
- 指数退避重试
|
|
105
|
+
- 优雅关闭
|
|
106
|
+
|
|
107
|
+
## 腾讯云 CLS
|
|
108
|
+
### Class: TencentCloudCLSHandler
|
|
109
|
+
|
|
110
|
+
```pycon
|
|
111
|
+
from pylogging_ext import TencentCloudCLSHandler
|
|
112
|
+
|
|
113
|
+
handler = TencentCloudCLSHandler(
|
|
114
|
+
endpoint='ap-shanghai.cls.tencentcs.com',
|
|
115
|
+
secret_id='your-secret-id',
|
|
116
|
+
secret_key='your-secret-key',
|
|
117
|
+
topic_id='your-topic-id',
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
### 参数
|
|
121
|
+
- `endpoint` CLS接入地址
|
|
122
|
+
- `secret_id` 腾讯云密钥
|
|
123
|
+
- `secret_key` 腾讯云密钥
|
|
124
|
+
- `topic_id` 日志主题 ID
|
|
125
|
+
|
|
126
|
+
## 微信机器人
|
|
127
|
+
### Class: WeChatHandler
|
|
128
|
+
|
|
129
|
+
```pycon
|
|
130
|
+
from pylogging_ext import WeChatHandler
|
|
131
|
+
|
|
132
|
+
handler = WeChatHandler(
|
|
133
|
+
webhook='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx'
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 参数
|
|
138
|
+
- `webhook` 企业微信机器人地址
|
|
139
|
+
|
|
140
|
+
## Telegram
|
|
141
|
+
### Class: TelegramHandler
|
|
142
|
+
|
|
143
|
+
```pycon
|
|
144
|
+
from pylogging_ext import TelegramHandler
|
|
145
|
+
|
|
146
|
+
handler = TelegramHandler(
|
|
147
|
+
token='your-bot-token',
|
|
148
|
+
chat_id='your-chat-id',
|
|
149
|
+
proxies={
|
|
150
|
+
'http': 'http://127.0.0.1:7890',
|
|
151
|
+
'https': 'http://127.0.0.1:7890',
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
### 参数
|
|
156
|
+
- `token` Telegram Bot Token
|
|
157
|
+
- `chat_id` 聊天 ID
|
|
158
|
+
- `proxies` 代理配置(可选)
|
|
159
|
+
- `ssl_verify` SSL校验 (默认 True)
|
|
160
|
+
|
|
161
|
+
## 支持的通知渠道
|
|
162
|
+
- 腾讯云 CLS
|
|
163
|
+
- 企业微信
|
|
164
|
+
- Telegram
|
|
165
|
+
|
|
166
|
+
持续扩展中...
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
requirements.txt
|
|
5
|
+
setup.py
|
|
6
|
+
pylogging_ext/__init__.py
|
|
7
|
+
pylogging_ext/base.py
|
|
8
|
+
pylogging_ext/cls.py
|
|
9
|
+
pylogging_ext/dingtalk.py
|
|
10
|
+
pylogging_ext/telegram.py
|
|
11
|
+
pylogging_ext/wechat.py
|
|
12
|
+
pylogging_ext.egg-info/PKG-INFO
|
|
13
|
+
pylogging_ext.egg-info/SOURCES.txt
|
|
14
|
+
pylogging_ext.egg-info/dependency_links.txt
|
|
15
|
+
pylogging_ext.egg-info/requires.txt
|
|
16
|
+
pylogging_ext.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pylogging_ext
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open('README.md', 'r', encoding='utf-8') as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name='pylogging-ext',
|
|
8
|
+
version='0.1.0',
|
|
9
|
+
author='chao',
|
|
10
|
+
author_email='mr.qchao@gmail.com',
|
|
11
|
+
description='一个支持 异步、批量、重试、多通道通知 的 logging 扩展库。',
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type='text/markdown',
|
|
14
|
+
url='https://github.com/cnmax/pylogging-ext',
|
|
15
|
+
project_urls={
|
|
16
|
+
'Homepage': 'https://github.com/cnmax/pylogging-ext',
|
|
17
|
+
'Bug Tracker': 'https://github.com/cnmax/pylogging-ext/issues',
|
|
18
|
+
'Source Code': "https://github.com/cnmax/pylogging-ext",
|
|
19
|
+
},
|
|
20
|
+
classifiers=[
|
|
21
|
+
'Programming Language :: Python :: 3',
|
|
22
|
+
'Programming Language :: Python :: 3.6',
|
|
23
|
+
'Programming Language :: Python :: 3.7',
|
|
24
|
+
'Programming Language :: Python :: 3.8',
|
|
25
|
+
'Programming Language :: Python :: 3.9',
|
|
26
|
+
'Programming Language :: Python :: 3.10',
|
|
27
|
+
'Programming Language :: Python :: 3.11',
|
|
28
|
+
'Programming Language :: Python :: 3.12',
|
|
29
|
+
'Programming Language :: Python :: 3.13',
|
|
30
|
+
'Programming Language :: Python :: 3.14',
|
|
31
|
+
'License :: OSI Approved :: MIT License',
|
|
32
|
+
'Operating System :: OS Independent',
|
|
33
|
+
'Topic :: System :: Logging',
|
|
34
|
+
'Topic :: System :: Monitoring',
|
|
35
|
+
'Topic :: Software Development :: Libraries',
|
|
36
|
+
],
|
|
37
|
+
keywords=[
|
|
38
|
+
'logging',
|
|
39
|
+
'async logging',
|
|
40
|
+
'handler',
|
|
41
|
+
'cls',
|
|
42
|
+
'tencent cloud',
|
|
43
|
+
'wechat',
|
|
44
|
+
'telegram',
|
|
45
|
+
'notification',
|
|
46
|
+
'monitoring',
|
|
47
|
+
],
|
|
48
|
+
license='MIT',
|
|
49
|
+
packages=find_packages(exclude=('tests', 'tests.*')),
|
|
50
|
+
install_requires=[
|
|
51
|
+
'requests',
|
|
52
|
+
'tencentcloud-cls-sdk-python',
|
|
53
|
+
],
|
|
54
|
+
extras_require={
|
|
55
|
+
'socks': ['requests[socks]'],
|
|
56
|
+
},
|
|
57
|
+
)
|