Jan 6, 2021
Adding Context to Your Application Log¶
Introduction¶
The BDE 3.71 release adds new features to the ball logging framework, the most important of which is the ability to add attributes to log records.
int processData(const bsl::string& security,
const bsl::vector<char>& data)
{
ball::ScopedAttribute securityAttribute("mylibrary.security", security);
// ...
int rc = reticulateSplines(data);
return rc;
}
int reticulateSplines(data)
{
// ...
if (0 != rc) {
BALL_LOG_ERROR << "Error computing splines (" << rc << ")";
}
return rc;
}
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:
ERROR example.cpp:105 EXAMPLE.CATEGORY mylibrary.security="IBM US Equity" Error computing splines (-1)
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.
Important
Attributes will not appear in your log unless your ball::Observer format specification is configured to render attributes (see below).
Configuring an Observer to Output Attributes¶
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:
ball::FileObserver
ball::AsyncFileObserver
ball::StreamObserver
ball::FileObserver2
Log message formatting is implemented by the ball_recordstringformatter
component, which supports the following (new) format specifiers to render log
attributes:
Format Specifier |
Description |
---|---|
%A |
Log all the attributes of the record |
%a |
Log only those attributes not already logged by the “%a[name]” specifier |
%a[name] |
Log a attribute’s value, where ‘name’ is the attribute’s name |
The following code snippet illustrates the creation and configuration commonly used by observers:
int initFileObserver() {
// 'ball::FileObserver' uses an internal record string formatter and
// is configured by supplying a format string via 'setLogFormat().
bslma::Allocator *alloc = bslma::Default::globalAllocator(0);
bsl::shared_ptr<ball::FileObserver> observer =
bsl::allocate_shared<ball::FileObserver>(alloc);
// Set the log format for file and console logs to "\n%d %p:%t %s %f:%l %c %a %m\n"
observer->setLogFormat(ball::RecordStringFormatter::k_BASIC_ATTRIBUTE_FORMAT,
ball::RecordStringFormatter::k_BASIC_ATTRIBUTE_FORMAT);
if (0 != observer->enableFileLogging("myapplication.log.%T")) {
bsl::cout << "Failed to enable logging" << bsl::endl;
return -1;
}
ball::LoggerManager::singleton().registerObserver(observer, "default");
return 0;
}
New Suggested Log Record Format¶
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.
Details on Using Attributes¶
The ball
library provides the following classes that implement ready-to-use
application solutions for log attributes:
ball::Attribute This class represents an attribute that consists of a (literal) name (held but not owned), and an associated value (owned) that can be an
int
,bsls::Type::Int64
, or absl::string
.ball::ScopedAttribute Scoped guard that creates an attribute container with a single attribute supplied at construction and adds this container to the current thread’s context. The container is removed from the thread context when the guard leaves the scope.
ball::RecordStringFormatter Implements a log record formatting functor. Formats log records into human-readable log messages according to the specified format specs.
Note that ball::ScopedAttribute
is recommended for most applications (see
Performance below). 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.
Advanced usage examples can be found in the BALL package documentation. We recommend reaching out to the BDE team for guidance before using these features .
Important
Use of ball::UserFields
is now deprecated in favor of the new log attributes API.
Attribute Naming Recommendations¶
Use the following naming conventions for attribute names:
An attribute name should start with an alphabetic character; no other special characters or digits should be allowed as the first character of an attribute name.
An attribute name should not contain whitespaces.
An attribute name should contain only alphanumeric characters, underscores(‘_’), and dots(‘.’). Do not use any other special characters.
It is highly recommended to use “namespaces” when naming attributes to avoid attribute name collisions. For example, consider these attribute names:
Attribute Name |
Notes |
---|---|
“context id” |
BAD (contains whitespace) |
“contextid” |
BAD (no namespace) |
“bde.contextid” |
GOOD |
“bde.logger.context_id” |
GOOD |
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.
Important
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.
Humio and Splunk¶
Humio and Splunk are two popular log management systems used at Bloomberg.
The format of the log attributes rendered by the ball
logging system is
designed to be parsed automatically by these systems.
Users of ball
logging are also encouraged to format the text of their messages
to be easily digested by these systems. For example, providing information
as key-value pairs, where appropriate. Additional information can be found at
Splunk Logging Best Practices
and Humio Built-In Parsers.
Performance¶
The benchmark below gives some insight into the the cost of attribute-based logging and other logging features. The absolute times should be taken with a grain of salt – they are specific to the platform, archicture, build mode, machine, and time of day at which the benchmark was run. However, they should give some sense of the relative cost of various logging actions.
“Marginal Cost” Explained
The benchmark results present the “Marginal Cost” of various operations. The intention is to show the added cost for performing a single additional operation to an existing piece of code. E.g., the marginal cost of “One Log Statement Logging to Memory” is computed by comparing a test run A’, where a log record is written to an in-memory buffer on each test iteration, to a test-run A, which is (as close as possible) identical, but with the log category disabled so the logging statement does not write to the log. The Marginal Cost (MC) is computed using (A’-A)/N (N is the number of test iterations, in this case 5,000,000).
Results¶
Operation |
Time microseconds |
Notes |
---|---|---|
Baseline (nearly empty function) |
0.003 |
|
Hash a ~25 char string |
0.031 |
Provided to give a sense of scale |
One Disabled Logging Statement |
0.000 |
Cost is below the ability to measure over background noise |
One Log Statement Logging to Memory |
1.340 |
One log statement that will write to an in-memory buffer |
One Log Statement Logging to a File |
2.155 |
One log statement that will write to a file |
Creating (3) ScopedAttributes |
0.077 |
1 scoped attribute would be ~.03 microseconds |
Publishing Attributes (w/ no attributes) |
0.021 |
Adding %a to the log format specification but with no attributes to log |
Publishing Attributes (w/attributes) |
0.437 |
Adding %a to the log format specification and 3 attributes to log |
High-Level Conclusions
Log statements that are not enabled are very low cost
Creating a log attribute has a small but measureable cost (similar to hashing a string)
The expense of publishing an attribute is mostly in writing the values, and is proportional with the amount of additional text being written
Our Recommendations
Use
ball::ScopedAttributes
where needed, but use them judiciously. Performance-critical infrastructure libraries might consider custom attribute containers (see Details on Using Attributes above).Most users should enable the (non-default) format specification to enable attributes to be published to their log.
Benchmark Details¶
The source code and build instructions for the benchmark are here: https://bbgithub.dev.bloomberg.com/bde-extras/ball-benchmark
The data for the test run is here: Ball Attributes Benchmark Google Sheet
- Context for the test:
OS: Red Hat Enterprise Linux Server 7.6 (Maipo)
Build: gcc-7.3.1, -02, -m64, –std=c++17
Processors: 47 CPU, Intel(R) Xeon(R) CPU E5-2687W v4 @ 3.00GHz (12 core)
Local Disk
Machine Load at Start: 99.6% idle
Discard initial test run data
Other Features¶
ball::StreamObserver now supports log record formatting by suppling a ball::RecordStringFormatter .
The BDE team is working on a JSON log record string formatter that will render published log records as JSON. This is expected to be released at the end of Q1.