QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
LogManager.cc
Go to the documentation of this file.
1#include "LogManager.h"
2
3#include <QtConcurrent/QtConcurrentRun>
4#include <QtCore/QCoreApplication>
5#include <QtCore/QElapsedTimer>
6#include <QtCore/QMutex>
7#include <QtCore/QMutexLocker>
8#include <QtCore/QPointer>
9#include <QtCore/QSaveFile>
10#include <QtCore/QThread>
11#include <QtQml/QJSEngine>
12#include <atomic>
13#include <cstring>
14
15#include "LogFormatter.h"
16#include "LogModel.h"
17#include "QGCFileWriter.h"
18#include "QGCLoggingCategory.h"
19#include "AppSettings.h"
20#include "LogManagerSettings.h"
21#include "SettingsManager.h"
22
23QGC_LOGGING_CATEGORY(LogManagerLog, "Utilities.LogManager")
24
25static std::atomic<LogManager*> s_instance{nullptr};
26
27// ---------------------------------------------------------------------------
28// Test capture storage
29// ---------------------------------------------------------------------------
30
31static std::atomic<bool> s_captureEnabled{false};
32static QMutex s_captureMutex;
33static QList<LogEntry> s_capturedMessages;
34
35// Elapsed timer started at static-init time (matches Qt's %{time process} epoch).
36static QElapsedTimer s_elapsedTimer = []() { QElapsedTimer t; t.start(); return t; }();
37
38// ---------------------------------------------------------------------------
39// Qt message handler
40// ---------------------------------------------------------------------------
41
42static QtMessageHandler s_defaultHandler = nullptr;
43
44void LogManager::msgHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg)
45{
46 auto* inst = s_instance.load(std::memory_order_acquire);
47
48 if (s_defaultHandler) {
49 s_defaultHandler(type, context, msg);
50 }
51
52 if (!inst) {
53 return;
54 }
55
56 // Suppress noisy Qt Quick internals (also matches qt.quickcontrols, etc.)
57 if (context.category && std::strncmp(context.category, "qt.quick", 8) == 0) {
58 return;
59 }
60
61 LogManager::captureIfEnabled(type, context, msg);
62 inst->log(type, context, msg);
63}
64
65// ---------------------------------------------------------------------------
66// LogManager (manager)
67// ---------------------------------------------------------------------------
68
70{
71 return s_instance.load(std::memory_order_acquire);
72}
73
74LogManager* LogManager::create(QQmlEngine* qmlEngine, QJSEngine* jsEngine)
75{
76 Q_UNUSED(jsEngine);
77 auto* inst = instance();
78 Q_ASSERT(inst);
79 QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership);
80 return inst;
81}
82
83LogManager::LogManager(QObject* parent) : QObject(parent)
84{
85 s_instance.store(this, std::memory_order_release);
86 _model = new LogModel(this);
87 _fileWriter = new QGCFileWriter(this);
88
89 (void)connect(_fileWriter, &QGCFileWriter::errorOccurred, this, [this](const QString& msg) { _setIoError(msg); });
90 (void)connect(_fileWriter, &QGCFileWriter::fileSizeChanged, this, [this](qint64 size) {
91 if (size >= _maxLogFileSize) {
92 _rotateLogs();
93 }
94 });
95
96 _flushTimer.setInterval(kFlushIntervalMSecs);
97 _flushTimer.setSingleShot(false);
98 (void)connect(&_flushTimer, &QTimer::timeout, this, &LogManager::_flushToDisk);
99 _flushTimer.start();
100}
101
103{
104 // Detach from the message handler so no new entries are queued.
105 if (s_instance.load(std::memory_order_relaxed) == this) {
106 s_instance.store(nullptr, std::memory_order_release);
107 }
108
109 // Drain any already-queued invokeMethod lambdas that reference us.
110 QCoreApplication::processEvents();
111
112 _flushTimer.stop();
113 _flushToDisk();
114 _fileWriter->close();
115
116 if (_exportFuture.isValid()) {
117 _exportFuture.waitForFinished();
118 }
119}
120
122{
123 Q_ASSERT(!s_instance.load(std::memory_order_relaxed));
124 auto* mgr = new LogManager();
125
126 qSetMessagePattern(
127 QStringLiteral("%{time process}%{if-warning} Warning:%{endif}%{if-critical} Critical:%{endif} %{message} - "
128 "%{category} - (%{function}:%{line})"));
129 s_defaultHandler = qInstallMessageHandler(&LogManager::msgHandler);
130}
131
133{
134 const QByteArray env = qgetenv("QGC_LOG_LEVEL");
135 if (env.isEmpty()) {
136 return;
137 }
138
139 const QString level = QString::fromUtf8(env).toLower().trimmed();
140 QString rules;
141
142 if (level == QStringLiteral("trace") || level == QStringLiteral("debug")) {
143 rules = QStringLiteral("*.debug=true\n");
144 } else if (level == QStringLiteral("info")) {
145 rules = QStringLiteral("*.debug=false\n*.info=true\n");
146 } else if (level == QStringLiteral("warning") || level == QStringLiteral("warn")) {
147 rules = QStringLiteral("*.debug=false\n*.info=false\n*.warning=true\n");
148 } else if (level == QStringLiteral("critical") || level == QStringLiteral("error")) {
149 rules = QStringLiteral("*.debug=false\n*.info=false\n*.warning=false\n*.critical=true\n");
150 } else if (level == QStringLiteral("off") || level == QStringLiteral("none")) {
151 rules = QStringLiteral("*.debug=false\n*.info=false\n*.warning=false\n*.critical=false\n");
152 } else {
153 qWarning("QGC_LOG_LEVEL: unknown level '%s' (use debug/info/warning/critical/off)", env.constData());
154 return;
155 }
156
157 QLoggingCategory::setFilterRules(rules);
158}
159
160// ---------------------------------------------------------------------------
161// Two-phase init (called after SettingsManager is ready)
162// ---------------------------------------------------------------------------
163
165{
166 auto* appSettings = SettingsManager::instance()->appSettings();
167 auto* logSettings = SettingsManager::instance()->logManagerSettings();
168
169 // --- Log directory ---
170 setLogDirectory(appSettings->logSavePath());
171 (void)connect(appSettings, &AppSettings::savePathsChanged, this, [this, appSettings]() {
172 setLogDirectory(appSettings->logSavePath());
173 });
174
175 // --- Disk logging settings ---
176 _setDiskLoggingEnabled(logSettings->diskLoggingEnabled()->rawValue().toBool());
177 (void)connect(logSettings->diskLoggingEnabled(), &Fact::rawValueChanged, this, [this, logSettings]() {
178 _setDiskLoggingEnabled(logSettings->diskLoggingEnabled()->rawValue().toBool());
179 });
180
181 // --- Disk log rotation settings ---
182 _maxLogFileSize = logSettings->diskLoggingMaxFileSizeMB()->rawValue().toInt() * 1024 * 1024;
183 (void)connect(logSettings->diskLoggingMaxFileSizeMB(), &Fact::rawValueChanged, this, [this, logSettings]() {
184 _maxLogFileSize = logSettings->diskLoggingMaxFileSizeMB()->rawValue().toInt() * 1024 * 1024;
185 });
186
187 _maxBackupFiles = logSettings->diskLoggingMaxBackupFiles()->rawValue().toInt();
188 (void)connect(logSettings->diskLoggingMaxBackupFiles(), &Fact::rawValueChanged, this, [this, logSettings]() {
189 _maxBackupFiles = logSettings->diskLoggingMaxBackupFiles()->rawValue().toInt();
190 });
191
192 // --- Elapsed time display setting ---
193 (void)connect(appSettings->showAppLogTimestampAsElapsedTime(), &Fact::rawValueChanged, _model, [this]() {
194 const int rows = _model->rowCount();
195 if (rows > 0) {
196 const auto col = static_cast<int>(LogEntry::TimestampColumn);
197 emit _model->dataChanged(_model->index(0, col), _model->index(rows - 1, col), {Qt::DisplayRole});
198 }
199 });
200
201 _replayEarlyEntries();
202 _initialized = true;
203}
204
205// ---------------------------------------------------------------------------
206// Early-message replay
207// ---------------------------------------------------------------------------
208
209void LogManager::_replayEarlyEntries()
210{
211 const QList<LogEntry> earlyEntries = _model->allEntriesSnapshot();
212 for (const auto& entry : earlyEntries) {
213 if (_diskLoggingEnabled) {
214 _pendingDiskWrites.append(entry);
215 }
216 }
217 if (_diskLoggingEnabled && !_pendingDiskWrites.isEmpty()) {
218 _flushToDisk();
219 }
220}
221
222// ---------------------------------------------------------------------------
223// Log ingestion
224// ---------------------------------------------------------------------------
225
226void LogManager::log(QtMsgType type, const QMessageLogContext& context, const QString& message)
227{
228 LogEntry entry = buildEntry(type, context, message);
229
230 QMetaObject::invokeMethod(
231 this,
232 [this, entry = std::move(entry)]() mutable {
233 entry.category = _internCategory(entry.category);
234 _handleEntry(entry);
235 },
236 Qt::QueuedConnection);
237}
238
239const QString& LogManager::_internCategory(const QString& category)
240{
241 auto it = _internedCategories.find(category);
242 if (it != _internedCategories.end()) {
243 return *it;
244 }
245 return *_internedCategories.insert(category);
246}
247
248void LogManager::_dispatchToSinks(const LogEntry& entry)
249{
250 _model->enqueue(entry);
251 if (_diskLoggingEnabled) {
252 _pendingDiskWrites.append(entry);
253 }
254}
255
256void LogManager::_handleEntry(const LogEntry& entry)
257{
258 if (_rateLimitingEnabled && !_rateLimitCheck(entry)) {
259 return;
260 }
261
262 _dispatchToSinks(entry);
263}
264
265bool LogManager::_rateLimitCheck(const LogEntry& entry)
266{
267 if (entry.category.isEmpty()) {
268 return true;
269 }
270
271 const qint64 now = QDateTime::currentMSecsSinceEpoch();
272 auto& bucket = _rateBuckets[entry.category];
273
274 if (bucket.lastRefillMs == 0) {
275 bucket.lastRefillMs = now;
276 bucket.tokens = kRateMaxTokens;
277 }
278
279 const qint64 elapsed = now - bucket.lastRefillMs;
280 if (elapsed > 0) {
281 const int refill = static_cast<int>(elapsed * kRateTokensPerSecond / 1000);
282 if (refill > 0) {
283 bucket.tokens = qMin(bucket.tokens + refill, kRateMaxTokens);
284 bucket.lastRefillMs = now;
285
286 if (bucket.suppressed > 0 && bucket.tokens > 0) {
287 _emitSuppressedSummary(entry.category, bucket.suppressed);
288 bucket.suppressed = 0;
289 }
290 }
291 }
292
293 if (bucket.tokens > 0) {
294 --bucket.tokens;
295 return true;
296 }
297
298 ++bucket.suppressed;
299 return false;
300}
301
302void LogManager::_emitSuppressedSummary(const QString& category, int count)
303{
304 LogEntry summary;
305 summary.timestamp = QDateTime::currentDateTime();
306 summary.level = LogEntry::Warning;
307 summary.category = category;
308 summary.message = QStringLiteral("... %1 messages suppressed (rate limited)").arg(count);
309 summary.buildFormatted();
310
311 _dispatchToSinks(summary);
312}
313
314// ---------------------------------------------------------------------------
315// Manager operations
316// ---------------------------------------------------------------------------
317
319{
320 if (_ioError) {
321 _ioError = false;
322 _lastError.clear();
323 emit hasErrorChanged();
324 emit lastErrorChanged();
325 }
326}
327
329{
330 Q_ASSERT(QThread::currentThread() == thread());
331 _flushToDisk();
332 _fileWriter->flush();
333}
334
335void LogManager::_setDiskLoggingEnabled(bool enabled)
336{
337 if (_diskLoggingEnabled != enabled) {
338 if (!enabled) {
339 _flushToDisk();
340 _fileWriter->close();
341 }
342 _diskLoggingEnabled = enabled;
343 }
344}
345
346// ---------------------------------------------------------------------------
347// Disk writing
348// ---------------------------------------------------------------------------
349
350void LogManager::_setIoError(const QString& message)
351{
352 _ioError = true;
353 _lastError = message;
354 emit hasErrorChanged();
355 emit lastErrorChanged();
356}
357
358void LogManager::setLogDirectory(const QString& path)
359{
360 if (_logDirectory == path) {
361 return;
362 }
363 _logDirectory = path;
364
365 if (path.isEmpty()) {
366 _fileWriter->setFilePath(QString());
367 return;
368 }
369
370 const QDir dir(path);
371 _fileWriter->setFilePath(dir.absoluteFilePath(QStringLiteral("AppLog.log")));
372}
373
374void LogManager::_rotateLogs()
375{
376 _fileWriter->flush();
377 _fileWriter->close();
378
379 const QString path = _fileWriter->filePath();
380 const QFileInfo fileInfo(path);
381 const QString dir = fileInfo.absolutePath();
382 const QString name = fileInfo.baseName();
383 const QString ext = fileInfo.completeSuffix();
384
385 for (int i = _maxBackupFiles - 1; i >= 1; --i) {
386 const QString from = QStringLiteral("%1/%2.%3.%4").arg(dir, name).arg(i).arg(ext);
387 const QString to = QStringLiteral("%1/%2.%3.%4").arg(dir, name).arg(i + 1).arg(ext);
388 if (QFile::exists(to)) {
389 (void)QFile::remove(to);
390 }
391 if (QFile::exists(from)) {
392 (void)QFile::rename(from, to);
393 }
394 }
395
396 const QString firstBackup = QStringLiteral("%1/%2.1.%3").arg(dir, name, ext);
397 (void)QFile::rename(path, firstBackup);
398
399 _fileWriter->setFilePath(path);
400}
401
402void LogManager::_flushToDisk()
403{
404 if (_pendingDiskWrites.isEmpty() || _ioError || !_diskLoggingEnabled || _logDirectory.isEmpty()) {
405 return;
406 }
407
408 auto entries = std::move(_pendingDiskWrites);
409 _fileWriter->write(LogFormatter::formatAsText(entries));
410}
411
412// ---------------------------------------------------------------------------
413// Export
414// ---------------------------------------------------------------------------
415
416void LogManager::writeMessages(const QString& destFile)
417{
418 _exportEntries(_model->allEntriesSnapshot(), destFile);
419}
420
421void LogManager::_exportEntries(QList<LogEntry> entries, const QString& destFile)
422{
423 emit writeStarted();
424
425 QPointer<LogManager> guard(this);
426 _exportFuture = QtConcurrent::run([guard, destFile, entries = std::move(entries)]() {
427 bool success = false;
428 QSaveFile file(destFile);
429 if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
430 const int fmt = destFile.endsWith(QStringLiteral(".csv"), Qt::CaseInsensitive)
432 const QByteArray content = LogFormatter::format(entries, fmt);
433 file.write(content);
434 success = file.commit();
435 } else {
436 qCWarning(LogManagerLog) << "write failed:" << file.errorString();
437 }
438 if (guard) {
439 QMetaObject::invokeMethod(
440 guard.data(),
441 [guard, success]() {
442 if (guard) {
443 emit guard->writeFinished(success);
444 }
445 },
446 Qt::QueuedConnection);
447 }
448 });
449}
450
451// ---------------------------------------------------------------------------
452// Test capture
453// ---------------------------------------------------------------------------
454
456{
457 s_captureEnabled.store(enabled, std::memory_order_relaxed);
458}
459
461{
462 const QMutexLocker locker(&s_captureMutex);
463 s_capturedMessages.clear();
464}
465
466QList<LogEntry> LogManager::capturedMessages(const QString& category)
467{
468 const QMutexLocker locker(&s_captureMutex);
469
470 if (category.isEmpty()) {
471 return s_capturedMessages;
472 }
473
474 QList<LogEntry> filtered;
475 for (const auto& msg : std::as_const(s_capturedMessages)) {
476 if (msg.category == category) {
477 filtered.append(msg);
478 }
479 }
480 return filtered;
481}
482
483bool LogManager::hasCapturedMessage(const QString& category, LogEntry::Level level)
484{
485 const QMutexLocker locker(&s_captureMutex);
486 for (const auto& msg : std::as_const(s_capturedMessages)) {
487 if (msg.category == category && msg.level == level) {
488 return true;
489 }
490 }
491 return false;
492}
493
494bool LogManager::hasCapturedWarning(const QString& category)
495{
496 return hasCapturedMessage(category, LogEntry::Warning);
497}
498
499bool LogManager::hasCapturedCritical(const QString& category)
500{
501 return hasCapturedMessage(category, LogEntry::Critical);
502}
503
505{
506 const QMutexLocker locker(&s_captureMutex);
507 for (const auto& msg : std::as_const(s_capturedMessages)) {
508 if (msg.category.isEmpty() || msg.category == QStringLiteral("default")) {
509 return true;
510 }
511 }
512 return false;
513}
514
515void LogManager::captureIfEnabled(QtMsgType type, const QMessageLogContext& context, const QString& msg)
516{
517 if (!s_captureEnabled.load(std::memory_order_relaxed)) {
518 return;
519 }
520
521 LogEntry entry = buildEntry(type, context, msg);
522
523 const QMutexLocker locker(&s_captureMutex);
524 s_capturedMessages.append(std::move(entry));
525}
526
527LogEntry LogManager::buildEntry(QtMsgType type, const QMessageLogContext& context, const QString& message)
528{
529 LogEntry entry;
530 entry.elapsedMs = s_elapsedTimer.elapsed();
531 entry.timestamp = QDateTime::currentDateTime();
532 entry.level = LogEntry::fromQtMsgType(type);
533 entry.category = context.category ? QString::fromLatin1(context.category) : QString();
534 entry.message = message;
535 if (context.file) {
536 const QString fullPath = QString::fromLatin1(context.file);
537 const int lastSlash = fullPath.lastIndexOf(QLatin1Char('/'));
538 entry.file = (lastSlash >= 0) ? fullPath.mid(lastSlash + 1) : fullPath;
539 }
540 entry.function = context.function ? QString::fromLatin1(context.function) : QString();
541 entry.line = context.line;
542 entry.threadId = QThread::currentThreadId();
543 entry.buildFormatted();
544 return entry;
545}
static std::atomic< bool > s_captureEnabled
Definition LogManager.cc:31
static QtMessageHandler s_defaultHandler
Definition LogManager.cc:42
static QMutex s_captureMutex
Definition LogManager.cc:32
static QList< LogEntry > s_capturedMessages
Definition LogManager.cc:33
static std::atomic< LogManager * > s_instance
Definition LogManager.cc:25
static QElapsedTimer s_elapsedTimer
Definition LogManager.cc:36
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void savePathsChanged()
void rawValueChanged(const QVariant &value)
static void applyEnvironmentLogLevel()
void writeStarted()
Q_INVOKABLE void clearError()
static bool hasCapturedMessage(const QString &category, LogEntry::Level level)
static void clearCapturedMessages()
static bool hasCapturedCritical(const QString &category)
Q_INVOKABLE void flush()
static bool hasCapturedWarning(const QString &category)
Q_INVOKABLE void writeMessages(const QString &destFile)
static LogManager * create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)
Definition LogManager.cc:74
void lastErrorChanged()
static bool hasCapturedUncategorizedMessage()
static QList< LogEntry > capturedMessages(const QString &category={})
void setLogDirectory(const QString &path)
void init()
static void installHandler()
static LogManager * instance()
Definition LogManager.cc:69
void hasErrorChanged()
static void captureIfEnabled(QtMsgType type, const QMessageLogContext &context, const QString &msg)
static void setCaptureEnabled(bool enabled)
void enqueue(LogEntry entry)
Definition LogModel.cc:220
QList< LogEntry > allEntriesSnapshot() const
Definition LogModel.h:67
int rowCount(const QModelIndex &parent=QModelIndex()) const override
Definition LogModel.cc:28
bool flush(int timeoutMs=5000)
void fileSizeChanged(qint64 size)
void write(const QByteArray &data)
void setFilePath(const QString &path)
void errorOccurred(const QString &message)
QString filePath() const
static SettingsManager * instance()
LogManagerSettings * logManagerSettings() const
AppSettings * appSettings() const
QByteArray formatAsText(const QList< LogEntry > &entries)
QByteArray format(const QList< LogEntry > &entries, int fmt)
qint64 elapsedMs
Definition LogEntry.h:45
QString file
Definition LogEntry.h:41
QString function
Definition LogEntry.h:42
int line
Definition LogEntry.h:46
QString message
Definition LogEntry.h:40
QDateTime timestamp
Definition LogEntry.h:37
static Level fromQtMsgType(QtMsgType type)
Definition LogEntry.cc:29
Level level
Definition LogEntry.h:38
QString category
Definition LogEntry.h:39
@ TimestampColumn
Definition LogEntry.h:71
@ Critical
Definition LogEntry.h:26
@ Warning
Definition LogEntry.h:25
Qt::HANDLE threadId
Definition LogEntry.h:44
void buildFormatted()
Definition LogEntry.cc:23