/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 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 . * * 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 "nymeasettings.h" #include "logengine.h" #include "loggingcategories.h" #include "logging.h" #include "logvaluetool.h" #include "integrations/thingmanager.h" #include #include #include #include #include #include #include #include #include #include #include #define DB_SCHEMA_VERSION 4 namespace nymeaserver { // IMPORTANT: // DatabaseJobs run threaded, however, QSql is *not* threadsafe. // It is crucial to *not* access m_db while the job queue is being processed. // That is, entire setup of the DB must happen before processQueue() is called // and teardown must happen only after the job queue is empty. LogEngine::LogEngine(const QString &driver, const QString &dbName, const QString &hostname, const QString &username, const QString &password, int maxDBSize, QObject *parent): QObject(parent), m_username(username), m_password(password), m_dbMaxSize(maxDBSize) { m_db = QSqlDatabase::addDatabase(driver, "logs"); m_db.setDatabaseName(dbName); m_db.setHostName(hostname); m_trimSize = qRound(0.01 * m_dbMaxSize); m_maxQueueLength = 1000; qCDebug(dcLogEngine) << "Opening logging database" << m_db.databaseName() << "(Max size:" << m_dbMaxSize << "trim size:" << m_trimSize << ")"; if (!m_db.isValid()) { qCWarning(dcLogEngine) << "Database not valid:" << m_db.lastError().driverText() << m_db.lastError().databaseText(); rotate(m_db.databaseName()); } if (!initDB(username, password)) { qCWarning(dcLogEngine()) << "Error initializing database. Trying to correct it."; if (QFileInfo(m_db.databaseName()).exists()) { rotate(m_db.databaseName()); if (!initDB(username, password)) { qCWarning(dcLogEngine()) << "Error fixing log database. Giving up. Logs can't be stored."; return; } } } connect(&m_jobWatcher, SIGNAL(finished()), this, SLOT(handleJobFinished())); checkDBSize(); } LogEngine::~LogEngine() { // Process the job queue before allowing to shut down while (m_currentJob) { qCDebug(dcLogEngine()) << "Waiting for job to finish... (" << m_jobQueue.count() << "jobs left in queue)"; m_jobWatcher.waitForFinished(); // Make sure that the job queue is processes // We can't call processQueue ourselves because thread synchronisation is done via queued connections qApp->processEvents(); } qCDebug(dcLogEngine()) << "Closing Database"; m_db.close(); } void LogEngine::setThingManager(ThingManager *thingManager) { m_thingManager = thingManager; connect(thingManager, &ThingManager::eventTriggered, this, &LogEngine::logEvent); connect(thingManager, &ThingManager::actionExecuted, this, &LogEngine::logAction); } LogEntriesFetchJob *LogEngine::fetchLogEntries(const LogFilter &filter) { QList results; QString limitString; if (filter.limit() >= 0) { limitString.append(QString("LIMIT %1 ").arg(filter.limit())); } if (filter.offset() > 0) { limitString.append(QString("OFFSET %1").arg(QString::number(filter.offset()))); } QString queryString; if (filter.isEmpty()) { queryString = QString("SELECT * FROM entries ORDER BY timestamp DESC %1;").arg(limitString); } else { queryString = QString("SELECT * FROM entries WHERE %1 ORDER BY timestamp DESC %2;").arg(filter.queryString()).arg(limitString); } DatabaseJob *job = new DatabaseJob(m_db, queryString, filter.values()); LogEntriesFetchJob *fetchJob = new LogEntriesFetchJob(this); connect(job, &DatabaseJob::finished, this, [job, fetchJob](){ fetchJob->deleteLater(); if (job->error().isValid()) { qCWarning(dcLogEngine) << "Error fetching log entries. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); fetchJob->finished(); return; } foreach (const QSqlRecord &result, job->results()) { LogEntry entry( QDateTime::fromMSecsSinceEpoch(result.value("timestamp").toLongLong()), static_cast(result.value("loggingLevel").toInt()), static_cast(result.value("sourceType").toInt()), result.value("errorCode").toInt()); entry.setTypeId(result.value("typeId").toUuid()); entry.setThingId(ThingId(result.value("thingId").toString())); entry.setValue(result.value("value").toString()); entry.setEventType(static_cast(result.value("loggingEventType").toInt())); entry.setActive(result.value("active").toBool()); fetchJob->m_results.append(entry); } qCDebug(dcLogEngine) << "Fetched" << fetchJob->results().count() << "entries for db query:" << job->executedQuery(); fetchJob->finished(); }); enqueJob(job, true); return fetchJob; } ThingsFetchJob *LogEngine::fetchThings() { QString queryString = QString("SELECT thingId FROM entries WHERE thingId != \"%1\" GROUP BY thingId;").arg(QUuid().toString()); DatabaseJob *job = new DatabaseJob(m_db, queryString); ThingsFetchJob *fetchJob = new ThingsFetchJob(this); connect(job, &DatabaseJob::finished, this, [job, fetchJob](){ fetchJob->deleteLater(); if (job->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine()) << "Error fetching device entries from log database:" << job->error().driverText() << job->error().databaseText(); fetchJob->finished(); return; } foreach (const QSqlRecord &result, job->results()) { fetchJob->m_results.append(ThingId(result.value("thingId").toUuid())); } fetchJob->finished(); }); enqueJob(job, true); return fetchJob; } bool LogEngine::jobsRunning() const { return !m_jobQueue.isEmpty() || m_currentJob; } void LogEngine::setMaxLogEntries(int maxLogEntries, int trimSize) { m_dbMaxSize = maxLogEntries; m_trimSize = trimSize; trim(); } void LogEngine::clearDatabase() { qCWarning(dcLogEngine) << "Clearing logging database."; QString queryDeleteString = QString("DELETE FROM entries;"); DatabaseJob *job = new DatabaseJob(m_db, queryDeleteString); connect(job, &DatabaseJob::finished, this, [this, job](){ if (job->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error clearing log database. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); return; } m_entryCount = 0; emit logDatabaseUpdated(); }); enqueJob(job); } void LogEngine::logSystemEvent(const QDateTime &dateTime, bool active, Logging::LoggingLevel level) { qCDebug(dcLogEngine()) << "Logging system event:" << active; LogEntry entry(dateTime, level, Logging::LoggingSourceSystem); entry.setEventType(Logging::LoggingEventTypeActiveChange); entry.setActive(active); appendLogEntry(entry); } void LogEngine::logEvent(const Event &event) { if (!event.logged()) { return; } QVariantList valueList; Logging::LoggingSource sourceType; if (event.isStateChangeEvent()) { sourceType = Logging::LoggingSourceStates; // There should only be one param if (!event.params().isEmpty()) valueList << event.params().first().value(); } else { sourceType = Logging::LoggingSourceEvents; foreach (const Param ¶m, event.params()) { valueList << param.value(); } } LogEntry entry(sourceType); entry.setTypeId(event.eventTypeId()); entry.setThingId(event.thingId()); if (valueList.count() == 1) { entry.setValue(valueList.first()); } else { entry.setValue(valueList); } appendLogEntry(entry); } void LogEngine::logAction(const Action &action, Thing::ThingError status) { Logging::LoggingLevel level = status == Thing::ThingErrorNoError ? Logging::LoggingLevelInfo : Logging::LoggingLevelAlert; LogEntry entry(QDateTime::currentDateTime(), level, Logging::LoggingSourceActions, status); entry.setTypeId(action.actionTypeId()); entry.setThingId(action.thingId()); if (action.params().isEmpty()) { entry.setValue(QVariant()); } else if (action.params().count() == 1) { entry.setValue(action.params().first().value()); } else { QVariantList valueList; foreach (const Param ¶m, action.params()) { valueList << param.value(); } entry.setValue(valueList); } appendLogEntry(entry); } void LogEngine::logBrowserAction(const BrowserAction &browserAction, Logging::LoggingLevel level, int errorCode) { LogEntry entry(level, Logging::LoggingSourceBrowserActions, errorCode); entry.setThingId(browserAction.thingId()); entry.setValue(browserAction.itemId()); appendLogEntry(entry); } void LogEngine::logBrowserItemAction(const BrowserItemAction &browserItemAction, Logging::LoggingLevel level, int errorCode) { LogEntry entry(level, Logging::LoggingSourceBrowserActions, errorCode); entry.setThingId(browserItemAction.thingId()); entry.setTypeId(browserItemAction.actionTypeId()); entry.setValue(browserItemAction.itemId()); appendLogEntry(entry); } void LogEngine::logRuleTriggered(const Rule &rule) { LogEntry entry(Logging::LoggingSourceRules); entry.setTypeId(rule.id()); entry.setEventType(Logging::LoggingEventTypeTrigger); appendLogEntry(entry); } void LogEngine::logRuleActiveChanged(const Rule &rule) { LogEntry entry(Logging::LoggingSourceRules); entry.setTypeId(rule.id()); entry.setActive(rule.active()); entry.setEventType(Logging::LoggingEventTypeActiveChange); appendLogEntry(entry); } void LogEngine::logRuleEnabledChanged(const Rule &rule, const bool &enabled) { LogEntry entry(Logging::LoggingSourceRules); entry.setTypeId(rule.id()); entry.setEventType(Logging::LoggingEventTypeEnabledChange); entry.setActive(enabled); appendLogEntry(entry); } void LogEngine::logRuleActionsExecuted(const Rule &rule) { LogEntry entry(Logging::LoggingSourceRules); entry.setTypeId(rule.id()); entry.setEventType(Logging::LoggingEventTypeActionsExecuted); appendLogEntry(entry); } void LogEngine::logRuleExitActionsExecuted(const Rule &rule) { LogEntry entry(Logging::LoggingSourceRules); entry.setTypeId(rule.id()); entry.setEventType(Logging::LoggingEventTypeExitActionsExecuted); appendLogEntry(entry); } void LogEngine::removeThingLogs(const ThingId &thingId) { qCDebug(dcLogEngine) << "Deleting log entries from device" << thingId.toString(); QString queryDeleteString = QString("DELETE FROM entries WHERE thingId = '%1';").arg(thingId.toString()); DatabaseJob *job = new DatabaseJob(m_db, queryDeleteString); connect(job, &DatabaseJob::finished, this, [this, job, thingId](){ if (job->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error deleting log entries from device" << thingId.toString() << ". Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); return; } emit logDatabaseUpdated(); checkDBSize(); }); enqueJob(job); } void LogEngine::removeRuleLogs(const RuleId &ruleId) { qCDebug(dcLogEngine) << "Deleting log entries from rule" << ruleId.toString(); QString queryDeleteString = QString("DELETE FROM entries WHERE typeId = '%1';").arg(ruleId.toString()); DatabaseJob *job = new DatabaseJob(m_db, queryDeleteString); connect(job, &DatabaseJob::finished, this, [this, job, ruleId](){ if (job->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error deleting log entries from rule" << ruleId.toString() << ". Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); return; } emit logDatabaseUpdated(); checkDBSize(); }); enqueJob(job); } void LogEngine::appendLogEntry(const LogEntry &entry) { qCDebug(dcLogEngine()) << "Adding log entry:" << entry; QString queryString = QString("INSERT INTO entries (timestamp, loggingEventType, loggingLevel, sourceType, typeId, thingId, value, active, errorCode) values (?, ?, ?, ?, ?, ?, ?, ?, ?);"); QVariantList bindValues; bindValues.append(entry.timestamp().toMSecsSinceEpoch()); bindValues.append(entry.eventType()); bindValues.append(entry.level()); bindValues.append(entry.source()); bindValues.append(entry.typeId().toString()); bindValues.append(entry.thingId().toString()); bindValues.append(LogValueTool::convertVariantToString(entry.value())); bindValues.append(entry.active()); bindValues.append(entry.errorCode()); DatabaseJob *job = new DatabaseJob(m_db, queryString, bindValues); // Check for log flooding. If we are exceeding the queue we'll start flagging log events of a certain type. // If we'll get more log events of the same type while the queue is still exceededd, we'll discard the old // ones and queue up the new one instead. The most recent one is more important (i.e. we don't want to lose // the last event in a series). if (m_jobQueue.count() > m_maxQueueLength) { qCDebug(dcLogEngine()) << "An excessive amount of data is being logged. (" << m_jobQueue.length() << "jobs in the queue)"; if (m_flaggedJobs.contains(entry.typeId().toString() + entry.thingId().toString())) { if (m_flaggedJobs.value(entry.typeId().toString() + entry.thingId().toString()).count() > 10) { qCWarning(dcLogEngine()) << "Discarding log entry because of excessive log flooding."; DatabaseJob *job = m_flaggedJobs[entry.typeId().toString() + entry.thingId().toString()].takeFirst(); int jobIdx = m_jobQueue.indexOf(job); m_jobQueue.takeAt(jobIdx)->deleteLater(); } } m_flaggedJobs[entry.typeId().toString() + entry.thingId().toString()].append(job); } connect(job, &DatabaseJob::finished, this, [this, job, entry](){ m_flaggedJobs[entry.typeId().toString() + entry.thingId().toString()].removeAll(job); if (job->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error writing log entry. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); qCWarning(dcLogEngine) << entry; m_dbMalformed = true; return; } emit logEntryAdded(entry); m_entryCount++; trim(); }); enqueJob(job); } void LogEngine::checkDBSize() { DatabaseJob *job = new DatabaseJob(m_db, "SELECT COUNT(*) FROM entries;"); connect(job, &DatabaseJob::finished, this, [this, job](){ if (job->error().type() != QSqlError::NoError || job->results().count() == 0) { qCWarning(dcLogEngine()) << "Error fetching log DB size. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); m_entryCount = 0; return; } m_entryCount = job->results().first().value(0).toInt(); }); enqueJob(job, true); } void LogEngine::trim() { if (m_dbMaxSize == -1 || m_entryCount < m_dbMaxSize) { // No trimming required return; } QDateTime startTime = QDateTime::currentDateTime(); QString queryDeleteString = QString("DELETE FROM entries WHERE ROWID IN (SELECT ROWID FROM entries ORDER BY timestamp DESC LIMIT -1 OFFSET %1);").arg(QString::number(m_dbMaxSize - m_trimSize)); DatabaseJob *deleteJob = new DatabaseJob(m_db, queryDeleteString); connect(deleteJob, &DatabaseJob::finished, this, [this, deleteJob, startTime](){ if (deleteJob->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error deleting oldest log entries to keep size. Driver error:" << deleteJob->error().driverText() << "Database error:" << deleteJob->error().databaseText(); } qCDebug(dcLogEngine()) << "Ran housekeeping on log database in" << startTime.msecsTo(QDateTime::currentDateTime()) << "ms. (Deleted" << m_entryCount - (m_dbMaxSize - m_trimSize) << "entries)"; m_entryCount = m_dbMaxSize - m_trimSize; emit logDatabaseUpdated(); }); qCDebug(dcLogEngine()) << "Scheduling housekeeping job."; enqueJob(deleteJob, true); } void LogEngine::enqueJob(DatabaseJob *job, bool priority) { if (priority) { m_jobQueue.prepend(job); } else { m_jobQueue.append(job); } qCDebug(dcLogEngine()) << "Scheduled job at position" << (priority ? 0 : m_jobQueue.count() - 1) << "(" << m_jobQueue.count() << "jobs in the queue)"; processQueue(); } void LogEngine::processQueue() { if (!m_initialized) { return; } if (m_jobQueue.isEmpty()) { emit jobsRunningChanged(); return; } if (m_currentJob) { return; } emit jobsRunningChanged(); if (m_dbMalformed) { qCWarning(dcLogEngine()) << "Database is malformed. Trying to recover..."; m_db.close(); rotate(m_db.databaseName()); initDB(m_username, m_password); m_dbMalformed = false; } DatabaseJob *job = m_jobQueue.takeFirst(); qCDebug(dcLogEngine()) << "Processing DB queue. (" << m_jobQueue.count() << "jobs left in queue," << m_entryCount << "entries in DB)"; m_currentJob = job; QFuture future = QtConcurrent::run([job](){ QSqlQuery query(job->m_db); query.prepare(job->m_queryString); foreach (const QVariant &value, job->m_bindValues) { query.addBindValue(value); } query.exec(); job->m_error = query.lastError(); job->m_executedQuery = query.executedQuery(); if (!query.lastError().isValid()) { while (query.next()) { job->m_results.append(query.record()); } } return job; }); m_jobWatcher.setFuture(future); } void LogEngine::handleJobFinished() { DatabaseJob *job = m_jobWatcher.result(); job->finished(); job->deleteLater(); m_currentJob = nullptr; qCDebug(dcLogEngine()) << "DB job finished. (" << m_entryCount << "entries in DB)"; processQueue(); } void LogEngine::rotate(const QString &dbName) { int index = 1; while (QFileInfo(QString("%1.%2").arg(dbName).arg(index)).exists()) { index++; } qCDebug(dcLogEngine()) << "Backing up old database file to" << QString("%1.%2").arg(dbName).arg(index); QFile f(dbName); if (!f.rename(QString("%1.%2").arg(dbName).arg(index))) { qCWarning(dcLogEngine()) << "Error backing up old database."; } else { qCDebug(dcLogEngine()) << "Successfully moved old database"; } } bool LogEngine::migrateDatabaseVersion3to4() { QSqlQuery renameQuery = m_db.exec("ALTER TABLE entries RENAME TO _entries_v3;"); if (m_db.lastError().isValid()) { qCWarning(dcLogEngine) << "Error migrating database verion 3 -> 4 (renaming table). Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); return false; } qCDebug(dcLogEngine()) << "Renamed entries table to entries_v3:" << m_db.lastError().text(); m_db.close(); m_db.open(m_username, m_password); QSqlQuery createQuery = m_db.exec("CREATE TABLE entries " "(" "timestamp BIGINT," "loggingLevel INT," "sourceType INT," "typeId VARCHAR(38)," "thingId VARCHAR(38)," "value VARCHAR(100)," "loggingEventType INT," "active BOOL," "errorCode INT," "FOREIGN KEY(sourceType) REFERENCES sourceTypes(id)," "FOREIGN KEY(loggingEventType) REFERENCES loggingEventTypes(id)" ");"); if (m_db.lastError().isValid()) { qCWarning(dcLogEngine) << "Error migrating database verion 3 -> 4 (creating new table). Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); return false; } qCDebug(dcLogEngine()) << "Created new entries table:" << m_db.lastError().text(); qCDebug(dcLogEngine()) << "Updating database version to" << DB_SCHEMA_VERSION; m_db.exec(QString("UPDATE metadata SET data = %1 WHERE `key` = 'version';").arg(DB_SCHEMA_VERSION)); if (m_db.lastError().isValid()) { qCWarning(dcLogEngine) << "Error updating database verion 3 -> 4. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); return false; } qCDebug(dcLogEngine()) << "Migrated database schema from version 3 to 4."; return true; } void LogEngine::migrateEntries3to4() { QString selectQuery = QString("SELECT * FROM _entries_v3;"); DatabaseJob *job = new DatabaseJob(m_db, selectQuery); connect(job, &DatabaseJob::finished, this, [this, job](){ if (job->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error fetching entries to migrate. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); m_dbMalformed = true; return; } if (job->results().isEmpty()) { qCDebug(dcLogEngine()) << "No items to migrate from schema 3 to 4 remaining."; finalizeMigration3To4(); return; } int count = job->results().count(); QSqlRecord result = job->results().first(); QString encodedValue = result.value("value").toByteArray(); QString decodedValue = LogValueTool::convertVariantToString(LogValueTool::deserializeValue(encodedValue)); QString insertCall = QString("INSERT INTO entries (timestamp, loggingEventType, loggingLevel, sourceType, typeId, thingId, value, active, errorCode) values ('%1', '%2', '%3', '%4', '%5', '%6', '%7', '%8', '%9');") .arg(result.value("timestamp").toLongLong() * 1000) .arg(result.value("loggingEventType").toInt()) .arg(result.value("loggingLevel").toInt()) .arg(result.value("sourceType").toInt()) .arg(result.value("typeId").toString()) .arg(result.value("deviceId").toString()) .arg(decodedValue) .arg(result.value("active").toBool()) .arg(result.value("errorCode").toInt()); DatabaseJob *insertJob = new DatabaseJob(m_db, insertCall); connect(insertJob, &DatabaseJob::finished, this, [this, insertJob, count, result](){ if (insertJob->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error fetching entries to migrate. Driver error:" << insertJob->error().driverText() << "Database error:" << insertJob->error().databaseText(); m_dbMalformed = true; return; } QString deleteCall = QString("DELETE FROM _entries_v3 WHERE timestamp = '%1' AND loggingEventType = '%2' AND loggingLevel = '%3' AND sourceType = '%4' AND typeId = '%5' AND deviceId = '%6' AND value = '%7' AND active = '%8' AND errorCode = '%9';") .arg(result.value("timestamp").toLongLong()) .arg(result.value("loggingEventType").toInt()) .arg(result.value("loggingLevel").toInt()) .arg(result.value("sourceType").toInt()) .arg(result.value("typeId").toString()) .arg(result.value("deviceId").toString()) .arg(result.value("value").toString()) .arg(result.value("active").toBool()) .arg(result.value("errorCode").toInt()); DatabaseJob *deleteJob = new DatabaseJob(m_db, deleteCall); connect(deleteJob, &DatabaseJob::finished, this, [this, deleteJob, count, result](){ if (deleteJob->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error deleting old entry during migration. Driver error:" << deleteJob->error().driverText() << "Database error:" << deleteJob->error().databaseText(); finalizeMigration3To4(); return; } qCDebug(dcLogEngine()) << "Migrated log entry from version 3 to 4." << (count - 1) << "items left to migrate"; if (count - 1 > 0) { migrateEntries3to4(); } else { finalizeMigration3To4(); } }); enqueJob(deleteJob); }); enqueJob(insertJob); }); enqueJob(job); } void LogEngine::finalizeMigration3To4() { qCDebug(dcLogEngine()) << "Finalizing migration of database version 3 to 4."; QString selectQuery = QString("DROP TABLE _entries_v3;"); DatabaseJob *job = new DatabaseJob(m_db, selectQuery); enqueJob(job); connect(job, &DatabaseJob::finished, this, [job](){ if (job->error().type() != QSqlError::NoError) { qCWarning(dcLogEngine) << "Error finalizing migration from 3 to 4 (drop entries_v3). Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); return; } }); } bool LogEngine::initDB(const QString &username, const QString &password) { m_db.close(); bool opened = m_db.open(username, password); if (!opened) { qCWarning(dcLogEngine()) << "Can't open Log DB. Init failed."; return false; } if (!m_db.tables().contains("metadata")) { qCDebug(dcLogEngine()) << "Empty Database. Setting up metadata..."; m_db.exec("CREATE TABLE metadata (`key` VARCHAR(10), data VARCHAR(40));"); if (m_db.lastError().isValid()) { qCWarning(dcLogEngine) << "Error initualizing database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); return false; } m_db.exec(QString("INSERT INTO metadata (`key`, data) VALUES('version', '%1');").arg(DB_SCHEMA_VERSION)); } QSqlQuery query = m_db.exec("SELECT data FROM metadata WHERE `key` = 'version';"); if (query.next()) { int version = query.value("data").toInt(); // Migration from 3 -> 4 if (version == 3) { if (!migrateDatabaseVersion3to4()) { qCWarning(dcLogEngine()) << "Migration process failed."; return false; } else { // Successfully migrated version = 4; } } if (version != DB_SCHEMA_VERSION) { qCWarning(dcLogEngine) << "Log schema version not matching! Schema upgrade not implemented for this version change."; return false; } else { qCDebug(dcLogEngine) << QString("Log database schema version \"%1\" matches").arg(DB_SCHEMA_VERSION).toLatin1().data(); // If there is still a deviceId column, schedule items to be migrated in the // background with low priority as this might take hours if (m_db.tables().contains("_entries_v3")) { migrateEntries3to4(); } } } else { qCWarning(dcLogEngine) << "Broken log database. Version not found in metadata table."; return false; } if (!m_db.tables().contains("sourceTypes")) { m_db.exec("CREATE TABLE sourceTypes (id int, name varchar(20), PRIMARY KEY(id));"); //qCDebug(dcLogEngine) << m_db.lastError().databaseText(); QMetaEnum logTypes = Logging::staticMetaObject.enumerator(Logging::staticMetaObject.indexOfEnumerator("LoggingSource")); Q_ASSERT_X(logTypes.isValid(), "LogEngine", "Logging has no enum LoggingSource"); for (int i = 0; i < logTypes.keyCount(); i++) { m_db.exec(QString("INSERT INTO sourceTypes (id, name) VALUES(%1, '%2');").arg(i).arg(logTypes.key(i))); } } if (!m_db.tables().contains("loggingEventTypes")) { m_db.exec("CREATE TABLE loggingEventTypes (id int, name varchar(40), PRIMARY KEY(id));"); //qCDebug(dcLogEngine) << m_db.lastError().databaseText(); QMetaEnum logTypes = Logging::staticMetaObject.enumerator(Logging::staticMetaObject.indexOfEnumerator("LoggingEventType")); Q_ASSERT_X(logTypes.isValid(), "LogEngine", "Logging has no enum LoggingEventType"); for (int i = 0; i < logTypes.keyCount(); i++) { m_db.exec(QString("INSERT INTO loggingEventTypes (id, name) VALUES(%1, '%2');").arg(i).arg(logTypes.key(i))); if (m_db.lastError().isValid()) { qCWarning(dcLogEngine()) << "Failed to insert loggingEventTypes into DB. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); } } } if (!m_db.tables().contains("entries")) { qCDebug(dcLogEngine()) << "No \"entries\" table in database. Creating it."; m_db.exec("CREATE TABLE entries " "(" "timestamp BIGINT," "loggingLevel INT," "sourceType INT," "typeId VARCHAR(38)," "thingId VARCHAR(38)," "value VARCHAR(100)," "loggingEventType INT," "active BOOL," "errorCode INT," "FOREIGN KEY(sourceType) REFERENCES sourceTypes(id)," "FOREIGN KEY(loggingEventType) REFERENCES loggingEventTypes(id)" ");"); if (m_db.lastError().isValid()) { qCWarning(dcLogEngine) << "Error creating log table in database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); return false; } } qCDebug(dcLogEngine) << "Initialized logging DB successfully. (maximum DB size:" << m_dbMaxSize << ")"; m_initialized = true; return true; } }