From 543da232ed13f54dd28a1ed063fd65f75a33692d Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Mon, 18 May 2026 22:05:02 +0300 Subject: [PATCH] feat(logging): add dedicated executor controls Expose config-first, explicit, and dedicated registration macros for built-in async loggers while preserving the existing macro API. Add dedicated executor lifecycle handling, cooperative single-threaded Emscripten queueing, wakeup fixes for blocked producers, and coverage for shutdown, macro registration, and C++11 Windows debug construction. --- .github/workflows/ci.yml | 3 +- README.md | 34 ++ docs/TaskExecutor.md | 9 + docs/mainpage.dox | 27 + include/logit_cpp/logit/Logger.hpp | 20 +- .../logit_cpp/logit/detail/QueuePolicy.hpp | 16 + .../logit/detail/SingleThreadExecutor.hpp | 345 +++++++++++ .../logit_cpp/logit/detail/TaskExecutor.hpp | 10 +- include/logit_cpp/logit/log_macros.hpp | 536 ++++++++++++++++++ include/logit_cpp/logit/loggers.hpp | 2 + .../logit_cpp/logit/loggers/ConsoleLogger.hpp | 178 +++++- .../logit/loggers/EventLogLogger.hpp | 56 +- .../logit_cpp/logit/loggers/FileLogger.hpp | 120 +++- include/logit_cpp/logit/loggers/ILogger.hpp | 8 + .../logit_cpp/logit/loggers/SyslogLogger.hpp | 59 +- .../logit/loggers/UniqueFileLogger.hpp | 76 ++- .../logit/loggers/WindowsDebugLogger.hpp | 217 +++++++ tests/CMakeLists.txt | 3 + .../console_logger_dedicated_config_test.cpp | 78 +++ tests/dedicated_executor_macro_api_test.cpp | 119 ++++ tests/dedicated_executor_shutdown_test.cpp | 68 +++ tests/ems/single_thread_executor.cpp | 15 + tests/per_logger_isolation_test.cpp | 135 +++++ tests/per_logger_mixed_mode_test.cpp | 150 +++++ tests/single_thread_executor_test.cpp | 334 +++++++++++ tests/windows_debug_macro_compile_test.cpp | 11 + 26 files changed, 2581 insertions(+), 48 deletions(-) create mode 100644 include/logit_cpp/logit/detail/QueuePolicy.hpp create mode 100644 include/logit_cpp/logit/detail/SingleThreadExecutor.hpp create mode 100644 include/logit_cpp/logit/loggers/WindowsDebugLogger.hpp create mode 100644 tests/console_logger_dedicated_config_test.cpp create mode 100644 tests/dedicated_executor_macro_api_test.cpp create mode 100644 tests/dedicated_executor_shutdown_test.cpp create mode 100644 tests/ems/single_thread_executor.cpp create mode 100644 tests/per_logger_isolation_test.cpp create mode 100644 tests/per_logger_mixed_mode_test.cpp create mode 100644 tests/single_thread_executor_test.cpp create mode 100644 tests/windows_debug_macro_compile_test.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7013ad2..937b14c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -276,11 +276,12 @@ jobs: -DLOGIT_CPP_BUILD_TESTS=ON \ -DLOGIT_EMSCRIPTEN=ON -DLOGIT_FORCE_ASYNC_OFF=ON - name: Build - run: cmake --build build-ems --target ems_console ems_async_flush -j + run: cmake --build build-ems --target ems_console ems_async_flush ems_single_thread_executor -j - name: Run smoke tests run: | node --no-experimental-fetch build-ems/tests/ems_console.js node --no-experimental-fetch build-ems/tests/ems_async_flush.js + node --no-experimental-fetch build-ems/tests/ems_single_thread_executor.js - name: Upload logs if: failure() uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index 16dc179..8be3de6 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,40 @@ your workload needs a different baseline. See [`docs/TaskExecutor.md`](docs/TaskExecutor.md) for a full breakdown and tuning tips. +Logger backend `Config` structs can opt into `use_dedicated_executor=true` when +one slow sink must not delay other async loggers. On native builds this creates +one worker thread per configured logger, so use it deliberately for expensive or +isolated backends. Single-threaded Emscripten builds keep the same per-instance +queue semantics but drain cooperatively on the browser event loop. + +You can always pass a configured backend through the generic macro: + +```cpp +logit::ConsoleLogger::Config cfg; +cfg.async = true; +cfg.use_dedicated_executor = true; +cfg.queue_capacity = 1024; +cfg.queue_policy = logit::detail::QueuePolicy::Block; + +LOGIT_ADD_LOGGER( + logit::ConsoleLogger, + (cfg), + logit::SimpleLogFormatter, + (LOGIT_CONSOLE_PATTERN) +); +``` + +Built-in helpers also expose config-first and short dedicated forms: + +```cpp +LOGIT_ADD_CONSOLE_CONFIG(cfg, LOGIT_CONSOLE_PATTERN); +LOGIT_ADD_CONSOLE_DEDICATED( + LOGIT_CONSOLE_PATTERN, + 1024, + logit::detail::QueuePolicy::DropNewest +); +``` + ## Features - **Flexible Log Formatting**: diff --git a/docs/TaskExecutor.md b/docs/TaskExecutor.md index 154c3bb..5074cdc 100644 --- a/docs/TaskExecutor.md +++ b/docs/TaskExecutor.md @@ -108,6 +108,13 @@ outlive static destructors inside logger components. Applications may call `shutdown()` explicitly (for example during test teardown), but the singleton remains valid until the process terminates. +Logger backends with `Config::use_dedicated_executor=true` own a +`SingleThreadExecutor` instead of using this singleton. Native builds create one +worker thread per configured logger, while single-threaded Emscripten builds use +a cooperative per-instance queue. `Logger::shutdown()` calls each backend's +`ILogger::shutdown()` hook before stopping the global executor so these +logger-owned workers drain and stop cleanly. + ## 6. Emscripten (no pthreads) When targeting Emscripten without pthread support: @@ -118,6 +125,8 @@ When targeting Emscripten without pthread support: * Tasks are executed by `emscripten_async_call`, which schedules a drain on the browser event loop. This keeps logging compatible with the cooperative execution model used in WebAssembly UI scenarios. +* Dedicated logger executors use the same cooperative scheduling model in this + build; no OS thread is created. * Typical use cases: browser-hosted tools or demos that need asynchronous-style logging without pulling in pthread support. diff --git a/docs/mainpage.dox b/docs/mainpage.dox index fd4b234..d1c0dd7 100644 --- a/docs/mainpage.dox +++ b/docs/mainpage.dox @@ -225,6 +225,33 @@ LOGIT_SET_QUEUE_POLICY(LOGIT_QUEUE_BLOCK); // Block when full \endcode Available policies: `LOGIT_QUEUE_DROP_NEWEST`, `LOGIT_QUEUE_DROP_OLDEST`, `LOGIT_QUEUE_BLOCK`. +Backend `Config` structs can set `use_dedicated_executor=true` to isolate a slow +async sink from the global task executor. Native builds create one worker thread +per configured logger; single-threaded Emscripten builds use a cooperative +per-instance queue instead. + +\code{.cpp} +logit::ConsoleLogger::Config cfg; +cfg.async = true; +cfg.use_dedicated_executor = true; +cfg.queue_capacity = 1024; +cfg.queue_policy = logit::detail::QueuePolicy::Block; + +LOGIT_ADD_LOGGER( + logit::ConsoleLogger, + (cfg), + logit::SimpleLogFormatter, + (LOGIT_CONSOLE_PATTERN) +); + +LOGIT_ADD_CONSOLE_CONFIG(cfg, LOGIT_CONSOLE_PATTERN); +LOGIT_ADD_CONSOLE_DEDICATED( + LOGIT_CONSOLE_PATTERN, + 1024, + logit::detail::QueuePolicy::DropNewest +); +\endcode + \subsection stream_logging Stream-Based Logging Use stream operators for complex messages. diff --git a/include/logit_cpp/logit/Logger.hpp b/include/logit_cpp/logit/Logger.hpp index 71432a2..21d0c4c 100644 --- a/include/logit_cpp/logit/Logger.hpp +++ b/include/logit_cpp/logit/Logger.hpp @@ -388,8 +388,8 @@ namespace logit { /// /// Ensures that all log messages are fully processed before continuing. void wait() { - LoggerReadLock lock(m_loggers_mx); - for (const auto& strategy : m_loggers) { + const auto snapshot = get_all_strategy_snapshots(); + for (const auto& strategy : snapshot) { if (!strategy) continue; strategy->logger->wait(); } @@ -400,9 +400,14 @@ namespace logit { /// Disables further logging, waits for asynchronous tasks to complete, /// and shuts down TaskExecutor. void shutdown() { - if (m_shutdown) return; - m_shutdown = true; - wait(); + if (m_shutdown.exchange(true)) return; + + const auto snapshot = get_all_strategy_snapshots(); + for (const auto& strategy : snapshot) { + if (!strategy) continue; + std::lock_guard exec_lock(strategy->exec_mx); + strategy->logger->shutdown(); + } detail::TaskExecutor::get_instance().shutdown(); } @@ -441,6 +446,11 @@ namespace logit { return std::shared_ptr(); } + std::vector> get_all_strategy_snapshots() const { + LoggerReadLock lock(m_loggers_mx); + return m_loggers; + } + std::vector> m_loggers; ///< Container for logger-formatter pairs. mutable LoggerMutex m_loggers_mx; ///< Protects access to logger strategies. std::atomic m_shutdown = ATOMIC_VAR_INIT(false); ///< Flag indicating if shutdown was requested. diff --git a/include/logit_cpp/logit/detail/QueuePolicy.hpp b/include/logit_cpp/logit/detail/QueuePolicy.hpp new file mode 100644 index 0000000..cecb6e2 --- /dev/null +++ b/include/logit_cpp/logit/detail/QueuePolicy.hpp @@ -0,0 +1,16 @@ +#pragma once +#ifndef _LOGIT_DETAIL_QUEUE_POLICY_HPP_INCLUDED +#define _LOGIT_DETAIL_QUEUE_POLICY_HPP_INCLUDED + +namespace logit { namespace detail { + +/// \brief Queue overflow handling policy used by TaskExecutor and SingleThreadExecutor. +enum class QueuePolicy { + DropNewest, ///< Reject the incoming task when the queue is full. + DropOldest, ///< Drop the oldest queued task. + Block ///< Producers wait until capacity is available. +}; + +}} // namespace logit::detail + +#endif // _LOGIT_DETAIL_QUEUE_POLICY_HPP_INCLUDED diff --git a/include/logit_cpp/logit/detail/SingleThreadExecutor.hpp b/include/logit_cpp/logit/detail/SingleThreadExecutor.hpp new file mode 100644 index 0000000..d8ca1d1 --- /dev/null +++ b/include/logit_cpp/logit/detail/SingleThreadExecutor.hpp @@ -0,0 +1,345 @@ +#pragma once +#ifndef _LOGIT_DETAIL_SINGLE_THREAD_EXECUTOR_HPP_INCLUDED +#define _LOGIT_DETAIL_SINGLE_THREAD_EXECUTOR_HPP_INCLUDED + +/// \file SingleThreadExecutor.hpp +/// \brief Per-instance single-thread executor for isolated async logging. + +#include "QueuePolicy.hpp" +#include +#include +#include +#include +#include +#if !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__) +#include +#include +#else +#include +#endif + +namespace logit { +namespace detail { + +/// \class SingleThreadExecutor +/// \brief Simplified per-logger task executor. +/// \details Provides the same public API as TaskExecutor (add_task, wait, shutdown, +/// set_max_queue_size, set_queue_policy, dropped_tasks, reset_dropped_tasks) so +/// loggers can use either executor interchangeably. Unlike the global TaskExecutor +/// singleton, each native instance owns its own worker thread, providing isolation +/// between loggers. Single-threaded Emscripten builds use a per-instance +/// cooperative queue drained through the browser event loop. +#if !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__) +class SingleThreadExecutor { +public: + /// \brief Construct and immediately start the worker thread. + SingleThreadExecutor() + : m_stop(false) + , m_shutdown_done(false) + , m_max_queue_size(0) + , m_overflow_policy(QueuePolicy::Block) + , m_dropped_tasks(0) + , m_active_tasks(0) + { + m_worker = std::thread(&SingleThreadExecutor::worker_loop, this); + } + + /// \brief Destructor drains and joins the worker thread. + ~SingleThreadExecutor() { + shutdown(); + } + + SingleThreadExecutor(const SingleThreadExecutor&) = delete; + SingleThreadExecutor& operator=(const SingleThreadExecutor&) = delete; + SingleThreadExecutor(SingleThreadExecutor&&) = delete; + SingleThreadExecutor& operator=(SingleThreadExecutor&&) = delete; + + /// \brief Enqueue a task for asynchronous execution. + void add_task(std::function task) { + if (!task) return; + std::unique_lock lock(m_mutex); + if (m_stop.load(std::memory_order_acquire)) return; + if (m_max_queue_size > 0 && m_queue.size() >= m_max_queue_size) { + switch (m_overflow_policy) { + case QueuePolicy::DropNewest: + ++m_dropped_tasks; + return; + case QueuePolicy::DropOldest: + if (!m_queue.empty()) { + m_queue.pop_front(); + ++m_dropped_tasks; + } + break; + case QueuePolicy::Block: + m_cv.wait(lock, [this]() { + return m_max_queue_size == 0 || + m_queue.size() < m_max_queue_size || + m_stop.load(std::memory_order_acquire); + }); + if (m_stop.load(std::memory_order_acquire)) return; + break; + } + } + m_queue.push_back(std::move(task)); + lock.unlock(); + m_cv.notify_one(); + } + + /// \brief Block until the queue is empty and no active tasks remain. + void wait() { + std::unique_lock lock(m_mutex); + m_cv.wait(lock, [this]() { + return (m_queue.empty() && + m_active_tasks.load(std::memory_order_relaxed) == 0) || + m_stop.load(std::memory_order_acquire); + }); + } + + /// \brief Stop accepting new tasks, drain remaining, and join the worker thread. + void shutdown() { + bool notify_worker = false; + { + std::lock_guard lock(m_mutex); + if (!m_shutdown_done.load(std::memory_order_acquire)) { + m_shutdown_done.store(true, std::memory_order_release); + m_stop.store(true, std::memory_order_release); + notify_worker = true; + } + } + if (notify_worker) { + m_cv.notify_all(); + } + if (m_worker.joinable() && m_worker.get_id() != std::this_thread::get_id()) { + m_worker.join(); + } + } + + /// \brief Change the maximum queue size (0 disables the limit). + void set_max_queue_size(std::size_t size) { + { + std::lock_guard lock(m_mutex); + if (m_stop.load(std::memory_order_acquire)) return; + m_max_queue_size = size; + } + m_cv.notify_all(); + } + + /// \brief Change the queue overflow policy. + void set_queue_policy(QueuePolicy policy) { + std::lock_guard lock(m_mutex); + m_overflow_policy = policy; + } + + /// \brief Return the number of tasks dropped by the overflow policy. + std::size_t dropped_tasks() const noexcept { + return m_dropped_tasks.load(std::memory_order_relaxed); + } + + /// \brief Reset the drop counter to zero. + void reset_dropped_tasks() noexcept { + m_dropped_tasks.store(0, std::memory_order_relaxed); + } + +private: + std::deque> m_queue; + mutable std::mutex m_mutex; + std::condition_variable m_cv; + std::thread m_worker; + std::atomic m_stop; + std::atomic m_shutdown_done; + std::size_t m_max_queue_size; + QueuePolicy m_overflow_policy; + std::atomic m_dropped_tasks; + std::atomic m_active_tasks; + + void worker_loop() { + for (;;) { + std::function task; + { + std::unique_lock lock(m_mutex); + m_cv.wait(lock, [this]() { + return !m_queue.empty() || m_stop.load(std::memory_order_acquire); + }); + if (m_stop.load(std::memory_order_acquire) && m_queue.empty()) { + break; + } + if (m_queue.empty()) continue; + task = std::move(m_queue.front()); + m_queue.pop_front(); + m_active_tasks.fetch_add(1, std::memory_order_relaxed); + } + m_cv.notify_all(); + + try { + task(); + } catch (...) { + // Suppress exceptions from user tasks. + } + + { + std::lock_guard lock(m_mutex); + m_active_tasks.fetch_sub(1, std::memory_order_relaxed); + if (m_queue.empty() && m_active_tasks.load(std::memory_order_relaxed) == 0) { + m_cv.notify_all(); + } + } + } + } +}; + +#else // single-threaded Emscripten: cooperative per-instance queue +class SingleThreadExecutor { +public: + SingleThreadExecutor() + : m_state(new State()) {} + + ~SingleThreadExecutor() { + shutdown(); + } + + SingleThreadExecutor(const SingleThreadExecutor&) = delete; + SingleThreadExecutor& operator=(const SingleThreadExecutor&) = delete; + SingleThreadExecutor(SingleThreadExecutor&&) = delete; + SingleThreadExecutor& operator=(SingleThreadExecutor&&) = delete; + + void add_task(std::function task) { + if (!task) return; + const std::shared_ptr state = m_state; + bool schedule = false; + + for (;;) { + bool drain_for_capacity = false; + { + std::lock_guard lock(state->mutex); + if (state->shutdown_requested) return; + + if (state->max_queue_size > 0 && + state->queue.size() >= state->max_queue_size) { + switch (state->overflow_policy) { + case QueuePolicy::DropNewest: + ++state->dropped_tasks; + return; + case QueuePolicy::DropOldest: + if (!state->queue.empty()) { + state->queue.pop_front(); + ++state->dropped_tasks; + } + break; + case QueuePolicy::Block: + drain_for_capacity = true; + break; + } + } + + if (drain_for_capacity && + state->max_queue_size > 0 && + state->queue.size() >= state->max_queue_size) { + // Drain outside the lock to emulate producer backpressure + // without dropping the incoming task. + } else { + state->queue.push_back(std::move(task)); + schedule = !state->scheduled; + state->scheduled = state->scheduled || schedule; + break; + } + } + + drain_state(state); + } + + if (schedule) { + schedule_drain(state); + } + } + + void wait() { + drain_state(m_state); + } + + void shutdown() { + const std::shared_ptr state = m_state; + { + std::lock_guard lock(state->mutex); + if (state->shutdown_requested) return; + state->shutdown_requested = true; + } + drain_state(state); + } + + void set_max_queue_size(std::size_t size) { + std::lock_guard lock(m_state->mutex); + if (m_state->shutdown_requested) return; + m_state->max_queue_size = size; + } + + void set_queue_policy(QueuePolicy policy) { + std::lock_guard lock(m_state->mutex); + m_state->overflow_policy = policy; + } + + std::size_t dropped_tasks() const noexcept { + return m_state->dropped_tasks.load(std::memory_order_relaxed); + } + + void reset_dropped_tasks() noexcept { + m_state->dropped_tasks.store(0, std::memory_order_relaxed); + } + +private: + struct State { + State() + : max_queue_size(0) + , overflow_policy(QueuePolicy::Block) + , dropped_tasks(0) + , scheduled(false) + , shutdown_requested(false) {} + + std::deque> queue; + std::mutex mutex; + std::size_t max_queue_size; + QueuePolicy overflow_policy; + std::atomic dropped_tasks; + bool scheduled; + bool shutdown_requested; + }; + + std::shared_ptr m_state; + + static void schedule_drain(const std::shared_ptr& state) { + std::shared_ptr* token = new std::shared_ptr(state); + emscripten_async_call(&SingleThreadExecutor::drain_thunk, token, 0); + } + + static void drain_thunk(void* arg) { + std::unique_ptr> token( + static_cast*>(arg)); + drain_state(*token); + } + + static void drain_state(const std::shared_ptr& state) { + for (;;) { + std::function task; + { + std::lock_guard lock(state->mutex); + if (state->queue.empty()) { + state->scheduled = false; + break; + } + task = std::move(state->queue.front()); + state->queue.pop_front(); + } + + try { + task(); + } catch (...) { + // Suppress exceptions from user tasks. + } + } + } +}; +#endif // !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__) + +} // namespace detail +} // namespace logit + +#endif // _LOGIT_DETAIL_SINGLE_THREAD_EXECUTOR_HPP_INCLUDED diff --git a/include/logit_cpp/logit/detail/TaskExecutor.hpp b/include/logit_cpp/logit/detail/TaskExecutor.hpp index da813c0..6893cbd 100644 --- a/include/logit_cpp/logit/detail/TaskExecutor.hpp +++ b/include/logit_cpp/logit/detail/TaskExecutor.hpp @@ -29,15 +29,10 @@ #endif #endif +#include "QueuePolicy.hpp" + namespace logit { namespace detail { - /// \brief Queue overflow handling policy used by TaskExecutor. - enum class QueuePolicy { - DropNewest, ///< Reject the incoming task when the queue is full. - DropOldest, ///< Drop the oldest queued task (or the incoming one in MPSC builds). - Block ///< Producers wait until capacity is available. - }; - # if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__) /// \class TaskExecutor @@ -531,4 +526,3 @@ namespace logit { namespace detail { }} // namespace logit::detail #endif // _LOGIT_DETAIL_TASK_EXECUTOR_HPP_INCLUDED - diff --git a/include/logit_cpp/logit/log_macros.hpp b/include/logit_cpp/logit/log_macros.hpp index 9b06542..ef85b13 100644 --- a/include/logit_cpp/logit/log_macros.hpp +++ b/include/logit_cpp/logit/log_macros.hpp @@ -1640,6 +1640,42 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::make_unique(pattern), \ true) +/// \brief Add a console logger from an explicit ConsoleLogger::Config. +#define LOGIT_ADD_CONSOLE_CONFIG(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(pattern)) + +/// \brief Add a console logger from an explicit ConsoleLogger::Config in single_mode. +#define LOGIT_ADD_CONSOLE_CONFIG_SINGLE_MODE(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(pattern), \ + true) + +/// \brief Add a console logger with explicit async executor settings. +#define LOGIT_ADD_CONSOLE_EX(pattern, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (async), (use_dedicated_executor), (queue_capacity), (queue_policy)), \ + std::make_unique(pattern)) + +/// \brief Add a console logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_CONSOLE_EX_SINGLE_MODE(pattern, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (async), (use_dedicated_executor), (queue_capacity), (queue_policy)), \ + std::make_unique(pattern), \ + true) + +/// \brief Add an async console logger backed by a dedicated executor. +#define LOGIT_ADD_CONSOLE_DEDICATED(pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_CONSOLE_EX((pattern), true, true, (queue_capacity), (queue_policy)) + +/// \brief Add an async console logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_CONSOLE_DEDICATED_SINGLE_MODE(pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_CONSOLE_EX_SINGLE_MODE((pattern), true, true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding the default console logger. /// This logger uses the default format pattern and asynchronous logging. /// This version uses `std::make_unique`, available in C++17 and later. @@ -1683,6 +1719,44 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::make_unique(pattern), \ true) +/// \brief Add a file logger from an explicit FileLogger::Config. +#define LOGIT_ADD_FILE_LOGGER_CONFIG(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(pattern)) + +/// \brief Add a file logger from an explicit FileLogger::Config in single_mode. +#define LOGIT_ADD_FILE_LOGGER_CONFIG_SINGLE_MODE(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(pattern), \ + true) + +/// \brief Add a file logger with explicit async executor settings. +#define LOGIT_ADD_FILE_LOGGER_EX(directory, async, auto_delete_days, pattern, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (directory), (async), (auto_delete_days), (use_dedicated_executor), \ + (queue_capacity), (queue_policy)), \ + std::make_unique(pattern)) + +/// \brief Add a file logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_FILE_LOGGER_EX_SINGLE_MODE(directory, async, auto_delete_days, pattern, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (directory), (async), (auto_delete_days), (use_dedicated_executor), \ + (queue_capacity), (queue_policy)), \ + std::make_unique(pattern), \ + true) + +/// \brief Add an async file logger backed by a dedicated executor. +#define LOGIT_ADD_FILE_LOGGER_DEDICATED(directory, auto_delete_days, pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_FILE_LOGGER_EX((directory), true, (auto_delete_days), (pattern), true, (queue_capacity), (queue_policy)) + +/// \brief Add an async file logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_FILE_LOGGER_DEDICATED_SINGLE_MODE(directory, auto_delete_days, pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_FILE_LOGGER_EX_SINGLE_MODE((directory), true, (auto_delete_days), (pattern), true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding the default file logger. /// This logger writes logs to the default file path and deletes logs older than the default number of days. /// This version uses `std::make_unique`, available in C++17 and later. @@ -1715,6 +1789,27 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::make_unique(pattern), \ true) +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_EX(dir, async, days, pattern, max_bytes, max_files, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (dir), (async), (days), (max_bytes), (max_files), \ + (use_dedicated_executor), (queue_capacity), (queue_policy)), \ + std::make_unique(pattern)) + +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_EX_SINGLE_MODE(dir, async, days, pattern, max_bytes, max_files, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (dir), (async), (days), (max_bytes), (max_files), \ + (use_dedicated_executor), (queue_capacity), (queue_policy)), \ + std::make_unique(pattern), \ + true) + +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_DEDICATED(dir, days, pattern, max_bytes, max_files, queue_capacity, queue_policy) \ + LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_EX((dir), true, (days), (pattern), (max_bytes), (max_files), true, (queue_capacity), (queue_policy)) + +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_DEDICATED_SINGLE_MODE(dir, days, pattern, max_bytes, max_files, queue_capacity, queue_policy) \ + LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_EX_SINGLE_MODE((dir), true, (days), (pattern), (max_bytes), (max_files), true, (queue_capacity), (queue_policy)) + #define LOGIT_ADD_FILE_LOGGER_DEFAULT_WITH_ROTATION() \ logit::Logger::get_instance().add_logger( \ std::make_unique( \ @@ -1750,6 +1845,44 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::make_unique(pattern), \ true) +/// \brief Add a unique file logger from an explicit UniqueFileLogger::Config. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_CONFIG(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(pattern)) + +/// \brief Add a unique file logger from an explicit UniqueFileLogger::Config in single_mode. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_CONFIG_SINGLE_MODE(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(pattern), \ + true) + +/// \brief Add a unique file logger with explicit async executor settings. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_EX(directory, async, auto_delete_days, hash_length, pattern, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (directory), (async), (auto_delete_days), (hash_length), \ + (use_dedicated_executor), (queue_capacity), (queue_policy)), \ + std::make_unique(pattern)) + +/// \brief Add a unique file logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_EX_SINGLE_MODE(directory, async, auto_delete_days, hash_length, pattern, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (directory), (async), (auto_delete_days), (hash_length), \ + (use_dedicated_executor), (queue_capacity), (queue_policy)), \ + std::make_unique(pattern), \ + true) + +/// \brief Add an async unique file logger backed by a dedicated executor. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_DEDICATED(directory, auto_delete_days, hash_length, pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_UNIQUE_FILE_LOGGER_EX((directory), true, (auto_delete_days), (hash_length), (pattern), true, (queue_capacity), (queue_policy)) + +/// \brief Add an async unique file logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_DEDICATED_SINGLE_MODE(directory, auto_delete_days, hash_length, pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_UNIQUE_FILE_LOGGER_EX_SINGLE_MODE((directory), true, (auto_delete_days), (hash_length), (pattern), true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding a unique file logger with default parameters. /// This macro adds a `UniqueFileLogger` with default settings, which writes each log message to a new file. /// Log files are stored in the directory specified by `LOGIT_UNIQUE_FILE_LOGGER_PATH`, using asynchronous mode @@ -1829,6 +1962,63 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT #define LOGIT_ADD_MEMORY_LOGGER_DEFAULT_SINGLE_MODE() \ LOGIT_ADD_MEMORY_LOGGER_SINGLE_MODE(1000, 1024 * 1024, 24LL * 60 * 60 * 1000) +/// \brief Macro for adding a Windows debug logger. +/// \param async Boolean indicating whether logging should be asynchronous (`true`) or synchronous (`false`). +/// This version uses `std::make_unique`, available in C++17 and later. +#define LOGIT_ADD_WINDOWS_DEBUG(async) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(logit::WindowsDebugLogger::Config{async}), \ + std::make_unique(LOGIT_CONSOLE_PATTERN)) + +/// \brief Macro for adding a Windows debug logger in single_mode. +/// \param async Boolean indicating whether logging should be asynchronous (`true`) or synchronous (`false`). +/// This version uses `std::make_unique`, available in C++17 and later. +#define LOGIT_ADD_WINDOWS_DEBUG_SINGLE_MODE(async) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(logit::WindowsDebugLogger::Config{async}), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), \ + true) + +/// \brief Add a Windows debug logger from an explicit WindowsDebugLogger::Config. +#define LOGIT_ADD_WINDOWS_DEBUG_CONFIG(config) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(LOGIT_CONSOLE_PATTERN)) + +/// \brief Add a Windows debug logger from an explicit WindowsDebugLogger::Config in single_mode. +#define LOGIT_ADD_WINDOWS_DEBUG_CONFIG_SINGLE_MODE(config) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), \ + true) + +/// \brief Add a Windows debug logger with explicit async executor settings. +#define LOGIT_ADD_WINDOWS_DEBUG_EX(async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (async), (use_dedicated_executor), (queue_capacity), (queue_policy)), \ + std::make_unique(LOGIT_CONSOLE_PATTERN)) + +/// \brief Add a Windows debug logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_WINDOWS_DEBUG_EX_SINGLE_MODE(async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (async), (use_dedicated_executor), (queue_capacity), (queue_policy)), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), \ + true) + +/// \brief Add an async Windows debug logger backed by a dedicated executor. +#define LOGIT_ADD_WINDOWS_DEBUG_DEDICATED(queue_capacity, queue_policy) \ + LOGIT_ADD_WINDOWS_DEBUG_EX(true, true, (queue_capacity), (queue_policy)) + +/// \brief Add an async Windows debug logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_WINDOWS_DEBUG_DEDICATED_SINGLE_MODE(queue_capacity, queue_policy) \ + LOGIT_ADD_WINDOWS_DEBUG_EX_SINGLE_MODE(true, true, (queue_capacity), (queue_policy)) + +/// \brief Macro for adding a Windows debug logger with default settings. +#define LOGIT_ADD_WINDOWS_DEBUG_DEFAULT() \ + LOGIT_ADD_WINDOWS_DEBUG(true) + /// \brief Macro for adding a syslog logger with custom configuration. /// \param ident Syslog identifier used to tag the log entries. /// \param facility Syslog facility value (e.g., `LOG_USER`). @@ -1850,6 +2040,42 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::make_unique(ident, facility, async), \ std::make_unique(LOGIT_CONSOLE_PATTERN), true) +/// \brief Add a syslog logger from an explicit SyslogLogger::Config. +#define LOGIT_ADD_SYSLOG_CONFIG(config) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), false) + +/// \brief Add a syslog logger from an explicit SyslogLogger::Config in single_mode. +#define LOGIT_ADD_SYSLOG_CONFIG_SINGLE_MODE(config) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), true) + +/// \brief Add a syslog logger with explicit async executor settings. +#define LOGIT_ADD_SYSLOG_EX(ident, facility, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (ident), (facility), (async), (use_dedicated_executor), \ + (queue_capacity), (queue_policy)), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), false) + +/// \brief Add a syslog logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_SYSLOG_EX_SINGLE_MODE(ident, facility, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (ident), (facility), (async), (use_dedicated_executor), \ + (queue_capacity), (queue_policy)), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), true) + +/// \brief Add an async syslog logger backed by a dedicated executor. +#define LOGIT_ADD_SYSLOG_DEDICATED(ident, facility, queue_capacity, queue_policy) \ + LOGIT_ADD_SYSLOG_EX((ident), (facility), true, true, (queue_capacity), (queue_policy)) + +/// \brief Add an async syslog logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_SYSLOG_DEDICATED_SINGLE_MODE(ident, facility, queue_capacity, queue_policy) \ + LOGIT_ADD_SYSLOG_EX_SINGLE_MODE((ident), (facility), true, true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding a syslog logger with default configuration. /// This version uses `std::make_unique`, available in C++17 and later. #define LOGIT_ADD_SYSLOG_DEFAULT() LOGIT_ADD_SYSLOG("log-it", LOG_USER, true) @@ -1873,6 +2099,42 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::make_unique(source_wide, async), \ std::make_unique(LOGIT_CONSOLE_PATTERN), true) +/// \brief Add a Windows Event Log logger from an explicit EventLogLogger::Config. +#define LOGIT_ADD_EVENT_LOG_CONFIG(config) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), false) + +/// \brief Add a Windows Event Log logger from an explicit EventLogLogger::Config in single_mode. +#define LOGIT_ADD_EVENT_LOG_CONFIG_SINGLE_MODE(config) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(config), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), true) + +/// \brief Add a Windows Event Log logger with explicit async executor settings. +#define LOGIT_ADD_EVENT_LOG_EX(source_wide, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (source_wide), (async), (use_dedicated_executor), \ + (queue_capacity), (queue_policy)), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), false) + +/// \brief Add a Windows Event Log logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_EVENT_LOG_EX_SINGLE_MODE(source_wide, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + (source_wide), (async), (use_dedicated_executor), \ + (queue_capacity), (queue_policy)), \ + std::make_unique(LOGIT_CONSOLE_PATTERN), true) + +/// \brief Add an async Windows Event Log logger backed by a dedicated executor. +#define LOGIT_ADD_EVENT_LOG_DEDICATED(source_wide, queue_capacity, queue_policy) \ + LOGIT_ADD_EVENT_LOG_EX((source_wide), true, true, (queue_capacity), (queue_policy)) + +/// \brief Add an async Windows Event Log logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_EVENT_LOG_DEDICATED_SINGLE_MODE(source_wide, queue_capacity, queue_policy) \ + LOGIT_ADD_EVENT_LOG_EX_SINGLE_MODE((source_wide), true, true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding a Windows Event Log logger with default configuration. /// This version uses `std::make_unique`, available in C++17 and later. #define LOGIT_ADD_EVENT_LOG_DEFAULT() LOGIT_ADD_EVENT_LOG(L"LogIt", true) @@ -1925,6 +2187,44 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ true) +/// \brief Add a console logger from an explicit ConsoleLogger::Config. +#define LOGIT_ADD_CONSOLE_CONFIG(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::ConsoleLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + false) + +/// \brief Add a console logger from an explicit ConsoleLogger::Config in single_mode. +#define LOGIT_ADD_CONSOLE_CONFIG_SINGLE_MODE(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::ConsoleLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +/// \brief Add a console logger with explicit async executor settings. +#define LOGIT_ADD_CONSOLE_EX(pattern, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::ConsoleLogger( \ + (async), (use_dedicated_executor), (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + false) + +/// \brief Add a console logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_CONSOLE_EX_SINGLE_MODE(pattern, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::ConsoleLogger( \ + (async), (use_dedicated_executor), (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +/// \brief Add an async console logger backed by a dedicated executor. +#define LOGIT_ADD_CONSOLE_DEDICATED(pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_CONSOLE_EX((pattern), true, true, (queue_capacity), (queue_policy)) + +/// \brief Add an async console logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_CONSOLE_DEDICATED_SINGLE_MODE(pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_CONSOLE_EX_SINGLE_MODE((pattern), true, true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding the default console logger. /// This logger uses the default format pattern and asynchronous logging. /// This version uses `new` and `std::unique_ptr` for C++11 compatibility. @@ -1971,6 +2271,44 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ true) +/// \brief Add a file logger from an explicit FileLogger::Config. +#define LOGIT_ADD_FILE_LOGGER_CONFIG(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern))) + +/// \brief Add a file logger from an explicit FileLogger::Config in single_mode. +#define LOGIT_ADD_FILE_LOGGER_CONFIG_SINGLE_MODE(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +/// \brief Add a file logger with explicit async executor settings. +#define LOGIT_ADD_FILE_LOGGER_EX(directory, async, auto_delete_days, pattern, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger( \ + (directory), (async), (auto_delete_days), (use_dedicated_executor), \ + (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern))) + +/// \brief Add a file logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_FILE_LOGGER_EX_SINGLE_MODE(directory, async, auto_delete_days, pattern, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger( \ + (directory), (async), (auto_delete_days), (use_dedicated_executor), \ + (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +/// \brief Add an async file logger backed by a dedicated executor. +#define LOGIT_ADD_FILE_LOGGER_DEDICATED(directory, auto_delete_days, pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_FILE_LOGGER_EX((directory), true, (auto_delete_days), (pattern), true, (queue_capacity), (queue_policy)) + +/// \brief Add an async file logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_FILE_LOGGER_DEDICATED_SINGLE_MODE(directory, auto_delete_days, pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_FILE_LOGGER_EX_SINGLE_MODE((directory), true, (auto_delete_days), (pattern), true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding the default file logger. /// This logger writes logs to the default file path and deletes logs older than the default number of days. /// This version uses `new` and `std::unique_ptr` for C++11 compatibility. @@ -2007,6 +2345,27 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ true) +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_EX(dir, async, days, pattern, max_bytes, max_files, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger( \ + (dir), (async), (days), (max_bytes), (max_files), \ + (use_dedicated_executor), (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern))) + +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_EX_SINGLE_MODE(dir, async, days, pattern, max_bytes, max_files, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger( \ + (dir), (async), (days), (max_bytes), (max_files), \ + (use_dedicated_executor), (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_DEDICATED(dir, days, pattern, max_bytes, max_files, queue_capacity, queue_policy) \ + LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_EX((dir), true, (days), (pattern), (max_bytes), (max_files), true, (queue_capacity), (queue_policy)) + +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_DEDICATED_SINGLE_MODE(dir, days, pattern, max_bytes, max_files, queue_capacity, queue_policy) \ + LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_EX_SINGLE_MODE((dir), true, (days), (pattern), (max_bytes), (max_files), true, (queue_capacity), (queue_policy)) + #define LOGIT_ADD_FILE_LOGGER_DEFAULT_WITH_ROTATION() \ logit::Logger::get_instance().add_logger( \ std::unique_ptr(new logit::FileLogger( \ @@ -2042,6 +2401,44 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ true) +/// \brief Add a unique file logger from an explicit UniqueFileLogger::Config. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_CONFIG(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::UniqueFileLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern))) + +/// \brief Add a unique file logger from an explicit UniqueFileLogger::Config in single_mode. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_CONFIG_SINGLE_MODE(config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::UniqueFileLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +/// \brief Add a unique file logger with explicit async executor settings. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_EX(directory, async, auto_delete_days, hash_length, pattern, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::UniqueFileLogger( \ + (directory), (async), (auto_delete_days), (hash_length), \ + (use_dedicated_executor), (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern))) + +/// \brief Add a unique file logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_EX_SINGLE_MODE(directory, async, auto_delete_days, hash_length, pattern, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::UniqueFileLogger( \ + (directory), (async), (auto_delete_days), (hash_length), \ + (use_dedicated_executor), (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +/// \brief Add an async unique file logger backed by a dedicated executor. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_DEDICATED(directory, auto_delete_days, hash_length, pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_UNIQUE_FILE_LOGGER_EX((directory), true, (auto_delete_days), (hash_length), (pattern), true, (queue_capacity), (queue_policy)) + +/// \brief Add an async unique file logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_UNIQUE_FILE_LOGGER_DEDICATED_SINGLE_MODE(directory, auto_delete_days, hash_length, pattern, queue_capacity, queue_policy) \ + LOGIT_ADD_UNIQUE_FILE_LOGGER_EX_SINGLE_MODE((directory), true, (auto_delete_days), (hash_length), (pattern), true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding the default unique file logger. /// This macro adds a `UniqueFileLogger` with default settings, which writes each log message to a new file. /// Log files are stored in the directory specified by `LOGIT_UNIQUE_FILE_LOGGER_PATH`, using asynchronous mode @@ -2123,6 +2520,65 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT #define LOGIT_ADD_MEMORY_LOGGER_DEFAULT_SINGLE_MODE() \ LOGIT_ADD_MEMORY_LOGGER_SINGLE_MODE(1000, 1024 * 1024, 24LL * 60 * 60 * 1000) +/// \brief Macro for adding a Windows debug logger. +/// \param async Boolean indicating whether logging should be asynchronous (`true`) or synchronous (`false`). +/// This version uses `new` and `std::unique_ptr` for C++11 compatibility. +#define LOGIT_ADD_WINDOWS_DEBUG(async) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::WindowsDebugLogger( \ + logit::WindowsDebugLogger::Config{async})), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN))) + +/// \brief Macro for adding a Windows debug logger in single_mode. +/// \param async Boolean indicating whether logging should be asynchronous (`true`) or synchronous (`false`). +/// This version uses `new` and `std::unique_ptr` for C++11 compatibility. +#define LOGIT_ADD_WINDOWS_DEBUG_SINGLE_MODE(async) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::WindowsDebugLogger( \ + logit::WindowsDebugLogger::Config{async})), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + true) + +/// \brief Add a Windows debug logger from an explicit WindowsDebugLogger::Config. +#define LOGIT_ADD_WINDOWS_DEBUG_CONFIG(config) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::WindowsDebugLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN))) + +/// \brief Add a Windows debug logger from an explicit WindowsDebugLogger::Config in single_mode. +#define LOGIT_ADD_WINDOWS_DEBUG_CONFIG_SINGLE_MODE(config) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::WindowsDebugLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + true) + +/// \brief Add a Windows debug logger with explicit async executor settings. +#define LOGIT_ADD_WINDOWS_DEBUG_EX(async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::WindowsDebugLogger( \ + (async), (use_dedicated_executor), (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN))) + +/// \brief Add a Windows debug logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_WINDOWS_DEBUG_EX_SINGLE_MODE(async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::WindowsDebugLogger( \ + (async), (use_dedicated_executor), (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + true) + +/// \brief Add an async Windows debug logger backed by a dedicated executor. +#define LOGIT_ADD_WINDOWS_DEBUG_DEDICATED(queue_capacity, queue_policy) \ + LOGIT_ADD_WINDOWS_DEBUG_EX(true, true, (queue_capacity), (queue_policy)) + +/// \brief Add an async Windows debug logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_WINDOWS_DEBUG_DEDICATED_SINGLE_MODE(queue_capacity, queue_policy) \ + LOGIT_ADD_WINDOWS_DEBUG_EX_SINGLE_MODE(true, true, (queue_capacity), (queue_policy)) + +/// \brief Macro for adding a Windows debug logger with default settings. +#define LOGIT_ADD_WINDOWS_DEBUG_DEFAULT() \ + LOGIT_ADD_WINDOWS_DEBUG(true) + /// \brief Macro for adding a syslog logger with custom configuration. /// \param ident Syslog identifier used to tag the log entries. /// \param facility Syslog facility value (e.g., `LOG_USER`). @@ -2146,6 +2602,46 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ true) +/// \brief Add a syslog logger from an explicit SyslogLogger::Config. +#define LOGIT_ADD_SYSLOG_CONFIG(config) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::SyslogLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + false) + +/// \brief Add a syslog logger from an explicit SyslogLogger::Config in single_mode. +#define LOGIT_ADD_SYSLOG_CONFIG_SINGLE_MODE(config) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::SyslogLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + true) + +/// \brief Add a syslog logger with explicit async executor settings. +#define LOGIT_ADD_SYSLOG_EX(ident, facility, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::SyslogLogger( \ + (ident), (facility), (async), (use_dedicated_executor), \ + (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + false) + +/// \brief Add a syslog logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_SYSLOG_EX_SINGLE_MODE(ident, facility, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::SyslogLogger( \ + (ident), (facility), (async), (use_dedicated_executor), \ + (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + true) + +/// \brief Add an async syslog logger backed by a dedicated executor. +#define LOGIT_ADD_SYSLOG_DEDICATED(ident, facility, queue_capacity, queue_policy) \ + LOGIT_ADD_SYSLOG_EX((ident), (facility), true, true, (queue_capacity), (queue_policy)) + +/// \brief Add an async syslog logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_SYSLOG_DEDICATED_SINGLE_MODE(ident, facility, queue_capacity, queue_policy) \ + LOGIT_ADD_SYSLOG_EX_SINGLE_MODE((ident), (facility), true, true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding a syslog logger with default configuration. /// This version uses `new` and `std::unique_ptr` for C++11 compatibility. #define LOGIT_ADD_SYSLOG_DEFAULT() LOGIT_ADD_SYSLOG("log-it", LOG_USER, true) @@ -2171,6 +2667,46 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ true) +/// \brief Add a Windows Event Log logger from an explicit EventLogLogger::Config. +#define LOGIT_ADD_EVENT_LOG_CONFIG(config) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::EventLogLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + false) + +/// \brief Add a Windows Event Log logger from an explicit EventLogLogger::Config in single_mode. +#define LOGIT_ADD_EVENT_LOG_CONFIG_SINGLE_MODE(config) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::EventLogLogger(config)), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + true) + +/// \brief Add a Windows Event Log logger with explicit async executor settings. +#define LOGIT_ADD_EVENT_LOG_EX(source_wide, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::EventLogLogger( \ + (source_wide), (async), (use_dedicated_executor), \ + (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + false) + +/// \brief Add a Windows Event Log logger with explicit async executor settings in single_mode. +#define LOGIT_ADD_EVENT_LOG_EX_SINGLE_MODE(source_wide, async, use_dedicated_executor, queue_capacity, queue_policy) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::EventLogLogger( \ + (source_wide), (async), (use_dedicated_executor), \ + (queue_capacity), (queue_policy))), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN)), \ + true) + +/// \brief Add an async Windows Event Log logger backed by a dedicated executor. +#define LOGIT_ADD_EVENT_LOG_DEDICATED(source_wide, queue_capacity, queue_policy) \ + LOGIT_ADD_EVENT_LOG_EX((source_wide), true, true, (queue_capacity), (queue_policy)) + +/// \brief Add an async Windows Event Log logger backed by a dedicated executor in single_mode. +#define LOGIT_ADD_EVENT_LOG_DEDICATED_SINGLE_MODE(source_wide, queue_capacity, queue_policy) \ + LOGIT_ADD_EVENT_LOG_EX_SINGLE_MODE((source_wide), true, true, (queue_capacity), (queue_policy)) + /// \brief Macro for adding a Windows Event Log logger with default configuration. /// This version uses `new` and `std::unique_ptr` for C++11 compatibility. #define LOGIT_ADD_EVENT_LOG_DEFAULT() LOGIT_ADD_EVENT_LOG(L"LogIt", true) diff --git a/include/logit_cpp/logit/loggers.hpp b/include/logit_cpp/logit/loggers.hpp index 8b820a3..fc063d2 100644 --- a/include/logit_cpp/logit/loggers.hpp +++ b/include/logit_cpp/logit/loggers.hpp @@ -12,6 +12,7 @@ #include "config.hpp" #include "utils.hpp" #include "detail/TaskExecutor.hpp" +#include "detail/SingleThreadExecutor.hpp" #ifndef __EMSCRIPTEN__ #include "detail/CompressionWorker.hpp" #endif @@ -25,6 +26,7 @@ #include "loggers/EventLogLogger.hpp" #include "loggers/SystemLogger.hpp" #include "loggers/CrashLogger.hpp" +#include "loggers/WindowsDebugLogger.hpp" #ifdef LOGIT_WITH_OTLP #include "loggers/OtlpHttpLogger.hpp" diff --git a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp index c4406d0..350b028 100644 --- a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp +++ b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp @@ -6,7 +6,9 @@ /// \brief Console logger implementation that outputs logs to the console with color support. #include "ILogger.hpp" +#include #include +#include #if defined(_WIN32) #include #endif @@ -69,6 +71,9 @@ namespace logit { #else bool async = true; ///< Flag indicating whether logging should be asynchronous. #endif + bool use_dedicated_executor = false; ///< Use a dedicated executor instead of the global TaskExecutor; native builds create one worker thread per logger. + std::size_t queue_capacity = 0; ///< Maximum queue size for the dedicated executor (0 = unlimited). + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; ///< Overflow policy for the dedicated executor. }; /// \brief Default constructor that uses default configuration. @@ -80,6 +85,10 @@ namespace logit { /// \param config The configuration for the logger. ConsoleLogger(const Config& config) : m_config(config) { reset_color(); + if (m_config.async && m_config.use_dedicated_executor) { + m_executor.reset(new detail::SingleThreadExecutor()); + configure_executor(m_executor, m_config); + } } /// \brief Constructor with asynchronous flag. @@ -89,14 +98,44 @@ namespace logit { reset_color(); } - virtual ~ConsoleLogger() = default; + /// \brief Constructor with asynchronous and dedicated executor options. + ConsoleLogger( + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity = 0, + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block) + : ConsoleLogger(make_config( + async, + use_dedicated_executor, + queue_capacity, + queue_policy)) {} + + virtual ~ConsoleLogger() { + shutdown(); + } /// \brief Sets the logger configuration. /// This method sets the logger's configuration and ensures thread safety with a mutex lock. /// \param config The new configuration. void set_config(const Config& config) { - std::lock_guard lock(m_mutex); - m_config = config; + std::shared_ptr old_executor; + { + std::unique_lock lock(m_mutex); + wait_for_pending_enqueues(lock); + m_config = config; + if (m_config.async && m_config.use_dedicated_executor) { + if (!m_executor) { + m_executor.reset(new detail::SingleThreadExecutor()); + } + configure_executor(m_executor, m_config); + } else if (m_executor) { + old_executor = m_executor; + m_executor.reset(); + } + } + if (old_executor) { + old_executor->shutdown(); + } } /// \brief Gets the current logger configuration. @@ -119,6 +158,7 @@ namespace logit { #ifdef __EMSCRIPTEN__ std::unique_lock lock(m_mutex); const int lvl = static_cast(record.log_level); + std::shared_ptr executor = m_executor; if (!m_config.async) { # if defined(LOGIT_EM_BROWSER_COLORS) log_ansi_js(lvl, message.c_str(), text_color_to_css(m_config.default_color)); @@ -129,18 +169,32 @@ namespace logit { } auto msg_copy = std::string(message); const auto def_color = m_config.default_color; + ++m_pending_enqueues; lock.unlock(); - detail::TaskExecutor::get_instance().add_task([this, lvl, msg_copy, def_color]() { - std::lock_guard inner_lock(m_mutex); + PendingEnqueue pending_enqueue(*this); + if (executor) { + executor->add_task([this, lvl, msg_copy, def_color]() { + std::lock_guard inner_lock(m_mutex); # if defined(LOGIT_EM_BROWSER_COLORS) - log_ansi_js(lvl, msg_copy.c_str(), text_color_to_css(def_color)); + log_ansi_js(lvl, msg_copy.c_str(), text_color_to_css(def_color)); # else - log_level(lvl, msg_copy.c_str()); + log_level(lvl, msg_copy.c_str()); # endif - }); + }); + } else { + detail::TaskExecutor::get_instance().add_task([this, lvl, msg_copy, def_color]() { + std::lock_guard inner_lock(m_mutex); +# if defined(LOGIT_EM_BROWSER_COLORS) + log_ansi_js(lvl, msg_copy.c_str(), text_color_to_css(def_color)); +# else + log_level(lvl, msg_copy.c_str()); +# endif + }); + } return; #else std::unique_lock lock(m_mutex); + std::shared_ptr executor = m_executor; if (!m_config.async) { # if defined(_WIN32) // For Windows, parse the message for ANSI color codes and apply them @@ -151,17 +205,32 @@ namespace logit { # endif return; } + ++m_pending_enqueues; lock.unlock(); - detail::TaskExecutor::get_instance().add_task([this, message](){ - std::lock_guard lock(m_mutex); + PendingEnqueue pending_enqueue(*this); + if (executor) { + executor->add_task([this, message](){ + std::lock_guard lock(m_mutex); # if defined(_WIN32) - // For Windows, parse the message for ANSI color codes and apply them - handle_ansi_colors_windows(message); + // For Windows, parse the message for ANSI color codes and apply them + handle_ansi_colors_windows(message); # else - // For other systems, output the message as is - std::cout << message << std::endl; + // For other systems, output the message as is + std::cout << message << std::endl; # endif - }); + }); + } else { + detail::TaskExecutor::get_instance().add_task([this, message](){ + std::lock_guard lock(m_mutex); +# if defined(_WIN32) + // For Windows, parse the message for ANSI color codes and apply them + handle_ansi_colors_windows(message); +# else + // For other systems, output the message as is + std::cout << message << std::endl; +# endif + }); + } #endif } @@ -224,9 +293,33 @@ namespace logit { /// If asynchronous logging is enabled, waits for all pending log messages to be written. void wait() override { std::unique_lock lock(m_mutex); + wait_for_pending_enqueues(lock); + std::shared_ptr executor = m_executor; if (!m_config.async) return; lock.unlock(); - detail::TaskExecutor::get_instance().wait(); + if (executor) { + executor->wait(); + } else { + detail::TaskExecutor::get_instance().wait(); + } + } + + /// \brief Stops the dedicated executor after draining pending messages. + void shutdown() override { + std::shared_ptr executor; + bool async = false; + { + std::unique_lock lock(m_mutex); + wait_for_pending_enqueues(lock); + async = m_config.async; + executor = m_executor; + m_executor.reset(); + } + if (executor) { + executor->shutdown(); + } else if (async) { + detail::TaskExecutor::get_instance().wait(); + } } private: @@ -234,6 +327,59 @@ namespace logit { Config m_config; ///< Configuration for the console logger. std::atomic m_last_log_ts = ATOMIC_VAR_INIT(0); std::atomic m_log_level = ATOMIC_VAR_INIT(static_cast(LogLevel::LOG_LVL_TRACE)); + std::shared_ptr m_executor; + std::condition_variable m_enqueue_cv; + std::size_t m_pending_enqueues = 0; + + static void configure_executor( + const std::shared_ptr& executor, + const Config& config) { + if (!executor) return; + executor->set_max_queue_size(config.queue_capacity); + executor->set_queue_policy(config.queue_policy); + } + + static Config make_config( + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) { + Config config; + config.async = async; + config.use_dedicated_executor = use_dedicated_executor; + config.queue_capacity = queue_capacity; + config.queue_policy = queue_policy; + return config; + } + + class PendingEnqueue { + public: + explicit PendingEnqueue(ConsoleLogger& logger) : m_logger(&logger) {} + ~PendingEnqueue() { + if (m_logger) { + m_logger->finish_pending_enqueue(); + } + } + PendingEnqueue(const PendingEnqueue&) = delete; + PendingEnqueue& operator=(const PendingEnqueue&) = delete; + + private: + ConsoleLogger* m_logger; + }; + + void wait_for_pending_enqueues(std::unique_lock& lock) { + m_enqueue_cv.wait(lock, [this]() { return m_pending_enqueues == 0; }); + } + + void finish_pending_enqueue() { + std::lock_guard lock(m_mutex); + if (m_pending_enqueues > 0) { + --m_pending_enqueues; + } + if (m_pending_enqueues == 0) { + m_enqueue_cv.notify_all(); + } + } # ifdef __EMSCRIPTEN__ /// \brief Convert TextColor to a CSS color name. diff --git a/include/logit_cpp/logit/loggers/EventLogLogger.hpp b/include/logit_cpp/logit/loggers/EventLogLogger.hpp index ee00841..8ae75d6 100644 --- a/include/logit_cpp/logit/loggers/EventLogLogger.hpp +++ b/include/logit_cpp/logit/loggers/EventLogLogger.hpp @@ -5,6 +5,7 @@ #include "ILogger.hpp" #include #include +#include /// \file EventLogLogger.hpp /// \brief Logger writing to Windows Event Log. @@ -29,6 +30,9 @@ namespace logit { struct Config { const wchar_t* source; ///< Event source name. bool async; ///< Use TaskExecutor when true. + bool use_dedicated_executor = false; ///< Use a dedicated executor instead of the global TaskExecutor; native builds create one worker thread per logger. + std::size_t queue_capacity = 0; ///< Maximum queue size for the dedicated executor (0 = unlimited). + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; ///< Overflow policy for the dedicated executor. /// \brief Initialize configuration. /// \param s Source name. /// \param a Run asynchronously. @@ -42,6 +46,11 @@ namespace logit { /// \param c Configuration options. explicit EventLogLogger(const Config& c) : m_cfg(c) { m_hsrc = RegisterEventSourceW(nullptr, m_cfg.source); + if (m_cfg.async && m_cfg.use_dedicated_executor) { + m_executor.reset(new detail::SingleThreadExecutor()); + m_executor->set_max_queue_size(m_cfg.queue_capacity); + m_executor->set_queue_policy(m_cfg.queue_policy); + } } /// \brief Construct with explicit parameters. @@ -50,8 +59,22 @@ namespace logit { EventLogLogger(const wchar_t* source, bool async) : EventLogLogger(Config(source, async)) {} + /// \brief Construct with explicit parameters and dedicated executor options. + EventLogLogger( + const wchar_t* source, + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) + : EventLogLogger(make_config( + source, + async, + use_dedicated_executor, + queue_capacity, + queue_policy)) {} + /// \brief Deregister event source on destruction. - ~EventLogLogger() override { if (m_hsrc) DeregisterEventSource(m_hsrc); } + ~EventLogLogger() override { shutdown(); if (m_hsrc) DeregisterEventSource(m_hsrc); } /// \brief Send message to Event Log. /// \param rec Log metadata. @@ -70,7 +93,7 @@ namespace logit { LPCWSTR arr[1] = { wmsg.c_str() }; ReportEventW(m_hsrc, type, 0, 0, nullptr, 1, 0, arr, nullptr); }; - if (m_cfg.async) { detail::TaskExecutor::get_instance().add_task(task); } + if (m_cfg.async) { if (m_executor) { m_executor->add_task(task); } else { detail::TaskExecutor::get_instance().add_task(task); } } else { task(); } m_last_ts.store(rec.timestamp_ms); } @@ -99,7 +122,10 @@ namespace logit { LogLevel get_log_level() const override { return static_cast(m_level.load()); } /// \brief Wait for asynchronous tasks to finish. - void wait() override { if (m_cfg.async) detail::TaskExecutor::get_instance().wait(); } + void wait() override { if (m_cfg.async) { if (m_executor) { m_executor->wait(); } else { detail::TaskExecutor::get_instance().wait(); } } } + + /// \brief Stops logger-owned asynchronous resources after draining pending messages. + void shutdown() override { if (m_executor) { m_executor->shutdown(); } else if (m_cfg.async) { detail::TaskExecutor::get_instance().wait(); } } private: static WORD m_map(LogLevel l) { @@ -116,6 +142,20 @@ namespace logit { HANDLE m_hsrc = nullptr; std::atomic m_level{static_cast(LogLevel::LOG_LVL_TRACE)}; std::atomic m_last_ts{0}; + std::unique_ptr m_executor; + + static Config make_config( + const wchar_t* source, + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) { + Config config(source, async); + config.use_dedicated_executor = use_dedicated_executor; + config.queue_capacity = queue_capacity; + config.queue_policy = queue_policy; + return config; + } }; # else // stub @@ -127,6 +167,9 @@ namespace logit { struct Config { const wchar_t* source; ///< Unused source name. bool async; ///< Unused flag. + bool use_dedicated_executor = false; ///< Unused flag. + std::size_t queue_capacity = 0; ///< Unused. + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; ///< Unused. Config(const wchar_t* s = L"", bool a = false) : source(s), async(a) {} }; @@ -142,6 +185,13 @@ namespace logit { /// \param async Ignored flag. EventLogLogger(const wchar_t* source, bool async) { (void)source; (void)async; } + /// \brief Construct with parameters and ignored dedicated executor options. + EventLogLogger(const wchar_t* source, bool async, bool use_dedicated_executor, + std::size_t queue_capacity, detail::QueuePolicy queue_policy) { + (void)source; (void)async; (void)use_dedicated_executor; + (void)queue_capacity; (void)queue_policy; + } + /// \brief Ignore log request. /// \param rec Log metadata. /// \param msg Message text. diff --git a/include/logit_cpp/logit/loggers/FileLogger.hpp b/include/logit_cpp/logit/loggers/FileLogger.hpp index 61a9af5..1e7629f 100644 --- a/include/logit_cpp/logit/loggers/FileLogger.hpp +++ b/include/logit_cpp/logit/loggers/FileLogger.hpp @@ -41,12 +41,19 @@ namespace logit { std::string external_cmd; RotationNaming naming = RotationNaming::Sequence; uint32_t seq_width = 3; + bool use_dedicated_executor = false; + std::size_t queue_capacity = 0; + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; }; FileLogger() { warn(); } FileLogger(const Config&) { warn(); } FileLogger(const std::string&, const bool& = true, const int& = 30, const uint64_t& = 0, const uint32_t& = 0) { warn(); } + FileLogger(const std::string&, const bool&, const int&, bool, std::size_t, + detail::QueuePolicy) { warn(); } + FileLogger(const std::string&, const bool&, const int&, uint64_t, uint32_t, + bool, std::size_t, detail::QueuePolicy) { warn(); } void log(const LogRecord&, const std::string&) override { warn(); } std::string get_string_param(const LoggerParam&) const override { return {}; } @@ -97,6 +104,9 @@ namespace logit { std::string external_cmd; ///< External command template. RotationNaming naming = RotationNaming::Sequence; ///< Naming policy for rotated files. uint32_t seq_width = 3; ///< Width of sequence index. + bool use_dedicated_executor = false; ///< Use a dedicated executor instead of the global TaskExecutor; native builds create one worker thread per logger. + std::size_t queue_capacity = 0; ///< Maximum queue size for the dedicated executor (0 = unlimited). + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; ///< Overflow policy for the dedicated executor. }; /// \brief Default constructor that uses default configuration. @@ -107,6 +117,11 @@ namespace logit { /// \brief Constructor with custom configuration. /// \param config The configuration for the logger. FileLogger(const Config& config) : m_config(config) { + if (m_config.async && m_config.use_dedicated_executor) { + m_executor.reset(new detail::SingleThreadExecutor()); + m_executor->set_max_queue_size(m_config.queue_capacity); + m_executor->set_queue_policy(m_config.queue_policy); + } start_logging(); } @@ -124,6 +139,24 @@ namespace logit { start_logging(); } + /// \brief Constructor with directory, async flag, and dedicated executor options. + FileLogger( + const std::string& directory, + const bool& async, + const int& auto_delete_days, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) + : FileLogger(make_config( + directory, + async, + auto_delete_days, + 0, + 0, + use_dedicated_executor, + queue_capacity, + queue_policy)) {} + /// \brief Constructor with directory, size-based rotation and additional options. FileLogger( const std::string& directory, @@ -139,8 +172,29 @@ namespace logit { start_logging(); } + /// \brief Constructor with directory, rotation, and dedicated executor options. + FileLogger( + const std::string& directory, + const bool& async, + const int& auto_delete_days, + uint64_t max_file_size_bytes, + uint32_t max_rotated_files, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) + : FileLogger(make_config( + directory, + async, + auto_delete_days, + max_file_size_bytes, + max_rotated_files, + use_dedicated_executor, + queue_capacity, + queue_policy)) {} + /// \brief Destructor to stop logging and close file. virtual ~FileLogger() { + shutdown(); stop_logging(); if (m_compressor) m_compressor->wait(); } @@ -165,14 +219,25 @@ namespace logit { return; } auto timestamp_ms = record.timestamp_ms; - detail::TaskExecutor::get_instance().add_task([this, message, timestamp_ms]() { - std::lock_guard lock(m_mutex); - try { - write_log(message, timestamp_ms); - } catch (const std::exception& e) { - std::cerr << "Log async log error: " << e.what() << std::endl; - } - }); + if (m_executor) { + m_executor->add_task([this, message, timestamp_ms]() { + std::lock_guard lock(m_mutex); + try { + write_log(message, timestamp_ms); + } catch (const std::exception& e) { + std::cerr << "Log async log error: " << e.what() << std::endl; + } + }); + } else { + detail::TaskExecutor::get_instance().add_task([this, message, timestamp_ms]() { + std::lock_guard lock(m_mutex); + try { + write_log(message, timestamp_ms); + } catch (const std::exception& e) { + std::cerr << "Log async log error: " << e.what() << std::endl; + } + }); + } } /// \brief Retrieves a string parameter from the logger. @@ -324,11 +389,26 @@ namespace logit { /// \brief Waits for all asynchronous tasks to complete. void wait() override { if (!m_config.async) return; - detail::TaskExecutor::get_instance().wait(); + if (m_executor) { + m_executor->wait(); + } else { + detail::TaskExecutor::get_instance().wait(); + } std::lock_guard lock(m_mutex); if (m_file.is_open()) m_file.flush(); } + /// \brief Stops logger-owned asynchronous resources after draining pending writes. + void shutdown() override { + if (m_executor) { + m_executor->shutdown(); + std::lock_guard lock(m_mutex); + if (m_file.is_open()) m_file.flush(); + } else if (m_config.async) { + wait(); + } + } + private: mutable std::mutex m_mutex; ///< Mutex to protect file operations. Config m_config; ///< Configuration for the file logger. @@ -339,10 +419,32 @@ namespace logit { int64_t m_current_date_ts = 0; ///< Timestamp of the current log file's date. uint64_t m_current_file_size = 0; ///< Current size of the log file. std::unique_ptr m_compressor; ///< Background compressor. + std::unique_ptr m_executor; ///< Dedicated executor (null = use global). std::atomic m_last_log_ts = ATOMIC_VAR_INIT(0); ///< Timestamp of the last log. std::atomic m_last_log_mono_ts = ATOMIC_VAR_INIT(0); ///< Timestamp of the last log. std::atomic m_log_level = ATOMIC_VAR_INIT(static_cast(LogLevel::LOG_LVL_TRACE)); + static Config make_config( + const std::string& directory, + bool async, + int auto_delete_days, + uint64_t max_file_size_bytes, + uint32_t max_rotated_files, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) { + Config config; + config.directory = directory; + config.async = async; + config.auto_delete_days = auto_delete_days; + config.max_file_size_bytes = max_file_size_bytes; + config.max_rotated_files = max_rotated_files; + config.use_dedicated_executor = use_dedicated_executor; + config.queue_capacity = queue_capacity; + config.queue_policy = queue_policy; + return config; + } + /// \brief Starts the logging process by initializing the file and directory. void start_logging() { // I/O streams (e.g., std::cin, std::cout, std::cerr) may be closed before the program exits. diff --git a/include/logit_cpp/logit/loggers/ILogger.hpp b/include/logit_cpp/logit/loggers/ILogger.hpp index 2298f6c..b7d1ca4 100644 --- a/include/logit_cpp/logit/loggers/ILogger.hpp +++ b/include/logit_cpp/logit/loggers/ILogger.hpp @@ -113,6 +113,14 @@ namespace logit { /// This pure virtual function must be implemented by derived logger classes. /// It ensures that any pending log messages are fully processed, especially when logging asynchronously. virtual void wait() = 0; + + /// \brief Stops logger-owned asynchronous resources after draining pending work. + /// \details Custom loggers may override this when they own worker + /// threads or queues. The default implementation preserves the + /// existing custom logger contract by delegating to `wait()`. + virtual void shutdown() { + wait(); + } }; // ILogger }; // namespace logit diff --git a/include/logit_cpp/logit/loggers/SyslogLogger.hpp b/include/logit_cpp/logit/loggers/SyslogLogger.hpp index 501371b..ae9f58e 100644 --- a/include/logit_cpp/logit/loggers/SyslogLogger.hpp +++ b/include/logit_cpp/logit/loggers/SyslogLogger.hpp @@ -5,6 +5,7 @@ #include "ILogger.hpp" #include #include +#include /// \file SyslogLogger.hpp /// \brief Logger writing to system syslog. @@ -30,6 +31,9 @@ namespace logit { const char* ident; ///< Identifier passed to openlog. int facility; ///< Syslog facility. bool async; ///< Use TaskExecutor when true. + bool use_dedicated_executor = false; ///< Use a dedicated executor instead of the global TaskExecutor; native builds create one worker thread per logger. + std::size_t queue_capacity = 0; ///< Maximum queue size for the dedicated executor (0 = unlimited). + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; ///< Overflow policy for the dedicated executor. /// \brief Initialize configuration. /// \param i Identifier string. /// \param f Facility code. @@ -45,6 +49,11 @@ namespace logit { /// \param c Configuration options. explicit SyslogLogger(const Config& c) : m_cfg(c) { openlog(m_cfg.ident, LOG_PID | LOG_NDELAY, m_cfg.facility); + if (m_cfg.async && m_cfg.use_dedicated_executor) { + m_executor.reset(new detail::SingleThreadExecutor()); + m_executor->set_max_queue_size(m_cfg.queue_capacity); + m_executor->set_queue_policy(m_cfg.queue_policy); + } } /// \brief Construct with explicit parameters. @@ -54,8 +63,24 @@ namespace logit { SyslogLogger(const char* ident, int facility, bool async) : SyslogLogger(Config(ident, facility, async)) {} + /// \brief Construct with explicit parameters and dedicated executor options. + SyslogLogger( + const char* ident, + int facility, + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) + : SyslogLogger(make_config( + ident, + facility, + async, + use_dedicated_executor, + queue_capacity, + queue_policy)) {} + /// \brief Close syslog on destruction. - ~SyslogLogger() override { closelog(); } + ~SyslogLogger() override { shutdown(); closelog(); } /// \brief Send message to syslog. /// \param rec Log metadata. @@ -68,7 +93,7 @@ namespace logit { if (!raw_mode && static_cast(lvl) < m_level.load()) return; syslog(m_map(lvl), "%s", s.c_str()); }; - if (m_cfg.async) { detail::TaskExecutor::get_instance().add_task(task); } + if (m_cfg.async) { if (m_executor) { m_executor->add_task(task); } else { detail::TaskExecutor::get_instance().add_task(task); } } else { task(); } m_last_ts.store(rec.timestamp_ms); } @@ -96,7 +121,10 @@ namespace logit { LogLevel get_log_level() const override { return static_cast(m_level.load()); } /// \brief Wait for asynchronous tasks to finish. - void wait() override { if (m_cfg.async) detail::TaskExecutor::get_instance().wait(); } + void wait() override { if (m_cfg.async) { if (m_executor) { m_executor->wait(); } else { detail::TaskExecutor::get_instance().wait(); } } } + + /// \brief Stops logger-owned asynchronous resources after draining pending messages. + void shutdown() override { if (m_executor) { m_executor->shutdown(); } else if (m_cfg.async) { detail::TaskExecutor::get_instance().wait(); } } private: static int m_map(LogLevel l) { @@ -112,6 +140,21 @@ namespace logit { Config m_cfg{}; std::atomic m_level{static_cast(LogLevel::LOG_LVL_TRACE)}; std::atomic m_last_ts{0}; + std::unique_ptr m_executor; + + static Config make_config( + const char* ident, + int facility, + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) { + Config config(ident, facility, async); + config.use_dedicated_executor = use_dedicated_executor; + config.queue_capacity = queue_capacity; + config.queue_policy = queue_policy; + return config; + } }; # else // stub on unsupported @@ -124,6 +167,9 @@ namespace logit { const char* ident; ///< Unused identifier. int facility; ///< Unused facility. bool async; ///< Unused flag. + bool use_dedicated_executor = false; ///< Unused flag. + std::size_t queue_capacity = 0; ///< Unused. + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; ///< Unused. Config(const char* i="", int f=0, bool a=false) : ident(i), facility(f), async(a) {} }; @@ -140,6 +186,13 @@ namespace logit { /// \param async Ignored flag. SyslogLogger(const char* ident,int facility,bool async) { (void)ident; (void)facility; (void)async; } + /// \brief Construct with parameters and ignored dedicated executor options. + SyslogLogger(const char* ident, int facility, bool async, bool use_dedicated_executor, + std::size_t queue_capacity, detail::QueuePolicy queue_policy) { + (void)ident; (void)facility; (void)async; (void)use_dedicated_executor; + (void)queue_capacity; (void)queue_policy; + } + /// \brief Ignore log request. /// \param rec Log metadata. /// \param msg Message text. diff --git a/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp b/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp index 9553063..ffeb38e 100644 --- a/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp +++ b/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp @@ -19,6 +19,7 @@ #include #include #include +#include namespace logit { @@ -31,11 +32,16 @@ namespace logit { bool async = false; int auto_delete_days = 30; size_t hash_length = 8; + bool use_dedicated_executor = false; + std::size_t queue_capacity = 0; + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; }; UniqueFileLogger() { warn(); } UniqueFileLogger(const Config&) { warn(); } UniqueFileLogger(const std::string&, bool = true, int = 30, size_t = 8) { warn(); } + UniqueFileLogger(const std::string&, bool, int, size_t, bool, std::size_t, + detail::QueuePolicy) { warn(); } void log(const LogRecord&, const std::string&) override { warn(); } std::string get_string_param(const LoggerParam&) const override { return {}; } @@ -80,6 +86,9 @@ namespace logit { bool async = true; ///< Flag indicating whether logging should be asynchronous. int auto_delete_days = 30; ///< Number of days after which old log files are deleted. size_t hash_length = 8; ///< Length of the hash used in filenames. + bool use_dedicated_executor = false; ///< Use a dedicated executor instead of the global TaskExecutor; native builds create one worker thread per logger. + std::size_t queue_capacity = 0; ///< Maximum queue size for the dedicated executor (0 = unlimited). + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block; ///< Overflow policy for the dedicated executor. }; /// \brief Default constructor that uses default configuration. @@ -90,6 +99,11 @@ namespace logit { /// \brief Constructor with custom configuration. /// \param config The configuration for the logger. UniqueFileLogger(const Config& config) : m_config(config) { + if (m_config.async && m_config.use_dedicated_executor) { + m_executor.reset(new detail::SingleThreadExecutor()); + m_executor->set_max_queue_size(m_config.queue_capacity); + m_executor->set_queue_policy(m_config.queue_policy); + } start_logging(); } @@ -110,7 +124,26 @@ namespace logit { start_logging(); } + /// \brief Constructor with directory, async flag, and dedicated executor options. + UniqueFileLogger( + const std::string& directory, + bool async, + int auto_delete_days, + size_t hash_length, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) + : UniqueFileLogger(make_config( + directory, + async, + auto_delete_days, + hash_length, + use_dedicated_executor, + queue_capacity, + queue_policy)) {} + virtual ~UniqueFileLogger() { + shutdown(); stop_logging(); } @@ -170,7 +203,7 @@ namespace logit { info_lock.unlock(); auto timestamp_ms = record.timestamp_ms; - detail::TaskExecutor::get_instance().add_task([this, message, timestamp_ms, thread_id]() { + auto async_task = [this, message, timestamp_ms, thread_id]() { std::lock_guard lock(m_mutex); std::string file_path; try { @@ -203,7 +236,12 @@ namespace logit { } catch (const std::exception& e) { std::cerr << "Async log error: " << e.what() << std::endl; } - }); + }; + if (m_executor) { + m_executor->add_task(std::move(async_task)); + } else { + detail::TaskExecutor::get_instance().add_task(std::move(async_task)); + } } /// \brief Retrieves a string parameter from the logger. @@ -262,12 +300,26 @@ namespace logit { /// \brief Waits for all asynchronous tasks to complete. void wait() override { if (!m_config.async) return; - detail::TaskExecutor::get_instance().wait(); + if (m_executor) { + m_executor->wait(); + } else { + detail::TaskExecutor::get_instance().wait(); + } + } + + /// \brief Stops logger-owned asynchronous resources after draining pending writes. + void shutdown() override { + if (m_executor) { + m_executor->shutdown(); + } else if (m_config.async) { + detail::TaskExecutor::get_instance().wait(); + } } private: mutable std::mutex m_mutex; ///< Mutex to protect file operations. Config m_config; ///< Configuration for the unique file logger. + std::unique_ptr m_executor; ///< Dedicated executor (null = use global). struct ThreadLogInfo { int pending_logs; @@ -294,6 +346,24 @@ namespace logit { std::atomic m_last_log_mono_ts = ATOMIC_VAR_INIT(0); ///< Timestamp of the last log. std::atomic m_log_level = ATOMIC_VAR_INIT(static_cast(LogLevel::LOG_LVL_TRACE)); + static Config make_config( + const std::string& directory, + bool async, + int auto_delete_days, + size_t hash_length, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) { + Config config; + config.directory = directory; + config.async = async; + config.auto_delete_days = auto_delete_days; + config.hash_length = hash_length; + config.use_dedicated_executor = use_dedicated_executor; + config.queue_capacity = queue_capacity; + config.queue_policy = queue_policy; + return config; + } /// \brief Starts the logging process by initializing the directory and removing old logs. void start_logging() { diff --git a/include/logit_cpp/logit/loggers/WindowsDebugLogger.hpp b/include/logit_cpp/logit/loggers/WindowsDebugLogger.hpp new file mode 100644 index 0000000..55207e0 --- /dev/null +++ b/include/logit_cpp/logit/loggers/WindowsDebugLogger.hpp @@ -0,0 +1,217 @@ +#pragma once +#ifndef _LOGIT_WINDOWS_DEBUG_LOGGER_HPP_INCLUDED +#define _LOGIT_WINDOWS_DEBUG_LOGGER_HPP_INCLUDED + +/// \file WindowsDebugLogger.hpp +/// \brief Logger that writes to the Windows debug output (OutputDebugStringW) or stderr on other platforms. + +#include "ILogger.hpp" +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +namespace logit { + + /// \class WindowsDebugLogger + /// \ingroup LogBackends + /// \brief Outputs formatted log messages to the Windows debug console or stderr. + /// + /// On Windows this logger forwards messages to `OutputDebugStringW()`, making them + /// visible in Visual Studio Output, DebugView (Sysinternals), WinDbg and other + /// debuggers. On non-Windows platforms it falls back to `std::cerr`. + /// + /// **Key Features:** + /// - No-op when no debugger is attached (OutputDebugStringW cost is minimal). + /// - Synchronous or asynchronous operation. + /// - Thread-safe. + class WindowsDebugLogger : public ILogger { + public: + + /// \struct Config + /// \brief Configuration for the Windows debug logger. + struct Config { +#ifdef __EMSCRIPTEN__ + /// \brief Initializes configuration. + explicit Config(bool async_value = false) +#else + /// \brief Initializes configuration. + explicit Config(bool async_value = true) +#endif + : async(async_value) + , use_dedicated_executor(false) + , queue_capacity(0) + , queue_policy(detail::QueuePolicy::Block) {} + + bool async; ///< Flag indicating whether logging should be asynchronous. + bool use_dedicated_executor; ///< Use a dedicated executor instead of the global TaskExecutor; native builds create one worker thread per logger. + std::size_t queue_capacity; ///< Maximum queue size for the dedicated executor (0 = unlimited). + detail::QueuePolicy queue_policy; ///< Overflow policy for the dedicated executor. + }; + + /// \brief Default constructor that uses default configuration. + WindowsDebugLogger() : WindowsDebugLogger(Config()) {} + + /// \brief Constructor with custom configuration. + /// \param config The configuration for the logger. + explicit WindowsDebugLogger(const Config& config) : m_config(config) { + if (m_config.async && m_config.use_dedicated_executor) { + m_executor.reset(new detail::SingleThreadExecutor()); + m_executor->set_max_queue_size(m_config.queue_capacity); + m_executor->set_queue_policy(m_config.queue_policy); + } + } + + /// \brief Constructor with asynchronous and dedicated executor options. + WindowsDebugLogger( + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) + : WindowsDebugLogger(make_config( + async, + use_dedicated_executor, + queue_capacity, + queue_policy)) {} + + /// \brief Logs a message to the Windows debug output or stderr. + /// + /// If asynchronous logging is enabled, the message is added to the task queue; + /// otherwise, it is logged directly. + /// + /// \param record The log record containing log information. + /// \param message The formatted log message. + void log(const LogRecord& record, const std::string& message) override { + if (m_config.async) { + if (m_executor) { + m_executor->add_task([message]() { + write_impl(message); + }); + } else { + detail::TaskExecutor::get_instance().add_task([message]() { + write_impl(message); + }); + } + } else { + write_impl(message); + } + m_last_log_ts.store(record.timestamp_ms); + } + + /// \brief Retrieves a string parameter from the logger. + /// \param param The parameter type to retrieve. + /// \return A string representing the requested parameter. + std::string get_string_param(const LoggerParam& param) const override { + switch (param) { + case LoggerParam::LastLogTimestamp: + return std::to_string(m_last_log_ts.load()); + default: + break; + } + return {}; + } + + /// \brief Retrieves an integer parameter from the logger. + /// \param param The parameter type to retrieve. + /// \return An integer representing the requested parameter, or 0 if unsupported. + int64_t get_int_param(const LoggerParam& param) const override { + switch (param) { + case LoggerParam::LastLogTimestamp: + return m_last_log_ts.load(); + default: + break; + } + return 0; + } + + /// \brief Retrieves a floating-point parameter from the logger. + /// \param param The parameter type to retrieve. + /// \return A double representing the requested parameter, or 0.0 if unsupported. + double get_float_param(const LoggerParam& param) const override { + switch (param) { + case LoggerParam::LastLogTimestamp: + return static_cast(m_last_log_ts.load()) / 1000.0; + default: + break; + } + return 0.0; + } + + /// \brief Sets the minimal log level for this logger. + void set_log_level(LogLevel level) override { + m_log_level = static_cast(level); + } + + /// \brief Gets the minimal log level for this logger. + LogLevel get_log_level() const override { + return static_cast(m_log_level.load()); + } + + /// \brief Waits for all asynchronous tasks to complete. + void wait() override { + if (!m_config.async) return; + if (m_executor) { + m_executor->wait(); + } else { + detail::TaskExecutor::get_instance().wait(); + } + } + + /// \brief Stops logger-owned asynchronous resources after draining pending messages. + void shutdown() override { + if (m_executor) { + m_executor->shutdown(); + } else if (m_config.async) { + detail::TaskExecutor::get_instance().wait(); + } + } + + private: + Config m_config; + std::atomic m_log_level{static_cast(LogLevel::LOG_LVL_TRACE)}; + std::atomic m_last_log_ts{0}; + std::unique_ptr m_executor; + + static Config make_config( + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity, + detail::QueuePolicy queue_policy) { + Config config(async); + config.use_dedicated_executor = use_dedicated_executor; + config.queue_capacity = queue_capacity; + config.queue_policy = queue_policy; + return config; + } + + static void write_impl(const std::string& message) { +#if defined(_WIN32) + // Convert UTF-8 message to wide string for OutputDebugStringW. + if (message.empty()) { + OutputDebugStringW(L"\n"); + return; + } + const int len = static_cast(message.size()); + const int wlen = MultiByteToWideChar(CP_UTF8, 0, message.c_str(), len, nullptr, 0); + if (wlen > 0) { + std::wstring wmsg; + wmsg.resize(static_cast(wlen)); + MultiByteToWideChar(CP_UTF8, 0, message.c_str(), len, &wmsg[0], wlen); + wmsg += L'\n'; + OutputDebugStringW(wmsg.c_str()); + } else { + OutputDebugStringA((message + "\n").c_str()); + } +#else + std::cerr << message << '\n'; +#endif + } + }; + +} // namespace logit + +#endif // _LOGIT_WINDOWS_DEBUG_LOGGER_HPP_INCLUDED diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 587c8be..5734c6c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,9 @@ if(LOGIT_EMSCRIPTEN) add_executable(ems_async_flush ems/async_flush.cpp) target_link_libraries(ems_async_flush PRIVATE log-it-cpp) + + add_executable(ems_single_thread_executor ems/single_thread_executor.cpp) + target_link_libraries(ems_single_thread_executor PRIVATE log-it-cpp) else() file(GLOB TEST_SOURCES CONFIGURE_DEPENDS *.cpp) if(NOT LOGIT_WITH_GZIP) diff --git a/tests/console_logger_dedicated_config_test.cpp b/tests/console_logger_dedicated_config_test.cpp new file mode 100644 index 0000000..19bb37c --- /dev/null +++ b/tests/console_logger_dedicated_config_test.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include +#include + +static bool test_set_config_switches_dedicated_executor() { + logit::ConsoleLogger logger(false); + logit::LogRecord rec(logit::LogLevel::LOG_LVL_INFO, 0, "", 0, "", "", "", -1, false); + + logit::ConsoleLogger::Config cfg; + cfg.async = true; + cfg.use_dedicated_executor = true; + cfg.queue_capacity = 1; + cfg.queue_policy = logit::detail::QueuePolicy::Block; + logger.set_config(cfg); + logger.log(rec, "console dedicated on"); + logger.wait(); + + cfg.use_dedicated_executor = false; + logger.set_config(cfg); + logger.log(rec, "console dedicated off"); + logger.wait(); + + cfg.use_dedicated_executor = true; + cfg.queue_policy = logit::detail::QueuePolicy::DropNewest; + logger.set_config(cfg); + logger.log(rec, "console dedicated on again"); + logger.wait(); + + const logit::ConsoleLogger::Config observed = logger.get_config(); + logger.shutdown(); + + return observed.async && + observed.use_dedicated_executor && + observed.queue_capacity == 1 && + observed.queue_policy == logit::detail::QueuePolicy::DropNewest; +} + +static bool test_set_config_concurrent_log_does_not_hang() { + logit::ConsoleLogger logger(false); + logit::LogRecord rec(logit::LogLevel::LOG_LVL_INFO, 0, "", 0, "", "", "", -1, false); + + logit::ConsoleLogger::Config cfg; + cfg.async = true; + cfg.use_dedicated_executor = true; + cfg.queue_capacity = 1; + cfg.queue_policy = logit::detail::QueuePolicy::Block; + logger.set_config(cfg); + + std::atomic producer_done(false); + std::thread producer([&]() { + for (int i = 0; i < 32; ++i) { + logger.log(rec, "console concurrent dedicated config"); + } + producer_done.store(true); + }); + + for (int i = 0; i < 16; ++i) { + cfg.use_dedicated_executor = (i % 2) == 0; + cfg.queue_policy = (i % 3) == 0 + ? logit::detail::QueuePolicy::DropNewest + : logit::detail::QueuePolicy::Block; + logger.set_config(cfg); + } + + producer.join(); + logger.wait(); + logger.shutdown(); + return producer_done.load(); +} + +int main() { + const bool ok = test_set_config_switches_dedicated_executor() && + test_set_config_concurrent_log_does_not_hang(); + std::cout << (ok ? "PASS" : "FAIL") << ": console_logger_dedicated_config" << std::endl; + return ok ? 0 : 1; +} diff --git a/tests/dedicated_executor_macro_api_test.cpp b/tests/dedicated_executor_macro_api_test.cpp new file mode 100644 index 0000000..0ebebb3 --- /dev/null +++ b/tests/dedicated_executor_macro_api_test.cpp @@ -0,0 +1,119 @@ +#include + +#include + +int main() { + logit::ConsoleLogger::Config console_cfg; + console_cfg.async = true; + console_cfg.use_dedicated_executor = true; + console_cfg.queue_capacity = 4; + console_cfg.queue_policy = logit::detail::QueuePolicy::Block; + LOGIT_ADD_CONSOLE_CONFIG(console_cfg, "%v"); + LOGIT_ADD_CONSOLE_DEDICATED("%v", 4, logit::detail::QueuePolicy::DropNewest); + LOGIT_ADD_CONSOLE_EX("%v", true, true, 4, logit::detail::QueuePolicy::Block); + + logit::FileLogger::Config file_cfg; + file_cfg.directory = "macro_api_file_config"; + file_cfg.async = true; + file_cfg.auto_delete_days = 1; + file_cfg.use_dedicated_executor = true; + file_cfg.queue_capacity = 4; + file_cfg.queue_policy = logit::detail::QueuePolicy::Block; + LOGIT_ADD_FILE_LOGGER_CONFIG(file_cfg, "%v"); + LOGIT_ADD_FILE_LOGGER_DEDICATED( + "macro_api_file_dedicated", + 1, + "%v", + 4, + logit::detail::QueuePolicy::DropNewest); + LOGIT_ADD_FILE_LOGGER_EX( + "macro_api_file_ex", + true, + 1, + "%v", + true, + 4, + logit::detail::QueuePolicy::Block); + LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_DEDICATED( + "macro_api_file_rotation", + 1, + "%v", + 1024, + 2, + 4, + logit::detail::QueuePolicy::Block); + + logit::UniqueFileLogger::Config unique_cfg; + unique_cfg.directory = "macro_api_unique_config"; + unique_cfg.async = true; + unique_cfg.auto_delete_days = 1; + unique_cfg.hash_length = 8; + unique_cfg.use_dedicated_executor = true; + unique_cfg.queue_capacity = 4; + unique_cfg.queue_policy = logit::detail::QueuePolicy::Block; + LOGIT_ADD_UNIQUE_FILE_LOGGER_CONFIG(unique_cfg, "%v"); + LOGIT_ADD_UNIQUE_FILE_LOGGER_DEDICATED( + "macro_api_unique_dedicated", + 1, + 8, + "%v", + 4, + logit::detail::QueuePolicy::DropNewest); + LOGIT_ADD_UNIQUE_FILE_LOGGER_EX( + "macro_api_unique_ex", + true, + 1, + 8, + "%v", + true, + 4, + logit::detail::QueuePolicy::Block); + + logit::WindowsDebugLogger::Config debug_cfg(true); + debug_cfg.use_dedicated_executor = true; + debug_cfg.queue_capacity = 4; + debug_cfg.queue_policy = logit::detail::QueuePolicy::Block; + LOGIT_ADD_WINDOWS_DEBUG_CONFIG(debug_cfg); + LOGIT_ADD_WINDOWS_DEBUG_DEDICATED(4, logit::detail::QueuePolicy::DropNewest); + LOGIT_ADD_WINDOWS_DEBUG_EX(true, true, 4, logit::detail::QueuePolicy::Block); + + logit::SyslogLogger::Config syslog_cfg("macro-api", 0, true); + syslog_cfg.use_dedicated_executor = true; + syslog_cfg.queue_capacity = 4; + syslog_cfg.queue_policy = logit::detail::QueuePolicy::Block; + LOGIT_ADD_SYSLOG_CONFIG_SINGLE_MODE(syslog_cfg); + LOGIT_ADD_SYSLOG_DEDICATED_SINGLE_MODE( + "macro-api", + 0, + 4, + logit::detail::QueuePolicy::DropNewest); + LOGIT_ADD_SYSLOG_EX_SINGLE_MODE( + "macro-api", + 0, + true, + true, + 4, + logit::detail::QueuePolicy::Block); + + logit::EventLogLogger::Config event_cfg(L"LogItMacroApi", true); + event_cfg.use_dedicated_executor = true; + event_cfg.queue_capacity = 4; + event_cfg.queue_policy = logit::detail::QueuePolicy::Block; + LOGIT_ADD_EVENT_LOG_CONFIG_SINGLE_MODE(event_cfg); + LOGIT_ADD_EVENT_LOG_DEDICATED_SINGLE_MODE( + L"LogItMacroApi", + 4, + logit::detail::QueuePolicy::DropNewest); + LOGIT_ADD_EVENT_LOG_EX_SINGLE_MODE( + L"LogItMacroApi", + true, + true, + 4, + logit::detail::QueuePolicy::Block); + + LOGIT_INFO("dedicated executor macro API test"); + LOGIT_SHUTDOWN(); + + std::cout << "PASS: dedicated_executor_macro_api" << std::endl; + return 0; +} diff --git a/tests/dedicated_executor_shutdown_test.cpp b/tests/dedicated_executor_shutdown_test.cpp new file mode 100644 index 0000000..1c59e02 --- /dev/null +++ b/tests/dedicated_executor_shutdown_test.cpp @@ -0,0 +1,68 @@ +#include + +#include +#include +#include + +class ShutdownProbeLogger : public logit::ILogger { +public: + ShutdownProbeLogger() + : m_executor(new logit::detail::SingleThreadExecutor()) {} + + void log(const logit::LogRecord&, const std::string&) override { + m_executor->add_task([this]() { + m_count.fetch_add(1, std::memory_order_relaxed); + }); + } + + std::string get_string_param(const logit::LoggerParam&) const override { return std::string(); } + int64_t get_int_param(const logit::LoggerParam&) const override { return 0; } + double get_float_param(const logit::LoggerParam&) const override { return 0.0; } + void set_log_level(logit::LogLevel level) override { m_level.store(static_cast(level)); } + logit::LogLevel get_log_level() const override { + return static_cast(m_level.load()); + } + + void wait() override { + m_executor->wait(); + } + + void shutdown() override { + m_shutdown_called.store(true, std::memory_order_relaxed); + m_executor->shutdown(); + } + + bool shutdown_called() const { + return m_shutdown_called.load(std::memory_order_relaxed); + } + + std::size_t count() const { + return m_count.load(std::memory_order_relaxed); + } + +private: + std::unique_ptr m_executor; + std::atomic m_level{static_cast(logit::LogLevel::LOG_LVL_TRACE)}; + std::atomic m_shutdown_called{false}; + std::atomic m_count{0}; +}; + +int main() { + ShutdownProbeLogger* raw_logger = new ShutdownProbeLogger(); + logit::Logger::get_instance().add_logger( + std::unique_ptr(raw_logger), + std::unique_ptr( + new logit::SimpleLogFormatter(LOGIT_CONSOLE_PATTERN))); + + const int message_count = 16; + for (int i = 0; i < message_count; ++i) { + LOGIT_INFO("shutdown probe"); + } + + LOGIT_SHUTDOWN(); + + const bool ok = raw_logger->shutdown_called() && + raw_logger->count() == static_cast(message_count); + std::cout << (ok ? "PASS" : "FAIL") << ": dedicated_executor_shutdown" << std::endl; + return ok ? 0 : 1; +} diff --git a/tests/ems/single_thread_executor.cpp b/tests/ems/single_thread_executor.cpp new file mode 100644 index 0000000..e788c06 --- /dev/null +++ b/tests/ems/single_thread_executor.cpp @@ -0,0 +1,15 @@ +#include + +#include + +int main() { + logit::detail::SingleThreadExecutor executor; + std::atomic counter{0}; + + executor.add_task([&counter]() { + counter.fetch_add(1, std::memory_order_relaxed); + }); + executor.wait(); + + return counter.load(std::memory_order_relaxed) == 1 ? 0 : 1; +} diff --git a/tests/per_logger_isolation_test.cpp b/tests/per_logger_isolation_test.cpp new file mode 100644 index 0000000..7781a5c --- /dev/null +++ b/tests/per_logger_isolation_test.cpp @@ -0,0 +1,135 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef __EMSCRIPTEN__ + +using logit::LogLevel; +using logit::LogRecord; + +/// \brief Probe logger that records timing and can inject a delay. +class ProbeLogger : public logit::ILogger { +public: + struct Config { + bool async = true; + bool use_dedicated_executor = false; + std::chrono::milliseconds task_delay{0}; + }; + + explicit ProbeLogger(const Config& cfg) : m_cfg(cfg) { + if (m_cfg.async && m_cfg.use_dedicated_executor) { + m_executor.reset(new logit::detail::SingleThreadExecutor()); + } + } + + ~ProbeLogger() override { + if (m_executor) m_executor->shutdown(); + } + + void log(const LogRecord& record, const std::string& message) override { + m_last_ts.store(record.timestamp_ms); + if (!m_cfg.async) { + run_task(message); + return; + } + if (m_executor) { + m_executor->add_task([this, message]() { run_task(message); }); + } else { + logit::detail::TaskExecutor::get_instance().add_task([this, message]() { run_task(message); }); + } + } + + std::string get_string_param(const logit::LoggerParam&) const override { return {}; } + int64_t get_int_param(const logit::LoggerParam&) const override { return 0; } + double get_float_param(const logit::LoggerParam&) const override { return 0.0; } + void set_log_level(LogLevel l) override { m_level.store(static_cast(l)); } + LogLevel get_log_level() const override { return static_cast(m_level.load()); } + + void wait() override { + if (!m_cfg.async) return; + if (m_executor) m_executor->wait(); + else logit::detail::TaskExecutor::get_instance().wait(); + } + + std::size_t count() const { return m_count.load(std::memory_order_relaxed); } + void reset_count() { m_count.store(0, std::memory_order_relaxed); } + +private: + void run_task(const std::string&) { + if (m_cfg.task_delay.count() > 0) { + std::this_thread::sleep_for(m_cfg.task_delay); + } + m_count.fetch_add(1, std::memory_order_relaxed); + } + + Config m_cfg; + std::atomic m_level{static_cast(LogLevel::LOG_LVL_TRACE)}; + std::atomic m_last_ts{0}; + std::atomic m_count{0}; + std::unique_ptr m_executor; +}; + +static bool test_isolation() { + // Slow logger with dedicated executor + ProbeLogger::Config slow_cfg; + slow_cfg.async = true; + slow_cfg.use_dedicated_executor = true; + slow_cfg.task_delay = std::chrono::milliseconds(50); + ProbeLogger slow_logger(slow_cfg); + + // Fast logger with dedicated executor + ProbeLogger::Config fast_cfg; + fast_cfg.async = true; + fast_cfg.use_dedicated_executor = true; + fast_cfg.task_delay = std::chrono::milliseconds(0); + ProbeLogger fast_logger(fast_cfg); + + // Enqueue a slow task + LogRecord rec(LogLevel::LOG_LVL_INFO, 0, "", 0, "", "", "", -1, false); + + slow_logger.log(rec, "slow1"); + + // Enqueue a fast task -- should complete quickly even though slow is blocked + const auto start = std::chrono::steady_clock::now(); + fast_logger.log(rec, "fast1"); + fast_logger.wait(); + const auto fast_duration = std::chrono::steady_clock::now() - start; + + slow_logger.wait(); + + // Fast should complete well under 50ms (it has its own thread) + auto ms = std::chrono::duration_cast(fast_duration).count(); + bool fast_was_quick = ms < 40; + bool counts_ok = slow_logger.count() == 1 && fast_logger.count() == 1; + + if (!fast_was_quick) { + std::cout << "Fast logger took " << ms << "ms (expected < 40)" << std::endl; + } + + return fast_was_quick && counts_ok; +} + +int main() { + int passed = 0; + int failed = 0; + + auto run = [&](const char* name, bool result) { + if (result) { ++passed; std::cout << "PASS: " << name << std::endl; } + else { ++failed; std::cout << "FAIL: " << name << std::endl; } + }; + + run("per_logger_isolation", test_isolation()); + + std::cout << "\n" << passed << " passed, " << failed << " failed" << std::endl; + return failed > 0 ? 1 : 0; +} + +#else // __EMSCRIPTEN__ +int main() { return 0; } +#endif diff --git a/tests/per_logger_mixed_mode_test.cpp b/tests/per_logger_mixed_mode_test.cpp new file mode 100644 index 0000000..818ab56 --- /dev/null +++ b/tests/per_logger_mixed_mode_test.cpp @@ -0,0 +1,150 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef __EMSCRIPTEN__ + +using logit::LogLevel; +using logit::LogRecord; + +/// \brief Probe logger that records task count. +class CountingLogger : public logit::ILogger { +public: + struct Config { + bool async = true; + bool use_dedicated_executor = false; + }; + + explicit CountingLogger(const Config& cfg) : m_cfg(cfg) { + if (m_cfg.async && m_cfg.use_dedicated_executor) { + m_executor.reset(new logit::detail::SingleThreadExecutor()); + } + } + + ~CountingLogger() override { + if (m_executor) m_executor->shutdown(); + } + + void log(const LogRecord& record, const std::string& message) override { + m_last_ts.store(record.timestamp_ms); + if (!m_cfg.async) { + m_count.fetch_add(1, std::memory_order_relaxed); + return; + } + if (m_executor) { + m_executor->add_task([this]() { m_count.fetch_add(1, std::memory_order_relaxed); }); + } else { + logit::detail::TaskExecutor::get_instance().add_task([this]() { m_count.fetch_add(1, std::memory_order_relaxed); }); + } + } + + std::string get_string_param(const logit::LoggerParam&) const override { return {}; } + int64_t get_int_param(const logit::LoggerParam&) const override { return 0; } + double get_float_param(const logit::LoggerParam&) const override { return 0.0; } + void set_log_level(LogLevel l) override { m_level.store(static_cast(l)); } + LogLevel get_log_level() const override { return static_cast(m_level.load()); } + + void wait() override { + if (!m_cfg.async) return; + if (m_executor) m_executor->wait(); + else logit::detail::TaskExecutor::get_instance().wait(); + } + + std::size_t count() const { return m_count.load(std::memory_order_relaxed); } + +private: + Config m_cfg; + std::atomic m_level{static_cast(LogLevel::LOG_LVL_TRACE)}; + std::atomic m_last_ts{0}; + std::atomic m_count{0}; + std::unique_ptr m_executor; +}; + +static bool test_mixed_mode_drain() { + // Logger A: dedicated executor + CountingLogger::Config cfg_a; + cfg_a.async = true; + cfg_a.use_dedicated_executor = true; + CountingLogger logger_a(cfg_a); + + // Logger B: global executor + CountingLogger::Config cfg_b; + cfg_b.async = true; + cfg_b.use_dedicated_executor = false; + CountingLogger logger_b(cfg_b); + + LogRecord rec(LogLevel::LOG_LVL_INFO, 0, "", 0, "", "", "", -1, false); + + const int N = 20; + for (int i = 0; i < N; ++i) { + logger_a.log(rec, "a"); + logger_b.log(rec, "b"); + } + + logger_a.wait(); + logger_b.wait(); + + bool a_ok = logger_a.count() == N; + bool b_ok = logger_b.count() == N; + + if (!a_ok) std::cout << "Logger A: expected " << N << " got " << logger_a.count() << std::endl; + if (!b_ok) std::cout << "Logger B: expected " << N << " got " << logger_b.count() << std::endl; + + return a_ok && b_ok; +} + +static bool test_mixed_mode_shutdown() { + // Ensure destructor properly drains both executors + std::size_t a_count = 0; + std::size_t b_count = 0; + + { + CountingLogger::Config cfg_a; + cfg_a.async = true; + cfg_a.use_dedicated_executor = true; + CountingLogger logger_a(cfg_a); + + CountingLogger::Config cfg_b; + cfg_b.async = true; + cfg_b.use_dedicated_executor = false; + CountingLogger logger_b(cfg_b); + + LogRecord rec(LogLevel::LOG_LVL_INFO, 0, "", 0, "", "", "", -1, false); + + for (int i = 0; i < 10; ++i) { + logger_a.log(rec, "a"); + logger_b.log(rec, "b"); + } + + // Destructors call shutdown/wait + } + + // If we reach here without hang, the test passes + return true; +} + +int main() { + int passed = 0; + int failed = 0; + + auto run = [&](const char* name, bool result) { + if (result) { ++passed; std::cout << "PASS: " << name << std::endl; } + else { ++failed; std::cout << "FAIL: " << name << std::endl; } + }; + + run("mixed_mode_drain", test_mixed_mode_drain()); + run("mixed_mode_shutdown", test_mixed_mode_shutdown()); + + std::cout << "\n" << passed << " passed, " << failed << " failed" << std::endl; + return failed > 0 ? 1 : 0; +} + +#else // __EMSCRIPTEN__ +int main() { return 0; } +#endif diff --git a/tests/single_thread_executor_test.cpp b/tests/single_thread_executor_test.cpp new file mode 100644 index 0000000..0785cd4 --- /dev/null +++ b/tests/single_thread_executor_test.cpp @@ -0,0 +1,334 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using logit::detail::SingleThreadExecutor; +using logit::detail::QueuePolicy; + +static bool test_basic_enqueue_execute() { + SingleThreadExecutor ex; + std::atomic counter{0}; + ex.add_task([&counter]() { counter.fetch_add(1, std::memory_order_relaxed); }); + ex.wait(); + return counter.load() == 1; +} + +static bool test_fifo_ordering() { + SingleThreadExecutor ex; + std::vector order; + std::mutex m; + for (int i = 0; i < 10; ++i) { + ex.add_task([&order, &m, i]() { + std::lock_guard lk(m); + order.push_back(i); + }); + } + ex.wait(); + if (order.size() != 10) return false; + for (int i = 0; i < 10; ++i) { + if (order[i] != i) return false; + } + return true; +} + +static bool test_wait_blocks_until_empty() { + SingleThreadExecutor ex; + std::atomic counter{0}; + for (int i = 0; i < 5; ++i) { + ex.add_task([&counter]() { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + counter.fetch_add(1, std::memory_order_relaxed); + }); + } + ex.wait(); + return counter.load() == 5; +} + +static bool test_shutdown_drains_remaining() { + std::atomic counter{0}; + { + SingleThreadExecutor ex; + for (int i = 0; i < 5; ++i) { + ex.add_task([&counter]() { + counter.fetch_add(1, std::memory_order_relaxed); + }); + } + // shutdown() should drain before joining + ex.shutdown(); + } + return counter.load() == 5; +} + +static bool test_drop_newest_policy() { + SingleThreadExecutor ex; + ex.set_max_queue_size(2); + ex.set_queue_policy(QueuePolicy::DropNewest); + + // Fill with slow tasks to block the worker + std::atomic processed{0}; + std::mutex gate_mutex; + std::condition_variable gate_cv; + bool gate_open = false; + + // First task holds the worker + ex.add_task([&]() { + std::unique_lock lk(gate_mutex); + gate_cv.wait(lk, [&]{ return gate_open; }); + processed.fetch_add(1, std::memory_order_relaxed); + }); + + // Give worker time to start the first task + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + // Second task enters the queue (size 1) + ex.add_task([&]() { + processed.fetch_add(1, std::memory_order_relaxed); + }); + + // Third task fills queue (size 2) + ex.add_task([&]() { + processed.fetch_add(1, std::memory_order_relaxed); + }); + + // Fourth task should be dropped (queue full) + ex.add_task([&]() { + processed.fetch_add(1, std::memory_order_relaxed); + }); + + // Release the gate + { + std::lock_guard lk(gate_mutex); + gate_open = true; + } + gate_cv.notify_all(); + + ex.wait(); + bool dropped_ok = ex.dropped_tasks() > 0; + ex.reset_dropped_tasks(); + bool reset_ok = ex.dropped_tasks() == 0; + return dropped_ok && reset_ok; +} + +static bool test_drop_oldest_policy() { + SingleThreadExecutor ex; + ex.set_max_queue_size(2); + ex.set_queue_policy(QueuePolicy::DropOldest); + + std::atomic processed{0}; + std::mutex gate_mutex; + std::condition_variable gate_cv; + bool gate_open = false; + + // Hold the worker + ex.add_task([&]() { + std::unique_lock lk(gate_mutex); + gate_cv.wait(lk, [&]{ return gate_open; }); + processed.fetch_add(1, std::memory_order_relaxed); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + // Fill queue to capacity + ex.add_task([&]() { processed.fetch_add(1, std::memory_order_relaxed); }); + ex.add_task([&]() { processed.fetch_add(1, std::memory_order_relaxed); }); + + // This should drop the oldest queued task + ex.add_task([&]() { processed.fetch_add(1, std::memory_order_relaxed); }); + + { + std::lock_guard lk(gate_mutex); + gate_open = true; + } + gate_cv.notify_all(); + ex.wait(); + + return ex.dropped_tasks() > 0; +} + +static bool test_block_policy() { + SingleThreadExecutor ex; + ex.set_max_queue_size(2); + ex.set_queue_policy(QueuePolicy::Block); + + std::atomic processed{0}; + std::mutex gate_mutex; + std::condition_variable gate_cv; + bool gate_open = false; + + // Hold the worker + ex.add_task([&]() { + std::unique_lock lk(gate_mutex); + gate_cv.wait(lk, [&]{ return gate_open; }); + processed.fetch_add(1, std::memory_order_relaxed); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + // Fill queue + ex.add_task([&]() { processed.fetch_add(1, std::memory_order_relaxed); }); + ex.add_task([&]() { processed.fetch_add(1, std::memory_order_relaxed); }); + + // This enqueue should block until queue drains + std::thread blocker([&]() { + ex.add_task([&]() { processed.fetch_add(1, std::memory_order_relaxed); }); + }); + + // Wait a bit, then release the gate + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + { + std::lock_guard lk(gate_mutex); + gate_open = true; + } + gate_cv.notify_all(); + + blocker.join(); + ex.wait(); + + // All 4 tasks should have been processed, no drops + return processed.load() == 4 && ex.dropped_tasks() == 0; +} + +static bool test_block_policy_wakes_on_capacity() { + SingleThreadExecutor ex; + ex.set_max_queue_size(1); + ex.set_queue_policy(QueuePolicy::Block); + + std::mutex gate_mutex; + std::condition_variable gate_cv; + bool first_gate_open = false; + bool second_gate_open = false; + + std::mutex done_mutex; + std::condition_variable done_cv; + bool producer_done = false; + + ex.add_task([&]() { + std::unique_lock lk(gate_mutex); + gate_cv.wait(lk, [&]() { return first_gate_open; }); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + ex.add_task([&]() { + std::unique_lock lk(gate_mutex); + gate_cv.wait(lk, [&]() { return second_gate_open; }); + }); + + std::thread producer([&]() { + ex.add_task([]() {}); + { + std::lock_guard lk(done_mutex); + producer_done = true; + } + done_cv.notify_all(); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + { + std::lock_guard lk(gate_mutex); + first_gate_open = true; + } + gate_cv.notify_all(); + + bool woke_before_second_task_finished = false; + { + std::unique_lock lk(done_mutex); + woke_before_second_task_finished = + done_cv.wait_for(lk, std::chrono::milliseconds(200), [&]() { + return producer_done; + }); + } + + { + std::lock_guard lk(gate_mutex); + second_gate_open = true; + } + gate_cv.notify_all(); + + producer.join(); + ex.wait(); + + return woke_before_second_task_finished; +} + +static bool test_exception_in_task() { + SingleThreadExecutor ex; + std::atomic counter{0}; + + ex.add_task([&counter]() { + counter.fetch_add(1, std::memory_order_relaxed); + }); + ex.add_task([&counter]() { + throw std::runtime_error("test exception"); + }); + ex.add_task([&counter]() { + counter.fetch_add(1, std::memory_order_relaxed); + }); + + ex.wait(); + return counter.load() == 2; +} + +static bool test_concurrent_producers() { + SingleThreadExecutor ex; + std::atomic counter{0}; + const int num_threads = 4; + const int tasks_per_thread = 25; + + std::vector producers; + for (int t = 0; t < num_threads; ++t) { + producers.emplace_back([&ex, &counter, tasks_per_thread]() { + for (int i = 0; i < tasks_per_thread; ++i) { + ex.add_task([&counter]() { + counter.fetch_add(1, std::memory_order_relaxed); + }); + } + }); + } + for (auto& t : producers) t.join(); + ex.wait(); + + return counter.load() == num_threads * tasks_per_thread; +} + +static bool test_post_shutdown_rejection() { + SingleThreadExecutor ex; + std::atomic counter{0}; + ex.add_task([&counter]() { counter.fetch_add(1, std::memory_order_relaxed); }); + ex.shutdown(); + ex.add_task([&counter]() { counter.fetch_add(100, std::memory_order_relaxed); }); + ex.add_task([&counter]() { counter.fetch_add(100, std::memory_order_relaxed); }); + return counter.load() == 1; +} + +int main() { + int passed = 0; + int failed = 0; + + auto run = [&](const char* name, bool result) { + if (result) { ++passed; std::cout << "PASS: " << name << std::endl; } + else { ++failed; std::cout << "FAIL: " << name << std::endl; } + }; + + run("basic_enqueue_execute", test_basic_enqueue_execute()); + run("fifo_ordering", test_fifo_ordering()); + run("wait_blocks_until_empty", test_wait_blocks_until_empty()); + run("shutdown_drains_remaining", test_shutdown_drains_remaining()); + run("drop_newest_policy", test_drop_newest_policy()); + run("drop_oldest_policy", test_drop_oldest_policy()); + run("block_policy", test_block_policy()); + run("block_policy_wakes_on_capacity", test_block_policy_wakes_on_capacity()); + run("exception_in_task", test_exception_in_task()); + run("concurrent_producers", test_concurrent_producers()); + run("post_shutdown_rejection", test_post_shutdown_rejection()); + + std::cout << "\n" << passed << " passed, " << failed << " failed" << std::endl; + return failed > 0 ? 1 : 0; +} diff --git a/tests/windows_debug_macro_compile_test.cpp b/tests/windows_debug_macro_compile_test.cpp new file mode 100644 index 0000000..3ddfbb4 --- /dev/null +++ b/tests/windows_debug_macro_compile_test.cpp @@ -0,0 +1,11 @@ +#include + +#include + +int main() { + LOGIT_ADD_WINDOWS_DEBUG(true); + LOGIT_INFO("windows debug macro compile test"); + LOGIT_SHUTDOWN(); + std::cout << "PASS: windows_debug_macro_compile" << std::endl; + return 0; +}