Search code examples
c++loggingboostboost-log

Boost.Log change filter per channel with unknown channel names


in Boost.Log I want to set filters based on channels. Using this example I implemented using text_file_backend. But in my program, channel names are given by user as input argument. So I decided to implement a method that set severity filter for channel.

commons.h

#ifndef COMMONS_H_
#define COMMONS_H_

#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/log/common.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/attributes.hpp>
#include <boost/log/sources/logger.hpp>
#include <boost/log/sources/severity_channel_logger.hpp>
#include <boost/log/sinks/syslog_backend.hpp>

#include <boost/log/utility/setup/common_attributes.hpp>
#include <boost/log/utility/manipulators/add_value.hpp>
#include <boost/log/utility/setup/file.hpp>

#include <boost/thread/thread.hpp>


namespace logging = boost::log;
namespace src = boost::log::sources;
namespace sinks = boost::log::sinks;
namespace expr = boost::log::expressions;
namespace keywords = boost::log::keywords;

enum severity_levels
{
    normal,
    notification,
    warning,
    error,
    critical
};

// Define the attribute keywords
BOOST_LOG_ATTRIBUTE_KEYWORD(line_id, "LineID", unsigned int)
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", severity_levels)
BOOST_LOG_ATTRIBUTE_KEYWORD(channel, "Channel", std::string)

typedef expr::channel_severity_filter_actor< std::string, severity_levels > 
min_severity_filter;
typedef src::severity_channel_logger_mt< severity_levels, std::string > 
logger_type_mt;

BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT(test_lg, logger_type_mt)

typedef sinks::synchronous_sink< sinks::text_file_backend > File_sink;

#define ADD_LOG(severity, channel, msg, args...) add_log_message(__FILE__, __LINE__, severity, channel, boost::this_thread::get_id(), msg, args)

#define MY_GLOBAL_LOGGER(log_, channel, sv, file, line, thread) BOOST_LOG_CHANNEL_SEV( log_, channel, sv) \
<< boost::log::add_value("Line", line)      \
<< boost::log::add_value("File", file)       \
<< boost::log::add_value("Thread_id", thread)    


std::ostream& operator<< (std::ostream& strm, severity_levels level)
{
    static const char* strings[] =
    {
        "normal",
        "notification",
        "warning",
        "error",
        "critical"
    };

    if (static_cast< std::size_t >(level) < sizeof(strings) / sizeof(*strings))
        strm << strings[level];
    else
        strm << static_cast< int >(level);

    return strm;
}

#endif

Logger.h

#ifndef LOGGER_H_
#define LOGGER_H_

#define BOOST_LOG_DYN_LINK 1

#include <string.h>
#include <ctime>
#include <chrono>

#include "commons.h"

class Logger
{
public:
    static min_severity_filter min_severity;
    static void init_logging()
    {
        logging::add_common_attributes();

        // Create a text file sink
        boost::shared_ptr< sinks::text_file_backend> backend(new sinks::text_file_backend());
        backend->set_file_name_pattern<std::string>("sample_%N.log");
        backend->set_rotation_size(2 * 1024 * 1024);

        boost::shared_ptr< File_sink > sink(new File_sink(backend));

        // Set up where the rotated files will be stored
        init_file_collecting <File_sink>(sink);
        sink->set_formatter(&file_log_formatter);
        sink->locked_backend()->scan_for_files();
        logging::core::get()->add_sink(sink);   
        logging::core::get()->set_filter(min_severity || severity >= normal);
    }

    template <typename T>
    static void init_file_collecting(boost::shared_ptr< T > sink)
    {
        sink->locked_backend()->set_file_collector(sinks::file::make_collector(
            keywords::target = "logs",                      /*< the target directory >*/
            keywords::max_size = 64 * 1024 * 1024,          /*< maximum total size of the stored files, in bytes >*/
            keywords::min_free_space = 100 * 1024 * 1024    /*< minimum free space on the drive, in bytes >*/
        ));
    }

