Kea  2.1.2-git
Kea Logging

Basic Ideas

The Kea logging system is based on the log4J logging system common in Java development, and includes the following ideas:

  • A set of severity levels.
  • A hierarchy of logging sources.
  • Separation of message use from message text.

Message Severity

Each message logged by Kea has a severity associated with it, ranging from FATAL - the most severe - to DEBUG - messages output as the code executes to facilitate debugging. In order of decreasing severity, the levels are:

FATAL

The program has encountered an error that is so severe that it cannot continue (or there is no point in continuing). For example, an unhandled exception generated deep within the code has been caught by the top-level program. When a fatal error has been logged, the program will exit immediately (or shortly afterwards) after dumping some diagnostic information.

ERROR

Something has happened such that the program can continue but the results for the current (or future) operations cannot be guaranteed to be correct, or the results will be correct but the service is impaired. For example, the program started but attempts to open one or more network interfaces failed.

WARN

(Warning) An unusual event happened. Although the program will continue working normally, the event was sufficiently out of the ordinary to warrant drawing attention to it. For example, the authoritative server loaded a zone that contained no resource records.

INFO

(Information) A normal but significant event has occurred that should be recorded, e.g. the program has started or is just about to terminate, a new zone has been created, etc.

DEBUG
Debug messages are output at this severity. Each message also has a debug level associated with it, ranging from 0 (the default) for high-level messages and level 99 (the maximum) for the lowest level.

When logging is enabled for a component, it is enabled for a particular severity level and all higher severities. So if logging is enabled for INFO messages, WARN, ERROR and FATAL messages will also be logged, but not DEBUG ones. If enabled for ERROR, only ERROR and FATAL messages will be logged.

As noted above, DEBUG messages are also associated with a debug level. This allows the developer to control the amount of debugging information produced; the higher the debug level, the more information is output. For example, if debugging the NSAS (nameserver address store), debug levels might be assigned as follows: a level 0 debug message records the creation of a new zone, a level 10 logs every timeout when trying to get a nameserver address, a level of 50 records every query for an address and a level of 70 records every update of the round-trip time.

Like severities, levels are cumulative; so if level 25 is set as the debug level, all debug messages associated levels 0 to 25 (inclusive) will be output. In fact, it is probably easier to visualize the debug levels as part of the severity system:

FATAL Most severe
DEBUG level 0
DEBUG level 1
:
DEBUG level 99 Least severe

When a particular debug level is set, it - and all debug levels and severities above it - will be logged.

To try to ensure that the information from different modules is roughly comparable for the same debug level, a set of standard debug levels has been defined for common types of debug output. (These can be found in log_dbglevels.h) However, modules are free to set their own debug levels or define additional ones.

Hierarchical Logging System

When a program writes a message to the logging system, it does so using an instance of the isc::log::Logger class. As well as performing the write of the message, the logger identifies the source of the message: different sources can write to different destinations and can log different severities of messages. For example, the logger associated with the resolver's cache code could write debugging and other messages to a file while all other components only write messages relating to errors to the syslog file.

The loggers are hierarchical in that each logger is the child of another logger. The top of the hierarchy is the root logger; this does not have a parent. The reason for this hierarchy is that unless a logger explicitly assigns a value to an attribute (such as severity of messages it should log), it picks it up the value from the parent. In Kea, each component (kea-dhcp4, kea-dhcp-ddns etc.) has a root logger (named after the program) and every other logger in the component is a child of that. So in the example above, the error/syslog attributes could be associated with the kea-resolver logger while the logger associated with the cache sets its own values for the debug/file attributes.

More information about the logging hierarchy can be found in the section on Logging configuration in the Kea Administrator Reference Manual.

Separation of Messages Use from Message Text

Separating the use of the message from the text associated with it - in essence, defining message text in an external file - allows for the replacement the supplied text of the messages with a local language version. It also means that other attributes can be associated with the message, for example, an explanation of the meaning of the message and other information such as remedial action in the case of errors.

Each message has an identifier such as "LOG_WRITE_ERROR". Within the program, this is the symbol passed to the logging system which uses the symbol as an index into a dictionary to retrieve the message associated with it (e.g. "unable to open %1 for input"), after which it substitutes any message parameters (in this example, the name of the file where the write operation failed) and logs the result to the destination.

