Skip to content

Commit 8af3efa

Browse files
authored
Merge pull request #30 from richardkoehler/feat/improve-logging
Improve logging
2 parents 4534146 + ebb4aa3 commit 8af3efa

11 files changed

Lines changed: 306 additions & 85 deletions

src/clinical_dbs_annotator/__main__.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import PySide6.QtSvg # noqa: F401 - required to enable SVG rendering in QSS
1212
from PySide6.QtWidgets import QApplication
1313

14-
from .logging_config import setup_logging
14+
from .logging_config import setup_bootstrap_logging, setup_logging
1515
from .utils import get_theme_manager
1616
from .views import WizardWindow
1717

18+
logger = logging.getLogger(__name__)
19+
1820

1921
def main() -> int:
2022
"""
@@ -23,23 +25,28 @@ def main() -> int:
2325
Returns:
2426
Exit code (0 for success)
2527
"""
26-
app = QApplication(sys.argv)
28+
setup_bootstrap_logging()
29+
try:
30+
app = QApplication(sys.argv)
2731

28-
app.setApplicationName("Clinical DBS Annotator")
29-
app.setOrganizationName("BML")
32+
app.setApplicationName("Clinical DBS Annotator")
33+
app.setOrganizationName("BML")
3034

31-
setup_logging(app)
35+
setup_logging(app)
3236

33-
theme_manager = get_theme_manager()
34-
try:
35-
theme_manager.apply_theme(theme_manager.get_current_theme(), app)
36-
except Exception as e:
37-
logging.warning("Could not load theme: %s", e)
37+
theme_manager = get_theme_manager()
38+
try:
39+
theme_manager.apply_theme(theme_manager.get_current_theme(), app)
40+
except Exception:
41+
logger.exception("Could not load current theme")
3842

39-
window = WizardWindow(app)
40-
window.show()
43+
window = WizardWindow(app)
44+
window.show()
4145

42-
return app.exec()
46+
return app.exec()
47+
except Exception:
48+
logger.critical("Fatal startup failure", exc_info=True)
49+
return 1
4350

4451

4552
if __name__ == "__main__":

src/clinical_dbs_annotator/logging_config.py

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import faulthandler
12
import logging
23
import platform
34
import sys
@@ -17,10 +18,53 @@
1718

1819
_configured = False
1920
_log_file_path: Path | None = None
21+
_crash_log_file = None
22+
23+
24+
def _install_exception_hooks() -> None:
25+
def exc_hook(exc_type, exc, tb):
26+
logging.getLogger("uncaught").critical(
27+
"Uncaught exception",
28+
exc_info=(exc_type, exc, tb),
29+
)
30+
31+
sys.excepthook = exc_hook
32+
33+
def thread_exc_hook(args: threading.ExceptHookArgs) -> None:
34+
logging.getLogger("uncaught").critical(
35+
"Uncaught thread exception",
36+
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
37+
)
38+
39+
threading.excepthook = thread_exc_hook
40+
41+
def unraisable_hook(args: sys.UnraisableHookArgs) -> None:
42+
logging.getLogger("uncaught").error(
43+
"Unraisable exception in %r",
44+
args.object,
45+
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
46+
)
47+
48+
sys.unraisablehook = unraisable_hook
49+
50+
51+
def setup_bootstrap_logging() -> None:
52+
root = logging.getLogger()
53+
if root.handlers:
54+
_install_exception_hooks()
55+
return
56+
57+
fmt = logging.Formatter("%(asctime)s ¦ %(levelname)s ¦ %(name)s ¦ %(message)s")
58+
sh = logging.StreamHandler(sys.stderr)
59+
sh.setLevel(logging.INFO)
60+
sh.setFormatter(fmt)
61+
root.setLevel(logging.DEBUG)
62+
root.addHandler(sh)
63+
_install_exception_hooks()
2064

2165

2266
def setup_logging(_app: QApplication) -> Path:
23-
global _configured, _log_file_path
67+
global _configured, _log_file_path, _crash_log_file
2468
if _configured:
2569
assert _log_file_path is not None
2670
return _log_file_path
@@ -53,21 +97,7 @@ def setup_logging(_app: QApplication) -> Path:
5397
sh.setFormatter(fmt)
5498
root.addHandler(sh)
5599

56-
def exc_hook(exc_type, exc, tb):
57-
logging.getLogger("uncaught").critical(
58-
"Uncaught exception",
59-
exc_info=(exc_type, exc, tb),
60-
)
61-
62-
sys.excepthook = exc_hook
63-
64-
def thread_exc_hook(args: threading.ExceptHookArgs) -> None:
65-
logging.getLogger("uncaught").critical(
66-
"Uncaught thread exception",
67-
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
68-
)
69-
70-
threading.excepthook = thread_exc_hook
100+
_install_exception_hooks()
71101

