C++ 日志系统实战第五步:日志器的设计
全是通俗易懂的讲解,如果你本节之前的知识都掌握清楚,那就速速来看我的项目笔记吧~
本文项目代码编写收尾!
日志器类 (Logger) 设计(建造者模式)
日志器主要用于和前端交互。当我们需要使用日志系统打印 log 时,只需创建 Logger 对象,调用该对象的 debug、info、warn、error、fatal 等方法,即可输出想打印的日志。它支持解析可变参数列表和输出格式,能做到像使用 printf 函数一样打印日志。
当前日志系统支持同步日志与异步日志两种模式。两种日志器的差异仅在于日志的落地方式:
- 同步日志器:直接输出日志消息。
- 异步日志器:将日志消息放入缓冲区,由异步线程进行输出。
因此,设计日志器类时,先设计一个 Logger 基类,在此基础上,继承出 SyncLogger 同步日志器和 AsyncLogger 异步日志器。
由于日志器模块整合了多个模块,创建一个日志器时,需要设置日志器名称、日志输出等级、日志器类型、日志输出格式以及落地方向(落地方向可能有多个) ,整个日志器的创建过程较为复杂。为保持良好的代码风格,编写出优雅的代码,日志器的创建采用建造者模式。
logger.hpp
#pragma once /* 日志器实现 1.抽象基类 2.派生出日志器具体实现类 */ #include "level.hpp" #include "format.hpp" #include "sink.hpp" #include "message.hpp" #include "looper.hpp" #include #include #include namespace mylog { class Logger { public: Logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector sinks) : logger_name_(logger_name), limit_level_(limit_level), sinks_(sinks.begin(), sinks.end()), format_builder_(format_builder) { } using ptr = std::shared_ptr; // 完成构造日志消息构造过程并格式化,然后调用log函数输出日志 void debug(std::string file_, size_t line_, const std::string &fat, ...) { // 1.判断是否达到日志等级 if (limit_level_ > Level::Debug) { return; } // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息 va_list args; va_start(args, fat); char *res; int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中 if (len Level::Info) { return; } // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息 va_list args; va_start(args, fat); char *res; int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中 if (len Level::Warning) { return; } // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息 va_list args; va_start(args, fat); char *res; int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中 if (len Level::Error) { return; } // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息 va_list args; va_start(args, fat); char *res; int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中 if (len Level::Fatal) { return; } // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息 va_list args; va_start(args, fat); char *res; int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中 if (len format(ss, message); // 5.输出日志消息 log(ss.str().c_str(), ss.str().size()); } // 抽象接口完成实际的落地输出——不同日志器实现类完成具体的落地输出 virtual void log(const char *msg, size_t line) = 0; protected: std::mutex mutex_; // 日志器互斥锁 std::string logger_name_; // 日志器名称 std::atomic limit_level_; // 日志等级限制 std::vector sinks_; // 日志输出器 FormatBuilder::ptr format_builder_; // 日志格式构造器 }; // 日志器具体实现类——同步日志器 class sync_logger : public Logger { // 同步日志器,将日志直接通过落地模块句柄进行日志落地 public: sync_logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector sinks) : Logger(logger_name, limit_level, format_builder, sinks) { } void log(const char *data, size_t len) override { // 加锁 std::unique_lock lock(mutex_); if (sinks_.empty()) { return; } for (auto &sink : sinks_) { sink->log(data, len); // 调用sink的log函数输出日志 } } }; class Async_logger : public Logger { public: Async_logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector sinks, AsyncType async_type) : Logger(logger_name, limit_level, format_builder, sinks) { looper_ = std::make_shared(std::bind(&Async_logger::reallog, this, std::placeholders::_1)); } void log(const char *data, size_t len) override // 将数据写入缓冲区 { looper_->push(data, len); } void reallog(Buffer &buf) { if (sinks_.empty()) { return; } for (auto &sink : sinks_) { sink->log(buf.begin(), buf.readable_size()); // 调用sink的log函数输出日志 } } private: Async_looper::ptr looper_; }; /*使用建造者模式来建造日志器,而不是让用户直接构造日志器,这样可以避免用户构造错误的日志器,简化用户的使用复杂度*/ enum class LoggerType { Loggertype_Sync, // 同步日志器 Loggertype_Async, // 异步日志器 }; // 1.抽象一个建筑者类 // 1.设置日志器类型 // 2.将不同类型日志器的创建放到同一个日志器建造者中完成 class Logger_builder { public: Logger_builder() : type_(LoggerType::Loggertype_Sync), limit_level_(Level::Debug), async_type_(AsyncType::ASYNC_SAFE) { } void buildLoggerType(LoggerType type) { type_ = type; } void buildLoggerName(std::string logger_name) { logger_name_ = logger_name; } void buildLimitLevel(Level limit_level) { limit_level_ = limit_level; } void buildFormatBuilder(const std::string &pattern) { format_builder_ = std::make_shared(pattern); } void buildAsyncType(AsyncType async_type) { async_type_ = async_type; } template void buildSinks(Args &&...args) { LogSink::ptr psink = std::make_shared(std::forward(args)...); sinks_.push_back(psink); } virtual Logger::ptr build() = 0; protected: AsyncType async_type_; LoggerType type_; std::string logger_name_; Level limit_level_; FormatBuilder::ptr format_builder_; std::vector sinks_; }; // 2.派生出具体建筑者类——局部日志器的建造者 & 全局的日志器建造者 class Local_logger_builder : public Logger_builder { public: Logger::ptr build() override { assert(logger_name_.empty() == false); if (format_builder_.get() == nullptr) { format_builder_ = std::make_shared(); } if (sinks_.empty()) { sinks_.push_back(std::make_shared()); } if (type_ == LoggerType::Loggertype_Async) { return std::make_shared(logger_name_,limit_level_, format_builder_, sinks_, async_type_); } return std::make_shared(logger_name_, limit_level_, format_builder_, sinks_); } }; }
buffer.hpp
#pragma once /*实现异步日志缓冲区*/ #include "util.hpp" #include #include namespace mylog { #define BUFFER_SIZE (1 * 1024 * 1024) #define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024) // 内存阈值,没有超过阈值,内存翻倍扩容,达到阈值后,内存线性增长 #define INCREASE_BUFFER_SIZE (1 * 1024 * 1024) // 增加缓冲区大小 class Buffer { private: std::vector buffer_; // 缓冲区 size_t reader_index; // 当前可读的位置 size_t writer_index; // 当前可写的位置 private: void move_writer(size_t len) // 移动写指针 { assert(len + writer_index push(data, len); } void reallog(Buffer &buf) { if(sinks_.empty()) { return; } for(auto &sink : sinks_) { sink->log(buf.begin(), buf.writeable_size()); // 调用sink的log函数输出日志 } } private: Async_looper::ptr looper_; };
单例日志器管理类设计(单例模式)
日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了一个日志器之后,就会受到日志器所在作用域的访问属性限制。
因此,为了突破访问区域的限制,我们创建一个日志器管理类,且这个类是一个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的日志器来进行日志输出了。
基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位置通过日志器名称能够获取到指定的日志器进行日志输出。
在logger.hpp里面
class LoggerManager { private: std::mutex mutex_; Logger::ptr root_logger_; // 默认日志器 std::unordered_map loggers_; // 日志器名称-日志器映射表 private: LoggerManager() { std::unique_ptr builder(new mylog::Local_logger_builder()); builder->buildLoggerName("root"); root_logger_ = builder->build(); addLogger(root_logger_); } public: static LoggerManager &getInstance() { // 在C++11之后,针对静态变量,编译器在编译的层面实现了线程安全 // 当静态局部变量在没有构造完成之前,其他线程进入就会阻塞 static LoggerManager eton; return eton; } void addLogger(Logger::ptr logger) { if (hasLogger(logger->get_logger_name())) return; std::unique_lock lock(mutex_); loggers_.insert(std::make_pair(logger->get_logger_name(), logger)); } bool hasLogger(const std::string &name) { std::unique_lock lock(mutex_); auto it = loggers_.find(name); if (it != loggers_.end()) { return true; } return false; } Logger::ptr getLogger(const std::string &name) { std::unique_lock lock(mutex_); auto it = loggers_.find(name); if (it != loggers_.end()) { return it->second; } return nullptr; } Logger::ptr rootLogger() { return root_logger_; } };
日志宏 & 全局接口设计(代理模式)
- 提供全局的日志器获取接口。
- 使用代理模式,通过全局函数或宏函数来代理 Logger 类的 log、debug、info、warn、error、fatal 等接口,以便于控制源码文件名称和行号的输出控制,简化用户操作。
- 当仅需标准输出日志的时候,可以通过主日志器来打印日志。且操作时只需要通过宏函数直接进行输出即可。
mylog.h
#pragma once #include"logger.hpp" namespace mylog { Logger::ptr get_logger(const std::string &name) { return mylog::LoggerManager::getInstance().getLogger(name); } Logger::ptr rootLogger() { return mylog::LoggerManager::getInstance().rootLogger(); } #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__) #define DEBUG(fmt, ...) mylog::rootLogger()->debug(fmt, ##__VA_ARGS__) #define INFO(fmt, ...) mylog::rootLogger()->info(fmt, ##__VA_ARGS__) #define WARN(fmt, ...) mylog::rootLogger()->warn(fmt, ##__VA_ARGS__) #define ERROR(fmt, ...) mylog::rootLogger()->error(fmt, ##__VA_ARGS__) #define FATAL(fmt, ...) mylog::rootLogger()->fatal(fmt, ##__VA_ARGS__) }
后续还有测试代码,期待共同实现~
如果你对日志系统感到兴趣,欢迎关注我👉【A charmer】