// 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_