From 19a117889c2db6b817af9458c04c04f324162e75 Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Mon, 9 Mar 2026 10:50:47 +0100 Subject: Eliminate spdlog dependency (#773) Removes the vendored spdlog library (~12,000 lines) and replaces it with a purpose-built logging system in zencore (~1,800 lines). The new implementation provides the same functionality with fewer abstractions, no shared_ptr overhead, and full control over the logging pipeline. ### What changed **New logging core in zencore/logging/:** - LogMessage, Formatter, Sink, Logger, Registry - core abstractions matching spdlog's model but simplified - AnsiColorStdoutSink - ANSI color console output (replaces spdlog stdout_color_sink) - MsvcSink - OutputDebugString on Windows (replaces spdlog msvc_sink) - AsyncSink - async logging via BlockingQueue worker thread (replaces spdlog async_logger) - NullSink, MessageOnlyFormatter - utility types - Thread-safe timestamp caching in formatters using RwLock **Moved to zenutil/logging/:** - FullFormatter - full log formatting with timestamp, logger name, level, source location, multiline alignment - JsonFormatter - structured JSON log output - RotatingFileSink - rotating file sink with atomic size tracking **API changes:** - Log levels are now an enum (LogLevel) instead of int, eliminating the zen::logging::level namespace - LoggerRef no longer wraps shared_ptr - it holds a raw pointer with the registry owning lifetime - Logger error handler is wired through Registry and propagated to all loggers on registration - Logger::Log() now populates ThreadId on every message **Cleanup:** - Deleted thirdparty/spdlog/ entirely (110+ files) - Deleted full_test_formatter (was ~80% duplicate of FullFormatter) - Renamed snake_case classes to PascalCase (full_formatter -> FullFormatter, json_formatter -> JsonFormatter, sentry_sink -> SentrySink) - Removed spdlog from xmake dependency graph ### Build / test impact - zencore no longer depends on spdlog - zenutil and zenvfs xmake.lua updated to drop spdlog dep - zentelemetry xmake.lua updated to drop spdlog dep - All existing tests pass, no test changes required beyond formatter class renames --- thirdparty/spdlog/tests/test_misc.cpp | 224 ---------------------------------- 1 file changed, 224 deletions(-) delete mode 100644 thirdparty/spdlog/tests/test_misc.cpp (limited to 'thirdparty/spdlog/tests/test_misc.cpp') diff --git a/thirdparty/spdlog/tests/test_misc.cpp b/thirdparty/spdlog/tests/test_misc.cpp deleted file mode 100644 index deb18e745..000000000 --- a/thirdparty/spdlog/tests/test_misc.cpp +++ /dev/null @@ -1,224 +0,0 @@ -#ifdef _WIN32 // to prevent fopen warning on windows - #define _CRT_SECURE_NO_WARNINGS -#endif - -#include "includes.h" -#include "test_sink.h" - -template -std::string log_info(const T& what, spdlog::level::level_enum logger_level = spdlog::level::info) { - std::ostringstream oss; - auto oss_sink = std::make_shared(oss); - - spdlog::logger oss_logger("oss", oss_sink); - oss_logger.set_level(logger_level); - oss_logger.set_pattern("%v"); - oss_logger.info(what); - - return oss.str().substr(0, oss.str().length() - strlen(spdlog::details::os::default_eol)); -} - -TEST_CASE("basic_logging ", "[basic_logging]") { - // const char - REQUIRE(log_info("Hello") == "Hello"); - REQUIRE(log_info("").empty()); - - // std::string - REQUIRE(log_info(std::string("Hello")) == "Hello"); - REQUIRE(log_info(std::string()).empty()); - - // Numbers - REQUIRE(log_info(5) == "5"); - REQUIRE(log_info(5.6) == "5.6"); - - // User defined class - // REQUIRE(log_info(some_logged_class("some_val")) == "some_val"); -} - -TEST_CASE("log_levels", "[log_levels]") { - REQUIRE(log_info("Hello", spdlog::level::err).empty()); - REQUIRE(log_info("Hello", spdlog::level::critical).empty()); - REQUIRE(log_info("Hello", spdlog::level::info) == "Hello"); - REQUIRE(log_info("Hello", spdlog::level::debug) == "Hello"); - REQUIRE(log_info("Hello", spdlog::level::trace) == "Hello"); -} - -TEST_CASE("level_to_string_view", "[convert_to_string_view]") { - REQUIRE(spdlog::level::to_string_view(spdlog::level::trace) == "trace"); - REQUIRE(spdlog::level::to_string_view(spdlog::level::debug) == "debug"); - REQUIRE(spdlog::level::to_string_view(spdlog::level::info) == "info"); - REQUIRE(spdlog::level::to_string_view(spdlog::level::warn) == "warning"); - REQUIRE(spdlog::level::to_string_view(spdlog::level::err) == "error"); - REQUIRE(spdlog::level::to_string_view(spdlog::level::critical) == "critical"); - REQUIRE(spdlog::level::to_string_view(spdlog::level::off) == "off"); -} - -TEST_CASE("to_short_c_str", "[convert_to_short_c_str]") { - REQUIRE(std::string(spdlog::level::to_short_c_str(spdlog::level::trace)) == "T"); - REQUIRE(std::string(spdlog::level::to_short_c_str(spdlog::level::debug)) == "D"); - REQUIRE(std::string(spdlog::level::to_short_c_str(spdlog::level::info)) == "I"); - REQUIRE(std::string(spdlog::level::to_short_c_str(spdlog::level::warn)) == "W"); - REQUIRE(std::string(spdlog::level::to_short_c_str(spdlog::level::err)) == "E"); - REQUIRE(std::string(spdlog::level::to_short_c_str(spdlog::level::critical)) == "C"); - REQUIRE(std::string(spdlog::level::to_short_c_str(spdlog::level::off)) == "O"); -} - -TEST_CASE("to_level_enum", "[convert_to_level_enum]") { - REQUIRE(spdlog::level::from_str("trace") == spdlog::level::trace); - REQUIRE(spdlog::level::from_str("debug") == spdlog::level::debug); - REQUIRE(spdlog::level::from_str("info") == spdlog::level::info); - REQUIRE(spdlog::level::from_str("warning") == spdlog::level::warn); - REQUIRE(spdlog::level::from_str("warn") == spdlog::level::warn); - REQUIRE(spdlog::level::from_str("error") == spdlog::level::err); - REQUIRE(spdlog::level::from_str("critical") == spdlog::level::critical); - REQUIRE(spdlog::level::from_str("off") == spdlog::level::off); - REQUIRE(spdlog::level::from_str("null") == spdlog::level::off); -} - -TEST_CASE("periodic flush", "[periodic_flush]") { - using spdlog::sinks::test_sink_mt; - auto logger = spdlog::create("periodic_flush"); - auto test_sink = std::static_pointer_cast(logger->sinks()[0]); - - spdlog::flush_every(std::chrono::seconds(1)); - std::this_thread::sleep_for(std::chrono::milliseconds(1250)); - REQUIRE(test_sink->flush_counter() == 1); - spdlog::flush_every(std::chrono::seconds(0)); - spdlog::drop_all(); -} - -TEST_CASE("clone-logger", "[clone]") { - using spdlog::sinks::test_sink_mt; - auto test_sink = std::make_shared(); - auto logger = std::make_shared("orig", test_sink); - logger->set_pattern("%v"); - auto cloned = logger->clone("clone"); - - REQUIRE(cloned->name() == "clone"); - REQUIRE(logger->sinks() == cloned->sinks()); - REQUIRE(logger->level() == cloned->level()); - REQUIRE(logger->flush_level() == cloned->flush_level()); - logger->info("Some message 1"); - cloned->info("Some message 2"); - - REQUIRE(test_sink->lines().size() == 2); - REQUIRE(test_sink->lines()[0] == "Some message 1"); - REQUIRE(test_sink->lines()[1] == "Some message 2"); - - spdlog::drop_all(); -} - -TEST_CASE("clone async", "[clone]") { - using spdlog::sinks::test_sink_mt; - spdlog::init_thread_pool(4, 1); - auto test_sink = std::make_shared(); - auto logger = std::make_shared("orig", test_sink, spdlog::thread_pool()); - logger->set_pattern("%v"); - auto cloned = logger->clone("clone"); - - REQUIRE(cloned->name() == "clone"); - REQUIRE(logger->sinks() == cloned->sinks()); - REQUIRE(logger->level() == cloned->level()); - REQUIRE(logger->flush_level() == cloned->flush_level()); - - logger->info("Some message 1"); - cloned->info("Some message 2"); - - spdlog::details::os::sleep_for_millis(100); - - REQUIRE(test_sink->lines().size() == 2); - REQUIRE(test_sink->lines()[0] == "Some message 1"); - REQUIRE(test_sink->lines()[1] == "Some message 2"); - - spdlog::drop_all(); -} - -TEST_CASE("default logger API", "[default logger]") { - std::ostringstream oss; - auto oss_sink = std::make_shared(oss); - - spdlog::set_default_logger(std::make_shared("oss", oss_sink)); - spdlog::set_pattern("*** %v"); - - spdlog::default_logger()->set_level(spdlog::level::trace); - spdlog::trace("hello trace"); - REQUIRE(oss.str() == "*** hello trace" + std::string(spdlog::details::os::default_eol)); - - oss.str(""); - spdlog::debug("hello debug"); - REQUIRE(oss.str() == "*** hello debug" + std::string(spdlog::details::os::default_eol)); - - oss.str(""); - spdlog::info("Hello"); - REQUIRE(oss.str() == "*** Hello" + std::string(spdlog::details::os::default_eol)); - - oss.str(""); - spdlog::warn("Hello again {}", 2); - REQUIRE(oss.str() == "*** Hello again 2" + std::string(spdlog::details::os::default_eol)); - - oss.str(""); - spdlog::error(123); - REQUIRE(oss.str() == "*** 123" + std::string(spdlog::details::os::default_eol)); - - oss.str(""); - spdlog::critical(std::string("some string")); - REQUIRE(oss.str() == "*** some string" + std::string(spdlog::details::os::default_eol)); - - oss.str(""); - spdlog::set_level(spdlog::level::info); - spdlog::debug("should not be logged"); - REQUIRE(oss.str().empty()); - spdlog::drop_all(); - spdlog::set_pattern("%v"); -} - -#if (defined(SPDLOG_WCHAR_TO_UTF8_SUPPORT) || defined(SPDLOG_WCHAR_FILENAMES)) && defined(_WIN32) -TEST_CASE("utf8 to utf16 conversion using windows api", "[windows utf]") { - spdlog::wmemory_buf_t buffer; - - spdlog::details::os::utf8_to_wstrbuf("", buffer); - REQUIRE(std::wstring(buffer.data(), buffer.size()) == std::wstring(L"")); - - spdlog::details::os::utf8_to_wstrbuf("abc", buffer); - REQUIRE(std::wstring(buffer.data(), buffer.size()) == std::wstring(L"abc")); - - spdlog::details::os::utf8_to_wstrbuf("\xc3\x28", buffer); // Invalid UTF-8 sequence. - REQUIRE(std::wstring(buffer.data(), buffer.size()) == std::wstring(L"\xfffd(")); - - spdlog::details::os::utf8_to_wstrbuf("\xe3\x81\xad\xe3\x81\x93", - buffer); // "Neko" in hiragana. - REQUIRE(std::wstring(buffer.data(), buffer.size()) == std::wstring(L"\x306d\x3053")); -} -#endif - -struct auto_closer { - FILE* fp = nullptr; - explicit auto_closer(FILE* f) - : fp(f) {} - auto_closer(const auto_closer&) = delete; - auto_closer& operator=(const auto_closer&) = delete; - ~auto_closer() { - if (fp != nullptr) (void)std::fclose(fp); - } -}; - -TEST_CASE("os::fwrite_bytes", "[os]") { - using spdlog::details::os::create_dir; - using spdlog::details::os::fwrite_bytes; - const char* filename = "log_tests/test_fwrite_bytes.txt"; - const char* msg = "hello"; - prepare_logdir(); - REQUIRE(create_dir(SPDLOG_FILENAME_T("log_tests")) == true); - { - auto_closer closer(std::fopen(filename, "wb")); - REQUIRE(closer.fp != nullptr); - REQUIRE(fwrite_bytes(msg, std::strlen(msg), closer.fp) == true); - REQUIRE(fwrite_bytes(msg, 0, closer.fp) == true); - std::fflush(closer.fp); - REQUIRE(spdlog::details::os::filesize(closer.fp) == 5); - } - // fwrite_bytes should return false on write failure - auto_closer closer(std::fopen(filename, "r")); - REQUIRE(closer.fp != nullptr); - REQUIRE_FALSE(fwrite_bytes("Hello", 5, closer.fp)); -} -- cgit v1.2.3