diff options
-rw-r--r-- | .clang-tidy | 1 | ||||
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | log.h | 176 | ||||
-rw-r--r-- | test/log.cc | 19 |
4 files changed, 197 insertions, 0 deletions
diff --git a/.clang-tidy b/.clang-tidy index ac2683b..49c1de0 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -33,6 +33,7 @@ Checks: > readability-identifier-naming, misc-*, -misc-non-private-member-variables-in-classes, + -misc-const-correctness, #cert-*, bugprone-*, -bugprone-use-after-move, @@ -1,6 +1,7 @@ TEST += bitfield TEST += option TEST += timer +TEST += log # -- INTERNALS ----------------------------------------------------------------- @@ -0,0 +1,176 @@ +#ifndef UTILS_LOG_H +#define UTILS_LOG_H + +#include <bits/chrono.h> +#include <cassert> +#include <chrono> +#include <cstdio> +#include <type_traits> + +// -- LOGGER MACROS ------------------------------------------------------------ + +#define FAIL(log, fmt, ...) LOG(log, kFail, fmt, ##__VA_ARGS__) +#define WARN(log, fmt, ...) LOG(log, kWarn, fmt, ##__VA_ARGS__) +#define INFO(log, fmt, ...) LOG(log, kInfo, fmt, ##__VA_ARGS__) +#define DBG0(log, fmt, ...) LOG(log, kDbg0, fmt, ##__VA_ARGS__) + +// -- LOGGER MACROS DETAILS ---------------------------------------------------- + +#define LOG(log_, lvl, fmt, ...) \ + do { \ + struct fmt_str_must_be_str_literal { \ + constexpr fmt_str_must_be_str_literal(const char* ptr) : ptr{ptr} {} \ + const char* ptr; \ + }; \ + /* Check if FMT is a string literal, construction fails otherwise. */ \ + constexpr fmt_str_must_be_str_literal _{fmt}; \ + (void)_; \ + \ + if (logging::log_level::lvl <= (log_)->get_level()) \ + (log_)->template log<logging::log_level::lvl>(fmt, ##__VA_ARGS__); \ + } while (0) + +namespace logging { + +// -- LOGGING LEVELS ----------------------------------------------------------- + +#define LOG_LEVELS(M) \ + M(kFail, "FAIL") \ + M(kWarn, "WARN") \ + M(kInfo, "INFO") \ + M(kDbg0, "DBG0") + +#define M(val, ignore) val, +enum log_level { LOG_LEVELS(M) }; +#undef M + +#define M(ignore, val) val, +constexpr const char* kLogPrefix[] = {LOG_LEVELS(M)}; +#undef M + +namespace detail { + +// -- SANITIZE FMT ARG HELPER (META FN) ---------------------------------------- + +template <typename T> +constexpr inline bool is_one_of() { + return false; +} + +template <typename T, typename U, typename... Args> +constexpr inline bool is_one_of() { + return std::is_same<T, U>::value || is_one_of<T, Args...>(); +} + +template <typename Arg> +constexpr inline Arg sanitize_fmt_args(Arg arg) { + static_assert( + is_one_of<Arg, char, unsigned char, int, unsigned, float, double>() || + std::is_pointer<Arg>::value, + "Invalid FMT arg type!"); + return arg; +} + +// -- TIME STAMP HELPER -------------------------------------------------------- + +template <int UtcOffset = 0> +struct time_stamp { + using clock = std::chrono::system_clock; + using repr = clock::rep; + using time_point = clock::time_point; + + repr us() const { + return to_duration<std::chrono::microseconds>() % 1000; + } + repr ms() const { + return to_duration<std::chrono::milliseconds>() % 1000; + } + repr s() const { + return to_duration<std::chrono::seconds>() % 60; + } + repr m() const { + return to_duration<std::chrono::minutes>() % 60; + } + repr h() const { + return to_duration<std::chrono::hours>() % 24 + UtcOffset; + } + + private: + template <typename ToDuration> + repr to_duration() const { + return std::chrono::duration_cast<ToDuration>(m_time.time_since_epoch()) + .count(); + } + + time_point m_time{clock::now()}; +}; +} // namespace detail + +// -- LOGGER ------------------------------------------------------------------- + +template <bool WithTimestamp = true, size_t BufSize = 128> +struct logger { + constexpr logger() = default; + constexpr logger(log_level lvl) : m_lvl{lvl} {} + + log_level get_level() const { + return m_lvl; + } + void set_level(log_level lvl) { + m_lvl = lvl; + } + + template <log_level L, typename... Args> + constexpr void log(const char* fmt, Args... args) + __attribute__((format(printf, 2, 0))); + + private: + log_level m_lvl{kInfo}; + char m_buf[BufSize]; +}; + +// -- LOGGER IMPLEMENTATION ---------------------------------------------------- + +template <bool WithTimestamp, size_t BufSize> +template <log_level L, typename... Args> +constexpr void logger<WithTimestamp, BufSize>::log(const char* fmt, + Args... args) { + size_t pos{0}; + + // Add timestamp if enabled. + if (WithTimestamp) { + detail::time_stamp<2> ts; + pos += std::snprintf(m_buf + pos, BufSize - pos, + "[%02ld:%02ld:%02ld:%03ld%03ld] ", ts.h(), ts.m(), + ts.s(), ts.ms(), ts.us()); + assert(pos > 0); + } + + // Add log level prefix. + pos += std::snprintf(m_buf + pos, BufSize - pos, "%s: ", kLogPrefix[L]); + assert(pos < BufSize); + + // Add log message using user specified fmt string. + // + // SAFETY: User of this function is responsible to provide a "safe" fmt + // string. When using the provided macros we check that the user specifies a + // string literal as fmt string and hence the user controls the fmt string. + // Additionally, we sanitize the arguments to only allow explicitly specified + // argument types. + // + // NOLINTNEXTLINE + pos += std::snprintf(m_buf + pos, BufSize - pos, fmt, + detail::sanitize_fmt_args(args)...); + assert(pos < BufSize); + + // Ensure terminated with new line and null terminator. + assert(pos < BufSize - 1); + m_buf[pos++] = '\0'; + + // Write out log message. + std::fprintf(stderr, "%s\n", m_buf); +} + +} // namespace logging + +#endif diff --git a/test/log.cc b/test/log.cc new file mode 100644 index 0000000..c059ba8 --- /dev/null +++ b/test/log.cc @@ -0,0 +1,19 @@ +#include <log.h> + +int main() { + logging::logger<> mlog; + mlog.set_level(logging::kDbg0); + + INFO(&mlog, "Hallo %d", 42); + WARN(&mlog, "Hallo %x", 0x1337); + FAIL(&mlog, "Hallo %u", 666); + DBG0(&mlog, "Hallo %p", (void*)0xf00df00d); + + { + logging::logger<false> mlog; + INFO(&mlog, "Hallo no time, %d", 42); + WARN(&mlog, "Hallo no time, %x", 0x1337); + FAIL(&mlog, "Hallo no time, %u", 666); + DBG0(&mlog, "Hallo no time, %p", (void*)0xf00df00d); + } +} |