2. Poco日志的实现
2.1 日志模块应该实现的业务
在讨论日志的实现之前,先来聊一下日志模块应该实现那些业务。日志的业务说简单可以很简单,就是输出记录。说复杂也复杂,来看它的复杂性:
首先,日志的输出对象是不同的,有控制台输出,本地文件输出,网络文件输出,输出到系统日志等。假如是网络日志,日志库中其实还会包含网络模块,真是越来越复杂了。
第二,日志输出的格式和内容。不同用户关心的内容和喜欢的输出格式是不同的,要满足所有人的需求,就必须能够提供全面的信息,并提供选项供用户选择。
第三,日志的级别。程序的日志一定是需要动态可调的。程序日志过多,消耗资源;日志过少,无法提供足够的信息,用来定位和解决问题。
第四,日志的存储策略。日志是具有实效性的,日志保存的时间越久,信息熵越低;日志存储也是需要成本的,大量的日志会挤占硬盘空间,所以需要对日志的存储进行管理。超过一定时间的日志可以考虑删除。在磁盘资源紧张的情况下,必须考虑控制日志的大小。
第五,日志是用来查询和排除问题的。为了能够快速的定位问题,最好能够把日志按照模块输出,这就要求日志库设计的时候考虑日志模块的分类。
第六,这一点和日志的业务无关,和库的实现相关。跨平台的话,必须考虑操作系统底层API的不同。
对于日志模块的业务就讨论到这里,还是回到Poco的日志模块上。
2.2. Message类
下面是Message类的头文件。其定义如下:
class Foundation_API Message
{
public:
enum Priority
{
PRIO_FATAL = 1, /// A fatal error. The application will most likely terminate. This is the highest priority.
PRIO_CRITICAL, /// A critical error. The application might not be able to continue running successfully.
PRIO_ERROR, /// An error. An operation did not complete successfully, but the application as a whole is not affected.
PRIO_WARNING, /// A warning. An operation completed with an unexpected result.
PRIO_NOTICE, /// A notice, which is an information with just a higher priority.
PRIO_INFORMATION, /// An informational message, usually denoting the successful completion of an operation.
PRIO_DEBUG, /// A debugging message.
PRIO_TRACE /// A tracing message. This is the lowest priority.
};
Message();
Message(const std::string& source, const std::string& text, Priority prio);
Message(const std::string& source, const std::string& text, Priority prio, const char* file, int line);
Message(const Message& msg);
Message(const Message& msg, const std::string& text);
~Message();
Message& operator = (const Message& msg);
void swap(Message& msg);
void setSource(const std::string& src);
const std::string& getSource() const;
void setText(const std::string& text);
const std::string& getText() const;
void setPriority(Priority prio);
Priority getPriority() const;
void setTime(const Timestamp& time);
const Timestamp& getTime() const;
void setThread(const std::string& thread);
const std::string& getThread() const;
void setTid(long pid);
long getTid() const;
void setPid(long pid);
long getPid() const;
void setSourceFile(const char* file);
const char* getSourceFile() const;
void setSourceLine(int line);
int getSourceLine() const;
const std::string& operator [] (const std::string& param) const;
std::string& operator [] (const std::string& param);
protected:
void init();
typedef std::map<std::string, std::string> StringMap;
private:
std::string _source; // 产生日志的源
std::string _text; // 日志主内容
Priority _prio; // 日志的优先级(某种程度上表明了日志本身的信息含量)
Timestamp _time; // 日志产生的时间
int _tid; // 日志产生的线程
std::string _thread; // 日志产生的线程名
long _pid; // 日志产生的进程名
const char* _file; // 日志产生的代码文件
int _line; // 日志产生的代码文件行号
StringMap* _pMap; // 供用户存储其他信息的map容器
};
它的默认初始化函数为:
Message::Message():
_prio(PRIO_FATAL),
_tid(0),
_pid(0),
_file(0),
_line(0),
_pMap(0)
{
init();
}
void Message::init()
{
#if !defined(POCO_VXWORKS)
_pid = Process::id();
#endif
Thread* pThread = Thread::current();
if (pThread)
{
_tid = pThread->id();
_thread = pThread->name();
}
}
从上面的代码可以看出Message类提供了非常多的存储选项,有日志的源、线程信息、进程信息、优先级等。在此基础上,为了满足用户的需求,还放了一个map来支持用户定制。所有的信息,都在Message类构造的时候被赋值,真的挺强大。当然这一做法也会带来一点程序上的开销。
2.3 Configurable类
在Poco库里,Configurable类是用来对日志特性做配置的。其定义如下:
class Foundation_API Configurable
{
public:
Configurable();
virtual ~Configurable();
virtual void setProperty(const std::string& name, const std::string& value) = 0;
virtual std::string getProperty(const std::string& name) const = 0;
};
从代码看它本身是一个抽象类,提供了两个接口,用来设置和获取日志属性。看子类的代码,能够知道,这两个接口是用来完成字符解析工作的。
2.4 LogFile类
LogFile是Poco日志模块的内部类,封装了不同操作系统存档文件记录之间的差异,也就是说隐藏了操作系统之间对于文件输入的区别。其定义如下:
#if defined(POCO_OS_FAMILY_WINDOWS) && defined(POCO_WIN32_UTF8)
#include "Poco/LogFile_WIN32U.h"
#elif defined(POCO_OS_FAMILY_WINDOWS)
#include "Poco/LogFile_WIN32.h"
#elif defined(POCO_OS_FAMILY_VMS)
#include "Poco/LogFile_VMS.h"
#else
#include "Poco/LogFile_STD.h"
#endif
namespace Poco {
class Foundation_API LogFile: public LogFileImpl
{
public:
LogFile(const std::string& path);
~LogFile();
void write(const std::string& text);
UInt64 size() const;
Timestamp creationDate() const;
const std::string& path() const;
};
2.5 策略类(Strategy)
Strategy类也同样是日志系统内部的实现类,同时也是针对存档文件操作设计的。对于存档文件,Poco认为存在3种策略,即:
1. 对于文件存档的策略
2. 对于文件删除的策略
3. 对于文件覆盖的策略
对于文件存档的策略由ArchiveStrategy类和其子类完成。它们完成的工作是对日志文件的命名。ArchiveByNumberStrategy完成了日志文件的数字命名,即程序产生的日志会以log0、log1、...logn命名。ArchiveByTimestampStrategy完成了日志文件的时间戳命名,即程序产生的日志会以时间戳方式命名。
在ArchiveStrategy类上还留有一个压缩接口,用来设置存档文件是否需要被压缩。在Poco中,内置了gzip压缩方式,这个具体由类ArchiveCompressor实现。关于这一点,我们会在以后介绍。
对于文件删除的策略由PurgeStrategy类和其子类完成。PurgeByCountStrategy类,实现了按文件大小删除的策略。而PurgeByAgeStrategy实现了按文件存储时间删除的
策略。来看一段PurgeByAgeStrategy::purge动作的代码:
void PurgeByAgeStrategy::purge(const std::string& path)
{
std::vector<File> files;
list(path, files);
for (std::vector<File>::iterator it = files.begin(); it != files.end(); ++it)
{
if (it->getLastModified().isElapsed(_age.totalMicroseconds()))
{
it->remove();
}
}
}
void PurgeStrategy::list(const std::string& path, std::vector<File>& files)
{
Path p(path);
p.makeAbsolute();
Path parent = p.parent();
std::string baseName = p.getFileName();
baseName.append(".");
DirectoryIterator it(parent);
DirectoryIterator end;
while (it != end)
{
if (it.name().compare(0, baseName.size(), baseName) == 0)
{
files.push_back(*it);
}
++it;
}
}
从代码看PurgeByAgeStrategy::purge函数的输入为一个路径。purge函数会遍历这个目录,查看文件信息,当文件历史超过一定时间,则删除。PurgeByCountStrategy与之类似。
对于文件覆盖的策略是由类RotateStrategy和其子类完成的。文件的覆盖策略同删除策略是不同的,覆盖策略是一个循环策略。RotateAtTimeStrategy实现了按时间循环的功能。RotateByIntervalStrategy实现了按时间间隔循环的策略。RotateBySizeStrategy实现了按大小循环的策略。
2.6 格式类(Formatter)
格式类是用来确定输出日志最终内容的格式的。Message类提供了非常多的日志信息,但并不是所有信息都是用户所感兴趣的。Formatter被用来确定最终消息输出。在Poco库中内置了一些格式输出选项,由PatternFormatter完成。其定义如下:
class Foundation_API PatternFormatter: public Formatter
/// This Formatter allows for custom formatting of
/// log messages based on format patterns.
///
/// The format pattern is used as a template to format the message and
/// is copied character by character except for the following special characters,
/// which are replaced by the corresponding value.
///
/// * %s - message source
/// * %t - message text
/// * %l - message priority level (1 .. 7)
/// * %p - message priority (Fatal, Critical, Error, Warning, Notice, Information, Debug, Trace)
/// * %q - abbreviated message priority (F, C, E, W, N, I, D, T)
/// * %P - message process identifier
/// * %T - message thread name
/// * %I - message thread identifier (numeric)
/// * %N - node or host name
/// * %U - message source file path (empty string if not set)
/// * %u - message source line number (0 if not set)
/// * %w - message date/time abbreviated weekday (Mon, Tue, ...)
/// * %W - message date/time full weekday (Monday, Tuesday, ...)
/// * %b - message date/time abbreviated month (Jan, Feb, ...)
/// * %B - message date/time full month (January, February, ...)
/// * %d - message date/time zero-padded day of month (01 .. 31)
/// * %e - message date/time day of month (1 .. 31)
/// * %f - message date/time space-padded day of month ( 1 .. 31)
/// * %m - message date/time zero-padded month (01 .. 12)
/// * %n - message date/time month (1 .. 12)
/// * %o - message date/time space-padded month ( 1 .. 12)
/// * %y - message date/time year without century (70)
/// * %Y - message date/time year with century (1970)
/// * %H - message date/time hour (00 .. 23)
/// * %h - message date/time hour (00 .. 12)
/// * %a - message date/time am/pm
/// * %A - message date/time AM/PM
/// * %M - message date/time minute (00 .. 59)
/// * %S - message date/time second (00 .. 59)
/// * %i - message date/time millisecond (000 .. 999)
/// * %c - message date/time centisecond (0 .. 9)
/// * %F - message date/time fractional seconds/microseconds (000000 - 999999)
/// * %z - time zone differential in ISO 8601 format (Z or +NN.NN)
/// * %Z - time zone differential in RFC format (GMT or +NNNN)
/// * %E - epoch time (UTC, seconds since midnight, January 1, 1970)
/// * %[name] - the value of the message parameter with the given name
/// * %% - percent sign
{
public:
PatternFormatter();
/// Creates a PatternFormatter.
/// The format pattern must be specified with
/// a call to setProperty.
PatternFormatter(const std::string& format);
/// Creates a PatternFormatter that uses the
/// given format pattern.
~PatternFormatter();
/// Destroys the PatternFormatter.
void format(const Message& msg, std::string& text);
/// Formats the message according to the specified
/// format pattern and places the result in text.
void setProperty(const std::string& name, const std::string& value);
/// Sets the property with the given name to the given value.
///
/// The following properties are supported:
///
/// * pattern: The format pattern. See the PatternFormatter class
/// for details.
/// * times: Specifies whether times are adjusted for local time
/// or taken as they are in UTC. Supported values are "local" and "UTC".
///
/// If any other property name is given, a PropertyNotSupported
/// exception is thrown. std::string getProperty(const std::string& name) const;
/// Returns the value of the property with the given name or
/// throws a PropertyNotSupported exception if the given
/// name is not recognized.
static const std::string PROP_PATTERN;
static const std::string PROP_TIMES;
protected:
static const std::string& getPriorityName(int); /// Returns a string for the given priority value.
private:
bool _localTime;
std::string _pattern;
};
当然如果用户对已有的格式不满意,可以自己扩展。
2.7 Channel类
Channel类可以被看成为所有输出对象的抽象,它也是个抽像类。它继承自Configurable和RefCountedObject。继承自Configurable说明需要对配置信息进行一定的解析工作,继承自RefCountedObject说明其本身是个引用计数对象,会使用AutoPtr去管理。
其具体定义如下:
class Foundation_API Channel: public Configurable, public RefCountedObject
{
public:
Channel();
virtual void open();
virtual void close();
virtual void log(const Message& msg) = 0;
void setProperty(const std::string& name, const std::string& value);
std::string getProperty(const std::string& name) const;
protected:
virtual ~Channel();
private:
Channel(const Channel&);
Channel& operator = (const Channel&);
};
Poco内部实现了非常多的Channel子类,被用于向不同的目标输出日志信息。很多Channel是依赖于平台的,如EventLogChannel、SyslogChannel、OpcomChannel、WindowsConsoleChannel。它们都实现单一功能即向一个特殊的目标输出。
在Channel的子类中,比较特殊的有以下几个:
AsyncChannel:
AsyncChannel类是个主动对象,在内部包含一个Thread对象,通过内部NotificationQueue队列,完成了日志生成和输出的解耦。
SplitterChannel:
SplitterChannel类完成了一份消息,多份输出的工作。它本身是一个Channel类的容器。其定义如下:
class Foundation_API SplitterChannel: public Channel
/// This channel sends a message to multiple
/// channels simultaneously.
{
public:
SplitterChannel();
/// Creates the SplitterChannel.
void addChannel(Channel* pChannel);
/// Attaches a channel, which may not be null.
void removeChannel(Channel* pChannel);
/// Removes a channel.
void log(const Message& msg);
/// Sends the given Message to all
/// attaches channels.
void setProperty(const std::string& name, const std::string& value);
/// Sets or changes a configuration property.
///
/// Only the "channel" property is supported, which allows
/// adding a comma-separated list of channels via the LoggingRegistry.
/// The "channel" property is set-only.
/// To simplify file-based configuration, all property
/// names starting with "channel" are treated as "channel".
void close();
/// Removes all channels.
int count() const;
/// Returns the number of channels in the SplitterChannel.
protected:
~SplitterChannel();
private:
typedef std::vector<Channel*> ChannelVec;
ChannelVec _channels;
mutable FastMutex _mutex;
};
它的日志输出就是遍历所有的Channel对象,调用其输出。
void SplitterChannel::log(const Message& msg)
{
FastMutex::ScopedLock lock(_mutex);
for (ChannelVec::iterator it = _channels.begin(); it != _channels.end(); ++it)
{
(*it)->log(msg);
}
}
Logger:
Logger是个接口类,它主要有3个功能:
1. 它是一个Logger对象的工厂。调用静态函数get(const std::string& name)可以获得对应的Logger对象。
2. 它实现了日志逻辑上的继承体系。在其内部定义了一个静态变量_pLoggerMap。
static std::map<std::string, Logger*>* _pLoggerMap;
这个静态变量管理了所有的日志对象。
3. 用户接口
调用Logger对象的接口函数会触发其内部Channel对象的对应接口函数。比如说日志的记录动作:
void Logger::log(const Message& msg)
{
if (_level >= msg.getPriority() && _pChannel)
{
_pChannel->log(msg);
}
}
2.8 概述
应该说Poco库的日志功能实现的非常强大,同专门的日志库Logcpp相比也并不逊色。大家都知道,在Logcpp库中,category 、appender 和layout具有重要地位。做个对应比较的话:
Logcpp中layout类控制输出信息的格式和样式,相当于Poco中的Formater。
Logcpp中appender类用来输出信息到设备上,相当于Poco中的Channel。
Logcpp中category类为用户接口,可以附加任意appender,这相当于Poco中的Logger类。
在Poco库中,Logger和Channel的关系为包含关系,在Logcpp库中,category与appender同样也是,并且在两个库的实现上,其内部都使用了引用计数技术。对于这一点,大家想一想就明白,引用计数的开销最小。
如果说不同,同Logcpp相比,Poco库把消息单独抽象成Message类,在增加消息内容和扩展性的同时,也使Logger的输出接口变得稍复杂。