Novell Home

Using BloCxx/System Logging with BloCxx

From Developer Community

BloCxx includes great support for message logging in C++ applications, although the facilities BloCxx provides are a bit daunting at first glance. This tutorial will introduce you to the logging facilities in BloCxx, and in particular will show you how to make use of the Linux system logging facility, syslog, in your application.

Contents

BloCxx Logging Facilities

There are two basic components to the BloCxx logging scheme: Loggers and LogAppenders. LogAppenders are object representations of destinations for log messages. Loggers are used to write messages to a logging destination. In particular, one key subclass of Logger will send log messages to one or more destinations as defined by a LogAppender instance. Thus, in BloCxx, these is a distinction and separation between how a destination is being written to (the Logger) and what the destination is (the LogAppender).

BloCxx LogAppenders

In BloCxx, a LogAppender is basically a destination for logging. When you set up a message logging scheme in your application, you will first decide what destination should be used for a certain type of logging. This is where the LogAppender comes in.


Here is the inheritance hierarchy for the LogAppender class and child classes:

Image:LogAppenderHierarchy.png


When you set up a logging scheme, you will usually select one of the child classes as your destination, or define your own destination by subclassing either LogAppender or one of the child classes. LogAppender is an abstract base class and cannot be directly instantiated.

Description of LogAppender destinations:

  • NullAppender - Sends log messages to the "bit bucket".
  • CerrAppender - Sends log messages to the standard error stream.
  • FileAppender - Sends log messages to a file that you specify.
  • SyslogAppender - Sends log messages to the system logging facility.


When you create a LogAppender, you should use the LogAppenderRef class as the type of the instantiated LogAppender subclass. LogAppenderRef is a reference-counted pointer to the appropriate subclass of LogAppender.


Here is how you create a LogAppender:

    #include <blocxx/LogAppender.hpp>
    #include <blocxx/AppenderLogger.hpp>
    #include <blocxx/Logger.hpp>
    #include <blocxx/LogConfig.hpp>
    #include <blocxx/String.hpp>
    using namespace BLOCXX_NAMESPACE;

    // ...

    LoggerConfigMap configmap;
    LogAppenderRef systemLogAppender =
        LogAppender::createLogAppender("syslog",
                                       LogAppender::ALL_COMPONENTS,
                                       LogAppender::ALL_CATEGORIES,
                                       LogAppender::STR_TTCC_MESSAGE_FORMAT,
                                       LogAppender::TYPE_SYSLOG,
                                       configmap);

We use a factory method, LogAppender::createLogAppender(), to create our LogAppender instance. We have to pass in the following pieces of information:

  • The name of the logger to create (in our case, "syslog").
  • The message components that the logger will log. In our case, we specify LogAppender::ALL_COMPONENTS which means that any Logger using this LogAppender will have messages logged to this destination regardless of the component specified. It is also possible to provide an array of Strings, specifying the components that can be logged to this destination.
  • The message categories that the logger will log. In our case, we specify LogAppender::ALL_CATEGORIES which means that any Logger using this LogAppender will have messages logged to this destination regardless of the category specified. This filter behaves similarly to the components filter above.
  • The format of messages being written to the destination. LogAppender::STR_TTCC_MESSAGE_FORMAT refers to the Log4J "Time Thread Category Component" logging format.
  • The type of LogAppender to create. This is a String value. In our case, we pass in LogAppender::TYPE_SYSLOG which has the value of "syslog". This will cause the factory method to instantiate and return a SyslogAppender object. The other types are:
    • LogAppender::TYPE_STDERR - will create a CerrAppender
    • LogAppender::TYPE_NULL - will create a NullAppender
    • Any other String, other than "stderr", "syslog", "", and "null", will return a FileAppender that sends messages to a file by the same name as the String passed in.
  • The configuration map. The configuration map should be empty unless you are creating a FileAppender, but it is still required in every case.


BloCxx Loggers

You can create one or more Logger instances that log messages to different destinations. There are several types of Logger objects available in BloCxx:

Image:LoggerHierarchy.png


Description of the types of BloCxx Loggers:

  • NullLogger - All messages sent to this logger are discarded.
  • CerrLogger - Sends all messages to standard error.
  • AppenderLogger - Sends all messages to one or more LogAppender objects.


The class you really want to know how to use is the AppenderLogger class. By using AppenderLoggers in conjuction with LogAppenders, you can have a great deal of flexibility. For example:

  • You can set up the logging independently of the destination in your code. This makes it easy to send messages to stderr during the code writing phase, and then later send them to syslog.
  • You can easily log to a different destination based on the type of compilation - for example, log to stderr for debug builds and to syslog for production builds.
  • You can create a destination that is written to by several different loggers.
  • You can create a logger that can write to several different destinations with a single write request.


