Use channel hiearchy of Boost.Log for severity and sink filtering
Asked Answered
A

1

2

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.

Aoudad answered 18/12, 2015 at 19:46 Comment(0)
A
13

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.

Avesta answered 19/12, 2015 at 15:49 Comment(7)
Thanks for your answer. I appreciate your help to get me started. Channel hierarchy is important to me, so I will have to go with additional complexity. With the outline that you have given me, it seems like my problem distills down to writing the data structures and logic to support threshold and sink filters with child nodes inheriting from parent nodes. With log4j they emphasize how they have an optimal scheme for doing this. Do you know if someone has implemented a similar scheme using the Boost.Log library?Aoudad
No, I don't think I know of anyone doing this. However, making the data structure for severity thresholds doesn't look that difficult - you can implement it with a number of nested std::map/std::unordered_map or with Boost.PropertyTree (boost.org/doc/libs/1_60_0/doc/html/property_tree.html), which already has serialization/deserialization to/from text.Avesta
How is channel_hierarchy defined?Babirusa
@AndreySemashev - is it any way to define filter in the add_file_log call using lambda expression (without phoenix::bind)? If yes, could you please give us an example?Crosscountry
A filter is just a function object with a specific signature: bool (boost::log::attribute_value_set const&). It doesn't matter whether that function object is implemented with Boost.Phoenix, C++11 lambda or a plain function. If it supports that signature, it can be set as a filter. For example: keywords::filter = [](boost::log::attribute_value_set const& attrs) { return true; }.Avesta
@AndreySemashev - I tried to define a filter using the example you provided and got a compilation error. The compiler complained about aux::acquire_filter mostly. It looks like I didn't include some headers - could you please advice about them as well?Crosscountry
Hmm, looks like there is indeed some missing machinery behind the filter and formatter keywords. I suppose, right now you can manually call set_filter on the created sink.Avesta

© 2022 - 2024 — McMap. All rights reserved.