目录
前言
日志存储的实现
日志输出的实现
总结
muduo中的日志,是诊断日志。用于将代码运行时的重要信息进行保存,方便故障诊断和追踪。
日志一般有两种,一种是同步日志,一种是异步日志,同步日志就是当需要写出一条日志信息的时候,只有等到这条日志消息完全写出之后才能执行后续的程序,可见,这种方式的日志的问题就在于程序可能会阻塞在磁盘写入操作上;
而另一种异步日志则不会,异步日志的思路是需要写日志消息的时候只是将日志消息进行存储,当积累到一定量或者到达一定时间间隔时,由后台线程自动将存储的所有日志进行输出,可见,对于异步日志来说,每次有日志消息产生的时候,只需要一个存储的行为即可,存储结束就可以执行后面的业务代码了,而真正写入到磁盘的操作,是由后台线程进行的,这样做的好处就是:前台线程不会阻塞在写日志上,后台线程真正写出日志时,日志消息往往已经积累了很多,此时只需要调用一次IO函数,如fwrite,而不需要每条消息都调用一个IO函数,如此减少了IO函数调用次数,提高了效率。
而muduo中实现的,就是异步日志。
muduo中的日志消息格式为:时间 线程id 日志级别 日志正文 源文件名及行号。
日志设置了6种级别:TRACE、DEBUG、INFO、WARN、ERROR和FATAL。
日志的输出形式为流形式:日志级别< 如上所述,异步日志的实现关键,是先将日志消息进行存储,然后由后台线程进行写出。muduo将“日志存储”和“日志输出”分为了两大块,现在先来看看日志的存储部分。 前面说过,日志的输出形式为流形式,但muduo并没有使用C++自带的iostream等流库,而是使用自己实现的LogStream,用简短的代码量实现需要的功能,抛去其它不需要的冗杂的功能,使得代码效率更高。 实现”日志流“的基础,还需要一个内置的缓冲区,muduo通过FixedBuffer类来实现,如下所示: FixedBuffer是一个模板类,模板参数用来指定缓冲区data_的大小,并提供一系列缓冲区操作函数。 此时再来看LogStream类,LogStream重载了多个版本的"<<"运算符,如下所示: 可以看到,LogStream类通过重载”<<“来实现”日志流“的形式,不管是哪一种重载版本,最终都是会调用append函数,而append函数中又会去调用LogStream类中buffer_的append函数,可以看到,这里的buffer_实际上就是一个FixedBuffer的对象,也就相当于是一个缓冲区,其大小由kSmallBuffer指定,为4K字节。 通过上面的分析,可以知道”日志流“是通过LogStream来实现的,而”日志流“中的缓冲区则是由FixedBuffer来实现。 至此也就明白了每条日志是如何通过"<<"来进行存储的,但是光是存储是不行的,存储只是一个收集日志消息的作用,不可能把大量的日志都放在内存中,最终还是得将收集到的所有日志输出到某一个指定的地方去,因此接着来分析日志的输出。 显然,作为一个日志输出的类,其中应包含LogStream的一个实例用来存储日志消息,这一点体现在Impl类中,如下所示: Impl的5个成员,分别对应了日志消息生成的时间、存储的日志消息、日志消息级别、日志消息所在的文件名(注意,这个文件名指的是产生日志的线程所在的文件名)及其行数,这几乎构成了在本文最初所说的muduo日志消息格式中的大部分要素。而对于Impl的构造函数,则需要指定这些信息,来看下Impl类唯一的构造函数以及finish函数: 可以看到,在构造函数中,就会将当前时间、当前线程id以及日志级别写入到缓冲区stream_中,如果构造函数的第二个参数不为0,那么还会把系统错误errno对应的错误描述也添加到缓冲区stream_中。而在finish函数中,则会将文件名和行号写入缓冲区。至此可以看到,通过Impl类可以将日志格式中除日志正文外的所有要素都写入缓冲区stream_中,那么,日志正文又是何时写入呢? 完整的日志输出,是通过Logger类来实现的,其定义如下所示: 可以看到,在Logger类中,存在一个Impl类的实例,Logger类的多种构造函数实际上也是用构造参数去构造了impl,因此,日志消息本身实际上是存在于impl_中的,如前所述,它在构造时会向impl_的缓冲区stream_中写入时间、线程id和日志级别,而在Logger析构时,又会调用finish函数,向impl_的缓冲区stream_中写入文件名和行号。因此,如果想要写入日志正文,实际上也是对impl_的缓冲区stream_进行写入,如通过Logger实例执行stream()<<"log message";那么在该Logger实例析构时,就会将时间+线程id+日志级别+"log message"+文件名+行号写到Logger中impl_成员的缓冲区中。 当然,Logger调用析构函数时,日志才算是按照格式完整写好,此时就需要立刻将这条日志”写出到某个指定的地方“来完成日志的输出功能。实际的日志输出,是通过一个全局函数指针g_output实现的,当日志写完后,Logger的析构函数就会调用g_output所指向的函数进行日志输出,g_output可由setOutput函数进行设置,也提供默认的输出函数,如下所示: 可见,默认的输出函数就是将日志消息写到到stdout即标准输出中,在未重定向stdout的情况下,会将这条日志消息打印到终端屏幕上。 至此,也就完成了日志的输出。 根据Logger类也可以知道,每一条日志消息应当都对应一个Logger类的实例,实例构造和析构时会自动写入一些必备的消息元素,并在析构时自动将该日志消息通过g_output进行输出。 muduo中每一种日志级别都对应了一条宏定义,如下所示: 这些宏定义,最终都是对应impl_成员的缓冲区,只不过用来构造impl_的日志级别参数有所不同。这也就相当于构造了不同的临时缓冲区对象,如调用LOG_Info<<"log info";就会构造一个临时的Logger对象,在该条语句执行结束后就会析构,这样就可以通过一条语句实现日志消息的写入和输出,最终将 时间+线程id+日志级别+"log message"+文件名+行号 打印到终端屏幕上。 每一条日志消息都会对应一个Logger的临时对象,这样的好处是可以及时的将日志消息进行输出,占用的内存也可以立即释放。 至此,分析了muduo中日志消息的存储以及输出,不过还未涉及到异步的实现,将在下文中分析。 日志的存储主要涉及到两个类: FixedBuffer用于实现一个指定大小的缓冲区,是每条日志消息的实际存储位置; LogSteam中含有FixedBuffer的一个实例,其大小默认为4K,因此LogStream也通过FixedBuffer实现了日志消息的存储,除此之外,LogStream类还重载了一系列的"<<",由此来实现”日志流“,从而可以流的形式来写入日志消息; 日志的输出也主要涉及到两个类: Impl类包含了一个LogStream实例,用于存储日志消息,Impl类在构造和调用finish函数时会向LogStream中写入时间等信息; Logger类中包含了一个Impl类实例,可以通过stream()函数来获取该实例下的LogStream,从而向其中写入日志正文。在Logger析构时调用g_output所指向的函数,将缓冲区中的日志消息全部输出。 每条日志消息对应了一个Logger的临时对象,每条日志消息对应的缓冲区大小默认为4K。日志存储的实现
/*LogStream.h*/
template
/*LogStream.h*/
class LogStream : noncopyable
{
typedef LogStream self;
public:
typedef detail::FixedBuffer
日志输出的实现
class Impl
{
public:
typedef Logger::LogLevel LogLevel;
Impl(LogLevel level, int old_errno, const SourceFile& file, int line);//需要用日志级别、错误信息、文件名和行号构造
void formatTime(); //获取当前的时间到time_中
void finish();
Timestamp time_; //时间
LogStream stream_; //自带一个4K大小缓冲区
LogLevel level_; //日志级别
int line_; //行数
SourceFile basename_; //文件名
};
Impl::Impl(LogLevel level, int savedErrno, const SourceFile& file, int line)
: time_(Timestamp::now()),
stream_(),
level_(level),
line_(line),
basename_(file)
{
formatTime();//根据time_获取当前时间,写入缓冲区
CurrentThread::tid();
stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength());//向缓冲区中写入线程id
stream_ << T(LogLevelName[level], 6);//向缓冲区中写入日志级别
if (savedErrno != 0)
{
stream_ << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") ";
}
}
void Impl::finish()//向缓冲区中写入文件名和行号
{
stream_ << " - " << basename_ << ':' << line_ << '\n';
}
class Logger
{
public:
enum LogLevel //几种日志级别
{
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
NUM_LOG_LEVELS,
};
// compile time calculation of basename of source file
...
//用构造函数的参数去初始化impl类型成员,即初始化缓冲区
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();
LogStream& stream() { return impl_.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);
private:
Impl impl_;//构造Logger类时就会构造impl_,此时缓冲区中就会自动写入当前线程id和日志级别
};
Logger::~Logger()
{
impl_.finish();//析构时向缓冲区写入文件名和行号
const LogStream::Buffer& buf(stream().buffer());//获取当前缓冲区的所有数据
g_output(buf.data(), buf.length());//调用输出函数
...
}
void defaultOutput(const char* msg, int len)
{
size_t n = fwrite(msg, 1, len, stdout);
//FIXME check n
(void)n;
}
#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
muduo::Logger(__FILE__, __LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()
总结