...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
#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, formatting and thread synchronization is done there.
In order to develop a sink backend, you derive your class from either basic_sink_backend
or basic_formatted_sink_backend
,
depending on whether your backend requires formatted log records or not.
Both base classes define a set of types that are required to interface with
sink frontends. One of these types is frontend_requirements
.
#include <boost/log/sinks/frontend_requirements.hpp
>
In order to work with sink backends, frontends use the frontend_requirements
type defined by all backends. The type combines one or several requirement
tags:
synchronized_feeding
.
If the backend has this requirement, it expects log records to be passed
from frontend in synchronized manner (i.e. only one thread should be
feeding a record at a time). Note that different threads may be feeding
different records, the requirement merely states that there will be no
concurrent feeds.
concurrent_feeding
.
This requirement extends synchronized_feeding
by allowing different threads to feed records concurrently. The backend
implements all necessary thread synchronization in this case.
formatted_records
.
The backend expects formatted log records. The frontend implements formatting
to a string with character type defined by the char_type
typedef within the backend. The formatted string will be passed along
with the log record to the backend. The basic_formatted_sink_backend
base class automatically adds this requirement to the frontend_requirements
type.
flushing
. The
backend supports flushing its internal buffers. If the backend indicates
this requirement it has to implement the flush
method taking no arguments; this method will be called by the frontend
when flushed.
Tip | |
---|---|
By choosing either of the thread synchronization requirements you effectively allow or prohibit certain sink frontends from being used with your backend. |
Multiple requirements can be combined into frontend_requirements
type with the combine_requirements
metafunction:
typedef sinks::combine_requirements< sinks::synchronized_feeding, sinks::formatted_records, sinks::flushing >::type frontend_requirements;
It must be noted that synchronized_feeding
and concurrent_feeding
should not be combined together as it would make the synchronization requirement
ambiguous. The synchronized_feeding
is a more strict requirement than concurrent_feeding
,
so whenever the backend requires concurrent feeding it is also capable of
synchronized feeding.
The has_requirement
metafunction can be used to test for a specific requirement in the frontend_requirements
typedef.
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< sinks::combine_requirements< sinks::synchronized_feeding, sinks::flushing >::type > { private: // The file to write the collected information to std::ofstream m_csv_file; // Here goes the data collected so far: // Active connections unsigned int m_active_connections; // Sent bytes unsigned int m_sent_bytes; // Received bytes unsigned int m_received_bytes; // The number of collected records since the last write to the file unsigned int m_collected_count; // The time when the collected data has been written to the file last time boost::posix_time::ptime m_last_store_time; public: // The constructor initializes the internal data explicit stat_collector(const char* file_name); // The function consumes the log records that come from the frontend void consume(logging::record_view const& rec); // The function flushes the file void flush(); private: // The function resets statistical accumulators to initial values void reset_accumulators(); // The function writes the collected data to the file void write_data(); };
we will have to store internal data, so let's require frontend to synchronize feeding calls to the backend |
|
also enable flushing support |
As you can see, the public interface of the backend is quite simple. Only
the consume
and flush
methods are called by frontends.
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. But from the other attributes we can extract the statistical data to
accumulate and write to the file. We can use attribute
keywords and value
visitation to accomplish this.
BOOST_LOG_ATTRIBUTE_KEYWORD(sent, "Sent", unsigned int) BOOST_LOG_ATTRIBUTE_KEYWORD(received, "Received", unsigned int) // The function consumes the log records that come from the frontend void stat_collector::consume(logging::record_view const& rec) { // Accumulate statistical readings if (rec.attribute_values().count("Connected")) ++m_active_connections; else if (rec.attribute_values().count("Disconnected")) --m_active_connections; else { namespace phoenix = boost::phoenix; logging::visit(sent, rec, phoenix::ref(m_sent_bytes) += phoenix::placeholders::_1); logging::visit(received, rec, phoenix::ref(m_received_bytes) += phoenix::placeholders::_1); } ++m_collected_count; // Check if it's time to write the accumulated data to the file boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); if (now - m_last_store_time >= boost::posix_time::minutes(1)) { write_data(); m_last_store_time = now; } } // The function writes the collected data to the file void stat_collector::write_data() { m_csv_file << m_active_connections << ',' << m_sent_bytes << ',' << m_received_bytes << std::endl; reset_accumulators(); } // The function resets statistical accumulators to initial values void stat_collector::reset_accumulators() { m_sent_bytes = m_received_bytes = 0; m_collected_count = 0; }
Note that we used Boost.Phoenix to automatically generate visitor function objects for attribute values.
The last bit of implementation is the flush
method. It is used to flush all buffered data to the external storage, which
is a file in our case. The method can be implemented in the following way:
// The function flushes the file void stat_collector::flush() { // Store any data that may have been collected since the list write to the file if (m_collected_count > 0) { write_data(); m_last_store_time = boost::posix_time::microsec_clock::universal_time(); } m_csv_file.flush(); }
You can find the complete code of this example here.
As an example of a formatting sink backend, let's implement a sink that will display text notifications for every log record passed to it.
Tip | |
---|---|
Real world applications would probably use some GUI toolkit API to display
notifications but GUI programming is out of scope of this documentation.
In order to display notifications we shall use an external program which
does just that. In this example we shall employ |
The definition of the backend is very similar to what we have seen in the previous section:
// The backend starts an external application to display notifications class app_launcher : public sinks::basic_formatted_sink_backend< char, sinks::synchronized_feeding > { public: // The function consumes the log records that come from the frontend void consume(logging::record_view const& rec, string_type const& command_line); };
target character type |
|
in order not to spawn too many application instances we require records to be processed serial |
The first thing to notice is that the app_launcher
backend derives from basic_formatted_sink_backend
rather than basic_sink_backend
.
This base class accepts the character type in addition to the requirements.
The specified character type defines the target string type the formatter
will compose in the frontend and it typically corresponds to the underlying
API the backend uses to process records. It must be mentioned that the character
type the backend requires is not related to the character types of string
attribute values, including the message text. The formatter will take care
of character code conversion when needed.
The second notable difference from the previous examples is that consume
method takes an additional string
parameter besides the log record. This is the result of formatting. The
string_type
type is defined
by the basic_formatted_sink_backend
base class and it corresponds to the requested character type.
We don't need to flush any buffers in this example, so we didn't specify
the flushing
requirement
and omitted the flush
method
in the backend. Although we don't need any synchronization in our backend,
we specified synchronized_feeding
requirement so that we don't spawn multiple instances of notify-send
program
and cause a "fork bomb".
Now, the consume
implementation
is trivial:
// The function consumes the log records that come from the frontend void app_launcher::consume(logging::record_view const& rec, string_type const& command_line) { std::system(command_line.c_str()); }
So the formatted string is expected to actually be a command line to start the application. The exact application name and arguments are to be determined by the formatter. This approach adds flexibility because the backend can be used for different purposes and updating the command line is as easy as updating the formatter.
The sink can be configured with the following code:
BOOST_LOG_ATTRIBUTE_KEYWORD(process_name, "ProcessName", std::string) BOOST_LOG_ATTRIBUTE_KEYWORD(caption, "Caption", std::string) // Custom severity level formatting function std::string severity_level_as_urgency( logging::value_ref< logging::trivial::severity_level, logging::trivial::tag::severity > const& level) { if (!level || level.get() == logging::trivial::info) return "normal"; logging::trivial::severity_level lvl = level.get(); if (lvl < logging::trivial::info) return "low"; else return "critical"; } // The function initializes the logging library void init_logging() { boost::shared_ptr< logging::core > core = logging::core::get(); typedef sinks::synchronous_sink< app_launcher > sink_t; boost::shared_ptr< sink_t > sink(new sink_t()); const std::pair< const char*, const char* > shell_decorations[] = { std::pair< const char*, const char* >("\"", "\\\""), std::pair< const char*, const char* >("$", "\\$"), std::pair< const char*, const char* >("!", "\\!") }; // Make the formatter generate the command line for notify-send sink->set_formatter ( expr::stream << "notify-send -t 2000 -u " << boost::phoenix::bind(&severity_level_as_urgency, logging::trivial::severity.or_none()) << expr::if_(expr::has_attr(process_name)) [ expr::stream << " -a '" << process_name << "'" ] << expr::if_(expr::has_attr(caption)) [ expr::stream << " \"" << expr::char_decor(shell_decorations)[ expr::stream << caption ] << "\"" ] << " \"" << expr::char_decor(shell_decorations)[ expr::stream << expr::message ] << "\"" ); core->add_sink(sink); // Add attributes that we will use core->add_global_attribute("ProcessName", attrs::current_process_name()); }
The most interesting part is the sink setup. The synchronous_sink
frontend (as well as any other frontend) will detect that the app_launcher
backend requires formatting
and enable the corresponding functionality. The set_formatter
method becomes available and can be used to set the formatting expression
that composes the command line to start the notify-send
program.
We used attribute
keywords to identify particular attribute values in the formatter.
Notice that string attribute values have to be preprocessed so that special
characters interpreted by the shell are escaped in the command line. We achieve
that with the char_decor
decorator with
our custom replacement map. After the sink is configured we also add the
current process name
attribute to the core so that we don't have to add it to every record.
After all this is done, we can finally display some notifications:
void test_notifications() { BOOST_LOG_TRIVIAL(debug) << "Hello, it's a simple notification"; BOOST_LOG_TRIVIAL(info) << logging::add_value(caption, "Caption text") << "And this notification has caption as well"; }
The complete code of this example is available here.