Making your own logging library in C/C++ is pretty easy

I was recently working on my C++ application Miscible and needed some pretty (or just informative) logs. I needed a few things from it:

  • It should have some visual difference based on the type of log.
  • It should give me enough information about the location it originated from (If required).
  • It should support printing an additional message along with printf like string formatting.

And, after slowly re-iterating, I have reached at making this

The best part, it's all do-able within 30 LINES OF C compatible single header!!

Screenshot

Now, let's build this piece by piece

1. Basic printing

The most basic type of logger begins with just using printf wherever you want to log some information (printf debugging xD). So, let's just wrap printf into our custom C macro and just put it in our code wherever we want to log something.

logger.h

#pragma once            // So compilers don't annoy about macro redifinitions
#include <stdio.h>      // Optional, but this makes sure we can use printf wherever use the logger
 
#define mscbl_log(fmt, ...) printf(fmt, __VA_ARGS__)

main.c

#include "logger.h"
 
int main()
{
    // ...
    mscbl_log("(Error): return code=%d\n", res);
    mscbl_log("(Warning): return code=%d\n", res);
    mscbl_log("(Debug): return code=%d\n", res);
    // ...
}

2. Adding some flags

The above example is usually 'good enough' for most of the dummy/test/proof-of-concept code examples, but not for anything larger than that. The problem is that it requires a lot of manual and repetitive typing anytime we want to use it. In the previous example, the only 'real' log parameter was the res variable (and maybe some words to accompany it) but to make sure it's actully informative, we had to type in lots of unnecessary repetitive text like the log type (Error/Warning/Debug) and the newline \n. It's pretty easy to forget the newline which can mess up the output entirely. We need something to produce this kind of output [($type of log)][User message with formatting][\n] for which we need to put some text before and after our log information and thankfully C supports string concatenation by default and it's pretty simple.

In C, 2 or more string literals can be concatenated simply by writing them one after another. Writing "Hello World\n" is same as writing "Hello" " " "World" "\n" and "H" "e" "ll" "o" " World\n" (or any other combination of string splits) and thankfully, the fmt parameter in a printf is also a string literal. So, let's use this to our advantage.

logger.h

#pragma once
#include <stdio.h>
 
#define mscbl_log_error(fmt, ...) printf("(Error): " fmt "\n", __VA_ARGS__)
#define mscbl_log_warn(fmt, ...)  printf("(Warning): " fmt "\n", __VA_ARGS__)
#define mscbl_log_dbg(fmt, ...)   printf("(Debug): " fmt "\n", __VA_ARGS__)

main.c

#include "logger.h"
 
int main()
{
    // ...
    mscbl_log_error("return code=%d", res);
    mscbl_log_warn("return code=%d", res);
    mscbl_log_dbg("return code=%d", res);
    // ...
}

3. Adding location macros

This make it much easier to write logging statements and reduces chances of errors even furthur. Now, it's time to add some information about log location in the code. All C/C++ compilers define some Standard Predefined Macros. Commonly, loggers make of these macros:

  • __FILE__: compile time string literal containing the full path of the file as seen by the compiler.
  • __LINE__: compile time integer value telling the line number where macro was called.
  • __func__: not a compile time macro, but contains the string of the current function.

logger.h

#pragma once
#include <stdio.h>
 
#define mscbl_log_error(fmt, ...) printf("(Error)[" __FILE__ ":%s]: " fmt "\n", __VA_ARGS__)
#define mscbl_log_warn(fmt, ...)  printf("(Warning)[" __FILE__ ":%s]: " fmt "\n", __VA_ARGS__)
#define mscbl_log_dbg(fmt, ...)   printf("(Debug)[" __FILE__ ":%s]: " fmt "\n", __VA_ARGS__)

main.c

#include "logger.h"
 
int main()
{
    // ...
    mscbl_log_error("return code=%d", res);
    mscbl_log_warn("return code=%d", res);
    mscbl_log_dbg("return code=%d", res);
    // ...
}

I have omitted the __LINE__ macro since filename and function name usually make it obvious when looking for the source of logging statement, however you can choose to use the __LINE__ macro as well.

Here's the final code

#pragma once
#include <stdio.h>
#include <time.h>
 
#define ANSI_RESET  "\x1b[0m"
#define ANSI_BOLD   "\x1b[1m"
#define ANSI_RED    "\x1b[31m"
#define ANSI_YELLOW "\x1b[33m"
#define ANSI_CYAN   "\x1b[38;2;0;205;255m"
#define ANSI_GREEN  "\x1b[38;2;119;255;0m"
#define ANSI_PINK   "\x1b[38;2;255;0;154m"
#define ANSI_WHITE  "\x1b[37m"
 
#define log_build_path_len sizeof(__FILE__) - sizeof("src/base/log.h")
#define mscbl_log_error(fmt, ...) printf(ANSI_RED "[%s]" ANSI_PINK "[%s] " ANSI_RESET fmt "\n", __func__, __FILE__ + log_build_path_len, __VA_ARGS__)
#define mscbl_log_warn(fmt, ...)  printf(ANSI_YELLOW "[%s]" ANSI_PINK "[%s] " ANSI_RESET fmt "\n", __func__, __FILE__ + log_build_path_len, __VA_ARGS__)
#define mscbl_log_dbg(fmt, ...)   printf(ANSI_CYAN "[%s]" ANSI_PINK "[%s] " ANSI_RESET fmt "\n", __func__, __FILE__ + log_build_path_len, __VA_ARGS__)
 
#define PERF_BEGIN(A) U64 perf_start_##A = clock()
#define PERF_END(A)   printf(ANSI_GREEN "[perf]" ANSI_BOLD #A ANSI_RESET ": %.3fms\n", (float)(clock() - perf_start_##A) / CLOCKS_PER_SEC * 1000)
 
#define Assert(x, message, ...)                                            \
    do                                                                     \
    {                                                                      \
        if (!(x))                                                          \
        {                                                                  \
            mscbl_log_error("(" Stringify(x) ") " message, ##__VA_ARGS__); \
            TRAP();                                                        \
        }                                                                  \
    } while (0)
 

Here's this code actually being used in Miscible.