POCO C++库学习和分析 -- 日志 (二)

时间:2021-11-22 05:43:03

POCO C++库学习和分析 -- 日志 (二)


2. Poco日志的实现

2.1 日志模块应该实现的业务

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

         对于日志模块的业务就讨论到这里,还是回到Poco的日志模块上。首先来看一张Poco 日志模块的类图:

POCO C++库学习和分析 -- 日志 (二)


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