606 lines
18 KiB
C++
606 lines
18 KiB
C++
/****************************************************************************
|
||
** Copyright (c) 2025 Evgeny Teterin (nayk) <nayk@nxt.ru>
|
||
** 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 <QtCore/QMetaType>
|
||
#include <QtCore/QMetaObject>
|
||
#include <QtCore/QCoreApplication>
|
||
#include <QtCore/QDir>
|
||
#include <QtCore/QDebug>
|
||
#include <QtCore/QDateTime>
|
||
#include <QtCore/QMutexLocker>
|
||
#include <QtCore/QMetaEnum>
|
||
#include <QtCore/QTextStream>
|
||
#include <iostream>
|
||
|
||
#include "log_worker.h"
|
||
#include "application_config.h"
|
||
|
||
//==============================================================================
|
||
std::atomic<size_t> 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<Logger::LogType>();
|
||
const char* name = metaEnum.valueToKey(logType);
|
||
|
||
if (!name) {
|
||
|
||
return "[inf]";
|
||
}
|
||
|
||
return logTypeToString( static_cast<LogType>(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<int>(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<Logger::LogType>() ) )
|
||
qRegisterMetaType<Logger::LogType>();
|
||
|
||
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<quint8>(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<int>(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<quint64>(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();
|
||
}
|
||
//==============================================================================
|
||
|