Plog - 简洁便携的 C++ 日志库






4.85/5 (33投票s)
不到 1000 行代码,功能强大的日志库。
引言
Hello log!
Plog 是一个 C++ 日志库,旨在尽可能做到简洁、小巧和灵活。它是现有大型库的替代品,并提供了一些独特的功能,例如 CSV 日志格式和 自动 'this' 指针捕获。
下面是一个最简单的 hello log 示例
#include <plog/Log.h> // Step1: include the header.
int main()
{
plog::init(plog::debug, "Hello.txt"); // Step2: initialize the logger.
// Step3: write log messages using a special macro.
// There are several log macros, use the macro you liked the most.
LOGD << "Hello log!"; // short macro
LOG_DEBUG << "Hello log!"; // long macro
LOG(plog::debug) << "Hello log!"; // function-style macro
return 0;
}
及其输出
2015-05-18 23:12:43.921 DEBUG [21428] [main@13] Hello log! 2015-05-18 23:12:43.968 DEBUG [21428] [main@14] Hello log! 2015-05-18 23:12:43.968 DEBUG [21428] [main@15] Hello log!
特点
- 体积小巧(不到 1000 行代码)
- 易于使用
- 仅头文件
- 无第三方依赖
- 跨平台:Windows、Linux、Mac OS X、Android(gcc、clang、msvc)
- 线程安全、类型安全
- 格式化器:TXT、CSV、FuncMessage
- Appender:RollingFile、Console、Android
- 自动 'this' 指针捕获(仅支持 msvc)
- 惰性流求值
- 支持 Unicode,文件以 UTF8 格式存储
- 不需要 C++11
- 可扩展
用法
要开始使用 plog,您需要完成 3 个简单的步骤。
步骤 1:添加包含
首先,您的项目需要了解 plog。为此,您需要
- 将 `plog/include` 添加到项目包含路径中
- 在您的 cpp/h 文件中添加 `#include
`(如果您有预编译头文件,这是一个很好的添加位置)
步骤 2:初始化
下一步是初始化 Logger。这通过以下 `plog::init` 函数完成
Logger& init(Severity maxSeverity, const char/wchar_t* fileName, size_t maxFileSize = 0, int maxFiles = 0);
`maxSeverity` 是日志级别的上限。所有日志消息都有自己的级别,如果其级别高于限制,则这些消息将被丢弃。Plog 定义了以下日志级别
enum Severity
{
none = 0,
fatal = 1,
error = 2,
warning = 3,
info = 4,
debug = 5,
verbose = 6
};
日志格式由 `fileName` 文件的扩展名自动确定
滚动行为由 `maxFileSize` 和 `maxFiles` 参数控制
- `maxFileSize` - 日志文件的最大大小(字节)
- `maxFiles` - 要保留的日志文件数量
如果其中任何一个为零,则禁用日志滚动。
示例
plog::init(plog::warning, "c:\\logs\\log.csv", 1000000, 5);
在此,日志记录器被初始化为将所有警告及以下级别的消息以 csv 格式写入文件。日志文件最大大小设置为 1,000,000 字节,并保留 5 个日志文件。
注意:有关高级用法,请参阅 自定义初始化。
步骤 3:日志记录
日志记录是通过特殊宏实现的。日志消息使用流输出运算符 `<<` 构建。因此,它与格式字符串输出相比,是类型安全且可扩展的。
基础日志宏
这是最常用的日志宏类型。它们进行无条件日志记录。
长宏
LOG_VERBOSE << "verbose";
LOG_DEBUG << "debug";
LOG_INFO << "info";
LOG_WARNING << "warning";
LOG_ERROR << "error";
LOG_FATAL << "fatal";
短宏
LOGV << "verbose";
LOGD << "debug";
LOGI << "info";
LOGW << "warning";
LOGE << "error";
LOGF << "fatal";
函数式宏
LOG(severity) << "msg";
条件日志宏
这些宏用于进行条件日志记录。它们接受一个条件作为参数,并在条件为真时执行日志记录。
长宏
LOG_VERBOSE_IF(cond) << "verbose";
LOG_DEBUG_IF(cond) << "debug";
LOG_INFO_IF(cond) << "info";
LOG_WARNING_IF(cond) << "warning";
LOG_ERROR_IF(cond) << "error";
LOG_FATAL_IF(cond) << "fatal";
短宏
LOGV_IF(cond) << "verbose";
LOGD_IF(cond) << "debug";
LOGI_IF(cond) << "info";
LOGW_IF(cond) << "warning";
LOGE_IF(cond) << "error";
LOGF_IF(cond) << "fatal";
函数式宏
LOG_IF(severity, cond) << "msg";
日志级别检查器
在某些情况下,需要根据当前的日志级别执行一组操作。为此有一个特殊的宏。它有助于在日志记录器不活跃时最小化性能开销。
IF_LOG(severity)
示例
IF_LOG(plog::debug) // we want to execute the following statements only at debug severity (and higher)
{
for (int i = 0; i < vec.size(); ++i)
{
LOGD << "vec[" << i << "]: " << vec[i];
}
}
高级用法
运行时更改日志级别
不仅可以在日志记录器初始化时设置最大日志级别,也可以在之后随时设置。有特殊的访问器方法
Severity Logger::getMaxSeverity() const;
Logger::setMaxSeverity(Severity severity);
要获取日志记录器,请使用 `plog::get` 函数
Logger* get();
示例
plog::get()->setMaxSeverity(plog::debug);
自定义初始化
非典型日志案例需要使用自定义初始化。这通过以下 `plog::init` 函数完成
Logger& init(Severity maxSeverity = none, IAppender* appender = NULL);
您需要构造一个 Appender,并用 Formatter 进行参数化,然后将其传递给 `plog::init` 函数。
注意:Appender 的生命周期应该是静态的!
示例
static plog::ConsoleAppender<plog::TxtFormatter> consoleAppender;
plog::init(plog::debug, &consoleAppender);
多 Appender
单个 Logger 中可以有多个 Appenders。在这种情况下,日志消息将写入所有 Appender。使用以下方法来实现此目的
Logger& Logger::addAppender(IAppender* appender);
示例
static plog::RollingFileAppender<plog::CsvFormatter> fileAppender("MultiAppender.csv", 8000, 3); // Create the 1st appender.
static plog::ConsoleAppender<plog::TxtFormatter> consoleAppender; // Create the 2nd appender.
plog::init(plog::debug, &fileAppender).addAppender(&consoleAppender); // Initialize the logger with the both appenders.
在此,日志记录器被初始化为将日志消息同时写入文件和控制台。
有关完整的示例,请参阅 MultiAppender。
多 Logger
可以同时使用多个 Loggers,每个 Logger 都有其自己的独立配置。 Loggers 通过其实例编号(实现为模板参数)进行区分。默认实例为零。通过相应的模板 `plog::init` 函数完成初始化
Logger<instance>& init<instance>(...);
要获取 Logger,请使用 `plog::get` 函数(如果 Logger 未初始化,则返回 `NULL`)
Logger<instance>* get<instance>();
所有日志宏都有其特殊版本,接受实例参数。这类宏在末尾有一个下划线
LOGD_(instance) << "debug";
LOGD_IF_(instance, condition) << "conditional debug";
IF_LOG_(instance, severity)
示例
enum // Define log instances. Default is 0 and is omitted from this enum.
{
SecondLog = 1
};
int main()
{
plog::init(plog::debug, "MultiInstance-default.txt"); // Initialize the default logger instance.
plog::init<SecondLog>(plog::debug, "MultiInstance-second.txt"); // Initialize the 2nd logger instance.
// Write some messages to the default log.
LOGD << "Hello default log!";
// Write some messages to the 2nd log.
LOGD_(SecondLog) << "Hello second log!";
return 0;
}
有关完整的示例,请参阅 MultiInstance。
链式 Logger
Logger 可以作为另一个 Logger 的 Appender 工作。因此,您可以将多个 Logger 链接在一起。这对于将日志消息从共享库流式传输到主应用程序二进制文件很有用。
示例
// shared library
// Function that initializes the logger in the shared library.
extern "C" void EXPORT initialize(plog::Severity severity, plog::IAppender* appender)
{
plog::init(severity, appender); // Initialize the shared library logger.
}
// Function that produces a log message.
extern "C" void EXPORT foo()
{
LOGI << "Hello from shared lib!";
}
// main app
// Functions imported form the shared library.
extern "C" void initialize(plog::Severity severity, plog::IAppender* appender);
extern "C" void foo();
int main()
{
plog::init(plog::debug, "ChainedApp.txt"); // Initialize the main logger.
LOGD << "Hello from app!"; // Write a log message.
initialize(plog::debug, plog::get()); // Initialize the logger in the shared library. Note that it has its own severity.
foo(); // Call a function from the shared library that produces a log message.
return 0;
}
有关完整的示例,请参阅 Chained。
架构
概述
Plog 的设计旨在小巧而灵活,因此它偏爱模板而不是接口继承。所有主要实体都显示在以下 UML 图中
有 5 个功能部分
- Logger - 主要对象,实现为单例
- Record - 存储日志数据:时间、消息等
- Appender - 表示日志数据目的地:文件、控制台等
- Formatter - 将日志数据格式化为字符串
- Converter - 将 Formatter 的输出转换为原始缓冲区
日志数据流如下所示
Logger
Logger 是整个日志系统的中心对象。它是一个单例,因此它形成了配置和处理日志数据的已知单一入口点。 Logger 可以充当另一个 Logger 的 Appender,因为它实现了 `IAppender` 接口。此外,还可以有多个独立的 Logger,它们通过整数实例编号进行参数化。默认实例为 0。
template<int instance>
class Logger : public util::Singleton<Logger<instance> >, public IAppender
{
public:
Logger(Severity maxSeverity = none);
Logger& addAppender(IAppender* appender);
Severity getMaxSeverity() const;
void setMaxSeverity(Severity severity);
bool checkSeverity(Severity severity) const;
virtual void write(const Record& record);
void operator+=(const Record& record);
};
Record
Record 存储所有日志数据。它包括
- 时间
- 日志级别
- 线程 ID
- 'this' 指针(如果日志消息是从对象内部写入的)
- 源文件名
- 源函数名
- message
此外,Record 具有一组重载的流输出运算符来构造消息。
class Record
{
public:
Record(Severity severity, const char* func, size_t line, const void* object);
//////////////////////////////////////////////////////////////////////////
// Stream output operators
Record& operator<<(char data);
Record& operator<<(wchar_t data);
template<typename T>
Record& operator<<(const T& data);
//////////////////////////////////////////////////////////////////////////
// Getters
const util::Time& getTime() const;
Severity getSeverity() const;
unsigned int getTid() const;
const void* getObject() const;
size_t getLine() const;
const util::nstring getMessage() const;
std::string getFunc() const;
};
请参阅 流的 std::ostream 改进。
有关日志流可以写入的内容,请参阅 Demo 示例。
Formatter
Formatter 负责将 Record 中的日志数据格式化为各种字符串表示形式(也可以使用二进制形式)。Formatter 没有基类,它们被实现为带有静态函数 `format` 和 `header` 的类
class Formatter
{
public:
static util::nstring header();
static util::nstring format(const Record& record);
};
有关如何实现自定义 Formatter,请参阅 如何实现自定义 Formatter。
TxtFormatter
这是几乎所有日志库中都存在的经典日志格式。它非常适合控制台输出,并且无需任何工具即可轻松阅读。
2014-11-11 00:29:06.245 FATAL [4460] [main@22] fatal
2014-11-11 00:29:06.261 ERROR [4460] [main@23] error
2014-11-11 00:29:06.261 INFO [4460] [main@24] info
2014-11-11 00:29:06.261 WARN [4460] [main@25] warning
2014-11-11 00:29:06.261 DEBUG [4460] [main@26] debug
2014-11-11 00:29:06.261 INFO [4460] [main@32] This is a message with "quotes"!
2014-11-11 00:29:06.261 DEBUG [4460] [Object::Object@8]
2014-11-11 00:29:06.261 DEBUG [4460] [Object::~Object@13]
CsvFormatter
这是功能最强大的日志格式。无需任何工具即可轻松阅读(但比 TXT 格式 稍困难),并且如果使用支持 CSV 的工具(如 Excel)打开,可以进行大量分析。可以根据单元格值突出显示某一行,隐藏其他行,操作列,甚至可以在日志数据上运行 SQL 查询!如果日志量大且需要大量分析,则推荐使用此格式。此外,还会显示 'this' 指针,以便区分对象实例。
Date;Time;Severity;TID;This;Function;Message
2014/11/14;15:22:25.033;FATAL;4188;00000000;main@22;"fatal"
2014/11/14;15:22:25.033;ERROR;4188;00000000;main@23;"error"
2014/11/14;15:22:25.033;INFO;4188;00000000;main@24;"info"
2014/11/14;15:22:25.033;WARN;4188;00000000;main@25;"warning"
2014/11/14;15:22:25.048;DEBUG;4188;00000000;main@26;"debug"
2014/11/14;15:22:25.048;INFO;4188;00000000;main@32;"This is a message with ""quotes""!"
2014/11/14;15:22:25.048;DEBUG;4188;002EF4E3;Object::Object@8;
2014/11/14;15:22:25.048;DEBUG;4188;002EF4E3;Object::~Object@13;
注意:消息大小限制为 32000 字符。
FuncMessageFormatter
此格式旨在与提供自身时间戳的 Appender 一起使用(例如 AndroidAppender 或 linux syslog facility)。
main@22: fatal
main@23: error
main@24: info
main@25: warning
main@26: debug
main@32: This is a message with "quotes"!
Object::Object@8:
Object::~Object@13:
转换器
Converter 负责将 Formatter 的输出数据转换为原始缓冲区(表示为 `std::string`)。RollingFileAppender 在写入文件之前使用它进行转换。Converter 没有基类,它们被实现为带有静态函数 `convert` 和 `header` 的类
class Converter
{
public:
static std::string header(const util::nstring& str);
static std::string convert(const util::nstring& str);
};
有关如何实现自定义 Converter,请参阅 如何实现自定义 Converter。
UTF8Converter
UTF8Converter 是 plog 开箱即用的唯一 Converter。它将字符串数据转换为带 BOM 的 UTF-8。
Appender
Appender 使用 Formatter 和 Converter 来获得所需的日志数据表示,并将其输出(附加)到文件/控制台/等。所有 Appender 都必须实现 `IAppender` 接口(plog 中唯一的接口)
class IAppender
{
public:
virtual ~IAppender();
virtual void write(const Record& record) = 0;
};
有关如何实现自定义 Appender,请参阅 如何实现自定义 Appender。
RollingFileAppender
此 Appender 将日志数据输出到具有滚动行为的文件。作为模板参数,它接受 Formatter 和 Converter。
RollingFileAppender<Formatter, Converter>::RollingFileAppender(const char* fileName, size_t maxFileSize = 0, int maxFiles = 0);
- `fileName` - 日志文件名
- `maxFileSize` - 日志文件的最大大小(字节)
- `maxFiles` - 要保留的日志文件数量
如果 `maxFileSize` 或 `maxFiles` 为 0,则滚动行为被关闭。
此 Appender 生成的示例文件名
- mylog.log <== 当前日志文件(大小 < maxFileSize)
- mylog.1.log <== 前一个日志文件(大小 >= maxFileSize)
- mylog.2.log <== 前一个日志文件(大小 >= maxFileSize)
注意:最小 `maxFileSize` 为 1000 字节。
注意:日志文件在第一个日志消息时创建。
ConsoleAppender
此 Appender 将日志数据输出到 `stdout`。它接受 Formatter 作为模板参数。
ConsoleAppender<Formatter>::ConsoleAppender();
AndroidAppender
AndroidAppender 使用 Android 日志系统输出日志数据。它可以通过 logcat 或 Android IDE 的日志窗口查看。作为模板参数,此 Appender 接受 Formatter(通常是 FuncMessageFormatter)。
AndroidAppender<Formatter>::AndroidAppender(const char* tag);
其他说明
惰性流求值
日志消息使用惰性流求值来构建。这意味着如果一条日志消息将被丢弃(因为它的大小),那么流输出运算符将不会被执行。因此,未打印日志消息的性能开销可以忽略不计。
LOGD << /* the following statements will be executed only when the logger severity is debug or higher */ ...
流的 std::ostream 改进
Plog 中的流输出相比标准的 `std::ostream` 有一些改进
- 处理宽字符/字符串:`wchar_t`、`wchar_t*`、`std::wstring`
- 处理 C 字符串的 `NULL` 值:`char*` 和 `wchar_t*`
- 隐式将对象转换为:`std::string` 和 `std::wstring`(如果它们具有适当的转换运算符)
自动捕获 'this' 指针
'this' 指针会自动捕获到日志数据中,并且可以被 CsvFormatter 打印。不幸的是,此功能仅支持 msvc 2010 及更高版本。
需要包含的头文件
核心 plog 功能通过包含 `plog/Log.h` 文件提供。额外的组件需要包含相应的额外头文件,放在 `plog/Log.h` 之后。
Unicode
Plog 支持 Unicode 且对宽字符串友好。所有消息都转换为系统原生的字符类型
- `wchar_t` - 在 Windows 上
- `char` - 在所有其他系统上
此外,`char` 被视为
- 活动代码页 - 在 Windows 上
- UTF-8 - 在所有其他系统上
内部,plog 使用 `nstring` 和 `nstringstream`('n' 代表原生),它们定义为
#ifdef _WIN32
typedef std::wstring nstring;
typedef std::wstringstream nstringstream;
#else
typedef std::string nstring;
typedef std::stringstream nstringstream;
#endif
默认情况下,所有日志文件均以带 BOM 的 UTF-8 格式存储,这归功于 UTF8Converter。
注意:在 Android 上,plog 中的宽字符串支持被禁用。
性能
Plog 不使用任何异步技术,因此在大批量日志消息时可能会减慢您的应用程序。
生成单个日志消息所需的时间
CPU | 操作系统 | 每次日志调用的时间(微秒) |
---|---|---|
AMD Phenom II 1055T @3.5GHz | Windows 2008 R2 | 12 |
AMD Phenom II 1055T @3.5GHz | Linux Mint 17.1 | 8 |
Intel Core i3-3120M @2.5GHz | Windows 2012 R2 | 25 |
Intel Core i5-2500K @4.2GHz | Windows 2008 R2 | 8 |
Intel Atom N270 @1.6GHz | Windows 2003 | 68 |
假设每次日志调用花费 20 微秒,那么每秒 500 次日志调用将使应用程序减慢 1%。对于大多数用例来说,这是可以接受的。
有关完整的示例,请参阅 Performance。
扩展
Plog 可以轻松扩展以支持新的
自定义数据类型
要将自定义数据类型输出到日志消息,请实现以下函数
namespace plog
{
Record& operator<<(Record& record, const MyType& t);
}
有关完整的示例,请参阅 CustomType。
自定义 Appender
自定义 Appender 必须实现 `IAppender` 接口。它还可以接受 Formatter 和 Converter 作为模板参数,但这并非强制。
namespace plog
{
template<class Formatter>
class MyAppender : public IAppender
{
public:
virtual void write(const Record& record);
};
}
有关完整的示例,请参阅 CustomAppender。
自定义 Formatter
与现有 Appender 兼容的 Formatter 必须是一个具有 2 个静态方法的类
- `header` - 返回新日志的标题
- `format` - 将 Record 格式化为字符串
namespace plog
{
class MyFormatter
{
public:
static util::nstring header();
static util::nstring format(const Record& record);
};
}
有关完整的示例,请参阅 CustomFormatter。
自定义 Converter
Converter 必须是一个具有 2 个静态方法的类
- `header` - 为新日志转换标题
- `convert` - 转换日志消息
namespace plog
{
class MyConverter
{
public:
static std::string header(const util::nstring& str);
static std::string convert(const util::nstring& str);
};
}
有关完整的示例,请参阅 CustomConverter。
示例
有许多示例演示了 plog 的各种用法。它们可以在 `samples` 文件夹中找到
示例 | 描述 |
---|---|
Android | 展示了如何使用特定于 android 的 Appender。 |
Chained | 展示了如何在共享库中的 Logger 与主 Logger 之间进行链接(路由消息)。 |
库 | 展示了在静态库中使用 plog。 |
Hello | 一个最小的入门示例,展示了开始使用 plog 的基本 3 个步骤。 |
MultiAppender | 展示了如何与同一个 Logger 一起使用多个 Appender。 |
MultiInstance | 展示了如何使用多个 Logger 实例,每个实例都有自己的独立配置。 |
ObjectiveC | 展示了 plog 可以在 ObjectiveC++ 中使用。 |
演示 | 演示了日志流的功能,打印各种类型的消息。 |
CustomAppender | 展示了如何实现一个将日志消息存储在内存中的自定义 Appender。 |
CustomFormatter | 展示了如何实现一个自定义 Formatter。 |
CustomConverter | 展示了如何实现一个加密日志消息的自定义 Converter。 |
CustomType | 展示了如何将自定义类型输出到日志流。 |
Facilities | 展示了如何通过多个 Logger 实例按设施使用日志(适用于大型项目)。 |
性能 | 测量每次日志调用的时间。 |
GitHub
该项目可在 GitHub 上找到:https://github.com/SergiusTheBest/plog
参考文献
竞争的 C++ 日志库
工具和有用信息
许可证
Plog 在 MPL 2.0 许可下发布。您可以在商业或开源软件中自由使用它。最新的源代码可在 https://github.com/SergiusTheBest/plog 上获取。