【项目实战】日志系统

目录

前言

整体架构

工具类的实现

日期类

文件类

判断文件存在

获取文件路径

创建目录

日志等级的规划

日志信息模块

消息格式化模块

格式化组件

抽象基类

派生子类

日期格式化子类 

其他内容格式化子类

格式化类

根据字符创建不同对象

格式化字符串的解析

函数整体

格式化函数

日志落地类模块

基类实现

标准落地子类

文件落地子类

滚动文件落地子类

构造函数

获取文件的拓展名

虚函数重写

落地类工厂

日志器模块

日志器基类

日志器外部接口实现

基类代码

同步日志器

异步日志器

异步工作线程

缓冲区

缓冲区的基础操作

保证足够空间写入

向缓冲区插入数据

开始搭建

生产者的数据写入

消费者的数据处理

外部函数的实现

日志器建造者

抽象基类

局部日志器建造

全局日志器建造者

日志器管理模块

单例对象的获取

日志器的管理

构造函数

代理日志器的接口

提供指定日志器的全局接口

使用宏函数对日志器的接口进行代理

提供宏函数直接进行日志的标准输出

拓展

测试

测试函数的实现

测试结果 

总结

源码


前言

一般而言,业务的服务都是周而复始的运行,若程序出现问题而未进行记录,则在后期修复时会出现无从下手的情况。

因此,本次我们将实现一个日志系统用于记录程序运行状态的信息,以便程序员随时根据信息进行分析。

该系统拥有以下功能:

  • 支持多级别日志消息
  • 支持同步日志和异步日志
  • 支持可靠写入日志到控制台、文件和滚动文件中
  • 支持多线程程序并发写日志
  • 支持拓展不同的日志落地方向

整体架构

🍧一个项目的实现,离不开各个模块的共同合作,我们将根据功能划分几个模块。

  • 日志信息模块:记录日志输出所需的相关信息
  • 消息格式化模块:设置日志输出格式,并提供对日志信息进行格式化
  • 日志落地模块:将格式化文成后的日志消息字符串输出到指定的位置
  • 日志器模块:由以上模块组建,进行各个等级日志的输出操作
  • 日志器管理模块:对所有创建的日志器进行管理

🍧接下来就一起来看看代码实现吧。

工具类的实现

🍧在落实具体模块前,我们先对常用的工具类进行一个实现。

日期类

🍧在之后的日志输出中,我们便经常需要使用到时间这个信息,所以将其封装起来,之后直接调用即可。

namespace Alpaca
{namespace util{class Date{public:static time_t now(){return time(nullptr);}};}
}

文件类

判断文件存在

🍧有一个结构体叫做 stat 用于记录文件的状态,我们可以试图通过获取文件状态来验证该文件是否存在。

🍧或是使用 Linux 下的 access 函数同样也能够达到同样的效果。

static bool exists(const std::string &pathname)
{// 多操作系统共用struct stat st;if (stat(pathname.c_str(), &st) < 0)return false;return true;// Linux专用// return access(pathname.c_str(), F_OK) == 0;
}

获取文件路径

🍧对于一个整体文件名而言,从末尾开始第一个 /(Linux) \(Windows) 前的字符串都是文件的路径。

🍧所以我们只要检索对应的字符,之前的字符串便是该文件的路径。若查找不到对应字符则说明该文件是存在根目录下。

static std::string path(const std::string &pathname)
{size_t pos = pathname.find_last_of("/\\");    //匹配其中的任意字符if (pos == std::string::npos)return ".";        //返回根目录return pathname.substr(0, pos + 1);            //截取对应的字符串
}

创建目录

🍧这个接口我们需要根据传进来的路径进行目录的创建,而创建一个文件需要保证前路径的目录存在

🍧因此,我们从头开始遍历路径名,若找到 / \ 便能确定前路径的目录,我们需要判断该目录是否存在,若不存在则创建

🍧当再找不到前路径便可以直接创建目标目录。

static void create_directory(const std::string& pathname)
{if (pathname.empty())        //路径为空return;if (exists(pathname))        //路径已存在return;size_t pos = 0, idx = 0;while (idx < pathname.size()){pos = pathname.find_first_of("/\\", idx);if (pos == std::string::npos)    //已无前路径,直接创建目录{mkdir(pathname.c_str(), 0755);    //记得设置文件权限return;}if (pos == idx)        //避免符号连续的情况{idx = pos + 1;continue;}std::string parent_dir = pathname.substr(0, pos);    //截取前路径if (parent_dir == "." || parent_dir == "..")      // . 或 .. 必定存在不用考虑{idx = pos + 1;continue;}if (exists(parent_dir))        //判断前路径是否存在{idx = pos + 1;continue;}mkdir(pathname.c_str(), 0755);    //创建前路径idx = pos + 1;                    //迭代}
}

日志等级的规划

🍧我们创建一个日志等级类,在其中使用枚举设定出不同的日志等级。

🍧由于我们都是以字符串的形式在外部使用,因而还需要实现一个函数用于枚举类型与字符串间的转换。

namespace Alpaca
{class LogLevel{public:enum value{UNKNOW = 0,DEBUG,INFO,WARN,ERROR,FATAL,OFF};static const char *Tostring(LogLevel::value level){switch (level){case LogLevel::value::DEBUG:return "DEBUG";case LogLevel::value::INFO:return "INFO";case LogLevel::value::WARN:return "WARN";case LogLevel::value::ERROR:return "ERROR";case LogLevel::value::FATAL:return "FATAL";case LogLevel::value::OFF:return "OFF";}return "UNKNOW";}};
}

日志信息模块

🍧该模块用于存储记录日志输出所需的相关信息,根据使用的需要可以列举出以下信息:

  • 输出时间:_time
  • 日志等级:_level
  • 源文件名称:_file
  • 源文件行号:_line
  • 线程id:_tid
  • 日志的主体信息:_logger
  • 日志器名称:_payload

🍧而对应的构造函数只需要将对应的信息依次填入成员变量中即可。

namespace Alpaca
{struct LogMsg{time_t _time;LogLevel::value _level;std::string _file;size_t _line;std::thread::id _tid;std::string _logger;std::string _payload;LogMsg(LogLevel::value level, std::string file, size_t line,std::string logger, std::string msg): _time(util::Date::now()), _level(level), _file(file)\, _line(line), _tid(std::this_thread::get_id())\, _logger(logger), _payload(msg){}};
}

消息格式化模块

🍧平时我们在使用 printf 时也常常进行格式化操作,这里同样借鉴了该方式。

🍧在该模块中,首先需要由外部传入输出的格式,接着根据格式化字符串进行解析,最终将信息模块的数据填充进需要返回的字符串之中。

🍧同样,我们也同样规定了对应格式化字符所对应的意义:

  • %d表示日期,包含子格式 {%H:%M:%S}
  • %t表示线程ID
  • %p表示日志等级
  • %c表示日志器名称
  • %f表示文件名
  • %l表示行号
  • %T表示制表符缩进
  • %m表示有效载荷
  • %n表示换行

格式化组件

抽象基类

🍧对于数据填充操作,我们想用统一的眼光看待,通过同一函数调用,但最终的结果根据对象的不同而不同

🍧经这么一说,很自然就能联想到多态,因此格式化类的业务就很明了了。

🍧经由解析格式化字符串,我们获得了一个父类指针数组,之后遍历这个数组时对虚函数进行调用即可。

🍧因此这个基类必须拥有一个虚函数以便子类进行重写,而这个虚函数的参数为流插入信息类的对象,方便我们直接添加数据。

namespace Alpaca
{class FormatItem{public:using ptr = std::shared_ptr<FormatItem>;        //外部类中可能使用到该对象,声明一个智能指针类型方便使用virtual void format(std::ostream& out, LogMsg& msg) = 0;    //为了严谨这里定义成纯虚函数最佳};
}

派生子类

