准备:认识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 XX
return 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;
else
is_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_formatter
MutexType::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_formatter
MutexType::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_formatter
MutexType::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没有LogFormatter
LogFormatter::ptr m_formatter;
//改动:设置默认Logger
Logger::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时候会生成一个默认的LogFormatter
m_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中的LogFormatter
appender->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_appenders
MutexType::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_formatter
MutexType::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中,并且break
if(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 存储XXX
if(pcahr.size())
{
mv.push_back(std::make_tuple(str, std::string(), 0));
str.clear();
}
//%XXX 存储XXX 且 {YYY} 存储YYY
mv.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;
//线程ID
uint32_t m_threadid = 0;
//线程名称
std::string m_thread_name = "";
//协程ID
uint32_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会动态分配内存 失败返回-1
if(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获取的是相对于进程的线程控制块的首地址, 只是用来描述统一进程中的不同线程,进程采用虚拟地址空间是有可能产生相同的值的。