-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp_logger.py
More file actions
163 lines (128 loc) · 4.92 KB
/
Copy pathapp_logger.py
File metadata and controls
163 lines (128 loc) · 4.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""
Application Logger — singleton logger pro celou aplikaci.
Poskytuje:
- FileHandler → data/app.log (rotující, max 1 MB, 3 zálohy)
- StreamHandler → konzole
- GUI dispatch handler → volá registrované callbacky pro GUI log panel
Použití:
from src.utils.app_logger import get_logger
logger = get_logger()
logger.info("Zpráva")
logger.warning("Varování")
logger.error("Chyba")
"""
import logging
import os
from logging.handlers import RotatingFileHandler
# --- Cesta k log souboru (stejný vzor jako database.py) ---
DATA_DIR = "data"
LOG_FILE = os.path.join(DATA_DIR, "app.log")
# Formát pro soubor a konzoli
_FILE_FORMAT = "%(asctime)s [%(levelname)-8s] %(message)s"
_DATE_FORMAT = "%d.%m.%Y %H:%M:%S"
# Formát pro GUI panel — pouze čas a zpráva, bez level textu
_GUI_FORMAT = "%(asctime)s %(message)s"
_GUI_DATE_FORMAT = "%H:%M:%S"
# --- Singleton instance ---
_logger: logging.Logger | None = None
# --- Registrované GUI callbacky ---
# Každý callback má signaturu: callback(message: str, level: str) -> None
_gui_callbacks: list = []
class _GUIDispatchHandler(logging.Handler):
"""
Logging handler pro GUI.
Při každém záznamu zavolá všechny registrované GUI callbacky
s naformátovanou zprávou a názvem úrovně (INFO, WARNING, ERROR...).
"""
def emit(self, record: logging.LogRecord) -> None:
"""Předá záznam všem registrovaným GUI callbackům."""
try:
message = self.format(record)
level = record.levelname
for callback in list(_gui_callbacks):
try:
callback(message, level)
except Exception:
# Callback selhal — tiše ignorujeme, aby se nezastavilo logování
pass
except Exception:
self.handleError(record)
def get_logger() -> logging.Logger:
"""
Vrátí singleton instanci aplikačního loggeru.
Při prvním zavolání inicializuje handlery (soubor, konzole, GUI).
Při dalších zavoláních vrátí již existující instanci.
Returns:
logging.Logger: Nakonfigurovaný logger "measuring_capacity"
"""
global _logger
if _logger is not None:
return _logger
_logger = logging.getLogger("measuring_capacity")
_logger.setLevel(logging.DEBUG)
# Zabrání duplicitnímu přidávání handlerů při opakované inicializaci
if _logger.handlers:
return _logger
formatter = logging.Formatter(_FILE_FORMAT, datefmt=_DATE_FORMAT)
# --- Konzolový handler ---
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(formatter)
_logger.addHandler(console_handler)
# --- Souborový handler (rotující) ---
os.makedirs(DATA_DIR, exist_ok=True)
file_handler = RotatingFileHandler(
LOG_FILE,
maxBytes=1_000_000, # 1 MB
backupCount=3,
encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
_logger.addHandler(file_handler)
# --- GUI dispatch handler ---
gui_handler = _GUIDispatchHandler()
gui_handler.setLevel(logging.INFO) # Do GUI posíláme jen INFO a výše
gui_handler.setFormatter(
logging.Formatter(_GUI_FORMAT, datefmt=_GUI_DATE_FORMAT)
)
_logger.addHandler(gui_handler)
return _logger
def add_gui_handler(callback) -> None:
"""
Zaregistruje callback pro příjem log zpráv v GUI.
Args:
callback: Funkce se signaturou callback(message: str, level: str)
"""
if callback not in _gui_callbacks:
_gui_callbacks.append(callback)
def remove_gui_handler(callback) -> None:
"""
Odregistruje dříve zaregistrovaný GUI callback.
Args:
callback: Dříve zaregistrovaná funkce
"""
if callback in _gui_callbacks:
_gui_callbacks.remove(callback)
def make_ctk_error_handler():
"""
Vrátí error handler vhodný pro přiřazení do Tkinter root.report_callback_exception.
Potlačí známé neškodné 'invalid command name' chyby pocházející z interních
after() callbacků CustomTkinteru (DPI scaling, animace tlačítek, update smyčky).
Tyto vznikají při zavření dialogů dříve než CTk stihne zrušit naplánované
callbacky. Všechny ostatní výjimky jsou zalogovány jako ERROR.
Returns:
Callable vhodný pro root.report_callback_exception
"""
_logger = get_logger()
_CTK_NOISE = ("check_dpi_scaling", "_click_animation", "update")
def _handler(exc, val, tb):
msg = str(val)
if exc is RuntimeError and "invalid command name" in msg:
if any(pattern in msg for pattern in _CTK_NOISE):
return # interní CTk šum — tiše ignorovat
_logger.error(
f"Neočekávaná chyba v Tkinter callbacku: {val}",
exc_info=(exc, val, tb),
)
return _handler