pycommonlog 0.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alvian Rahman Hanif
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,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycommonlog
3
+ Version: 0.0.0
4
+ Summary: Unified logging and alerting library for Python.
5
+ Home-page: https://github.com/alvianhanif/pycommonlog
6
+ Author: Alvian Rahman Hanif
7
+ Author-email: alvian.hanif@pasarpolis.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: license
22
+ Dynamic: license-file
23
+ Dynamic: requires-python
24
+ Dynamic: summary
25
+
26
+ # pycommonlog
27
+
28
+ [![CI](https://github.com/alvianhanif/pycommonlog/actions/workflows/ci.yml/badge.svg)](https://github.com/alvianhanif/pycommonlog/actions/workflows/ci.yml)
29
+ [![PyPI version](https://badge.fury.io/py/pycommonlog.svg)](https://badge.fury.io/py/pycommonlog)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+
32
+ A unified logging and alerting library for Python, supporting Slack and Lark integrations via WebClient and Webhook. Features configurable providers, alert levels, and file attachment support.
33
+
34
+ ## Installation
35
+
36
+ Install via pip:
37
+
38
+ ```bash
39
+ pip install pycommonlog
40
+ ```
41
+
42
+ Or copy the `pycommonlog/` directory to your project.
43
+
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from pycommonlog import commonlog, Config, SendMethod, AlertLevel, Attachment, LarkToken
49
+
50
+ # Configure logger
51
+ config = Config(
52
+ provider="lark", # or "slack"
53
+ send_method=SendMethod.WEBCLIENT,
54
+ token="app_id++app_secret", # for Lark, use "app_id++app_secret" format
55
+ slack_token="xoxb-your-slack-token", # dedicated Slack token
56
+ lark_token=LarkToken(app_id="your-app-id", app_secret="your-app-secret"), # dedicated Lark token
57
+ channel="your_lark_channel_id",
58
+ redis_host="localhost", # required for Lark
59
+ redis_port="6379", # required for Lark
60
+ )
61
+ logger = commonlog(config)
62
+
63
+ # Send error with attachment
64
+ try:
65
+ logger.send(AlertLevel.ERROR, "System error occurred", Attachment(url="https://example.com/log.txt"))
66
+ except Exception as e:
67
+ print(f"Failed to send alert: {e}")
68
+
69
+ # Send info (logs only)
70
+ logger.send(AlertLevel.INFO, "Info message")
71
+
72
+ # Send to a specific channel
73
+ try:
74
+ logger.send_to_channel(AlertLevel.ERROR, "Send to another channel", channel="another-channel-id")
75
+ except Exception as e:
76
+ print(f"Failed to send alert: {e}")
77
+
78
+ # Send to a different provider dynamically
79
+ try:
80
+ logger.custom_send("slack", AlertLevel.ERROR, "Message via Slack", channel="slack-channel")
81
+ except Exception as e:
82
+ print(f"Failed to send alert: {e}")
83
+ ```
84
+
85
+ ## Send Methods
86
+
87
+ commonlog supports two send methods: WebClient (API-based) and Webhook (simple HTTP POST).
88
+
89
+ ### WebClient Usage
90
+
91
+ WebClient uses the full API with authentication tokens:
92
+
93
+ ```python
94
+ config = Config(
95
+ provider="lark",
96
+ send_method=SendMethod.WEBCLIENT,
97
+ token="app_id++app_secret", # for Lark
98
+ slack_token="xoxb-your-slack-token", # for Slack
99
+ lark_token=LarkToken(app_id="your-app-id", app_secret="your-app-secret"),
100
+ channel="your_channel",
101
+ redis_host="localhost", # required for Lark
102
+ redis_port="6379",
103
+ )
104
+ ```
105
+
106
+ ### Webhook Usage
107
+
108
+ Webhook is simpler and requires only a webhook URL:
109
+
110
+ ```python
111
+ config = Config(
112
+ provider="slack",
113
+ send_method=SendMethod.WEBHOOK,
114
+ token="https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
115
+ channel="optional-channel-override", # optional
116
+ )
117
+ ```
118
+
119
+ ### Lark Token Caching
120
+
121
+ When using Lark, the tenant_access_token is cached in Redis. The expiry is set dynamically from the API response minus 10 minutes. You must set `redis_host` and `redis_port` in your config.
122
+
123
+ ## Channel Mapping
124
+
125
+ You can configure different channels for different alert levels using a channel resolver:
126
+
127
+ ```python
128
+ from commonlog import commonlog, Config, SendMethod, AlertLevel, DefaultChannelResolver
129
+
130
+ # Create a channel resolver
131
+ resolver = DefaultChannelResolver(
132
+ channel_map={
133
+ AlertLevel.INFO: "#general",
134
+ AlertLevel.WARN: "#warnings",
135
+ AlertLevel.ERROR: "#alerts",
136
+ },
137
+ default_channel="#general"
138
+ )
139
+
140
+ # Create config with channel resolver
141
+ config = Config(
142
+ provider="slack",
143
+ send_method=SendMethod.WEBCLIENT,
144
+ token="xoxb-your-slack-bot-token",
145
+ channel_resolver=resolver,
146
+ service_name="user-service",
147
+ environment="production"
148
+ )
149
+
150
+ logger = commonlog(config)
151
+
152
+ # These will go to different channels based on level
153
+ logger.send(AlertLevel.INFO, "Info message") # goes to #general
154
+ logger.send(AlertLevel.WARN, "Warning message") # goes to #warnings
155
+ logger.send(AlertLevel.ERROR, "Error message") # goes to #alerts
156
+ ```
157
+
158
+ ### Custom Channel Resolver
159
+
160
+ You can implement custom channel resolution logic:
161
+
162
+ ```python
163
+ class CustomResolver(ChannelResolver):
164
+ def resolve_channel(self, level):
165
+ if level == AlertLevel.ERROR:
166
+ return "#critical-alerts"
167
+ elif level == AlertLevel.WARN:
168
+ return "#monitoring"
169
+ else:
170
+ return "#general"
171
+ ```
172
+
173
+ ## Configuration Options
174
+
175
+ ### Common Settings
176
+
177
+ - **provider**: `"slack"` or `"lark"`
178
+ - **send_method**: `"webclient"` (token-based authentication)
179
+ - **channel**: Target channel or chat ID (used if no resolver)
180
+ - **channel_resolver**: Optional resolver for dynamic channel mapping
181
+ - **service_name**: Name of the service sending alerts
182
+ - **environment**: Environment (dev, staging, production)
183
+ - **debug**: `True` to enable detailed debug logging of all internal processes
184
+
185
+ ### Provider-Specific
186
+
187
+ - **token**: API token for WebClient authentication (required)
188
+
189
+ ## Alert Levels
190
+
191
+ - **INFO**: Logs locally only
192
+ - **WARN**: Logs + sends alert
193
+ - **ERROR**: Always sends alert
194
+
195
+ ## File Attachments
196
+
197
+ Provide a public URL. The library appends it to the message for simplicity.
198
+
199
+ ```python
200
+ attachment = Attachment(url="https://example.com/log.txt")
201
+ logger.send(AlertLevel.ERROR, "Error with log", attachment)
202
+ ```
203
+
204
+ ## Trace Log Section
205
+
206
+ When `include_trace` is set to `True`, you can pass trace information as the fourth parameter to `send()`:
207
+
208
+ ```python
209
+ trace = """Traceback (most recent call last):
210
+ File "app.py", line 10, in main
211
+ raise ValueError("Something went wrong")
212
+ ValueError: Something went wrong"""
213
+
214
+ logger.send(AlertLevel.ERROR, "System error occurred", None, trace)
215
+ ```
216
+
217
+ This will format the trace as a code block in the alert message.
218
+
219
+ ## Testing
220
+
221
+ ```bash
222
+ cd python
223
+ PYTHONPATH=.. python -m unittest test_commonlog.py
224
+ ```
225
+
226
+ ## API Reference
227
+
228
+ ### Classes
229
+
230
+ - `Config`: Configuration class
231
+ - `Attachment`: File attachment class
232
+ - `Provider`: Abstract base class for alert providers
233
+ - `commonlog`: Main logger class
234
+
235
+ ### Constants
236
+
237
+ - `SendMethod.WEBCLIENT`: Send method (token-based authentication)
238
+ - `AlertLevel.INFO`, `AlertLevel.WARN`, `AlertLevel.ERROR`: Alert levels
239
+
240
+ ### Methods
241
+
242
+ - `commonlog(config)`: Create a new logger
243
+ - `commonlog.send(level, message, attachment=None, trace="")`: Send alert with optional trace
@@ -0,0 +1,218 @@
1
+ # pycommonlog
2
+
3
+ [![CI](https://github.com/alvianhanif/pycommonlog/actions/workflows/ci.yml/badge.svg)](https://github.com/alvianhanif/pycommonlog/actions/workflows/ci.yml)
4
+ [![PyPI version](https://badge.fury.io/py/pycommonlog.svg)](https://badge.fury.io/py/pycommonlog)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A unified logging and alerting library for Python, supporting Slack and Lark integrations via WebClient and Webhook. Features configurable providers, alert levels, and file attachment support.
8
+
9
+ ## Installation
10
+
11
+ Install via pip:
12
+
13
+ ```bash
14
+ pip install pycommonlog
15
+ ```
16
+
17
+ Or copy the `pycommonlog/` directory to your project.
18
+
19
+
20
+ ## Usage
21
+
22
+ ```python
23
+ from pycommonlog import commonlog, Config, SendMethod, AlertLevel, Attachment, LarkToken
24
+
25
+ # Configure logger
26
+ config = Config(
27
+ provider="lark", # or "slack"
28
+ send_method=SendMethod.WEBCLIENT,
29
+ token="app_id++app_secret", # for Lark, use "app_id++app_secret" format
30
+ slack_token="xoxb-your-slack-token", # dedicated Slack token
31
+ lark_token=LarkToken(app_id="your-app-id", app_secret="your-app-secret"), # dedicated Lark token
32
+ channel="your_lark_channel_id",
33
+ redis_host="localhost", # required for Lark
34
+ redis_port="6379", # required for Lark
35
+ )
36
+ logger = commonlog(config)
37
+
38
+ # Send error with attachment
39
+ try:
40
+ logger.send(AlertLevel.ERROR, "System error occurred", Attachment(url="https://example.com/log.txt"))
41
+ except Exception as e:
42
+ print(f"Failed to send alert: {e}")
43
+
44
+ # Send info (logs only)
45
+ logger.send(AlertLevel.INFO, "Info message")
46
+
47
+ # Send to a specific channel
48
+ try:
49
+ logger.send_to_channel(AlertLevel.ERROR, "Send to another channel", channel="another-channel-id")
50
+ except Exception as e:
51
+ print(f"Failed to send alert: {e}")
52
+
53
+ # Send to a different provider dynamically
54
+ try:
55
+ logger.custom_send("slack", AlertLevel.ERROR, "Message via Slack", channel="slack-channel")
56
+ except Exception as e:
57
+ print(f"Failed to send alert: {e}")
58
+ ```
59
+
60
+ ## Send Methods
61
+
62
+ commonlog supports two send methods: WebClient (API-based) and Webhook (simple HTTP POST).
63
+
64
+ ### WebClient Usage
65
+
66
+ WebClient uses the full API with authentication tokens:
67
+
68
+ ```python
69
+ config = Config(
70
+ provider="lark",
71
+ send_method=SendMethod.WEBCLIENT,
72
+ token="app_id++app_secret", # for Lark
73
+ slack_token="xoxb-your-slack-token", # for Slack
74
+ lark_token=LarkToken(app_id="your-app-id", app_secret="your-app-secret"),
75
+ channel="your_channel",
76
+ redis_host="localhost", # required for Lark
77
+ redis_port="6379",
78
+ )
79
+ ```
80
+
81
+ ### Webhook Usage
82
+
83
+ Webhook is simpler and requires only a webhook URL:
84
+
85
+ ```python
86
+ config = Config(
87
+ provider="slack",
88
+ send_method=SendMethod.WEBHOOK,
89
+ token="https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
90
+ channel="optional-channel-override", # optional
91
+ )
92
+ ```
93
+
94
+ ### Lark Token Caching
95
+
96
+ When using Lark, the tenant_access_token is cached in Redis. The expiry is set dynamically from the API response minus 10 minutes. You must set `redis_host` and `redis_port` in your config.
97
+
98
+ ## Channel Mapping
99
+
100
+ You can configure different channels for different alert levels using a channel resolver:
101
+
102
+ ```python
103
+ from commonlog import commonlog, Config, SendMethod, AlertLevel, DefaultChannelResolver
104
+
105
+ # Create a channel resolver
106
+ resolver = DefaultChannelResolver(
107
+ channel_map={
108
+ AlertLevel.INFO: "#general",
109
+ AlertLevel.WARN: "#warnings",
110
+ AlertLevel.ERROR: "#alerts",
111
+ },
112
+ default_channel="#general"
113
+ )
114
+
115
+ # Create config with channel resolver
116
+ config = Config(
117
+ provider="slack",
118
+ send_method=SendMethod.WEBCLIENT,
119
+ token="xoxb-your-slack-bot-token",
120
+ channel_resolver=resolver,
121
+ service_name="user-service",
122
+ environment="production"
123
+ )
124
+
125
+ logger = commonlog(config)
126
+
127
+ # These will go to different channels based on level
128
+ logger.send(AlertLevel.INFO, "Info message") # goes to #general
129
+ logger.send(AlertLevel.WARN, "Warning message") # goes to #warnings
130
+ logger.send(AlertLevel.ERROR, "Error message") # goes to #alerts
131
+ ```
132
+
133
+ ### Custom Channel Resolver
134
+
135
+ You can implement custom channel resolution logic:
136
+
137
+ ```python
138
+ class CustomResolver(ChannelResolver):
139
+ def resolve_channel(self, level):
140
+ if level == AlertLevel.ERROR:
141
+ return "#critical-alerts"
142
+ elif level == AlertLevel.WARN:
143
+ return "#monitoring"
144
+ else:
145
+ return "#general"
146
+ ```
147
+
148
+ ## Configuration Options
149
+
150
+ ### Common Settings
151
+
152
+ - **provider**: `"slack"` or `"lark"`
153
+ - **send_method**: `"webclient"` (token-based authentication)
154
+ - **channel**: Target channel or chat ID (used if no resolver)
155
+ - **channel_resolver**: Optional resolver for dynamic channel mapping
156
+ - **service_name**: Name of the service sending alerts
157
+ - **environment**: Environment (dev, staging, production)
158
+ - **debug**: `True` to enable detailed debug logging of all internal processes
159
+
160
+ ### Provider-Specific
161
+
162
+ - **token**: API token for WebClient authentication (required)
163
+
164
+ ## Alert Levels
165
+
166
+ - **INFO**: Logs locally only
167
+ - **WARN**: Logs + sends alert
168
+ - **ERROR**: Always sends alert
169
+
170
+ ## File Attachments
171
+
172
+ Provide a public URL. The library appends it to the message for simplicity.
173
+
174
+ ```python
175
+ attachment = Attachment(url="https://example.com/log.txt")
176
+ logger.send(AlertLevel.ERROR, "Error with log", attachment)
177
+ ```
178
+
179
+ ## Trace Log Section
180
+
181
+ When `include_trace` is set to `True`, you can pass trace information as the fourth parameter to `send()`:
182
+
183
+ ```python
184
+ trace = """Traceback (most recent call last):
185
+ File "app.py", line 10, in main
186
+ raise ValueError("Something went wrong")
187
+ ValueError: Something went wrong"""
188
+
189
+ logger.send(AlertLevel.ERROR, "System error occurred", None, trace)
190
+ ```
191
+
192
+ This will format the trace as a code block in the alert message.
193
+
194
+ ## Testing
195
+
196
+ ```bash
197
+ cd python
198
+ PYTHONPATH=.. python -m unittest test_commonlog.py
199
+ ```
200
+
201
+ ## API Reference
202
+
203
+ ### Classes
204
+
205
+ - `Config`: Configuration class
206
+ - `Attachment`: File attachment class
207
+ - `Provider`: Abstract base class for alert providers
208
+ - `commonlog`: Main logger class
209
+
210
+ ### Constants
211
+
212
+ - `SendMethod.WEBCLIENT`: Send method (token-based authentication)
213
+ - `AlertLevel.INFO`, `AlertLevel.WARN`, `AlertLevel.ERROR`: Alert levels
214
+
215
+ ### Methods
216
+
217
+ - `commonlog(config)`: Create a new logger
218
+ - `commonlog.send(level, message, attachment=None, trace="")`: Send alert with optional trace
@@ -0,0 +1,20 @@
1
+ """
2
+ commonlog: Unified logging and alerting for Slack/Lark (Python)
3
+ """
4
+
5
+ from .log_types import SendMethod, AlertLevel, Attachment, Config, Provider, ChannelResolver, DefaultChannelResolver
6
+ from .providers import SlackProvider, LarkProvider
7
+ from .logger import commonlog
8
+
9
+ __all__ = [
10
+ "SendMethod",
11
+ "AlertLevel",
12
+ "Attachment",
13
+ "Config",
14
+ "Provider",
15
+ "ChannelResolver",
16
+ "DefaultChannelResolver",
17
+ "SlackProvider",
18
+ "LarkProvider",
19
+ "commonlog"
20
+ ]
@@ -0,0 +1,70 @@
1
+ """
2
+ commonlog: Unified logging and alerting for Slack/Lark (Python)
3
+ """
4
+ from abc import ABC, abstractmethod
5
+
6
+ class SendMethod:
7
+ WEBCLIENT = "webclient"
8
+ WEBHOOK = "webhook"
9
+
10
+ class AlertLevel:
11
+ INFO = 0
12
+ WARN = 1
13
+ ERROR = 2
14
+
15
+ class Attachment:
16
+ def __init__(self, url=None, file_name=None, content=None):
17
+ self.url = url
18
+ self.file_name = file_name
19
+ self.content = content
20
+
21
+ class ChannelResolver(ABC):
22
+ @abstractmethod
23
+ def resolve_channel(self, level):
24
+ pass
25
+
26
+ class DefaultChannelResolver(ChannelResolver):
27
+ def __init__(self, channel_map=None, default_channel=None):
28
+ self.channel_map = channel_map or {}
29
+ self.default_channel = default_channel
30
+
31
+ def resolve_channel(self, level):
32
+ return self.channel_map.get(level, self.default_channel)
33
+
34
+ class LarkToken:
35
+ def __init__(self, app_id=None, app_secret=None):
36
+ self.app_id = app_id
37
+ self.app_secret = app_secret
38
+
39
+ class Config:
40
+ def __init__(self, provider, send_method, token=None, slack_token=None, lark_token=None, channel=None, channel_resolver=None, service_name=None, environment=None, redis_host=None, redis_port=None, debug=False):
41
+ self.provider = provider
42
+ self.send_method = send_method
43
+ self.token = token
44
+ self.slack_token = slack_token
45
+ self.lark_token = lark_token
46
+ self.channel = channel
47
+ self.channel_resolver = channel_resolver
48
+ self.service_name = service_name
49
+ self.environment = environment
50
+ self.redis_host = redis_host
51
+ self.redis_port = redis_port
52
+ self.debug = debug
53
+
54
+ class Provider(ABC):
55
+ @abstractmethod
56
+ def send_to_channel(self, level, message, attachment, config, channel):
57
+ pass
58
+
59
+ # Debug logging
60
+ import logging
61
+
62
+ debug_logger = logging.getLogger('commonlog.debug')
63
+ debug_logger.setLevel(logging.DEBUG)
64
+ handler = logging.StreamHandler()
65
+ handler.setFormatter(logging.Formatter('[COMMONLOG DEBUG] %(filename)s:%(lineno)d - %(message)s'))
66
+ debug_logger.addHandler(handler)
67
+
68
+ def debug_log(config, message, *args):
69
+ if hasattr(config, 'debug') and config.debug:
70
+ debug_logger.debug(message, *args)
@@ -0,0 +1,151 @@
1
+ """
2
+ Main logger for commonlog
3
+ """
4
+ import logging
5
+ import sys
6
+ import os
7
+
8
+ # Add current directory to path for direct imports
9
+ _current_dir = os.path.dirname(os.path.abspath(__file__))
10
+ if _current_dir not in sys.path:
11
+ sys.path.insert(0, _current_dir)
12
+
13
+ from providers import SlackProvider, LarkProvider
14
+ from log_types import AlertLevel, Attachment, debug_log
15
+
16
+ # ====================
17
+ # Configuration and Logger
18
+ # ====================
19
+
20
+ class commonlog:
21
+ def send_to_channel(self, level, message, attachment=None, trace="", channel=None):
22
+ debug_log(self.config, f"send_to_channel called with level: {level}, message length: {len(message)}, channel: {channel}, has attachment: {attachment is not None}, has trace: {bool(trace)}")
23
+
24
+ if level == AlertLevel.INFO:
25
+ logging.info(message)
26
+ debug_log(self.config, "INFO level message logged locally, skipping provider send")
27
+ return
28
+ try:
29
+ # Use provided channel or fallback to resolved channel
30
+ target_channel = channel if channel else self._resolve_channel(level)
31
+ if channel is None:
32
+ debug_log(self.config, f"Resolved channel using resolver: {target_channel}")
33
+ else:
34
+ debug_log(self.config, f"Using provided channel: {target_channel}")
35
+
36
+ original_channel = self.config.channel
37
+ self.config.channel = target_channel
38
+ if trace:
39
+ debug_log(self.config, f"Processing trace attachment, trace length: {len(trace)}")
40
+ if attachment is None:
41
+ attachment = Attachment(content=trace, file_name="trace.log")
42
+ debug_log(self.config, "Created new trace attachment")
43
+ else:
44
+ if attachment.content:
45
+ attachment.content += "\n\n--- Trace Log ---\n" + trace
46
+ debug_log(self.config, "Appended trace to existing attachment content")
47
+ else:
48
+ attachment.content = trace
49
+ attachment.file_name = "trace.log"
50
+ debug_log(self.config, "Set trace as attachment content")
51
+
52
+ debug_log(self.config, f"Calling provider.send_to_channel with resolved channel: {target_channel}")
53
+ self.provider.send_to_channel(level, message, attachment, self.config, target_channel)
54
+ self.config.channel = original_channel
55
+ debug_log(self.config, "Provider send_to_channel completed successfully")
56
+ except Exception as e:
57
+ debug_log(self.config, f"Provider send_to_channel failed: {e}")
58
+ logging.error(f"Failed to send alert: {e}")
59
+ raise
60
+
61
+ def custom_send(self, provider, level, message, attachment=None, trace="", channel=None):
62
+ debug_log(self.config, f"custom_send called with custom provider: {provider}, level: {level}, message length: {len(message)}")
63
+
64
+ if provider == "slack":
65
+ custom_provider = SlackProvider()
66
+ elif provider == "lark":
67
+ custom_provider = LarkProvider()
68
+ else:
69
+ logging.warning(f"Unknown provider: {provider}, defaulting to Slack")
70
+ custom_provider = SlackProvider()
71
+ debug_log(self.config, f"Unknown provider '{provider}', defaulted to slack")
72
+
73
+ debug_log(self.config, f"Created custom provider: {provider}")
74
+
75
+ if level == AlertLevel.INFO:
76
+ logging.info(message)
77
+ debug_log(self.config, "INFO level message logged locally for custom provider, skipping send")
78
+ return
79
+ try:
80
+ # Use provided channel or fallback to resolved channel
81
+ target_channel = channel if channel else self._resolve_channel(level)
82
+ debug_log(self.config, f"Resolved channel for custom send: {target_channel}")
83
+
84
+ original_channel = self.config.channel
85
+ self.config.channel = target_channel
86
+ if trace:
87
+ debug_log(self.config, f"Processing trace for custom send, trace length: {len(trace)}")
88
+ if attachment is None:
89
+ attachment = Attachment(content=trace, file_name="trace.log")
90
+ else:
91
+ if attachment.content:
92
+ attachment.content += "\n\n--- Trace Log ---\n" + trace
93
+ else:
94
+ attachment.content = trace
95
+ attachment.file_name = "trace.log"
96
+ debug_log(self.config, f"Calling custom provider.send with provider: {provider}, channel: {target_channel}")
97
+ custom_provider.send(level, message, attachment, self.config)
98
+ self.config.channel = original_channel
99
+ debug_log(self.config, "Custom provider send completed successfully")
100
+ except Exception as e:
101
+ debug_log(self.config, f"Custom provider send failed: {e}")
102
+ logging.error(f"Failed to send alert: {e}")
103
+ raise
104
+
105
+ def __init__(self, config):
106
+ self.config = config
107
+ if config.provider == "slack":
108
+ self.provider = SlackProvider()
109
+ elif config.provider == "lark":
110
+ self.provider = LarkProvider()
111
+ else:
112
+ logging.warning(f"Unknown provider: {config.provider}, defaulting to Slack")
113
+ self.provider = SlackProvider()
114
+
115
+ debug_log(config, f"Created logger with provider: {config.provider}, send method: {config.send_method}, debug: {config.debug}")
116
+
117
+ def _resolve_channel(self, level):
118
+ if self.config.channel_resolver:
119
+ return self.config.channel_resolver.resolve_channel(level)
120
+ return self.config.channel
121
+
122
+ def send(self, level, message, attachment=None, trace=""):
123
+ if level == AlertLevel.INFO:
124
+ logging.info(message)
125
+ return
126
+ try:
127
+ # Resolve the channel for this alert level
128
+ resolved_channel = self._resolve_channel(level)
129
+
130
+ # Temporarily modify config with resolved channel
131
+ original_channel = self.config.channel
132
+ self.config.channel = resolved_channel
133
+
134
+ # If trace is provided, create an attachment
135
+ if trace:
136
+ if attachment is None:
137
+ attachment = Attachment(content=trace, file_name="trace.log")
138
+ else:
139
+ # If there's already an attachment, combine the trace content
140
+ if attachment.content:
141
+ attachment.content += "\n\n--- Trace Log ---\n" + trace
142
+ else:
143
+ attachment.content = trace
144
+ attachment.file_name = "trace.log"
145
+ self.provider.send(level, message, attachment, self.config)
146
+
147
+ # Restore original channel
148
+ self.config.channel = original_channel
149
+ except Exception as e:
150
+ logging.error(f"Failed to send alert: {e}")
151
+ raise
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycommonlog
3
+ Version: 0.0.0
4
+ Summary: Unified logging and alerting library for Python.
5
+ Home-page: https://github.com/alvianhanif/pycommonlog
6
+ Author: Alvian Rahman Hanif
7
+ Author-email: alvian.hanif@pasarpolis.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: license
22
+ Dynamic: license-file
23
+ Dynamic: requires-python
24
+ Dynamic: summary
25
+
26
+ # pycommonlog
27
+
28
+ [![CI](https://github.com/alvianhanif/pycommonlog/actions/workflows/ci.yml/badge.svg)](https://github.com/alvianhanif/pycommonlog/actions/workflows/ci.yml)
29
+ [![PyPI version](https://badge.fury.io/py/pycommonlog.svg)](https://badge.fury.io/py/pycommonlog)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+
32
+ A unified logging and alerting library for Python, supporting Slack and Lark integrations via WebClient and Webhook. Features configurable providers, alert levels, and file attachment support.
33
+
34
+ ## Installation
35
+
36
+ Install via pip:
37
+
38
+ ```bash
39
+ pip install pycommonlog
40
+ ```
41
+
42
+ Or copy the `pycommonlog/` directory to your project.
43
+
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from pycommonlog import commonlog, Config, SendMethod, AlertLevel, Attachment, LarkToken
49
+
50
+ # Configure logger
51
+ config = Config(
52
+ provider="lark", # or "slack"
53
+ send_method=SendMethod.WEBCLIENT,
54
+ token="app_id++app_secret", # for Lark, use "app_id++app_secret" format
55
+ slack_token="xoxb-your-slack-token", # dedicated Slack token
56
+ lark_token=LarkToken(app_id="your-app-id", app_secret="your-app-secret"), # dedicated Lark token
57
+ channel="your_lark_channel_id",
58
+ redis_host="localhost", # required for Lark
59
+ redis_port="6379", # required for Lark
60
+ )
61
+ logger = commonlog(config)
62
+
63
+ # Send error with attachment
64
+ try:
65
+ logger.send(AlertLevel.ERROR, "System error occurred", Attachment(url="https://example.com/log.txt"))
66
+ except Exception as e:
67
+ print(f"Failed to send alert: {e}")
68
+
69
+ # Send info (logs only)
70
+ logger.send(AlertLevel.INFO, "Info message")
71
+
72
+ # Send to a specific channel
73
+ try:
74
+ logger.send_to_channel(AlertLevel.ERROR, "Send to another channel", channel="another-channel-id")
75
+ except Exception as e:
76
+ print(f"Failed to send alert: {e}")
77
+
78
+ # Send to a different provider dynamically
79
+ try:
80
+ logger.custom_send("slack", AlertLevel.ERROR, "Message via Slack", channel="slack-channel")
81
+ except Exception as e:
82
+ print(f"Failed to send alert: {e}")
83
+ ```
84
+
85
+ ## Send Methods
86
+
87
+ commonlog supports two send methods: WebClient (API-based) and Webhook (simple HTTP POST).
88
+
89
+ ### WebClient Usage
90
+
91
+ WebClient uses the full API with authentication tokens:
92
+
93
+ ```python
94
+ config = Config(
95
+ provider="lark",
96
+ send_method=SendMethod.WEBCLIENT,
97
+ token="app_id++app_secret", # for Lark
98
+ slack_token="xoxb-your-slack-token", # for Slack
99
+ lark_token=LarkToken(app_id="your-app-id", app_secret="your-app-secret"),
100
+ channel="your_channel",
101
+ redis_host="localhost", # required for Lark
102
+ redis_port="6379",
103
+ )
104
+ ```
105
+
106
+ ### Webhook Usage
107
+
108
+ Webhook is simpler and requires only a webhook URL:
109
+
110
+ ```python
111
+ config = Config(
112
+ provider="slack",
113
+ send_method=SendMethod.WEBHOOK,
114
+ token="https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
115
+ channel="optional-channel-override", # optional
116
+ )
117
+ ```
118
+
119
+ ### Lark Token Caching
120
+
121
+ When using Lark, the tenant_access_token is cached in Redis. The expiry is set dynamically from the API response minus 10 minutes. You must set `redis_host` and `redis_port` in your config.
122
+
123
+ ## Channel Mapping
124
+
125
+ You can configure different channels for different alert levels using a channel resolver:
126
+
127
+ ```python
128
+ from commonlog import commonlog, Config, SendMethod, AlertLevel, DefaultChannelResolver
129
+
130
+ # Create a channel resolver
131
+ resolver = DefaultChannelResolver(
132
+ channel_map={
133
+ AlertLevel.INFO: "#general",
134
+ AlertLevel.WARN: "#warnings",
135
+ AlertLevel.ERROR: "#alerts",
136
+ },
137
+ default_channel="#general"
138
+ )
139
+
140
+ # Create config with channel resolver
141
+ config = Config(
142
+ provider="slack",
143
+ send_method=SendMethod.WEBCLIENT,
144
+ token="xoxb-your-slack-bot-token",
145
+ channel_resolver=resolver,
146
+ service_name="user-service",
147
+ environment="production"
148
+ )
149
+
150
+ logger = commonlog(config)
151
+
152
+ # These will go to different channels based on level
153
+ logger.send(AlertLevel.INFO, "Info message") # goes to #general
154
+ logger.send(AlertLevel.WARN, "Warning message") # goes to #warnings
155
+ logger.send(AlertLevel.ERROR, "Error message") # goes to #alerts
156
+ ```
157
+
158
+ ### Custom Channel Resolver
159
+
160
+ You can implement custom channel resolution logic:
161
+
162
+ ```python
163
+ class CustomResolver(ChannelResolver):
164
+ def resolve_channel(self, level):
165
+ if level == AlertLevel.ERROR:
166
+ return "#critical-alerts"
167
+ elif level == AlertLevel.WARN:
168
+ return "#monitoring"
169
+ else:
170
+ return "#general"
171
+ ```
172
+
173
+ ## Configuration Options
174
+
175
+ ### Common Settings
176
+
177
+ - **provider**: `"slack"` or `"lark"`
178
+ - **send_method**: `"webclient"` (token-based authentication)
179
+ - **channel**: Target channel or chat ID (used if no resolver)
180
+ - **channel_resolver**: Optional resolver for dynamic channel mapping
181
+ - **service_name**: Name of the service sending alerts
182
+ - **environment**: Environment (dev, staging, production)
183
+ - **debug**: `True` to enable detailed debug logging of all internal processes
184
+
185
+ ### Provider-Specific
186
+
187
+ - **token**: API token for WebClient authentication (required)
188
+
189
+ ## Alert Levels
190
+
191
+ - **INFO**: Logs locally only
192
+ - **WARN**: Logs + sends alert
193
+ - **ERROR**: Always sends alert
194
+
195
+ ## File Attachments
196
+
197
+ Provide a public URL. The library appends it to the message for simplicity.
198
+
199
+ ```python
200
+ attachment = Attachment(url="https://example.com/log.txt")
201
+ logger.send(AlertLevel.ERROR, "Error with log", attachment)
202
+ ```
203
+
204
+ ## Trace Log Section
205
+
206
+ When `include_trace` is set to `True`, you can pass trace information as the fourth parameter to `send()`:
207
+
208
+ ```python
209
+ trace = """Traceback (most recent call last):
210
+ File "app.py", line 10, in main
211
+ raise ValueError("Something went wrong")
212
+ ValueError: Something went wrong"""
213
+
214
+ logger.send(AlertLevel.ERROR, "System error occurred", None, trace)
215
+ ```
216
+
217
+ This will format the trace as a code block in the alert message.
218
+
219
+ ## Testing
220
+
221
+ ```bash
222
+ cd python
223
+ PYTHONPATH=.. python -m unittest test_commonlog.py
224
+ ```
225
+
226
+ ## API Reference
227
+
228
+ ### Classes
229
+
230
+ - `Config`: Configuration class
231
+ - `Attachment`: File attachment class
232
+ - `Provider`: Abstract base class for alert providers
233
+ - `commonlog`: Main logger class
234
+
235
+ ### Constants
236
+
237
+ - `SendMethod.WEBCLIENT`: Send method (token-based authentication)
238
+ - `AlertLevel.INFO`, `AlertLevel.WARN`, `AlertLevel.ERROR`: Alert levels
239
+
240
+ ### Methods
241
+
242
+ - `commonlog(config)`: Create a new logger
243
+ - `commonlog.send(level, message, attachment=None, trace="")`: Send alert with optional trace
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ pycommonlog/__init__.py
5
+ pycommonlog/log_types.py
6
+ pycommonlog/logger.py
7
+ pycommonlog.egg-info/PKG-INFO
8
+ pycommonlog.egg-info/SOURCES.txt
9
+ pycommonlog.egg-info/dependency_links.txt
10
+ pycommonlog.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ pycommonlog
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,35 @@
1
+ from setuptools import setup, find_packages
2
+ import subprocess
3
+
4
+ def get_latest_git_tag():
5
+ try:
6
+ tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"]).decode().strip()
7
+ # Clean up invalid version formats
8
+ if '-v' in tag:
9
+ # Extract base version from tags like 0.1.7-v24
10
+ base_version = tag.split('-v')[0]
11
+ return base_version
12
+ return tag
13
+ except Exception:
14
+ return "0.0.0"
15
+
16
+ setup(
17
+ name="pycommonlog",
18
+ version=get_latest_git_tag(),
19
+ description="Unified logging and alerting library for Python.",
20
+ long_description=open("README.md").read(),
21
+ long_description_content_type="text/markdown",
22
+ author="Alvian Rahman Hanif",
23
+ author_email="alvian.hanif@pasarpolis.com",
24
+ url="https://github.com/alvianhanif/pycommonlog",
25
+ packages=["pycommonlog"],
26
+ install_requires=[],
27
+ license="MIT",
28
+ python_requires=">=3.8",
29
+ classifiers=[
30
+ "Programming Language :: Python :: 3",
31
+ "License :: OSI Approved :: MIT License",
32
+ "Operating System :: OS Independent",
33
+ ],
34
+ include_package_data=True,
35
+ )