1. 程式人生 > >POCO C++庫學習和分析 -- 日誌 (一)

POCO C++庫學習和分析 -- 日誌 (一)

         日誌對於程式來說是非常重要的,特別是對一些大型程式而言。一旦程式被髮布,在現場日誌幾乎是程式設計師唯一可以獲取程式資訊的手段。Poco作為一個框架類庫,提供了非常多的日誌種類供程式設計師選用。文章將分兩個部分,對於Poco日誌進行介紹。第一部分主要以翻譯Poco文件為主,第二部分則探討Poco日誌的實現。

1. Poco庫日誌介面

1.1  總體介紹 

         Poco中的日誌模組主要涉及下列幾個部分。
          1. 訊息,日誌和通道
          2. 格式
          3. 執行效率的考量

          模組框架圖:

1.2  訊息(Message類):

          1. 所有的訊息都被儲存並通過類Poco::Message傳遞
          2. 一個訊息包括了下述特性:
             a. 優先順序
             b. 訊息源
             c. 訊息內容
             d. 時間戳
             e. 程序與執行緒標記
             f. 可選引數(名字-值)對

           訊息優先順序:
           Poco定義了8種訊息優先順序:
                PRIO_FATAL
                PRIO_CRITICAL
                PRIO_ERROR
                PRIO_WARNING
                PRIO_NOTICE
                PRIO_INFORMATION
                PRIO_DEBUG
                PRIO_TRACE
          可以通過函式設定和獲取訊息優先順序:
void
setPriority(Priority prio) Priority getPriority() const

           訊息源:
          訊息源用來描述日誌訊息的源。通常狀態下,使用Poco::Logger的名字來命名。因此應該合理的命名Poco::Logger的名字。
          可以通過函式設定和獲取訊息源:
void setSource(const std::string& source)
                const std::string& getSource() const

   
           訊息內容:

          在Poco中訊息內容是不考慮格式和長度等問題的,只是訊息內容。當訊息最終輸出時,訊息內容有可能被類Poco::formatter修改。
          可以通過函式設定和獲取訊息內容:
void setText(const std::string& text)
                const std::string& getText() const

           訊息時間戳:
          記錄訊息產生時的時間戳,精度為毫秒。
          可以通過函式設定和獲取時間戳:  
void setTime(const Timestamp& time)
                const Timestamp& getTime() const

           程序和執行緒識別符號:
          程序識別符號(PID)為長整形的int值,用來儲存系統的程序ID。
          執行緒識別符號(TID)同樣為長整形的int值,用於儲存當前執行緒的ID值。
          同樣的當前執行緒的名字也會被儲存。程序識別符號(PID)、執行緒識別符號(TID)、執行緒名在Poco::Message初始化時會自動生成。
          可以使用下列函式對程序識別符號(PID)、執行緒識別符號(TID)、執行緒名進行操作:
void setThread(const std::string& threadName)
             const std::string& getThread() const
             void setTid(long tid)
             long getTid() const
             void setPid(long pid)
             long getPid() const

           訊息引數:
          一個訊息可以儲存任意數目的name-value對 。
          name-value可以是任意字串。
          訊息引數可以被用於最終的格式輸出。
          訊息引數支援下標索引。

