muduo原始碼解析11-logger類

WoodInEast發表於2020-08-24

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<<" - "<<m_basename<<":"<<m_line<<'\n';
        void finish();

        timestamp m_time;
        logstream m_stream;
        LogLevel m_level;
        int m_line;
        SourceFile m_basename;
    };

這個Impl類是logger的核心,它內部的m_stream資料成員儲存具體的日誌資訊,可以說這個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<errno.h>
#include<stdio.h>
#include<string.h>

#include<sstream>

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<time_t>(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<<m_time2<<" ";
}
//表明一條日誌寫入logstream結束.
void logger::Impl::finish()
{
    m_stream<<" - "<<m_basename<<":"<<m_line<<'\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<<func<<" ";
}

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"
#include<iostream>
using 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(預設)或其他地方(使用者可以自定義)

 

這樣就完成了一條日誌的儲存。

 

相關文章