pycommonlog 0.2.0__py3-none-any.whl → 0.2.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.
- pycommonlog/log_types.py +10 -0
- pycommonlog/logger.py +5 -4
- pycommonlog/providers/__init__.py +7 -0
- pycommonlog/providers/lark.py +279 -0
- pycommonlog/providers/redis_client.py +51 -0
- pycommonlog/providers/slack.py +103 -0
- {pycommonlog-0.2.0.dist-info → pycommonlog-0.2.2.dist-info}/METADATA +44 -27
- pycommonlog-0.2.2.dist-info/RECORD +13 -0
- pycommonlog-0.2.0.dist-info/RECORD +0 -9
- {pycommonlog-0.2.0.dist-info → pycommonlog-0.2.2.dist-info}/WHEEL +0 -0
- {pycommonlog-0.2.0.dist-info → pycommonlog-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {pycommonlog-0.2.0.dist-info → pycommonlog-0.2.2.dist-info}/top_level.txt +0 -0
pycommonlog/log_types.py
CHANGED
|
@@ -49,6 +49,16 @@ class Config:
|
|
|
49
49
|
self.environment = environment
|
|
50
50
|
self.provider_config = provider_config or {}
|
|
51
51
|
self.debug = debug
|
|
52
|
+
|
|
53
|
+
# Populate provider_config with top-level fields for consistency, only if top-level is set
|
|
54
|
+
if self.provider:
|
|
55
|
+
self.provider_config["provider"] = self.provider
|
|
56
|
+
if self.token:
|
|
57
|
+
self.provider_config["token"] = self.token
|
|
58
|
+
if self.slack_token:
|
|
59
|
+
self.provider_config["slack_token"] = self.slack_token
|
|
60
|
+
if self.lark_token and (self.lark_token.app_id or self.lark_token.app_secret):
|
|
61
|
+
self.provider_config["lark_token"] = self.lark_token
|
|
52
62
|
|
|
53
63
|
class Provider(ABC):
|
|
54
64
|
@abstractmethod
|
pycommonlog/logger.py
CHANGED
|
@@ -104,15 +104,16 @@ class commonlog:
|
|
|
104
104
|
|
|
105
105
|
def __init__(self, config):
|
|
106
106
|
self.config = config
|
|
107
|
-
|
|
107
|
+
provider_name = config.provider_config.get("provider", "slack")
|
|
108
|
+
if provider_name == "slack":
|
|
108
109
|
self.provider = SlackProvider()
|
|
109
|
-
elif
|
|
110
|
+
elif provider_name == "lark":
|
|
110
111
|
self.provider = LarkProvider()
|
|
111
112
|
else:
|
|
112
|
-
logging.warning(f"Unknown provider: {
|
|
113
|
+
logging.warning(f"Unknown provider: {provider_name}, defaulting to Slack")
|
|
113
114
|
self.provider = SlackProvider()
|
|
114
115
|
|
|
115
|
-
debug_log(config, f"Created logger with provider: {
|
|
116
|
+
debug_log(config, f"Created logger with provider: {provider_name}, send method: {config.send_method}, debug: {config.debug}")
|
|
116
117
|
|
|
117
118
|
def _resolve_channel(self, level):
|
|
118
119
|
if self.config.channel_resolver:
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lark Provider for commonlog
|
|
3
|
+
"""
|
|
4
|
+
import requests
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Dict, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
# Add directories to path for imports
|
|
13
|
+
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
_parent_dir = os.path.dirname(_current_dir)
|
|
15
|
+
if _parent_dir not in sys.path:
|
|
16
|
+
sys.path.insert(0, _parent_dir)
|
|
17
|
+
if _current_dir not in sys.path:
|
|
18
|
+
sys.path.insert(0, _current_dir)
|
|
19
|
+
|
|
20
|
+
from log_types import SendMethod, Provider, debug_log
|
|
21
|
+
from redis_client import get_redis_client, RedisConfigError
|
|
22
|
+
from cache import get_memory_cache
|
|
23
|
+
|
|
24
|
+
class LarkProvider(Provider):
|
|
25
|
+
def send_to_channel(self, level, message, attachment, config, channel):
|
|
26
|
+
original_channel = config.channel
|
|
27
|
+
config.channel = channel
|
|
28
|
+
title, formatted_message = self._format_message(message, attachment, config)
|
|
29
|
+
if config.send_method == SendMethod.WEBCLIENT:
|
|
30
|
+
self._send_lark_webclient(title, formatted_message, config)
|
|
31
|
+
elif config.send_method == SendMethod.WEBHOOK:
|
|
32
|
+
self._send_lark_webhook(title, formatted_message, config)
|
|
33
|
+
config.channel = original_channel
|
|
34
|
+
|
|
35
|
+
def cache_lark_token(self, config, app_id, app_secret, token, expire):
|
|
36
|
+
key = f"commonlog_lark_token:{app_id}:{app_secret}"
|
|
37
|
+
try:
|
|
38
|
+
client = get_redis_client(config)
|
|
39
|
+
expire_seconds = expire - 600
|
|
40
|
+
if expire_seconds <= 0:
|
|
41
|
+
expire_seconds = 60
|
|
42
|
+
client.setex(key, expire_seconds, token)
|
|
43
|
+
debug_log(config, f"Lark token cached in Redis for key: {key}")
|
|
44
|
+
except RedisConfigError:
|
|
45
|
+
# Fallback to in-memory cache
|
|
46
|
+
expire_seconds = expire - 600
|
|
47
|
+
if expire_seconds <= 0:
|
|
48
|
+
expire_seconds = 60
|
|
49
|
+
get_memory_cache().set(key, token, expire_seconds)
|
|
50
|
+
debug_log(config, f"Lark token cached in memory for key: {key}")
|
|
51
|
+
|
|
52
|
+
def get_cached_lark_token(self, config, app_id, app_secret):
|
|
53
|
+
key = f"commonlog_lark_token:{app_id}:{app_secret}"
|
|
54
|
+
try:
|
|
55
|
+
client = get_redis_client(config)
|
|
56
|
+
token = client.get(key)
|
|
57
|
+
if token:
|
|
58
|
+
debug_log(config, f"Lark token retrieved from Redis for key: {key}")
|
|
59
|
+
return token
|
|
60
|
+
except RedisConfigError:
|
|
61
|
+
# Fallback to in-memory cache
|
|
62
|
+
token = get_memory_cache().get(key)
|
|
63
|
+
if token:
|
|
64
|
+
debug_log(config, f"Lark token retrieved from memory for key: {key}")
|
|
65
|
+
return token
|
|
66
|
+
|
|
67
|
+
def cache_chat_id(self, config, channel_name, chat_id):
|
|
68
|
+
key = f"commonlog_lark_chat_id:{config.environment}:{channel_name}"
|
|
69
|
+
try:
|
|
70
|
+
client = get_redis_client(config)
|
|
71
|
+
client.set(key, chat_id) # No expiry
|
|
72
|
+
debug_log(config, f"Lark chat ID cached in Redis for key: {key}")
|
|
73
|
+
except RedisConfigError:
|
|
74
|
+
# Fallback to in-memory cache (no expiry for chat IDs)
|
|
75
|
+
get_memory_cache().set(key, chat_id, 86400 * 30) # 30 days expiry
|
|
76
|
+
debug_log(config, f"Lark chat ID cached in memory for key: {key}")
|
|
77
|
+
|
|
78
|
+
def get_cached_chat_id(self, config, channel_name):
|
|
79
|
+
key = f"commonlog_lark_chat_id:{config.environment}:{channel_name}"
|
|
80
|
+
try:
|
|
81
|
+
client = get_redis_client(config)
|
|
82
|
+
chat_id = client.get(key)
|
|
83
|
+
if chat_id:
|
|
84
|
+
debug_log(config, f"Lark chat ID retrieved from Redis for key: {key}")
|
|
85
|
+
return chat_id
|
|
86
|
+
except RedisConfigError:
|
|
87
|
+
# Fallback to in-memory cache
|
|
88
|
+
chat_id = get_memory_cache().get(key)
|
|
89
|
+
if chat_id:
|
|
90
|
+
debug_log(config, f"Lark chat ID retrieved from memory for key: {key}")
|
|
91
|
+
return chat_id
|
|
92
|
+
|
|
93
|
+
def get_tenant_access_token(self, config, app_id, app_secret):
|
|
94
|
+
cached = self.get_cached_lark_token(config, app_id, app_secret)
|
|
95
|
+
if cached:
|
|
96
|
+
return cached
|
|
97
|
+
url = "https://open.larksuite.com/open-apis/auth/v3/tenant_access_token/internal"
|
|
98
|
+
payload = {"app_id": app_id, "app_secret": app_secret}
|
|
99
|
+
response = requests.post(url, json=payload)
|
|
100
|
+
result = response.json()
|
|
101
|
+
if result.get("code", 1) != 0:
|
|
102
|
+
raise Exception(f"lark token error: {result.get('msg')}")
|
|
103
|
+
token = result.get("tenant_access_token")
|
|
104
|
+
expire = result.get("expire", 0)
|
|
105
|
+
self.cache_lark_token(config, app_id, app_secret, token, expire)
|
|
106
|
+
return token
|
|
107
|
+
|
|
108
|
+
def get_chat_id_from_channel_name(self, config, token, channel_name):
|
|
109
|
+
"""Get chat_id from channel name using Lark API with pagination"""
|
|
110
|
+
# Try Redis cache first
|
|
111
|
+
cached = self.get_cached_chat_id(config, channel_name)
|
|
112
|
+
if cached:
|
|
113
|
+
return cached
|
|
114
|
+
|
|
115
|
+
base_url = "https://open.larksuite.com/open-apis/im/v1/chats"
|
|
116
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
117
|
+
|
|
118
|
+
all_chats = []
|
|
119
|
+
page_token = ""
|
|
120
|
+
has_more = True
|
|
121
|
+
|
|
122
|
+
while has_more:
|
|
123
|
+
url = f"{base_url}?page_size=10"
|
|
124
|
+
if page_token:
|
|
125
|
+
url += f"&page_token={page_token}"
|
|
126
|
+
|
|
127
|
+
response = requests.get(url, headers=headers)
|
|
128
|
+
if response.status_code != 200:
|
|
129
|
+
raise Exception(f"Lark chats API response: {response.status_code}")
|
|
130
|
+
|
|
131
|
+
result = response.json()
|
|
132
|
+
|
|
133
|
+
# Check for API error
|
|
134
|
+
if result.get("code", 1) != 0:
|
|
135
|
+
raise Exception(f"Lark API error: {result.get('msg', 'Unknown error')}")
|
|
136
|
+
|
|
137
|
+
data = result.get("data", {})
|
|
138
|
+
items = data.get("items", [])
|
|
139
|
+
|
|
140
|
+
# Add current page items to all chats
|
|
141
|
+
all_chats.extend(items)
|
|
142
|
+
|
|
143
|
+
# Update pagination info
|
|
144
|
+
page_token = data.get("page_token", "")
|
|
145
|
+
has_more = data.get("has_more", False)
|
|
146
|
+
|
|
147
|
+
# Find the chat with matching name
|
|
148
|
+
for item in all_chats:
|
|
149
|
+
if item.get("name") == channel_name:
|
|
150
|
+
chat_id = item.get("chat_id")
|
|
151
|
+
# Cache the chat_id without expiry
|
|
152
|
+
self.cache_chat_id(config, channel_name, chat_id)
|
|
153
|
+
return chat_id
|
|
154
|
+
|
|
155
|
+
raise Exception(f"Channel '{channel_name}' not found")
|
|
156
|
+
|
|
157
|
+
def send(self, level, message, attachment, config):
|
|
158
|
+
debug_log(config, f"LarkProvider.send called with level: {level}, send method: {config.send_method}")
|
|
159
|
+
title, formatted_message = self._format_message(message, attachment, config)
|
|
160
|
+
if config.send_method == SendMethod.WEBCLIENT:
|
|
161
|
+
debug_log(config, "Using Lark webclient method")
|
|
162
|
+
self._send_lark_webclient(title, formatted_message, config)
|
|
163
|
+
elif config.send_method == SendMethod.WEBHOOK:
|
|
164
|
+
debug_log(config, "Using Lark webhook method")
|
|
165
|
+
self._send_lark_webhook(title, formatted_message, config)
|
|
166
|
+
else:
|
|
167
|
+
error_msg = f"Unknown send method for Lark: {config.send_method}"
|
|
168
|
+
debug_log(config, f"Error: {error_msg}")
|
|
169
|
+
raise ValueError(error_msg)
|
|
170
|
+
|
|
171
|
+
def _format_message(self, message, attachment, config):
|
|
172
|
+
# Extract title from service and environment
|
|
173
|
+
title = "Alert"
|
|
174
|
+
if config.service_name and config.environment:
|
|
175
|
+
title = f"{config.service_name} - {config.environment}"
|
|
176
|
+
elif config.service_name:
|
|
177
|
+
title = config.service_name
|
|
178
|
+
elif config.environment:
|
|
179
|
+
title = config.environment
|
|
180
|
+
|
|
181
|
+
# Format message content without the header
|
|
182
|
+
formatted = message
|
|
183
|
+
if attachment and attachment.content:
|
|
184
|
+
filename = attachment.file_name or "Trace Logs"
|
|
185
|
+
formatted += f"\n\n**{filename}:**\n```\n{attachment.content}\n```"
|
|
186
|
+
if attachment and attachment.url:
|
|
187
|
+
formatted += f"\n\n**Attachment:** {attachment.url}"
|
|
188
|
+
return title, json.dumps(formatted)
|
|
189
|
+
|
|
190
|
+
def _send_lark_webclient(self, title, formatted_message, config):
|
|
191
|
+
debug_log(config, "send_lark_webclient: preparing API request")
|
|
192
|
+
token = config.provider_config.get("token", "")
|
|
193
|
+
|
|
194
|
+
# Use lark_token if available, otherwise fall back to token parsing
|
|
195
|
+
lark_token = config.provider_config.get("lark_token")
|
|
196
|
+
if lark_token and lark_token.app_id and lark_token.app_secret:
|
|
197
|
+
debug_log(config, "send_lark_webclient: fetching tenant access token using lark_token")
|
|
198
|
+
token = self.get_tenant_access_token(config, lark_token.app_id, lark_token.app_secret)
|
|
199
|
+
debug_log(config, "send_lark_webclient: tenant access token fetched")
|
|
200
|
+
elif token and len(token) < 100 and "++" in token:
|
|
201
|
+
# If token is in "app_id++app_secret" format, fetch the tenant_access_token
|
|
202
|
+
debug_log(config, "send_lark_webclient: parsing token in app_id++app_secret format")
|
|
203
|
+
parts = token.split("++")
|
|
204
|
+
if len(parts) == 2:
|
|
205
|
+
token = self.get_tenant_access_token(config, parts[0], parts[1])
|
|
206
|
+
debug_log(config, "send_lark_webclient: tenant access token fetched from parsed token")
|
|
207
|
+
|
|
208
|
+
# Get chat_id from channel name
|
|
209
|
+
debug_log(config, f"send_lark_webclient: resolving chat_id for channel '{config.channel}'")
|
|
210
|
+
chat_id = self.get_chat_id_from_channel_name(config, token, config.channel)
|
|
211
|
+
debug_log(config, f"send_lark_webclient: resolved chat_id")
|
|
212
|
+
|
|
213
|
+
url = "https://open.larksuite.com/open-apis/im/v1/messages?receive_id_type=chat_id"
|
|
214
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
215
|
+
payload = {
|
|
216
|
+
"receive_id": chat_id,
|
|
217
|
+
"msg_type": "post",
|
|
218
|
+
"content": {
|
|
219
|
+
"post": {
|
|
220
|
+
"zh_cn": {
|
|
221
|
+
"title": title,
|
|
222
|
+
"content": [
|
|
223
|
+
[
|
|
224
|
+
{
|
|
225
|
+
"tag": "text",
|
|
226
|
+
"text": formatted_message
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
debug_log(config, f"send_lark_webclient: sending HTTP request, payload size: {len(str(payload))}, payload: {json.dumps(payload)}")
|
|
235
|
+
|
|
236
|
+
response = requests.post(url, headers=headers, json=payload)
|
|
237
|
+
debug_log(config, f"send_lark_webclient: response status: {response.status_code}")
|
|
238
|
+
if response.status_code != 200:
|
|
239
|
+
error_msg = f"Lark WebClient response: {response.status_code}"
|
|
240
|
+
debug_log(config, f"send_lark_webclient: error: {error_msg}")
|
|
241
|
+
raise Exception(error_msg)
|
|
242
|
+
debug_log(config, "send_lark_webclient: message sent successfully")
|
|
243
|
+
|
|
244
|
+
def _send_lark_webhook(self, title, formatted_message, config):
|
|
245
|
+
debug_log(config, "send_lark_webhook: preparing webhook request")
|
|
246
|
+
# For webhook, the token field contains the webhook URL
|
|
247
|
+
webhook_url = config.token
|
|
248
|
+
if not webhook_url:
|
|
249
|
+
error_msg = "Webhook URL is required for Lark webhook method"
|
|
250
|
+
debug_log(config, f"Error: {error_msg}")
|
|
251
|
+
raise Exception(error_msg)
|
|
252
|
+
|
|
253
|
+
debug_log(config, "send_lark_webhook: using webhook URL")
|
|
254
|
+
payload = {
|
|
255
|
+
"msg_type": "post",
|
|
256
|
+
"content": {
|
|
257
|
+
"post": {
|
|
258
|
+
"zh_cn": {
|
|
259
|
+
"title": title,
|
|
260
|
+
"content": [
|
|
261
|
+
[
|
|
262
|
+
{
|
|
263
|
+
"tag": "text",
|
|
264
|
+
"text": formatted_message
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
debug_log(config, f"send_lark_webhook: payload prepared, size: {len(str(payload))}, payload: {json.dumps(payload)}")
|
|
273
|
+
response = requests.post(webhook_url, json=payload)
|
|
274
|
+
debug_log(config, f"send_lark_webhook: response status: {response.status_code}, response data: {response.text}")
|
|
275
|
+
if response.status_code != 200:
|
|
276
|
+
error_msg = f"Lark webhook response: {response.status_code}"
|
|
277
|
+
debug_log(config, f"send_lark_webhook: error: {error_msg}")
|
|
278
|
+
raise Exception(error_msg)
|
|
279
|
+
debug_log(config, "send_lark_webhook: webhook sent successfully")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis client for commonlog (Python)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class RedisConfigError(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
def get_redis_client(config):
|
|
9
|
+
import redis # Import lazily to avoid distutils issues in Python 3.12+
|
|
10
|
+
provider_config = getattr(config, 'provider_config', {})
|
|
11
|
+
host = provider_config.get('redis_host')
|
|
12
|
+
port = provider_config.get('redis_port')
|
|
13
|
+
password = provider_config.get('redis_password')
|
|
14
|
+
ssl = provider_config.get('redis_ssl', False)
|
|
15
|
+
cluster_mode = provider_config.get('redis_cluster_mode', False)
|
|
16
|
+
db = provider_config.get('redis_db', 0)
|
|
17
|
+
|
|
18
|
+
if not host or not port:
|
|
19
|
+
raise RedisConfigError("redis_host and redis_port must be set in provider_config")
|
|
20
|
+
|
|
21
|
+
if cluster_mode:
|
|
22
|
+
# Use RedisCluster for cluster mode (ElastiCache with cluster mode enabled)
|
|
23
|
+
try:
|
|
24
|
+
from redis.cluster import RedisCluster
|
|
25
|
+
except ImportError:
|
|
26
|
+
raise RedisConfigError("Redis cluster support is not available. Please upgrade redis package to version 4.0.0 or later")
|
|
27
|
+
|
|
28
|
+
# For cluster mode, we need to handle startup nodes differently
|
|
29
|
+
# ElastiCache cluster mode provides a single endpoint
|
|
30
|
+
startup_nodes = [{"host": host, "port": int(port)}]
|
|
31
|
+
|
|
32
|
+
return RedisCluster(
|
|
33
|
+
startup_nodes=startup_nodes,
|
|
34
|
+
password=password,
|
|
35
|
+
ssl=ssl,
|
|
36
|
+
decode_responses=True,
|
|
37
|
+
skip_full_coverage_check=True, # Allow partial cluster access
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
# Standard Redis client for single node or ElastiCache without cluster mode
|
|
41
|
+
return redis.StrictRedis(
|
|
42
|
+
host=host,
|
|
43
|
+
port=int(port),
|
|
44
|
+
password=password,
|
|
45
|
+
db=int(db),
|
|
46
|
+
ssl=ssl,
|
|
47
|
+
decode_responses=True,
|
|
48
|
+
socket_connect_timeout=5,
|
|
49
|
+
socket_timeout=5,
|
|
50
|
+
retry_on_timeout=True,
|
|
51
|
+
)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slack Provider for commonlog
|
|
3
|
+
"""
|
|
4
|
+
import requests
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
# Add parent directory to path for imports
|
|
9
|
+
_parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
10
|
+
if _parent_dir not in sys.path:
|
|
11
|
+
sys.path.insert(0, _parent_dir)
|
|
12
|
+
|
|
13
|
+
from log_types import SendMethod, Provider, debug_log
|
|
14
|
+
|
|
15
|
+
class SlackProvider(Provider):
|
|
16
|
+
def send_to_channel(self, level, message, attachment, config, channel):
|
|
17
|
+
original_channel = config.channel
|
|
18
|
+
config.channel = channel
|
|
19
|
+
self.send(level, message, attachment, config)
|
|
20
|
+
config.channel = original_channel
|
|
21
|
+
|
|
22
|
+
def send(self, level, message, attachment, config):
|
|
23
|
+
debug_log(config, f"SlackProvider.send called with level: {level}, send method: {config.send_method}")
|
|
24
|
+
formatted_message = self._format_message(message, attachment, config)
|
|
25
|
+
if config.send_method == SendMethod.WEBCLIENT:
|
|
26
|
+
debug_log(config, "Using Slack webclient method")
|
|
27
|
+
self._send_slack_webclient(formatted_message, config)
|
|
28
|
+
elif config.send_method == SendMethod.WEBHOOK:
|
|
29
|
+
debug_log(config, "Using Slack webhook method")
|
|
30
|
+
self._send_slack_webhook(formatted_message, config)
|
|
31
|
+
else:
|
|
32
|
+
error_msg = f"Unknown send method for Slack: {config.send_method}"
|
|
33
|
+
debug_log(config, f"Error: {error_msg}")
|
|
34
|
+
raise ValueError(error_msg)
|
|
35
|
+
|
|
36
|
+
def _format_message(self, message, attachment, config):
|
|
37
|
+
formatted = ""
|
|
38
|
+
|
|
39
|
+
# Add service and environment header
|
|
40
|
+
if config.service_name and config.environment:
|
|
41
|
+
formatted += f"*[{config.service_name} - {config.environment}]*\n"
|
|
42
|
+
elif config.service_name:
|
|
43
|
+
formatted += f"*[{config.service_name}]*\n"
|
|
44
|
+
elif config.environment:
|
|
45
|
+
formatted += f"*[{config.environment}]*\n"
|
|
46
|
+
|
|
47
|
+
formatted += message
|
|
48
|
+
|
|
49
|
+
if attachment and attachment.content:
|
|
50
|
+
filename = attachment.file_name or "Trace Logs"
|
|
51
|
+
formatted += f"\n\n*{filename}:*\n```\n{attachment.content}\n```"
|
|
52
|
+
if attachment and attachment.url:
|
|
53
|
+
formatted += f"\n\n*Attachment:* {attachment.url}"
|
|
54
|
+
|
|
55
|
+
return formatted
|
|
56
|
+
|
|
57
|
+
def _send_slack_webclient(self, formatted_message, config):
|
|
58
|
+
debug_log(config, "send_slack_webclient: preparing API request")
|
|
59
|
+
# Use slack_token if available, otherwise fall back to token
|
|
60
|
+
token = config.provider_config.get("token", "")
|
|
61
|
+
slack_token = config.provider_config.get("slack_token", "")
|
|
62
|
+
if slack_token:
|
|
63
|
+
token = slack_token
|
|
64
|
+
debug_log(config, "send_slack_webclient: using slack_token")
|
|
65
|
+
else:
|
|
66
|
+
debug_log(config, "send_slack_webclient: using token")
|
|
67
|
+
|
|
68
|
+
url = "https://slack.com/api/chat.postMessage"
|
|
69
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json; charset=utf-8"}
|
|
70
|
+
payload = {"channel": config.channel, "text": formatted_message}
|
|
71
|
+
debug_log(config, f"send_slack_webclient: sending to channel: {config.channel}, payload size: {len(str(payload))}")
|
|
72
|
+
|
|
73
|
+
response = requests.post(url, headers=headers, json=payload)
|
|
74
|
+
debug_log(config, f"send_slack_webclient: response status: {response.status_code}, response data: {response.text}")
|
|
75
|
+
if response.status_code != 200:
|
|
76
|
+
error_msg = f"Slack WebClient response: {response.status_code}"
|
|
77
|
+
debug_log(config, f"send_slack_webclient: error: {error_msg}")
|
|
78
|
+
raise Exception(error_msg)
|
|
79
|
+
debug_log(config, "send_slack_webclient: message sent successfully")
|
|
80
|
+
|
|
81
|
+
def _send_slack_webhook(self, formatted_message, config):
|
|
82
|
+
debug_log(config, "send_slack_webhook: preparing webhook request")
|
|
83
|
+
# For webhook, the token field contains the webhook URL
|
|
84
|
+
webhook_url = config.provider_config.get("token", "")
|
|
85
|
+
if not webhook_url:
|
|
86
|
+
error_msg = "Webhook URL is required for Slack webhook method"
|
|
87
|
+
debug_log(config, f"Error: {error_msg}")
|
|
88
|
+
raise Exception(error_msg)
|
|
89
|
+
|
|
90
|
+
debug_log(config, f"send_slack_webhook: using webhook URL, channel: {config.channel}")
|
|
91
|
+
payload = {"text": formatted_message}
|
|
92
|
+
# If channel is specified, include it in the payload
|
|
93
|
+
if config.channel:
|
|
94
|
+
payload["channel"] = config.channel
|
|
95
|
+
|
|
96
|
+
debug_log(config, f"send_slack_webhook: payload prepared, size: {len(str(payload))}")
|
|
97
|
+
response = requests.post(webhook_url, json=payload)
|
|
98
|
+
debug_log(config, f"send_slack_webhook: response status: {response.status_code}, response data: {response.text}")
|
|
99
|
+
if response.status_code != 200:
|
|
100
|
+
error_msg = f"Slack webhook response: {response.status_code}"
|
|
101
|
+
debug_log(config, f"send_slack_webhook: error: {error_msg}")
|
|
102
|
+
raise Exception(error_msg)
|
|
103
|
+
debug_log(config, "send_slack_webhook: webhook sent successfully")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycommonlog
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Unified logging and alerting library for Python.
|
|
5
5
|
Home-page: https://github.com/alvianhanif/pycommonlog
|
|
6
6
|
Author: Alvian Rahman Hanif
|
|
@@ -12,6 +12,9 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.8
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Provides-Extra: redis
|
|
17
|
+
Requires-Dist: redis>=4.0.0; extra == "redis"
|
|
15
18
|
Dynamic: author
|
|
16
19
|
Dynamic: author-email
|
|
17
20
|
Dynamic: classifier
|
|
@@ -20,6 +23,8 @@ Dynamic: description-content-type
|
|
|
20
23
|
Dynamic: home-page
|
|
21
24
|
Dynamic: license
|
|
22
25
|
Dynamic: license-file
|
|
26
|
+
Dynamic: provides-extra
|
|
27
|
+
Dynamic: requires-dist
|
|
23
28
|
Dynamic: requires-python
|
|
24
29
|
Dynamic: summary
|
|
25
30
|
|
|
@@ -50,13 +55,13 @@ from pycommonlog import commonlog, Config, SendMethod, AlertLevel, Attachment, L
|
|
|
50
55
|
|
|
51
56
|
# Configure logger
|
|
52
57
|
config = Config(
|
|
53
|
-
provider="lark", # or "slack"
|
|
54
58
|
send_method=SendMethod.WEBCLIENT,
|
|
55
|
-
token="app_id++app_secret", # for Lark, use "app_id++app_secret" format
|
|
56
|
-
slack_token="xoxb-your-slack-token", # dedicated Slack token
|
|
57
|
-
lark_token=LarkToken(app_id="your-app-id", app_secret="your-app-secret"), # dedicated Lark token
|
|
58
59
|
channel="your_lark_channel_id",
|
|
59
60
|
provider_config={
|
|
61
|
+
"provider": "lark", # or "slack"
|
|
62
|
+
"token": "app_id++app_secret", # for Lark, use "app_id++app_secret" format
|
|
63
|
+
"slack_token": "xoxb-your-slack-token", # dedicated Slack token
|
|
64
|
+
"lark_token": LarkToken(app_id="your-app-id", app_secret="your-app-secret"), # dedicated Lark token
|
|
60
65
|
"redis_host": "localhost", # required for Lark
|
|
61
66
|
"redis_port": 6379, # required for Lark
|
|
62
67
|
}
|
|
@@ -95,13 +100,13 @@ WebClient uses the full API with authentication tokens:
|
|
|
95
100
|
|
|
96
101
|
```python
|
|
97
102
|
config = Config(
|
|
98
|
-
provider="lark",
|
|
99
103
|
send_method=SendMethod.WEBCLIENT,
|
|
100
|
-
token="app_id++app_secret", # for Lark
|
|
101
|
-
slack_token="xoxb-your-slack-token", # for Slack
|
|
102
|
-
lark_token=LarkToken(app_id="your-app-id", app_secret="your-app-secret"),
|
|
103
104
|
channel="your_channel",
|
|
104
105
|
provider_config={
|
|
106
|
+
"provider": "lark", # or "slack"
|
|
107
|
+
"token": "app_id++app_secret", # for Lark
|
|
108
|
+
"slack_token": "xoxb-your-slack-token", # for Slack
|
|
109
|
+
"lark_token": LarkToken(app_id="your-app-id", app_secret="your-app-secret"),
|
|
105
110
|
"redis_host": "localhost", # required for Lark
|
|
106
111
|
"redis_port": 6379, # required for Lark
|
|
107
112
|
}
|
|
@@ -114,10 +119,12 @@ Webhook is simpler and requires only a webhook URL:
|
|
|
114
119
|
|
|
115
120
|
```python
|
|
116
121
|
config = Config(
|
|
117
|
-
provider="slack",
|
|
118
122
|
send_method=SendMethod.WEBHOOK,
|
|
119
|
-
token="https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
|
|
120
123
|
channel="optional-channel-override", # optional
|
|
124
|
+
provider_config={
|
|
125
|
+
"provider": "slack",
|
|
126
|
+
"token": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
|
|
127
|
+
}
|
|
121
128
|
)
|
|
122
129
|
```
|
|
123
130
|
|
|
@@ -129,11 +136,11 @@ Lark integration requires proper token configuration for authentication. You can
|
|
|
129
136
|
|
|
130
137
|
```python
|
|
131
138
|
config = Config(
|
|
132
|
-
provider="lark",
|
|
133
139
|
send_method=SendMethod.WEBCLIENT,
|
|
134
|
-
token="your_app_id++your_app_secret", # Combined format: app_id++app_secret
|
|
135
140
|
channel="your_channel_id",
|
|
136
141
|
provider_config={
|
|
142
|
+
"provider": "lark",
|
|
143
|
+
"token": "your_app_id++your_app_secret", # Combined format: app_id++app_secret
|
|
137
144
|
"redis_host": "localhost", # Optional: enables caching
|
|
138
145
|
"redis_port": 6379,
|
|
139
146
|
}
|
|
@@ -143,17 +150,15 @@ config = Config(
|
|
|
143
150
|
#### Method 2: Dedicated Lark Token Object
|
|
144
151
|
|
|
145
152
|
```python
|
|
146
|
-
from pycommonlog import LarkToken
|
|
147
|
-
|
|
148
153
|
config = Config(
|
|
149
|
-
provider="lark",
|
|
150
154
|
send_method=SendMethod.WEBCLIENT,
|
|
151
|
-
lark_token=LarkToken(
|
|
152
|
-
app_id="your_app_id",
|
|
153
|
-
app_secret="your_app_secret"
|
|
154
|
-
),
|
|
155
155
|
channel="your_channel_id",
|
|
156
156
|
provider_config={
|
|
157
|
+
"provider": "lark",
|
|
158
|
+
"lark_token": LarkToken(
|
|
159
|
+
app_id="your_app_id",
|
|
160
|
+
app_secret="your_app_secret"
|
|
161
|
+
),
|
|
157
162
|
"redis_host": "localhost", # Optional: enables caching
|
|
158
163
|
"redis_port": 6379,
|
|
159
164
|
}
|
|
@@ -199,12 +204,14 @@ resolver = DefaultChannelResolver(
|
|
|
199
204
|
|
|
200
205
|
# Create config with channel resolver
|
|
201
206
|
config = Config(
|
|
202
|
-
provider="slack",
|
|
203
207
|
send_method=SendMethod.WEBCLIENT,
|
|
204
|
-
token="xoxb-your-slack-bot-token",
|
|
205
208
|
channel_resolver=resolver,
|
|
206
209
|
service_name="user-service",
|
|
207
|
-
environment="production"
|
|
210
|
+
environment="production",
|
|
211
|
+
provider_config={
|
|
212
|
+
"provider": "slack",
|
|
213
|
+
"token": "xoxb-your-slack-bot-token",
|
|
214
|
+
}
|
|
208
215
|
)
|
|
209
216
|
|
|
210
217
|
logger = commonlog(config)
|
|
@@ -234,17 +241,27 @@ class CustomResolver(ChannelResolver):
|
|
|
234
241
|
|
|
235
242
|
### Common Settings
|
|
236
243
|
|
|
237
|
-
- **
|
|
238
|
-
- **send_method**: `"webclient"` (token-based authentication)
|
|
244
|
+
- **send_method**: `"webclient"` (token-based authentication) or `"webhook"`
|
|
239
245
|
- **channel**: Target channel or chat ID (used if no resolver)
|
|
240
246
|
- **channel_resolver**: Optional resolver for dynamic channel mapping
|
|
241
247
|
- **service_name**: Name of the service sending alerts
|
|
242
248
|
- **environment**: Environment (dev, staging, production)
|
|
243
249
|
- **debug**: `True` to enable detailed debug logging of all internal processes
|
|
244
250
|
|
|
245
|
-
###
|
|
251
|
+
### ProviderConfig Settings
|
|
246
252
|
|
|
247
|
-
-
|
|
253
|
+
All provider-specific configuration is now done via the `provider_config` dict:
|
|
254
|
+
|
|
255
|
+
- **provider**: `"slack"` or `"lark"`
|
|
256
|
+
- **token**: API token for WebClient authentication or webhook URL for Webhook method
|
|
257
|
+
- **slack_token**: Dedicated Slack token (optional, overrides token for Slack)
|
|
258
|
+
- **lark_token**: `LarkToken` object with app_id and app_secret (optional, overrides token for Lark)
|
|
259
|
+
- **redis_host**: Redis host for Lark caching (optional)
|
|
260
|
+
- **redis_port**: Redis port for Lark caching (optional)
|
|
261
|
+
- **redis_password**: Redis password (optional)
|
|
262
|
+
- **redis_ssl**: Enable SSL for Redis (optional)
|
|
263
|
+
- **redis_cluster_mode**: Enable Redis cluster mode (optional)
|
|
264
|
+
- **redis_db**: Redis database number (optional)
|
|
248
265
|
|
|
249
266
|
## Alert Levels
|
|
250
267
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pycommonlog/__init__.py,sha256=ide8ECcqnHuEbimAz5yT5GIGYWyEVlrxkefvxcFLYII,480
|
|
2
|
+
pycommonlog/cache.py,sha256=UFmwZ2gtsHwn1b0IOUYr4m1v_wnuey4TeMW44m2c-XI,2662
|
|
3
|
+
pycommonlog/log_types.py,sha256=j8YgqsvIHfpdYexH2reTuG4QsZi1vWtk5hkkBD6mYVM,2661
|
|
4
|
+
pycommonlog/logger.py,sha256=4b3mBTIUoxZsew8sOGN1IRniUgzOX-wxIJBpHMKmTfY,7137
|
|
5
|
+
pycommonlog/providers/__init__.py,sha256=NfIV3103q6ZMPMiJCeS_I9sPZ7XQNyk3bFSe0Kh4xPE,148
|
|
6
|
+
pycommonlog/providers/lark.py,sha256=3h2u5lziJUu4cE7HapuCqGQ9qn-zu--_lUSDXX9yjRg,12464
|
|
7
|
+
pycommonlog/providers/redis_client.py,sha256=rOkj4_Wpa13scyqtTyDpkkZjXGbnA5iiX4hzcCUcUY8,1855
|
|
8
|
+
pycommonlog/providers/slack.py,sha256=8AgBa4Sc74hKPsl6avxO6SpmwhByQ7b1pWYTSX26Tnc,4923
|
|
9
|
+
pycommonlog-0.2.2.dist-info/licenses/LICENSE,sha256=bxyMRuc_Y6GKeCFV0_vcJf24FqyCVNL_mtUaJ7lfuFo,1075
|
|
10
|
+
pycommonlog-0.2.2.dist-info/METADATA,sha256=6KN4mrCiQO2eLi0Wguus5mrXScXvVGIANtS-SpeuuD4,9733
|
|
11
|
+
pycommonlog-0.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
+
pycommonlog-0.2.2.dist-info/top_level.txt,sha256=tHB8NrMYpDeBGSIsRB3JZ17XMlpdTZWKQ-Ejdg_wUb4,12
|
|
13
|
+
pycommonlog-0.2.2.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
pycommonlog/__init__.py,sha256=ide8ECcqnHuEbimAz5yT5GIGYWyEVlrxkefvxcFLYII,480
|
|
2
|
-
pycommonlog/cache.py,sha256=UFmwZ2gtsHwn1b0IOUYr4m1v_wnuey4TeMW44m2c-XI,2662
|
|
3
|
-
pycommonlog/log_types.py,sha256=uXtEunhuOToU409-QaJ0YjmQhyuxixYRgHxHjl4VQcQ,2140
|
|
4
|
-
pycommonlog/logger.py,sha256=rO_nSEb1IyYcTFzzv6kENe9yy-E_NTTudP-73nrDmpc,7073
|
|
5
|
-
pycommonlog-0.2.0.dist-info/licenses/LICENSE,sha256=bxyMRuc_Y6GKeCFV0_vcJf24FqyCVNL_mtUaJ7lfuFo,1075
|
|
6
|
-
pycommonlog-0.2.0.dist-info/METADATA,sha256=4qz3wA4NfsSx8sdWU5B5bRi_KpO4wTFA9S-AySshKtk,8806
|
|
7
|
-
pycommonlog-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
-
pycommonlog-0.2.0.dist-info/top_level.txt,sha256=tHB8NrMYpDeBGSIsRB3JZ17XMlpdTZWKQ-Ejdg_wUb4,12
|
|
9
|
-
pycommonlog-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|