NAV

Purpose

The pid-log package implement the runtime logging facility of PID. By logging we mean generating messages that inform about the state of a running program. These messages can be read from standard outputs or files and may be useful to notify some tools of interesting events, to provide debug information to developpers, or traces of program execution.

pid-log intend to provide a powerful but easy to configure logging tool for developpers with features:

  • possibility to deactivate logs for a the whole program. Useful when you want to get real-time compatible performance and log is not required.
  • filtering log messages either according to their severity or depending on their emitting place (component, package, framework).
  • generating various log outputs from the same global log trace in a program. For instance usefull to discriminate errors or debug traces from other information.
  • formatting of log messages is standardized and can be configured to get various information such as: calling site information (line in source file, calling function), runtime trace generation (date, thread) or PID related information (framework, package, component of the emitting component).
  • easy configuration of logger system at global level. This way libraries can be completely agnostic from the way logs they generate are finally managed.

Concepts

There are a few basic concepts to know before using pid-log:

  • The logger refers to the unique global object that controls the generation of log messages. It can be configured and controlled at runtime. It is accessed either via the pid::logger() function or via the PID_LOGGER macro.
  • A component is loggable if it can be identified at runtime by the logger. At CMake definition time, it is declared as LOGGABLE.
  • A proxy refers to the object used to generate log traces. This object is local to each loggable component. There is also a global anonymous proxy for non loggable components. Proxies are accessed in code using the pid_log macro.
  • A filter is an object in charge of filtering log traces according to some rules. The logger has a global filter that is empty by default (i.e. every log message is valid) and can be configured.
  • A sink is an objec in charge of generating output from log traces. A sink has:

  • a filter that is used to filer log messages coming from proxies.
  • a formatter used to format the output with additional information
  • a set of outputs streams, that are the streams where the filtered messages are written with specific format.

Sinks and filters need to be configured in order for the logger to work properly (i.e. generate outputs from log traces). This is achieved either directly using the pid-log API or by using configuration files.

Using pid-log

Basically, to use logging system of PID, a package needs to depend on the pid-log package. In root CMakeLists.txt of the package:

...
PID_Dependency(pid-log)
...

Defining loggable components

Then, let’s suppose you want a component to generate log traces you need to declare your component as loggable. In the CMakeLists.txt of the package, where the component is defined:

...
PID_Component(SHARED NAME my-component DIRECTORY my_folder LOGGABLE)
...

The LOGGABLE argument tells PID to generate a specific header file that will be included in public headers (for components having public interface) or sources (e.g. for applications). This header file will be used by the PID system to identify the emitting component at runtime. LOGGABLE is particularly usefull for libraries because we sometime want to filter log messages coming from a given package or component and LOGGABLE is used to automatically deduce the calling site. So by default any library that generates log traces using pid-log should be a loggable object.

The generated header file has pattern name <package>_<component>.h and is located in the pid/log subfolder of:

  • component include folder if it is a component with public headers (header, static or shared library).
  • component source folder otherwise (module, test or application).

Its content looks like:

#pragma once

#ifdef LOG_pid_log_pid_log_test_hdr

#include <pid/log.h>

#undef PID_LOG_FRAMEWORK_NAME
#undef PID_LOG_PACKAGE_NAME
#undef PID_LOG_COMPONENT_NAME

#define PID_LOG_FRAMEWORK_NAME "pid"
#define PID_LOG_PACKAGE_NAME "pid-log"
#define PID_LOG_COMPONENT_NAME "pid-log-test-hdr"

#endif

Values for PID_LOG_FRAMEWORK_NAME, PID_LOG_PACKAGE_NAME and PID_LOG_COMPONENT_NAME is adapted depending of component and package information.

The only thing that remain to be done by the developer is to include this header in its source file, respecting these constraints:

  • this header must be the last one included in your source or header files. Indeed this is the only way you are ensured that this code will not be overwritten by a header of another included loggable component.
  • as far as possible include this header only in sources, not in headers. A notable exception is when public headers contain template code that generates log traces.

Defining non loggable components that use the pid-log API

If your component does not generate log traces by itself (i.e. its code is not using the pid-log API to write logs using proxies), there is no need to declare it as loggable. This may be for instance the case for an application whose generated traces are coming only from the libraries it uses. In this case your component may require to use the pid-log API to configure the logger, so simply do this as a usual dependency to pid-log library:

...
PID_Component(SHARED NAME my-component DIRECTORY my_folder DEPEND pid-log/pid-log)
...

In the code using pid-log API to configure the logger (typically main() function of your program) you simply have to include the pid-log headers:

#include <alib.h>
#include <pid/log.h>

int main(int argc, char * argv[]){
	PID_EXE(argv[0]);
	pid::logger().configure(...);

	...
}

Now it is time to look inside API of pid-log to learn:

  • how to write log traces in a loggable component.
  • how to configure the logger to get desired outputs.

