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>
36static QElapsedTimer
s_elapsedTimer = []() { QElapsedTimer t; t.start();
return t; }();
44void LogManager::msgHandler(QtMsgType type,
const QMessageLogContext& context,
const QString& msg)
46 auto* inst =
s_instance.load(std::memory_order_acquire);
57 if (context.category && std::strncmp(context.category,
"qt.quick", 8) == 0) {
62 inst->log(type, context, msg);
71 return s_instance.load(std::memory_order_acquire);
79 QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership);
83LogManager::LogManager(QObject* parent) : QObject(parent)
85 s_instance.store(
this, std::memory_order_release);
91 if (size >= _maxLogFileSize) {
96 _flushTimer.setInterval(kFlushIntervalMSecs);
97 _flushTimer.setSingleShot(
false);
98 (void)connect(&_flushTimer, &QTimer::timeout,
this, &LogManager::_flushToDisk);
105 if (
s_instance.load(std::memory_order_relaxed) ==
this) {
106 s_instance.store(
nullptr, std::memory_order_release);
110 QCoreApplication::processEvents();
114 _fileWriter->
close();
116 if (_exportFuture.isValid()) {
117 _exportFuture.waitForFinished();
123 Q_ASSERT(!
s_instance.load(std::memory_order_relaxed));
127 QStringLiteral(
"%{time process}%{if-warning} Warning:%{endif}%{if-critical} Critical:%{endif} %{message} - "
128 "%{category} - (%{function}:%{line})"));
134 const QByteArray env = qgetenv(
"QGC_LOG_LEVEL");
139 const QString level = QString::fromUtf8(env).toLower().trimmed();
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");
153 qWarning(
"QGC_LOG_LEVEL: unknown level '%s' (use debug/info/warning/critical/off)", env.constData());
157 QLoggingCategory::setFilterRules(rules);
176 _setDiskLoggingEnabled(logSettings->diskLoggingEnabled()->rawValue().toBool());
177 (void)connect(logSettings->diskLoggingEnabled(), &
Fact::rawValueChanged,
this, [
this, logSettings]() {
178 _setDiskLoggingEnabled(logSettings->diskLoggingEnabled()->rawValue().toBool());
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;
187 _maxBackupFiles = logSettings->diskLoggingMaxBackupFiles()->rawValue().toInt();
188 (void)connect(logSettings->diskLoggingMaxBackupFiles(), &
Fact::rawValueChanged,
this, [
this, logSettings]() {
189 _maxBackupFiles = logSettings->diskLoggingMaxBackupFiles()->rawValue().toInt();
193 (void)connect(appSettings->showAppLogTimestampAsElapsedTime(), &
Fact::rawValueChanged, _model, [
this]() {
194 const int rows = _model->
rowCount();
197 emit _model->dataChanged(_model->index(0, col), _model->index(rows - 1, col), {Qt::DisplayRole});
201 _replayEarlyEntries();
209void LogManager::_replayEarlyEntries()
212 for (
const auto& entry : earlyEntries) {
213 if (_diskLoggingEnabled) {
214 _pendingDiskWrites.append(entry);
217 if (_diskLoggingEnabled && !_pendingDiskWrites.isEmpty()) {
226void LogManager::log(QtMsgType type,
const QMessageLogContext& context,
const QString& message)
228 LogEntry entry = buildEntry(type, context, message);
230 QMetaObject::invokeMethod(
232 [
this, entry = std::move(entry)]()
mutable {
236 Qt::QueuedConnection);
239const QString& LogManager::_internCategory(
const QString& category)
241 auto it = _internedCategories.find(category);
242 if (it != _internedCategories.end()) {
245 return *_internedCategories.insert(category);
248void LogManager::_dispatchToSinks(
const LogEntry& entry)
251 if (_diskLoggingEnabled) {
252 _pendingDiskWrites.append(entry);
256void LogManager::_handleEntry(
const LogEntry& entry)
258 if (_rateLimitingEnabled && !_rateLimitCheck(entry)) {
262 _dispatchToSinks(entry);
265bool LogManager::_rateLimitCheck(
const LogEntry& entry)
271 const qint64 now = QDateTime::currentMSecsSinceEpoch();
272 auto& bucket = _rateBuckets[entry.
category];
274 if (bucket.lastRefillMs == 0) {
275 bucket.lastRefillMs = now;
276 bucket.tokens = kRateMaxTokens;
279 const qint64 elapsed = now - bucket.lastRefillMs;
281 const int refill =
static_cast<int>(elapsed * kRateTokensPerSecond / 1000);
283 bucket.tokens = qMin(bucket.tokens + refill, kRateMaxTokens);
284 bucket.lastRefillMs = now;
286 if (bucket.suppressed > 0 && bucket.tokens > 0) {
287 _emitSuppressedSummary(entry.
category, bucket.suppressed);
288 bucket.suppressed = 0;
293 if (bucket.tokens > 0) {
302void LogManager::_emitSuppressedSummary(
const QString& category,
int count)
305 summary.
timestamp = QDateTime::currentDateTime();
308 summary.
message = QStringLiteral(
"... %1 messages suppressed (rate limited)").arg(count);
311 _dispatchToSinks(summary);
330 Q_ASSERT(QThread::currentThread() == thread());
332 _fileWriter->
flush();
335void LogManager::_setDiskLoggingEnabled(
bool enabled)
337 if (_diskLoggingEnabled != enabled) {
340 _fileWriter->
close();
342 _diskLoggingEnabled = enabled;
350void LogManager::_setIoError(
const QString& message)
353 _lastError = message;
360 if (_logDirectory == path) {
363 _logDirectory = path;
365 if (path.isEmpty()) {
370 const QDir dir(path);
371 _fileWriter->
setFilePath(dir.absoluteFilePath(QStringLiteral(
"AppLog.log")));
374void LogManager::_rotateLogs()
376 _fileWriter->
flush();
377 _fileWriter->
close();
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();
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);
391 if (QFile::exists(from)) {
392 (void)QFile::rename(from, to);
396 const QString firstBackup = QStringLiteral(
"%1/%2.1.%3").arg(dir, name, ext);
397 (void)QFile::rename(path, firstBackup);
402void LogManager::_flushToDisk()
404 if (_pendingDiskWrites.isEmpty() || _ioError || !_diskLoggingEnabled || _logDirectory.isEmpty()) {
408 auto entries = std::move(_pendingDiskWrites);
421void LogManager::_exportEntries(QList<LogEntry> entries,
const QString& destFile)
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)
434 success = file.commit();
436 qCWarning(LogManagerLog) <<
"write failed:" << file.errorString();
439 QMetaObject::invokeMethod(
443 emit guard->writeFinished(success);
446 Qt::QueuedConnection);
470 if (category.isEmpty()) {
474 QList<LogEntry> filtered;
476 if (msg.category == category) {
477 filtered.append(msg);
487 if (msg.category == category && msg.level == level) {
508 if (msg.category.isEmpty() || msg.category == QStringLiteral(
"default")) {
521 LogEntry entry = buildEntry(type, context, msg);
527LogEntry LogManager::buildEntry(QtMsgType type,
const QMessageLogContext& context,
const QString& message)
531 entry.
timestamp = QDateTime::currentDateTime();
533 entry.
category = context.category ? QString::fromLatin1(context.category) : QString();
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;
540 entry.
function = context.function ? QString::fromLatin1(context.function) : QString();
541 entry.
line = context.line;
542 entry.
threadId = QThread::currentThreadId();
static std::atomic< bool > s_captureEnabled
static QtMessageHandler s_defaultHandler
static QMutex s_captureMutex
static QList< LogEntry > s_capturedMessages
static std::atomic< LogManager * > s_instance
static QElapsedTimer s_elapsedTimer
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void rawValueChanged(const QVariant &value)
static void applyEnvironmentLogLevel()
Q_INVOKABLE void clearError()
static bool hasCapturedMessage(const QString &category, LogEntry::Level level)
static void clearCapturedMessages()
static bool hasCapturedCritical(const QString &category)
static bool hasCapturedWarning(const QString &category)
Q_INVOKABLE void writeMessages(const QString &destFile)
static LogManager * create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)
static bool hasCapturedUncategorizedMessage()
static QList< LogEntry > capturedMessages(const QString &category={})
void setLogDirectory(const QString &path)
static void installHandler()
static LogManager * instance()
static void captureIfEnabled(QtMsgType type, const QMessageLogContext &context, const QString &msg)
static void setCaptureEnabled(bool enabled)
void enqueue(LogEntry entry)
QList< LogEntry > allEntriesSnapshot() const
int rowCount(const QModelIndex &parent=QModelIndex()) const override
bool flush(int timeoutMs=5000)
void fileSizeChanged(qint64 size)
void write(const QByteArray &data)
void setFilePath(const QString &path)
void errorOccurred(const QString &message)
static SettingsManager * instance()
LogManagerSettings * logManagerSettings() const
AppSettings * appSettings() const
static Level fromQtMsgType(QtMsgType type)