I wanted to make an extremely light-weight logging system. What are your thoughts on this and what can be done to improve it?
First, the usage:
int main( )
{
logging::level = logging::fatal | logging::warning;
mini_log( logging::fatal, "entry point", "nothing", " is ", "happening!" );
}
Now, the code:
#if defined( NDEBUG )
#define mini_log( ... ) static_cast< void >( 0 )
#else
namespace logging
{
enum level_t
{
none = 0b00000,
information = 0b00001,
debug = 0b00010,
warning = 0b00100,
error = 0b01000,
fatal = 0b10000,
all = 0b11111
} inline level = all; // level should only be used in the entry point
inline std::mutex stream;
}
#define mini_log \
[ ]( logging::level_t const level_message, \
std::string_view const location, \
auto &&... message ) -> void \
{ \
std::lock_guard< std::mutex > lock_stream( logging::stream ); \
struct tm buf; \
auto time = [ & ]( ) \
{ \
auto t = std::time( nullptr ); \
localtime_s( &buf, \
&t ); \
return std::put_time( &buf, \
"[%H:%M:%S]" ); \
}; \
auto level = [ = ]( ) -> std::string \
{ \
switch( level_message ) \
{ \
case logging::information: \
return " [" __FILE__ "@" stringize_val( __LINE__ ) "] [Info] ["; \
case logging::debug: \
return " [" __FILE__ "@" stringize_val( __LINE__ ) "] [Dbug] ["; \
case logging::warning: \
return " [" __FILE__ "@" stringize_val( __LINE__ ) "] [Warn] ["; \
case logging::error: \
return " [" __FILE__ "@" stringize_val( __LINE__ ) "] [Erro] ["; \
case logging::fatal: \
return " [" __FILE__ "@" stringize_val( __LINE__ ) "] [Fatl] ["; \
} \
}; \
if( level_message & logging::level ) \
( ( std::cout << time( ) << level( ) << location << "]: " << message ) << ... ) << '\n'; \
}
#endif
2 Answers 2
Macros
Most of your code is in a macro. This makes it harder to write, read and debug.
Why not place the logic in normal functions and only use macros to pass __FILE__
and __LINE__
?
That is until you can switch to C++20 which provides std::source_location
Naming
Why is the mutex called stream
?
-
\$\begingroup\$ These feels like it should be a comment asking for clarity instead of an answer. Regardless, I made it a macro because it works. I will never need to debug or touch this again. As for naming, I name the mutex
stream
because it is the mutex for the console-out stream. \$\endgroup\$user226075– user2260752020年07月19日 08:20:18 +00:00Commented Jul 19, 2020 at 8:20 -
\$\begingroup\$ You can actually avoid the
std::mutex
entirely if you first build the log message in a string, and then pass this string tostd::cout
in one go. Individual calls tooperator<<
,put()
,write()
and so on are thread-safe. \$\endgroup\$G. Sliepen– G. Sliepen2020年07月19日 09:31:58 +00:00Commented Jul 19, 2020 at 9:31 -
\$\begingroup\$ I guess I could make a string stream and push it to cout \$\endgroup\$user226075– user2260752020年07月19日 10:59:29 +00:00Commented Jul 19, 2020 at 10:59
If you want the logger to log the details in case the program crashes, you should flush the buffer before the crash happens.
( std::cout << time( ) << level( ) << location << "]: " << message ) << ... ) << '\n'
This does not flush the buffer to the console so you might lose info that is in the buffer when the abort happens. So use the following:
( std::cout << time( ) << level( ) << location << "]: " << message ) << ... ) << std::endl;
Also, remove the macro and directly execute the lambda. If that is not suitable, make it a normal static function and make it inline.
Unfortunately, it must remain a lambda because of
__FILE__
and__LINE__
.
Pass __FILE__
and __LINE__
directly to the function (if you make one) and the function signature will be like:
void log_to_file(std::string _file_, std::string _line_);
// usage:
log_to_file(__FILE__, __LINE__);
I personally like __func__
.
-
1\$\begingroup\$ Yes, this is one of those rare cases where
std::endl
is the right thing to use. \$\endgroup\$G. Sliepen– G. Sliepen2020年07月19日 09:29:08 +00:00Commented Jul 19, 2020 at 9:29