  🍧对于派生子类而言,就只需要重写上面的 format 函数,后根据对应的类将对应的数据插入进流中即可。

🍧不同的类的数据类型不同,一定要保证插入进流的为字符串即可。  

class LineFormatItem : public FormatItem
{
public:virtual void format(std::ostream& out, LogMsg& msg) override{out << msg._line;}
};
日期格式化子类 

🍧而凡是总有例外,日期的格式化还有包含子串

🍧因此需要在构造函数中保存对应的字符串(不能加到虚函数中,这样将不构成重写)。

🍧之后我们从信息类中获取对应日志的时间,在此基础上使用 strftime 将其转换成字符串,由于 strftime 的参数为  struct tm*  

🍧因此需要先 time_t 类型的数据转换成 struct tm ,而 localtime_r 函数便能帮助我们实现。

🍧获取到格式化日期字符串后,直接将其插入流中即可。

class TimeFormatItem : public FormatItem
{
public:TimeFormatItem(const std::string& fmt = "%H:%M:%S") : _time_fmt(fmt) {}void format(std::ostream& out, LogMsg& msg) override{struct tm t;localtime_r(&msg._time, &t);char tmp[32] = { 0 };strftime(tmp, sizeof(tmp) - 1, _time_fmt.c_str(), &t);out << tmp;}private:std::string _time_fmt;
};
其他内容格式化子类

🍧在格式化字串中也常有其他字符用作分隔或其他用途,但这些字符并不在信息类中,而是需要外部传入,因此做法与上面日期子类相同

class OtherFormatItem : public FormatItem
{
public:OtherFormatItem(const std::string& str) : _str(str) {}virtual void format(std::ostream& out, LogMsg& msg) override    //检查重写{out << _str;}private:std::string _str;
};

🍧该部分代码参考如下。

namespace Alpaca
{class FormatItem{public:using ptr = std::shared_ptr<FormatItem>;virtual void format(std::ostream &out, LogMsg &msg) = 0;};class LevelFormatItem : public FormatItem{public:virtual void format(std::ostream &out, LogMsg &msg) override{out << LogLevel::Tostring(msg._level);}};class LineFormatItem : public FormatItem{public:virtual void format(std::ostream &out, LogMsg &msg) override{out << msg._line;}};class FileFormatItem : public FormatItem{public:virtual void format(std::ostream &out, LogMsg &msg) override{out << msg._file;}};class ThreadFormatItem : public FormatItem{public:virtual void format(std::ostream &out, LogMsg &msg) override{out << msg._tid;}};class LoggerFormatItem : public FormatItem{public:virtual void format(std::ostream &out, LogMsg &msg) override{out << msg._logger;}};class MsgFormatItem : public FormatItem{public:virtual void format(std::ostream &out, LogMsg &msg) override{out << msg._payload;}};class TimeFormatItem : public FormatItem{public:TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}void format(std::ostream &out, LogMsg &msg) override{struct tm t;localtime_r(&msg._time, &t);char tmp[32] = {0};strftime(tmp, sizeof(tmp) - 1, _time_fmt.c_str(), &t);out << tmp;}private:std::string _time_fmt;};class TableFormatItem : public FormatItem{public:virtual void format(std::ostream &out, LogMsg &msg) override{out << "\t";}};class NLineFormatItem : public FormatItem{public:virtual void format(std::ostream &out, LogMsg &msg) override{out << "\n";}};class OtherFormatItem : public FormatItem{public:OtherFormatItem(const std::string &str) : _str(str) {}virtual void format(std::ostream &out, LogMsg &msg) override{out << _str;}private:std::string _str;};
}

格式化类

🍧日志输出的格式一旦确定后便不再改变了,如此操作便是为了避免每次都进行格式的解析,只需要变换传入的信息对象即可。

🍧因此格式化字符串在构造函数中接收,而在成员函数中每次接收不同的信息对象。

🍧从而可以编写出该结构的大概框架。

class Formatter
{
public:using ptr = std::shared_ptr<Formatter>;Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"): _patter(pattern){}std::string format(LogMsg& msg);    //外部调用void format(std::ostream& out, LogMsg& msg);    //内部嵌套// 解析格式化规则字符串bool ParsePattern();private:// 根据格式化字符创建不同的格式化对象FormatItem::ptr createItem(const std::string& key, const std::string& val);private:std::string _patter; // 格式化字符串std::vector<FormatItem::ptr> _items;   //父类指针数组
};

根据字符创建不同对象

🍧在解析格式化字符串前,我们需要先实现这个接口,用于根据我们传入的格式化字符创建不同的格式化对象

🍧早在父类组件实现时,便声明了父类智能指针这个类型,而成员中的数组中存的便是这个智能指针

🍧因此,该函数只需要返回对应派生类对象的智能指针即可,且使用智能指针还可以帮助我们完成空间的释放,无需我们手动管理。

FormatItem::ptr createItem(const std::string& key, const std::string& val)
{if (key == "d")return std::make_shared<TimeFormatItem>(val);if (key == "T")return std::make_shared<TableFormatItem>();if (key == "t")return std::make_shared<ThreadFormatItem>();if (key == "p")return std::make_shared<LevelFormatItem>();if (key == "c")return std::make_shared<LoggerFormatItem>();if (key == "f")return std::make_shared<FileFormatItem>();if (key == "l")return std::make_shared<LineFormatItem>();if (key == "m")return std::make_shared<MsgFormatItem>();if (key == "n")return std::make_shared<NLineFormatItem>();if (key.empty())return std::make_shared<OtherFormatItem>(val);std::cout << "使用非法格式化字符: %" << key << std::endl;abort();return FormatItem::ptr();
}

格式化字符串的解析

🍧接下来我们就可以进行格式化字符串的解析了。

🍧在创建个别子项中需要参数的传入,因而我们需要将格式化字符串中相应的内容保存下来(例如日期的子串)。

🍧我们不妨使用一个 pair 为成员的数组进行保存,若是有参数就将参数保存,若无对应参数则第二个位置留空。

🍧其中的每组对象都对应着函数调用时的两个参数。

std::vector<std::pair<std::string, std::string>> fmt_order;

🍧接着便开始循环解析,只需要在格式化字符串中查找 % ,遇到  %  前的所有字符都是原始字符,我们暂时将他们保留起来

if (_patter[pos] != '%')
{val.push_back(_patter[pos++]);continue;
}

🍧遇到两个 % 的情况我们将其视为转义字符,表示一个 %  的原始字符,同样存于原始字符串中。

if (pos + 1 < _patter.size() && _patter[pos + 1] == '%')
{val.push_back('%');pos += 2;continue;
}

 🍧若都未能满足上面两个判断的条件,那么当下 pos 位置一定为 % ,因此我们需要先将之前保存起来的原始字符串转移至解析数组之中。

// 走到这说明原始字符串结束,先推送
if (!val.empty())
{fmt_order.push_back(std::make_pair("", val));val.clear();
}

🍧接下来我们让 pos 向下移动一位,若此时越界则说明 % 匹配失败,直接返回 false 即可。

🍧匹配成功,那么当前位置的字符就是我们在查找的格式化字符,我们将其存到 key 中。

pos++;
if (pos >= _patter.size())  //检查越界
{std::cout << "%后未有对应的格式化字符" << std::endl;return false;
}
key = _patter[pos];

🍧接下来,还需要考虑字符后面可能还带着的子串,往下一位判断是否为 { ,接着进行对 } 的查找。

🍧若查找不到 } 进行匹配,便直接返回 false ,否则就不断往 val 中存入数据。

// 考虑子规则的情况
pos++;
if (pos < _patter.size() && _patter[pos] == '{')
{pos++;while (pos < _patter.size() && _patter[pos] != '}'){val.push_back(_patter[pos++]);}if (pos >= _patter.size()){std::cout << "子规则{}匹配出错" << std::endl;return false;}pos++;
}