Generating log traces

Log traces can be generated by any component using the pid-log API. Simply remember that only loggable components can be identified as specific emitting places, while non loggable components are considered as anonymous (and so their traces cannot be discriminated from those of other non loggable components).

To generate log traces a component needs to use a proxy. A proxy is automatically generated, configured and registered in logger using pid_log. pid_log is a macro that provides the adequate proxy object (either specific to a loggable component or anonymous) that is usable quite like a c++ stream:

void my_function(){
	int error_number=0;
	...
	if(bad_situation){
		pid_log << pid::error << "error number: " << error_number << pid::endl<<" one more line ..."<<pid::flush;
	}
	...
	if(strange_case_to_test > 0.0){
		pid_log << pid::debug << "bad situation "<< strange_case_to_test << pid::flush;
	}
}

As the example shows, you can use standard output formatters for specific types as well as other stream modifiers:

  • pid::endl adds a newline to the current log message but does not flush the output.
  • pid::flush flush the proxy. This will launch the message filtering process.
  • pid::info, pid::debug, pid::warning, pid::error and pid::critical are severity specifiers used to specify the nature of the new log message to generate. The flush of the proxy is automatic whenever the current severity specifier changes.
  • pid::align_on and pid::align_off are used respectively to activate and deactivate the automatic alignment of multi lines messages. Alignment consists in adding same padding spaces for all lines of the message.

Configuring generated outputs

The configuration and control of the logging system in itself should most of time takes place in the main program. This consists in using the API of the logger, you can find it in <pid/logger.h>. The logger itself is obtained using the PID_LOGGER macro or pid::logger() function, which give access to the global logger object. By default the logger is configured to write all logs in standard output without any filtering. The simplest way to configure the logger is to write and use a configuration file:

int main(int argc, char * argv[]){
	PID_EXE(argv[0]);
	PID_LOGGER.configure("path_to_a_file.yaml");
	...
}

This simply tells to configure the logger with the given configuration, whose content could look like this:

activate: true
accept:
  severity: debug
reject:
  frameworks: [a_framework, another_framework]
sinks:
 - outputs: [STD]
   format: [date, source]

In this example, the logger will remove any message that is not a debug message (due to the global accept rule), then any message coming from components belonging to frameworks a_framework or another_framework is also removed (due to the global reject rule). All messages allowed by the global filter are then forwarded to the unique sink. This later simply writes to the standard output messages with labels specifying source code that made the call, and date when message has been generated.

Here are the possible yaml fields available:

  • activate is used to tell if the logger is activate dor not. In later case no output traces will be generated.
  • accept is used to define which traces are accepted. Any log message with at least one adequate property among those listed will be accepted. In the example any debug message is accepted. Any log message whose property does not match any of the accepted properties will be rejected.
  • reject is used to define which messages, among those that are accepted, is rejected. In the example, any message coming from a component belonging either to framework a_framework or to framework another_framework will be rejected.
  • sinks is the set of sinks used by the logger. Each sink must define at least one output stream (using outputs keyword).

accept and reject can be used either at global scope (like in the example) or at sink scope. If used at global scope, they will configure the global filter, otheriwse they configure the sink specific filter. The global filter is used to remove messages before forwarding the messages to the sinks, so every message rejected at global level will never appear in any sink. At sink scope, they are used to filter messages that will be finally printed in outputs. From yaml perspective accept and reject are maps with following subproperties:

  • severity is the set of severity matched by the filter. Value for this field can be either a scalar (e.g. with value chosen in info, debug, warning, error and critical) or a sequence of scalars. If the message matches one of the specified severities, the filter condition is satisfied.
  • frameworks is the set of frameworks matched by the filter. Value for this field can be either a scalar (e.g. with name of target framework as value) or a sequence of scalars. If the message matches one of the specified frameworks, the filter condition is satisfied.
  • packages is the set of packages matched by the filter. Value for this field can be either a scalar (e.g. with name of target package as value) or a sequence of scalars. If the message matches one of the specified packages, the filter condition is satisfied.
  • components is the set of packages matched by the filter. Value for this field can be either a scalar (e.g. with name of target component as value) or a sequence of scalars. If the message matches one of the specified components, the filter condition is satisfied.

Each sink defined by sinks is an element of a sequence. It defines:

  • outputs: this mandatory property defines where the sink will generate output traces from logged messages. Possible values are STD (standard output), ERROR (error output), LOG (log output), FILE (file output).
  • accept and reject are used to define the sink specific filter.
  • format is used to define the format of the generated output. It is a scalar or sequence of scalar used to specify output using specific values: date (printing timing info), severity (printing severity info), source (printing info about source code that generated the log), function (printing info about function that generated the log message), thread (printing info about the thread that generated the log message), framework, package and component (printing info about component that generated the log message).