diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0601161b0..b4d2e22b8 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -257,7 +257,8 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] - toolchain: [stable, stable-gnu, 1.75.0] + # toolchain: [stable, stable-gnu, 1.75.0] + toolchain: [stable, 1.75.0] mode: - name: "release" arg: "--release" diff --git a/iceoryx2-bb/log/src/lib.rs b/iceoryx2-bb/log/src/lib.rs index db9e2eef8..19a3ce3ea 100644 --- a/iceoryx2-bb/log/src/lib.rs +++ b/iceoryx2-bb/log/src/lib.rs @@ -150,8 +150,6 @@ use std::{ sync::{atomic::Ordering, Once}, }; -use logger::Logger; - #[cfg(feature = "logger_tracing")] static DEFAULT_LOGGER: logger::tracing::Logger = logger::tracing::Logger::new(); @@ -163,10 +161,15 @@ static DEFAULT_LOGGER: logger::console::Logger = logger::console::Logger::new(); const DEFAULT_LOG_LEVEL: u8 = LogLevel::Info as u8; -static mut LOGGER: Option<&'static dyn logger::Logger> = None; +static mut LOGGER: Option<&'static dyn Log> = None; static LOG_LEVEL: IoxAtomicU8 = IoxAtomicU8::new(DEFAULT_LOG_LEVEL); static INIT: Once = Once::new(); +pub trait Log: Send + Sync { + /// logs a message + fn log(&self, log_level: LogLevel, origin: Arguments, formatted_message: Arguments); +} + /// Describes the log level. #[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] @@ -190,9 +193,9 @@ pub fn get_log_level() -> u8 { LOG_LEVEL.load(Ordering::Relaxed) } -/// Sets the [`Logger`]. Can be only called once at the beginning of the program. If the -/// [`Logger`] is already set it returns false and does not update it. -pub fn set_logger(value: &'static T) -> bool { +/// Sets the [`Log`]ger. Can be only called once at the beginning of the program. If the +/// [`Log`]ger is already set it returns false and does not update it. +pub fn set_logger(value: &'static T) -> bool { let mut set_logger_success = false; INIT.call_once(|| { unsafe { LOGGER = Some(value) }; @@ -202,8 +205,8 @@ pub fn set_logger(value: &'static T) -> bool { set_logger_success } -/// Returns a reference to the [`Logger`]. -pub fn get_logger() -> &'static dyn Logger { +/// Returns a reference to the [`Log`]ger. +pub fn get_logger() -> &'static dyn Log { INIT.call_once(|| { unsafe { LOGGER = Some(&DEFAULT_LOGGER) }; }); diff --git a/iceoryx2-bb/log/src/logger/buffer.rs b/iceoryx2-bb/log/src/logger/buffer.rs index daba51a87..17ed4743d 100644 --- a/iceoryx2-bb/log/src/logger/buffer.rs +++ b/iceoryx2-bb/log/src/logger/buffer.rs @@ -84,7 +84,7 @@ impl Logger { } } -impl crate::logger::Logger for Logger { +impl crate::Log for Logger { fn log( &self, log_level: LogLevel, diff --git a/iceoryx2-bb/log/src/logger/console.rs b/iceoryx2-bb/log/src/logger/console.rs index 31c125aa9..7270e1369 100644 --- a/iceoryx2-bb/log/src/logger/console.rs +++ b/iceoryx2-bb/log/src/logger/console.rs @@ -171,7 +171,7 @@ impl Logger { } } -impl crate::logger::Logger for Logger { +impl crate::Log for Logger { fn log( &self, log_level: crate::LogLevel, diff --git a/iceoryx2-bb/log/src/logger/file.rs b/iceoryx2-bb/log/src/logger/file.rs index cd43a4733..cd0578462 100644 --- a/iceoryx2-bb/log/src/logger/file.rs +++ b/iceoryx2-bb/log/src/logger/file.rs @@ -118,7 +118,7 @@ impl Drop for Logger { } } -impl crate::Logger for Logger { +impl crate::Log for Logger { fn log( &self, log_level: LogLevel, diff --git a/iceoryx2-bb/log/src/logger/log.rs b/iceoryx2-bb/log/src/logger/log.rs index ab1f6586b..83fd39dad 100644 --- a/iceoryx2-bb/log/src/logger/log.rs +++ b/iceoryx2-bb/log/src/logger/log.rs @@ -22,7 +22,7 @@ impl Logger { } } -impl crate::logger::Logger for Logger { +impl crate::Log for Logger { fn log( &self, log_level: crate::LogLevel, diff --git a/iceoryx2-bb/log/src/logger/mod.rs b/iceoryx2-bb/log/src/logger/mod.rs index 1a8ba43fe..00d1951c7 100644 --- a/iceoryx2-bb/log/src/logger/mod.rs +++ b/iceoryx2-bb/log/src/logger/mod.rs @@ -21,11 +21,19 @@ pub mod log; #[cfg(feature = "logger_tracing")] pub mod tracing; -use std::fmt::Arguments; +/// Sets the [`console::Logger`] as default logger +pub fn use_console_logger() -> bool { + // LazyLock is only available from 1.80 but currently iceoryx2 has the minimum version 1.75. + // But since static values are never dropped in Rust, we can also use Box::leak + let logger = Box::leak(Box::new(console::Logger::new())); + crate::set_logger(&*logger) +} -use crate::LogLevel; +/// Sets the [`file::Logger`] as default logger +pub fn use_file_logger(log_file_name: &str) -> bool { + // we cannot capture non const parameters in a LazyLock and since static values are not + // dropped in Rust we can also use Box::leak + let logger = Box::leak(Box::new(file::Logger::new(log_file_name))); -pub trait Logger: Send + Sync { - /// logs a message - fn log(&self, log_level: LogLevel, origin: Arguments, formatted_message: Arguments); + crate::set_logger(logger) } diff --git a/iceoryx2-bb/log/src/logger/tracing.rs b/iceoryx2-bb/log/src/logger/tracing.rs index 7362f6523..c5c79df3c 100644 --- a/iceoryx2-bb/log/src/logger/tracing.rs +++ b/iceoryx2-bb/log/src/logger/tracing.rs @@ -22,7 +22,7 @@ impl Logger { } } -impl crate::logger::Logger for Logger { +impl crate::Log for Logger { fn log( &self, log_level: crate::LogLevel, diff --git a/iceoryx2-ffi/cxx/include/iox2/enum_translation.hpp b/iceoryx2-ffi/cxx/include/iox2/enum_translation.hpp index 5a36cc762..20893257a 100644 --- a/iceoryx2-ffi/cxx/include/iox2/enum_translation.hpp +++ b/iceoryx2-ffi/cxx/include/iox2/enum_translation.hpp @@ -646,22 +646,43 @@ constexpr auto from(const int value) noexcept -> template <> constexpr auto from(iox2::LogLevel value) noexcept -> iox2_log_level_e { switch (value) { - case iox2::LogLevel::TRACE: + case iox2::LogLevel::Trace: return iox2_log_level_e_TRACE; - case iox2::LogLevel::DEBUG: + case iox2::LogLevel::Debug: return iox2_log_level_e_DEBUG; - case iox2::LogLevel::INFO: + case iox2::LogLevel::Info: return iox2_log_level_e_INFO; - case iox2::LogLevel::WARN: + case iox2::LogLevel::Warn: return iox2_log_level_e_WARN; - case iox2::LogLevel::ERROR: + case iox2::LogLevel::Error: return iox2_log_level_e_ERROR; - case iox2::LogLevel::FATAL: + case iox2::LogLevel::Fatal: return iox2_log_level_e_FATAL; } IOX_UNREACHABLE(); } +template <> +constexpr auto from(int value) noexcept -> iox2::LogLevel { + const auto variant = static_cast(value); + switch (value) { + case iox2_log_level_e_TRACE: + return iox2::LogLevel::Trace; + case iox2_log_level_e_DEBUG: + return iox2::LogLevel::Debug; + case iox2_log_level_e_INFO: + return iox2::LogLevel::Info; + case iox2_log_level_e_WARN: + return iox2::LogLevel::Warn; + case iox2_log_level_e_ERROR: + return iox2::LogLevel::Error; + case iox2_log_level_e_FATAL: + return iox2::LogLevel::Fatal; + default: + IOX_UNREACHABLE(); + } +} + template <> constexpr auto from(const int value) noexcept -> iox2::WaitSetCreateError { const auto variant = static_cast(value); diff --git a/iceoryx2-ffi/cxx/include/iox2/log.hpp b/iceoryx2-ffi/cxx/include/iox2/log.hpp index db76de775..21ead3fd5 100644 --- a/iceoryx2-ffi/cxx/include/iox2/log.hpp +++ b/iceoryx2-ffi/cxx/include/iox2/log.hpp @@ -17,6 +17,50 @@ namespace iox2 { +/// The abstract base class every custom logger has to implement. +/// +/// # Example +/// +/// @code +/// class ConsoleLogger : public Log { +/// public: +/// void log(LogLevel log_level, const char* origin, const char* message) override { +/// std::cout << "origin = " << origin << ", message = " << message << std::endl; +/// } +/// }; +/// +/// static ConsoleLogger CUSTOM_LOGGER = ConsoleLogger(); +/// +/// set_logger(CUSTOM_LOGGER); +/// @endcode +class Log { + public: + Log() = default; + Log(const Log&) = default; + Log(Log&&) = default; + auto operator=(const Log&) -> Log& = default; + auto operator=(Log&&) -> Log& = default; + virtual ~Log() = default; + + /// The actual log method. The system provides the log level, the origin of the message and + /// the actual message. + virtual void log(LogLevel log_level, const char* origin, const char* message) = 0; +}; + +/// Adds a log message to the logger. +void log(LogLevel log_level, const char* origin, const char* message); + +/// Sets the console logger as default logger. Returns true if the logger was set, otherwise false. +auto use_console_logger() -> bool; + +/// Sets the file logger as default logger. Returns true if the logger was set, otherwise false. +auto use_file_logger(const char* log_file) -> bool; + +/// Sets the logger that shall be used. This function can only be called once and must be called +/// before any log message was created. +/// It returns true if the logger was set, otherwise false. +auto set_logger(Log& logger) -> bool; + /// Sets the global log level for the application auto set_log_level(LogLevel level) -> void; diff --git a/iceoryx2-ffi/cxx/include/iox2/log_level.hpp b/iceoryx2-ffi/cxx/include/iox2/log_level.hpp index 1909b714c..cfa0621f8 100644 --- a/iceoryx2-ffi/cxx/include/iox2/log_level.hpp +++ b/iceoryx2-ffi/cxx/include/iox2/log_level.hpp @@ -18,12 +18,12 @@ namespace iox2 { enum class LogLevel : uint8_t { - TRACE = 0, - DEBUG = 1, - INFO = 2, - WARN = 3, - ERROR = 4, - FATAL = 5, + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Fatal = 5, }; } // namespace iox2 diff --git a/iceoryx2-ffi/cxx/src/log.cpp b/iceoryx2-ffi/cxx/src/log.cpp index 0dbf4e483..66908e3b3 100644 --- a/iceoryx2-ffi/cxx/src/log.cpp +++ b/iceoryx2-ffi/cxx/src/log.cpp @@ -11,9 +11,31 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT #include "iox2/log.hpp" +#include "iox/into.hpp" +#include "iox/optional.hpp" #include "iox2/internal/iceoryx2.hpp" namespace iox2 { +namespace { +//NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables): it is in an anonymous namespace and therefore only accessible in this compilation unit +iox::optional global_logger = iox::nullopt; +} // namespace + +void internal_log_callback(iox2_log_level_e log_level, const char* origin, const char* message) { + (*global_logger)->log(iox::into(static_cast(log_level)), origin, message); +} + +auto set_logger(Log& logger) -> bool { + auto success = iox2_set_logger(internal_log_callback); + if (success) { + global_logger.emplace(&logger); + } + return success; +} + +void log(LogLevel log_level, const char* origin, const char* message) { + iox2_log(iox::into(log_level), origin, message); +} auto set_log_level(LogLevel level) -> void { iox2_set_log_level(iox::into(level)); @@ -23,4 +45,11 @@ auto get_log_level() -> LogLevel { return LogLevel(iox2_get_log_level()); } +auto use_console_logger() -> bool { + return iox2_use_console_logger(); +} + +auto use_file_logger(const char* log_file) -> bool { + return iox2_use_file_logger(log_file); +} } // namespace iox2 diff --git a/iceoryx2-ffi/cxx/tests/src/log.cpp b/iceoryx2-ffi/cxx/tests/src/log.cpp deleted file mode 100644 index a2e846973..000000000 --- a/iceoryx2-ffi/cxx/tests/src/log.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2024 Contributors to the Eclipse Foundation -// -// See the NOTICE file(s) distributed with this work for additional -// information regarding copyright ownership. -// -// This program and the accompanying materials are made available under the -// terms of the Apache Software License 2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license -// which is available at https://opensource.org/licenses/MIT. -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -#include "iox2/log.hpp" -#include "iox2/log_level.hpp" - -#include "test.hpp" - -namespace { -using namespace iox2; - -TEST(LogLevel, can_set_and_get_log_level) { - set_log_level(LogLevel::INFO); - EXPECT_EQ(get_log_level(), LogLevel::INFO); - - set_log_level(LogLevel::DEBUG); - EXPECT_EQ(get_log_level(), LogLevel::DEBUG); - - set_log_level(LogLevel::WARN); - EXPECT_EQ(get_log_level(), LogLevel::WARN); - - set_log_level(LogLevel::ERROR); - EXPECT_EQ(get_log_level(), LogLevel::ERROR); - - set_log_level(LogLevel::FATAL); - EXPECT_EQ(get_log_level(), LogLevel::FATAL); -} - -} // namespace diff --git a/iceoryx2-ffi/cxx/tests/src/log_tests.cpp b/iceoryx2-ffi/cxx/tests/src/log_tests.cpp new file mode 100644 index 000000000..6cd171cbf --- /dev/null +++ b/iceoryx2-ffi/cxx/tests/src/log_tests.cpp @@ -0,0 +1,110 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#include "iox/string.hpp" +#include "iox/vector.hpp" +#include "iox2/log.hpp" +#include "iox2/log_level.hpp" + +#include "test.hpp" + +namespace { +using namespace iox2; +constexpr uint64_t TEST_LOGGER_CAPACITY = 10; +constexpr uint64_t STRING_CAPACITY = 64; +class Entry { + private: + LogLevel m_log_level; + iox::string m_origin; + iox::string m_message; + + public: + Entry(LogLevel log_level, const char* origin, const char* message) + : m_log_level { log_level } + , m_origin { iox::TruncateToCapacity, origin } + , m_message { iox::TruncateToCapacity, message } { + } + + auto is_equal(LogLevel log_level, const char* origin, const char* message) -> bool { + return m_log_level == log_level && m_origin == iox::string(iox::TruncateToCapacity, origin) + && m_message == iox::string(iox::TruncateToCapacity, message); + } +}; + + +class TestLogger : public Log { + public: + static auto get_instance() -> TestLogger& { + static TestLogger INSTANCE; + return INSTANCE; + } + + void log(LogLevel log_level, const char* origin, const char* message) override { + if (m_log_buffer.size() < TEST_LOGGER_CAPACITY) { + m_log_buffer.emplace_back(log_level, origin, message); + } + } + + auto get_log_buffer() -> iox::vector { + auto buffer = m_log_buffer; + m_log_buffer.clear(); + return buffer; + } + + private: + iox::vector m_log_buffer; +}; + +TEST(Log, custom_logger_works) { + ASSERT_TRUE(set_logger(TestLogger::get_instance())); + + log(LogLevel::Trace, "hello", "world"); + log(LogLevel::Debug, "goodbye", "hypnotoad"); + log(LogLevel::Info, "Who is looking for freedom?", "The Hoff!"); + log(LogLevel::Warn, "Blümchen", "Bassface"); + log(LogLevel::Error, "Blümchen should record a single with", "The almighty Hypnotoad"); + log(LogLevel::Fatal, "It is the end", "my beloved toad."); + + auto log_buffer = TestLogger::get_instance().get_log_buffer(); + + ASSERT_THAT(log_buffer.size(), Eq(6)); + + ASSERT_TRUE(log_buffer[0].is_equal(LogLevel::Trace, "hello", "world")); + ASSERT_TRUE(log_buffer[1].is_equal(LogLevel::Debug, "goodbye", "hypnotoad")); + ASSERT_TRUE(log_buffer[2].is_equal(LogLevel::Info, "Who is looking for freedom?", "The Hoff!")); + ASSERT_TRUE(log_buffer[3].is_equal(LogLevel::Warn, "Blümchen", "Bassface")); + ASSERT_TRUE( + log_buffer[4].is_equal(LogLevel::Error, "Blümchen should record a single with", "The almighty Hypnotoad")); + ASSERT_TRUE(log_buffer[5].is_equal(LogLevel::Fatal, "It is the end", "my beloved toad.")); +} + +TEST(Log, can_set_and_get_log_level) { + set_log_level(LogLevel::Trace); + EXPECT_EQ(get_log_level(), LogLevel::Trace); + + set_log_level(LogLevel::Debug); + EXPECT_EQ(get_log_level(), LogLevel::Debug); + + set_log_level(LogLevel::Info); + EXPECT_EQ(get_log_level(), LogLevel::Info); + + set_log_level(LogLevel::Warn); + EXPECT_EQ(get_log_level(), LogLevel::Warn); + + set_log_level(LogLevel::Error); + EXPECT_EQ(get_log_level(), LogLevel::Error); + + set_log_level(LogLevel::Fatal); + EXPECT_EQ(get_log_level(), LogLevel::Fatal); +} + +} // namespace diff --git a/iceoryx2-ffi/ffi/src/api/log.rs b/iceoryx2-ffi/ffi/src/api/log.rs index 53bf4fcd9..1e6d423af 100644 --- a/iceoryx2-ffi/ffi/src/api/log.rs +++ b/iceoryx2-ffi/ffi/src/api/log.rs @@ -11,10 +11,18 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT #![allow(clippy::upper_case_acronyms)] +#![allow(non_camel_case_types)] // BEGIN type definition -use iceoryx2_bb_log::{get_log_level, set_log_level, LogLevel}; +use iceoryx2_bb_log::{ + get_log_level, set_log_level, set_logger, Log, LogLevel, __internal_print_log_msg, + logger::{use_console_logger, use_file_logger}, +}; +use std::{ + ffi::{c_char, CStr}, + sync::Once, +}; #[repr(C)] #[derive(Copy, Clone)] @@ -54,18 +62,134 @@ impl From for LogLevel { } } +impl From for iox2_log_level_e { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Trace => iox2_log_level_e::TRACE, + LogLevel::Debug => iox2_log_level_e::DEBUG, + LogLevel::Info => iox2_log_level_e::INFO, + LogLevel::Warn => iox2_log_level_e::WARN, + LogLevel::Error => iox2_log_level_e::ERROR, + LogLevel::Fatal => iox2_log_level_e::FATAL, + } + } +} + +static mut LOGGER: Option = None; +static INIT: Once = Once::new(); + +struct CLogger { + callback: iox2_log_callback, +} + +impl CLogger { + const fn new(callback: iox2_log_callback) -> Self { + Self { callback } + } +} + +impl Log for CLogger { + fn log( + &self, + log_level: LogLevel, + origin: std::fmt::Arguments, + formatted_message: std::fmt::Arguments, + ) { + let mut origin = origin.to_string(); + origin.push('\0'); + let mut formatted_message = formatted_message.to_string(); + formatted_message.push('\0'); + + (self.callback)( + log_level.into(), + origin.as_bytes().as_ptr().cast(), + formatted_message.as_bytes().as_ptr().cast(), + ); + } +} + +/// The custom log callback for [`iox2_set_logger`] +/// +/// # Arguments +/// +/// 1. The log level of the message +/// 2. The origin of the message +/// 3. The actual log message +pub type iox2_log_callback = extern "C" fn(iox2_log_level_e, *const c_char, *const c_char); + // END type definition // BEGIN C API +/// Adds a log message to the logger. +/// +/// # Safety +/// +/// * origin must be either NULL or a valid pointer to a string. +/// * message must be a valid pointer to a string +#[no_mangle] +pub unsafe extern "C" fn iox2_log( + log_level: iox2_log_level_e, + origin: *const c_char, + message: *const c_char, +) { + debug_assert!(!message.is_null()); + + let empty_origin = b"\0"; + let origin = if origin.is_null() { + CStr::from_bytes_with_nul(empty_origin).unwrap() + } else { + CStr::from_ptr(origin) + }; + let message = CStr::from_ptr(message); + + __internal_print_log_msg( + log_level.into(), + format_args!("{}", origin.to_string_lossy()), + format_args!("{}", message.to_string_lossy()), + ); +} +/// Sets the console logger as default logger. Returns true if the logger was set, otherwise false. +#[no_mangle] +pub extern "C" fn iox2_use_console_logger() -> bool { + use_console_logger() +} + +/// Sets the file logger as default logger. Returns true if the logger was set, otherwise false. +/// +/// # Safety +/// +/// * log_file must be a valid pointer to a string +#[no_mangle] +pub unsafe extern "C" fn iox2_use_file_logger(log_file: *const c_char) -> bool { + debug_assert!(!log_file.is_null()); + + let log_file = CStr::from_ptr(log_file).to_string_lossy(); + use_file_logger(&log_file) +} + +/// Sets the log level. #[no_mangle] pub unsafe extern "C" fn iox2_set_log_level(v: iox2_log_level_e) { set_log_level(v.into()); } +/// Returns the current log level. #[no_mangle] pub unsafe extern "C" fn iox2_get_log_level() -> iox2_log_level_e { get_log_level().into() } +/// Sets the logger that shall be used. This function can only be called once and must be called +/// before any log message was created. +/// It returns true if the logger was set, otherwise false. +#[no_mangle] +pub unsafe extern "C" fn iox2_set_logger(logger: iox2_log_callback) -> bool { + INIT.call_once(|| { + LOGGER = Some(CLogger::new(logger)); + }); + + set_logger(LOGGER.as_ref().unwrap()) +} + // END C API