From b846437dd84f394d124c8b5882afeb080e15b34f Mon Sep 17 00:00:00 2001 From: Mathias Kraus Date: Mon, 11 Mar 2024 21:19:25 +0100 Subject: [PATCH] iox-#1755 Add a log frontend to the platform --- .../include/iox/iceoryx_hoofs_types.hpp | 16 +- .../include/iceoryx_platform/logging.hpp | 58 ++++++ iceoryx_platform/generic/source/logging.cpp | 168 ++++++++++++++++++ 3 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 iceoryx_platform/generic/include/iceoryx_platform/logging.hpp create mode 100644 iceoryx_platform/generic/source/logging.cpp diff --git a/iceoryx_hoofs/primitives/include/iox/iceoryx_hoofs_types.hpp b/iceoryx_hoofs/primitives/include/iox/iceoryx_hoofs_types.hpp index 2f5171fd85..88ddbaa83f 100644 --- a/iceoryx_hoofs/primitives/include/iox/iceoryx_hoofs_types.hpp +++ b/iceoryx_hoofs/primitives/include/iox/iceoryx_hoofs_types.hpp @@ -20,6 +20,8 @@ /// @note since this file will be included by many other files, it should not include other header except /// iceoryx_platform or STL header +#include "iceoryx_platform/logging.hpp" + #include namespace iox @@ -31,13 +33,13 @@ namespace log /// @brief This enum defines the log levels used for logging. enum class LogLevel : uint8_t { - OFF = 0, - FATAL, - ERROR, - WARN, - INFO, - DEBUG, - TRACE + OFF = IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_OFF, + FATAL = IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_FATAL, + ERROR = IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_ERROR, + WARN = IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_WARN, + INFO = IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_INFO, + DEBUG = IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_DEBUG, + TRACE = IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_TRACE, }; /// @brief converts LogLevel into a string literal diff --git a/iceoryx_platform/generic/include/iceoryx_platform/logging.hpp b/iceoryx_platform/generic/include/iceoryx_platform/logging.hpp new file mode 100644 index 0000000000..25cabefb04 --- /dev/null +++ b/iceoryx_platform/generic/include/iceoryx_platform/logging.hpp @@ -0,0 +1,58 @@ +// Copyright (c) 2024 by Mathias Kraus . All rights reserved. +// +// 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef IOX_PLATFORM_LOGGING_HPP +#define IOX_PLATFORM_LOGGING_HPP + +enum IceoryxPlatformLogLevel +{ + IOX_PLATFORM_LOG_LEVEL_OFF = 0, + IOX_PLATFORM_LOG_LEVEL_FATAL, + IOX_PLATFORM_LOG_LEVEL_ERROR, + IOX_PLATFORM_LOG_LEVEL_WARN, + IOX_PLATFORM_LOG_LEVEL_INFO, + IOX_PLATFORM_LOG_LEVEL_DEBUG, + IOX_PLATFORM_LOG_LEVEL_TRACE +}; + +/// @brief Typedef to the platform log backend +/// @param[in] file should be the '__FILE__' compiler intrinsic +/// @param[in] line should be the '__LINE__' compiler intrinsic +/// @param[in] function should be the '__FUNCTION__' compiler intrinsic +/// @param[in] log_level is the log level to be used for the log message +/// @param[in] msg is the message to be logged +typedef void (*IceoryxPlatformLogBackend)( + const char* file, int line, const char* function, IceoryxPlatformLogLevel log_level, const char* msg); + +/// @brief Sets the logging backend to the provided function +/// @param[in] log_backend to be used +/// @note The log_backend must have a static lifetime and must be thread-safe +void iox_platform_set_log_backend(IceoryxPlatformLogBackend log_backend); + +/// @note Implementation detail! Do not use directly +void iox_platform_detail_log( + const char* file, int line, const char* function, IceoryxPlatformLogLevel log_level, const char* msg); + +// NOLINTJUSTIFICATION The functionality of the macro (obtaining the source location) cannot be achieved with C++17 +// NOLINTBEGIN(cppcoreguidelines-macro-usage) +/// @brief Frontend for logging in the iceoryx platform +/// @param[in] log_level is the log level to be used for the log message +/// @param[in] msg is the message to be logged +#define IOX_PLATFORM_LOG(log_level, msg) \ + iox_platform_detail_log(__FILE__, __LINE__, static_cast(__FUNCTION__), log_level, msg) +// NOLINTEND(cppcoreguidelines-macro-usage) + +#endif // IOX_PLATFORM_LOGGING_HPP diff --git a/iceoryx_platform/generic/source/logging.cpp b/iceoryx_platform/generic/source/logging.cpp new file mode 100644 index 0000000000..972fb4f799 --- /dev/null +++ b/iceoryx_platform/generic/source/logging.cpp @@ -0,0 +1,168 @@ +// Copyright (c) 2024 by Mathias Kraus . All rights reserved. +// +// 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_platform/logging.hpp" + +#include +#include +#include +#include +#include + +namespace +{ +// NOLINTJUSTIFICATION only used in this file; should be fine +// NOLINTNEXTLINE(readability-function-size) +void default_log_backend( + const char* file, int line, const char* function, IceoryxPlatformLogLevel log_level, const char* msg) +{ + if (log_level == IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_OFF) + { + return; + } + + std::stringstream stream; + stream << file << ":" << line << " (" << function << ") "; + + switch (log_level) + { + case IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_FATAL: + stream << "[Fatal]"; + break; + case IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_ERROR: + stream << "[Error]"; + break; + case IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_WARN: + stream << "[Warn ]"; + break; + case IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_INFO: + stream << "[Info ]"; + break; + case IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_DEBUG: + stream << "[Debug]"; + break; + case IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_TRACE: + stream << "[Trace]"; + break; + default: + stream << "[UNDEF]"; + break; + } + + stream << " " << msg; + + // NOLINTJUSTIFICATION We want to flush the line with each log output; if performance matters a custom log backend can be used, e.g. the logger in hoofs + // NOLINTNEXTLINE(performance-avoid-endl) + std::cout << stream.str() << std::endl; +} + +enum class LoggerExchangeState : uint8_t +{ + DEFAULT, + PENDING, + CUSTOM, +}; + +struct IceoryxPlatformLogger +{ + std::atomic log_backend{&default_log_backend}; + std::atomic logger_exchange_state{LoggerExchangeState::DEFAULT}; +}; + +IceoryxPlatformLogger& active_logger(IceoryxPlatformLogBackend new_log_backend) +{ + static IceoryxPlatformLogger logger; + + if (new_log_backend != nullptr) + { + auto exchange_state = LoggerExchangeState::DEFAULT; + auto successful = + logger.logger_exchange_state.compare_exchange_strong(exchange_state, LoggerExchangeState::PENDING); + if (!successful) + { + logger.log_backend.load(std::memory_order_relaxed)(__FILE__, + __LINE__, + static_cast(__FUNCTION__), + IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_ERROR, + "Trying to replace logger after already initialized"); + + new_log_backend(__FILE__, + __LINE__, + static_cast(__FUNCTION__), + IceoryxPlatformLogLevel::IOX_PLATFORM_LOG_LEVEL_ERROR, + "Trying to replace logger after already initialized"); + + constexpr uint64_t YIELDS_BEFORE_SLEEP{10000}; + uint64_t yield_counter = 0; + while (logger.logger_exchange_state.load() != LoggerExchangeState::CUSTOM) + { + if (yield_counter < YIELDS_BEFORE_SLEEP) + { + std::this_thread::yield(); + ++yield_counter; + } + else + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + return logger; + } + + logger.log_backend.store(new_log_backend); + logger.logger_exchange_state.store(LoggerExchangeState::CUSTOM); + } + + return logger; +} +} // namespace + +// NOLINTJUSTIFICATION only used from a macro which hides most of the parameter; should be fine +// NOLINTNEXTLINE(readability-function-size) +void iox_platform_detail_log( + const char* file, int line, const char* function, IceoryxPlatformLogLevel log_level, const char* msg) +{ + // NOLINTJUSTIFICATION needed for the functionality + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static IceoryxPlatformLogger* logger = &active_logger(nullptr); + thread_local static LoggerExchangeState current_logger_exchange_state = logger->logger_exchange_state.load(); + // NOTE if the logger got updated in between, the current_logger_exchange_state has an old value and we will enter + // the if statement + thread_local static IceoryxPlatformLogBackend log_backend = logger->log_backend.load(); + + if (current_logger_exchange_state == LoggerExchangeState::PENDING + || current_logger_exchange_state != logger->logger_exchange_state.load(std::memory_order_relaxed)) + { + // the logger can be changed only once and if we enter this if statement the 'active_logger' function will only + // return once 'logger_exchange_state' equals 'CUSTOM' + logger = &active_logger(nullptr); + log_backend = logger->log_backend.load(); + current_logger_exchange_state = logger->logger_exchange_state.load(); + } + + log_backend(file, line, function, log_level, msg); +} + +void iox_platform_set_log_backend(IceoryxPlatformLogBackend log_backend) +{ + if (log_backend == nullptr) + { + IOX_PLATFORM_LOG(IOX_PLATFORM_LOG_LEVEL_ERROR, "'log_backend' must not be a nullptr!"); + return; + } + + active_logger(log_backend); +}