Given the LogAppender we created with the sample above, here is a sample of how we would tie that LogAppender to a Logger instance:

    #include <blocxx/LogAppender.hpp>
    #include <blocxx/AppenderLogger.hpp>
    #include <blocxx/Logger.hpp>
    #include <blocxx/LogConfig.hpp>
    #include <blocxx/String.hpp>
    using namespace BLOCXX_NAMESPACE;

    // ...

    LoggerRef systemLogger = new AppenderLogger("my_application_name", E_ERROR_LEVEL, systemLogAppender);
    Logger::setDefaultLogger(systemLogger);
    Logger::setThreadLogger(systemLogger);


Again we see the use of smart pointers. LoggerRef is a smart pointer to a Logger subclass, in this case, to an AppenderLogger. We provide three pieces of information:

  • The default component. This String value also shows up in syslog. Here we choose to provide the name of the running application.
  • The error level threshold for messages, or in other words, the lowest message severity that this Logger will log. All more severe messages will be logged also, but less severe ones will not. The possible values for this include:
    • E_FATAL_ERROR_LEVEL
    • E_ERROR_LEVEL
    • E_INFO_LEVEL
    • E_DEBUG_LEVEL
  • The LogAppender to use. We pass in systemLogAppender, the name of the instance we created before.

After creating our Logger instance, you'll also notice that we call two more methods to make this logger accessible. Calling Logger::SetDefaultLogger() makes this Logger the default logger for the entire application. Calling Logger::setThreadLogger() makes this Logger the default logger for the current thread, overriding any previously-set default. You can always retrieve the default logger by calling Logger::getCurrentLogger(). This means that you can set up at least your default logging scheme without having to keep the Logger instance around and handy.


Writing Log Messages

Now that we've figured out how to set up our logging scheme, it is time to figure out how to do what we intended to do in the first place - write log messages. There are basically two ways to do this in BloCxx.


Log methods of the Logger class

The Logger class provides several methods for logging messages: logFatalError(), logError(), logInfo(), and logDebug(), as well as the more generic logMessage().

The first four of these methods assume the default component for this Logger, which can be set by passing the component name as a String to the setDefaultComponent() method. logMessage() has several variants, including some where you can specify the component. This allows you to work with the LogAppender objects so only certain components actually get logged. Others allow you to ignore the default log level of the LogAppender.

Of all these methods, logMessage() requires the most typing to get the job done. But if you need flexibility, this is the one to use.


Logging Macros

BloCxx also provides several macros for logging messages: BLOCXX_LOG_FATAL_ERROR(), BLOCXX_LOG_ERROR(), BLOCXX_LOG_INFO(), and BLOCXX_LOG_DEBUG(), as well as the more generic BLOCXX_LOG(). The first four require a a Logger instance as well as the message you want to log. Unlike the Logger class methods, the macros will automatically write the correct __FILE__ and __LINE__ values in your log message. The macros also evaluate your log level before trying to process the message. For example, if instead of passing a simple String as the value to log you invoke the Format() method to return a formatted string, using the Logger methods will cause the message to be formatted always, perhaps unnecessarily if the log level is of a lower importance than the LogAppender will accept. In both cases the log level filter is applied, but the macro applies the filter before the formatting takes place.

BloCxx also provides stream logging variants for each of these macros, i.e. there is a BLOCXX_SLOG_INFO() macro that does the same thing as BLOCXX_LOG_INFO(). The difference is that BLOCXX_SLOG_INFO() and the like will accept a stream-like argument as the second parameter of the macro.

The stream logging variants are convenient especially if you are used to C++-style insertion streams. However, avoid using these variants if you are passing in a plain String or something similar. If you don't need formatting or insertion into a stream, you will waste CPU as the macro creates a stringstream object unnecessarily.


Examples

Here are six different ways to write the same log message:

    #include <blocxx/LogAppender.hpp>
    #include <blocxx/AppenderLogger.hpp>
    #include <blocxx/Logger.hpp>
    #include <blocxx/LogConfig.hpp>
    #include <blocxx/String.hpp>
    #include <blocxx/Format.hpp>
    using namespace BLOCXX_NAMESPACE;

    // ...

    String logLevel("INFO");
    systemLogger->logMessage(execName,
                             logLevel,
                             Format("This is log message %1 of level %2 for component %3",1,logLevel,execName),
                             __FILE__,
                             __LINE__,
                             BLOCXX_LOGGER_PRETTY_FUNCTION);
    systemLogger->logInfo(Format("This is log message %1 of level %2 for component %3",2,logLevel,execName),
                          __FILE__,
                          __LINE__,
                          BLOCXX_LOGGER_PRETTY_FUNCTION);
    systemLogger->logInfo(Format("This is log message %1 of level %2 for component %3",3,logLevel,execName));
    BLOCXX_LOG_INFO(systemLogger,
                    Format("This is log message %1 of level %2 for component %3",4,logLevel,execName));
    BLOCXX_SLOG_INFO(systemLogger,"This is log message " << 5 << " of level " << logLevel << " for component " << execName );
    BLOCXX_SLOG_INFO(Logger::getCurrentLogger(),"This is log message " << 6 << " of level " << logLevel << " for component " << execName );
 

The results from /var/log/messages:

Jan 25 11:47:15 my_host blocxx: 0 [1077336288] INFO  blocxx_app - This is log message 1 of level INFO for component blocxx_app
Jan 25 11:47:15 my_host blocxx: 0 [1077336288] INFO  blocxx_app - This is log message 2 of level INFO for component blocxx_app
Jan 25 11:47:15 my_host blocxx: 0 [1077336288] INFO  blocxx_app - This is log message 3 of level INFO for component blocxx_app
Jan 25 11:47:15 my_host blocxx: 0 [1077336288] INFO  blocxx_app - This is log message 4 of level INFO for component blocxx_app
Jan 25 11:47:15 my_host blocxx: 1 [1077336288] INFO  blocxx_app - This is log message 5 of level INFO for component blocxx_app
Jan 25 11:47:15 my_host blocxx: 1 [1077336288] INFO  blocxx_app - This is log message 6 of level INFO for component blocxx_app


As we can see, all six variants log the same log message. Some of these are more flexible but also rather verbose; others are more succinct at the expense of flexibility.


Which Should I Use?

  1. Prefer the BLOCXX_LOG_* macros when it is possible to use them. These macros will avoid unnecessary formating processing for messages that won't be logged because they are beneath the specified severity threshold.
  2. If your message to be logged requires formatting, it makes no real difference whether you use the BLOCXX_LOG_* macros or the BLOCXX_SLOG_* macros. Both will eventually use a stringstream to format your message.
  3. Use the logging methods of the Logger class when you need more flexibility in logging than what the macros can provide.
  4. Remember that you can always get the current logger by calling Logger::getCurrentLogger() if you have previously set it using Logger::setDefaultLogger() or Logger::setThreadLogger(). You don't need to keep an instance around if you don't want to.


Implementation Example

In this example, I have a very simple Linux application written in C++. When I start execution, before I do anything major, I'm going to do three things:

  1. Initialize logging
  2. Set up default signal handlers
  3. Read and store my configuration

I do logging first, because then at least I have a way to inform the system of what is going on with the other steps. Here is my implementation of the function that initializes the logging:

#include <blocxx/LogAppender.hpp>
#include <blocxx/AppenderLogger.hpp>
#include <blocxx/Logger.hpp>
#include <blocxx/LogConfig.hpp>
#include <blocxx/String.hpp>
#include <blocxx/Format.hpp>

using namespace BLOCXX_NAMESPACE;

// ...

void initLogging()
{
    LoggerConfigMap configmap;
    Array<LogAppenderRef> logappenders;
    logappenders.push_back(LogAppender::createLogAppender("syslog",
                                                          LogAppender::ALL_COMPONENTS,
                                                          LogAppender::ALL_CATEGORIES,
                                                          LogAppender::STR_TTCC_MESSAGE_FORMAT,
                                                          LogAppender::TYPE_SYSLOG,
                                                          configmap));
#ifndef NDEBUG
    logappenders.push_back(LogAppender::createLogAppender("cerr",
                                                          LogAppender::ALL_COMPONENTS,
                                                          LogAppender::ALL_CATEGORIES,
                                                          LogAppender::STR_TTCC_MESSAGE_FORMAT,
                                                          LogAppender::TYPE_STDERR,
                                                          configmap));
#endif // NDEBUG
    systemLogger = new AppenderLogger(execName,
                                      logappenders);
    Logger::setDefaultLogger(systemLogger);
    Logger::setThreadLogger(systemLogger);

    return;
}

In this sample, we set up logging to syslog by default for any logging that uses the currently defined default logger. In addition, this same logger will also print the messages to standard error - but only if NDEBUG is not defined. Generally, this is the case, unless you specifically define it. Defining NDEBUG usually implies that you are creating a production build.


Here's an example of code using the loggers once they are set up:

    initLogging();
    LoggerRef systemLogger = Logger::getCurrentLogger();
    BLOCXX_LOG_DEBUG(systemLogger,"Logging setup completed.");
    initSignalHandlers();
    BLOCXX_LOG_DEBUG(systemLogger,"System Handler setup completed.");
    loadConfiguration();
    BLOCXX_LOG_DEBUG(systemLogger,"Configuration loaded.");

This is extremely convenient. By using BloCxx's separation of loggers and logging destinations, and the support for multiple destinations by the AppenderLogger class, I can log messages to both standard error and to syslog with a single logging call. When I am ready to do a production build, I simply pass -DNDEBUG as one of the options to the compiler in my makefile. The application then writes only to syslog, and no longer to standard error. I didn't have to change a single line of code to cause this to come about.


Note: This article is a part of the Using BloCxx tutorial.

Novell® Making IT Work As One

© 2009 Novell, Inc. All Rights Reserved.