日志系统项目(3)项目实现(日志落地类、日志器类)

2024-02-27 06:12

本文主要是介绍日志系统项目(3)项目实现(日志落地类、日志器类),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

日志落地类设计(简单工厂模式)

日志落地类主要负责落地日志消息到目的地。它主要包括以下内容:
这个类支持可扩展,其成员函数log设置为纯虚函数,当我们需要增加一个log输出目标,可以增加一个类继承自该类并重写log方法实现具体的落地日志逻辑。
目前实现了三个不同方向上的日志落地:

  • 标准输出: StdoutSink
  • 固定文件: FileSink
  • 滚动文件: RollSink
    • 滚动日志文件输出的必要性
      • 由于机器磁盘空间有限,我们不可能一直无限地向一个文件中增加数据
      • 如果一个日志文件体积太大,一方面是不好打开,另一方面是即时打开了由于包含数据巨大,也不利于查找我们需要的信息
      • 所以实际开发中会对单个日志文件的大小也会做一些控制,即当大小超过某个大小时〈如1GB),我们就重新创建一个新的日志文件来滚动写日志。对于那些过期的日志,大部分企业内部都有专门的运维人员去定时清理过期的日志,或者设置系统定时任务,定时清理过期日志。
    • 日志文件的滚动思想:
      日志文件滚动的条件有两个:文件大小和时间。我们可以选择:
      • 日志文件在大于1GB的时候会更换新的文件
      • 每天定点滚动一个日志文件

本项目基于文件大小的判断滚动生成新的文件

/* 日志落地模块的实现1. 抽象落地基类2. 派生子类(根据不同的落地方向进行派生)3. 使用工厂模式进行创建与表示分离
*/
namespace zyqlog
{class LogSink{public:using ptr = std::shared_ptr<LogSink>;LogSink() {}virtual ~LogSink() {} virtual void log(const char *data, size_t len) = 0;};// 落地方向:标准输出class StdoutSink : public LogSink{public:// 将日志消息写入到标准输出void log(const char *data, size_t len) override{// std::cout << // 无法指定大小std::cout.write(data, len);}};// 落地方向:指定文件class FileSink : public LogSink{public:// 构造时传入文件名将操作句柄管理起来FileSink(const std::string &pathname) : _pathname(pathname) {// 1. 创建日志文件所在的目录util::File::createDirectory(util::File::path(_pathname));// 2. 创建并打开日志文件_ofs.open(_pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());}// 将日志消息写入到指定文件void log(const char *data, size_t len) override{_ofs.write(data, len);assert(_ofs.good()); // 判断写入是否出错}private:std::string _pathname;std::ofstream _ofs;};// 落地方向:滚动文件(以大小进行滚动)class RollBySizeSink : public LogSink{public:// 构造时传入文件名将操作句柄管理起来RollBySizeSink(const std::string &basename, size_t max_size): _basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_count(0){std::string pathname = createNewFileName();// 1. 创建日志文件所在的目录util::File::createDirectory(util::File::path(pathname));// 2. 创建并打开日志文件    _ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());}// 将日志消息写入到指定文件 -- 写入前需要判断文件大小,超过大小就要切换文件void log(const char *data, size_t len) override{if (_cur_fsize >= _max_fsize){_ofs.close(); // 关闭原来已经打开的文件std::string pathname = createNewFileName();_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());_cur_fsize = 0;}_ofs.write(data, len);assert(_ofs.good());_cur_fsize += len;}private:std::string createNewFileName() // 进行大小判断,超过大小就创建新文件{// 获取系统时间,以时间来构造文件名扩展名time_t t = util::Date::now();struct tm lt; // 接受转换后的数据结构localtime_r(&t, &lt); // 时间转换std::stringstream filename;filename << _basename;filename << lt.tm_year + 1900;filename << lt.tm_mon + 1;filename << lt.tm_mday;filename << lt.tm_hour;filename << lt.tm_min;filename << lt.tm_sec;filename << "-";filename << _name_count++;filename << ".log";return filename.str();}private:// 通过基础文件名 + 扩展文件名组成一个实际的当前输出文件名std::string _basename; // ./logs/base- -> ./logs/base-20020809132332.logstd::ofstream _ofs;size_t _max_fsize; // 记录最大大小,当前文件超过这个大小就要切换文件size_t _cur_fsize; // 记录当前文件已经写入数据大小size_t _name_count;};class SinkFactory{public:// static LogSink::ptr create(int type)// {//     switch (type)//     {//     case 1://         return std::make_shared<StdoutSink>(); // 这样子编写是没有可扩展性的,只能通过修改源代码的方式进行处理//     }// }template<typename SinkType, typename ...Args> // 引入了模板参数之后还有问题就是不同落地类型的参数不同,有0个、一个、两个等等,因此还需要引入可变参数包static LogSink::ptr create(Args &&...args){return std::make_shared<SinkType>(std::forward<Args>(args)...);          }};
}

