From cdb994e2993a7e60ab1e26e8b5b5a2831f804977 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 24 Aug 2026 10:07:01 -0500 Subject: [PATCH] refactor(logger): give each component library its own logger The logger was a single process-wide instance hosted in one compiled translation unit, so every solver library shared it. Splitting libcuopt into components means routing and mathopt should log independently, and nothing should have to exist purely to host the state. The logger is now header-only and, crucially, hidden. Hidden visibility is what does the separating: the static local of an inline function is emitted as an STB_GNU_UNIQUE symbol, which glibc merges across the whole process regardless of RTLD_LOCAL, so a header-only logger with default visibility would still have been one shared instance. Callers outside the libraries cannot reach a hidden logger, so each component exports a configure entry point. `init_logger_t` keeps its meaning -- configure the logger of whichever image constructs it, which is what the pdlp, mip and grpc solve paths already want -- and the new `init_component_logger_t` reaches a chosen library from outside. It defaults to mathopt, so all eight existing external call sites keep working unchanged, and routing is opted into explicitly. Two things had to change to make one log file survive several loggers: - The exported entry point now takes the same ref-count guard that `init_logger_t` takes. Without it the MIP solve path reconfigured the logger mid-run and, with truncate set, cleared a file the caller had already written to. - File sinks always open in append mode, with a single explicit truncate up front. A non-appending sink writes from offset 0 and silently overwrites what another logger has appended. routing::solve now initialises its own logger from the settings. Routing never constructed one, so its CUOPT_LOG_ERROR calls went into a buffer that nothing drained and were lost. Verified: libcuopt.so exports the four entry points and none of the logger state; cuopt_cli writes both its own and the solver's messages to one file and still truncates between runs. ctest failures are identical to clean main in this environment (10 suites, 908 gtest failures, both). Co-Authored-By: Claude Opus 5 --- cpp/CMakeLists.txt | 6 + cpp/cuopt_cli.cpp | 11 +- cpp/src/CMakeLists.txt | 1 - cpp/src/math_optimization/CMakeLists.txt | 1 + cpp/src/math_optimization/logger_entry.cpp | 24 ++ cpp/src/routing/CMakeLists.txt | 3 +- cpp/src/routing/logger_entry.cpp | 24 ++ cpp/src/routing/solve.cu | 4 + cpp/src/utilities/logger.cpp | 191 ---------- cpp/src/utilities/logger.hpp | 343 +++++++++++++++++- cpp/tests/dual_simplex/unit_tests/solve.cpp | 8 +- .../dual_simplex/unit_tests/solve_barrier.cu | 4 +- 12 files changed, 408 insertions(+), 212 deletions(-) create mode 100644 cpp/src/math_optimization/logger_entry.cpp create mode 100644 cpp/src/routing/logger_entry.cpp delete mode 100644 cpp/src/utilities/logger.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 4ce11b830b..3d121d7357 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -569,6 +569,12 @@ target_compile_definitions(cuopt_objs PUBLIC CUSPARSE_ENABLE_EXPERIMENTAL_API ) +# Lets callers reach routing's logger through init_component_logger_t. Routing is optional, +# so the entry point it declares is only linkable when routing was actually built. +if(NOT SKIP_ROUTING_BUILD) + target_compile_definitions(cuopt_objs PUBLIC CUOPT_HAS_ROUTING) +endif() + target_compile_options(cuopt_objs PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" "$<$:${CUOPT_CUDA_FLAGS}>" diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index e070425eab..539f9f1321 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -100,8 +100,15 @@ int run_single_file(const std::string& file_path, cuopt::mathematical_optimization::io::mps_reader_type_t mps_reader, cuopt::mathematical_optimization::solver_settings_t& settings) { - cuopt::init_logger_t log(settings.get_parameter(CUOPT_LOG_FILE), - settings.get_parameter(CUOPT_LOG_TO_CONSOLE)); + // The solver's logger lives in the solver library and is not reachable from here, so + // configure it through its exported entry point. The CLI then configures its own logger + // for the messages it emits itself; it appends rather than truncates so that it does not + // clear the file the solver has just opened. + const auto log_file = settings.get_parameter(CUOPT_LOG_FILE); + const auto log_console = settings.get_parameter(CUOPT_LOG_TO_CONSOLE); + + cuopt::init_component_logger_t solver_log(log_file, log_console); + cuopt::init_logger_t log(log_file, log_console, /*truncate=*/false); std::string base_filename = file_path.substr(file_path.find_last_of("/\\") + 1); diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index e8737cf6da..db71f8fa4d 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -4,7 +4,6 @@ # cmake-format: on set(UTIL_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/seed_generator.cu - ${CMAKE_CURRENT_SOURCE_DIR}/utilities/logger.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/version_info.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/timestamp_utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/work_unit_scheduler.cpp) diff --git a/cpp/src/math_optimization/CMakeLists.txt b/cpp/src/math_optimization/CMakeLists.txt index efa1600c54..25449bc929 100644 --- a/cpp/src/math_optimization/CMakeLists.txt +++ b/cpp/src/math_optimization/CMakeLists.txt @@ -9,6 +9,7 @@ list(PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/solution_reader.cu ${CMAKE_CURRENT_SOURCE_DIR}/solution_writer.cu ${CMAKE_CURRENT_SOURCE_DIR}/tic_toc.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/logger_entry.cpp ) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} diff --git a/cpp/src/math_optimization/logger_entry.cpp b/cpp/src/math_optimization/logger_entry.cpp new file mode 100644 index 0000000000..b1ed0907a7 --- /dev/null +++ b/cpp/src/math_optimization/logger_entry.cpp @@ -0,0 +1,24 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +/* + * The logger itself is header-only and hidden, so it is private to each component library. + * This translation unit is compiled into cuopt_mathopt only, which is what makes the + * functions below reach mathopt's instance and no other. + */ +namespace cuopt::mathematical_optimization { + +void configure_logging(const std::string& log_file, bool log_to_console, bool truncate) +{ + cuopt::configure_logging_impl(log_file, log_to_console, truncate); +} + +void reset_logging() { cuopt::reset_logging_impl(); } + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/routing/CMakeLists.txt b/cpp/src/routing/CMakeLists.txt index 452c4806da..abf11bd6d0 100644 --- a/cpp/src/routing/CMakeLists.txt +++ b/cpp/src/routing/CMakeLists.txt @@ -1,9 +1,10 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on set(ROUTING_SRC_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/logger_entry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/local_search/compute_insertions.cu ${CMAKE_CURRENT_SOURCE_DIR}/ges/squeeze.cu ${CMAKE_CURRENT_SOURCE_DIR}/local_search/sliding_window.cu diff --git a/cpp/src/routing/logger_entry.cpp b/cpp/src/routing/logger_entry.cpp new file mode 100644 index 0000000000..b284623e1f --- /dev/null +++ b/cpp/src/routing/logger_entry.cpp @@ -0,0 +1,24 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +/* + * The logger itself is header-only and hidden, so it is private to each component library. + * This translation unit is compiled into cuopt_routing only, which is what makes the + * functions below reach routing's instance and no other. + */ +namespace cuopt::routing { + +void configure_logging(const std::string& log_file, bool log_to_console, bool truncate) +{ + cuopt::configure_logging_impl(log_file, log_to_console, truncate); +} + +void reset_logging() { cuopt::reset_logging_impl(); } + +} // namespace cuopt::routing diff --git a/cpp/src/routing/solve.cu b/cpp/src/routing/solve.cu index a7caf88ad9..89c6ed2c59 100644 --- a/cpp/src/routing/solve.cu +++ b/cpp/src/routing/solve.cu @@ -16,6 +16,10 @@ template assignment_t solve(data_model_view_t const& data_model, solver_settings_t const& settings) { + // Routing's logger is private to cuopt_routing and starts out sinking into a buffer, so + // without this the CUOPT_LOG_ERROR calls below are recorded and never emitted anywhere. + init_logger_t log("", settings.get_error_logging_mode()); + try { cuopt::routing::solver_t solver(data_model, settings); return solver.solve(); diff --git a/cpp/src/utilities/logger.cpp b/cpp/src/utilities/logger.cpp deleted file mode 100644 index 217f9c64cb..0000000000 --- a/cpp/src/utilities/logger.cpp +++ /dev/null @@ -1,191 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#include -#include - -namespace cuopt { - -struct buffered_entry { - rapids_logger::level_enum level; - std::string msg; -}; - -// Buffer to store log messages -class log_buffer { - public: - log_buffer() = default; - ~log_buffer() = default; - - void log(rapids_logger::level_enum lvl, const char* msg) - { - std::lock_guard lock(mutex); - if (!msg) return; - std::string str(msg); - - if (!str.empty() && str.back() == '\n') { str.pop_back(); } - messages.push_back({lvl, std::move(str)}); - } - - size_t size() const - { - std::lock_guard lock(mutex); - return messages.size(); - } - - std::vector drain_all() - { - std::lock_guard lock(mutex); - std::vector out; - out.swap(messages); - return out; - } - - std::vector messages; - mutable std::mutex mutex; -}; - -log_buffer& global_log_buffer() -{ - static log_buffer buffer; - return buffer; -} - -// Callback function for the buffer sink -static void buffer_log_callback(int lvl, const char* msg) -{ - // store level with message; actual filtering happens at logger time - global_log_buffer().log(static_cast(lvl), msg); -} - -/** - * @brief Returns the default sink for the global logger. - * - * If the environment variable `CUOPT_DEBUG_LOG_FILE` is defined, the default sink is a sink to that - * file. Otherwise, the default is to dump to stderr. - * - * @return sink_ptr The sink to use - */ -rapids_logger::sink_ptr default_sink() -{ - return std::make_shared(buffer_log_callback); -} - -/** - * @brief Returns the default log pattern for the global logger. - * - * @return std::string The default log pattern. - */ -inline std::string default_pattern() { return "[%Y-%m-%d %H:%M:%S:%f] [%n] [%-6l] %v"; } - -/** - * @brief Returns the default log level for the global logger. - * - * @return rapids_logger::level_enum The default log level. - */ -inline rapids_logger::level_enum default_level() -{ -#if CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_TRACE - return rapids_logger::level_enum::trace; -#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_DEBUG - return rapids_logger::level_enum::debug; -#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_INFO - return rapids_logger::level_enum::info; -#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_WARN - return rapids_logger::level_enum::warn; -#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_ERROR - return rapids_logger::level_enum::error; -#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_CRITICAL - return rapids_logger::level_enum::critical; -#else - return rapids_logger::level_enum::info; -#endif -} - -rapids_logger::logger& default_logger() -{ - static rapids_logger::logger logger_ = [] { - rapids_logger::logger logger_{"CUOPT", {default_sink()}}; -#if CUOPT_LOG_ACTIVE_LEVEL >= RAPIDS_LOGGER_LOG_LEVEL_INFO - logger_.set_pattern("%v"); -#else - logger_.set_pattern(default_pattern()); -#endif - logger_.set_level(default_level()); - logger_.flush_on(rapids_logger::level_enum::debug); - - return logger_; - }(); - - return logger_; -} - -void reset_default_logger() -{ - default_logger().sinks().clear(); - default_logger().sinks().push_back(default_sink()); -#if CUOPT_LOG_ACTIVE_LEVEL >= RAPIDS_LOGGER_LOG_LEVEL_INFO - default_logger().set_pattern("%v"); -#else - default_logger().set_pattern(default_pattern()); -#endif - default_logger().set_level(default_level()); - default_logger().flush_on(rapids_logger::level_enum::debug); -} - -// Guard object whose destructor resets the logger -struct logger_config_guard { - ~logger_config_guard() { cuopt::reset_default_logger(); } -}; - -// Weak reference to detect if any init_logger_t instance is still alive -static std::weak_ptr g_active_guard; -static std::mutex g_guard_mutex; - -init_logger_t::init_logger_t(std::string log_file, bool log_to_console) -{ - std::lock_guard lock(g_guard_mutex); - - auto existing_guard = g_active_guard.lock(); - if (existing_guard) { - // Reuse existing configuration, just hold a reference to keep it alive - guard_ = existing_guard; - return; - } - - cuopt::default_logger().sinks().clear(); - - // re-initialize sinks - if (log_to_console) { - cuopt::default_logger().sinks().push_back( - std::make_shared(std::cout)); - } - if (!log_file.empty()) { - cuopt::default_logger().sinks().push_back( - std::make_shared(log_file, true)); - cuopt::default_logger().flush_on(rapids_logger::level_enum::debug); - } - -#if CUOPT_LOG_ACTIVE_LEVEL >= RAPIDS_LOGGER_LOG_LEVEL_INFO - cuopt::default_logger().set_pattern("%v"); -#else - cuopt::default_logger().set_pattern(cuopt::default_pattern()); -#endif - - // Extract messages from the global buffer and log to the default logger - auto buffered_messages = global_log_buffer().drain_all(); - for (const auto& entry : buffered_messages) { - cuopt::default_logger().log(entry.level, entry.msg.c_str()); - } - - // Create guard and store weak reference for future instances to find - auto guard = std::make_shared(); - g_active_guard = guard; - guard_ = guard; -} - -} // namespace cuopt diff --git a/cpp/src/utilities/logger.hpp b/cpp/src/utilities/logger.hpp index 2f9053b05f..bdc9564319 100644 --- a/cpp/src/utilities/logger.hpp +++ b/cpp/src/utilities/logger.hpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -22,32 +23,352 @@ #include #include -namespace CUOPT_EXPORT cuopt { +/* + * The logger and its buffer are defined inline and with hidden visibility, so each library + * that links this header owns its own. cuOpt ships as separate solver libraries and + * rapids_logger provides the logger type rather than a shared instance, so there is no + * single place to host one without a library existing purely to hold it. Each solver + * configures its own logging through its own settings. + * + * Hidden visibility is what does the separating, and it is not optional. The static local + * of an inline function is emitted as an STB_GNU_UNIQUE symbol, which glibc merges across + * the whole process regardless of RTLD_LOCAL, so a header-only logger with default + * visibility would still be one shared instance. Do not mark this namespace CUOPT_EXPORT. + * + * Callers outside the libraries cannot reach a hidden logger, so each component exports a + * configure entry point instead -- see log_target_t and init_component_logger_t below. + */ +namespace cuopt { + +struct buffered_entry { + rapids_logger::level_enum level; + std::string msg; +}; + +// Buffer to store log messages +class log_buffer { + public: + log_buffer() = default; + ~log_buffer() = default; + + void log(rapids_logger::level_enum lvl, const char* msg) + { + std::lock_guard lock(mutex); + if (!msg) return; + std::string str(msg); + + if (!str.empty() && str.back() == '\n') { str.pop_back(); } + messages.push_back({lvl, std::move(str)}); + } + + size_t size() const + { + std::lock_guard lock(mutex); + return messages.size(); + } + + std::vector drain_all() + { + std::lock_guard lock(mutex); + std::vector out; + out.swap(messages); + return out; + } + + std::vector messages; + mutable std::mutex mutex; +}; + +inline log_buffer& global_log_buffer() +{ + static log_buffer buffer; + return buffer; +} + +// Callback function for the buffer sink +inline void buffer_log_callback(int lvl, const char* msg) +{ + // store level with message; actual filtering happens at logger time + global_log_buffer().log(static_cast(lvl), msg); +} /** - * @brief Get the default logger. + * @brief Returns the default sink for the global logger. + * + * If the environment variable `CUOPT_DEBUG_LOG_FILE` is defined, the default sink is a sink to that + * file. Otherwise, the default is to dump to stderr. * - * @return logger& The default logger + * @return sink_ptr The sink to use */ -rapids_logger::logger& default_logger(); +inline rapids_logger::sink_ptr default_sink() +{ + return std::make_shared(buffer_log_callback); +} /** - * @brief Reset the default logger to the default settings. - * This is needed when we are running multiple tests and each test has different logger settings - * and we need to reset the logger to the default settings before each test. + * @brief Returns the default log pattern for the global logger. + * + * @return std::string The default log pattern. */ -void reset_default_logger(); +inline std::string default_pattern() { return "[%Y-%m-%d %H:%M:%S:%f] [%n] [%-6l] %v"; } + +/** + * @brief Returns the default log level for the global logger. + * + * @return rapids_logger::level_enum The default log level. + */ +inline rapids_logger::level_enum default_level() +{ +#if CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_TRACE + return rapids_logger::level_enum::trace; +#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_DEBUG + return rapids_logger::level_enum::debug; +#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_INFO + return rapids_logger::level_enum::info; +#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_WARN + return rapids_logger::level_enum::warn; +#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_ERROR + return rapids_logger::level_enum::error; +#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_CRITICAL + return rapids_logger::level_enum::critical; +#else + return rapids_logger::level_enum::info; +#endif +} + +inline rapids_logger::logger& default_logger() +{ + static rapids_logger::logger logger_ = [] { + rapids_logger::logger logger_{"CUOPT", {default_sink()}}; +#if CUOPT_LOG_ACTIVE_LEVEL >= RAPIDS_LOGGER_LOG_LEVEL_INFO + logger_.set_pattern("%v"); +#else + logger_.set_pattern(default_pattern()); +#endif + logger_.set_level(default_level()); + logger_.flush_on(rapids_logger::level_enum::debug); + + return logger_; + }(); + + return logger_; +} + +inline void reset_default_logger() +{ + default_logger().sinks().clear(); + default_logger().sinks().push_back(default_sink()); +#if CUOPT_LOG_ACTIVE_LEVEL >= RAPIDS_LOGGER_LOG_LEVEL_INFO + default_logger().set_pattern("%v"); +#else + default_logger().set_pattern(default_pattern()); +#endif + default_logger().set_level(default_level()); + default_logger().flush_on(rapids_logger::level_enum::debug); +} + +/** + * @brief Point this image's logger at the given sinks and flush anything buffered so far. + * + * @param log_file File to log to, or empty for none. + * @param log_to_console Whether to also log to stdout. + * @param truncate Whether opening @p log_file clears it. Pass false when another + * image is already logging to the same path and has truncated it. + */ +inline void apply_logger_config(const std::string& log_file, bool log_to_console, bool truncate) +{ + cuopt::default_logger().sinks().clear(); + + // re-initialize sinks + if (log_to_console) { + cuopt::default_logger().sinks().push_back( + std::make_shared(std::cout)); + } + if (!log_file.empty()) { + // Clear the file up front rather than letting the sink truncate. Several loggers in one + // process can share a path -- the CLI has its own and the solver library has another -- + // and a truncating sink writes from offset 0, silently overwriting whatever the other + // one has already appended. Opening every sink in append mode keeps them interleaving. + if (truncate) { std::ofstream(log_file, std::ios::trunc); } + cuopt::default_logger().sinks().push_back( + std::make_shared(log_file, /*truncate=*/false)); + cuopt::default_logger().flush_on(rapids_logger::level_enum::debug); + } + +#if CUOPT_LOG_ACTIVE_LEVEL >= RAPIDS_LOGGER_LOG_LEVEL_INFO + cuopt::default_logger().set_pattern("%v"); +#else + cuopt::default_logger().set_pattern(cuopt::default_pattern()); +#endif + + // Extract messages from the global buffer and log to the default logger + auto buffered_messages = global_log_buffer().drain_all(); + for (const auto& entry : buffered_messages) { + cuopt::default_logger().log(entry.level, entry.msg.c_str()); + } +} -// Ref-counted logger initializer +/** + * @brief Ref-counted initializer for the logger of the image that constructs it. + * + * Library code uses this directly: constructed inside cuopt_routing it configures routing's + * logger, inside cuopt_mathopt it configures mathopt's. Callers outside the libraries get + * their own logger this way and should use init_component_logger_t to reach a library's. + */ class init_logger_t { // Using shared_ptr for ref-counting std::shared_ptr guard_; public: - init_logger_t(std::string log_file, bool log_to_console); + init_logger_t(std::string log_file, bool log_to_console, bool truncate = true); +}; + +// Guard object whose destructor resets the logger +struct logger_config_guard { + ~logger_config_guard() { cuopt::reset_default_logger(); } +}; + +// Weak reference to detect if any init_logger_t instance is still alive +inline std::weak_ptr g_active_guard; +inline std::mutex g_guard_mutex; + +// Holds this library's configuration alive when it was set from outside, since the external +// caller has no object in this image to own it. +inline std::shared_ptr& external_config_guard() +{ + static std::shared_ptr guard; + return guard; +} + +/** + * @brief Body of a component's exported configure entry point. + * + * Takes the same guard that init_logger_t takes, and keeps it alive. Library code that later + * constructs an init_logger_t of its own -- the MIP and PDLP solve paths both do -- then sees + * a live configuration and reuses it. Without that, the solver would reconfigure the logger + * mid-run and, with truncate set, clear a log file the caller had already written to. + */ +inline void configure_logging_impl(const std::string& log_file, bool log_to_console, bool truncate) +{ + std::lock_guard lock(g_guard_mutex); + + apply_logger_config(log_file, log_to_console, truncate); + + auto guard = std::make_shared(); + g_active_guard = guard; + external_config_guard() = guard; +} + +inline void reset_logging_impl() +{ + std::lock_guard lock(g_guard_mutex); + external_config_guard().reset(); +} + +inline init_logger_t::init_logger_t(std::string log_file, bool log_to_console, bool truncate) +{ + std::lock_guard lock(g_guard_mutex); + + auto existing_guard = g_active_guard.lock(); + if (existing_guard) { + // Reuse existing configuration, just hold a reference to keep it alive + guard_ = existing_guard; + return; + } + + apply_logger_config(log_file, log_to_console, truncate); + + // Create guard and store weak reference for future instances to find + auto guard = std::make_shared(); + g_active_guard = guard; + guard_ = guard; +} + +/** + * @brief Which component library's logger to configure. + */ +enum class log_target_t { + mathopt, ///< LP / MILP / QP, in cuopt_mathopt + routing ///< VRP, in cuopt_routing +}; + +} // namespace cuopt + +/* + * Exported per-component entry points. Each is defined in exactly one component library and + * configures that library's own hidden logger. They are the only logging symbols that cross + * a library boundary. + */ +namespace cuopt::mathematical_optimization { +CUOPT_EXPORT void configure_logging(const std::string& log_file, + bool log_to_console, + bool truncate); +CUOPT_EXPORT void reset_logging(); +} // namespace cuopt::mathematical_optimization + +#ifdef CUOPT_HAS_ROUTING +namespace cuopt::routing { +CUOPT_EXPORT void configure_logging(const std::string& log_file, + bool log_to_console, + bool truncate); +CUOPT_EXPORT void reset_logging(); +} // namespace cuopt::routing +#endif + +namespace cuopt { + +/** + * @brief Configures a component library's logger from outside that library. + * + * `init_logger_t` configures the logger of whichever image constructs it, which is what + * library code wants but not what an external caller wants: the CLI, the tests and the + * Python bindings each hold their own logger and need to reach into the solver's. This + * dispatches to the component's exported entry point instead. + * + * Defaults to mathopt because every external caller today is LP or MILP; routing is opted + * into explicitly. + */ +class init_component_logger_t { + log_target_t target_; + + public: + explicit init_component_logger_t(const std::string& log_file, + bool log_to_console, + log_target_t target = log_target_t::mathopt, + bool truncate = true) + : target_(target) + { + switch (target_) { + case log_target_t::routing: +#ifdef CUOPT_HAS_ROUTING + cuopt::routing::configure_logging(log_file, log_to_console, truncate); +#endif + break; + case log_target_t::mathopt: + default: + cuopt::mathematical_optimization::configure_logging(log_file, log_to_console, truncate); + break; + } + } + + ~init_component_logger_t() + { + switch (target_) { + case log_target_t::routing: +#ifdef CUOPT_HAS_ROUTING + cuopt::routing::reset_logging(); +#endif + break; + case log_target_t::mathopt: + default: cuopt::mathematical_optimization::reset_logging(); break; + } + } + + init_component_logger_t(const init_component_logger_t&) = delete; + init_component_logger_t& operator=(const init_component_logger_t&) = delete; }; -} // namespace CUOPT_EXPORT cuopt +} // namespace cuopt namespace cuopt::detail { diff --git a/cpp/tests/dual_simplex/unit_tests/solve.cpp b/cpp/tests/dual_simplex/unit_tests/solve.cpp index 2e44442599..d502ca9292 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve.cpp +++ b/cpp/tests/dual_simplex/unit_tests/solve.cpp @@ -23,7 +23,7 @@ namespace cuopt::mathematical_optimization::simplex::test { TEST(dual_simplex, chess_set) { - cuopt::init_logger_t log("", true); + cuopt::init_component_logger_t log("", true); namespace simplex = cuopt::mathematical_optimization::simplex; raft::handle_t handle{}; simplex::user_problem_t user_problem(&handle); @@ -97,7 +97,7 @@ TEST(dual_simplex, chess_set) TEST(dual_simplex, burglar) { - cuopt::init_logger_t log("", true); + cuopt::init_component_logger_t log("", true); constexpr int num_items = 8; constexpr double max_weight = 102; @@ -173,7 +173,7 @@ TEST(dual_simplex, burglar) TEST(dual_simplex, empty_columns) { - cuopt::init_logger_t log("", true); + cuopt::init_component_logger_t log("", true); // Same as burglar problem above but with an empty column inserted constexpr int num_items = 9; constexpr double max_weight = 102; @@ -262,7 +262,7 @@ TEST(dual_simplex, empty_columns) TEST(dual_simplex, dual_variable_greater_than) { - cuopt::init_logger_t log("", true); + cuopt::init_component_logger_t log("", true); // minimize 3*x0 + 2 * x1 // subject to x0 + x1 >= 1 // x0 + 2x1 >= 3 diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index 16640c6c60..9f5963acec 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -41,7 +41,7 @@ static void init_handler(const raft::handle_t* handle_ptr) TEST(barrier, chess_set) { - cuopt::init_logger_t log("", true); + cuopt::init_component_logger_t log("", true); namespace simplex = cuopt::mathematical_optimization::simplex; raft::handle_t handle{}; init_handler(&handle); @@ -111,7 +111,7 @@ TEST(barrier, chess_set) TEST(barrier, dual_variable_greater_than) { - cuopt::init_logger_t log("", true); + cuopt::init_component_logger_t log("", true); // minimize 3*x0 + 2 * x1 // subject to x0 + x1 >= 1 // x0 + 2x1 >= 3