aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes Stoelp <johannes.stoelp@gmail.com>2023-10-16 20:11:55 +0200
committerJohannes Stoelp <johannes.stoelp@gmail.com>2023-10-16 20:11:55 +0200
commitd80134afe11289bf68630d59e0db5edcbb898c20 (patch)
tree16d20a673e56449a3502d15d669d76a1a761b255
parenta28790b91d89002edb27dd1bc3997b1d03a94d7e (diff)
downloadcpp-utils-d80134afe11289bf68630d59e0db5edcbb898c20.tar.gz
cpp-utils-d80134afe11289bf68630d59e0db5edcbb898c20.zip
log: add minimal stderr logger
-rw-r--r--.clang-tidy1
-rw-r--r--Makefile1
-rw-r--r--log.h176
-rw-r--r--test/log.cc19
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,
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 <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);
+ }
+}