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 thePID_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
andpid::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
andpid::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 frameworka_framework
or to frameworkanother_framework
will be rejected.sinks
is the set of sinks used by the logger. Each sink must define at least one output stream (usingoutputs
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 ininfo
,debug
,warning
,error
andcritical
) 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 areSTD
(standard output),ERROR
(error output),LOG
(log output),FILE
(file output).accept
andreject
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
andcomponent
(printing info about component that generated the log message).