easy-tg-logger 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easy_tg_logger-0.1.0/LICENSE +21 -0
- easy_tg_logger-0.1.0/PKG-INFO +111 -0
- easy_tg_logger-0.1.0/README.md +99 -0
- easy_tg_logger-0.1.0/pyproject.toml +22 -0
- easy_tg_logger-0.1.0/setup.cfg +4 -0
- easy_tg_logger-0.1.0/src/easy_tg_logger.egg-info/PKG-INFO +111 -0
- easy_tg_logger-0.1.0/src/easy_tg_logger.egg-info/SOURCES.txt +8 -0
- easy_tg_logger-0.1.0/src/easy_tg_logger.egg-info/dependency_links.txt +1 -0
- easy_tg_logger-0.1.0/src/easy_tg_logger.egg-info/top_level.txt +1 -0
- easy_tg_logger-0.1.0/src/pytelegram_logger/__init__.py +384 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2011-2025 The Bootstrap Authors
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy_tg_logger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minimal Python logger with Telegram notifications
|
|
5
|
+
Author: Beltrán Offerrall
|
|
6
|
+
Project-URL: Homepage, https://github.com/offerrall/py-telegram-logger
|
|
7
|
+
Project-URL: Repository, https://github.com/offerrall/py-telegram-logger
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# easy_tg_logger
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/easy_tg_logger/)
|
|
16
|
+
[](https://pypi.org/project/easy_tg_logger/)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
|
|
19
|
+
Fast, minimal async logger for local files with optional Telegram alerts. **Zero dependencies** (stdlib only).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install easy_tg_logger
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from pytelegram_logger import init_telegram_logger, log, shutdown_logger
|
|
31
|
+
|
|
32
|
+
init_telegram_logger(name="my_app")
|
|
33
|
+
|
|
34
|
+
log("App started")
|
|
35
|
+
log("Database error", is_error=True)
|
|
36
|
+
|
|
37
|
+
shutdown_logger() # optional, flushes pending logs
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### With Telegram alerts
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
init_telegram_logger(
|
|
44
|
+
name="my_app",
|
|
45
|
+
telegram_token_logs="BOT_TOKEN_1", # routine logs
|
|
46
|
+
telegram_token_errors="BOT_TOKEN_2", # error alerts
|
|
47
|
+
telegram_chat_ids=["-1001234567890"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
log("Payment received", send_telegram=True)
|
|
51
|
+
log("Critical failure", is_error=True, send_telegram=True)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Two tokens = two channels: routine logs stay separate from high-priority error alerts.
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### `init_telegram_logger(name, ...)`
|
|
59
|
+
|
|
60
|
+
| Argument | Default | Description |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `name` | — | **Required.** Unique instance id (e.g. `"api"`, `"worker_1"`). |
|
|
63
|
+
| `log_dir` | `"logs"` | Directory for log files. |
|
|
64
|
+
| `telegram_token_logs` | `None` | Bot token for routine logs. |
|
|
65
|
+
| `telegram_token_errors` | `None` | Bot token for errors. |
|
|
66
|
+
| `telegram_chat_ids` | `[]` | List of chat IDs. |
|
|
67
|
+
| `retention_days` | `30` | Auto-delete files older than N days. |
|
|
68
|
+
| `queue_maxsize` | `10000` | Max pending messages per queue. |
|
|
69
|
+
|
|
70
|
+
### `log(message, is_error=False, send_telegram=False, save=True)`
|
|
71
|
+
|
|
72
|
+
- `is_error=True` → error file + error token
|
|
73
|
+
- `send_telegram=True` → also send to Telegram
|
|
74
|
+
- `save=False` → Telegram only, no file
|
|
75
|
+
|
|
76
|
+
### `shutdown_logger()`
|
|
77
|
+
|
|
78
|
+
Graceful shutdown: drains queues and closes files. Optional but recommended.
|
|
79
|
+
|
|
80
|
+
### `get_dropped_count(sink=None)`
|
|
81
|
+
|
|
82
|
+
Messages dropped because a queue was full (logging never blocks the caller).
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
get_dropped_count() # total (disk + Telegram)
|
|
86
|
+
get_dropped_count("file") # local logs lost ← the one that matters
|
|
87
|
+
get_dropped_count("telegram") # Telegram alerts lost
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## How it works
|
|
91
|
+
|
|
92
|
+
- **Disk and network are isolated.** Separate queue + worker each, so a slow or down Telegram never delays local logs. The file is the critical path: fast and reliable.
|
|
93
|
+
- **Non-blocking.** `log()` drops and counts when a queue is full instead of freezing your app.
|
|
94
|
+
- **Crash-safe.** Every line is flushed to disk immediately (durability over throughput).
|
|
95
|
+
- **Daily rotation** per named instance, with auto-cleanup after `retention_days`.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
logs/
|
|
99
|
+
├── my_app_logs_2025_01_21.log
|
|
100
|
+
├── my_app_errors_2025_01_21.log
|
|
101
|
+
└── worker_1_logs_2025_01_21.log
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Requirements
|
|
105
|
+
|
|
106
|
+
- Python 3.10+
|
|
107
|
+
- No external dependencies
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# easy_tg_logger
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/easy_tg_logger/)
|
|
4
|
+
[](https://pypi.org/project/easy_tg_logger/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Fast, minimal async logger for local files with optional Telegram alerts. **Zero dependencies** (stdlib only).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install easy_tg_logger
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from pytelegram_logger import init_telegram_logger, log, shutdown_logger
|
|
19
|
+
|
|
20
|
+
init_telegram_logger(name="my_app")
|
|
21
|
+
|
|
22
|
+
log("App started")
|
|
23
|
+
log("Database error", is_error=True)
|
|
24
|
+
|
|
25
|
+
shutdown_logger() # optional, flushes pending logs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### With Telegram alerts
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
init_telegram_logger(
|
|
32
|
+
name="my_app",
|
|
33
|
+
telegram_token_logs="BOT_TOKEN_1", # routine logs
|
|
34
|
+
telegram_token_errors="BOT_TOKEN_2", # error alerts
|
|
35
|
+
telegram_chat_ids=["-1001234567890"],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
log("Payment received", send_telegram=True)
|
|
39
|
+
log("Critical failure", is_error=True, send_telegram=True)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Two tokens = two channels: routine logs stay separate from high-priority error alerts.
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### `init_telegram_logger(name, ...)`
|
|
47
|
+
|
|
48
|
+
| Argument | Default | Description |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `name` | — | **Required.** Unique instance id (e.g. `"api"`, `"worker_1"`). |
|
|
51
|
+
| `log_dir` | `"logs"` | Directory for log files. |
|
|
52
|
+
| `telegram_token_logs` | `None` | Bot token for routine logs. |
|
|
53
|
+
| `telegram_token_errors` | `None` | Bot token for errors. |
|
|
54
|
+
| `telegram_chat_ids` | `[]` | List of chat IDs. |
|
|
55
|
+
| `retention_days` | `30` | Auto-delete files older than N days. |
|
|
56
|
+
| `queue_maxsize` | `10000` | Max pending messages per queue. |
|
|
57
|
+
|
|
58
|
+
### `log(message, is_error=False, send_telegram=False, save=True)`
|
|
59
|
+
|
|
60
|
+
- `is_error=True` → error file + error token
|
|
61
|
+
- `send_telegram=True` → also send to Telegram
|
|
62
|
+
- `save=False` → Telegram only, no file
|
|
63
|
+
|
|
64
|
+
### `shutdown_logger()`
|
|
65
|
+
|
|
66
|
+
Graceful shutdown: drains queues and closes files. Optional but recommended.
|
|
67
|
+
|
|
68
|
+
### `get_dropped_count(sink=None)`
|
|
69
|
+
|
|
70
|
+
Messages dropped because a queue was full (logging never blocks the caller).
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
get_dropped_count() # total (disk + Telegram)
|
|
74
|
+
get_dropped_count("file") # local logs lost ← the one that matters
|
|
75
|
+
get_dropped_count("telegram") # Telegram alerts lost
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
- **Disk and network are isolated.** Separate queue + worker each, so a slow or down Telegram never delays local logs. The file is the critical path: fast and reliable.
|
|
81
|
+
- **Non-blocking.** `log()` drops and counts when a queue is full instead of freezing your app.
|
|
82
|
+
- **Crash-safe.** Every line is flushed to disk immediately (durability over throughput).
|
|
83
|
+
- **Daily rotation** per named instance, with auto-cleanup after `retention_days`.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
logs/
|
|
87
|
+
├── my_app_logs_2025_01_21.log
|
|
88
|
+
├── my_app_errors_2025_01_21.log
|
|
89
|
+
└── worker_1_logs_2025_01_21.log
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Requirements
|
|
93
|
+
|
|
94
|
+
- Python 3.10+
|
|
95
|
+
- No external dependencies
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "easy_tg_logger"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Beltrán Offerrall"}
|
|
10
|
+
]
|
|
11
|
+
description = "Minimal Python logger with Telegram notifications"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/offerrall/py-telegram-logger"
|
|
18
|
+
Repository = "https://github.com/offerrall/py-telegram-logger"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
22
|
+
include = ["pytelegram_logger*"]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy_tg_logger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minimal Python logger with Telegram notifications
|
|
5
|
+
Author: Beltrán Offerrall
|
|
6
|
+
Project-URL: Homepage, https://github.com/offerrall/py-telegram-logger
|
|
7
|
+
Project-URL: Repository, https://github.com/offerrall/py-telegram-logger
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# easy_tg_logger
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/easy_tg_logger/)
|
|
16
|
+
[](https://pypi.org/project/easy_tg_logger/)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
|
|
19
|
+
Fast, minimal async logger for local files with optional Telegram alerts. **Zero dependencies** (stdlib only).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install easy_tg_logger
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from pytelegram_logger import init_telegram_logger, log, shutdown_logger
|
|
31
|
+
|
|
32
|
+
init_telegram_logger(name="my_app")
|
|
33
|
+
|
|
34
|
+
log("App started")
|
|
35
|
+
log("Database error", is_error=True)
|
|
36
|
+
|
|
37
|
+
shutdown_logger() # optional, flushes pending logs
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### With Telegram alerts
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
init_telegram_logger(
|
|
44
|
+
name="my_app",
|
|
45
|
+
telegram_token_logs="BOT_TOKEN_1", # routine logs
|
|
46
|
+
telegram_token_errors="BOT_TOKEN_2", # error alerts
|
|
47
|
+
telegram_chat_ids=["-1001234567890"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
log("Payment received", send_telegram=True)
|
|
51
|
+
log("Critical failure", is_error=True, send_telegram=True)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Two tokens = two channels: routine logs stay separate from high-priority error alerts.
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### `init_telegram_logger(name, ...)`
|
|
59
|
+
|
|
60
|
+
| Argument | Default | Description |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `name` | — | **Required.** Unique instance id (e.g. `"api"`, `"worker_1"`). |
|
|
63
|
+
| `log_dir` | `"logs"` | Directory for log files. |
|
|
64
|
+
| `telegram_token_logs` | `None` | Bot token for routine logs. |
|
|
65
|
+
| `telegram_token_errors` | `None` | Bot token for errors. |
|
|
66
|
+
| `telegram_chat_ids` | `[]` | List of chat IDs. |
|
|
67
|
+
| `retention_days` | `30` | Auto-delete files older than N days. |
|
|
68
|
+
| `queue_maxsize` | `10000` | Max pending messages per queue. |
|
|
69
|
+
|
|
70
|
+
### `log(message, is_error=False, send_telegram=False, save=True)`
|
|
71
|
+
|
|
72
|
+
- `is_error=True` → error file + error token
|
|
73
|
+
- `send_telegram=True` → also send to Telegram
|
|
74
|
+
- `save=False` → Telegram only, no file
|
|
75
|
+
|
|
76
|
+
### `shutdown_logger()`
|
|
77
|
+
|
|
78
|
+
Graceful shutdown: drains queues and closes files. Optional but recommended.
|
|
79
|
+
|
|
80
|
+
### `get_dropped_count(sink=None)`
|
|
81
|
+
|
|
82
|
+
Messages dropped because a queue was full (logging never blocks the caller).
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
get_dropped_count() # total (disk + Telegram)
|
|
86
|
+
get_dropped_count("file") # local logs lost ← the one that matters
|
|
87
|
+
get_dropped_count("telegram") # Telegram alerts lost
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## How it works
|
|
91
|
+
|
|
92
|
+
- **Disk and network are isolated.** Separate queue + worker each, so a slow or down Telegram never delays local logs. The file is the critical path: fast and reliable.
|
|
93
|
+
- **Non-blocking.** `log()` drops and counts when a queue is full instead of freezing your app.
|
|
94
|
+
- **Crash-safe.** Every line is flushed to disk immediately (durability over throughput).
|
|
95
|
+
- **Daily rotation** per named instance, with auto-cleanup after `retention_days`.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
logs/
|
|
99
|
+
├── my_app_logs_2025_01_21.log
|
|
100
|
+
├── my_app_errors_2025_01_21.log
|
|
101
|
+
└── worker_1_logs_2025_01_21.log
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Requirements
|
|
105
|
+
|
|
106
|
+
- Python 3.10+
|
|
107
|
+
- No external dependencies
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytelegram_logger
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import html
|
|
4
|
+
import json
|
|
5
|
+
import urllib.request
|
|
6
|
+
import urllib.error
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from queue import Queue, Empty, Full
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
__all__ = ['init_telegram_logger', 'log', 'shutdown_logger', 'get_dropped_count']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LoggerState:
|
|
18
|
+
log_dir: Path | None = None
|
|
19
|
+
telegram_token_logs: str | None = None
|
|
20
|
+
telegram_token_errors: str | None = None
|
|
21
|
+
telegram_chat_ids: list = field(default_factory=list)
|
|
22
|
+
retention_days: int = 30
|
|
23
|
+
name: str = ""
|
|
24
|
+
|
|
25
|
+
file_queue: Queue | None = None
|
|
26
|
+
telegram_queue: Queue | None = None
|
|
27
|
+
|
|
28
|
+
running: bool = False
|
|
29
|
+
file_worker_thread: threading.Thread | None = None
|
|
30
|
+
telegram_worker_thread: threading.Thread | None = None
|
|
31
|
+
cleanup_thread: threading.Thread | None = None
|
|
32
|
+
|
|
33
|
+
log_file: object = None
|
|
34
|
+
error_file: object = None
|
|
35
|
+
current_log_path: str = ""
|
|
36
|
+
current_error_path: str = ""
|
|
37
|
+
|
|
38
|
+
cached_date: str = ""
|
|
39
|
+
cached_log_path: Path | None = None
|
|
40
|
+
cached_error_path: Path | None = None
|
|
41
|
+
|
|
42
|
+
dropped_file: int = 0
|
|
43
|
+
dropped_telegram: int = 0
|
|
44
|
+
dropped_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
state = LoggerState()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def init_telegram_logger(
|
|
51
|
+
log_dir: str = "logs",
|
|
52
|
+
telegram_token_logs: str | None = None,
|
|
53
|
+
telegram_token_errors: str | None = None,
|
|
54
|
+
telegram_chat_ids: list | None = None,
|
|
55
|
+
retention_days: int = 30,
|
|
56
|
+
name: str = "",
|
|
57
|
+
queue_maxsize: int = 10000,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize the Telegram logger with specified configuration.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
log_dir: Directory where log files will be stored
|
|
63
|
+
telegram_token_logs: Bot token for general log notifications
|
|
64
|
+
telegram_token_errors: Bot token for error notifications
|
|
65
|
+
telegram_chat_ids: List of Telegram chat IDs to send notifications to
|
|
66
|
+
retention_days: Number of days to keep log files before auto-deletion
|
|
67
|
+
name: Unique identifier for this logger instance (required)
|
|
68
|
+
queue_maxsize: Max pending messages per queue (file and Telegram have
|
|
69
|
+
independent queues). When a queue is full, new messages for that
|
|
70
|
+
sink are dropped and counted (see get_dropped_count()).
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
RuntimeError: If logger is already initialized
|
|
74
|
+
ValueError: If name is empty or whitespace
|
|
75
|
+
"""
|
|
76
|
+
if state.running:
|
|
77
|
+
raise RuntimeError("Logger already initialized")
|
|
78
|
+
|
|
79
|
+
if not name or name.strip() == "":
|
|
80
|
+
raise ValueError("Logger name must be provided, cannot be empty")
|
|
81
|
+
|
|
82
|
+
state.log_dir = Path(log_dir)
|
|
83
|
+
state.log_dir.mkdir(exist_ok=True)
|
|
84
|
+
|
|
85
|
+
state.telegram_token_logs = telegram_token_logs
|
|
86
|
+
state.telegram_token_errors = telegram_token_errors
|
|
87
|
+
state.telegram_chat_ids = telegram_chat_ids or []
|
|
88
|
+
state.retention_days = retention_days
|
|
89
|
+
state.name = name
|
|
90
|
+
|
|
91
|
+
state.file_queue = Queue(maxsize=queue_maxsize)
|
|
92
|
+
state.telegram_queue = Queue(maxsize=queue_maxsize)
|
|
93
|
+
state.dropped_file = 0
|
|
94
|
+
state.dropped_telegram = 0
|
|
95
|
+
state.running = True
|
|
96
|
+
|
|
97
|
+
state.file_worker_thread = threading.Thread(target=file_worker, daemon=True)
|
|
98
|
+
state.file_worker_thread.start()
|
|
99
|
+
|
|
100
|
+
state.telegram_worker_thread = threading.Thread(target=telegram_worker, daemon=True)
|
|
101
|
+
state.telegram_worker_thread.start()
|
|
102
|
+
|
|
103
|
+
state.cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
|
|
104
|
+
state.cleanup_thread.start()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_daily_file(is_error: bool = False) -> Path:
|
|
108
|
+
"""Get the path for today's log file.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
is_error: If True, return error log path; otherwise return general log path
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Path object for the appropriate log file
|
|
115
|
+
"""
|
|
116
|
+
now = datetime.now()
|
|
117
|
+
date_str = f"{now.year}_{now.month:02d}_{now.day:02d}"
|
|
118
|
+
|
|
119
|
+
if state.cached_date != date_str:
|
|
120
|
+
state.cached_log_path = state.log_dir / f"{state.name}_logs_{date_str}.log"
|
|
121
|
+
state.cached_error_path = state.log_dir / f"{state.name}_errors_{date_str}.log"
|
|
122
|
+
state.cached_date = date_str
|
|
123
|
+
|
|
124
|
+
return state.cached_error_path if is_error else state.cached_log_path
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def write_to_file(message: str, is_error: bool = False) -> None:
|
|
128
|
+
"""Write a log message to the appropriate file.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
message: The log message to write
|
|
132
|
+
is_error: If True, write to error log; otherwise write to general log
|
|
133
|
+
"""
|
|
134
|
+
filepath = get_daily_file(is_error)
|
|
135
|
+
filepath_str = str(filepath)
|
|
136
|
+
|
|
137
|
+
now = datetime.now()
|
|
138
|
+
timestamp = '%04d-%02d-%02d %02d:%02d:%02d' % (now.year, now.month, now.day, now.hour, now.minute, now.second)
|
|
139
|
+
|
|
140
|
+
if is_error:
|
|
141
|
+
if state.current_error_path != filepath_str:
|
|
142
|
+
if state.error_file:
|
|
143
|
+
state.error_file.close()
|
|
144
|
+
state.error_file = open(filepath, "a", encoding="utf-8")
|
|
145
|
+
state.current_error_path = filepath_str
|
|
146
|
+
|
|
147
|
+
file_handle = state.error_file
|
|
148
|
+
else:
|
|
149
|
+
if state.current_log_path != filepath_str:
|
|
150
|
+
if state.log_file:
|
|
151
|
+
state.log_file.close()
|
|
152
|
+
state.log_file = open(filepath, "a", encoding="utf-8")
|
|
153
|
+
state.current_log_path = filepath_str
|
|
154
|
+
|
|
155
|
+
file_handle = state.log_file
|
|
156
|
+
|
|
157
|
+
file_handle.write(f"[{timestamp}] {message}\n")
|
|
158
|
+
file_handle.flush()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def send_telegram(message: str, is_error: bool = False) -> None:
|
|
162
|
+
"""Send a log message to Telegram.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
message: The log message to send
|
|
166
|
+
is_error: If True, use error token and prefix; otherwise use log token
|
|
167
|
+
"""
|
|
168
|
+
token = state.telegram_token_errors if is_error else state.telegram_token_logs
|
|
169
|
+
|
|
170
|
+
if not token or not state.telegram_chat_ids:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
log_type = "🔴 ERROR" if is_error else "ℹ️ LOG"
|
|
174
|
+
full_message = f"{log_type}\n\n{html.escape(message)}"
|
|
175
|
+
|
|
176
|
+
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
177
|
+
|
|
178
|
+
for chat_id in state.telegram_chat_ids:
|
|
179
|
+
try:
|
|
180
|
+
data = json.dumps({
|
|
181
|
+
"chat_id": chat_id,
|
|
182
|
+
"text": full_message,
|
|
183
|
+
"parse_mode": "HTML",
|
|
184
|
+
}).encode("utf-8")
|
|
185
|
+
|
|
186
|
+
req = urllib.request.Request(
|
|
187
|
+
url,
|
|
188
|
+
data=data,
|
|
189
|
+
headers={"Content-Type": "application/json"},
|
|
190
|
+
method="POST",
|
|
191
|
+
)
|
|
192
|
+
urllib.request.urlopen(req, timeout=5)
|
|
193
|
+
time.sleep(0.05)
|
|
194
|
+
except urllib.error.URLError as e:
|
|
195
|
+
print(f"[pytelegram_logger] Telegram API error for chat {chat_id}: {e}", file=sys.stderr)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
print(f"[pytelegram_logger] Unexpected error sending to Telegram chat {chat_id}: {e}", file=sys.stderr)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def file_worker() -> None:
|
|
201
|
+
"""Background worker thread that writes log messages to disk."""
|
|
202
|
+
while state.running:
|
|
203
|
+
try:
|
|
204
|
+
item = state.file_queue.get(timeout=1)
|
|
205
|
+
except Empty:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
if item is None:
|
|
209
|
+
state.file_queue.task_done()
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
message, is_error = item
|
|
213
|
+
try:
|
|
214
|
+
write_to_file(message, is_error)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
print(f"[pytelegram_logger] Error writing log to file: {e}", file=sys.stderr)
|
|
217
|
+
finally:
|
|
218
|
+
state.file_queue.task_done()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def telegram_worker() -> None:
|
|
222
|
+
"""Background worker thread that sends log messages to Telegram."""
|
|
223
|
+
while state.running:
|
|
224
|
+
try:
|
|
225
|
+
item = state.telegram_queue.get(timeout=1)
|
|
226
|
+
except Empty:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
if item is None:
|
|
230
|
+
state.telegram_queue.task_done()
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
message, is_error = item
|
|
234
|
+
try:
|
|
235
|
+
send_telegram(message, is_error)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print(f"[pytelegram_logger] Error sending log to Telegram: {e}", file=sys.stderr)
|
|
238
|
+
finally:
|
|
239
|
+
state.telegram_queue.task_done()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def cleanup_old_logs() -> None:
|
|
243
|
+
"""Delete log files older than the configured retention period."""
|
|
244
|
+
if state.log_dir is None:
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
cutoff_date = datetime.now() - timedelta(days=state.retention_days)
|
|
248
|
+
|
|
249
|
+
log_pattern = f"{state.name}_logs_*.log"
|
|
250
|
+
error_pattern = f"{state.name}_errors_*.log"
|
|
251
|
+
|
|
252
|
+
for pattern in [log_pattern, error_pattern]:
|
|
253
|
+
for log_file in state.log_dir.glob(pattern):
|
|
254
|
+
try:
|
|
255
|
+
mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
|
|
256
|
+
if mtime < cutoff_date:
|
|
257
|
+
log_file.unlink()
|
|
258
|
+
except (OSError, ValueError) as e:
|
|
259
|
+
print(f"[pytelegram_logger] Error deleting old log file {log_file}: {e}", file=sys.stderr)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
print(f"[pytelegram_logger] Unexpected error during cleanup of {log_file}: {e}", file=sys.stderr)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cleanup_worker() -> None:
|
|
265
|
+
"""Background worker thread that periodically cleans up old log files."""
|
|
266
|
+
while state.running:
|
|
267
|
+
time.sleep(3600)
|
|
268
|
+
cleanup_old_logs()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def shutdown_logger() -> None:
|
|
272
|
+
"""Gracefully shutdown the logger and close all resources.
|
|
273
|
+
|
|
274
|
+
Waits for all queued messages (file and Telegram) to be processed before
|
|
275
|
+
shutting down.
|
|
276
|
+
"""
|
|
277
|
+
if not state.running:
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if state.file_queue:
|
|
281
|
+
state.file_queue.join()
|
|
282
|
+
if state.telegram_queue:
|
|
283
|
+
state.telegram_queue.join()
|
|
284
|
+
|
|
285
|
+
state.running = False
|
|
286
|
+
|
|
287
|
+
if state.file_worker_thread:
|
|
288
|
+
state.file_worker_thread.join(timeout=5)
|
|
289
|
+
|
|
290
|
+
if state.telegram_worker_thread:
|
|
291
|
+
state.telegram_worker_thread.join(timeout=5)
|
|
292
|
+
|
|
293
|
+
if state.cleanup_thread:
|
|
294
|
+
state.cleanup_thread.join(timeout=1)
|
|
295
|
+
|
|
296
|
+
if state.log_file:
|
|
297
|
+
state.log_file.close()
|
|
298
|
+
state.log_file = None
|
|
299
|
+
|
|
300
|
+
if state.error_file:
|
|
301
|
+
state.error_file.close()
|
|
302
|
+
state.error_file = None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _drop_file() -> None:
|
|
306
|
+
"""Increment the dropped-file-message counter (thread-safe)."""
|
|
307
|
+
with state.dropped_lock:
|
|
308
|
+
state.dropped_file += 1
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _drop_telegram() -> None:
|
|
312
|
+
"""Increment the dropped-Telegram-message counter (thread-safe)."""
|
|
313
|
+
with state.dropped_lock:
|
|
314
|
+
state.dropped_telegram += 1
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_dropped_count(sink: str | None = None) -> int:
|
|
318
|
+
"""Return how many messages were dropped because a queue was full.
|
|
319
|
+
|
|
320
|
+
Useful to detect a saturated disk sink or a stuck/down Telegram without
|
|
321
|
+
ever blocking the application. Disk and Telegram are counted separately:
|
|
322
|
+
losing a local log is worse than losing a Telegram notification.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
sink: "file" for disk-only, "telegram" for Telegram-only, or None
|
|
326
|
+
(default) for the combined total.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Number of dropped messages for the requested sink. Returns 0 cleanly
|
|
330
|
+
even if called before init_telegram_logger().
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
ValueError: If sink is not None, "file" or "telegram".
|
|
334
|
+
"""
|
|
335
|
+
if sink not in (None, "file", "telegram"):
|
|
336
|
+
raise ValueError('sink must be None, "file" or "telegram"')
|
|
337
|
+
|
|
338
|
+
with state.dropped_lock:
|
|
339
|
+
if sink == "file":
|
|
340
|
+
return state.dropped_file
|
|
341
|
+
if sink == "telegram":
|
|
342
|
+
return state.dropped_telegram
|
|
343
|
+
return state.dropped_file + state.dropped_telegram
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def log(message: str, is_error: bool = False, send_telegram: bool = False, save: bool = True) -> None:
|
|
347
|
+
"""Log a message to file and/or Telegram.
|
|
348
|
+
|
|
349
|
+
Non-blocking: if a queue is full the message is dropped and counted
|
|
350
|
+
(see get_dropped_count()). Logging must never freeze the caller.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
message: The message to log
|
|
354
|
+
is_error: If True, treat as error (different file and Telegram token)
|
|
355
|
+
send_telegram: If True, send notification to Telegram
|
|
356
|
+
save: If True, save to log file; if False, only send to Telegram
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
RuntimeError: If logger not initialized
|
|
360
|
+
ValueError: If Telegram is requested but not properly configured
|
|
361
|
+
"""
|
|
362
|
+
if not state.running or state.file_queue is None or state.telegram_queue is None:
|
|
363
|
+
raise RuntimeError("Logger not initialized. Call init_telegram_logger() first")
|
|
364
|
+
|
|
365
|
+
if send_telegram and not state.telegram_chat_ids:
|
|
366
|
+
raise ValueError("Telegram chat IDs not configured")
|
|
367
|
+
|
|
368
|
+
if send_telegram and is_error and not state.telegram_token_errors:
|
|
369
|
+
raise ValueError("Telegram token for errors not configured")
|
|
370
|
+
|
|
371
|
+
if send_telegram and not is_error and not state.telegram_token_logs:
|
|
372
|
+
raise ValueError("Telegram token for logs not configured")
|
|
373
|
+
|
|
374
|
+
if save:
|
|
375
|
+
try:
|
|
376
|
+
state.file_queue.put_nowait((message, is_error))
|
|
377
|
+
except Full:
|
|
378
|
+
_drop_file()
|
|
379
|
+
|
|
380
|
+
if send_telegram:
|
|
381
|
+
try:
|
|
382
|
+
state.telegram_queue.put_nowait((message, is_error))
|
|
383
|
+
except Full:
|
|
384
|
+
_drop_telegram()
|