Devlog #1: Logger implementation (Commit b565bfd1f7)
6 changed files with 594 additions and 9 deletions
The first core module I implemented is the Logger. I feel like I spent a huge amount of time on this, but it was well worth it, as it’s a marvel. Below you can read about the engineering of the logger.
The engineering of the Logger
This is a quick overview, for the full context you can check out the source code.
Multi-Stage Gating (Compile-Time + Runtime)
The logger uses a two-tier filtering system.
-
Compile-Time: Using C23 variadic macros and
__VA_OPT__, the logger identifies the build type. In areleasebuild,TRACEandDEBUGcalls are completely stripped from the binary. They don’t just “not print”; they don’t exist in the final machine code. - Runtime: Even for levels that survive the preprocessor, the logger checks the internal state before doing any work. This ensures that there is no wasted time on string formatting or timestamping.
Thread-Safe Singleton Architecture
While zinn doesn’t use multithreading yet, I felt like it’s best to “future-proof” the Logger by guarding everything with pthread. This ensures, that whenever (or if at all) I add multithreading, the stream output will be clean and not a garbled mess of characters.
Runtime stream redirection
Again, this isn’t used currently, so this is another “future-proofing” measure. The logger can seamlessly start writing into any stream desired, as long as it exists. This will be used in the future, but no spoilers as to what will use it ;)
Modern C23 & POSIX Integration
-
Precision Timing: Using C23
timespec_getandlocaltime_rfor nanosecond-precision timestamps with thread-safe formatting. -
Compiler Safety: The logging function is marked with
[[gnu::format(printf, x, y)]], allowing the compiler to catch type-mismatches in log messages at build-time. -
Deterministic Memory: Zero
malloc. All formatting happens in a fixed 1024-byte stack buffer. This also prevents nasty, difficult-to-catch memory bugs, which could be fatal in a core module such as the logger.
Screenshots explanation
1st screenshot
This screenshot perfectly demonstrates the preprocessor filtering. Notice the difference between the release and debug runs. In release, the TRACE log is vanished. In debug, the logger is fully verbose. This confirms that the compile-time filtering is working.
2nd screenshot
Showcase of multiple severities, as well as the FATAL one. Notice the Return code: 2 at the bottom. This proves that the logger successfully captured the error, flushed the stream, and triggered a clean abort() to prevent further corruption.
Ignore the makefile error, I suspect it occurs when I run
make run, and the binary exits abnormally (e.g. return code != 0), it thinks it’s an error.
3rd screenshot
Compared the the previous screenshot (right above), you can see the TRACE log level appears. This didn’t show in the previous screenshot as the minimum log level was higher than TRACE, thus showing the minimum log level works.
4th screenshot
Here you can see how the code looked for the previous two screenshots. In the init function (zinn_log_init), you pass the minimum log level as the parameter, so when it was ZINN_LOG_TRACE, it logged all the severities. When it was something higher, it didn’t log TRACE, for example.
If you were to take a look at the code on the repository, you might notice that it is a bit different. That is due to this screenshot being a bit old, and I’ve went through like 2 hours of improving the Logger:)
Future plans
With the first core module done, the only correct next step is to implement another core feature.
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.