Gmane
Gravatar
From: Caleb Epstein <caleb.epstein <at> gmail.com>
Subject: Re: [Review] Boost.Logging: formal review
Newsgroups: gmane.comp.lib.boost.devel
Date: 2005-12-19 22:15:20 GMT (3 years, 28 weeks, 8 hours and 20 minutes ago)
On 12/19/05, Gennadiy Rozental <gennadiy.rozental <at> thomson.com> wrote:
>
> I am all for it ;)

OK, so to that end, please find attached a sketchy implementation of some of
these concepts.  The concepts are:

   - Entry - represents a message to be logged.   This can be a single
   std::string or a more complex structure, whatever the user likes.  Should
   model OutputStreamable, but this requirement is up to the Sink.  Filters may
   place additional requirements on the Entry.
   - Filter - a functor that is used to determine if an Entry should be
   sent to a Logger's Sink list.  A model of std::unary_function<Entry, bool>
   - Sink - a destination for Entries.  Concrete implementations might
   write to a file, debug window, etc.  A model of std::unary_function<Entry,
   void>
   - Logger - the object to which Entries are sent by the user.  Contains
   a Filter and a list of Sinks.  When the user sends an Entry to the Logger,
   it will call the Filter functor and, if it returns true, pass it to each of
   the Sinks it holds.

I've attached a sample implementation (log.h) which contains the following
classes in "namespace logging" (I ran into collisions with math.h when I
tried "namespace log"):

   - template<typename Entry> null_filter: a Filter that always returns
   true.
   - template<typename Entry> basic_file_sink: a Sink implementation that
   opens a std::ofstream and writes Entries to it
   - template<typename Entry> basic_ostream_sink: a Sink that holds an
   std::ostream reference and writes Entries to it.
   - template<typename Entry> basic_logger: the implementation of the
   Logger concept.  Holds a Filter and a std::list of Sinks, where Filter is
   implemented as boost::function<bool(Entry)> and Sink is implemented as
   boost::function<void(Entry)>.  Provides non-const reference getters for the
   filter and sinks members, and of course implements the all important "write"
   method that does the real work of checking the Filter and passing the Entry
   to the Sinks if the Filter returned true.

I've also provided (in tuple_log.h) an implementation of the Entry concept
which attempts to implement Gennadiy's ideas of level, category, and
keyword.  It is in "namespace tuple_log" and is simply a boost::tuple<level,
category, std::string, std::string> where level and category are just
wrapped enums with streaming operations added (the proposed BOOST_ENUM would
be handy here).

Along with the tuple-style Entry class there is an associated filter
functor.  It can be used to filter messages based on level (>= some
threshold), category (matching a bitmask), and keyword (keyword appears in
an "include" list and does not appear in an "exclude" list).  There is a
test program (log.cpp) that exercises these classes and attempts to do some
benchmarking.

Tested on g++ 3.3.4 on Linux and MSVC8 on Win2000.

Some open issues I'm looking for help with:

   - I'm more or less ignorant when it comes to wide-strings,
   wide-streams and the like.  Would these interfaces need to be paramterized
   on CharT, or would/should it be possible to support both narrow and wide
   input with a single interface?
   - I decided that the Entry concept would represent the entire
   message-to-be-logged, including any metadata that goes along with it (
   e.g. level, category, keyword, thread ID, timestamp, __FILE__,
   __LINE__, etc).  The Filter implementation is consequently parameterized on
   Entry, which means you need a fully-formed Entry (including the message
   portion) to call "basic_logger::is_enabled" to check to see if an Entry
   should even be formatted for writing.

Thoughts and feedback are most welcome,

--
Caleb Epstein
caleb dot epstein at gmail dot com
#ifndef __CAE_LOG_H
#define __CAE_LOG_H

#include <iostream>             // For basic_ostream_sink
#include <fstream>              // For basic_file_sink
#include <string>
#include <functional>
#include <list>

#include <boost/function.hpp>

namespace logging {

/// No-op filter for use as the default Filter on a Logger.  Passes
/// all messages.
template<typename Entry>
struct null_filter : public std::unary_function<bool, Entry> {
    bool operator () (const Entry&) const { return true; }
};

/// Sink that opens an output file and writes Entries to that file
template<typename Entry>
struct basic_file_sink {
    std::ofstream m_file;
    bool m_flush;

    basic_file_sink (const std::string& filename,
                       std::ios_base::openmode mode = std::ios_base::app,
                       bool flush = true)
        : m_file (filename.c_str(), mode),
          m_flush (flush)
        {}

    void operator () (const Entry& entry) {
        m_file << entry << '\n';

        if (m_flush)
            m_file.flush();
    }
};

/// Sink implementation that holds a reference to an ostream and
/// writes Entries to that stream
template<typename Entry>
struct basic_ostream_sink {
    std::ostream& m_os;
    bool m_flush;

    basic_ostream_sink (std::ostream& os, bool flush = true)
        : m_os (os),
          m_flush (flush)
        {}

    void operator () (const Entry& entry) {
        m_os << entry << '\n';
        if (m_flush)
            m_os.flush();
    }
};

/// Basic Logger implementation.  Contains a Filter and a list of
/// Sinks to which to write Entries
template<typename Entry>
class basic_logger {
public:
    typedef Entry                       entry_type;
    typedef boost::function<bool (Entry)> filter_type;
    typedef boost::function<void (Entry)> sink_type;
    typedef std::list<sink_type>        sink_list;

    basic_logger ()
        : m_filter (null_filter<entry_type> ())
        {}

    basic_logger (const filter_type& filter)
        : m_filter (filter)
        {}

