This document describes the features of the logging library. There are optional and core features.
Core Features - are mandatory features requred by the library for the normal operation:
- Print - provides formatted printing to streams and buffers (not exposed to the users and used by other features internally)
- Outputs - printing endpoints logic and stdout output
- Levels - severity filters per output
- Events - containers to distribute logging info across outputs
- Lock - logic to inject external thread-safety mechanism
- Logging - logic that generates an event and dispatch it to outputs
- Static Configuration - compile-time logic to enable/disable optional features and configure core features
Optional Features - are configurable optional features that can extend the core library capabilities:
- Color - add ANSI colors to the output
- Time - add time stamps
- Prefix - add custom data after the time stamp
- Extra Outputs - additional user-defined outputs, including files
- Source Location - prints
file:linelocation of a logging call - Level Style - full or short severity level name
- Topics - label based message filtering
- Dynamic Configuration - run-time configuration of all features
- Warnings Stubs for Non-Enabled Features - generate stubs for disabled features with warning message or just fail linking if the function is disabled.
Part of features are configured compile-time. You can use defines in the compiler options, e.g. -DULOG_BUILD_COLOR=1.
For CMake projects, you can use the add_compile_definitions function.
target_compile_definitions(microlog PRIVATE ULOG_BUILD_COLOR=1)For Meson projects, you can use the meson command.
add_global_arguments('-DULOG_BUILD_COLOR=1', language: 'c')Note: For meson, you might want to adjust the compiler argument -fmacro-prefix-map=OLD_PATH=NEW_PATH to to get the right file paths, e.g. for meson:
add_global_arguments('-fmacro-prefix-map=../=',language: 'c')See also: Configuration Header feature.
The full list of build options for static configuration is shown bellow:
| Build Option | Default | Purpose |
|---|---|---|
| ULOG_BUILD_COLOR | 0 | Compile color code paths |
| ULOG_BUILD_PREFIX_SIZE | 0 | Prefix buffer logic |
| ULOG_BUILD_EXTRA_OUTPUTS | 0 | Extra output backends |
| ULOG_BUILD_SOURCE_LOCATION | 1 | File:line output |
| ULOG_BUILD_LEVEL_SHORT | 0 | Print levels with short names, e.g. 'E' |
| ULOG_BUILD_TIME | 0 | Timestamp support |
| ULOG_BUILD_TOPICS_MODE | ULOG_BUILD_TOPICS_MODE_OFF | Topic allocation mode |
| ULOG_BUILD_TOPICS_STATIC_NUM | 0 | Number of static topics (0 = disabled) |
| ULOG_BUILD_DYNAMIC_CONFIG | 0 | Runtime toggles |
| ULOG_BUILD_WARN_NOT_ENABLED | 1 | Warning stubs |
| ULOG_BUILD_CONFIG_HEADER_ENABLED | 0 | Use external configuration header |
| ULOG_BUILD_CONFIG_HEADER_NAME | "ulog_config.h" | Configuration header name |
| ULOG_BUILD_DISABLED | 0 | Disable microlog completely |
WARNING! Do not use ULOG_BUILD_* options with a precompiled microlog library. Use dynamic configuration instead.
There are 8 log severity levels (by ascending severity): ULOG_LEVEL_0 ... ULOG_LEVEL7. To log a message there is a general macro:
ulog(ulog_level level, const char *fmt, ...);By default these generic levels are aliased with:
ULOG_LEVEL_TRACE- for tracing the execution path (ULOG_LEVEL_0)ULOG_LEVEL_DEBUG- for debug information (ULOG_LEVEL_1)ULOG_LEVEL_INFO- for general information (ULOG_LEVEL_2)ULOG_LEVEL_WARN- for important information (ULOG_LEVEL_3)ULOG_LEVEL_ERROR- for information about recoverable errors (ULOG_LEVEL_4)ULOG_LEVEL_FATAL- for information about condition causing the system failure (ULOG_LEVEL_5)- Leveles
ULOG_LEVEL_6andULOG_LEVEL_7are not used by default
The library provides also default level macros for logging:
ulog_trace(const char *fmt, ...);
ulog_debug(const char *fmt, ...);
ulog_info(const char *fmt, ...);
ulog_warn(const char *fmt, ...);
ulog_error(const char *fmt, ...);
ulog_fatal(const char *fmt, ...);Each function takes a printf format string followed by additional arguments:
ulog_info("Info message %f", 3.0)The user can also define custom levels by using the ulog_level_set_new_levels(ulog_level_descriptor *levels) function. The default levels can be restored by calling ulog_level_reset_levels(). E.g.
static ulog_level_descriptor syslog_levels = {
.max_level = ULOG_LEVEL_7, // allow 0..7
.names = {"DEBUG", "INFO", "NOTICE", "WARN", "ERR", "CRIT", "ALERT", "EMERG"},
};
#define LOG_NOTICE ULOG_LEVEL_2
// ...
ulog_level_set_new_levels(&syslog_levels);
ulog(LOG_NOTICE, "This is a notice message");
// Output: NOTICE src/main.c:12: This is a notice message
ulog_level_reset_levels();
ulog(ULOG_LEVEL_2, "This is a default level message");
// Output: INFO src/main.c:14: This is a default level message
The default log level is ULOG_LEVEL_TRACE, such that nothing is ignored. And by default there is only one available output - stdout. To configure its severity the user can use these two functions:
ulog_output_level_set(ULOG_OUTPUT_STDOUT, ULOG_LEVEL_INFO);
// or
ulog_output_level_set_all(ULOG_LEVEL_TRACE);In this case the stdout-printed line will be:
INFO src/main.c:66: Info message 3.000000The events care information depending on the static configuration. The whole list of possible data:
- Message
- Message format arguments
- Topic
- Time
- File
- Line
- Level
The data is accessible via getters (see header file for details):
ulog_event_get_message(...)ulog_event_get_topic(...)ulog_event_get_time(...)ulog_event_get_file(...)ulog_event_get_line(...)ulog_event_get_level(...)
If the log will be written to from multiple threads a lock function can be set. To do this use the ulog_lock_set_fn() function.
The lock function must match the ulog_lock_fn type and return non-ULOG_STATUS_OK value on error:
typedef ulog_status (*ulog_lock_fn)(bool lock, void *udata);The function is passed the boolean true if the lock should be acquired or false if the lock should be released and the given udata value.
ulog_status lock_function(bool lock, void *lock_arg) {
pthread_mutex_t *mutex = (pthread_mutex_t *) lock_arg; // retrieve the mutex
int result = -1;
if (lock) {
result = pthread_mutex_lock(mutex);
} else {
result = pthread_mutex_unlock(mutex);
}
return (result == 0) ? ULOG_STATUS_OK : ULOG_STATUS_ERROR;
}
. . .
pthread_mutex_t mutex; // We pass the mutex as the lock_arg
pthread_mutex_init(&mutex, NULL);
. . .
ulog_lock_set_fn(lock_function, mutex);For platform-specific convenience helpers (pthread, Windows, FreeRTOS, ThreadX, Zephyr, CMSIS‑RTOS2, macOS unfair lock) and the syslog level extension, see extensions/README.md.
If the library is used with dynamic features (e.g. topics, extra outputs, dynamic configuration) it is recommended to call the ulog_cleanup() function before program exit to free all allocated resources.
. . .
ulog_cleanup();The clean up can be also used to remove all topics and outputs if needed during the program execution even if the allocation mode is static.
- Static configuration options:
ULOG_BUILD_DISABLED - Values (bool):
0/1 - Default:
0.
NOTE: ULOG_BUILD_DISABLED=1 overrides all other ULOG_BUILD_* feature flags.
This feature allows disabling all logging calls at compile time for zero-overhead logging. To enable this feature, define ULOG_BUILD_DISABLED=1 in the compiler options.
When the feature is enabled, all logging macros are replaced with ((void)0) and function calls return disabled status codes. Important: Logging macros become true no-ops - their arguments are not evaluated, providing complete zero-overhead when disabled.
Example:
int expensive_calculation() { /* ... */ }
// When ULOG_BUILD_DISABLED=1:
ulog_info("Result: %d", expensive_calculation()); // expensive_calculation() is NOT calledWhen the feature is enabled all logging macros are replaced with empty stubs or return negative status codes:
| Function | Return Value When Disabled |
|---|---|
| ulog_cleanup | ULOG_STATUS_DISABLED |
| ulog_color_config | ULOG_STATUS_DISABLED |
| ulog_event_get_file | "" |
| ulog_event_get_level | ULOG_LEVEL_0 |
| ulog_event_get_line | -1 |
| ulog_event_get_message | ULOG_STATUS_DISABLED |
| ulog_event_get_time | NULL |
| ulog_event_get_topic | ULOG_TOPIC_ID_INVALID |
| ulog_event_to_cstr | ULOG_STATUS_DISABLED |
| ulog_level_config | ULOG_STATUS_DISABLED |
| ulog_level_reset_levels | ULOG_STATUS_DISABLED |
| ulog_level_set_new_levels | ULOG_STATUS_DISABLED |
| ulog_level_to_string | "?" |
| ulog_lock_set_fn | ULOG_STATUS_DISABLED |
| ulog_log | (void)0 |
| ulog_output_add | ULOG_OUTPUT_INVALID |
| ulog_output_add_file | ULOG_OUTPUT_INVALID |
| ulog_output_level_set | ULOG_STATUS_DISABLED |
| ulog_output_level_set_all | ULOG_STATUS_DISABLED |
| ulog_output_remove | ULOG_STATUS_DISABLED |
| ulog_prefix_config | ULOG_STATUS_DISABLED |
| ulog_prefix_set_fn | ULOG_STATUS_DISABLED |
| ulog_source_location_config | ULOG_STATUS_DISABLED |
| ulog_time_config | ULOG_STATUS_DISABLED |
| ulog_topic_add | ULOG_TOPIC_ID_INVALID |
| ulog_topic_config | ULOG_STATUS_DISABLED |
| ulog_topic_get_id | ULOG_TOPIC_ID_INVALID |
| ulog_topic_level_set | ULOG_STATUS_DISABLED |
| ulog_topic_remove | ULOG_STATUS_DISABLED |
- Static configuration options:
ULOG_BUILD_CONFIG_HEADER_ENABLED,ULOG_BUILD_CONFIG_HEADER_NAME - Values (bool, string):
0/1,ANY - Default:
0,"ulog_config.h"
As an alternative to defining build options individually via compiler flags, you can define ULOG_BUILD_CONFIG_HEADER_ENABLED=1 to include a single header file named ulog_config.h that contains all configuration options. This approach simplifies configuration management by centralizing all build options in one file.
When ULOG_BUILD_CONFIG_HEADER_ENABLED is defined:
- The library will include
ulog_config.h- You can overwrite it with
ULOG_BUILD_CONFIG_HEADER_NAME="my_ulog_conf.h"
- You can overwrite it with
- All other
ULOG_BUILD_*macros must be defined in this header file - Defining other
ULOG_BUILD_*macros via compiler flags will cause a compilation error
Example ulog_config.h:
#pragma once
// Define all build options in one place
#define ULOG_BUILD_COLOR 1
#define ULOG_BUILD_PREFIX_SIZE 64
#define ULOG_BUILD_EXTRA_OUTPUTS 8
#define ULOG_BUILD_SOURCE_LOCATION 1
#define ULOG_BUILD_LEVEL_SHORT 0
#define ULOG_BUILD_TIME 1
#define ULOG_BUILD_TOPICS_MODE ULOG_BUILD_TOPICS_MODE_STATIC
#define ULOG_BUILD_TOPICS_STATIC_NUM 10
#define ULOG_BUILD_DYNAMIC_CONFIG 0
#define ULOG_BUILD_WARN_NOT_ENABLED 1Usage with CMake:
target_compile_definitions(microlog PRIVATE ULOG_BUILD_CONFIG_HEADER_ENABLED=1)
target_include_directories(microlog PRIVATE path/to/config/directory)This approach is particularly useful when you have multiple configurations or want to keep configuration separate from build scripts.
- Static configuration options:
ULOG_BUILD_TOPICS_MODE,ULOG_BUILD_TOPICS_STATIC_NUM - Values (enum, int):
ULOG_BUILD_TOPICS_MODE_OFF,ULOG_BUILD_TOPICS_MODE_STATIC,ULOG_BUILD_TOPICS_MODE_DYNAMIC,0...UINT_MAX - Default:
ULOG_BUILD_TOPICS_MODE_OFF,0.
The feature is controlled by ULOG_BUILD_TOPICS_MODE. It allows to filter log messages by subsystems, e.g. "network", "storage", etc. Use ULOG_BUILD_TOPICS_MODE_STATIC with ULOG_BUILD_TOPICS_STATIC_NUM for a fixed number of topics, or ULOG_BUILD_TOPICS_MODE_DYNAMIC for runtime allocation.
There are two mechanism of working with the topics:
- Dynamic allocation - slightly slower than static allocation
- Static allocation - faster
If you want to use dynamic topics, set ULOG_BUILD_TOPICS_MODE to ULOG_BUILD_TOPICS_MODE_DYNAMIC. For static allocation set ULOG_BUILD_TOPICS_MODE to ULOG_BUILD_TOPICS_MODE_STATIC and define ULOG_BUILD_TOPICS_STATIC_NUM to the desired number of topics.
Printing the log message with the topic is done by the set of function-like macros similar to ulog_xxx, but with the topic as the first argument:
ulog_topic_log(ulog_level level, const char *topic_name, const char *fmt, ...);
ulog_topic_trace(const char *topic_name, const char *fmt, ...);
ulog_topic_debug(const char *topic_name, const char *fmt, ...);
ulog_topic_info(const char *topic_name, const char *fmt, ...);
ulog_topic_warn(const char *topic_name, const char *fmt, ...);
ulog_topic_error(const char *topic_name, const char *fmt, ...);
ulog_topic_fatal(const char *topic_name, const char *fmt, ...);
// or short versions:
ulog_t(ulog_level level, const char *topic_name, const char *fmt, ...);
ulog_t_trace(const char *topic_name, const char *fmt, ...);
ulog_t_debug(const char *topic_name, const char *fmt, ...);
ulog_t_info(const char *topic_name, const char *fmt, ...);
ulog_t_warn(const char *topic_name, const char *fmt, ...);
ulog_t_error(const char *topic_name, const char *fmt, ...);
ulog_t_fatal(const char *topic_name, const char *fmt, ...);All topic must be added before usage via ulog_topic_add. At this step it is possible to select particular output for the topic (e.g. print topic Credentials only to a local file, or Operator Info only to ULOG_OUTPUT_STDOUT)
For example (static topics):
ulog_topic_add("network", ULOG_OUTPUT_ALL, ULOG_LEVEL_TRACE); // Added and initialized to TRACE level
ulog_topic_add("storage", ULOG_OUTPUT_ALL, ULOG_LEVEL_INFO); // Added and initialized to INFO level
ulog_topic_info("network", "Connected to server");
ulog_topic_warn("storage", "No free space");or dynamic topics:
// by default all topics are disabled
ulog_topic_enable("storage");
ulog_topic_error("storage", "No free space");
ulog_topic_enable_all();
ulog_topic_trace("network", "Disconnected from server");
ulog_topic_fatal("video", "No signal");The logging level of each topic is set to the value given as parameter. It is possible to alter this behavior by calling ulog_topic_level_set(). All topics below the level set by ulog_output_level_set() will not generate log.
For example:
// Both topic logging levels are set to ULOG_LEVEL_TRACE
ulog_topic_add("network", ULOG_OUTPUT_ALL, ULOG_LEVEL_TRACE);
ulog_topic_add("storage", ULOG_OUTPUT_ALL, ULOG_LEVEL_TRACE);
// Both topics generate log as global logging level is set to ULOG_LEVEL_TRACE
ulog_topic_info("network", "Connected to server");
ulog_topic_warn("storage", "No free space");
ulog_output_level_set_all(ULOG_LEVEL_INFO); // All outputs are set to INFO
ulog_topic_level_set("storage", ULOG_LEVEL_WARN); // Storage is set to WARN
ulog_topic_info("storage", "No free space"); // generated
ulog_topic_info("network", "Connected to server"); // filtered out topic
ulog_topic_debug("storage", "No free space"); // filtered out level DEBUG < INFOTopics can be removed by using the ulog_topic_remove() function.
- Static configuration options:
ULOG_BUILD_EXTRA_OUTPUTS - Values (int):
0...INT32_MAX - Default:
0.
The feature is controlled by the following define:
ULOG_BUILD_EXTRA_OUTPUTS- The maximum number of extra logging outputs that can be added. Each extra output requires some memory. When it is 0, the only available output is STDOUT. Default is 0.
One or more file pointers where the log will be written can be provided to the library by using the ulog_output_add_file() function. The data written to the file output is of the following format (with the full time stamp):
2047-03-11 20:18:26 TRACE src/main.c:11: Hello world
vs
20:18:26 TRACE src/main.c:11: Hello worldTo write to a file open a file and pass it to the ulog_output_add_file function.
ulog_topic_add("Outputs", ULOG_OUTPUT_ALL, ULOG_LEVEL_TRACE);
FILE *fp = fopen("log.txt", "w");
if (fp) {
ulog_output_id file_output = ulog_output_add_file(fp, ULOG_LEVEL_INFO);
if (file_output != ULOG_OUTPUT_INVALID) {
ulog_topic_info("Outputs", "File output added");
ulog_output_level_set(file_output, ULOG_LEVEL_TRACE);
ulog_topic_trace("Outputs", "File output level set to TRACE");
}
ulog_output_remove(file_output); // For demo purposes
fclose(fp); // For demo purposes
}Outputs can be removed by using the ulog_output_remove() function.
One or more output handler functions which are called with the log data can be provided to the library by using the ulog_output_add() function. You can use ulog_event_to_cstr to convert the ulog_event structure to a string.
void arduino_output_handler(ulog_event *ev, void *arg) {
static char buffer[128];
int result = ulog_event_to_cstr(ev, buffer, sizeof(buffer));
if (result == 0) {
Serial.println(buffer);
}
}
. . .
ulog_output_id ard_output = ulog_output_add(arduino_output_handler, NULL, ULOG_LEVEL_INFO);
if (ard_output != ULOG_OUTPUT_INVALID) {
ulog_info("Will be printed to Arduino serial");
ulog_output_remove(ard_output); // For demo purposes
}WARNING: The handler function is called with the lock acquired, so if you are using logging inside the handler, it may cause a deadlocks: e.g.
void faulty_output_handler(ulog_event *ev, void *arg) {
ulog_info("This might cause a deadlock or unexpected behavior");
}- Static configuration options:
ULOG_BUILD_PREFIX_SIZE - Values (int):
0...INT_MAX - Default:
0.
Sets a prefix function that can be used to customize the log output. The function is called with the log event and should fill a string (prefix) that will be printed right before the log level. It can be used to add custom data to the log messages, e.g. millisecond time.
Requires ULOG_BUILD_PREFIX_SIZE to be more than 0.
void prefix_handler(ulog_event *ev, char *prefix, size_t prefix_size) {
snprintf(prefix, prefix_size, ", %03d ms", millis());
}
// . . .
ulog_prefix_set_fn(prefix_handler);The output will be:
19:51:42, 005 ms ERROR src/main.c:38: Error messageWARNING: The handler function is called with the lock acquired, so if you are using logging inside the handler, it may cause a deadlocks. E.g.:
void faulty_prefix(ulog_event *ev, char *prefix, size_t prefix_size) {
ulog_info("This might cause a deadlock or unexpected behavior");
}- Static configuration options:
ULOG_BUILD_TIME - Values (bool):
0/1 - Default:
0.
Prints a time stamp in from of all log messages. Your platform must support time.h.
The time to the file output will be written with the date, while time to the console and other outputs will be written with the time only.
log.txt:
2021-03-11 20:18:26 TRACE src/main.c:11: Hello world
console:
20:18:26 TRACE src/main.c:11: Hello world- Static configuration options:
ULOG_BUILD_COLOR - Values (bool):
0/1 - Default:
0.
Use ANSI color escape codes when printing to stdout. If the terminal supports, the output will be colorized.
- TRACE - white
- DEBUG - cyan
- INFO - green
- WARN - yellow
- ERROR - red
- FATAL - red on white background
- UNUSED levels (6,7) - yellow on red and white on red background
- Static configuration options:
ULOG_BUILD_SOURCE_LOCATION - Values (bool):
0/1 - Default:
1.
Hide or show the file name and line number. See output examples below:
- ULOG_BUILD_SOURCE_LOCATION=0:
TRACE Hello world - ULOG_BUILD_SOURCE_LOCATION=1:
TRACE src/main.c:11: Hello world
- Static configuration options:
ULOG_BUILD_LEVEL_SHORT - Values (bool):
0/1 - Default:
0.
Allows to use short level strings, e.g. "T" for "TRACE", "I" for "INFO":
- ULOG_BUILD_LEVEL_SHORT=0:
TRACE src/main.c:11: Hello world - ULOG_BUILD_LEVEL_SHORT=1:
T src/main.c:11: Hello world
- Static configuration options:
ULOG_BUILD_DYNAMIC_CONFIG - Values (bool):
0/1 - Default:
0.
Most of the library features are configured compile time to reduce the code size and complexity. However, if the code size is not a concern, you can enable Dynamic Config by defining ULOG_BUILD_DYNAMIC_CONFIG=1. When the feature is enables all other features are enabled too in some default mode described in bellow. All Dynamic Config functions named like: ulog_FEATURE_config. The default configuration is following:
| Build Config | Default Value |
|---|---|
| ULOG_BUILD_PREFIX_SIZE | 64 |
| ULOG_BUILD_EXTRA_OUTPUTS | 8 |
| ULOG_BUILD_TIME | 1 |
| ULOG_BUILD_SOURCE_LOCATION | 1 |
| ULOG_BUILD_COLOR | 1 |
| ULOG_BUILD_LEVEL_SHORT | 0 |
| ULOG_BUILD_TOPICS_MODE | ULOG_BUILD_TOPICS_MODE_DYNAMIC |
| ULOG_BUILD_WARN_NOT_ENABLED | 0 |
If Dynamic Config enabled topics are created runtime in the dynamic allocation mode.
Configuration functions:
ulog_status ulog_topic_config(bool enabled)- show or hide topics in the log output when usingulog_topic_xxxmacros. ReturnsULOG_STATUS_OKon success,ULOG_STATUS_BUSYif the logger is currently locked, orULOG_STATUS_ERRORif the feature is not compiled in.
Example output with and without topics:
ulog_topic_info("WORLD", "Hello");- enabled=true:
INFO [WORLD] src/main.c:13: Hello - enabled=false:
INFO src/main.c:13: Hello
If Dynamic Config enabled, ULOG_BUILD_PREFIX_SIZE is set to 64, so the prefix will be limited to: 63 characters + 1 null terminator.
Functions to configure the prefix:
ulog_status ulog_prefix_config(bool enabled)- enable or disable prefix in the log output. ReturnsULOG_STATUS_OK/ULOG_STATUS_BUSY/ULOG_STATUS_ERROR(feature disabled).
Functions to configure the timestamp:
ulog_status ulog_time_config(bool enabled)- enable or disable time in the log output. ReturnsULOG_STATUS_OK/ULOG_STATUS_BUSY/ULOG_STATUS_ERROR(feature disabled).
Functions to configure:
ulog_status ulog_color_config(bool enabled)- enable or disable ANSI color escape codes when printing to stdout. ReturnsULOG_STATUS_OK/ULOG_STATUS_BUSY/ULOG_STATUS_ERROR(feature disabled).
Functions to configure:
ulog_status ulog_source_location_config(bool enabled)- show or hide file and line in the log output. ReturnsULOG_STATUS_OK/ULOG_STATUS_BUSY/ULOG_STATUS_ERROR(feature disabled).
Functions to configure:
ulog_status ulog_level_config(ulog_level_config_style style)- enable or disable short level strings (e.g. "T" for "TRACE", "I" for "INFO"). ReturnsULOG_STATUS_OK/ULOG_STATUS_BUSY/ULOG_STATUS_ERROR(feature disabled).
The style argument can be one of the following values:
ULOG_LEVEL_CONFIG_STYLE_DEFAULT: long level strings (default)ULOG_LEVEL_CONFIG_STYLE_SHORT: short level strings
