造轮子-c++日志库

关于怎么造c++日志库这个轮子

日志库在做什么

这个问题乍一看很简单,记录日志么。但其实实现起来没那么简单,放在多线程环境下,如果设计不合理会很容易导致代码臃肿。

因此,这里设计了一个总的处理类,将log提交的日志汇总起来一起输出。

实现

如何记录日志行号、文件名、函数名

c++只能通过宏的方式获取到这些信息

void test(const std::string& _func){
    test(__FUNCTION__);
    std::cout << _func<< std::endl;
}

int main(){
    test(__FUNCTION__);
}

以上代码只是一个示例,死递归应该很好看出来吧,在mian函数中调用的test方法,输出的方法名为"main",但在test函数中调用的,输出方法名为"test"。确实看起来没什么问题,为什么提这一点呢,这意味着必须在log.debug调用时传入__FUNTION__等参数,每个调用的地方都需要把三个宏都传入一遍,有时候日志还不需要记录这些内容,非常的麻烦,因此借用宏来做。以下是示例代码

# define debug(...) log_debug(__FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)
# define info(...) log_info(__FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)
# define warn(...) log_warn(__FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)
# define error(...) log_error(__FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)

class log{
template <typename ... Args>
		void log_debug(const std::string& _file_name, int _line, const std::string& _func_name, Args && ... _args) {
			this->m_logger->work(this->m_formater->writeLog(this->getTime(), "DEBUG", this->dealFileName(_file_name), _func_name, _line, std::this_thread::get_id(), std::forward<Args>(_args)...), 
			this->m_works);
		}
}

int main(){

    log.debug("message: {}", 1);
    return 0;
}

因为宏定义在运行阶段是直接展开字面量,因此log.debug在编译后会变为log.log_debug()并根据宏进行参数传递。log_debug就做了一个简单的可变参数。

定义输出流

这里主要是介绍如何定义日志的输出位置,控制台/文件,提供了一个接口

	class LogStreamInterface {
	public:
		virtual void output(const std::string& _log) = 0;
	private:

	};

可以看到,这里接收一个string类型的变量,因此调用到这里之前需要进行一些字符串格式化处理,下文会提到。

输出流由单例对象生成,统一获取并设置给Log对象,原因是考虑到多个Log操控同一File时读写以及释放的问题。

其中控制台输出流非常简单,其实就是std::cout <<

ZDSJ::LogStream::LogStream()
{
	this->m_ostream = &std::cout;
}

ZDSJ::LogStream::~LogStream()
{
	
}

void ZDSJ::LogStream::output(const std::string& _log)
{
	*this->m_ostream << _log << std::endl;
}

文件流由于写操作是由单例对象统一完成,因此不涉及多线程问题,写起来也比较简单。

void ZDSJ::LogFile::output(const std::string& _log)
{
	std::string out = _log + "\r\n";
	size_t length = out.size();
	if (this->m_offset + length > this->m_max_size) {
		switch (this->m_stratgy)
		{
		case OverflowStratgy::overflow:
			this->overflow(out);
			break;
		case OverflowStratgy::truncation:
			this->truncation(out);
			break;
		case OverflowStratgy::strict:
			this->strict(out);
			break;
		default:
			break;
		}
		return;
	}
	this->m_offset = this->m_out->tellp();
	this->m_out->write(out.c_str(), out.size());
}

这里关键是做一个判断,写入内容是否超过规定大小,超过时当前数据进行什么处理,文件进行什么处理。

overflow表示虽然写入当前数据会导致文件超过指定大小,但依然坚持写完整当前数据,随后进行文件的处理(循环/新开文件)。

truncation表示截断当前输入,强制最大值为指定大小,剩余部分根据文件处理方式写入(写入头部/写入新文件)。

strict表示依然保持写入数据完整,先进行文件处理,再进行写入。

void ZDSJ::LogFile::dealOverFlow()
{
	if (!this->m_loop) {
		this->m_out->close();
		std::string* tmp = new std::string(this->m_file_name->substr(0, this->m_file_name->size() - 5) + "1.txt");
		delete this->m_file_name;
		this->m_file_name = tmp;
		this->openFile();
	}
	else {
		this->m_out->flush();
	}
	this->m_out->seekp(0, std::ios::beg);
	this->m_offset = 0;
}

void ZDSJ::LogFile::openFile()
{
	this->m_out->open(*this->m_file_name, std::ios::binary | std::ios::out | std::ios::in);
	if (!this->m_out->is_open()) {
		std::ofstream* temp = new std::ofstream();
		temp->open(*this->m_file_name, std::ios::app);
		temp->close();
		delete temp;
		this->m_out->open(*this->m_file_name, std::ios::binary | std::ios::out | std::ios::in);
	}
	this->m_out->seekp(0, std::ios::end);
	this->m_offset = this->m_out->tellp();
}

文件处理逻辑也比较简单了,循环/新开文件。

字符串处理

