整理以前的硬盘,发现下载过一份glog的代码,正好有空就顺便看了看。简单的整理一下源码的笔记:
glog是什么?
glog是一个同步的,支持多线程的,支持c98的log库。总体的流程分为写入,分发,落盘三个部分。
一个日志库的核心的逻辑,应该要考虑如下三个问题:
1)日志数据如何写入
2)写入以后如何被决定分分发到哪里
3)如何落到到磁盘上
另外几个问题,多线程的环境下如何保证线程安全,如何保证数据既可以不阻塞io,又可以及时的把数据落到磁盘上,不出现进程挂掉丢日志的情况。带着这几个问题,看一下glog是如何解决的。
-
日志数据如何写入?
先看代码#include <iostream> #include "glog/logging.h" using namespace std; int main() { google::InitGoogleLogging("test_log"); google::SetLogDestination(google::GLOG_INFO,"./test"); LOG(INFO) << "this is log"; LOG(WARNING) << "this is warnning"; LOG(ERROR) << "this is error"; LOG(FATAL) << "this is FATAL"; return 0; }
通过如下定义的宏展开:
// 提供了宏定义,对于记log的地方,提供了如下宏进行展开 LOG(INFO) << "this is log"; #define LOG(severity) COMPACT_GOOGLE_LOG_ ## severity.stream() // 通过宏展开之后,代码可以理解为变成如下: google::LogMessage infor_obj(__FILE__, __LINE__); infor_obj.stream() << "this is infor glog message";
通过利用宏,会创建一个临时对象。利用创建的临时对象,获得它的stream,利用stream的<<运算符,把数据写入buff中。
-
日志如何分发,什么时机进行落盘?
把日志写入buff之后,后面的问题就是日志被如何分发?比如是写到标准输出中,还是文件中,还是写入到网络中。以及写入的时机是什么?如果太频繁,是否会频繁io 降低性能;如果太不频繁,是否会出现服务挂掉的时候,关键的日志打不出来。带着这些问题,看了一下glog是如何解决的?
在临时对象析构的时候,会调动flush函数。进行日志分发,落盘的工作。Flush中的核心逻辑是,调用send_method方法,对于普通的日志输出,send_method_ = &LogMessage::SendToLog 。这样利用对象析构就实现了日志的落盘。
{ MutexLock l(&log_mutex); (this->*(data_->send_method_))(); ++num_messages_[static_cast<int>(data_->severity_)]; } LogDestination::WaitForSinks(data_);
LogMessage::SendToLog核心的逻辑如下:
void LogMessage::SendToLog() EXCLUSIVE_LOCKS_REQUIRED(log_mutex) { static bool already_warned_before_initgoogle = false; log_mutex.AssertHeld(); RAW_DCHECK(data_->num_chars_to_log_ > 0 && data_->message_text_[data_->num_chars_to_log_-1] == '\n', ""); // Messages of a given severity get logged to lower severity logs, too if (!already_warned_before_initgoogle && !IsGoogleLoggingInitialized()) { const char w[] = "WARNING: Logging before InitGoogleLogging() is " "written to STDERR\n"; WriteToStderr(w, strlen(w)); already_warned_before_initgoogle = true; } // global flag: never log to file if set. Also -- don't log to a // file if we haven't parsed the command line flags to get the // program name. if (FLAGS_logtostderr || !IsGoogleLoggingInitialized()) { ColoredWriteToStderr(data_->severity_, data_->message_text_, data_->num_chars_to_log_); // this could be protected by a flag if necessary. LogDestination::LogToSinks(data_->severity_, data_->fullname_, data_->basename_, data_->line_, &data_->tm_time_, data_->message_text_ + data_->num_prefix_chars_, (data_->num_chars_to_log_ - data_->num_prefix_chars_ - 1)); } else { // log this message to all log files of severity <= severity_ LogDestination::LogToAllLogfiles(data_->severity_, data_->timestamp_, data_->message_text_, data_->num_chars_to_log_); LogDestination::MaybeLogToStderr(data_->severity_, data_->message_text_, data_->num_chars_to_log_); LogDestination::MaybeLogToEmail(data_->severity_, data_->message_text_, data_->num_chars_to_log_); LogDestination::LogToSinks(data_->severity_, data_->fullname_, data_->basename_, data_->line_, &data_->tm_time_, data_->message_text_ + data_->num_prefix_chars_, (data_->num_chars_to_log_ - data_->num_prefix_chars_ - 1)); // NOTE: -1 removes trailing \n } // ... 省略一下无关代码 }
glog的解决方案是:
对象析构的时候,触发日志分发的逻辑。分发的时候,首先判断一下,是否注册了日志分发的目的地。如果没有的话,就走标准输出,写到终端。如果注册了的话,就写到对应的地方。这里glog在这里提供了相应的接口的回调函数,支持写入到文件,邮件,最后的一个接口应该是可以自定义里面的实现。
如果写入的日志等级是FATAL等级的话,那么立刻开始落盘,进行flush。这里有一个特殊的地方,如果写入的是FATAL级别的log,会主动让进程结束。void LogMessage::SendToLog() EXCLUSIVE_LOCKS_REQUIRED(log_mutex) { // ... 省略部分无关代码 // If we log a FATAL message, flush all the log destinations, then toss // a signal for others to catch. We leave the logs in a state that // someone else can use them (as long as they flush afterwards) if (data_->severity_ == GLOG_FATAL && exit_on_dfatal) { if (data_->first_fatal_) { // Store crash information so that it is accessible from within signal // handlers that may be invoked later. RecordCrashReason(&crash_reason); SetCrashReason(&crash_reason); // Store shortened fatal message for other logs and GWQ status const int copy = min<int>(data_->num_chars_to_log_, sizeof(fatal_message)-1); memcpy(fatal_message, data_->message_text_, copy); fatal_message[copy] = '\0'; fatal_time = data_->timestamp_; } if (!FLAGS_logtostderr) { for (int i = 0; i < NUM_SEVERITIES; ++i) { if ( LogDestination::log_destinations_[i] ) LogDestination::log_destinations_[i]->logger_->Write(true, 0, "", 0); } } // release the lock that our caller (directly or indirectly) // LogMessage::~LogMessage() grabbed so that signal handlers // can use the logging facility. Alternately, we could add // an entire unsafe logging interface to bypass locking // for signal handlers but this seems simpler. log_mutex.Unlock(); LogDestination::WaitForSinks(data_); const char* message = "*** Check failure stack trace: ***\n"; if (write(STDERR_FILENO, message, strlen(message)) < 0) { // Ignore errors. } Fail(); }
-
日志的落盘:
inline void LogDestination::MaybeLogToLogfile(LogSeverity severity,
time_t timestamp,
const char* message,
size_t len) {
//判断是立即flush还是先缓存,logbuflevel默认值=0,
//各日志级别的定义:const int GLOG_INFO = 0, GLOG_WARNING = 1, GLOG_ERROR = 2, GLOG_FATAL = 3
//可以看到默认只会对INFO级别缓存,should_flush = false
const bool should_flush = severity > FLAGS_logbuflevel;
//从log_destinations_数组获取到该级别对应的LogDestination*
LogDestination* destination = log_destination(severity);
//完成日志的写入
destination->logger_->Write(should_flush, timestamp, message, len);
}
分发的函数是在创建临时对象的时候注册的,对应的逻辑如下。如果要自己实现分发的逻辑的话,魔改这个地方应该也是可以的。
![](https://img.haomeiwen.com/i4717565/0388fa07198ff8a0.png)
按照源码的实现,如果日志的等级比较低的话,还是会出现丢日志的情况。写段代码测试一下:
出现一个访问空指针的情况,判断是否进程挂之前是否可以打印前的关键log。
if (true)
{
int * p = NULL;
LOG(INFO) << "before visit null pointer";
int val = *p;
LOG(INFO) << "after visit null pointer";
}
if (true)
{
int * p = NULL;
LOG(ERROR) << "before visit null pointer"; // 调整为error等级
int val = *p;
LOG(INFO) << "after visit null pointer";
}
跑一下发现 infor级别的日志,在进程挂的时候,并不可以打印出来要死之前最关键的一条日志。但是,error的级别可以打印出来。如果关键的log,最好要更高的级别去打印。没有答应出来的原因在这行代码:
![](https://img.haomeiwen.com/i4717565/145f0cc3377cebc5.png)
gdb上去看了一下,应该是这里。
(gdb) bt
#0 MaybeLogToLogfile (len=67, message=0x603174 "I0506 17:14:20.299226 30100 test.cpp:30] before visit null pointer\n",
timestamp=1620292460, severity=0) at src/logging.cc:762
#1 LogToAllLogfiles (len=<optimized out>,
message=0x603174 "I0506 17:14:20.299226 30100 test.cpp:30] before visit null pointer\n", timestamp=<optimized out>,
severity=<optimized out>) at src/logging.cc:775
#2 google::LogMessage::SendToLog (this=0x7fffffffda90) at src/logging.cc:1441
---Type <return> to continue, or q <return> to quit---
#3 0x00007ffff7bb6813 in google::LogMessage::Flush (this=this@entry=0x7fffffffda90) at src/logging.cc:1362
#4 0x00007ffff7bb6a19 in google::LogMessage::~LogMessage (this=0x7fffffffda90, __in_chrg=<optimized out>)
at src/logging.cc:1311
#5 0x0000000000400c07 in main () at test.cpp:30
(gdb) p should_flush
$5 = false
- 多线程环境如何保证日志记录正常,不会被写乱?
glog的实现方案比较简单,只有一个单线程,通过加锁在解决。基本的流程是如下,似乎没有太复杂的地方。不过缺点是 io的时候,是会阻塞当前线程。
lock();
dosomething();
fwrite();
unlock();
小结一下,glog的日志写入流程如下图所示:
![](https://img.haomeiwen.com/i4717565/d168422733b4f045.png)
其余一些印象深刻的地方:
-
在日志写入的时候,继承了一个LogStreamBuf的类,来继承 << 运算符,实现字符串写入。
infor_obj.stream() << "this is infor glog message";
这个感觉比较奇怪,继承了一大堆东西,看起来就是为了重载运算符。个人觉得是不是自己写一个函数做这一点会不会更好?源码里面也没有给解释。
-
利用对象的析构函数实现日志的分发和落盘,非常简洁,respect。
-
如果获得函数的调用栈
https://izualzhy.cn/glog-source-how-to-get-stack-trace -
如何获得时间
glog就整理到这里,看glog的时候,顺便也看了一下muduo里面实现的日志库。先挖个坑,后面慢慢补上。
网友评论