If you write your own logging sinks or use your own types in attributes, you may want to add support for these components to the settings parser provided by the library. Without doing this, the library will not be aware of your types and thus will not work properly.
#include <boost/log/utility/init/formatter_parser.hpp>
In order to add support for user-defined types to the formatter parser, one
has to register a formatter factory. The factory is basically a function
object that, when called, will construct a formatter for the particular attribute.
Factories are registered with the register_formatter_factory
function, that besides the factory functor accepts the attribute name that
will trigger this factory usage. This way the application can expose the
knowledge of the particular attribute to the library. Here's a quick example:
// Suppose, this class can be used as an attribute value struct Point { double m_X, m_Y; // Streaming operator template< typename CharT, typename TraitsT > friend std::basic_ostream< CharT, TraitsT >& operator<< ( std::basic_ostream< CharT, TraitsT >& strm, Point const& point) { strm << "(" << point.m_X << ", " << point.m_Y << ")"; } }; // This is a helper traits that defines most of the types used by the formatter factories typedef logging::formatter_types< char > types; // Formatter factory types::formatter_type point_formatter_factory( types::string_type const& attr_name, types::formatter_factory_args const& args) { return types::formatter_type(fmt::attr< Point >(attr_name)); } // We can associate the attribute with the name "Coordinates" with the type Point logging::register_formatter_factory("Coordinates", &point_formatter_factory);
Now, whenever the formatter parser (the parse_formatter
function) encounters the "Coordinates" attribute in the format
string being parsed, the point_formatter_factory
will be called to construct the appropriate formatter. This formatter, since
it is generated in the user's application, will use the custom streaming
operator that is defined for the Point
class.
The formatter factory can additionally accept a number of parameters separated with commas that can be specified in the format string. These parameters are broken into (name, value) pairs and passed as the second argument to the factory. For example, we could allow customizing the way our coordinates are presented in log by accepting an additional parameter in the format string like this:
%TimeStamp% %Coordinates(format="{%0.3f; %0.3f}")% %_%
Now in order to support this parameter we should rewrite our factory like this:
namespace lambda = boost::lambda; // This formatter will use custom format string to format the point coordinates void custom_point_formatter( types::string_type const& attr_name, types::ostream_type& strm, types::record_type const& rec, types::string_type const& format) { Point point; if (logging::extract< Point >( attr_name, rec.attribute_values(), lambda::var(point) = lambda::_1)) { // If the attribute set contains the needed attribute value, // format it into the stream strm << boost::format(format) % point.m_X % point.m_Y; } } // Formatter factory types::formatter_type point_formatter_factory( types::string_type const& attr_name, types::formatter_factory_args const& args) { types::formatter_factory_args::const_iterator it = args.find("format"); if (it != args.end()) { // The custom format is specified, use the special formatter return types::formatter_type(lambda::bind( &custom_point_formatter, attr_name, lambda::_1, lambda::_2, it->second)); } else { // No special format specified, do things the traditional way return types::formatter_type(fmt::attr< Point >(attr_name)); } }
However, if you don't need this additional flexibility and all you want is to use your custom streaming operators to format the attribute value, you can omit writing the formatter factory altogether. You can use a simple call like this:
logging::register_simple_formatter_factory< Point >("Coordinates");
to achieve the same effect that the first version of the point_formatter_factory
function provides.
#include <boost/log/utility/init/filter_parser.hpp>
You can extend filter parser the similar way you can extend the formatter parser - by registering your types into the library. However, since it takes a considerably more complex syntax to describe filters, a filter factory is more than a mere function.
Filter factories should be objects that derive from the filter_factory
interface. This base class declares a number of virtual functions that will
be called in order to create filters, according to the filter expression.
If some functions are not overriden by the factory, the corresponding operations
are considered to be not supported by the attribute value. For example, we
can define the filter factory for the slightly improved Point
class defined in the previous section the following way:
// Suppose, this class can be used as an attribute value struct Point { double m_X, m_Y; // Comparison operators bool operator== (Point const& that) const; bool operator!= (Point const& that) const; // Streaming operators template< typename CharT, typename TraitsT > friend std::basic_ostream< CharT, TraitsT >& operator<< ( std::basic_ostream< CharT, TraitsT >& strm, Point const& point); template< typename CharT, typename TraitsT > friend std::basic_istream< CharT, TraitsT >& operator>> ( std::basic_istream< CharT, TraitsT >& strm, Point& point); }; struct point_filter_factory : public logging::filter_factory< char > { // The callback for filter for the attribute existence test filter_type on_exists_test(string_type const& name) { return filter_type(flt::has_attr< Point >(name)); } // The callback for equality relation filter filter_type on_equality_relation(string_type const& name, string_type const& arg) { return filter_type(flt::attr< Point >(name) == boost::lexical_cast< Point >(arg)); } // The callback for inequality relation filter filter_type on_inequality_relation(string_type const& name, string_type const& arg) { return filter_type(flt::attr< Point >(name) != boost::lexical_cast< Point >(arg)); } }; // The factory can be registered in the following way logging::register_filter_factory("Coordinates", boost::make_shared< point_filter_factory >());
Having done that, whenever the filter parser (the parse_filter
function) encounters the "Coordinates" attribute mentioned in the
filter, it will use the point_filter_factory
object to construct the appropriate filter. For example, in the case of the
following filter
%Coordinates% = "(10, 10)"
the on_equality_relation
method will be called with name
argument being "Coordinates" and arg
being "10, 10".
Note | |
---|---|
The quotes around the parenthesis are necessary because the filter parser
only supports binary relations, while round brackets are already used to
group subexpressions of the filter expression. Whenever there is need to
pass several parameters to the relation (like in this case - a number of
components of the |
The constructed filter will use the corresponding comparison operators for
the Point
class. Some relation
operations, like ">" or "<=", will not be supported
for attributes named "Coordinates", and this is just the way we
want it, because the Point
class does not support them either.
The library allows not only adding support for new types, but also associating new relations with them. For instance, we can create a new relation "is_in_rect" that will yield positive if the coordinates fit into a rectangle denoted with two points. The filter might look like this:
%Coordinates% is_in_rect "(10, 10) - (20, 20)"
To support it one has to define the on_custom_relation
method in the filter factory:
namespace bll = boost::lambda; struct Rectangle { Point m_TopLeft, m_BottomRight; // Streaming operators template< typename CharT, typename TraitsT > friend std::basic_ostream< CharT, TraitsT >& operator<< ( std::basic_ostream< CharT, TraitsT >& strm, Rectangle const& rect); template< typename CharT, typename TraitsT > friend std::basic_istream< CharT, TraitsT >& operator>> ( std::basic_istream< CharT, TraitsT >& strm, Rectangle& rect); }; // Our custom filter type class is_in_rect_filter : public flt::basic_filter< char, is_in_rect_filter > { private: string_type m_Name; Rectangle m_Rect; public: is_in_rect_filter(string_type const& attr_name, Rectangle const& rect) : m_Name(attr_name), m_Rect(rect) { } bool operator() (values_view_type const& attrs) const { Point point; if (logging::extract< Point >(m_Name, attrs, bll::var(point) = bll::_1)) { // Check that the point fits into the rectangle region return point.m_X >= m_Rect.m_TopLeft.m_X && point.m_X <= m_Rect.m_BottomRight.m_X && point.m_Y >= m_Rect.m_TopLeft.m_Y && point.m_Y <= m_Rect.m_BottomRight.m_Y; } else return false; } }; struct point_filter_factory : public logging::filter_factory< char > { // The callback for custom relation filter filter_type on_custom_relation( string_type const& name, string_type const& rel, string_type const& arg) { if (rel == "is_in_rect") { // Parse the coordinates of the rectangle region and construct the filter return filter_type(is_in_rect_filter(name, boost::lexical_cast< Rectangle >(arg)); } else throw std::runtime_error("Relation " + rel + " is not supported"); } };
Like with formatters, if all these bells and whistles are not needed, user can register a trivial filter factory with a simple call:
logging::register_simple_filter_factory< Point >("Coordinates");
In this case, however, the Point
class has to support all the standard relational operations and have appropriate
streaming operators in order to be parsed from a string.
#include <boost/log/utility/init/from_stream.hpp>
The library provides mechanism of extending support for sinks similar to the formatter and filter parsers. In order to be able to mention user-defined sinks in a settings file, the user has to register a sink factory, which is essentially a function object that receives a number of named parameters and returns a pointer to the initialized sink. The factory is registered for a specific destination (see the settings file description), so whenever a sink with the specified destination is mentioned in the settings file, the factory gets called. For instance, if we have a sink that emits SNMP traps as a result of processing log records, we can register it the following way:
class snmp_backend : public sinks::basic_sink_backend< char, sinks::frontend_synchronization_tag > { public: // The constructor takes an address of the receiver of the traps explicit snmp_backend(std::string const& trap_receiver); // The function consumes the log records that come from the frontend and emits SNMP traps void consume(record_type const& rec); }; // Factory function for the SNMP sink boost::shared_ptr< sinks::sink< char > > create_snmp_sink( std::map< std::string, std::string > const& params) { // Read parameters for the backend and create it std::map< std::string, std::string >::const_iterator it = params.find("TrapReceiver"); if (it == params.end()) throw std::runtime_error("TrapReceiver parameter not specified for the SNMP backend"); boost::shared_ptr< snmp_backend > backend = boost::make_shared< snmp_backend >(it->second); // Construct and initialize the final sink typedef sinks::synchronous_sink< snmp_backend > sink_t; boost::shared_ptr< sink_t > sink = boost::make_shared< sink_t >(backend); it = params.find("Filter"); if (it != params.end()) sink->set_filter(logging::parse_filter(it->second)); return sink; } logging::register_sink_factory("SNMP", &create_snmp_sink);
Now the SNMP sink can be constructed with the following settings:
[Sink:MySNMPSink] Destination=SNMP Filter="%Severity% > 3"
Tip | |
---|---|
Although users are free to name parameters of their sinks the way they like, a good choice would be to follow the naming policy established by the library. That is, it should be obvious that the parameter "Filter" means the same for both the library-provided "TextFile" sink and your custom "SNMP" sink backend. |
Note | |
---|---|
As the "Destination" parameter is used to determine the sink factory, this parameter is reserved and cannot be used by sink factories for their own purposes. |