版權聲明:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/breaksoftware/article/details/51363353 </div><link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-f57960eb32.css"><link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-f57960eb32.css"><div class="htmledit_views" id="content_views"><p> GLog是Google開發的一套日志輸出框架。由于其具有功能強大、方便使用等特性,它被眾多開源項目使用。本文將通過分析其源碼,解析Glog實現的過程。</p>
? ? ? ? 該框架的源碼在https://github.com/google/glog上可以獲取到。本文將以目前最新的0.3.3版本源碼為范例進行分析。(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 首先我們介紹下Glog簡單的使用方法。Glog暴露了很多方法供使用者調用,一般它們都是通過宏的形式提供的。這些方法的功能包括:參數設置、判斷、日志輸出、自定義行為。基于重要性的考慮,我并不準備在本文中介紹參數設置和判斷兩個特性的函數。而將重點放在Glog的核心——日志輸出和自定義行為上。而且在日志輸出環節,我們將重點關注于文件形式的輸出。下文中很多未加設定的場景一般都是以文件輸出為例說明的。
? ? ? ? 我們以一個簡單的例子作為開始
- int _tmain(int argc, _TCHAR* argv[])
- {
- google::InitGoogleLogging(argv[0]);
- FLAGS_log_dir = "D:\\Dev\\glog-0.3.3\\glog-0.3.3\\Debug\\log";
- LOG(INFO) << "INFO";
- LOG(INFO) << "INFO1";
- LOG(WARNING) << "WARNING";
- LOG(WARNING) << "WARNING1";
- LOG(ERROR) << "ERROR";
- LOG(ERROR) << "ERROR1";
- LOG(FATAL) << "FATAL";
- google::ShutdownGoogleLogging();
- return 0;
- }
? ? ? ? 第3和12行是對稱的,用于初始化和關閉Glog系統。這兩個過程的實現也非常簡單,其比較有意義的是ShutdownGoogleLogging中實現了對過程中新建對象的清理。即調用了
- static LogDestination* log_destinations_[NUM_SEVERITIES];
- void LogDestination::DeleteLogDestinations() {
- for (int severity = 0; severity < NUM_SEVERITIES; ++severity) {
- delete log_destinations_[severity];
- log_destinations_[severity] = NULL;
- }
- }
? ? ? ??LogDestination是是GLog非常核心的一個類。按英文翻譯過來,它就是“日志目標”類。正如它名稱,我們對日志的輸出最終將落到該類上去執行。之后我們將一直和它打交道。
Glog是什么意思?? ? ? ? 上面main函數的04行對Glog的全局變量重新賦值,它用于標記日志文件的生成路徑。這類全局變量在logging.h中暴露了很多,它們很多是以DECLARE_bool、DECLARE_int32或DECLARE_string等宏聲明的。這些就是我前文所述的“參數”。我們先只要知道FLAGS_log_dir的作用就行了。
? ? ? ? 05到11行向GLog系統中輸出了4中類型的日志,即INFO、WARNING、ERROR和FATAL。GLog框架一共提供了上述四種類型的日志,這些日志將被分別輸出到四個文件中。如本例我們將生成如下四個文件:
glog_test.exe.computername.username.log.ERROR.20160510-153013.9704
glog_test.exe.computername.username.log.FATAL.20160510-153013.9704
glog_test.exe.computername.username.log.INFO.20160510-153013.9704
glog_test.exe.computername.username.log.WARNING.20160510-153013.9704
? ? ? ? 其中computername是設備的ID,username是登錄用戶的名稱。
? ? ? ? 這種設計是非常有意義的。在我們開發過程中,我們可以通過INFO類型的日志進行過程分析。在自測階段,我們可能更多要關注于是否存在WARNING類型的日志。而上線后,可能會出現一些我們認為不合法或者異常的場景,則這個時候就要關注ERROR和FATAL類型的日志了。這四種日志并不是相互獨立的,而是存在包含關系。按照重要性,INFO日志文件將包含INFO、WARNING、ERROR和FATAL日志,因為在開發過程中我們需要關注所有信息。WARNING日志文件將包含WARNING、ERROR和FATAL日志。ERROR日志將包含ERROR和FATAL日志。FATAL日志文件里只有FATAL類型的日志。這種層級關系將非常有助于我們在不同時期關注不同性質的問題。我們看下INFO日志文件的內容
Log file created at: 2016/05/10 15:30:13
Running on machine: COMPUTERNAME
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0510 15:30:13.564411 11392 glog_test.cpp:11] INFO
I0510 15:30:13.566411 11392 glog_test.cpp:12] INFO1
W0510 15:30:13.566411 11392 glog_test.cpp:13] WARNING
W0510 15:30:13.566411 11392 glog_test.cpp:14] WARNING1
E0510 15:30:13.567411 11392 glog_test.cpp:15] ERROR
E0510 15:30:13.567411 11392 glog_test.cpp:16] ERROR1
F0510 15:30:13.568413 11392 glog_test.cpp:17] FATAL
? ? ? ? 眼尖的同學可能發現INFO、ERROR和WARNING日志都有兩條,而FATAL日志只有一條。這個地方引出FATAL類型日志的使用場景問題。一般情況下,如果出現程序已經無法執行的場景才使用FATAL日志用于記錄臨死之前的事情。所以如果我們在項目中發現日志中出現一連串的FATAL日志,往往是對Glog的錯誤使用。
? ? ? ? Glog的基本使用我們講完了,我們開始進行源碼的講解。
? ? ? ? 我們先看下LOG宏的定義
#define LOG(severity) COMPACT_GOOGLE_LOG_ ## severity.stream()
? ? ? ? 可以見得,LOG宏的等級參數和COMPACT_GOOGLE_LOG_聯合在一塊組成一個宏。比如INFO則對應于COMPACT_GOOGLE_LOG_INFO;FATAL對應于COMPACT_GOOGLE_LOG_FATAL。這些宏最終都將對應于一個google::LogMessage對象,比如
- #define LOG_TO_STRING_ERROR(message) google::LogMessage( \
- __FILE__, __LINE__, google::GLOG_ERROR, message)
? ? ? ? 通過對不同日志類型的LogMessage構造對象對比說明,它們是通過LogMessage的第三個參數進行區分的,上例中ERROR日志的等級就是google::GLOG_ERROR。在log_severity.h文件中,定義了GLog四個等級對應的值。
- const int GLOG_INFO = 0, GLOG_WARNING = 1, GLOG_ERROR = 2, GLOG_FATAL = 3,
- NUM_SEVERITIES = 4;
? ? ? ? 這四個值并非隨便定義的。它們這樣設計,將有助于完成之前所說的日志包含輸出問題。在將日志輸出到文件的函數LogDestination::LogToAllLogfiles中
- for (int i = severity; i >= 0; --i)
- LogDestination::MaybeLogToLogfile(i, timestamp, message, len);
- }
? ? ? ? 這段簡單的for循環,讓日志輸出到自己以及低于自己等級的日志文件中。
? ? ? ? 在現實使用中,我們往往會通過一個臨時變量或者宏,來區分開發環境和線上環境。比如開發環境我們需要INFO級別的日志,而線上環境我們需要ERROR及其以上等級的日志。當我們測試完畢,準備上線前,我們不可能將輸出INFO類型日志的語句刪掉——因為可能很多、很分散且刪除代碼是不安全的。那么我們就需要使用一種手段讓INFO和WARNING等級的日志失效。GLog是這么做的
- #if GOOGLE_STRIP_LOG == 0
- #define COMPACT_GOOGLE_LOG_INFO google::LogMessage( \
- __FILE__, __LINE__)
- #define LOG_TO_STRING_INFO(message) google::LogMessage( \
- __FILE__, __LINE__, google::GLOG_INFO, message)
- #else
- #define COMPACT_GOOGLE_LOG_INFO google::NullStream()
- #define LOG_TO_STRING_INFO(message) google::NullStream()
- #endif
-
- #if GOOGLE_STRIP_LOG <= 1
- #define COMPACT_GOOGLE_LOG_WARNING google::LogMessage( \
- __FILE__, __LINE__, google::GLOG_WARNING)
- #define LOG_TO_STRING_WARNING(message) google::LogMessage( \
- __FILE__, __LINE__, google::GLOG_WARNING, message)
- #else
- #define COMPACT_GOOGLE_LOG_WARNING google::NullStream()
- #define LOG_TO_STRING_WARNING(message) google::NullStream()
- #endif
-
- #if GOOGLE_STRIP_LOG <= 2
- #define COMPACT_GOOGLE_LOG_ERROR google::LogMessage( \
- __FILE__, __LINE__, google::GLOG_ERROR)
- #define LOG_TO_STRING_ERROR(message) google::LogMessage( \
- __FILE__, __LINE__, google::GLOG_ERROR, message)
- #else
- #define COMPACT_GOOGLE_LOG_ERROR google::NullStream()
- #define LOG_TO_STRING_ERROR(message) google::NullStream()
- #endif
-
- #if GOOGLE_STRIP_LOG <= 3
- #define COMPACT_GOOGLE_LOG_FATAL google::LogMessageFatal( \
- __FILE__, __LINE__)
- #define LOG_TO_STRING_FATAL(message) google::LogMessage( \
- __FILE__, __LINE__, google::GLOG_FATAL, message)
- #else
- #define COMPACT_GOOGLE_LOG_FATAL google::NullStreamFatal()
- #define LOG_TO_STRING_FATAL(message) google::NullStreamFatal()
- #endif
? ? ? ? 我們可以通過定義GOOGLE_STRIP_LOG的值,來控制相應宏的定義。比如我們給GOOGLE_STRIP_LOG賦值2,則COMPACT_GOOGLE_LOG_WARNING會被定義為google::NullStream(),從而不會進行輸出。ERROR和FATAL都將定義為LogMessage的臨時對象進行之后的業務邏輯。
? ? ? ? 以一個常用的LogMessage的構造函數來看臨時對象的初始化
- LogMessage::LogMessage(const char* file, int line, LogSeverity severity)
- : allocated_(NULL) {
- Init(file, line, severity, &LogMessage::SendToLog);
- }
? ? ? ? 該函數中傳入了執行語句所在的文件名和行號,這些我們都將在輸出的日志中見到過。第三個參數是日志等級,即GLOG_INFO等值。Init傳入的第四個參數是該類的一個成員函數地址。它才是我們關注的重點。
? ? ? ??LogMessage::SendToLog函數主要執行的是下面這段代碼
- // 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));
? ? ? ? 這段代碼分別是:嘗試向文件中輸出、嘗試向標準錯誤流中輸出、嘗試發送郵件、嘗試向用戶自定義池中輸出。
? ? ? ? 嘗試發送郵件的方式我們很少使用到,它實際是借用了linux系統上/bin/mail程序去發送郵件,所以對這塊有興趣的同學,可以主要關注下郵件內容的組裝和發送命令的使用。
?? ? ? ?LogToAllLogfiles函數不僅具有向文件輸出的能力,還有向標準錯誤流中輸出的能力。
- inline void LogDestination::LogToAllLogfiles(LogSeverity severity,
- time_t timestamp,
- const char* message,
- size_t len) {
-
- if ( FLAGS_logtostderr ) { // global flag: never log to file
- ColoredWriteToStderr(severity, message, len);
- } else {
- for (int i = severity; i >= 0; --i)
- LogDestination::MaybeLogToLogfile(i, timestamp, message, len);
- }
- }
? ? ? ? else中的for循環就是之前我們講解的日志包含的實現。
? ? ? ??MaybeLogToLogfile方法將讓一個全局的LogDestination對象執行日志輸出邏輯
- inline void LogDestination::MaybeLogToLogfile(LogSeverity severity,
- time_t timestamp,
- const char* message,
- size_t len) {
- const bool should_flush = severity > FLAGS_logbuflevel;
- LogDestination* destination = log_destination(severity);
- destination->logger_->Write(should_flush, timestamp, message, len);
- }
? ? ? ??log_destination方法將通過日志等級新建并返回或者獲取一個保存在全局區域中LogDestination指針
- LogDestination* LogDestination::log_destinations_[NUM_SEVERITIES];
-
- inline LogDestination* LogDestination::log_destination(LogSeverity severity) {
- assert(severity >=0 && severity < NUM_SEVERITIES);
- if (!log_destinations_[severity]) {
- log_destinations_[severity] = new LogDestination(severity, NULL);
- }
- return log_destinations_[severity];
- }
? ? ? ? 如此可見,我們的測試代碼將新建四個LogDestination對象,并將指針保存在全局數組log_destinations_中。這四個指針分別對應于INFO、WARNING、ERROR和FATAL日志的輸出目標對象。所以,不管在多線程還是在單線程環境中,我們日志輸出都將調用到這四個對象指針。
? ? ? ?LogDestination類有個用于實際輸出日志的成員變量
- LogFileObject fileobject_;
- base::Logger* logger_; // Either &fileobject_, or wrapper around it
? ? ? ??Logger是個接口類,其暴露出來的方法都是純虛。所以實際操作的是其繼承類。LogFileObject繼承于Logger接口,對于文件類型的輸出,logger_指向fileobject_對象。
- LogDestination::LogDestination(LogSeverity severity,
- const char* base_filename)
- : fileobject_(severity, base_filename),
- logger_(&fileobject_) {
- }
? ? ? ??LogFileObject的Write的實現非常簡單,除了在文件不存在的情況下新建了日志文件,還有就是日志個格式化輸出。它的整個邏輯是在鎖內部完成的,這樣可以保證多線程寫操作是安全的。
? ? ? ? 我們分析完LogToAllLogfiles的實現,再探索下它是在何處被調用的。之前我們講過LOG宏構建了一個LogMessage臨時對象。這個臨時對象的生命周期就是C++語句中其所在的那一行的執行周期。
- LOG(ERROR) << "ERROR";
- LOG(ERROR) << "ERROR1";
? ? ? ? 上述兩行,就構建了兩個LogMessage臨時對象。第一行的臨時對象在第二行執行之前就消亡了,第二行的臨時對象在之后一行執行之前就消亡了。而對LogToAllLogfiles的調用就是在LogMessage的析構函數中(實際是Flush中)。
- LogMessage::~LogMessage() {
- Flush();
- delete allocated_;
- }
? ? ? ? Flush中的核心代碼是
- {
- MutexLock l(&log_mutex);
- (this->*(data_->send_method_))();
- ++num_messages_[static_cast<int>(data_->severity_)];
- }
- LogDestination::WaitForSinks(data_);
? ? ? ? 注意在調用send_method_所指向的函數(保存為文件時默認的就是LogToAllLogfiles方法)之前上了鎖。這個鎖非常必要,可以保證之后的操作是受到保護的。否則之前在全局區域保存LogDestination對象指針的邏輯就存在多線程訪問的問題。
? ? ? ? 我們可以總結下,每條日志的輸出都通過一個LogMessage臨時對象的析構,傳遞到全局變量log_destinations_中相應等級對應的LogDestination指針所指向的對象。那么LogDestination對象又是在何時進行日志寫入文件的呢?我們知道文件的Write方法并不一定馬上把內容寫入文件,而是存在一定的緩存中,再根據系統的判斷或者用戶主動調用fflush,將數據實際寫入文件。LogFileObject提供了兩個Flush方法
- void LogFileObject::Flush() {
- MutexLock l(&lock_);
- FlushUnlocked();
- }
-
- void LogFileObject::FlushUnlocked(){
- if (file_ != NULL) {
- fflush(file_);
- bytes_since_flush_ = 0;
- }
- // Figure out when we are due for another flush.
- const int64 next = (FLAGS_logbufsecs
- * static_cast<int64>(1000000)); // in usec
- next_flush_time_ = CycleClock_Now() + UsecToCycles(next);
- }
? ? ? ? 一個是通過鎖保證多線程安全的版本,一個是不安全的版本。它們都是執行了fflush,并計算了下一次flush時間。在LogFileObject的Write方法末尾,通過該時間的判斷,確定是否真的將緩存寫入文件中。
- // See important msgs *now*. Also, flush logs at least every 10^6 chars,
- // or every "FLAGS_logbufsecs" seconds.
- if ( force_flush ||
- (bytes_since_flush_ >= 1000000) ||
- (CycleClock_Now() >= next_flush_time_) ) {
- FlushUnlocked();
- #ifdef OS_LINUX
- if (FLAGS_drop_log_memory) {
- if (file_length_ >= logging::kPageSize) {
- // don't evict the most recent page
- uint32 len = file_length_ & ~(logging::kPageSize - 1);
- posix_fadvise(fileno(file_), 0, len, POSIX_FADV_DONTNEED);
- }
- }
- #endif
- }
? ? ? ? 還有一種強制寫入文件的方法,就是LogDestination::FlushLogFilesUnsafe和LogDestination::FlushLogFiles方法,它們的實現是
- inline void LogDestination::FlushLogFilesUnsafe(int min_severity) {
- // assume we have the log_mutex or we simply don't care
- // about it
- for (int i = min_severity; i < NUM_SEVERITIES; i++) {
- LogDestination* log = log_destination(i);
- if (log != NULL) {
- // Flush the base fileobject_ logger directly instead of going
- // through any wrappers to reduce chance of deadlock.
- log->fileobject_.FlushUnlocked();
- }
- }
- }
-
- inline void LogDestination::FlushLogFiles(int min_severity) {
- // Prevent any subtle race conditions by wrapping a mutex lock around
- // all this stuff.
- MutexLock l(&log_mutex);
- for (int i = min_severity; i < NUM_SEVERITIES; i++) {
- LogDestination* log = log_destination(i);
- if (log != NULL) {
- log->logger_->Flush();
- }
- }
- }
? ? ? ? 使用者可以通過這兩個函數人為主動的刷新緩沖區,讓內容落地。
? ? ? ? 以上,我們講解了GLog主要使用方法及其實現原理。實際GLog作為一個框架,也不失靈活性。
? ? ? ? 比如我們可以通過SetLogger方法修改不同等級的日志輸出方法。
- base::Logger* base::GetLogger(LogSeverity severity) {
- MutexLock l(&log_mutex);
- return LogDestination::log_destination(severity)->logger_;
- }
-
- void base::SetLogger(LogSeverity severity, base::Logger* logger) {
- MutexLock l(&log_mutex);
- LogDestination::log_destination(severity)->logger_ = logger;
- }
? ? ? ? 下面代碼是其使用的一個實例
- struct MyLogger : public base::Logger {
- string data;
-
- virtual void Write(bool /* should_flush */,
- time_t /* timestamp */,
- const char* message,
- int length) {
- data.append(message, length);
- }
-
- virtual void Flush() { }
-
- virtual uint32 LogSize() { return data.length(); }
- };
-
- static void TestWrapper() {
- fprintf(stderr, "==== Test log wrapper\n");
-
- MyLogger my_logger;
- base::Logger* old_logger = base::GetLogger(GLOG_INFO);
- base::SetLogger(GLOG_INFO, &my_logger);
- LOG(INFO) << "Send to wrapped logger";
- FlushLogFiles(GLOG_INFO);
- base::SetLogger(GLOG_INFO, old_logger);
-
- CHECK(strstr(my_logger.data.c_str(), "Send to wrapped logger") != NULL);
- }
? ? ? ? 我們還可以使用AddLogSink和RemoveLogSink方法自定義處理日志的邏輯。AddLogSink最終會調用到
- inline void LogDestination::AddLogSink(LogSink *destination) {
- // Prevent any subtle race conditions by wrapping a mutex lock around
- // all this stuff.
- MutexLock l(&sink_mutex_);
- if (!sinks_) sinks_ = new vector<LogSink*>;
- sinks_->push_back(destination);
- }
? ? ? ? 其中sinks_是全局vector指針。它是獨立于之前介紹的log_destinations_數組管理的日志輸出方式。
- // arbitrary global logging destinations.
- static vector<LogSink*>* sinks_;
? ? ? ? 最終它會在SendToLog方法中的LogToSinks中被調用。
- inline void LogDestination::LogToSinks(LogSeverity severity,
- const char *full_filename,
- const char *base_filename,
- int line,
- const struct ::tm* tm_time,
- const char* message,
- size_t message_len) {
- ReaderMutexLock l(&sink_mutex_);
- if (sinks_) {
- for (int i = sinks_->size() - 1; i >= 0; i--) {
- (*sinks_)[i]->send(severity, full_filename, base_filename,
- line, tm_time, message, message_len);
- }
- }
- }
? ? ? ? 作為使用者,我們需要定義send方法,并根據日志等級的不同,處理不同的邏輯。下面是GLog測試代碼中的一個例子
- virtual void send(LogSeverity severity, const char* /* full_filename */,
- const char* base_filename, int line,
- const struct tm* tm_time,
- const char* message, size_t message_len) {
- // Push it to Writer thread if we are the original logging thread.
- // Note: Something like ThreadLocalLogSink is a better choice
- // to do thread-specific LogSink logic for real.
- if (pthread_equal(tid_, pthread_self())) {
- writer_.Buffer(ToString(severity, base_filename, line,
- tm_time, message, message_len));
- }
- }
? ? ? ? 最后介紹LOG_TO_SINK和LOG_TO_SINK_BUT_NOT_TO_LOGFILE宏,它是設置單個Sink的方式(AddLogSink是設置一個到多個sink的方式)
- #define LOG_TO_SINK(sink, severity) \
- google::LogMessage( \
- __FILE__, __LINE__, \
- google::GLOG_ ## severity, \
- static_cast<google::LogSink*>(sink), true).stream()
- #define LOG_TO_SINK_BUT_NOT_TO_LOGFILE(sink, severity) \
- google::LogMessage( \
- __FILE__, __LINE__, \
- google::GLOG_ ## severity, \
- static_cast<google::LogSink*>(sink), false).stream()
? ? ? ? 它調用的是5個參數版本的LogMessage構造函數
- LogMessage::LogMessage(const char* file, int line, LogSeverity severity,
- LogSink* sink, bool also_send_to_log)
- : allocated_(NULL) {
- Init(file, line, severity, also_send_to_log ? &LogMessage::SendToSinkAndLog :
- &LogMessage::SendToSink);
- data_->sink_ = sink; // override Init()'s setting to NULL
- }
? ? ? ? 構造函數中true或false標識是否需要調用之前分析過的SendToLog()方法,還是只是調用SendToSink方法
- void LogMessage::SendToSink() EXCLUSIVE_LOCKS_REQUIRED(log_mutex) {
- if (data_->sink_ != NULL) {
- RAW_DCHECK(data_->num_chars_to_log_ > 0 &&
- data_->message_text_[data_->num_chars_to_log_-1] == '\n', "");
- data_->sink_->send(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));
- }
- }
? ? ? ? 至此,我們便將Glog的實現原理分析完畢了。
? ? ? ? 在閱讀代碼和實驗其使用過程中,可以發現GLog是一個非常優秀的日志開源庫。但是如果想讓GLog靈活的應用于產品中,其實還有很多事情可以做。比如我們可以將參數放到配置文件中,這樣不至于我們每次修改參數時要重新編譯代碼。再比如我們可以定制自己的Sink,將日志數據發送到指定機器。
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态