    template<typename Iter>
    basic_logger (const filter_type& filter,
                  Iter sink_begin, Iter sink_end)
        : m_filter (filter),
          m_sinks (sink_begin, sink_end)
        {}

    filter_type& filter ()
        { return m_filter; }

    sink_list& sinks ()
        { return m_sinks; }

    void is_enabled (const entry_type& entry)
        { return m_filter (entry); }

    void write (const entry_type& entry)
        {
            if (!m_sinks.empty() && m_filter(entry))
                {
                for (typename sink_list::iterator i = m_sinks.begin (),
                     e = m_sinks.end(); i != e; ++i)
                    (*i) (entry);
                }
        }

private:
    filter_type                 m_filter;
    std::list<sink_type>        m_sinks;
};

} // namespace logging

#endif // macro guard
#ifndef __CAE_TUPLE_LOG_H
#define __CAE_TUPLE_LOG_H

#include <set>

#include <boost/tuple/tuple.hpp>
#include <boost/tuple/tuple_io.hpp>
#include <boost/tokenizer.hpp>

#include "log.h"

namespace tuple_log {

// The proposed BOOST_ENUM might be a nice replacement for this class:
struct level {
    enum value_type {
        trace,
        debug,
        info,
        warning,
        error,
        fatal,
        default_value = info
    } value;

    level () : value (default_value) {}

    level (value_type l) : value (l) {}

    /// Get the name of this level
    const char* name () const { return name (value); }

    /// Convert a level to a string representation
    static const char* name (value_type val) {
        const char* str = "";
        switch (val)
            {
            case trace   : str = "trace"; break;
            case debug   : str = "debug"; break;
            case info    : str = "info"; break;
            case warning : str = "warning"; break;
            case error   : str = "error"; break;
            case fatal   : str = "fatal"; break;
            default      : break;
            }
        return str;
    }
};

// Stream formatting for a level object 
std::ostream& operator<< (std::ostream& os, const level& l)
{
    return os << l.name ();
}

// The proposed BOOST_ENUM might be a nice replacement for this class:
struct category {
    enum value_type {
        program_flow            = 0x01,
        function_arguments      = 0x02,
        return_value            = 0x04,
        data_flow               = 0x08,
        default_value           = program_flow,
        all_categories          = 0xffff
    } value;

    category () : value (default_value) {}

    category (value_type t) : value (t) {}

    std::string name () const { return name (value); }

    static std::string name (value_type t) {
        std::string str;
        if (t & program_flow)
            str.append ("program_flow|");
        if (t & function_arguments)
            str.append ("function_arguments|");
        if (t & return_value)
            str.append ("return_value|");
        if (t & data_flow)
            str.append ("data_flow|");
        // Remove trailing pipe
        if (!str.empty())
            str.erase (str.size() - 1);
        if (str.empty())
            str = "(null)";
        return str;
    }
};

std::ostream&
operator<< (std::ostream& os, const category& c)
{
    return os << c.name();
}

/// Specific implementation of Entry concept containing level,
/// category, keyword and message
typedef boost::tuple<level, category, std::string, std::string> entry;

/// Simple filter that implements level, category and keyword
/// filtering.  Will match entry tuples where the level is >=
/// m_level, the category matches the m_category bitmask, and the
/// keyword is either in the include list or not in the exclude list.
class filter : public std::unary_function<entry, bool> {
public:
    filter () {}

    filter (level l, category c, const std::string& labels)
        : m_level (l),
          m_category (c)
        {
            configure (l, c, labels);
        }

    /// Configure level, category and keyword lists
    void configure (level l, category c, const std::string& labels)
        {
            m_level = l;
            m_category = c;
            configure (labels);
        }

    /// Configure the include/exclude keyword list from a pipe or
    /// comma delimited list of tokens.  If a token begins with "!" 
    /// it is added to the exclude list (after removing the "!" 
    /// prefix), else it is added to the include list.
    void configure (const std::string& labels)
        {
            m_include.clear ();
            m_exclude.clear ();

            boost::char_separator<char> sep ("|,");
            typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
            tokenizer tokens (labels, sep);

            for (tokenizer::iterator i = tokens.begin(),
                 e = tokens.end(); i != e; ++i)
                {
                if (i->empty())
                    continue;

                // Token starting with '!' is added to the exclude list
                if (*(i->begin()) == '!' && i->length() > 1)
                    m_exclude.insert (i->substr (1));
                else
                    m_include.insert (*i);
                }
        }

    /// Returns true if the Entry matches (see class description),
    /// else false
    result_type operator() (const argument_type& arg) const {
        // If level must be >= m_level
        if (arg.get<0>().value < m_level.value)
            return false;
        // Category must match m_category mask
        else if (!(arg.get<1>().value & m_category.value))
            return false;

        const std::string& label = arg.get<2>();

        if (label.empty())      // Can we filter on an empty label?
            return true;
        // If we have an include list, the label must match
        else if (!m_include.empty() &&
            m_include.find (label) == m_include.end())
            return false;
        // If we have an exclude list, the label must NOT match
        else if (!m_exclude.empty() &&
                 m_exclude.find (label) != m_exclude.end())
            return false;

        return true;
    }

private:
    level                               m_level;
    category                            m_category;
    typedef std::set<std::string>       keyword_set;
    keyword_set                         m_include;
    keyword_set                         m_exclude;
};

typedef logging::basic_file_sink<entry> file_sink;

typedef logging::basic_ostream_sink<entry> ostream_sink;

typedef logging::basic_logger<entry> logger;

} // namespace tuple_log 

#endif // macro guard
Attachment (log.cpp): application/force-download, 2832 bytes
_______________________________________________
Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost