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 |
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).
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:
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:
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:
You can create one or more Logger instances that log messages to different destinations. There are several types of Logger objects available in BloCxx:
Description of the types of BloCxx Loggers:
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:
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:
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.
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.
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.
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.
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.
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:
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.
© 2009 Novell, Inc. All Rights Reserved.