🍧能平安走到这里,就代表以获取一个格式化字符的相关内容,接下来我们便可以将对应的  key val  的值推送到数组之中了。

// 走出子规则,推送上方解析的数据
fmt_order.push_back(std::make_pair(key, val));
key.clear();
val.clear();

🍧最后循环结束,我们便可以根据解析出来的数组,获取对应对象的指针并填充成员数组。 

// 根据解析到的数据,初始化成员
for (auto& it : fmt_order)
{_items.push_back(createItem(it.first, it.second));
}
return true;
函数整体
// 解析格式化规则字符串
bool ParsePattern()
{std::vector<std::pair<std::string, std::string>> fmt_order;int pos = 0;std::string key, val;while (pos < _patter.size()){if (_patter[pos] != '%'){val.push_back(_patter[pos++]);continue;}// 走到当前pos当前便指向一个%if (pos + 1 < _patter.size() && _patter[pos + 1] == '%'){val.push_back('%');pos += 2;continue;}// 走到这说明原始字符串结束,先推送if (!val.empty()){fmt_order.push_back(std::make_pair("", val));val.clear();}// 现在进行格式化字符的处理,此时pos指向%pos++;if (pos >= _patter.size())  //检查越界{std::cout << "%后未有对应的格式化字符" << std::endl;return false;}key = _patter[pos];// 考虑子规则的情况pos++;if (pos < _patter.size() && _patter[pos] == '{'){pos++;while (pos < _patter.size() && _patter[pos] != '}'){val.push_back(_patter[pos++]);}if (pos >= _patter.size()){std::cout << "子规则{}匹配出错" << std::endl;return false;}pos++;}// 走出子规则,推送上方解析的数据fmt_order.push_back(std::make_pair(key, val));key.clear();val.clear();}// 根据解析到的数据,初始化成员for (auto& it : fmt_order){_items.push_back(createItem(it.first, it.second));}return true;
}

格式化函数

🍧实现了对格式化字符串的解析,我们只需要在构造函数调用一次即可,之后都使用格式化子项数组即可,而这次解析务必成功否则直接出错。

Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"): _patter(pattern)
{assert(ParsePattern());
}

🍧还记得上方定义时写的两个  format  函数吗?外部就是通过这个函数进行格式化后的字符串的获取。

🍧而我们上面写的格式化子项都是将对应的数据插入到之中,且因我们最后要获取的是一个字符串,那么不妨使用 stringstream 进行信息的获取。

🍧同时,stringstream ostream 的子类可以进行赋值转换,因此可以作为参数。

🍧因此,通过对子项数组的遍历,同样调用 log 函数便可完成字符串的组合,遍历结束将流中的数据返回即可

std::string format(LogMsg& msg)        //外部调用
{std::stringstream ss;format(ss, msg);return ss.str();
}void format(std::ostream& out, LogMsg& msg)    //遍历操作
{for (auto it : _items){it->format(out, msg);}
}

日志落地类模块

🍧该模块用于将格式化后的日志消息字符串输出到指定的位置,因为有多种不同的落地方式,我们不妨再次使用多态的方式进行模块的搭建。

🍧同时,外部拓展新落地方向时只需要继承基类并重写虚函数即可。

基类实现

🍧这里需要注意,析构函数也要定义成虚函数,这样即使在外部用父类指针进行管理但最后调用析构函数时会触发多态调用派生类的析构函数

🍧而这个 log 函数则是我们接下来要进行重写的主要落地函数,而参数使用了字符串和长度的形式则是借鉴了文件操作

namespace Alpaca
{class LogSink{public:using ptr = std::shared_ptr<LogSink>;LogSink() {}virtual ~LogSink() {}virtual void log(const char* data, size_t len) = 0;    //定义成纯虚函数};
}

标准落地子类

🍧标准落地自然不用多说,以防万一我们根据指定长度写入,因此要使用 write 这个成员函数。

class StdoutSink : public LogSink
{
public:void log(const char* data, size_t len){std::cout.write(data, len);}
};

文件落地子类

🍧这个落地子类,我们需要将日志消息写入到一个文件中,那么需要知道文件的路径,而因为一个子类只对应一个文件,所以文件路径这一信息在构造函数中传入即可。

🍧而打开文件前还需要确保文件前路径存在,因此我们可以调用之前在工具类中实现的  create_directory  函数,对传入文件的路径进行创建。若前段目录未存在则创建,而若存在便会直接返回。

🍧这些前置操作完成后,使用 ofstream 成员打开对应的文件(记得加追加选项,不然就是覆盖写入),如此我们的构造函数便算完成。

🍧而重写的虚函数也是直接往文件中写入数据即可,写入后判断一下是否写入成功

class FileSink : public LogSink
{
public:// 构造时需要文件名打开文件,并保留句柄FileSink(const std::string& pathname) : _pathname(pathname){// 创建文件所在路径,存在即返回util::File::create_directory(util::File::path(pathname));// 打开文件_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());    //检查打开成功}// 将日志信息写入文件void log(const char* data, size_t len){_ofs.write(data, len);assert(_ofs.good());}private:std::string _pathname;std::ofstream _ofs;
};

🍧再补充一点,当 ofstream 对象被销毁时,任何打开的文件都会自动关闭,因此无需我们手动关闭。

滚动文件落地子类

🍧日志输出的频率极快,若是只在一个文件中输出,查看起来可能会造成不便,因此我们可以使用滚动文件的方式。

🍧例如,我们可以设置成当一个文件写入数据量已达 1Mb,便打开新文件往新文件中写入。

构造函数

🍧这里我们使用的便是实现文件大小达到一定程度便打开新文件的策略即 rollbysize

🍧因此,我们需要有成员分别记录当前文件的大小文件的最大值,除此以外我们还为文件增加一个唯一标识_name_count 表示落地类直到现在打开了多少个滚动文件,最后便是基础文件名和 ofsream 句柄。

🍧而构造函数的内部与上一个文件落地类似,都是先保证路径上的目录存在,接着打开完整拓展名的文件即可。

class RollBySizeSink : public LogSink
{
public:RollBySizeSink(const std::string& basename, size_t max_size): _basename(basename), _max_size(max_size), _cur_size(0), _name_count(1){std::string pathname = createNewFile();// 创建文件所在路径util::File::create_directory(util::File::path(pathname));// 打开文件_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());}
private:size_t _name_count;         //文件标识std::string _basename;      //基础文件名std::ofstream _ofs;size_t _max_size;           //最大文件大小size_t _cur_size;           //当前文件大小
};

获取文件的拓展名

🍧因为我们使用的是滚动文件落地,因此会涉及到打开不同名的文件,因此在构造时只需传入基础文件名即可,而拓展名由我们根据唯一变量进行添加。

//拓展名 = 基础文件名 + 时间信息 + 滚动文件的计数器
std::string createNewFile()
{time_t t = util::Date::now();struct tm lt;localtime_r(&t, &lt);std::stringstream filename;filename << _basename;            //基础文件名filename << lt.tm_year + 1900 << lt.tm_mon + 1     //时间信息<< lt.tm_mday << lt.tm_hour<< lt.tm_min << lt.tm_sec;filename << "-";filename << _name_count++;    //滚动文件的计数器filename << ".log";return filename.str();
}

虚函数重写

🍧在每次落地前,我们都需要计算一下当前文件大小是否超过了文件的限制,若超过了则获取一个新文件名,接着关闭原文件的流并打开文件的流。(一定要先关闭原文件,否则可能导致文件描述符被占用完了,进而导致程序崩溃)

void log(const char* data, size_t len)
{// 文件过大时创建一个新文件if (_cur_size > _max_size){std::string pathname = createNewFile();_ofs.close();        //先关闭再打开_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());_cur_size = 0;}_ofs.write(data, len);assert(_ofs.good());_cur_size += len;
}

落地类工厂

🍧最后我们需要一个工厂专门负责落地对象的获取

🍧但我们不能写死,因为我们还要支持新落地方式的拓展,因此我们使用模板的方式进行对应对象的获取。

🍧同时,又由于各个对象初始化所需的参数不同,便还需要可变参数列表接收不同数量的参数。

class SinkFactory
{
public:template <class Sinktype, class... Args>static LogSink::ptr create(Args &&...args){return std::make_shared<Sinktype>(std::forward<Args>(args)...);    //将可变参数包展开}
};

🍧之后只要在外部指定落地类类型调用该函数并传入对应参数即可获取为对应的落地对象。

日志器模块

🍧我们实现的日志器还分作同步日志器异步日志器两种。

🍧同步写日志时,是以串行的模式进行运行,日志完成前不可以进行接下来的业务处理。

🍧而异步日志器中则有专门的线程负责日志的输出操作。

日志器基类

🍧同样,我们先抽象出一个日志器基类,之后的各个日志器类型都是在此基础上建立的。

🍧我们先梳理一下该类需要的相关成员,首先便是日志的限制等级,对于一个日志器设定有对应的等级限制,写日志时若输出等级小于限制等级将不会输出

🍧接着,在下一个模块我们会将日志器管理起来,作为标识符,我们需要对每个日志器进行命名。这样一个个日志器组合起来,便成为了一个庞大的日志系统。

🍧不仅如此,前几个模块都是这个模块的一部分,在日志器中我们还要有专属的格式化模块落地模块数组(一个日志器可能有多种落地方式)。

🍧最后,为了保证该日志系统能够被线程并发访问,因而在成员中还需要一个互斥锁来保证不会出现冲突。

namespace Alpaca
{class Logger{public:using ptr = std::shared_ptr<Logger>;Logger(const std::string& logger_name, LogLevel::value limit_level,Formatter::ptr& formatter, std::vector<LogSink::ptr>& sinks): _logger_name(logger_name), _limit_level(limit_level),_formatter(formatter), _sinks(sinks.begin(), sinks.end()) {}// 通过传入的参数构建一个msg对象,且调用目标方法所生成的日志等级一定与该方法一致void debug(const std::string& file, size_t line, const std::string& fmt, ...);void info(const std::string& file, size_t line, const std::string& fmt, ...);void warn(const std::string& file, size_t line, const std::string& fmt, ...);void error(const std::string& file, size_t line, const std::string& fmt, ...);void fatal(const std::string& file, size_t line, const std::string& fmt, ...);std::string GetLoggerName(); // 不返回引用防止外界修改protected:virtual void log(const char* data, size_t len) = 0;void serialize(LogLevel::value level, const std::string& file, size_t line, char* str);protected:std::mutex _mutex;std::string _logger_name;std::atomic<LogLevel::value> _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};
}

🍧这里将日志等级定义成了原子性的了,因此之后对其访问就不用加锁了。

🍧其中,各个日志等级的成员函数是给到外部调用的,而在该函数内部,我们需要完成对限制等级的判断,获取传入参数并进行格式化,最后基于落地类数组进行实际落地

日志器外部接口实现

🍧需要注意的一点是,参数需要传入对应的文件名日志输出时的行号(如果在类内获取,就失去了对应定位的效果)。

🍧函数刚进入时,我们需要对限制等级进行一次判断,同时因为我们使用枚举定义的日志等级,因此可以直接比较(下面以 debug 等级进行演示)。

if (LogLevel::value::DEBUG < _limit_level)return;

🍧接下来我们便需要解析不定参数,需要 va_list 这个类型协助我们进行解析操作。

🍧值得注意的一点是,这个传入的不定参数,外部用于对日志正文的格式化。因此这里我们根据对应的格式化字符串将其转化成对应的字符串,作为日志内容用于接下来日志信息类的构建,而这个操作刚好由 vasprintf 完成。

🍧关于这个  va_list 可以将其看作一个指针,我们通过转换指向从而获得不定参数中的各个成员。

// 解析不定参数
va_list ap;            //定义类型
va_start(ap, fmt);     //让ap指向不定参数开始
char* res;             //定义一个指针用于接收数据
int ret = vasprintf(&res, fmt.c_str(), ap);    //转化成字符串
if (ret == -1)         //失败则输出相关信息
{std::cout << "vasprintf failed" << std::endl;return;
}
va_end(ap);            //让ap指向不定参数的结尾

🍧而格式化并落地部分,我们将其封装到了 serialize 函数中,下面一起看看如何实现吧。

🍧根据传入的参数,我们创建出对应的信息对象,紧接着传入格式化模块中转换成字符串,最后交由 log 函数处理。

void serialize(LogLevel::value level, const std::string& file, size_t line, char* str)
{// 构建Logmsg对象LogMsg msg(level, file, line, _logger_name, str);// 获取格式化后的字符串std::stringstream ss;_formatter->format(ss, msg);// 日志落地log(ss.str().c_str(), ss.str().size());
}

🍧正因两种日志器的落地方式,因此这个 log 函数则是接下来子类需要重写的虚函数。

🍧走完  serialize  函数后,回到原输出函数,因为 vsprintf 内部会动态开辟空间给 res ,在函数结束前还需要释放对应的空间

free(res); // vsprintf内部会动态开辟空间给res

🍧由此,debug 部分功能便已实现,而其他等级的函数只需要在该函数的基础上更改其中的等级即可。

基类代码

namespace Alpaca
{class Logger{public:using ptr = std::shared_ptr<Logger>;Logger(const std::string& logger_name, LogLevel::value limit_level,Formatter::ptr& formatter, std::vector<LogSink::ptr>& sinks): _logger_name(logger_name), _limit_level(limit_level),_formatter(formatter), _sinks(sinks.begin(), sinks.end()) {}// 通过传入的参数构建一个msg对象,且调用目标方法所生成的日志等级一定与该方法一致void debug(const std::string& file, size_t line, const std::string& fmt, ...){// 判断输出等级if (LogLevel::value::DEBUG < _limit_level)return;// 解析不定参数va_list ap;va_start(ap, fmt);char* res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed" << std::endl;return;}va_end(ap);serialize(LogLevel::value::DEBUG, file, line, res);free(res); // vsprintf内部会动态开辟空间给res}void info(const std::string& file, size_t line, const std::string& fmt, ...){// 判断输出等级if (LogLevel::value::INFO < _limit_level)return;// 解析不定参数va_list ap;va_start(ap, fmt);char* res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed" << std::endl;return;}va_end(ap);serialize(LogLevel::value::INFO, file, line, res);free(res); // vsprintf内部会动态开辟空间给res}void warn(const std::string& file, size_t line, const std::string& fmt, ...){// 判断输出等级if (LogLevel::value::WARN < _limit_level)return;// 解析不定参数va_list ap;va_start(ap, fmt);char* res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed" << std::endl;return;}va_end(ap);serialize(LogLevel::value::WARN, file, line, res);free(res); // vsprintf内部会动态开辟空间给res}void error(const std::string& file, size_t line, const std::string& fmt, ...){// 判断输出等级if (LogLevel::value::ERROR < _limit_level)return;// 解析不定参数va_list ap;va_start(ap, fmt);char* res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed" << std::endl;return;}va_end(ap);serialize(LogLevel::value::ERROR, file, line, res);free(res); // vsprintf内部会动态开辟空间给res}void fatal(const std::string& file, size_t line, const std::string& fmt, ...){// 判断输出等级if (LogLevel::value::FATAL < _limit_level)return;// 解析不定参数va_list ap;va_start(ap, fmt);char* res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vsprintf failed" << std::endl;return;}va_end(ap);serialize(LogLevel::value::FATAL, file, line, res);free(res); // vsprintf内部会动态开辟空间给res}std::string GetLoggerName() // 不返回引用防止外界修改{return _logger_name;}protected:virtual void log(const char* data, size_t len) = 0;void serialize(LogLevel::value level, const std::string& file, size_t line, char* str){// 构建Logmsg对象LogMsg msg(level, file, line, _logger_name, str);// 获取格式化后的字符串std::stringstream ss;_formatter->format(ss, msg);// 日志落地log(ss.str().c_str(), ss.str().size());}protected:std::mutex _mutex;std::string _logger_name;std::atomic<LogLevel::value> _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};
}

同步日志器

🍧同步日志器不需要增加新的成员,对于 log 函数的重写只需要遍历落地类的数组将对应的数据进行落地即可。

class SyncLogger : public Logger
{
public:SyncLogger(const std::string& logger_name, LogLevel::value limit_level,Formatter::ptr& formatter, std::vector<LogSink::ptr>& sinks): Logger(logger_name, limit_level, formatter, sinks) {}protected:// 直接通过落地模块的句柄进行日志落地void log(const char* data, size_t len){std::unique_lock<std::mutex> lock(_mutex);    //加锁if (_sinks.empty())return;for (auto sink : _sinks)        //实际落地{sink->log(data, len);}}
};

异步日志器

🍧在异步日志器实现前,还有一个重要的拼图还未凑齐,那就是负责异步写日志的线程

异步工作线程

🍧我们将该线程封装进一个类中,为类中的一个成员在构造函数中创建线程 ,在析构函数中进行线程关闭的工作

🍧该类的成员负责数据的输入,而成员中的工作线程负责日志的实际落地

AsyncLooper(func_t func, AsyncType looper_type = AsyncType::ASYNC_SAFE): _stop(false),_looper_type(looper_type),_thread(std::thread(&AsyncLooper::threadEntry, this)),    //设置一个工作线程的入口函数_func(func) {}~AsyncLooper()
{stop();
}void stop()
{_stop = true;_cond_con.notify_all();_thread.join(); // 等待工作线程退出
}

🍧在实际开发中,写日志操作并不会分配太多的资源,因此工作线程只需要一个就够了。

🍧那么我们传入进来的每条信息都是立刻输出的吗?在文件系统时我们学过,系统调用是十分低效的,若是每有一条就直接输出到文件中,便会严重影响整体线程的效率。因此,我们需要实现一个缓冲区模块,临时存放传入进来的日志消息。

🍧现在我们一起来分析一下,在运行过程中可能涉及的冲突问题,生产者生产者之间的冲突(写入线程间),生产者消费者之间的冲突(写入线程和异步工作线程间)。当前的问题便是,锁冲突较为严重所有线程间都存在互斥关系

🍧不如,我们的缓冲区模块就使用两个缓冲区组成,如此便有效地减少了锁冲突。

🍧经由改变结构,优化了生产者和消费者之间的冲突,只有在交换缓冲区的过程中才需要进行锁的申请。

缓冲区

🍧现在一起来看看单个缓冲区类是如何实现的吧。

🍧首先便是如何存放一条条日志信息了,因为此时写入已经是格式化后的字符串,我们直接使用 vector<char> 进行存储即可。

🍧接着,我们这个缓冲区是一个单向的缓冲区只有缓冲区的数据被清空后才会从头开始写入,当缓冲区被写满后会根据异步线程的设定决定是阻塞还是扩容,因此需要两个指针,分别告诉我们从哪里开始读、哪里开始写,以便进一步操作。

namespace Alpaca
{
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)    //默认大小
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)  //小于这个大小每次扩容 *2
#define INCREASE_BUFFER_SIZE (1 * 1024 * 1024)   //大于上方大小每次扩容增加这个数class Buffer{public:Buffer(): _buffer(DEFAULT_BUFFER_SIZE),_read_idx(0), _write_idx(0) {}private:std::vector<char> _buffer;size_t _read_idx;size_t _write_idx;};
}
缓冲区的基础操作

🍧对于这个缓冲区而言,我们需要获取一些基础信息偏移读写指针,不妨将其作为接口封装起来。

const char* begin()      //返回可读数据的起始地址
{return &_buffer[_read_idx];
}size_t ReadAbleSize()     //能读取的数据量
{return (_write_idx - _read_idx);
}
size_t WriteAbleSize()    //还能写的空间
{return (_buffer.size() - _write_idx);
}void MoveReader(size_t len)    //移动读端
{assert(len <= ReadAbleSize());_read_idx += len;
}
void MoveWriter(size_t len)    //移动写端
{assert(len + _write_idx <= _buffer.size());_write_idx += len;
}void reset()        //将偏移量初始化
{_write_idx = 0;_read_idx = 0;
}void swap(Buffer& buffer)        //交换缓冲区
{_buffer.swap(buffer._buffer);std::swap(buffer._read_idx, _read_idx);std::swap(buffer._write_idx, _write_idx);
}bool empty()        //缓冲区判空
{return (_read_idx == _write_idx);
}
保证足够空间写入

🍧该函数用于确保缓冲区有足够的空间进行写入,本质上为一种扩容函数。

🍧缓冲区的数据若经过几次扩容则可能变得相当庞大,因此不能每次都以两倍进行增长,我们可以设置当空间大于某个数值后,缓冲区每次扩容成线性增长

void ensureEnoughSize(size_t len)
{if (len < WriteAbleSize())    //空间足够直接返回return;size_t newsize = 0;while (newsize < len){if (_buffer.size() < THRESHOLD_BUFFER_SIZE)        //小于指定数值每次扩容两倍newsize = _buffer.size() * 2;elsenewsize = _buffer.size() + INCREASE_BUFFER_SIZE;    //大于指定数值每次线性扩容}_buffer.resize(newsize);
}
向缓冲区插入数据

🍧在外部我们会进行根据异步日志的模式对线程进行限制,因此我们这里直接确保空间足够即可。

🍧往缓冲区写入数据后还要记得把可写指针向后偏移

void push(const char* data, size_t len)
{//将是否扩容的决定权交给用户,因此这里只扩容ensureEnoughSize(len);std::copy(data, data + len, &_buffer[_write_idx]);// 将可写位置向后偏移MoveWriter(len);
}
开始搭建

🍧现在,我们已经完成了缓冲区的实现,接下来便可以进行异步线程部分的搭建了。

🍧于日志器而言,成员需要一个异步类型的标志,一个运行标识符两个缓冲区,与之对应的条件变量,为了使用条件变量还需要一个互斥锁,以及异步线程和其中的回调函数

namespace Alpaca
{enum class AsyncType{ASYNC_SAFE,  // 缓冲区满了则阻塞ASYNC_UNSAFE // 无限扩容};class AsyncLooper{private:AsyncType _looper_type;std::atomic<bool> _stop;    //运行标识符Buffer _pro_buf; // 生产者缓冲区Buffer _con_buf; // 消费者缓冲区std::mutex _mutex;std::condition_variable _cond_pro;std::condition_variable _cond_con;std::thread _thread; // 异步工作器对应的工作线程func_t _func;        // 回调函数};
}

🍧接下来将分成两部分进行实现,分别是生产者写入数据,与消费者处理数据

生产者的数据写入

🍧对生产者而言,数据的写入即将对应数据拷贝到缓冲区中即可,首先缓冲区可能处于一个并发访问的状态,需要先加上

🍧接着判断日志器的写入方式为无限扩容还是阻塞,若是阻塞模式下则进行判断缓冲区中的数据是否足够写入,为否则阻塞。

🍧我们实现的 push 函数内部会保证缓冲区中的空间足够写入,因此直接拷贝数据即可,最后唤醒消费者处理数据即可。

void push(const char* data, size_t len)
{// 1.无限扩容  --非安全 2.固定大小std::unique_lock<std::mutex> lock(_mutex);if (_looper_type == AsyncType::ASYNC_SAFE)_cond_pro.wait(lock, [&](){ return _pro_buf.WriteAbleSize() >= len; })    //使用lambda表达式;// 走到这说明缓冲区中有足够空间够我们写入 或是处于无限扩容状态下_pro_buf.push(data, len);_cond_con.notify_one(); // 唤醒消费者对缓冲区进行处理
}
消费者的数据处理

🍧消费者即异步线程,一开始异步线程所属的缓冲区便是空的,因此每次都需要先获取新的数据

🍧由此需要先对生产者的缓冲区状态进行判断,若该缓冲区为空,交换过来也没有意义。

🍧进入临界区,我们先对运行状态判断一下,但别着急 break 万一生产者中还有数据,还是要进行处理的。

🍧接着根据条件变量进行阻塞,若成功走到下面说明生产者缓冲区满足条件,可以交换,交换后唤醒生产者继续填充数据。

🍧走出临界区,说明数据已经准备好了,接着调用回调函数进行数据落地即可,最后清空缓冲区的偏移量,等待获取新数据。

void threadEntry() // 线程入口函数
{while (1){//判断生产者的缓冲区是否满足交换要求{ std::unique_lock<std::mutex> lock(_mutex);    //加上{}使锁的临界区就在{}内部分内容// 避免生产缓冲区中有数据但没有被完全处理的情况if (_stop && _pro_buf.empty())break;//运行状态下,生产者缓冲区为空阻塞,非运行状态下不阻塞_cond_con.wait(lock, [&](){ return _stop || !_pro_buf.empty(); });_con_buf.swap(_pro_buf);_cond_pro.notify_all();}_func(_con_buf);_con_buf.reset();}
}

外部函数的实现

🍧异步线程实现后,剩下的最后一步便是外部函数的实现,在这里我们需要实现一个用于插入日志的接口,以及异步线程中的回调函数

🍧首先为插入日志的接口,因为内部帮我们加锁了,所以直接调用异步线程对象的成员函数即可。

void log(const char* data, size_t len)
{_looper->push(data, len);
}

 🍧而回调函数也很简单,像同步日志器那样,遍历落地对象数据进行落地操作即可

void realLog(Buffer& buf)   //实际落地函数
{if (_sinks.empty())return;for (auto sink : _sinks){sink->log(buf.begin(), buf.ReadAbleSize());}
}

🍧最后需要注意一点,我们在类内写的函数都是默认带有 this 指针的,因此在传入前需要先绑定参数。

_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), logger_type)

日志器建造者

🍧经过上面模块的实现,我们希望通过一个统一的方式进行日志器的建造,接下来我们实现一个日志器建造者负责各个类型日志器的建造。

抽象基类

🍧为了方便同步与异步日志器的创建,我们创建了  LoggerType  字段用于二者的区分。

enum LoggerType
{LOGGER_SYNC,LOGGER_ASYNC
};

🍧在构造函数完成部分成员的初始化,而其他成员则在成员函数中传入即可,而 build 为纯虚函数,需要子类进行重写。 

class LoggerBuilder
{
public:LoggerBuilder(): _logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::value::DEBUG),_looper_type(AsyncType::ASYNC_SAFE) {}void BuildLoggerType(LoggerType type){_logger_type = type;}void BuildLoggerName(std::string name){_logger_name = name;}void BuildLoggerLevel(LogLevel::value level){_limit_level = level;}void BuildEnableUsafeAsync(){_looper_type = AsyncType::ASYNC_UNSAFE;}void BuildFormatter(const std::string& pattern){_formatter = std::make_shared<Formatter>(pattern);}template <class SinkType, class... Args>void BuildSinks(Args &&...args){LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);_sinks.push_back(psink);}virtual Logger::ptr build() = 0;protected:AsyncType _looper_type;         //异步线程模式LoggerType _logger_type;        //日志器模式std::string _logger_name;       //日志器名称 std::atomic<LogLevel::value> _limit_level;    //日志限制等级Formatter::ptr _formatter;           //格式化模块std::vector<LogSink::ptr> _sinks;    //落地模块
};

🍧局部日志器和全局日志器的区别就是在  build  函数中全局日志器会被保存起来,而局部日志器不会。

局部日志器建造

🍧首先我们需要对部分重要对象先进行检测,若不存在则使用默认的设置,但日志器的名字不能没有,若检测到未传入名字则直接报错。

🍧之后根据日志器的模式返回对应的日志器指针即可。

class LocalLoggerBuilder : public LoggerBuilder
{
public:Logger::ptr build() override{assert(_logger_name.empty() == false);if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){BuildSinks<StdoutSink>();}if (_logger_type == LOGGER_ASYNC){return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}
};

全局日志器建造者

🍧全局相比局部只需要多加一步,那么就是将获取的日志器指针添加到管理模块的单例对象中,而管理模块接下来我们会进行实现。

class GlobalLoggerBuilder : public LoggerBuilder
{
public:Logger::ptr build() override{assert(_logger_name.empty() == false);if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){BuildSinks<StdoutSink>();}Logger::ptr logger;if (_logger_type == LOGGER_ASYNC)logger = std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);elselogger = std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);LoggerManager::getInstance().AddLogger(logger);        //添加到单例对象中return logger;}
};

