美文网首页
glog的源码分析

glog的源码分析

作者: DayDayUpppppp | 来源:发表于2021-05-06 21:13 被阅读0次

整理以前的硬盘,发现下载过一份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);
}

分发的函数是在创建临时对象的时候注册的,对应的逻辑如下。如果要自己实现分发的逻辑的话,魔改这个地方应该也是可以的。


按照源码的实现,如果日志的等级比较低的话,还是会出现丢日志的情况。写段代码测试一下:

出现一个访问空指针的情况,判断是否进程挂之前是否可以打印前的关键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,最好要更高的级别去打印。没有答应出来的原因在这行代码:


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的日志写入流程如下图所示:


网上找的一张非常好的图,图片来源coredump博客
其余一些印象深刻的地方:
  1. 在日志写入的时候,继承了一个LogStreamBuf的类,来继承 << 运算符,实现字符串写入。

    infor_obj.stream() << "this is infor glog message";
    

    这个感觉比较奇怪,继承了一大堆东西,看起来就是为了重载运算符。个人觉得是不是自己写一个函数做这一点会不会更好?源码里面也没有给解释。

  2. 利用对象的析构函数实现日志的分发和落盘,非常简洁,respect。

  3. 如果获得函数的调用栈
    https://izualzhy.cn/glog-source-how-to-get-stack-trace

  4. 如何获得时间

glog就整理到这里,看glog的时候,顺便也看了一下muduo里面实现的日志库。先挖个坑,后面慢慢补上。

相关文章

网友评论

      本文标题:glog的源码分析

      本文链接:https://www.haomeiwen.com/subject/pprwrltx.html