CCCPaste

kjmj_log.h logging for C/C++

// File: kjmj_log.h
// SPDX-License-Identifier: Unlicensed
// This is free and unencumbered software released into the public domain.

/*!
 * \file kjmj_log.h
 *
 * \brief   This file is a \e thin wrapper around the C Macro Collections
 *          \c log.h header file.
 *
 * Defines the macro functions \c LOG_INIT, \c LOG_TO_FILE, LOG_TO_TERMINAL,
 * and \c LOG_TERM. These macros initialize, setup, and terminate logging.
 *
 * Defines the macro values \c LOG_LEVEL_TRACE, \c LOG_LEVEL_DEBUG,
 * \c LOG_LEVEL_INFO, \c LOG_LEVEL_WARN, \c LOG_LEVEL_ERROR, and
 * \c LOG_LEVEL_FATAL. These macro values are used to setup logging to the
 * given level or higher (i.e., \c LOG_LEVEL_TRACE is the lowest level, and
 * \c LOG_LEVEL_FATAL is the highest level).
 *
 * Defines the logging macro functions \c LOG_TRACE, \c LOG_DEBUG,
 * \c LOG_INFO, \c LOG_WARN, \c LOG_ERROR, and \c WARN_FATAL.
 *
 * \version 1.0
 * \date    2023
 */

#ifndef KJMJ_LOG_H_
#define KJMJ_LOG_H_