日志器管理模块

🍧对日志器的管理我们采取 KV 模型,以日志器名称作为定义一个日志器的唯一标识。为了处理并发访问的问题,成员中还需要增加一个互斥锁。

🍧同时,我们还要将这个管理模块设计成单例模式,使其在全局函数中只有一份。而另一个成员则是自动生成的默认日志器。

class LoggerManager
{
public:
private:LoggerManager()    //构造函数私有化{}
private:std::mutex _mutex;Logger::ptr _root_logger;std::unordered_map<std::string, Logger::ptr> _loggers;
};

单例对象的获取

🍧 C++11 中直接获取  static  变量是线程安全的,我们可以直接用这种方式进行单例对象的获取。

static LoggerManager& getInstance()    //返回引用
{static LoggerManager eton;return eton;
}

日志器的管理

🍧对于日志器的管理,我们需要实现日志器的添加判断存在获取

🍧判空非常简单,加锁后直接对名字进行查找最后判断结果即可,同时哈希表的查找效率极高,因此消耗并不大。

bool IsLoggerExist(const std::string& name)
{std::unique_lock<std::mutex> lock(_mutex);auto pos = _loggers.find(name);if (pos == _loggers.end())return false;return true;
}

🍧增加时,我们先判断对应名称的日志器是否存在,若存在直接返回即可,之后加锁往哈希表中插入数据即可。

