// SPDX-License-Identifier: LGPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of libnymea-app. * * libnymea-app is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * libnymea-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnymea-app. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "applogcontroller.h" #include #include #include #include #include #include #include "logging.h" QtMessageHandler AppLogController::s_oldLogMessageHandler = nullptr; AppLogController::LogLevel AppLogController::qtMsgTypeToLogLevel(QtMsgType msgType) { switch (msgType) { case QtDebugMsg: return LogLevelDebug; case QtInfoMsg: return LogLevelInfo; case QtWarningMsg: return LogLevelWarning; default: return LogLevelCritical; } } QtMsgType AppLogController::logLevelToQtMsgType(AppLogController::LogLevel logLevel) { switch (logLevel) { case LogLevelDebug: return QtDebugMsg; case LogLevelInfo: return QtInfoMsg; case LogLevelWarning: return QtWarningMsg; default: return QtCriticalMsg; } } QObject *AppLogController::appLogControllerProvider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) return instance(); } AppLogController *AppLogController::instance() { static AppLogController* thiz = nullptr; if (!thiz) { thiz = new AppLogController(); } return thiz; } AppLogController::AppLogController(QObject *parent) : QObject(parent) { // qt.qml.connections warnings are disabled since the replace only exists // in Qt 5.12. Remove that once 5.12 is the minimum supported version. QLoggingCategory::setFilterRules("*.debug=false\n" "qt.qml.connections.warning=false\n" ); m_loggingCategories = new LoggingCategories(this); QSettings settings; settings.beginGroup("LoggingLevels"); foreach (const QString &category, nymeaLoggingCategories()) { m_logLevels[category] = static_cast(settings.value(category, LogLevelWarning).toInt()); } // Unless specified otherwise by the user, Application info messages are enabled by default for basic information if (!settings.childKeys().contains("Application")) { m_logLevels["Application"] = LogLevelInfo; } settings.endGroup(); updateFilters(); // Finally, install the logMessageHandler s_oldLogMessageHandler = qInstallMessageHandler(&logMessageHandler); if (enabled()) { openLogFile(); } } bool AppLogController::enabled() const { QSettings settings; return settings.value("AppLoggingEnabled", false).toBool(); } void AppLogController::setEnabled(bool enabled) { if (enabled == this->enabled()) { return; } if (enabled) { openLogFile(); } else { m_logFile.close(); } QSettings settings; settings.setValue("AppLoggingEnabled", enabled); emit enabledChanged(); } LoggingCategories *AppLogController::loggingCategories() const { return m_loggingCategories; } AppLogController::LogLevel AppLogController::logLevel(const QString &category) const { return m_logLevels.value(category); } void AppLogController::setLogLevel(const QString &category, AppLogController::LogLevel logLevel) { m_logLevels[category] = logLevel; QSettings settings; settings.beginGroup("LoggingLevels"); settings.setValue(category, logLevel); settings.endGroup(); emit categoryChanged(category, logLevel); updateFilters(); } QString AppLogController::logPath() const { return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/logs/"; } QString AppLogController::currentLogFile() const { return logPath() + "/" + QGuiApplication::applicationName() + ".log"; } QStringList AppLogController::logFiles() const { QDir dir(logPath()); QStringList files; foreach (const QString &file, dir.entryList({QGuiApplication::applicationName() + ".log*"})) { files.append(logPath() + "/" + file); } return files; } QString AppLogController::exportLogs() { if (m_logFile.isOpen()) { m_logFile.flush(); } QFile f(logPath() + "/" + QGuiApplication::applicationName() + "-logs.txt"); if (!f.open(QFile::WriteOnly)) { return QString(); } foreach (const QString &logFile, logFiles()) { QFile l(logFile); if (!l.open(QFile::ReadOnly)) { continue; } l.seek(0); f.write("\n******** App start ********\n"); f.write(logFile.toUtf8() + "\n"); f.write(l.readAll()); } f.close(); return f.fileName(); } void AppLogController::logMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message) { s_oldLogMessageHandler(type, context, message); QMetaObject::invokeMethod(instance(), "append", Q_ARG(QString, context.category), Q_ARG(QString, message), Q_ARG(AppLogController::LogLevel, qtMsgTypeToLogLevel(type))); } void AppLogController::append(const QString &category, const QString &message, LogLevel level) { if (m_logLevels.value(category) < level) { return; } QDateTime timestamp = QDateTime::currentDateTime(); if (m_logFile.isOpen()) { QHash t = { {LogLevelDebug, "D"}, {LogLevelInfo, "I"}, {LogLevelWarning, "W"}, {LogLevelCritical, "C"} }; QString line = QString("%0:%1:%2: %3\n").arg(timestamp.toString("yyyy-MM-dd hh:mm:ss.zzz"), t.value(level), category, message); m_logFile.write(line.toUtf8()); m_logFile.flush(); } emit messageAdded(timestamp, category, message, level); } void AppLogController::updateFilters() { QStringList loggingRules = {"*.warn=false", "*.info=false", "*.debug=false"}; // Load the rules from nymead.conf file and append them to the rules foreach (const QString &category, nymeaLoggingCategories()) { LogLevel level = m_logLevels.value(category, LogLevelWarning); loggingRules << QString("%1.debug=%2").arg(category).arg(level >= LogLevelDebug ? "true" : "false"); loggingRules << QString("%1.info=%2").arg(category).arg(level >= LogLevelInfo ? "true" : "false"); loggingRules << QString("%1.warn=%2").arg(category).arg(level >= LogLevelWarning ? "true" : "false"); } loggingRules << "qt.qml.connections.warning=false"; QLoggingCategory::setFilterRules(loggingRules.join('\n')); } void AppLogController::openLogFile() { // Make sure log dir exists if (!QDir().mkpath(logPath())) { qWarning() << "Cannot create cache location. Logging will not work."; } // Rotate old log files, keeping the last 5 for (int i = 4; i > 0; i--) { if (QFile::exists(currentLogFile() + "." + QString::number(i))) { if (QFile::exists(currentLogFile() + "." + QString::number(i + 1))) { QFile::remove(currentLogFile() + "." + QString::number(i + 1)); } QFile::rename(currentLogFile() + "." + QString::number(i), currentLogFile() + "." + QString::number(i + 1)); } } if (QFile::exists(currentLogFile())) { QFile::rename(currentLogFile(), currentLogFile() + ".1"); } m_logFile.setFileName(currentLogFile()); if (!m_logFile.open(QFile::ReadWrite | QFile::Truncate)) { qWarning() << "Cannot open logfile for writing."; } else { qDebug() << "App log opened at" << m_logFile.fileName(); } } LogMessages::LogMessages(QObject *parent): QAbstractListModel(parent) { QFile f(AppLogController::instance()->currentLogFile()); if (!f.open(QFile::ReadOnly)) { return; } QHash map = { {"C", AppLogController::LogLevelCritical}, {"W", AppLogController::LogLevelWarning}, {"I", AppLogController::LogLevelInfo}, {"D", AppLogController::LogLevelDebug} }; while (!f.atEnd()) { QByteArray line = f.readLine().trimmed(); QList parts = line.split(':'); if (parts.length() < 6) { continue; } LogMessage message; QString timestampString = parts.takeFirst(); timestampString.append(":" + parts.takeFirst()); timestampString.append(":" + parts.takeFirst()); message.timestamp = QDateTime::fromString(timestampString, "yyyy-MM-dd hh:mm:ss.zzz"); message.level = map.value(parts.takeFirst()); message.category = parts.takeFirst(); message.message = parts.join(":"); m_messages.append(message); } connect(AppLogController::instance(), &AppLogController::messageAdded, this, &LogMessages::append); } int LogMessages::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return static_cast(m_messages.count()); } QVariant LogMessages::data(const QModelIndex &index, int role) const { LogMessage message = m_messages.at(index.row()); switch (role) { case RoleTimestamp: return message.timestamp; case RoleCategory: return message.category; case RoleMessage: return message.message; case RoleLevel: return message.level; case RoleText: return message.timestamp.toString("hh:mm:ss") + ": " + message.category + ": " + message.message; } return QVariant(); } QHash LogMessages::roleNames() const { QHash roles; roles.insert(RoleTimestamp, "timestamp"); roles.insert(RoleCategory, "category"); roles.insert(RoleMessage, "message"); roles.insert(RoleLevel, "level"); roles.insert(RoleText, "text"); return roles; } void LogMessages::append(const QDateTime ×tamp, const QString &category, const QString &message, AppLogController::LogLevel level) { beginInsertRows(QModelIndex(), static_cast(m_messages.count()), static_cast(m_messages.count())); LogMessage msg; msg.timestamp = timestamp; msg.category = category; msg.message = message; msg.level = level; m_messages.append(msg); endInsertRows(); int maxEntries = 1024; if (m_messages.size() > maxEntries) { beginRemoveRows(QModelIndex(), 0, 0); m_messages.removeFirst(); endRemoveRows(); } } LoggingCategories::LoggingCategories(AppLogController *parent): QAbstractListModel(parent), m_controller(parent) { connect(m_controller, &AppLogController::categoryChanged, this, [=](const QString &category) { QModelIndex idx = index(nymeaLoggingCategories().indexOf(category)); emit dataChanged(idx, idx, {RoleLevel}); }); } int LoggingCategories::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return static_cast(nymeaLoggingCategories().count()); } QVariant LoggingCategories::data(const QModelIndex &index, int role) const { switch (role) { case RoleName: return nymeaLoggingCategories().at(index.row()); case RoleLevel: return m_controller->logLevel(nymeaLoggingCategories().at(index.row())); } return QVariant(); } QHash LoggingCategories::roleNames() const { QHash roles; roles.insert(RoleName, "name"); roles.insert(RoleLevel, "logLevel"); return roles; } QVariant LoggingCategories::data(int index, const QString &role) { return data(this->index(index), roleNames().key(role.toUtf8())); }