BDE 4.14.0 Production release
|
Provide a manager of core logging functionality.
LoggerManager
singletonThis component provides the core of the ball
logging toolkit: the logger class itself, ball::Logger
, that manages log record storage and publication control; the logger manager class, ball::LoggerManager
, typically instantiated as a singleton, that is both a factory for loggers and a category manager; and the logger manager scoped guard, ball::LoggerManagerScopedGuard
, that provides a convenient way to initialize and manage lifetime of the logger manager singleton object.
The ball
logging toolkit is very flexible. A user can log messages with very little effort, and with only a superficial understanding of logger operation, in which case the logger will exhibit its "default behavior". The user can also elect to customize many aspects of logging, such as storage and publication behavior, both at start-up and dynamically during program execution. Naturally, to exercise such control, the user must become more familiar with ball
logger operation; the user can choose more convenience or more versatility, with a reasonably fine granularity.
Log records incorporate both fixed (logger-defined) and optional (user-defined) fields, affording yet more flexibility (see "Log Record Contents" below). The logger directly populates certain of the required fields, and indirectly manages population of any optional fields by invoking a client-supplied callback function that sets the optional values.
Clients obtain one or more loggers from the logger manager, although at most one logger may be "active" in any one thread; a request to log a message is directed to the active logger in that thread. Each logger both stores and publishes appropriate log records.
All loggers share a single internal broadcast observer to which log records are transmitted when they are published (see the component-level documentation of ball_observer for more information on observers).
A logger can achieve high performance through the use of an in-memory record buffer for storing the records logged by a program. Each logger is constructed with a record manager that is an instance of a concrete class derived from ball::RecordBuffer
. The singleton logger manager supplies a "default" record manager to the default logger; loggers allocated by the logger manager's allocateLogger
method use a record manager supplied by the client. The default log record buffer is of user-configurable static size and is circular (see the ball_circularrecordbuffer component for details), whereby continuous logging (without publication of logged records) can result in older records being overwritten by newer ones. A circular buffer provides an efficient "trace-back" strategy, wherein only log records proximate to a user-specified logging event (see below) are published. Such a circular buffer may not be appropriate for all situations; the user can change the behavior of the default logger by adjusting the logging threshold levels (see below) or can install a logger that uses a different kind of record buffer.
The recommended way to initialize the logger manager singleton is to create a ball::LoggerManagerScopedGuard
object in main
before creating any threads. The logger manager scoped guard constructor takes a configuration object (an instance of ball::LoggerManagerConfiguration
), and an optional allocator. The logger manager singleton is created as a side-effect of creating the scoped guard object. When the guard object goes out of scope (i.e., on program exit), the logger manager singleton is automatically destroyed.
The ball::LoggerManagerConfiguration
object is used to supply a set of user-defined "default" values and other options. However, to obtain the "default" logging behavior, it is sufficient to instantiate a default ball::LoggerManagerConfiguration
object and pass that to the constructor of the scoped guard along with an observer. (See {Usage} below.)
As an alternative to using the scoped guard, the initSingleton
method that takes the same arguments as the scoped guard may be used to initialize the singleton. However, in this case the shutDownSingleton
method must be explicitly called to destroy the logger manager singleton on program exit. Unless shutDownSingleton
is called, the singleton will not be destroyed and resources used by the singleton will leak.
Note that the logger manager singleton can be reinitialized after it has been destroyed. However, such practice should generally be restricted to test drivers and very specialized use cases. Clients should generally avoid initializing and destroying the singleton more than once in a program unless they know what they are doing.
Direct use of any of the ball::LoggerManager
or ball::LoggerManagerScopedGuard
methods that take raw pointers to observers is deprecated. These methods will be eliminated in a future release.
The ball::LoggerManagerCategoryIter
and ball::LoggerManagerCategoryManip
classes are deprecated. Clients of ball::LoggerManager
should use the visitCategories
accessor (the replacement for LoggerManagerCategoryIter
) or visitCategories
manipulator (replacing LoggerManagerCategoryManip
) instead.
The logger supports the notions of "severity level" and "category"; every record is logged at some severity level and to some category. Categories are user-defined (except for the "default category"), and have unique names. Severity levels are integers in the range [0 .. 255]
, and are most typically chosen from among the enumeration in the ball_severity component, although use of the enum
is optional. The severity level and the category name are each among the fixed fields of the record being logged (see "Log Record Contents" below).
From the logger's perspective, all categories are peers; there is no special significance to any sequence of characters in a category name. The user may impose a hierarchical meaning to category names, and the logger manager facilitates a certain degree of hierarchical behavior via several callback functors provided within this component (see below, and also the ball_loggerfunctorpayloads component). However, such hierarchy is not fundamental to categories, nor to the behavior described in this section. Similarly, there is no a priori significance to severity levels except that they are ordered and may be compared for inequality, although the enumerator names in the ball::Severity::Level
enumeration (e.g., DEBUG
, WARN
, ERROR
, etc.) suggest the intended "standard" meanings.
Every category has associated with it four "severity threshold levels" that may be set explicitly by the user on category creation/registration (via the addCategory
method) or else will default to specific values via one of several mechanisms described below (invoked by the one-argument setCategory
method). Category threshold levels may also be changed during program execution via the five-argument setCategory
method.
When the user logs a record to a given category and at a given severity (via the ball::Logger
logMessage
method or via the logging macros – see the ball_log
component), the logger manager uses the specified severity and the category's registered severity threshold levels to govern the logger's behavior; depending on the thresholds, the message may be recorded to an in-memory buffer, published to an external observer, or ignored. In addition, if thresholds are set appropriately, the entire contents of the in-memory buffer of one or more loggers may be published to external observers. Clients of the logger can use, and dynamically administer, the category threshold levels to enhance run-time performance and/or to reduce message volume while still capturing all critical log messages.
The names and exact meanings of the four severity threshold levels are as follows:
If the severity level of the record is at least as severe as the Record threshold level of the associated category, then the record will be stored by the logger in its log record buffer (i.e., it will be recorded).
If the severity of the record is at least as severe as the Pass threshold level of the associated category, then the record will be immediately published by the logger (i.e., it will be transmitted to the logger's downstream recipient – the observer).
If the severity of the record is at least as severe as the Trigger threshold level of the associated category, then the record will cause immediate publication of that record and any records in the logger's log record buffer (i.e., this record will trigger a general log record dump).
Note that more than one of the above actions can apply to a given log record, since the four threshold levels are independent of one another. Note also that all of these actions are governed by the threshold levels of the record being logged, and not by the threshold levels of any stored records that are published as a result of a Trigger or Trigger-All event.If the severity of the record is at least as severe as the Trigger-All threshold level of the associated category, then the record will cause immediate publication of that record and all other log records stored by all active loggers.
The logger manager supplies "default values" for category threshold levels whenever a category is created without client-supplied values. These default values can come from any one of several possible sources, depending upon options that the user has elected; the system is flexible, but leads to a bit of confusion in terminology. This section explains the meaning of "factory default" values and introduces the various "default" threshold mechanisms.
The logger manager is a "factory" for loggers; we therefore define "factory
defaults" to be the default values that the ball::LoggerManager
singleton is aware of at construction. Depending on the values and options in the ball::LoggerManagerConfiguration
object provided to the logger manager on construction, the factory defaults may be either implementation-defined or user-defined.
In either case, the user can change the default values during logger operation via the setDefaultThresholdLevels
method. These threshold levels become the "default" values for new categories, but they do not affect the "factory defaults" that subsequently can be restored via the resetDefaultThresholdLevels
method.
A third mechanism, the ball::LoggerManager::DefaultThresholdLevelsCallback
functor, adds even more flexibility. If this callback is installed by the user at construction, or subsequently via the setDefaultThresholdLevelsCallback
method, the callback is the source of all default thresholds, and the above mechanisms are not used. The next section covers category thresholds in more detail.
When the logger manager singleton is created, a unique category known as the Default Category is created, and is given "factory-supplied" default threshold levels. The default values for the default category are each in the range [0 .. 255]
, but are otherwise unspecified. The user can also specify default values explicitly when the logger manager singleton is constructed. This is accomplished by constructing a ball::LoggerManagerDefaults
object, setting the desired values, and then setting that object as an attribute of the ball::LoggerManagerConfiguration
argument to the ball::LoggerManagerScopedGuard
constructor.
The default category is issued to the user via the return value of the setCategory(const char *categoryName)
method whenever a new category cannot be created due to a capacity limitation on the category registry maintained by the logger manager. The method's normal behavior is to return the category having categoryName
.
Categories that are added to the registry during logging through calls to the setCategory(const char *)
method are given threshold levels by one of two means. The "default" mechanism (a slightly overloaded term in ball
) is to use the same default thresholds as described above for the default category. The alternative is to specify a ball::LoggerManager::DefaultThresholdLevelsCallback
functor, either when the logger manager singleton is initialized or else afterwards via the setDefaultThresholdLevelsCallback
method. This functor, if provided, is used by the logger manager to supply the four int
threshold values; the functor may generate these values by any means that the user sees fit. See the ball_loggerfunctorpayloads component for an example payload function for the functor.
The default threshold levels can be adjusted (setDefaultThresholdLevels
) and reset to their original values (resetDefaultThresholdLevels
). Note that if factory values are overridden at initialization, a reset will restore thresholds to the user-specified default values. In addition, there is a method to set the threshold levels of a given category to the current default threshold levels (setCategoryThresholdsToCurrentDefaults
) or to the factory-supplied (or client-overridden) default values (setCategoryThresholdsToFactoryDefaults
).
As a final note regarding categories, a client can optionally supply to the logger manager on construction a ball::LoggerManager::CategoryNameFilterCallback
functor (via the ball::LoggerManagerConfiguration
object) to translate category names from an external to an internal representation. For example, a project may allow programmers to refer to categories using mixed-case within an application, but provide a toLower
CategoryNameFilterCallback
to map all external upper-case letters to lower-case internally. In this scenario, the (hypothetical) external category names "EQUITY.MARKET.NYSE" and "equity.market.nyse" would be mapped to the same category internally by the presumed toLower
functor.
Each log record contains a set of fixed fields and a set of optional, user-definable fields and attributes. The following table lists the fixed fields in each log record (see the component-level documentation of ball_recordattributes for more information on the fixed fields of a log record):
The following table lists optional fields and attributes in each log record (see the component-level documentation of ball_userfields and ball_managedattribute for more information):
[DEPRECATED] If a ball::LoggerManager::UserFieldsPopulatorCallback
functor is supplied by the client (see ball_loggermanagerconfiguration ), thereafter, every logged record has its user-defined fields (indirectly) populated by an invocation of the UserFieldsPopulatorCallback
functor.
The log record's attributes are populated by attribute collector functor(s) registered by the user.
The ball
logging toolkit may be used in single-threaded and multi-threaded library code and applications with equal ease, and with virtually no difference in coding. In particular, the same use of the ball::LoggerManagerScopedGuard
class to initialize the logger manager singleton is required in main
in both cases, and individual calls to the ball::Logger
instance method logMessage
(and logging calls via the logging macros – see ball_log
) are identical, from the user's perspective. Category threshold administration is also identical in both cases.
Differences in logger usage, or, more precisely, additional options for the multi-threaded user, arise when the user wishes to allocate one or more loggers beyond the default logger that is owned by the singleton logger manager. If a user does not explicitly allocate a logger (via the logger manager instance method allocateLogger
) and install that logger for a given thread (via the manager instance method setLogger
), then all records from all threads in a program will be logged to the one default logger. However, since each thread of execution may have its own logger instance, multi-threaded users may choose to allocate and install multiple loggers. Note that each thread may have at most one logger, but a single logger may be used by any number of threads.
Multi-threaded users of logging may prefer to allocate and install one logger per thread in order to take advantage of the "trace-back" feature described above on a per-thread basis. In the event of an error condition as defined by the programmer, such a logging configuration provides a trace-back through the record buffer of the thread that caused the error, without any dilution from records from other threads. Conversely, if several threads are known to interact closely, it may be advantageous to have them share a common logger so that the trace-back log does include all relevant records.
The ball::LoggerManager
singleton, on construction, redirects bsls::Log
messages to ball
. Such messages use the logging category "BSLS.LOG". Upon its destruction, the logger manager singleton redirects bsls::Log
messages back to the bsls::Log
message handler that was in effect prior to the creation of the singleton (see bsls_log
).
This section illustrates instantiation of the logger manager singleton that is required in main
, and also shows direct use of the logger and logger manager interfaces, much of which is actually not recommended. The most basic logger functionality has been wrapped in macros defined in the ball_log
component. See the ball
package-level documentation and the ball_log
component documentation for recommended real-world usage examples.
Clients that perform logging must first instantiate the singleton logger manager using the ball::LoggerManagerScopedGuard
class. This example shows how to create a logger manager with the most basic "default behavior". Subsequent examples will show more customized behavior.
The following snippets of code illustrate the initialization sequence (typically performed near the top of main
).
First, we create a ball::LoggerManagerConfiguration
object, configuration
, and set the logging "pass-through" level – the level at which log records are published to registered observers – to WARN
(see {Categories, Severities, and Threshold Levels
}):
Next, create a ball::LoggerManagerScopedGuard
object whose constructor takes the configuration object just created. The guard will initialize the logger manager singleton on creation and destroy the singleton upon destruction. This guarantees that any resources used by the logger manager will be properly released when they are not needed:
Note that the application is now prepared to log messages using the ball
logging subsystem, but until the application registers an observer, all log messages will be discarded.
Finally, we create a ball::StreamObserver
object observer
that will publish records to stdout
and register it with the logger manager singleton. Note that observers must be registered by name; this example simply uses "default" for a name:
The application is now prepared to log messages using the ball
logging subsystem:
Note that concrete observers that can be configured after their creation (e.g., as to whether log records are published in UTC or local time) generally can have their configuration adjusted at any time, either before or after being registered with a logger manager. For an example of such an observer, see ball_asyncfileobserver .
In this example, we demonstrate a more elaborate initial configuration for the logger manager. In particular, we create the singleton logger manager with a configuration that has a category name filter functor, a DefaultThresholdLevelsCallback
functor, and user-chosen values for the "factory default" threshold levels.
First, we define three static
functions that are employed by the two functors. The toLower
function implements our category name filter. It is wrapped within a functor object and maps category names to lower-case:
The following two functions provide the implementation for our DefaultThresholdLevelsCallback
functor. The inheritThresholdLevels
function is wrapped within a functor object; the getDefaultThresholdLevels
function is a helper that does the hard work. We assume a hierarchical category naming scheme that uses .
to delimit the constituents of names. For example, the three categories named "x", "x.y", and "x.y.z" are related in the sense that "x" is an ancestor of both "x.y" and "x.y.z", and "x.y" is an ancestor "x.y.z". Suppose that "x" is added to the registry first. If "x.y" is then added to the registry by calling setCategory(const char *)
, it would "inherit" threshold level values from "x". Similarly, when "x.y.z" is added to the registry by calling the 1-argument setCategory
method, it inherits threshold level values from "x.y" (i.e., a category inherits from its nearest ancestor that exists in the registry when it is added). Note that a category named "xx.y" (for example) is not related to either of "x", "x.y", or "x.y.z":
Then, we create the callback functors that will be supplied to the logger manager singleton initialization (as in "Example 1" above, we assume that the initialization sequence occurs somewhere near the top of main
):
Next, we define four values for our custom "factory default" thresholds. These values will be stored within the logger manager and will be available to all users whenever the "factory defaults" are needed, for the life of the logger manager. In this example, however, we will also be installing the thresholdsCallback
defined above, so unless that functor is un-installed (by a call to setDefaultThresholdLevelsCallback
), these four "factory
defaults" will have no practical effect, since the callback mechanism "steps
in front of" the default values:
Then, we can configure a ball::LoggerManagerDefaults
object, defaults
, with these four threshold values. defaults
can then be used to configure the ball::LoggerManagerConfiguration
object that will be passed to the ball::LoggerManagerScopedGuard
constructor (below):
Next, we create and set the ball::LoggerManagerConfiguration
object, configuration
, that will describe our desired configuration:
Then, we instantiate the singleton logger manager, passing in the configuration
that we have just created:
Note that the application is now prepared to log messages using the ball
logging subsystem, but until the application registers an observer, all log messages will be discarded.
Now, we will demonstrate the functors and client-supplied default threshold overrides.
First, we obtain a reference to the singleton logger manager:
Then, we obtain a reference to the Default Category and assert
that its threshold levels match the client-supplied values that override the "factory-supplied" default values:
Next, we add a category named "BloombergLP" (by calling addCategory
). Note that threshold levels supplied with the category override all defaults (including thresholds set by the supplied callback). Also note that the logger manager invokes the supplied category name filter to map the category name to lower-case before the new category is added to the category registry. The name filter is also invoked by lookupCategory
whenever a category is searched for (i.e., by name) in the registry:
Then, we add a second category named "BloombergLP.bal.ball" (by calling setCategory
) and assert
that the threshold levels are "inherited" from category "BloombergLP":
Now, we add a third category named "Other.equities", again by calling setCategory
. This category has no ancestor currently in the registry, so its threshold levels match those of the Default Category:
Finally, we create a ball::StreamObserver
object observer
that will publish records to stdout
and register it with the logger manager singleton. Note that observers must be registered by name; this example simply uses "default" for a name:
The following example demonstrates how instances of a class supporting streaming to bsl::ostream
(via overloaded operator<<
) can be logged. It also demonstrates how to use the logMessage
method to log messages to a logger. Suppose we want to efficiently log instances of the following class:
In addition, we define the following free operator for streaming instances of Information
to an bsl::ostream
:
The following function logs an Information
object to the specified logger
:
First, obtain a record that has its fileName
and lineNumber
attributes set:
Then, we get a non-const
reference to the fixed fields of record
:
Next, we create a bsl::ostream
to which the string representation information
can be output. Note that stream
is supplied with the stream buffer of attributes
:
Then, we stream information
into our output stream
. This will set the message attribute of record
to the streamed data:
Finally, we log record
using logger
:
Notice that we did not need to allocate a scratch buffer to stream the object contents into. That would have required an extra copy and the cost of allocation and deallocation, and thus would have been more inefficient.
This example demonstrates using a ball::Logger
directly to log messages. In practice, clients are encouraged to use the logging macros (see {ball_log
}. The following example assumes logging has been correctly initialized (see prior examples).
The following simple factorial
function takes and returns values of type int
. Note that this function has a very limited range of input, namely integers in the range [0 .. 13]
. This limited range serves to illustrate a usage pattern of the logger, namely to log "warnings" whenever a key function is given bad input.
For this example, it is sufficient to use the severity levels defined in the ball::Severity::Level
enumeration:
Note that the intervals left between enumerator values allow applications to define additional values in case there is a desire to log with more finely-graduated levels of severity. We will not need that granularity here; ball::Severity::e_WARN
is appropriate to log a warning message if the input argument to our factorial function is not in this range of values.
We will register a unique category for this function, so that logged messages from our function will be identified in the published output. Also, with a unique category name, the logging behavior of this function can be administered by resetting the various threshold levels for the category. In this example, we will accept the default thresholds.
The setCategory
method accepts a name and returns the address of a ball::Category
with that name or, in some circumstances, the address of the Default Category (see the function-level documentation of setCategory
for details). The address returned by setCategory
is stored in a function-static pointer variable (i.e., it is fetched only once upon first use). In this example, we assume that we are writing a function for Equities Graphics that will live in that group's Math library. The dot "delimiters" (.
) have no particular significance to the logger, but may be used by the administration methods to "induce" a hierarchical behavior on our category, should that be useful. See, e.g., the callback functor ball::LoggerManager::DefaultThresholdLevelsCallback
and its documentation, and Usage Example 2 above for information on how to use category names to customize logger behavior:
We must also obtain a reference to a logger by calling the logger manager getLogger
method. Note that this logger may not safely be cached as a function static
variable since our function may be called in different threads having different loggers. Even in a single-threaded program, the owner of main
is free to install new loggers at any point, so a statically-cached logger would be a problem:
Now we validate the input value n
. If n
is either negative or too large, we will log a warning message (at severity level ball::Severity::e_WARN
) and return a negative value. Note that calls to logMessage
have no run-time overhead (beyond the execution of a simple if
test) unless ball::Severity::e_WARN
is at least as severe as one of the threshold levels of factorialCategory
:
The remaining code proceeds mostly as expected, but adds one last message that tracks control flow when ball::Severity::e_TRACE
is at least as severe as one of the threshold levels of factorialCategory
(e.g., as might be the case during debugging):