准备:认识Log4J 日志
出处:《百度百科》
概念:Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。这些功能可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
· 类关系

1. LogLevel 类封装
目的:标识日志器、日志输出器的格式,只有满足一定的层级关系,日志输出器才能输出相应日志内容。能够区分一些日志信息的重要程度,在开发调试以及线上可以有选择的显示日志内容。UNKNOW < DEBUG < INFO < WARN < ERROR < FATAL
日志级别:=DEBUG,输出器级别:<= DEBUG 此时可以输出日志内容
日志级别:=DEBUG,输出器级别:= INFO
1.1 成员变量
class LogLevel{......public://日志级别enum Level{UNKNOW = 0, //未知信息DEBUG = 1, //调试信息INFO = 2, //一般信息WARN = 3, //警告信息ERROR = 4, //一般错误FATAL = 5 //致命错误};}
1.2 接口
1.2.1 ToString()
功能:日志级别从枚举类型转为字符串
/*** @brief 从枚举类型转为字符串* @param[in] level 日志级别枚举类型* @return const char**/static const char* ToString(LogLevel::Level level);const char* LogLevel::ToString(LogLevel::Level level){/*采用宏替换能减少重复代码*/switch(level){#define XX(name)\case LogLevel::name:\return #name;\break;XX(DEBUG);XX(INFO);XX(WARN);XX(FATAL);XX(ERROR);#undef XX //用完一个宏XX 希望下面的代码不再使用到它default:return "UNKNOW";}return "UNKNOW";}
1.2.2 FromString()
功能:日志级别从字符串转为日志枚举类型
/*** @brief 从字符串转为日志枚举类型* @param[in] val 日志级别字符串* @return LogLevel::Level*/static LogLevel::Level FromString(const std::string& val);LogLevel::Level LogLevel::FromString(const std::string& val){#define XX(name)\if(strcasecmp(val.c_str(), #name) == 0)\{\return LogLevel::name;\}//解决小写不识别XX(DEBUG);XX(INFO);XX(WARN);XX(FATAL);XX(ERROR);#undef XXreturn LogLevel::UNKNOW;}
2. LoggerAppender 类封装 (接口类)
* 成员变量
class LogAppender{......protected://输出器日志级别LogLevel::Level m_level;//日志格式器智能指针LogFormatter::ptr m_formatter;//互斥锁MutexType m_mutex;//是否设置过日志格式bool is_set_formatter = false;};
* 接口
1. 虚接口
1.1 log()
功能:日志输出的多态接口。通过该多态接口,会去调用具体子类中的log()函数执行输出动作。
/*** @brief 日志输出的多态接口* @param[in] logger 传入的日志器* @param[in] level 日志级别* @param[in] event 日志属性*/virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
1.2 toYamlString()
功能:日志输出器信息以yaml格式用字符串输出,多态接口
/*** @brief 日志输出器信息以yaml格式用字符串输出 多态接口* @return std::string*/virtual std::string toYamlString() = 0;
2. 常用接口
/*** @brief 设置日志输出器输出格式* @param[in] val 具体格式*/void setFormatter(LogFormatter::ptr val);void LogAppender::setFormatter(LogFormatter::ptr val){MutexType::Lock lock(m_mutex);m_formatter = val;//传入的日志格式器不为nullptr 该输出器被设置过格式if(m_formatter)is_set_formatter = true;elseis_set_formatter = false;}/*** @brief 获取日志输出器输出格式* @return LogFormatter::ptr*/LogFormatter::ptr getFormatter();LogFormatter::ptr LogAppender::getFormatter(){MutexType::Lock lock(m_mutex);return m_formatter;}/*** @brief 获取日志输出器的日志级别* @return LogLevel::Level*/LogLevel::Level getLevel() const {return m_level;}/*** @brief 设置日志输出器的日志级别* @param val*/void setLevel(LogLevel::Level val){m_level = val; }/*** @brief 设置是否设置过日志格式*/void setIsFormatter() {is_set_formatter = true;}/*** @brief 获取是否设置过日志格式* @return true 设置过* @return false 没有设置过*/bool getIsFormatter() {return is_set_formatter;}
2.1 StdoutLogAppender 输出到控制台类 具体子类
2.1.1 重写接口
2.1.1.1 log()
/*** @brief 输出日志内容到控制台显示* @param[in] logger 具体日志器* @param[in] level 日志级别* @param[in] event 日志属性*/void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override;void StdoutLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event){if(level >= m_level){//std::cout 不是线程安全的//锁m_formatterMutexType::Lock lock(m_mutex);std::cout << m_formatter->format(logger, level, event);}}
2.1.1.2 toYamlString()
功能:将日志器输出器信息以yaml格式用字符串输出
/*** @brief 将日志器输出器信息以yaml格式用字符串输出* @return std::string*/std::string toYamlString() override;std::string StdoutLogAppender::toYamlString(){//锁m_formatterMutexType::Lock lock(m_mutex);YAML::Node node;node["type"] = "StdoutLogAppender";node["level"] = LogLevel::ToString(m_level);//如果输出器单独设置了格式 也要显示if(m_formatter && getIsFormatter())node["formatter"] = m_formatter->getPattern();std::stringstream ss;ss << node;return ss.str();}
2.2 FileLogAppender 输出到文件类 具体子类
2.2.1 成员变量
class FileLogAppender: public LogAppender{......private://输出文件路径std::string m_filename;//输出的文件流std::ofstream m_filestream;//上一次打开文件的时间uint64_t m_last_time = 0;};
2.2.2 接口
2.2.2.1 构造函数
/*** @brief 输出到文件的日志输出器类构造函数* @param[in] filename 输出到的具体文件路径*/FileLogAppender(const std::string& filename);FileLogAppender::FileLogAppender(const std::string& filename):m_filename(filename){//初始化时间结构体memset(&tv_cur, 0, sizeof(struct timeval));//将当前文件重打开一次reopen();}
2.2.2.2 reopen()
功能:文件重打开。在多线程情况下,有可能当前我们操作的这个文件,已经被别的线程关闭,当前线程操作时候需要重新以追加内容的方式打开文件。
/*** @brief 文件重打开* @return true 文件重打开成功* @return false 文件重打开失败*/bool reopen();bool FileLogAppender::reopen(){//锁 文件句柄MutexType::Lock lock(m_mutex);if(m_filestream){m_filestream.close();}//以追加方式打开文件m_filestream.open(m_filename, std::ios::app);return !m_filestream;}
2.2.2.3 重写接口
1). log()
功能:输出日志内容到文件中。选择每间隔1s重打开文件,由于过分频繁的重打开应该会有较大的开销
/*** @brief 输出日志内容到文件中* @param[in] logger 具体日志器* @param[in] level 日志级别* @param[in] event 日志属性*/void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override;void FileLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event){if(level >= m_level){//获取当前的时间uint64_t now = time(0);if(now != m_last_time) //这个!=不等于意味着 每过1s就重打开一次{reopen();m_last_time = now;}MutexType::Lock lock(m_mutex);m_filestream << m_formatter->format(logger, level, event);;}}
2). toYamlString()
功能:将日志器输出器以yaml格式用字符串输出
/*** @brief 将日志器输出器以yaml格式用字符串输出* @return std::string*/std::string toYamlString() override;std::string FileLogAppender::toYamlString(){//锁m_formatterMutexType::Lock lock(m_mutex);YAML::Node node;node["type"] = "FileLogAppender";node["file"] = m_filename;node["level"] = LogLevel::ToString(m_level);if(m_formatter && getIsFormatter())node["formatter"] = m_formatter->getPattern();std::stringstream ss;ss << node;return ss.str();}
3. Logger 类封装
3.1 成员变量
class Logger :public std::enable_shared_from_this<Logger>{........private://日志器名字std::string m_name;//日志级别LogLevel::Level m_level;//日志输出器的指针队列std::list<LogAppender::ptr> m_appenders;//互斥锁MutexType m_mutex;//Logger自带的一个LogFormatter 防止LogAppender没有LogFormatterLogFormatter::ptr m_formatter;//改动:设置默认LoggerLogger::ptr default_root;};
3.2 接口
3.2.1 构造函数
/*** @brief 日志器类构造函数,日志器默认级别为DEBUG* @param[in] name 日志器的名称 默认为"root"*/Logger(const std::string &name = "root");Logger::Logger(const std::string &name):m_name(name), m_level(LogLevel::DEBUG){//在生成Logger时候会生成一个默认的LogFormatterm_formatter.reset(new LogFormatter("[%p]%T<%f:%l>%T%d{%Y-%m-%d %H:%M:%S}%T%t(%tn)%T%c%T%g%T%m%n"));}
3.2.2 log()(核心)
功能:输出日志内容。通过组合LogAppender对象实现实际的日志内容输出
/*** @brief 输出日志内容* @param[in] level 日志级别* @param[in] event 日志属性*/void log(LogLevel::Level level, const LogEvent::ptr event);void Logger::log(LogLevel::Level level, const LogEvent::ptr event){//如果传入的日志级别 >= 当前日志器的级别都能进行输出if(level >= m_level){//拿到this指针的智能指针auto self = shared_from_this();if(m_appenders.size()){//操作日志输出器的时候要独占 锁住MutexType::Lock lock(m_mutex);for(auto &x : m_appenders){//调用的是appender中的log()进行实际输出//有点类似观察者模式x->log(self, level, event);}}else if(default_root) //如果当前的Logger没有分配LogAppender就使用default_root中的LogAppender来打印{//是一层递归 使用default_root中的日志输出器来打印default_root->log(level, event);}}}
· 以log()为基础的扩展便捷接口
/*** @brief DUBEG级别输出日志内容* @param[in] event 日志属性*/void debug(LogEvent::ptr event);void Logger::debug(LogEvent::ptr event){log(LogLevel::DEBUG, event);}/*** @brief INFO级别输出日志内容* @param[in] event 日志属性*/void info(LogEvent::ptr event);void Logger::info(LogEvent::ptr event){log(LogLevel::INFO, event);}/*** @brief WARN级别输出日志内容* @param[in] event 日志属性*/void warn(LogEvent::ptr event);void Logger::warn(LogEvent::ptr event){log(LogLevel::WARN, event);}/*** @brief ERROR级别输出日志内容* @param[in] event 日志属性*/void error(LogEvent::ptr event);void Logger::error(LogEvent::ptr event){log(LogLevel::ERROR, event);}/*** @brief FATAL级别输出日志内容* @param[in] event 日志属性*/void fatal(LogEvent::ptr event);void Logger::fatal(LogEvent::ptr event){log(LogLevel::FATAL, event);}
3.2.3 addAppender()
功能:添加日志输出器
/*** @brief 添加日志输出器* @param[in] appender 具体的日志输出器*/void addAppender(LogAppender::ptr appender);void Logger::addAppender(LogAppender::ptr appender){//加锁MutexType::Lock lock(m_mutex);//发现加入的日志输出器没有格式器的话 赋予默认格式器if(!appender->getFormatter()){//锁 LogAppender中的LogFormatterappender->setFormatter(m_formatter);}m_appenders.push_back(appender);}
3.2.4 delAppender()
功能:删除日志输出器
/*** @brief 删除日志输出器* @param[in] appender 具体的日志输出器*/void delAppender(LogAppender::ptr appender);void Logger::delAppender(LogAppender::ptr appender){//锁 m_appendersMutexType::Lock lock(m_mutex);auto it = find(m_appenders.begin(), m_appenders.end(), appender);m_appenders.erase(it);/*等价*///遍历的方式删除// for(auto it = m_appenders.begin();it != m_appenders.end();it++)// {// if(*it == appender)// {// m_appenders.erase(it);// break;// }// }}
3.2.5 clearAppender()
功能:清空日志输出队列
/*** @brief 清空日志输出队列*/void clearAppender();void Logger::clearAppender(){MutexType::Lock lock(m_mutex);m_appenders.clear();}
3.2.6 setFormatter()
功能:设置日志器格式。检查所有日志输出器,如果没有设置过格式,就和当前日志器的格式同步。
/*** @brief 设置日志器格式* @param[in] val 具体格式字符串*/void setFormatter(const std::string& val);void Logger::setFormatter(const std::string& val){//解析模板并创建新的日志格式器LogFormatter::ptr new_value(new LogFormatter(val));//模板解析是否有错误if(new_value->isError()){std::cout << "Logger setFormatter name=" << m_name<< "value=" << val << "invaild formatter" << std::endl;return;}//加锁MutexType::Lock lock(m_mutex);m_formatter = new_value;//将所有LogAppender 没有设置过格式的formatter和Logger同步for(auto &x : m_appenders){if(!x->getIsFormatter())x->setFormatter(new_value);}}
3.2.7 getFormatter()
功能:获取日志器格式
/*** @brief 获取日志器格式* @return LogFormatter::ptr*/LogFormatter::ptr getFormatter();LogFormatter::ptr Logger::getFormatter(){MutexType::Lock lock(m_mutex);return m_formatter;}
3.2.8 toYamlString()
功能:将日志器信息以yaml格式用字符串输出
/*** @brief 将日志器信息以yaml格式用字符串输出* @return std::string*/std::string toYamlString();std::string Logger::toYamlString(){//锁 m_formatterMutexType::Lock lock(m_mutex);YAML::Node node;node["name"] = m_name;node["level"] = LogLevel::ToString(m_level);node["formatter"] = m_formatter->getPattern();for(auto &x : m_appenders){node["appender"].push_back(YAML::Load(x->toYamlString()));}//如果日志输出器队列为空 也需要标识if(!node["appender"].size())node["appender"] = "NULL";std::stringstream ss;ss << node;return ss.str();}
3.2.9 其他接口
/*** @brief 获取日志器级别* @return LogLevel::Level*/LogLevel::Level getLevel() const {return m_level;}/*** @brief 设置日志器级别* @param[in] val 具体日志级别*/void setLevel(LogLevel::Level val){m_level = val;}/*** @brief 获取日志的名字* @return const std::string&*/const std::string& getName() const {return m_name;}/*** @brief 设置默认Root日志器* @param[in] p 默认的日志器*/void setDefaultRoot(Logger::ptr p) {default_root = p;}
4. LogFormatter 类封装
目的:给定日志器、日志输出器日志输出格式。在输出器没有格式的情况下,从属于日志器的格式,日志器生成时未指定格式会有默认的格式被赋予。
4.1 成员变量
class LogFormatter{......public:/*** @brief 实现具体格式输出的子类*/class FormatItem{...};private://日志格式模板 对应模板解析对应内容std::string m_pattern;//具体格式输出的具体子类队列std::vector<FormatItem::ptr> m_items;//格式模板是否发生错误bool m_error = false;};
4.2 接口
4.2.1 构造函数
/*** @brief 日志格式器类构造函数* @param[in] pattern 具体模板格式字符串*/LogFormatter(const std::string &pattern);LogFormatter::LogFormatter(const std::string &pattern):m_pattern(pattern){//解析传入模板格式init();}
** 私有接口
1). init() (核心)
功能:日志格式器初始化,解析模板字符串,分析出具体要使用的模板格式
- 字符对应的格式输出:
%n-------换行符 '\n'%m-------日志内容%p-------level日志级别%r-------程序启动到现在的耗时%%-------输出一个'%'%t-------当前线程ID%T-------Tab键%tn------当前线程名称%d-------日期和时间%f-------文件名%l-------行号%g-------日志器名字%c-------当前协程ID
"%d [%p] %f %l %m %n"为例子1
"%test%n" 为例子2
"[%p]%T<%f:%l>%T%d{%Y-%m-%d %H:%M:%S}%T%t(%tn)%T%c%T%g%T%m%n" 为例子3
使用vector暂存由m_pattern模板格式中分解得到的 模板内容 + 普通字符串内容
std::vector<std::tuple<std::string, std::string, int> > mv;**例子1中mv中包含的结果: (_表示空格 1为有效模板内容 0为无效模板内容)"_" "_" 0"d" "_" 1"_[" "_" 0"p" "_" 1"]_" "_" 0"_" "_" 0"f" "_" 1"_" "_" 0"l" "_" 1"_" "_" 0"m" "_" 1"_" "_" 0"n" "_" 1**例子2中mv中包含的结果: (_表示空格 1为有效模板内容 0为无效模板内容)"_" "_" 0"test" "_" 1"_" "_" 0"n" "_" 1**例子3中mv中包含的结果: (_表示空格 1为有效模板内容 0为无效模板内容)"[" "_" 0"p" "_" 1"]" "_" 0"T" "_" 1"<" "_" 0"f" "_" 1":" "_" 0"l" "_" 1">" "_" 0"T" "_" 1"d" "%Y-%m-%d %H:%M:%S" 1"T" "_" 1"t" "_" 1"(" "_" 0"tn" "_" 1")" "_" 0"T" "_" 1"c" "_" 1"T" "_" 1"g" "_" 1"T" "_" 1"m" "_" 1"n" "_" 1
- 技巧点:
- 利用
map容器将解析出来的这些可能的模板字符串,与具体实现子类建立映射关系。 - 使用宏定义替换,代替传统
switch....case写法,很便捷。 - 使用
function<智能指针(传参)> = {return 实例化对象智能指针;},结合多态实现一个接口多实现。如果采用函数指针,则需要每一种函数写一个指针依次赋值。
/*** @brief 日志格式器初始化,解析模板字符串*/void init();void LogFormatter::init(){// 1.模板字符串 2.指定的参数字符串 3.是否为正确有效模板std::vector<std::tuple<std::string, std::string, int> > mv;//缓存和模板内容无关的普通字符串 一并加入到mv中std::string str = "";for(size_t i = 0;i < m_pattern.size();i++){//遇到"%"前的普通字符存入到str中并且跳到下一个字符if(m_pattern[i] != '%'){str += m_pattern[i];continue;}//连续遇到两个"%%" 进行转义处理if(i + 1 < m_pattern.size() && m_pattern[i + 1] == '%'){str += '%';continue;}/*遇到'%'后开始判断是否是某种格式模板*/std::string pcahr = ""; //暂时存储相关模板代表字母std::string fmt = ""; //存储"{...}"间的模板内容size_t index = i + 1; // '%'后的第一个位置int fmt_status = 0; //有限状态机表示size_t fmt_begin = 0; // "{...}"间内容的起点while(index < m_pattern.size()){// 碰到符号H:非字符 && 非'{' && 非'}' 后将"%XXXH"紧跟串XXX存入temp中,并且breakif(fmt_status == 0 && (!isalpha(m_pattern[index]) && m_pattern[index] != '{' && m_pattern[index] != '}')){pcahr = m_pattern.substr(i + 1, index - i - 1);break;}//fmt_status表示一种有限状态机,处理括号中的内容//0 = 未遇到 '{'//1 = 已经遇到'{' , 未遇到'}'if(fmt_status == 0){//%XXX{ 存储XXX串if(m_pattern[index] == '{'){pcahr = m_pattern.substr(i + 1, index - i - 1);fmt_status = 1;fmt_begin = index;++index;continue;}}else if(fmt_status == 1){// {XXXX} 存储XXX串if(m_pattern[index] == '}'){fmt = m_pattern.substr(fmt_begin + 1, index - fmt_begin - 1);fmt_status = 0;++index;break;}}++index;// "%XXXXXX" 说明整个字符串中只有一个"%"符if(index == m_pattern.size()){if(!pcahr.size()){pcahr = m_pattern.substr(i + 1);}}}//没有遇到'{' 或者 已经完成"{...}"的扫描if(fmt_status == 0){//%aaaaXXX%bbbb 或者 XXX%aaaa 存储XXXif(pcahr.size()){mv.push_back(std::make_tuple(str, std::string(), 0));str.clear();}//%XXX 存储XXX 且 {YYY} 存储YYYmv.push_back(std::make_tuple(pcahr, fmt, 1));i = index - 1;}else if(fmt_status == 1) //有 { 但没有遇到 } 一定是一个错误的序列{std::cout << "pattern parse error" << m_pattern << "--" << m_pattern.substr(i) << std::endl;m_error = true;mv.push_back(std::make_tuple("<<pattern error>>", fmt, 0));}}// "%aaaXXX"存储XXX 即最后尾部为普通字符串的情况if(str.size()){mv.push_back(std::make_tuple(str, "", 0));}//将字符 和 对应的函数操作建立映射关系//可以使用函数指针完成,这里使用了函数包装器function + lambda表达式,非常方便static std::map<std::string, std::function<FormatItem::ptr(const std::string& str)> > s_format_items = {#define XX(str, C)\{#str, [](const std::string& fmt){ return FormatItem::ptr(new C(fmt));}}/*注意必须是 ',' */XX(m, MessageFormatItem),XX(p, LevelFormatItem),XX(r, ElapseFormatItem),XX(t, ThreadIdFormatItem),XX(tn, ThreadNameFormatItem),XX(d, DateTimeFormatItem),XX(l, LineFormatItem),XX(n, NewLineFormatItem),XX(f, FileNameFormatItem),XX(T, TabFormatItem),XX(c, CoroutineIdFormatItem),XX(g, LogNameFormatItem)//XX(test, TestFormatItem),#undef XX};for(auto &x : mv){//元组中标志int = 0 说明不是模板内容 构造为普通字符串对象if(std::get<2>(x) == 0){//构造输出文本内容的子类对象m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(x))));}else{auto it = s_format_items.find(std::get<0>(x));//如果mv中 存储的<0>位置的字符串 无法与map中的映射关系对应,说明存储了一个错误的字符模板if(it == s_format_items.end()){//构造输出文本内容的子类对象 输出一下错误m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(x) + ">>")));m_error = true;}else{//给对应的子类构造传参<1>位置的fmt字符串m_items.push_back(it->second(std::get<1>(x)));}}}}
4.2.2 format()(核心)
功能:输出具体格式要求内容,经过解析后会按照既定的合格式输出日志内容。通过调用FormatItem接口下实现的各个子类的format()完成日志内容的迭代处理。
效果演示:
**调用**:LogEvent::format("This is vsprintf test:%s %d\n", "string", 1024);**过程**:LogEvent::format(const char* fmt, ...)|| "string"、1024------>va_list|LogEvent::format(const char* fmt, va_list al)buf:"This is vsprintf test:string 1024"
/*** @brief 输出具体格式要求内容* @param[in] logger 具体日志器* @param[in] level 具体日志级别* @param[in] event 具体日志属性* @return std::string*/std::string format(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event);std::string LogFormatter::format(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event){std::stringstream ss;for(auto &x : m_items){x->format(ss, logger, level, event);}return ss.str();}
4.3 FormatItem 类封装(接口类)(核心)
目的:设置一个接口,能够由不同子类实现该接口,从而满足多种日志格式的需求
* 虚接口
/*** @brief 输出具体格式要求内容* @param[in] os 标准输出流* @param[in] logger 具体日志器* @param[in] level 具体日志级别* @param[in] event 具体日志属性*/virtual void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
4.3.1 输出日志器名称 子类
/*** @brief 输出日志名*/class LogNameFormatItem: public LogFormatter::FormatItem{public://该构造函数为了和基类保持一致 无作用LogNameFormatItem(const std::string& str = ""){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << event->getLogger()->getName();}};
4.3.2 输出线程ID 子类
/*** @brief 输出线程ID*/class ThreadIdFormatItem: public LogFormatter::FormatItem{public:ThreadIdFormatItem(const std::string& str = ""){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << event->getThreadId();}};
4.3.3 输出线程名 子类
/*** @brief 输出线程名*/class ThreadNameFormatItem: public LogFormatter::FormatItem{public:ThreadNameFormatItem(const std::string& str = ""){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << event->getThreadName();}};
4.3.4 输出协程ID 子类
/*** @brief 输出协程ID*/class CoroutineIdFormatItem: public LogFormatter::FormatItem{public:CoroutineIdFormatItem(const std::string& str = ""){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << event->getCoroutineId();}};
4.3.5 输出日期时间 子类
/*** @brief 输出日期时间*/class DateTimeFormatItem: public LogFormatter::FormatItem{public:DateTimeFormatItem(const std::string& format = "%Y-%m-%d %H:%M:%S"):m_format(format){if(!m_format.size())m_format = "%Y-%m-%d %H:%M:%S";}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{time_t t = (time_t)(event->getTime());struct tm tm;char s[100];tm = *localtime(&t);strftime(s, sizeof(s), m_format.c_str(), &tm);os << s;}private://时间显示格式std::string m_format;};
4.3.6 输出行号 子类
/*** @brief 输出行号*/class LineFormatItem: public LogFormatter::FormatItem{public:LineFormatItem(const std::string& str = ""){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << event->getLine();}};
4.3.7 输出换行符 子类
/*** @brief 输出换行符*/class NewLineFormatItem: public LogFormatter::FormatItem{public:NewLineFormatItem(const std::string& str = ""){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << std::endl;}};
4.3.8 输出文件名 子类
/*** @brief 输出文件名*/class FileNameFormatItem: public LogFormatter::FormatItem{public:FileNameFormatItem(const std::string& str = ""){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << event->getFileName();}};
4.3.9 输出文本内容 子类
/*** @brief 输出文本内容*/class StringFormatItem: public LogFormatter::FormatItem{public:StringFormatItem(const std::string& m_s):m_string(m_s){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << m_string;}private://文本内容std::string m_string;};
4.3.10 输出一个Tab键 子类
/*** @brief 输出一个Tab键*/class TabFormatItem: public LogFormatter::FormatItem{public:TabFormatItem(const std::string& str = ""){}void format(std::ostream &os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override{os << "\t";}};
5. LogEvent 类封装
目的:记录要输出的日志内容来自某一文件、来自某一函数、线程ID、所属日志器、日志级别等信息
5.1 成员变量
class LogEvent{......private://文件名const char* m_file = nullptr;//行号int32_t m_line = 0;//程序启动到现在的毫秒数uint32_t m_elapse = 0;//线程IDuint32_t m_threadid = 0;//线程名称std::string m_thread_name = "";//协程IDuint32_t m_coroutineid = 0;//日志时间戳uint64_t m_time;//日志内容std::stringstream m_content;//所属日志器std::shared_ptr<Logger> m_logger;//日志级别LogLevel::Level m_level;};
5.2 接口
5.2.1 构造函数
/*** @brief 日志属性类构造函数* @param[in] logger 具体日志器* @param[in] level 具体日志级别* @param[in] file 从哪一个文件输出* @param[in] m_line 从哪一行输出* @param[in] elapse 程序启动后总共时间 单位ms* @param[in] thread_id 线程号* @param[in] thread_name 线程名称* @param[in] coroutine_id 协程号* @param[in] time 日志输出时间*/LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level, const char* file,int32_t m_line, uint32_t elapse, uint32_t thread_id,const std::string& thread_name, uint32_t coroutine_id, uint64_t time);LogEvent::LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level, const char* file,int32_t line, uint32_t elapse, uint32_t thread_id,const std::string& thread_name, uint32_t coroutine_id, uint64_t time):m_file(file),m_line(line),m_elapse(elapse),m_threadid(thread_id),m_thread_name(thread_name),m_coroutineid(coroutine_id),m_time(time),m_logger(logger),m_level(level){}
5.2.2 常用接口
/*** @brief 获取文件名称* @return const char**/const char* getFileName() const {return m_file;}/*** @brief 获取行号* @return int32_t*/int32_t getLine() const {return m_line;}/*** @brief 获取程序启动后总共时间 单位ms* @return uint32_t*/uint32_t getElapse() const {return m_elapse;}/*** @brief 获取线程号* @return uint32_t*/uint32_t getThreadId() const {return m_threadid;}/*** @brief 获取线程名称* @return const std::string&*/const std::string& getThreadName() const {return m_thread_name;}/*** @brief 获取协程号* @return uint32_t*/uint32_t getCoroutineId() const {return m_coroutineid;}/*** @brief 获取输出时间* @return uint64_t*/uint64_t getTime() const {return m_time;}/*** @brief 获取日志内容,以字符串返回* @return std::string*/std::string getContent() const {return m_content.str();}/*** @brief 获取日志内容,以流返回(能加const修饰,需要不断的对"流"内容进行追加和修改)* @return std::stringstream&*/std::stringstream& getSS() {return m_content;}/*** @brief 获取日志器* @return std::shared_ptr<Logger>*/std::shared_ptr<Logger> getLogger() const {return m_logger;}/*** @brief 获取日志级别* @return LogLevel::Level*/LogLevel::Level getLevel() const {return m_level;}
5.2.3 format()(核心)
功能:实现类似printf("%d", 6);这样的功能,让日志内容能够带上参数。
/*** @brief 自定义日志模板格式* @param[in] fmt 传入的具体日志模板* @param[in] ... 可变参数*/void format(const char *fmt, ...);void LogEvent::format(const char* fmt, ...){va_list al;va_start(al, fmt);format(fmt, al);va_end(al);}/*** @brief 自定义日志模板格式* @param[in] fmt 传入的具体日志模板* @param[in] al 可变参数列表*/void format(const char *fmt, va_list al);//传入已经准备好的可变参数 进行一个组合的得到的结果放在buf中//并且构造一个string对象以"流"的方式送入到stringstream中去作为日志内容void LogEvent::format(const char* fmt, va_list al){char *buf = nullptr;int len = vasprintf(&buf, fmt, al);//vasprintf会动态分配内存 失败返回-1if(len != -1){m_content << std::string(buf, len);free(buf);}}
6. LogEventWrap 类封装
目的:封装便捷的宏函数调用时,无法使得LogEvent对象正常析构。封装一层包装器,在其析构函数中去实现原本想实现的功能,而又不产生内存泄漏。使用了一个LogEventWrap的匿名对象,由于匿名对象的生命周期只有一行,因此会马上回收空间触发析构函数。析构函数中就有对应日志打印语句:Logger::log()。
6.1 成员变量
class LogEventWrap
{
...
private:
//日志属性对象智能指针
LogEvent::ptr m_event;
};
6.2 接口
6.2.1 构造函数
/**
* @brief 日志属性包装器类构造函数
* @param[in] event 日志属性对象智能指针
*/
LogEventWrap(LogEvent::ptr event);
LogEventWrap::LogEventWrap(LogEvent::ptr event)
:m_event(event)
{
}
6.2.2 析构函数 (核心)
/**
* @brief 日志属性包装器类析构函数
*/
~LogEventWrap();
LogEventWrap::~LogEventWrap()
{
//通过日志属性对象中日志器去打印日志内容
m_event->getLogger()->log(m_event->getLevel(), m_event);
}
6.2.3 其他接口
/**
* @brief 获取日志属性中的日志内容
* @return std::stringstream&
*/
std::stringstream& getSS();
std::stringstream& LogEventWrap::getSS()
{
return m_event->getSS();
}
/**
* @brief 获取日志属性智能指针
* @return std::shared_ptr<LogEvent>
*/
std::shared_ptr<LogEvent> getEvent();
std::shared_ptr<LogEvent> LogEventWrap::getEvent()
{
return m_event;
}
C\C++知识点补充复习:tuple 元组和 std::get()函数
概念:tuple是多元组,pair是二元组,这是两个相似的概念。使用std::get<>(tuple)配合取出元组中对应元素的值。
tuple<c0,c1,c2,...> t
std::get<0>(t)-----> c0类型的值
std::get<1>(t)-----> c1类型的值
std::get<2>(t)-----> c2类型的值
pair<c0, c1> p;
p.first-------->c0类型的值
p.second------->c1类型的值
C\C++知识点补充复习:strftime()函数
功能:根据格式规范 format 格式化分解时间 tm,并将结果放入大小为 max 的字符数组 s 中。格式规范是一个以 null 结尾的字符串,可能包含称为转换规范的特殊字符序列,每个字符序列都由一个 ‘%’ 字符引入,并由一些称为转换说明符字符的其他字符终止。 所有其他字符序列都是普通字符序列。
返回值: (注意返回0不一定代表错误)
- 如果结果字符串(包括终止空字节)不超过最大字节数,则 strftime() 返回字节数(不包括终止空字节)放在数组 s 中。
- 如果结果字符串的长度(包括终止的空字节)将超过最大字节,则 strftime() 返回 0,并且数组的内容未定义。
#include <time.h>
size_t strftime(char *s, size_t max, const char *format,
const struct tm *tm);
C\C++知识点补充复习:vasprintf()函数
功能:和sprintf()函数类似,但是能够接受可变参数。
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <stdio.h>
int asprintf(char **strp, const char *fmt, ...);
int vasprintf(char **strp, const char *fmt, va_list ap);
C\C++知识点补充复习:函数宏、文件宏、行号宏
C/C++提供了三个宏FUNCTION, FILE和LINE定位程序运行时的错误。程序预编译时预编译器将用所在的函数名,文件名和行号替换。当运行时错误产生后这三个宏分别能返回错误所在的函数,所在的文件名和所在的行号。
getpid()/pthread_self()/syscall(SYS_gettid)区别
gettid 获取的是内核中真实线程ID, 对于多线程进程来说,每个tid实际是不一样的。
而pthread_self获取的是相对于进程的线程控制块的首地址, 只是用来描述统一进程中的不同线程,进程采用虚拟地址空间是有可能产生相同的值的。
