C++/Qt实现的异步日志类

2025-08-18更新于 2025-10-22562 字22 分钟代码C++Qt
目录

日志类的基本实现

对于日志类的新的想法和改进

对于内部的静态log中的锁,这个锁在多次高并发调用后可能会导致卡顿,因为锁定加锁和释放需要时间。所以我的新想法就是将锁去掉,将日志内可以被实例化,内部维护一个日志队列,日志内部记录时间,所有创建的日志类都要被存储在日志类内部的静态队列里面。当调用存储方法时,异步保存会将所有存储在队列内部的日志类中的日志队列通过swap全部移动到静态队列里面,此时异步方法会根据日志的时间来排序,按照时间将所有日志写入日志文件,当然静态队列要有两个,当保存到静态队列一时,另一个队列就可以接收对应的日志类产生的日志。同步方法一般在程序结束时使用,当结束时不应该使用异步方法来保存数据,这样可能会产生不可预料的错误。

PanelLog主要的日志输出类

cpp
#pragma once
#include <QString>
#include <QQueue>
#include <QUuId>
#include <QMutex>
#include <QHash>
#include <QtConcurrent>
#include <QDateTime>
#include "ConfigManager.h"
#include "PanelLogItem.h" 
#include "PanelLogModel.h"
#include "GY.h"
#include <atomic>

#define DefaultName QString{"GYPanel"}
constexpr auto DefaultModelId = 0;

using namespace GY;

class LogFolderManager
{
private:
	static QList<LogFolderManager*> folderList;
	QString folderName;
	QFile logFile;
	QTextStream stream;
	QMutex lock;
public:
	static void updateTime();
	LogFolderManager(QString folderName);
	~LogFolderManager();
	void writeLog(PanelLogItem* log);
	void cheakAndUpdate();
};
/// <summary>
/// 日志单例类,输出日志都由此管理和写入
/// </summary>
class PanelLog
{
private:
	static QQueue<PanelLogItem*> logList;
	static QHash<const int, PanelLogModel*> modelMap;
	static QHash<const QString, LogFolderManager*> folderMap;
	static QMutex lock;
	static int MAX_ITEMS;
	static ConfigNode* logConfig;
	static int modelIdCounter;
	struct LogMessage
	{
		QString message;
		template<typename... Args>
		LogMessage(const QString& format, Args&&... args) {
			message = format;
			((message = message.arg(std::forward<Args>(args))), ...);
		}
	};
	PanelLog(const PanelLog&) = delete;
	PanelLog& operator= (const PanelLog&) = delete;

	const int modelId = DefaultModelId;
	const QString logName = DefaultName;
public:
	PanelLog();
	PanelLog(int modelId, QString name);
	static PanelLogModel* getModel(const int& uid);
	/// <summary>
	/// 创建model,并且返回model的指针和id
	/// </summary>
	/// <param name="id">返回创建完成的id</param>
	/// <param name="model">返回创建完成的model指针</param>
	static PanelLogModel* createModel(int& id);
	static void removeModel(const int& id);
	static void init();

	//void logInfo(LogMessage msg, int modelId = DefaultModelId, QString sendName = DefaultName);
	//void logWarn(LogMessage msg, int modelId = DefaultModelId, QString sendName = DefaultName);
	//void logError(LogMessage msg, int modelId = DefaultModelId, QString sendName = DefaultName);
	//void logFatal(LogMessage msg, int modelId = DefaultModelId, QString sendName = DefaultName);
	template<typename... Args>
	void logInfo(QString format, Args&&... args) {
		((format = format.arg(std::forward<Args>(args))), ...);
		log(LogLevel::LOGINFO, modelId, logName, format);
	}
	template<typename... Args>
	void logWarn(QString format, Args&&... args) {
		((format = format.arg(std::forward<Args>(args))), ...);
		log(LogLevel::LOGWARN, modelId, logName, format);
	}
	template<typename... Args>
	void logError(QString format, Args&&... args) {
		((format = format.arg(std::forward<Args>(args))), ...);
		log(LogLevel::LOGERROR, modelId, logName, format);
	}
	template<typename... Args>
	void logFatal(QString format, Args&&... args) {
		((format = format.arg(std::forward<Args>(args))), ...);
		log(LogLevel::LOGFATAL, modelId, logName, format);
	}

	static void desLog();
	static void asyncSave();
	static void syncSave();
private:
	static void log(LogLevel level, const int modelId, const QString sendName,const QString message);
};

改类用于输出对应的日志,可以单独实例化用于多次输出,提供静态方法用于集中输出日志,瓶颈在于多次且异步输出日志时用于log内部的锁可能会导致运行卡顿,保存有同步和异步保存方法,在软件关闭时才要同步保存,在软件内部存储的日志数量达到清理标准后会异步开始清理,当清理时会把使用存储的日志转入到一个异步队列,然后在这个队列开始异步写入日志

cpp
#include "PanelLog.h"

//===== LogFolderManager =====
QList<LogFolderManager*> LogFolderManager::folderList;