In Kea, the default text for each message is linked into the program. Each program is able to read a locally-defined message file when it starts, updating the stored definitions with site-specific text. When the message is logged, the updated text is output. However, the message identifier is always included in the output so that the origin of the message can be identified even if the text has been changed.

Note
Local message files have not yet been implemented in Kea.

Using Logging in a Kea Component

The steps in using the logging system in a Kea component (such as an executable or library) are:

  1. Create a message file. This defines messages by an identification string and text that explains the message in more detail. Ideally the file should have a file type of ".mes".

  2. Run it through the message compiler to produce the files for your module. This step should be included in the build process. The message compiler is a Kea program and is one of the first programs built and linked in the build process. As a result, it should be available for compiling the message files of all Kea components and libraries.

    For C++ development, the message compiler produces two files in the default directory, having the same name as the input file but with file types of ".h" and ".cc".

  3. Include the resultant files in your source code to define message symbols, and compile the code and link it into your program.

  4. Declare loggers in your code and use them to log messages.

  5. Call the logger initialization function in the program's main module.

The following sections describe these steps in more detail.

Create a Message File

A message file contains message definitions. Typically there will be one message file for each component that uses Kea logging. An example file could be:

# Example message file
$NAMESPACE isc::log
% LOG_UNRECOGNIZED_DIRECTIVE line %1: unrecognized directive '%2'
A line starting with a dollar symbol was found, but the first word on the line
(shown in the message) was not a recognized message compiler directive.
% LOG_WRITE_ERROR error writing to %1: %2
The specified error was encountered by the message compiler when writing to
the named output file.

