util::logger

The logger submodule provides a straightforward interface for submodules to emit log messages. It offers a set of methods for handling log messages while leaving the implementation details to the application. The application determines how to filter and output received messages. By default, without concrete implementation, log message calls are 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

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

As a general rule, the info log level should be the default without generating an excessive amount of logs. It is good practice to avoid repetitive logging of the same state and instead focus on logging state changes.

Logger Components

With many modules covering diverse functionality, it is crucial to have an identifier that indicates the origin of a log message. Therefore, each message must be assigned to a logger component to specify its source. Typically, a logging implementation allows filtering messages by components.

Defining a logger component

When a module emits logs, the key decision is determining which logger component to assign to the message. Typically, a module defines a uniquely named logger component. This definition must reside in a source file because the logger component is mapped to a global uint8_t variable. For instance, the TEST_COMPONENT logger component can be defined in a TestComponentLogger.cpp file as follows:

#include "util/logger/Logger.h"

DEFINE_LOGGER_COMPONENT(TEST_COMPONENT)

Important

The logger component declaration and definition macros create components within the util::logger namespace. It is crucial to use these macros only outside of any namespace and not from within one!

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 no dedicated header file contains the logger component’s declaration, the DECLARE_LOGGER_COMPONENT macro can be placed in any other source or header file where the component is used, provided the declaration is made outside of any namespace.

If a log is used from within a source file it might make sense to place a using declaration to the top of the file that allows unqualified usage of the component for logging:

using ::util::logger::TEST_COMPONENT;

Functionality

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 implementation.

Class diagram

Logger : log()
Logger : critical()
Logger : error()
Logger : warn()
Logger : info()
Logger : debug()
Logger : isEnabled()
Logger : getLevel()

Module1 ..> Logger
Module2 ..> Logger

class IComponentMapping
class ILoggerOutput

package "Logger implementation 1" {
    ComponentMapping1 <|-- IComponentMapping
    LoggerOutput1 <|-- ILoggerOutput
}

package "Logger implementation 2" {
    ComponentMapping2 <|-- IComponentMapping
    LoggerOutput2 <|-- ILoggerOutput
}

Logger ..> ComponentMapping1
Logger ..> LoggerOutput1

Logger ..> ComponentMapping2
Logger ..> LoggerOutput2

Usage

When emitting logs from within a source file, it is advisable to include a using directive at the top of the file. This allows unqualified usage of both the util::logger::Logger class and the logger component:

using ::util::logger::Logger;
using ::util::logger::TEST_COMPONENT;

A log message is emitted by calling a log method with the component and a printf-style text, as shown:

Logger::info(TEST_COMPONENT, "Using version %d.%02d", major, minor);

In the above example, the severity is specified using the static method util::logger::Logger::info, and the logger component is util::logger::TEST_COMPONENT. The additional arguments have effect similar to arguments in the printf function.

In rare cases where the severity is not fixed, the more generic method util::logger::Logger::log can be used:

Logger::log(TEST_COMPONENT, getLevel(), "Using version %d.%02d", major, minor);

If preparing arguments for emitting a log is time-consuming, it may be advisable to first check whether logging is enabled for the given component and desired log level. A log can then be emitted as follows:

if (Logger::isEnabled(::util::logger::LEVEL_INFO))
{
    Logger::info(TEST_COMPONENT, "Time consuming log of value %d", getComputedValue());
}

Initialization and shutdown

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 levels

  • A 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 then 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 is, by design, a synchronous call (see util::logger::Logger::doLog() method). However, the subsequent util::logger::ILoggerOutput::logOutput() interface method can be implemented according to the needs of the application. For example, the logger::BufferedLoggerOutput class (logger - Logger module) initially stores messages in a buffer. These messages are then popped and sent to the output stream by calling the logger::BufferedLoggerOutput::outputEntry() method, which can, in turn, be invoked in a loop within the OS’s lower-priority tasks of the application.

Timestamps

The logger interface does not provide timing output. Typically, appropriate timestamp information is added in the implementation of the util::logger::ILoggerOutput::logOutput() method of the util::logger::ILoggerOutput interface.