日志对于程序来说是非常重要的,特别是对一些大型程序而言。一旦程序被发布,在现场日志几乎是程序员唯一可以获取程序信息的手段。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
可以通过函数设置和获取消息优先级:
消息源:
消息源用来描述日志消息的源。通常状态下,使用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
消息时间戳:
记录消息产生时的时间戳,精度为毫秒。
可以通过函数设置和获取时间戳:
进程和线程标识符:
进程标识符(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 如果logger的日志级别等于或高于给定的日志级别,返回true
访问日志对象:
POCO库在内部管理了一个全局的日志map。用户不需要自己创建logger对象,用户可以向POCO库申请一个logger对象的引用。POCO会根据需要创建新的日志对象。
static Logger& get(const std::string& name)
使用上面函数可以获取到给定名称所关联的logger对象的引用,如果有必要,POCO库会在内部创建一个logger对象。出于效率上的考虑,Poco使用文档推荐用户保存所使用的logger对象的引用,而不是频繁的调用此函数。理所当然的,POCO库能保证logger对象的引用始终有效。
下面是一个例子:
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字节的话,循环覆盖
下面是一个例子:
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:指定归档日志文件的最大数目。如果生成日志的数目超过此最大数目,生成日期最早的文件将被删除。
下面是一个例子:
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允许在另外一个分离的线程中去记录通道的日志。这可以把产生日志的线程和记录日志的线程分开而实现解耦。所有的消息先被存储在一个先进先出的消息队列中,然后由一个单独的线程从消息队列中获取,并最终把消息发送到输出通道。下面是一个例子:
1.4.9 拆分通道(SplitterChannel)
使用Poco::SplitterChannel可以把消息发送给一个或者多个其他的通道,即输出日志在多个目标中。使用下面的函数可以在SplitterChannel中加入一个新通道:void addChannel(Channel* pChannel)
下面是一个例子
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
下面是一个例子:
1.6 FormattingChannel类和Formatter类
消息的格式FormattingChannel类和Formatter类负责格式化日志消息。Poco::FormattingChannel会把它接受到的每一个消息通过Poco::Formatter传递给下一个的输出通道。 Poco::Formatter是所有格式类的基类,同通道一样,可以被设置属性。
1.6.1 PatternFormatter类
Poco::PatternFormatter可以根据打印格式去格式化消息。想要知道更多细节,可以查看相关文档。下面是一个例子:
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");