This is a partial contributor’s guide for the Babeltrace project. If you have any questions that are not answered by this guide, please post them on Babeltrace’s mailing list.
This section covers the rationale behind the design of Babeltrace’s object lifetime management. This applies to the Babeltrace library, as well as to the CTF writer library (although the public reference counting functions are not named the same way).
Starting from Babeltrace 2.0, all publicly exposed objects inherit a
common base: bt_object
. This base provides a number of facilities to
all objects, chief amongst which are lifetime management functions.
The lifetime of some public objects is managed by reference counting. In
this case, the API offers the bt_*_get_ref()
and bt_*_put_ref()
functions which respectively increment and decrement an object’s
reference count.
As far as lifetime management in concerned, Babeltrace makes a clear distinction between regular objects, which have a single parent, and root objects, which don’t.
Let us consider a problematic case to illustrate the need for this distinction.
A user of the Babeltrace library creates a trace class, which has a stream class (the class of a stream) and that stream class, in turn, has an event class (the class of an event).
Nothing prevents this user from releasing his reference on any one of these objects in any order. However, all objects in the trace—stream class—event class hierarchy can be retrieved from any other.
For instance, the user could discard his reference on both the event class and the stream class, only keeping a reference on the trace class. From this trace class reference, stream classes can be enumerated, providing the user with a new reference to the stream class he discarded earlier. Event classes can also be enumerated from stream classes, providing the user with references to the individual event classes.
Conversely, the user could also hold a reference to an event class and retrieve its parent stream class. The trace class, in turn, can then be retrieved from the stream class.
This example illustrates what could be interpreted as a circular reference dependency existing between these objects. Of course, if the objects in such a scenario were to hold references to each other (in both directions), we would be in presence of a circular ownership resulting in a leak of both objects as their reference counts would never reach zero.
Nonetheless, the API must offer the guarantee that holding a node to any node of the graph keeps all other reachable nodes alive.
The scheme employed in Babeltrace to break this cycle consists in the "children" holding reverse component references to their parents. That is, in the context of the trace IR, that event classes hold a reference to their parent stream class and stream classes hold a reference to their parent trace class.
On the other hand, parents hold claiming aggregation references to their children. A claiming aggregation reference means that the object being referenced should not be deleted as long as the reference still exists. In this respect, it can be said that parents truly hold the ownership of their children, since they control their lifetime. Conversely, the reference counting mechanism is leveraged by children to notify parents that no other child indirectly exposes the parent.
When a parented object’s reference count reaches zero, it invokes
bt_*_put_ref()
on its parent and does not free itself. However,
from that point, the object depends on its parent to signal the moment
when it can be safely reclaimed.
The invocation of bt_*_put_ref()
by the last children holding a
reference to its parent might trigger a cascade of bt_*_put_ref()
from child to parent. Eventually, a root object is reached. At that
point, if this orphaned object’s reference count reaches zero, the
object invokes the destructor method defined by everyone of its children
as part of their base struct bt_object
. The key point here is that the
cascade of destructor will necessarily originate from the root and
propagate in preorder to the children. These children will propagate the
destruction to their own children before reclaiming their own memory.
This ensures that a node’s pointer to its parent is always valid since
the parent has the responsibility of tearing-down their children before
cleaning themselves up.
Assuming a reference to an object is acquired by calling
bt_*_get_ref()
while its reference count is zero, the object
acquires, in turn, a reference on its parent using bt_*_get_ref()
.
At that point, the child can be thought of as having converted its weak
reference to its parent into a regular reference. That is why this
reference is referred to as a claiming aggregation reference.
This scheme imposes a number of strict rules defining the relation between objects:
-
Objects may only have one parent.
-
Objects, beside the root, are only retrievable from their direct parent or children.
The initial situation is rather simple: User A is holding a reference to a trace class, TC1. As per the rules previously enounced, stream classes SC1 and SC2 don’t hold a reference to TC1 since their own reference counts are zero. The same holds true for EC1, EC2 and EC3 with respect to SC1 and SC2.
In this second step, we can see that User A has acquired a reference on SC2 through the trace class, TC1.
The stream class’s reference count transitions from zero to one, triggering the acquisition of a strong reference on TC1 from SC2.
Hence, at this point, the trace class’s ownership is shared by User A and SC2.
Next, User A acquires a reference on the EC3 event class through its parent stream class, SC2. Again, the transition of an object’s reference count from 0 to 1 triggers the acquisition of a reference on its parent.
Note that SC2’s reference count was incremented to 2. The trace class’s reference count remains unchanged.
User A decides to drop its reference on SC2. SC2's reference count returns back to 1, everything else remaining unchanged.
User A can then decide to drop its reference on the trace class. This results in a reversal of the initial situation: User A now owns an event, EC3, which is keeping everything else alive and reachable.
If another object, User B, enters the picture and acquires a reference on the SC1 stream class, we see that SC1's reference count transitioned from 0 to 1, triggering the acquisition of a reference on TC1.
User B hands off a reference to EC1, acquired through SC1, to another object, User C. The acquisition of a reference on EC1, which transitions from 0 to 1, triggers the acquisition of a reference on its parent, SC1.
At some point, User A releases its reference on EC3. Since EC3's reference count transitions to zero, it releases its reference on SC2. SC2's reference count, in turn, reaches zero and it releases its reference to TC1.
TC1's reference count is now 1 and no further action is taken.
User B releases its reference on SC1. User C becomes the sole owner of the whole hierarchy through his ownership of EC1.
Finally, User C releases his ownership of EC1, triggering the release of the whole hierarchy. Let’s walk through the reclamation of the whole graph.
Mirroring what happened when User A released its last reference on EC3, the release of EC1 by User C causes its reference count to fall to zero.
This transition to zero causes EC1 to release its reference on SC1. SC1's reference count reaching zero causes it to release its reference on TC1.
Since the reference count of TC1, a root object, has reached zero, it invokes the destructor method on its children. This method is recursive and causes the stream classes to call the destructor method on their event classes.
The event classes are reached and, having no children of their own, are reclaimed.
The stream classes having destroyed their children, are then reclaimed by the trace class.
Finally, the stream classes having been reclaimed, TC1 is reclaimed.
Logging is a great instrument for a developer to be able to collect information about a running software.
Babeltrace is a complex software with many layers. When a Babeltrace
graph fails to run, what caused the failure? It could be caused by any
component, any message iterator, and any deeply nested validation of a
CTF IR object (within the ctf
plugin), for example. With the
appropriate logging statements manually placed in the source code, we
can find the cause of a bug faster.
While care must be taken when placing DEBUG to FATAL logging statements, you should liberally instrument your Babeltrace module with TRACE logging statements to help future you and other developers understand what’s happening at run time.
The Babeltrace logging API is internal: it is not exposed to the users
of the library; only to their developers. The only thing that a library
user can control is the current log level of the library itself with
bt_logging_set_global_level()
and the initial library’s log level with
the LIBBABELTRACE2_INIT_LOG_LEVEL
environment variable.
This API is based on zf_log, a
lightweight, yet featureful, MIT-licensed core logging library for C and
C++. The zf_log source files were modified to have the BT_
and
bt_
prefixes, and other small changes, like color support and using
the project’s BT_DEBUG_MODE
definition instead of the standard
NDEBUG
.
The logging functions are implemented in the logging convenience
library (src/logging
directory).
The logging API headers are:
<babeltrace2/logging.h>
-
Public header which a library user can use to set and get libbabeltrace2’s current log level.
"logging/log.h"
-
Internal, generic logging API which you can use in any Babeltrace module. This is the translation of
zf_log.h
.This header offers the generic logging statement macros.
"lib/logging.h"
-
Specific internal header to use within the library.
This header defines
BT_LOG_OUTPUT_LEVEL
to a custom, library-wide hidden symbol which is the library’s current log level before including"logging/log.h"
.This header offers the library-specific logging statement macros.
"logging/comp-logging.h"
-
Specific internal header to use within a component class.
This header offers the component-specific logging statement macros.
The internal logging API offers the following log levels, in ascending order of severity:
Log level name | Log level short name | Internal API enumerator | Public API enumerator |
---|---|---|---|
TRACE |
|
|
|
DEBUG |
|
|
|
INFO |
|
|
|
WARNING |
|
|
|
ERROR |
|
|
|
FATAL |
|
|
|
NONE |
|
|
|
The short name is accepted by the log level environment variables and by
the CLI’s --log-level
options.
See how to decide which one to use below.
There are two important log level expressions:
- Build-time, minimal log level
-
The minimal log level, or build-time log level, is set at build time and determines the minimal log level of the logging statements which can be executed. This applies to all the modules (CLI, library, plugins, bindings, etc.).
All the logging statements with a level below this level are not built at all. All the logging statements with a level equal to or greater than this level can be executed, depending on the run-time log level.
You can set this level at configuration time with the
BABELTRACE_MINIMAL_LOG_LEVEL
environment variable, for example:$ BABELTRACE_MINIMAL_LOG_LEVEL=INFO ./configure
The default build-time log level is
DEBUG
. For optimal performance, set it toINFO
, which effectively disables all fast path logging in all the Babeltrace modules. You can’t set it toWARNING
,ERROR
,FATAL
, orNONE
because the impact on performance is minuscule starting from the INFO log level anyway and we want any Babeltrace build to always be able to print INFO-level logs.The library’s public API provides
bt_logging_get_minimal_level()
to get the configured minimal log level. - Run-time, dynamic log level
-
The dynamic log level is set at run time and determines the current, active log level. All the logging statements with a level below this level are not executed, but they still evaluate the condition. All the logging statements with a level equal to or greater than this level are executed, provided that their level is also enabled at build time.
zf_log
has a concept of a global run-time log level which uses the_bt_log_global_output_lvl
symbol. In practice, we never use this symbol, and always make sure thatBT_LOG_OUTPUT_LEVEL
is defined to a module-wise expression before including"logging/log.h"
.In the library,
"lib/logging.h"
defines its ownBT_LOG_OUTPUT_LEVEL
to the library’s log level symbol before it includes"logging/log.h"
itself.In libbabeltrace2, the user can set the current run-time log level with the
bt_logging_set_global_level()
function, for example:bt_logging_set_global_level(BT_LOGGING_LEVEL_INFO);
The library’s initial run-time log level is defined by the
LIBBABELTRACE2_INIT_LOG_LEVEL
environment variable, or set to NONE if this environment variable is undefined.Other modules have their own way of setting their run-time log level.
For example, the CLI uses the
BABELTRACE_CLI_LOG_LEVEL
environment variable, as well as its global--log-level
option:$ babeltrace2 --log-level=I ...
The components use their own log level (as returned by
bt_component_get_logging_level()
). With the CLI, you can set a specific component’s log level with its own, position-dependent--log-level
option:$ babeltrace2 /path/to/trace -c sink.ctf.fs --log-level=D
Code which is common to the whole project, for example
src/common
andsrc/compat
, use function parameters to get its run-time log level, for example:BT_HIDDEN char *bt_common_get_home_plugin_path(int log_level);
Typically, when a logging-enabled module calls such a function, it passes its own log level expression directly (
BT_LOG_OUTPUT_LEVEL
):path = bt_common_get_home_plugin_path(BT_LOG_OUTPUT_LEVEL);
Otherwise, just pass
BT_LOG_NONE
:path = bt_common_get_home_plugin_path(BT_LOG_NONE);
The Babeltrace logging statement macros work just like printf()
(except the BT_LOG*_STR()
ones) and contain their log
level (short name) in their name.
Each of the following macros evaluate the
build-time log level definition and
run-time log level expression (as defined by
BT_LOG_OUTPUT_LEVEL
) to log conditionally.
See Instrument a C source file (generic) and Instrument a C header file (generic) to learn how to be able to use the following macros.
BT_LOGT("format string", ...)
-
Generic trace logging statement.
BT_LOGD("format string", ...)
-
Generic debug logging statement.
BT_LOGI("format string", ...)
-
Generic info logging statement.
BT_LOGW("format string", ...)
-
Generic warning logging statement.
BT_LOGE("format string", ...)
-
Generic error logging statement.
BT_LOGF("format string", ...)
-
Generic fatal logging statement.
BT_LOGT_STR("preformatted string")
-
Generic preformatted string trace logging statement.
BT_LOGD_STR("preformatted string")
-
Generic preformatted string debug logging statement.
BT_LOGI_STR("preformatted string")
-
Generic preformatted string info logging statement.
BT_LOGW_STR("preformatted string")
-
Generic preformatted string warning logging statement.
BT_LOGE_STR("preformatted string")
-
Generic preformatted string error logging statement.
BT_LOGF_STR("preformatted string")
-
Generic preformatted string fatal logging statement.
BT_LOGT_MEM(data_ptr, data_size, "format string", ...)
-
Generic memory trace logging statement.
BT_LOGD_MEM(data_ptr, data_size, "format string", ...)
-
Generic memory debug logging statement.
BT_LOGI_MEM(data_ptr, data_size, "format string", ...)
-
Generic memory info logging statement.
BT_LOGW_MEM(data_ptr, data_size, "format string", ...)
-
Generic memory warning logging statement.
BT_LOGE_MEM(data_ptr, data_size, "format string", ...)
-
Generic memory error logging statement.
BT_LOGF_MEM(data_ptr, data_size, "format string", ...)
-
Generic memory fatal logging statement.
BT_LOGT_ERRNO("initial message", "format string", ...)
-
Generic
errno
string trace logging statement. BT_LOGD_ERRNO("initial message", "format string", ...)
-
Generic
errno
string debug logging statement. BT_LOGI_ERRNO("initial message", "format string", ...)
-
Generic
errno
string info logging statement. BT_LOGW_ERRNO("initial message", "format string", ...)
-
Generic
errno
string warning logging statement. BT_LOGE_ERRNO("initial message", "format string", ...)
-
Generic
errno
string error logging statement. BT_LOGF_ERRNO("initial message", "format string", ...)
-
Generic
errno
string fatal logging statement.
The Babeltrace library contains an internal logging API based on the generic logging framework. You can use it to log known Babeltrace objects without having to manually log each member.
See Instrument a library C source file and Instrument a library C header file to learn how to be able to use the following macros.
The library logging statement macros are named BT_LIB_LOG*()
instead
of BT_LOG*()
:
BT_LIB_LOGT("format string", ...)
-
Library trace logging statement.
BT_LIB_LOGD("format string", ...)
-
Library debug logging statement.
BT_LIB_LOGI("format string", ...)
-
Library info logging statement.
BT_LIB_LOGW("format string", ...)
-
Library warning logging statement.
BT_LIB_LOGE("format string", ...)
-
Library error logging statement.
BT_LIB_LOGF("format string", ...)
-
Library fatal logging statement.
BT_LIB_LOGW_APPEND_CAUSE("format string", ...)
-
Library warning logging statement, and unconditional error cause appending.
BT_LIB_LOGE_APPEND_CAUSE("format string", ...)
-
Library error logging statement, and unconditional error cause appending.
BT_LIB_LOGF_APPEND_CAUSE("format string", ...)
-
Library fatal logging statement, and unconditional error cause appending.
The macros above accept the typical printf()
conversion specifiers
with the following limitations:
-
The
*
width specifier is not accepted. -
The
*
precision specifier is not accepted. -
The
j
andt
length modifiers are not accepted. -
The
n
format specifier is not accepted. -
The format specifiers defined in
<inttypes.h>
are not accepted, except forPRId64
,PRIu64
,PRIx64
,PRIX64
,PRIo64
, andPRIi64
.
The Babeltrace library custom conversion specifier is accepted. Its
syntax is either %!u
to format a UUID (bt_uuid
type), or:
-
Introductory
%!
sequence. -
Optional:
[
followed by a custom prefix for the printed fields of this specifier, followed by]
. The standard form is to end this prefix with-
so that, for example, with the prefixtc-
, the complete field name becomestc-addr
. -
Optional:
+
to print extended object members. This depends on the provided format specifier. -
Format specifier (see below).
The available format specifiers are:
Specifier | Object | Expected C type |
---|---|---|
|
Trace IR field class |
|
|
Trace IR field |
|
|
Trace IR field path |
|
|
Trace IR event class |
|
|
Trace IR event |
|
|
Trace IR stream class. |
|
|
Trace IR stream |
|
|
Trace IR packet |
|
|
Trace IR trace class |
|
|
Trace IR trace |
|
|
Trace IR clock class |
|
|
Trace IR clock snapshot |
|
|
Value object |
|
|
Integer range set |
|
|
Message |
|
|
Message iterator class |
|
|
Message iterator |
|
|
Component class |
|
|
Component |
|
|
Port |
|
|
Connection |
|
|
Graph |
|
|
Interrupter |
|
|
Plugin |
|
|
Error cause |
|
|
Object pool |
|
|
Object |
|
Conversion specifier examples:
-
%!f
-
%![my-event-]+e
-
%!t
-
%!+F
The ,
string (comma and space) is printed between individual
fields, but not after the last one. Therefore, you must put this
separator in the format string between two conversion specifiers, for
example:
BT_LIB_LOGW("Message: count=%u, %!E, %!+K", count, event_class, clock_class);
Example with a custom prefix:
BT_LIB_LOGI("Some message: %![ec-a-]e, %![ec-b-]+e", ec_a, ec_b);
It is safe to pass NULL
as any Babeltrace object parameter: the macros
only print its null address.
Warning
|
Build-time printf() format checks are disabled for the
BT_LIB_LOG*() macros because there are custom conversion specifiers,
so make sure to test your logging statements.
|
There are available logging macros for components. They prepend a prefix including the component’s name to the logging message.
See Instrument a component class C source file and Instrument a component class C header file to learn how to be able to use the following macros.
The component logging statement macros are named BT_COMP_LOG*()
instead of BT_LOG*()
:
BT_COMP_LOGT("format string", ...)
-
Component trace logging statement.
BT_COMP_LOGD("format string", ...)
-
Component debug logging statement.
BT_COMP_LOGI("format string", ...)
-
Component info logging statement.
BT_COMP_LOGW("format string", ...)
-
Component warning logging statement.
BT_COMP_LOGE("format string", ...)
-
Component error logging statement.
BT_COMP_LOGF("format string", ...)
-
Component fatal logging statement.
BT_COMP_LOGT_STR("preformatted string")
-
Component preformatted string trace logging statement.
BT_COMP_LOGD_STR("preformatted string")
-
Component preformatted string debug logging statement.
BT_COMP_LOGI_STR("preformatted string")
-
Component preformatted string info logging statement.
BT_COMP_LOGW_STR("preformatted string")
-
Component preformatted string warning logging statement.
BT_COMP_LOGE_STR("preformatted string")
-
Component preformatted string error logging statement.
BT_COMP_LOGF_STR("preformatted string")
-
Component preformatted string fatal logging statement.
BT_COMP_LOGT_ERRNO("initial message", "format string", ...)
-
Component
errno
string trace logging statement. BT_COMP_LOGD_ERRNO("initial message", "format string", ...)
-
Component
errno
string debug logging statement. BT_COMP_LOGI_ERRNO("initial message", "format string", ...)
-
Component
errno
string info logging statement. BT_COMP_LOGW_ERRNO("initial message", "format string", ...)
-
Component
errno
string warning logging statement. BT_COMP_LOGE_ERRNO("initial message", "format string", ...)
-
Component
errno
string error logging statement. BT_COMP_LOGF_ERRNO("initial message", "format string", ...)
-
Component
errno
string fatal logging statement. BT_COMP_LOGT_MEM(data_ptr, data_size, "format string", ...)
-
Component memory trace logging statement.
BT_COMP_LOGD_MEM(data_ptr, data_size, "format string", ...)
-
Component memory debug logging statement.
BT_COMP_LOGI_MEM(data_ptr, data_size, "format string", ...)
-
Component memory info logging statement.
BT_COMP_LOGW_MEM(data_ptr, data_size, "format string", ...)
-
Component memory warning logging statement.
BT_COMP_LOGE_MEM(data_ptr, data_size, "format string", ...)
-
Component memory error logging statement.
BT_COMP_LOGF_MEM(data_ptr, data_size, "format string", ...)
-
Component memory fatal logging statement.
BT_LOG_IF(cond, statement)
-
Execute
statement
only ifcond
is true.Example:
BT_LOG_IF(i < count / 2, BT_LOGD("Log this: i=%d", i));
To check the build-time log level:
#if BT_LOG_ENABLED_DEBUG
...
#endif
This tests if the DEBUG level was enabled at build time. This means
that the current, run-time log level could be
DEBUG, but it could also be higher. The rule of thumb is to use only
logging statements at the same level in a BT_LOG_ENABLED_*
conditional
block.
The available definitions for build-time conditions are:
-
BT_LOG_ENABLED_TRACE
-
BT_LOG_ENABLED_DEBUG
-
BT_LOG_ENABLED_INFO
-
BT_LOG_ENABLED_WARNING
-
BT_LOG_ENABLED_ERROR
-
BT_LOG_ENABLED_FATAL
To check the current, run-time log level:
if (BT_LOG_ON_DEBUG) {
...
}
This tests if the DEBUG log level is dynamically turned on (implies that it’s also enabled at build time). This check could have a noticeable impact on performance.
The available definitions for run-time conditions are:
-
BT_LOG_ON_TRACE
-
BT_LOG_ON_DEBUG
-
BT_LOG_ON_INFO
-
BT_LOG_ON_WARNING
-
BT_LOG_ON_ERROR
-
BT_LOG_ON_FATAL
Those macros check the module-specific log level symbol (defined by
BT_LOG_OUTPUT_LEVEL
).
Never, ever write code which would be executed only to compute the fields of a logging statement outside a conditional logging scope, for example:
int number = get_number_of_event_classes_with_property_x(...);
BT_LOGD("Bla bla: number=%d", number);
Do this instead:
if (BT_LOG_ON_DEBUG) {
int number = get_number_of_event_classes_with_property_x(...);
BT_LOGD("Bla bla: number=%d", number);
}
Or even this:
BT_LOGD("Bla bla: number=%d", get_number_of_event_classes_with_property_x(...));
To instrument a C source file (.c
):
-
At the top of the file, before the first
#include
line (if any), define your file’s logging tag name:#define BT_LOG_TAG "SUBSYS/MY-MODULE/MY-FILE"
-
Below the line above, define the source file’s log level expression,
BT_LOG_OUTPUT_LEVEL
. This expression is evaluated for each logging statement to know the current run-time log level.Examples:
/* Global log level variable */ #define BT_LOG_OUTPUT_LEVEL module_global_log_level
/* Local log level variable; must exist where you use BT_LOG*() */ #define BT_LOG_OUTPUT_LEVEL log_level
/* Object's log level; `obj` must exist where you use BT_LOG*() */ #define BT_LOG_OUTPUT_LEVEL (obj->log_level)
-
Include
"logging/log.h"
:#include "logging/log.h"
-
In the file, instrument your code with the generic logging statement macros.
To instrument a C header file (.h
), if you have static inline
functions in it:
-
Do not include
"logging/log.h"
! -
Do one of:
-
In the file, instrument your code with the generic logging statement macros, making each of them conditional to the existence of the macro you’re using:
static inline int some_function(int x) { /* ... */ #ifdef BT_LOGT BT_LOGT(...); #endif /* ... */ #ifdef BT_LOGW_STR BT_LOGW_STR(...); #endif /* ... */ }
The C source files which include this header file determine if logging is enabled or not for them, and if so, what is their logging tag and run-time log level expression.
-
Require that logging be enabled:
/* Protection: this file uses BT_LOG*() macros directly */ #ifndef BT_LOG_SUPPORTED # error Please include "logging/log.h" before including this file. #endif
Then, in the file, instrument your code with the generic logging statement macros.
-
To instrument a library C source file (.c
):
-
At the top of the file, before the first
#include
line (if any), define your file’s logging tag name (this tag must start withLIB/
):#define BT_LOG_TAG "LIB/THE-FILE"
-
Include
"lib/logging.h"
:#include "lib/logging.h"
-
In the file, instrument your code with the library logging statement macros or with the generic logging statement macros.
To instrument a library C header file (.h
), if you have static
inline
functions in it:
-
Do not include
"lib/logging.h"
! -
Require that library logging be enabled:
/* Protection: this file uses BT_LIB_LOG*() macros directly */ #ifndef BT_LIB_LOG_SUPPORTED # error Please include "lib/logging.h" before including this file. #endif
-
In the file, instrument your code with the library logging statement macros or with the generic logging statement macros.
To instrument a component class C source file (.c
):
-
At the top of the file, before the first
#include
line (if any), define your file’s logging tag name (this tag must start withPLUGIN/
followed by the component class identifier):#define BT_LOG_TAG "PLUGIN/SRC.MY-PLUGIN.MY-SRC"
-
Below the line above, define the source file’s log level expression,
BT_LOG_OUTPUT_LEVEL
. This expression is evaluated for each logging statement to know the current run-time log level.For a component class file, it is usually a member of a local component private structure variable:
#define BT_LOG_OUTPUT_LEVEL (my_comp->log_level)
-
Below the line above, define
BT_COMP_LOG_SELF_COMP
to an expression which, evaluated in the context of the logging statements, evaluates to the self component address (bt_self_component *
) of the component.This is usually a member of a local component private structure variable:
#define BT_COMP_LOG_SELF_COMP (my_comp->self_comp)
-
Include
"logging/comp-logging.h"
:#include "logging/comp-logging.h"
-
In the component initialization method, make sure to set the component private structure’s log level member to the initial component’s log level:
struct my_comp { bt_logging_level log_level; /* ... */ }; BT_HIDDEN bt_self_component_status my_comp_init( bt_self_component_source *self_comp_src, bt_value *params, void *init_method_data) { struct my_comp *my_comp = g_new0(struct my_comp, 1); bt_self_component *self_comp = bt_self_component_source_as_self_component(self_comp_src); const bt_component *comp = bt_self_component_as_component(self_comp); BT_ASSERT(my_comp); my_comp->log_level = bt_component_get_logging_level(comp); /* ... */ }
-
In the file, instrument your code with the component logging statement macros.
To instrument a component class C header file (.h
), if you have
static inline
functions in it:
-
Do not include
"logging/comp-logging.h"
! -
Require that component logging be enabled:
/* Protection: this file uses BT_COMP_LOG*() macros directly */ #ifndef BT_COMP_LOG_SUPPORTED # error Please include "logging/comp-logging.h" before including this file. #endif
-
In the file, instrument your code with the component logging statement macros.
Each logging-enabled C source file must define BT_LOG_TAG
to a logging
tag. A logging tag is a namespace to identify the logging messages of
this specific source file.
In general, a logging tag name must be only uppercase letters, digits,
and the -
, .
, and /
characters.
Use /
to show the subsystem to source file hierarchy.
For the Babeltrace library, start with LIB/
.
For the CTF writer library, start with CTF-WRITER/
.
For component classes, use:
PLUGIN/CCTYPE.PNAME.CCNAME[/FILE]
With:
CCTYPE
-
Component class’s type (
SRC
,FLT
, orSINK
). PNAME
-
Plugin’s name.
CCNAME
-
Component class’s name.
FILE
-
Additional information to specify the source file name or module.
For plugins (files common to many component classes), use:
PLUGIN/PNAME[/FILE]
With:
PNAME
-
Plugin’s name.
FILE
-
Additional information to specify the source file name or module.
Choosing the appropriate level for your logging statement is very important.
Log level | Description | Use cases | Expected impact on performance |
---|---|---|---|
FATAL |
The program, library, or plugin cannot continue to work in this condition: it must be terminated immediately. A FATAL-level logging statement should always be followed by
|
|
Almost none: always enabled. |
ERROR |
An important error which is somewhat not fatal, that is, the program, library, or plugin can continue to work after this, but you judge that it should be reported to the user. Usually, the program cannot recover from such an error, but it can at least exit cleanly. |
|
Almost none: always enabled. |
WARNING |
An error which still allows the execution to continue, but you judge that it should be reported to the user. WARNING-level logging statements are for any error or weird action that is directly or indirectly caused by the user, often through some bad input data. For example, not having enough memory is considered beyond the user’s control, so we always log memory errors with an ERROR level (not FATAL because we usually don’t abort in this condition). |
|
Almost none: always enabled. |
INFO |
Any useful information which a non-developer user would possibly understand. Anything logged with this level must not happen repetitively on the fast path, that is, nothing related to each message, for example. This level is used for sporadic and one-shot events. |
|
Very little: always enabled. |
DEBUG |
Something that only Babeltrace developers would be interested into, which can occur on the fast path, but not more often than once per message. The DEBUG level is the default build-time log level as, since it’s not too verbose, the performance is similar to an INFO build. |
|
Noticeable, but not as much as the TRACE level: could be executed in production if you’re going to need a thorough log for support tickets without having to rebuild the project. |
TRACE |
Low-level debugging context information (anything that does not fit the other log levels). More appropriate for tracing in general. |
|
Huge: not executed in production. |
Important
|
Make sure not to use a WARNING (or higher) log level when the condition leading to the logging statement can occur under normal circumstances. For example, a public function to get some object or property from an object by name or key that fails to find the value is not a warning scenario: the user could legitimately use this function to check if the name/key exists in the object. In this case, use the TRACE level (or do not log at all). |
Follow those rules when you write a logging statement’s message:
-
Use an English sentence which starts with a capital letter.
-
Start the sentence with the appropriate verb tense depending on the context. For example:
-
Beginning of operation (present continuous): Creating …, Copying …, Serializing …, Freezing …, Destroying …
-
End of operation (simple past): Created …, Successfully created …, Failed to create …, Set … (simple past of to set which is also set)
For warning and error messages, you can start the message with Cannot or Failed to followed by a verb if it’s appropriate.
-
-
Do not include the log level in the message itself. For example, do not start the message with Error while or Warning:.
-
Do not put newlines, tabs, or other special characters in the message, unless you want to log a string with such characters. Note that multiline logging messages can be hard to parse, analyze, and filter, however, so prefer multiple logging statements over a single statement with newlines.
-
If there are fields that your logging statement must record, follow the message with
:
followed by a space, then with the list of fields (more about this below). If there are no fields, end the sentence with a period.
The statement’s fields must be a comma-separated list of
name=value
tokens. Keep name
as simple as possible; use
kebab case if possible. If value
is a non-alphanumeric string, put
it between double quotes ("%s"
specifier). Always use the PRId64
and
PRIu64
specifiers to log an int64_t
or an uint64_t
value. Use %d
to log a boolean value.
Example:
"Cannot read stream data for indexing: path=\"%s\", name=\"%s\", " "stream-id=%" PRIu64 ", stream-fd=%d, " "index=%" PRIu64 ", status=%s, is-mapped=%d"
By following a standard format for the statement fields, it is easier to use tools like Logstash or even Splunk to split fields and analyze logs.
Prefer the following suffixes in field names:
Field name suffix | Description | Format specifier |
---|---|---|
|
Memory address |
|
|
File descriptor |
|
|
File stream ( |
|
|
Object’s ID |
|
|
Index |
|
|
Object’s name |
|
The log is printed to the standard error stream. A log line contains the time, the process and thread IDs, the log level, the logging tag, the source’s function name, file name and line number, and the message.
When Babeltrace supports terminal color codes (depends on the
BABELTRACE_TERM_COLOR
environment variable’s value and what the
standard output and error streams are plugged into), INFO-level lines
are blue, WARNING-level lines are yellow, and ERROR-level and
FATAL-level lines are red.
Log line example:
05-11 00:58:03.691 23402 23402 D VALUES bt_value_destroy@values.c:498 Destroying value: addr=0xb9c3eb0
You can easily filter the log with grep
or ag
. For example, to keep
only the DEBUG-level logging messages that the FIELD-CLASS
module
generates:
$ babeltrace2 --log-level=D /path/to/trace |& ag 'D FIELD-CLASS'
To use Valgrind on an application (for example, the CLI or a test) which loads libbabeltrace2, use:
$ G_SLICE=always-malloc G_DEBUG=gc-friendly PYTHONMALLOC=malloc \ LIBBABELTRACE2_NO_DLCLOSE=1 valgrind --leak-check=full app
G_SLICE=always-malloc
and G_DEBUG=gc-friendly
is for GLib and
PYTHONMALLOC=malloc
is for the Python interpreter, if it is used by
the Python plugin provider (Valgrind will probably show a lot of errors
which originate from the Python interpreter anyway).
LIBBABELTRACE2_NO_DLCLOSE=1
makes libbabeltrace2 not close the shared
libraries (plugins) which it loads. You need this to see the appropriate
backtrace when Valgrind shows errors.
tests/utils/utils.sh
sets the environment variables for any Babeltrace
test script.
utils.sh
only needs to know the path to the tests
directory within
the source and the build directories. By default, utils.sh
assumes the
build is in tree, that is, you ran ./configure
from the source’s root
directory, and sets the BT_TESTS_SRCDIR
and BT_TESTS_BUILDDIR
environment variables accordingly. You can override those variables, for
example if you build out of tree.
All test scripts eventually do something like this to source utils.sh
,
according to where they are located relative to the tests
directory:
if [ "x${BT_TESTS_SRCDIR:-}" != "x" ]; then
UTILSSH="$BT_TESTS_SRCDIR/utils/utils.sh"
else
UTILSSH="$(dirname "$0")/../utils/utils.sh"
fi
You can use the tests/utils/run_python_bt2
script to run any command
within an environment making the build’s bt2
Python package available.
run_python_bt2
uses utils.sh
which needs to know the
build directory, so make sure you set the BT_TESTS_BUILDDIR
environment variable correctly if you build out of tree, for example:
$ export BT_TESTS_BUILDDIR=/path/to/build/babeltrace/tests
You can run any command which needs the bt2
Python package through
run_python_bt2
, for example:
$ ./tests/utils/run_python_bt2 ipython3
All test scripts output the test results following the Test Anything Protocol (TAP) format.
The TAP format has two mechanisms to print additional information about a test:
-
Print a line starting with
#
to the standard output.This is usually done with the
diag()
C function or thediag
shell function. -
Print to the standard error.
The bt2
Python package tests are located in
tests/bindings/python/bt2
.
tests/utils/python/testrunner.py
is Babeltrace’s Python test runner
which loads Python files containing unit tests, finds all the test
cases, and runs the tests, producing a TAP report.
You can see the test runner command’s help with:
$ python3 ./tests/utils/python/testrunner.py --help
By default, the test runner reports failing tests (TAP’s not ok
line), but continues to run other tests. You can use the --failfast
option to make the test runner fail as soon as a test fails.
To run all the bt2
Python package tests:
-
Run:
$ ./tests/utils/run_python_bt2 ./tests/bindings/python/bt2/test_python_bt2
or:
$ ./tests/utils/run_python_bt2 python3 ./tests/utils/python/testrunner.py \ ./tests/bindings/python/bt2/ -p '*.py'
To run all the tests in a test module (for example,
test_value.py
):
-
Run:
$ ./tests/utils/run_python_bt2 python3 ./tests/utils/python/testrunner.py \ ./tests/bindings/python/bt2 -t test_value
To run a specific test case (for example, RealValueTestCase
within
test_value.py
):
-
Run:
$ ./tests/utils/run_python_bt2 python3 ./tests/utils/python/testrunner.py \ ./tests/bindings/python/bt2/ -t test_value.RealValueTestCase
To run a specific test (for example,
RealValueTestCase.test_assign_pos_int
within test_value.py
):
-
Run:
$ ./tests/utils/run_python_bt2 python3 ./tests/utils/python/testrunner.py \ ./tests/bindings/python/bt2/ -t test_value.RealValueTestCase.test_assign_pos_int