Channels ▼
RSS

Tools

A Highly Configurable Logging Framework In C++


Behind the Scenes

The architecture of the framework has three parts:

  • Front-end
  • Core
  • Back-end

You can configure and adapt each part, and the configuration determines what the framework does. My implementation uses C++ features like function overloading, free operator functions, templates, and template-specialization to name a few only, and all of the logging framework functionality is encapsulated in a separate "logging" namespace, avoiding name clashes.

The front-end is the top of the framework, presenting the API. The log structure that provides the emit() functions and two enumerations is shown in Listing One.

Listing One


struct log {

    template<typename Level>
    static inline
    typename Logger<Level>::return_type& emit () {
        return Logger<Level>::logging() << Level::level() << Level::desc();
    }

    static inline
    Logger<>::return_type& emit () {
        return Logger<>::logging() << Void::level();
    }

    enum Numerative {
        bin  =  2,   ///< switch to binary output
        oct  =  8,   ///< switch to octal output
        dec  = 10,   ///< switch to decimal output
        hex  = 16    ///< switch to hexadecimal output
    };

    enum Manipulator {
        tab  =  '\t',   ///< prints a tabulator to the output
        endl =  '\n'    ///< adds a line feed to the output
    };
};

All of the logging levels are not shown. However, I do present the definition of Error as an example that's typical of all logging levels:


struct Error {
    static Level::levels level () {
        return Level::error;
    }

    static const char * desc() {
        return "[ ERROR ]";
    }
};

Again, each log statement starts with the emit() function. emit() exists in two flavors — a general form and one with a template-parameter, giving the logging level (see Listing One). The template version of the emit() function is used this way:


log::emit< Error >()<<Logging an Error<< log::endl;

The code outputs "[ ERROR ] Logging an Error" and sets the logging level to Error for this statement. The return type of emit(), directly usable with operator<<() for outputting logging data, is determined by the Logger component (part of the core). The return type depends first on the logging level and second, on a default template parameter of the Logger component (not visible here; see Listing Two). The configuration of this type is key to meeting Requirements #2, #3, and #6 because, depending on how you deploy it, different behaviors are realized. Furthermore, configuring the return type can let you enhance the framework functionality (for example with coloration, as I'll show shortly). If you look carefully at Listing One, you see, that endl, tab, and so on are defined within an enumeration — not as manipulator functions. If I would use manipulator functions in the same way as in std::cout, then code generation always occurs because it implies taking the address of the function. Because of Requirement #6, I have avoided manipulator functions in the general design. However, users have the possibility to write manipulator functions, and the core is able to execute manipulators.

Most of the core is not interesting from the concepts point of view, because there are a lot of housekeeping code (e.g., logging level treatment) and code responsible for converting numbers to a given base, pointers, etc. to character streams. However, the Logger component is important because of its job for determining and handling the correct return type — the arcane template argument loggingReturnType (Listing Two).

Listing Two


struct loggingReturnType;

template<typename Level = ::logging::Void, typename R = loggingReturnType>
class Logger {
    public:
        typedef R return_type;

        static return_type& logging () {
            return Obj<typename return_type::output_base_type, int>::obj();
        }

    private:
        template <typename log_t, typename T>
        struct Obj {
            static return_type& obj () {
                typedef singleton<return_type> log_output;
                return log_output::instance();
            }
        };

        template <typename T>
        struct Obj<NullOutput, T> {
            static return_type& obj () {
                return *reinterpret_cast<return_type*>(0x0);
            }
        };
};

loggingReturnType is forward declared to allow including the framework and later defining the type by the user of the framework. Enabling this needs an indirection level via an additional template parameter R that is the loggingReturnType by default. Through that indirection, it is possible to defer the type definition to a later point, but allowing the use of this not-yet-defined type within the Logger component. Later at log statements, when the template code is evaluated, loggingReturnType has to be defined.

