ToB企服应用市场:ToB评测及商务社交产业平台
标题:
日志模块
[打印本页]
作者:
泉缘泉
时间:
2024-5-27 14:31
标题:
日志模块
介绍
在学习了sylar的
C++高性能分布式服务器框架
后,想把自己在学习过程中的感想记录下来。当然主要缘故原由还是sylar的
B站视频
过于难以理解了,也是想加强一下自己对这个框架的理解。许多内容也是借鉴了其他大佬的博文,比如
找人找不到北
,
zhongluqiang
日志模块概述
日志模块的目标:
用于格式化输出程序日志,方便从日志中定位程序运行过程中出现的问题。
同时应该包罗文件名/行号,时间戳,线程/协程号,模块名称,日志级别等额外信息。
在打印致命的日志时,还应该附加程序的栈回溯信息,以便于分析和排查问题。
从设计上看,一个完备的日志模块应该具备以下功能:
区分不同的级别,比如常的DEBUG/INFO/WARN/ERROR等级别。
区分不同的输出地。不同的日志可以输出到不同的位置,比如可以输出到尺度输出,输出到文件,输出到syslog,输出到网络上的日志服务器等,甚至同一条日志可以同时输出到多个输出地。
区分不同的类别。日志可以分类并命名,一个程序的各个模块可以使用不同的名称来输出日志,这样可以很方便地判断出当前日志是哪个程序模块输出的。
日志格式可灵活配置。可以按需指定每条日志是否包含文件名/行号、时间戳、线程/协程号、日志级别、启动时间等内容。
可通过配置文件的方式配置以上功能。
日志模块设计
类似于log4cpp,日志模块拥有以下几个主要类:
class LogLevel:定义日志级别。并提供将日志级别与文本之间的互相转化
class Logger:日志器。定义日志级别,设置输出地,设置日志格式。
class LogEvent:记录日志事件。主要记录一下信息
class LogEventWarp:日志事件包装器。将logEvent打包,可以直接通过使用该类完成对日志的定义。
class LogFormatter:日志格式化。
class LogAppender:日志输出目标。有两个子类 class StdoutLogAppender 和 class FileLogAppender,可以分别输出到控制台和文件
class LoggerManager:日志管理器。单例模式
具体实现
日志级别 class LogLevel
enum Level {
/// 致命情况,系统不可用
FATAL = 0,
/// 高优先级情况,例如数据库系统崩溃
ALERT = 100,
/// 严重错误,例如硬盘错误
CRIT = 200,
/// 错误
ERROR = 300,
/// 警告
WARN = 400,
/// 正常但值得注意
NOTICE = 500,
/// 一般信息
INFO = 600,
/// 调试信息
DEBUG = 700,
/// 未设置
NOTSET = 800,
};
复制代码
ToString(提供从日志级别 TO 文本的转换)
通过X宏(X Macros)将不同的级别放入switch case语句中。X宏的根本思想是将重复的代码片段定义为一个宏,然后在必要使用这些代码的地方多次调用这个宏。这样可以避免手动编写和维护大量重复代码。
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(ERROR);
XX(FATAL);
#undef XX
default:
return "UNKNOW";
}
return "UNKNOW";
}
复制代码
swtich (level):对传入的 level 参数举行 switch 分支判断。
//#define XX(name) ... #undef XX:定义了一个名为 XX 的宏,用于减少重复代码。宏 XX 接受一个参数 name,并生成对应的 case 语句。宏会睁开成:
case LogLevel::DEBUG:
return "DEBUG";
break;
case LogLevel::INFO:
return "INFO";
break;
// 依次类推
复制代码
注1:常见的X宏使用场景:
枚举与字符串映射:如将枚举值转换为字符串。
多次声明相似的代码结构:如函数声明、结构体初始化等。
生成重复的测试代码:如生成一系列测试用例。
FromString(提供从文本 To 日志级别的转换)
同样通过宏定义处理多种情况。转换时不针对大小写,DEBUG和debug都可以完成对应的转化
LogLevel::Level LogLevel::FromString(const std::string &str) {
#define XX(level, v) \
if(str == #v) { \
return LogLevel::level; \
}
XX(DEBUG, debug);
XX(INFO, info);
XX(WARN, warn);
XX(ERROR, error);
XX(FATAL, fatal);
XX(DEBUG, DEBUG);
XX(INFO, INFO);
XX(WARN, WARN);
XX(ERROR, ERROR);
XX(FATAL, FATAL);
return LogLevel::UNKNOW;
#undef XX
}
复制代码
日志事件 class LogEvent
用于记录日志现场,比如该日志的级别,文件名/行号,日志消息,线程/协程号,所属日志器名称等。
成员变量
const char* m_file = nullptr; //文件名
int32_t m_line = 0; //行号
uint32_t m_elapse = 0; //程序启动开始到现在的毫秒数
uint32_t m_thieadId = 0; //线程id
uint32_t m_fiberId = 0; //协程id
uint64_t m_time; //时间戳
std::string m_threadName; //线程名称
std::stringstream m_ss; //日志内容流
std::shared_ptr<Logger> m_logger; //日志器
LogLevel::Level m_level; //日志等级
复制代码
成员函数
/**
* @brief 构造函数
*
* @param[in] logger 日志器
* @param[in] level 日志级别
* @param[in] file 文件名
* @param[in] line 文件行号
* @param[in] elapse 程序启动依赖的耗时(毫秒)
* @param[in] thread_id 线程id
* @param[in] fiber_id 协程id
* @param[in] time 日志时间
* @param[in] thread_name 线程名称
*/
LogEvent::LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level
, const char* file, int32_t line, uint32_t elapse
, uint32_t thread_id, uint32_t fiber_id, uint64_t time
, const std::string& thread_name)
:m_file(file)
,m_line(line)
,m_elapse(elapse)
,m_thieadId(thread_id)
,m_fiberId(fiber_id)
,m_time(time)
,m_threadName(thread_name)
,m_logger(logger)
,m_level(level) {
}
void LogEvent::format(const char* fmt, ...) {
va_list al; //1)
va_start(al, fmt); //2)
format(fmt, al); //3)
va_end(al); //6)
}
void LogEvent::format(const char* fmt, va_list al){
char *buf = nullptr;
// len返回写入buf的长度
int len = vasprintf(&buf, fmt, al); //4)
if(len != -1) {
m_ss << std::string(buf, len); //5)
free(buf);
}
}
复制代码
在此说一下使用日志的宏,这里定义了SYLAR_LOG_LEVEL宏,用来输出Level级别的LogEvent,并将LogEvent写入到Logger中。
// 日志事件
LogEvent::ptr m_event;
// 构造函数
LogEventWarp::LogEventWarp(LogEvent::ptr e)
:m_event(e){
}
// 析构函数
LogEventWarp::~LogEventWarp() {
m_event->getLogger()->log(m_event->getLevel(), m_event);
}
复制代码
日志输出
日志输出器 class LogAppender
class LogAppender是抽象类,有两个子类,分别为StdoutLogAppender和FileLogAppender,分别实现控制台和文件的输出。两个类都重写纯虚函数log方法实现写入日志,重写纯虚函数toYamlString方法实现将日志转化为YAML格式的字符串
成员变量
#define SYLAR_LOG_LEVEL(logger, level) \
if (logger->getLevel() <= level) \
sylar::LogEventWarp(sylar::LogEvent::ptr (new sylar::LogEvent(logger, level, \
__FILE__, __LINE__, 0, sylar::GetThreadId(), \
sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()
#define SYLAR_LOG_DEBUG(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::DEBUG)
复制代码
成员函数
// 消息format
class MessageFormatItem : public LogFormatter::FormatItem{
public:
MessageFormatItem(const std::string& str = "") {}
void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
os << event->getContent();
}
};
// 日志级别format
class LevelFormatItem : public LogFormatter::FormatItem{
public:
LevelFormatItem(const std::string& str = "") {}
void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
os << LogLevel::ToString(level);
}
};
// 执行时间format
class ElapseFormatItem : public LogFormatter::FormatItem{
public:
ElapseFormatItem(const std::string& str = "") {}
void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
os << event->getElapse();
}
};
// 日志器名称format
class NameFormatItem : public LogFormatter::FormatItem{
public:
NameFormatItem(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();
}
};
// 线程id format
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->getThieadId();
}
};
// 协程id format
class FiberIdFormatItem : public LogFormatter::FormatItem{
public:
FiberIdFormatItem(const std::string& str = "") {}
void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
os << event->getFiberId();
}
};
// 线程名称format
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();
}
};
// 时间format
class DateTimeFormatItem : public LogFormatter::FormatItem{
public:
DateTimeFormatItem(const std::string& format = "%Y-%m-%d %H:%M:%S")
:m_format(format) {
if(m_format.empty()) {
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 {
struct tm tm;
time_t time = event->getTime(); //创建event时默认给的 time(0) 当前时间戳
localtime_r(&time, &tm); //将给定时间戳转换为本地时间,并将结果存储在tm中
char buf[64];
strftime(buf, sizeof(buf), m_format.c_str(), &tm); //将tm格式化为m_format格式,并存储到buf中
os << buf;
}
private:
std::string m_format;
};
复制代码
class StdoutLogAppender(输出到控制台的Appender)
// 成员变量
// 日志格式模板
std::string m_pattern;
// 日志格式解析后格式
std::vector<FormatItem::ptr> m_items;
// 判断日志格式错误
bool m_error = false;
// 构造函数
LogFormatter::LogFormatter(const std::string& pattern)
:m_pattern(pattern) {
init();
}
// 将解析后的日志信息输出到流中
std::string LogFormatter::format (std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event){
std::stringstream ss;
for(auto& i : m_items) {
i->format(ss, logger, level, event);
}
return ss.str();
}
// init(解析格式)
// 得到相应FormatItem放入m_items
// 默认格式模板为:"%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
// e.g.Y-M-D H:M:S threadId threadName fiberId [Level] [logName] FILE:LINE message
//%xxx %xxx{xxx} %%
// m_pattern "%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
void LogFormatter::init(){
//string, format, type
std::vector<std::tuple<std::string, std::string, int>> vec;
std::string nstr; // 存放 [ ] :
for(size_t i = 0; i < m_pattern.size(); ++i) {
if (m_pattern[i] != '%') //若解析的不是'%'
{
nstr.append(1, m_pattern[i]); //在nstr后面添加一个该字符
continue;
}
if((i + 1) < m_pattern.size()) { //保证m_pattern不越界
if (m_pattern[i + 1] == '%') { //解析 "%%"
nstr.append(1, '%'); //在nstr后面加上%
continue;
}
size_t n = i + 1; //遇到'%'往下 (e.g.) n = 1, m_pattern[1] = 'd'
int fmt_status = 0; //状态1: 解析时间{%Y-%m-%d %H:%M:%S} 状态0:解析之后的
size_t fmt_begin = 0; //开始位置 为{
std::string str; //d T t N等格式
std::string fmt; //保存时间格式 %Y-%m-%d %H:%M:%S
while(n < m_pattern.size()){
// fmt_status != 0, m_attern[n]不是字母,m_pattern[n]不是'{', m_pattern[n]不是'}'
// (e.g.) %T% (i -> %, n -> T, while循环 n -> % 此时解析完一个T, break
// (e.g.) 遇到 [ ] break,取出[%p]中的p
if(!fmt_status && (!isalpha(m_pattern[n]) && m_pattern[n] != '{' //返回0表示该字符不是字母字符。
&& m_pattern[n] != '}')) {
str = m_pattern.substr(i + 1, n - i - 1);
break;
}
if(fmt_status == 0){ //开始解析时间格式
if(m_pattern[n] == '{'){
str = m_pattern.substr(i + 1, n - i - 1); //str = "d"
fmt_status = 1;
fmt_begin = n;
++n;
continue;
}
} else if(fmt_status == 1) { //结束解析时间格式
if(m_pattern[n] == '}') {
// fmt = %Y-%m-%d %H:%M:%S
fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1);
fmt_status = 0;
++n;
break; //解析时间结束break
}
}
++n;
if (n == m_pattern.size()) { //最后一个字符
if (str.empty()) {
str = m_pattern.substr(i + 1);
}
}
}
if(fmt_status == 0){
if(!nstr.empty()){ // nstr: [ :
vec.push_back(std::make_tuple(nstr, std::string(), 0)); // 将[ ]放入, type为0
nstr.clear();
}
vec.push_back(std::make_tuple(str, fmt, 1)); //(e.g.) ("d", %Y-%m-%d %H:%M:%S, 1) type为1
i = n - 1; //跳过已解析的字符,让i指向当前处理的字符,下个for循环会++i处理下个字符
} else if(fmt_status == 1) {
std::cout << "Pattern parde error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;
m_error = true;
vec.push_back(std::make_tuple("<<pattern_error>>", fmt, 0));
}
}
if(!nstr.empty()) {
vec.push_back(std::make_tuple(nstr, "", 0)); //(e.g.) 最后一个字符为[ ] :
}
// map类型为<string, cb>, string为相应的日志格式, cb返回相应的FormatItem智能指针
static std::map<std::string, std::function<FormatItem::ptr(const std::string& fmt)> > s_format_items = {
#define XX(str, C) \
{#str, [] (const std::string& fmt) { return FormatItem::ptr(new C(fmt)); }}
XX(m, MessageFormatItem), //m:消息
XX(p, LevelFormatItem), //p:日志级别
XX(r, ElapseFormatItem), //r:累计毫秒数
XX(c, NameFormatItem), //c:日志名称
XX(t, ThreadIdFormatItem), //t:线程id
XX(n, NewLineFormatItem), //n:换行
XX(d, DateTimeFormatItem), //d:时间
XX(f, FilenameFormatItem), //f:文件名
XX(l, LineFormatItem), //l:行号
XX(T, TabFormatItem), //T:Tab
XX(F, FiberIdFormatItem), //F:协程id
XX(N, ThreadNameFormatItem), //N:线程名称
#undef XX
};
for (auto& i : vec){
if (std::get<2>(i) == 0) { //若type为0
//将解析出的FormatItem放到m_items中 [ ] :
m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));
} else { //type为1
auto it = s_format_items.find(std::get<0>(i)); //从map中找到相应的FormatItem
if(it == s_format_items.end()) { //若没有找到则用StringFormatItem显示错误信息 并设置错误标志位
m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));
m_error = true;
} else { //返回相应格式的FormatItem,其中std::get<1>(i)作为cb的参数
m_items.push_back(it->second(std::get<1>(i)));
}
}
}
}
复制代码
获得主日志器
//日志级别
LogLevel::Level m_level = LogLevel::DEBUG;
//日志格式器
LogFormatter::ptr m_formatter;
// 互斥锁
MutexType m_mutex;
// 是否有formatter
bool m_hasFormatter = false;
复制代码
获得相应名字的日志器
// setFormatter(更改日志格式器)
void LogAppender::setFormatter(LogFormatter::ptr val) {
MutexType::Lock lock(m_mutex);
m_formatter = val;
if (m_formatter) {
m_hasFormatter = true;
} else {
m_hasFormatter = false;
}
}
// getFormatter(获得日志格式器)
LogFormatter::ptr LogAppender::getFormatter() {
MutexType::Lock lock(m_mutex);
return m_formatter;
}
复制代码
总结
总结一下日志模块的工作流程:
初始化LogFormatter,LogAppender, Logger。
通过宏定义提供流式风格和格式化风格的日志接口。每次写日志时,通过宏主动生成对应的日志事件LogEvent,并且将日志事件和日志器Logger包装到一起,生成一个LogEventWrap对象。
日志接口执行结束后,LogEventWrap对象析构,在析构函数里调用Logger的log方法将日志事件举行输出。
待增补与完善
目前来看,sylar日志模块已经实现了一个完备的日志框架,并且配合后面的配置模块,可用性很高,待增补与完善的地方主要存在于LogAppender,目前只提供了输出到终端与输出到文件两类LogAppender,但从现实项目来看,以下几种类型的LogAppender都黑白常有必要的:
Rolling File Appender,循环覆盖写文件
Rolling Memory Appender,循环覆盖写内存缓冲区
支持日志文件按大小分片或是按日期分片
支持网络日志服务器,比如syslog
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4