Points to note are:

  • Leading and trailing spaces are trimmed from each line before it is processed. Although the above example has every line starting at column 1, the lines could be indented if desired.

  • Lines starting with "#" are comments are are ignored. Comments must be on a line by themselves; inline comments will be interpreted as part of the text of that line.

  • Lines starting with "$" are directives. At present, just one directive is recognized:

    $NAMESPACE <namespace-name>
    The sole argument is the name of the namespace in which the symbols are created. In the absence of a $NAMESPACE directive, symbols will be put in the anonymous namespace.

  • Lines starting with "%" are message definitions and comprise the message identification and the message text. For example:

    % LOG_WRITE_ERROR error writing to %1: %2

    There may be zero or more spaces between the leading "%" and the message identification (which, in the example above, is the string "LOG_WRITE_ERROR").

  • The message identification can be any string of letters, digits and underscores, but must not start with a digit.

  • The rest of the line - from the first non-space character to the last non- space character - is the text of the message. There are no restrictions on what characters may be in this text, other than they be printable (so both single-quote (') and double-quote (") characters are allowed). The message text may include replacement tokens (the strings "%1", "%2" etc.). When a message is logged, these are replaced with the arguments passed to the logging call: %1 refers to the first argument, %2 to the second etc. Within the message text, the placeholders can appear in any order and placeholders can be repeated. Otherwise, the message is printed unmodified.

  • Remaining lines indicate an explanation for the preceding message. The explanation can comprise multiple paragraphs, the paragraphs being separated by blank lines. These lines are intended to be processed by a separate program to generate an error messages manual; they are ignored by the message compiler.

  • Except when used to separate paragraphs in the message explanation, blank lines are ignored.

Although there are few restriction on what can be in the message identification and text, there are a number of conventions used by Kea, both in the contents of the message and in the usage. All code should adhere to these:

  • Message identifications should include at least one underscore. The component before the first underscore is a string indicating the origin of the message, and the remainder describes the condition. So in the example above, the LOG indicates that the error originated from the logging library and the "WRITE_ERROR" indicates that there was a problem in a write operation.

  • The part of the message identification describing the error (e.g. "WRITE_ERROR" in the example above) should comprise no more than two or three words separated by underscores. An excessive number of words or overly long message identification should be avoided; such information should be put in the text of the message. For example, "RECEIVED_EMPTY_HOSTNAME_FROM_CLIENT" is excessively long, "EMPTY_HOSTNAME" being better.

  • Similarly, the text of the message should be reasonably concise. It should include enough information (possibly supplied at run-time in the form of parameters) to allow further investigations to be undertaken if required.

    Taking the above example, a suitable error message to indicate that the resolver has failed to read a name from an upstream authoritative server could be:

    % RESOLVER_FETCH_ERROR fetch from %1 failed, error code %2 (%3)

    ... where %1 indicates the name or IP address of the server to which the fetch was sent, %2 the errno value returned and %3 the message associated with that error number (retrieved via a call to "strerror()").

  • The message should not have a comma after the message identification. The message text should neither start with a capital letter (unless the first word is a proper noun or is normally written in capitals) nor end with a period. The message reporting system takes care of such punctuation.

  • The parameters substituted into the message text should not include line breaks. Messages are normally output to the syslog file which has the inbuilt assumption of one line per message. Splitting a message across multiple lines makes it awkward to search the file for messages and associated information.

  • The message identifier should be unique across the entire Kea system. (An error will be reported at system start-up if an identifier is repeated.)

  • A particular message identifier should only be used at one place in the Kea code. In this way, if the message indicates a problem, the code in question can be quickly identified.

  • The explanation of the message - the free-form text following the message identification - appears in the Kea message manual. It should:

    • Describe the severity of the message (debug, informational etc.)

    • Expand on the text of the message. In some cases, such as debug messages, the message text may provide more or less sufficient description. For warnings and errors, the explanation should provide sufficient background to the problem to allow a non-developer to understand the issue and to begin fault-finding. If possible, the explanation should also include suggested remedial action.

Produce Source Files

The message file created in the previous step is then run through the message compiler to produce source files that are included in the Kea programs.

Message Compiler

The message compiler is a program built in the src/log/compiler directory. It is invoked by the command:

kea-msg-compiler [-h] [-v] [-d dir] <message-file>

"-v" prints the version number and exits; "-h" prints brief help text. Finally, the "-d" switch directs the compiler to produce the output files in the specified directory (the default being the current working directory).

C++ Files

  1. A C++ header file (called <message-file-name>.h) holding lines of the form:

    namespace <namespace-name> {
    :
    }

    The symbols define keys in the global message dictionary, with the namespace enclosing the symbols set by the $NAMESPACE directive. (This is the reason for the restriction on message identifiers - they have to be valid C++ symbol names.)

  2. A C++ source file (called <message-file-name>.cc) that holds the definitions of the global symbols and code to insert the symbols and messages into an internal dictionary.

    Symbols are defined to be equal to strings equal to the identifier, e.g.

    extern const isc::log::MessageID LOG_BAD_DESTINATION = "LOG_BAD_DESTINATION";
    extern const isc::log::MessageID LOG_BAD_SEVERITY = "LOG_BAD_SEVERITY";
    :

    (The current implementation allows symbols to be compared. However, use of strings should not be assumed - a future implementation may change this.) In addition, the file declares an array of identifiers/messages and an object to add them to the global dictionary, e.g.:

    namespace {
    const char* values[] = {
    "LOG_BAD_DESTINATION", "unrecognized log destination: %1",
    "LOG_BAD_SEVERITY", "unrecognized log severity: %1",
    :
    NULL
    };
    }

    The constructor of the isc::log::MessageInitializer object retrieves the singleton global isc::log::MessageDictionary object (created using standard methods to avoid the "static initialization fiasco") and adds each identifier and associated text to it. These constructors are run when the program starts; a check is made as each identifier is added and, if the identifier already exists in the dictionary, a warning message is printed to the main logging output when logging is finally enabled. The appearance of such a message indicates a programming error.

Include Message Compilation in Makefile

The source file for the messages is the ".mes" file, but the files used by the code which must be compiled and linked are the output of the message compiler. (The compiler is produced very early on in the Kea build sequence, so is available for use in the building of subsequent components.) To allow this, certain dependencies must be included in the Makefile.am for each component that uses logging.

Including Message files in C++ Component Builds
The following segment from the "hooks" Makefile.am illustrates the entries needed.

# Ensure that the message file is included in the distribution
EXTRA_DIST = hooks_messages.mes
# If we want to get rid of all generated messages files, we need to use
# make maintainer-clean. The proper way to introduce custom commands for
# that operation is to define maintainer-clean-local target. However,
# make maintainer-clean also removes Makefile, so running configure script
# is required. To make it easy to rebuild messages without going through
# reconfigure, a new target messages-clean has been added.
maintainer-clean-local:
rm -f hooks_messages.h hooks_messages.cc
# To regenerate messages files, one can do:
#
# make messages-clean
# make messages
#
# This is needed only when a .mes file is modified.
messages-clean: maintainer-clean-local
if GENERATE_MESSAGES
# Define rule to build logging source files from message file
messages: hooks_messages.h hooks_messages.cc
@echo Message files regenerated
hooks_messages.h hooks_messages.cc: hooks_messages.mes
$(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/lib/hooks/hooks_messages.mes
else
messages hooks_messages.h hooks_messages.cc:
@echo Messages generation disabled. Configure with --enable-generate-messages to enable it.
endif

The first rule adds a hook to make maintainer-clean which is the standard way to regenerate all messages files. The second rule adds the new messages-clean target to regenerate local messages files.

The GENERATE_MESSAGES conditional part is the (re)generation of the messages files. When configured with –enable-generate-messages and the source (.mes file) is newer these files are generated using the Kea message compiler. When configured without –enable-generate-messages (the default) and the source (.mes file) is newer a message is emitted.

Not shown are the Makefile.am lines where the .h and .cc file are used. These are the same as other lines specifying .h and .cc source files.

And finally please note the message header (e.g. hooks_messages.h) should be installed, i.e. in the include_HEADERS list.

Using Logging Files in Program Development

Use in a C++ Program or Module

To use logging in a C++ program or module:

  1. Build the message header file and source file as described above.

  2. In each C++ file in which logging is to be used, declare a logger through which the message will be logged.

    This declaration can be per-function, or it can be declared statically in file scope. The string passed to the constructor is the name of the logger (it can be any string) and is used when configuring it. (Remember though that the name of root logger for the program will be prepended to the name chosen. So if, for example, the name "cache" is chosen and the model is included in the "kea-resolver" program, the full name of the logger will be "kea-resolver.cache".) Loggers with the same name share the same configuration. For this reason if, as is usual, messages logged in different files in the same component (e.g. hooks module, nameserver address store, etc.) originate from loggers with the same name, the logger declaration can be placed into a header file.

  3. Issue logging calls using supplied macros in "log/macros.h", e.g.

    LOG_ERROR(logger, LOG_WRITE_ERROR).arg("output.txt");
    LOG_DEBUG(nsas_logger, NSAS_DBG_TRACE, NSAS_LOOKUP_CANCEL).arg(zone);

    All macros (with the exception of LOG_DEBUG) take two arguments: the C++ logger object that will be used to log the message, and the identification of the message to be logged. LOG_DEBUG takes three arguments, the additional one being the debug level associated with the message. The .arg() call appended to the end of the LOG_XXX() macro handles the arguments to the message. A chain of these is used in cases where a message takes multiple arguments, e.g.

    LOG_DEBUG(nsas_logger, NSAS_DBG_RTT, NSAS_UPDATE_RTT)
    .arg(addresses_[family][index].getAddress().toText())
    .arg(old_rtt).arg(new_rtt);

    Using the macros is more efficient than direct calls to the methods on the logger class: they avoid the overhead of evaluating the parameters to arg() if the logging settings are such that the message is not going to be output (e.g. it is a DEBUG message and the logging is set to output messages of INFO severity or above).

  4. The main program unit must include a call to isc::log::initLogger() (described in more detail below) to set the initial logging severity, debug log level, and external message file.

Logging Initialization

In all cases, if an attempt is made to use a logging method before the logging has been initialized, the program will terminate with a LoggingNotInitialized exception.

C++ Initialization

Logging Initialization is carried out by calling isc::log::initLogger(). There are two variants to the call, one for use by production programs and one for use by unit tests.

Variant #1, Used by Production Programs

The call that should be used by all production programs is:

void isc::log::initLogger(const std::string& root,
int dbglevel = 0, const char* file = NULL,
bool buffer = false);

Arguments are:

root

Name of the root logger. This should be the name of the program (e.g. "kea-auth") and is used when configuring logging.

severity

Default severity that the program will start logging with. Although this may be overridden when the program obtains its configuration from the configuration database, this is the severity that it used until then. The logging severity is one of the enum defined in logger.h, i.e.

(The level NONE may be used to disable logging.)

dbglevel

The debug log level is only interpreted when the severity is isc::log::DEBUG and is an integer ranging from 0 to 99. 0 should be used for the highest-level debug messages and 99 for the lowest-level (and typically more verbose) messages.

file

The name of a local message file. This will be read and its definitions used to replace the compiled-in text of the messages. The default value of NULL indicates that no local message file is supplied.

buffer
If set to true, initial log messages will be internally buffered, until the first time a logger specification is processed. This way the program can use logging before even processing its logging configuration. As soon as any specification is processed (even an empty one), the buffered log messages will be flushed according to the specification. Note that if this option is used, the program SHOULD call one of the isc::log::LoggerManager::process() calls. If the program exits before this is done, all log messages are dumped in a raw format to stdout (so that no messages get lost).

Variant #2, Used by Unit Tests

This is the call that should be used by unit tests. In this variant, all the options are supplied by environment variables: it should not be used for production programs to avoid the chance that the program operation is affected by inadvertently-defined environment variables. The environment variables are:

KEA_LOGGER_ROOT

Sets the "root" for the unit test. If not defined, the name "kea" is used.

KEA_LOGGER_SEVERITY

The severity to set for the root logger in the unit test. Valid values are "DEBUG", "INFO", "WARN", "ERROR", "FATAL" and "NONE". If not defined, "INFO" is used.

KEA_LOGGER_DBGLEVEL

If KEA_LOGGER_SEVERITY is set to "DEBUG", the debug level. This can be a number between 0 and 99, and defaults to 0.

KEA_LOGGER_LOCALMSG

If defined, points to a local message file. The default is not to use a local message file.

KEA_LOGGER_DESTINATION
The location to which log message are written. This can be one of:
  • stdout Message are written to stdout.
  • stderr Messages are written to stderr.
  • syslog[:facility] Messages are written to syslog. If the optional "facility" is used, the messages are written using that facility. (This defaults to "local0" if not specified.)
  • Anything else Interpreted as the name of a file to which output is appended. If the file does not exist, a new one is opened.
In the case of "stdout", "stderr" and "syslog", they must be written exactly as is - no leading or trailing spaces, and in lower-case.

Hooks Specific Notes

All hooks libraries should use Kea logging mechanisms. The loggers and the library specific log messages are created in the same way as for the core Kea modules. The loggers created within the hook library belong to the logging hierarchy of the Kea process and their configuration can be controlled from the Kea configuration file. If the configuration file doesn't contain the specific configuration for the logger used in the library, this logger is given the configuration of the root Kea logger.

The hook libraries are loaded dynamically. This requires that the global log messages dictionary, holding the mapping of specific log message identifiers to the actual messages, is updated to include the messages specified in the hook library when the library is loaded. Conversely, the messages have to be removed from the dictionary when the library is unloaded.

The new messages are added to the global dictionary using the isc::log::MessageInitializer::loadDictionary static function. It is called by the isc::hooks::LibraryManager::loadLibrary for each loaded library.

When the library is unloaded, the instance of the isc::log::MessageInitializer defined in the library is destroyed and its destructor removes the messages registered by the destroyed instance from the global dictionary.

The hook library itself must not perform any action to register or unregister log messages in the global dictionary!

Notes on the Use of Logging

One thing that should always be kept in mind is whether the logging could be used as a means for a DOS attack. For example, if a warning message is logged every time an invalid packet is received, an attacker could simply send large numbers of invalid packets. Of course, warnings could be disabled (or just warnings for that that particular logger), but nevertheless the message is an attack vector. As a general rule, if the message can be triggered by a user action, it can be used as an attack vector.

There are two approaches to get round this:

  1. Log messages generated by such user actions as DEBUG messages. DEBUG is not enabled by default, so these events will not be recorded unless DEBUG is specifically enabled. Choosing a suitable debug level for such messages will select only those messages and not the more general debug messages.

  2. Record system-related and packet-related messages via different loggers. As the loggers are independent and the severity levels independent, fine-tuning of what and what is not recorded can be achieved.

Multi-Threading Consideration for Logging

Logging is thread safe: messages emitted at the same time do not mix.

When the KEA_LOCKFILE_DIR environment variable is not set to none Logging to files is multi-process safe too: for instance two servers can be configured to put log messages in the same file.

The isc::log::Logger class initializes its implementation in a lazy (i.e. on demand) but thread safe way so it is always initialized at most once even in a multi-threaded environment.