diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt index d90c05421..031974761 100644 --- a/src/iceberg/CMakeLists.txt +++ b/src/iceberg/CMakeLists.txt @@ -49,7 +49,8 @@ set(ICEBERG_SOURCES arrow_c_data_guard_internal.cc util/murmurhash3_internal.cc util/timepoint.cc - util/gzip_internal.cc) + util/gzip_internal.cc + util/logger.cc) set(ICEBERG_STATIC_BUILD_INTERFACE_LIBS) set(ICEBERG_SHARED_BUILD_INTERFACE_LIBS) diff --git a/src/iceberg/util/logger.cc b/src/iceberg/util/logger.cc new file mode 100644 index 000000000..fa9a0ca8f --- /dev/null +++ b/src/iceberg/util/logger.cc @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 "iceberg/util/logger.h" + +#include +#include + +#include "iceberg/util/spdlog_logger.h" + +namespace iceberg { + +namespace { + +/// \brief Convert iceberg LogLevel to spdlog level +spdlog::level::level_enum ToSpdlogLevel(LogLevel level) { + switch (level) { + case LogLevel::kTrace: + return spdlog::level::trace; + case LogLevel::kDebug: + return spdlog::level::debug; + case LogLevel::kInfo: + return spdlog::level::info; + case LogLevel::kWarn: + return spdlog::level::warn; + case LogLevel::kError: + return spdlog::level::err; + case LogLevel::kCritical: + return spdlog::level::critical; + case LogLevel::kOff: + return spdlog::level::off; + default: + return spdlog::level::info; + } +} + +/** + * \brief Convert spdlog level to iceberg LogLevel + */ +LogLevel FromSpdlogLevel(spdlog::level::level_enum level) { + switch (level) { + case spdlog::level::trace: + return LogLevel::kTrace; + case spdlog::level::debug: + return LogLevel::kDebug; + case spdlog::level::info: + return LogLevel::kInfo; + case spdlog::level::warn: + return LogLevel::kWarn; + case spdlog::level::err: + return LogLevel::kError; + case spdlog::level::critical: + return LogLevel::kCritical; + case spdlog::level::off: + return LogLevel::kOff; + default: + return LogLevel::kInfo; + } +} + +} // namespace + +// SpdlogLogger implementation +SpdlogLogger::SpdlogLogger(std::string_view logger_name) { + auto spdlog_logger = spdlog::get(std::string(logger_name)); + if (!spdlog_logger) { + spdlog_logger = spdlog::stdout_color_mt(std::string(logger_name)); + } + logger_ = std::static_pointer_cast(spdlog_logger); + current_level_ = FromSpdlogLevel(spdlog_logger->level()); +} + +SpdlogLogger::SpdlogLogger(std::shared_ptr spdlog_logger) + : logger_(std::move(spdlog_logger)) { + auto typed_logger = std::static_pointer_cast(logger_); + current_level_ = FromSpdlogLevel(typed_logger->level()); +} + +bool SpdlogLogger::ShouldLogImpl(LogLevel level) const noexcept { + return level >= current_level_; +} + +void SpdlogLogger::LogRawImpl(LogLevel level, const std::source_location& location, + const std::string& message) const { + auto typed_logger = std::static_pointer_cast(logger_); + auto spdlog_level = ToSpdlogLevel(level); + + // Add source location information + std::string full_message = + std::format("[{}:{}:{}] {}", location.file_name(), location.line(), + location.function_name(), message); + + typed_logger->log(spdlog_level, full_message); +} + +void SpdlogLogger::SetLevelImpl(LogLevel level) { + current_level_ = level; + auto typed_logger = std::static_pointer_cast(logger_); + typed_logger->set_level(ToSpdlogLevel(level)); +} + +LogLevel SpdlogLogger::GetLevelImpl() const noexcept { return current_level_; } + +// LoggerRegistry implementation +LoggerRegistry& LoggerRegistry::Instance() { + static LoggerRegistry instance; + return instance; +} + +void LoggerRegistry::InitializeDefault(std::string_view logger_name) { + auto spdlog_logger = std::make_shared(logger_name); + SetDefaultLogger(spdlog_logger); +} + +} // namespace iceberg diff --git a/src/iceberg/util/logger.h b/src/iceberg/util/logger.h new file mode 100644 index 000000000..1c7aab7a7 --- /dev/null +++ b/src/iceberg/util/logger.h @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace iceberg { + +/// \brief Log levels for the iceberg logger system +enum class LogLevel : int { + kTrace = 0, + kDebug = 1, + kInfo = 2, + kWarn = 3, + kError = 4, + kCritical = 5, + kOff = 6 +}; + +/// \brief Convert log level to string representation +ICEBERG_EXPORT constexpr std::string_view LogLevelToString(LogLevel level) { + switch (level) { + case LogLevel::kTrace: + return "TRACE"; + case LogLevel::kDebug: + return "DEBUG"; + case LogLevel::kInfo: + return "INFO"; + case LogLevel::kWarn: + return "WARN"; + case LogLevel::kError: + return "ERROR"; + case LogLevel::kCritical: + return "CRITICAL"; + case LogLevel::kOff: + return "OFF"; + default: + return "UNKNOWN"; + } +} + +/// \brief Logger interface that uses CRTP to avoid virtual function overhead +/// \tparam Derived The concrete logger implementation +template +class ICEBERG_EXPORT LoggerInterface { + public: + /// \brief Check if a log level is enabled + bool ShouldLog(LogLevel level) const noexcept { + return derived()->ShouldLogImpl(level); + } + + /// \brief Log a message with the specified level + template + void Log(LogLevel level, const std::source_location& location, + std::string_view format_str, Args&&... args) const { + derived()->LogImpl(level, location, format_str, std::forward(args)...); + } + + /// \brief Set the minimum log level + void SetLevel(LogLevel level) { derived()->SetLevelImpl(level); } + + /// \brief Get the current minimum log level + LogLevel GetLevel() const noexcept { return derived()->GetLevelImpl(); } + + protected: + LoggerInterface() = default; + ~LoggerInterface() = default; + + private: + /// \brief Get const pointer to the derived class instance + const Derived* derived() const noexcept { return static_cast(this); } + + /// \brief Get non-const pointer to the derived class instance + Derived* derived() noexcept { return static_cast(this); } +}; + +/// \brief Concept to constrain types that implement the Logger interface +template +concept Logger = requires(const T& t, T& nt, LogLevel level, + const std::source_location& location, + std::string_view format_str) { + { t.ShouldLogImpl(level) } -> std::convertible_to; + { t.LogImpl(level, location, format_str) } -> std::same_as; + { nt.SetLevelImpl(level) } -> std::same_as; + { t.GetLevelImpl() } -> std::convertible_to; +} && std::is_base_of_v, T>; + +/// \brief Global logger registry for managing logger instances +class ICEBERG_EXPORT LoggerRegistry { + public: + /// \brief Get the singleton instance of the logger registry + static LoggerRegistry& Instance(); + + /// \brief Set the default logger implementation + /// + /// \tparam LoggerImpl The logger implementation type + /// \param logger The logger instance to set as default + template + void SetDefaultLogger(std::shared_ptr logger) { + default_logger_ = std::static_pointer_cast(logger); + log_func_ = [](const void* logger_ptr, LogLevel level, + const std::source_location& location, std::string_view format_str, + std::format_args args) { + auto* typed_logger = static_cast(logger_ptr); + std::string formatted_message = std::vformat(format_str, args); + typed_logger->Log(level, location, formatted_message); + }; + should_log_func_ = [](const void* logger_ptr, LogLevel level) -> bool { + auto* typed_logger = static_cast(logger_ptr); + return typed_logger->ShouldLog(level); + }; + } + + /// \brief Get the default logger + /// + /// \tparam LoggerImpl The expected logger implementation type + /// \return Shared pointer to the logger, or nullptr if type doesn't match + template + std::shared_ptr GetDefaultLogger() const { + return std::static_pointer_cast(default_logger_); + } + + /// \brief Log using the default logger + template + void Log(LogLevel level, const std::source_location& location, + std::string_view format_str, Args&&... args) const { + if (default_logger_ && should_log_func_ && log_func_) { + if (should_log_func_(default_logger_.get(), level)) { + try { + if constexpr (sizeof...(args) > 0) { + auto args_store = std::make_format_args(args...); + log_func_(default_logger_.get(), level, location, format_str, args_store); + } else { + log_func_(default_logger_.get(), level, location, format_str, + std::make_format_args()); + } + } catch (const std::exception& e) { + std::cerr << "Logging error: " << e.what() << std::endl; + } + } + } + } + + /// \brief Initialize with default spdlog logger + void InitializeDefault(std::string_view logger_name = "iceberg"); + + private: + LoggerRegistry() = default; + + std::shared_ptr default_logger_; + void (*log_func_)(const void*, LogLevel, const std::source_location&, std::string_view, + std::format_args) = nullptr; + bool (*should_log_func_)(const void*, LogLevel) = nullptr; +}; + +/// \brief Convenience macros for logging with automatic source location +#define ICEBERG_LOG_WITH_LOCATION(level, format_str, ...) \ + do { \ + ::iceberg::LoggerRegistry::Instance().Log(level, std::source_location::current(), \ + format_str __VA_OPT__(, ) __VA_ARGS__); \ + } while (false) + +#define ICEBERG_LOG_TRACE(format_str, ...) \ + ICEBERG_LOG_WITH_LOCATION(::iceberg::LogLevel::kTrace, \ + format_str __VA_OPT__(, ) __VA_ARGS__) + +#define ICEBERG_LOG_DEBUG(format_str, ...) \ + ICEBERG_LOG_WITH_LOCATION(::iceberg::LogLevel::kDebug, \ + format_str __VA_OPT__(, ) __VA_ARGS__) + +#define ICEBERG_LOG_INFO(format_str, ...) \ + ICEBERG_LOG_WITH_LOCATION(::iceberg::LogLevel::kInfo, \ + format_str __VA_OPT__(, ) __VA_ARGS__) + +#define ICEBERG_LOG_WARN(format_str, ...) \ + ICEBERG_LOG_WITH_LOCATION(::iceberg::LogLevel::kWarn, \ + format_str __VA_OPT__(, ) __VA_ARGS__) + +#define ICEBERG_LOG_ERROR(format_str, ...) \ + ICEBERG_LOG_WITH_LOCATION(::iceberg::LogLevel::kError, \ + format_str __VA_OPT__(, ) __VA_ARGS__) + +#define ICEBERG_LOG_CRITICAL(format_str, ...) \ + ICEBERG_LOG_WITH_LOCATION(::iceberg::LogLevel::kCritical, \ + format_str __VA_OPT__(, ) __VA_ARGS__) +} // namespace iceberg diff --git a/src/iceberg/util/spdlog_logger.h b/src/iceberg/util/spdlog_logger.h new file mode 100644 index 000000000..dadffeed3 --- /dev/null +++ b/src/iceberg/util/spdlog_logger.h @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "iceberg/util/logger.h" + +namespace iceberg { + +/// \brief Default spdlog-based logger implementation +class SpdlogLogger : public LoggerInterface { + public: + /// \brief Create a new spdlog logger with the given name + explicit SpdlogLogger(std::string_view logger_name = "iceberg"); + + /// \brief Create a spdlog logger from an existing spdlog::logger + explicit SpdlogLogger(std::shared_ptr spdlog_logger); + + // Implementation methods required by LoggerInterface + bool ShouldLogImpl(LogLevel level) const noexcept; + + template + void LogImpl(LogLevel level, const std::source_location& location, + std::string_view format_str, Args&&... args) const { + if constexpr (sizeof...(args) > 0) { + std::string formatted_message = + std::vformat(format_str, std::make_format_args(args...)); + LogRawImpl(level, location, formatted_message); + } else { + LogRawImpl(level, location, std::string(format_str)); + } + } + + void SetLevelImpl(LogLevel level); + LogLevel GetLevelImpl() const noexcept; + + /// \brief Get the underlying spdlog logger (for advanced usage) + std::shared_ptr GetUnderlyingLogger() const { return logger_; } + + private: + void LogRawImpl(LogLevel level, const std::source_location& location, + const std::string& message) const; + std::shared_ptr logger_; // Type-erased spdlog::logger + LogLevel current_level_ = LogLevel::kInfo; +}; + +} // namespace iceberg diff --git a/test/logger_test.cc b/test/logger_test.cc new file mode 100644 index 000000000..7001c31ac --- /dev/null +++ b/test/logger_test.cc @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 "iceberg/util/logger.h" + +#include + +#include + +#include "iceberg/util/spdlog_logger.h" + +namespace iceberg { + +/// \brief Example custom logger implementation using std::cout for testing +/// +/// This shows how downstream projects can implement their own logger +/// by inheriting from LoggerInterface and implementing the required methods. +class StdoutLogger : public LoggerInterface { + public: + explicit StdoutLogger(LogLevel min_level = LogLevel::kInfo) : min_level_(min_level) {} + + // Required implementation methods + bool ShouldLogImpl(LogLevel level) const noexcept { return level >= min_level_; } + + template + void LogImpl(LogLevel level, const std::source_location& location, + std::string_view format_str, Args&&... args) const { + if constexpr (sizeof...(args) > 0) { + std::string formatted_message = + std::vformat(format_str, std::make_format_args(args...)); + LogRawImpl(level, location, formatted_message); + } else { + LogRawImpl(level, location, std::string(format_str)); + } + } + + void SetLevelImpl(LogLevel level) noexcept { min_level_ = level; } + + LogLevel GetLevelImpl() const noexcept { return min_level_; } + + private: + void LogRawImpl(LogLevel level, const std::source_location& location, + const std::string& message) const { + std::cout << "[" << LogLevelToString(level) << "] " << message << std::endl; + } + + LogLevel min_level_; +}; + +class LoggerTest : public ::testing::Test { + protected: + void SetUp() override { + // Each test starts with a fresh logger registry + } + + void TearDown() override { + // Reset to default state + LoggerRegistry::Instance().InitializeDefault("test_logger"); + } +}; + +TEST_F(LoggerTest, DefaultSpdlogLogger) { + // Initialize with default spdlog logger + LoggerRegistry::Instance().InitializeDefault("test_logger"); + + // Test basic logging functionality + ICEBERG_LOG_INFO("This is an info message"); + ICEBERG_LOG_DEBUG("This is a debug message with value: {}", 42); + ICEBERG_LOG_WARN("This is a warning message"); + ICEBERG_LOG_ERROR("This is an error message"); + + // The test passes if no exceptions are thrown + SUCCEED(); +} + +TEST_F(LoggerTest, CustomStdoutLogger) { + // Create and register a custom logger + auto custom_logger = std::make_shared(LogLevel::kDebug); + LoggerRegistry::Instance().SetDefaultLogger(custom_logger); + + // Test logging with custom logger + ICEBERG_LOG_DEBUG("Debug message from custom logger"); + ICEBERG_LOG_INFO("Info message with parameter: {}", "test"); + ICEBERG_LOG_WARN("Warning from custom logger"); + + SUCCEED(); +} + +TEST_F(LoggerTest, LoggerLevels) { + auto logger = std::make_shared("test_level_logger"); + + // Test level filtering + logger->SetLevel(LogLevel::kWarn); + EXPECT_EQ(logger->GetLevel(), LogLevel::kWarn); + + // These should be filtered out + EXPECT_FALSE(logger->ShouldLog(LogLevel::kTrace)); + EXPECT_FALSE(logger->ShouldLog(LogLevel::kDebug)); + EXPECT_FALSE(logger->ShouldLog(LogLevel::kInfo)); + + // These should pass through + EXPECT_TRUE(logger->ShouldLog(LogLevel::kWarn)); + EXPECT_TRUE(logger->ShouldLog(LogLevel::kError)); + EXPECT_TRUE(logger->ShouldLog(LogLevel::kCritical)); +} +} // namespace iceberg