3#include <QtConcurrent/QtConcurrentRun>
4#include <QtCore/QDateTime>
6#include <QtCore/QPointer>
7#include <QtCore/QMutexLocker>
8#include <QtCore/QThread>
12#include <QtSql/QSqlDatabase>
13#include <QtSql/QSqlError>
14#include <QtSql/QSqlQuery>
22static std::atomic<
int> s_instanceCounter{0};
24LogStore::LogStore(QObject* parent)
25 : QObject(parent), _sessionId(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
27 const int id = s_instanceCounter.fetch_add(1);
28 _writeConnName = QStringLiteral(
"LogStore_write_%1").arg(
id);
34 if (_exportFuture.isValid()) {
35 _exportFuture.waitForFinished();
39QString LogStore::databasePath()
const
41 const QMutexLocker locker(&_mutex);
45void LogStore::open(
const QString& dbPath)
47 if (_isOpen.load(std::memory_order_relaxed)) {
52 const QMutexLocker locker(&_mutex);
63 const QMutexLocker locker(&_mutex);
65 _isOpen.store(
false, std::memory_order_relaxed);
71 if (!_isOpen.load(std::memory_order_relaxed)) {
75 bool wakeWorker =
false;
77 const QMutexLocker locker(&_mutex);
78 _pendingWrites.push_back(std::move(entry));
79 wakeWorker = (
static_cast<int>(_pendingWrites.size()) >= kBatchSize);
89 const QMutexLocker locker(&_mutex);
102 const QMutexLocker locker(&store._mutex);
103 if (!store._dbPath.isEmpty()) {
104 _conn.emplace(store._dbPath,
true);
108 [[nodiscard]]
bool isValid()
const {
return _conn && _conn->isValid(); }
109 [[nodiscard]] QSqlDatabase
database()
const {
return _conn->database(); }
112 std::optional<QGCSqlHelper::ScopedConnection> _conn;
119QList<LogEntry> LogStore::query(
const QueryParams& params)
const
121 QList<LogEntry> result;
123 if (!_isOpen.load(std::memory_order_relaxed)) {
127 ScopedReadConnection conn(*
this);
128 if (!conn.isValid()) {
132 QSqlDatabase db = conn.database();
136 sql += QStringLiteral(
137 "SELECT timestamp, level, category, message, file, function, line, formatted "
138 "FROM log_entries WHERE 1=1");
143 if (!params.sessionId.isEmpty()) {
144 sql += QStringLiteral(
" AND session_id = ?");
146 if (params.fromTime.isValid()) {
147 sql += QStringLiteral(
" AND timestamp >= ?");
149 if (params.toTime.isValid()) {
150 sql += QStringLiteral(
" AND timestamp <= ?");
152 if (params.minLevel > LogEntry::Debug) {
153 sql += QStringLiteral(
" AND level >= ?");
155 if (!params.category.isEmpty()) {
156 sql += QStringLiteral(
" AND category LIKE ? ESCAPE '\\'");
158 if (!params.textFilter.isEmpty()) {
159 sql += QStringLiteral(
" AND message LIKE ? ESCAPE '\\'");
162 sql += QStringLiteral(
" ORDER BY id ASC LIMIT ? OFFSET ?");
166 if (!params.sessionId.isEmpty()) {
167 q.addBindValue(params.sessionId);
169 if (params.fromTime.isValid()) {
170 q.addBindValue(params.fromTime.toMSecsSinceEpoch());
172 if (params.toTime.isValid()) {
173 q.addBindValue(params.toTime.toMSecsSinceEpoch());
175 if (params.minLevel > LogEntry::Debug) {
176 q.addBindValue(params.minLevel);
178 if (!params.category.isEmpty()) {
181 if (!params.textFilter.isEmpty()) {
184 q.addBindValue(params.limit);
185 q.addBindValue(params.offset);
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();
198 result.append(std::move(entry));
205QStringList LogStore::sessions()
const
208 if (!_isOpen.load(std::memory_order_relaxed)) {
212 ScopedReadConnection conn(*
this);
213 if (!conn.isValid()) {
217 QSqlQuery q(conn.database());
218 q.exec(QStringLiteral(
"SELECT DISTINCT session_id FROM log_entries ORDER BY id ASC"));
220 result << q.value(0).toString();
226qint64 LogStore::sessionEntryCount(
const QString& sessionId)
const
228 if (!_isOpen.load(std::memory_order_relaxed)) {
232 ScopedReadConnection conn(*
this);
233 if (!conn.isValid()) {
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();
247bool LogStore::deleteSession(
const QString& sessionId)
249 if (!_isOpen.load(std::memory_order_relaxed)) {
255 const QMutexLocker locker(&_mutex);
260 if (!conn.isValid()) {
264 QSqlQuery q(conn.database());
265 q.prepare(QStringLiteral(
"DELETE FROM log_entries WHERE session_id = ?"));
266 q.addBindValue(sessionId);
270void LogStore::exportSession(
const QString& sessionId,
const QString& destFile,
int format)
272 if (!_isOpen.load(std::memory_order_relaxed)) {
277 QPointer<LogStore> guard(
this);
278 _exportFuture = QtConcurrent::run([guard, sessionId, destFile, format]() {
283 LogStore::QueryParams params;
284 params.sessionId = sessionId;
285 params.limit = 1000000;
286 const auto entries = guard->query(params);
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));
298 QMetaObject::invokeMethod(
299 guard.data(), [guard, success]() {
301 emit guard->exportFinished(success);
304 Qt::QueuedConnection);
313void LogStore::_bindAndExec(QSqlQuery& q,
const QString& sessionId,
const LogEntry& entry)
315 q.addBindValue(sessionId);
316 q.addBindValue(entry.
timestamp.toMSecsSinceEpoch());
317 q.addBindValue(
static_cast<int>(entry.
level));
320 q.addBindValue(entry.
file);
322 q.addBindValue(entry.
line);
327void LogStore::_startWorker()
329 const QMutexLocker locker(&_mutex);
334 _quit.store(
false, std::memory_order_relaxed);
335 _thread = QThread::create([
this]() { _workerLoop(); });
336 _thread->setObjectName(QStringLiteral(
"LogStore"));
340void LogStore::_stopWorker()
343 const QMutexLocker locker(&_mutex);
347 _quit.store(
true, std::memory_order_relaxed);
349 _condition.wakeOne();
351 _thread->deleteLater();
353 const QMutexLocker locker(&_mutex);
357void LogStore::_workerLoop()
361 const QMutexLocker locker(&_mutex);
366 QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral(
"QSQLITE"), _writeConnName);
367 db.setDatabaseName(dbPath);
370 QMetaObject::invokeMethod(
371 this, [
this, err = db.lastError().text()]() { emit errorOccurred(err); }, Qt::QueuedConnection);
372 QSqlDatabase::removeDatabase(_writeConnName);
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,"
388 " line INTEGER DEFAULT 0,"
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)"));
398 _isOpen.store(
true, std::memory_order_relaxed);
399 QMetaObject::invokeMethod(
405 Qt::QueuedConnection);
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 (?, ?, ?, ?, ?, ?, ?, ?, ?)"));
412 while (!_quit.load(std::memory_order_relaxed)) {
413 std::vector<LogEntry> batch;
416 QMutexLocker locker(&_mutex);
417 while (_pendingWrites.empty() && !_quit.load(std::memory_order_relaxed)) {
418 _condition.wait(&_mutex, kFlushIntervalMs);
419 if (!_pendingWrites.empty()) {
423 batch.swap(_pendingWrites);
432 for (
const auto& entry : batch) {
433 _bindAndExec(insertQuery, _sessionId, entry);
434 if (insertQuery.lastError().isValid()) {
443 QMetaObject::invokeMethod(
445 [
this, err = insertQuery.lastError().text()]() { emit errorOccurred(err); },
446 Qt::QueuedConnection);
449 _entryCount.fetch_add(
static_cast<qint64
>(batch.size()), std::memory_order_release);
450 QMetaObject::invokeMethod(
this, [
this]() { emit
entryCountChanged(); }, Qt::QueuedConnection);
455 const QMutexLocker locker(&_mutex);
456 if (!_pendingWrites.empty()) {
458 for (
const auto& entry : _pendingWrites) {
459 _bindAndExec(insertQuery, _sessionId, entry);
462 _pendingWrites.clear();
469 QSqlDatabase::removeDatabase(_writeConnName);
470 _isOpen.store(
false, std::memory_order_relaxed);
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QSqlDatabase database() const
ScopedReadConnection(const LogStore &store)
void exportFinished(bool success)
void databasePathChanged()
QString escapeLikePattern(const QString &text)
void applySqlitePragmas(QSqlDatabase &db)
Applies standard QGC pragmas: WAL journal mode + NORMAL synchronous.