diff --git a/CMakeLists.txt b/CMakeLists.txt index 271a48e..8b06706 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,11 +37,13 @@ if(NOT GZ_UTILS_VENDOR_CLI11) gz_find_package(CLI11 REQUIRED_BY cli PKGCONFIG_IGNORE) endif() +gz_find_package(spdlog REQUIRED_BY logger) + #============================================================================ # Configure the build #============================================================================ gz_configure_build(QUIT_IF_BUILD_ERRORS - COMPONENTS cli) + COMPONENTS cli logger) #============================================================================ # Create package information diff --git a/cli/src/CMakeLists.txt b/cli/src/CMakeLists.txt index 1d3ef22..3213d75 100644 --- a/cli/src/CMakeLists.txt +++ b/cli/src/CMakeLists.txt @@ -2,6 +2,7 @@ gz_add_component( cli INTERFACE INDEPENDENT_FROM_PROJECT_LIB + LIB_DEPS spdlog::spdlog GET_TARGET_NAME gz_utils_cli_target_name) # Make sure that the name is visible also in cli/include/CMakeLists.txt diff --git a/logger/include/CMakeLists.txt b/logger/include/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/logger/include/gz/utils/CMakeLists.txt b/logger/include/gz/utils/CMakeLists.txt new file mode 100644 index 0000000..646d386 --- /dev/null +++ b/logger/include/gz/utils/CMakeLists.txt @@ -0,0 +1 @@ +gz_install_all_headers(COMPONENT logger) diff --git a/logger/include/gz/utils/logger/LogMessage.hh b/logger/include/gz/utils/logger/LogMessage.hh new file mode 100644 index 0000000..016ddf5 --- /dev/null +++ b/logger/include/gz/utils/logger/LogMessage.hh @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef GZ_UTILS_LOGGER_LOGMESSAGE_HH_ +#define GZ_UTILS_LOGGER_LOGMESSAGE_HH_ + +#include + +#include +#include + +namespace gz::utils::logger +{ + +class LogMessage +{ + /// \brief Constructor. + /// \param[in] _logLevel Log level. + public: explicit LogMessage(spdlog::level::level_enum _logLevel, + const char *_logger = nullptr, + const char *_file = nullptr, + int _line = -1); + + /// \brief Destructor. + public: ~LogMessage(); + + /// \brief Get access to the underlying stream. + /// \return The underlying stream. + public: std::ostream &stream(); + + /// \brief Log level. + private: spdlog::level::level_enum severity; + + private: std::shared_ptr logger; + + /// \brief Source file location information. + private: spdlog::source_loc sourceLocation; + + /// \brief Underlying stream. + private: std::ostringstream ss; +}; +} // namespace gz::utils::logger + +#endif // GZ_UTILS_LOGGER_LOGMESSAGE_HH_ diff --git a/logger/include/gz/utils/logger/SplitSink.hh b/logger/include/gz/utils/logger/SplitSink.hh new file mode 100644 index 0000000..94ecc9a --- /dev/null +++ b/logger/include/gz/utils/logger/SplitSink.hh @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef GZ_UTILS_LOGGER_SPLITSINK_HH_ +#define GZ_UTILS_LOGGER_SPLITSINK_HH_ + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace gz::utils::logger +{ +/// \brief Logging sink for spdlog that logs in Gazebo-conventions +/// +/// This will route messages with severity (warn, err, critical) to stderr, +/// and all other levels (info, debug, trace) to stdout +template +class SplitConsoleSink : public spdlog::sinks::base_sink +{ + public: SplitConsoleSink() = default; + public: SplitConsoleSink(const SplitConsoleSink &) = delete; + public: SplitConsoleSink &operator=(const SplitConsoleSink &) = delete; + + protected: void sink_it_(const spdlog::details::log_msg &_msg) override + { + if (!this->should_log(_msg.level)) + { + return; + } + + if (_msg.level == spdlog::level::warn || + _msg.level == spdlog::level::err || + _msg.level == spdlog::level::critical) + { + this->stderr.log(_msg); + } + else + this->stdout.log(_msg); + } + + protected: void flush_() override + { + this->stdout.flush(); + this->stderr.flush(); + } + + void set_pattern_(const std::string &pattern) override + { + this->set_formatter_(spdlog::details::make_unique(pattern)); + } + + void set_formatter_(std::unique_ptr sink_formatter) override + { + spdlog::sinks::base_sink::formatter_ = std::move(sink_formatter); + this->stdout.set_formatter(spdlog::sinks::base_sink::formatter_->clone()); + this->stderr.set_formatter(spdlog::sinks::base_sink::formatter_->clone()); + } + + /// \brief Standard output. + private: spdlog::sinks::stdout_color_sink_st stdout; + + /// \brief Standard error. + private: spdlog::sinks::stderr_color_sink_st stderr; +}; + +using SplitConsoleSinkMt = SplitConsoleSink; +using SplitConsoleSinkSt = SplitConsoleSink; + +/// \brief Logging sink for spdlog that logs in Gazebo-conventions +/// +/// This will route messages with severity (warn, err, critical) to stderr, +/// and all other levels (info, debug, trace) to stdout +template +class SplitRingBufferSink: public spdlog::sinks::base_sink +{ + public: SplitRingBufferSink() = default; + public: SplitRingBufferSink(const SplitRingBufferSink &) = delete; + public: SplitRingBufferSink &operator=(const SplitRingBufferSink &) = delete; + + public: std::vector last_raw_stdout(size_t lim = 0) + { + return this->stdout.last_raw(lim); + } + + public: std::vector last_raw_stderr(size_t lim = 0) + { + return this->stderr.last_raw(lim); + } + + public: std::vector last_formatted_stdout(size_t lim = 0) + { + return this->stdout.last_formatted(lim); + } + + public: std::vector last_formatted_stderr(size_t lim = 0) + { + return this->stderr.last_formatted(lim); + } + + protected: void sink_it_(const spdlog::details::log_msg &_msg) override + { + if (!this->should_log(_msg.level)) + { + return; + } + + if (_msg.level == spdlog::level::warn || + _msg.level == spdlog::level::err || + _msg.level == spdlog::level::critical) + { + this->stderr.log(_msg); + } + else + this->stdout.log(_msg); + } + + protected: void flush_() override + { + this->stdout.flush(); + this->stderr.flush(); + } + + protected: void set_pattern_(const std::string &pattern) override + { + this->set_formatter_(spdlog::details::make_unique(pattern)); + } + + protected: void set_formatter_(std::unique_ptr sink_formatter) override + { + spdlog::sinks::base_sink::formatter_ = std::move(sink_formatter); + this->stdout.set_formatter(spdlog::sinks::base_sink::formatter_->clone()); + this->stderr.set_formatter(spdlog::sinks::base_sink::formatter_->clone()); + } + + + /// \brief Standard output. + private: spdlog::sinks::ringbuffer_sink_st stdout {n_items}; + + /// \brief Standard error. + private: spdlog::sinks::ringbuffer_sink_st stderr {n_items}; +}; + +template +using SplitRingBufferSinkMt = SplitRingBufferSink; + +} // namespace gz::utils::logger +#endif // GZ_UTILS_LOGGER_SPLITSINK_HH__ diff --git a/logger/src/CMakeLists.txt b/logger/src/CMakeLists.txt new file mode 100644 index 0000000..df5ab44 --- /dev/null +++ b/logger/src/CMakeLists.txt @@ -0,0 +1,14 @@ +gz_get_libsources_and_unittests(sources gtest_sources) + +gz_add_component( + logger + SOURCES ${sources} + INDEPENDENT_FROM_PROJECT_LIB + GET_TARGET_NAME gz_utils_logger_target_name) + +target_link_libraries(${gz_utils_logger_target_name} PUBLIC spdlog::spdlog) + +gz_build_tests(TYPE UNIT + SOURCES ${gtest_sources} + LIB_DEPS ${gz_utils_logger_target_name} +) diff --git a/logger/src/LogMessage.cc b/logger/src/LogMessage.cc new file mode 100644 index 0000000..98d0024 --- /dev/null +++ b/logger/src/LogMessage.cc @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include +#include + +namespace gz::utils::logger +{ +///////////////////////////////////////////////// +LogMessage::LogMessage(spdlog::level::level_enum _logLevel, + const char *_logger, + const char *_file, + int _line): + severity(_logLevel), + logger(_logger == nullptr ? spdlog::default_logger() : spdlog::get(_logger)), + sourceLocation(_file, _line, "") +{ +} + +///////////////////////////////////////////////// +LogMessage::~LogMessage() +{ + if (sourceLocation.filename != nullptr) + { + logger->log(this->sourceLocation, this->severity, this->ss.str()); + } else { + logger->log(this->severity, this->ss.str()); + } +} +///////////////////////////////////////////////// +std::ostream &LogMessage::stream() +{ + return this->ss; +} + +} // namespace gz::utils::logger diff --git a/logger/src/SplitSink_TEST.cc b/logger/src/SplitSink_TEST.cc new file mode 100644 index 0000000..e004765 --- /dev/null +++ b/logger/src/SplitSink_TEST.cc @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include + +#include + +///////////////////////////////////////////////// +TEST(SplitConsoleSink, foo) +{ + auto split_sink = std::make_shared(); + + spdlog::logger logger("split_sink", {split_sink}); + logger.set_level(spdlog::level::trace); + + logger.trace("trace"); + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + logger.critical("critical"); +} + +///////////////////////////////////////////////// +TEST(SplitRingBufferSink, foo) +{ + auto split_sink = std::make_shared>(); + auto split_sink_console = std::make_shared(); + + spdlog::logger logger("split_sink", {split_sink, split_sink_console}); + logger.set_level(spdlog::level::trace); + + logger.trace("trace"); + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + logger.critical("critical"); + + { + auto logs = split_sink->last_raw_stdout(); + EXPECT_EQ(logs[0].payload, "trace"); + EXPECT_EQ(logs[1].payload, "debug"); + EXPECT_EQ(logs[2].payload, "info"); + } + + { + auto logs = split_sink->last_raw_stderr(); + EXPECT_EQ(logs[0].payload, "warn"); + EXPECT_EQ(logs[1].payload, "error"); + EXPECT_EQ(logs[2].payload, "critical"); + } +}