Skip to content

Commit 27316fa

Browse files
committed
feat: write logs to a rotating file
This gives less technical users an easy way to find and send logs when they run into problems. A few startup and environment messages move up to info level so the default log carries enough context to diagnose issues without needing to enable debug mode.
1 parent 6abccc2 commit 27316fa

11 files changed

Lines changed: 120 additions & 24 deletions

File tree

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,9 @@ If applicable, add screenshots to help explain your problem.
3333

3434
- If you have modified the mpv configuration (`Options``Edit mpv.conf...`), list all your changes or a copy of your config
3535

36+
**Logs**
37+
38+
- Please attach the log file. Open `Help``Open App Data Folder...`, then grab `mpvQC.log` from the `logs/` folder
39+
3640
**Additional context**
3741
Add any other context about the problem here.

mpvqc/logging_utils.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,25 @@
99
import re
1010
import sys
1111
from enum import StrEnum
12+
from logging.handlers import RotatingFileHandler
1213
from typing import TYPE_CHECKING, Final, override
1314

14-
from PySide6.QtCore import QtMsgType
15+
from PySide6.QtCore import QtMsgType, qInstallMessageHandler
1516

1617
if TYPE_CHECKING:
1718
from collections.abc import Callable
19+
from pathlib import Path
1820

1921
from PySide6.QtCore import QMessageLogContext
2022

2123
MPV_LEVEL: Final[int] = 25
2224
logging.addLevelName(MPV_LEVEL, "MPV")
2325

26+
FILE_LOG_MAX_BYTES: Final[int] = 1_000_000
27+
FILE_LOG_BACKUP_COUNT: Final[int] = 5
28+
29+
_URL_SCHEME_PATTERN: Final = re.compile(r"^\w+:")
30+
2431

2532
class AnsiColor(StrEnum):
2633
RESET = "\033[0m"
@@ -49,9 +56,9 @@ def use_color() -> bool:
4956

5057

5158
class MpvqcFormatter(logging.Formatter):
52-
def __init__(self) -> None:
59+
def __init__(self, *, colored: bool) -> None:
5360
super().__init__()
54-
self._use_color = use_color()
61+
self._use_color = colored
5562

5663
@override
5764
def format(self, record: logging.LogRecord) -> str:
@@ -84,6 +91,12 @@ def format(self, record: logging.LogRecord) -> str:
8491

8592

8693
def setup_mpvqc_logging() -> None:
94+
_setup_console_logging()
95+
_setup_file_logging()
96+
qInstallMessageHandler(_qt_log_handler())
97+
98+
99+
def _setup_console_logging() -> None:
87100
is_debug = os.getenv("MPVQC_DEBUG")
88101

89102
root_logger = logging.getLogger()
@@ -93,7 +106,7 @@ def setup_mpvqc_logging() -> None:
93106
root_logger.removeHandler(handler)
94107

95108
console_handler = logging.StreamHandler(sys.stdout)
96-
console_handler.setFormatter(MpvqcFormatter())
109+
console_handler.setFormatter(MpvqcFormatter(colored=use_color()))
97110
root_logger.addHandler(console_handler)
98111

99112
if is_debug:
@@ -102,15 +115,29 @@ def setup_mpvqc_logging() -> None:
102115
logging.getLogger(name).setLevel(logging.WARNING)
103116

104117

105-
_URL_SCHEME_PATTERN: Final = re.compile(r"^\w+:")
118+
def _setup_file_logging() -> None:
119+
try:
120+
from mpvqc.services.application_paths import ApplicationPathsService
106121

122+
paths = ApplicationPathsService()
123+
paths.dir_logs.mkdir(parents=True, exist_ok=True)
124+
attach_file_logging(paths.file_log)
125+
except Exception:
126+
logging.getLogger(__name__).exception("Could not set up file logging")
107127

108-
def logger_name_from(path: str) -> str:
109-
path = _URL_SCHEME_PATTERN.sub("", path).lstrip("/").removeprefix("qt/qml/")
110-
return path.replace("/", ".").removesuffix(".qml")
128+
129+
def attach_file_logging(log_file: Path) -> None:
130+
handler = RotatingFileHandler(
131+
log_file,
132+
maxBytes=FILE_LOG_MAX_BYTES,
133+
backupCount=FILE_LOG_BACKUP_COUNT,
134+
encoding="utf-8",
135+
)
136+
handler.setFormatter(MpvqcFormatter(colored=False))
137+
logging.getLogger().addHandler(handler)
111138

