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