Introduction
Hello log!
Plog is a C++ logging library that is designed to be as simple, small and flexible as possible. It is created as an alternative to existing large libraries and provides some unique features as CSV log format and automatic 'this' pointer capture.
Here is a minimal hello log sample:
#include <plog/Log.h> // Step1: include the header.
int main()
{
plog::init(plog::debug, "Hello.txt");
LOGD << "Hello log!"; LOG_DEBUG << "Hello log!"; LOG(plog::debug) << "Hello log!";
return 0;
}
And its output:
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!
Features
- Very small (less than 1000 LOC)
- Easy to use
- Headers only
- No 3rd-party dependencies
- Cross-platform: Windows, Linux, Mac OS X, Android (gcc, clang, msvc)
- Thread and type safe
- Formatters: TXT, CSV, FuncMessage
- Appenders: RollingFile, Console, Android
- Automatic 'this' pointer capture (supported only on msvc)
- Lazy stream evaluation
- Unicode aware, files are stored in UTF8
- Doesn't require C++11
- Extendable
Usage
To start using plog you need to make 3 simple steps.
Step 1: Adding includes
At first your project needs to know about plog. For that you have to:
- Add
plog/inlcude
to the project include paths - Add
#include <plog/Log.h>
into your cpp/h files (if you have precompiled headers it is a good place to add this include there)
Step 2: Initialization
The next step is to initialize the Logger. This is done by the following plog::init
function:
Logger& init(Severity maxSeverity, const char/wchar_t* fileName, size_t maxFileSize = 0, int maxFiles = 0);
maxSeverity
is the logger severity upper limit. All log messages have its own severity and if it is higher than the limit those messages are dropped. Plog defines the following severity levels:
enum Severity
{
none = 0,
fatal = 1,
error = 2,
warning = 3,
info = 4,
debug = 5,
verbose = 6
};
The log format is determined automatically by fileName
file extension:
The rolling behavior is controlled by maxFileSize
and maxFiles
parameters:
maxFileSize
- the maximum log file size in bytes maxFiles
- a number of log files to keep
If one of them is zero then log rolling is disabled.
Sample:
plog::init(plog::warning, "c:\\logs\\log.csv", 1000000, 5);
Here the logger is initialized to write all messages with up to warning severity to a file in csv format. Maximum log file size is set to 1'000'000 bytes and 5 log files are kept.
Note: see Custom initialization for advanced usage.
Step 3: Logging
Logging is performed with the help of special macros. A log message is constructed using stream output operators <<
. Thus it is type-safe and extendable in contrast to a format string output.
Basic logging macros
This is the most used type of logging macros. They do unconditional logging.
Long macros:
LOG_VERBOSE << "verbose";
LOG_DEBUG << "debug";
LOG_INFO << "info";
LOG_WARNING << "warning";
LOG_ERROR << "error";
LOG_FATAL << "fatal";
Short macros:
LOGV << "verbose";
LOGD << "debug";
LOGI << "info";
LOGW << "warning";
LOGE << "error";
LOGF << "fatal";
Function-style macros:
LOG(severity) << "msg";
Conditional logging macros
These macros are used to do a conditional logging. They accept a condition as a parameter and perform logging if the condition is true.
Long macros:
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";
Short macros:
LOGV_IF(cond) << "verbose";
LOGD_IF(cond) << "debug";
LOGI_IF(cond) << "info";
LOGW_IF(cond) << "warning";
LOGE_IF(cond) << "error";
LOGF_IF(cond) << "fatal";
Function-style macros:
LOG_IF(severity, cond) << "msg";
Logger severity checker
In some cases there is a need to perform a group of actions depending on the current logger severity level. There is a special macro for that. It helps to minimize performance penalty when the logger is inactive.
IF_LOG(severity)
Sample:
IF_LOG(plog::debug) {
for (int i = 0; i < vec.size(); ++i)
{
LOGD << "vec[" << i << "]: " << vec[i];
}
}
Advanced usage
Changing severity at runtime
It is possible to set the maximum severity not only at the logger initialization time but at any time later. There are special accessor methods:
Severity Logger::getMaxSeverity() const;
Logger::setMaxSeverity(Severity severity);
To get the logger use plog::get
function:
Logger* get();
Sample:
plog::get()->setMaxSeverity(plog::debug);
Custom initialization
Non-typical log cases require the use of custom initialization. It is done by the following plog::init
function:
Logger& init(Severity maxSeverity = none, IAppender* appender = NULL);
You have to construct an Appender parameterized with a Formatter and pass it to the plog::init
function.
Note: a lifetime of the appender should be static!
Sample:
static plog::ConsoleAppender<plog::TxtFormatter> consoleAppender;
plog::init(plog::debug, &consoleAppender);
Multiple appenders
It is possible to have multiple Appenders within a single Logger. In such case log message will be written to all of them. Use the following method to accomplish that:
Logger& Logger::addAppender(IAppender* appender);
Sample:
static plog::RollingFileAppender<plog::CsvFormatter> fileAppender("MultiAppender.csv", 8000, 3); static plog::ConsoleAppender<plog::TxtFormatter> consoleAppender; plog::init(plog::debug, &fileAppender).addAppender(&consoleAppender);
Here the logger is initialized in the way when log messages are written to both a file and a console.
Refer to MultiAppender for a complete sample.
Multiple loggers
Multiple Loggers can be used simultaneously each with their own separate configuration. The Loggers differ by their instance number (that is implemented as a template parameter). The default instance is zero. Initialization is done by the appropriate template plog::init
functions:
Logger<instance>& init<instance>(...);
To get a logger use plog::get
function (returns NULL
if the logger is not initialized):
Logger<instance>* get<instance>();
All logging macros have their special versions that accept an instance parameter. These kind of macros have an underscore at the end:
LOGD_(instance) << "debug";
LOGD_IF_(instance, condition) << "conditional debug";
IF_LOG_(instance, severity)
Sample:
enum {
SecondLog = 1
};
int main()
{
plog::init(plog::debug, "MultiInstance-default.txt"); plog::init<SecondLog>(plog::debug, "MultiInstance-second.txt");
LOGD << "Hello default log!";
LOGD_(SecondLog) << "Hello second log!";
return 0;
}
Refer to MultiInstance for a complete sample.
Chained loggers
A Logger can work as an Appender for another Logger. So you can chain several loggers together. This is useful for streaming log messages from a shared library to the main application binary.
Sample:
extern "C" void EXPORT initialize(plog::Severity severity, plog::IAppender* appender)
{
plog::init(severity, appender); }
extern "C" void EXPORT foo()
{
LOGI << "Hello from shared lib!";
}
extern "C" void initialize(plog::Severity severity, plog::IAppender* appender);
extern "C" void foo();
int main()
{
plog::init(plog::debug, "ChainedApp.txt"); LOGD << "Hello from app!";
initialize(plog::debug, plog::get()); foo();
return 0;
}
Refer to Chained for a complete sample.
Architecture
Overview
Plog is designed to be small but flexible, so it prefers templates to interface inheritance. All main entities are shown on the following UML diagram:
There are 5 functional parts:
- Logger - the main object, implemented as singleton
- Record - keeps log data: time, message, etc
- Appender - represents a log data destination: file, console, etc
- Formatter - formats log data into a string
- Converter - converts formatter output into a raw buffer
The log data flow is shown below:
Logger
Logger is a center object of the whole logging system. It is a singleton and thus it forms a known single entry point for configuration and processing log data. Logger can act as Appender for another Logger because it implements IAppender
interface. Also there can be several independent loggers that are parameterized by an integer instance number. The default instance is 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 stores all log data. It includes:
- time
- severity
- thread id
- 'this' pointer (if a log message is written from within an object)
- source line
- function name
- message
Also Record has a number of overloaded stream output operators to construct a message.
class Record
{
public:
Record(Severity severity, const char* func, size_t line, const void* object);
Record& operator<<(char data);
Record& operator<<(wchar_t data);
template<typename T>
Record& operator<<(const T& data);
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;
};
See Stream improvements over std::ostream.
Refer to Demo sample to see what can be written to the log stream.
Formatter is responsible for formatting log data from Record into various string representations (binary forms can be used too). There is no base class for formatters, they are implemented as classes with static functions format
and header
:
class Formatter
{
public:
static util::nstring header();
static util::nstring format(const Record& record);
};
See How to implement a custom formatter.
This is a classic log format available in almost any log library. It is good for console output and it is easy to read without any tools.
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]
This is the most powerful log format. It can be easily read without any tools (but slighlty harder than TXT format) and can be heavily analyzed if it is opened with a CSV-aware tool (like Excel). One rows can be highlighted according to their cell values, another rows can be hidden, columns can be manipulated and you can even run SQL queries on log data! This is a recommended format if logs are big and require heavy analysis. Also 'this' pointer is shown so object instances can be told apart.
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;
Note: message size is limited to 32000 chars.
This format is designed to be used with appenders that provide their own timestamps (like AndroidAppender or 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
Converter is responsible for conversion of Formatter output data to a raw buffer (represented as std::string
). It is used by RollingFileAppender to perform a conversion before writing to a file. There is no base class for converters, they are implemented as classes with static functions convert
and header
:
class Converter
{
public:
static std::string header(const util::nstring& str);
static std::string convert(const util::nstring& str);
};
See How to implement a custom converter.
UTF8Converter
UTF8Converter is the only converter available in plog out of the box. It converts string data to UTF-8 with BOM.
Appender
Appender uses Formatter and Converter to get a desired representation of log data and outputs (appends) it to a file/console/etc. All appenders must implement IAppender
interface (the only interface in plog):
class IAppender
{
public:
virtual ~IAppender();
virtual void write(const Record& record) = 0;
};
See How to implement a custom appender.
RollingFileAppender
This appender outputs log data to a file with rolling behaviour. As template parameters it accepts both Formatter and Converter.
RollingFileAppender<Formatter, Converter>::RollingFileAppender(const char* fileName, size_t maxFileSize = 0, int maxFiles = 0);
fileName
- a log file name maxFileSize
- the maximum log file size in bytes maxFiles
- a number of log files to keep
If maxFileSize
or maxFiles
is 0 then rolling behaviour is turned off.
The sample file names produced by this appender:
- mylog.log <== current log file (size < maxFileSize)
- mylog.1.log <== previous log file (size >= maxFileSize)
- mylog.2.log <== previous log file (size >= maxFileSize)
Note: the lowest maxFileSize is 1000 bytes.
Note: a log file is created on the first log message.
ConsoleAppender
This appender outputs log data to stdout
. As a template parameter it accepts Formatter.
ConsoleAppender<Formatter>::ConsoleAppender();
AndroidAppender
AndroidAppender uses Android logging system to output log data. It can be viewed with logcat or in a log window of Android IDEs. As a template parameter this appender accepts Formatter (usually FuncMessageFormatter).
AndroidAppender<Formatter>::AndroidAppender(const char* tag);
Miscellaneous notes
Lazy stream evaluation
Log messages are constructed using lazy stream evaluation. It means that if a log message will be dropped (because of its severity) then stream output operators are not executed. Thus performance penalty of unprinted log messages is negligible.
LOGD << ...
Stream improvements over std::ostream
Stream output in plog has several improvements over the standard std::ostream
:
- handles wide chars/strings:
wchar_t
, wchar_t*
, std::wstring
- handles
NULL
values for C-strings: char*
and wchar_t*
- implicitly casts objects to:
std::string
and std::wstring
(if they have an appropriate cast operator)
Automatic 'this' pointer capture
'This' pointer is captured automatically to log data and can be printed by CsvFormatter. Unfortunately this feature is supported only on msvc 2010 and higher.
The core plog functionality is provided by inclusion of plog/Log.h
file. Extra components require inclusion of corresponding extra headers after plog/Log.h
.
Unicode
Plog is unicode aware and wide string friendly. All messages are converted to a system native char type:
wchar_t
- on Windows char
- on all other systems
Also char
is treated as:
- active code page - on Windows
- UTF-8 - on all other systems
Internally plog uses nstring
and nstringstream
('n' for native) that are defined as:
#ifdef _WIN32
typedef std::wstring nstring;
typedef std::wstringstream nstringstream;
#else
typedef std::string nstring;
typedef std::stringstream nstringstream;
#endif
By default all log files are stored in UTF-8 with BOM thanks to UTF8Converter.
Note: on Android wide string support in plog is disabled.
Plog is not using any asynchronous techniques so it may slow down your application on large volumes of log messages.
Producing a single log message takes the following amount of time:
CPU | OS | Time per a log call, microsec |
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 |
Assume 20 microsec per a log call then 500 log calls per a second will slow down an application by 1%. It is acceptable for the most use cases.
Refer to Performance for a complete sample.
Extending
Plog can be easily extended to support new:
Custom data type
To output a custom data type to a log message implement the following function:
namespace plog
{
Record& operator<<(Record& record, const MyType& t);
}
Refer to CustomType for a complete sample.
Custom appender
A custom appender must implement IAppender
interface. Also it may accept Formatter and Converter as template parameters however this is optional.
namespace plog
{
template<class Formatter>
class MyAppender : public IAppender
{
public:
virtual void write(const Record& record);
};
}
Refer to CustomAppender for a complete sample.
A formatter that is compatible with existing appenders must be a class with 2 static methods:
header
- returns a header for a new log format
- formats Record to a string
namespace plog
{
class MyFormatter
{
public:
static util::nstring header();
static util::nstring format(const Record& record);
};
}
Refer to CustomFormatter for a complete sample.
Custom converter
A converter must be a class with 2 static methods:
header
- converts a header for a new log convert
- converts log messages
namespace plog
{
class MyConverter
{
public:
static std::string header(const util::nstring& str);
static std::string convert(const util::nstring& str);
};
}
Refer to CustomConverter for a complete sample.
Samples
There are a number of samples that demonstrate various aspects of using plog. They can be found in the samples folder:
Sample | Description |
Android | Shows how to use the android-specific appender. |
Chained | Shows how to chain a logger in a shared library with the main logger (route messages). |
Library | Shows plog usage in static libraries. |
Hello | A minimal introduction sample, shows the basic 3 steps to start using plog. |
MultiAppender | Shows how to use multiple appenders with the same logger. |
MultiInstance | Shows how to use multiple logger instances, each instance has its own independent configuration. |
ObjectiveC | Shows that plog can be used in ObjectiveC++. |
Demo | Demonstrates log stream abilities, prints various types of messages. |
CustomAppender | Shows how to implement a custom appender that stores log messages in memory. |
CustomFormatter | Shows how to implement a custom formatter. |
CustomConverter | Shows how to implement a custom converter that encrypts log messages. |
CustomType | Shows how to print a custom type to the log stream. |
Facilities | Shows how to use logging per facilities via multiple logger instances (useful for big projects). |
Performance | Measures time per a log call. |
GitHub
The project is available on GitHub: https://github.com/SergiusTheBest/plog
References
Competing C++ log libraries
License
Plog is licensed under the MPL version 2.0. You can freely use it in your commercial or opensource software. The latest source code is available on https://github.com/SergiusTheBest/plog.