日志器类(Logger)设计(建造者模式)

日志器主要是用来和前端交互,当我们需要使用日志系统打印log的时候,只需要创建Logger对象调用该对象debug、info、warn、error、fatal等方法输出自己想打印的日志即可,支持解析可变参数列表和输出格式,即可以做到像使用printf函数—样打印日志。
当前日志系统支持同步日志&异步日志两种模式,两个不同的日志器唯一不同的地方在于他们在日志的落地方式上有所不同:
同步日志器:直接对日志消息进行输出。
异步日志器:将日志消息放入缓冲区,由异步线程进行输出。
因此日志器类在设计的时候先设计出一个Logger基类,在Logger基类的基础上,继承出SyncLogger同步日志器和AsyncLogger异步日志器。
且因为日志器模块是对前边多个模块的整合,想要创建一个日志器,需要设置日志器名称,设置日志输出等级,设置日志器类型,设置日志输出格式,设置落地方向,且落地方向有可能存在多个,整个日志器的创建过程较为复杂,为了保持良好的代码风格,编写出优雅的代码,因此日志器的创建这里采用了建造者模式来进行创建。

/*日志器模块:功能:对前面所有模块进行整合,向外提供接口完成不同等级日志的输出管理的成员:1. 格式化模块对象2. 落地模块对象3. 默认的日志器输出限制等级(大于等于限制等级的日志才能输出)4. 互斥锁(保证日志输出过程是线程安全的,不会出现交叉日志)5. 日志器名称(日志器的唯一标识,以便于查找)提供的操作:debug等级的日志输出操作(分别封装出日志消息LogMsg--各个接口日志等级不同)info等级的日志输出操作WARNING等级的日志输出操作error等级的日志输出操作fatal等级的日志输出操作实现:1. 抽象Logger基类(派生出同步日志器类 & 异步日志器类)2. 因为两种不同的日志器只有落地方式不同,因此将落地操作给抽象出来不同的日志器调用各自的落地操作进行日志落地模块关联中使用基类指针针对子类日志器对象进行日志管理和操作
*//*完成日志器模块:1. 抽象日志器基类2. 派生出不同的子类(同步日志器类 & 异步日志器类)
*/
namespace zyqlog
{class Logger{public:using ptr = std::shared_ptr<Logger>;Logger(const std::string &logger_name, LogLevel::value level, ForMatter::ptr &formatter, std::vector<LogSink::ptr> &sinks): _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sinks(sinks.begin(), sinks.end()){}const std::string &name(){return _logger_name;}// 完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出void debug(const std::string &file, size_t line, const std::string &fmt, ...){// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出// 1. 判断当前日志是否达到了输出等级if (LogLevel::value::DEBUG < _limit_level) {return ;}// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) {std::cout << "vsaprintf failed!!\n";return ;}va_end(ap);serialize(LogLevel::value::DEBUG, file, line, res);free(res);}void info(const std::string &file, size_t line, const std::string &fmt, ...){// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出// 1. 判断当前日志是否达到了输出等级if (LogLevel::value::INFO < _limit_level) {return ;}// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) {std::cout << "vsaprintf failed!!\n";return ;}va_end(ap);serialize(LogLevel::value::INFO, file, line, res);free(res);}void warning(const std::string &file, size_t line, const std::string &fmt, ...){// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出// 1. 判断当前日志是否达到了输出等级if (LogLevel::value::WARNING < _limit_level) {return ;}// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) {std::cout << "vsaprintf failed!!\n";return ;}va_end(ap);serialize(LogLevel::value::WARNING, file, line, res);free(res);}void error(const std::string &file, size_t line, const std::string &fmt, ...){// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出// 1. 判断当前日志是否达到了输出等级if (LogLevel::value::ERROR < _limit_level) {return ;}// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) {std::cout << "vsaprintf failed!!\n";return ;}va_end(ap);serialize(LogLevel::value::ERROR, file, line, res);free(res);}void fatal(const std::string &file, size_t line, const std::string &fmt, ...){// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出// 1. 判断当前日志是否达到了输出等级if (LogLevel::value::FATAL < _limit_level) {return ;}// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) {std::cout << "vsaprintf failed!!\n";return ;}va_end(ap);serialize(LogLevel::value::FATAL, file, line, res);free(res);}protected:void serialize(LogLevel::value level, const std::string &file, size_t line, char *str){// 3. 构造LogMsg对象LogMsg msg(level, line, file, _logger_name, str);// 4. 通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串std::stringstream ss;_formatter->format(ss, msg);// 5. 进行日志落地log(ss.str().c_str(), ss.str().size());}// 抽象接口完成实际的落地输出--不同的日志器会有不同的实际落地方式virtual void log(const char *data, size_t len) = 0;protected:std::mutex _mutex;std::string _logger_name;std::atomic<LogLevel::value> _limit_level;ForMatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};class SyncLogger : public Logger{public:SyncLogger(const std::string &logger_name, LogLevel::value level, ForMatter::ptr &formatter, std::vector<LogSink::ptr> &sinks): Logger(logger_name, level, formatter, sinks){}protected:void log(const char *data, size_t len) override{std::unique_lock<std::mutex> lock(_mutex);if (_sinks.empty()) return;for (auto &sink : _sinks){sink->log(data, len);}}};enum class LoggerType{LOGGER_SYNC,LOGGER_ASYNC,};
	/*同步日志器输出 test*/int main(){std::string logger_name = "sync_logger";zyqlog::LogLevel::value limit = zyqlog::LogLevel::value::WARNING;zyqlog::Formatter::ptr fmt(new zyqlog::Formatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n"));zyqlog::LogSink::ptr stdout_lsp = zyqlog::SinkFactory::create<zyqlog::StdoutSink>();zyqlog::LogSink::ptr file_lsp = zyqlog::SinkFactory::create<zyqlog::FileSink>("./logfile/test.log");zyqlog::LogSink::ptr roll_lsp = zyqlog::SinkFactory::create<zyqlog::RollBySizeSink>("./logfile/roll-", 1024*1024);std::vector<zyqlog::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};// std::vector<zyqlog::LogSink::ptr> sinks = {stdout_lsp};zyqlog::Logger::ptr logger(new zyqlog::SyncLogger(logger_name, limit, fmt, sinks));// // 测试logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warning(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t count = 0;size_t cursize = 0;while (cursize < 1024 * 1024 * 10){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}}

从上面的同步日志器的测试可以得到,我们需要先构建出一个一个的零部件然后在使用日志器进行落地,这样用户使用起来会变得非常的不方便,因此这里使用建造者模式来建造日志器,不要让用户直接去构造日志器。

    /*使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用复杂度*/// 1. 抽象一个日志器建造者类//  1. 设置日志器类型//  2. 将不同类型日志器的创建放到同一个日志器建造者类中完成    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 buildEnableUnSafeAsync(){_looper_type = AsyncType::ASYNC_UNSAFE;}void buildLoggerName(const std::string &name){_logger_name = name;}void buildLoggerLevel(LogLevel::value level){_limit_level = level;}// 设置输出规则void buildFormatter(const std::string &pattern){_formatter = std::make_shared<ForMatter>(pattern);}template<typename SinkType, typename ...Args> // 可能会存在多个方向的落地因此使用vector来进行记录void buildSink(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;LogLevel::value _limit_level;ForMatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};// 2. 派生出具体的建造者类---局部日志器的建造者 & 全局的日志器建造者 (后面添加全局单例管理器之后,将日志器添加全局管理)class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{assert(!_logger_name.empty()); // 必须有日志器名称if (_formatter.get() == nullptr) // 用户没有传入日志器的输出格式,需要给以一个默认格式{_formatter = std::make_shared<ForMatter>();}if (_sinks.empty()) // 用户没有指定落地方式,这里有我们自己进行指定{buildSink<StdoutSink>();}if (_logger_type == LoggerType::LOGGER_ASYNC){return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}else{return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}}};
// 日志器建造者类test
int main()
{std::unique_ptr<zyqlog::LoggerBuilder> builder(new zyqlog::LocalLoggerBuilder());builder->buildLoggerLevel(zyqlog::LogLevel::value::WARNING); // 这里对零件的顺序没有要求,因此不需要指挥者--指挥零件按照什么样的顺序进行构造builder->buildLoggerName("sync_logger");builder->buildLoggerType(zyqlog::LoggerType::LOGGER_SYNC);builder->buildFormatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n");builder->buildSink<zyqlog::FileSink>("./logfile/test.log");builder->buildSink<zyqlog::RollBySizeSink>("./logfile/roll-", 1024*1024);zyqlog::Logger::ptr logger = builder->build();// 测试logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warning(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t count = 0;size_t cursize = 0;while (cursize < 1024 * 1024 * 10){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}
}

这篇关于日志系统项目(3)项目实现(日志落地类、日志器类)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/751440

相关文章

C语言小项目实战之通讯录功能

《C语言小项目实战之通讯录功能》:本文主要介绍如何设计和实现一个简单的通讯录管理系统,包括联系人信息的存储、增加、删除、查找、修改和排序等功能,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录功能介绍:添加联系人模块显示联系人模块删除联系人模块查找联系人模块修改联系人模块排序联系人模块源代码如下

Java中使用Java Mail实现邮件服务功能示例

《Java中使用JavaMail实现邮件服务功能示例》:本文主要介绍Java中使用JavaMail实现邮件服务功能的相关资料,文章还提供了一个发送邮件的示例代码,包括创建参数类、邮件类和执行结... 目录前言一、历史背景二编程、pom依赖三、API说明(一)Session (会话)(二)Message编程客

Java中List转Map的几种具体实现方式和特点

《Java中List转Map的几种具体实现方式和特点》:本文主要介绍几种常用的List转Map的方式,包括使用for循环遍历、Java8StreamAPI、ApacheCommonsCollect... 目录前言1、使用for循环遍历:2、Java8 Stream API:3、Apache Commons

C#提取PDF表单数据的实现流程

《C#提取PDF表单数据的实现流程》PDF表单是一种常见的数据收集工具,广泛应用于调查问卷、业务合同等场景,凭借出色的跨平台兼容性和标准化特点,PDF表单在各行各业中得到了广泛应用,本文将探讨如何使用... 目录引言使用工具C# 提取多个PDF表单域的数据C# 提取特定PDF表单域的数据引言PDF表单是一

使用Python实现高效的端口扫描器

《使用Python实现高效的端口扫描器》在网络安全领域,端口扫描是一项基本而重要的技能,通过端口扫描,可以发现目标主机上开放的服务和端口,这对于安全评估、渗透测试等有着不可忽视的作用,本文将介绍如何使... 目录1. 端口扫描的基本原理2. 使用python实现端口扫描2.1 安装必要的库2.2 编写端口扫

PyCharm接入DeepSeek实现AI编程的操作流程

《PyCharm接入DeepSeek实现AI编程的操作流程》DeepSeek是一家专注于人工智能技术研发的公司,致力于开发高性能、低成本的AI模型,接下来,我们把DeepSeek接入到PyCharm中... 目录引言效果演示创建API key在PyCharm中下载Continue插件配置Continue引言

MySQL分表自动化创建的实现方案

《MySQL分表自动化创建的实现方案》在数据库应用场景中,随着数据量的不断增长,单表存储数据可能会面临性能瓶颈,例如查询、插入、更新等操作的效率会逐渐降低,分表是一种有效的优化策略,它将数据分散存储在... 目录一、项目目的二、实现过程(一)mysql 事件调度器结合存储过程方式1. 开启事件调度器2. 创

使用Python实现操作mongodb详解

《使用Python实现操作mongodb详解》这篇文章主要为大家详细介绍了使用Python实现操作mongodb的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、示例二、常用指令三、遇到的问题一、示例from pymongo import MongoClientf

SQL Server使用SELECT INTO实现表备份的代码示例

《SQLServer使用SELECTINTO实现表备份的代码示例》在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误,在SQLServer中,可以使用SELECTINT... 在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误。在 SQL Server 中,可以使用 SE

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端