#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:
basic_sink_backend
base class template is your choice. Actually, this class only defines
types that are needed for the sink to function.
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:
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.
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.
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.
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.
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);