Было консольное Python приложение, в котором много где пишутся логи с использованием модуля logging. Затем прикрутил GUI на PyQt6, конечно хочется продублировать логи в какой-нибудь виджет в уголочке. Категорически не хочется ничего менять в консольной части, и спокойно использовать дальше стандартный logging.
В этом посте будет рассмотрено два примера. Простой — виджет, который дублировал бы вывод стандартного Python логгера. Усложнение — имеется несколько потоков, они тоже пишут логи. Нужно их логи тоже увидеть на виджете, но он в родительской части, а потоки не могут напрямую в него писать — получим сегфолт.
Введение
В интернетах такой вопрос я встречал на лоре, редите, оверфлоу… Толковых решений не было, пол года назад. Предлагалась дичь с перенаправлением stdout. Или читать логи из файла и потом их на виджет пихать. Меня это не устраивало.
Колхозный вариант пришел с ходу — слазить в ядро приложения, добавить там классам наследованием от QObject, и при записи в логгер заодно слать сигнал с тем же сообщением. В gui части его ловить и выводить на виджет. Попробовал в одном месте — работает, сегфолта нет.
Решение, конечно, отстой. Во-первых наглухо привязываем ядро к Qt, и смешивает интерфейс и вычисления. Во-вторых лень вносить столько правок для такого топорного и неправильного с точки зрения проектирования решения.
Задачу я решил на следующий день, перепробовав несколько подходов. Поэтому решил высечь в камне опубликовать на хабре этот кейс.
Статья не рассматривает основы основ, и подразумевает что читатель знает как пользоваться logging и может сделать «Hellow, world!» на PyQt6 (на PySide и C++ Qt я думаю тоже будет работать, возможно с (не)большими изменениями).
Ссылки на материалы по сабжу:
-
Базовые знания про модуль logging, на Хабре уже хорошо описали.
-
Документация модуля logging.
-
Базовые знания про PyQt6, на Хабре тоже уже имеются.
1. Простой пример — черный ящик лога в QPlainText
Суть.
В модуле logging есть класс StreamHandler. У него есть метод emit(), который вызывается каждый раз когда в логгер отправляется сообщение, например
logger.info("Форматирование всех дисков завершено, хорошего вечера.")
Создаем класс множественным наследованием StreamHandler и QPlainText, внутри него прячем все подробности. Юзаем глобальный логгер и не паримся. Просто короткий пример, чтобы понять суть. В коде избыточное количество комментариев, так что не знаю, что тут еще объяснять.
#!/usr/bin/env python3 import sys import logging from PyQt6 import QtWidgets # Создаем логгер NAME = "ULTIMATE SUPER MEGA BEST LOGGER v0.1.0" logger = logging.getLogger(NAME) logger.setLevel(logging.DEBUG) # Наследуем QPlainTextEdit & StreamHandler class LogWidget(QtWidgets.QPlainTextEdit, logging.StreamHandler): def __init__(self, parent=None): QtWidgets.QPlainTextEdit.__init__(self, parent) logging.StreamHandler.__init__(self) # Создаем formatter stream_formatter = logging.Formatter( "%(module)s: %(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", ) # Устанавливаем formatter в self # self является и текст эдитом и хендлером self.setFormatter(stream_formatter) # Устанавливаем уроверь вывода лога self.setLevel(logging.INFO) # Ну это просто для иллюстрации как получить тот же логгер, # хотя в данном примере можно было бы просто взять глобальный 'logger'. # В реальных задачах логгер может быть объявлен и # сконфигурирован в другом месте, теперь тут мы получаем # тот же логгер через имя logger = logging.getLogger(NAME) # Добавляем в self еще один хендлер, # который мутант StreamHandler и QPlainTextEdit logger.addHandler(self) # Переопределяем метод StreamHandler.emit, этот метод # вызывается при каждой записи в логгер, типо: # logger.warning("У вас хлеб кончился, вам надо хлеба купить!") def emit(self, record: str): # record - строка которую мы передали логгеру # метод формат применяет к ней форматтер установленный # ранее в конструкторе log_msg = self.format(record) # отправляем отформатированную строку в QPlainTextEdit self.appendPlainText(log_msg) self.__scrollDown() # сбрасываем буфер self.flush() def __scrollDown(self): scroll_bar = self.verticalScrollBar() end_text = scroll_bar.maximum() scroll_bar.setValue(end_text) def main(): # создаем приложение, и наш виджет app = QtWidgets.QApplication(sys.argv) w = LogWidget() w.show() # Напишем что-нибудь в логгер logger.debug("Это дебаг сообщение, мы его не увидим") logger.info("Hello, Habr!") logger.warning("Винни: Я тучка тучка тучка, я вовсе не медведь") logger.error("Пятачок: Ой, кажется дождь собирается") logger.critical("Винни: Это неправильные пчелы...") sys.exit(app.exec()) if __name__ == "__main__": main()
Проблемы данного решения:
— если в логгер напишет кто-то из дочернего потока получим Segmentation fault
— ну вообще не кошерно
2. Более человеческий пример
У меня заработало. По рынку я наверное не дотягиваю до джуна, проект пишу для себя, так что не претендую на каноничность этого решения.
-
Пусть где-то у нас есть логгер, который пишет инфо стрим в консоль, и дебаг в файл.
-
Возьмем на этот раз QTextEdit и заодно раскрасим сообщения. Именно такой вопрос попадался мне в сети.
-
Сделаем свой хендлер, наследуем logging.StreamHandler и он будет при записи нового сообщения, заодно создавать сигнал, в котором будет передаваться отформатированное и раскрашенное сообщение.
-
Лог виджет (отнаследованный от QTextEdit) будет принимать в конструктор наш хендлер и коннектить его сигнал к своему методу __updateText(). __
-
Профит — все сообщения отправленные в логгер, где бы они не были, и в дочерних потоках тоже, будут спокойно дублироваться на этот виджет. Qt позволяет передавать сигналы от дочерних потоков в родительский, или вообще куда угодно.
-
Смотрим код с комментариями:
#!/usr/bin/env python3 import sys import logging from PyQt6 import QtCore, QtWidgets # Отнаследуем StreamHandler и QtObject # QtObject нужен чтобы отправлять сигналы class MyHandler(logging.StreamHandler, QtCore.QObject): # Сигнал, передающий отформатированное сообщение логгера # по сути мы его можем теперь ловить любым виджетом # и что угодно с ним делать, хоть на QLabel выводить message = QtCore.pyqtSignal(str) def __init__(self, parent=None): logging.StreamHandler.__init__(self) QtCore.QObject.__init__(self, parent) def emit(self, record: str): # record - строка которую мы передали логгеру # метод формат применяет к ней форматтер если он установлен log_msg = self.format(record) # Раскрасим строку перед отправкой if "DEBUG" in log_msg: text = f"""<span style='color:#888888;'>{log_msg}</span>""" elif "INFO" in log_msg: text = f"""<span style='color:#008800;'>{log_msg}</span>""" elif "WARNING" in log_msg: text = f"""<span style='color:#888800;'>{log_msg}</span>""" elif "ERROR" in log_msg: text = f"""<span style='color:#000088;'>{log_msg}</span>""" elif "CRITICAL" in log_msg: text = f"""<span style='color:#880000;'>{log_msg}</span>""" # генерируем сигнал, передаем раскрашенный текст self.message.emit(text) # сбрасываем буфер self.flush() class LogWidget(QtWidgets.QTextEdit): def __init__(self, handler: logging.StreamHandler, parent=None): QtWidgets.QTabWidget.__init__(self, parent) # Сохраним хендлер чтобы его сборщик мусора не прибил self.handler = handler # Конектим сигнал от хендлера self.handler.message.connect(self.__updateText) def __scrollDown(self): logger.debug(f"{self.__class__.__name__}.__scrollDown()") scroll_bar = self.verticalScrollBar() end_text = scroll_bar.maximum() scroll_bar.setValue(end_text) def __updateText(self, msg: str): logger.debug(f"{self.__class__.__name__}.__updateText(msg)") self.append(msg) self.__scrollDown() # Пусть где то и когда-то мы создали и настроили логгер NAME = "NEO_IMPROVE_LOGGER v0.10.0" logger = logging.getLogger(NAME) logger.setLevel(logging.DEBUG) # Со стрим хендлером для вывода лога в консоль, уровень [INFO] stream_formatter = logging.Formatter( "%(module)s: %(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", ) stream_handler = logging.StreamHandler() stream_handler.setFormatter(stream_formatter) stream_handler.setLevel(logging.INFO) logger.addHandler(stream_handler) # И с файл хендлером для вывода лога в файл, уровень [DEBUG] file_formatter = logging.Formatter( "%(module)s: %(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) file_path = "debug.log" file_handler = logging.FileHandler(file_path, mode='w') file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) def main(): logger.critical( "Это сообщение пойдет в файл и в консоль. " "Но оно не появится на виджете, он еще не создан." ) # Пусть теперь мы еще хотим выводить лог на виджет # создаем стрим хендлер для вывода лога в gui, уровень [WARNING] formatter = logging.Formatter( "%(module)s: %(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", ) gui_stream_handler = MyHandler() # наш хендлер, выдающий сигналы gui_stream_handler.setFormatter(formatter) gui_stream_handler.setLevel(logging.WARNING) # связываем его с общим для всего логгером logging.getLogger(NAME) logger.addHandler(gui_stream_handler) # Здесь запускаем приложение, создаем лог виджет # и передаем ему gui_stream_handler, хендлер уровня WARNING # только его сообщения и будут писаться в виджет app = QtWidgets.QApplication(sys.argv) w = LogWidget(gui_stream_handler) w.show() # Напишем что-нибудь в логгер logger.debug("Это дебаг сообщение, мы его увидим только в файле") logger.info("Это инфо сообщение, мы его увидим в консоли") logger.warning("Винни: ... они несут не правильный мед!") logger.error("Пяточок: И что же теперь делать?") logger.critical("Goodbuy Habr! Thanks for your attention.") sys.exit(app.exec()) if __name__ == "__main__": main()
Побочный бонус — так как теперь мы перехватываем процесс отправки лог сообщения, есть возможность, например так же продолжать все выводить на QTextEdit, а сообщения уровня WARNING и выше показывать в диалоговых окнах. Таким образом, мы получаем единую систему отправки всех сообщений в приложении (через логгер). И единое место которое отвечает за дублирование этих сообщений в GUI. По-моему это хорошо.
П.С.
Надеюсь данное решение будет полезно. Я постарался сделать самое полное изложение вопроса из кусочков и бреда, которые попадались в сети, и своего понимания после чтения документации. Буду рад услышать комментарии более опытных разработчиков.
ссылка на оригинал статьи https://habr.com/ru/articles/823744/
Добавить комментарий