In general, the Logger implements a compile-time selector, using the C++ feature of partial template-specialisation. The selection mechanism uses an embedded output_base_type definition within the loggingReturnType (which is ensured through its declaration) for choosing the right implementation. If the output_base_type is not of type NullOutput, a singleton of type loggingReturnType is constructed and returned as reference. On the reference, methods for switching levels (printing numbers or characters) can be called. If the output_base_type is of type NullOutput, a reference to an object at address zero will be returned. "What is that?" you ask. "A reference to an object that does not really exist and calling methods on it? Is this allowed?" No, calling methods on it is not legal, or like the standard says, it leads to "undefined behavior". Therefore, I do not call methods on this reference. The NullOutput is defined as:


struct NullOutput {};

...and it is empty, thus it is also impossible to call methods on it. However, the type, in this case the returned reference, is usable to select an implementation. Now I use an overloaded version of a free operator function.


template<typename T>
static inline NullOutput& operator << (NullOutput& no, T) {
  return no;
}

The overloaded operator<<() has a template parameter matching on everything, meaning it catches every call having a NullOutput as reference, and also allows chaining of consecutive calls. Due to its definition as static inline and through its empty implementation, the compiler can fully inline the free operator function and omit the call as well as the generation of the function itself, allowing a full deactivation of the logging framework (Requirement #6). To meet Requirement #5 — deactivation of certain parts/levels at compile-time — I use the NullOutput again. A full template specialization of the Logger component enables the desired functionality. The following code snippet disables Info logging level output:


template<>
class Logger<Info, loggingReturnType> {
    public:
        typedef NullOutput return_type;
        static return_type& logging (){
            return *reinterpret_cast<return_type*>(0x0);
        }
}

Such code needs not to be written by hand, but instead it is generated by a user-friendly macro (LOGGING_DISABLE_LEVEL(level)). Yes, you read it right — a macro — but this is not critical because this kind of macro is not part of a log statement within user-code, instead, it is only part of the logging framework configuration.

The used NullOutput is from the architecture point of view a special back-end used for deactivation, however, one will not always want to deactivate logging. Usually, you want to output something. Hence, the consideration of the lowest part, the back-end is still missing. In general, a back-end or sink defines an output device for logged data. It could be a file, a console, a serial port, or something that you imagine. For that, sinks have to provide a simple interface to be a possible back-end.


struct Sink {
    Sink& operator<<(const char c) {
        // implement the sink specific behavior for printing c
        return *this;
    }
};

A sink takes a single character and delivers a reference to itself, allowing the chaining of consecutive output actions. However, the performed action depends on the back-ends type.

Having all parts of the architecture together, one question remains: How do you configure and setup the framework to a working state? In the first instance, you have to choose what back-end you want. As an example, I show a configuration with StdOutput as back-end parameterized with std::clog stream as output medium. Furthermore, for the example, I do not want to switch logging levels on or off at runtime, so I configure OutputLevelSwitchDisabled. The contained OutputStream is responsible for handling numbers, pointers, conversions, and so on. I provide the configuration as a typedef.


typedef OutputLevelSwitchDisabled <
            OutputStream <
                StdOutput< ::std::clog>
            >
        > StdLogType;

The architecture is implemented as mixin-layers, thus it is easy to extend, allowing adding new functionality. (See "Mixin layers: an object-oriented implementation technique for refinements and collaboration-based designs" by Y. Smaragdakis and D. Batory, ACM Transactions on Software Engineering and Methodology, vol. 11, 2002.) The defined StdLogType is directly usable. However, if I would do so, Requirements #5 and #6 are not able to be met because this type is not bound to the frameworks front-end; therefore, log::emit() in its flavors is not configured yet. To get the conjunction, I need the following macro:


#define LOGGING_DEFINE_OUTPUT(BASE)                                   \
    namespace logging {                                               \
        struct loggingReturnType : public BASE {                      \
                // The provided typedef is used for compile-time      \
                // selection of different implementations of the      \
                // logging framework. Thus, it is necessary           \
                // that any output type supports this type            \
                // definition, why it is defined here.                \
                typedef BASE    output_base_type;                     \
            };                                                        \
    }

that is used this way:


LOGGING_DEFINE_OUTPUT( StdLogType)

After that, the logging framework is ready for action.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video