关于怎么造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