QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
LogStore.cc
Go to the documentation of this file.
1#include "LogStore.h"
2
3#include <QtConcurrent/QtConcurrentRun>
4#include <QtCore/QDateTime>
5#include <QtCore/QFile>
6#include <QtCore/QPointer>
7#include <QtCore/QMutexLocker>
8#include <QtCore/QThread>
9#include <QtCore/QUuid>
10
11#include <optional>
12#include <QtSql/QSqlDatabase>
13#include <QtSql/QSqlError>
14#include <QtSql/QSqlQuery>
15
16#include "LogFormatter.h"
17#include "QGCLoggingCategory.h"
18#include "QGCSqlHelper.h"
19
20QGC_LOGGING_CATEGORY(LogStoreLog, "Utilities.LogStore")
21
22static std::atomic<int> s_instanceCounter{0};
23
24LogStore::LogStore(QObject* parent)
25 : QObject(parent), _sessionId(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
26{
27 const int id = s_instanceCounter.fetch_add(1);
28 _writeConnName = QStringLiteral("LogStore_write_%1").arg(id);
29}
30
31LogStore::~LogStore()
32{
33 close();
34 if (_exportFuture.isValid()) {
35 _exportFuture.waitForFinished();
36 }
37}
38
39QString LogStore::databasePath() const
40{
41 const QMutexLocker locker(&_mutex);
42 return _dbPath;
43}
44
45void LogStore::open(const QString& dbPath)
46{
47 if (_isOpen.load(std::memory_order_relaxed)) {
48 close();
49 }
50
51 {
52 const QMutexLocker locker(&_mutex);
53 _dbPath = dbPath;
54 }
55
56 _startWorker();
57}
58
59void LogStore::close()
60{
61 _stopWorker();
62
63 const QMutexLocker locker(&_mutex);
64 _dbPath.clear();
65 _isOpen.store(false, std::memory_order_relaxed);
66 emit isOpenChanged();
67}
68
69void LogStore::append(LogEntry entry)
70{
71 if (!_isOpen.load(std::memory_order_relaxed)) {
72 return;
73 }
74
75 bool wakeWorker = false;
76 {
77 const QMutexLocker locker(&_mutex);
78 _pendingWrites.push_back(std::move(entry));
79 wakeWorker = (static_cast<int>(_pendingWrites.size()) >= kBatchSize);
80 }
81
82 if (wakeWorker) {
83 _condition.wakeOne();
84 }
85}
86
87void LogStore::flush()
88{
89 const QMutexLocker locker(&_mutex);
90 _condition.wakeOne();
91}
92
93// ---------------------------------------------------------------------------
94// Scoped read connection (delegates to QGCSqlHelper::ScopedConnection)
95// ---------------------------------------------------------------------------
96
98{
99public:
100 explicit ScopedReadConnection(const LogStore& store)
101 {
102 const QMutexLocker locker(&store._mutex);
103 if (!store._dbPath.isEmpty()) {
104 _conn.emplace(store._dbPath, true);
105 }
106 }
107
108 [[nodiscard]] bool isValid() const { return _conn && _conn->isValid(); }
109 [[nodiscard]] QSqlDatabase database() const { return _conn->database(); }
110
111private:
112 std::optional<QGCSqlHelper::ScopedConnection> _conn;
113};
114
115// ---------------------------------------------------------------------------
116// Query methods (use persistent read connection, no mutex during SQL)
117// ---------------------------------------------------------------------------
118
119QList<LogEntry> LogStore::query(const QueryParams& params) const
120{
121 QList<LogEntry> result;
122
123 if (!_isOpen.load(std::memory_order_relaxed)) {
124 return result;
125 }
126
127 ScopedReadConnection conn(*this);
128 if (!conn.isValid()) {
129 return result;
130 }
131
132 QSqlDatabase db = conn.database();
133
134 QString sql;
135 sql.reserve(256);
136 sql += QStringLiteral(
137 "SELECT timestamp, level, category, message, file, function, line, formatted "
138 "FROM log_entries WHERE 1=1");
139
140 // Build parameterized WHERE clause; bind values added in order below
141 QSqlQuery q(db);
142
143 if (!params.sessionId.isEmpty()) {
144 sql += QStringLiteral(" AND session_id = ?");
145 }
146 if (params.fromTime.isValid()) {
147 sql += QStringLiteral(" AND timestamp >= ?");
148 }
149 if (params.toTime.isValid()) {
150 sql += QStringLiteral(" AND timestamp <= ?");
151 }
152 if (params.minLevel > LogEntry::Debug) {
153 sql += QStringLiteral(" AND level >= ?");
154 }
155 if (!params.category.isEmpty()) {
156 sql += QStringLiteral(" AND category LIKE ? ESCAPE '\\'");
157 }
158 if (!params.textFilter.isEmpty()) {
159 sql += QStringLiteral(" AND message LIKE ? ESCAPE '\\'");
160 }
161
162 sql += QStringLiteral(" ORDER BY id ASC LIMIT ? OFFSET ?");
163 q.prepare(sql);
164
165 // Bind values in same order as WHERE clauses above
166 if (!params.sessionId.isEmpty()) {
167 q.addBindValue(params.sessionId);
168 }
169 if (params.fromTime.isValid()) {
170 q.addBindValue(params.fromTime.toMSecsSinceEpoch());
171 }
172 if (params.toTime.isValid()) {
173 q.addBindValue(params.toTime.toMSecsSinceEpoch());
174 }
175 if (params.minLevel > LogEntry::Debug) {
176 q.addBindValue(params.minLevel);
177 }
178 if (!params.category.isEmpty()) {
179 q.addBindValue(QLatin1Char('%') + QGCSqlHelper::escapeLikePattern(params.category) + QLatin1Char('%'));
180 }
181 if (!params.textFilter.isEmpty()) {
182 q.addBindValue(QLatin1Char('%') + QGCSqlHelper::escapeLikePattern(params.textFilter) + QLatin1Char('%'));
183 }
184 q.addBindValue(params.limit);
185 q.addBindValue(params.offset);
186
187 if (q.exec()) {
188 while (q.next()) {
189 LogEntry entry;
190 entry.timestamp = QDateTime::fromMSecsSinceEpoch(q.value(0).toLongLong());
191 entry.level = static_cast<LogEntry::Level>(q.value(1).toInt());
192 entry.category = q.value(2).toString();
193 entry.message = q.value(3).toString();
194 entry.file = q.value(4).toString();
195 entry.function = q.value(5).toString();
196 entry.line = q.value(6).toInt();
197 entry.formatted = q.value(7).toString();
198 result.append(std::move(entry));
199 }
200 }
201
202 return result;
203}
204
205QStringList LogStore::sessions() const
206{
207 QStringList result;
208 if (!_isOpen.load(std::memory_order_relaxed)) {
209 return result;
210 }
211
212 ScopedReadConnection conn(*this);
213 if (!conn.isValid()) {
214 return result;
215 }
216
217 QSqlQuery q(conn.database());
218 q.exec(QStringLiteral("SELECT DISTINCT session_id FROM log_entries ORDER BY id ASC"));
219 while (q.next()) {
220 result << q.value(0).toString();
221 }
222
223 return result;
224}
225
226qint64 LogStore::sessionEntryCount(const QString& sessionId) const
227{
228 if (!_isOpen.load(std::memory_order_relaxed)) {
229 return 0;
230 }
231
232 ScopedReadConnection conn(*this);
233 if (!conn.isValid()) {
234 return 0;
235 }
236
237 QSqlQuery q(conn.database());
238 q.prepare(QStringLiteral("SELECT COUNT(*) FROM log_entries WHERE session_id = ?"));
239 q.addBindValue(sessionId);
240 if (q.exec() && q.next()) {
241 return q.value(0).toLongLong();
242 }
243
244 return 0;
245}
246
247bool LogStore::deleteSession(const QString& sessionId)
248{
249 if (!_isOpen.load(std::memory_order_relaxed)) {
250 return false;
251 }
252
253 QString dbPath;
254 {
255 const QMutexLocker locker(&_mutex);
256 dbPath = _dbPath;
257 }
258
260 if (!conn.isValid()) {
261 return false;
262 }
263
264 QSqlQuery q(conn.database());
265 q.prepare(QStringLiteral("DELETE FROM log_entries WHERE session_id = ?"));
266 q.addBindValue(sessionId);
267 return q.exec();
268}
269
270void LogStore::exportSession(const QString& sessionId, const QString& destFile, int format)
271{
272 if (!_isOpen.load(std::memory_order_relaxed)) {
273 emit exportFinished(false);
274 return;
275 }
276
277 QPointer<LogStore> guard(this);
278 _exportFuture = QtConcurrent::run([guard, sessionId, destFile, format]() {
279 if (!guard) {
280 return;
281 }
282
283 LogStore::QueryParams params;
284 params.sessionId = sessionId;
285 params.limit = 1000000;
286 const auto entries = guard->query(params);
287
288 bool success = false;
289 if (!entries.isEmpty()) {
290 QFile file(destFile);
291 if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
292 file.write(LogFormatter::format(entries, format));
293 success = true;
294 }
295 }
296
297 if (guard) {
298 QMetaObject::invokeMethod(
299 guard.data(), [guard, success]() {
300 if (guard) {
301 emit guard->exportFinished(success);
302 }
303 },
304 Qt::QueuedConnection);
305 }
306 });
307}
308
309// ---------------------------------------------------------------------------
310// Worker thread
311// ---------------------------------------------------------------------------
312
313void LogStore::_bindAndExec(QSqlQuery& q, const QString& sessionId, const LogEntry& entry)
314{
315 q.addBindValue(sessionId);
316 q.addBindValue(entry.timestamp.toMSecsSinceEpoch());
317 q.addBindValue(static_cast<int>(entry.level));
318 q.addBindValue(entry.category);
319 q.addBindValue(entry.message);
320 q.addBindValue(entry.file);
321 q.addBindValue(entry.function);
322 q.addBindValue(entry.line);
323 q.addBindValue(entry.formatted);
324 q.exec();
325}
326
327void LogStore::_startWorker()
328{
329 const QMutexLocker locker(&_mutex);
330 if (_thread) {
331 return;
332 }
333
334 _quit.store(false, std::memory_order_relaxed);
335 _thread = QThread::create([this]() { _workerLoop(); });
336 _thread->setObjectName(QStringLiteral("LogStore"));
337 _thread->start();
338}
339
340void LogStore::_stopWorker()
341{
342 {
343 const QMutexLocker locker(&_mutex);
344 if (!_thread) {
345 return;
346 }
347 _quit.store(true, std::memory_order_relaxed);
348 }
349 _condition.wakeOne();
350 _thread->wait();
351 _thread->deleteLater();
352
353 const QMutexLocker locker(&_mutex);
354 _thread = nullptr;
355}
356
357void LogStore::_workerLoop()
358{
359 QString dbPath;
360 {
361 const QMutexLocker locker(&_mutex);
362 dbPath = _dbPath;
363 }
364
365 {
366 QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), _writeConnName);
367 db.setDatabaseName(dbPath);
368
369 if (!db.open()) {
370 QMetaObject::invokeMethod(
371 this, [this, err = db.lastError().text()]() { emit errorOccurred(err); }, Qt::QueuedConnection);
372 QSqlDatabase::removeDatabase(_writeConnName);
373 return;
374 }
375
376 {
377 QSqlQuery q(db);
378 q.exec(
379 QStringLiteral("CREATE TABLE IF NOT EXISTS log_entries ("
380 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
381 " session_id TEXT NOT NULL,"
382 " timestamp INTEGER NOT NULL,"
383 " level INTEGER NOT NULL,"
384 " category TEXT,"
385 " message TEXT,"
386 " file TEXT,"
387 " function TEXT,"
388 " line INTEGER DEFAULT 0,"
389 " formatted TEXT"
390 ")"));
391 q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_session ON log_entries(session_id)"));
392 q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_timestamp ON log_entries(timestamp)"));
393 q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_level ON log_entries(level)"));
394 q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_category ON log_entries(category)"));
395 }
397
398 _isOpen.store(true, std::memory_order_relaxed);
399 QMetaObject::invokeMethod(
400 this,
401 [this]() {
402 emit isOpenChanged();
403 emit databasePathChanged();
404 },
405 Qt::QueuedConnection);
406
407 QSqlQuery insertQuery(db);
408 insertQuery.prepare(QStringLiteral(
409 "INSERT INTO log_entries (session_id, timestamp, level, category, message, file, function, line, formatted) "
410 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"));
411
412 while (!_quit.load(std::memory_order_relaxed)) {
413 std::vector<LogEntry> batch;
414
415 {
416 QMutexLocker locker(&_mutex);
417 while (_pendingWrites.empty() && !_quit.load(std::memory_order_relaxed)) {
418 _condition.wait(&_mutex, kFlushIntervalMs);
419 if (!_pendingWrites.empty()) {
420 break;
421 }
422 }
423 batch.swap(_pendingWrites);
424 }
425
426 if (batch.empty()) {
427 continue;
428 }
429
430 db.transaction();
431 bool batchOk = true;
432 for (const auto& entry : batch) {
433 _bindAndExec(insertQuery, _sessionId, entry);
434 if (insertQuery.lastError().isValid()) {
435 batchOk = false;
436 break;
437 }
438 }
439 if (batchOk) {
440 db.commit();
441 } else {
442 db.rollback();
443 QMetaObject::invokeMethod(
444 this,
445 [this, err = insertQuery.lastError().text()]() { emit errorOccurred(err); },
446 Qt::QueuedConnection);
447 }
448
449 _entryCount.fetch_add(static_cast<qint64>(batch.size()), std::memory_order_release);
450 QMetaObject::invokeMethod(this, [this]() { emit entryCountChanged(); }, Qt::QueuedConnection);
451 }
452
453 // Drain remaining
454 {
455 const QMutexLocker locker(&_mutex);
456 if (!_pendingWrites.empty()) {
457 db.transaction();
458 for (const auto& entry : _pendingWrites) {
459 _bindAndExec(insertQuery, _sessionId, entry);
460 }
461 db.commit();
462 _pendingWrites.clear();
463 }
464 }
465
466 db.close();
467 }
468 // All QSqlQuery objects are destroyed — safe to remove the connection.
469 QSqlDatabase::removeDatabase(_writeConnName);
470 _isOpen.store(false, std::memory_order_relaxed);
471}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QSqlDatabase database() const
Definition LogStore.cc:109
ScopedReadConnection(const LogStore &store)
Definition LogStore.cc:100
void entryCountChanged()
void exportFinished(bool success)
void databasePathChanged()
void isOpenChanged()
QString escapeLikePattern(const QString &text)
void applySqlitePragmas(QSqlDatabase &db)
Applies standard QGC pragmas: WAL journal mode + NORMAL synchronous.
QString file
Definition LogEntry.h:41
QString function
Definition LogEntry.h:42
int line
Definition LogEntry.h:45
QString message
Definition LogEntry.h:40
QDateTime timestamp
Definition LogEntry.h:37
QString formatted
Definition LogEntry.h:43
Level level
Definition LogEntry.h:38
QString category
Definition LogEntry.h:39