實現一個微型的日誌庫
序言
對一個服務端程式來說,日誌是用於發現系統問題,診斷系統執行情況的一個重要工具,所以日誌庫的設計要以幫助跟蹤程式執行狀態為宗旨,這篇文章來源於最近我在一個通訊協議庫中所寫的一個微型的日誌元件,總共也就兩百來行程式碼,實現了日誌的蒐集、過濾、輸出功能。
日誌庫的功能與設計
一個日誌庫,應該把來自於程式各個部分的日誌資訊蒐集起來,按照一定的過濾規則(通常是按日誌級別過濾),將通過過濾的日誌資訊輸出到指定的目標地點,可以是終端控制檯,也可以是磁碟檔案或者網路,甚至可以是它們的組合。
下面是一條日誌資訊:
<2015-08-21 09:51:46.939797> [trace] [tcp-client-124658]
receive 11 bytes data from: [127.0.0.1]:2404
68 09 96 00 bc 00 43 f8 ad 5c 88
一條日誌輸出,主要包括時間戳、日誌級別、標記、日誌內容這些資訊。
日誌蒐集器
日誌蒐集器的作用是為每一條日誌打上相應的標記,這個標記可以是程式中某個類的名字,也可以是某個容器的名字,總之可以是你想要在最終的日誌檔案中進行跟蹤的一個關鍵字。程式中在需要輸出日誌資訊的地方,指定一個蒐集器,也就為該條日誌打上了對應的標記。
日誌級別與過濾器
日誌資訊通常是分等級的,一般情況下包含普通訊息、警告、錯誤、致命異常這些級別,並且等級由低到高,以區分日誌資訊的重要程度。
過濾器給了應用層控制日誌量的機會,如果系統日誌量過大,可以通過擡高日誌的級別來加以控制,所以常見的過濾器就是按級別過濾,只有大於等於某個指定級別的日誌才會真的被輸出。
日誌輸出器
日誌資訊最終都要儲存到某個儲存裝置,例如檔案或者網路,通常也可以輸出到標準控制檯,在日誌庫中,只有它才真正關心日誌該怎樣輸出到儲存裝置的,通常它要考慮的主題是緩衝區和多執行緒安全。
如果輸出到終端,可以使用顯眼的顏色來輸出錯誤資訊,以使人注意到它的出現。可以同時指定多個輸出地,比如同時輸出到控制檯和檔案,也可以為不同的輸出地設定不同的過濾器,比如控制檯顯示全部日誌資訊,但是隻將錯誤資訊寫入到檔案,本文要實現的日誌庫沒有考慮這個功能,要實現也只要稍加修改即可。
日誌管理器
前面的日誌蒐集器、過濾器、輸出器都只是日誌庫的重要零件,得有一個管理器把它們整合起來,管理器的作用就是提供列印日誌的介面,以接受來自程式各個角落的日誌輸出請求,並用過濾器對日誌資訊進行過濾,將能通過過濾器的日誌資訊傳遞到輸出器進行儲存。
日誌庫的實現
日誌蒐集器
日誌蒐集器的作用就是儲存標記,可以為程式的每個模組設定一個日誌蒐集器,該模組列印的日誌都交給這個蒐集器,以便帶上這個蒐集器的標記,例如在上一節中所展示的日誌資訊示例,[tcp-client-124658] 就代表這是某個 TCP 客戶端所列印的日誌。
它的實現如下:
// 負責蒐集日誌,含有日誌標籤等資訊
class log_collector
{
public:
const std::string name_;
public:
explicit log_collector(const std::string& name) : name_(name) {}
};
日誌級別
這個就更簡單了:
class log_level
{
public:
const int level_;
const std::string name_;
public:
log_level(int level, const char *name) : level_(level), name_(name) {}
};
log_level log_trace(0, "trace");
log_level log_info(1, "info");
log_level log_warning(2, "warning");
log_level log_error(3, "error");
log_level log_fatal(4, "fatal");
如上,還預置了幾個級別。
日誌過濾器
過濾器決定一條日誌資訊是否需要輸出到目的地,通常就是按級別過濾,所以它本質上就是一個函式物件:
// 日誌過濾器
typedef boost::function<bool (const log_level&)> log_filter;
現在寫一個級別過濾器:
// 按等級過濾日誌,大於等於指定等級的日誌可以通過
struct log_level_filter
{
log_level level_;
explicit log_level_filter(const log_level& level);
bool operator()(const log_level& level)
{
return level.level_ >= this->level_.level_;
}
};
此外,我們再分別提供一個不攔截任何日誌的過濾器和一個攔截全部日誌的過濾器,當然這兩個過濾器一般不會用到,僅作為預設的過濾器:
// 預設日誌過濾器,不攔截任何日誌
class log_all_print
{
public:
bool operator()(const log_level& level)
{
return true;
}
};
// 攔截全部日誌
class log_all_no_print
{
public:
bool operator()(const log_level& level)
{
return false;
}
};
日誌輸出器
當一條日誌通過了過濾器的考核後,就要儲存到某個地方,所以它們的核心功能就是儲存日誌,但是要考慮到多執行緒的情況,這個寫日誌的函式是需要加鎖保護的。
class log_destination
{
public:
virtual void save(const level& lv, const std::string& message) = 0;
};
這個基類只提供了一個介面,之所以要傳遞日誌等級,是考慮到有些輸出地可能會需要根據這個等級實現不同的輸出形式,例如終端可以將錯誤資訊加亮顯示。
終端:
// 預設日誌輸出地,控制檯螢幕
class log_to_console : public log_destination
{
boost::mutex console_mutex;
public:
void save(const level& lv, const std::string& message)
{
boost::mutex::scoped_lock console_lock(console_mutex);
if (lv.level_ >= log_error.level_)
{
std::cerr << message << std::endl;
}
else
{
std::cout << message << std::endl;
}
}
};
注意這裡將錯誤資訊使用標準錯誤流進行輸出。
檔案:
// 輸出到檔案
class log_to_file : public log_destination
{
boost::mutex file_mutex;
ofstream ofs;
public:
explicit log_to_file(const std::string& filename) : ofs(filename) {}
public:
void save(const level& lv, const std::string& message)
{
boost::mutex::scoped_lock file_lock(file_mutex);
ofs << message << "\n";
}
};
網路的暫時沒有實現,以後有需要再說吧,下面提供一個黑洞輸出地,交給它的日誌資訊將被直接丟棄:
// 黑洞,直接丟棄
class log_to_blackhole : public log_destination
{
public:
void save(const level& lv, const std::string& message)
{
}
};
通常程式中都有一個專門用於列印日誌的回撥函式,它使得日誌元件不用關心日誌具體輸出到哪,只要交給回撥函式就可以了,當然外部提供的這個回撥函式通常還是會將日誌資訊輸出到終端或者檔案,只是這給了日誌元件呼叫者DIY日誌目的地的機會,回撥函式長的這副模樣:
typedef void (*log_callback)(int level, const char * message);
然後下面是輸出到回撥函式:
class log_to_cb : public log_destination
{
boost::mutex cb_mutex;
log_callback cb_;
public:
explicit log_to_cb(log_callback cb) : cb_(cb) {}
public:
void save(const level& lv, const std::string& message)
{
boost::mutex::scoped_lock cb_lock(cb_mutex);
cb_(lv.level_, message.c_str());
}
};
夠簡單吧?
日誌管理器
現在是萬事俱備,只欠東風,日誌管理器提供日誌庫的對外介面,當然就是供程式其它地方列印日誌的介面,它應當是全域性唯一的物件,所以這裡實現了一個簡單的單例,同時它應當儲存日誌過濾器和輸出目的地,在接受一條日誌時,它先呼叫過濾器進行過濾,如果通過了,再呼叫日誌輸出地進行日誌的列印操作,下面是它的實現:
// 日誌管理器
class logger
{
log_filter the_filter;
boost::shared_ptr<log_destination> the_destination;
private:
logger(const log_filter& filter, boost::shared_ptr<log_destination> destination)
: the_filter(filter), the_destination(destination) {}
public:
// 獲取唯一例項
static logger& instance()
{
static log_all_print default_filter;
static log_to_console default_destination;
static logger the_logger(default_filter, default_destination);
return the_logger;
}
// 設定過濾器
void set_filter(const log_filter& filter)
{
the_filter = filter;
}
// 設定日誌輸出目的地
void set_destination(boost::shared_ptr<log_destination> destination)
{
the_destination = destination;
}
// 輸出日誌
void log(const log_collector& collector, const log_level& level, const std::string& message) const
{
if (the_filter(level))
{
// 獲取當前時間並轉換為字串
boost::posix_time::ptime now(boost::posix_time::microsec_clock::local_time());
std::string now_str = boost::posix_time::to_iso_extended_string(now);
boost::replace_first(now_str, "T", " ");
std::stringstream ss;
ss << "<" << now_str << "> "
<< "[" << level.name_ << "] "
<< "[" << collector.name_ << "]\n"
<< message << "\n";
the_destination->save(level, ss.str());
}
}
};
這裡日誌輸出地是使用智慧指標儲存的,是因為如果不使用指標,將會存在複製這個log_destination物件的行為,但通常它們都內含一個用於多執行緒保護的互斥量,而這個互斥量是不能複製的。
日誌庫的使用
現在,在程式的其它地方已經可以使用這個日誌元件了,在啟動的時候設定日誌過濾器和輸出目的地:
log_level_filter my_filter(log_warning);
logger::instance().set_filter(my_filter);
boost::shared_ptr<log_to_file> my_log_file(new log_to_file("hello.log"));
logger::instance().set_destination(my_log_file);
然後假定在程式中有一個 TCP 客戶端,需要在收到來自網路的資料時列印日誌,可以這樣使用日誌庫:
class tcp_client
{
private:
log_collect my_log_col;
public:
tcp_client() : my_log_col("a-tcp-client") {}
void receive(const std::string& data)
{
logger::instance().log(my_log_col, log_info, data);
}
}
怎麼樣,沒有比這更簡單的了吧?
本文只展示了最基本的功能,但並不完善,有些錯誤比如輸出到檔案時檔案建立失敗之類的錯誤並沒有處理。
在功能方面,本文也沒有實現太多,比如同時新增多個輸出地,並且每個輸出地可以有自己單獨的過濾器,等等,要實現也簡單,本文就不囉嗦了。