logging module without qt in globals

This commit is contained in:
iLLiCiTiT 2020-09-25 16:08:34 +02:00
parent d740c17b91
commit 64052aa44e
5 changed files with 5 additions and 3 deletions

View file

@ -0,0 +1,28 @@
from Qt import QtWidgets, QtCore
from .widgets import LogsWidget, OutputWidget
from avalon import style
class LogsWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super(LogsWindow, self).__init__(parent)
self.setStyleSheet(style.load_stylesheet())
self.resize(1400, 800)
log_detail = OutputWidget(parent=self)
logs_widget = LogsWidget(log_detail, parent=self)
main_layout = QtWidgets.QHBoxLayout()
log_splitter = QtWidgets.QSplitter()
log_splitter.setOrientation(QtCore.Qt.Horizontal)
log_splitter.addWidget(logs_widget)
log_splitter.addWidget(log_detail)
main_layout.addWidget(log_splitter)
self.logs_widget = logs_widget
self.log_detail = log_detail
self.setLayout(main_layout)
self.setWindowTitle("Logs")

View file

@ -0,0 +1,136 @@
import collections
from Qt import QtCore, QtGui
from pype.api import Logger
from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME
log = Logger().get_logger("LogModel", "LoggingModule")
class LogModel(QtGui.QStandardItemModel):
COLUMNS = (
"process_name",
"hostname",
"hostip",
"username",
"system_name",
"started"
)
colums_mapping = {
"process_name": "Process Name",
"process_id": "Process Id",
"hostname": "Hostname",
"hostip": "Host IP",
"username": "Username",
"system_name": "System name",
"started": "Started at"
}
process_keys = (
"process_id", "hostname", "hostip",
"username", "system_name", "process_name"
)
log_keys = (
"timestamp", "level", "thread", "threadName", "message", "loggerName",
"fileName", "module", "method", "lineNumber"
)
default_value = "- Not set -"
ROLE_LOGS = QtCore.Qt.UserRole + 2
ROLE_PROCESS_ID = QtCore.Qt.UserRole + 3
def __init__(self, parent=None):
super(LogModel, self).__init__(parent)
self.log_by_process = None
self.dbcon = None
# Crash if connection is not possible to skip this module
database = _bootstrap_mongo_log()
if LOG_COLLECTION_NAME in database.list_collection_names():
self.dbcon = database[LOG_COLLECTION_NAME]
def headerData(self, section, orientation, role):
if (
role == QtCore.Qt.DisplayRole
and orientation == QtCore.Qt.Horizontal
):
if section < len(self.COLUMNS):
key = self.COLUMNS[section]
return self.colums_mapping.get(key, key)
super(LogModel, self).headerData(section, orientation, role)
def add_process_logs(self, process_logs):
items = []
first_item = True
for key in self.COLUMNS:
display_value = str(process_logs[key])
item = QtGui.QStandardItem(display_value)
if first_item:
first_item = False
item.setData(process_logs["_logs"], self.ROLE_LOGS)
item.setData(process_logs["process_id"], self.ROLE_PROCESS_ID)
items.append(item)
self.appendRow(items)
def refresh(self):
self.log_by_process = collections.defaultdict(list)
self.process_info = {}
self.clear()
self.beginResetModel()
if self.dbcon:
result = self.dbcon.find({})
for item in result:
process_id = item.get("process_id")
# backwards (in)compatibility
if not process_id:
continue
if process_id not in self.process_info:
proc_dict = {"_logs": []}
for key in self.process_keys:
proc_dict[key] = (
item.get(key) or self.default_value
)
self.process_info[process_id] = proc_dict
log_item = {}
for key in self.log_keys:
log_item[key] = item.get(key) or self.default_value
if "exception" in item:
log_item["exception"] = item["exception"]
self.process_info[process_id]["_logs"].append(log_item)
for item in self.process_info.values():
item["_logs"] = sorted(
item["_logs"], key=lambda item: item["timestamp"]
)
item["started"] = item["_logs"][0]["timestamp"]
self.add_process_logs(item)
self.endResetModel()
class LogsFilterProxy(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(LogsFilterProxy, self).__init__(*args, **kwargs)
self.col_usernames = None
self.filter_usernames = set()
def update_users_filter(self, users):
self.filter_usernames = set()
for user in users or tuple():
self.filter_usernames.add(user)
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
if self.col_usernames is not None:
index = self.sourceModel().index(
source_row, self.col_usernames, source_parent
)
user = index.data(QtCore.Qt.DisplayRole)
if user not in self.filter_usernames:
return False
return True

View file

@ -0,0 +1,331 @@
from Qt import QtCore, QtWidgets
from avalon.vendor import qtawesome
from .models import LogModel, LogsFilterProxy
class SearchComboBox(QtWidgets.QComboBox):
"""Searchable ComboBox with empty placeholder value as first value"""
def __init__(self, parent=None, placeholder=""):
super(SearchComboBox, self).__init__(parent)
self.setEditable(True)
self.setInsertPolicy(self.NoInsert)
self.lineEdit().setPlaceholderText(placeholder)
# Apply completer settings
completer = self.completer()
completer.setCompletionMode(completer.PopupCompletion)
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
# Force style sheet on popup menu
# It won't take the parent stylesheet for some reason
# todo: better fix for completer popup stylesheet
if parent:
popup = completer.popup()
popup.setStyleSheet(parent.styleSheet())
self.currentIndexChanged.connect(self.onIndexChange)
def onIndexChange(self, index):
print(index)
def populate(self, items):
self.clear()
self.addItems([""]) # ensure first item is placeholder
self.addItems(items)
def get_valid_value(self):
"""Return the current text if it's a valid value else None
Note: The empty placeholder value is valid and returns as ""
"""
text = self.currentText()
lookup = set(self.itemText(i) for i in range(self.count()))
if text not in lookup:
return None
return text
class SelectableMenu(QtWidgets.QMenu):
selection_changed = QtCore.Signal()
def mouseReleaseEvent(self, event):
action = self.activeAction()
if action and action.isEnabled():
action.trigger()
self.selection_changed.emit()
else:
super(SelectableMenu, self).mouseReleaseEvent(event)
class CustomCombo(QtWidgets.QWidget):
selection_changed = QtCore.Signal()
def __init__(self, title, parent=None):
super(CustomCombo, self).__init__(parent)
toolbutton = QtWidgets.QToolButton(self)
toolbutton.setText(title)
toolmenu = SelectableMenu(self)
toolbutton.setMenu(toolmenu)
toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
layout = QtWidgets.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(toolbutton)
self.setLayout(layout)
# toolmenu.selection_changed.connect(self.on_selection_changed)
toolmenu.selection_changed.connect(self.selection_changed)
self.toolbutton = toolbutton
self.toolmenu = toolmenu
self.main_layout = layout
def populate(self, items):
self.toolmenu.clear()
self.addItems(items)
def addItems(self, items):
for item in items:
action = self.toolmenu.addAction(item)
action.setCheckable(True)
action.setChecked(True)
self.toolmenu.addAction(action)
def items(self):
for action in self.toolmenu.actions():
yield action
class LogsWidget(QtWidgets.QWidget):
"""A widget that lists the published subsets for an asset"""
def __init__(self, detail_widget, parent=None):
super(LogsWidget, self).__init__(parent=parent)
model = LogModel()
proxy_model = LogsFilterProxy()
proxy_model.setSourceModel(model)
proxy_model.col_usernames = model.COLUMNS.index("username")
filter_layout = QtWidgets.QHBoxLayout()
# user_filter = SearchComboBox(self, "Users")
user_filter = CustomCombo("Users", self)
users = model.dbcon.distinct("username")
user_filter.populate(users)
user_filter.selection_changed.connect(self._user_changed)
proxy_model.update_users_filter(users)
level_filter = CustomCombo("Levels", self)
# levels = [(level, True) for level in model.dbcon.distinct("level")]
levels = model.dbcon.distinct("level")
level_filter.addItems(levels)
level_filter.selection_changed.connect(self._level_changed)
detail_widget.update_level_filter(levels)
spacer = QtWidgets.QWidget()
icon = qtawesome.icon("fa.refresh", color="white")
refresh_btn = QtWidgets.QPushButton(icon, "")
filter_layout.addWidget(user_filter)
filter_layout.addWidget(level_filter)
filter_layout.addWidget(spacer, 1)
filter_layout.addWidget(refresh_btn)
view = QtWidgets.QTreeView(self)
view.setAllColumnsShowFocus(True)
view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(filter_layout)
layout.addWidget(view)
view.setModel(proxy_model)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setSortingEnabled(True)
view.sortByColumn(
model.COLUMNS.index("started"),
QtCore.Qt.DescendingOrder
)
view.selectionModel().selectionChanged.connect(self._on_index_change)
refresh_btn.clicked.connect(self._on_refresh_clicked)
# Store to memory
self.model = model
self.proxy_model = proxy_model
self.view = view
self.user_filter = user_filter
self.level_filter = level_filter
self.detail_widget = detail_widget
self.refresh_btn = refresh_btn
# prepare
self.refresh()
def refresh(self):
self.model.refresh()
self.detail_widget.refresh()
def _on_refresh_clicked(self):
self.refresh()
def _on_index_change(self, to_index, from_index):
index = self._selected_log()
if index:
logs = index.data(self.model.ROLE_LOGS)
else:
logs = []
self.detail_widget.set_detail(logs)
def _user_changed(self):
checked_values = set()
for action in self.user_filter.items():
if action.isChecked():
checked_values.add(action.text())
self.proxy_model.update_users_filter(checked_values)
def _level_changed(self):
checked_values = set()
for action in self.level_filter.items():
if action.isChecked():
checked_values.add(action.text())
self.detail_widget.update_level_filter(checked_values)
def on_context_menu(self, point):
# TODO will be any actions? it's ready
return
point_index = self.view.indexAt(point)
if not point_index.isValid():
return
# Get selected subsets without groups
selection = self.view.selectionModel()
rows = selection.selectedRows(column=0)
def _selected_log(self):
selection = self.view.selectionModel()
rows = selection.selectedRows(column=0)
if len(rows) == 1:
return rows[0]
return None
class OutputWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(OutputWidget, self).__init__(parent=parent)
layout = QtWidgets.QVBoxLayout(self)
show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp")
output_text = QtWidgets.QTextEdit()
output_text.setReadOnly(True)
# output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth)
layout.addWidget(show_timecode_checkbox)
layout.addWidget(output_text)
show_timecode_checkbox.stateChanged.connect(
self.on_show_timecode_change
)
self.setLayout(layout)
self.output_text = output_text
self.show_timecode_checkbox = show_timecode_checkbox
self.refresh()
def refresh(self):
self.set_detail()
def show_timecode(self):
return self.show_timecode_checkbox.isChecked()
def on_show_timecode_change(self):
self.set_detail(self.las_logs)
def update_level_filter(self, levels):
self.filter_levels = set()
for level in levels or tuple():
self.filter_levels.add(level.lower())
self.set_detail(self.las_logs)
def add_line(self, line):
self.output_text.append(line)
def set_detail(self, logs=None):
self.las_logs = logs
self.output_text.clear()
if not logs:
return
show_timecode = self.show_timecode()
for log in logs:
level = log["level"].lower()
if level not in self.filter_levels:
continue
line_f = "<font color=\"White\">{message}"
if level == "debug":
line_f = (
"<font color=\"Yellow\"> -"
" <font color=\"Lime\">{{ {loggerName} }}: ["
" <font color=\"White\">{message}"
" <font color=\"Lime\">]"
)
elif level == "info":
line_f = (
"<font color=\"Lime\">>>> ["
" <font color=\"White\">{message}"
" <font color=\"Lime\">]"
)
elif level == "warning":
line_f = (
"<font color=\"Yellow\">*** WRN:"
" <font color=\"Lime\"> >>> {{ {loggerName} }}: ["
" <font color=\"White\">{message}"
" <font color=\"Lime\">]"
)
elif level == "error":
line_f = (
"<font color=\"Red\">!!! ERR:"
" <font color=\"White\">{timestamp}"
" <font color=\"Lime\">>>> {{ {loggerName} }}: ["
" <font color=\"White\">{message}"
" <font color=\"Lime\">]"
)
exc = log.get("exception")
if exc:
log["message"] = exc["message"]
line = line_f.format(**log)
if show_timecode:
timestamp = log["timestamp"]
line = timestamp.strftime("%Y-%d-%m %H:%M:%S") + " " + line
self.add_line(line)
if not exc:
continue
for _line in exc["stackTrace"].split("\n"):
self.add_line(_line)

View file

@ -1,6 +1,4 @@
from Qt import QtWidgets
from pype.api import Logger
from ..gui.app import LogsWindow
class LoggingModule:
@ -8,7 +6,11 @@ class LoggingModule:
self.parent = parent
self.log = Logger().get_logger(self.__class__.__name__, "logging")
self.tray_init(main_parent, parent)
def tray_init(self, main_parent, parent):
try:
from .gui.app import LogsWindow
self.window = LogsWindow()
self.tray_menu = self._tray_menu
except Exception:
@ -18,9 +20,9 @@ class LoggingModule:
# Definition of Tray menu
def _tray_menu(self, parent_menu):
from Qt import QtWidgets
# Menu for Tray App
menu = QtWidgets.QMenu('Logging', parent_menu)
# menu.setProperty('submenu', 'on')
show_action = QtWidgets.QAction("Show Logs", menu)
show_action.triggered.connect(self.on_show_logs)