112139

113-
def qt_log_handler() -> Callable[[QtMsgType, QMessageLogContext, str], None]:
140+
def _qt_log_handler() -> Callable[[QtMsgType, QMessageLogContext, str], None]:
114141
levels: Final[dict[QtMsgType, int]] = {
115142
QtMsgType.QtDebugMsg: logging.DEBUG,
116143
QtMsgType.QtInfoMsg: logging.INFO,
@@ -144,3 +171,8 @@ def handler(message_type: QtMsgType, context: QMessageLogContext, message: str)
144171
qml_logger.handle(record)
145172

146173
return handler
174+
175+
176+
def logger_name_from(path: str) -> str:
177+
path = _URL_SCHEME_PATTERN.sub("", path).lstrip("/").removeprefix("qt/qml/")
178+
return path.replace("/", ".").removesuffix(".qml")

mpvqc/services/application_paths.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
class _Directories:
1414
backup: Path
1515
config: Path
16+
logs: Path
1617
screenshots: Path
1718
export_templates: Path
1819

@@ -25,6 +26,7 @@ def _portable_directories(executing_directory: Path) -> _Directories:
2526
return _Directories(
2627
backup=executing_directory / "appdata" / "backups",
2728
config=executing_directory / "appdata",
29+
logs=executing_directory / "appdata" / "logs",
2830
screenshots=executing_directory / "appdata" / "screenshots",
2931
export_templates=executing_directory / "appdata" / "export-templates",
3032
)
@@ -36,6 +38,7 @@ def _xdg_directories() -> _Directories:
3638
return _Directories(
3739
backup=Path(config) / appname / "backups",
3840
config=Path(config) / appname,
41+
logs=Path(config) / appname / "logs",
3942
screenshots=Path(config) / appname / "screenshots",
4043
export_templates=Path(config) / appname / "export-templates",
4144
)
@@ -60,6 +63,10 @@ def dir_backup(self) -> Path:
6063
def dir_config(self) -> Path:
6164
return self._dirs.config
6265

66+
@property
67+
def dir_logs(self) -> Path:
68+
return self._dirs.logs
69+
6370
@property
6471
def dir_screenshots(self) -> Path:
6572
return self._dirs.screenshots
@@ -76,6 +83,10 @@ def file_input_conf(self) -> Path:
7683
def file_mpv_conf(self) -> Path:
7784
return self.dir_config / "mpv.conf"
7885

86+
@property
87+
def file_log(self) -> Path:
88+
return self.dir_logs / "mpvQC.log"
89+
7990
@property
8091
def file_settings(self) -> Path:
8192
return self.dir_config / "settings.ini"

mpvqc/services/host_environment/portals.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(self) -> None:
3333

3434
def __enter__(self) -> Self:
3535
if not QTDBUS_AVAILABLE:
36-
logger.debug("QtDBus not available, cannot read portal settings")
36+
logger.info("QtDBus not available, cannot read portal settings")
3737
return self
3838

3939
self._connection = connection = QDBusConnection.connectToBus(
@@ -52,7 +52,7 @@ def __enter__(self) -> Self:
5252
)
5353

5454
if not interface.isValid():
55-
logger.debug("D-Bus settings portal interface is not valid")
55+
logger.info("D-Bus settings portal interface is not valid")
5656

5757
return self
5858

@@ -81,7 +81,7 @@ def _portal_version(self) -> int:
8181
if version is not None:
8282
return int(version)
8383
except (TypeError, ValueError):
84-
logger.debug("Could not determine Settings portal version")
84+
logger.info("Could not determine Settings portal version")
8585

8686
return 0
8787

@@ -116,12 +116,12 @@ def _read_setting(self, interface: QDBusInterface, namespace: str, key: str, por
116116
method_name = "ReadOne"
117117
else:
118118
method_name = "Read"
119-
logger.debug("Using deprecated Read() method (portal version: %s)", portal_version)
119+
logger.info("Using deprecated Read() method (portal version: %s)", portal_version)
120120

121121
reply = interface.call(method_name, namespace, key)
122122

123123
if reply.type() == QDBusMessage.MessageType.ErrorMessage:
124-
logger.debug("D-Bus error reading %s %s: %s", namespace, key, reply.errorMessage())
124+
logger.info("D-Bus error reading %s %s: %s", namespace, key, reply.errorMessage())
125125
return None
126126

127127
dbus_variant = reply.arguments()[0]
@@ -132,7 +132,7 @@ def _read_setting(self, interface: QDBusInterface, namespace: str, key: str, por
132132
value = dbus_variant.variant().variant()
133133

134134
if value is None:
135-
logger.debug("Portal setting %s %s returned None", namespace, key)
135+
logger.info("Portal setting %s %s returned None", namespace, key)
136136
return None
137137

138138
return str(value)

mpvqc/services/host_environment/service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ def is_tiling_window_manager() -> bool:
6565
is_tiling_wm = bool(desktops & tiling_wms)
6666

6767
if is_tiling_wm:
68-
logger.debug("Running on tiling window manager")
68+
logger.info("Running on tiling window manager")
6969

7070
return is_tiling_wm

mpvqc/services/i18n.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def retranslate(self, app: QGuiApplication, language_code: str) -> None:
3030
app.removeTranslator(self._translator_qt)
3131

3232
locale: QLocale = create_locale_from(language_code)
33-
logger.debug("Loading mpvQC translation %s for locale %s", language_code, locale.name())
33+
logger.info("Loading mpvQC translation %s for locale %s", language_code, locale.name())
3434

3535
QLocale.setDefault(locale)
3636
logger.debug("Set default Qt locale to %s", locale.name())

mpvqc/services/importer/service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def _set_busy(self, value: bool) -> None:
5757

5858
def open(self, document_paths: list[Path], video_paths: list[Path], subtitle_paths: list[Path]) -> None:
5959
if self._busy:
60-
logger.debug(
60+
logger.warning(
6161
"Skipping import while another is in progress; documents=%s videos=%s subtitles=%s",
6262
document_paths,
6363
video_paths,

mpvqc/services/main_window.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def initialize(self) -> None:
6767
self._zoom_monitor = zoom_monitor = _DisplayZoomMonitor(window, self._on_zoom_factor_changed)
6868
window.installEventFilter(zoom_monitor)
6969

70-
logger.debug("wired up main window service")
70+
logger.debug("Wired up main window service")
7171

7272
def install_event_filter(self, event_filter: QObject) -> None:
7373
self._active_window.installEventFilter(event_filter)

mpvqc/startup.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,9 @@ def configure_qt_settings() -> None:
4545

4646

4747
def configure_logging() -> None:
48-
from PySide6 import QtCore
49-
50-
from mpvqc.logging_utils import qt_log_handler, setup_mpvqc_logging
48+
from mpvqc.logging_utils import setup_mpvqc_logging
5149

5250
setup_mpvqc_logging()
53-
QtCore.qInstallMessageHandler(qt_log_handler())
5451

5552

5653
def configure_dependency_injection() -> None:

test/services/test_application_paths.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ def test_portable(tmp_path):
1212
assert "appdata" in f"{service.dir_config}"
1313
assert service.dir_backup == tmp_path / "appdata" / "backups"
1414
assert service.dir_config == tmp_path / "appdata"
15+
assert service.dir_logs == tmp_path / "appdata" / "logs"
1516
assert service.dir_screenshots == tmp_path / "appdata" / "screenshots"
1617
assert service.dir_export_templates == tmp_path / "appdata" / "export-templates"
1718
assert service.file_input_conf == tmp_path / "appdata" / "input.conf"
19+
assert service.file_log == tmp_path / "appdata" / "logs" / "mpvQC.log"
1820
assert service.file_mpv_conf == tmp_path / "appdata" / "mpv.conf"
1921
assert service.file_settings == tmp_path / "appdata" / "settings.ini"
2022

@@ -24,4 +26,6 @@ def test_non_portable(tmp_path):
2426

2527
assert "appdata" not in f"{service.dir_config}"
2628
assert service.dir_backup == service.dir_config / "backups"
29+
assert service.dir_logs == service.dir_config / "logs"
2730
assert service.dir_screenshots == service.dir_config / "screenshots"
31+
assert service.file_log == service.dir_config / "logs" / "mpvQC.log"

0 commit comments

Comments
 (0)