Coverage for bim2sim/kernel/log.py: 98%
97 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1import logging
2from pathlib import Path
3from typing import List, Tuple
5# from bim2sim.project import Project
7USER = 'user'
9user = {'audience': USER}
12# TODO: check log calls
13# TODO: fix errors exposed by log messages
16class AudienceFilter(logging.Filter):
17 def __init__(self, audience, name=''):
18 super().__init__(name)
19 self.audience = audience
21 def filter(self, record: logging.LogRecord) -> bool:
22 audience = getattr(record, 'audience', None)
23 return audience == self.audience
26class ThreadLogFilter(logging.Filter):
27 """This filter only show log entries for specified thread name."""
29 def __init__(self, thread_name, *args, **kwargs):
30 super().__init__(*args, **kwargs)
31 self.thread_name = thread_name
33 def filter(self, record):
34 return record.threadName == self.thread_name
37def get_user_logger(name):
38 return logging.LoggerAdapter(logging.getLogger(name), user)
41class BufferedHandler(logging.Handler):
42 """Handler to buffer messages and don't loose them."""
43 def __init__(self, level=logging.NOTSET):
44 super().__init__(level)
45 self.buffer = []
47 def emit(self, record):
48 self.buffer.append(record)
50 def flush_buffer(self, file_handler):
51 for record in self.buffer:
52 file_handler.emit(record)
53 self.buffer.clear()
56def initial_logging_setup(level=logging.DEBUG):
57 """Initial setup before project folder exists.
59 This is the first of the two-step logging setup. It makes sure that logging
60 messages before project folder creation are still prompted to stream
61 handler and saved to BufferHandler to get complete log files later.
62 """
63 general_logger = logging.getLogger('bim2sim', )
64 general_logger.setLevel(level)
65 general_log_stream_handler = logging.StreamHandler()
66 general_log_stream_handler.setFormatter(dev_formatter)
67 # general_log_stream_handler.addFilter(AudienceFilter(USER))
68 general_logger.addHandler(general_log_stream_handler)
70 # buffered handler to maintain messages that are logged before project
71 # folder and therefore storage place for file handler exists
72 buffered_handler = BufferedHandler()
73 general_logger.addHandler(buffered_handler)
75 return general_logger
78def project_logging_setup(
79 prj=None,
80) -> Tuple[List[logging.Handler], List[ThreadLogFilter]]:
81 """Setup project logging
83 This is the second step of the two-step logging setup. After project folder
84 creation this step adds completes the setup and adds the filder handlers
85 that store the logs.
87 This creates the following:
88 * the file output file bim2sim.log where the logs are stored
89 * the logger quality_logger which stores all information about the quality
90 of existing information of the BIM model. This logger is only on file to
91 keep the logs cleaner
92 """
93 # get general_logger from initial_logging_setup
94 general_logger = logging.getLogger('bim2sim')
96 prj_log_path = prj.paths.log
98 # get existing stream handler and buffer handler from initial_logging_setup
99 handlers = []
100 general_log_stream_handler = None
101 buffered_handler = None
102 for handler in general_logger.handlers:
103 if isinstance(handler, logging.StreamHandler):
104 general_log_stream_handler = handler
105 handlers.append(general_log_stream_handler)
106 if isinstance(handler, BufferedHandler):
107 buffered_handler = handler
108 handlers.append(buffered_handler)
110 # add file handler for general log and flush logs from buffer log to keep
111 # all log messages between FolderStructure creation and project init
112 general_log_file_handler = logging.FileHandler(
113 prj_log_path / 'bim2sim.log')
114 general_log_file_handler.setFormatter(dev_formatter)
115 general_log_file_handler.addFilter(AudienceFilter(audience=None))
116 general_logger.addHandler(general_log_file_handler)
117 if buffered_handler:
118 buffered_handler.flush_buffer(general_log_file_handler)
119 handlers.append(general_log_file_handler)
121 # add quality logger with stream and file handler
122 quality_logger = logging.getLogger('bim2sim.QualityReport')
123 # do not propagate messages to main bim2sim logger to keep logs cleaner
124 quality_logger.propagate = False
125 quality_handler = logging.FileHandler(
126 Path(prj_log_path / "IFCQualityReport.log"))
127 quality_handler.setFormatter(quality_formatter)
128 quality_logger.addHandler(quality_handler)
129 handlers.append(quality_handler)
131 # set thread log filter
132 thread_log_filter = ThreadLogFilter(prj.thread_name)
133 for handler in handlers:
134 handler.addFilter(thread_log_filter)
136 return handlers, [thread_log_filter]
139def teardown_loggers():
140 """Closes and removes all handlers from loggers in the 'bim2sim' hierarchy.
142 Iterates through all existing loggers and cleans up those that start
143 with 'bim2sim'.
144 For each matching logger, all handlers are properly closed and removed.
145 Errors during file handler closure (e.g., due to already deleted l
146 og files) are silently ignored.
148 """
149 # Get all loggers from logging manager hierarchy
150 logger_dict = logging.Logger.manager.loggerDict
152 # Iterate through all loggers that start with 'bim2sim'
153 for logger_name, logger_instance in logger_dict.items():
154 if isinstance(logger_instance,
155 logging.Logger) and logger_name.startswith('bim2sim'):
156 for handler in logger_instance.handlers[:]:
157 try:
158 handler.close()
159 except (PermissionError, FileNotFoundError):
160 pass
161 logger_instance.removeHandler(handler)
164class CustomFormatter(logging.Formatter):
165 """Custom logging design based on
166 https://stackoverflow.com/questions/384076/how-can-i-color-python
167 -logging-output"""
169 def __init__(self, fmt):
170 super().__init__()
171 self._fmt = fmt
173 def format(self, record):
174 grey = "\x1b[37;20m"
175 green = "\x1b[32;20m"
176 yellow = "\x1b[33;20m"
177 red = "\x1b[31;20m"
178 bold_red = "\x1b[31;1m"
179 reset = "\x1b[0m"
180 # format = "[%(levelname)s] %(name)s: %(message)s"
182 FORMATS = {
183 logging.DEBUG: grey + self._fmt + reset,
184 logging.INFO: green + self._fmt + reset,
185 logging.WARNING: yellow + self._fmt + reset,
186 logging.ERROR: red + self._fmt + reset,
187 logging.CRITICAL: bold_red + self._fmt + reset
188 }
189 log_fmt = FORMATS.get(record.levelno)
190 formatter = logging.Formatter(log_fmt)
191 return formatter.format(record)
194quality_formatter = CustomFormatter('[QUALITY-%(levelname)s] %(name)s:'
195 ' %(message)s')
196user_formatter = CustomFormatter('[USER-%(levelname)s]:'
197 ' %(message)s')
198dev_formatter = CustomFormatter('[DEV-%(levelname)s] -'
199 ' %(asctime)s %(name)s.%(funcName)s:'
200 ' %(message)s')