/**************************************************************************** ** Copyright (c) 2025 Evgeny Teterin (nayk) ** All right reserved. ** ** Permission is hereby granted, free of charge, to any person obtaining ** a copy of this software and associated documentation files (the ** "Software"), to deal in the Software without restriction, including ** without limitation the rights to use, copy, modify, merge, publish, ** distribute, sublicense, and/or sell copies of the Software, and to ** permit persons to whom the Software is furnished to do so, subject to ** the following conditions: ** ** The above copyright notice and this permission notice shall be ** included in all copies or substantial portions of the Software. ** ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE ** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION ** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION ** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ** ****************************************************************************/ #include "logger.h" #include #include #include #include #include #include #include #include #include #include #include "log_worker.h" #include "application_config.h" //============================================================================== std::atomic FunctionLogger::counter {0}; //============================================================================== void qtLogMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { if ((type == QtWarningMsg) || (type == QtCriticalMsg) || (type == QtFatalMsg)) { std::cerr << msg.toLocal8Bit().constData() << std::endl; } Logger::instance().writeLogQtType(type, context, msg); } //============================================================================== // // Logger - general class // //============================================================================== bool Logger::isRunning() { QCoreApplication *application = QCoreApplication::instance(); return application && !application->property("loggerRun").isNull() && application->property("loggerRun").toBool(); } //============================================================================== Logger &Logger::instance() { static Logger loggerInstance; return loggerInstance; } //============================================================================== QString Logger::logTypeToString(LogType logType) { switch (logType) { case LogType::LogInfo: return "[inf]"; case LogType::LogWarning: return "[wrn]"; case LogType::LogError: return "[err]"; case LogType::LogInput: return "[<<<]"; case LogType::LogOutput: return "[>>>]"; case LogType::LogDebug: return "[dbg]"; default: break; } return "[inf]"; } //============================================================================== QString Logger::logTypeToString(quint8 logType) { QMetaEnum metaEnum = QMetaEnum::fromType(); const char* name = metaEnum.valueToKey(logType); if (!name) { return "[inf]"; } return logTypeToString( static_cast(logType) ); } //============================================================================== Logger::LogType Logger::qtMsgToLogType(QtMsgType type) { switch (type) { case QtDebugMsg: return Logger::LogType::LogDebug; case QtInfoMsg: return Logger::LogType::LogInfo; case QtWarningMsg: return Logger::LogType::LogWarning; case QtCriticalMsg: return Logger::LogType::LogError; case QtFatalMsg: return Logger::LogType::LogError; } return Logger::LogType::LogInfo; } //============================================================================== const QLoggingCategory &Logger::inputData() { static const QLoggingCategory category("input"); return category; } //============================================================================== const QLoggingCategory &Logger::outputData() { static const QLoggingCategory category("output"); return category; } //============================================================================== QString Logger::logDirectory() { return Application::applicationProfilePath() + "log/"; } //============================================================================== void Logger::start() { if (logWorker || workThread) return; QString logDir { logDirectory() }; QDir logPath {logDir}; if (!logPath.mkpath(logDir)) { qCritical() << "Error make path: " << logDir; return; } QDateTime startDateTime {QDateTime::currentDateTime()}; QString logFileName {startDateTime.toString("yyMMdd_HHmmss_zzz") + ".log"}; logWorker = new LogWorker(logDir + logFileName, nullptr); workThread = new QThread(this); logWorker->moveToThread(workThread); connect(workThread, &QThread::started, logWorker, &LogWorker::start, Qt::QueuedConnection); connect(logWorker, &LogWorker::stopped, workThread, &QThread::quit); workThread->start(); qInstallMessageHandler( qtLogMessageOutput ); writeLog(tr("Удаление старых лог файлов из '%1', " "количество сохраняемых лог файлов: %2") .arg(logDir).arg(maximumLogCount)); QDir dir(logDir); QStringList list = dir.entryList(QStringList() << "*.log", QDir::Files | QDir::Writable, QDir::Name); writeLog(tr("Найдено лог файлов: %1").arg(list.size()), LogType::LogDebug); while (list.size() > maximumLogCount) { QString fileName = list.takeFirst(); if (QFile::remove(logDir + fileName)) { writeLog(tr("Удаление файла '%1'").arg(fileName), LogType::LogDebug); } else { writeLog(tr("Ошибка при удалении файла '%1'").arg(fileName), LogType::LogError); } } } //============================================================================== void Logger::stop() { if (!logWorker && !workThread) return; qInstallMessageHandler(0); if (logWorker) { QMetaObject::invokeMethod( logWorker, &LogWorker::stop, Qt::QueuedConnection ); } if (workThread && workThread->isRunning()) { if (!workThread->wait(1500)) { workThread->quit(); if (!workThread->wait(1500)) { workThread->terminate(); workThread->wait(); } } } if (workThread) delete workThread; if (logWorker) delete logWorker; workThread = nullptr; logWorker = nullptr; } //============================================================================== void Logger::writeLogQtType(QtMsgType type, const QMessageLogContext &context, const QString &msg) { if (!logWorker) return; QString category(context.category); LogType logType {LogType::LogInfo}; QString objectName; if (category.indexOf("input") == 0) { logType = LogType::LogInput; if (category.length() > 5) objectName = category.mid(5); } else if (category.indexOf("output") == 0) { logType = LogType::LogOutput; if (category.length() > 6) objectName = category.mid(6); } else { logType = qtMsgToLogType(type); if (!category.isEmpty() && (category != "default")) objectName = category; } writeLog(msg, logType, objectName); } //============================================================================== void Logger::writeLog(const QString &msg, LogType type, const QString &objectName) { if (!logWorker) return; static bool dbg {Application::isDebug()}; if (!dbg && ((type == LogType::LogDebug) || (type == LogType::LogInput) || (type == LogType::LogOutput))) return; int typeInt { static_cast(type) }; QMetaObject::invokeMethod( logWorker, "writeLog", Qt::QueuedConnection, Q_ARG(QString, msg), Q_ARG(int, typeInt), Q_ARG(QString, objectName) ); } //============================================================================== Logger::LogType Logger::stringToLogType(const QString &stringType) { if (stringType.contains("wrn", Qt::CaseInsensitive) || stringType.contains("warn", Qt::CaseInsensitive)) return LogType::LogWarning; else if (stringType.contains("dbg", Qt::CaseInsensitive) || stringType.contains("debug", Qt::CaseInsensitive)) return LogType::LogDebug; else if (stringType.contains("err", Qt::CaseInsensitive) || stringType.contains("critic", Qt::CaseInsensitive) || stringType.contains("fatal", Qt::CaseInsensitive)) return LogType::LogError; else if (stringType.contains(">>>", Qt::CaseInsensitive) || stringType.contains("output", Qt::CaseInsensitive)) return LogType::LogOutput; else if (stringType.contains("<<<", Qt::CaseInsensitive) || stringType.contains("input", Qt::CaseInsensitive)) return LogType::LogInput; return LogType::LogInfo; } //============================================================================== Logger::Logger(QObject *parent) : QObject{parent} { if ( !QMetaType::isRegistered( qMetaTypeId() ) ) qRegisterMetaType(); QCoreApplication *application = QCoreApplication::instance(); if (application) application->setProperty("loggerRun", true); } //============================================================================== Logger::~Logger() { stop(); QCoreApplication *application = QCoreApplication::instance(); if (application) application->setProperty("loggerRun", false); } //============================================================================== //============================================================================== // // LogWorker - internal class // //============================================================================== LogWorker::LogWorker(const QString &logFilePath, QObject *parent) : QObject{parent} { file.setFileName(logFilePath); } //============================================================================== LogWorker::~LogWorker() { stop(); } //============================================================================== void LogWorker::start() { if (running) return; if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { qCritical() << "Не удалось открыть файл для записи: " << file.fileName(); return; } linesPosition = 0; linesCounter = 0; startTime = QDateTime::currentDateTime(); writeFirstLines(); writeLastLines(); timer = new QTimer(nullptr); timer->setInterval(queueIntervalMs); timer->setSingleShot(false); connect(timer, &QTimer::timeout, this, &LogWorker::checkQueue); timer->start(); running = true; emit started(); } //============================================================================== void LogWorker::stop() { if (!running) return; QMutexLocker locker(&mutex); running = false; if (timer) { timer->stop(); delete timer; timer = nullptr; } if (file.isOpen()) { writeAllFromQueue(); writeLastLines(); file.flush(); file.close(); } emit stopped(); } //============================================================================== void LogWorker::writeLog(const QString &msg, int type, const QString &objectName) { if (!running) return; QMutexLocker lock(&mutex); LogRecord rec {type, msg, objectName, QDateTime::currentDateTime()}; queue.enqueue(rec); } //============================================================================== void LogWorker::checkQueue() { if (!running) return; if (!mutex.tryLock()) return; writeAllFromQueue(); mutex.unlock(); } //============================================================================== void LogWorker::writeFirstLines() { if (!file.isOpen()) return; writeLine( tr("----- Начало %1 -") .arg(startTime.toString("yyyy-MM-dd HH:mm:ss")) .leftJustified(80,'-'), -1); QString runMode; if (Application::isDebug()) { runMode = "DEBUG"; } if (Application::isPortable()) { if (!runMode.isEmpty()) runMode += ", "; runMode += "PORTABLE"; } if (runMode.isEmpty()) runMode = "NORMAL"; writeLine(tr("ПО: %1").arg(QCoreApplication::applicationName()), -1); writeLine(tr("Версия: %1").arg(QCoreApplication::applicationVersion()), -1); writeLine(tr("Разработчик: %1 %2") .arg( QCoreApplication::organizationName(), QCoreApplication::organizationDomain().isEmpty() ? QString() : QString("(" + QCoreApplication::organizationDomain() + ")") ), -1 ); writeLine(tr("Строка запуска: '%1'").arg( QCoreApplication::arguments().join(' ') ), -1); writeLine(tr("Режим запуска: %1").arg(runMode), -1); if (file.write(QByteArray(1,'\n')) == 1) { ++linesCounter; ++linesPosition; } file.flush(); } //============================================================================== void LogWorker::writeLastLines() { if (!file.isOpen()) return; if ((file.pos() != linesPosition) && !file.seek(linesPosition)) { return; } QDateTime endTime = QDateTime::currentDateTime(); qint64 n = endTime.toMSecsSinceEpoch() - startTime.toMSecsSinceEpoch(); qint64 hr = n / 3600000; qint64 ms = n - 3600000 * hr; qint64 min = ms / 60000; ms = ms - 60000 * min; qint64 sec = ms / 1000; ms = ms - 1000 * sec; file.write(QByteArray(1,'\n')); writeLine(tr("Текущее состояние: %1") .arg( running ? "в процессе работы" : "работа завершена" ), -2 ); writeLine(tr("Общее время: %1 ч, %2 мин, %3 сек, %4 мсек") .arg(hr).arg(min).arg(sec).arg(ms), -2 ); writeLine(tr("Общее количество строк: %1") .arg( linesCounter + 5 ), -2 ); writeLine(tr("----- Окончание %1 -") .arg( QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss") ) .leftJustified(80,'-'), -2); file.flush(); } //============================================================================== void LogWorker::writeLine(const QString &msg, int type, const QString &objectName, const QDateTime &dateTime) { QByteArray data { QString( dateTime.toString("[HH:mm:ss.zzz] ") + QString(type < 0 ? "[sys]" : Logger::logTypeToString(static_cast(type))) + " " + QString(objectName.isEmpty() ? QString() : "{" + objectName + "} ") + msg + "\n" ).toUtf8() }; qint64 bytesWrite {0}; int cnt {3}; while ((bytesWrite < data.size()) && (cnt > 0)) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) qint64 n = file.write(data.mid(bytesWrite)); #else qint64 n = file.write(data.mid(static_cast(bytesWrite))); #endif if (n > 0) { bytesWrite += n; } else { --cnt; } } if (type < -1) return; linesPosition += bytesWrite; if (bytesWrite == data.size()) ++linesCounter; } //============================================================================== void LogWorker::writeAllFromQueue() { if (queue.isEmpty()) return; if (!file.isOpen()) { queue.clear(); return; } if ((file.pos() != linesPosition) && !file.seek(linesPosition)) { return; } while (!queue.isEmpty()) { LogRecord rec = queue.dequeue(); writeLine( rec.msg, rec.type, rec.objectName, rec.dateTime ); } writeLastLines(); } //============================================================================== // // FunctionLogger // //============================================================================== FunctionLogger::FunctionLogger(const char *file, int line, const char *name, const QLoggingCategory &category) : functionName{name} , logCategory{category.categoryName()} { id = counter++; if (logCategory.isEmpty()) logCategory = "Function"; quint64 threadId = reinterpret_cast(QThread::currentThreadId()); qDebug() << QString("{%1} ID: %2, Thread: %3, File: '%4', Line: %5, Entering function: %6") .arg(logCategory) .arg(id) .arg(threadId) .arg(file) .arg(line) .arg(functionName) .toStdString().c_str(); } //============================================================================== FunctionLogger::~FunctionLogger() { qDebug() << QString("{%1} ID: %2, Exiting function: %3") .arg(logCategory) .arg(id) .arg(functionName) .toStdString().c_str(); } //==============================================================================