Qt 自定义 log 日志

Qt 自定义日志工具

Qt

Qt 自定义 log 日志_第1张图片
C++ 中比较不错的日志工具有 log4cxx,log4qt 等,但是它们都不能和 qDebug(), qInfo() 等有机的结合在一起,所以在 Qt 中使用总觉得不够舒服,感谢 Qt 提供了 qInstallMessageHandler() 这个函数,使用这个函数可以安装自定义的日志输出处理函数,把日志输出到文件,控制台等,具体的使用可以查看 Qt 的帮助文档。

本文主要是介绍使用 qInstallMessageHandler() 实现一个简单的日志工具,例如调用 qDebug() << “Hi”,输出的内容会同时输出到日志文件和控制台,并且日志文件如果不是当天创建的,会使用它的创建日期备份起来,涉及到的文件有:

kcLog.pro:工程
main.cpp: 使用示例
Singleton.h: 单例模版
kcLog.h: 自定义日志相关类的头文件
kcLog.cpp: 自定义日志相关类的实现文件

工程下载地址:
https://download.csdn.net/download/sirkang/12418621

定义 QT_MESSAGELOGCONTEXT

qDebug 其实是一个宏: #define qDebug QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).debug,在 Debug 版本的时候会输出行号,文件名,函数名等,但是在 Release 版本的时候不会输出,为了输出它们,需要在 .pro 文件里加入下面的定义:

DEFINES += QT_MESSAGELOGCONTEXT

kcLog.pro

QT -= core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11
#打印日志方式
CONFIG += console

# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
DEFINES += QT_MESSAGELOGCONTEXT

# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
        kcLog.cpp \
        main.cpp

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

HEADERS += \
    Singleton.h \
    kcLog.h

main.cpp

#include 
#include 
#include "kcLog.h"

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

    //安装消息处理函数
    Singleton::getInstance().installMessageHandler();
    //输出测试,查看是否写入到文件(写入)

    qDebug("qDebug:安装消息处理函数,写入到文件!");
    qInfo("qInfo:安装消息处理函数,写入到文件!");
    qWarning("qWarning:安装消息处理函数,写入到文件!");
    qCritical("qCritical:安装消息处理函数,写入到文件!");
    //    qFatal("qFatal:安装消息处理函数,写入到文件!");  // 写入该行直接致命错误!,以下代码不执行!

    //卸载消息处理函数
    Singleton::getInstance().uninstallMessageHandler();
    //输出测试,查看是否写入到文件(不写入)
    qDebug() << "qDebug:卸载消息处理函数,不写入到文件!";
    qInfo() << "qInfo:卸载消息处理函数,不写入到文件!";

    //再次安装消息处理函数
    Singleton::getInstance().installMessageHandler();
    //输出测试,查看是否写入到文件(写入)
    qDebug() << "qDebug:再次安装消息处理函数,写入到文件!";
    qInfo() << "qInfo:再次安装消息处理函数,写入到文件!";

    return app.exec();
}

kcLog.cpp

/******************************************************************************************
 * loghanfler.cpp
 * by kangchuang
 * time:20200514
 *
 * 使用方法如下:

#include "kcLog.h"
#include 
#include 

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

    //安装消息处理函数
    Singleton::getInstance().installMessageHandler();
    //输出测试,查看是否写入到文件(写入)

    qDebug("qDebug:安装消息处理函数,写入到文件!");
    qInfo("qInfo:安装消息处理函数,写入到文件!");
    qWarning("qWarning:安装消息处理函数,写入到文件!");
    qCritical("qCritical:安装消息处理函数,写入到文件!");
    //    qFatal("qFatal:安装消息处理函数,写入到文件!");  // 写入该行直接致命错误!,以下代码不执行!

    //卸载消息处理函数
    Singleton::getInstance().uninstallMessageHandler();
    //输出测试,查看是否写入到文件(不写入)
    qDebug() << "qDebug:卸载消息处理函数,不写入到文件!";
    qInfo() << "qInfo:卸载消息处理函数,不写入到文件!";

    //再次安装消息处理函数
    Singleton::getInstance().installMessageHandler();
    //输出测试,查看是否写入到文件(写入)
    qDebug() << "qDebug:再次安装消息处理函数,写入到文件!";
    qInfo() << "qInfo:再次安装消息处理函数,写入到文件!";

    return app.exec();
}
******************************************************************************************/
#include "kcLog.h"

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/************************************************************************************************************
 *                                                                                                          *
 *                                               kcLogPrivate                                          *
 *                                                                                                          *
 ***********************************************************************************************************/
struct kcLogPrivate {
    kcLogPrivate();
    ~kcLogPrivate();

    // 打开日志文件 log.txt,如果日志文件不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt
    void openAndBackupLogFile();

    // 消息处理函数
    static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);

    // 如果日志所在目录不存在,则创建
    void makeSureLogDirectory() const;

    QDir logDir;                // 日志文件夹
    QTimer renameLogFileTimer;  // 重命名日志文件使用的定时器
    QTimer flushLogFileTimer;   // 刷新输出到日志文件的定时器
    QDate logFileCreatedDate;   // 日志文件创建的时间

    static QFile *logFile;       // 日志文件
    static QTextStream *logOut;  // 输出日志的 QTextStream,使用静态对象就是为了减少函数调用的开销
    static QMutex logMutex;      // 同步使用的 mutex
};

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