这一块因为用的都是c++11的特性,c++20有新的std::format,c++11没有,因此借助模板实现了类似功能。这一块网上也有很多,这里简单贴一下。

		template <class ... Args>
		std::string format(Args&... _args) {
			std::ostringstream oss;
			this->format(oss, _args...);
			return std::move(oss.str());
		}

		template <class T, class... Args>
		void format(std::ostringstream& _oss, T& _format, Args&... _args) {
			this->format(_oss, _format, 0, _args...);
		}

		void format(std::ostringstream& _oss, const std::string& _format, std::size_t _offset)
		{
			_oss << _format.substr(_offset, _format.size() - _offset);
		}

		template <class T, class... Args>
		void format(
			std::ostringstream& _oss, const std::string& _format, std::size_t _offset, T& _first, Args&... _args)
		{
			std::size_t off = _format.find("{}", _offset);
			if (off == std::string::npos) {
				_oss << _format.substr(_offset, _format.size() - _offset);
				return;
			}
			_oss << _format.substr(_offset, off - _offset) << _first;
			this->format(_oss, _format, off + 2, _args...);
		}

另外需要格式化的是日志的格式,比如要输出日期,行号,等等把。

这块的处理是将[%line%]视为需要输出行号,以此类推,代码处理如下:

	const std::vector<std::string>& support = Logger::getInstance()->getSupport();
	std::string result(*this->m_formater);
	std::regex reg(R"(\[%([^\]]*?)%\])");
	std::smatch match;
	std::vector<std::tuple<std::string, std::tuple<size_t, size_t>>> matches;
	size_t match_index = 0;
	int index = 0;
	std::string temp;
	std::string::const_iterator start = result.cbegin();
	std::string::const_iterator end = result.cend();
	while (std::regex_search(start, end, match, reg)) {
		start = match.suffix().first;
		match_index += match.position();
		std::string match_str = match[1];
		matches.push_back(std::make_tuple(match_str, std::make_tuple(match_index, match_str.size() + 4)));
		match_index += match_str.size() + 4;
	}
	for (auto iter = matches.rbegin(); iter != matches.rend(); ++iter) {
		result.erase(std::get<0>(std::get<1>(*iter)), std::get<1>(std::get<1>(*iter)));
		for (int i = 0; i < support.size(); ++i) {
			index = std::get<0>(*iter).find(support.at(i));
			if (index != std::string::npos) {
				temp = std::get<0>(*iter);
				temp.erase(index, support.at(i).size());
				temp.insert(index, "{}");
				this->m_payload->push_back(Support(i));
			}
		}
		result.insert(std::get<0>(std::get<1>(*iter)), temp);
	}
	std::reverse(this->m_payload->begin(), this->m_payload->end());
	delete this->m_formater;
	this->m_formater = new std::string(result);

首先获取支持列表,也就是日志库支持的输出内容,比如支持行号,文件名,线程id这些。

然后正则匹配,匹配完成后将[%%]中支持部分换为{}便于format替换,假如有个字符串为"line: [%line - dd%]",转换后得到"{} - dd",在经过format,这样才是正确的结果。

输出

之前代码可以看到,log_debug其实是将处理后的字符串传给了Logger.work,其中Logger对象是单例对象,work函数定义如下:

void work(const std::string& _log, std::vector<LogStreamInterface*> _workers);

一个函数执行我们简单可以看做需要两部分,数据与上下文信息,每个log对应不同字符串格式化方式对应的就是数据,不同文件输出流对应不同文件处理方式对应的就是上下文信息。字符串的格式化在调用之前已经做完了,上下文信息通过参数传递过来,之后调用接口的output方法就可以实现真正的输出了。

实现如下:

void ZDSJ::Logger::work(const std::string& _log, std::vector<LogStreamInterface*> _workers)
{
    for (auto item : _workers) {
        m_queue.enqueue(new Work(_log, item));
    }
}

这里用到了无锁队列,将数据与上下文信息存入无锁队列里,统一通过单例类中开的线程进行处理。也就是,每一个调用log的线程都是生产者,只有单例类的线程是消费者。

原谅我无锁队列这个轮子暂时没有造,会有的。

线程函数写法就比较简单了,无锁队列用的是concurrentqueue

void ZDSJ::Logger::threadWordFunc()
{
    bool found = false;
    while (this->m_alive) {
        Work* one_work;
        found = this->m_queue.try_dequeue(one_work);
        if (found) {
            one_work->work();
            delete one_work;
            this->m_sleep_time /= 2;
            this->m_sleep_time = this->m_sleep_time < m_min_sleep_time ? m_min_sleep_time : this->m_sleep_time;
        }
        else {
            this->m_sleep_time *= 2;
            this->m_sleep_time = this->m_sleep_time > this->m_max_sleep_time ? m_max_sleep_time : this->m_sleep_time;
        }
        std::this_thread::sleep_for(this->m_sleep_time);
    }
}

关于单例

这里单例实现借用静态局部变量实现单例,具体原理可以参考csdn文章

ZDSJ::Logger* ZDSJ::Logger::getInstance()
{
    static ZDSJ::Logger instance;
    return &instance;
}

以上,是c++日志库轮子的构造,源码放在gitee

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值