72102
def qt_handler(mode: QtMsgType, context: QMessageLogContext, message: str) -> None:
73103
if mode == QtMsgType.QtDebugMsg:
@@ -86,6 +116,19 @@ def qt_handler(mode: QtMsgType, context: QMessageLogContext, message: str) -> No
86116
logging.getLogger("qt").log(level, "%s%s", message, suffix)
87117

88118
qInstallMessageHandler(qt_handler)
119+
_app.aboutToQuit.connect(
120+
lambda: logging.getLogger("clinical_dbs_annotator").info("Application shutdown")
121+
)
122+
123+
crash_log_path = log_dir / "clinical-dbs-annotator-crash.log"
124+
try:
125+
_crash_log_file = open(crash_log_path, "a", encoding="utf-8")
126+
faulthandler.enable(file=_crash_log_file, all_threads=True)
127+
except Exception:
128+
logging.getLogger("clinical_dbs_annotator").exception(
129+
"Failed to enable faulthandler with crash log %s",
130+
crash_log_path,
131+
)
89132

90133
logging.getLogger("clinical_dbs_annotator").info(
91134
"Started v%s Python %s | %s | log=%s",

src/clinical_dbs_annotator/models/session_data.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
import csv
9+
import logging
910
from datetime import datetime
1011
from pathlib import Path
1112
from typing import TextIO
@@ -15,6 +16,8 @@
1516
from .clinical_scale import ClinicalScale, SessionScale
1617
from .stimulation import StimulationParameters
1718

19+
logger = logging.getLogger(__name__)
20+
1821

1922
class SessionData:
2023
"""
@@ -82,6 +85,7 @@ def open_file_append(
8285
# Calculate next session_id and block_id
8386
max_block = -1
8487
max_session = 0
88+
parse_errors = 0
8589
try:
8690
with open(file_path, newline="", encoding="utf-8") as f:
8791
reader = csv.DictReader(f, delimiter="\t")
@@ -97,10 +101,22 @@ def open_file_append(
97101
if session_val is not None and session_val != "":
98102
max_session = max(max_session, int(float(session_val)))
99103
except Exception:
104+
parse_errors += 1
100105
continue
101106
except Exception:
107+
logger.warning(
108+
"Failed to inspect existing session file before append: %s",
109+
file_path,
110+
exc_info=True,
111+
)
102112
max_block = -1
103113
max_session = 0
114+
if parse_errors:
115+
logger.warning(
116+
"Skipped %d malformed rows while opening session file in append mode: %s",
117+
parse_errors,
118+
file_path,
119+
)
104120

105121
if start_block_id is None:
106122
start_block_id = max_block + 1
@@ -114,6 +130,11 @@ def open_file_append(
114130
reader = csv.DictReader(f, delimiter="\t")
115131
existing_fieldnames = reader.fieldnames
116132
except Exception:
133+
logger.warning(
134+
"Failed to read existing TSV headers, using defaults: %s",
135+
file_path,
136+
exc_info=True,
137+
)
117138
existing_fieldnames = None
118139

119140
self.tsv_fieldnames = existing_fieldnames or list(TSV_COLUMNS)
@@ -128,7 +149,11 @@ def open_file_append(
128149
if Path(file_path).stat().st_size == 0:
129150
self.tsv_writer.writeheader()
130151
except Exception:
131-
pass
152+
logger.warning(
153+
"Failed to initialize TSV header for file: %s",
154+
file_path,
155+
exc_info=True,
156+
)
132157
self.session_start_time = datetime.now()
133158

134159
def close_file(self) -> None:
@@ -365,6 +390,11 @@ def open_simple_file_append(self, filepath: str) -> None:
365390
reader = csv.DictReader(f, delimiter="\t")
366391
fieldnames = reader.fieldnames
367392
except Exception:
393+
logger.warning(
394+
"Failed reading annotation TSV headers, using defaults: %s",
395+
filepath,
396+
exc_info=True,
397+
)
368398
fieldnames = None
369399

370400
fieldnames = fieldnames or ["date", "time", "timezone", "annotation"]
@@ -381,7 +411,11 @@ def open_simple_file_append(self, filepath: str) -> None:
381411
self.tsv_writer.writeheader()
382412
self.tsv_file.flush()
383413
except Exception:
384-
pass
414+
logger.warning(
415+
"Failed to initialize annotation TSV header: %s",
416+
filepath,
417+
exc_info=True,
418+
)
385419

386420
def write_simple_annotation(self, annotation: str) -> None:
387421
"""

0 commit comments

Comments
 (0)