对一个服务端程序来说,日志是用于发现系统问题,诊断系统运行情况的一个重要工具,所以日志库的设计要以帮助跟踪程序运行状态为宗旨,这篇文章来源于最近我在一个通信协议库中所写的一个微型的日志组件,总共也就两百来行代码,实现了日志的搜集、过滤、输出功能。
一个日志库,应该把来自于程序各个部分的日志信息搜集起来,按照一定的过滤规则(通常是按日志级别过滤),将通过过滤的日志信息输出到指定的目标地点,可以是终端控制台,也可以是磁盘文件或者网络,甚至可以是它们的组合。
下面是一条日志信息:
<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 the_destination;
private:
logger(const log_filter& filter, boost::shared_ptr 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 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 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);
}
}
怎么样,没有比这更简单的了吧?
本文只展示了最基本的功能,但并不完善,有些错误比如输出到文件时文件创建失败之类的错误并没有处理。
在功能方面,本文也没有实现太多,比如同时添加多个输出地,并且每个输出地可以有自己单独的过滤器,等等,要实现也简单,本文就不啰嗦了。