From d80134afe11289bf68630d59e0db5edcbb898c20 Mon Sep 17 00:00:00 2001 From: Johannes Stoelp Date: Mon, 16 Oct 2023 20:11:55 +0200 Subject: log: add minimal stderr logger --- .clang-tidy | 1 + Makefile | 1 + log.h | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/log.cc | 19 +++++++ 4 files changed, 197 insertions(+) create mode 100644 log.h create mode 100644 test/log.cc 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, diff --git a/Makefile b/Makefile index 05d7233..8d1fb21 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ TEST += bitfield TEST += option TEST += timer +TEST += log # -- INTERNALS ----------------------------------------------------------------- diff --git a/log.h b/log.h new file mode 100644 index 0000000..c0880fb --- /dev/null +++ b/log.h @@ -0,0 +1,176 @@ +#ifndef UTILS_LOG_H +#define UTILS_LOG_H + +#include +#include +#include +#include +#include + +// -- 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(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 +constexpr inline bool is_one_of() { + return false; +} + +template +constexpr inline bool is_one_of() { + return std::is_same::value || is_one_of(); +} + +template +constexpr inline Arg sanitize_fmt_args(Arg arg) { + static_assert( + is_one_of() || + std::is_pointer::value, + "Invalid FMT arg type!"); + return arg; +} + +// -- TIME STAMP HELPER -------------------------------------------------------- + +template +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() % 1000; + } + repr ms() const { + return to_duration() % 1000; + } + repr s() const { + return to_duration() % 60; + } + repr m() const { + return to_duration() % 60; + } + repr h() const { + return to_duration() % 24 + UtcOffset; + } + + private: + template + repr to_duration() const { + return std::chrono::duration_cast(m_time.time_since_epoch()) + .count(); + } + + time_point m_time{clock::now()}; +}; +} // namespace detail + +// -- LOGGER ------------------------------------------------------------------- + +template +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 + 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 +template +constexpr void logger::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 + +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 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); + } +} -- cgit v1.2.3