void AddLogger(Logger::ptr& logger)
{if (IsLoggerExist(logger->GetLoggerName())){std::cout << "该名称的日志器以存在" << std::endl;return;}std::unique_lock<std::mutex> lock(_mutex);_loggers[logger->GetLoggerName()] = logger;
}

🍧获取日志器时,若查找不到对应日志器的话,可以选择直接报错也可以返回默认的日志器

Logger::ptr GetLogger(const std::string& name)
{std::unique_lock<std::mutex> lock(_mutex);auto pos = _loggers.find(name);if (pos == _loggers.end()){std::cout << "no such name logger" << std::endl;assert(false);}return pos->second;
}

构造函数

🍧之前成员中就定义了一个默认的日志器,而构造函数主要便是进行该默认日志器的初始化

LoggerManager()
{std::unique_ptr<LoggerBuilder> builder(new LocalLoggerBuilder());    //获取建造者builder->BuildLoggerName("root");        //填充默认日志器名称_root_logger = builder->build();         //日志器建造,并保存在成员变量中_loggers["root"] = _root_logger;         //全局化
}

🍧而默认日志器的获取,直接返回成员变量即可。

Logger::ptr rootLogger()
{return _root_logger;
}

代理日志器的接口

提供指定日志器的全局接口

🍧若是不进行一层封装,那么用户在使用时会感到十分的不便,需要先获取单例对象再调用函数

