现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

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

2019-08-03 07:44 工业·编程 ⁄ 共 11523字 ⁄ 字号 暂无评论

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的输出接口变得稍复杂。

给我留言

留言无头像?