Qt 自定义日志工具

    Qt

![效果图]20200514112241348[1].png

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](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 <QApplication>

#include <QDebug>

#include "kcLog.h"

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

    QApplication app(argc, argv);

    //安装消息处理函数

    Singleton<kcLog>::getInstance().installMessageHandler();

    //输出测试,查看是否写入到文件(写入)

    qDebug("qDebug:安装消息处理函数,写入到文件!");

    qInfo("qInfo:安装消息处理函数,写入到文件!");

    qWarning("qWarning:安装消息处理函数,写入到文件!");

    qCritical("qCritical:安装消息处理函数,写入到文件!");

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

    //卸载消息处理函数

    Singleton<kcLog>::getInstance().uninstallMessageHandler();

    //输出测试,查看是否写入到文件(不写入)

    qDebug() << "qDebug:卸载消息处理函数,不写入到文件!";

    qInfo() << "qInfo:卸载消息处理函数,不写入到文件!";

    //再次安装消息处理函数

    Singleton<kcLog>::getInstance().installMessageHandler();

    //输出测试,查看是否写入到文件(写入)

    qDebug() << "qDebug:再次安装消息处理函数,写入到文件!";

    qInfo() << "qInfo:再次安装消息处理函数,写入到文件!";

    return app.exec();

}

```

**kcLog.cpp**

```

/******************************************************************************************

 * loghanfler.cpp

 * by kangchuang

 * time:20200514

 *

 * 使用方法如下:

#include "kcLog.h"

#include <QApplication>

#include <QDebug>

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

    QApplication app(argc, argv);

    //安装消息处理函数

    Singleton<kcLog>::getInstance().installMessageHandler();

    //输出测试,查看是否写入到文件(写入)

    qDebug("qDebug:安装消息处理函数,写入到文件!");

    qInfo("qInfo:安装消息处理函数,写入到文件!");

    qWarning("qWarning:安装消息处理函数,写入到文件!");

    qCritical("qCritical:安装消息处理函数,写入到文件!");

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

    //卸载消息处理函数

    Singleton<kcLog>::getInstance().uninstallMessageHandler();

    //输出测试,查看是否写入到文件(不写入)

    qDebug() << "qDebug:卸载消息处理函数,不写入到文件!";

    qInfo() << "qInfo:卸载消息处理函数,不写入到文件!";

    //再次安装消息处理函数

    Singleton<kcLog>::getInstance().installMessageHandler();

    //输出测试,查看是否写入到文件(写入)

    qDebug() << "qDebug:再次安装消息处理函数,写入到文件!";

    qInfo() << "qInfo:再次安装消息处理函数,写入到文件!";

    return app.exec();

}

******************************************************************************************/

#include "kcLog.h"

#include <stdio.h>

#include <stdlib.h>

#include <QDateTime>

#include <QDebug>

#include <QDir>

#include <QFile>

#include <QFileInfo>

#include <QMutexLocker>

#include <QTextCodec>

#include <QTextStream>

#include <QTimer>

#include <QtGlobal>

#include <iostream>

/************************************************************************************************************

 *                                                                                                          *

 *                                               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<kcLog>::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

```

**效果**

![效果图](https://img-blog.csdnimg.cn/20200514112241348.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Npcmthbmc=,size_16,color_FFFFFF,t_70)

**未完待续!!!**

    main() 函数里的 qDebug() 输出都是在 UI 线程,kcLog是否多线程安全?怎么测试?

    日志的相关配置数据例如输出目录等都是写死在程序里的,如果写到配置文件里是不是更灵活?

    日志的格式也是写死在程序里的,如果能做到通过配置修改日志格式那就更强大了,就像 log4cxx 一样

    测试如何快速的看到不同日期生成的日志文件不同?

    删除超过 30 天的日志

    单个日志文件例如大于 100M 后重新创建一个新的日志文件

**工程下载地址:**

[https://download.csdn.net/download/sirkang/12418621](https://download.csdn.net/download/sirkang/12418621)

未经允许不得转载:康闯 »

赞 (0) 打赏

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