kcLogPrivate::kcLogPrivate() {
    logDir.setPath("log");                                 // TODO: 日志文件夹的路径,为 exe 所在目录下的 log 文件夹,可从配置文件读取
    QString logPath = logDir.absoluteFilePath("log.txt");  // 日志的路径
    // 日志文件创建的时间
    // QFileInfo::created(): On most Unix systems, this function returns the time of the last status change.
    // 所以不能运行时使用这个函数检查创建时间,因为会在运行时变化,于是在程序启动时保存下日志文件的最后修改时间,
    // 在后面判断如果不是今天则用于重命名 log.txt
    // 如果是 Qt 5.10 后,lastModified() 可以使用 birthTime() 代替
    logFileCreatedDate = QFileInfo(logPath).lastModified().date();

    // 打开日志文件,如果不是当天创建的,备份已有日志文件
    openAndBackupLogFile();

    // 十分钟检查一次日志文件创建时间
    renameLogFileTimer.setInterval(1000 * 60 * 10);  // TODO: 可从配置文件读取
    // renameLogFileTimer.setInterval(1000); // 为了快速测试看到日期变化后是否新创建了对应的日志文件,所以 1 秒检查一次
    renameLogFileTimer.start();
    QObject::connect(&renameLogFileTimer, &QTimer::timeout, [this] {
        QMutexLocker locker(&kcLogPrivate::logMutex);
        openAndBackupLogFile();
    });

    // 定时刷新日志输出到文件,尽快的能在日志文件里看到最新的日志
    flushLogFileTimer.setInterval(1000);  // TODO: 可从配置文件读取
    flushLogFileTimer.start();
    QObject::connect(&flushLogFileTimer, &QTimer::timeout, [] {
        // qDebug() << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); // 测试不停的写入内容到日志文件
        QMutexLocker locker(&kcLogPrivate::logMutex);
        if (nullptr != logOut) {
            logOut->flush();
        }
    });
}

kcLogPrivate::~kcLogPrivate() {
    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 kcLogPrivate::openAndBackupLogFile() {
    // 总体逻辑:
    // 1. 程序启动时 logFile 为 nullptr,初始化 logFile,有可能是同一天打开已经存在的 logFile,所以使用 Append 模式
    // 2. logFileCreatedDate is nullptr, 说明日志文件在程序开始时不存在,所以记录下创建时间
    // 3. 程序运行时检查如果 logFile 的创建日期和当前日期不相等,则使用它的创建日期重命名,然后再生成一个新的 log.txt 文件

    makeSureLogDirectory();                                // 如果日志所在目录不存在,则创建
    QString logPath = logDir.absoluteFilePath("log.txt");  // 日志的路径

    // [[1]] 程序启动时 logFile 为 nullptr
    if (nullptr == logFile) {
        logFile = new QFile(logPath);
        logOut  = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) ? new QTextStream(logFile) : nullptr;

        if (nullptr != logOut) {
            logOut->setCodec("UTF-8");
        }

        // [[2]] 如果文件是第一次创建,则创建日期是无效的,把其设置为当前日期
        if (logFileCreatedDate.isNull()) {
            logFileCreatedDate = QDate::currentDate();
        }

        // TODO: 可以检查日志文件超过 30 个,删除 30 天前的日志文件
    }

    // [[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);            // 删除重新创建,改变创建时间

        logFile            = new QFile(logPath);
        logOut             = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ? new QTextStream(logFile) : nullptr;
        logFileCreatedDate = QDate::currentDate();

        if (nullptr != logOut) {
            logOut->setCodec("UTF-8");
        }
    }
}

// 如果日志所在目录不存在,则创建
void kcLogPrivate::makeSureLogDirectory() const {
    if (!logDir.exists()) {
        logDir.mkpath(".");  // 可以递归的创建文件夹
    }
}

// 消息处理函数
void kcLogPrivate::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
    QMutexLocker locker(&kcLogPrivate::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 == kcLogPrivate::logOut) {
        return;
    }

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

    (*kcLogPrivate::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);
}

/************************************************************************************************************
 *                                                                                                          *
 *                                               kcLog                                                 *
 *                                                                                                          *
 ***********************************************************************************************************/
kcLog::kcLog() : d(nullptr) {
}

kcLog::~kcLog() {
}

void kcLog::installMessageHandler() {
    QMutexLocker locker(&kcLogPrivate::logMutex);

    if (nullptr == d) {
        d = new kcLogPrivate();
        qInstallMessageHandler(kcLogPrivate::messageHandler);  // 给 Qt 安装自定义消息处理函数
    }
}

void kcLog::uninstallMessageHandler() {
    QMutexLocker locker(&kcLogPrivate::logMutex);
    qInstallMessageHandler(nullptr);
    delete d;
    d = nullptr;
}

kcLog.h

#ifndef kcLog_H
#define kcLog_H

#include "Singleton.h"

#define kcLogInstance Singleton::getInstance()

struct kcLogPrivate;

class kcLog {
    SINGLETON(kcLog)  // 使用单例模式
 public:
    void uninstallMessageHandler();  // 释放资源
    void installMessageHandler();    // 给 Qt 安装消息处理函数

 private:
    kcLogPrivate *d;
};

#endif  // kcLog_H

Singleton.h

#ifndef SINGLETON_H
#define SINGLETON_H

//Singleton.h下载地址:
//https://download.csdn.net/download/sirkang/12418621

#endif  // SINGLETON_H

效果
Qt 自定义 log 日志_第2张图片
未完待续!!!

main() 函数里的 qDebug() 输出都是在 UI 线程,kcLog是否多线程安全?怎么测试?
日志的相关配置数据例如输出目录等都是写死在程序里的,如果写到配置文件里是不是更灵活?
日志的格式也是写死在程序里的,如果能做到通过配置修改日志格式那就更强大了,就像 log4cxx 一样
测试如何快速的看到不同日期生成的日志文件不同?
删除超过 30 天的日志
单个日志文件例如大于 100M 后重新创建一个新的日志文件

工程下载地址:
https://download.csdn.net/download/sirkang/12418621

你可能感兴趣的:(日志,log,Qt,c++,qt)