Logger is a lightweight and portable logging component written in C99, with some preprocessor utilities (still C99-compliant).
Features:
- Supports
printf-compliant format syntax - Built-in format syntax checking (GNU compilers)
- Unlimited number of compile-time channels
- SYSLOG-compatible verbosity levels
- Runtime output stream selection
- Thread-safety hooks available
The library consists of two parts: compile-time channel/verbosity selection and runtime switches.
For each channel you may specify the maximum verbosity level that will be compiled — messages with higher verbosity are optimized out from the build, reducing memory requirements.
include(FetchContent)
FetchContent_Declare(
logger
GIT_REPOSITORY https://github.com/embetech-official/logger.git
GIT_TAG main
)
FetchContent_MakeAvailable(logger)Add the channel definition at the beginning of your source file:
#include <embetech/logger.h>
// your code starts here ...
void example_function(void) {
LOGGER_INFO("eat veggies");
LOGGER_WARNING("... or else 3:> ");
}This will produce the following log messages:
DEFAULT (I): eat veggies
DEFAULT (W): ... or else 3:>You can assign your source file to a compile-time Log Channel to enable precise verbosity configuration. The Log Channel will be visible in the message header:
#define LOGGER_CHANNEL FOOBAR
#include <embetech/logger.h>
// your code starts here ...
void example_function(void) {
LOGGER_INFO("eating veggies is fun");
}This will result in the following log message:
FOOBAR (I): eating veggies is funLogger can control which messages are compiled into your binary. You control this by setting each channel's verbosity. If you don't specify a verbosity for a channel, it remains disabled.
Each channel's compiled verbosity (<CHANNEL>_LOG_CHANNEL_LEVEL) is set per build configuration from CMake using logger_set_max_level — see CMake Integration below. Alternatively, you may define <CHANNEL>_LOG_CHANNEL_LEVEL directly (e.g. via target_compile_definitions or a compiler -D flag). If you don't specify a verbosity for a channel, it remains disabled.
To stay flexible and minimalistic, the logger uses compile-time configuration options that affect global behavior. The list below reflects options available in the CMakeLists.txt file:
If enabled, the logger header contains a timestamp acquired from a user-defined callback via LOGGER_SetTimeSource:
LOGGER_SetTimeSource([](){return std::uint32_t(1);}); // Short C++ lambda
LOGGER_INFO("test message");Printed as (assuming DEFAULT channel):
1 DEFAULT (I): test messageAvailable only when LOGGER_TIMESTAMPS is ON.
Changes the timestamp format to hh:mm:ss.ms:
02:13:20.085 DEFAULT (I): test messageAllows setting a custom prefix/suffix for every logger message using LOGGER_SetPrefix/LOGGER_SetSuffix.
Both are printed as binary data, so no '\0' termination is required.
Useful when working with custom terminal protocols.
LOGGER_SetPrefix("pre- ", 5U);
LOGGER_SetSuffix(" -post", 6U);
LOGGER_NOTICE("test message1");Produces:
pre- DEFAULT (N): test message -postEnables reducing verbosity of printed messages at runtime using LOGGER_SetRuntimeLevel. Compile-time level still applies, so the resulting set of messages is bounded by both:
LOGGER_NOTICE("test message1");
LOGGER_SetRuntimeLevel(LOGGER_LEVEL_WARNING);
LOGGER_NOTICE("test message2");
LOGGER_SetRuntimeLevel(LOGGER_LEVEL_DEBUG);
LOGGER_NOTICE("test message3");Produces:
DEFAULT (N): test message1
DEFAULT (N): test message3The second message is not printed; however, its code remains available to be enabled (assuming that DEFAULT_LOG_CHANNEL_LEVEL is at least NOTICE).
Expands the header with code location [file:line]:
DEFAULT (N) [x:\long_path\file.cpp:66]: test messageSince file paths might be long, a CMake utility can override compiler-generated file names with shorter versions:
# assuming the logger library is already found
include(logger_utils)
logger_normalize_printable_filenames()This changes the above message to:
DEFAULT (N) [file.cpp:66]: test messageBy design, the logger is as thread-safe as your output callback (often: not). When this option is enabled, the logger uses user-provided lock/unlock functions to ensure thread safety. Register lock/unlock callbacks:
static Mutex_t myMutex;
bool myLockFunction(void* context) {
int const customTimeoutMS = 100; // If your mutex requires a timeout
int const mutexSuccess = 69; // Assuming that Mutex_Take returns 69 on success
Mutex_t* mutex = (Mutex_t*)context;
return mutexSuccess == Mutex_Take(mutex, customTimeoutMS);
}
void myUnlockFunction(void* context) {
Mutex_t* mutex = (Mutex_t*)context;
Mutex_Give(mutex);
}
// ...
LOGGER_SetLockingMechanism(myLockFunction, myUnlockFunction, &myMutex);The callback API is universal — you can often plug your OS's mutex functions.
The logger always tries to lock before printing a message. In scoped printing, the lock is acquired by LOGGER_START and released by LOGGER_END/LOGGER_ENDL.
Enables binding a function to flush the output. The function may be called explicitly using LOGGER_Flush(), or automatically at the end of each message (i.e., every LOGGER_INFO/etc., or after each LOGGER_END/LOGGER_ENDL):
LOGGER_DisableHeader();
LOGGER_NOTICE("marco?");
LOGGER_SetFlushHook([](){puts("polo!");}, false); // Logger will flush when asked to
LOGGER_NOTICE("marco??");
LOGGER_NOTICE("marco???");
LOGGER_Flush();
LOGGER_SetFlushHook([](){puts("polo!!");}, true); // now logger will flush automatically
LOGGER_NOTICE("marco!");Would print:
marco?
marco??
marco???
polo!
marco!
polo!!When a user makes a mistake, preprocessor errors can be hard to parse. If enabled, and your compiler is either Clang or GCC-like, each compile-time error is appended with information indicating which logger channel caused the issue.
Set a channel's compiled verbosity per build configuration from CMake:
logger_set_max_level(
my_target
CHANNEL COMPONENT1
CONFIG "Debug:DEBUG" "Release:WARNING"
)CMake cache variable, default: DISABLED.
Level used for any build configuration that is not listed in a logger_set_max_level() CONFIG list (e.g. you only specified Debug/Release but the project — or a multi-config generator — also builds RelWithDebInfo/MinSizeRel). Without this fallback, such a build configuration would compile with no valid channel level at all.
Set it once to change the fallback for the whole project/CI:
cmake -B build -DLOGGER_DEFAULT_MAX_LEVEL=INFOMust be one of the verbosity levels listed above (DISABLED, EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, VERBOSE, DEBUG, TRACE).
To make the logger print anything, you must provide the output function callback first:
void out(char c, void* context) {
(void)context; // unused parameter
putchar(c);
}
LOGGER_SetOutput(out, NULL);You may provide a context pointer for your function; it is stored until the next invocation of LOGGER_SetOutput.
Next, enable the logger:
assert(LOGGER_Enable()); // The function returns whether the logger was enabledThere are two intended ways to produce log messages. Both are presented below:
void example_function(void) {
LOGGER_TRACE("Trace message. Rarely enabled");
LOGGER_DEBUG("Entering function in " __FILE__ ":%d", __LINE__);
LOGGER_VERBOSE("Attempting to start to defuse the nuke");
LOGGER_NOTICE("Nuke defusing started");
LOGGER_WARNING("The Apple authorized service might not be the best place to fix the bomb");
LOGGER_ERROR("Screwdriver broken");
LOGGER_CRITICAL("Nuke warranty voided. Reason: liquid damage");
LOGGER_ALERT("Nuke will blow in %d seconds", 3);
LOGGER_EMERGENCY("Goodbye cruel world");
LOGGER_DISABLED("There is no bright light at the end of the tunnel"); // Actually useful, when you want to disable some low level debugging messages in low latency code, and not comment it out (remember: commented-out code is a bad code)
LOGGER_START(NOTICE);
int trainingMsgNo = 42; // Scope reduced to LOGGER_START/LOGGER_ENDL
LOGGER_CONTINUE("\n --- This was a training message no %d: \n", trainingMsgNo);
LOGGER_CONTINUE("%s\n", getPrintableMsgNo(trainingMsgNo));
LOGGER_CONTINUE("you may log as much information as you wish.\n");
LOGGER_CONTINUE("log channel between LOGGER_START and LOGGER_END is locked");
LOGGER_CONTINUE("(assuming you implemented a locking mechanism)");
LOGGER_ENDL();
LOGGER_NOTICE("bananas.")
}This produces the following output:
DEFAULT (T): Trace message. Rarely enabled
DEFAULT (D): Entering function in ./example.c:499
DEFAULT (V): Attempting to start to defuse the nuke
DEFAULT (N): Nuke defusing started
DEFAULT (W): The Apple authorized service might not be the best place to fix the bomb
DEFAULT (E): Screwdriver broken
DEFAULT (C): Nuke warranty voided. Reason: liquid damage
DEFAULT (A): Nuke will blow in 3 seconds
DEFAULT (M): Goodbye cruel world
DEFAULT (N):
--- This was a training message no 42:
foo
you may log as many information, as you wish.
log channel between LOGGER_START and LOGGER_END is locked(assuming you implemented locking mechanism)
DEFAULT (N): bananasYou can disable/enable printing the header using LOGGER_EnableHeader / LOGGER_DisableHeader.