欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > 日志项目——代码设计

日志项目——代码设计

2026/4/19 23:11:50 来源:https://blog.csdn.net/xsc2004zyj/article/details/148430066  浏览:    关键词:日志项目——代码设计

1.实用类

一条日志消息需要有时间,并且日志落地的具体位置还不一定存在。所以,我们需要使用:实用类用来获取当前时间,以及创建日志落地的目录。

创建路径时,需要一级一级目录的创建,有了上级目录才能创建下级目录。

/*实用工具类, 在日志项目中可能会频繁使用1.获取当前事件2.判断指定文件是否存在3.获取文件路径4.创建目录
*/#pragma once
#include <iostream>
#include <string>
#include <ctime>
#include <sys/stat.h>
#include <sys/types.h>namespace logging
{namespace util{class Date{public:static time_t getTime() { return static_cast<time_t>(time(nullptr)); }};class File{public:static bool exists(const std::string &pathname){struct stat statbuf;if(stat(pathname.c_str(), &statbuf) == 0) return true;return false;}static std::string getFilePath(const std::string &pathname){// ./abc/def/g.txtsize_t pos = pathname.find_last_of("/\\");if(pos == std::string::npos) return "."; // 如果没找到分隔符,则说明该文件就在当前目录下return pathname.substr(0, pos + 1);}static void createDirectory(const std::string &directory){// ./abc/def/g.txt// 判断是纯路径,还是带了文件名,如果带了文件名,需要将文件名去掉 TODO//if(directory.rfind(".") == std::string::npos) {//    return;//}size_t pos = 0, cur = 0;while(cur < directory.size()){pos = directory.find_first_of("/\\", cur);if(pos == std::string::npos) {// ./abc/def/g.txt// 最后一次进入这里,有可能会包含文件名,导致创建失败// 但我们的目的只有创建目录,目录在下面的过程中已经创建好了,这里失败了也没有关系int n = mkdir(directory.c_str(), 0777);break;}std::string parent_dir = directory.substr(0, pos + 1);if(exists(parent_dir)) {cur = pos + 1;continue;}mkdir(parent_dir.c_str(), 0777);cur = pos + 1;}}};}
}

实用类测试:

int main()
{time_t time = logging::util::Date::getTime();struct tm timeset;localtime_r(&time, &timeset);std::cout << timeset.tm_year + 1900 << "/";std::cout << timeset.tm_mon + 1 << "/";std::cout << timeset.tm_mday << "-";std::cout << timeset.tm_hour << ":";std::cout << timeset.tm_min << ":";std::cout << timeset.tm_sec << std::endl;std::string pathname = "./test-util/test.log";logging::util::File::createDirectory(logging::util::File::getFilePath(pathname));return 0;
}

 结果:

2.日志等级类

该类主要用于定义日志等级,从而借助等级来对日志进行分类处理。因为最后输出的日志是一条格式化字符串,所以,该类还提供了将对应的日志等级转换为字符串的功能。

/*日志等级模块通过日志等级来判断程序的健康状况并且可以通过该模块,实现特定等级日志的不可见性1.枚举类——日志等级2.将日志等级转换为字符串
*/#pragma once
namespace logging
{class logLevel{public:enum class value{UNKNOWN = 0,DEBUG,INFO,WARNNING,ERROR,FATAL,OFF};static const char *toString(logging::logLevel::value v){switch (v){case logging::logLevel::value::DEBUG:return "DEBUG";case logging::logLevel::value::INFO:return "INFO";case logging::logLevel::value::WARNNING:return "WARNNING";case logging::logLevel::value::ERROR:return "ERROR";case logging::logLevel::value::FATAL:return "FATAL";}return "UNKNOWN";}};
}

3.日志消息类

日志消息类用于存储一条日志的各个部分,比如时间、线程ID、文件名、行号等等

/*日志消息类,用来存储一条日志消息所需要的数据1.日志输出时间2.日志等级3.日志所属源文件4.日志所属源文件行号5.线程ID6.日志器名称7.日志主体内容
*/#pragma once#include <iostream>
#include <string>
#include <thread>
#include "level.hpp"
#include "util.hpp"namespace logging
{struct logMessage{logMessage(logLevel::value level, const std::string &file, size_t line, const std::string &logger, const std::string &msg): _ctime(util::Date::getTime()),_tid(std::this_thread::get_id()),_src_file(file),_line(line),_rank(level),_payload(msg),_logger(logger){}time_t _ctime;         // 输出时间的时间,时间戳size_t _line;          // 行号std::thread::id _tid;  // 线程IDlogLevel::value _rank; // 日志等级std::string _src_file; // 日志所属源文件std::string _payload;  // 日志有效载荷std::string _logger;   // 日志器名称};
}

4.日志输出格式化类

日志的输出是要有格式的,在该项目中,日志的输出格式可以由用户自己定义:

    /*
        格式化字符串类,用于将日志消息,按照一定规则的格式字符样式,生成一个格式化字符串
        %d 表示日期 ,包含子格式 {%H:%M:%S}
        %t 表示线程ID
        %c 表示日志器名称
        %f 表示源码文件名
        %l 表示源码行号
        %p 表示日志级别
        %T 表示制表符缩进
        %m 表示主体消息
        %n 表示换行
    */

根据不同的格式字符,来表示不同的格式化结果。 

日志输出格式化类有两个部分,分别是格式化字符串类,以及格式化子项类。

格式化字符串类用于将我们指定的格式(如:"[%d{%Y%m%d-%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"),转化为一个一个的子项(如%d,%t,%c等等),存储到一个数组中。然后根据该数组,将日志消息对象的每一个部分依次拿出来,拼接到一个字符串后面。

格式化子项类:就是用于将指定的内容 转换成字符串 拼接到 结果字符串后面。如果%d,就要将日志消息对象的时间拼接到字符串后面...

/*格式化字符串类 以及 格式化子项类格式化字符串类:将给定的格式转换成一个一个的格式化子项, 将所有的格式化子项存储在一个数组中,通过遍历该数组,调用格式化子项,完成字符串拼接格式化子项类:用于将日志消息拼接到以字符串后面,以便形成一个特定格式的格式化字符串
*/#pragma once
#include <memory>
#include <vector>
#include <sstream>
#include <utility>
#include <ctime>
#include "message.hpp"
#include "level.hpp"namespace logging
{/*格式化消息子项,根据不同格式化字符的样式,按照该样式顺序,依次将日志消息内容拼接成一个特定格式的字符串时间、等级、线程ID、文件名、行号、日志器名称、日志有效载荷、制表符、换行符、其他的*/class formatItem{public:using s_ptr = std::shared_ptr<formatItem>;virtual void format(std::ostream &out, logMessage &msg) = 0;};// 时间子项class timeFormatItem : public formatItem{public:timeFormatItem(const std::string &fmt = "%Y%m%d%H:%M:%S") : time_fmt(fmt) {}void format(std::ostream &out, logMessage &msg) override{struct tm timeset;                  // 时间信息集合,包含年月日,时分秒localtime_r(&msg._ctime, &timeset); // 用于将时间戳,转化为时间集合char s[32] = {0};strftime(s, sizeof(s), time_fmt.c_str(), &timeset); // 将时间结合按照格式转换为字符串out << s;}private:// 时间有不同的输出格式,比如:时分秒/年月日时分秒/等等// 所以要根据不同的时间格式,来追加不同的时间字符串std::string time_fmt;};// 等级子项class rankFormatItem : public formatItem{public:void format(std::ostream &out, logMessage &msg) override { out << logLevel::toString(msg._rank); }};// 线程ID子项class threadFormatItem : public formatItem{public:void format(std::ostream &out, logMessage &msg) override { out << msg._tid; }};// 文件名子项class fileFormatItem : public formatItem{public:void format(std::ostream &out, logMessage &msg) override { out << msg._src_file; }};// 行号子项class lineFormatItem : public formatItem{public:void format(std::ostream &out, logMessage &msg) override { out << msg._line; }};// 日志器名称子项class loggerFormatItem : public formatItem{public:void format(std::ostream &out, logMessage &msg) override { out << msg._logger; }};// 有效载荷子项class payloadFormatItem : public formatItem{public:void format(std::ostream &out, logMessage &msg) override { out << msg._payload; }};// 制表符子项class tabFormatItem : public formatItem{public:void format(std::ostream &out, logMessage &msg) override { out << "\t"; }};// 换行符子项class newlineFormatItem : public formatItem{public:void format(std::ostream &out, logMessage &msg) override { out << "\n"; }};// 其他子项class otherFormatItem : public formatItem{public:otherFormatItem(const std::string &str) : _str(str) {}void format(std::ostream &out, logMessage &msg) override { out << _str; }private:std::string _str;};/*格式化字符串类,用于将日志消息,按照一定规则的格式字符样式,生成一个格式化字符串%d 表示日期 ,包含子格式 {%H:%M:%S}%t 表示线程ID%c 表示日志器名称%f 表示源码文件名%l 表示源码行号%p 表示日志级别%T 表示制表符缩进%m 表示主体消息%n 表示换行*/class formatter{public:using s_ptr = std::shared_ptr<formatter>;public:formatter(const std::string &pattern = "[%d{%Y%m%d-%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") :_pattern(pattern) { parsePattern(); }// 将日志内容,按照规定好的格式,进行字符串拼接,最后返回一个格式化字符串std::string format(logMessage &msg){std::stringstream ss;format(ss, msg);return ss.str();}void format(std::ostream &out, logMessage &msg){for (auto item : _items){item->format(out, msg);}}private:// 对格式化字符串进行解析,按照格式化字符的样式,填写子项数组bool parsePattern(){//std::cout << "进行格式字符串解析" << std::endl;// abcde[%d{%H:%M:%S}][%p][%c]%T%m%n}size_t pos = 0;std::string val, key;std::vector<std::pair<std::string, std::string>> elem;while (pos < _pattern.size()) {// 1. 找%,%之前的都是原始字符串size_t percent_pos = _pattern.find('%', pos);if (percent_pos == std::string::npos) {// 到了字符串结尾,都没有找到%,此时这些部分都是原始字符串// 添加之后,直接退出循环val = _pattern.substr(pos);if(!val.empty()){elem.emplace_back("", val);val.clear();}break;}// 找到了%,保存原始字符串if(percent_pos > pos) {val = _pattern.substr(pos, percent_pos - pos);elem.emplace_back("", val);val.clear();}// 2. 到了这里说明找到了%,接下来判断%后面的是什么字符pos = percent_pos + 1; // pos指向%下一个位置if(pos >= _pattern.size()) {std::cerr << "%后无字符,格式错误" << std::endl;return false; // 如果%后面没有字符了,说明该格式字符串不符合规范,直接返回false}key = _pattern[pos]; // %后面的字符if(key == "%") {elem.emplace_back("", "%");pos += 1;continue;}// 到这里,说明%后面是一个格式字符 即key// 继续判断格式化字符后面是否有子格式{}pos += 1; // pos指向格式化字符的后面val.clear();if(pos < _pattern.size() && _pattern[pos] == '{') {// 这里,说明格式化字符后面还有子格式 pos指向{pos += 1; // pos 指向{后的第一个字符while(pos < _pattern.size() && _pattern[pos] != '}') {val.push_back(_pattern[pos++]);}if(pos == _pattern.size()) {std::cerr << "子格式{}匹配失败" << std::endl;return false;}elem.emplace_back(key, val);pos += 1;}else {// 格式化字符后面没有其他元素了 ,或者后面不是子格式elem.emplace_back(key, "");key.clear();}}for(auto e : elem) {//std::cout << e.first << ":" << e.second << std::endl;_items.emplace_back(createItem(e.first, e.second));}return true;}// 根据格式化字符,来创建对应的格式化子项formatItem::s_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<threadFormatItem>();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 == "p")return std::make_shared<rankFormatItem>();if (key == "T")return std::make_shared<tabFormatItem>();if (key == "m")return std::make_shared<payloadFormatItem>();if (key == "n")return std::make_shared<newlineFormatItem>();if(key == "")  return std::make_shared<otherFormatItem>(val);std::cerr << "非法格式字符->" << "%" << key << std::endl;abort(); }private:std::string _pattern; // 格式化规则字符串---格式化字符样式std::vector<formatItem::s_ptr> _items;};
}

测试日志输出格式化类:

int main()
{// 默认的日志输出格式"[%d{%Y%m%d-%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")logging::formatter::s_ptr formatter = std::make_shared<logging::formatter>();logging::logMessage msg(logging::logLevel::value::DEBUG, __FILE__, __LINE__, "root", "test for formatter...");std::string log = formatter->format(msg);std::cout << log;return 0;
}

 结果:

5.日志落地类

日志落地类,其实就是控制日志具体是输出到那里的:stdout、指定文件、滚动文件。

而在设计日志落地类时,我们采用简单工厂模式,通过基类来指向其他具体的落地方向。这样,用户就可以在任意地方修改该日志的落地策略。

而且对于每一种具体的落地策略来说,会有不同的参数。比如对于输出到标准输出来说,不需要任何其他参数,就可以直接输出。而对于落地到指定文件,我们得把文件传给该落地策略。

 而在设计工厂的时候,我们要根据不同的落地方向,来创建不同的落地策略。而每个策略的参数又不同,所以我们在设计工厂的时候,采用模板+可变参数来实现。

/*日志落地模块---负责将格式化字符串输出到指定的位置上指定位置:1.标准输出---常用于内部测试/debug2.指定文件---将日志消息输出到指定文件内部3.滚动文件---按照时间/文件大小,将日志滚动式的切换到不同的文件中,进行输出。防止单一文件过大的问题实现思想:借助工厂设计模式1.抽象出一个基类2.有该基类派生出其他不同的落地模式
*/#pragma once
#include "util.hpp"
#include <fstream>
#include <sstream>
#include <memory>
#include <cassert>namespace logging
{// 基类落地// 指向其他的派生落地方向// 往后用户自己想要派生出其他的落地方向,可以用该基类指针指向class sink{public: using s_ptr = std::shared_ptr<sink>;virtual ~sink() {}virtual void logSink(const char *data, size_t len) = 0;};// 落地方向:标准输出class stdoutSink : public sink{public:void logSink(const char *data, size_t len) override { std::cout.write(data, len); }};// 落地方向:指定文件class fileSink : public sink{public:fileSink(const std::string &pathname):_pathname(pathname){// 1.创建指定目录util::File::createDirectory(util::File::getFilePath(_pathname));// 2.打开指定文件// 以二进制 追加方式打开文件_ofs.open(_pathname, std::ios::binary | std::ios::app);// 3.判断文件是否打开成功assert(_ofs.is_open());}void logSink(const char *data, size_t len) override { _ofs.write(data, len);assert(_ofs.good());}private:std::string _pathname;std::ofstream _ofs;};// 落地方向:滚动文件——通过大小切换文件class rollSinkBySize : public sink{public:rollSinkBySize(const std::string &basename, size_t maxFileSize):_basename(basename), _max_fsize(maxFileSize), _cur_fsize(0), _name_count(0){// 1.构建完整文件名std::string filename = createNewFile();// 2.创建指定目录util::File::createDirectory(util::File::getFilePath(filename));// 3.打开指定文件// 以二进制 追加方式打开文件_ofs.open(filename, std::ios::binary | std::ios::app);// 4.判断文件是否打开成功assert(_ofs.is_open());}void logSink(const char *data, size_t len) override{// 1.写入前判断当前文件大小是否已经超过范围if(_cur_fsize >= _max_fsize) {// 1.1 关闭原来的文件_ofs.close();//1.2 创建一个新的文件std::string newFile = createNewFile();// 1.3打开该文件_ofs.open(newFile, std::ios::binary | std::ios::app);assert(_ofs.is_open());_cur_fsize = 0; // 打开新文件后,将当前文件大小清空}// 2.写入日志_ofs.write(data, len);_cur_fsize += len;}private:// 切换文件std::string createNewFile(){// 通过基础文件名,创建出一个具体的带有时间的文件名// 1.获取当前时间time_t t = util::Date::getTime();struct tm tmset;localtime_r(&t, &tmset);// 将基础文件名与时间后缀拼接构成一个完整的文件名std::stringstream ss;ss << _basename;ss << tmset.tm_year + 1900;ss << tmset.tm_mon + 1;ss << tmset.tm_mday;ss << tmset.tm_hour; ss << tmset.tm_min;ss << tmset.tm_sec;ss << "-";ss << _name_count++; ss << ".log";return ss.str();}private:std::string _basename;  // 基础文件名:./logs/base-xxxx(时间).log ->  ./logs/base-20250601101832.logstd::ofstream _ofs;     // 用于管理文件的句柄size_t _max_fsize;      // 最大文件的大小,超过该大小就要切换一个新的文件size_t _cur_fsize;      // 当前文件的大小size_t _name_count;};// 借助简单工厂模式,实现生成不同的日志落地模式// 再借助模板参数和不定参函数,来实现创建不同的参数的函数// 如果设置成模板类,会导致要传入多个类型,不方便// 所以这里将模板类,改为静态模板//template <typename sinkType, typename ...Args>class sinkFactory{public:template <typename sinkType, typename ...Args>static sink::s_ptr create(Args &&...args){return std::make_shared<sinkType>(std::forward<Args>(args)...);}};
}

测试日志落地类:

int main()
{// 将一条日志消息,按照指定格式,整合为一个格式化字符串// 默认的日志输出格式"[%d{%Y%m%d-%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")logging::formatter::s_ptr formatter = std::make_shared<logging::formatter>();logging::logMessage msg(logging::logLevel::value::DEBUG, __FILE__, __LINE__, "root", "test for formatter...");std::string log = formatter->format(msg);size_t size = log.size();// 通过日志落地类来确定格式化日志消息落地到那个具体方向logging::sink::s_ptr ssink = logging::sinkFactory::create<logging::stdoutSink>(); // 落地到标准输出logging::sink::s_ptr fsink = logging::sinkFactory::create<logging::fileSink>("./testsink/filesink/sink.log");logging::sink::s_ptr rfsink = logging::sinkFactory::create<logging::rollSinkBySize>("./testsink/rollfilesink/roll-file-", 1024 * 1024);ssink->logSink(log.c_str(), log.size());fsink->logSink(log.c_str(), log.size());int count = 0;while(count < 1024 * 1024 * 10) {rfsink->logSink(log.c_str(), log.size());count += 84;}return 0;
}

 结果:

落地到stdout:

落地到指定文件:

落地到滚动文件:共输出了10M的数据,而滚动文件的大小为1M,所以这些日志会输出到10个滚动文件中,大小为1M左右

 

6.日志器类

日志器类主要是对前面几个类的整合,用于简化日志的输出。

日志输出有等级之分,但是我们现在还是得指定等级,而在日志器类中,我们通过实现不同等级的接口,来实现日志输出的等级。同时通过这些不同等级的接口,还可以实现控制日志的输出等级,例如:我们在初始化的时候将限制日志等级设置为error,此时我们调用debug接口或者info接口来输出日志,其不会被真正输出。

/*日志器模块将格式化模块、日志等级、落地类、日志消息模块结合起来,用一个类来实现日志的创建,格式化,到落地这整个过程日志器中包含限制等级,低于限制等级的日志不会被打印日志器中的各种等级接口,用的是输出不同等级的日志---内部首先判断日志等级,对不定参正文进行解析成字符串,然后进行格式化还是落地抽象出来的日志器用来派生同步日志器和异步日志器但是直接使用日志器模块,会导致使用成本增高,学习成本升高因为建造一个日志器需要用到多个组件:等级、日志器名称、格式化对象、落地器对象等等所以我们可以使用建造者模式,来对创建日志器的过程进行建模
*/#pragma once
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "message.hpp"
#include "looper.hpp"
#include <atomic>
#include <mutex>
#include <cstdarg>
#include <unordered_map>namespace logging
{class logger{public:using s_ptr = std::shared_ptr<logger>;public:logger(const std::string &logger_name, logLevel::value level, formatter::s_ptr &formatter, std::vector<sink::s_ptr> &sinks):_limits_rank(level), _formatter(formatter), _sinks(sinks.begin(), sinks.end()), _logger_name(logger_name){}const std::string &name() { return _logger_name; }void debug(const std::string &file, const size_t line, const std::string &fmt, .../*正文部分*/){// 日志等级输出接口,用于将数据格式化为一个日志消息对象,然后通过日志落地模块,将日志器输出到不同位置// 1.判断日志等级与限制等级if(logLevel::value::DEBUG < _limits_rank) return;// 2. 使用fmt格式,对不定参进行解析va_list ap;va_start(ap, fmt);char *res;int n = vasprintf(&res, fmt.c_str(), ap);if(n == -1) {std::cout << "vasprintf filed!!!";free(res);return;}va_end(ap);// 3.构建一个日志消息对象, 并通过具体落地方式和方向,进行落地serialize(logLevel::value::DEBUG, file, line, res);}void info(const std::string &file, const size_t line, const std::string &fmt, ...){if(logLevel::value::INFO < _limits_rank) return;va_list ap;va_start(ap, fmt);char *res;int n = vasprintf(&res, fmt.c_str(), ap);if(n == -1) {std::cout << "vasprintf filed!!!";free(res);return;}va_end(ap);serialize(logLevel::value::INFO, file, line, res);}void warnning(const std::string &file, const size_t line, const std::string &fmt, ...){if(logLevel::value::WARNNING < _limits_rank) return;va_list ap;va_start(ap, fmt);char *res;int n = vasprintf(&res, fmt.c_str(), ap);if(n == -1) {std::cout << "vasprintf filed!!!";free(res);return;}va_end(ap);serialize(logLevel::value::WARNNING, file, line, res);}void error(const std::string &file, const size_t line, const std::string &fmt, ...){if(logLevel::value::ERROR < _limits_rank) return;va_list ap;va_start(ap, fmt);char *res;int n = vasprintf(&res, fmt.c_str(), ap);if(n == -1) {std::cout << "vasprintf filed!!!";free(res);return;}va_end(ap);serialize(logLevel::value::ERROR, file, line, res);}void fatal(const std::string &file, const size_t line, const std::string &fmt, ...){if(logLevel::value::FATAL < _limits_rank) return;va_list ap;va_start(ap, fmt);char *res;int n = vasprintf(&res, fmt.c_str(), ap);if(n == -1) {std::cout << "vasprintf filed!!!";free(res);return;}va_end(ap);serialize(logLevel::value::FATAL, file, line, res);}protected:void serialize(logLevel::value level, const std::string &file, const size_t line, const char *fmt){// 构建日志消息对象logMessage msg(level, file, line, _logger_name, fmt);// 将日志消息对象格式化为一个格式化字符串std::stringstream ss;_formatter->format(ss, msg);// 通过落地方式,进行落地logSink(ss.str().c_str(), ss.str().size());}virtual void logSink(const char *data, const size_t len) = 0;protected:// 日志器输出等级,日志输出前都必须访问该限制等级,判断是否可以输出,所以为了避免多线程导致线程安全,这里使用原子类型std::atomic<logLevel::value> _limits_rank;formatter::s_ptr _formatter;     // 日志器格式std::vector<sink::s_ptr> _sinks; // 日志器落地对象,因为一个日志可能又多个落地方向,所以这里使用数组,包含多个落地对象std::string _logger_name;        // 日志器名称——日志器的唯一标识符std::mutex _mutex;               // 互斥锁,保证在多线程输出时的线程安全};

而该日志系统是支持同步和异步写日志的。所以,日志器采用多态实现,分为同步日志器和异步日志器。

6.1同步日志器

对于同步日志器来说,就只是让业务线程自己去落地日志消息而已。所以,对于同步日志器,我们可以重写该日志器的logSink方法即可。

    // 同步日志器class syncLogger : public logger{public:syncLogger(const std::string &logger_name, const logLevel::value level, formatter::s_ptr &formatter, std::vector<sink::s_ptr> &sink):logger(logger_name, level, formatter, sink){}protected:void logSink(const char *data, const size_t len) override{std::unique_lock<std::mutex> lock(_mutex);if(_sinks.empty()) return;for(auto sink : _sinks)sink->logSink(data, len);}};

测试同步日志器的使用以及日志的输出:

int main()
{// 使用同步日志器输出日志std::string loggername = "sync_logger"; // 日志器名称logging::logLevel::value limit = logging::logLevel::value::WARNNING; // 日志器限制输出等级logging::formatter::s_ptr formatter = std::make_shared<logging::formatter>(); // 日志器日志的输出格式// 因为日志器可能同时有多种落地方向,即在标准输出,指定文件,滚动文件logging::sink::s_ptr ssink = logging::sinkFactory::create<logging::stdoutSink>();logging::sink::s_ptr fsink = logging::sinkFactory::create<logging::fileSink>("./testsink/filesink/sink.log");logging::sink::s_ptr rfsink = logging::sinkFactory::create<logging::rollSinkBySize>("./testsink/rollfilesink/roll-file-", 1024 * 1024);// 将以上的落地方向,都初始化到一个落地方向数组中std::vector<logging::sink::s_ptr> sinks;sinks.emplace_back(ssink);sinks.emplace_back(fsink);sinks.emplace_back(rfsink);// 创建日志器logging::logger::s_ptr synclogger = std::make_shared<logging::syncLogger>(loggername, limit, formatter, sinks);// 输出日志synclogger->debug(__FILE__, __LINE__, "%s", "test sync-logger");synclogger->info(__FILE__, __LINE__, "%s", "test sync-logger");synclogger->warnning(__FILE__, __LINE__, "%s", "test sync-logger");synclogger->error(__FILE__, __LINE__, "%s", "test sync-logger");synclogger->fatal(__FILE__, __LINE__, "%s", "test sync-logger");return 0;
}

测试结果:

如下图所示,我们设置的限制输出等级为warnning,所以低于warnning的等级都不会输出。并且,我们在日志的实际落地时,给了三种落地方向,所以每一条输出的日志都会落地到三个不同的方向上。

但是,日志器的创建太过于复杂,需要创建很多个参数,最后才能创建出一个日志器。

所以,我们采取建造者模式,对日志器的创建进行封装。

6.2建造者模式创建日志器 

首先,因为有同步日志器和异步日志器之分,所以,我们先定义出日志器类型,方便创建不同的日志器。

enum class loggerType{SYNC_LOGGER,ASYNC_LOGGER};

 而对于建造者类来说,需要实现的就是创建一个一个的参数。

class loggerBuilder{public:loggerBuilder():_limits_rank(logLevel::value::DEBUG),_logger_type(loggerType::SYNC_LOGGER){}public:void buildLimitRank(logLevel::value level) { _limits_rank = level; }void buildLoggerName(const std::string &loggername) { _logger_name = loggername; }void buildLoggerType(const loggerType &loggertype) { _logger_type = loggertype; }void buildFormatter(const std::string &pattern = "[%d{%Y%m%d-%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") { _formatter = std::make_shared<formatter>(pattern); }template <typename sinkType, typename ...Args>void buildSinks(Args &&...args) {sink::s_ptr sink = std::make_shared<sinkType>(std::forward<Args>(args)...);_sinks.emplace_back(sink);}virtual logger::s_ptr build() = 0;protected:loggerType _logger_type;logLevel::value _limits_rank;formatter::s_ptr _formatter;     // 日志器格式std::vector<sink::s_ptr> _sinks; // 日志器落地对象,因为一个日志可能又多个落地方向,所以这里使用数组,包含多个落地对象std::string _logger_name;        // 日志器名称——日志器的唯一标识符};

而具体的建造者不是分为同步建造者和异步建造者。这两个可以通过同一个建造者借助不同的日志器类型创建。

我们这里的思路是分为局部日志器建造者和全局日志器建造者。局部建造者即创建的对象只在局部有用。而全局则在任何地方都可以使用。这里先不是先全局建造者。

并且在创建日志器时,如果使用者没有设置日志输出格式、日志等级、日志落地方向。我们在这里提供默认的日志器。

    // 局部日志器建造者class localLoggerBuilder : public loggerBuilder{public:logger::s_ptr build() override{// 日志器必须得有名称assert(!_logger_name.empty());if(_formatter.get() == nullptr)  _formatter = std::make_shared<formatter>();if(_sinks.empty())  buildSinks<stdoutSink>();if(_logger_type == loggerType::ASYNC_LOGGER) return std::make_shared<asyncLogger>(_logger_name, _limits_rank, _formatter, _sinks, _async_type);return std::make_shared<syncLogger>(_logger_name, _limits_rank, _formatter, _sinks);}};

测试建造者创建同步日志器:

int main()
{// 1.创建一个局部日志器建造者logging::loggerBuilder::s_ptr llb = std::make_shared<logging::localLoggerBuilder>();// 2.建造部件llb->buildLoggerName("sync_logger");llb->buildLoggerType(logging::loggerType::SYNC_LOGGER);llb->buildLimitRank(logging::logLevel::value::ERROR);llb->buildFormatter();llb->buildSinks<logging::stdoutSink>();llb->buildSinks<logging::fileSink>("./testbuilder/file.log");llb->buildSinks<logging::rollSinkBySize>("./testbuilder/roll-", 1024 * 1024);// 3.进行组装-创建日志器logging::logger::s_ptr synclogger = llb->build();// 4.使用日志器进行日志输出synclogger->debug(__FILE__, __LINE__, "%s", "test sync-logger");synclogger->info(__FILE__, __LINE__, "%s", "test sync-logger");synclogger->warnning(__FILE__, __LINE__, "%s", "test sync-logger");synclogger->error(__FILE__, __LINE__, "%s", "test sync-logger");synclogger->fatal(__FILE__, __LINE__, "%s", "test sync-logger");return 0;
}

如果只是想创建一个简单的用于测试的日志器:

int main()
{// 如果只需要创建一个用于调式代码的日志器logging::loggerBuilder::s_ptr llb = std::make_shared<logging::localLoggerBuilder>();llb->buildLoggerName("debug_logger");llb->buildLimitRank(logging::logLevel::value::DEBUG);logging::logger::s_ptr debug_log = llb->build();debug_log->debug(__FILE__, __LINE__, "%s", "test sync-logger");debug_log->info(__FILE__, __LINE__, "%s", "test sync-logger");debug_log->warnning(__FILE__, __LINE__, "%s", "test sync-logger");debug_log->error(__FILE__, __LINE__, "%s", "test sync-logger");debug_log->fatal(__FILE__, __LINE__, "%s", "test sync-logger");return 0;
}

 7.双缓冲区异步任务处理器类

对于异步写日志来说,只需要让业务线程将一条日志消息放入到一个缓冲区中即可,然后就去执行自己的逻辑。剩下的交给异步线程来做,异步线程从缓冲区中读取日志,然后输出到指定的方向上即可。

而在这个过程中,其实就是一个生产者消费者模型。我们可以直接使用一个数组来充当缓冲区。

但是以数组充当缓冲区时,会导致空间浪费。写端一致向后写,读端从前向后读,被读的地方其实已经可以被覆盖了。

当然,我们可以使用环形链表来作为缓冲区。这样就解决了空间浪费的问题了,但是实现起来复杂度较高,我们需要判断指针的位置来确定当前缓冲区是为空还是为满。

并且,用以上两种方式实现时,会设计到锁的频繁冲突。因为对容器操作不是线程安全的。所以在添加消息和读消息,都需要加锁。使用数组或者环形链表都会导致生产者消费者之间频繁的锁冲突,导致效率降低。

实际上,我们可以采取双缓冲区思想来解决以上问题:

生产者将产生的日志都输出到输入缓冲区中,当输出缓冲区为空,并且输入缓冲区有数据时,此时就交换这两个缓冲区。

使用双缓冲不仅解决了空间浪费的问题,还有效降低了生产者和消费者之间的锁冲突概率。 

有了以上分析,我们就先来实现缓冲区:

7.1缓冲区实现

/*异步缓冲区异步写日志和同步写日志的区别就在于,异步写日志不需要业务线程亲自将日志落地到某个具体的方向而异步写日志只需要将日志消息放入到缓冲区中,剩下的事情就交给异步线程去完成--->读缓冲区,将日志消息落地而这个异步缓冲区内部放的内容不应该是一个日志消息类对象,如果这样的话,会有大量的构造和析构的消耗所以,缓冲区内部放入的内容就只需要是一个一个的格式化字符串即可,构造日志消息,等一系列工作都交给异步线程去做实现思路:借助双缓冲区,分为输入缓冲区和输出缓冲区,业务线程将字符串push到输入缓冲区中,而异步线程从输出缓冲区中读取字符串当输出缓冲区为空,并且输入缓冲区有数据,此时就交换两个缓冲区。之所以使用这中实现思路,就是为了避免使用队列而导致空间的频繁申请与释放虽然使用循环队列可以提高空间的使用率,但是业务线程和异步线程会同时访问一个队列,会造成线程安全的问题所以必须得加锁,这就会导致锁冲突的问题。使用双缓冲区,不仅有效解决了空间利用率的问题,还降低了锁冲突的概率,准确来说,是降低了生产者和消费者之前的锁冲突概率
*/#pragma once
#include <iostream>
#include <vector>
#include <cassert>namespace logging
{const static size_t DEFAULT_BUFFER_SIZE = 10 * 1024 * 1024; // 缓冲区默认大小const static size_t THRESHOLD_SIZE = 100 * 1024 * 1024;     // 缓冲区大小小于阈值,翻倍增长 const static size_t LINEAR_SIZE = 1024 * 1024;              // 大于阈值,线性增长 class buffer{public:buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader(0), _writer(0) {}// 写入操作void push(const char *data, size_t len){// 1.判断缓冲区大小是否足够 --- 满了/或者剩余空间不足len个长度//       解决方法:扩容(用于测试极限性能); 阻塞(实际落地)//if(writeAbleSize() < len) return; // 阻塞ensureEnoughSize(len); // 扩容// 2.向指定位置写入指定长度std::copy(data, data + len, &_buffer[_writer]);// 3.移动写位置moveWriter(len);}// 剩余可写大小size_t writeAbleSize() { return _buffer.size() - _writer; }// 读取操作 --- 不直接读取整个数据,会导致数据拷贝// 我们直接读取数据的起始位置和长度即可const char* begin() { return &_buffer[_reader]; }size_t readAbleSize() { return _writer - _reader; }// 移动读指针void moveReader(size_t len) {assert(len <= readAbleSize());_reader += len;}// 交换缓冲区void swap(buffer &b){_buffer.swap(b._buffer);std::swap(_reader, b._reader);std::swap(_writer, b._writer);}// 重置缓冲区---读写位置void reset() { _writer = _reader = 0; }// 判断缓冲区是否为空bool empty() { return (_reader == _writer); }private:// 可以在写入操作之后,我们自己在内部移动写位置void moveWriter(size_t len){assert(_writer + len <= _buffer.size());_writer += len;}// 扩容void ensureEnoughSize(size_t len) {if(len < writeAbleSize()) return; // 不需要扩容size_t new_size = 0;if(_buffer.size() < THRESHOLD_SIZE) new_size = 2 * _buffer.size();else new_size = _buffer.size() + LINEAR_SIZE;_buffer.resize(new_size);}private:std::vector<char> _buffer;size_t _reader;size_t _writer;};
}

7.2异步任务处理器实现

异步任务处理器实际上就是一个生产者和消费者模型,即一个任务池。业务线程和异步线程通过这个任务处理器来进行日志的输出。

我们在异步任务处理器这里引入了异步日志器类型,安全和非安全模式。如果是安全模式,当输入缓冲区满的时候,生产者就会进行阻塞等待。如果在非安全模式下,则不会进行阻塞,而是当空间满了,直接去调用底层的push,此时底层的push会进行扩容。

非安全模式用来测试极限性能。

/*异步工作器 - 创建异步工作线程来完成日志的具体落地采用双缓冲区的思想,来控制业务线程和异步线程的生产与消费关系
*/#pragma once
#include "buffer.hpp"
#include <mutex>
#include <condition_variable>
#include <thread>
#include <functional>
#include <atomic>namespace logging
{using func_t = std::function<void(buffer &buf)>;enum class asyncType{ASYNC_SAFE,ASYNC_UNSAFE};class asyncLooper{public:using s_ptr = std::shared_ptr<asyncLooper>;public:asyncLooper(func_t callback, asyncType looper_type = asyncType::ASYNC_SAFE): _isRunning(false),_thread(std::thread(&asyncLooper::threadRoutine, this)),_callBack(callback),_async_type(looper_type){}~asyncLooper() { stop(); }void stop(){// 当异步工作器要关闭时,要将所有的线程唤醒_isRunning = false;_consume_cond.notify_all();_thread.join();}void push(const char *data, size_t len){// isRunning是原子类型,所以不用担心线程安全问题if(!_isRunning) return;// 1.申请锁std::unique_lock<std::mutex> lock(_mutex);// 2.判断输入缓冲区还有没有足够的空间,如果不够,则需要让业务线程阻塞if(_async_type == asyncType::ASYNC_SAFE)_product_cond.wait(lock, [&](){ return _product_buf.writeAbleSize() >= len;/*输入缓冲区可写大小,大小len*/});// 3.将数据写入到输入缓冲区中_product_buf.push(data, len);// 到这里,就将锁释放掉,处理数据不需要占有锁// 此时已经有了数据,就可以唤醒消费者了_consume_cond.notify_one();}private:void threadRoutine(){_isRunning = true;while(1) {// 给锁一个声明周期,在交换完缓冲区之后,就释放掉锁,处理数据不需要加锁{// 1.加锁,保证异步线程进行缓冲区交换时是线程安全的std::unique_lock<std::mutex> lock(_mutex);if(!_isRunning && _product_buf.empty()) break;// 2.如果输入缓冲区有数据,或者异步线程要退出了,才能继续运行,否则就得阻塞等待//   输出缓冲区默认为空,所以,只要输入缓冲区有消息就交换//   下面的处理过程,会将输出缓冲区处理完,重新置空_consume_cond.wait(lock, [&](){ return !_isRunning || !_product_buf.empty(); });_consume_buf.swap(_product_buf);// 3.唤醒生产者线程if(_async_type == asyncType::ASYNC_SAFE)_product_cond.notify_all();}// 4.处理输出缓冲区_callBack(_consume_buf);// 5.将输出缓冲区置空_consume_buf.reset();}}func_t _callBack; // 回调函数private:// 双缓冲区buffer _product_buf; // 生产者缓冲区,用于业务线程push格式字符串buffer _consume_buf; // 消费者缓冲区,用于异步线程读取字符串,进行落地std::mutex _mutex;   // 互斥锁// 条件变量std::condition_variable _product_cond; // 生产者条件变量,当条件不满足时,让生产者线程阻塞std::condition_variable _consume_cond; // 消费者条件变量,当条件不满足时,让异步线程阻塞std::atomic<bool> _isRunning;std::thread _thread;asyncType _async_type;};
}

8.异步日志器

日志器是用来整合接口,简化日志的输出操作。而对于异步日志器来说,它只需要将日志消息,放到底层的缓冲区中即可。然后传入一个回调函数,剩下的就由异步任务处理器自行完成对日志的处理。

// 异步日志器class asyncLogger : public logger{public:asyncLogger(const std::string &logger_name, const logLevel::value level, formatter::s_ptr &formatter, std::vector<sink::s_ptr> &sink, asyncType looper_type):logger(logger_name, level, formatter, sink){_looper = std::make_shared<asyncLooper>(std::bind(&asyncLogger::realSink, this, std::placeholders::_1), looper_type);}// 对于异步日志器来说,只需要将消息写入到缓冲区中即可void logSink(const char *data, const size_t len) { _looper->push(data, len); }// 异步线程如何去处理日志消息void realSink(buffer &buf){if(_sinks.empty()) return;for(auto sink : _sinks) {sink->logSink(buf.begin(), buf.readAbleSize());}}private:asyncLooper::s_ptr _looper;};

9.单例日志器管理类

在同一时间,可能存在多个日志器在内存中。有的日志器可能刚刚创建,有的日志器正在使用,而有的日志器可能准备释放。所以我们要将所有的日志器管理起来。而对于这个管理日志器的类来说,只能由一个实例,也就是说该管理类的设计需要采取单例模式。

并且,当我们创建了⼀个⽇志器之后,就会受到⽇ 志器所在作⽤域的访问属性限制。有了管理单例的存在,就可以突破作用域限制,让我们在任何地方都可以访问指定的日志器。

实现单例的方式有很多种,有懒汉和饿汉方式。

这里我们使用懒汉模式中,静态局部变量来创建单例。在C++11之后,创建静态局部变量是线程安全的。多线程访问该接口,拿到的都是同一个静态变量。

除此之外,我们还可以使用double-check来实现懒汉模式创建单例。

我们在创建单例日志器管理者时,可以同时创建一个root同步日志器。该root日志器用于debug, 是一个最简单的日志器,用于将消息输出到标准输出。

    // 单例日志器管理器class loggerManager{public:// 获取单例接口static loggerManager& getInstance(){// c++11之后,创建静态局部变量是线程安全的static loggerManager eton;return eton;}public:void addLogger(logger::s_ptr &logger){if(hasLogger(logger->name())) return;std::unique_lock<std::mutex> lock(_mutex);_loggers.emplace(logger->name(), logger);}bool hasLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _loggers.find(name);if(it == _loggers.end()) return false;return true;}logger::s_ptr getLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _loggers.find(name);if(it == _loggers.end()) return logger::s_ptr();return it->second;}logger::s_ptr rootLogger() { return _root_logger; }private:loggerManager(){std::shared_ptr<loggerBuilder> builder = std::make_shared<localLoggerBuilder>();builder->buildLoggerName("root");_root_logger = builder->build();_loggers.emplace("root", _root_logger);}loggerManager(const loggerManager &manager) = delete;loggerManager operator=(const loggerManager &manager) = delete;private:logger::s_ptr _root_logger;std::unordered_map<std::string, logger::s_ptr> _loggers;std::mutex _mutex;};

9.1全局日志器建造者

全局日志器建造者,建造出来的日志器是可以在任意地方访问的。建造出来的全局日志器会被自动添加到管理者中。

    // 全局日志器建造者// 简便用户操作,全局日志器建造者建造出的日志器,会自动添加到日志器管理者内部class globalLoggerBuilder : public loggerBuilder{public:logger::s_ptr build() override{// 日志器必须得有名称assert(!_logger_name.empty());if(_formatter.get() == nullptr)  _formatter = std::make_shared<formatter>();if(_sinks.empty())  buildSinks<stdoutSink>();logger::s_ptr ret;if(_logger_type == loggerType::ASYNC_LOGGER) ret = std::make_shared<asyncLogger>(_logger_name, _limits_rank, _formatter, _sinks, _async_type);elseret = std::make_shared<syncLogger>(_logger_name, _limits_rank, _formatter, _sinks);loggerManager::getInstance().addLogger(ret);return ret;}};

10.日志宏 && 全局接口设计

/* 定义全局接口,简化使用难度
*/
#pragma once
#include "logger.hpp"namespace logging
{logger::s_ptr getLogger(std::string name) { return loggerManager::getInstance().getLogger(name); }logger::s_ptr rootLogger() { return loggerManager::getInstance().rootLogger(); }#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define warnning(fmt, ...) warnning(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)// 下面的宏,用来简化,指定日志器的日志打印#define UNIQUE_DEBUG(logger_name, fmt, ...) logging::getLogger(logger_name)->debug(fmt, ##__VA_ARGS__)#define UNIQUE_INFO(logger_name, fmt, ...) logging::getLogger(logger_name)->debug(fmt, ##__VA_ARGS__)#define UNIQUE_WARNNING(logger_name, fmt, ...) logging::getLogger(logger_name)->debug(fmt, ##__VA_ARGS__)#define UNIQUE_ERROR(logger_name, fmt, ...) logging::getLogger(logger_name)->debug(fmt, ##__VA_ARGS__)#define UNIQUE_FATAL(logger_name, fmt, ...) logging::getLogger(logger_name)->debug(fmt, ##__VA_ARGS__)// 下面的宏,用来简化标准输出的日志打印#define DEBUG(fmt, ...) logging::rootLogger()->debug(fmt, ##__VA_ARGS__)#define INFO(fmt, ...) logging::rootLogger()->debug(fmt, ##__VA_ARGS__)#define WARNNING(fmt, ...) logging::rootLogger()->debug(fmt, ##__VA_ARGS__)#define ERROR(fmt, ...) logging::rootLogger()->debug(fmt, ##__VA_ARGS__)#define FATAL(fmt, ...) logging::rootLogger()->debug(fmt, ##__VA_ARGS__)
}

测试代码:

void test_Log()
{logging::logger::s_ptr p = logging::getLogger("sync-looper");int a = 0;p->debug("%s-%d", "test", a++);p->info("%s-%d", "test", a++);p->warnning("%s-%d", "test", a++);p->error("%s-%d", "test", a++);p->fatal("%s-%d", "test", a++);int count = 0;UNIQUE_DEBUG("sync-looper", "测试同步日志器-%d", count++);UNIQUE_INFO("sync-looper", "测试全局接口-%d", count++);UNIQUE_WARNNING("sync-looper", "测试全局接口-%d", count++);UNIQUE_ERROR("sync-looper", "测试全局接口-%d", count++);UNIQUE_FATAL("sync-looper", "测试全局接口-%d", count++);DEBUG("%s,%d", "test", count++);INFO("%s,%d", "test", count++);WARNNING("%s,%d", "test", count++);ERROR("s,%d", "test", count++);FATAL("%s,%d", "test", count++);
}int main()
{logging::globalLoggerBuilder llb;llb.buildEnableUnsafe();llb.buildFormatter();llb.buildLimitRank(logging::logLevel::value::DEBUG);llb.buildLoggerName("sync-looper");llb.buildLoggerType(logging::loggerType::SYNC_LOGGER);llb.buildSinks<logging::stdoutSink>();llb.buildSinks<logging::fileSink>("./file-log/file.log");llb.buildSinks<logging::rollSinkBySize>("./file-log/async-looper-", 1024);logging::logger::s_ptr async_logger = llb.build();test_Log();return 0;
}

 

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词