    static void file_log_formatter(logging::record_view const& rec, logging::formatting_ostream& strm)
    {
        // Get the LineID attribute value and put it into the stream
        strm << logging::extract< unsigned int >("LineID", rec) << ": ";

        // TimeStamp
        strm << "[";

        strm << logging::extract<boost::posix_time::ptime>("TimeStamp", rec);
        strm << "]";

        // thread id
        strm << "[" << logging::extract< boost::thread::id >("Thread_id", rec) << "] ";
        strm << "[" << rec[channel] << "] ";
        strm << "[";
        strm << logging::extract< int >("Line", rec) << ", ";
        logging::value_ref< std::string > fullpath = logging::extract< std::string >("File", rec);
        strm << boost::filesystem::path(fullpath.get()).filename().string() << "] ";

        // The same for the severity level.
        // The simplified syntax is possible if attribute keywords are used.
        strm << "<" << rec[severity] << "> ";

        // Finally, put the record message to the stream
        strm << rec[expr::smessage];
    }

    static void set_channel_filter(std::string channel, severity_levels min_level)
    {
        min_severity[channel] = min_level;
        logging::core::get()->set_filter(min_severity);
    }

    static void add_log_message(const char* file, int line, severity_levels severity,
                                    std::string channel, boost::thread::id thread_id,
                                    const char* message, ...)
    {
        char buffer[256];
        va_list ap;
        va_start(ap, message);
        vsnprintf(buffer, 256, message, ap);
        MY_GLOBAL_LOGGER(test_lg::get(), channel, severity, file, line, thread_id) << buffer;
        va_end(ap);
    }
};

#endif
min_severity_filter Logger::min_severity = expr::channel_severity_filter(channel, severity);

At the first of program by calling init_logging() filter for all channels is set to normal.

the problem is when I invoke set_channel_filter() with some input (e.g. "CHANNEL_1", warning), I expect setting filter only for "CHANNEL_1", But filtering is set for all possible channels. (e.g. "CHANNEL_2, etc). When I add for example "OTHER_CHANNEL" manually to set_channel_filter() it works for it. I want to have c++ map like data structure saving all severity filter per channel. and anytime user invoke to add a new or existing channel with a filter, it just change filtering for that particular channel, not for all.

main.cpp

int main()
{
    Logger::init_logging();

    Logger::ADD_LOG(severity_levels::normal, "NETWORK", "a warning message with id %d", 34);
    Logger::ADD_LOG(severity_levels::notification, "NETWORK", "a warning message with id %d", 65);

    Logger::ADD_LOG(severity_levels::notification, "GENERAL", "a notification message with id %d", 12);
    Logger::ADD_LOG(severity_levels::warning, "GENERAL", "a warning message with id %d", 13);

    // Logs in GENERAL category must have severity level of warning or higher in-order to record.
    Logger::set_channel_filter("GENERAL", severity_levels::warning);

    Logger::ADD_LOG(severity_levels::notification, "GENERAL", "a notification message with id %d", 14);
    for (int i = 0; i < 2; i++)
    {
        Logger::ADD_LOG(severity_levels::warning, "GENERAL", "a warning message with id %d", 15);
    }

    Logger::ADD_LOG(severity_levels::normal, "NETWORK", "a warning message with id %d", 34); // Expected to sent to file. but filtered!!
    Logger::ADD_LOG(severity_levels::notification, "NETWORK", "a warning message with id %d", 65); //Also filtered !!
}

Solution

  • The problem is that initially, in init_logging, you set the filter as follows:

    logging::core::get()->set_filter(min_severity || severity >= normal);
    

    At this point, min_severity is empty, as you haven't added any channels/severity thresholds. By default, min_severity will return false when a channel is not found in the channel/severity mapping, which means the filter you set here is effectively severity >= normal.

    Later, when you call set_channel_filter, you add the first entry to the min_severity mapping, so that it works for the "GENERAL" channel but not for any other. However, you set a different filter this time:

    logging::core::get()->set_filter(min_severity);
    

    This time, if min_severity returns false the log record is discarded, which is what happens with the last two records in the "NETWORK" channel. You need to set the same filter expression every time to have a consistent behavior.