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 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
- if config.provider == "slack":
107
+ provider_name = config.provider_config.get("provider", "slack")
108
+ if provider_name == "slack":
108
109
  self.provider = SlackProvider()
109
- elif config.provider == "lark":
110
+ elif provider_name == "lark":
110
111
  self.provider = LarkProvider()
111
112
  else:
112
- logging.warning(f"Unknown provider: {config.provider}, defaulting to Slack")
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: {config.provider}, send method: {config.send_method}, debug: {config.debug}")
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,7 @@
1
+ """
2
+ Providers package for commonlog
3
+ """
4
+ from .slack import SlackProvider
5
+ from .lark import LarkProvider
6
+
7
+ __all__ = ["SlackProvider", "LarkProvider"]
@@ -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.0
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
- - **provider**: `"slack"` or `"lark"`
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
- ### Provider-Specific
251
+ ### ProviderConfig Settings
246
252
 
247
- - **token**: API token for WebClient authentication (required)
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,,