diff --git a/include/ygm/comm.hpp b/include/ygm/comm.hpp index bd9739fd..155b806c 100644 --- a/include/ygm/comm.hpp +++ b/include/ygm/comm.hpp @@ -261,6 +261,31 @@ class comm { m_logger.set_log_level(level); } + /** + * @brief Get the log level currently used in YGM + * + * @return Current log level + */ + log_level get_log_level() { return m_logger.get_log_level(); } + + /** + * @brief Set the logger target to use in YGM + * + * @param target Logger target to use. Possible values are + * ygm::logger_target::file, ygm::logger_target::stdout, and + * ygm::logger_target::stderr + */ + void set_logger_target(const ygm::logger_target target) { + m_logger.set_logger_target(target); + } + + /** + * @brief Get the logger target currently used in YGM + * + * @return Current logger target + */ + logger_target get_logger_target() { return m_logger.get_logger_target(); } + /** * @brief Add a message to the YGM logs * @@ -278,9 +303,33 @@ class comm { m_logger.log(level, args...); } + /** + * @brief Add a message to the YGM logs written to multiple targets + * + * @tparam Args... Variadic types to add to log + * @param targets Vector of targets to write logs to + * @param Minimum log level for logging message + * @args Variadic arguments add to log + */ + template + void log(const std::vector &targets, + const ygm::log_level level, Args &&...args) const { + m_logger.log(targets, level, args...); + } + + /** + * @brief Set the log location to use when logging to files + * + * @param s Log location + */ template void set_log_location(const StringType &s); + /** + * @brief Set the log location to use when logging to files + * + * @param p Log location + */ void set_log_location(std::filesystem::path p); // Private member functions diff --git a/include/ygm/detail/comm.ipp b/include/ygm/detail/comm.ipp index a63771dc..30f81723 100644 --- a/include/ygm/detail/comm.ipp +++ b/include/ygm/detail/comm.ipp @@ -36,7 +36,8 @@ struct comm::header_t { inline comm::comm(int *argc, char ***argv) : pimpl_if(std::make_shared(argc, argv)), m_layout(MPI_COMM_WORLD), - m_router(m_layout, config.routing) { + m_router(m_layout, config.routing), + m_logger(m_layout.size()) { // pimpl_if = std::make_shared(argc, argv); comm_setup(MPI_COMM_WORLD); } @@ -48,7 +49,9 @@ inline comm::comm(int *argc, char ***argv) * @return Constructed ygm::comm object */ inline comm::comm(MPI_Comm mcomm) - : m_layout(mcomm), m_router(m_layout, config.routing) { + : m_layout(mcomm), + m_router(m_layout, config.routing), + m_logger(m_layout.size()) { pimpl_if.reset(); int flag(0); YGM_ASSERT_MPI(MPI_Initialized(&flag)); diff --git a/include/ygm/detail/logger.hpp b/include/ygm/detail/logger.hpp index cb575810..67bba969 100644 --- a/include/ygm/detail/logger.hpp +++ b/include/ygm/detail/logger.hpp @@ -5,10 +5,13 @@ #pragma once +#include "spdlog/pattern_formatter.h" #include "spdlog/sinks/basic_file_sink.h" +#include "spdlog/sinks/stdout_sinks.h" #include "spdlog/spdlog.h" #include +#include namespace ygm { @@ -21,8 +24,35 @@ enum class log_level { debug = 5 }; +enum class logger_target { file, stdout, stderr }; + namespace detail { +static std::vector ygm_level_to_spdlog_level{ + spdlog::level::level_enum::off, spdlog::level::level_enum::critical, + spdlog::level::level_enum::err, spdlog::level::level_enum::warn, + spdlog::level::level_enum::info, spdlog::level::level_enum::debug}; + +class rank_formatter_flag : public spdlog::custom_flag_formatter { + public: + rank_formatter_flag(const int rank) : m_rank(rank) { + m_rank_msg = std::string("Rank ") + std::to_string(m_rank); + } + + void format(const spdlog::details::log_msg &, const std::tm &, + spdlog::memory_buf_t &dest) override { + dest.append(m_rank_msg.data(), m_rank_msg.data() + m_rank_msg.size()); + } + + std::unique_ptr clone() const override { + return spdlog::details::make_unique(m_rank); + } + + private: + int m_rank; + std::string m_rank_msg; +}; + /** * @brief Simple logger for applications using YGM */ @@ -30,20 +60,42 @@ class logger { public: using rank_logger_t = spdlog::logger; using rank_file_sink_t = spdlog::sinks::basic_file_sink_st; + using rank_cout_sink_t = spdlog::sinks::stdout_sink_st; + using rank_cerr_sink_t = spdlog::sinks::stderr_sink_st; - logger() : logger(std::filesystem::path("./log/")) {} + logger(const int rank) : logger(rank, std::filesystem::path("./log/")) {} - logger(const std::filesystem::path &path) : m_path(path) { + logger(const int rank, const std::filesystem::path &path) + : m_logger_target(logger_target::file), + m_cout_logger("ygm_cout_logger", std::make_shared()), + m_cerr_logger("ygm_cerr_logger", std::make_shared()), + m_path(path) { if (std::filesystem::is_directory(path)) { m_path += "/ygm_logs"; } + + // Set custom logging message format for stdout and stderr to include MPI + // rank + auto stdout_formatter = std::make_unique(); + stdout_formatter->add_flag('k', rank).set_pattern( + "[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] [%k] %v"); + m_cout_logger.set_formatter(std::move(stdout_formatter)); + + auto stderr_formatter = std::make_unique(); + stderr_formatter->add_flag('k', rank).set_pattern( + "[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] [%k] %v"); + m_cerr_logger.set_formatter(std::move(stderr_formatter)); + + // We will control logging levels + m_cout_logger.set_level(spdlog::level::trace); + m_cerr_logger.set_level(spdlog::level::trace); } void set_path(const std::filesystem::path p) { m_path = p; - if (m_logger_ptr) { - m_logger_ptr.reset(); + if (m_file_logger_ptr) { + m_file_logger_ptr.reset(); } } @@ -51,26 +103,73 @@ class logger { void set_log_level(const log_level level) { m_log_level = level; } - log_level get_log_level() { return m_log_level; } + log_level get_log_level() const { return m_log_level; } + + void set_logger_target(const logger_target target) { + m_logger_target = target; + } + + logger_target get_logger_target() const { return m_logger_target; } + + template + void log(const std::vector &targets, const log_level level, + Args &&...args) const { + for (const auto target : targets) { + log_impl(target, level, std::forward(args)...); + } + } template void log(const log_level level, Args &&...args) const { + log(std::vector({m_logger_target}), level, std::forward(args)...); + } + + /* + * @brief Force a flush of file-backed logs + */ + void flush() { + if (m_file_logger_ptr) { + m_file_logger_ptr->flush(); + } + } + + private: + template + void log_impl(logger_target t, const log_level level, Args &&...args) const { if (level > m_log_level) { return; } - if (not m_logger_ptr) { - std::filesystem::create_directories(m_path.parent_path()); - - m_logger_ptr = std::make_unique( - "ygm_logger", - std::make_shared(m_path.c_str(), false)); + switch (t) { + case logger_target::file: + if (not m_file_logger_ptr) { + std::filesystem::create_directories(m_path.parent_path()); + + m_file_logger_ptr = std::make_unique( + "ygm_file_logger", + std::make_shared(m_path.c_str(), false)); + m_file_logger_ptr->set_level(spdlog::level::trace); + } + m_file_logger_ptr->log( + ygm_level_to_spdlog_level[static_cast(level)], args...); + break; + + case logger_target::stdout: + m_cout_logger.log(ygm_level_to_spdlog_level[static_cast(level)], + args...); + break; + + case logger_target::stderr: + m_cerr_logger.log(ygm_level_to_spdlog_level[static_cast(level)], + args...); + break; } - m_logger_ptr->info(args...); } - private: - mutable std::unique_ptr m_logger_ptr; + logger_target m_logger_target; + mutable std::unique_ptr m_file_logger_ptr; + mutable rank_logger_t m_cout_logger; + mutable rank_logger_t m_cerr_logger; log_level m_log_level = log_level::off; diff --git a/test/test_logger.cpp b/test/test_logger.cpp index 52380ee3..002f0732 100644 --- a/test/test_logger.cpp +++ b/test/test_logger.cpp @@ -22,7 +22,7 @@ int main() { std::filesystem::path p("./test_log"); file_cleanup c(p); - ygm::detail::logger l(p); + ygm::detail::logger l(0, p); YGM_ASSERT_RELEASE(std::filesystem::exists(l.get_path()) == false); @@ -32,7 +32,9 @@ int main() { l.set_log_level(ygm::log_level::info); l.log(ygm::log_level::info, "Do log"); + l.flush(); YGM_ASSERT_RELEASE(std::filesystem::exists(l.get_path()) == true); + YGM_ASSERT_RELEASE(std::filesystem::is_empty(l.get_path()) == false); } // Test logging level ordering @@ -40,7 +42,7 @@ int main() { std::filesystem::path p("./test_log"); file_cleanup c(p); - ygm::detail::logger l(p); + ygm::detail::logger l(0, p); YGM_ASSERT_RELEASE(std::filesystem::exists(l.get_path()) == false); @@ -58,7 +60,7 @@ int main() { std::filesystem::path p("./test_log"); file_cleanup c(p); - ygm::detail::logger l(p); + ygm::detail::logger l(0, p); YGM_ASSERT_RELEASE(std::filesystem::exists(l.get_path()) == false); @@ -86,7 +88,7 @@ int main() { std::filesystem::path p("./test_log"); file_cleanup c(p); - ygm::detail::logger l(p); + ygm::detail::logger l(0, p); YGM_ASSERT_RELEASE(std::filesystem::exists(l.get_path()) == false); @@ -109,5 +111,17 @@ int main() { YGM_ASSERT_RELEASE(std::filesystem::is_empty(p) == false); } + // Test log files do not get created when logging to cout + { + std::filesystem::path p("./test_log"); + + ygm::detail::logger l(8, p); + + l.set_logger_target(ygm::logger_target::stdout); + l.set_log_level(ygm::log_level::debug); + l.log(ygm::log_level::warn, "Log to stdout"); + YGM_ASSERT_RELEASE(std::filesystem::exists(l.get_path()) == false); + } + return 0; }