Boost C++ LibrariesSourceForge.net Logo

PrevUpHomeNext

Extending the library

Writing your own sinks
Writing your own sources
Writing your own attributes
Extending library settings support
#include <boost/log/sinks/basic_sink_backend.hpp>

As was described in the Design overview section, sinks consist of two parts: frontend and backend. Frontends are provided by the library and usually do not need to be reimplemented. Thanks to frontends, implementing backends is much easier than it could be: all filtering and thread synchronization is done there.

In order to develop a sink backend, you have two options where to start:

  • If you don't need any formatting, the minimalistic basic_sink_backend base class template is your choice. Actually, this class only defines types that are needed for the sink to function.
  • If you need to create a sink with formatting capabilities, you can use the basic_formatting_sink_backend class template as a base class for your backend. It extends the basic_sink_backend class and implements log record formatting and character code conversion, leaving you to develop only the record storing code.

Before we move on and see these instruments in action, one thing should be noted. As was said before, sink frontends take the thread safety burden from the backend. Also, there are three kinds of frontends (not counting the ordering one). Each of them provides different guarantees regarding thread safety. The backend has no idea which sink frontend is used with it, yet it may require a certain degree of thread safety from it to function properly. In order to protect itself from misuse the backend declares the threading model it needs to operate with. There are three of them:

  1. The backend_synchronization_tag means that the backend itself is responsible for thread synchronization (which may imply there is no need for synchronization at all). When a backend declares this threading model, any sink frontend can be used with it.
  2. The frontend_synchronization_tag means that the frontend must serialize calls to the backend from different threads. The unlocked_sink frontend cannot fulfill this requirement, so it will not compile if instantiated with such a backend.
  3. The single_thread_tag means that all log records must be passed to the backend in a single thread. Note that other methods can be called in other threads, however, these calls must be serialized. Only asynchronous_sink frontend meets this requirement, other frontends will refuse to compile with such a backend.

The threading model tag is used to instantiate the backend base classes. Since basic_formatting_sink_backend base class uses internal data to implement log record formatting, it requires the threading model to be either frontend_synchronization_tag or single_thread_tag. On the other hand, basic_sink_backend doesn't have this restriction.

Minimalistic sink backend

As an example of the basic_sink_backend class usage, let's implement a simple statistical information collector backend. Assume we have a network server and we want to monitor how many incoming connections are active and how much data was sent or received. The collected information should be written to a CSV-file every minute. The backend definition could look something like this:

// The backend collects statistical information about network activity of the application
class stat_collector :
    public sinks::basic_sink_backend<
        // Character type. We use narrow-character logging in this example.
        char,
        // We will have to store internal data, so let's require frontend to
        // synchronize calls to the backend.
        sinks::frontend_synchronization_tag
    >
{
private:
    // The file to write the collected information to
    std::ofstream m_CSVFile;

    // Here goes the data collected so far:
    // Active connections
    unsigned int m_ActiveConnections;
    // Sent bytes
    unsigned int m_SentBytes;
    // Received bytes
    unsigned int m_ReceivedBytes;

    // A thread that writes the statistical information to the file
    std::auto_ptr< boost::thread > m_WriterThread;

public:
    // The function creates an instance of the sink
    template< template< typename > class FrontendT >
    static boost::shared_ptr< FrontendT< stat_collector > > create(const char* file_name);

    // The function consumes the log records that come from the frontend
    void consume(record_type const& rec);

private:
    // The constructor initializes the internal data
    explicit stat_collector(const char* file_name) :
        m_CSVFile(file_name, std::ios::app),
        m_ActiveConnections(0)
    {
        reset_accumulators();
        if (!m_CSVFile.is_open())
            throw std::runtime_error("could not open the CSV file");
    }

    // The function runs in a separate thread and calls write_data periodically
    template< template< typename > class FrontendT >
    static void writer_thread(boost::weak_ptr< FrontendT< stat_collector > > const& sink);

    // The function resets statistical accumulators to initial values
    void reset_accumulators()
    {
        m_SentBytes = m_ReceivedBytes = 0;
    }

    // The function writes the collected data to the file
    void write_data()
    {
        m_CSVFile << m_ActiveConnections
            << ',' << m_SentBytes
            << ',' << m_ReceivedBytes
            << std::endl;
        reset_accumulators();
    }
};

As you can see, the public interface of the backend is quite simple. In fact, only the consume function is needed by frontends, the create function is introduced for our own convenience. The create function simply creates the sink and initializes the thread that will write the collected data to the file.

// The function creates an instance of the sink
template< template< typename > class FrontendT >
boost::shared_ptr< FrontendT< stat_collector > > stat_collector::create(const char* file_name)
{
    // Create the backend
    boost::shared_ptr< stat_collector > backend(new stat_collector(file_name));

    // Wrap it into the specified frontend
    typedef FrontendT< stat_collector > sink_t;
    boost::shared_ptr< sink_t > sink(new sink_t(backend));

    // Now we can start the thread that writes the data to the file
    backend->m_WriterThread.reset(new boost::thread(
        &stat_collector::writer_thread< FrontendT >,
        boost::weak_ptr< FrontendT< stat_collector > >(sink)
    ));

    return sink;
}