void LogFolderManager::updateTime()
{
	gdb("LogFolderManager: Updating log folder managers");
	for(LogFolderManager* f : folderList) {
		f->cheakAndUpdate();
	}
}

LogFolderManager::LogFolderManager(QString folderName):folderName(folderName)
{
	folderList.append(this);
	QDir dir{Utils::getRelativeFilePath(QString{"log/%1"}.arg(folderName))};
	if (!dir.exists()) dir.mkpath(".");
	//logDir = std::move(dir);
	QString fileName = std::move(QString{ "%1/%2.log" }.arg(dir.absolutePath()).arg(QDateTime::currentDateTime().toString("yyyy-MM-dd")));
	logFile.setFileName(fileName);
	if (!logFile.open(QIODevice::Append | QIODevice::Text)) {
		//todo 文件无法打开
		gdb("LogFolderManager: Failed to open log file %1", fileName);
		stream.setStatus(QTextStream::Status::WriteFailed);
	}
	stream.setStatus(QTextStream::Status::Ok);
	stream.setEncoding(QStringConverter::Utf8);
	stream.setDevice(&logFile);
}

LogFolderManager::~LogFolderManager()
{
	gdb("LogFolderManager: Destroying folder manager for %1", folderName);
	logFile.close();
	stream.flush();
	folderList.removeAll(this);
}

void LogFolderManager::writeLog(PanelLogItem* log)
{
	QMutexLocker locker(&lock);
	if(stream.status()==QTextStream::Status::WriteFailed) {
		gdb("LogFolderManager: Failed to write log, stream status is WriteFailed");
		return;
	}
	stream << log->toString_logformat() << "\n";
	static int writeCount = 0;
	if(++writeCount >= 100) {
		writeCount = 0;
		stream.flush();
	}
}

void LogFolderManager::cheakAndUpdate()
{
	//auto nowName = QString{ "%1/%2.log" }.arg(Utils::getRelativeFilePath(QString{ "log/%1" }.
	//	arg(folderName))).arg(QDateTime::currentDateTime().toString("yyyy-MM-dd"));
	auto nowName = Utils::getRelativeFilePath(QStringLiteral("log/") % folderName) % QStringLiteral("/") %
		QDateTime::currentDateTime().toString("yyyy-MM-dd") % QStringLiteral(".log");
	if (logFile.fileName() == nowName) return; // no need to update if the file name is the same
	QMutexLocker locker(&lock);
	logFile.close();
	logFile.setFileName(nowName);
	if (!logFile.exists()) {
		if (!logFile.open(QIODevice::Append | QIODevice::Text)) {
			gdb("LogFolderManager: Failed to open log file %1", logFile.fileName());
			stream.setStatus(QTextStream::Status::WriteFailed);
		}
		stream.setStatus(QTextStream::Status::Ok);
		stream.setEncoding(QStringConverter::Utf8);
		stream.setDevice(&logFile);
	}

}

//===== PanelLog =====

QQueue<PanelLogItem*> PanelLog::logList;
QMutex PanelLog::lock;
int PanelLog::MAX_ITEMS = 0;
ConfigNode* PanelLog::logConfig = nullptr;
QHash<const int, PanelLogModel*> PanelLog::modelMap;
QHash<const QString, LogFolderManager*> PanelLog::folderMap;
int PanelLog::modelIdCounter = 1;

PanelLog::PanelLog()
{
}

PanelLog::PanelLog(int modelId, QString name)
{
}

PanelLogModel* PanelLog::getModel(const int& uid)
{
	return modelMap.value(uid, nullptr);
}

PanelLogModel* PanelLog::createModel(int& id_model)
{
	QMutexLocker locker(&lock);
	int id = modelIdCounter++;
	auto model = new PanelLogModel(id);
	modelMap.insert(id, model);
	id_model = id;
	return model;
}

void PanelLog::removeModel(const int& id)
{
	QMutexLocker locker(&lock);
	if (modelMap.contains(id)) {
		auto m = modelMap.value(id);
		delete m;
		m = nullptr;
		modelMap.remove(id);
	}
}

void PanelLog::init() {
	ConfigManager::getIns()->getConfig("config").flatMap([&](ConfigFileParser* c) {
		return c->getNode("log");
	}).ifTrueDo([&](ConfigNode* n) {
		logConfig = n;
		n->getValue<int>("max_items").ifTrueDo([&](int max) {
			PanelLog::MAX_ITEMS = max;
			PanelLogModel::MAX_ITEMS = max;
		});
		});
	modelMap.insert(DefaultModelId, new PanelLogModel(DefaultModelId));
	QDir dir{ Utils::getRelativeFilePath("log") };
	if (!dir.exists()) {
		dir.mkpath(".");
	}
	for (const QString& folderName : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
		LogFolderManager* folder = new LogFolderManager(folderName);
		folderMap.insert(folderName, folder);
		gdb("LogFolderManager: Created folder manager for %1", folderName);
	}
}

