BDE 4.14.0 Production release
|
Modules | |
ball_administration | |
Provide a suite of utility functions for logging administration. | |
ball_asyncfileobserver | |
Provide an asynchronous observer that logs to a file and stdout . | |
ball_attribute | |
Provide a representation of (literal) name/value pairs. | |
ball_attributecollectorregistry | |
Provide a registry for attribute collector functors. | |
ball_attributecontainer | |
Provide a protocol for containers holding logging attributes. | |
ball_attributecontainerlist | |
Provide a list of attribute container addresses. | |
ball_attributecontext | |
Provide a container for storing attributes and caching results. | |
ball_broadcastobserver | |
Provide a broadcast observer that forwards to other observers. | |
ball_category | |
Provide a container for a name and associated thresholds. | |
ball_categorymanager | |
Provide a manager of named categories each having "thresholds". | |
ball_context | |
Provide a container for the context of a transmitted log record. | |
ball_countingallocator | |
Provide a concrete allocator that keeps count of allocated bytes. | |
ball_defaultattributecontainer | |
Provide a default container for storing attribute name/value pairs. | |
ball_fileobserver | |
Provide a thread-safe observer that logs to a file and to stdout . | |
ball_fileobserver2 | |
Provide a thread-safe observer that emits log records to a file. | |
ball_filteringobserver | |
Provide an observer that filters log records. | |
ball_fixedsizerecordbuffer | |
Provide a thread-safe fixed-size buffer of record handles. | |
ball_log | |
Provide macros and utility functions to facilitate logging. | |
ball_logfilecleanerutil | |
Provide a utility class for removing log files. | |
ball_loggercategoryutil | |
Provide a suite of utility functions for category management. | |
ball_loggerfunctorpayloads | |
Provide a suite of logger manager singleton functor payloads. | |
ball_loggermanager | |
Provide a manager of core logging functionality. | |
ball_loggermanagerconfiguration | |
Provide a constrained-attribute class for the logger manager. | |
ball_loggermanagerdefaults | |
Provide constrained default attributes for the logger manager. | |
ball_logthrottle | |
Provide throttling equivalents of some of the ball_log macros. | |
ball_managedattribute | |
Provide a wrapper for ball::Attribute with managed name storage. | |
ball_managedattributeset | |
Provide a container for managed attributes. | |
ball_multiplexobserver | |
Provide a multiplexing observer that forwards to other observers. | |
ball_observer | |
Define a protocol for receiving and processing log records. | |
ball_observeradapter | |
Provide a helper for implementing the ball::Observer protocol. | |
ball_patternutil | |
Provide a utility class for string pattern matching. | |
ball_predicate | |
Provide a predicate object that consists of a name/value pair. | |
ball_predicateset | |
Provide a container for managed attributes. | |
ball_record | |
Provide a container for the fields and attributes of a log record. | |
ball_recordattributes | |
Provide a container for a fixed set of fields suitable for logging. | |
ball_recordbuffer | |
Provide a protocol for managing log record handles. | |
ball_recordjsonformatter | |
Provide a formatter for log records that renders output in JSON. | |
ball_recordstringformatter | |
Provide a record formatter that uses a printf -style format spec. | |
ball_rule | |
Provide an object having a pattern, thresholds, and attributes. | |
ball_ruleset | |
Provide a set of unique rules. | |
ball_scopedattribute | |
Provide a scoped guard for a single BALL attribute. | |
ball_scopedattributes | |
Provide a class to add and remove attributes automatically. | |
ball_severity | |
Enumerate a set of logging severity levels. | |
ball_severityutil | |
Provide a suite of utility functions on ball::Severity levels. | |
ball_streamobserver | |
Provide an observer that emits log records to a stream. | |
ball_testobserver | |
Provide an instrumented observer for testing. | |
ball_thresholdaggregate | |
Provide an aggregate of the four logging threshold levels. | |
ball_transmission | |
Enumerate the set of states for log record transmission. | |
ball_userfields | |
Provide a container of user supplied field values. | |
ball_userfieldtype | |
Enumerate the set of data types for a user supplied attribute. | |
ball_userfieldvalue | |
Provide a type for the value of a user supplied field. | |
Provide thread-safe logging toolkit suitable for all platforms.
Basic Application Library Logging (ball)
The 'ball' package provides a toolkit for logging messages in applications and library code. The logger toolkit has an administration layer that allows configuration both at start-up and during program execution, a basic logger API for the most general possible use, and two sets of macros for somewhat less flexible but simpler, more convenient use. In particular, messages may be logged via these macros in either a C++ stream-style syntax (i.e., with the '<<' operator) or a 'printf'-style syntax. Users are encouraged to use the macros exclusively, because they provide uniformity, and because they are less error-prone. See the {Appendix: Macro Reference} section below.
This document contains a number of code examples, explained below:
The following section provides short examples highlighting some important features of logging.
The following trivial example shows how to use the logging macros to log messages at various levels of severity.
First, we include 'ball_log.h', then create an example function where we initialize the log category within the context of this function. The logging macros such as 'BALL_LOG_ERROR' will not compile unless a category has been specified in the current lexical scope:
Then, we record messages at various levels of severity. These messages will be conditionally written to the log depending on the current logging threshold of the category (configured using the 'ball::LoggerManager' singleton):
Next, we demonstrate how to use proprietary code within logging macros. Suppose you want to add the content of a vector to the log trace:
Notice that the code block will be conditionally executed depending on the current logging threshold of the category. The code within the block must not produce any side effects, because its execution depends on the current logging configuration. The special macro 'BALL_LOG_OUTPUT_STREAM' provides access to the log stream within the block.
Then we show a simple class that declares a log category for the class (log categories can also be configured at namespace scope in a '.cpp' file):
Finally we can use a 'ball::ScopedAttribute' to associate an attribute with the current thread's logging context.
Notice that the attribute "mylibrary.security" will be associated with each log message generated by the current thread until the destruction of the 'securityAttribute' object (including the log message created by 'privateRetrieveData'). To publish the attribute to the log the 'ball::Observer' must be configured correctly (e.g., using the "%a" format specification with 'ball::FileObserver' or 'ball::RecordStringFormatter'), as we do in the subsequent example.
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 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::FileObserver' object 'observer' that will publish records to a file, and exceptional records to 'stdout'. We configure the log format to publish log attributes (see {Key Example 1: Write to a Log}, enable the logger to write to a log file, and then register 'observer' with the logger manager. 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:
The 'ball' package currently has 51 components having 17 levels of physical dependency. The list below shows the hierarchical ordering of the components. The order of components within each level is not architecturally significant, just alphabetical.
ball_administration : Provide a suite of utility functions for logging administration.
ball_asyncfileobserver : Provide an asynchronous observer that logs to a file and stdout
.
ball_attribute : Provide a representation of (literal) name/value pairs.
ball_attributecollectorregistry : Provide a registry for attribute collector functors.
ball_attributecontainer : Provide a protocol for containers holding logging attributes.
ball_attributecontainerlist : Provide a list of attribute container addresses.
ball_attributecontext : Provide a container for storing attributes and caching results.
ball_broadcastobserver : Provide a broadcast observer that forwards to other observers.
ball_category : Provide a container for a name and associated thresholds.
ball_categorymanager : Provide a manager of named categories each having "thresholds".
ball_context : Provide a container for the context of a transmitted log record.
ball_countingallocator : Provide a concrete allocator that keeps count of allocated bytes.
ball_defaultattributecontainer : Provide a default container for storing attribute name/value pairs.
ball_fileobserver : Provide a thread-safe observer that logs to a file and to stdout
.
ball_fileobserver2 : Provide a thread-safe observer that emits log records to a file.
ball_filteringobserver : Provide an observer that filters log records.
ball_fixedsizerecordbuffer : Provide a thread-safe fixed-size buffer of record handles.
'ball_log': Provide macros and utility functions to facilitate logging.
ball_logfilecleanerutil : Provide a utility class for removing log files.
ball_loggercategoryutil : Provide a suite of utility functions for category management.
ball_loggerfunctorpayloads : Provide a suite of logger manager singleton functor payloads.
ball_loggermanager : Provide a manager of core logging functionality.
ball_loggermanagerconfiguration : Provide a constrained-attribute class for the logger manager.
ball_loggermanagerdefaults : Provide constrained default attributes for the logger manager.
ball_logthrottle : Provide throttling equivalents of some of the ball_log
macros.
ball_managedattribute : Provide a wrapper for ball::Attribute
with managed name storage.
ball_managedattributeset : Provide a container for managed attributes.
ball_multiplexobserver : !DEPRECATED! Provide a multiplexing observer that forwards to other observers.
ball_observer : Define a protocol for receiving and processing log records.
ball_observeradapter : Provide a helper for implementing the ball::Observer
protocol.
ball_patternutil : Provide a utility class for string pattern matching.
ball_predicate : !DEPRECATED! Provide a predicate object that consists of a name/value pair.
ball_predicateset : !DEPRECATED! Provide a container for managed attributes.
ball_record : Provide a container for the fields and attributes of a log record.
ball_recordattributes : Provide a container for a fixed set of fields suitable for logging.
ball_recordbuffer : Provide a protocol for managing log record handles.
ball_recordjsonformatter : Provide a formatter for log records that renders output in JSON.
ball_recordstringformatter : Provide a record formatter that uses a printf
-style format spec.
ball_rule : Provide an object having a pattern, thresholds, and attributes.
ball_ruleset : Provide a set of unique rules.
ball_scopedattribute : Provide a scoped guard for a single BALL attribute.
ball_scopedattributes : Provide a class to add and remove attributes automatically.
ball_severity : Enumerate a set of logging severity levels.
ball_severityutil : Provide a suite of utility functions on ball::Severity
levels.
ball_streamobserver : Provide an observer that emits log records to a stream.
ball_testobserver : Provide an instrumented observer for testing.
ball_thresholdaggregate : Provide an aggregate of the four logging threshold levels.
ball_transmission : Enumerate the set of states for log record transmission.
ball_userfields : Provide a container of user supplied field values.
ball_userfieldtype : Enumerate the set of data types for a user supplied attribute.
ball_userfieldvalue : Provide a type for the value of a user supplied field.
The 'ball' logging toolkit is thread-enabled and suitable for multi-threaded applications. At the user's option, the multi-threaded toolkit permits each thread to install a distinct instance of 'ball::Logger'; the process-wide "default" logger is available to any thread that does not install its own logger.
This section provides a brief overview of the features of the 'ball' logging toolkit, and introduces (without formal definition) some of the terminology used in 'ball'. Refer to the hierarchical and alphabetical Synopsis sections above to associate these features with the overall 'ball' design, and see the various sections below for more detailed descriptions.
The 'ball' package provides a flexible logging toolkit that supports several standard and non-standard features. Perhaps most notable among the non-standard features is the ability to write messages to an in-memory "circular" (finite) buffer with the expectation that those messages will be overwritten and never actually "logged" to any permanent medium. With this feature, during normal production operation a large quantity of "trace-back" information can be "logged" to memory, and typically be discarded, without having clogged production resources. If, however, some "error" occurs, the information just prior to that error will be available for fast diagnosis and debugging. It is easy to re-configure logger operation so that every message is archived, if that is the preferred behavior, but the efficient trace-back feature is an explicit design goal. See the "Hello world!" examples under {Usage} below for illustrations of how to configure the logger behavior.
Another key design feature is the "observer" object. 'ball' defines the 'ball::Observer' protocol (abstract interface), and provides a few concrete observers. It is expected that most users will find the concrete observers that are provided in 'ball' to be sufficient for their needs. However, users are free to define their own observers tailored to meet their specific requirements. In the current release, exactly one observer is registered with the 'ball' logger on initialization; we anticipate that multiple observers, e.g., one per thread or perhaps one per logger, may be available in future releases. An observer makes its 'publish' method available to the logger; since the 'publish' method is free to do almost anything that the user wants, the limitation of one observer per process is not very restrictive. The observer may: write the message to a simple file, write the message to a set of managed files, write the message to the console (perhaps with some information removed and/or added), process the message (complete with Category and Severity information) and take specific responsive actions, or any combination of these or other behaviors. In particular, 'ball' provides a "broadcast observer", 'ball::BroadcastObserver', that forwards log records to any number of observers registered with the broadcast observer. Note that 'ball::LoggerManager' contains an integrated broadcast observer and all observers registered with the logger manager will receive log records.
The logger supports the notions of severity levels and categories. Every message is logged at some severity level and to some category. Categories are user-defined (except for the "default category"), and can be separately managed; in particular, the behavior of any given message-logging operation depends upon specific severity level threshold settings for the category to which the message is being logged. See the ball_loggermanager component for more details about categories and category administration.
Severity levels are, from the perspective of the basic logger API, user-settable in the range '[0 .. 255]'. Much more commonly, however, (and necessarily when using the 'ball' convenience macros defined in the 'ball_log' component), the user will choose to use the fixed set of enumerated severity levels as defined in the ball_severity component. The enumerator names suggest a meaning that is consistent with common practice, but no policy is enforced by the basic logger API or the macros.
For convenient reference only, the 'ball::Severity::Level' 'enum' implementation is presented below. Note that this implementation and the specific numerical values may not be relied upon. Also note that the enumerator 'NONE' is deprecated, since its name has proven to be confusing. As an argument to 'logMessage', it would satisfy "none" of the thresholds, but when provided as a threshold value itself, it would enable "all" of the enumerated 'logMessage' requests (and all of the macros). Use 'e_OFF' as a threshold level to disable all logging events for a particular logging behavior (see the "Log Record Storage and Publication" section below).
Note that numerically lower 'Level' values correspond to conditions having greater severity.
The entity that the logger logs is called a "log record" in 'ball'. A log record consists of a number of fixed and user-defined fields; one of the fixed fields is called a "log message" (or "message" for short). In casual usage, we use "record" and "message" interchangeably, but note that they are distinct concepts. The set of fixed fields that constitute a log record are defined in the ball_recordattributes component. A brief description of the fixed fields (or "record attributes") is given in the following table.
The user may wish to specify a set of user-defined fields to be included within every log record for a given program. Such user-defined fields are represented in each log record by an instance of 'ball::UserFields'. A 'ball::LoggerManagerConfiguration::UserFieldsPopulatorCallback' functor may be provided to the logger manager on construction (via a 'ball::LoggerManagerConfiguration' object) that, when invoked, "populates" (i.e., provides the values for) all elements of the 'ball::UserFields' object containing the user-defined fields. See the ball_record component for more information about the use of 'ball::UserFields' in logging, and see the ball_loggermanager component for more information about installing a user populator callback.
The 'ball' infrastructure has no provisions for specifying or enforcing constraints on the encoding used for logged messages (in general, logged messages are published to registered observers in the encoding in which they were originally supplied). However, particular applications, frameworks, and consumers of log records (e.g., readers of log files) may place their own constraints on log records with respect to encoding. Clients generating log records should be aware of the encoding requirements for logged messages generated by their application (e.g., many applications at Bloomberg require UTF-8 encoded messages).
The logger achieves good run-time performance while capturing critical log messages by selectively storing log records and selectively publishing a subset of those stored records. The determination of whether a log record should be stored or published is based on the "category" and "severity" level that are associated with each record. The severity of a record is intended to indicate that record's relative urgency. A category is comprised of a name (an arbitrary string) and four severity threshold levels (see the component-level documentation of ball_severity for more information on typical severity levels). A message's text, its associated severity, and the name of its associated category are each among the fixed fields in the logged record (see the "Messages, Records, and other 'ball' Terminology" section above).
The following four settings define the log record severity levels at which the logger takes certain actions. The logger determines the disposition of a record based on its severity level in relation to the four severity threshold levels of the category associated with the message:
Record: 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).
Pass: If the severity level 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).
Trigger: If the severity level 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).
Trigger-All: If the severity level 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.
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. Also note that the determination of whether a log record should cause a Trigger or Trigger-All event is based on the trigger and trigger-all threshold levels, respectively, of the category associated with the record being logged. The categories and severities associated with log records stored earlier are not taken into account.
This section will be expanded upon in the future. For now, we provide a brief introduction to the objects that are most central to understanding basic logger operation, so that we may use their names elsewhere in this documentation.
'ball::LoggerManager' is a singleton that must be constructed with a configuration object (see below) and an optional 'bslma' allocator before any true logging operations can be performed. The logger manager's main tasks are to administer categories and to "allocate" loggers. If the user has not allocated and installed any logger instances, then the 'getLogger' method will return the "default logger" that is a specific instance of 'ball::Logger' owned and managed by the logger manager. This default logger is suitable for single-threaded and multi-threaded use, and is used "transparently" by users willing to accept default-logger behavior. See {Usage} below.
The logger manager maintains an internal broadcast observer (see below) and provides the 'registerObserver', 'deregisterObserver', and 'findObserver' methods that register, deregister, and find observers, respectively. The internal broadcast observer forwards all log records that it receives to all registered observers.
The logger manager provides the 'allocateLogger' and 'setLogger' methods that allocate arbitrarily many loggers and install (at most) one logger per thread, respectively. Any thread in which the 'setLogger' method was not called will use the default logger. See {Example 6} and {Example 7} below for a discussion of how and why to allocate multiple loggers.
'ball::Logger' provides the most central logging functionality, namely the 'logMessage' method. However, most users will never call 'logMessage' directly, but rather will use the sets of macros defined in the 'ball_log' component. See the {Appendix: Macro Reference} section below.
The "default" 'ball::Logger' instance managed by the logger manager is suitable for many purposes, so typical users need never interact with logger instances explicitly at all. However, in multi-threaded applications (and perhaps also in certain special-purpose single-threaded programs), the user may want to instantiate one or more logger instances and install at most one logger instance per thread. See {Example 6} and {Example 7} below.
'ball::LoggerManagerConfiguration' is a "configuration" object that must be supplied to the logger manager at construction. The default object configures the "default" logging behavior; the user can change the logger behavior by changing attributes in the configuration object. The name, type, and description of the configuration attributes are presented in the two tables below; the awkward repetition of 'NAME' is due to the long type names of the functor types.
Note that the configuration object is not value-semantic because its three functor attributes are not value-semantic. For this reason, the single "defaults" attribute, a value-semantic simply-constrained attribute type, was factored out from the configuration type. The defaults object holds six numerical attributes and is described next.
'ball::LoggerManagerDefaults' is a value-semantic simply-constrained attribute type that is itself an attribute of the above "configuration" object. The "defaults" object contains the following six constrained attributes.
The constraints are as follows:
'ball::LoggerManagerDefaults' was factored out of the configuration object because the former is purely value-semantic, and can be generated from, e.g., a configuration file. From this perspective, it is very convenient to set this one attribute in the configuration object. It is quite possible to ignore the independent existence of 'ball::LoggerManagerDefaults' and set the above constrained attributes directly in the 'ball::LoggerManagerConfiguration' object. The latter is more convenient in "simple" usage, and is illustrated in {Usage} below.
'ball::Observer' is the object that receives "published" log messages. Specifically, 'ball::Observer' is a protocol (abstract interface), and the user must supply a concrete implementation (separately written or chosen from existing 'ball' observers). The observer provides a 'publish' method that defines the behavior of the logical concept of "publication" of a message used throughout this document. Note that multiple observers may be registered with the logger manager and each registered observer will receive "published" log messages.
For both convenience and uniformity of programming, 'ball' provides a suite of logging macros (defined in the 'ball_log' component). All but the most demanding and customized applications will use the logging macros for basic logging operations (e.g., setting categories and logging messages). The logger manager interface is used primarily to administer global and category-specific severity threshold levels, and to allocate loggers. See the {Usage} and {Appendix: Macro Reference} sections below for examples and reference documentation, respectively.
The user should note the following two facts about macro usage:
When the logger manager singleton is created, a distinguished category known as the Default Category is created. At initialization, the Default Category is given "factory-supplied" default threshold levels. These threshold levels, values distributed in the range '[0 .. 255]', are not published and are subject to change over time. Explicit settings for these factory values may be specified when the singleton is constructed. This is done by constructing a 'ball::LoggerManagerConfiguration' object (needed for logger manager construction in any event) and then calling the 'setDefaultThresholdLevelsIfValid' method with the desired values for its four arguments before supplying the configuration object to the logger manager configuration.
The Default Category may arise during logging whenever the 'setCategory(const char <em>categoryName)' method is called. That method returns the address of the category having 'categoryName', if it exists; if no such category exists, and a category having 'categoryName' cannot be created due to a capacity limitation on the category registry maintained by the logger manager singleton, then the *Default Category is returned.
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. One alternative is to use the 'ball::LoggerManagerConfiguration::DefaultThresholdLevelsCallback' functor that is optionally supplied by the client when the logger manager singleton is initialized. If such a functor is provided by the client, then it is used to supply threshold levels to categories added by 'setCategory(const char *)'. Otherwise, default threshold levels maintained by the logger manager for that purpose are used. At initialization, these default threshold levels are given the same "factory-supplied" settings as those for the Default Category that, again, may be explicitly overridden at construction.
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').
One final note regarding categories is that the client can optionally supply a 'ball::LoggerManagerConfiguration::CategoryNameFilterCallback' functor 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. Such a name-filtering functor is set in the 'ball::LoggerManagerConfiguration' object before that configuration object is passed to the logger manager scoped guard constructor. 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.
The 'ball' logging framework provides the ability to associatiate "attributes" (name-value pairs) with the current logging context, which can be both be written to the log as part of a log record, as well as used in logging rules (see {Rule-Based Logging} below). This is typically done using the ball_scopedattribute component.
Below is a simple example using log attributes:
In the above example a logging attribute, "mylibrary.security", is associated with the current thread's logging context for the lifetime of the 'securityAttribute' object. As a result, if attributes have been enabled in the log format, the error log message generated by this example might look like:
Notice the attribute rendered does not appear in the logging message itself ("Error computing splines (-1)"), and is rendered as a name-value pair, meaning it can be easily parsed by log management systems like Humio or Splunk.
Attributes will not appear in your log unless your ball::Observer format specification is configured to render attributes (see below).
Log attributes are not rendered by default as part of the log message (for backward compatibility). Clients can enable log attributes to be rendered for observers that support log record formatting:
Log message formatting is implemented by the ball_recordstringformatter component, which supports the following (new) format specifiers to render log attributes:
The following code snippet illustrates the creation and configuration commonly used by observers:
The logger now provides a new constant, 'ball::RecordStringFormatter::k_BASIC_ATTRIBUTE_FORMAT', which is the recommended log format specification for most users (see the example above). The default log format specification remains the same to avoid changing behavior for existing applications. Note that the default format specification might be changed in a subsequent release of BDE.
The 'ball' library provides the following classes that implement ready-to-use application solutions for log attributes:
Note that 'ball::ScopedAttribute' is recommended for most applications. However, some low-level performance-critical systems (e.g., BAS) may implement custom attribute-collection mechanisms. This adds considerable complexity, but can deliver a small benefit in performance by taking advantage of compile time knowledge of the attributes being collected. See ball_attributecollectorregistry for more details on attribute collection mechanism.
Use the following naming conventions for attribute names:
It is highly recommended to use "namespaces" when naming attributes to avoid attribute name collisions. For example, consider these attribute names:
Handling of attributes with identical names is dictated by the underlying container(s) that store the attributes and the 'ball' library cannot guarantee any deterministic behavior in this case. It is the responsibility of the users to guarantee uniqueness of attribute names.
Log record attributes are rendered as space-separated 'name="value"' pairs (for example: 'mylibrary.requestType="POST"'). Note that attribute names are not quoted, whereas attribute values, if they are strings, are always quoted.
The 'ball' logging toolkit provides a set of components that collectively allow clients to define rules to modify logging severity levels depending on attributes associated with the current thread. The canonical example of rule-based logging is to enable verbose logging when processing requests for a particular user in a service.
A usage example can be found in {'Advanced Features Example 1: Rule-Based Logging'}.
To enable rule-based logging clients must do two things:
The configuration of process-wide rules is accomplished by creating rules (see {ball_rule }) that set a logging category's thresholds if a set of attributes associated with the current thread matches those of the rule (see {ball_managedattribute }). For example, a 'ball::Rule' might express: set the logging threshold for any logging category starting with the prefix "DATABASE" to 'e_TRACE' if the "myLib.userid" is 123456 and the "myLib.requesttype" is "trade" (where "myLib.userid" and "myLib.requesttype" are names of attributes that will be associated with a processing thread). Clients can then call 'ball::LoggerManager::addRule' to add this rule to the logger manager.
To associate an attribute (like "myLib.userid" and "myLib.requesttype") with a particular thread, clients must add a 'ball::AttributeContainer' holding the attribute to the 'ball::AttributeContext' (note that an attribute context is a thread-local object). The simplest means for doing this is to create a 'ball::ScopedAttribute' object as a local variable, which adds an attribute container (holding a single attribute) that exists for the lifetime of the scoped-attribute object. A 'ball::ScopedAttribute' appends a container having a single attribute to a list of containers, which is easy and efficient if there are only a small number of attributes. Clients with a large number of attributes could use a 'ball::DefaultAttributeContainer', which provides an efficient container for a large number of attributes. However, clients must take care to remove the container from the 'ball::AttributeContext' before the object goes out of scope and is destroyed. See ball_scopedattributes (plural) for an example. Finally, clients may also provide their own implementations of 'ball::AttributeContainer' customized for their needs (e.g., by creating a simple, efficient 'ball::AttributeContainer' specifically for the "myLib.userid" and "myLib.requesttype" attributes); see ball_attributecontainer for an example.
{'Advanced Features Example 1: Rule-Based Logging'} below demonstrates creating a simple logging rule, and associating attributes with a processing thread.
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 call to the logger manager scoped guard constructor 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' and the {Appendix: Macro Reference} below) are identical from the user's perspective. Category threshold administration is also identical in both cases, since that is done by the singleton logger manager.
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 user), such a logging configuration can provide a trace-back through the record buffer of the thread that caused the error, without any dilution from records logged 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 relevant records from all designated threads, in reverse chronological order.
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.
As an alternative to using the logger manager scoped guard, the 'static' 'ball::LoggerManager::initSingleton' method that takes the same arguments as the scoped guard constructor may be used to initialize the singleton. However, in this case the 'ball::LoggerManager::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.
Direct use of the 'public' logger manager constructor to initialize the logger manager singleton is deprecated. The constructor will be declared 'private' in a future release.
Direct use of any of the 16 'ball::LoggerManager::initSingleton' methods that do not take an instance of 'ball::LoggerManagerConfiguration' to initialize the logger manager singleton is deprecated. These methods will be eliminated in a future release.
This section provides a set of logging examples designed to introduce and illustrate basic concepts, features, and resulting operation. The first four examples are very simple "Hello world!" programs that show single-threaded usage. The next three examples illustrate multi-threaded usage. As simple as these examples are, they show sufficient functionality to use the 'ball' logger in a fair number of production applications. In particular, all of these examples use the macros from the 'ball_log' component to perform the actual logging. Methods from ball_loggermanager are used for initialization and administration, but for most applications, anything that can be done using the logging macros should be done with the macros.
This "Hello world!" example illustrates the most basic use of logging within a single translation unit containing a 'main' function and essentially nothing else. Among the points of usage that we illustrate are:
Note that, when logging using the macros, the 'BALL_LOG_SET_CATEGORY' macro is not only extremely convenient, it is typically required, since it in turn defines symbols that the logging macros expect to see. Also note that 'BALL_LOG_SET_CATEGORY' must be used at function scope, and can be used only once within a scope, although it can be nested in inner scopes.
The entire program file is as follows.
If the above program is run with no command-line arguments, the output will appear on 'stdout' as:
This case illustrates the default behavior, namely that "extraordinary" severity events (i.e., 'e_ERROR' and 'e_FATAL') are published immediately, but other events (such as 'e_INFO', 'e_DEBUG', and 'e_TRACE') are ignored. The user can easily modify this default behavior; subsequent examples will address these alternate usage modes.
If, however, any argument at all is provided to the executable on the command line, the output to 'stdout' will appear something like the following. (The spacing is changed to aid readability.)
Note that any number of observers can be registered with a logger manager. Also 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. For an example of such an observer, see ball_asyncfileobserver .
This "Hello world!" example illustrates how to enable "recording" log records to the in-memory record buffer, and also how to change the severity level at which records are published as "pass-through". The code is identical to {Example 1}, except that attributes of 'ball::LoggerManagerConfiguration' are changed from their defaults (in a single line, in this case).
The 'setDefaultThresholdLevelsIfValid' method of 'ball::LoggerManagerConfiguration' that we will use to change the default configuration takes four arguments, each in the range '[0 .. 255]'. The method returns a status, and will fail (with no effect) if any argument is not a valid threshold level. Note that any 'ball::Severity::Level' enumerator value is valid, and it is good practice to limit arguments to one of those enumerators. The one line that effects our change is:
This method call sets the four severity threshold levels as indicated and has the following effect on logging behavior.
With the one extra line of code as described above, plus the explicit '#include' directive of 'ball_severity.h' (since we use symbols from that header explicitly), the code is otherwise exactly as in {Example 1} above.
Once again, if the program is run with no command-line arguments, the output will appear on 'stdout' as:
Now, however, if a command-line argument is provided, the output is a little more extensive. (The spacing is changed to aid readability, and the timestamps are altered for illustrative purposes to show the temporal sequence more clearly).
Notice that the 'e_ERROR' message appears twice. This is not a bug but, as mentioned above, an intended consequence of the settings. As described above, the sequence of events is as follows: (1) The 'e_INFO' message is written to an in-memory buffer but not published (because 'e_INFO' is above the "Record" threshold but below the "Pass-Through", "Trigger", and "Trigger-All" thresholds). (2) The (one) 'e_ERROR' message is first published (a relatively quick operation insuring that the error indication is seen) because 'e_ERROR' is above the "Pass-Through" threshold, and then is written to the in-memory buffer, because 'e_ERROR' is above the "Record" threshold. Then, the entire contents of the in-memory buffer is published, in "Last In, First Out" (LIFO) order, because 'e_ERROR' is at the "Trigger" threshold.
This "Hello world!" example alters {Example 2} in one aspect. The conditional 'e_ERROR' message is removed, as is the use of command-line arguments, since they are not needed to illustrate our point. Instead, the user calls the 'publish' method of the logger instance "manually" before exiting, to force the publication of all unpublished messages in the logger's buffer. Note that this is not "default" behavior, but is accomplished simply enough. The line:
uses the singleton logger manager instance to obtain the default logger instance, and with that logger instance invokes the 'publish' method. Note that the logger's "manual" 'publish' method should not be confused with the 'ball::Observer' 'publish' method that is invoked by all "manual" and "automatic" logger "publishing" operations. Also note that should the 'manager' instance for some reason not be visible, the class method 'ball::LoggerManager::singleton()' can be used to obtain a reference to that instance. The full program file appears as follows.
This program produces the following output on 'stdout', altered here again in spacing format and timestamp value for ease of reading.
Note again that the output is published in LIFO order.
This example adds a few new aspects to the scenario of {Example 1} and {Example 2}. First of all, in addition to the file defining 'main', we introduce a separate translation unit 'f_log.cpp' that defines a function 'logThisInfoMsg'. The 'f_log.cpp' file defines its own category, "function category", that is distinct from the "main category" defined in 'main'. When 'logThisInfoMsg' is invoked, the resulting message is logged to "function category". Note, however, that when the now-familiar logging macros are used from 'main' directly, those messages are logged to "main category".
In addition to the above, we also show another way to alter the default thresholds. In this case, we choose values so that all messages logged with a severity at least that of 'e_INFO' are passed through immediately to the observer (in this case, to 'stdout'). In this example, we make the change by calling the 'setDefaultThresholdLevels' method of the logger manager:
The above call has an effect that differs subtly from the configuration object's 'setDefaultThresholdLevelsIfValid' method. The latter sets the permanent "factory defaults", as well as the "instantaneous defaults" that may be altered as often as desired using the above call. If, after the above call is made, the 'setCategoryThresholdsToFactoryDefaults' method is called, the effects of 'setDefaultThresholdLevels' are erased. This distinction is important for more advanced administration, but is not central to this example; we offer the alternative to illustrate logger functionality.
Note that it is both unnecessary and a fatal error for 'f_log' (or any library code) to instantiate a logger manager. The logger manager is a singleton, and should be constructed using the logger manager scoped guard ('ball::LoggerManagerScopedGuard'). In any sound design, this should be done in 'main' before any other threads have been created.
The main 'logging.m.cpp' file, plus 'f_log.cpp' and its associated 'f_log.h' file, are shown below. First the 'f_log.h' header file:
Then the 'f_log.cpp' implementation file:
Finally, the 'logging.m.cpp' file that defines 'main':
Building these two files and running the executable with no additional command-line arguments produces output from the two direct logging calls only. Once again, the format and timestamps are altered for readability.
Note that this sequence is not in LIFO order since this output is not from the message buffer, but rather is direct "Pass-Through" output.
If we provide a command-line argument that will cause the 'for' loop to execute, we then get the following output (with formats and timestamps altered as is now familiar in these examples).
This example illustrates how to use logging in a multi-threaded application. Since we have already shown how logging works seamlessly across multiple translation units, we will implement everything that we need for this example in a single file named 'mtlogging1.cpp'.
We define two functions, 'f1' and 'f2', that log their 'message' text argument to function-defined categories, at 'e_INFO' and 'e_WARN' severities, respectively. We also define two 'extern' "C" thread functions, 'threadFunction1' and 'threadFunction2', that call 'f1' and 'f2', respectively, in a loop of three iterations. The thread functions also differ slightly in their calls to 'bslmt::ThreadUtil::sleep' within the loop. These 'sleep' calls, in conjunction with the logged timestamps, show the interleaved operation of the threads. Note that although the thread functions define their own categories, the logging macros in 'f1' and 'f2' use only the category defined within the scope of 'f1' and 'f2', respectively.
'main' creates an observer that will write to 'stdout', initializes the logger manager singleton, sets a category, and sets the default threshold values, just as in the single-threaded examples above. Then 'main' uses 'bslmt' to spawn two threads running the above two thread functions. Each thread logs its messages, but only the second thread's log messages having 'e_WARN' severity will be published immediately as "Pass-Through" records. Finally, using the same 'verbose' mechanism (set by the presence or absence of a command-line argument) as the above examples, we conditionally log a message in 'main' at 'e_ERROR' severity. If this block of code executes, then the entire record buffer for the example (i.e., for both threads and 'main') will be published, in reverse chronological order.
The full code as described above is presented without interruption for ease of reading.
Running the above program with no extra command-line argument produces the output shown below. Note that only the 'e_WARN' records (from thread 3) are published, and that they are in chronological order.
However, if we supply an additional command-line argument, the 'if' statement containing the 'BALL_LOG_ERROR' macro will execute. This causes the 'e_ERROR' record itself to be published (from thread 1, the 'main' thread, as a "pass-through"), and it also triggers the publication of the entire record buffer. Note how the records from threads 1, 2, and 3 are published in inverse chronological order. Note in particular how the 'e_INFO' and 'e_WARN records interleave irregularly because of the different 'sleep' statements in the above code.
This example is similar to {Example 5} above, with only a few "minor" changes. Nevertheless, as we use more of the logger's features, we need to become more familiar with the details of logger operation. In this example, we install a distinct logger instance (i.e., not the default logger) for one of the threads. (The other thread continues to use the default logger that is shared with 'main', to make clear that the "one-logger-per-thread" usage pattern applies on a thread-by-thread basis, and is not "all-or-nothing".) By choosing this more sophisticated usage, however, we need to address the question of logger resource management and lifetime – a question that is completely avoided by using the default logger. As is often the case in programming, there is no one "right" answer to this resource question, and so we will discuss our choices and the alternatives in this example.
Installing a distinct logger instance in 'threadFunction1' is simple enough to do, but a question arises immediately: who should own the logger resources? Before we discuss this question, let's look at the specific solution that we have chosen to clarify the question of what resources a logger needs.
We've modified 'threadFunction1' to take a logger as an argument (passed as a 'void *') and to use the logger manager's 'setLogger' function to install the logger as the active logger for that thread. The code to do this in 'threadFunction1' is straightforward:
Clearly, in this solution, 'threadFunction1' does not own 'logger' or any of its resources. This must be the case, because the function has no way of knowing in how many other threads the passed-in 'logger' might be installed. Recall that any one logger may be installed in any number of threads.
This one-line addition to call the 'setLogger' method is, from the thread function's perspective, the only change needed if the thread is not to manage its own logger resources. But if 'threadFunction1' does not manage the logger resources, then 'main' must do so. Let's look at what 'main' needs to do so that we can be concrete about what resources are needed in the first place.
Several steps are needed to instantiate a 'ball::Logger' instance. The logger needs a (concrete) 'ball::RecordBuffer', and the preferred record buffer, 'ball::FixedSizeRecordBuffer', in turn needs a size for the record buffer ('LOGBUF_SIZE'). We also provide an explicit 'bslma::Allocator' to supply memory; the global allocator returned by 'bslma::Default::globalAllocator()' is sufficient here. Assuming that 32k bytes is a good size for the buffer, the following eight lines will appear in 'main'.
The last line frees resources and is executed just before 'main' returns.
With this implementation choice, 'main' clearly owns 'logger's resources. We now have enough information to address the question of what the consequences of this decision are, and whether or not 'main' actually should own these resources.
At first glance, it might seem that the thread itself should own and manage everything it needs for its own logger. This is certainly a reasonable choice, and {Example 6} below shows what 'threadFunction1' would look like in that case, but there are two separate reasons why the owner of 'main' might prefer not to delegate those responsibilities to the thread function.
The first reason – not relevant in this particular example – is that 'main' may wish to install the same logger in several threads. In this case, no one thread can manage "its own" logger, because the logger is (or at least might be) shared. Sharing loggers among groups of threads is at least a reasonable design choice, and so it is useful to see how 'main' takes ownership of logger resources.
The second reason for wanting 'main' and not the thread function to own the logger's resources is illustrated – albeit trivially – in this example: 'main' can potentially log a 'e_FATAL' record (here the "Trigger-All" threshold) after all other threads have terminated. If the user wants the (now-terminated) thread's trace-back log to be published by the "Trigger-All", then 'main' must preserve the logger resources. Otherwise, if a thread were to own its own logger resources, then any "trace-back" records generated by that thread would necessarily be discarded (or, at best, unconditionally published) when the thread function returned.
In this example we also made a trivial change that has nothing to do with threading, but illustrates a useful feature of 'ball::StreamObserver', namely that an instance can write to a file as easily as to 'stdout'. The three lines in 'main':
install a "file observer" that will write to "outFile", creating the file if it doesn't exist and overwriting it if it does. Clearly, this is just taking advantage of the standard C++ 'ofstream' class, but it is useful all the same.
The full code as described above is presented without interruption for ease of reading.
Running the above program with no extra command-line argument produces the output shown below that in this example is written to 'outFile'. Note that, except for the timestamps, process id, and filename, the output is the same as for {Example 5} above: only the 'e_WARN' records (from thread 3) are published and they are in chronological order.
When we supply an additional command-line argument, causing the logging macro in 'main' (now 'BALL_LOG_FATAL') to execute, the initial output is similar to {Example 5}. Specifically, the three 'e_WARN' records are published in chronological order as "Pass-Through" records, as is the 'e_FATAL' record (that now replaces the 'e_ERROR' record). From here on, however, the sequence of events is a bit different.
Recall that in this example 'e_FATAL' is the "Trigger-All" threshold level. For a "Trigger-All", each record buffer is published in turn. Within a record buffer, records are published in reverse chronological order, but, as can clearly be seen from the timestamps, the sequence of records as a whole is not well ordered in time at all. Note, rather, that the 'e_FATAL' and 'e_WARN' records from the default logger are published in reverse chronological order, and then the 'e_INFO' records from the installed 'logger' are published in reverse chronological order.
This example is almost identical to {Example 6} above, except that the eight lines (as highlighted above) needed to manage resources are moved from 'main' to 'threadFunction1', so that the latter function no longer needs an argument. Therefore, we show only 'threadFunction1' and 'main' explicitly in this example. Refer to the discussion of {Example 6} for details.
When the above program is run with a command-line argument, causing the 'e_FATAL' record to be logged and thus a "Trigger-All", the records from thread 2 are not available to be published. This is of course not an error but desired behavior chosen by the programmer. The output is as follows.
The following section shows examples of features used to customize the behavior and performance of the 'ball' logging system. They might be used, for example, by a low-level infrastructure library where the trade-off of greater complexity would be justified.
The following example demonstrates the use of attributes and rules to conditionally enable logging.
We start by defining a function, 'processData', that is passed data in a 'vector<char>' and information about the user who sent the data. This example function performs no actual processing, but does log a single message at the 'ball::Severity::e_DEBUG' threshold level. The 'processData' function also adds the user information passed to this function to the thread's attribute context. We will use these attributes later, to create a logging rule that enables verbose logging only for a particular user.
We add our attributes using 'ball::ScopedAttribute', which adds an attribute container with one attribute to a list of containers. This is easy and efficient if the number of attributes is small, but should not be used if there are a large number of attributes. If motivated, we could use 'ball::DefaultAttributeContainer', which provides an efficient container for a large number of attributes, or even create a more efficient attribute container implementation specifically for these three attributes (uuid, luw, and terminalNumber). See {ball_scopedattributes } (plural) for an example of using a different attribute container, and {ball_attributecontainer } for an example of creating a custom attribute container.
In this simplified example we perform no actual processing, and simply log a message at the 'ball::Severity::e_DEBUG' level.
Notice that if we were not using a "scoped" attribute container like that provided automatically by 'ball::ScopedAttribute' (e.g., if we were using a local 'ball::DefaultAttributeContainer' instead), then the container must be removed from the 'ball::AttributeContext' before it is destroyed! See ball_scopedattributes (plural) for an example.
Next we demonstrate how to create a logging rule that sets the pass-through logging threshold to 'ball::Severity::e_TRACE' (i.e., enables verbose logging) for a particular user when calling the 'processData' function defined above.
We start by creating the singleton logger manager that we configure with the stream observer and a default configuration. We then call the 'processData' function: This first call to 'processData' will not result in any logged messages because 'processData' logs its message at the 'ball::Severity::e_DEBUG' level, which is below the default configured logging threshold.
Now we add a logging rule, setting the pass-through threshold to be 'ball::Severity::e_TRACE' (i.e., enabling verbose logging) if the thread's context contains a "uuid" of 3938908. Note that we use the wild-card value '*' for the category so that the 'ball::Rule' rule will apply to all categories.
The final call to the 'processData' function below, passes a "uuid" of 2171395 (not 3938908) so the logging rule we defined will not apply and no message will be logged.
The resulting logged output for this example looks like the following:
Please see {Key Example 2: Initialization} to learn how scoped attributes can be added to log messages to provide additional log context.
This example demonstrates how to customize the collection of log attributes. Attributes are typically associated with a log record using a 'ball::ScopedAttribute' object (see {Key Example 1: Write to a Log} and {Key Example 2: Initialization}). However, advanced users can customize the collection of attributes, either by registering an attribute-collector callback with the 'ball::LoggerManager', or by creating their own 'ball::AttributeCollector' implementation, or both. These customizations can be used to implement alternative behavior, or provide faster implementation (e.g., by taking advantage of compile-time knowledge of the attributes being recorded).
Suppose we are writing a performance critical infrastructure system that processes requests. We want to:
First we create a 'ball::AttributeContainer' implementation. Although the performance gains from doing so are typically insignificant, the use of a context-specific attribute container object allows us to take advantage of compile-time knowledge of the attributes being collected and make small improvements in the overhead required, which may be important for performance critical systems.
Then we create a guard to add and remove a 'ServiceAttributes' container from the current logging attribute context:
Now we use a 'ServiceAttributesGuard' in a critical infrastructure function:
Notice that when 'processData' is called, attributes for 'uuid', 'luw', and 'firmId' will be associated with each log message emitted during that function call.
Next, we create a callback function that will be used to associate a hostname attribute to each log record for the lifetime of the process:
Finally we demonstrate a function that registers the 'loadHostnameAttribute' with the logger manager:
Notice that the attribute "mylibrary.hostname" will now be associated with every log message created (until "mylibrary.hostnamecollector" is unregistered or the logger manager is destroyed).
This section documents the preprocessor macros defined in 'ball_log' that are most commonly used.
The following two macros establish the logging context required by the other macros. A use of one of these two macros must be visible from within the lexical scope where the C++ stream-based and 'printf'-style macros are used:
'BALL_LOG_SET_CATEGORY(CATEGORY)': Set the category for logging to the specified 'CATEGORY' (assumed to be of type convertible to 'const char *'). Note that this macro must be used at block scope and can be used at most once in any given block (or else a compiler diagnostic will result). Also note that this macro invokes the 'ball::Log::setCategory' method to retrieve the address of an appropriate category structure for its scope. (See the function- level documentation of 'ball::Log::setCategory' for more information.) Also note that the category is set only on the first invocation of this macro in a code block; subsequent invocations will use a cached address of the category.
'BALL_LOG_SET_DYNAMIC_CATEGORY(CATEGORY)': Set, on each invocation, the category for logging to the specified 'CATEGORY'. This macro is identical to 'BALL_LOG_SET_CATEGORY' in scoping, parameter, and use of the 'ball::Log::setCategory' method. However, the address returned from 'ball::Log::setCategory' is not cached for subsequent calls. Use this macro to create categories that depend on run-time values (e.g., UUID).
The seven macros based on C++ streams, 'BALL_LOG_TRACE', 'BALL_LOG_DEBUG', 'BALL_LOG_INFO', 'BALL_LOG_WARN', 'BALL_LOG_ERROR', and 'BALL_LOG_FATAL', have the following usage pattern:
The remaining macros are based on 'printf'-style format specifications: