日志系统第五弹:同步日志器模块
- 一、Logger类的设计
- 1.功能
- 2.如何打印日志
- 3.设计 - - - 成员变量
- 1.日志输出限制等级
- 2.资源整合
- 3.唯一标识
- 4.互斥锁
- 4.设计 - - - 成员函数
- 1.对外的日志打印接口
- 2.抽象的日志实际落地接口
- 3.其他接口
- 5.Logger类的框架
- 二、Logger类的实现
- 1.构造和get接口
- 2.debug等等接口的实现
- 3.construct的实现
- 4.完整代码
- 三、SyncLogger类的实现
- 1.实现
- 2.测试
- 四、LoggerBuilder类的实现
- 1.设计
- 2.代码
- 五、LocalLoggerBuilder的实现
- 六、日志器管理模块的实现
- 1.设计
- 2.实现
- 七、GlobalLoggerBuilder的实现
- 八、测试
- 1.局部日志器的使用
- 2.全局日志器的使用
一、Logger类的设计
1.功能
日志器模块主要负责对日志消息模块,格式化模块和日志落地模块进行整合
对外提供便捷、好用的接口来完成消息的输出
注意:
- 一个日志器支持多个落地方向
- 一个日志器只支持一种日志消息格式
- 日志器是日志打印的基本单位
- 日志器分为同步日志器和异步日志器,分别负责同步日志记录和异步日志记录
想要整合日志的打印,我们首先就需要搞清楚该如何打印日志
2.如何打印日志
首先我们需要构造日志消息:
LogMessage message(LogLevel::value::FATAL,__FILE__,__LINE__,"default_logger","Hello World");
有了日志消息之后,下面就需要构造Formatter并将日志消息转换为带有格式的日志消息
Formatter formatter; // 就用默认日志格式了
std::string log_message = formatter.format(message);
有了带有格式的日志消息之后,下面我只需要将这个log_message 进行日志落地即可
LogSink::ptr log_sink=LogSinkFactory::create<FileSink>("./logfile/test.log");
log_sink->log(log_message.c_str(),log_message.size());
总体代码:
void test_log()
{LogMessage message(LogLevel::value::FATAL, __FILE__, __LINE__, "default_logger", "Hello World");Formatter formatter; // 就用默认日志格式了std::string log_message = formatter.format(message);LogSink::ptr log_sink=LogSinkFactory::create<FileSink>("./logfile/test.log");log_sink->log(log_message.c_str(),log_message.size());
}
可见,如果不进行整合,那么用户打印日志时就必须这么一步一步来,用户体验太差
而且用户无法像使用printf一样来打印日志,只能打印字符串,灵活性太差
因此才需要我们的日志器模块
3.设计 - - - 成员变量
1.日志输出限制等级
我们的日志器模块需要一个日志输出限制等级,只有当日志等级大于等于这个限制等级的时候,才允许该日志进行打印
这是为了能够更加灵活的来打印日志,而无需在需要限制日志的打印输出时采用注释和解除注释的方式
2.资源整合
一个日志器支持多个落地方向,因此需要一个vector<LogSink::ptr>
一个日志器只支持一种日志消息格式,因此需要一个Formatter::ptr
3.唯一标识
因为日志器需要被管理,因此我们给日志器一个唯一标识,就是日志器名称
4.互斥锁
因为一个日志器可能会被多个线程同时访问,因此存在线程安全问题(主要是日志落地的部分存在线程安全问题)
所以需要互斥锁
4.设计 - - - 成员函数
1.对外的日志打印接口
我们要像printf一样来支持灵活的打印方式,因此 需要用户传入format和后面的不定参,由我们进行解析,构造日志
同时为了方便用户使用,我们为每个日志等级都提供这么一个对外的日志打印接口
void debug(const std::string &file, size_t line, const std::string &body_format, ...);
void info(const std::string &file, size_t line, const std::string &body_format, ...);
void warning(const std::string &file, size_t line, const std::string &body_format, ...);
void error(const std::string &file, size_t line, const std::string &body_format, ...);
void fatal(const std::string &file, size_t line, const std::string &body_format, ...);
2.抽象的日志实际落地接口
因为我们既要实现同步日志器,又要实现异步日志器,因此日志的实际打印就有两种不同的方式了
所以我们对外提供的接口一定是需要复用一个抽象的日志实际落地接口
// 抽象接口完成实际的落地输出, 不同的日志器会有不同的实际落地方式
virtual void log(const char *data, size_t len) = 0;
3.其他接口
- debug, info… 等等接口,他们打印日志唯一的区别就是日志等级不同,因此我们可以抽离出共同的部分来,而这些接口的核心任务就是:
- 解析用户的传参,构造成LogMessage
- 格式化LogMessage
- 复用log实际落地接口,进行日志落地
所以共同部分我们命名为:construct
因为C的可变参数列表不支持传参,因此我们只能在debug,info…接口当中进行解析
void construct(LogLevel::value level, const std::string &file, size_t line, char *body)
- 对外提供一个日志器名称的get接口
5.Logger类的框架
class Logger
{
public:Logger(const std::string &logger_name, LogLevel::value limit_level, const Formatter::ptr &formatter, const std::vector<LogSink::ptr> &sinks);virtual ~Logger() {}const std::string& name() {}void debug(const std::string &file, size_t line, const std::string &body_format, ...);void info(const std::string &file, size_t line, const std::string &body_format, ...);void warning(const std::string &file, size_t line, const std::string &body_format, ...);void error(const std::string &file, size_t line, const std::string &body_format, ...);void fatal(const std::string &file, size_t line, const std::string &body_format, ...);// 因为下面的成员需要能够被子类所访问到, 因此我们不搞成private, 而是搞成protected
protected:void construct(LogLevel::value level, const std::string &file, size_t line, char *body);virtual void log(const char *data, size_t len) = 0;std::string _logger_name;std::mutex _mutex;std::atomic<LogLevel::value> _limit_level;std::vector<LogSink::ptr> _sinks;Formatter::ptr _formatter;
};
这个limit_level为何要搞成原子类?
因为后面的LoggerBuilder类需要设置这个值,因此这个值即会被读,又会被写
但是写操作只需要很简单的一步即可,所以搞成原子类就可以线程安全,就无需再给他加锁了
二、Logger类的实现
1.构造和get接口
Logger(const std::string &logger_name, LogLevel::value limit_level, const Formatter::ptr &formatter, const std::vector<LogSink::ptr> &sinks): _logger_name(logger_name), _limit_level(limit_level), _formatter(formatter), _sinks(sinks){}const std::string &name() {return _logger_name;}
2.debug等等接口的实现
核心任务:根据format来解析...
我们可以使用vasprintf来帮助我们完成这一任务
void debug(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::DEBUG)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "debug: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::DEBUG, file, line, body);
}
同理,info,warning…都是这么写:
void info(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::INFO)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "info: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::INFO, file, line, body);
}void warning(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::WARNING)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "warning: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::WARNING, file, line, body);
}void error(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::ERROR)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "error: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::ERROR, file, line, body);
}void fatal(const std::string &file, size_t line, const std::string &body_format, ...)
{// 1. 比较日志等级if (_limit_level > LogLevel::value::FATAL)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "fatal: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::FATAL, file, line, body);
}
3.construct的实现
void construct(LogLevel::value level, const std::string &file, size_t line, char *body)
{// 1. 构造LogMessageLogMessage message(level, file, line, _logger_name, body);// 2. free掉body,否则会内存泄漏free(body);body = nullptr;// 3. 将LogMessage进行格式化std::string real_message = _formatter->format(message);// 4. 复用log进行实际的日志落地log(real_message.c_str(), real_message.size());
}
4.完整代码
class Logger
{
public:Logger(const std::string &logger_name, LogLevel::value limit_level, const Formatter::ptr &formatter, const std::vector<LogSink::ptr> &sinks): _logger_name(logger_name), _limit_level(limit_level), _formatter(formatter), _sinks(sinks) {}virtual ~Logger() {}const std::string &name() { return _logger_name; }void debug(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::DEBUG)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "debug: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::DEBUG, file, line, body);}void info(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::INFO)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "info: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::INFO, file, line, body);}void warning(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::WARNING)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "warning: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::WARNING, file, line, body);}void error(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::ERROR)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "error: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::ERROR, file, line, body);}void fatal(const std::string &file, size_t line, const std::string &body_format, ...){// 1. 比较日志等级if (_limit_level > LogLevel::value::FATAL)return;// 2. 解析参数va_list ap;va_start(ap, body_format);char *body = nullptr;if (-1 == vasprintf(&body, body_format.c_str(), ap)){std::cout << "fatal: vasprintf失败\n";return;}va_end(ap);// 3. 复用construct(LogLevel::value::FATAL, file, line, body);}// 因为下面的成员需要能够被子类所访问到, 因此我们不搞成private, 而是搞成protected
protected:void construct(LogLevel::value level, const std::string &file, size_t line, char *body){// 1. 构造LogMessageLogMessage message(level, file, line, _logger_name, body);// 2. free掉body,否则会内存泄漏free(body);body = nullptr;// 3. 将LogMessage进行格式化std::string real_message = _formatter->format(message);// 4. 复用log进行实际的日志落地log(real_message.c_str(), real_message.size());}virtual void log(const char *data, size_t len) = 0;std::string _logger_name;std::mutex _mutex;std::atomic<LogLevel::value> _limit_level;std::vector<LogSink::ptr> _sinks;Formatter::ptr _formatter;
};
三、SyncLogger类的实现
1.实现
对于同步日志器来说只需要实现其log纯虚函数即可
只需要遍历_sinks,复用其log接口即可
因此:
class SyncLogger : public Logger
{
public:SyncLogger(const std::string &logger_name, LogLevel::value limit_level, const Formatter::ptr &formatter, const std::vector<LogSink::ptr> &sinks): Logger(logger_name, limit_level, formatter, sinks) {}virtual void log(const char *data, size_t len){// 这里必须要加锁,因为存在多线程同时调用同一个日志器对象的log函数的情况std::unique_lock<std::mutex> ulock(_mutex);for (auto &sp : _sinks){sp->log(data, len);}}
};
2.测试
void test_log2()
{std::vector<LogSink::ptr> sinks ={LogSinkFactory::create<StdoutSink>(),LogSinkFactory::create<FileSink>("./logfile/test.log"),LogSinkFactory::create<RollSinkBySize>("./logfile/roll-", 1024 * 1024)};Logger::ptr logger = std::make_shared<SyncLogger>("default_logger", LogLevel::value::WARNING, std::make_shared<Formatter>(), sinks);logger->debug(__FILE__, __LINE__, "Hello %s:%d", "DEBUG", 1);logger->info(__FILE__, __LINE__, "Hello %s:%d", "INFO", 1);logger->warning(__FILE__, __LINE__, "Hello %s:%d", "WARNING", 1);logger->error(__FILE__, __LINE__, "Hello %s:%d", "ERROR", 1);logger->fatal(__FILE__, __LINE__, "Hello %s:%d", "FATAL", 1);
}
测试成功,可是日志器这样用,还是有些不好用,用户体验太差
能不能不给用户暴露这些细节呢?
主要是构造日志器较为麻烦,需要用户先自行构造日志器所需零件
因此我们可以使用建造者模式
而建造者模式需要:
- 抽象产品类
- 具体产品类
- 抽象Builder类
- 具体Builder类
- 指挥者Director类
我们日志器模块当中每个对象之间的构造没有任何先后的顺序要求,因此无需Director类
四、LoggerBuilder类的实现
1.设计
跟建造者模式一样,用户需要先通过我们提供的接口构造完零件之后,再构造整个日志器对象
因此,我们需要对外提供:
- logger_name的build接口
- limit_level的build接口
- formatter的build接口
- Logsink的build接口(添加对应落地方式)
- 整体的build接口(纯虚函数)
其中,因为同步日志器和异步日志器仅仅只是实际落地方式不同,因此我们没必要在搞一个同步日志器建造者类和异步日志器建造者类
给一个LoggerType类型进行区分即可
所以还需要给一个logger_type的build接口
如果用户没有构造某个零件,那么我们的默认值是:
logger_name必须要有
limit_level是DEBUG
formatter在build时检查,若为空,则给一个默认构造的Formatter
Logsink在build时检查,若为空,则给一个StdoutSink
logger_type给Sync
2.代码
enum class LoggerType
{SYNC,ASYNC
};class LoggerBuilder
{
public:LoggerBuilder() : _logger_type(LoggerType::SYNC), _limit_level(LogLevel::value::DEBUG) {}virtual ~LoggerBuilder() {}void buildLoggerName(const std::string &logger_name){_logger_name = logger_name;}void buildLoggerType(LoggerType logger_type){_logger_type = logger_type;}void buildLimitLevel(LogLevel::value limit_level){_limit_level = limit_level;}void buildFormatter(const std::string &pattern = ""){if (pattern.empty())_formatter = std::make_shared<Formatter>();else_formatter = std::make_shared<Formatter>(pattern);}template <class SinkType, class... Args>void buildLogSink(Args &&...args){_sinks.push_back(LogSinkFactory::create<SinkType>(std::forward<Args>(args)...));}virtual void build() = 0;protected:// 允许一个建造者在调用build接口构造完对象之后将建造者当中保存的原数据进行重置void reset(){_logger_name.clear();_logger_type = LoggerType::SYNC;_limit_level = LogLevel::value::DEBUG;_sinks.clear();_formatter.reset();}std::string _logger_name;LoggerType _logger_type;LogLevel::value _limit_level;std::vector<LogSink::ptr> _sinks;Formatter::ptr _formatter;
};
五、LocalLoggerBuilder的实现
为了提高日志器使用的灵活性,我们允许用户创建局部日志器和全局日志器这两种日志器
局部日志器就是只在创建该日志器的作用域当中有效
而全局日志器需要在日志器管理模块当中被管理,作用域是全局的
只需要实现LoggerBuilder类的build接口即可
class LocalLoggerBuilder : public LoggerBuilder
{
public:virtual Logger::ptr build(){if (_logger_name.empty()){std::cout << "日志器名称为空!!\n";abort();}if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){_sinks.push_back(LogSinkFactory::create<StdoutSink>());}Logger::ptr ret;if (_logger_type == LoggerType::SYNC){ret = std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}else{// 后面实现完异步日志器之后完善}// 重置this->reset();return ret;}
};
六、日志器管理模块的实现
1.设计
因为日志器管理模块的作用域和声明周期需要是全局的
因此我们搞成单例类
如何管理呢?
增删查改,用一个哈希表建立起<logger_name,Logger::ptr>的映射
这个哈希表会被多线程访问,因此需要加锁
我们在单例对象刚被创建的时候,默认先创建一个日志器(用于标准输出的打印)
目的: 让用户在不创建任何日志器的情况下,也能进行标准输出的打印, 方便用户使用
2.实现
const std::string default_logger_name = "_default_logger";class LoggerManager
{
public:static LoggerManager &getInstance(){static LoggerManager logger_manager;return logger_manager;}// 增删查void addLogger(const Logger::ptr &logger){std::unique_lock<std::mutex> ulock(_mutex);_logger_map.insert(std::make_pair(logger->name(), logger));}void delLogger(const std::string &logger_name){std::unique_lock<std::mutex> ulock(_mutex);_logger_map.erase(logger_name);}Logger::ptr getLogger(const std::string &logger_name){std::unique_lock<std::mutex> ulock(_mutex);auto iter = _logger_map.find(logger_name);if (iter == _logger_map.end())return Logger::ptr();elsereturn iter->second;}bool existLogger(const std::string &logger_name){std::unique_lock<std::mutex> ulock(_mutex);return _logger_map.count(logger_name) > 0;}Logger::ptr default_logger(){// 无需加锁,因为没人修改它return _default_logger;}private:// 构造和析构私有LoggerManager(){// 创建默认日志器// 这里必须是局部日志器, 如果是全局日志器的话, 调用其build创建对象之后, 其会将该日志器添加到单例对象当中// 而此时单例对象还没有创建好, 此时就发生相互依赖问题导致的死循环LoggerBuilder::ptr builder = std::make_shared<LocalLoggerBuilder>();builder->buildLoggerName(default_logger_name);_default_logger = builder->build();_logger_map.insert(std::make_pair(default_logger_name, _default_logger));}~LoggerManager() {}// 禁掉拷贝和赋值LoggerManager(const LoggerManager &) = delete;const LoggerManager &operator=(const LoggerManager &) = delete;std::mutex _mutex;Logger::ptr _default_logger;std::unordered_map<std::string, Logger::ptr> _logger_map;
};
七、GlobalLoggerBuilder的实现
全局日志器建造者跟局部日志器建造者唯一的区别就是:
全局日志器建造者需要将建造好的日志器放到日志器管理模块当中
class GlobalLoggerBuilder : public LoggerBuilder
{
public:virtual Logger::ptr build(){if (_logger_name.empty()){std::cout << "日志器名称为空!!\n";abort();}// 查找全局当中是否有该日志器Logger::ptr ret = LoggerManager::getInstance().getLogger(_logger_name);if (ret.get() != nullptr)return ret;if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){_sinks.push_back(LogSinkFactory::create<StdoutSink>());}if (_logger_type == LoggerType::SYNC){ret = std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}else{// 后面实现完异步日志器之后完善}// 重置this->reset();LoggerManager::getInstance().addLogger(ret);return ret;}
};
八、测试
1.局部日志器的使用
void test_log3()
{LoggerBuilder::ptr builder = std::make_shared<LocalLoggerBuilder>();builder->buildLoggerName("logger1");builder->buildLogSink<StdoutSink>();builder->buildLogSink<FileSink>("./new_logfile/test.log");builder->buildLogSink<RollSinkBySize>("./new_logfile/roll-", 1024 * 1024);// 其他的都用默认的Logger::ptr logger1 = builder->build();logger1->debug(__FILE__, __LINE__, "%s:%d", "DEBUG", 1);logger1->info(__FILE__, __LINE__, "%s:%d", "INFO", 1);logger1->warning(__FILE__, __LINE__, "%s:%d", "WARNING", 1);logger1->error(__FILE__, __LINE__, "%s:%d", "ERROR", 1);logger1->fatal(__FILE__, __LINE__, "%s:%d", "FATAL", 1);
}
2.全局日志器的使用
void test_log4()
{LoggerBuilder::ptr builder = std::make_shared<GlobalLoggerBuilder>();builder->buildLoggerName("logger1");builder->buildLogSink<StdoutSink>();builder->buildLogSink<FileSink>("./new_logfile/test2.log");builder->buildLogSink<RollSinkBySize>("./new_logfile/roll2-", 1024 * 1024);// 其他的都用默认的Logger::ptr logger1 = builder->build();logger1->debug(__FILE__, __LINE__, "%s:%d", "DEBUG", 1);logger1->info(__FILE__, __LINE__, "%s:%d", "INFO", 1);logger1->warning(__FILE__, __LINE__, "%s:%d", "WARNING", 1);logger1->error(__FILE__, __LINE__, "%s:%d", "ERROR", 1);logger1->fatal(__FILE__, __LINE__, "%s:%d", "FATAL", 1);
}
只需要把LocalLoggerBuilder改成GlobalLoggerBuilder即可
验证成功
以上就是日志系统第五弹:同步日志器模块的全部内容哦