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