diff --git a/tesseract_common/CMakeLists.txt b/tesseract_common/CMakeLists.txt index 47c55ff9fe7..1120bf64b60 100644 --- a/tesseract_common/CMakeLists.txt +++ b/tesseract_common/CMakeLists.txt @@ -101,7 +101,8 @@ add_library( src/types.cpp src/stopwatch.cpp src/timer.cpp - src/yaml_utils.cpp) + src/yaml_utils.cpp + src/logging.cpp) target_link_libraries( ${PROJECT_NAME} PUBLIC Eigen3::Eigen diff --git a/tesseract_common/include/tesseract_common/logging.h b/tesseract_common/include/tesseract_common/logging.h new file mode 100644 index 00000000000..34491e41345 --- /dev/null +++ b/tesseract_common/include/tesseract_common/logging.h @@ -0,0 +1,207 @@ +/** + * @file logging.h + * @brief Tesseract Logging + * + * Improved Tesseract logging inspired by Robot Raconteur log system + * + * @author John Wason + * @date August 14, 2025 + * @version TODO + * @bug No known bugs + * + * @copyright Copyright (c) 2025, Wason Technology, LLC + * + * @par License + * Software License Agreement (Apache License) + * @par + * 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 + * @par + * 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 TESSERACT_LOGGING_H +#define TESSERACT_LOGGING_H + +#include +TESSERACT_COMMON_IGNORE_WARNINGS_PUSH +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +TESSERACT_COMMON_IGNORE_WARNINGS_POP + +namespace tesseract_common +{ + /** + * @brief Available log levels for Tesseract + * + */ + enum class LogLevel + { + /** @brief `trace` log level */ + Trace, + /** @brief `debug` log level */ + Debug, + /** @brief `info` log level */ + Info, + /** @brief `warning` log level */ + Warning, + /** @brief `error` log level */ + Error, + /** @brief `fatal` log level */ + Fatal, + /** @brief `disabled` log level */ + Disable = 1000 + }; + + class LogManager; + + + /** + * @brief Tesseract log record + * + * Records information about a logging event + * + */ + struct TesseractLogRecord + { + /** The log record error level */ + LogLevel level{LogLevel::Warning}; + /** An identifier for an error. Either a gcc style "diagnostic ID" or a UUID in hex string */ + std::string error_ident; + /** The component that generated the message (typically package name or namespace) */ + std::string component; + /** The subcomponent that generate the message (typically class name) */ + std::string subcomponent; + /** The instance ID of the object that generated the message (pointer or UUID)*/ + std::string object_instance; + /** The path of the object. Use string UUIDs of taskflow nodes */ + std::vector object_path; + /** Human readable log message */ + std::string message; + /** Time of logging event */ + std::chrono::system_clock::time_point time; + /** The sourcecode filename */ + std::string source_file; + /** The line within the sourcecode file */ + uint32_t source_line{0}; + /** The source thread */ + std::string thread_id; + /** Machine readable parameters attached to event */ + std::unordered_map parameters; + }; + + /** Write a TesseractLogRecord to stream */ + std::ostream& operator<<(std::ostream& out, const TesseractLogRecord& record); + + /** Convert a LogLevel to string */ + std::string tesseractLogLevelToString(LogLevel level); + + + class TesseractRecordStream : public boost::intrusive_ref_counter + { + protected: + TesseractLogRecord record; + std::stringstream ss; + std::weak_ptr log_manager; + + public: + TesseractRecordStream(std::weak_ptr log_manager); + TesseractRecordStream(std::weak_ptr log_manager, LogLevel level, const std::string& error_ident, + const std::string& component, const std::string& subcomponent, + const std::string& object_instance, const std::vector& object_path, + const std::string& source_file, uint32_t source_line, const std::string& thread_id); + ~TesseractRecordStream(); + std::stringstream& stream(); + + static boost::intrusive_ptr openRecordStream(std::weak_ptr log_manager, + LogLevel level, const std::string& error_ident, const std::string& component, + const std::string& subcomponent, const std::string& object_instance, + const std::vector& object_path, const std::string& source_file, + uint32_t source_line, const std::string& thread_id="-"); + }; + + class LogRecordHandler + { + public: + virtual ~LogRecordHandler() = default; + + virtual void handleRecord(const TesseractLogRecord& record) = 0; + }; + + class LogManager + { + public: + LogManager() = default; + /** @brief Log a record */ + void logRecord(const tesseract_common::TesseractLogRecord& record); + /** @brief Get the default log manager instance */ + static std::shared_ptr getInstance(); + /** @brief Compare the log level with the current log level */ + bool compareLogLevel(LogLevel level) const; + + /** @brief Set the log level */ + void setLogLevel(LogLevel level); + + /** @brief Get the current log level */ + LogLevel getLogLevel() const; + + /** @brief Add a log record handler */ + void addLogRecordHandler(std::shared_ptr handler); + + /** @brief Remove a log record handler */ + void removeLogRecordHandler(std::shared_ptr handler); + + /** @brief Remove all log record handlers */ + void clearLogRecordHandlers(); + + protected: + std::atomic current_log_level{LogLevel::Warning}; + boost::shared_mutex config_mutex; + std::vector> handlers; + }; + + /* + #define ROBOTRACONTEUR_LOG(node, lvl, component, component_name, component_object_id, ep, service_path, member, args) \ + { \ + boost::intrusive_ptr ____rr_log_record_stream____ = \ + RobotRaconteur::RRLogRecordStream::OpenRecordStream( \ + node, RobotRaconteur::RobotRaconteur_LogLevel_##lvl, \ + RobotRaconteur::RobotRaconteur_LogComponent_##component, component_name, component_object_id, ep, \ + service_path, member, __FILE__, __LINE__); \ + if (____rr_log_record_stream____) \ + { \ + ____rr_log_record_stream____->Stream() << args; \ + } \ + } + */ + + #define TESSERACT_LOG(log_manager, lvl, error_ident, component, subcomponent, object_instance, object_path, args) \ + { \ + boost::intrusive_ptr ____tesseract_log_record_stream____ = \ + TesseractRecordStream::openRecordStream(log_manager, lvl, error_ident, component, subcomponent, \ + object_instance, object_path, __FILE__, __LINE__); \ + if (____tesseract_log_record_stream____) \ + { \ + ____tesseract_log_record_stream____->stream() << args; \ + } \ + } + + // TODO: Fill in various macros, see https://github.com/robotraconteur/robotraconteur/blob/master/RobotRaconteurCore/include/RobotRaconteur/Logging.h + // for examples +} + +#endif diff --git a/tesseract_common/src/logging.cpp b/tesseract_common/src/logging.cpp new file mode 100644 index 00000000000..1253b9d4644 --- /dev/null +++ b/tesseract_common/src/logging.cpp @@ -0,0 +1,189 @@ +/** + * @file logging.h + * @brief Tesseract Logging + * + * Improved Tesseract logging inspired by Robot Raconteur log system + * + * @author John Wason + * @date August 14, 2025 + * @version TODO + * @bug No known bugs + * + * @copyright Copyright (c) 2025, Wason Technology, LLC + * + * @par License + * Software License Agreement (Apache License) + * @par + * 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 + * @par + * 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 + +TESSERACT_COMMON_IGNORE_WARNINGS_PUSH +#include +#include +#include +#include +TESSERACT_COMMON_IGNORE_WARNINGS_POP + + namespace tesseract_common + { + std::ostream& operator<<(std::ostream& out, const TesseractLogRecord& record) + { + std::time_t t = std::chrono::system_clock::to_time_t(record.time); + std::tm tm = *std::localtime(&t); + out << "[" << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S") << "] [" << tesseractLogLevelToString(record.level) + << "," << record.error_ident + << "] [" + << record.thread_id << "] [" + << record.component; + if (!record.subcomponent.empty()) + { + out << "," << record.subcomponent; + } + if (!record.object_instance.empty()) + { + out << "," << record.object_instance; + } + out << "] "; + + if (!record.object_path.empty()) + { + out << " ["; + // comman-separated list of object paths + for (const auto& path : record.object_path) + { + out << path << ","; + } + // Remove the last comma + if (!record.object_path.empty()) + out.seekp(-1, std::ios_base::end); + out << "] "; + } + + if (!record.source_file.empty()) + { + out << "[" << std::filesystem::path(record.source_file).filename().string() << ":" << record.source_line + << "] "; + } + + out << record.message; + return out; + } + + std::string tesseractLogLevelToString(LogLevel level) + { + switch (level) + { + case LogLevel::Trace: + return "trace"; + case LogLevel::Debug: + return "debug"; + case LogLevel::Info: + return "info"; + case LogLevel::Warning: + return "warning"; + case LogLevel::Error: + return "error"; + case LogLevel::Fatal: + return "fatal"; + default: + return "unknown"; + } + } + + TesseractRecordStream::TesseractRecordStream(std::weak_ptr log_manager) + { + this->log_manager = log_manager; + record.time = std::chrono::system_clock::now(); + } + TesseractRecordStream::TesseractRecordStream(std::weak_ptr log_manager, LogLevel level, const std::string& error_ident, + const std::string& component, const std::string& subcomponent, + const std::string& object_instance, const std::vector& object_path, + const std::string& source_file, uint32_t source_line, const std::string& thread_id) + : record{level, error_ident, component, subcomponent, object_instance, object_path, {}, {}, source_file, source_line, thread_id} + { + record.time = std::chrono::system_clock::now(); + this->log_manager = log_manager; + } + TesseractRecordStream::~TesseractRecordStream() + { + record.message = ss.str(); + try + { + auto log_manager_ptr = log_manager.lock(); + if (!log_manager_ptr) + { + // If log manager is not available, we just ignore the record + return; + } + log_manager_ptr->logRecord(record); + } + catch (std::exception& exp) + { + // If logging fails, we just ignore it + // This is to prevent logging from throwing exceptions that could crash the application + std::cerr << "Failed to log record: " << exp.what() << std::endl; + } + } + std::stringstream& TesseractRecordStream::stream() + { + return ss; + } + + boost::intrusive_ptr openRecordStream(std::weak_ptr log_manager, + LogLevel level, const std::string& error_ident, const std::string& component, + const std::string& subcomponent, const std::string& object_instance, + const std::vector& object_path, const std::string& source_file, + uint32_t source_line, const std::string& thread_id) + { + auto log_manager_ptr = log_manager.lock(); + if (!log_manager_ptr) + { + return nullptr; + } + + if (!log_manager_ptr->compareLogLevel(level)) + { + return nullptr; // If the log level is not enabled, return nullptr + } + + return new TesseractRecordStream(log_manager, level, error_ident, component, subcomponent, + object_instance, object_path, source_file, source_line, thread_id); + + } + + void LogManager::logRecord(const tesseract_common::TesseractLogRecord& record) + { + + } + std::shared_ptr LogManager::getInstance() + { + + } + bool LogManager::compareLogLevel(LogLevel level) const + { + return true; + } + + /** @brief Set the log level */ + void LogManager::setLogLevel(LogLevel level) + { + + } + + /** @brief Get the current log level */ + LogLevel LogManager::getLogLevel() const + { + + } + } \ No newline at end of file