#ifdef __cplusplus
extern "C" {
#endif

#include       // for FILE and file-related functions, stdout, stderr
#include      // for string manipulation functions
#include        // for time_t, time, localtime
#include      // for va_list, va_start, va_end, vfprintf
#include      // for assert

// ANSI color codes.
#define ANSI_BLACK_FG                   "\e[0;30m"
#define ANSI_RED_FG                     "\e[0;31m"
#define ANSI_GREEN_FG                   "\e[0;32m"
#define ANSI_YELLOW_FG                  "\e[0;33m"
#define ANSI_BLUE_FG                    "\e[0;34m"
#define ANSI_PURPLE_FG                  "\e[0;35m"
#define ANSI_CYAN_FG                    "\e[0;36m"
#define ANSI_WHITE_FG                   "\e[0;37m"

#define ANSI_BOLD_BLACK_FG              "\e[1;30m"
#define ANSI_BOLD_RED_FG                "\e[1;31m"
#define ANSI_BOLD_GREEN_FG              "\e[1;32m"
#define ANSI_BOLD_YELLOW_FG             "\e[1;33m"
#define ANSI_BOLD_BLUE_FG               "\e[1;34m"
#define ANSI_BOLD_PURPLE_FG             "\e[1;35m"
#define ANSI_BOLD_CYAN_FG               "\e[1;36m"
#define ANSI_BOLD_WHITE_FG              "\e[1;37m"

#define ANSI_BRIGHT_BLACK_FG            "\e[0;90m"
#define ANSI_BRIGHT_RED_FG              "\e[0;91m"
#define ANSI_BRIGHT_GREEN_FG            "\e[0;92m"
#define ANSI_BRIGHT_YELLOW_FG           "\e[0;93m"
#define ANSI_BRIGHT_BLUE_FG             "\e[0;94m"
#define ANSI_BRIGHT_PURPLE_FG           "\e[0;95m"
#define ANSI_BRIGHT_CYAN_FG             "\e[0;96m"
#define ANSI_BRIGHT_WHITE_FG            "\e[0;97m"

#define ANSI_BOLD_BRIGHT_BLACK_FG       "\e[1;90m"
#define ANSI_BOLD_BRIGHT_RED_FG         "\e[1;91m"
#define ANSI_BOLD_BRIGHT_GREEN_FG       "\e[1;92m"
#define ANSI_BOLD_BRIGHT_YELLOW_FG      "\e[1;93m"
#define ANSI_BOLD_BRIGHT_BLUE_FG        "\e[1;94m"
#define ANSI_BOLD_BRIGHT_PURPLE_FG      "\e[1;95m"
#define ANSI_BOLD_BRIGHT_CYAN_FG        "\e[1;96m"
#define ANSI_BOLD_BRIGHT_WHITE_FG       "\e[1;97m"

#define ANSI_UNDERLINE_BLACK_FG         "\e[4;30m"
#define ANSI_UNDERLINE_RED_FG           "\e[4;31m"
#define ANSI_UNDERLINE_GREEN_FG         "\e[4;32m"
#define ANSI_UNDERLINE_YELLOW_FG        "\e[4;33m"
#define ANSI_UNDERLINE_BLUE_FG          "\e[4;34m"
#define ANSI_UNDERLINE_PURPLE_FG        "\e[4;35m"
#define ANSI_UNDERLINE_CYAN_FG          "\e[4;36m"
#define ANSI_UNDERLINE_WHITE_FG         "\e[4;37m"

#define ANSI_BLACK_BG                   "\e[40m"
#define ANSI_RED_BG                     "\e[41m"
#define ANSI_GREEN_BG                   "\e[42m"
#define ANSI_YELLOW_BG                  "\e[43m"
#define ANSI_BLUE_BG                    "\e[44m"
#define ANSI_PURPLE_BG                  "\e[45m"
#define ANSI_CYAN_BG                    "\e[46m"
#define ANSI_WHITE_BG                   "\e[47m"

#define ANSI_RESET                      "\e[0;0m"

// Log colors.
#ifndef ANSI_START_TRACE
#define ANSI_START_TRACE                ANSI_BRIGHT_WHITE_FG
#endif
#ifndef ANSI_START_DEBUG
#define ANSI_START_DEBUG                ANSI_BRIGHT_BLUE_FG
#endif
#ifndef ANSI_START_INFO
#define ANSI_START_INFO                 ANSI_BRIGHT_CYAN_FG
#endif
#ifndef ANSI_START_WARN
#define ANSI_START_WARN                 ANSI_BRIGHT_YELLOW_FG
#endif
#ifndef ANSI_START_ERROR
#define ANSI_START_ERROR                ANSI_BRIGHT_RED_FG
#endif
#ifndef ANSI_START_FATAL
#define ANSI_START_FATAL                ANSI_BRIGHT_PURPLE_FG
#endif

/*!
 * \enum    kjmjLogLevelEnum
 *
 * \brief   The possible logging levels.
 *
 * The log level \c KJMJ_LOG_LEVEL_FATAL can be used to specify that only the
 * most important (i.e., fatal) log messages shold be logged. Using this level
 * to specify the log level for \c kjmj_log_to_file, \c kjmj_log_to_stdout, or
 * \c kjmj_log_to_stderr means that only messages logged using the \c FATAL
 * macro will be logged.
 *
 * Conversely, the log level \c KJMJ_LOG_LEVEL_TRACE can be used to specify
 * that \em any logging message will be logged.
 *
 * The log level \c KJMJ_LOG_LEVEL_DISABLED can be used to specify that
 * logging should not occur.
 *
 * The log level \c KJMJ_LOG_LEVEL_ALL can be used to specify that all log
 * messages should be logged.
 *
 * \see     kjmj_log_to_file
 * \see     kjmj_tog_to_stdout
 * \see     kjmj_log_to_stderr
 * \see     FATAL
*/
enum kjmjLogLevelEnum {
    KJMJ_LOG_LEVEL_DISABLED,
    KJMJ_LOG_LEVEL_FATAL,
    KJMJ_LOG_LEVEL_ERROR,
    KJMJ_LOG_LEVEL_WARN,
    KJMJ_LOG_LEVEL_INFO,
    KJMJ_LOG_LEVEL_DEBUG,
    KJMJ_LOG_LEVEL_TRACE,
    KJMJ_LOG_LEVEL_ALL
};
typedef enum kjmjLogLevelEnum kjmjLogLevel;

/*!
 * \def     KJMJ_LEVEL_IS_AT_LEAST
 *
 * \brief   Returns true if a level is at least as important as the
 *          other level.
 *
 * \param   LEVEL the level to compare against \p OTHER.
 * \param   OTHER the level that LEVEL should be at least as important as.
 *
 * \see     kjmjLogLevelEnum
 *
 * \note    Because the log levels are specified from the
 *          most important (fatal) to the least important (trace)
 *          the lower the log level the more important it is.
 */
#define KJMJ_LEVEL_AT_LEAST( LEVEL, OTHER ) \
    ( LEVEL <= OTHER )

/*!
 * \internal
 *
 * \def     KJMJ_FNAME
 *
 * \brief   Returns the filename suffix portion of \c __FILE__.
 *
 * \endinternal
 */
#define KJMJ_FNAME \
    ( strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__ )

/*!
 * \internal
 *
 * \def KJMJ_MACRO_START
 *
 * \brief   Used at the beginning of macro function definitions that contain
 *          more than one line of code.
 *
 * Used to wrap the macro function in a do { ... } while(0) block.
 *
 * \see     KJMJ_MACRO_END
 *
 * \endinternal
 */
#define KJMJ_MACRO_START do {

/*!
 * \internal
 *
 * \def     KJMJ_MACRO_END
 *
 * \brief   Used at the end of macro function definitions that contain
 *          more than one line of code.
 *
 * Used to wrap the macro function in a do { ... } while (0) block.
 *
 * \see     KJMJ_MACRO_START
 *
 * \endinternal
 */
#define KJMJ_MACRO_END } while (0)

/*!
 * \def     TRACE
 *
 * \brief   Used to log at the trace level.
 *
 * Trace is used to describe the inner workings of a function or an
 * algorithm. It can be useful when trying to find an error in an
 * algorithm or for corner cases.
 *
 * \param FMT   a C printf style format string.
 * \param ...   any additional variadic arguments will be used like printf
 *              variadic arguments.
 */
#ifndef TRACE
#define TRACE( FMT, ... ) \
    kjmj_log( KJMJ_LOG_LEVEL_TRACE, "[TRACE]", ANSI_START_TRACE, \
        __func__, KJMJ_FNAME, __LINE__, FMT, ##__VA_ARGS__ )
#else   // TRACE is defined
#error "TRACE already defined"
#endif

/*!
 * \def     DEBUG
 *
 * \brief   Used to log at the debug level or higher.
 *
 * Mainly used for debugging, tracking certain variables or
 * anything useful to help visualizing what your program is doing.
 *
 * \param FMT   a C printf style format string.
 * \param ...   any additional variadic arguments will be used like printf
 *              variadic arguments.
 */
#ifndef DEBUG
#define DEBUG( FMT, ... ) \
    kjmj_log( KJMJ_LOG_LEVEL_DEBUG, "[DEBUG]", ANSI_START_DEBUG, \
        __func__, KJMJ_FNAME, __LINE__, FMT, ##__VA_ARGS__ )
#else
#error "DEBUG already defined"
#endif

/*!
 * \def     INFO
 * \brief   Used to log at the info level.
 *
 * Info can be used as a heads up or for normal messages.
 *
 * \param FMT   a C printf style format string.
 * \param ...   any additional variadic arguments will be used like printf
 *              variadic arguments.
 */
#ifndef INFO
#define INFO( FMT, ... ) \
    kjmj_log( KJMJ_LOG_LEVEL_INFO, "[INFO]", ANSI_START_INFO, \
        __func__, KJMJ_FNAME, __LINE__, FMT, ##__VA_ARGS__ )
#else
#error "INFO already defined"
#endif

/*!
 * \def     WARN
 *
 * \brief   Used to log at the warning level.
 *
 * Warnings are attributed to all cases where the code can recover from the
 * condition and they are usually treated by it. In most cases the user can
 * treat these warnings themselves.
 *
 * \param FMT   a C printf style format string.
 * \param ...   any additional variadic arguments will be used like printf
 *              variadic arguments.
 */
#ifndef WARN
#define WARN( FMT, ... ) \
    kjmj_log( KJMJ_LOG_LEVEL_WARN, "[WARN]", ANSI_START_WARN, \
        __func__, KJMJ_FNAME, __LINE__, FMT, ##__VA_ARGS__ )
#else
#error "WARN already defined"
#endif

/*!
 * \def     ERROR
 *
 * \brief   Used to log at the error level.
 *
 * Something really bad happened. Your program will not crash yet but
 * depending on what comes next, it probably will.
 *
 * \param FMT   a C printf style format string.
 * \param ...   any additional variadic arguments will be used like printf
 *              variadic arguments.
 */
#ifndef ERROR
#define ERROR( FMT, ... ) \
    kjmj_log( KJMJ_LOG_LEVEL_ERROR, "[ERROR]", ANSI_START_ERROR, \
        __func__, KJMJ_FNAME, __LINE__, FMT, ##__VA_ARGS__ )
#else
#error "ERROR already defined"
#endif

/*!
 * \def     FATAL
 *
 * \brief   Used to log at the fatal level.
 *
 * This will probably be the last logging message before your program
 * crashes or goes into madness. Fatal errors are commonly attributed
 * to dereferencing \c NULL pointers.
 *
 * \param FMT   a C printf style format string.
 * \param ...   any additional variadic arguments will be used like printf
 *              variadic arguments.
 */
#ifndef FATAL
#define FATAL( FMT, ... ) \
    kjmj_log( KJMJ_LOG_LEVEL_FATAL, "[FATAL]", ANSI_START_FATAL, \
        __func__, KJMJ_FNAME, __LINE__, FMT, ##__VA_ARGS__ );
#else
    #error "FATAL already defined"
#endif

/*!
 * \internal
 *
 * \struct  kjmjLogStruct
 *
 * \brief   The data structure that holds all information neccessary to
 *          determine what to do when a message is logged.
 *
 * The one and only variable that uses this struct is the global variable
 * \c g_kjmj_log.
 *
 * \endinternal
 */
static struct kjmjLogStruct {
    FILE* file;
    int file_log_level;
    int stdout_log_level;
    int stderr_log_level;
} g_kjmj_log = {
    NULL,
    KJMJ_LOG_LEVEL_DISABLED,
    KJMJ_LOG_LEVEL_DISABLED,
    KJMJ_LOG_LEVEL_DISABLED
};

/*!
 * \fn      kjmj_log_init
 *
 * \brief   Prepares the logging system. Both file and terminal logging is
 *          disabled.
 *
 * \note    If the logging system is currently setup to log to a file, the
 *          file will be closed.
 */
static void kjmj_log_init()
{
    if ( g_kjmj_log.file != NULL ) {
        fclose( g_kjmj_log.file );
    }

    g_kjmj_log.file = NULL,
    g_kjmj_log.file_log_level = KJMJ_LOG_LEVEL_DISABLED;
    g_kjmj_log.stdout_log_level = KJMJ_LOG_LEVEL_DISABLED,
    g_kjmj_log.stderr_log_level = KJMJ_LOG_LEVEL_DISABLED;
}

/*!
 * \fn      kjmj_log_terminate
 *
 * \brief   Should be called once to clean up.
 *
 * \see     kjmj_log_init
 */
static void kjmj_log_terminate()
{
    kjmj_log_init();
}

/*!
 * \fn      kjmj_log_to_file
 *
 * \brief   Enables logging to \p file at the \p level logging level.
 *
 * \param   file    handle (pointer) to \c FILE. Must be open unless the
 *                  \p LEVEL is \c KJMJ_LOG_LEVEL_DISABLED.
 * \param   level   the level (or higher) at which logging is enabled.
 *
 * \see     kjmjLogLevel
 */
static void kjmj_log_to_file( FILE* file, kjmjLogLevel level )
{
    assert( ( file != NULL ) ||
        ( KJMJ_LEVEL_AT_LEAST( KJMJ_LOG_LEVEL_TRACE, level ) ) );
    assert( KJMJ_LEVEL_AT_LEAST( KJMJ_LOG_LEVEL_DISABLED, level ) );
    assert( KJMJ_LEVEL_AT_LEAST( level, KJMJ_LOG_LEVEL_ALL ) );

    g_kjmj_log.file = file;
    g_kjmj_log.file_log_level = level;
}

/*!
 * \def     kjmj_log_to_stdout
 *
 * \brief   Enables logging to \c stdout at the \p level logging level.
 *
 * \param   level   the level (or more important) at which logging is enabled.
 *
 * \see     kjmjLogLevel
 */
static void kjmj_log_to_stdout( kjmjLogLevel level )
{
    assert( KJMJ_LEVEL_AT_LEAST( KJMJ_LOG_LEVEL_DISABLED, level ) );
    assert( KJMJ_LEVEL_AT_LEAST( level, KJMJ_LOG_LEVEL_ALL ) );

    g_kjmj_log.stdout_log_level = level;
}

/*!
 * \def     kjmj_log_to_stderr
 *
 * \brief   Enables logging to \c stderr at the \p level logging level.
 *
 * \param   level   the level (or more important) at which logging is enabled.
 */
static void kjmj_log_to_stderr( kjmjLogLevel level )
{
    assert( KJMJ_LEVEL_AT_LEAST( KJMJ_LOG_LEVEL_DISABLED, level ) );
    assert( KJMJ_LEVEL_AT_LEAST( level, KJMJ_LOG_LEVEL_ALL ) );

    g_kjmj_log.stderr_log_level = level;
}

/*!
 * \internal
 *
 * \fn      kjmj_log
 *
 * \brief   Logs a message to a file, stdout, and/or stderr if enabled.
 *
 * \param   level               the level of the log message.
 * \param   level_name          a string representation of the level.
 * \param   ansi_start_level    the ANSI string that should be used to start
 *                              the log message if log colors are enabled.
 * \param   function_name       the name of the function currently executing.
 * \param   filename            the name of the file containing this function.
 * \param   line                the line number where the message is being
 *                              logged from.
 * \param   fmt                 a C printf style format string.
 * \param ...                   any additional variadic arguments will be used
 *                              like printf variadic arguments.
 *
 * \internal
 */
void kjmj_log( int level, const char* level_name,
    const char* ansi_start_level, const char* function_name,
    const char* filename, unsigned line, const char* fmt, ... )
{
    assert( KJMJ_LEVEL_AT_LEAST( KJMJ_LOG_LEVEL_DISABLED, level ) );
    assert( KJMJ_LEVEL_AT_LEAST( level, KJMJ_LOG_LEVEL_ALL ) );
    assert( level_name != NULL );
    assert( ansi_start_level != NULL );
    assert( function_name != NULL );
    assert( filename != NULL );
    assert( fmt != NULL );

    _Bool log_to_file = ( g_kjmj_log.file != NULL )
        && ( KJMJ_LEVEL_AT_LEAST( level, g_kjmj_log.file_log_level ) );
    _Bool log_to_stdout =
        KJMJ_LEVEL_AT_LEAST( level, g_kjmj_log.stdout_log_level );
    _Bool log_to_stderr =
        KJMJ_LEVEL_AT_LEAST( level, g_kjmj_log.stderr_log_level );

    if ( !log_to_file && !log_to_stdout && !log_to_stderr ) {
        return;
    }

    // Determine the time string for the time now.
    time_t now;
    time( &now );
    struct tm* local;
    local = localtime( &now );
    char timestamp[ 0x20 ];
    timestamp[ strftime( timestamp, sizeof( timestamp ),
            "%Y/%m/%d %H:%M:%S", local ) ] = '\0';

    // Determine the prefix to be printed before the log message.
    char msg_prefix[ 0x100 ];
    snprintf( msg_prefix, sizeof( msg_prefix ),
        "%s %-7s %s (%s:%u) ", timestamp, level_name,
        function_name, filename, line );

    // Optionally log to file.
    if ( log_to_file ) {
        fprintf( g_kjmj_log.file, msg_prefix );

        va_list args;
        va_start( args, fmt );
        vfprintf( g_kjmj_log.file, fmt, args );
        va_end( args );
        fprintf( g_kjmj_log.file, "\n" );

        fflush( g_kjmj_log.file );
    }

    // Optionally log to stdout.
    if ( log_to_stdout ) {
        #ifndef KJMJ_LOG_NO_COLOR
        fprintf( stdout, ansi_start_level );
        #endif

        fprintf( stdout, msg_prefix );

        #ifndef KJMJ_LOG_NO_COLOR
        fprintf( stdout, ANSI_RESET );
        #endif

        va_list args;
        va_start( args, fmt );
        vfprintf( stdout, fmt, args );
        va_end( args );
        fprintf( stdout, "\n" );
    }

    // Optionally log to stderr.
    if ( log_to_stderr ) {
        #ifndef KJMJ_LOG_NO_COLOR
        fprintf( stderr, ansi_start_level );
        #endif

        fprintf( stderr, msg_prefix );

        #ifndef KJMJ_LOG_NO_COLOR
        fprintf( stderr, ANSI_RESET );
        #endif

        va_list args;
        va_start( args, fmt );
        vfprintf( stderr, fmt, args );
        va_end( args );
        fprintf( stderr, "\n" );
    }
} // kjmj_log

#ifdef __cplusplus
}
#endif

#endif // LOG_H_