Use channel hiearchy of Boost.Log for severity and sink filtering
I have been studying Boost.Log for a while and I believe now is the time for me to transition my code base from log4cxx to Boost.Log. I believe the design and implementation of Boost.Log will significantly improve my code maintenance and usage. I know the Boost.Log FAQ has a page that says
As for hierarchical loggers, there is no need for this feature in the current library design. One of the main benefits it provides in log4j is determining the appenders (sinks, in terms of this library) in which a log record will end up. This library achieves the same result by filtering.
I understand the conceptual equivalence and am not trying to make Boost.Log into log4j/log4cxx. Rather my question is: How do I use Boost.Log to get the same functionality that I currently use from log4cxx? In particular, I want to set severity thresholds and sinks for specific nodes in a log source or channel hierarchy. For example, I have logging sources organized by libA.moduleB.componentC.logD
with levels in the hierarchy separated by dots .
. Using log4cxx one can set the overall threshold of libA
to INFO with the more specific logger, libA.moduleB
, having a threshold of DEBUG.
libA.threshold=INFO
libA.moduleB.threshold=DEBUG
Similarly one can attach sinks to arbitrary nodes in the hierarchy.
I believe that a similar capability is possible with Boost.Log but I need help/guidance on how to actually implement this. Plus, I am sure others who would like to transition to Boost.Log from other frameworks will have the same question.
I sincerely appreciate your comments.
Solution 1:
In Boost.Log sinks (the objects that write log files) and loggers (the objects through which your application emits log records) are not connected directly, and any sink may receive a log message from any logger. In order to make records from certain loggers appear only in particular sinks you will have to arrange filters in sinks so that the unnecessary records are suppressed for sinks that are not supposed to receive them and passed for others. To distinguish records from different loggers the loggers have to add distinct attributes to every record they make. Typically this is achieved with channels - loggers will attach a Channel attribute that can be used to identify the logger in the filters, formatters or sinks. Channels can be combined with other attributes, such as severity levels. It must be noted though that channels and severity levels are orthogonal, and any channel may have records of any level. Values of different attributes are analyzed separately in filters.
So, for example, if you want records from channel A to be written to file A.log, and from channel B - to B.log, you have to create two sinks - one for each file, and set their filters accordingly.
BOOST_LOG_ATTRIBUTE_KEYWORD(a_severity, "Severity", severity_level)
BOOST_LOG_ATTRIBUTE_KEYWORD(a_channel, "Channel", std::string)
logging::add_file_log(
keywords::file_name = "A.log",
keywords::filter = a_channel == "A");
logging::add_file_log(
keywords::file_name = "B.log",
keywords::filter = a_channel == "B");
See the docs about defining attribute keywords and convenience setup functions. Now you can create loggers for each channel and log records will be routed to sinks by filters.
typedef src::severity_channel_logger< severity_level, std::string > logger_type;
logger_type lg_a(keywords::channel = "A");
logger_type lg_b(keywords::channel = "B");
BOOST_LOG_SEV(lg_a, info) << "Hello, A.log!";
BOOST_LOG_SEV(lg_b, info) << "Hello, B.log!";
You can have as many loggers for a single channel as you like - messages from each of them will be directed to a single sink.
However, there are two problems here. First, the library has no knowledge of the channel nature and considers it just an opaque value. It has no knowledge of channel hierarchy, so "A" and "A.bb" are considered different and unrelated channels. Second, setting up filters like above can be difficult if you want multiple channels to be written to a single file (like, "A" and "A.bb"). Things will become yet more complicated if you want different severity levels for different channels.
If channel hierarchy is not crucial for you, you can make filter configuration easier with a severity threshold filter. With that filter you can set minimal severity level for each corresponding channel. If you want to inherit severity thresholds in sub-channels then your only way is to write your own filter; the library does not provide that out of the box.
There are multiple ways to create a filter but it boils down to writing a function that accepts attribute values from log records and returns true
if this record passed the filter and false
otherwise. Perhaps, the easiest way is shown in Tutorial, see the example with phoenix::bind
from Boost.Phoenix.
bool my_filter(
logging::value_ref< severity_level, tag::a_severity > const& level,
logging::value_ref< std::string, tag::a_channel > const& channel,
channel_hierarchy const& thresholds)
{
// See if the log record has the severity level and the channel attributes
if (!level || !channel)
return false;
std::string const& chan = channel.get();
// Parse the channel string, look for it in the hierarchy
// and find out the severity threshold for this channel
severity_level threshold = thresholds.find(chan);
return level.get() >= threshold;
}
Now setting up sinks would change like this to make use of your new filter:
logging::add_file_log(
keywords::file_name = "A.log",
keywords::filter = phoenix::bind(&my_filter, a_severity.or_none(), a_channel.or_none(), hierarchy_A));
logging::add_file_log(
keywords::file_name = "B.log",
keywords::filter = phoenix::bind(&my_filter, a_severity.or_none(), a_channel.or_none(), hierarchy_B));
Here hierarchy_A
and hierarchy_B
are your data structures used to store severity thresholds for different channels for the two log files.