POCO C++库学习和分析 -- 日志 (二)
2. Poco日志的实现
2.1 日志模块应该实现的业务
在讨论日志的实现之前,先来聊一下日志模块应该实现那些业务。日志的业务说简单可以很简单,就是输出记录。说复杂也复杂,来看它的复杂性:首先,日志的输出对象是不同的,有控制台输出,本地文件输出,网络文件输出,输出到系统日志等。假如是网络日志,日志库中其实还会包含网络模块,真是越来越复杂了。
第二,日志输出的格式和内容。不同用户关心的内容和喜欢的输出格式是不同的,要满足所有人的需求,就必须能够提供全面的信息,并提供选项供用户选择。
第三,日志的级别。程序的日志一定是需要动态可调的。程序日志过多,消耗资源;日志过少,无法提供足够的信息,用来定位和解决问题。
第四,日志的存储策略。日志是具有实效性的,日志保存的时间越久,信息熵越低;日志存储也是需要成本的,大量的日志会挤占硬盘空间,所以需要对日志的存储进行管理。超过一定时间的日志可以考虑删除。在磁盘资源紧张的情况下,必须考虑控制日志的大小。
第五,日志是用来查询和排除问题的。为了能够快速的定位问题,最好能够把日志按照模块输出,这就要求日志库设计的时候考虑日志模块的分类。
第六,这一点和日志的业务无关,和库的实现相关。跨平台的话,必须考虑操作系统底层API的不同。
对于日志模块的业务就讨论到这里,还是回到Poco的日志模块上。首先来看一张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的输出接口变得稍复杂。
(版权所有,转载时请注明作者和出处 http://blog.csdn.net/arau_sh/article/details/8809799)