当前位置: 首页 > news >正文

Qt 自定义日志类总结

一、前言

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

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

  • main.cpp: 使用示例
  • LogHandler.h: 自定义日志相关类的头文件
  • LogHandler.cpp: 自定义日志相关类的实现文件
    另外实现功能:
  • 单个日志文件例如大于 5M 后重新创建一个新的日志文件;
  • 删除超过 30 天的日志;
  • 使用锁确保多线程安全。
    后期考虑实现功能:
  • 日志的相关配置数据例如输出目录写到配置文件;
  • 日志可以选择存放在服务器。

二、代码实现

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 运行效果

控制台输出:

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

日志文件(exe 所在目录的 log 目录下的 log.txt):

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): 退出

注意:
Release 版本默认不包含文件名、函数名和行数信息,需要在 .pro 文件中加入一行代码,重新 make 运行后生效。

DEFINES += QT_MESSAGELOGCONTEXT

三、其他 C++ 日志框架

C++ 中的日志框架有很多,其中比较著名的有:

  • log4cxx:Java 社区著名的 Log4j 的 C++ 移植版,用于为 C++
    程序提供日志功能,以便开发者对目标程序进行调试和审计。
  • log4cplus:一个简单易用的 C++ 日志记录 API,它提供了对日志管理和配置的线程安全、灵活和任意粒度控制(也基于
    Log4j)。
  • Log4cpp:一个 C++ 类库,可以灵活地记录到文件、syslog、IDSA 和其他目的地(也基于 Log4j)。
  • google-glog:一个 C++ 语言的应用级日志记录框架,提供了 C++ 风格的流操作和各种辅助宏。
  • Pantheios:一个类型安全、高效、泛型和可扩展性的 C++ 日志 API 库(号称 C++ 领域速度最快的日志库)。
  • POCO:还提供了一个 好的日志支持文档。
  • ACE:ACE 也有日志支持。
  • Boost.Log:设计的非常模块化,并且可扩展。
  • Easylogging++:轻量级高性能 C++ 日志库(只有一个头文件)。
  • G3log:一个开源、支持跨平台的异步 C++ 日志框架,支持自定义日志格式。基于 g2log 构建,提升了性能,支持自定义格式。
  • Plog:可移植、简单和可扩展的 C++ 日志库。
  • spdlog:一个快速的 C++ 日志库,只包含头文件,兼容 C++11。

相关文章:

  • 算法学习|动态规划 LeetCode 416. 分割等和子集
  • Scala泛型(泛型方法,泛型类,泛型特质,上下界,协变、逆变、非变)
  • C/C++字符串
  • 基于Python GDAL库实现图像的几何校正详细教程
  • SpringBoot接参注解与校验失败后的三种异常
  • 【C语言学习】变量和数据类型
  • 【Vue2从入门到精通】详解Vue.js的15种常用指令及其使用场景
  • SpringMVC(8)——SSM整合
  • 【内网安全】横向移动Exchange服务有账户CVE漏洞无账户口令爆破
  • 10、Django开发总结:Django缓存Cache应用场景、设置以及高级使用技巧
  • 【黑客技术】LOIC —— 低轨道离子炮工具使用
  • 华为OD机试用java实现 -【吃火锅】
  • C语言 —— 数组
  • 35岁大龄程序员职业转型规划
  • IntelliJ IDEA 2023.1 最新变化
  • 【编码】-360实习笔试编程题(二)-2016.03.29
  • Centos6.8 使用rpm安装mysql5.7
  • JAVA多线程机制解析-volatilesynchronized
  • JS函数式编程 数组部分风格 ES6版
  • JS进阶 - JS 、JS-Web-API与DOM、BOM
  • node学习系列之简单文件上传
  • Python 反序列化安全问题(二)
  • Python十分钟制作属于你自己的个性logo
  • vue--为什么data属性必须是一个函数
  • Xmanager 远程桌面 CentOS 7
  • 分布式熔断降级平台aegis
  • 关于springcloud Gateway中的限流
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 后端_ThinkPHP5
  • 机器学习中为什么要做归一化normalization
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • MiKTeX could not find the script engine ‘perl.exe‘ which is required to execute ‘latexmk‘.
  • ionic入门之数据绑定显示-1
  • 交换综合实验一
  • ​猴子吃桃问题:每天都吃了前一天剩下的一半多一个。
  • #pragma 指令
  • $.extend({},旧的,新的);合并对象,后面的覆盖前面的
  • (0)Nginx 功能特性
  • (1/2)敏捷实践指南 Agile Practice Guide ([美] Project Management institute 著)
  • (ctrl.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“
  • (C语言)求出1,2,5三个数不同个数组合为100的组合个数
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY)讲解
  • (二)丶RabbitMQ的六大核心
  • (深入.Net平台的软件系统分层开发).第一章.上机练习.20170424
  • (五)网络优化与超参数选择--九五小庞
  • (转)程序员疫苗:代码注入
  • .axf 转化 .bin文件 的方法
  • .Net IE10 _doPostBack 未定义
  • .Net Memory Profiler的使用举例
  • .NET Standard / dotnet-core / net472 —— .NET 究竟应该如何大小写?
  • .net web项目 调用webService
  • .NET 设计模式—简单工厂(Simple Factory Pattern)
  • .net 托管代码与非托管代码
  • .NET 指南:抽象化实现的基类