1.3 Logger類:

          應用程式可以使用Poco::Logger類去產生日誌訊息。每一個日誌物件內部都包含了一個通道物件(Channel),通道用於最終把訊息送到目的地。
          每一個logger物件都有名字,logger物件的名字會被用於命名所有由此物件產生的訊息的訊息源名稱。名字一旦被設定,將不能被改變。
          每一個Poco::Logge物件都有其自己的優先順序。有了優先順序後,Poco::Logge物件便可以對訊息進行過濾。只有訊息的優先順序比Poco::Logge物件的優先順序高,訊息才會被Poco::Logge物件所傳遞。

          Logger的繼承體系。
          1. 基於Logger的名字,可以形成日誌的樹狀繼承體系。
          2. 一個Logger物件的名字包含了一個或多個部分,不同部分之間使用'.'分隔。每個日誌元件的名稱都包含了上級日誌元件的名稱
          3. 存在一個特殊的Logger,即root Logger,其名字為空。它是所有Logger的根。
          4. 對於Logger繼承的深度Poco庫並沒有限制。

          下面是對於Logger繼承的一個說明:
          Logger Hierarchy Example
             |
             |---- "" (the root logger)
                |
                |-----"HTTPServer"
                    |
                    |-----"HTTPServer.RequestHandler"
                    |
                    |-----"HTTPServer.RequestHandler.File"
                    |
                    |-----"HTTPServer.RequestHandler.CGI"
                    |
                    |------"HTTPServer.Listener"

           說明:
         1. 一個新的logger將繼承它的上級日誌元件的級別和通道。比如說,上例中"HTTPServer.RequestHandler.CGI"會繼承"HTTPServer.RequestHandler"的日誌級別和通道。
          2. 一旦一個logger被完全建立,它就將與它的上級無關。完全建立指,logger擁有自己的channel和日誌級別,而不是和其它logger共用。換句話說,改變日誌級別和通道將不會影響的到其他的已經存在的logger物件。
          3. 儘可能的對日誌物件一次設定所有的引數,比如說日誌級別和通道。

           記錄訊息:
          1. void log(const Message& msg)
          如果訊息的優先順序高於或者等於logger的優先順序,訊息將被傳遞到logger對應的通道中。訊息傳遞時並不會發生改變。
          2. void log(const Exception& exc)
          使用最高優先順序PRIO_ERROR,建立並記錄訊息。訊息內容為異常內容。
          3. 使用下列不同優先順序和給定的文字建立並記錄訊息