Now the writer_thread function can look like this:

// The function runs in a separate thread and writes the collected data to the file
template< template< typename > class FrontendT >
void stat_collector::writer_thread(
    boost::weak_ptr< FrontendT< stat_collector > > const& sink)
{
    while (true)
    {
        // Sleep for one minute
        boost::this_thread::sleep(boost::get_xtime(
            boost::get_system_time() + boost::posix_time::minutes(1)));

        // Get the pointer to the sink
        boost::shared_ptr< FrontendT< stat_collector > > p = sink.lock();
        if (p)
            p->locked_backend()->write_data(); // write the collected data to the file
        else
            break; // the sink is dead, terminate the thread
    }
}

The consume function is called every time a logging record passes filtering in the frontend. The record, as was stated before, contains a set of attribute values and the message string.

Since we have no need for the record message, we will ignore it for now.

// The function consumes the log records that come from the frontend
void stat_collector::consume(record_type const& rec)
{
    namespace lambda = boost::lambda;

    if (rec.attribute_values().count("Connected"))
        ++m_ActiveConnections;
    else if (rec.attribute_values().count("Disconnected"))
        --m_ActiveConnections;
    else
    {
        logging::extract< unsigned int >(
            "Sent",
            rec.attribute_values(),
            lambda::var(m_SentBytes) += lambda::_1);
        logging::extract< unsigned int >(
            "Received",
            rec.attribute_values(),
            lambda::var(m_ReceivedBytes) += lambda::_1);
    }
}

The code above is quite straightforward. We can parse through attribute values like through a regular map, or use extractors with function objects to acquire individual values. Boost.Lambda and similar libraries simplify generation of function objects that will receive the extracted value.

Formatting sink backend

As an example of a formatting sink backend, let's implement a sink that will emit events to a Windows event trace. Assume there's another process that will monitor these events and display them to the user as a balloon window in the notification area. The definition of such backend would look something like this:

class event_notifier :
    public sinks::basic_formatting_sink_backend<
        // the "source" character type
        char,
        // the "target" character type
        // (optional, by default is the same as the source character type)
        wchar_t
    >
{
    // A handle for the event provider
    REGHANDLE m_ProviderHandle;

public:
    // Constructor. Initializes the event source handle.
    explicit event_notifier(CLSID const& provider_id)
    {
        if (EventRegister(&provider_id, NULL, NULL, &m_ProviderHandle) != ERROR_SUCCESS)
            throw std::runtime_error("Could not register event provider");
    }
    // Destructor. Unregisters the event source.
    ~event_notifier()
    {
        EventUnregister(m_ProviderHandle);
    }

    // The method puts the formatted message to the event trace
    void do_consume(record_type const& rec, target_string_type const& formatted_message);
};

The basic_formatting_sink_backend class template is instantiated on two character types: the one that is used by the rest of logging system and the one that is required by the backend for further usage. Either of these types can be char or wchar_t. These character types may be the same, in which case the formatting is done without character conversion, pretty much equivalent to streaming attribute values into a regular std::ostringstream. In our case the underlying API requires wide strings, so we'll have to do character conversion while formatting. The conversion will be done according to the locale that is set up in the basic_formatting_sink_backend base class (see its imbue and getloc functions).

In order to differentiate the resulting string type from the string types used throughout the rest of logging library, the basic_formatting_sink_backend class defines the target_string_type type along with the standard string_type. In our case, target_string_type will contain wide characters, while string_type will be narrow.

The threading model of the sink backend can be specified as the third optional parameter of the basic_formatting_sink_backend class template. The default threading model is frontend_synchronization_tag, which fits us just fine.

The basic_formatting_sink_backend base class implements just about everything that is required by the library from the backend. The only thing left is to implement the virtual do_consume method that receives the original log record and the result of formatting. In our case this method will pass the formatted message to the corresponding API:

// The method puts the formatted message to the event log
void event_notifier::do_consume(
    record_type const& rec, target_string_type const& formatted_message)
{
    EventWriteString(
        m_ProviderHandle,
        WINEVENT_LEVEL_LOG_ALWAYS,
        0ULL /* keyword */,
        formatted_message.c_str());
}

That's it. The example can be extended to make use of attribute values to fill other parameters, like event level and keywords mask. A more elaborate version of this example can be found in the library examples.

The resulting sink backend can be used similarly to other formatting sinks, like text_ostream_backend:

boost::shared_ptr< event_notifier > backend(new event_notifier(CLSID_MyNotifier));
backend->set_formatter
(
    fmt::stream
        << "[" << fmt::time("TimeStamp")
        << "] " << fmt::message()
);

typedef sinks::synchronous_sink< event_notifier > sink_t;
boost::shared_ptr< sink_t > sink(new sink_t(backend));
logging::core::get()->add_sink(sink);

PrevUpHomeNext