I am a member of a university team designing a nanosatellite.
We decided to implement our own (more lite) logging library to use, instead of, say, Google's glog
, spdlog
, plog
and Boost::Log
.
- The concept of different log levels is introduced, as a means of dividing the log messages into subcategories, according to their severity and whether they were expected to occur.
- Furthermore, there will be a "global log level" that can be defined. Everything less severe than the severity set as the global log level, will not be logged.
Due to obvious restrictions, it is imperative that log calls below the global log level get optimized away at compile time.
The first attempt was something like this (single header file): log levels:
// We can set the global log level by defining one of these
#if defined LOGLEVEL_TRACE
#define LOGLEVEL Logger::trace
#elif defined LOGLEVEL_DEBUG
#define LOGLEVEL Logger::debug
#elif defined LOGLEVEL_INFO
[...]
#else
#define LOGLEVEL Logger::disabled
#endif
the levels themselves are enum
members:
enum LogLevel {
trace = 32, // Very detailed information, useful for tracking the individual steps of an operation
debug = 64, // General debugging information
info = 96, // Noteworthy or periodical events
[...]
};
A operator<<
overload for better readability:
template <class T>
Logger::LogEntry& operator<<(Logger::LogEntry& entry, const T value) {
etl::to_string(value, entry.message, entry.format, true);
return entry;
}
And the macro-constexpr
sorcery to make the compiler do what we want:
#define LOG(level)
if (Logger::isLogged(level)) \
if (Logger::LogEntry entry(level); true) \
entry
// [...]
static constexpr bool isLogged(LogLevelType level) {
return static_cast<LogLevelType>(LOGLEVEL) <= level;
}
There were many issues with this code (see the MR discussion for more).
- A call operator to the
enum LogLevel
has been added to return a newstatic LogEntry
. - It is
inline
d to forceconst
propagation at-O1
. - Two
LogEntry
enums
have been created. - The second one is a
nop
with everythinginline
. if constexpr
syntax has been added.
and more (see here and below for justification.)
That's the (chopped) state of the code currently:
#include <cstdint>
#include <string>
#define LOGLEVEL_EMERGENCY
#if defined LOGLEVEL_TRACE
#define LOGLEVEL Logger::trace
#elif defined LOGLEVEL_DEBUG
#define LOGLEVEL Logger::debug
#elif defined LOGLEVEL_INFO
#define LOGLEVEL Logger::info
#elif defined LOGLEVEL_NOTICE
#define LOGLEVEL Logger::notice
#elif defined LOGLEVEL_WARNING
#define LOGLEVEL Logger::warning
#elif defined LOGLEVEL_ERROR
#define LOGLEVEL Logger::error
#elif defined LOGLEVEL_EMERGENCY
#define LOGLEVEL Logger::emergency
#else
#define LOGLEVEL Logger::disabled
#endif
#define LOG_TRACE (LOG<Logger::trace>())
#define LOG_DEBUG (LOG<Logger::debug>())
#define LOG_INFO (LOG<Logger::info>())
#define LOG_NOTICE (LOG<Logger::notice>())
#define LOG_WARNING (LOG<Logger::warning>())
#define LOG_ERROR (LOG<Logger::error>())
#define LOG_EMERGENCY (LOG<Logger::emergency>())
class Logger {
public:
Logger() = delete;
typedef uint8_t LogLevelType;
enum LogLevel : LogLevelType {
trace = 32,
debug = 64,
info = 96,
notice = 128,
warning = 160,
error = 192,
emergency = 254,
disabled = 255,
};
enum class NoLogEntry {};
struct LogEntry {
std::string message = "";
LogLevel level;
explicit LogEntry(LogLevel level);
~LogEntry();
LogEntry(LogEntry const&) = delete;
template <class T>
Logger::LogEntry& operator<<(const T value) noexcept {
message.append(value);
return *this;
}
Logger::LogEntry& operator<<(const std::string& value);
};
static constexpr bool isLogged(LogLevelType level) {
return static_cast<LogLevelType>(LOGLEVEL) <= level;
}
static void log(LogLevel level, std::string & message);
};
template <Logger::LogLevel level>
constexpr inline auto LOG() {
if constexpr (Logger::isLogged(level)) {
return Logger::LogEntry(level);
} else {
return Logger::NoLogEntry();
}
};
template <typename T>
[[maybe_unused]] constexpr Logger::NoLogEntry operator<<(const Logger::NoLogEntry noLogEntry, T value) {
return noLogEntry;
}
int main() {
LOG_NOTICE << "I am getting optimized away!";
LOG_EMERGENCY << "I am not getting optimized away, and rightfully so";
return 0;
}
As you can see in e.g.Compiler Explorer, the LOG_NOTICE
is getting optimized away at -O1
.
Do you have any suggestions?
- Caveats and pitfalls I might have missed?
- Better ways to implement this?
- Ways to decrease overhead?