From d4d18d81a0840383ce2afb340cbc714b852975bb Mon Sep 17 00:00:00 2001 From: tianzhendong <1203886034@qq.com> Date: Tue, 26 Dec 2023 16:33:45 +0800 Subject: [PATCH] v1.0.0 --- CMakeLists.txt | 7 ++ MyLogger.pri | 7 ++ README.md | 68 ++++++++++++++ Singleton.h | 91 ++++++++++++++++++ loghandler.cpp | 245 +++++++++++++++++++++++++++++++++++++++++++++++++ loghandler.h | 65 +++++++++++++ 6 files changed, 483 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 MyLogger.pri create mode 100644 README.md create mode 100644 Singleton.h create mode 100644 loghandler.cpp create mode 100644 loghandler.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..799b660 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,7 @@ +project(MyLogger) + +add_library(${PROJECT_NAME} ${PROJECT_SOURCE_DIR}/loghandler.cpp) + +target_link_libraries(${PROJECT_NAME} + Qt5::Core + ) \ No newline at end of file diff --git a/MyLogger.pri b/MyLogger.pri new file mode 100644 index 0000000..f1a99b1 --- /dev/null +++ b/MyLogger.pri @@ -0,0 +1,7 @@ +INCLUDEPATH += $$PWD/ + +#在release下输出debug信息 +DEFINES += QT_MESSAGELOGCONTEXT + +SOURCES += \ + $$PWD/loghandler.cpp diff --git a/README.md b/README.md new file mode 100644 index 0000000..9191977 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +## 概述 + +- Singleton.h + +懒汉模式的单例模板类 + +- loghandler.h/cpp + +日志类 + +- MyLogger.pri + +子模块qt pri文件 + +## 使用 + +### 克隆代码 + +克隆代码并复制到工程Libs目录下 + +### 添加子工程 + +在主工程pro文件中,添加: + +```pro +include($$PWD/Libs/MyLogger/MyLogger.pri) +``` + +### 使用 + +#### 日志使用 + +```c++ +#include +//头文件 +#include "loghandler.h" +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + //安装 + LogHandler::Get().installMessageHandler(); + Debug() << "Hello"; + qDebug() << "当前时间是: " << QTime::currentTime().toString("hh:mm:ss"); + qInfo() << QString("God bless you!"); + //卸载 + LogHandler::Get().uninstallMessageHandler(); + return a.exec(); +} +``` + + + +#### 单例模板类使用 + +```c++ +#include "Singleton.h" +class AppConfig : public QObject +{ + Q_OBJECT + //通过宏 + SINGLETON(AppConfig); + ..... +} + +``` + + + diff --git a/Singleton.h b/Singleton.h new file mode 100644 index 0000000..d23e7c1 --- /dev/null +++ b/Singleton.h @@ -0,0 +1,91 @@ +#ifndef SINGLETON_H +#define SINGLETON_H + +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +/// /// +/// Singleton signature /// +/// /// +//////////////////////////////////////////////////////////////////////////////// +/** + * 使用方法: + * 1. 定义类为单例: + * class ConnectionPool { + * SINGLETON(ConnectionPool) // Here + * public: + * + * 2. 实现无参构造函数,析构函数 + * 3. 获取单例类的对象: + * Singleton::getInstance(); + * ConnectionPool &pool = Singleton::getInstance(); + * 注意: 如果单例的类需要释放的资源和 Qt 底层的信号系统有关系,例如 QSettings,QSqlDatabase 等, + * 需要在程序结束前手动释放(也就是在 main() 函数返回前调用释放资源的函数), + * 否则有可能在程序退出时报系统底层的信号错误,导致如 QSettings 的数据没有保存。 + */ +template +class Singleton +{ +public: + static T& getInstance() + { + static T instance; + return instance; + }; + + Singleton(T &&) = delete; + Singleton(const T &) = delete; + void operator = (const T &) = delete; + T *operator &() = delete; + +private: + Singleton() = default; + virtual ~Singleton() = default; +// static QMutex mutex; +// static QScopedPointer instance; +}; + +//////////////////////////////////////////////////////////////////////////////// +/// /// +/// Singleton definition /// +/// /// +//////////////////////////////////////////////////////////////////////////////// +#if 0 +//template QMutex Singleton::mutex; +//template QScopedPointer Singleton::instance; + +//template +//T& Singleton::getInstance() +//{ +// if (instance.isNull()) +// { +// mutex.lock(); +// if (instance.isNull()) +// { +// instance.reset(new T()); +// } +// mutex.unlock(); +// } + +// return *instance.data(); +//} +#endif + +//////////////////////////////////////////////////////////////////////////////// +/// /// +/// Singleton Macro /// +/// /// +//////////////////////////////////////////////////////////////////////////////// +#if 0 +//#define SINGLETON(Class) \ +//private: \ +// Class(); \ +// ~Class(); \ +// Class(const Class &other); \ +// Class& operator=(const Class &other); \ +// friend class Singleton; \ +// friend struct QScopedPointerDeleter; +#endif +#define SINGLETON(Class) friend class Singleton +#endif // SINGLETON_H diff --git a/loghandler.cpp b/loghandler.cpp new file mode 100644 index 0000000..1174156 --- /dev/null +++ b/loghandler.cpp @@ -0,0 +1,245 @@ +#include "loghandler.h" + +#include + +// 初始化 static 变量 +QMutex LogHandlerPrivate::logMutex; +QFile* LogHandlerPrivate::logFile = nullptr; +QTextStream* LogHandlerPrivate::logOut = nullptr; + +LogHandlerPrivate::LogHandlerPrivate() +{ + auto localApplicationDirPath = QCoreApplication::applicationDirPath(); + logDir.setPath(localApplicationDirPath + "/log"); // TODO: 日志文件夹的路径,为 exe 所在目录下的 log 文件夹,可从配置文件读取 + QString logPath = logDir.absoluteFilePath("today.log"); // 获取日志的路径 + + // ========获取日志文件创建的时间======== + // QFileInfo::created(): On most Unix systems, this function returns the time of the last status change. + // 所以不能运行时使用这个函数检查创建时间,因为会在运行时变化,于是在程序启动时保存下日志文件的最后修改时间, + logFileCreatedDate = QFileInfo(logPath).lastModified().date(); // 若日志文件不存在,返回nullptr + + // 打开日志文件,如果不是当天创建的,备份已有日志文件 + openAndBackupLogFile(); + + // 十分钟检查一次日志文件创建时间 + renameLogFileTimer.setInterval(1000 * 10); // TODO: 可从配置文件读取 + renameLogFileTimer.start(); + QObject::connect(&renameLogFileTimer, &QTimer::timeout, [this] + { + QMutexLocker locker(&LogHandlerPrivate::logMutex); + openAndBackupLogFile(); // 打开日志文件 + checkLogFiles(); // 检测当前日志文件大小 + autoDeleteLog(); // 自动删除30天前的日志 + }); + + // 定时刷新日志输出到文件,尽快的能在日志文件里看到最新的日志 + flushLogFileTimer.setInterval(1000); // TODO: 可从配置文件读取 + flushLogFileTimer.start(); + QObject::connect(&flushLogFileTimer, &QTimer::timeout, [] + { + // qDebug() << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); // 测试不停的写入内容到日志文件 + QMutexLocker locker(&LogHandlerPrivate::logMutex); + if (nullptr != logOut) + { + logOut->flush(); + } + }); +} + +LogHandlerPrivate::~LogHandlerPrivate() +{ + if (nullptr != logFile) + { + logFile->flush(); + logFile->close(); + delete logOut; + delete logFile; + + // 因为他们是 static 变量 + logOut = nullptr; + logFile = nullptr; + } +} + +// 打开日志文件 log.txt,如果不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt +void LogHandlerPrivate::openAndBackupLogFile() +{ + // 总体逻辑: + // 1. 程序启动时 logFile 为 nullptr,初始化 logFile,有可能是同一天打开已经存在的 logFile,所以使用 Append 模式 + // 2. logFileCreatedDate is nullptr, 说明日志文件在程序开始时不存在,所以记录下创建时间 + // 3. 程序运行时检查如果 logFile 的创建日期和当前日期不相等,则使用它的创建日期重命名,然后再生成一个新的 log.txt 文件 + // 4. 检查日志文件超过 LOGLIMIT_NUM 个,删除最早的 + // 备注:log.txt 始终为当天的日志文件,当第二天,会执行第3步,将使用 log.txt 的创建日期重命名它 + + // 如果日志所在目录不存在,则创建 + if (!logDir.exists()) + { + logDir.mkpath("."); // 可以递归的创建文件夹 + } + QString logPath = logDir.absoluteFilePath("today.log"); // log.txt的路径 + + // [[1]] 程序每次启动时 logFile 为 nullptr + if (logFile == nullptr) + { + logFile = new QFile(logPath); + logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) ? new QTextStream(logFile) : nullptr; + if (logOut != nullptr) + logOut->setCodec("UTF-8"); + + // [[2]] 如果文件是第一次创建,则创建日期是无效的,把其设置为当前日期 + if (logFileCreatedDate.isNull()) + { + logFileCreatedDate = QDate::currentDate(); + } + } + + // [[3]] 程序运行时如果创建日期不是当前日期,则使用创建日期重命名,并生成一个新的 log.txt + if (logFileCreatedDate != QDate::currentDate()) + { + logFile->flush(); + logFile->close(); + delete logOut; + delete logFile; + + QString newLogPath = logDir.absoluteFilePath(logFileCreatedDate.toString("yyyy-MM-dd.log"));; + QFile::copy(logPath, newLogPath); // Bug: 按理说 rename 会更合适,但是 rename 时最后一个文件总是显示不出来,需要 killall Finder 后才出现 + QFile::remove(logPath); // 删除重新创建,改变创建时间 + + // 重新创建 log.txt + logFile = new QFile(logPath); + logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ? new QTextStream(logFile) : nullptr; + logFileCreatedDate = QDate::currentDate(); + if (logOut != nullptr) + logOut->setCodec("UTF-8"); + } +} + +// 检测当前日志文件大小 +void LogHandlerPrivate::checkLogFiles() +{ + // 如果 protocal.log 文件大小超过5M,重新创建一个日志文件,原文件存档为yyyy-MM-dd_hhmmss.log + if (logFile->size() > 1024 * g_logLimitSize) + { + logFile->flush(); + logFile->close(); + delete logOut; + delete logFile; + + QString logPath = logDir.absoluteFilePath("today.log"); // 日志的路径 + QString newLogPath = logDir.absoluteFilePath(logFileCreatedDate.toString("yyyy-MM-dd.log")); + QFile::copy(logPath, newLogPath); // Bug: 按理说 rename 会更合适,但是 rename 时最后一个文件总是显示不出来,需要 killall Finder 后才出现 + QFile::remove(logPath); // 删除重新创建,改变创建时间 + + logFile = new QFile(logPath); + logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ? new QTextStream(logFile) : NULL; + logFileCreatedDate = QDate::currentDate(); + if (logOut != nullptr) + logOut->setCodec("UTF-8"); + } +} + +// 自动删除30天前的日志 +void LogHandlerPrivate::autoDeleteLog() +{ + QDateTime now = QDateTime::currentDateTime(); + + // 前30天 + QDateTime dateTime1 = now.addDays(-30); + QDateTime dateTime2; + + QString logPath = logDir.absoluteFilePath("today.log"); // 日志的路径 + QDir dir(logPath); + QFileInfoList fileList = dir.entryInfoList(); + foreach (QFileInfo f, fileList ) + { + // "."和".."跳过 + if (f.baseName() == "" || f.baseName() == "today" || f.baseName() == "today.log") + continue; + + dateTime2 = QDateTime::fromString(f.baseName(), "yyyy-MM-dd"); + if (dateTime2 < dateTime1) // 只要日志时间小于前30天的时间就删除 + { + dir.remove(f.absoluteFilePath()); + } + } +} + +// 消息处理函数 +void LogHandlerPrivate::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + QMutexLocker locker(&LogHandlerPrivate::logMutex); + QString level; + + switch (type) + { + case QtDebugMsg: + level = "DEBUG"; + break; + case QtInfoMsg: + level = "INFO "; + break; + case QtWarningMsg: + level = "WARN "; + break; + case QtCriticalMsg: + level = "ERROR"; + break; + case QtFatalMsg: + level = "FATAL"; + break; + default: + break; + } + + // 输出到标准输出: Windows 下 std::cout 使用 GB2312,而 msg 使用 UTF-8,但是程序的 Local 也还是使用 UTF-8 +#if defined(Q_OS_WIN) + QByteArray localMsg = QTextCodec::codecForName("GB2312")->fromUnicode(msg); //msg.toLocal8Bit(); +#else + QByteArray localMsg = msg.toLocal8Bit(); +#endif + +// std::cout << std::string(localMsg) << std::endl; +// qSetMessagePattern("%{appname} %{type} %{time [yyyy-MM-dd hh:mm:ss]} %{file} %{line} %{function} %{message}"); + + + if (nullptr == LogHandlerPrivate::logOut) + { + return; + } + + // 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息 + QString fileName = context.file; + int index = fileName.lastIndexOf(QDir::separator()); + fileName = fileName.mid(index + 1); + QString msgOut = QString("%1 - [%2] (%3:%4, %5): %6") + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(level) + .arg(fileName).arg(context.line).arg(context.function).arg(msg); + std::cout << msgOut.toLocal8Bit().constData() << std::endl; + (*LogHandlerPrivate::logOut) << msgOut << "\n"; +} + +LogHandler::LogHandler() : d(nullptr) +{ +} + +// 给Qt安装消息处理函数 +void LogHandler::installMessageHandler() +{ + QMutexLocker locker(&LogHandlerPrivate::logMutex); // 类似C++11的lock_guard,析构时自动解锁 + + if (nullptr == d) + { + d = new LogHandlerPrivate(); + qInstallMessageHandler(LogHandlerPrivate::messageHandler); // 给 Qt 安装自定义消息处理函数 + } +} + +// 取消安装消息处理函数并释放资源 +void LogHandler::uninstallMessageHandler() +{ + QMutexLocker locker(&LogHandlerPrivate::logMutex); + + qInstallMessageHandler(nullptr); + delete d; + d = nullptr; +} diff --git a/loghandler.h b/loghandler.h new file mode 100644 index 0000000..2dfc3a2 --- /dev/null +++ b/loghandler.h @@ -0,0 +1,65 @@ +#ifndef LOGHANDLER_H +#define LOGHANDLER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Singleton.h" + +//#define log_d(format,...) qDebug(format,__VA_ARGS__) +//#define log_i(format,...) qInfo(format,__VA_ARGS__) +//#define log_w(format,...) qWarning(format,__VA_ARGS__) +//#define log_e(format,...) qCritical(format,__VA_ARGS__) +//#define log_f(format,...) qFatal(format,__VA_ARGS__) + +const int g_logLimitSize = 5; + +struct LogHandlerPrivate +{ + LogHandlerPrivate(); + ~LogHandlerPrivate(); +// qDebug() + // 打开日志文件 log.txt,如果日志文件不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt + void openAndBackupLogFile(); + void checkLogFiles(); // 检测当前日志文件大小 + void autoDeleteLog(); // 自动删除30天前的日志 + + // 消息处理函数 + static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); + + QDir logDir; // 日志文件夹 + QTimer renameLogFileTimer; // 重命名日志文件使用的定时器 + QTimer flushLogFileTimer; // 刷新输出到日志文件的定时器 + QDate logFileCreatedDate; // 日志文件创建的时间 + + static QFile *logFile; // 日志文件 + static QTextStream *logOut; // 输出日志的 QTextStream,使用静态对象就是为了减少函数调用的开销 + static QMutex logMutex; // 同步使用的 mutex +}; + +class LogHandler +{ +public: + void installMessageHandler(); // 给Qt安装消息处理函数 + void uninstallMessageHandler(); // 取消安装消息处理函数并释放资源 + + static LogHandler& Get() + { + static LogHandler m_logHandler; + return m_logHandler; + } + +private: + LogHandler(); + + LogHandlerPrivate *d; +}; + +#endif // LOGHANDLER_H