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/QMutex>
6#include <QtCore/QMutexLocker>
7#include <QtCore/QPointer>
8#include <QtCore/QSaveFile>
9#include <QtCore/QThread>
10#include <QtQml/QJSEngine>
11#include <atomic>
12#include <cstring>
13
14#include "LogFormatter.h"
15#include "LogModel.h"
16#include "LogRemoteSink.h"
17#include "LogStore.h"
18#include "LogStoreQueryModel.h"
19#include "QGCCompression.h"
20#include "QGCFileWriter.h"
21#include "QGCLoggingCategory.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// ---------------------------------------------------------------------------
36// Qt message handler
37// ---------------------------------------------------------------------------
38
39static QtMessageHandler s_defaultHandler = nullptr;
40
41void LogManager::msgHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg)
42{
43 auto* inst = s_instance.load(std::memory_order_acquire);
44
45 if (s_defaultHandler) {
46 s_defaultHandler(type, context, msg);
47 }
48
49 if (!inst) {
50 return;
51 }
52
53 // Suppress noisy Qt Quick internals (also matches qt.quickcontrols, etc.)
54 if (context.category && std::strncmp(context.category, "qt.quick", 8) == 0) {
55 return;
56 }
57
58 LogManager::captureIfEnabled(type, context, msg);
59 inst->log(type, context, msg);
60}
61
62// ---------------------------------------------------------------------------
63// LogManager (manager)
64// ---------------------------------------------------------------------------
65
66LogManager* LogManager::instance()
67{
68 return s_instance.load(std::memory_order_acquire);
69}
70
71LogManager* LogManager::create(QQmlEngine* qmlEngine, QJSEngine* jsEngine)
72{
73 Q_UNUSED(jsEngine);
74 auto* inst = instance();
75 Q_ASSERT(inst);
76 QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership);
77 return inst;
78}
79
80LogManager::LogManager(QObject* parent) : QObject(parent)
81{
82 s_instance.store(this, std::memory_order_release);
83 _model = new LogModel(this);
84 _remoteSink = new LogRemoteSink(this);
85 _logStore = new LogStore(this);
86 _historyModel = new LogStoreQueryModel(_logStore, 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 >= kMaxLogFileSize) {
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
102LogManager::~LogManager()
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 _remoteSink->setEnabled(false);
115 _fileWriter->close();
116 _logStore->close();
117
118 if (_exportFuture.isValid()) {
119 _exportFuture.waitForFinished();
120 }
121}
122
123void LogManager::installHandler()
124{
125 Q_ASSERT(!s_instance.load(std::memory_order_relaxed));
126 auto* mgr = new LogManager();
127
128 qSetMessagePattern(
129 QStringLiteral("%{time process}%{if-warning} Warning:%{endif}%{if-critical} Critical:%{endif} %{message} - "
130 "%{category} - (%{function}:%{line})"));
131 s_defaultHandler = qInstallMessageHandler(&LogManager::msgHandler);
132}
133
134void LogManager::applyEnvironmentLogLevel()
135{
136 const QByteArray env = qgetenv("QGC_LOG_LEVEL");
137 if (env.isEmpty()) {
138 return;
139 }
140
141 const QString level = QString::fromUtf8(env).toLower().trimmed();
142 QString rules;
143
144 if (level == QStringLiteral("trace") || level == QStringLiteral("debug")) {
145 rules = QStringLiteral("*.debug=true\n");
146 } else if (level == QStringLiteral("info")) {
147 rules = QStringLiteral("*.debug=false\n*.info=true\n");
148 } else if (level == QStringLiteral("warning") || level == QStringLiteral("warn")) {
149 rules = QStringLiteral("*.debug=false\n*.info=false\n*.warning=true\n");
150 } else if (level == QStringLiteral("critical") || level == QStringLiteral("error")) {
151 rules = QStringLiteral("*.debug=false\n*.info=false\n*.warning=false\n*.critical=true\n");
152 } else if (level == QStringLiteral("off") || level == QStringLiteral("none")) {
153 rules = QStringLiteral("*.debug=false\n*.info=false\n*.warning=false\n*.critical=false\n");
154 } else {
155 qWarning("QGC_LOG_LEVEL: unknown level '%s' (use debug/info/warning/critical/off)", env.constData());
156 return;
157 }
158
159 QLoggingCategory::setFilterRules(rules);
160}
161
162// ---------------------------------------------------------------------------
163// Log ingestion
164// ---------------------------------------------------------------------------
165
166void LogManager::log(QtMsgType type, const QMessageLogContext& context, const QString& message)
167{
168 LogEntry entry = buildEntry(type, context, message);
169
170 QMetaObject::invokeMethod(
171 this,
172 [this, entry = std::move(entry)]() mutable {
173 entry.category = _internCategory(entry.category);
174 _handleEntry(entry);
175 },
176 Qt::QueuedConnection);
177}
178
179const QString& LogManager::_internCategory(const QString& category)
180{
181 auto it = _internedCategories.find(category);
182 if (it != _internedCategories.end()) {
183 return *it;
184 }
185 return *_internedCategories.insert(category);
186}
187
188void LogManager::_dispatchToSinks(const LogEntry& entry)
189{
190 _model->enqueue(entry);
191 if (_logStore->isOpen()) {
192 _logStore->append(entry);
193 }
194 if (_diskLoggingEnabled) {
195 _pendingDiskWrites.append(entry);
196 }
197 if (_remoteSink) {
198 _remoteSink->send(entry);
199 }
200}
201
202void LogManager::_handleEntry(const LogEntry& entry)
203{
204 if (!_rateLimitCheck(entry)) {
205 return;
206 }
207
208 _dispatchToSinks(entry);
209
210 if (_flushOnLevel >= 0 && static_cast<int>(entry.level) >= _flushOnLevel) {
211 _flushToDisk();
212 }
213}
214
215bool LogManager::_rateLimitCheck(const LogEntry& entry)
216{
217 if (entry.category.isEmpty()) {
218 return true;
219 }
220
221 const qint64 now = QDateTime::currentMSecsSinceEpoch();
222 auto& bucket = _rateBuckets[entry.category];
223
224 if (bucket.lastRefillMs == 0) {
225 bucket.lastRefillMs = now;
226 bucket.tokens = kRateMaxTokens;
227 }
228
229 const qint64 elapsed = now - bucket.lastRefillMs;
230 if (elapsed > 0) {
231 const int refill = static_cast<int>(elapsed * kRateTokensPerSecond / 1000);
232 if (refill > 0) {
233 bucket.tokens = qMin(bucket.tokens + refill, kRateMaxTokens);
234 bucket.lastRefillMs = now;
235
236 if (bucket.suppressed > 0 && bucket.tokens > 0) {
237 _emitSuppressedSummary(entry.category, bucket.suppressed);
238 bucket.suppressed = 0;
239 }
240 }
241 }
242
243 if (bucket.tokens > 0) {
244 --bucket.tokens;
245 return true;
246 }
247
248 ++bucket.suppressed;
249 return false;
250}
251
252void LogManager::_emitSuppressedSummary(const QString& category, int count)
253{
254 LogEntry summary;
255 summary.timestamp = QDateTime::currentDateTime();
256 summary.level = LogEntry::Warning;
257 summary.category = category;
258 summary.message = QStringLiteral("... %1 messages suppressed (rate limited)").arg(count);
259 summary.buildFormatted();
260
261 _dispatchToSinks(summary);
262}
263
264// ---------------------------------------------------------------------------
265// Manager operations
266// ---------------------------------------------------------------------------
267
268void LogManager::clearError()
269{
270 if (_ioError) {
271 _ioError = false;
272 _lastError.clear();
273 emit hasErrorChanged();
274 emit lastErrorChanged();
275 }
276}
277
278void LogManager::flush()
279{
280 Q_ASSERT(QThread::currentThread() == thread());
281 _flushToDisk();
282 _fileWriter->flush();
283}
284
285void LogManager::setDiskLoggingEnabled(bool enabled)
286{
287 if (_diskLoggingEnabled != enabled) {
288 if (!enabled) {
289 _flushToDisk();
290 _fileWriter->close();
291 }
292 _diskLoggingEnabled = enabled;
294 }
295}
296
297void LogManager::setDiskCompressionEnabled(bool enabled)
298{
299 if (_diskCompressionEnabled != enabled) {
300 _diskCompressionEnabled = enabled;
302 }
303}
304
305void LogManager::setFlushOnLevel(int level)
306{
307 if (_flushOnLevel != level) {
308 _flushOnLevel = level;
309 emit flushOnLevelChanged();
310 }
311}
312
313QStringList LogManager::categoryLogLevelNames()
314{
315 return {tr("Debug"), tr("Info"), tr("Warning"), tr("Critical")};
316}
317
318QVariantList LogManager::categoryLogLevelValues()
319{
320 return {QtDebugMsg, QtInfoMsg, QtWarningMsg, QtCriticalMsg};
321}
322
323// ---------------------------------------------------------------------------
324// Disk writing
325// ---------------------------------------------------------------------------
326
327void LogManager::_setIoError(const QString& message)
328{
329 _ioError = true;
330 _lastError = message;
331 emit hasErrorChanged();
332 emit lastErrorChanged();
333}
334
335void LogManager::setLogDirectory(const QString& path)
336{
337 if (_logDirectory == path) {
338 return;
339 }
340 _logDirectory = path;
341
342 if (_logStore->isOpen()) {
343 _logStore->close();
344 }
345
346 if (path.isEmpty()) {
347 _fileWriter->setFilePath(QString());
348 return;
349 }
350
351 const QDir dir(path);
352 _fileWriter->setFilePath(dir.absoluteFilePath(QStringLiteral("QGCConsole.log")));
353 _logStore->open(dir.absoluteFilePath(QStringLiteral("QGCConsole.db")));
354}
355
356void LogManager::_rotateLogs()
357{
358 _fileWriter->flush();
359 _fileWriter->close();
360
361 const QString path = _fileWriter->filePath();
362 const QFileInfo fileInfo(path);
363 const QString dir = fileInfo.absolutePath();
364 const QString name = fileInfo.baseName();
365 const QString ext = fileInfo.completeSuffix();
366
367 for (int i = kMaxBackupFiles - 1; i >= 1; --i) {
368 const QString from = QStringLiteral("%1/%2.%3.%4").arg(dir, name).arg(i).arg(ext);
369 const QString to = QStringLiteral("%1/%2.%3.%4").arg(dir, name).arg(i + 1).arg(ext);
370 if (QFile::exists(to)) {
371 (void)QFile::remove(to);
372 }
373 if (QFile::exists(from)) {
374 (void)QFile::rename(from, to);
375 }
376 }
377
378 const QString firstBackup = QStringLiteral("%1/%2.1.%3").arg(dir, name, ext);
379 (void)QFile::rename(path, firstBackup);
380
381 _fileWriter->setFilePath(path);
382}
383
384void LogManager::_flushToDisk()
385{
386 if (_pendingDiskWrites.isEmpty() || _ioError || !_diskLoggingEnabled || _logDirectory.isEmpty()) {
387 return;
388 }
389
390 auto entries = std::move(_pendingDiskWrites);
391 _fileWriter->write(LogFormatter::formatAsText(entries));
392}
393
394// ---------------------------------------------------------------------------
395// Export
396// ---------------------------------------------------------------------------
397
398void LogManager::writeMessages(const QString& destFile, ExportFormat format)
399{
400 _exportEntries(_model->allEntriesSnapshot(), destFile, format);
401}
402
403void LogManager::writeFilteredMessages(const QString& destFile, ExportFormat format)
404{
405 _exportEntries(_model->filteredEntries(), destFile, format);
406}
407
408void LogManager::_exportEntries(QList<LogEntry> entries, const QString& destFile, ExportFormat format)
409{
410 emit writeStarted();
411
412 QPointer<LogManager> guard(this);
413 _exportFuture = QtConcurrent::run([guard, destFile, entries = std::move(entries), format]() {
414 bool success = false;
415 QSaveFile file(destFile);
416 if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
417 const QByteArray content = LogFormatter::format(entries, format);
418 file.write(content);
419 success = file.commit();
420 } else {
421 qCWarning(LogManagerLog) << "write failed:" << file.errorString();
422 }
423 if (guard) {
424 QMetaObject::invokeMethod(
425 guard.data(),
426 [guard, success]() {
427 if (guard) {
428 emit guard->writeFinished(success);
429 }
430 },
431 Qt::QueuedConnection);
432 }
433 });
434}
435
436// ---------------------------------------------------------------------------
437// Test capture
438// ---------------------------------------------------------------------------
439
440void LogManager::setCaptureEnabled(bool enabled)
441{
442 s_captureEnabled.store(enabled, std::memory_order_relaxed);
443}
444
445void LogManager::clearCapturedMessages()
446{
447 const QMutexLocker locker(&s_captureMutex);
448 s_capturedMessages.clear();
449}
450
451QList<LogEntry> LogManager::capturedMessages(const QString& category)
452{
453 const QMutexLocker locker(&s_captureMutex);
454
455 if (category.isEmpty()) {
456 return s_capturedMessages;
457 }
458
459 QList<LogEntry> filtered;
460 for (const auto& msg : std::as_const(s_capturedMessages)) {
461 if (msg.category == category) {
462 filtered.append(msg);
463 }
464 }
465 return filtered;
466}
467
468bool LogManager::hasCapturedMessage(const QString& category, LogEntry::Level level)
469{
470 const QMutexLocker locker(&s_captureMutex);
471 for (const auto& msg : std::as_const(s_capturedMessages)) {
472 if (msg.category == category && msg.level == level) {
473 return true;
474 }
475 }
476 return false;
477}
478
479bool LogManager::hasCapturedWarning(const QString& category)
480{
481 return hasCapturedMessage(category, LogEntry::Warning);
482}
483
484bool LogManager::hasCapturedCritical(const QString& category)
485{
486 return hasCapturedMessage(category, LogEntry::Critical);
487}
488
489bool LogManager::hasCapturedUncategorizedMessage()
490{
491 const QMutexLocker locker(&s_captureMutex);
492 for (const auto& msg : std::as_const(s_capturedMessages)) {
493 if (msg.category.isEmpty() || msg.category == QStringLiteral("default")) {
494 return true;
495 }
496 }
497 return false;
498}
499
500void LogManager::captureIfEnabled(QtMsgType type, const QMessageLogContext& context, const QString& msg)
501{
502 if (!s_captureEnabled.load(std::memory_order_relaxed)) {
503 return;
504 }
505
506 LogEntry entry = buildEntry(type, context, msg);
507
508 const QMutexLocker locker(&s_captureMutex);
509 s_capturedMessages.append(std::move(entry));
510}
511
512LogEntry LogManager::buildEntry(QtMsgType type, const QMessageLogContext& context, const QString& message)
513{
514 LogEntry entry;
515 entry.timestamp = QDateTime::currentDateTime();
516 entry.level = LogEntry::fromQtMsgType(type);
517 entry.category = context.category ? QString::fromLatin1(context.category) : QString();
518 entry.message = message;
519 if (context.file) {
520 const QString fullPath = QString::fromLatin1(context.file);
521 const int lastSlash = fullPath.lastIndexOf(QLatin1Char('/'));
522 entry.file = (lastSlash >= 0) ? fullPath.mid(lastSlash + 1) : fullPath;
523 }
524 entry.function = context.function ? QString::fromLatin1(context.function) : QString();
525 entry.line = context.line;
526 entry.threadId = QThread::currentThreadId();
527 entry.buildFormatted();
528 return entry;
529}
static QtMessageHandler s_defaultHandler
Definition LogManager.cc:39
static QMutex s_captureMutex
Definition LogManager.cc:32
static QList< LogEntry > s_capturedMessages
Definition LogManager.cc:33
static Q_LOGGING_CATEGORY(LogManagerLog, "Utilities.LogManager", QtWarningMsg) static std std::atomic< bool > s_captureEnabled
Definition LogManager.cc:31
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void writeStarted()
void lastErrorChanged()
void flushOnLevelChanged()
void hasErrorChanged()
void diskLoggingEnabledChanged()
void diskCompressionEnabledChanged()
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
QByteArray formatAsText(const QList< LogEntry > &entries)
QByteArray format(const QList< LogEntry > &entries, int fmt)
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
static Level fromQtMsgType(QtMsgType type)
Definition LogEntry.cc:29
Level level
Definition LogEntry.h:38
QString category
Definition LogEntry.h:39
Qt::HANDLE threadId
Definition LogEntry.h:44
void buildFormatted()
Definition LogEntry.cc:23