日志对于程序来说是非常重要的,特别是对一些大型程序而言。一旦程序被发布,在现场日志几乎是程序员唯一可以获取程序信息的手段。Poco作为一个框架类库,提供了非常多的日志种类供程序员选用。文章将分两个部分,对于Poco日志进行介绍。第一部分主要以翻译Poco文档为主,第二部分则探讨Poco日志的实现。
Poco中的日志模块主要涉及下列几个部分。
1. 消息,日志和通道
2. 格式
3. 执行效率的考量
模块框架图:
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可以是任意字符串。
消息参数可以被用于最终的格式输出。
消息参数支持下标索引。
应用程序可以使用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;
}
通道的子类负责传递消息给最终目的地。比如说控制台或者日志文件等。
每一个 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;
}
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. 创建消息可能要花费一定的时间(消息创建时需要获取系统当前时间、进程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");