From 01dd20ee3fe70823bbef90e375c8c33585bed81b Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Wed, 3 Jun 2026 01:00:57 +0300 Subject: [PATCH 1/4] feat(console): configurable output stream Let ConsoleLogger target any std::ostream& and add an optional level-based routing table so callers can split regular output from diagnostics across cout, cerr, and any caller-provided stream without breaking the historical default behavior. Constraint: Preserve default std::cout behavior and existing public API Rejected: Built-in auto routing by level | would force a fixed severity policy users cannot tune Confidence: high Scope-risk: moderate Co-Authored-By: Claude Opus 4.8 --- include/logit_cpp/logit/log_macros.hpp | 33 ++++ .../logit_cpp/logit/loggers/ConsoleLogger.hpp | 149 ++++++++++++---- .../logit/loggers/ConsoleStreamRoute.hpp | 32 ++++ tests/CMakeLists.txt | 1 + tests/console_logger_stream_test.cpp | 162 ++++++++++++++++++ 5 files changed, 341 insertions(+), 36 deletions(-) create mode 100644 include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp create mode 100644 tests/console_logger_stream_test.cpp diff --git a/include/logit_cpp/logit/log_macros.hpp b/include/logit_cpp/logit/log_macros.hpp index a1927a4..bce44bb 100644 --- a/include/logit_cpp/logit/log_macros.hpp +++ b/include/logit_cpp/logit/log_macros.hpp @@ -2243,6 +2243,39 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT #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 Add a console logger writing to a caller-provided std::ostream& (e.g. std::cerr). +/// \param stream Output stream reference; lifetime is owned by the caller. +#define LOGIT_ADD_CONSOLE_TO_STREAM(stream, pattern, async) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::ConsoleLogger( \ + (stream), (async))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + false) + +/// \brief Add a console logger writing to a caller-provided std::ostream& in single_mode. +#define LOGIT_ADD_CONSOLE_TO_STREAM_SINGLE_MODE(stream, pattern, async) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::ConsoleLogger( \ + (stream), (async))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +/// \brief Add a console logger from an explicit Config writing to a caller-provided std::ostream&. +#define LOGIT_ADD_CONSOLE_TO_STREAM_EX(stream, config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::ConsoleLogger( \ + (stream), (config))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + false) + +/// \brief Add a console logger from an explicit Config writing to a caller-provided std::ostream& in single_mode. +#define LOGIT_ADD_CONSOLE_TO_STREAM_EX_SINGLE_MODE(stream, config, pattern) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::ConsoleLogger( \ + (stream), (config))), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + /// \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. diff --git a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp index 0693988..6716060 100644 --- a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp +++ b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp @@ -6,9 +6,11 @@ /// \brief Console logger implementation that outputs logs to the console with color support. #include "ILogger.hpp" +#include "ConsoleStreamRoute.hpp" #include #include #include +#include #if defined(_WIN32) #include #endif @@ -59,6 +61,14 @@ namespace logit { /// - Cross-platform color support (ANSI on Linux/macOS, Windows-specific handling). /// - Thread-safe logging. /// - Synchronous or asynchronous operation. + /// - Configurable output stream: defaults to `std::cout`; can be redirected to + /// any `std::ostream&` (e.g. `std::cerr`, a file stream). The lifetime of the + /// target stream is owned by the caller. + /// - Optional level-based routing via `Config::routes`: maps log level ranges + /// to specific streams so diagnostics can be split from regular output. + /// When `routes` is empty, every record is written to the primary stream. + /// ANSI/Windows color support is preserved for `std::cout` and `std::cerr`; + /// for any other stream, colors are disabled. class ConsoleLogger : public ILogger { public: @@ -74,16 +84,41 @@ namespace logit { 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 Optional level-based stream routing. + /// \details When non-empty, the first matching route (inclusive range + /// `[min_level, max_level]`) wins. Falls back to the primary stream when + /// no route matches. Empty by default, preserving the historical single-stream behavior. + std::vector routes; }; - /// \brief Default constructor that uses default configuration. - ConsoleLogger() { + /// \brief Default constructor that uses default configuration and std::cout. + ConsoleLogger() : m_stream(&std::cout) { reset_color(); } - /// \brief Constructor with custom configuration. + /// \brief Constructor with custom configuration and the default std::cout stream. /// \param config The configuration for the logger. - ConsoleLogger(const Config& config) : m_config(config) { + ConsoleLogger(const Config& config) + : ConsoleLogger(std::cout, config) {} + + /// \brief Constructor with a custom primary output stream and default config. + /// \param stream Output stream to write records to. Lifetime is owned by the caller. + explicit ConsoleLogger(std::ostream& stream) + : m_stream(&stream) { + reset_color(); + } + + /// \brief Constructor with a custom primary output stream and configuration. + /// \param stream Output stream to write records to. Lifetime is owned by the caller. + /// \param config The configuration for the logger. + ConsoleLogger(std::ostream& stream, const Config& config) + : m_config(config), m_stream(&stream) { + m_config.async = config.async; + m_config.use_dedicated_executor = config.use_dedicated_executor; + m_config.queue_capacity = config.queue_capacity; + m_config.queue_policy = config.queue_policy; + m_config.default_color = config.default_color; + m_config.routes = config.routes; reset_color(); if (m_config.async && m_config.use_dedicated_executor) { m_executor.reset(new detail::SingleThreadExecutor()); @@ -91,9 +126,17 @@ namespace logit { } } - /// \brief Constructor with asynchronous flag. + /// \brief Constructor with asynchronous flag and std::cout. + /// \param async Boolean flag for asynchronous logging. + ConsoleLogger(const bool async) : m_stream(&std::cout) { + m_config.async = async; + reset_color(); + } + + /// \brief Constructor with asynchronous flag and a custom primary output stream. + /// \param stream Output stream to write records to. Lifetime is owned by the caller. /// \param async Boolean flag for asynchronous logging. - ConsoleLogger(const bool async) { + ConsoleLogger(std::ostream& stream, const bool async) : m_stream(&stream) { m_config.async = async; reset_color(); } @@ -110,6 +153,19 @@ namespace logit { queue_capacity, queue_policy)) {} + /// \brief Constructor with a custom primary output stream, async mode, and executor options. + ConsoleLogger( + std::ostream& stream, + bool async, + bool use_dedicated_executor, + std::size_t queue_capacity = 0, + detail::QueuePolicy queue_policy = detail::QueuePolicy::Block) + : ConsoleLogger(stream, make_config( + async, + use_dedicated_executor, + queue_capacity, + queue_policy)) {} + virtual ~ConsoleLogger() { shutdown(); } @@ -200,40 +256,23 @@ namespace logit { if (m_shutdown.load(std::memory_order_acquire)) return; m_last_log_ts = record.timestamp_ms; std::shared_ptr executor = m_executor; + std::ostream* stream = select_stream_for(record.log_level); if (!m_config.async) { -# 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 + write_colored_message(*stream, message); return; } ++m_pending_enqueues; lock.unlock(); PendingEnqueue pending_enqueue(*this); if (executor) { - executor->add_task([this, message](){ + executor->add_task([this, stream, 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 + write_colored_message(*stream, message); }); } else { - detail::TaskExecutor::get_instance().add_task([this, message](){ + detail::TaskExecutor::get_instance().add_task([this, stream, 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 + write_colored_message(*stream, message); }); } #endif @@ -331,6 +370,7 @@ namespace logit { private: mutable std::mutex m_mutex; ///< Mutex to protect console output Config m_config; ///< Configuration for the console logger. + std::ostream* m_stream; ///< Primary output stream; lifetime owned by the caller. 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::atomic m_shutdown = ATOMIC_VAR_INIT(false); @@ -338,6 +378,40 @@ namespace logit { std::condition_variable m_enqueue_cv; std::size_t m_pending_enqueues = 0; + /// \brief Selects the target stream for a given log level. + /// \details Uses Config::routes when non-empty; falls back to the + /// primary stream. Must be called under m_mutex. + std::ostream* select_stream_for(LogLevel level) const { + for (const auto& route : m_config.routes) { + if (route.min_level > route.max_level) continue; + if (level >= route.min_level && level <= route.max_level) { + switch (route.kind) { + case ConsoleStreamKind::Cout: return &std::cout; + case ConsoleStreamKind::Cerr: return &std::cerr; + case ConsoleStreamKind::Custom: return route.custom_stream; + } + } + } + return m_stream; + } + + /// \brief Writes one formatted message to a stream, applying colors + /// when supported. Must be called under m_mutex. + void write_colored_message(std::ostream& stream, const std::string& message) const { +# ifdef __EMSCRIPTEN__ + (void)stream; + (void)message; +# elif defined(_WIN32) + if (&stream == &std::cout || &stream == &std::cerr) { + handle_ansi_colors_windows(message, stream); + } else { + stream << message << std::endl; + } +# else + stream << message << std::endl; +# endif + } + static void configure_executor( const std::shared_ptr& executor, const Config& config) { @@ -437,16 +511,19 @@ namespace logit { /// \brief Handle ANSI color codes in the message for Windows console. /// \param message The message containing ANSI color codes. - void handle_ansi_colors_windows(const std::string& message) const { + /// \param stream Target output stream (std::cout or std::cerr). + void handle_ansi_colors_windows(const std::string& message, std::ostream& stream) const { std::string::size_type start = 0; std::string::size_type pos = 0; - HANDLE handle_stdout = GetStdHandle(STD_OUTPUT_HANDLE); + HANDLE handle_stdout = (&stream == &std::cerr) + ? GetStdHandle(STD_ERROR_HANDLE) + : GetStdHandle(STD_OUTPUT_HANDLE); while ((pos = message.find("\033[", start)) != std::string::npos) { // Output the part of the string before the ANSI code if (pos > start) { - std::cout << message.substr(start, pos - start); + stream << message.substr(start, pos - start); } // Find the end of the ANSI code @@ -465,9 +542,9 @@ namespace logit { // Output any remaining part of the message if (start < message.size()) { - std::cout << message.substr(start); + stream << message.substr(start); } - if (!message.empty()) std::cout << std::endl; + if (!message.empty()) stream << std::endl; // Reset the console color to default SetConsoleTextAttribute(handle_stdout, static_cast(text_color_to_win_color(m_config.default_color))); @@ -537,9 +614,9 @@ namespace logit { // No persistent console color in browsers return; # elif defined(_WIN32) - handle_ansi_colors_windows(std::string()); + handle_ansi_colors_windows(std::string(), *m_stream); # else - std::cout << to_string(m_config.default_color); + *m_stream << to_string(m_config.default_color); # endif } diff --git a/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp b/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp new file mode 100644 index 0000000..6a3ec64 --- /dev/null +++ b/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp @@ -0,0 +1,32 @@ +#pragma once +#ifndef _LOGIT_CONSOLE_STREAM_ROUTE_HPP_INCLUDED +#define _LOGIT_CONSOLE_STREAM_ROUTE_HPP_INCLUDED + +/// \file ConsoleStreamRoute.hpp +/// \brief Level-based output stream routing for ConsoleLogger. + +#include "../enums.hpp" +#include + +namespace logit { + + /// \enum ConsoleStreamKind + /// \brief Identifies a target output stream for ConsoleLogger routes. + enum class ConsoleStreamKind { + Cout, ///< Routes records to std::cout. + Cerr, ///< Routes records to std::cerr. + Custom ///< Routes records to a caller-provided std::ostream pointer. + }; + + /// \struct ConsoleStreamRoute + /// \brief Maps a log-level range to a target output stream. + struct ConsoleStreamRoute { + LogLevel min_level = LogLevel::LOG_LVL_TRACE; ///< Inclusive lower bound. + LogLevel max_level = LogLevel::LOG_LVL_FATAL; ///< Inclusive upper bound. + ConsoleStreamKind kind = ConsoleStreamKind::Cout; ///< Stream identifier. + std::ostream* custom_stream = nullptr; ///< Used when kind == Custom; null is ignored. + }; + +} // namespace logit + +#endif // _LOGIT_CONSOLE_STREAM_ROUTE_HPP_INCLUDED diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ec19675..fe7b362 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,7 @@ else() compiled_level_runtime_caveat_test.cpp compiled_level_test.cpp console_logger_dedicated_config_test.cpp + console_logger_stream_test.cpp crash_logger_test.cpp dedicated_executor_macro_api_test.cpp dedicated_executor_shutdown_test.cpp diff --git a/tests/console_logger_stream_test.cpp b/tests/console_logger_stream_test.cpp new file mode 100644 index 0000000..6714048 --- /dev/null +++ b/tests/console_logger_stream_test.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include + +namespace { + +std::string capture_stream(std::ostream& stream, const std::function& body) { + std::stringstream buffer; + auto* old = stream.rdbuf(buffer.rdbuf()); + body(); + stream.flush(); + stream.rdbuf(old); + return buffer.str(); +} + +logit::LogRecord make_record(logit::LogLevel level, const char* message) { + return logit::LogRecord( + level, + 0, + "console_logger_stream_test.cpp", + 0, + "test", + message, + "", + -1, + false, + false, + false); +} + +void test_default_constructor_writes_to_cout() { + logit::ConsoleLogger logger; + const std::string captured = capture_stream(std::cout, [&]() { + logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, "to-cout"), + "default-cout-message"); + logger.wait(); + }); + if (captured.find("default-cout-message") == std::string::npos) { + std::cerr << "FAIL: default constructor should write to std::cout" << std::endl; + std::exit(1); + } +} + +void test_explicit_cout_stream() { + std::stringstream sink; + logit::ConsoleLogger logger(sink); + logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, "explicit-cout"), + "explicit-cout-message"); + logger.wait(); + if (sink.str().find("explicit-cout-message") == std::string::npos) { + std::cerr << "FAIL: explicit ostringstream should receive message" << std::endl; + std::exit(1); + } +} + +void test_cerr_stream_routing() { + logit::ConsoleLogger cerr_logger(std::cerr); + const std::string cerr_captured = capture_stream(std::cerr, [&]() { + cerr_logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, "to-cerr"), + "cerr-message"); + cerr_logger.wait(); + }); + if (cerr_captured.find("cerr-message") == std::string::npos) { + std::cerr << "FAIL: cerr logger should write to std::cerr" << std::endl; + std::exit(1); + } +} + +void test_cout_does_not_pick_up_cerr_output() { + logit::ConsoleLogger cerr_logger(std::cerr); + const std::string cout_captured = capture_stream(std::cout, [&]() { + cerr_logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, "to-cerr"), + "isolation-check"); + cerr_logger.wait(); + }); + if (cout_captured.find("isolation-check") != std::string::npos) { + std::cerr << "FAIL: cerr logger should not write to std::cout" << std::endl; + std::exit(1); + } +} + +void test_level_based_routing_to_cerr() { + logit::ConsoleLogger::Config config; + logit::ConsoleStreamRoute err_route; + err_route.min_level = logit::LogLevel::LOG_LVL_ERROR; + err_route.max_level = logit::LogLevel::LOG_LVL_FATAL; + err_route.kind = logit::ConsoleStreamKind::Cerr; + config.routes.push_back(err_route); + + logit::ConsoleLogger primary(std::cout, config); + + const std::string cout_captured = capture_stream(std::cout, [&]() { + const std::string cerr_captured = capture_stream(std::cerr, [&]() { + primary.log(make_record(logit::LogLevel::LOG_LVL_INFO, "info-record"), + "info-route"); + primary.log(make_record(logit::LogLevel::LOG_LVL_ERROR, "error-record"), + "error-route"); + primary.wait(); + }); + if (cerr_captured.find("error-route") == std::string::npos) { + std::cerr << "FAIL: ERROR level should route to std::cerr" << std::endl; + std::exit(1); + } + if (cerr_captured.find("info-route") != std::string::npos) { + std::cerr << "FAIL: INFO level should not leak into std::cerr" << std::endl; + std::exit(1); + } + }); + if (cout_captured.find("info-route") == std::string::npos) { + std::cerr << "FAIL: INFO level should fall back to primary stream (cout)" << std::endl; + std::exit(1); + } + if (cout_captured.find("error-route") != std::string::npos) { + std::cerr << "FAIL: ERROR level should not fall through to primary stream" << std::endl; + std::exit(1); + } +} + +void test_level_based_routing_to_custom_stream() { + std::stringstream warn_sink; + logit::ConsoleLogger::Config config; + logit::ConsoleStreamRoute warn_route; + warn_route.min_level = logit::LogLevel::LOG_LVL_WARN; + warn_route.max_level = logit::LogLevel::LOG_LVL_WARN; + warn_route.kind = logit::ConsoleStreamKind::Custom; + warn_route.custom_stream = &warn_sink; + config.routes.push_back(warn_route); + + logit::ConsoleLogger primary(std::cout, config); + const std::string cout_captured = capture_stream(std::cout, [&]() { + primary.log(make_record(logit::LogLevel::LOG_LVL_INFO, "info-record"), + "info-fallback"); + primary.log(make_record(logit::LogLevel::LOG_LVL_WARN, "warn-record"), + "warn-routed"); + primary.log(make_record(logit::LogLevel::LOG_LVL_ERROR, "error-record"), + "error-fallback"); + primary.wait(); + }); + if (warn_sink.str().find("warn-routed") == std::string::npos) { + std::cerr << "FAIL: WARN should route to custom stream" << std::endl; + std::exit(1); + } + if (cout_captured.find("info-fallback") == std::string::npos || + cout_captured.find("error-fallback") == std::string::npos) { + std::cerr << "FAIL: non-matching levels should fall back to primary stream" << std::endl; + std::exit(1); + } +} + +} // namespace + +int main() { + test_default_constructor_writes_to_cout(); + test_explicit_cout_stream(); + test_cerr_stream_routing(); + test_cout_does_not_pick_up_cerr_output(); + test_level_based_routing_to_cerr(); + test_level_based_routing_to_custom_stream(); + return 0; +} From 6957708f6f68ee74ba8c6b785d3af38f39679055 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Wed, 3 Jun 2026 01:38:06 +0300 Subject: [PATCH 2/4] fix(console): harden stream routing Treat null custom streams as a no-op route, drop color writes into custom streams, trim the redundant config field copies in the dual-arg constructor, add static ConsoleStreamRoute helpers, rename TO_STREAM macros to STREAM, and cover sync and null-fallback paths in tests. Constraint: Preserve public API and default std::cout behavior Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.8 --- include/logit_cpp/logit/log_macros.hpp | 8 +-- .../logit_cpp/logit/loggers/ConsoleLogger.hpp | 36 ++++++---- .../logit/loggers/ConsoleStreamRoute.hpp | 33 ++++++++- tests/console_logger_stream_test.cpp | 68 +++++++++++++++++++ 4 files changed, 126 insertions(+), 19 deletions(-) diff --git a/include/logit_cpp/logit/log_macros.hpp b/include/logit_cpp/logit/log_macros.hpp index bce44bb..94fe8d1 100644 --- a/include/logit_cpp/logit/log_macros.hpp +++ b/include/logit_cpp/logit/log_macros.hpp @@ -2245,7 +2245,7 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT /// \brief Add a console logger writing to a caller-provided std::ostream& (e.g. std::cerr). /// \param stream Output stream reference; lifetime is owned by the caller. -#define LOGIT_ADD_CONSOLE_TO_STREAM(stream, pattern, async) \ +#define LOGIT_ADD_CONSOLE_STREAM(stream, pattern, async) \ logit::Logger::get_instance().add_logger( \ std::unique_ptr(new logit::ConsoleLogger( \ (stream), (async))), \ @@ -2253,7 +2253,7 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT false) /// \brief Add a console logger writing to a caller-provided std::ostream& in single_mode. -#define LOGIT_ADD_CONSOLE_TO_STREAM_SINGLE_MODE(stream, pattern, async) \ +#define LOGIT_ADD_CONSOLE_STREAM_SINGLE_MODE(stream, pattern, async) \ logit::Logger::get_instance().add_logger( \ std::unique_ptr(new logit::ConsoleLogger( \ (stream), (async))), \ @@ -2261,7 +2261,7 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT true) /// \brief Add a console logger from an explicit Config writing to a caller-provided std::ostream&. -#define LOGIT_ADD_CONSOLE_TO_STREAM_EX(stream, config, pattern) \ +#define LOGIT_ADD_CONSOLE_STREAM_EX(stream, config, pattern) \ logit::Logger::get_instance().add_logger( \ std::unique_ptr(new logit::ConsoleLogger( \ (stream), (config))), \ @@ -2269,7 +2269,7 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT false) /// \brief Add a console logger from an explicit Config writing to a caller-provided std::ostream& in single_mode. -#define LOGIT_ADD_CONSOLE_TO_STREAM_EX_SINGLE_MODE(stream, config, pattern) \ +#define LOGIT_ADD_CONSOLE_STREAM_EX_SINGLE_MODE(stream, config, pattern) \ logit::Logger::get_instance().add_logger( \ std::unique_ptr(new logit::ConsoleLogger( \ (stream), (config))), \ diff --git a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp index 6716060..f4776ff 100644 --- a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp +++ b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp @@ -113,12 +113,6 @@ namespace logit { /// \param config The configuration for the logger. ConsoleLogger(std::ostream& stream, const Config& config) : m_config(config), m_stream(&stream) { - m_config.async = config.async; - m_config.use_dedicated_executor = config.use_dedicated_executor; - m_config.queue_capacity = config.queue_capacity; - m_config.queue_policy = config.queue_policy; - m_config.default_color = config.default_color; - m_config.routes = config.routes; reset_color(); if (m_config.async && m_config.use_dedicated_executor) { m_executor.reset(new detail::SingleThreadExecutor()); @@ -380,21 +374,32 @@ namespace logit { /// \brief Selects the target stream for a given log level. /// \details Uses Config::routes when non-empty; falls back to the - /// primary stream. Must be called under m_mutex. + /// primary stream. Routes with kind == Custom and a null + /// custom_stream are skipped, so the next route or the primary + /// stream wins. Must be called under m_mutex. std::ostream* select_stream_for(LogLevel level) const { for (const auto& route : m_config.routes) { if (route.min_level > route.max_level) continue; - if (level >= route.min_level && level <= route.max_level) { - switch (route.kind) { - case ConsoleStreamKind::Cout: return &std::cout; - case ConsoleStreamKind::Cerr: return &std::cerr; - case ConsoleStreamKind::Custom: return route.custom_stream; - } + if (level < route.min_level || level > route.max_level) continue; + switch (route.kind) { + case ConsoleStreamKind::Cout: + return &std::cout; + case ConsoleStreamKind::Cerr: + return &std::cerr; + case ConsoleStreamKind::Custom: + if (route.custom_stream) return route.custom_stream; + break; } } return m_stream; } + /// \brief Returns true for streams whose console handle we may drive + /// (std::cout, std::cerr). Custom streams skip ANSI/Windows coloring. + static bool is_standard_console_stream(const std::ostream& stream) { + return &stream == &std::cout || &stream == &std::cerr; + } + /// \brief Writes one formatted message to a stream, applying colors /// when supported. Must be called under m_mutex. void write_colored_message(std::ostream& stream, const std::string& message) const { @@ -402,7 +407,7 @@ namespace logit { (void)stream; (void)message; # elif defined(_WIN32) - if (&stream == &std::cout || &stream == &std::cerr) { + if (is_standard_console_stream(stream)) { handle_ansi_colors_windows(message, stream); } else { stream << message << std::endl; @@ -609,7 +614,10 @@ namespace logit { /// \brief Resets the console text color to the default. + /// \details Only writes color escape codes for standard console + /// streams (std::cout / std::cerr); custom streams stay plain. void reset_color() { + if (!is_standard_console_stream(*m_stream)) return; # ifdef __EMSCRIPTEN__ // No persistent console color in browsers return; diff --git a/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp b/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp index 6a3ec64..bde6fd3 100644 --- a/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp +++ b/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp @@ -24,7 +24,38 @@ namespace logit { LogLevel min_level = LogLevel::LOG_LVL_TRACE; ///< Inclusive lower bound. LogLevel max_level = LogLevel::LOG_LVL_FATAL; ///< Inclusive upper bound. ConsoleStreamKind kind = ConsoleStreamKind::Cout; ///< Stream identifier. - std::ostream* custom_stream = nullptr; ///< Used when kind == Custom; null is ignored. + std::ostream* custom_stream = nullptr; ///< Used when kind == Custom; null is ignored and the route falls back. + + /// \brief Builds a route that targets std::cout for the given level range. + static ConsoleStreamRoute to_cout(LogLevel min_level, LogLevel max_level) { + ConsoleStreamRoute route; + route.min_level = min_level; + route.max_level = max_level; + route.kind = ConsoleStreamKind::Cout; + return route; + } + + /// \brief Builds a route that targets std::cerr for the given level range. + static ConsoleStreamRoute to_cerr(LogLevel min_level, LogLevel max_level) { + ConsoleStreamRoute route; + route.min_level = min_level; + route.max_level = max_level; + route.kind = ConsoleStreamKind::Cerr; + return route; + } + + /// \brief Builds a route that targets a caller-provided std::ostream for the given level range. + static ConsoleStreamRoute to_stream( + LogLevel min_level, + LogLevel max_level, + std::ostream& stream) { + ConsoleStreamRoute route; + route.min_level = min_level; + route.max_level = max_level; + route.kind = ConsoleStreamKind::Custom; + route.custom_stream = &stream; + return route; + } }; } // namespace logit diff --git a/tests/console_logger_stream_test.cpp b/tests/console_logger_stream_test.cpp index 6714048..e87583e 100644 --- a/tests/console_logger_stream_test.cpp +++ b/tests/console_logger_stream_test.cpp @@ -149,6 +149,71 @@ void test_level_based_routing_to_custom_stream() { } } +void test_null_custom_route_falls_back_to_primary_stream() { + logit::ConsoleLogger::Config config; + logit::ConsoleStreamRoute route; + route.min_level = logit::LogLevel::LOG_LVL_ERROR; + route.max_level = logit::LogLevel::LOG_LVL_FATAL; + route.kind = logit::ConsoleStreamKind::Custom; + route.custom_stream = nullptr; + config.routes.push_back(route); + + std::stringstream sink; + logit::ConsoleLogger logger(sink, config); + + logger.log(make_record(logit::LogLevel::LOG_LVL_ERROR, "error-record"), + "fallback-error"); + logger.wait(); + + if (sink.str().find("fallback-error") == std::string::npos) { + std::cerr << "FAIL: null custom stream should fall back to primary stream" << std::endl; + std::exit(1); + } +} + +void test_sync_custom_stream() { + std::stringstream sink; + logit::ConsoleLogger::Config config; + config.async = false; + + logit::ConsoleLogger logger(sink, config); + logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, "sync-record"), + "sync-message"); + + if (sink.str().find("sync-message") == std::string::npos) { + std::cerr << "FAIL: sync logger should write to custom stream" << std::endl; + std::exit(1); + } +} + +void test_sync_level_based_routing_to_custom_stream() { + std::stringstream primary_sink; + std::stringstream error_sink; + + logit::ConsoleLogger::Config config; + config.async = false; + config.routes.push_back(logit::ConsoleStreamRoute::to_stream( + logit::LogLevel::LOG_LVL_ERROR, + logit::LogLevel::LOG_LVL_FATAL, + error_sink)); + + logit::ConsoleLogger logger(primary_sink, config); + + logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, "info-record"), + "sync-info"); + logger.log(make_record(logit::LogLevel::LOG_LVL_ERROR, "error-record"), + "sync-error"); + + if (primary_sink.str().find("sync-info") == std::string::npos) { + std::cerr << "FAIL: INFO should go to primary stream" << std::endl; + std::exit(1); + } + if (error_sink.str().find("sync-error") == std::string::npos) { + std::cerr << "FAIL: ERROR should route to custom stream" << std::endl; + std::exit(1); + } +} + } // namespace int main() { @@ -158,5 +223,8 @@ int main() { test_cout_does_not_pick_up_cerr_output(); test_level_based_routing_to_cerr(); test_level_based_routing_to_custom_stream(); + test_null_custom_route_falls_back_to_primary_stream(); + test_sync_custom_stream(); + test_sync_level_based_routing_to_custom_stream(); return 0; } From f06417a19f0ddde08b0b1f2360fd45b2e0f92dd3 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Wed, 3 Jun 2026 02:19:06 +0300 Subject: [PATCH 3/4] refactor(console): colocate stream route header Move ConsoleStreamRoute.hpp under loggers/ConsoleLogger/ and rely on the existing enums pull-in via the logger aggregator instead of an explicit ../enums.hpp include. Constraint: Preserve public include path semantics through ConsoleLogger.hpp Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.8 --- include/logit_cpp/logit/loggers/ConsoleLogger.hpp | 2 +- .../logit/loggers/{ => ConsoleLogger}/ConsoleStreamRoute.hpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename include/logit_cpp/logit/loggers/{ => ConsoleLogger}/ConsoleStreamRoute.hpp (99%) diff --git a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp index f4776ff..34e3ea4 100644 --- a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp +++ b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp @@ -6,7 +6,7 @@ /// \brief Console logger implementation that outputs logs to the console with color support. #include "ILogger.hpp" -#include "ConsoleStreamRoute.hpp" +#include "ConsoleLogger/ConsoleStreamRoute.hpp" #include #include #include diff --git a/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp b/include/logit_cpp/logit/loggers/ConsoleLogger/ConsoleStreamRoute.hpp similarity index 99% rename from include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp rename to include/logit_cpp/logit/loggers/ConsoleLogger/ConsoleStreamRoute.hpp index bde6fd3..f3197d3 100644 --- a/include/logit_cpp/logit/loggers/ConsoleStreamRoute.hpp +++ b/include/logit_cpp/logit/loggers/ConsoleLogger/ConsoleStreamRoute.hpp @@ -5,7 +5,6 @@ /// \file ConsoleStreamRoute.hpp /// \brief Level-based output stream routing for ConsoleLogger. -#include "../enums.hpp" #include namespace logit { From 366f78158304a7b2e4c5cd64dc20c36218b0b8b3 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Wed, 3 Jun 2026 02:48:13 +0300 Subject: [PATCH 4/4] test(console): cover to_cerr helper and constify getter Add a small unit test for the to_cerr helper and mark ConsoleLogger::get_config() as const so it matches the rest of the read-only accessors. Constraint: Preserve public API Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.8 --- include/logit_cpp/logit/loggers/ConsoleLogger.hpp | 2 +- tests/console_logger_stream_test.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp index 34e3ea4..c506f38 100644 --- a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp +++ b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp @@ -192,7 +192,7 @@ namespace logit { /// \brief Gets the current logger configuration. /// Returns the logger's configuration with thread safety ensured. /// \return The current configuration. - Config get_config() { + Config get_config() const { std::lock_guard lock(m_mutex); return m_config; } diff --git a/tests/console_logger_stream_test.cpp b/tests/console_logger_stream_test.cpp index e87583e..d5c3ca2 100644 --- a/tests/console_logger_stream_test.cpp +++ b/tests/console_logger_stream_test.cpp @@ -214,6 +214,19 @@ void test_sync_level_based_routing_to_custom_stream() { } } +void test_route_helpers_to_cerr() { + logit::ConsoleStreamRoute route = logit::ConsoleStreamRoute::to_cerr( + logit::LogLevel::LOG_LVL_ERROR, + logit::LogLevel::LOG_LVL_FATAL); + + if (route.kind != logit::ConsoleStreamKind::Cerr || + route.min_level != logit::LogLevel::LOG_LVL_ERROR || + route.max_level != logit::LogLevel::LOG_LVL_FATAL) { + std::cerr << "FAIL: to_cerr helper should initialize route" << std::endl; + std::exit(1); + } +} + } // namespace int main() { @@ -226,5 +239,6 @@ int main() { test_null_custom_route_falls_back_to_primary_stream(); test_sync_custom_stream(); test_sync_level_based_routing_to_custom_stream(); + test_route_helpers_to_cerr(); return 0; }