日志落地与日志器模块实现文章目录日志落地与日志器模块实现一、日志落地模块1.1 日志落地基类1.2 日志落地各项子类标准输出子类文件日志子类滚动文件日志子类1.3 工厂模式设计二、日志器模块2.1 日志器基类2.2 同步日志器子类三、总结这是一套C 日志系统学习笔记涵盖同步与异步双模式日志的设计与实现。核心特点模块化分层格式→落地→日志器→建造者接口、双缓冲区生产者-消费者模型、多种设计模式单例/工厂/代理/建造者应用、C11 多线程与智能指针实践代码可直接在 Linux 下编译测试。一、日志落地模块日志落地模块的核心功能是将格式化后的日志消息输出到指定位置。该模块支持将日志同时输出到多个不同位置包括标准输出、指定文件和滚动文件三种主要方式。标准输出适用于调试和测试阶段而正式项目运行中更常用的是将日志写入文件。写入指定文件的方式便于事后分析系统运行状况但会导致文件不断增大。滚动文件方案可以按时间或大小进行文件切换例如每天生成一个新文件或当文件达到 1GB 时切换便于日志管理和清理。模块设计上支持扩展更多落地方向如数据库或远程服务器。为实现良好的扩展性将采用抽象基类、派生不同方向子类和使用工厂模式的实现思路。抽象基类定义统一接口不同落地方向从基类派生具体实现通过工厂模式管理对象创建实现创建与表示的分离便于后续功能扩展和调整。在代码实现层面日志落地模块的开发分为三个主要步骤第一步是抽象出日志落地的基类定义统一的接口规范。第二步根据不同落地方向派生具体子类如标准输出、文件输出和滚动文件输出等实现。第三步采用工厂模式来管理这些类的创建过程实现创建逻辑与使用逻辑的分离。1.1 日志落地基类抽象出日志落地的基类定义统一的接口规范无论后续定义的子类的落地方向是哪种我们统一使用log函数作为将日志格式化数据落地的接口。#includeutil.hpp#includememory#includefstream#includecassert#includesstreamnamespacelogsys{classLogSink{public:usingPtrstd::shared_ptrLogSink;LogSink(){}virtual~LogSink(){};virtualvoidlog(constchar*data,size_t size)0;};}1.2 日志落地各项子类有了统一的父类定义标准接口我们可以派生各种各样的子类这里我们展示标准输出、文件输出和滚动文件输出等实现。标准输出子类标准输出日志的实现是最简单的直接将数据写入std::cout。但不能使用流操作符因为流操作无法指定数据大小而且通常以反斜杠零作为结束符但日志输出不一定都是字符串。因此使用重载函数write进行写入该函数接收data和size两个参数表示从data位置开始写入size长度的数据。这个实现非常简单直接。namespacelogsys{// 标准输出classStdoutSink:publicLogSink{public:voidlog(constchar*data,size_t size)override{// 这里不直接用 因为会识别数据类型进行输出而我们需要的是直接输出std::cout.write(data,size);}};}文件日志子类构造函数需要完成两个操作首先创建日志文件所在目录然后创建并打开日志文件。使用 util.hpp 中的 File 工具类调用creatDirectory创建目录并通过getFilePath函数获取文件所在路径。打开文件时使用ofstream的open方法以二进制和追加模式ios::binary | ios::app打开。使用assert断言确保文件成功打开否则程序退出。数据写入同样使用write方法并可以通过assert判断写入是否成功失败时程序退出。namespacelogsys{// 指定文件输出classFileSink:publicLogSink{public:FileSink(conststd::stringfileName):_fileName(fileName){// 创建文件路径util::File::creatDirectory(util::File::getFilePath(_fileName));// 打开文件_ofs.open(_fileName,std::ios::binary|std::ios::app);// 判断是否打开成功assert(_ofs.is_open());}~FileSink(){if(_ofs.is_open()){_ofs.close();}}voidlog(constchar*data,size_t size)override{_ofs.write(data,size);if(_ofs.good()false){std::cout日志输出失败\n;}}private:std::string _fileName;std::ofstream _ofs;};}滚动文件日志子类滚动文件的策略有多种方式这里我们采用当一个文件写入日志到达一定大小时就会创建一个新文件。因此每一次写入日志log时我们都需要先判断当前文件的大小如果文件太大就关闭当前文件创建一个新文件并打开。构造函数首先需要创建文件路径并打开文件但基础文件名需要转换为实际文件名我们创建一个内部成员函数buildNewFileName来实现这个功能。实现方法是获取系统时间使用util::Date::getTime将时间戳转换为包含年月日时分秒的时间结构localtime_r。然后使用stringstream将基础文件名和时间信息拼接成实际文件名。namespacelogsys{// 滚动文件输出classScrollSinkBySize:publicLogSink{public:ScrollSinkBySize(conststd::stringbasename,size_t maxsize):_maxsize(maxsize),_baseName(basename),_cursize(0),_filecount(0){// 创建文件路径std::string fileNamebuildNewFileName();util::File::creatDirectory(util::File::getFilePath(fileName));// 打开文件_ofs.open(fileName,std::ios::binary|std::ios::app);// 判断是否打开成功assert(_ofs.is_open());}~ScrollSinkBySize(){if(_ofs.is_open()){_ofs.close();}}voidlog(constchar*data,size_t size)override{// 如果当前文件大小大于或等于设置的最大文件大小创建一个新的文件进行输出if(_cursize_maxsize){// 关闭原文件_ofs.close();// 创建新文件std::string fileNamebuildNewFileName();// 打开文件_ofs.open(fileName,std::ios::binary|std::ios::app);// 判断是否打开成功assert(_ofs.is_open());// 重置当前文件大小_cursize0;}_ofs.write(data,size);if(_ofs.good()false){std::cout日志输出失败\n;}_cursizesize;}private:// 构造一个新文件名conststd::stringbuildNewFileName(){structtmt;time_t timeutil::Date::getTime();localtime_r(time,t);std::stringstream ss;ss_baseNamet.tm_year1900_t.tm_mon1_t.tm_mday_t.tm_hourt.tm_mint.tm_sec-_filecount.log;returnss.str();}private:std::string _baseName;std::string _filePath;std::ofstream _ofs;size_t _filecount;size_t _maxsize;size_t _cursize;};}1.3 工厂模式设计使用工厂模式封装日志落地对象的生产过程使用模板参数控制输出类型template typename SinkType直接return make_shared通过模板参数类型创建对象。但各落地类型构造参数不同StdoutSink无需参数FileSink需要一个参数ScrollSinkBySize需要两个参数。使用不定参数函数解决参数传递问题定义 typename 参数包类型 Args通过完美转发将参数传递给构造函数用...展开参数包。用户可以根据需要传递不同参数创建不同落地对象。namespacelogsys{classSinkFactory{public:templatetypenameSinkType,typename...ArgsstaticLogSink::Ptrcreat(Args...args){returnstd::make_sharedSinkType(std::forwardArgs(args)...);}};}二、日志器模块日志器模块是对前几个模块的整合通过创建一个日志器来简化日志输出过程。日志器模块需要管理格式化模块对象、日志落地模块对象、默认日志输出限制等级和互斥锁。格式化模块对象负责对日志消息进行格式化日志落地模块对象负责将格式化后的消息落地。默认日志输出限制等级用于控制哪些等级的日志可以输出只有大于等于限制等级的日志才能输出。互斥锁用于保证多线程环境下日志输出的线程安全性避免出现冲突。日志器模块支持同步日志和异步日志两种模式。两种日志器类的唯一区别在于落地方式不同注意落地方式与落地方向不是一个概念落地方向由日志落地模块管理。同步日志直接写入磁盘或标准输出。异步日志先将日志写入内存再由异步线程写入磁盘或标准输出。实现时先抽象出一个 Logger 基类然后派生出同步日志器类 SyncLogger 和异步日志器类 AsyncLogger。落地操作被抽象出来由不同的日志器类实现各自的落地操作。基类指针用于管理和操作不同的日志器子类对象模块之间的关联关系通过基类进行管理而不是通过具体类。这种设计思想便于扩展和维护。2.1 日志器基类日志器基类的成员包括日志器名称、日志输出限制等级、格式化模块对象、互斥锁和落地模块对象数组。这些成员共同完成日志的格式化、输出控制、线程安全和多位置输出等功能。std::mutex _mutex;// 锁std::string _name;// 日志器名称std::atomicLogLevel::VALUE_limitLvl;// 日志器限制输出等级std::vectorLogSink::Ptr_sinkPtrs;// 一个日志器可以有多个不同的输出目的地Formater::Ptr _formater;// 日志信息格式化构建器定义日志器名称为 string 类型。日志输出限制等级定义为原子类型以避免频繁加锁导致的性能问题。格式化模块对象使用智能指针管理以提高资源管理效率一个日志器的每一条日志输出格式应该为统一的Formater 对象只需要一个。由于一个日志器可以有多个不同的输出目的地所以日志落地模块可能不止一个我们使用 vector 进行管理当我们需要将日志落地的时候遍历 vector调用 LogSink 中的 log 方法即可。namespacelogsys{classLogger{public:usingPtrstd::shared_ptrLogger;Logger(conststd::stringname,LogLevel::VALUE limitLvl,Formater::Ptrformater,std::vectorLogSink::PtrsinkPtrs):_name(name),_limitLvl(limitLvl),_formater(formater),_sinkPtrs(sinkPtrs){}// 行号、文件名、格式化字符串构建规则、日志信息主体需要用户传递不定参voiddebug(constsize_t line,conststd::stringfileName,conststd::stringfmt,...){// 判断日志等级是否能输出if(LogLevel::VALUE::DEBUG_limitLvl)return;// 将不定参数转化为字符串va_list ap;va_start(ap,fmt);char*res;intretvasprintf(res,fmt.c_str(),ap);if(ret-1){std::coutvasprintf err!;return;}va_end(ap);// 将ap指针置空// 构建日志信息LogMessagemsg(line,_name,fileName,LogLevel::VALUE::DEBUG,res);// 将日志信息格式化std::stringstream ss;_formater-format(ss,msg);// 日志落地与日志器类型有关log(ss.str().c_str(),ss.str().size());// 释放内存free(res);}voidinfo(constsize_t line,conststd::stringfileName,conststd::stringfmt,...){if(LogLevel::VALUE::INFO_limitLvl)return;va_list ap;va_start(ap,fmt);char*res;intretvasprintf(res,fmt.c_str(),ap);if(ret-1){std::coutvasprintf err!;return;}va_end(ap);LogMessagemsg(line,_name,fileName,LogLevel::VALUE::INFO,res);std::stringstream ss;_formater-format(ss,msg);log(ss.str().c_str(),ss.str().size());free(res);}voidwarning(constsize_t line,conststd::stringfileName,conststd::stringfmt,...){if(LogLevel::VALUE::WARNING_limitLvl)return;va_list ap;va_start(ap,fmt);char*res;intretvasprintf(res,fmt.c_str(),ap);if(ret-1){std::coutvasprintf err!;return;}va_end(ap);LogMessagemsg(line,_name,fileName,LogLevel::VALUE::WARNING,res);std::stringstream ss;_formater-format(ss,msg);log(ss.str().c_str(),ss.str().size());free(res);}voiderror(constsize_t line,conststd::stringfileName,conststd::stringfmt,...){if(LogLevel::VALUE::ERROR_limitLvl)return;va_list ap;va_start(ap,fmt);char*res;intretvasprintf(res,fmt.c_str(),ap);if(ret-1){std::coutvasprintf err!;return;}va_end(ap);LogMessagemsg(line,_name,fileName,LogLevel::VALUE::ERROR,res);std::stringstream ss;_formater-format(ss,msg);log(ss.str().c_str(),ss.str().size());free(res);}voidfatal(constsize_t line,conststd::stringfileName,conststd::stringfmt,...){if(LogLevel::VALUE::FATAL_limitLvl)return;va_list ap;va_start(ap,fmt);char*res;intretvasprintf(res,fmt.c_str(),ap);if(ret-1){std::coutvasprintf err!;return;}va_end(ap);LogMessagemsg(line,_name,fileName,LogLevel::VALUE::FATAL,res);std::stringstream ss;_formater-format(ss,msg);log(ss.str().c_str(),ss.str().size());free(res);}// 获取日志器名称conststd::stringgetName()const{return_name;}protected:// 日志器的核心函数其他不同级别的日志函数调用这个核心函数virtualvoidlog(constchar*data,size_t size)0;protected:std::mutex _mutex;// 锁std::string _name;// 日志器名称std::atomicLogLevel::VALUE_limitLvl;// 日志器限制输出等级std::vectorLogSink::Ptr_sinkPtrs;// 一个日志器可以有多个不同的输出目的地Formater::Ptr _formater;// 日志信息格式化构建器};}上述代码中可以看到每个级别的日志函数都遵循相同的模式判断日志等级是否达到输出限制若低于限制等级则直接返回。使用va_list和vasprintf将不定参数格式化为字符串。构建LogMessage对象封装日志的各项要素。使用Formater对日志消息进行格式化得到格式化后的字符串。调用纯虚函数log完成实际的日志落地操作由子类实现具体策略。释放vasprintf分配的内存。2.2 同步日志器子类同步日志器通过加锁机制保证线程安全使用std::unique_lock管理互斥锁在锁的有效期内自动加锁和解锁。若日志目标容器不为空则遍历日志目标数组通过迭代器调用每个日志目标的落地函数传入日志数据和长度完成日志输出。classSyncLogger:publicLogger{public:SyncLogger(conststd::stringname,LogLevel::VALUE limitLvl,Formater::Ptrformater,std::vectorLogSink::PtrsinkPtrs):Logger(name,limitLvl,formater,sinkPtrs){}protected:voidlog(constchar*data,size_t size)override{std::unique_lockstd::mutexlock(_mutex);for(autos:_sinkPtrs){s-log(data,size);}}};同步日志器的工作原理非常简单当业务线程调用日志输出函数如debug、info等时经过日志等级判断、消息格式化和字符串构建后最终调用log函数。在log函数内部首先加锁保证线程安全然后遍历所有日志落地目标LogSink将格式化后的日志数据写入每个目标。这种设计虽然保证了线程安全但也带来了一个潜在问题如果某个落地目标写入速度较慢如网络文件系统或高延迟磁盘业务线程会被阻塞影响程序的响应性能。这正是异步日志器要解决的问题。三、总结本文实现了日志系统的日志落地模块和同步日志器模块。日志落地模块采用抽象基类加派生类的设计支持标准输出、文件输出和滚动文件输出三种方式并通过工厂模式简化了落地对象的创建。日志器模块整合了格式化模块和落地模块提供了五种日志级别的输出接口并通过原子类型和互斥锁保证了线程安全。同步日志器虽然实现简单、逻辑清晰但在高并发或慢速 I/O 场景下可能导致业务线程阻塞。在下一篇文章中我们将实现异步日志器通过双缓冲区设计解决性能问题并完成日志系统的完整集成。