Summary of Qt custom log class

I. Introduction

The good log tools in C++ include log4cxx, log4qt, etc., but none of them can be combined with qDebug(), qInfo(), etc., so it is not comfortable to use in Qt. Thanks to Qt for providing qInstallMessageHandler() Function, use this function to install a custom log output processing function, and output the log to a file, console, etc. For specific usage, please refer to the help documentation of Qt.

This article mainly introduces the use of qInstallMessageHandler() to implement a simple log tool, such as calling qDebug() << "Hi", the output content will be output to the log file and console at the same time, and if the log file is not created on the same day, it will be used Its creation date is backed up, and the files involved are:

  • main.cpp: usage example
  • LogHandler.h: Header file for custom log-related classes
  • LogHandler.cpp: The implementation file of custom log-related classes
    implements additional functions:
  • Create a new log file after a single log file is larger than 5M, for example;
  • Delete logs older than 30 days;
  • Use locks to ensure multi-thread safety.
    Later consider implementing functions:
  • The relevant configuration data of the log, such as the output directory, is written to the configuration file;
  • Logs can optionally be stored on the server.

2. Code implementation

2.1 LogHandler.h

#ifndef LOGHANDLER_H
#define LOGHANDLER_H

#include <iostream>
#include <QDebug>
#include <QDateTime>
#include <QMutexLocker>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QTimer>
#include <QTextStream>
#include <QTextCodec>

const int g_logLimitSize = 5;

struct LogHandlerPrivate {
    
    
    LogHandlerPrivate();
    ~LogHandlerPrivate();

    // 打开日志文件 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

2.2 LogHandler.cpp

#include "LogHandler.h"

/************************************************************************************************************
 *                                                                                                          *
 *                                               LogHandlerPrivate                                          *
 *                                                                                                          *
 ***********************************************************************************************************/
// 初始化 static 变量
QMutex LogHandlerPrivate::logMutex;
QFile* LogHandlerPrivate::logFile = nullptr;
QTextStream* LogHandlerPrivate::logOut = nullptr;

LogHandlerPrivate::LogHandlerPrivate() {
    
    
    logDir.setPath("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 *  2); // 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() == "")
            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;

    if (nullptr == LogHandlerPrivate::logOut) {
    
    
        return;
    }

    // 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息
    QString fileName = context.file;
    int index = fileName.lastIndexOf(QDir::separator());
    fileName = fileName.mid(index + 1);

    (*LogHandlerPrivate::logOut) << QString("%1 - [%2] (%3:%4, %5): %6\n")
                                    .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(level)
                                    .arg(fileName).arg(context.line).arg(context.function).arg(msg);
}

/************************************************************************************************************
 *                                                                                                          *
 *                                               LogHandler                                                 *
 *                                                                                                          *
 ***********************************************************************************************************/
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;
}

2.3 main.cpp

#include "LogHandler.h"

#include <QApplication>
#include <QDebug>
#include <QTime>
#include <QPushButton>

int main(int argc, char *argv[]) {
    
    
    QApplication app(argc, argv);

    // [[1]] 安装消息处理函数
    LogHandler::Get().installMessageHandler();

    // [[2]] 输出测试,查看是否写入到文件
    qDebug() << "Hello";
    qDebug() << "当前时间是: " << QTime::currentTime().toString("hh:mm:ss");
    qInfo() << QString("God bless you!");

    QPushButton *button = new QPushButton("退出");
    button->show();
    QObject::connect(button, &QPushButton::clicked, [&app] {
    
    
        qDebug() << "退出";
        app.quit();
    });

    // [[3]] 取消安装自定义消息处理,然后启用
    LogHandler::Get().uninstallMessageHandler();
    qDebug() << "........"; // 不写入日志
    LogHandler::Get().installMessageHandler();

    int ret = app.exec(); // 事件循环结束

    // [[4]] 程序结束时释放 LogHandler 的资源,例如刷新并关闭日志文件
    LogHandler::Get().uninstallMessageHandler();

    return ret;
}

2.4 Operation effect

Console output:

Hello
当前时间是:  "16:29:42"
"God bless you!"
........
退出

Log file (log.txt under the log directory of the directory where the exe is located):

16:29:42 - [Debug] (main.cpp:15, int main(int, char **)): Hello
16:29:42 - [Debug] (main.cpp:16, int main(int, char **)): 当前时间是:  "16:29:42"
16:29:42 - [Info ] (main.cpp:17, int main(int, char **)): "God bless you!"
16:29:46 - [Debug] (main.cpp:22, auto main(int, char **)::(anonymous class)::operator()() const): 退出

Note:
The Release version does not include the file name, function name, and line number information by default. You need to add a line of code to the .pro file, and it will take effect after running make again.

DEFINES += QT_MESSAGELOGCONTEXT

3. Other C++ logging frameworks

There are many logging frameworks in C++, among which the famous ones are:

  • log4cxx: The C++ porting version of the well-known Log4j in the Java community, which is used to provide logging functions for C++
    programs, so that developers can debug and audit the target program.
  • log4cplus: A simple-to-use C++ logging API that provides thread-safe, flexible, and arbitrary-grained control over log management and configuration (also based on Log4j
    ).
  • Log4cpp: A C++ class library for flexible logging to file, syslog, IDSA and other destinations (also based on Log4j).
  • google-glog: An application-level logging framework in C++ language, which provides C++-style stream operations and various auxiliary macros.
  • Pantheios: A type-safe, efficient, generic and extensible C++ logging API library (known as the fastest logging library in the C++ field).
  • POCO: Also provides a good logging support documentation.
  • ACE: ACE also has logging support.
  • Boost.Log: Designed to be very modular and extensible.
  • Easylogging++: Lightweight high-performance C++ logging library (only one header file).
  • G3log: An open source, cross-platform asynchronous C++ logging framework that supports custom log formats. Based on g2log, it improves performance and supports custom formats.
  • Plog: Portable, simple and extensible logging library for C++.
  • spdlog: A fast C++ logging library that only includes header files and is compatible with C++11.

Guess you like

Origin blog.csdn.net/houxian1103/article/details/129921653