miu-logger 0.1.0__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.
- miu_logger/__init__.py +5 -0
- miu_logger/conditional.py +23 -0
- miu_logger/config.py +11 -0
- miu_logger/filters.py +18 -0
- miu_logger/formatters.py +26 -0
- miu_logger/listener.py +106 -0
- miu_logger/logger_factory.py +14 -0
- miu_logger/repository.py +76 -0
- miu_logger-0.1.0.dist-info/METADATA +10 -0
- miu_logger-0.1.0.dist-info/RECORD +13 -0
- miu_logger-0.1.0.dist-info/WHEEL +5 -0
- miu_logger-0.1.0.dist-info/licenses/LICENSE +9 -0
- miu_logger-0.1.0.dist-info/top_level.txt +1 -0
miu_logger/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConditionalLogger:
|
|
6
|
+
def __init__(self, logger: logging.Logger, enabled_predicate: Callable[[], bool]):
|
|
7
|
+
self._logger = logger
|
|
8
|
+
self._enabled = enabled_predicate
|
|
9
|
+
|
|
10
|
+
def __getattr__(self, item):
|
|
11
|
+
attr = getattr(self._logger, item)
|
|
12
|
+
|
|
13
|
+
if not callable(attr):
|
|
14
|
+
return attr
|
|
15
|
+
|
|
16
|
+
def wrapper(*a, **kw):
|
|
17
|
+
if self._enabled():
|
|
18
|
+
return attr(*a, **kw)
|
|
19
|
+
|
|
20
|
+
return wrapper
|
|
21
|
+
|
|
22
|
+
def get_real_logger(self) -> logging.Logger:
|
|
23
|
+
return self._logger
|
miu_logger/config.py
ADDED
miu_logger/filters.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LoggerNameFilter(logging.Filter):
|
|
5
|
+
def __init__(self, name: str):
|
|
6
|
+
super().__init__()
|
|
7
|
+
self.name = name
|
|
8
|
+
|
|
9
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
10
|
+
return record.name == self.name
|
|
11
|
+
|
|
12
|
+
class ExactLevelFilter(logging.Filter):
|
|
13
|
+
def __init__(self, level: int):
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.level = level
|
|
16
|
+
|
|
17
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
18
|
+
return record.levelno == self.level
|
miu_logger/formatters.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import colorama
|
|
3
|
+
from colorama import Fore, Style
|
|
4
|
+
from copy import copy
|
|
5
|
+
|
|
6
|
+
colorama.init(autoreset=True)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ColoredFormatter(logging.Formatter):
|
|
10
|
+
LEVEL_COLORS = {
|
|
11
|
+
logging.DEBUG: Fore.BLUE,
|
|
12
|
+
logging.INFO: Fore.GREEN,
|
|
13
|
+
logging.WARNING: Fore.YELLOW,
|
|
14
|
+
logging.ERROR: Fore.RED,
|
|
15
|
+
logging.CRITICAL: Fore.MAGENTA,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
19
|
+
record_copy = copy(record)
|
|
20
|
+
|
|
21
|
+
color = self.LEVEL_COLORS.get(record_copy.levelno, "")
|
|
22
|
+
record_copy.levelname = (
|
|
23
|
+
f"{color}{record_copy.levelname}{Style.RESET_ALL}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return super().format(record_copy)
|
miu_logger/listener.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
from multiprocessing import Queue
|
|
6
|
+
from logging.handlers import RotatingFileHandler, QueueListener
|
|
7
|
+
|
|
8
|
+
from .formatters import ColoredFormatter
|
|
9
|
+
from .filters import LoggerNameFilter, ExactLevelFilter
|
|
10
|
+
from .config import LogConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_FORMATTER = logging.Formatter(
|
|
14
|
+
fmt="%(asctime)s - %(processName)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s",
|
|
15
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
FILE_MAX_BYTES = 10 * 1024 * 1024
|
|
19
|
+
FILE_BACKUP_COUNT = 5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _make_file_handler(path: str, level: int) -> logging.Handler:
|
|
23
|
+
handler = RotatingFileHandler(
|
|
24
|
+
path,
|
|
25
|
+
maxBytes=FILE_MAX_BYTES,
|
|
26
|
+
backupCount=FILE_BACKUP_COUNT,
|
|
27
|
+
encoding="utf-8",
|
|
28
|
+
)
|
|
29
|
+
handler.setLevel(level)
|
|
30
|
+
handler.setFormatter(DEFAULT_FORMATTER)
|
|
31
|
+
return handler
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _make_stream_handler(level: int) -> logging.Handler:
|
|
35
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
36
|
+
handler.setLevel(level)
|
|
37
|
+
handler.setFormatter(
|
|
38
|
+
ColoredFormatter(DEFAULT_FORMATTER._fmt, DEFAULT_FORMATTER.datefmt)
|
|
39
|
+
)
|
|
40
|
+
return handler
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SafeQueueListener(QueueListener):
|
|
44
|
+
def _monitor(self):
|
|
45
|
+
try:
|
|
46
|
+
super()._monitor()
|
|
47
|
+
except (EOFError, OSError):
|
|
48
|
+
return
|
|
49
|
+
except Exception:
|
|
50
|
+
traceback.print_exc()
|
|
51
|
+
raise
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def setup_main_listener(config: LogConfig):
|
|
55
|
+
os.makedirs(config.log_dir, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
queue = Queue(-1)
|
|
58
|
+
handlers: list[logging.Handler] = []
|
|
59
|
+
|
|
60
|
+
# ─────────────────────────────
|
|
61
|
+
# Domain log files
|
|
62
|
+
# ─────────────────────────────
|
|
63
|
+
for domain in config.domains:
|
|
64
|
+
filename = (
|
|
65
|
+
f"{config.file_prefix}.{domain}.log"
|
|
66
|
+
if config.file_prefix
|
|
67
|
+
else f"{domain}.log"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
path = os.path.join(config.log_dir, filename)
|
|
71
|
+
fh = _make_file_handler(path, logging.DEBUG)
|
|
72
|
+
fh.addFilter(LoggerNameFilter(domain))
|
|
73
|
+
handlers.append(fh)
|
|
74
|
+
|
|
75
|
+
# ─────────────────────────────
|
|
76
|
+
# Level log files
|
|
77
|
+
# ─────────────────────────────
|
|
78
|
+
for lvl in (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR):
|
|
79
|
+
level_name = logging.getLevelName(lvl).lower()
|
|
80
|
+
|
|
81
|
+
filename = (
|
|
82
|
+
f"{config.file_prefix}.{level_name}.log"
|
|
83
|
+
if config.file_prefix
|
|
84
|
+
else f"{level_name}.log"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
path = os.path.join(config.log_dir, filename)
|
|
88
|
+
fh = _make_file_handler(path, lvl)
|
|
89
|
+
fh.addFilter(ExactLevelFilter(lvl))
|
|
90
|
+
handlers.append(fh)
|
|
91
|
+
|
|
92
|
+
# ─────────────────────────────
|
|
93
|
+
# Console
|
|
94
|
+
# ─────────────────────────────
|
|
95
|
+
handlers.append(_make_stream_handler(logging.DEBUG))
|
|
96
|
+
|
|
97
|
+
listener = SafeQueueListener(queue, *handlers, respect_handler_level=True)
|
|
98
|
+
listener.start()
|
|
99
|
+
|
|
100
|
+
return queue, listener
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def clear_logs(config: LogConfig):
|
|
104
|
+
for file in os.listdir(config.log_dir):
|
|
105
|
+
if file.endswith(".log"):
|
|
106
|
+
open(os.path.join(config.log_dir, file), "w").close()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging.handlers import QueueHandler
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_logger(name: str, level: int, queue) -> logging.Logger:
|
|
6
|
+
logger = logging.getLogger(name)
|
|
7
|
+
logger.setLevel(level)
|
|
8
|
+
logger.handlers.clear()
|
|
9
|
+
logger.propagate = False
|
|
10
|
+
|
|
11
|
+
qh = QueueHandler(queue)
|
|
12
|
+
logger.addHandler(qh)
|
|
13
|
+
|
|
14
|
+
return logger
|
miu_logger/repository.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
from typing import Dict
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .config import LogConfig
|
|
6
|
+
from .listener import setup_main_listener
|
|
7
|
+
from .logger_factory import get_logger
|
|
8
|
+
from .conditional import ConditionalLogger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LoggingRepository:
|
|
12
|
+
def __init__(self, config: LogConfig, *, use_listener: bool = True, queue=None):
|
|
13
|
+
self._is_shutdown = False
|
|
14
|
+
self.config = config
|
|
15
|
+
self.listener = None
|
|
16
|
+
|
|
17
|
+
self._domains = set(config.domains)
|
|
18
|
+
self._loggers: Dict[str, ConditionalLogger] = {}
|
|
19
|
+
|
|
20
|
+
if use_listener:
|
|
21
|
+
self.queue, self.listener = setup_main_listener(config)
|
|
22
|
+
else:
|
|
23
|
+
if queue is None:
|
|
24
|
+
raise ValueError("queue required when use_listener=False")
|
|
25
|
+
self.queue = queue
|
|
26
|
+
|
|
27
|
+
atexit.register(self.shutdown)
|
|
28
|
+
|
|
29
|
+
# ---------- domain access ----------
|
|
30
|
+
|
|
31
|
+
def _create_domain_logger(self, domain: str) -> ConditionalLogger:
|
|
32
|
+
if domain in self._loggers:
|
|
33
|
+
return self._loggers[domain]
|
|
34
|
+
|
|
35
|
+
base = get_logger(domain, logging.DEBUG, self.queue)
|
|
36
|
+
wrapped = ConditionalLogger(base, lambda: self.config.debug_enabled)
|
|
37
|
+
|
|
38
|
+
self._loggers[domain] = wrapped
|
|
39
|
+
return wrapped
|
|
40
|
+
|
|
41
|
+
def __getattr__(self, item: str) -> ConditionalLogger:
|
|
42
|
+
if item in self._domains:
|
|
43
|
+
logger = self._create_domain_logger(item)
|
|
44
|
+
setattr(self, item, logger)
|
|
45
|
+
return logger
|
|
46
|
+
raise AttributeError(item)
|
|
47
|
+
|
|
48
|
+
def get(self, domain: str) -> ConditionalLogger:
|
|
49
|
+
if domain not in self._domains:
|
|
50
|
+
raise ValueError(f"Unknown log domain: {domain}")
|
|
51
|
+
return self._create_domain_logger(domain)
|
|
52
|
+
|
|
53
|
+
# ---------- infra ----------
|
|
54
|
+
|
|
55
|
+
def get_queue(self):
|
|
56
|
+
return self.queue
|
|
57
|
+
|
|
58
|
+
def shutdown(self):
|
|
59
|
+
if getattr(self, "_is_shutdown", False):
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
self._is_shutdown = True
|
|
63
|
+
|
|
64
|
+
if self.listener and getattr(self.listener, "_thread", None):
|
|
65
|
+
try:
|
|
66
|
+
self.listener.stop()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
# ---------- pickle support ----------
|
|
71
|
+
|
|
72
|
+
def __getstate__(self):
|
|
73
|
+
return {"config": self.config, "queue": self.queue}
|
|
74
|
+
|
|
75
|
+
def __setstate__(self, state):
|
|
76
|
+
self.__init__(state["config"], use_listener=False, queue=state["queue"])
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: miu-logger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multiprocessing-safe structured logging framework
|
|
5
|
+
Author: Bruno Miura
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: colorama
|
|
10
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
miu_logger/__init__.py,sha256=QdFaE_rX7MdtjGuBI6nTvBoDpgcl4W4iRWkxMDbtUJg,165
|
|
2
|
+
miu_logger/conditional.py,sha256=j0XnxF7diXCvovhJlRMf7gKGtP1UOK72J5WXVYe6L7M,566
|
|
3
|
+
miu_logger/config.py,sha256=JyBDoSHMA4-BZT2ZnB2-978IoIPsn495OU6QsxUjlyA,197
|
|
4
|
+
miu_logger/filters.py,sha256=CPPjDzlrB07Ku4QxsuYl87RetzppwJTcVml657O6y68,475
|
|
5
|
+
miu_logger/formatters.py,sha256=JshOwo4B0mT241ElCAs6nN-_IF_SgcZwmDt7k7tibuo,675
|
|
6
|
+
miu_logger/listener.py,sha256=ZBw8i-f-pouPdnuw-DXEKS4ZvWIHoUtiAPMzsIomj4g,3301
|
|
7
|
+
miu_logger/logger_factory.py,sha256=MhNaP73J9RbhmmVR6simqkZRCel2g6rPImRlijYAQXI,319
|
|
8
|
+
miu_logger/repository.py,sha256=XQPkJjuYzUKNiNUmPSxRd2CygdKThB_PHIyq0WTFkic,2288
|
|
9
|
+
miu_logger-0.1.0.dist-info/licenses/LICENSE,sha256=jLLem0QFpsFv7Lkgp7I7FeN4DIdgCKIxzklfdkkesWg,1075
|
|
10
|
+
miu_logger-0.1.0.dist-info/METADATA,sha256=QlWdb9LomoTwTDmsy3EG4OUrpMyNFeV11nVD0ArB9yE,265
|
|
11
|
+
miu_logger-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
+
miu_logger-0.1.0.dist-info/top_level.txt,sha256=70Nuj1YRYLMkqiRZv1pPZPhi_VystPGqm1nC3s-uHz4,11
|
|
13
|
+
miu_logger-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruno Miura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
miu_logger
|