auto-trainer-core 2.0.1.dev349__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.
- auto_trainer_core-2.0.1.dev349.dist-info/METADATA +19 -0
- auto_trainer_core-2.0.1.dev349.dist-info/RECORD +73 -0
- auto_trainer_core-2.0.1.dev349.dist-info/WHEEL +5 -0
- auto_trainer_core-2.0.1.dev349.dist-info/top_level.txt +1 -0
- autotrainer/core/__init__.py +319 -0
- autotrainer/core/analysis/__init__.py +6 -0
- autotrainer/core/analysis/alarm_monitor.py +484 -0
- autotrainer/core/analysis/audio_spectrum_data.py +27 -0
- autotrainer/core/analysis/audio_spectrum_monitor.py +134 -0
- autotrainer/core/analysis/auto_tunnel_fan_monitor.py +92 -0
- autotrainer/core/analysis/detector.py +179 -0
- autotrainer/core/analysis/external_doors_monitor.py +124 -0
- autotrainer/core/analysis/global_animal_presence_monitor.py +69 -0
- autotrainer/core/analysis/head_fix_measurement.py +15 -0
- autotrainer/core/analysis/headbar_pressure_monitor.py +158 -0
- autotrainer/core/analysis/load_cell_monitor.py +377 -0
- autotrainer/core/analysis/load_cell_tare_monitor.py +217 -0
- autotrainer/core/analysis/measurement_data.py +80 -0
- autotrainer/core/analysis/pellet_position_monitor.py +108 -0
- autotrainer/core/analysis/sensor_analysis.py +390 -0
- autotrainer/core/analysis/system_fault_monitor.py +81 -0
- autotrainer/core/analysis/system_maintenance_monitor.py +88 -0
- autotrainer/core/animal/__init__.py +1 -0
- autotrainer/core/animal/animal_subject.py +208 -0
- autotrainer/core/capture.py +25 -0
- autotrainer/core/configuration/__init__.py +88 -0
- autotrainer/core/configuration/alarm_configuration.py +55 -0
- autotrainer/core/configuration/animal_presence_configuration.py +17 -0
- autotrainer/core/configuration/behavior_configuration.py +323 -0
- autotrainer/core/configuration/camera_configuration.py +108 -0
- autotrainer/core/configuration/external_doors_monitor_configuration.py +9 -0
- autotrainer/core/configuration/hardware_configuration.py +30 -0
- autotrainer/core/configuration/inference_configuration.py +26 -0
- autotrainer/core/configuration/load_cell_config.py +61 -0
- autotrainer/core/configuration/persistence_configuration.py +29 -0
- autotrainer/core/configuration/presence_detection_configuration.py +9 -0
- autotrainer/core/configuration/system_configuration.py +211 -0
- autotrainer/core/configuration/system_fault_config.py +9 -0
- autotrainer/core/configuration/system_maintenance_config.py +17 -0
- autotrainer/core/diamond_triangle_config.py +150 -0
- autotrainer/core/event/__init__.py +54 -0
- autotrainer/core/event/api_event_kind.py +5 -0
- autotrainer/core/event/api_event_plugin.py +57 -0
- autotrainer/core/event/event_info.py +53 -0
- autotrainer/core/event/event_manager.py +296 -0
- autotrainer/core/event/event_manager_plugin.py +45 -0
- autotrainer/core/event/file_event_plugin.py +91 -0
- autotrainer/core/event/logger_event_plugin.py +43 -0
- autotrainer/core/fixed_array_multiqueue.py +263 -0
- autotrainer/core/fixed_array_queue.py +124 -0
- autotrainer/core/frame_index.py +34 -0
- autotrainer/core/interfaces.py +347 -0
- autotrainer/core/logging.py +714 -0
- autotrainer/core/message/__init__.py +26 -0
- autotrainer/core/message/audio_spectrum_message.py +21 -0
- autotrainer/core/message/measurement_message.py +61 -0
- autotrainer/core/message/message_handler.py +158 -0
- autotrainer/core/message/motor_configuration_message.py +128 -0
- autotrainer/core/message/system_command_kind.py +74 -0
- autotrainer/core/message/system_message_handler.py +122 -0
- autotrainer/core/message/system_status_message.py +112 -0
- autotrainer/core/multiproc.py +50 -0
- autotrainer/core/notification/__init__.py +2 -0
- autotrainer/core/notification/notification_center.py +80 -0
- autotrainer/core/notification/trigger_notification.py +27 -0
- autotrainer/core/observable_object.py +105 -0
- autotrainer/core/perf_monitor.py +77 -0
- autotrainer/core/pose_elements.py +196 -0
- autotrainer/core/project/__init__.py +19 -0
- autotrainer/core/project/project_info.py +486 -0
- autotrainer/core/queue_util.py +60 -0
- autotrainer/core/reach_event.py +38 -0
- autotrainer/core/video_detection.py +326 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import dataclasses
|
|
3
|
+
import functools
|
|
4
|
+
import itertools
|
|
5
|
+
import logging
|
|
6
|
+
import logging.handlers
|
|
7
|
+
import multiprocessing
|
|
8
|
+
import operator
|
|
9
|
+
import signal
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from logging import LogRecord
|
|
14
|
+
from queue import Empty
|
|
15
|
+
from multiprocessing import Process
|
|
16
|
+
from typing import Optional, Dict, Union, List
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
import verboselogs
|
|
20
|
+
import coloredlogs
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
from autotrainer.core.multiproc import get_mp_ctx
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
_LogLevelT = Union[str, int]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_already_setup = False
|
|
31
|
+
_base_logger: logging.Logger = logging.root
|
|
32
|
+
_multiprocess_log_queue: Optional[multiprocessing.Queue] = None
|
|
33
|
+
_queue_listener: Optional[logging.handlers.QueueListener] = None
|
|
34
|
+
_queue_handler: Optional[logging.Handler] = None
|
|
35
|
+
_console_handler: Optional[logging.StreamHandler] = None
|
|
36
|
+
_root_handler = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s"
|
|
40
|
+
MULTIPROC_LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s[%(processName)s.%(process)d-%(threadName)s.%(thread_id)s] %(message)s"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# these loggers can be too verbose:
|
|
44
|
+
_limit_loggers_level = {
|
|
45
|
+
'botocore': {
|
|
46
|
+
'level': 'INFO'
|
|
47
|
+
},
|
|
48
|
+
'boto3': {
|
|
49
|
+
'level': 'INFO'
|
|
50
|
+
},
|
|
51
|
+
'urllib3': {
|
|
52
|
+
'level': 'INFO'
|
|
53
|
+
},
|
|
54
|
+
'py4j': {
|
|
55
|
+
'level': 'INFO'
|
|
56
|
+
},
|
|
57
|
+
'h5py': {
|
|
58
|
+
'level': 'INFO'
|
|
59
|
+
},
|
|
60
|
+
'watchdog': {
|
|
61
|
+
'level': 'INFO'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DateTimeFormats:
|
|
67
|
+
# could be an enum eventually
|
|
68
|
+
hour_time_precise = "%H:%M:%S.%f"
|
|
69
|
+
month_day_time_precise = f"%m/%d {hour_time_precise}"
|
|
70
|
+
year_precise = f"%Y/%m/%d {hour_time_precise}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
DEFAULT_FIELD_STYLES = dict(
|
|
74
|
+
asctime=dict(color='white', bold=False),
|
|
75
|
+
hostname=dict(color='magenta'),
|
|
76
|
+
levelname=dict(color='blue', bold=True),
|
|
77
|
+
name=dict(color='cyan', bold=False),
|
|
78
|
+
programname=dict(color='cyan'),
|
|
79
|
+
username=dict(color='yellow'),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
DEFAULT_LEVEL_STYLES = dict(
|
|
84
|
+
spam=dict(color='white', faint=True),
|
|
85
|
+
debug=dict(color='white', bold=False, faint=False),
|
|
86
|
+
verbose=dict(color='white', bold=True),
|
|
87
|
+
info=dict(color='blue', bold=False, faint=True),
|
|
88
|
+
notice=dict(color='magenta', faint=True),
|
|
89
|
+
warning=dict(color='yellow'),
|
|
90
|
+
success=dict(color='green', bold=False),
|
|
91
|
+
error=dict(color='red', bold=False, faint=True),
|
|
92
|
+
critical=dict(color='red', bold=True),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclasses.dataclass
|
|
97
|
+
class LogConfig:
|
|
98
|
+
base_logger_name: Optional[str] = None # i.e: "root" logger if None
|
|
99
|
+
logger_level: _LogLevelT = logging.NOTSET
|
|
100
|
+
root_level: _LogLevelT = logging.NOTSET
|
|
101
|
+
log_format: str = MULTIPROC_LOG_FORMAT
|
|
102
|
+
date_format: str = DateTimeFormats.hour_time_precise
|
|
103
|
+
time_precision: int = 3, # for sub seconds precision, nbr of digits after the dot.
|
|
104
|
+
level_styles: Dict[str, Dict[str, str]] = dataclasses.field(default_factory=lambda: copy.deepcopy(DEFAULT_LEVEL_STYLES))
|
|
105
|
+
field_styles: Dict[str, Dict[str, str]] = dataclasses.field(default_factory=lambda: copy.deepcopy(DEFAULT_FIELD_STYLES))
|
|
106
|
+
stream: str = "sys.stdout"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def make_console_handler(cfg: LogConfig):
|
|
110
|
+
stream = sys.stdout
|
|
111
|
+
console_handler = logging.StreamHandler(stream=stream)
|
|
112
|
+
console_handler.name = "console_handler"
|
|
113
|
+
console_handler.addFilter(thread_id_filter)
|
|
114
|
+
fmt = ColoredPreciseTimeFormatter(
|
|
115
|
+
cfg.log_format,
|
|
116
|
+
level_styles=cfg.level_styles,
|
|
117
|
+
field_styles=cfg.field_styles,
|
|
118
|
+
datefmt=cfg.date_format,
|
|
119
|
+
time_precision=cfg.time_precision,
|
|
120
|
+
)
|
|
121
|
+
console_handler.setFormatter(fmt)
|
|
122
|
+
return console_handler
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def listener_command(func):
|
|
126
|
+
"""Relay the given func to the log queue listener proc side"""
|
|
127
|
+
@functools.wraps(func)
|
|
128
|
+
def wrapped(self: "LogQueueListenerProc", *args, **kwargs):
|
|
129
|
+
self._send_command(func.__name__, (args, kwargs))
|
|
130
|
+
return wrapped
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class LogQueueListenerProc(Process):
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
log_queue,
|
|
138
|
+
log_config: LogConfig,
|
|
139
|
+
):
|
|
140
|
+
super().__init__(daemon=True)
|
|
141
|
+
self._queue = log_queue
|
|
142
|
+
mp_ctx = get_mp_ctx()
|
|
143
|
+
self._command_queue = mp_ctx.Queue()
|
|
144
|
+
self._command_executed = mp_ctx.Event()
|
|
145
|
+
self._log_config = log_config
|
|
146
|
+
self._listener: Optional[WithThreadIdQueueListener] = None
|
|
147
|
+
self._console_handler = None
|
|
148
|
+
self._file_handler: Optional[logging.FileHandler] = None
|
|
149
|
+
|
|
150
|
+
def _send_command(self, cmd, data):
|
|
151
|
+
self._command_executed.clear()
|
|
152
|
+
self._command_queue.put((cmd, data))
|
|
153
|
+
if not self._command_executed.wait(5):
|
|
154
|
+
logger.warning("command %s timeout after 5s, skipping waiting")
|
|
155
|
+
|
|
156
|
+
@listener_command
|
|
157
|
+
def set_handler_level(self, name, level):
|
|
158
|
+
"""Set handler level"""
|
|
159
|
+
|
|
160
|
+
def _set_handler_level(self, name, level):
|
|
161
|
+
if name == "console_handler":
|
|
162
|
+
self._console_handler.setLevel(level)
|
|
163
|
+
else:
|
|
164
|
+
logger.verbose("unhandled handler name: %r", name)
|
|
165
|
+
|
|
166
|
+
@listener_command
|
|
167
|
+
def set_logger_level(self, name, level):
|
|
168
|
+
"""Set logger level"""
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _set_logger_level(name, level):
|
|
172
|
+
logging.getLogger(name).setLevel(level)
|
|
173
|
+
logger.debug("set_logger_level(%s, %s)", name, level)
|
|
174
|
+
|
|
175
|
+
@listener_command
|
|
176
|
+
def add_file_handler(self, path):
|
|
177
|
+
"""Add file handler to path"""
|
|
178
|
+
|
|
179
|
+
def _add_file_handler(self, path: Path):
|
|
180
|
+
logger.info("Adding file handler to %s", path)
|
|
181
|
+
file_handler = logging.FileHandler(path)
|
|
182
|
+
file_handler.addFilter(thread_id_filter)
|
|
183
|
+
file_handler.setFormatter(
|
|
184
|
+
PreciseTimeFormatter(
|
|
185
|
+
MULTIPROC_LOG_FORMAT,
|
|
186
|
+
datefmt=DateTimeFormats.year_precise,
|
|
187
|
+
time_precision=6,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
file_handler.setLevel(verboselogs.SPAM + 1) # writes everything up to DEBUG which reaches it
|
|
191
|
+
self._listener.handlers += (file_handler,)
|
|
192
|
+
self._file_handler = file_handler
|
|
193
|
+
logger.info("logging.root.handlers=%s ; listener_handlers=%s",
|
|
194
|
+
logging.root.handlers, self._listener.handlers)
|
|
195
|
+
return file_handler
|
|
196
|
+
|
|
197
|
+
@listener_command
|
|
198
|
+
def switch_file_handler(self, path: Path):
|
|
199
|
+
"""Switch current, or set new, file handler to path"""
|
|
200
|
+
|
|
201
|
+
def _switch_file_handler(self, path: Path):
|
|
202
|
+
logger.info("Switching file handler to %s", path)
|
|
203
|
+
prev = self._file_handler
|
|
204
|
+
self._add_file_handler(path)
|
|
205
|
+
if prev is not None:
|
|
206
|
+
logger.verbose("removing previous handler %s", prev)
|
|
207
|
+
self._listener.handlers = tuple(
|
|
208
|
+
handler for handler in self._listener.handlers
|
|
209
|
+
if handler != prev
|
|
210
|
+
)
|
|
211
|
+
logger.debug("new handlers: %s", self._listener.handlers)
|
|
212
|
+
prev.close()
|
|
213
|
+
|
|
214
|
+
#
|
|
215
|
+
|
|
216
|
+
def stop(self):
|
|
217
|
+
self._command_queue.put(None)
|
|
218
|
+
# os.kill(self.pid, signal.SIGINT)
|
|
219
|
+
self.join()
|
|
220
|
+
|
|
221
|
+
def run(self):
|
|
222
|
+
# print(f"{logging.root.handlers}")
|
|
223
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
224
|
+
|
|
225
|
+
cfg = self._log_config
|
|
226
|
+
#
|
|
227
|
+
console_handler = self._console_handler = make_console_handler(cfg)
|
|
228
|
+
|
|
229
|
+
# NB: start the listener as soon as possible
|
|
230
|
+
listener = self._listener = WithThreadIdQueueListener(
|
|
231
|
+
self._queue,
|
|
232
|
+
console_handler,
|
|
233
|
+
respect_handler_level=True, # False, # True,
|
|
234
|
+
)
|
|
235
|
+
listener.start()
|
|
236
|
+
|
|
237
|
+
base_logger = get_verbose_logger(cfg.base_logger_name)
|
|
238
|
+
base_logger.addHandler(console_handler)
|
|
239
|
+
|
|
240
|
+
base_logger.setLevel(logging.INFO)
|
|
241
|
+
base_logger.info("Starting log queue listener. config=%s", self._log_config)
|
|
242
|
+
base_logger.setLevel(cfg.root_level)
|
|
243
|
+
|
|
244
|
+
# NB: must be installed AFTER the listener is started
|
|
245
|
+
install_log_exception_hook()
|
|
246
|
+
|
|
247
|
+
command_q = self._command_queue
|
|
248
|
+
command_executed = self._command_executed.set
|
|
249
|
+
while True:
|
|
250
|
+
try:
|
|
251
|
+
data = command_q.get()
|
|
252
|
+
except Empty:
|
|
253
|
+
continue
|
|
254
|
+
if data is None:
|
|
255
|
+
break
|
|
256
|
+
cmd = data[0]
|
|
257
|
+
args, kwargs = data[1]
|
|
258
|
+
meth = getattr(self, f"_{cmd}", None)
|
|
259
|
+
if meth is None:
|
|
260
|
+
logger.warning("unknown command: %sr", cmd)
|
|
261
|
+
command_executed()
|
|
262
|
+
continue
|
|
263
|
+
try:
|
|
264
|
+
meth(*args, **kwargs)
|
|
265
|
+
except Exception as err:
|
|
266
|
+
logger.error("Failed executing cmd %r: %s", cmd, err)
|
|
267
|
+
finally:
|
|
268
|
+
command_executed()
|
|
269
|
+
# end while True
|
|
270
|
+
listener.stop()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def main_exception_hook(exc_type, exc_value, exc_traceback):
|
|
274
|
+
logger.exception("Fatal unhandled main-thread exception: %s",
|
|
275
|
+
exc_value, exc_info=(exc_type, exc_value, exc_traceback))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def thread_exception_hook(arg):
|
|
279
|
+
logger.exception("Fatal unhandled thread exception: %s", arg.exc_value)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def install_log_exception_hook():
|
|
283
|
+
sys.excepthook = main_exception_hook
|
|
284
|
+
threading.excepthook = thread_exception_hook
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_root_handler():
|
|
288
|
+
return _root_handler
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_console_handler() -> logging.Handler:
|
|
292
|
+
return _console_handler
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def get_multiprocess_log_queue() -> Optional[multiprocessing.Queue]:
|
|
296
|
+
return _multiprocess_log_queue
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_log_queue_listener() -> Optional[LogQueueListenerProc]:
|
|
300
|
+
return _queue_listener
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def get_log_queue_handler():
|
|
304
|
+
return _queue_handler
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ThreadIdFilter(logging.Filter):
|
|
308
|
+
|
|
309
|
+
def filter(self, record):
|
|
310
|
+
"""Inject thread_id to log records"""
|
|
311
|
+
if not hasattr(record, "thread_id"):
|
|
312
|
+
record.thread_id = threading.get_native_id()
|
|
313
|
+
return True
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
thread_id_filter = ThreadIdFilter()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class PreciseTimeFormatter(logging.Formatter):
|
|
320
|
+
"""A logger formatter with time precision handling"""
|
|
321
|
+
|
|
322
|
+
converter = datetime.fromtimestamp
|
|
323
|
+
|
|
324
|
+
def __init__(self, *args, time_precision: int = 3, **kwargs):
|
|
325
|
+
self._time_precision = time_precision
|
|
326
|
+
super().__init__(*args, **kwargs)
|
|
327
|
+
|
|
328
|
+
def formatTime(self, record, datefmt=None):
|
|
329
|
+
ct = self.converter(record.created)
|
|
330
|
+
if not datefmt:
|
|
331
|
+
datefmt = DateTimeFormats.year_precise # "%Y-%m-%d %H:%M:%S.%f"
|
|
332
|
+
if self._time_precision > 0 and "%f" in datefmt:
|
|
333
|
+
msec_len = len(str(int(record.msecs)))
|
|
334
|
+
v = str(record.msecs).replace(".", "")
|
|
335
|
+
v0 = "0" * (3 - msec_len)
|
|
336
|
+
v = (v0 + v)[:self._time_precision]
|
|
337
|
+
else:
|
|
338
|
+
v = ""
|
|
339
|
+
with_dot = ".%f" in datefmt
|
|
340
|
+
rep = f".%f" if with_dot and self._time_precision == 0 else "%f"
|
|
341
|
+
datefmt = datefmt.replace(rep, v)
|
|
342
|
+
s = ct.strftime(datefmt)
|
|
343
|
+
return s
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class ColoredPreciseTimeFormatter(PreciseTimeFormatter, coloredlogs.ColoredFormatter):
|
|
347
|
+
"""A colored logger formatter with time precision handling"""
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def stop_multiproc_logging():
|
|
351
|
+
global _multiprocess_log_queue, _queue_listener, _queue_handler, _console_handler, _already_setup
|
|
352
|
+
|
|
353
|
+
mp_log_queue = _multiprocess_log_queue
|
|
354
|
+
q_listener = _queue_listener
|
|
355
|
+
q_handler = _queue_handler
|
|
356
|
+
|
|
357
|
+
if _already_setup and q_listener is not None:
|
|
358
|
+
_already_setup = False
|
|
359
|
+
# Must remove the handler before closing it:
|
|
360
|
+
for handler in _base_logger.handlers:
|
|
361
|
+
if isinstance(handler, WithThreadIdQueueHandler):
|
|
362
|
+
_base_logger.removeHandler(handler)
|
|
363
|
+
|
|
364
|
+
if q_listener is not None:
|
|
365
|
+
# must be before following log queue close()
|
|
366
|
+
q_listener.stop()
|
|
367
|
+
_queue_listener = None
|
|
368
|
+
|
|
369
|
+
if mp_log_queue is not None:
|
|
370
|
+
mp_log_queue.close()
|
|
371
|
+
mp_log_queue.join_thread()
|
|
372
|
+
_multiprocess_log_queue = None
|
|
373
|
+
|
|
374
|
+
if q_handler is not None:
|
|
375
|
+
q_handler.close()
|
|
376
|
+
_queue_handler = None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def repr_all_loggers():
|
|
380
|
+
vals = []
|
|
381
|
+
for k, v in itertools.chain(dict(root=logging.root).items(), logging.Logger.manager.loggerDict.items()):
|
|
382
|
+
if not isinstance(v, logging.PlaceHolder):
|
|
383
|
+
vals.append(
|
|
384
|
+
f"+ [{k.ljust(20)}] {v.__class__.__name__} lvl={v.level} prop={v.propagate} disabled={v.disabled}")
|
|
385
|
+
for h in v.handlers:
|
|
386
|
+
if not isinstance(v, logging.PlaceHolder):
|
|
387
|
+
vals.append(f" +++ {h.name}[{h.__class__.__name__}] lvl={h.level}")
|
|
388
|
+
return "\n".join(vals)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def repr_logger(obj: logging.Logger):
|
|
392
|
+
return (f"{obj.__class__.__name__}({obj.name} lvl={obj.level} prop={obj.propagate} disabled={obj.disabled} "
|
|
393
|
+
f"handlers={obj.handlers})")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def repr_handler(obj: logging.Handler):
|
|
397
|
+
return f"{obj.__class__.__name__}({obj.name} lvl={obj.level})"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class WithThreadIdQueueListener(logging.handlers.QueueListener):
|
|
401
|
+
|
|
402
|
+
def __init__(self, *args, **kwargs):
|
|
403
|
+
super().__init__(*args, **kwargs)
|
|
404
|
+
self._2_sorter = []
|
|
405
|
+
self._q_2_sorter_lock = threading.Lock()
|
|
406
|
+
self._delay_buffer = 0.25 # aggregate records up to that age, and only output when older than that, sorted
|
|
407
|
+
self._recheck_delay = 0.1 # delay between re-aggregate/sorts
|
|
408
|
+
self._sorter_thread = threading.Thread(target=self._sorter, daemon=True)
|
|
409
|
+
self._sorter_thread.start()
|
|
410
|
+
|
|
411
|
+
def _get_2_sorter(self):
|
|
412
|
+
with self._q_2_sorter_lock:
|
|
413
|
+
buff = self._2_sorter
|
|
414
|
+
self._2_sorter = []
|
|
415
|
+
return buff
|
|
416
|
+
|
|
417
|
+
def _sorter(self):
|
|
418
|
+
buffer: List[LogRecord] = []
|
|
419
|
+
recheck_delay = self._recheck_delay
|
|
420
|
+
p_next_sort = time.perf_counter() + recheck_delay
|
|
421
|
+
get_created = operator.attrgetter("created")
|
|
422
|
+
while True:
|
|
423
|
+
new_recs = self._get_2_sorter()
|
|
424
|
+
want_quit = False
|
|
425
|
+
while any(r is None for r in new_recs):
|
|
426
|
+
want_quit = True
|
|
427
|
+
new_recs.remove(None)
|
|
428
|
+
buffer.extend(new_recs)
|
|
429
|
+
p_now = time.perf_counter()
|
|
430
|
+
if p_now < p_next_sort and not want_quit:
|
|
431
|
+
if len(new_recs) == 0:
|
|
432
|
+
time.sleep(0.005)
|
|
433
|
+
else:
|
|
434
|
+
p_next_sort = p_now + recheck_delay
|
|
435
|
+
buffer = sorted(buffer, key=get_created)
|
|
436
|
+
t_now = time.time()
|
|
437
|
+
idx = 0
|
|
438
|
+
for record in buffer:
|
|
439
|
+
if t_now - record.created < self._delay_buffer and not want_quit:
|
|
440
|
+
break
|
|
441
|
+
self._handle(record)
|
|
442
|
+
idx += 1
|
|
443
|
+
del buffer[:idx]
|
|
444
|
+
#
|
|
445
|
+
if want_quit:
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
def _handle(self, record):
|
|
449
|
+
for handler in self.handlers:
|
|
450
|
+
if not self.respect_handler_level:
|
|
451
|
+
process = True
|
|
452
|
+
else:
|
|
453
|
+
process = record.levelno >= handler.level
|
|
454
|
+
if process:
|
|
455
|
+
handler.handle(record)
|
|
456
|
+
|
|
457
|
+
def handle(self, record):
|
|
458
|
+
record = self.prepare(record)
|
|
459
|
+
with self._q_2_sorter_lock:
|
|
460
|
+
self._2_sorter.append(record)
|
|
461
|
+
|
|
462
|
+
def prepare(self, record):
|
|
463
|
+
thread_id_filter.filter(record)
|
|
464
|
+
return record
|
|
465
|
+
|
|
466
|
+
def stop(self):
|
|
467
|
+
super().stop()
|
|
468
|
+
with self._q_2_sorter_lock:
|
|
469
|
+
self._2_sorter.append(None)
|
|
470
|
+
self._sorter_thread.join(5)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class WithThreadIdQueueHandler(logging.handlers.QueueHandler):
|
|
474
|
+
|
|
475
|
+
def prepare(self, record):
|
|
476
|
+
record = super().prepare(record)
|
|
477
|
+
thread_id_filter.filter(record)
|
|
478
|
+
return record
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class VerboseLoggerWithThreadId(verboselogs.VerboseLogger):
|
|
482
|
+
def filter(self, record):
|
|
483
|
+
return thread_id_filter.filter(record)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class RelayHandler(logging.Handler):
|
|
487
|
+
|
|
488
|
+
def __init__(self, listener: LogQueueListenerProc):
|
|
489
|
+
super().__init__()
|
|
490
|
+
self.listener = listener
|
|
491
|
+
|
|
492
|
+
def setLevel(self, level):
|
|
493
|
+
self.listener.set_handler_level(self.name, level)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class LoggerWriter:
|
|
497
|
+
def __init__(self, lvl):
|
|
498
|
+
self._lvl = lvl
|
|
499
|
+
|
|
500
|
+
def write(self, msg):
|
|
501
|
+
logging.root.log(self._lvl, msg)
|
|
502
|
+
|
|
503
|
+
def flush(self):
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def setup_logging(
|
|
508
|
+
name: str = "autotrainer",
|
|
509
|
+
*,
|
|
510
|
+
base_logger_name: Optional[str] = None, # i.e: "root" logger if None
|
|
511
|
+
logger_level: _LogLevelT = logging.NOTSET,
|
|
512
|
+
root_level: _LogLevelT = logging.NOTSET,
|
|
513
|
+
log_format: str = MULTIPROC_LOG_FORMAT,
|
|
514
|
+
date_format: str = DateTimeFormats.hour_time_precise,
|
|
515
|
+
time_precision: int = 3, # for sub seconds precision, nbr of digits after the dot.
|
|
516
|
+
level_styles: Optional[Dict[str, Dict[str, str]]] = None,
|
|
517
|
+
field_styles: Optional[Dict[str, Dict[str, str]]] = None,
|
|
518
|
+
multiprocess_enabled: bool = False,
|
|
519
|
+
fork_method: str = "spawn",
|
|
520
|
+
) -> verboselogs.VerboseLogger:
|
|
521
|
+
global _already_setup
|
|
522
|
+
global _multiprocess_log_queue, _queue_listener, _queue_handler, _console_handler, _root_handler
|
|
523
|
+
|
|
524
|
+
if _already_setup:
|
|
525
|
+
return get_verbose_logger(name)
|
|
526
|
+
|
|
527
|
+
# actually we do not require this:
|
|
528
|
+
# verboselogs.install() # thx to get_verbose_logger function above.
|
|
529
|
+
|
|
530
|
+
if level_styles is None:
|
|
531
|
+
level_styles = DEFAULT_LEVEL_STYLES
|
|
532
|
+
if field_styles is None:
|
|
533
|
+
field_styles = DEFAULT_FIELD_STYLES
|
|
534
|
+
#
|
|
535
|
+
cfg = LogConfig(
|
|
536
|
+
base_logger_name=base_logger_name,
|
|
537
|
+
logger_level=logger_level,
|
|
538
|
+
root_level=root_level,
|
|
539
|
+
log_format=log_format,
|
|
540
|
+
date_format=date_format,
|
|
541
|
+
time_precision=time_precision,
|
|
542
|
+
level_styles=level_styles,
|
|
543
|
+
field_styles=field_styles,
|
|
544
|
+
# stream: TextIO = sys.stdout,
|
|
545
|
+
)
|
|
546
|
+
#
|
|
547
|
+
stop_multiproc_logging()
|
|
548
|
+
#
|
|
549
|
+
# pre-set these too verbose loggers level:
|
|
550
|
+
for _limit_name, v in _limit_loggers_level.items():
|
|
551
|
+
logging.getLogger(_limit_name).setLevel(v["level"])
|
|
552
|
+
#
|
|
553
|
+
base_logger = get_verbose_logger(base_logger_name)
|
|
554
|
+
# set the base logger level before creating possible dedicated subproc log handling:
|
|
555
|
+
base_logger.setLevel(root_level)
|
|
556
|
+
#
|
|
557
|
+
if multiprocess_enabled:
|
|
558
|
+
# using queue created using the desired fork method context:
|
|
559
|
+
multiproc_ctx = multiprocessing.get_context(fork_method)
|
|
560
|
+
_multiprocess_log_queue = log_queue = multiproc_ctx.Queue()
|
|
561
|
+
|
|
562
|
+
listener = LogQueueListenerProc(log_queue, cfg)
|
|
563
|
+
listener.start()
|
|
564
|
+
_queue_listener = listener # keep global ref to ensure it stays alive
|
|
565
|
+
queue_handler = WithThreadIdQueueHandler(log_queue)
|
|
566
|
+
queue_handler.name = "queue_handler"
|
|
567
|
+
_queue_handler = queue_handler # keep global ref to ensure it stays alive
|
|
568
|
+
root_handler = _root_handler = queue_handler
|
|
569
|
+
_console_handler = RelayHandler(listener)
|
|
570
|
+
_console_handler.name = "console_handler" # "fake" it so that it will relay to the correct handler
|
|
571
|
+
logging.Logger.setLevel = lambda self, lvl: listener.set_logger_level(self.name, lvl)
|
|
572
|
+
else:
|
|
573
|
+
_console_handler = console_handler = make_console_handler(cfg)
|
|
574
|
+
root_handler = _root_handler = console_handler
|
|
575
|
+
|
|
576
|
+
base_logger.addHandler(root_handler)
|
|
577
|
+
|
|
578
|
+
#
|
|
579
|
+
# recursion issue atm, could be left todo:
|
|
580
|
+
# sys.stdout = LoggerWriter(logging.root.info)
|
|
581
|
+
# sys.stderr = LoggerWriter(logging.root.warning)
|
|
582
|
+
|
|
583
|
+
# logger.debug("Setup logging ; base_logger=%s", repr_logger(base_logger))
|
|
584
|
+
|
|
585
|
+
get_verbose_logger("transitions").setLevel(logger_level)
|
|
586
|
+
get_verbose_logger("tools").setLevel(logger_level)
|
|
587
|
+
get_verbose_logger("autotrainer").setLevel(logger_level)
|
|
588
|
+
get_verbose_logger("inference_algorithms").setLevel(logger_level)
|
|
589
|
+
|
|
590
|
+
for _limit_name, v in _limit_loggers_level.items():
|
|
591
|
+
logging.getLogger(_limit_name).setLevel(v["level"])
|
|
592
|
+
|
|
593
|
+
desired_logger = get_verbose_logger(name)
|
|
594
|
+
desired_logger.setLevel(logger_level)
|
|
595
|
+
|
|
596
|
+
# logger.info("all loggers:\n%s", repr_all_loggers())
|
|
597
|
+
# logger.info("top logger: %s", repr_logger(top_logger))
|
|
598
|
+
# logger.info("console: level=%s", c_h.level)
|
|
599
|
+
# logger.info("queue: level=%s", q_h.level)
|
|
600
|
+
# logger.info("autotrainer: level=%s prop=%s handlers=%s",
|
|
601
|
+
# top_logger.level, top_logger.propagate, top_logger.handlers)
|
|
602
|
+
|
|
603
|
+
_already_setup = True
|
|
604
|
+
|
|
605
|
+
install_log_exception_hook()
|
|
606
|
+
|
|
607
|
+
return desired_logger
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def set_logger_level(context: Dict[str, Union[str, int]]):
|
|
611
|
+
for name, value in context.items():
|
|
612
|
+
logging.getLogger(name).setLevel(value)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def make_log_dict_config(
|
|
616
|
+
*,
|
|
617
|
+
root_log_level: int = logging.NOTSET,
|
|
618
|
+
log_queue: Optional[multiprocessing.Queue] = None,
|
|
619
|
+
) -> Optional[Dict]:
|
|
620
|
+
# usable by logging.config.dictConfig
|
|
621
|
+
if log_queue is None:
|
|
622
|
+
log_queue = get_multiprocess_log_queue()
|
|
623
|
+
if log_queue is None:
|
|
624
|
+
return None
|
|
625
|
+
dct_cfg = {
|
|
626
|
+
'version': 1,
|
|
627
|
+
'disable_existing_loggers': False,
|
|
628
|
+
'handlers': {
|
|
629
|
+
'queue': {
|
|
630
|
+
'class': 'autotrainer.core.logging.WithThreadIdQueueHandler',
|
|
631
|
+
'queue': log_queue,
|
|
632
|
+
'level': logging.NOTSET, # pass everything to the listener
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
# root logger is here:
|
|
636
|
+
'root': {
|
|
637
|
+
'handlers': ['queue'],
|
|
638
|
+
# with its own level here:
|
|
639
|
+
'level': logging.NOTSET, # root_log_level,
|
|
640
|
+
# FORCE NOTSET to relay everything so that file handler can properly get DEBUG as well
|
|
641
|
+
},
|
|
642
|
+
# but eventual level of other loggers have to be defined here:
|
|
643
|
+
'loggers': copy.deepcopy(_limit_loggers_level),
|
|
644
|
+
}
|
|
645
|
+
return dct_cfg
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def get_verbose_logger(name: Optional[str] = None) -> VerboseLoggerWithThreadId:
|
|
649
|
+
obj = logging.getLogger(name)
|
|
650
|
+
if not isinstance(obj, VerboseLoggerWithThreadId):
|
|
651
|
+
obj.__class__ = VerboseLoggerWithThreadId
|
|
652
|
+
assert isinstance(obj, VerboseLoggerWithThreadId)
|
|
653
|
+
return obj
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def get_log_file_location(*, log_base_dir: str = "", full_format: str):
|
|
657
|
+
if not log_base_dir:
|
|
658
|
+
log_base_dir = Path.home().joinpath("Documents/RawDataLocal")
|
|
659
|
+
else:
|
|
660
|
+
log_base_dir = Path(log_base_dir)
|
|
661
|
+
|
|
662
|
+
date_stamp = datetime.now().strftime("%Y%m%d")
|
|
663
|
+
# pre-format with index=0, and create parent dir
|
|
664
|
+
log_location = Path(full_format.format(
|
|
665
|
+
log_location=log_base_dir,
|
|
666
|
+
date_stamp=date_stamp,
|
|
667
|
+
idx=0,
|
|
668
|
+
))
|
|
669
|
+
log_dir = log_location.parent
|
|
670
|
+
try:
|
|
671
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
672
|
+
except Exception as err:
|
|
673
|
+
logger.error("Failed to create directory for log location %s: %s", log_location, err)
|
|
674
|
+
raise
|
|
675
|
+
# then check for index:
|
|
676
|
+
tot_prev_log_files = len(tuple(log_dir.glob("*.log")))
|
|
677
|
+
log_location = Path(full_format.format(
|
|
678
|
+
log_location=log_base_dir,
|
|
679
|
+
date_stamp=date_stamp,
|
|
680
|
+
idx=tot_prev_log_files + 1,
|
|
681
|
+
))
|
|
682
|
+
|
|
683
|
+
return log_location
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
_prev_file_handler: Optional[logging.FileHandler] = None
|
|
687
|
+
|
|
688
|
+
def set_log_location(log_file: Path):
|
|
689
|
+
global _prev_file_handler
|
|
690
|
+
logger.verbose("Setting log file to %s", log_file)
|
|
691
|
+
#
|
|
692
|
+
q_listener = get_log_queue_listener()
|
|
693
|
+
if q_listener is not None:
|
|
694
|
+
q_listener.switch_file_handler(log_file)
|
|
695
|
+
else:
|
|
696
|
+
file_handler = logging.FileHandler(log_file)
|
|
697
|
+
file_handler.addFilter(thread_id_filter)
|
|
698
|
+
file_handler.setFormatter(
|
|
699
|
+
PreciseTimeFormatter(
|
|
700
|
+
MULTIPROC_LOG_FORMAT,
|
|
701
|
+
datefmt=DateTimeFormats.year_precise,
|
|
702
|
+
time_precision=6,
|
|
703
|
+
)
|
|
704
|
+
)
|
|
705
|
+
file_handler.setLevel(verboselogs.SPAM + 1) # writes everything up to DEBUG which reaches it
|
|
706
|
+
logging.root.addHandler(file_handler)
|
|
707
|
+
if _prev_file_handler is not None:
|
|
708
|
+
logging.root.removeHandler(_prev_file_handler)
|
|
709
|
+
_prev_file_handler = file_handler
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
# finally:
|
|
713
|
+
|
|
714
|
+
logger = get_verbose_logger(__name__) # noqa
|