logger - logger API
The logger API submodule contains a simple API that allows submodules to emit log messages. It does not provide any implementation (a logger framework) for handling of the log messages. It is up to an application to decide about how to filter and output received messages. By default (without any framework) log message calls are simply ignored.
Levels
A log message first has a given severity (level) that indicates the importance of the message. There’s a fixed set of levels defined (with descending importance):
Level |
Description |
---|---|
fatal/critical |
Any error that is hard to recover from or that indicates severe data loss |
error |
Any error of a service that can cause data loss and which cannot be handled automatically |
warn |
Events or unexpected behaviour that can be handled automatically |
info |
General useful information to log, such as normally handled user events or state changes. |
debug |
Information that is diagnostically helpful |
It is up to the developer to decide about assignment of severities to a message. But as a thumb rule info should be selectable as default log level without obtaining tons of logs. It is always good practice to avoid repetitive logging of the same state but focussing on logging of state changes.
Logger Components
While there are a lot of modules which cover very different functionality it is crucial to have an identifier that defines about the origin of a log message. Therefore to each message a logger component has to be assigned to indicate that origin. Typically a logging framework allows filtering messages by components.
Defining a logger component
When logs should be emitted by some module the most important decision is about the logger component
assigned to the message. Typically a module will define a uniquely named logger component. This
definition has to be placed into a source file because the logger component definition will be
mapped to a global uint8_t
variable. The definition of a TEST_COMPONENT
logger component
could be placed in a TestComponentLogger.cpp
file like:
#include "util/logger/Logger.h"
DEFINE_LOGGER_COMPONENT(TEST_COMPONENT)
Note
The logger component declaration and definition macros create the components in the namespace
::util::logger
. It is crucial to not use the define or declare macros from within
a namespace but only outside of any namespace!
To allow users to access the logger component (typically when configuring a logger in an application main file) it is good style to also add a header file that contains the declaration of the logger as follows:
#ifndef TEST_COMPONENT_LOGGER_H
#define TEST_COMPONENT_LOGGER_H
#include "util/logger/Logger.h"
DECLARE_LOGGER_COMPONENT(TEST_COMPONENT)
#endif // TEST_COMPONENT_LOGGER_H
Using a logger component
If there’s no include file that holds a logger component’s declaration, the declaration using
DECLARE_LOGGER_COMPONENT
can also be placed in any other source or header file where the component
is to be used, as long as the declaration is done outside of any namespace.
If a log is used from within a source file it might make sense to place a using
statement to the
top of the file that allows unqualified usage of the component for logging:
using ::util::logger::TEST_COMPONENT;
Macro-based API
There’s a macro based API that allows specifying logs using macros which can arbitrarily be defined by a logging framework.
Value based log macros
A value based log consists of two or more values. The first value is the component identifier and the second is the message (a zero-terminated string value). All additional data can simply be added by appending values which can be named and also equipped with a unit.
This might look complicated but has the advantage that message and data are strictly separated and can be interpreted by value.
Macro |
Description |
---|---|
|
Log with severity fatal |
|
Log with severity error |
|
Log with severity warn |
|
Log with severity info |
|
Log with severity debug |
Formatted message log macros
A formatted message consists of a message (a zero-terminated string value) that contains
printf
-like format specifiers for embedding additional values.
Macro |
Description |
---|---|
|
Log formatted message with severity fatal |
|
Log formatted message with severity error |
|
Log formatted message with severity warn |
|
Log formatted message with severity info |
|
Log formatted message with severity debug |
Atomic value macros
Atomic value macros add type information to atomic values.
Macro |
Type |
Comment |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
output as hex |
|
|
|
|
|
|
|
|
output as hex |
|
|
|
|
|
|
|
|
output as hex |
|
|
|
|
|
|
|
|
output as hex |
|
|
zero-terminated string |
|
|
sized string |
|
|
pointer |
Array value macros
Array value macros add type information to array values.
Macro |
Type |
Comment |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
output as hex |
|
|
|
|
|
|
|
|
output as hex |
|
|
|
|
|
|
|
|
output as hex |
|
|
|
|
|
|
|
|
output as hex |
Other macros
Macro |
Description |
---|---|
|
names the value (zero-terminated string). |
|
provide a unit (zero-terminated string). |
|
string value specified as a printf-like call where |
Examples
Value based error with named string value and named integer value:
LOG_E(FLEXRAY, "A voltage error has occurred",
L_NAME("reason", L_STR("unknown")),
L_NAME("voltage, L_UNIT("mV", L_U32(voltage))));
Value based info with named hex parameters:
LOG_I(TRANSPORT, "Transport message received",
L_NAME("src", L_H16(src)),
L_NAME("dst", L_H16(dst)));
Debug log with additional formatted parameter:
LOG_D(TEST_COMPONENT, "IPv4 address received",
L_FMT("%d.%d.%d.%d", L_U8(addr[0]), L_U8(addr[1]), L_U8(addr[2]), L_U8(addr[3])));
Printf-like message with formatted string and integer:
LOG_FMT_E(FLEXRAY, "A voltage error has occurred with reason=%s and voltage=%d mV",
L_STR("unknown"),L_U32(voltage));
Printf-like message with formatted hex parameters:
LOG_FMT_I(TRANSPORT, "Transport message received with src=%02x and dst=%02x",
L_U16(src), L_U16(dst));
Attaching a logging framework
To attach a logging framework (which can define the macros) it is sufficient to provide a file
util/logger/LoggerBinding.h
on an include path that precedes the include path of module
util
. Here all API macros can be defined (overwritten). Macros which aren’t defined here will
be defined void by the logging API.
Note
The macro-based API is recommended for new modules because it allows better optimizations (e.g.
for suppressing certain log messages). There is currently only the dlt
module that supports a
complete implementation for value based logging.
Method-based API
The method-based API is mainly based on the class ::util::logger::Logger
that is a
simple facade that can propagate calls to a logging framework.
Class diagram
Usage
When logs are emitted from within a source file it is advisable to place a using
statement to
the top of the file that allows unqualified usage of both class ::util::logger::Logger
and logger component:
using ::util::logger::Logger;
using ::util::logger::TEST_COMPONENT;
Emitting a log message is then simply calling a log method with the component and a printf-style text like:
Logger::info(TEST_COMPONENT, "Using version %d.%02d", major, minor);
In the above example the severity is expressed by usage of the static method
::util::logger::Logger::info
and the logger component is called
::util::logger::TEST_COMPONENT
. The additional arguments is like a simple printf
.
In rare cases the severity will not be fixed and for this the more generic method
::util::logger::Logger::log
can be used:
Logger::log(TEST_COMPONENT, getLevel(), "Using version %d.%02d", major, minor);
In case the preparation of arguments for emitting a log would be time consuming it can make sense to first check whether logging is enabled for the given component and the desired log level. So a log would be emitted like follows:
if (Logger::isEnabled(::util::logger::LEVEL_INFO))
{
Logger::info(TEST_COMPONENT, "Time consuming log of value %d", getComputedValue());
}
Providing a logger framework
The method-based logger API is just a facade that can propagate log messages to a logger implementation – if provided. For collecting log messages two interfaces have to be provided:
A so called component mapping interface (implementation of
::util::logger::IComponentMapping
) that filters logs and holds readable names for the components and levelsA logger output interface (implementation of
::util::logger::ILoggerOutput
) that receives already filtered log messages and information about the component and the level
Having decided and instantiated all necessary interfaces the logger than has to be initialized with a call to:
Logger::init(componentMapping, loggerOutput);
In case there’s a regular shutdown scenario the logger implementation can be disconnected by a call to the shutdown method like:
Logger::shutdown();
Synchronous/Asynchronous logging
The logger output by design is synchronously i.e. the logOutput method of the interface
::util::logger::ILoggerOutput
will be called immediately if the filtering was
successful. Thus it is up to the implementation to decide whether to emit the message in a
synchronous or asynchronous way.
Timestamps
Provision and handling of timestamps is not addressed in the logger interface. Typically appropriate
timestamp information will be added in the implementation of the logOutput
method of interface
::util::logger::ILoggerOutput
.