//void PanelLog::logInfo(LogMessage msg, int modelId, QString sendName)
//{
//	log(LogLevel::LOGINFO, modelId, sendName, msg.message);
//}
//
//void PanelLog::logWarn(LogMessage msg, int modelId, QString sendName)
//{
//	log(LogLevel::LOGWARN, modelId, sendName, msg.message);
//}
//
//void PanelLog::logError(LogMessage msg, int modelId, QString sendName)
//{
//	log(LogLevel::LOGERROR, modelId, sendName, msg.message);
//}
//
//void PanelLog::logFatal(LogMessage msg, int modelId, QString sendName)
//{
//	log(LogLevel::LOGFATAL, modelId, sendName, msg.message);
//}

void PanelLog::desLog()
{
	gdb("PanelLog MaxItem is %1", MAX_ITEMS);
	gdb("PanelLog: Cleaning up log system");
	syncSave();
	gdb("PanelLog: Synchronized log save complete");
	for (PanelLogModel* m : modelMap.values()) {
		delete m;
		m = nullptr;
	}
	for (LogFolderManager* f : folderMap.values()) {
		delete f;
		f = nullptr;
	}
}

void PanelLog::asyncSave()
{
	lock.lock();
	QQueue<PanelLogItem*> temp;
	std::swap(temp, logList);
	lock.unlock();
	log(LogLevel::LOGINFO, DefaultModelId, DefaultName, "正在异步写入日志数据");
	QtConcurrent::run([&]() {
		while (!temp.isEmpty()) {
			auto t = temp.dequeue();
			auto f = folderMap.value(t->name, nullptr);
			if (f != nullptr) {
				f->writeLog(t);
			}
		}
	});
}

void PanelLog::syncSave()
{
	log(LogLevel::LOGINFO, DefaultModelId, DefaultName, "正在同步写入日志数据,可能会造成卡顿,请耐心等待");
	QMutexLocker locker(&lock);
	while (!logList.isEmpty()) {
		auto temp = logList.dequeue();
		if (temp == nullptr) continue;
		auto f = folderMap.value(temp->name, nullptr);
		gdb("PanelLog: Writing log item to folder %1", temp->name);
		if (f != nullptr) {
			f->writeLog(temp);
		}
	}
}

void PanelLog::log(LogLevel level, const int modelId, const QString sendName, const QString message)
{
	QMutexLocker locker(&lock);
	PanelLogItem* i = new PanelLogItem(sendName, modelId, message, level);
	if (modelId != DefaultModelId && modelMap.contains(modelId)) {
		modelMap.value(modelId)->addData(i);
	} else {
		i->setModelId(DefaultModelId); // Default model ID if not found
	}
	//if (i->modelId == DefaultModelId) modelMap.value(DefaultModelId)->addData(i);
	modelMap.value(DefaultModelId)->addData(i);
	logList.append(i);
	if (logList.size() >= MAX_ITEMS) {
		asyncSave();
	}
}

日志类,用于存储日志,已经产生的地方方便在界面展示时写出

cpp
#pragma once
#include <QDateTime>
#include "GY.h"
using namespace GY;
class PanelLogItem
{
friend class PanelLog;
private:
	//todo 优化name和modelId的存储方式
	QString name;
	QString message;
	int modelId;
	LogLevel level;
	QDateTime time = QDateTime::currentDateTime();
	PanelLogItem(QString name, int modelId, QString message, LogLevel level);
	
public:
	~PanelLogItem() { gdb("des PanelLogItem"); };
	/// <summary>
	/// 输出日志格式的字符串
	/// </summary>
	/// <returns></returns>
	QString toString_logformat();
	/// <summary>
	/// 输出原始数据
	/// </summary>
	/// <returns></returns>
	QString toString_rawstr();

	void setModelId(int modelId);
	LogLevel getLevel();
	int getModelId();
};
cpp
#include "PanelLogItem.h"

PanelLogItem::PanelLogItem(QString name, int modelId, QString message, LogLevel level):
	name(name),modelId(modelId),message(message),level(level)
{

}

QString PanelLogItem::toString_logformat()
{
	return std::move(QString("[%1][%2][%3]:%4").arg(time.toString("yyyy-MM-dd HH:mm:ss")).arg(name).arg(Utils::getLogLevelStr(level)).arg(message));
}

QString PanelLogItem::toString_rawstr()
{
	return message;
}

void PanelLogItem::setModelId(int modelId)
{
	this->modelId = modelId;
}

LogLevel PanelLogItem::getLevel() {
	return level;
}

int PanelLogItem::getModelId()
{
	return modelId;
}

日志的文件夹又一个单的类进行维护对外提供一个函数输入日志即可进行写入,另外当内部逻辑进行更改时也无需更改外部的方法保障其完整性,文件夹类在第一个头文件内部

希望我的博客能够对你有帮助

hhhh6448

hhhh6448

转载请注明出处,欢迎留言交流~

评论

评论区还没开放,部署评论服务后就能留言啦~

主题色