Boost Logging v2
Hi all,
It's been a while - I admit, and I apologize for it. Well, finally,
I'm working on version 2 of Boost Logging, taking into account your
suggestions when the first version was rejected.
The extra short version: It's in alpha stage. Get it from here:
http://torjo.com/log2/
Without further ado, I'll present you Boost Logging Library v2, with
- a much nicer separation of concepts and
- a much more flexible interface.
- you don't pay for what you don't use.
- fits a lot of scenarios:
from very simple (dumping all to one log)
to very complex (multiple logs, some enabled/some not, levels, etc).
- you can now define your own LOG_ macros
Here are 2 small examples to show you a bit of v2's power:
----------------- Example 1:
struct write_to_cout_and_file { ... };
// defining the logs
logger<write_to_cout_and_file, manage_enabled_no_ts>
g_single_log("out.txt");
#define L_(x) if ( g_single_log) g_single_log.process_msg()(x)
// code
L_("reading word " + word);
----------------- Example 2:
struct write_to_file { ... };
struct write_to_cout { ... };
struct write_to_dbg { ... };
// defining the logs
typedef process_msg< ostream_like_return_str<>, write_to_cout>
processor_cout;
typedef process_msg< ostream_like_return_str<>, write_to_file>
processor_file;
typedef process_msg< ostream_like_return_str<>, write_to_dbg>
processor_dbg;
logger<processor_cout, manage_enabled_no_ts> g_log_app;
logger<processor_file, manage_enabled_no_ts> g_log_err("err.txt");
logger<processor_dbg, manage_enabled_no_ts> g_log_dbg;
#define LAPP_ if ( !g_log_app) ; else g_log_app-
read_msg().gather().out()
#define LERR_ if ( !g_log_err) ; else g_log_err-
read_msg().gather().out()
#define LDBG_ if ( !g_log_dbg) ; else g_log_dbg-
read_msg().gather().out()
// code
LAPP_ << idx << " : reading word " << word;
LERR_ << "error at " << idx << ", while reading " << word;
LDBG_ << "debug info: " << idx << ", reading " << word;
-----------------
The above was the short version. You can also look at the test_*.cpp
files, to see some more examples of how you can use the library.
Download it here : http://torjo.com/log2/
Long version follows:
First, a bit about the workflow:
Step 1: Filtering the message
Step 2: Gathering the message
Step 3: Writing the message
Step 1: Filtering the message
In order to be efficient, before anything happens to the message,
we see if the log it's written to, is enabled.
If so, we process the message.
If not, we ignore all of the message.
This is rather easy, with constructs like this:
#define L_(x) if ( g_single_log) g_single_log.process_msg()(x)
#define L_ if ( !g_log_app) ; else g_log_app-
read_msg().gather().out()
Step 2: Gathering the message
Now, that we know the log is enabled, all the message is gathered.
This depends on how you write the message. For instance, you can
gather it in one step, if you want to offer a syntax like this:
L_("this is my message");
Or, you can offer syntactic sugar:
i = 100;
L_ << "this is " << i << " times better that the average bear... ";
In this case, you'll gather all info in a stream of some kind.
Or, you can have more data besides the message string, like: module
name, severity of message, category, etc:
L_(fatal,"chart","Can't find the ChartX control");
Step 3: Writing the message
Now all info about the message is gathered. You can write it to its
destination(s).
What you wanna do with a message, is completely up to you. The library
will offer you possibilities.
You can go for the extremely easy way, and just have a functor which
dumps the message somewhere, like this:
struct write_to_cout {
void operator()(const std::string & msg) const {
std::cout << msg << std::endl ;
}
};
Or:
struct write_to_file {
typedef boost::shared_ptr<std::ofstream> ptr;
write_to_file(const std::string & filename) : m_out(new
std::ofstream(filename.c_str())) {}
void operator()(const std::string & msg) const {
(*m_out) << msg << std::endl ;
}
mutable ptr m_out;
};
Or, you can choose to Format and/or write your message to Destinations
(these will be explained later)
Download it here : http://torjo.com/log2/
------------------------------------------------------
Logger objects
For each log you want, you'll have a logger object. Note that "log" is
a concept - several logs can write to the same file and/or one log can
write to multiple destinations. I use the term "log" to identify a
type of log message you want to write -- for example, you can have a
"debug log" for writing debug messages; an "info" log, for writing
informations, and so on. The fact that they might end up writing to
the same destinations, is completely transparent to the user, as it
should.
A logger object contains 2 things:
- a filter: an is_enabled() function - to find out if it's enabled.
- a process_msg() object - this contains the logic of what to do with
the message, if the log is enabled.
The 2 concepts are orthogonal - Andrei would call them "policies" ;)
The process_msg() is the object that will process the message if the
log is enabled - it executes steps 2 and 3.
How this happens, is completely up to you - when you define your
macros (more on this later).
------------------------------------------------------
Filtering and Log Levels
I have implented several filters: see filter.hpp file:
- manage_enabled_no_ts - filter (not thread-safe)
- always_enabled - enable logs at compile time
- always_disabled - disable logs at compile time
- debug_enabled, release_enabled
- manage_enabled_ts - thread-safe filter
- etc.
Using v2, you don't really have to use Log Levels, if you don't wish
(see examples above)
A logged message can have an associated Log Level. By default we have
these levels:
info (smallest level),
warning ,
debug ,
error ,
fatal (highest level)
Depending on which level is enabled for your application, some
messages will reach the log: those messages having at least that
level. For instance, if info level is enabled, all logged messages
will reach the log.
If warning level is enabled, all messages are logged, but the
warnings. If debug level is enabled, messages that have levels debug,
error, fatal will be logged.
Note that there's an example of using Log Levels provided with the
code.
------------------------------------------------------
Usage syntax / Macros
Once you've set up what each of your logs does (writes to), it's a
must to have an easy and straightforward way to write to your logs -
logging can be fun.
While you can write directly to your logs, like this:
if ( !g_log_app) ; else g_log_app->read_msg().gather().out() <<
"coolio!" << std::endl;
Usually you'll prefer to have some macros to help you write less and
especially avoid being error-prone.
In the previous version, you've asked to be able to write your own
macros - well, you are.
And it's waaaay easier than before:
If you have a logger object, to test if it's enabled, call its
operator bool(). To see if it's not enabled, call its operator!
I will provide some helpers in the future - they're not there yet.
------------------------------------------------------
Logger objects and macros
So, to define a log macro, you'll say something like this:
logger<some_processor, some_filter_class> g_log;
#define L_ if ( !g_log) ; else g_log->...
Note that "g_log->" is equivalent to "g_log.process_msg()-
" (syntactic sugar)
What follows after "..." is all up to you. For example, you can just
get the message, and call a functor:
struct write_to_cout {
void operator()(const std::string & msg) const {
std::cout << msg << std::endl ;
}
};
logger<write_to_cout, manage_enabled_no_ts> g_single_log;
#define L_(x) if ( g_single_log) g_single_log.process_msg()(x)
And in code you'll say things like
L_("some great message");
Or, you can use something a bit more complex, which will gather a
message written using "<<", and then call a functor. I've already
implemented this class:
typedef process_msg< ostream_like_return_str<>, write_to_cout>
processor_cout;
logger<processor_cout, manage_enabled_no_ts> g_log;
#define L_ if ( !g_log) ; else g_log->read_msg().gather().out()
And in code you'll say things like:
L_ << "this" << " is " << "cool";
Or you can implement your own process message class.
------------------------------------------------------
Using process_msg()
Your process_msg class can hold context. And you can also manipulate
logs in code:
logger<...> g_log;
g_log->my_great_log_manipulator();
Thus, you can create your own process message handler classes.
------------------------------------------------------
Formatters and/or destinations
In my previous version, they were called modifiers and appenders.
So, after the message has been gathered (step 2), we do the writing
(step 3).
In step 3, if you use the "format_write" class, you can choose to add
formatters and/or destinations.
A formatter is a functor that formats the message - for instance, it
can prepend the current time, it can prepend the index, or thread ID,
or append an Enter.
A destination is a location where a message can be written, like:
console, a file, a socket, etc.
Dealing with formatters/destinations is easy: once you have a
format_write object, just use:
void add_formatter(formatter fmt)
void del_formatter(formatter fmt)
void add_destination(destination dest)
void del_destination(destination dest)
In the previous version, the formatters were called first, and
destinations last. Now you have more control.
However, this is not thoroughly tested - need to work more on this.
------------------------------------------------------
Examples. I've provided a few examples - you'll find them in the
"tests" directory.
The code compiles and was tested on VC8.
Am still working on formatters and destinations. I'm planning to host
it on sf.net.
Feedback and comments are welcome. Again, you'll find the library
here:
http://torjo.com/log2/
Took a quick walk, I think I found a bug.