void fatal(const std::string& text)
       void critical(const std::string& text)
       void error(const std::string& text)
       void warning(const std::string& text)   
       void notice(const std::string& text)
       void information(const std::string& text)
       void debug(const std::string& text)
       void trace(const std::string& text)
          4. 使用給定的優先順序和內容記錄訊息。訊息的內容為16進位制的給定Dump資料塊。
             Logging Messages (cont'd)
          5. 判斷日誌等級
             bool is(int level) const              如果logger的日誌級別等於或高於查詢的日誌級別,返回true
bool critical() const
       bool error() const
       bool warning() const
       bool notice() const
       bool information() const
       bool debug() const
       bool trace() const
       bool fatal() const
            如果logger的日誌級別等於或高於給定的日誌級別,返回true

           訪問日誌物件:
          POCO庫在內部管理了一個全域性的日誌map。使用者不需要自己建立logger物件,使用者可以向POCO庫申請一個logger物件的引用。POCO會根據需要建立新的日誌物件。
          static Logger& get(const std::string& name)
          使用上面函式可以獲取到給定名稱所關聯的logger物件的引用,如果有必要,POCO庫會在內部建立一個logger物件。出於效率上的考慮,Poco使用文件推薦使用者儲存所使用的logger物件的引用,而不是頻繁的呼叫此函式。理所當然的,POCO庫能保證logger物件的引用始終有效。

          下面是一個例子:
#include "Poco/Logger.h"
using Poco::Logger;
int main(int argc, char** argv)
{
          Logger& logger = Logger::get("TestLogger");
          logger.information("This is an informational message");
          logger.warning("This is a warning message");
          return 0;
}

1.4 通道:

          通道的子類負責傳遞訊息給最終目的地。比如說控制檯或者日誌檔案等。
          每一個 Poco::Logger類物件(它本身也是Poco::Channel的子類)都對應著一個Poco::Channel類物件。在Poco庫內部已經實現了各種Poco::Channel子類,用於向不同的目標輸出日誌,比如說控制檯,日誌檔案,或者系統日誌工具。使用者可以定義自己的channel類。在內部Poco::Channel使用了 引用計數技術 來實現記憶體管理。

          通道屬性:
          通道支援配置任意數目的屬性,屬性為一個名字值對。屬性可以通過以下函式獲取和設定:
void setProperty(const std::string& name, const std::string& value)
             std::string getProperty(const sdt::string& name)
          這兩個函式被定義在Poco::Configurable中,Poco::Configurable為Poco::Channel的父類。

1.4.1 控制檯通道(ConsoleChannel)

          Poco::ConsoleChannel可以滿足大多數的控制檯輸出。它只是簡單的把訊息內容寫入了標準輸出流(std::clog),並且不支援配置屬性。它是根logger預設關聯的通道(貌似這裡有點誤解,根logger並不會自動建立ConsoleChannel)。

1.4.2 windows控制檯通道(WindowsConsoleChannel)

          Poco::WindowsConsoleChannel同ConsoleChannel類似,唯一不同的是向windows控制檯輸出。它只是簡單把訊息內容寫入window控制檯,並且不支援配置屬性。向window控制檯輸出時,支援UTF-8編碼。

1.4.3 空白通道(NullChannel)

          Poco::NullChannel通道會拋棄所有發向它的訊息,並且忽略所有setProperty()函式設定的屬性。

1.4.4 簡單檔案通道(SimpleFileChannel)

          Poco::SimpleFileChannel類實現了向日志文件輸出的簡單功能。對於每一個訊息,其內容都會被新增到檔案中,並使用一個新行輸出。簡單日誌檔案支援檔案迴圈覆蓋,一旦主日誌檔案超過確定的大小,第二個日誌檔案會被建立,如果第二個日誌檔案已經存在,會被截斷。而當第二個日誌檔案超過大小限制,主日誌檔案將被覆蓋。如此迴圈。

          簡單檔案通道屬性
          path:
主日誌檔案路徑
           secondaryPath : 第二個日誌檔案路徑。默認同主日誌檔案路徑。
           rotation :日誌迴圈覆蓋模式。可以有以下幾種選擇:
               never: 不需要迴圈覆蓋
               <n>: 如果超過 <n> 位元組的話,迴圈覆蓋
               <n> K: 如果超過 <n> K位元組的話,迴圈覆蓋
               <n> M: 如果超過 <n> M位元組的話,迴圈覆蓋

          下面是一個例子:
#include "Poco/Logger.h"
#include "Poco/SimpleFileChannel.h"
#include "Poco/AutoPtr.h"
using Poco::Logger;
using Poco::SimpleFileChannel;
using Poco::AutoPtr;
int main(int argc, char** argv)
{
          AutoPtr<SimpleFileChannel> pChannel(new SimpleFileChannel);
          pChannel->setProperty("path", "sample.log");
          pChannel->setProperty("rotation", "2 K");
          Logger::root().setChannel(pChannel);
          Logger& logger = Logger::get("TestLogger"); // inherits root channel
          for (int i = 0; i < 100; ++i)
          logger.information("Testing SimpleFileChannel");
          return 0;
}

1.4.5 檔案通道

          Poco::FileChannel類提供了完整的日誌支援。每一個訊息的內容都會被新增到檔案中,並使用一個新行輸出。Poco::FileChannel類支援按檔案大小和時間間隔對日誌進行迴圈覆蓋,支援自動歸檔(使用不同的檔案命名策略),支援壓縮(GZIP)和清除(根據已歸檔檔案的日期或數量)歸檔日誌檔案。

           檔案通道屬性
          path:
日誌檔案的路徑
           rotation: 日誌迴圈覆蓋模式。可以有以下幾種選擇:
               never: 不需要迴圈覆蓋
               <n>: 如果超過 <n> 位元組的話,迴圈覆蓋
               <n> K: 如果超過 <n> K位元組的話,迴圈覆蓋
               <n> M: 如果超過 <n> M位元組的話,迴圈覆蓋
               [day][hh:][mm]: 按照指定的日期和時間進行日誌的迴圈覆蓋
               daily/weekly/monthly: 按照日/周/月迴圈覆蓋
               <n> hours/weeks/months: 按照<n>小時/周/月進行迴圈覆蓋
           archive: 歸檔日誌的目錄名
               number:從0開始自動增加的數字,被新增到日誌檔名後。最新的日誌檔案數字總是0。
               timestamp: 時間戳以YYYYMMDDHHMMSS格式被新增到日誌檔名後
               times:指定迴圈的時間是按照本地時間還是按照UTC時間。本地時間和utc時間都是可以接受的合法時間。
               compress:自動壓縮存檔檔案。指定true或者false。
              purgeAge:指定歸檔日誌的最大期限。當日志的生成時間超過此期限,將被刪除。格式為 <n> [seconds]/minutes/hours/days/weeks/months
               purgeCount:指定歸檔日誌檔案的最大數目。如果生成日誌的數目超過此最大數目,生成日期最早的檔案將被刪除。

          下面是一個例子:
#include "Poco/Logger.h"
#include "Poco/FileChannel.h"
#include "Poco/AutoPtr.h"
using Poco::Logger;
using Poco::FileChannel;
using Poco::AutoPtr;
int main(int argc, char** argv)
{
          AutoPtr<FileChannel> pChannel(new FileChannel);
          pChannel->setProperty("path", "sample.log");
          pChannel->setProperty("rotation", "2 K");
          pChannel->setProperty("archive", "timestamp");
          Logger::root().setChannel(pChannel);
          Logger& logger = Logger::get("TestLogger"); // inherits root channel
          for (int i = 0; i < 100; ++i)
          logger.information("Testing FileChannel");
          return 0;
}

1.4.6 事件日誌通道(EventLogChannel)

          Poco::EventLogChannel僅被使用於作業系統Windows NT中,它將把日誌寫到"Windows事件日誌"中.Poco::EventLogChannel會把PocoFoundation.dll作為訊息定義資源註冊到"Windows事件日誌"中。當使用Window事件檢視器來檢視系統事件日誌時,事件檢視器必須要找到PocoFoundation.dll,否則記錄的日誌訊息將不能夠被正常顯示。

           事件日誌通道屬性
               name:
事件源的名字,通常是程式名。
               loghost, host: 事件日誌服務在執行的主機的名稱。預設值為本地主機
                logfile: 日誌檔案的名稱。預設是應用程式本身。

1.4.7 系統日誌通道(SyslogChannel)

          Poco::SyslogChannel僅適用於Unix平臺,會把日誌輸出到本地系統日誌守護程式。
          包含RemoteSyslogChannel類的網路庫,可以通過基於UDP的系統日誌協議(Syslog protoco)把日誌輸出到遠端的日誌守護程式上。

1.4.8 非同步通道:

          Poco::AsyncChannel允許在另外一個分離的執行緒中去記錄通道的日誌。這可以把產生日誌的執行緒和記錄日誌的執行緒分開而實現解耦。所有的訊息先被儲存在一個先進先出的訊息佇列中,然後由一個單獨的執行緒從訊息佇列中獲取,並最終把訊息傳送到輸出通道。

          下面是一個例子:
#include "Poco/Logger.h"
#include "Poco/AsyncChannel.h"
#include "Poco/ConsoleChannel.h"
#include "Poco/AutoPtr.h"
using Poco::Logger;
using Poco::AsyncChannel;
using Poco::ConsoleChannel;
using Poco::AutoPtr;
int main(int argc, char** argv)
{
          AutoPtr<ConsoleChannel> pCons(new ConsoleChannel);
          AutoPtr<AsyncChannel> pAsync(new AsyncChannel(pCons));
          Logger::root().setChannel(pAsync);
          Logger& logger = Logger::get("TestLogger");
          for (int i = 0; i < 10; ++i)
          logger.information("This is a test");
          return 0;
}

1.4.9 拆分通道(SplitterChannel)

          使用Poco::SplitterChannel可以把訊息傳送給一個或者多個其他的通道,即輸出日誌在多個目標中。使用下面的函式可以在SplitterChannel中加入一個新通道:
                void addChannel(Channel* pChannel)

          下面是一個例子
#include "Poco/Logger.h"
#include "Poco/SplitterChannel.h"
#include "Poco/ConsoleChannel.h"
#include "Poco/SimpleFileChannel.h"
#include "Poco/AutoPtr.h"
using Poco::Logger;
using Poco::SplitterChannel;
using Poco::ConsoleChannel;
using Poco::SimpleFileChannel;
using Poco::AutoPtr;
int main(int argc, char** argv)
{
          AutoPtr<ConsoleChannel> pCons(new ConsoleChannel);
          AutoPtr<SimpleFileChannel> pFile(new SimpleFileChannel("test.log"));
          AutoPtr<SplitterChannel> pSplitter(new SplitterChannel);
          pSplitter->addChannel(pCons);
          pSplitter->addChannel(pFile);
          Logger::root().setChannel(pSplitter);
          Logger::root().information("This is a test");
          return 0;
}

1.5 LogStream類

          Poco::LogStream類提供了一個日誌的輸出流介面。可以在日誌流中,格式化輸出日誌記錄訊息。日誌訊息必須以std::endl(或CR和LF字元)結尾。

         下面是 LogStream在日誌體系中的示意圖:

          訊息的優先順序可以使用下列函式設定:
LogStream& priority(Message::Priority prio)
          LogStream& fatal()
          LogStream& critical()
          LogStream& error()
          LogStream& warning()
          LogStream& notice()
          LogStream& information()
          LogStream& debug()
          LogStream& trace

          下面是一個例子:
#include "Poco/LogStream.h"
#include "Poco/Logger.h"
using Poco::Logger;
using Poco::LogStream;
int main(int argc, char** argv)
{
          Logger& logger = Logger::get("TestLogger");
          LogStream lstr(logger);
          lstr << "This is a test" << std::endl;
          return 0;
}

1.6  FormattingChannel類和Formatter類

          訊息的格式
          
          FormattingChannel類和Formatter類負責格式化日誌訊息。Poco::FormattingChannel會把它接受到的每一個訊息通過Poco::Formatter傳遞給下一個的輸出通道。                        Poco::Formatter是所有格式類的基類,同通道一樣,可以被設定屬性。

1.6.1 PatternFormatter類

          Poco::PatternFormatter可以根據列印格式去格式化訊息。想要知道更多細節,可以檢視相關文件。

          下面是一個例子:
#include "Poco/ConsoleChannel.h"
#include "Poco/FormattingChannel.h"
#include "Poco/PatternFormatter.h"
#include "Poco/Logger.h"
#include "Poco/AutoPtr.h"
using Poco::ConsoleChannel;
using Poco::FormattingChannel;
using Poco::PatternFormatter;
using Poco::Logger;
using Poco::AutoPtr;
int main(int argc, char** argv)
{
          AutoPtr<ConsoleChannel> pCons(new ConsoleChannel);
          AutoPtr<PatternFormatter> pPF(new PatternFormatter);
          pPF->setProperty("pattern", "%Y-%m-%d %H:%M:%S %s: %t");
          AutoPtr<FormattingChannel> pFC(new FormattingChannel(pPF, pCons));
          Logger::root().setChannel(pFC);
          Logger::get("TestChannel").information("This is a test");
          return 0;
}

1. 7 日誌效率的考慮:

          1. 建立訊息可能要花費一定的時間(訊息建立時需要獲取系統當前時間、程序ID和執行緒ID)
          2. 建立一個有意義的訊息也需要時間,因為按輸出格式生成字串是存在開銷的
          3. 訊息通常情況下是通過引用的方式傳遞給下一個通道。例外的情況是,FormattingChannel和AsyncChannel類。它們會生成訊息的一個副本。
          4. 對於每一個日誌(logger)物件來說,一條訊息要麼被輸出,要麼不被輸出,這由日誌和訊息的級別共同決定。這個動作存在常數級別的開銷,僅是兩個int型的比較。
          5. 獲取日誌(logger)物件引用的操作開銷是基於對數的,這由std::map的查詢特性所決定。在查詢過程中,日誌(logger)物件名稱的比較是線性的,這由std::string字串比較特性所決定。
          6. 通常在一個程式中,獲取一個日誌(logger)物件引用(Logger::get())的操作,只會進行一次。
          7. 儘可能的避免頻繁的呼叫Logger::get()函式,更好的方法是在通過函式獲得日誌(logger)物件引用後,儲存它。
          8. 記錄和輸出日誌的效率取決於日誌輸出的通道。通道的效率非常依賴於作業系統的實現。
          9. 構造訊息(messages)的開銷包括了構造字串,字元拼接,數字格式化等。
          10. 在構造訊息前,推薦先查詢日誌器的等級,以決定是否需要構造訊息。查詢等級可以使用函式is(), fatal(), critical()等。
          11. 在Poco庫中提供了一些巨集,用於在構造訊息之前對日誌等級進行檢查。如poco_fatal(msg), poco_critical(msg), poco_error(msg)等。

          下面是一個例子:
// ...
if (logger.warning())
{
          std::string msg("This is a warning");
          logger.warning(msg);
}

// is equivalent to
poco_warning(logger, "This is a warning");