390 lines
12 KiB
C++
390 lines
12 KiB
C++
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright 2013 - 2020, nymea GmbH
|
|
* Contact: contact@nymea.io
|
|
*
|
|
* This file is part of nymea.
|
|
* This project including source code and documentation is protected by
|
|
* copyright law, and remains the property of nymea GmbH. All rights, including
|
|
* reproduction, publication, editing and translation, are reserved. The use of
|
|
* this project is subject to the terms of a license agreement to be concluded
|
|
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
|
|
* under https://nymea.io/license
|
|
*
|
|
* GNU General Public License Usage
|
|
* Alternatively, this project may be redistributed and/or modified under the
|
|
* terms of the GNU General Public License as published by the Free Software
|
|
* Foundation, GNU version 3. This project 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 General
|
|
* Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this project. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* For any further details and any questions please contact us under
|
|
* contact@nymea.io or see our FAQ/Licensing Information on
|
|
* https://nymea.io/license/faq
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "applogcontroller.h"
|
|
|
|
#include <QStandardPaths>
|
|
#include <QDebug>
|
|
#include <QSettings>
|
|
#include <QGuiApplication>
|
|
#include <QDir>
|
|
#include <QMutexLocker>
|
|
|
|
#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)
|
|
{
|
|
m_loggingCategories = new LoggingCategories(this);
|
|
|
|
QSettings settings;
|
|
settings.beginGroup("LoggingLevels");
|
|
foreach (const QString &category, nymeaLoggingCategories()) {
|
|
m_logLevels[category] = static_cast<LogLevel>(settings.value(category, LogLevelInfo).toInt());
|
|
}
|
|
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()
|
|
{
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
|
|
if (m_logFile.isOpen()) {
|
|
QHash<LogLevel, QString> t = {
|
|
{LogLevelDebug, "D"},
|
|
{LogLevelInfo, "I"},
|
|
{LogLevelWarning, "W"},
|
|
{LogLevelCritical, "C"}
|
|
};
|
|
QString line = QString("%0: %1: %2\n").arg(t.value(level), category, message);
|
|
m_logFile.write(line.toUtf8());
|
|
m_logFile.flush();
|
|
}
|
|
|
|
emit messageAdded(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<QString, AppLogController::LogLevel> map = {
|
|
{"C", AppLogController::LogLevelCritical},
|
|
{"W", AppLogController::LogLevelWarning},
|
|
{"I", AppLogController::LogLevelInfo},
|
|
{"D", AppLogController::LogLevelDebug}
|
|
};
|
|
while (!f.atEnd()) {
|
|
QByteArray line = f.readLine().trimmed();
|
|
QList<QByteArray> parts = line.split(':');
|
|
if (parts.length() < 2) {
|
|
continue;
|
|
}
|
|
LogMessage message;
|
|
message.level = map.value(parts.takeFirst());
|
|
message.category = parts.takeFirst();
|
|
message.message = parts.join(":");
|
|
m_messages.append(message);
|
|
}
|
|
connect(AppLogController::instance(), &AppLogController::messageAdded, this, [=](const QString &category, const QString &message, AppLogController::LogLevel level){
|
|
beginInsertRows(QModelIndex(), m_messages.count(), m_messages.count());
|
|
LogMessage msg;
|
|
msg.category = category;
|
|
msg.message = message;
|
|
msg.level = level;
|
|
m_messages.append(msg);
|
|
endInsertRows();
|
|
});
|
|
}
|
|
|
|
int LogMessages::rowCount(const QModelIndex &parent) const
|
|
{
|
|
Q_UNUSED(parent)
|
|
return m_messages.count();
|
|
}
|
|
|
|
QVariant LogMessages::data(const QModelIndex &index, int role) const
|
|
{
|
|
switch (role) {
|
|
case RoleCategory:
|
|
return m_messages.at(index.row()).category;
|
|
case RoleMessage:
|
|
return m_messages.at(index.row()).message;
|
|
case RoleLevel:
|
|
return m_messages.at(index.row()).level;
|
|
case RoleText:
|
|
return m_messages.at(index.row()).category + ": " + m_messages.at(index.row()).message;
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
QHash<int, QByteArray> LogMessages::roleNames() const
|
|
{
|
|
QHash<int, QByteArray> roles;
|
|
roles.insert(RoleCategory, "category");
|
|
roles.insert(RoleMessage, "message");
|
|
roles.insert(RoleLevel, "level");
|
|
roles.insert(RoleText, "text");
|
|
return roles;
|
|
}
|
|
|
|
void LogMessages::append(const QString &category, const QString &message, AppLogController::LogLevel level)
|
|
{
|
|
beginInsertRows(QModelIndex(), m_messages.count(), m_messages.count());
|
|
LogMessage msg;
|
|
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 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<int, QByteArray> LoggingCategories::roleNames() const
|
|
{
|
|
QHash<int, QByteArray> 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()));
|
|
}
|
|
|