Every program needs to display diagnostics: error messages, debugging output, and so on. Traditionally, we might use a number of printf()
calls or cerr
statements in our application in order to help trace execution paths or display helpful runtime information. ACE's logging facility provides us with ways to do these things while at the same time giving us great control over how much of the information is printed and where it is directed.
It is important to have a convenient way to create debug statements. In this modern age of graphical source-level debuggers, it might seem strange to pepper your application with the equivalent of a bunch of print statements. However, diagnostic statements are useful both during development and long after an application is considered to be bug free.
• They can record information while the program is running and a debugger isn't available or practical, such as with a server.
• They can record output during testing for regression analysis, as the ACE test suite does.
The ACE mechanisms allow us to enable and disable these statements at compile time. When compiled in, they can also be enabled and disabled at will at runtime. Thus, you don't have to pay for the overhead—in either CPU cycles or disk space—under normal conditions. But if a problem arises, you can easily cause copious amounts of debugging information to be recorded to assist you in finding and fixing it. It is an unfortunate fact that many bugs will never appear until the program is in the hands of the end user.
In this chapter, we cover how to
• Use basic logging and tracing techniques
• Enable and disable display of various logging message severities
• Customize the logging mechanics
• Direct the output messages to various logging sinks
• Capture log messages before they're output
• Use the distributed ACE logging service
• Combine various logging facility features
• Dynamically configure logging sinks and severity levels
Three macros are commonly used to display diagnostic output from your code: ACE_DEBUG
, ACE_ERROR
, and ACE_TRACE
. The arguments to the first two are the same; their operation is nearly identical, so for our purposes now, we'll treat them the same. They both take a severity indicator as one of the arguments, so you can display any message using either; however, the convention is to use ACE_DEBUG
for your own debugging statements and ACE_ERROR
for warnings and errors. The use of these macros is the same:
ACE_DEBUG ((severity, formatting-args));
ACE_ERROR ((severity, formatting-args));
The severity
parameter specifies the severity level of your message. The most common levels are LM_DEBUG
and LM_ERROR
. All the valid severity values are listed in Table 3.1.
The formatting-args
parameter is a printf()
-like set of format conversion operators and formatting arguments for insertion into the output. The complete set of formatting directives is described in Table 3.2. One might wonder why printf()
-like formatting was chosen instead of the more natural—to C++ coders—C++ iostream-style formatting. In some cases, it would have been easier to correctly log certain types of information with type-safe insertion operators. However, an important factor in the logging facility's design is the ability to effectively “no-op” the logging statements at compile time. Note that the ACE_DEBUG
and ACE_ERROR
invocations require two sets of parentheses. The outer set delimits the single macro argument. This single argument comprises all the arguments, and their enclosing parentheses, needed for a method call. If the preprocessor macro ACE_NLOGGING
is defined, the ACE_DEBUG
macro will expand to a blank line, ignoring the content of the inner set of parentheses. Achieving this same optimization with insertion operators would have resulted in a rather odd usage:
ACE_DEBUG ((debug_info << "Hi Mom" << endl));
Similarly, many of the formatting tokens, such as %I
, would have been awkward to implement and overly verbose to use:
ACE_DEBUG((debug_info<<ACE_Log_Msg::nested_indent<<"Hi Mom"<<endl));
One could argue away the compile-time optimization by causing ACE_NLOGGING
to put the debug output stream object into a no-op mode. That may be sufficient for some platforms, but for others, such as embedded real-time systems, you really do want the code to simply not exist.
Unlike ACE_DEBUG
and ACE_ERROR
, which cause output where the macro is placed, ACE_TRACE
causes one line of debug information to be printed at the point of the ACE_TRACE
statement and another when its enclosing scope is exited. Therefore, placing an ACE_TRACE
statement at the beginning of a function or method provides a trace of when that function or method is entered and exited. The ACE_TRACE
macro accepts a single character string rather than a set of formatting directives. Because C++ doesn't have a handy way to dump a stack trace, this can be very useful indeed.
Let's take a look at a simple application:
#include "ace/Log_Msg.h"
void foo (void);
int ACE_TMAIN (int, ACE_TCHAR *[])
{
ACE_TRACE(ACE_TEXT ("main"));
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IHi Mom
")));
foo();
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IGoodnight
")));
return 0;
}
void foo (void)
{
ACE_TRACE (ACE_TEXT ("foo"));
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IHowdy Pardner
")));
}
Our first step is always to include the Log_Msg.h
header file. It defines many helpful macros, including ACE_DEBUG
and ACE_ERROR
, to make your life easier. The full set of output-producing macros is listed in Table 3.3.
You can use ACE_DEBUG
to print just about any arbitrary string you want, and the many format directives listed in Table 3.2 can also be modified with printf()
-style modifiers for length, precision, and fill adjustments. (See a printf()
reference for details on the modifiers.) In the preceding example, we've used %I
so that the ACE_DEBUG
messages are nicely indented along with the ACE_TRACE
messages.
If you compile and execute the preceding code, you should get something like this:
(1024) calling main in file `Simple1.cpp' on line 7
Hi Mom
(1024) calling foo in file `Simple1.cpp' on line 18
Howdy Pardner
(1024) leaving foo
Goodnight
(1024) leaving main
The compile-time values of three configuration settings control whether the logging macros produce logging method calls: ACE_NTRACE
, ACE_NDEBUG
, and ACE_NLOGGING
. These macros are all interpreted as “not.” For example, ACE_NTRACE
is “not tracing” when its value is 1. To enable the configuration area, set the macro to 0. ACE_NTRACE
usually defaults to 1 (disabled), and the others default to 0 (enabled). Table 3.3 shows which configuration setting controls each logging macro. This allows you to sprinkle your code with as little or as much debug information as you want and then turn it on or off when compiling.
When deciding which features to enable, be aware that ACE_TRACE
output is conditional on both the ACE_NTRACE
and ACE_NDEBUG
configuration settings. The reason is that the ACE_TRACE
macro, when enabled, expands to instantiate an ACE_Trace
object. The ACE_Trace
class's constructor and destructor use ACE_DEBUG
to log the entry and exit messages. They're logged at the LM_TRACE
severity level, so that level also must be enabled at runtime to show any tracing output; it is enabled by default.
Consider this slightly modified code:
#include "ace/Log_Msg.h"
void foo(void);
int ACE_TMAIN (int, ACE_TCHAR *[])
{
ACE_TRACE (ACE_TEXT ("main"));
ACE_LOG_MSG->priority_mask (LM_DEBUG | LM_NOTICE,
ACE_Log_Msg::PROCESS);
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IHi Mom
")));
foo ();
ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("%IGoodnight
")));
return 0;
}
void foo(void)
{
ACE_TRACE (ACE_TEXT ("foo"));
ACE_DEBUG ((LM_NOTICE, ACE_TEXT ("%IHowdy Pardner
")));
}
The following output is produced:
(1024) calling main in file `Simple2.cpp' on line 7
Howdy Pardner
Goodnight
In this example, we changed the logging level at runtime so that only messages logged with LM_DEBUG
and LM_NOTICE
priority are displayed; all others are ignored. The LM_INFO
“Hi Mom” message is not displayed, and there is no ACE_TRACE
output.
We've also revealed a little more about how ACE's logging facility works. The ACE_Log_Msg
class implements the log message formatting capabilities in ACE. ACE automatically maintains a thread-specific singleton instance of the ACE_Log_Msg
class for each spawned thread, as well as the main thread. ACE_LOG_MSG
is a shortcut for obtaining the pointer to the thread's singleton instance. All the ACE logging macros use ACE_LOG_MSG
to make method calls on the correct ACE_Log_Msg
instance. There is seldom a reason to instantiate an ACE_Log_Msg
object directly. ACE automatically creates a new instance for each thread spawned, keeping each thread's logging output separate.
We can use the ACE_Log_Msg::priority_mask()
method to set the logging severity levels we desire output to be produced for: All the available logging levels are listed in Table 3.1. Each level is represented by a mask, so the levels can be combined. Let's look at the complete signature of the priority_mask()
methods:
/// Get the current ACE_Log_Priority mask.
u_long priority_mask (MASK_TYPE = THREAD);
/// Set the ACE_Log_Priority mask, returns original mask.
u_long priority_mask (u_long, MASK_TYPE = THREAD);
The first version is used to read the severity mask; the second changes it and returns the original mask so it can be restored later. The second argument must be one of two values, reflecting two different scopes of severity mask setting:
ACE_Log_Msg::PROCESS
: Specifying PROCESS
retrieves or sets the processwide mask affecting logging severity for all ACE_Log_Msg
instances.ACE_Log_Msg::THREAD
: Each ACE_Log_Msg
instance also has its own severity mask, and this value retrieves or sets it. THREAD
is technically a misnomer, as it refers to the ACE_Log_Msg
instance the method is invoked on, and you can create ACE_Log_Msg
instances in addition to those that ACE creates for each thread. However, that is a relatively rare thing to do, so we usually simply refer to ACE_Log_Msg
instances as thread specific.When evaluating a log message's severity, ACE_Log_Msg
examines both the processwide and per instance severity masks. If either of them has the message's severity enabled, the message is logged. By default, all bits are set at the process level and none at the instance level, so all message severities are logged. To make each thread decide for itself which severity levels will be logged, set the processwide mask to 0 and allow each thread set its own per instance mask. For example, the following code disables all logging severities at the process level and enables LM_DEBUG
and LM_NOTICE
severities in the current thread only:
ACE_LOG_MSG->priority_mask (0, ACE_Log_Msg::PROCESS);
ACE_LOG_MSG->priority_mask (LM_DEBUG | LM_NOTICE,
ACE_Log_Msg::THREAD);
A third mask maintained by ACE_Log_Msg
is important when you start setting individual severity masks on ACE_Log_Msg
instances. The per instance default mask is used to initialize each ACE_Log_Msg
instance's severity mask. The per instance default mask is initially 0 (no severities are enabled). Because each ACE_Log_Msg
instance's severity mask is set from the default value when the instance is created, you can change the default for groups of threads before spawning them. This puts the logging policy into the thread-spawning part of your application, alleviating the need for the threads to set their own level, although each thread can change its ACE_Log_Msg
instance's mask at any time. Consider this example:
ACE_LOG_MSG->priority_mask (0, ACE_Log_Msg::PROCESS);
ACE_Log_Msg::enable_debug_messages ();
ACE_Thread_Manager::instance ()->spawn (service);
ACE_Log_Msg::disable_debug_messages ();
ACE_Thread_Manager::instance ()->spawn_n (3, worker);
We'll learn about thread management in Chapter 13. For now, all you need to know is that ACE_Thread_Manager::spawn()
spawns one thread and that ACE_Thread_Manager::spawn_n()
spawns multiple threads. In the preceding example, the processwide severity mask is set to 0 (all disabled). This means that each ACE_Log_Msg
instance's mask controls its enabled severities totally. The thread executing the service()
function will have the LM_DEBUG
severity enabled, but the threads executing the worker()
function will not.
The complete method signatures for changing the per instance default mask are:
static void disable_debug_messages
(ACE_Log_Priority priority = LM_DEBUG);
static void enable_debug_messages
(ACE_Log_Priority priority = LM_DEBUG);
Our example used the default argument, LM_DEBUG
, in both cases. Even though the method names imply that LM_DEBUG
is the only severity that can be changed, you can also supply any set of legal severity masks to either method. Unlike the priority_mask()
method, which replaces the specified mask, the enable_debug_messages()
and disable_debug_messages()
methods add and subtract, respectively, the specified severity bits in both the calling thread's per instance mask and the per instance default severity mask.
Of course, you can use any message severity level at any time. However, take care to specify a reasonable level for each of your messages; then at runtime, you can use the priority_mask()
method to enable or disable messages you're interested in. This allows you to easily overinstrument your code and then enable only the things that are useful at any particular time.
ACE_Log_Msg
has a rich set of methods for recording the current state of your application. Table 3.4 summarizes the more commonly used functions. Most methods have both accessor and mutator signatures. For example, there are two op_status()
methods:
int op_status(void);
void op_status(int status));
Although the method calls are most often made indirectly via the ACE logging macros, they are also available for direct use.
In most cases, people will use the standard ACE tracing and logging macros shown in Table 3.3. Sometimes, however, their behavior may need to be customized. Or you might want to create wrapper macros in anticipation of future customization.
Perhaps you want to ensure that all your LM_DEBUG
messages contain a particular text string so that you can easily grep for them in your output file. Or maybe you want to ensure that every one of them is prefixed with the handy “%I” directive so they indent properly. If you lay the groundwork at the beginning of your project and encourage your coders to use your macros, it will be easy to implement these kinds of things in the future.
The following macro definitions wrap the ACE_DEBUG
macro in a handy way. Note how we've guaranteed that every message will be properly indented, and we've prefixed each message to make searching for specific strings in the output easier.
#define DEBUG_PREFIX ACE_TEXT ("DEBUG%I")
#define INFO_PREFIX ACE_TEXT ("INFO%I")
#define NOTICE_PREFIX ACE_TEXT ("NOTICE%I")
#define WARNING_PREFIX ACE_TEXT ("WARNING%I")
#define ERROR_PREFIX ACE_TEXT ("ERROR%I")
#define CRITICAL_PREFIX ACE_TEXT ("CRITICAL%I")
#define ALERT_PREFIX ACE_TEXT ("ALERT%I")
#define EMERGENCY_PREFIX ACE_TEXT ("EMERGENCY%I")
#define MY_DEBUG(FMT, ...)
ACE_DEBUG(( LM_DEBUG,
DEBUG_PREFIX FMT
__VA_ARGS__))
#define MY_INFO(FMT, ...)
ACE_DEBUG(( LM_INFO,
INFO_PREFIX FMT
__VA_ARGS__))
#define MY_NOTICE(FMT, ...)
ACE_DEBUG(( LM_NOTICE,
NOTICE_PREFIX FMT
__VA_ARGS__))
#define MY_WARNING(FMT, ...)
ACE_DEBUG(( LM_WARNING,
WARNING_PREFIX FMT
__VA_ARGS__))
#define MY_ERROR(FMT, ...)
ACE_DEBUG(( LM_ERROR,
ERROR_PREFIX FMT
__VA_ARGS__))
#define MY_CRITICAL(FMT, ...)
ACE_DEBUG(( LM_CRITICAL,
CRITICAL_PREFIX FMT
__VA_ARGS__))
#define MY_ALERT(FMT, ...)
ACE_DEBUG(( LM_ALERT,
ALERT_PREFIX FMT
__VA_ARGS__))
#define MY_EMERGENCY(FMT, ...)
ACE_DEBUG(( LM_EMERGENCY,
EMERGENCY_PREFIX FMT
__VA_ARGS__))
Of course, it would be more useful if each of our prefixes were surrounded by an #ifdef
to allow them to be overridden, but we leave that as an exercise to the reader.
Using these macros instead of the usual ACE_DEBUG
macros is, as expected, easy to do:
#include "Trace.h"
void foo (void);
int ACE_TMAIN (int, ACE_TCHAR *[])
{
ACE_TRACE (ACE_TEXT ("main"));
MY_DEBUG (ACE_TEXT ("Hi Mom
"));
foo ();
MY_DEBUG (ACE_TEXT ("Goodnight
"));
return 0;
}
void foo (void)
{
ACE_TRACE (ACE_TEXT ("foo"));
MY_DEBUG (ACE_TEXT ("Howdy Pardner
"));
}
Our output is nicely indented and prefixed as requested:
(1024) calling main in file `Wrap_Macros.cpp' on line 11
DEBUG Hi Mom
(1024) calling foo in file `Wrap_Macros.cpp' on line 20
DEBUG Howdy Pardner
(1024) leaving foo
DEBUG Goodnight
(1024) leaving main
The __VA_ARGS__
trick works fine for recent versions of the GNU C/C++ preprocessor but may not be available everywhere else, so be sure to read your compiler's documentation before committing yourself to this particular approach. If something similar isn't available to you, you can use a slightly less elegant approach:
#define MY_DEBUG LM_DEBUG, ACE_TEXT ("DEBUG%I")
#define MY_INFO LM_INFO, ACE_TEXT ("INFO%I")
#define MY_NOTICE LM_NOTICE, ACE_TEXT ("NOTICE%I")
#define MY_WARNING LM_WARNING, ACE_TEXT ("WARNING%I")
#define MY_ERROR LM_ERROR, ACE_TEXT ("ERROR%I")
#define MY_CRITICAL LM_CRITICAL, ACE_TEXT ("CRITICAL%I")
#define MY_ALERT LM_ALERT, ACE_TEXT ("ALERT%I")
#define MY_EMERGENCY LM_EMERGENCY, ACE_TEXT ("EMERGENCY%I")
This approach could be used something like this:
ACE_DEBUG ((MY_DEBUG ACE_TEXT ("Hi Mom
")));
ACE_DEBUG ((MY_DEBUG ACE_TEXT ("Goodnight
")));
It produces exactly the same output at the expense of slightly less attractive code.
We will now create an ACE_TRACE
variant that will display the line number at which a function exits. The default ACE_Trace
object implementation doesn't do this and doesn't provide an easy way for us to extend it, so, unfortunately, we have to create our own object from scratch. However, we can cut and paste from the ACE_Trace
implementation in order to give ourselves a head start.
Consider this simple class:
class Trace
{
public:
Trace (const ACE_TCHAR *prefix,
const ACE_TCHAR *name,
int line,
const ACE_TCHAR *file)
{
this->prefix_ = prefix;
this->name_ = name;
this->line_ = line;
this->file_ = file;
ACE_Log_Msg *lm = ACE_LOG_MSG;
if (lm->tracing_enabled ()
&& lm->trace_active () == 0)
{
lm->trace_active (1);
ACE_DEBUG
((LM_TRACE,
ACE_TEXT ("%s%*s(%t) calling %s in file `%s'")
ACE_TEXT (" on line %d
"),
this->prefix_,
Trace::nesting_indent_ * lm->inc (),
ACE_TEXT (""),
this->name_,
this->file_,
this->line_));
lm->trace_active (0);
}
}
void setLine (int line)
{
this->line_ = line;
}
~Trace (void)
{
ACE_Log_Msg *lm = ACE_LOG_MSG;
if (lm->tracing_enabled ()
&& lm->trace_active () == 0)
{
lm->trace_active (1);
ACE_DEBUG
((LM_TRACE,
ACE_TEXT ("%s%*s(%t) leaving %s in file `%s'")
ACE_TEXT (" on line %d
"),
this->prefix_,
Trace::nesting_indent_ * lm->dec (),
ACE_TEXT (""),
this->name_,
this->file_,
this->line_));
lm->trace_active (0);
}
}
private:
enum { nesting_indent_ = 3 };
const ACE_TCHAR *prefix_;
const ACE_TCHAR *name_;
const ACE_TCHAR *file_;
int line_;
};
Trace
is a simplified version of ACE_Trace
. Because our focus is printing a modified function exit message, we chose to leave out some of the more esoteric ACE_Trace
functionality. We did, however, include a prefix parameter to the constructor so that each entry/exit message can be prefixed (before indentation), if you want. In an ideal world, you would simply use the following method to select the messages you're interested in: ACE_Log_Msg::priority_mask()
. On the other hand, if you're asked to do a postmortem analysis of a massive, all-debug-enabled log file, the prefixes can be quite handy.
With our new Trace
class available to us, we can now create a set of simple macros that will use this new class to implement function tracing in our code:
#define TRACE_PREFIX ACE_TEXT ("TRACE ")
#if (ACE_NTRACE == 1)
# define TRACE(X)
# define TRACE_RETURN(V)
# define TRACE_RETURN_VOID()
#else
# define TRACE(X)
Trace ____ (TRACE_PREFIX,
ACE_TEXT (X),
__LINE__,
ACE_TEXT (__FILE__))
# define TRACE_RETURN(V)
do { ____.setLine(__LINE__); return V; } while (0)
# define TRACE_RETURN_VOID()
do { ____.setLine(__LINE__); } while (0)
#endif
The addition of the TRACE_RETURN
and TRACE_RETURN_VOID
macros is how our Trace
object's destructor will know to print the line number at which the function exits. Each of these macros uses the convenient setLine()
method to set the current line number before allowing the Trace
instance to go out of scope, destruct, and print our message.
This is a simple example using our new object:
#include "Trace.h"
void foo (void);
int ACE_TMAIN (int, ACE_TCHAR *[])
{
TRACE (ACE_TEXT ("main"));
MY_DEBUG (ACE_TEXT ("Hi Mom
"));
foo ();
MY_DEBUG (ACE_TEXT ("Goodnight
"));
TRACE_RETURN (0);
}
void foo (void)
{
TRACE (ACE_TEXT ("foo"));
MY_DEBUG (ACE_TEXT ("Howdy Pardner
"));
TRACE_RETURN_VOID ();
}
It produces the following output:
TRACE (1024) calling main in file `Trace_Return.cpp' on line 11
DEBUG Hi Mom
TRACE (1024) calling foo in file `Trace_Return.cpp' on line 22
DEBUG Howdy Pardner
TRACE (1024) leaving foo in file `Trace_Return.cpp' on line 24
DEBUG Goodnight
TRACE (1024) leaving main in file `Trace_Return.cpp' on line 17
Although the output is a bit wordy, we succeeded in our original intent of printing the line number at which each function returns. Although that may seem like a small thing for a trivial program, consider the fact that few useful programs are trivial. If you are trying to understand the flow of a legacy application, it may well be worth your time to liberally instrument it with TRACE
and TRACE_RETURN
macros to get a feel for the paths taken. Of course, training yourself to use TRACE_RETURN
may take some time, but in the end, you will have a much better idea of how the code flows.
As our previous examples have shown, the default logging sink for ACE's logging facility is the standard error stream. In this section, we discuss output to the standard error stream, as well as two other common and useful targets:
• The system logger (UNIX syslog or NT Event Log)
• A programmer-specified output stream, such as a file
Output to the standard error stream (STDERR) is so common that it is, in fact, the default sink for all ACE logging messages. Our examples so far have taken advantage of this. Sometimes, you may want to direct your output not only to STDERR but also to one of the other targets available to you. In these cases, you will have to explicitly include STDERR in your choices:
int ACE_TMAIN (int, ACE_TCHAR *argv[])
{
// open() requires the name of the application
// (e.g. -- argv[0]) because the underlying
// implementation may use it in the log output.
ACE_LOG_MSG->open (argv[0], ACE_Log_Msg::STDERR);
or
ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("%IHi Mom
")));
ACE_LOG_MSG->set_flags (ACE_Log_Msg::STDERR);
foo ();
If you choose the second approach, it may be necessary to invoke clr_flags()
to disable any other output destinations. Everything after the set_flags()
will be directed to STDERR until you invoke clr_flags()
to prevent it. The complete signatures of these methods are:
// Enable the bits in the logger's options flags.
void set_flags (unsigned long f);
// Disable the bits in the logger's options flags.
void clr_flags (unsigned long f);
The set of defined flag values are listed in Table 3.5.
Most modern operating systems support the notion of a system logger. The implementation details range from a library of function calls to a network daemon. The general idea is that all applications direct their logging activity to the system logger, which will, in turn, direct it to the correct file(s) or other configurable destination(s). For example, UNIX system administrators can configure the UNIX syslog facility so that different classes and levels of logging get directed to different destinations. Such an approach provides a good combination of scalability and configurability.
To use the system logger, you would do something like this:
int ACE_TMAIN (int, ACE_TCHAR *argv[])
{
ACE_LOG_MSG->open
(argv[0], ACE_Log_Msg::SYSLOG, ACE_TEXT ("syslogTest"));
Although one would think that we could use the set_flags()
method to enable syslog output after the ACE_Log_Msg
instance has been opened, that isn't the case, unfortunately. Likewise, if you want to quit sending output to syslog, a simple clr_flags()
won't do the trick.
In order to communicate with the system logger, ACE_Log_Msg
must perform a set of initialization procedures that are done only in the open()
method. Part of the initialization requires the program name that will be recorded in syslog: (the third argument). If we don't do this when our program starts, we will have to do it later, in order to get the behavior we expect from invoking set_flags()
. Similarly, the open()
method will properly close down any existing connection to the system logger if invoked without the ACE_Log_Msg::SYSLOG
flag:
#include "ace/Log_Msg.h"
void foo (void);
int ACE_TMAIN (int, ACE_TCHAR *argv[])
{
// This will be directed to stderr (the default ACE_Log_Msg
// behavior).
ACE_TRACE (ACE_TEXT ("main"));
ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("%IHi Mom
")));
// Everything from foo() will be directed to the system logger
ACE_LOG_MSG->open
(argv[0], ACE_Log_Msg::SYSLOG, ACE_TEXT ("syslogTest"));
foo ();
// Now we reset the log output to default (stderr)
ACE_LOG_MSG->open (argv[0]);
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IGoodnight
")));
return 0;
}
void foo (void)
{
ACE_TRACE (ACE_TEXT ("foo"));
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IHowdy Pardner
")));
}
Although it may seem strange to invoke ACE_LOG_MSG->open()
more than once in your application, nothing is wrong with it. Think of it as more of a reopen. Before we end this chapter, we will create a simple LogManager
class to help hide some of these kinds of details.
Directing logging output to SYSLOG
means different things on different platforms, according to what the platform's native “system logger” is and what it is capable of. If the runtime platform doesn't support any type of system logger, directing output to SYSLOG
has no effect. The following platforms have SYSLOG
support in ACE:
• Windows NT 4 and newer, such as Windows 2000 and XP: ACE directs SYSLOG
output to the system's Event Log. The third argument to ACE_Log_Msg::open()
is an ACE_TCHAR*
character string. It is optional; if supplied, it replaces the program name as the event source name for recording events in the system's event log. The ACE message severities are mapped to Event Log severities, as shown in Table 3.6.
• UNIX/Linux: ACE directs SYSLOG
output to the syslog facility. The syslog facility has its own associated configuration details about logging facilities, which are different from ACE's logging severity levels. ACE's syslog back end specifies the LOG_USER
syslog facility by default. This value can be changed at compile time by changing the config.h
setting ACE_DEFAULT_SYSLOG_FACILITY
. Please consult the syslog man page for details on how to configure the logging destination for the specified facility.
The preferred way to handle output to files and other targets in C++ is output streams (C++ ostream
objects). They provide enhanced functionality over the printf()
family of functions and usually result in more readable code. The ACE_Log_Msg::msg_ostream()
method lets us provide an output stream on which the logger will write our information:
ACE_OSTREAM_TYPE *output =
new std::ofstream ("ostream.output.test");
ACE_LOG_MSG->msg_ostream (output, 1);
ACE_LOG_MSG->set_flags (ACE_Log_Msg::OSTREAM);
ACE_LOG_MSG->clr_flags (ACE_Log_Msg::STDERR);
Note that it's perfectly safe to select OSTREAM
as output—via either open()
or set_flags()
—and then generate logging output before you invoke msg_ostream()
. If you do so, the output will simply disappear, because no ostream
is assigned. Also note that we have used the two-argument version of msg_ostream()
. This not only sets the ostream
for the ACE_Log_Msg
instance to use but also tells ACE_Log_Msg
that it should assume ownership and delete the ostream
instance when the ACE_Log_Msg
object is deleted. The single-argument version of msg_ostream()
doesn't specify its default behavior with regard to ownership, so it pays to be explicit in your wishes.
You may wonder why the stream type is ACE_OSTREAM_TYPE
instead of simply std::ostream
. This is another aspect of ACE that helps its portability to platforms of all sizes and capabilities. ACE_OSTREAM_TYPE
can be defined with or without the std
namespace declaration, and it can also be defined as FILE
for platforms without any C++ iostream support at all, such as some embedded environments.
We can now easily combine all these techniques and distribute our logging information among all three choices:
#include "ace/Log_Msg.h"
#include "ace/streams.h"
int ACE_TMAIN (int, ACE_TCHAR *argv[])
{
// Output to default destination (stderr)
ACE_LOG_MSG->open (argv[0]);
ACE_TRACE (ACE_TEXT ("main"));
ACE_OSTREAM_TYPE *output =
new std::ofstream ("ostream.output.test");
ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("%IThis will go to STDERR
")));
ACE_LOG_MSG->open
(argv[0], ACE_Log_Msg::SYSLOG, ACE_TEXT ("syslogTest"));
ACE_LOG_MSG->set_flags (ACE_Log_Msg::STDERR);
ACE_DEBUG
((LM_DEBUG, ACE_TEXT ("%IThis goes to STDERR & syslog
")));
ACE_LOG_MSG->msg_ostream (output, 0);
ACE_LOG_MSG->set_flags (ACE_Log_Msg::OSTREAM);
ACE_DEBUG ((LM_DEBUG,
ACE_TEXT ("%IThis will go to STDERR, ")
ACE_TEXT ("syslog & an ostream
")));
ACE_LOG_MSG->clr_flags (ACE_Log_Msg::OSTREAM);
delete output;
return 0;
}
Beware of a subtle bug waiting to get you when you use an ostream
. Note that before we deleted the ostream instance output
, we first cleared the OSTREAM
flag on the ACE_Log_Msg
instance. Remember that the ACE_TRACE
for main
still has to write its final message when the trace instance goes out of scope at the end of main()
. If we delete the ostream
without removing the OSTREAM
flag, ACE_Log_Msg
will dutifully attempt to write that final message on a deleted ostream
instance, and your program will most likely crash.
To this point, we've been content to give our logging output to ACE_Log_Msg
, which formatted the messages and directed them to the configured logging sinks. For most cases, that will be fine. What if, though, we want to do something with that output ourselves? Can we inspect or even modify the logging output before it reaches its final destination? Of course. That's where ACE_Log_Msg_Callback
comes in.
Using a callback object is quite easy. Follow these steps:
ACE_Log_Msg_Callback
, and reimplement the following method:
virtual void log (ACE_Log_Record &log_record);
ACE_Log_Msg
instance, pass a pointer to your callback object to the ACE_Log_Msg::msg_callback()
method.ACE_Log_Msg::set_flags()
to enable output to your callback object.Once registered and enabled, your callback object's log()
method will be invoked with an ACE_Log_Record
object any time ACE_Log_Msg::log()
is invoked. As it turns out, that is exactly what happens when an output-producing ACE logging macro is used.
Some important caveats to remember when using the callback approach are documented on the ACE_Log_Msg_Callback
reference page. They bear repeating here.
• Callback registration and enabling are specific to each ACE_Log_Msg
instance. Therefore, a callback set up in one thread won't be used by any other thread in your application.
• Callback objects are not inherited by the ACE_Log_Msg
instances created for any threads you create. So if you're going to be using callback objects with multithreaded applications, you need to take special care that each thread is given an appropriate callback instance. It is possible to use a single object safely: see the description of ACE_Singleton
in Section 1.6.3.
• As with the OSTREAM
caveat, be sure that you don't delete a callback instance that might still be used by the ACE_Log_Msg
instance it's registered with.
A simple callback implementation follows:
#include "ace/streams.h"
#include "ace/Log_Msg.h"
#include "ace/Log_Msg_Callback.h"
#include "ace/Log_Record.h"
class Callback : public ACE_Log_Msg_Callback
{
public:
void log (ACE_Log_Record &log_record) {
log_record.print (ACE_TEXT (""), 0, cerr);
log_record.print (ACE_TEXT (""), ACE_Log_Msg::VERBOSE, cerr);
}
};
The program that uses it follows:
#include "ace/Log_Msg.h"
#include "Callback.h"
int ACE_TMAIN (int, ACE_TCHAR *[])
{
Callback *callback = new Callback;
ACE_LOG_MSG->set_flags (ACE_Log_Msg::MSG_CALLBACK);
ACE_LOG_MSG->clr_flags (ACE_Log_Msg::STDERR);
ACE_LOG_MSG->msg_callback (callback);
ACE_TRACE (ACE_TEXT ("main"));
ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("%IHi Mom
")));
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IGoodnight
")));
return 0;
}
The program creates this output:
(1024) calling main in file `Use_Callback.cpp' on line 12
Sep 24 12:35:02.829 2003@@22396@LM_TRACE@(1024) calling main in file `Use_Callback.cpp' on line 12
Hi Mom
Sep 24 12:35:02.830 2003@@22396@LM_DEBUG@ Hi Mom
Goodnight
Sep 24 12:35:02.830 2003@@22396@LM_INFO@ Goodnight
(1024) leaving main
Sep 24 12:35:02.830 2003@@22396@LM_TRACE@(1024) leaving main
The first log_record.print()
simply prints the message we've always seen. The second, however, uses the VERBOSE
flag to provide much more information. Both direct their output to the standard error stream.
Once you have access to the ACE_Log_Record
instance, you have control to do anything you want. Let's take a look at a bit more of the information contained in ACE_Log_Record
:
#include "ace/streams.h"
#include "ace/Log_Msg_Callback.h"
#include "ace/Log_Record.h"
#include "ace/SString.h"
class Callback : public ACE_Log_Msg_Callback
{
public:
void log (ACE_Log_Record &log_record)
{
cerr << "Log Message Received:" << endl;
unsigned long msg_severity = log_record.type ();
ACE_Log_Priority prio =
ACE_static_cast (ACE_Log_Priority, msg_severity);
const ACE_TCHAR *prio_name =
ACE_Log_Record::priority_name (prio);
cerr << " Type: "
<< ACE_TEXT_ALWAYS_CHAR (prio_name)
<< endl;
cerr << " Length: " << log_record.length () << endl;
const time_t epoch = log_record.time_stamp ().sec ();
cerr << " Time_Stamp: "
<< ACE_TEXT_ALWAYS_CHAR (ACE_OS::ctime (&epoch))
<< flush;
cerr << " Pid: " << log_record.pid () << endl;
ACE_CString data (">> ");
data += ACE_TEXT_ALWAYS_CHAR (log_record.msg_data ());
cerr << " MsgData: " << data.c_str () << endl;
}
};
The following output is created:
Log Message Received:
Type: LM_TRACE
Length: 88
Time_Stamp: Wed Sep 24 12:35:09 2003
Pid: 22411
MsgData: >> (1024) calling main in file `Use_Callback2.cpp' on line 12
Log Message Received:
Type: LM_DEBUG
Length: 40
Time_Stamp: Wed Sep 24 12:35:09 2003
Pid: 22411
MsgData: >> Hi Mom
Log Message Received:
Type: LM_INFO
Length: 40
Time_Stamp: Wed Sep 24 12:35:09 2003
Pid: 22411
MsgData: >> Goodnight
Log Message Received:
Type: LM_TRACE
Length: 48
Time_Stamp: Wed Sep 24 12:35:09 2003
Pid: 22411
MsgData: >> (1024) leaving main
As you can see, we have quite a bit of access to the ACE_Log_Record
internals. We're not limited to changing only the message text. We can, in fact, change any of the values we want. Whether that makes any sense is up to your application. Table 3.7 lists the attributes of ACE_Log_Record
and what they mean.
Put simply, the ACE Logging Service is a configurable two-tier replacement for UNIX syslog. Both syslog and the Windows Event Logger are pretty good at what they do and can even be used to capture messages from remote hosts. But if you have a mixed environment, they simply aren't sufficient.
The ACE netsvcs logging framework has a client/server design. On one host in the network, you run the logging server that will accept logging requests from any other host. On that and every host in the network where you want to use the distributed logger, you invoke the logging client. The client acts somewhat like a proxy by accepting logging requests from clients on the local system and forwarding them to the server. This may seem to be a bit of an odd design, but it helps prevent pounding the server with a huge number of client connections, many of which may be transient. By using the proxy approach, the proxy on each host absorbs a little bit of the pounding, and everyone is better off.
To configure our server and client proxy, we will use the ACE Service Configurator framework. The Service Configurator is an advanced topic that is covered in Chapter 19. We will show you just enough here to get things off the ground. Feel free to jump ahead and read a bit more about the Service Configurator now, or wait and read it later.
To start the server, you need to first create a file server.conf
with the following content:
dynamic Logger Service_Object * ACE:_make_ACE_Logging_Strategy() "-s foobar -f STDERR|OSTREAM|VERBOSE"
dynamic Server_Logging_Service Service_Object * netsvcs:_make_ACE_Server_Logging_Acceptor() active "-p 20009"
Note these lines are wrapped for readability. Your server.conf
should contain only two lines, each beginning with the word dynamic
. The first line defines the logging strategy to write the log output to standard error and the output stream attached to a file named foobar
. This line also requests verbose log messages instead of a more terse format. (Section 3.8 discusses more ways to use this service.) The second line of server.conf
causes the server to listen for client connections at TCP (Transmission Control Protocol) port 200091 on all network interfaces available on your computer. You can now start the server with:
$ACE_ROOT/netsvcs/servers/main -f server.conf
The next step is to create the configuration file for the client proxy and start the proxy. The file could be named client.conf
and should look something like this:
dynamic Client_Logging_Service Service_Object * netsvcs:_make_ACE_Client_Logging_Acceptor() active "-p 20009 -h localhost"
Again, that's all on one line. The important parts are -p 20009
, which tells the proxy which TCP port the server will be listening to—this should match the -p
value in your server.conf
—and -h localhost
, which sets the host name where the logging server is executing. For our simple test, we are executing both client and server on the same system. In the real world, you will most likely have to change localhost
to the name of your real logging server.
Although we provide the port on which the server is listening, we did not provide a port value for clients of the proxy. This value is known as the logger key, and its form and value change, depending on the capabilities of the platform the client logger is built on. On some platforms, it's a pipe; where that's not possible, it's a loopback TCP socket at address localhost:20012
. If you want your client proxy to listen at a different address, you can specify that with the -k
parameter in client.conf
.
You can now start the client logger with:
$ACE_ROOT/netsvcs/servers/main -f client.conf
Using the logging service in one of our previous examples is trivial:
#include "ace/Log_Msg.h"
int ACE_TMAIN (int, ACE_TCHAR *argv[])
{
ACE_LOG_MSG->open (argv[0],
ACE_Log_Msg::LOGGER,
ACE_DEFAULT_LOGGER_KEY);
ACE_TRACE (ACE_TEXT ("main"));
ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("%IHi Mom
")));
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IGoodnight
")));
return 0;
}
As with the syslog example, we must use the open()
method when we want to use the logging service; set_flags()
isn't sufficient. Note also the open()
parameter ACE_DEFAULT_LOGGER_KEY
. This has to be the same logger key that the client logger is listening at; if you changed it with the -k
option in client.conf
, you must specify the new value to open()
.
To summarize: On every machine on which you want to use the logging service, you must execute an instance of the client logger. Each instance is configured to connect to a single instance of the logging server somewhere on your network. Then, of course, you execute that server instance on the appropriate system.
For the truly adventurous, your application can communicate directly with the logging server instance. This approach has two problems:
However, if you still want your application to talk directly to the logging server, here's a way to do so:
#include "ace/Log_Msg.h"
#include "Callback-3.h"
int ACE_TMAIN (int, ACE_TCHAR *[])
{
Callback *callback = new Callback;
ACE_LOG_MSG->set_flags (ACE_Log_Msg::MSG_CALLBACK);
ACE_LOG_MSG->clr_flags (ACE_Log_Msg::STDERR);
ACE_LOG_MSG->msg_callback (callback);
ACE_TRACE (ACE_TEXT ("main"));
ACE_DEBUG ((LM_DEBUG, ACE_TEXT ("%IHi Mom
")));
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IGoodnight
")));
return 0;
}
This looks very much like our previous callback example. We use the callback hook to capture the ACE_Log_Record
instance that contains our message. Our new Callback
object then sends that to the logging server:
#include "ace/streams.h"
#include "ace/Log_Msg.h"
#include "ace/Log_Msg_Callback.h"
#include "ace/Log_Record.h"
#include "ace/SOCK_Stream.h"
#include "ace/SOCK_Connector.h"
#include "ace/INET_Addr.h"
#define LOGGER_PORT 20009
class Callback : public ACE_Log_Msg_Callback
{
public:
Callback ()
{
this->logger_ = new ACE_SOCK_Stream;
ACE_SOCK_Connector connector;
ACE_INET_Addr addr (LOGGER_PORT, ACE_DEFAULT_SERVER_HOST);
if (connector.connect (*(this->logger_), addr) == -1)
{
delete this->logger_;
this->logger_ = 0;
}
}
virtual ~Callback ()
{
if (this->logger_)
{
this->logger_->close ();
}
delete this->logger_;
}
void log (ACE_Log_Record &log_record)
{
if (!this->logger_)
{
log_record.print
(ACE_TEXT (""), ACE_Log_Msg::VERBOSE, cerr);
return;
}
size_t len = log_record.length();
log_record.encode ();
if (this->logger_->send_n ((char *) &log_record, len) == -1)
{
delete this->logger_;
this->logger_ = 0;
}
}
private:
ACE_SOCK_Stream *logger_;
};
We've introduced some things here that you won't read about for a bit. The gist of what we're doing is that the callback object's constructor opens a socket to the logging service. The log()
method then sends the ACE_Log_Record
instance to the server via the socket. Because several of the ACE_Log_Record
attributes are numeric, we must use the encode()
method to ensure that they are in a network-neutral format before sending them. Doing so will prevent much confusion if the byte ordering of the host executing your application is different from that of the host executing your logging server.
The preceding sections explained how to direct the logging output to several places. We noted that you can change your mind at runtime and direct the logging output somewhere else. Unfortunately, what you need to do when you change your mind isn't always consistent. Let's take a look at a simple class that attempts to hide some of those details:
class LogManager
{
public:
LogManager ();
~LogManager ();
void redirectToDaemon
(const ACE_TCHAR *prog_name = ACE_TEXT (""));
void redirectToSyslog
(const ACE_TCHAR *prog_name = ACE_TEXT (""));
void redirectToOStream (ACE_OSTREAM_TYPE *output);
void redirectToFile (const char *filename);
void redirectToStderr (void);
ACE_Log_Msg_Callback * redirectToCallback
(ACE_Log_Msg_Callback *callback);
// ...
};
The idea is pretty simple: An application will use the redirect*
methods at any time to select the output destination:
void foo (void);
int ACE_TMAIN (int, ACE_TCHAR *[])
{
LOG_MANAGER->redirectToStderr ();
ACE_TRACE (ACE_TEXT ("main"));
LOG_MANAGER->redirectToSyslog ();
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IHi Mom
")));
foo ();
LOG_MANAGER->redirectToDaemon ();
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IGoodnight
")));
return 0;
}
void foo (void)
{
ACE_TRACE (ACE_TEXT ("foo"));
LOG_MANAGER->redirectToFile ("output.test");
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IHowdy Pardner
")));
}
“But wait,” you say. “Where did LOG_MANAGER
come from?” This is an example of the ACE_Singleton
template, mentioned in Section 1.6.3. That's what we're using behind LOG_MANAGER
. ACE_Singleton
simply ensures that we create one single instance of the LogManager
class at runtime, even if multiple threads all try to create one at the same time. Using a singleton gives you quick access to a single instance of an object anywhere in your application. To declare our singleton, we add the following to our header file:
typedef ACE_Singleton<LogManager, ACE_Null_Mutex>
LogManagerSingleton;
#define LOG_MANAGER LogManagerSingleton::instance()
To deal with compilers that don't do automatic template instantiation, we must add the following to our .cpp
file:
#if defined (ACE_HAS_EXPLICIT_TEMPLATE_INSTANTIATION)
template class ACE_Singleton<LogManager, ACE_Null_Mutex>;
#elif defined (ACE_HAS_TEMPLATE_INSTANTIATION_PRAGMA)
#pragma instantiate ACE_Singleton<LogManager, ACE_Null_Mutex>
#elif defined (__GNUC__) && (defined (_AIX) || defined (__hpux))
template ACE_Singleton<LogManager, ACE_Null_Mutex> *
ACE_Singleton<LogManager, ACE_Null_Mutex>::singleton_;
#endif /* ACE_HAS_EXPLICIT_TEMPLATE_INSTANTIATION */
Our LogManager
implementation is a straightforward application of the things discussed earlier in this chapter:
LogManager::LogManager ()
: log_stream_ (0), output_stream_ (0)
{ }
LogManager::~LogManager ()
{
if (log_stream_)
log_stream_->close ();
delete log_stream_;
}
void LogManager::redirectToSyslog (const ACE_TCHAR *prog_name)
{
ACE_LOG_MSG->open (prog_name, ACE_Log_Msg::SYSLOG, prog_name);
}
void LogManager::redirectToDaemon (const ACE_TCHAR *prog_name)
{
ACE_LOG_MSG->open (prog_name, ACE_Log_Msg::LOGGER,
ACE_DEFAULT_LOGGER_KEY);
}
void LogManager::redirectToOStream (ACE_OSTREAM_TYPE *output)
{
output_stream_ = output;
ACE_LOG_MSG->msg_ostream (this->output_stream_);
ACE_LOG_MSG->clr_flags
(ACE_Log_Msg::STDERR | ACE_Log_Msg::LOGGER);
ACE_LOG_MSG->set_flags (ACE_Log_Msg::OSTREAM);
}
void LogManager::redirectToFile (const char *filename)
{
log_stream_ = new std::ofstream ();
log_stream_->open (filename, ios::out | ios::app);
this->redirectToOStream (log_stream_);
}
void LogManager::redirectToStderr (void)
{
ACE_LOG_MSG->clr_flags
(ACE_Log_Msg::OSTREAM | ACE_Log_Msg::LOGGER);
ACE_LOG_MSG->set_flags (ACE_Log_Msg::STDERR);
}
ACE_Log_Msg_Callback *
LogManager::redirectToCallback (ACE_Log_Msg_Callback * callback)
{
ACE_Log_Msg_Callback *previous =
ACE_LOG_MSG->msg_callback (callback);
if (callback == 0)
ACE_LOG_MSG->clr_flags (ACE_Log_Msg::MSG_CALLBACK);
else
ACE_LOG_MSG->set_flags (ACE_Log_Msg::MSG_CALLBACK);
return previous;
}
The primary limitation of the LogManager
class is the assumption that output will go to only one place at a time. For our trivial examples, that may be sufficient but could be a problem for a real application. Modifying the LogManager
class to overcome this should be a fairly easy task, and we leave that to the reader.
Thus far, all our decisions about what to log and where to send the output have been determined at compile time. In many cases, it is unreasonable to require a recompile to change the logging options. We could, of course, provide parameters or a configuration file to our application, but we would have to spend valuable time writing and debugging that code. Fortunately, ACE has already provided us with a convenient solution in the form of the ACE_Logging_Strategy
object.
Consider the following file:
dynamic Logger Service_Object * ACE:_make_ACE_Logging_Strategy()
"-s log.out -f STDERR|OSTREAM -p INFO"
We've seen this kind of thing before when we were talking about the distributed logging service. In this case, we're instructing the ACE Service Configurator to create and configure a logging strategy instance just like the distributed logging server. Again, the Service Configurator is an advanced topic with many exciting features2 and is covered in Chapter 19.
The following sample application uses the preceding file:
int ACE_TMAIN (int argc, ACE_TCHAR *argv[])
{
if (ACE_Service_Config::open (argc,
argv,
ACE_DEFAULT_LOGGER_KEY,
1,
0,
1) < 0)
ACE_ERROR_RETURN ((LM_ERROR, ACE_TEXT ("%p
"),
ACE_TEXT ("Service Config open")),
1);
ACE_TRACE (ACE_TEXT ("main"));
ACE_DEBUG ((LM_NOTICE, ACE_TEXT ("%t%IHowdy Pardner
")));
ACE_DEBUG ((LM_INFO, ACE_TEXT ("%t%IGoodnight
")));
return 0;
}
The key is the call to ACE_Service_Config::open()
, which is given our command line parameters. By default it will open a file named svc.conf
, but we can specify an alternative by specifying -f someFile
. In either case, the file's content would be something like the preceding, which tells the logging service to direct the output to both STDERR and the file log.out
.
Be careful that you call ACE_Service_Config::open()
as shown rather than with the default parameters. If the final parameter is not 1, the open()
method will restore the logging flags to their preopen values. Because the logging service loads its configuration and sets the logging flags from within the service configuration's open()
, you will be unpleasantly surprised to find that the logging strategy had no effect on the priority mask once open()
completes.
Recall that, by default, all logging severity levels are enabled at a processwide level. If you specify -p INFO
in your config file, you will probably be surprised when you get other logging levels also; they were already enabled by default. To get what you want, be sure to use the disable flags, such as ~INFO
, as well; these are listed in Table 3.8.
One of the most powerful features of the logging strategy is the ability to rotate the application's log files when they reach a specified size. Use the -m
parameter to set the size and the -N
parameter to set the maximum number of files to keep. Authors of long-running applications will appreciate this, as it will go a long way toward preventing rampant disk space consumption.
Table 3.8 lists all the ACE Logging Strategy options that can be specified and their values. The possible values for -p
and -t
are the same as those listed in Table 3.1, but without the LM_
prefix. Any value can be prefixed with ~
to omit that log level from the output. Multiple flags can be OR'd (|) together as needed.
Every program needs to have a good logging mechanism. ACE provides you with more than one way to handle such things. Consider your application and how you expect it to grow over time. Your choices range from the simple ACE_DEBUG
macros to the highly flexible logging service. You can run “out of the box” or customize things to fit your specific environment. Take the time to try out several approaches before settling on one. With ACE, changing your mind is easy.
3.141.193.158