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>
37static QElapsedTimer
s_elapsedTimer = []() { QElapsedTimer t; t.start();
return t; }();
46void LogManager::msgHandler(QtMsgType type,
const QMessageLogContext& context,
const QString& msg)
48 auto* inst =
s_instance.load(std::memory_order_acquire);
54 const QByteArray
line = qFormatLogMessage(type, context, msg).toLocal8Bit();
55 std::fprintf(stderr,
"%s\n",
line.constData());
64 if (context.category && std::strncmp(context.category,
"qt.quick", 8) == 0) {
69 inst->log(type, context, msg);
78 return s_instance.load(std::memory_order_acquire);
85 qCCritical(LogManagerLog) <<
"LogManager must be initialized before QML requests it";
88 QJSEngine::setObjectOwnership(inst, QJSEngine::CppOwnership);
92LogManager::LogManager(QObject* parent) : QObject(parent)
94 s_instance.store(
this, std::memory_order_release);
100 if (size >= _maxLogFileSize) {
105 _flushTimer.setInterval(kFlushIntervalMSecs);
106 _flushTimer.setSingleShot(
false);
107 (void)connect(&_flushTimer, &QTimer::timeout,
this, &LogManager::_flushToDisk);
114 if (
s_instance.load(std::memory_order_relaxed) ==
this) {
115 s_instance.store(
nullptr, std::memory_order_release);
119 QCoreApplication::processEvents();
123 _fileWriter->
close();
125 if (_exportFuture.isValid()) {
126 _exportFuture.waitForFinished();
132 Q_ASSERT(!
s_instance.load(std::memory_order_relaxed));
138 QStringLiteral(
"%{time process}%{if-warning} Warning:%{endif}%{if-critical} Critical:%{endif} %{message} - "
139 "%{category} - (%{function}:%{line})"));
145 const QByteArray env = qgetenv(
"QGC_LOG_LEVEL");
150 const QString level = QString::fromUtf8(env).toLower().trimmed();
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");
164 qWarning(
"QGC_LOG_LEVEL: unknown level '%s' (use debug/info/warning/critical/off)", env.constData());
168 QLoggingCategory::setFilterRules(rules);
187 _setDiskLoggingEnabled(logSettings->diskLoggingEnabled()->rawValue().toBool());
188 (void)connect(logSettings->diskLoggingEnabled(), &
Fact::rawValueChanged,
this, [
this, logSettings]() {
189 _setDiskLoggingEnabled(logSettings->diskLoggingEnabled()->rawValue().toBool());
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;
198 _maxBackupFiles = logSettings->diskLoggingMaxBackupFiles()->rawValue().toInt();
199 (void)connect(logSettings->diskLoggingMaxBackupFiles(), &
Fact::rawValueChanged,
this, [
this, logSettings]() {
200 _maxBackupFiles = logSettings->diskLoggingMaxBackupFiles()->rawValue().toInt();
204 (void)connect(appSettings->showAppLogTimestampAsElapsedTime(), &
Fact::rawValueChanged, _model, [
this]() {
205 const int rows = _model->
rowCount();
208 emit _model->dataChanged(_model->index(0, col), _model->index(rows - 1, col), {Qt::DisplayRole});
212 _replayEarlyEntries();
220void LogManager::_replayEarlyEntries()
223 for (
const auto& entry : earlyEntries) {
224 if (_diskLoggingEnabled) {
225 _pendingDiskWrites.append(entry);
228 if (_diskLoggingEnabled && !_pendingDiskWrites.isEmpty()) {
237void LogManager::log(QtMsgType type,
const QMessageLogContext& context,
const QString& message)
239 LogEntry entry = buildEntry(type, context, message);
241 QMetaObject::invokeMethod(
243 [
this, entry = std::move(entry)]()
mutable {
247 Qt::QueuedConnection);
250const QString& LogManager::_internCategory(
const QString& category)
252 auto it = _internedCategories.find(category);
253 if (it != _internedCategories.end()) {
256 return *_internedCategories.insert(category);
259void LogManager::_dispatchToSinks(
const LogEntry& entry)
262 if (_diskLoggingEnabled) {
263 _pendingDiskWrites.append(entry);
267void LogManager::_handleEntry(
const LogEntry& entry)
269 if (_rateLimitingEnabled && !_rateLimitCheck(entry)) {
273 _dispatchToSinks(entry);
276bool LogManager::_rateLimitCheck(
const LogEntry& entry)
282 const qint64 now = QDateTime::currentMSecsSinceEpoch();
283 auto& bucket = _rateBuckets[entry.
category];
285 if (bucket.lastRefillMs == 0) {
286 bucket.lastRefillMs = now;
287 bucket.tokens = kRateMaxTokens;
290 const qint64 elapsed = now - bucket.lastRefillMs;
292 const int refill =
static_cast<int>(elapsed * kRateTokensPerSecond / 1000);
294 bucket.tokens = qMin(bucket.tokens + refill, kRateMaxTokens);
295 bucket.lastRefillMs = now;
297 if (bucket.suppressed > 0 && bucket.tokens > 0) {
298 _emitSuppressedSummary(entry.
category, bucket.suppressed);
299 bucket.suppressed = 0;
304 if (bucket.tokens > 0) {
313void LogManager::_emitSuppressedSummary(
const QString& category,
int count)
316 summary.
timestamp = QDateTime::currentDateTime();
319 summary.
message = QStringLiteral(
"... %1 messages suppressed (rate limited)").arg(count);
322 _dispatchToSinks(summary);
341 Q_ASSERT(QThread::currentThread() == thread());
343 _fileWriter->
flush();
346void LogManager::_setDiskLoggingEnabled(
bool enabled)
348 if (_diskLoggingEnabled != enabled) {
351 _fileWriter->
close();
353 _diskLoggingEnabled = enabled;
361void LogManager::_setIoError(
const QString& message)
364 _lastError = message;
371 if (_logDirectory == path) {
374 _logDirectory = path;
376 if (path.isEmpty()) {
381 const QDir dir(path);
382 _fileWriter->
setFilePath(dir.absoluteFilePath(QStringLiteral(
"AppLog.log")));
385void LogManager::_rotateLogs()
387 _fileWriter->
flush();
388 _fileWriter->
close();
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();
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);
402 if (QFile::exists(from)) {
403 (void)QFile::rename(from, to);
407 const QString firstBackup = QStringLiteral(
"%1/%2.1.%3").arg(dir, name, ext);
408 (void)QFile::rename(path, firstBackup);
413void LogManager::_flushToDisk()
415 if (_pendingDiskWrites.isEmpty() || _ioError || !_diskLoggingEnabled || _logDirectory.isEmpty()) {
419 auto entries = std::move(_pendingDiskWrites);
432void LogManager::_exportEntries(QList<LogEntry> entries,
const QString& destFile)
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)
445 success = file.commit();
447 qCWarning(LogManagerLog) <<
"write failed:" << file.errorString();
450 QMetaObject::invokeMethod(
454 emit guard->writeFinished(success);
457 Qt::QueuedConnection);
481 if (category.isEmpty()) {
485 QList<LogEntry> filtered;
487 if (msg.category == category) {
488 filtered.append(msg);
498 if (msg.category == category && msg.level == level) {
519 if (msg.category.isEmpty() || msg.category == QStringLiteral(
"default")) {
532 LogEntry entry = buildEntry(type, context, msg);
538LogEntry LogManager::buildEntry(QtMsgType type,
const QMessageLogContext& context,
const QString& message)
542 entry.
timestamp = QDateTime::currentDateTime();
544 entry.
category = context.category ? QString::fromLatin1(context.category) : QString();
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;
551 entry.
function = context.function ? QString::fromLatin1(context.function) : QString();
552 entry.
line = context.line;
553 entry.
threadId = QThread::currentThreadId();
static std::atomic< bool > s_echoToStderr
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 installHandler(bool logOutput)
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 bool hasCapturedUncategorizedMessage()
static QList< LogEntry > capturedMessages(const QString &category={})
void setLogDirectory(const QString &path)
static LogManager * create(QQmlEngine *, QJSEngine *)
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
QByteArray line(std::string_view command)
command as one CR/LF-terminated line.
static Level fromQtMsgType(QtMsgType type)