logger:
class logger { };
在说这个logger类之前,先看1个关键的内部类 Impl
private: //logger内部数据实现类Impl,内部含有以下成员变量 //时间戳,logstream数据流,日志级别,源文件行号,源文件名字. class Impl { public: typedef logger::loglevel LogLevel; //构造函数,最重要的地方,负责把日志头信息写入到m_stream中 //m_stream<<日志级别,old_errno,文件名,文件行号. Impl(LogLevel level,int old_errno,const SourceFile& file,int line); //得到时间,并且把时间记录到m_stream中去 void formatTime(); //好吧关于timezone我实在是没搞懂,反正都是格式化时间,再把时间字符串写入到logstream里面去 //干脆自己动手实现一下就好了,自己实现了myformatTime放弃使用formatTime void myformatTime(); string m_time2; //自己的时间字符串 //一行日志结束:m_stream<<" - "<void finish(); timestamp m_time; logstream m_stream; LogLevel m_level; int m_line; SourceFile m_basename; };
这个Impl类是logger的核心,它内部的m_buffer数据成员保存具体的日志信息,可以说这个Impl类就是把日志头信息写入到logstream中去。
在构造函数Impl中负责把时间信息,线程信息(如果有),日志级别,错误码信息(如果有)写入到logsteeam
在finish函数中负责把源文件名,行号写入到logstream中去。
logger作用:
这是一个日志类
一条普通日志格式如下
2020年08月24日 星期1 18:25:29.1598264729 WARN 日志4 - main.cpp:36
分别表示:时间,日志级别,日志内容 - 源文件名称:行号
日志数据的存储通过logstream成员变量来实现
目的是实现这样一个日志类:
在构造函数里面把日期基本信息例如时间,日志级别信息写入到logstream中
在析构函数里面先把源文件名,行号写入到logstream中,再把logstream中数据写入到日志输出地例如文件,stdout
从构造开始到析构结束只算一条日志.中间可以通过logger.stream()<<"new 日志" 操作来写入具体日志信息
logger类为用户提供自定义日志输出操作和日志缓冲区清空操作函数的实现,
代码中用下面两个函数实现回调函数的绑定
static void setOutput(outputFunc); 参数为函数指针
static void setFlush(flushFunc);
outputFunc,flushFunc应当在在一条日志完成之后被调用,用于实现日志的持久化(到文件/stdout)
注意日志级别并不是成员变量而是全局变量,这样有个好处就是loglevel的设定对所有的logger类生效
而不是只对所属的logger类生效
logger成员变量:
private: //logger唯一的数据成员,里面包含了上面的Impl信息 Impl m_impl;
Impl中的logstream m_stream是重点,负责日志数据的保存。
logger成员函数:
public: //日志级别,一共6个级别 enum loglevel { TRACE, DEBUG, INFO, WARN, ERROR, FATAL, NUM_LOG_LEVELS //表示日志级别的个数为6 }; //SourceFile类的作用就是从文件路径中获取文件名,例如在/home/zqc/123.cc中获取123.cc class SourceFile { public: template<int N> SourceFile(const char (&arr)[N]) :m_data(arr),m_size(N-1) { //从右往左找到'/'及其之后字符 /123.h const char* slash=strrchr(m_data,'/'); if(slash) { m_data=slash+1; m_size-=static_cast<int>(m_data-arr); } } explicit SourceFile(const char* filename) :m_data(filename) { const char* slash = strrchr(filename, '/'); if (slash) { m_data = slash + 1; } m_size = static_cast<int>(strlen(m_data)); } const char* m_data; //文件名,不含路径 int m_size; //文件名长度 }; //不同的构造函数,内部全部是使用logger::Impl构造函数完成把日志头信息写到logstream里面去. //除了源文件名file,行号line,还可以把loglevel,函数名都写到logstream里面 logger(SourceFile file,int line); logger(SourceFile file,int line,loglevel level); logger(SourceFile file,int line,loglevel level,const char* func); logger(SourceFile file,int line,bool toAbort); ~logger(); //返回内部数据流m_stream类型即为LogStream类型 logstream& stream(){return m_impl.m_stream;} //返回日志级别 static loglevel logLevel(); //设置日志级别 static void setLogLevel(loglevel level); //函数指针,用户可自定义日志输出地和清空日志输出地缓冲区 typedef void (*outputFunc)(const char* msg,int len); typedef void (*flushFunc)(); //使用用户自定义的函数来设置日志输出地点和清空缓冲区操作 static void setOutput(outputFunc); static void setFlush(flushFunc); //设置时区 static void setTimeZone(const timezone& tz);
SourceFile类其实就是把filepath转成filename,例如/home/zqc/123.cc转换成123.cc
logger宏定义:
#define LOG_TRACE if (mymuduo::logger::logLevel() <= mymuduo::logger::TRACE) \ mymuduo::logger(__FILE__, __LINE__, mymuduo::logger::TRACE, __func__).stream() #define LOG_DEBUG if (mymuduo::logger::logLevel() <= mymuduo::logger::DEBUG) \ mymuduo::logger(__FILE__, __LINE__, mymuduo::logger::DEBUG, __func__).stream() #define LOG_INFO if (mymuduo::logger::logLevel() <= mymuduo::logger::INFO) \ mymuduo::logger(__FILE__, __LINE__).stream() #define LOG_WARN mymuduo::logger(__FILE__, __LINE__, mymuduo::logger::WARN).stream() #define LOG_ERROR mymuduo::logger(__FILE__, __LINE__, mymuduo::logger::ERROR).stream() #define LOG_FATAL mymuduo::logger(__FILE__, __LINE__, mymuduo::logger::FATAL).stream() #define LOG_SYSERR mymuduo::logger(__FILE__, __LINE__, false).stream() #define LOG_SYSFATAL mymuduo::logger(__FILE__, __LINE__, true).stream()
为了便于操作,不用每次写日志都额外创建一个临时变量mymuduo::logger。
logger源文件(很长,不过我都写了注释):
#include "logging.h" #include"base/timezone.h" #include"base/currentthread.h" #include#include #include<string.h> #include namespace mymuduo { //下面这几个变量和strerror_tl是为了把错误码信息写入到logstream中去而定义的 __thread char t_errnobuf[512]; __thread char t_time[64]; __thread time_t t_lastSecond; const char* strerror_tl(int savedErrno) { return strerror_r(savedErrno, t_errnobuf, sizeof t_errnobuf); } //日志级别初始化,如果用户未自定义宏定义,默认日志级别为 INFO logger::loglevel initLogLevel() { if (::getenv("MUDUO_LOG_TRACE")) return logger::TRACE; else if (::getenv("MUDUO_LOG_DEBUG")) return logger::DEBUG; else return logger::INFO; } //定义全局日志级别变量,并用initLogLevel()初始化 logger::loglevel g_loglevel=initLogLevel(); //全局变量:日志级别数组 const char* LogLevelName[logger::NUM_LOG_LEVELS]= { "TRACE ", "DEBUG ", "INFO ", "WARN ", "ERROR ", "FATAL ", }; //编译期间用于已知字符串长度的帮助类 //不明白这个类有什么意义,自己实现了一个只能拷贝复制操作的最简单string类型 class T { public: T(const char* str,unsigned len):m_str(str),m_len(len) { assert(strlen(m_str)==m_len); } const char* m_str; const unsigned m_len; }; //重载logstream的<<操作符,这里又新加了两种类型 SourceFile文件名类 和 T精简字符串类 inline logstream& operator<<(logstream& s,const logger::SourceFile& v) { s.append(v.m_data,v.m_size); return s; } inline logstream& operator<<(logstream& s,T v) { s.append(v.m_str,v.m_len); return s; } //默认的日志输出,向stdout控制台中输出 void defaultOutput(const char* msg,int len) { size_t n=fwrite(msg,1,len,stdout); //FIXME check n (void)n; } //清空stdout缓冲区 void defaultFlush() { fflush(stdout); } //全局变量:两个函数指针,分别指向日志输出操作函数和日志清空缓冲区操作函数 //如果用户不自己实现这两个函数,就用默认的output和flush函数,输出到stdout中去 logger::outputFunc g_output=defaultOutput; logger::flushFunc g_flush=defaultFlush; //全局变量:时区... timezone g_logTimeZone; }//namespace mymuduo using namespace mymuduo; //很重要的构造函数,除了类成员的初始化,还负责把各种信息写入到logstream中去 logger::Impl::Impl(logger::loglevel level,int savedErrno,const SourceFile& file,int line) :m_time(timestamp::now()),m_stream(),m_level(level), m_line(line),m_basename(file) { //写入时间信息,用我自己的 myformatTime(); //formatTime(); currentthread::tid(); //写入线程信息 m_stream << T(currentthread::tidString(), currentthread::tidStringLength()); //写入日志级别 m_stream << T(LogLevelName[level], 6); //错误码不为0可写入错误码信息 if (savedErrno != 0) { m_stream << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") "; } //全部都是写到logstream中去 } //把时间写入到logstream里面去,这个时区可能有问题,不过可以自己重新实现这个函数 //只需要把类似于 %4d%02d%02d %02d:%02d:%02d 这种格式的时间字符串写入到logstream中即可 void logger::Impl::formatTime() { int64_t microSecondsSinceEpoch = m_time.microSecSinceEpoch(); time_t seconds = static_cast (microSecondsSinceEpoch / timestamp::microSecInSec); int microseconds = static_cast<int>(microSecondsSinceEpoch % timestamp::microSecInSec); if (seconds != t_lastSecond) { t_lastSecond = seconds; struct tm tm_time; if (g_logTimeZone.valid()) { tm_time = g_logTimeZone.toLocalTime(seconds); } else { ::gmtime_r(&seconds, &tm_time); // FIXME TimeZone::fromUtcTime } int len = snprintf(t_time, sizeof(t_time), "%4d%02d%02d %02d:%02d:%02d", tm_time.tm_year + 1900, tm_time.tm_mon + 1, tm_time.tm_mday, tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec); assert(len == 17); (void)len; } if (g_logTimeZone.valid()) { Fmt us(".%06d ", microseconds); assert(us.length() == 8); m_stream << T(t_time, 17) << T(us.data(), 8); } else { Fmt us(".%06dZ ", microseconds); assert(us.length() == 9); m_stream << T(t_time, 17) << T(us.data(), 9); } } void logger::Impl::myformatTime() { m_time2=timestamp::now().toFormattedString(); m_stream< " "; } //表明一条日志写入logstream结束. void logger::Impl::finish() { m_stream<<" - "< ":"< '\n'; } logger::logger(SourceFile file,int line) :m_impl(INFO,0,file,line) { } logger::logger(SourceFile file,int line,loglevel level,const char* func) :m_impl(level,0,file,line) { m_impl.m_stream< " "; } logger::logger(SourceFile file, int line, loglevel level) : m_impl(level, 0, file, line) { } logger::logger(SourceFile file, int line, bool toAbort) : m_impl(toAbort?FATAL:ERROR, errno, file, line) { } logger::~logger() { m_impl.finish(); const logstream::Buffer& buf(stream().buffer()); //把剩下的数据output出去,output由用户自定义或者使用默认stdout g_output(buf.data(),buf.length()); if(m_impl.m_level==FATAL) { g_flush(); abort(); } } //设置日志级别 void logger::setLogLevel(logger::loglevel level) { g_loglevel = level; } //设置日志输出操作函数 void logger::setOutput(outputFunc out) { g_output = out; } //设置日志清空缓冲区操作函数 void logger::setFlush(flushFunc flush) { g_flush = flush; } //设置时区 void logger::setTimeZone(const timezone& tz) { g_logTimeZone = tz; }
测试:
#include"base/logging.h" #includeusing namespace std; namespace mymuduo{ namespace currentthread { void cacheTid() { } } } using namespace mymuduo; int main() { //测试日志类logger,logger在构造函数里面完成Impl的初始化,并把相关日志信息写入logstream //在析构时调用Impl.finish(),并且调用output函数将logstream中的日志信息写入到日志地,这里默认是stdout //通过创建临时变量mymuduo::logger来实现日志,日志信息>>logstream>>stdout; //设置日志级别 std::cout<<"构建临时对象:\n"; mymuduo::logger::setLogLevel(mymuduo::logger::WARN); if(mymuduo::logger::logLevel()<=mymuduo::logger::TRACE) mymuduo::logger(__FILE__,__LINE__,mymuduo::logger::TRACE).stream()<<"日志1"; if(mymuduo::logger::logLevel()<=mymuduo::logger::DEBUG) mymuduo::logger(__FILE__,__LINE__,mymuduo::logger::DEBUG).stream()<<"日志2"; if(mymuduo::logger::logLevel()<=mymuduo::logger::INFO) mymuduo::logger(__FILE__,__LINE__,mymuduo::logger::INFO).stream()<<"日志3"; if(mymuduo::logger::logLevel()<=mymuduo::logger::WARN) mymuduo::logger(__FILE__,__LINE__,mymuduo::logger::WARN).stream()<<"日志4"; if(mymuduo::logger::logLevel()<=mymuduo::logger::ERROR) mymuduo::logger(__FILE__,__LINE__,mymuduo::logger::ERROR).stream()<<"日志5"; mymuduo::logger::setLogLevel(mymuduo::logger::TRACE); std::cout<<"宏定义:\n"; //通过宏定义来实现日志 LOG_TRACE<<"日志1"; LOG_DEBUG<<"日志2"; LOG_INFO<<"日志3"; LOG_WARN<<"日志4"; LOG_ERROR<<"日志5"; std::cout<<"over...\n"; }
打印结果:
构建临时对象:
2020年08月24日 星期1 18:51:39.1598266299 WARN 日志4 - main.cpp:36
2020年08月24日 星期1 18:51:39.1598266299 ERROR 日志5 - main.cpp:39
宏定义:
2020年08月24日 星期1 18:51:39.1598266299 TRACE main 日志1 - main.cpp:44
2020年08月24日 星期1 18:51:39.1598266299 DEBUG main 日志2 - main.cpp:45
2020年08月24日 星期1 18:51:39.1598266299 INFO 日志3 - main.cpp:46
2020年08月24日 星期1 18:51:39.1598266299 WARN 日志4 - main.cpp:47
2020年08月24日 星期1 18:51:39.1598266299 ERROR 日志5 - main.cpp:48
over...
总的来说最重要的就是直到从日志创建再到日志持久化(写入文件/stdout)的流程。
日志的数据保存在 logger.m_impl.m_stream中,因此大部分基于日志的操作肯定都是跟这个成员变量有关。
以一条日志为例子
2020年08月24日 星期1 18:25:29.1598264729 WARN 日志4 - main.cpp:36
下面是具体的过程:
logger构造函数中调用
Impl构造函数:把日志头时间和级别即2020年08月24日 星期1 18:25:29.1598264729 WARN写入logstream中去
logger.stream()<<"日志信息",把具体的日志信息即日志体写入到logstream中去
logger析构函数中调用
Impl.finish();把日志尾,源文件名,行号信息写入logstream
outputFunc(),进行日志持久化处理,把日志信息写入到stdout(默认)或其他地方(用户可以自定义)
这样就完成了一条日志的保存。