🍧这里直接提供两个全局接口,在函数中帮我们完成了上述操作,使用户可以直接获得指定的日志器。

// 1.提供指定日志器的全局接口
Logger::ptr Getlogger(const std::string& name)
{return LoggerManager::getInstance().GetLogger(name);
}Logger::ptr GetRoot()
{return LoggerManager::getInstance().rootLogger();
}

使用宏函数对日志器的接口进行代理

🍧接着我们可以使用宏函数原本日志器的接口进行代理,接下来调用对应的函数就不用手动传入文件名和行号了。

其中,##__VA_ARGS__ 代表的是可变参数列表。

// 2.使用宏函数对日志器的接口进行代理
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)

提供宏函数直接进行日志的标准输出

🍧同样提供宏函数直接调用默认日志器进行日志输出。

// 3.提供宏函数直接进行日志的标准输出
#define DEBUG(fmt, ...) GetRoot()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) GetRoot()->info(fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) GetRoot()->warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) GetRoot()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) GetRoot()->fatal(fmt, ##__VA_ARGS__)

拓展

🍧我们实现的日志系统支持落地类的拓展,因此我们在外部自己实现一个落地类,该落地类是根据时间进行滚动文件的。

enum TimeGap
{GAP_SECOND,GAP_MIN,GAP_HOUR,GAP_DAY
};class RollByTimeSink : public LogSink
{
public:RollByTimeSink(const std::string &basename, TimeGap gap_type): _basename(basename){switch (gap_type){case GAP_SECOND:_gap_size = 1;break;case GAP_MIN:_gap_size = 60;break;case GAP_HOUR:_gap_size = 3600;break;case GAP_DAY:_gap_size = 3600 * 24;break;}std::string filename = createNewFile();// 创建文件所在路径util::File::create_directory(util::File::path(filename));// 打开文件_ofs.open(filename, std::ios::binary | std::ios::app);assert(_ofs.is_open());}void log(const char *data, size_t len){time_t t = util::Date::now();// 判断当前文件是否在该时间段中,否则创建新文件if (t / _gap_size != _cur_size){std::string pathname = createNewFile();_ofs.close();_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());_cur_size = t / _gap_size;}_ofs.write(data, len);assert(_ofs.good());}private:std::string createNewFile(){time_t t = util::Date::now();struct tm lt;localtime_r(&t, &lt);std::stringstream filename;filename << _basename;filename << lt.tm_year + 1900 << lt.tm_mon + 1<< lt.tm_mday << lt.tm_hour<< lt.tm_min << lt.tm_sec;filename << "-";filename << _name_count++;filename << ".log";return filename.str();}private:size_t _name_count;std::string _basename;std::ofstream _ofs;size_t _cur_size;size_t _gap_size;
};

🍧在主函数中,我们搭建出对应的日志器,接着进行五秒的日志输出。

int main()
{std::unique_ptr<LoggerBuilder> builder(new GlobalLoggerBuilder());builder->BuildLoggerLevel(LogLevel::value::WARN);builder->BuildLoggerName("Async_logger");builder->BuildLoggerType(LoggerType::LOGGER_ASYNC);builder->BuildFormatter("[%c][%f:%l]%m%n");builder->BuildSinks<RollByTimeSink>("./logfile/roll-async-by-time", GAP_SECOND);Logger::ptr logger = builder->build();size_t cur = util::Date::now();while (util::Date::now() < cur + 5){logger->fatal("%s", "this is a test log");usleep(1000);}return 0;
}

🍧可以看到生成了 0~6 的文件,而 0 6 号文件因为位于计时的开始和结束并没有写入数据实际的数据主要写入在 1~5 号文件中。

 

测试

🍧在项目总体完成后,我们接下来我们将对该日志系统的性能进行测试。

🍧测试环境: 2核2G 云服务器 CentOS 7.6.1810 。

测试函数的实现

🍧为了测试该日志系统在多线程环境下的运行状态,因此该测试函数内部需要创建多个线程同时进行写日志的操作。

🍧而线程的数量交由外部用户决定,首先计算每个线程需要输出几条日志,接着创建线程输出日志并开始计时,任务完成后进行时间的计算

void bench(const std::string& logger_name, size_t thread_num, size_t msg_num, size_t msg_len)
{// 获取日志器Logger::ptr logger = Getlogger(logger_name);if (logger.get() == nullptr){return;}std::cout << "测试日志: " << msg_num << " 条,总大小: " << msg_num * msg_len / 1024 << "KB" << std::endl;// 组织指定长度的日志消息std::string msg(msg_len - 1, 'A'); //-1是为了添加\n// 创建指定数量的线程std::vector<std::thread> threads;std::vector<double> cost_array(thread_num);    //记录各个线程的消耗时间size_t per_num = msg_num / thread_num;for (int i = 0; i < thread_num; i++){threads.emplace_back([&, i](){// 开始测试int num = per_num;if (i + 1 == thread_num)num += (msg_num % thread_num);auto start = std::chrono::high_resolution_clock::now();for (int i = 0; i < num; i++){logger->fatal("%s", msg.c_str());}auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double> cost = end - start;cost_array[i] = cost.count(); });}for (int i = 0; i < thread_num; i++){threads[i].join();}// 计算总耗时,求并发最大值double max_cost = 0;for (int i = 0; i < cost_array.size(); i++){if (cost_array[i] > max_cost)max_cost = cost_array[i];if (i + 1 == thread_num)per_num += (msg_num % thread_num);std::cout << "线程" << i + 1 << ": "<< "\t输出日志数量: " << per_num << ",耗时: " << cost_array[i] << "s" << std::endl;}size_t per_sec_msg = msg_num / max_cost;size_t per_sec_size = per_sec_msg * msg_len / 1024;std::cout << "\t总耗时: " << max_cost << "s" << std::endl;std::cout << "\t每秒输出日志数量: " << per_sec_msg << "条" << std::endl;std::cout << "\t每秒输出日志大小: " << per_sec_size << "KB" << std::endl;
}

🍧我们还可以对日志器的创建进行封装,分成同步日志测试函数异步日志测试函数

void sync_bench()
{std::unique_ptr<LoggerBuilder> builder(new GlobalLoggerBuilder());builder->BuildLoggerName("sync_logger");builder->BuildLoggerType(LoggerType::LOGGER_SYNC);builder->BuildFormatter("%m%n");builder->BuildSinks<FileSink>("./logfile/sync.log");builder->build();bench("sync_logger", 5, 1000000, 100);
}void async_bench()
{std::unique_ptr<LoggerBuilder> builder(new GlobalLoggerBuilder());builder->BuildLoggerName("Async_logger");builder->BuildLoggerType(LoggerType::LOGGER_ASYNC);builder->BuildEnableUsafeAsync(); // 排除实际落地时间造成的影响builder->BuildFormatter("%m%n");builder->BuildSinks<FileSink>("./logfile/async.log");builder->build();bench("Async_logger", 5, 1000000, 100);
}

测试结果 

🍧图一为同步日志器,图二为异步日志器。

        

总结

🍧 在该项目中我们频繁地使用继承多态的相关操作,因此需要熟练掌握对应的操作方法,理解底层原理

🍧同时,我们还用到了多种设计模式,例如单例模式工厂模式建造者模式代理模式,同样需要学习对应模式的思想。

🍧在函数实现时还使用到了不定参数列表,一样需要熟悉它的使用方法,例如可变参数包的展开方式,及可变模板参数列表的使用。

🍧而对于双缓冲区的运行逻辑也要十分的清晰,它是如何实现数据写入,如何保证线程安全的。

🍧最后就是可以对整体项目进行结构的梳理,可以先尝试画出项目的各个模块,再在此基础上进行消息的拓展。 

源码

🍧好了,今天的项目分享到这里就结束了,下面是对应的源码可以进行参考。

日志系统

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/167898.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

先后在影酷/传祺E9/昊铂GT量产交付,这家ADAS厂商何以领跑

智能泊车赛道正在迎来黄金增长期&#xff0c;以魔视智能为代表的玩家正在驶入大规模量产的“快车道”。 继在广汽传祺影酷、广汽传祺 E9实现规模化量产交付之后&#xff0c;魔视智能的Magic Parking智能泊车系列解决方案再度在广汽埃安旗下高端智能轿跑——昊铂GT上面实现量产…

Go 代码包与引入:如何有效组织您的项目

一、引言 在软件开发中&#xff0c;代码的组织和管理是成功项目实施的基础之一。特别是在构建大型、可扩展和可维护的应用程序时&#xff0c;这一点尤为重要。Go语言为这一需求提供了一个强大而灵活的工具&#xff1a;代码包&#xff08;Packages&#xff09;。代码包不仅允许…

Selenium+Pytest自动化测试框架详解

前言 selenium自动化 pytest测试框架 本章你需要 一定的python基础——至少明白类与对象&#xff0c;封装继承&#xff1b;一定的selenium基础——本篇不讲selenium&#xff0c;不会的可以自己去看selenium中文翻译网 一、测试框架简介 测试框架有什么优点 代码复用率高&…

Java 基础 面试 多线程

1.多线程 1.1 线程&#xff08;Thread&#xff09; 线程时一个程序内部的一条执行流程&#xff0c;java的main方法就是由一条默认的主线程执行 1.2 多线程 多线程是指从软硬件上实现的多条执行流程的技术&#xff08;多条线程由CPU负责调度执行&#xff09; 许多平台都离不开多…

【Nginx34】Nginx学习:安全链接、范围分片以及请求分流模块

Nginx学习&#xff1a;安全链接、范围分片以及请求分流模块 又迎来新的模块了&#xff0c;今天的内容不多&#xff0c;但我们都进行了详细的测试&#xff0c;所以可能看起来会多一点哦。这三个模块之前也从来都没用过&#xff0c;但是通过学习之后发现&#xff0c;貌似还都挺有…

前端react入门day01-了解react和JSX基础

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 React介绍 React是什么 React的优势 React的市场情况 开发环境搭建 使用create-react-app快速搭建…

Qt窗体设计的布局

本文介绍Qt窗体的布局。 Qt窗体的布局分为手动布局和自动布局&#xff0c;手动布局即靠手工排布各控件的位置。而自动布局则是根据选择的布局类型自动按此类型排布各控件的位置&#xff0c;使用起来比较方便&#xff0c;本文主要介绍Qt的自动布局。 1.垂直布局 垂直布局就是…

看微功耗遥测终端机如何轻松应对野外环境挑战?

在野外&#xff0c;数据的实时监测和传输是至关重要的。无论是环境温度、湿度&#xff0c;还是水位、流量&#xff0c;都需要精准把控。然而&#xff0c;传统的监测方法往往受限于电源供应问题&#xff0c;而无法充分发挥其功能。这时候&#xff0c;一款微功耗遥测终端机&#…

Zabbix“专家坐诊”第207期问答汇总

问题一 Q&#xff1a;不小心把host表删除了&#xff0c;怎么处理&#xff1f;现在使用的zabbix 4.0.3的server&#xff0c;agent是4.2.1&#xff0c;能不能不动agent的情况下升级server版本&#xff0c;重新部署&#xff1f; A&#xff1a;数据库有备份话恢复即可&#xff0c;…

在线零售多用户多门店连锁商城系统

在线零售多用户商城系统和多门店连锁商城系统的核心都是线上线下相结合的&#xff0c;线上和线下结合&#xff0c;一体化是在线新零售多用户商城系统发展的趋势&#xff0c;现在移动互联网时代&#xff0c;越来越多的传统企业&#xff0c;如&#xff1a;连锁店铺&#xff0c;连…

SpringBoot中的日志使用

SpringBoot的默认使用 观察SpringBoot的Maven依赖图 可以看出来&#xff0c;SpringBoot默认使用的日志系统是使用Slf4j作为门户&#xff0c;logback作为日志实现 编写一个测试代码看是否是这样 SpringBootTest class SpringbootLogDemoApplicationTests {//使用Slf4j来创建LOG…

Android音视频开发之基础知识

一、视频文件 1、视频格式 常见格式&#xff1a;mp4、mkv、flv 封装的数据&#xff1a;音频码流、视频码流 常用工具&#xff1a; [FFmpeg下载]:https://ffmpeg.org/download.html 下载、安装并配置环境变量 ffmpeg.exe 视频编解码 ffplay.exe 播放器库 ffprobe.exe 音视频分…

17-spring aop调用过程概述

文章目录 1.源码2. debug过程 1.源码 public class TestAop {public static void main(String[] args) throws Exception {saveGeneratedCGlibProxyFiles(System.getProperty("user.dir") "/proxy");ApplicationContext ac new ClassPathXmlApplication…

【TES605】基于Virtex-7 FPGA的高性能实时信号处理平台

板卡概述 TES605是一款基于Virtex-7 FPGA的高性能实时信号处理平台&#xff0c;该平台采用1片TI的KeyStone系列多核DSP TMS320C6678作为主处理单元&#xff0c;采用1片Xilinx的Virtex-7系列FPGA XC7VX690T作为协处理单元&#xff0c;具有2个FMC子卡接口&#xff0c;各个处理节…

【PyTorch】深度学习实践 02 线性模型

深度学习的准备过程 准备数据集选择模型模型训练进行推理预测 问题 对某种产品花费 x 个工时&#xff0c;即可得到 y 收益&#xff0c;现有 x 和 y 的对应表格如下&#xff1a; x &#xff08;hours&#xff09; y&#xff08;points&#xff09;12243648 求花费4个工时可得…

Power BI 傻瓜入门 5. 准备数据源

本章内容将介绍&#xff1a; 定义Power BI支持的数据源类型探索如何在Power BI中连接和配置数据源了解选择数据源的最佳做法 现代组织有很多数据。因此&#xff0c;不用说&#xff0c;微软等企业软件供应商已经构建了数据源连接器&#xff0c;以帮助组织将数据导入Power BI等…

Microsoft Edge浏览器中使用免费的ChatGPT

一、双击打开浏览器 找到&#xff1a;扩展&#xff0c;打开 二、打开Microsoft Edge加载项 三、Move tab新标签 获取免费ChatGPT 四、启用Move tab。启用ChatGPT。 扩展 管理扩展 启用 五、新建标签页&#xff0c;使用GPT 六、使用举例 提问 GPT回复

asp.net网球馆计费管理系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio

一、源码特点 asp.net网球馆计费管理系统是一套完善的web设计管理系统&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver2008&#xff0c;使用c#语 言开发 aspnet网球馆计费管理系统1 二、…

零售创新:社交媒体如何改变跨境电商游戏规则?

在当今数字化的时代&#xff0c;社交媒体已经成为了我们日常生活中不可或缺的一部分。Facebook、Instagram、Twitter、WeChat等平台不仅让我们与朋友家人保持联系&#xff0c;还成为了一个新的商业战场。特别是在跨境电商领域&#xff0c;社交媒体的崛起正在彻底改变游戏规则。…

选择实验室超声波清洗机具有哪些作用?

实验室超声波清洗机之所以一经面世就能迅速赢得众多消费者的心&#xff0c;这是因为实验室超声波清洗机设备厂家所提供的高性能超声波清洗机具有非常好的清洗效果。清洗效果的高低一直是各实验室关注的焦点问题&#xff0c;现在就选择实验室超声波清洗机具有哪些作用作简要阐述…