You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ga...@apache.org on 2026/08/15 13:10:19 UTC
(iceberg-cpp) branch main updated: feat(logging): add spdlog backend behind ICEBERG_SPDLOG (#726)
This is an automated email from the ASF dual-hosted git repository.
wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 31274f5f feat(logging): add spdlog backend behind ICEBERG_SPDLOG (#726)
31274f5f is described below
commit 31274f5fd1b5f6b34ca18f833e4b25ea76faf1cd
Author: kamcheungting-db <91...@users.noreply.github.com>
AuthorDate: Sat Aug 15 06:10:14 2026 -0700
feat(logging): add spdlog backend behind ICEBERG_SPDLOG (#726)
Part 5 of the logging stack (builds on #725). Adds the spdlog backend and the build option that selects it — the default in production builds.
**What's here**
- `SpdLogger` wraps `spdlog::logger` (synchronous), maps levels (fatal/critical → spdlog critical; the abort stays in the macro layer), and forwards source location. Default sink is a colored stderr sink.
- New `ICEBERG_SPDLOG` CMake option and matching Meson `spdlog` feature option (enabled by default). When disabled, the build has no spdlog dependency and `CerrLogger` is the default.
- `pattern` property is honored here via `spdlog set_pattern` (the cerr backend keeps its fixed layout); `level` works on both backends.
- `SpdLogger` lives in `logging/spdlog_logger_internal.h` and is not installed — apps get it via the default logger.
- Runtime `kFatal` macro paths now use the same fatal-handler and abort sequence as `ICEBERG_LOG_FATAL`.
**Tests** — logging tests cover default-backend selection, level filtering and mapping including fatal→critical, source-location forwarding, the `pattern` property, and runtime fatal-handler routing.
This pull request and its description were written by Isaac.
---
CMakeLists.txt | 1 +
README.md | 2 +
cmake_modules/IcebergThirdpartyToolchain.cmake | 4 +-
meson.options | 7 ++
src/iceberg/CMakeLists.txt | 19 +++-
src/iceberg/logging/config.h.in | 25 +++++
src/iceberg/logging/log_macros.h | 72 ++++++-------
src/iceberg/logging/logger.cc | 14 ++-
src/iceberg/logging/meson.build | 8 ++
src/iceberg/logging/spdlog_logger.cc | 100 ++++++++++++++++++
src/iceberg/logging/spdlog_logger_internal.h | 66 ++++++++++++
src/iceberg/meson.build | 19 ++--
src/iceberg/test/CMakeLists.txt | 3 +-
src/iceberg/test/logger_test.cc | 12 ++-
src/iceberg/test/macros_test.cc | 23 ++--
src/iceberg/test/meson.build | 1 +
src/iceberg/test/spdlog_logger_test.cc | 139 +++++++++++++++++++++++++
17 files changed, 450 insertions(+), 65 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7cbd5e55..0882be68 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -54,6 +54,7 @@ option(ICEBERG_S3 "Build with S3 support" OFF)
option(ICEBERG_SIGV4 "Build with SigV4 support" OFF)
option(ICEBERG_BUNDLE_AWSSDK "Bundle AWS SDK for S3/SigV4 support" ON)
option(ICEBERG_BUNDLE_THRIFT "Bundle Thrift (from Arrow) for Hive catalog" ON)
+option(ICEBERG_SPDLOG "Use spdlog as the default logging backend" ON)
option(ICEBERG_ENABLE_ASAN "Enable Address Sanitizer" OFF)
option(ICEBERG_ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF)
diff --git a/README.md b/README.md
index 1318933d..7206c459 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,7 @@ cmake --install build
| `ICEBERG_BUILD_BUNDLE` | `ON` | Build the battery-included library |
| `ICEBERG_BUILD_REST` | `ON` | Build REST catalog client |
| `ICEBERG_BUILD_REST_INTEGRATION_TESTS` | `OFF` | Build REST catalog integration tests |
+| `ICEBERG_SPDLOG` | `ON` | Use spdlog as the default logging backend |
| `ICEBERG_BUILD_HIVE` | `OFF` | Build Hive (HMS) catalog client |
| `ICEBERG_BUILD_SQL_CATALOG` | `OFF` | Build SQL catalog client |
| `ICEBERG_SQL_SQLITE` | `OFF` | Build the SQLite connector for the SQL catalog |
@@ -124,6 +125,7 @@ Meson-specific options (configured via `-D<option>=<value>`):
|--------|---------|-------------|
| `rest` | `enabled` | Build REST catalog client |
| `rest_integration_test` | `disabled` | Build integration test for REST catalog |
+| `spdlog` | `enabled` | Use spdlog as the default logging backend |
| `tests` | `enabled` | Build tests |
### Build Examples
diff --git a/cmake_modules/IcebergThirdpartyToolchain.cmake b/cmake_modules/IcebergThirdpartyToolchain.cmake
index 8b868320..d74f91f5 100644
--- a/cmake_modules/IcebergThirdpartyToolchain.cmake
+++ b/cmake_modules/IcebergThirdpartyToolchain.cmake
@@ -869,7 +869,9 @@ resolve_nanoarrow_dependency()
resolve_croaring_dependency()
resolve_utf8proc_dependency()
resolve_nlohmann_json_dependency()
-resolve_spdlog_dependency()
+if(ICEBERG_SPDLOG)
+ resolve_spdlog_dependency()
+endif()
if(ICEBERG_S3 OR ICEBERG_SIGV4)
if(ICEBERG_SIGV4 AND NOT ICEBERG_BUILD_REST)
diff --git a/meson.options b/meson.options
index 0448c61d..739a9095 100644
--- a/meson.options
+++ b/meson.options
@@ -51,6 +51,13 @@ option(
value: 'disabled',
)
+option(
+ 'spdlog',
+ type: 'feature',
+ description: 'Use spdlog as the default logging backend',
+ value: 'enabled',
+)
+
option('tests', type: 'feature', description: 'Build tests', value: 'enabled')
option(
diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt
index e48209d6..26ff8e05 100644
--- a/src/iceberg/CMakeLists.txt
+++ b/src/iceberg/CMakeLists.txt
@@ -17,6 +17,7 @@
set(ICEBERG_INCLUDES "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/src>"
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>")
+
set(ICEBERG_SOURCES
arrow_c_data_guard_internal.cc
arrow_c_data_util.cc
@@ -55,6 +56,7 @@ set(ICEBERG_SOURCES
location_provider.cc
logging/cerr_logger.cc
logging/logger.cc
+ logging/spdlog_logger.cc
manifest/manifest_adapter.cc
manifest/manifest_entry.cc
manifest/manifest_filter_manager.cc
@@ -157,29 +159,38 @@ list(APPEND
ICEBERG_STATIC_BUILD_INTERFACE_LIBS
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,nanoarrow::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_static>,nanoarrow::nanoarrow_static,nanoarrow::nanoarrow_shared>>"
nlohmann_json::nlohmann_json
- spdlog::spdlog
utf8proc::utf8proc
ZLIB::ZLIB)
list(APPEND
ICEBERG_SHARED_BUILD_INTERFACE_LIBS
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,nanoarrow::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_shared>,nanoarrow::nanoarrow_shared,nanoarrow::nanoarrow_static>>"
nlohmann_json::nlohmann_json
- spdlog::spdlog
utf8proc::utf8proc
ZLIB::ZLIB)
list(APPEND
ICEBERG_STATIC_INSTALL_INTERFACE_LIBS
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,iceberg::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_static>,nanoarrow::nanoarrow_static,nanoarrow::nanoarrow_shared>>"
"$<IF:$<BOOL:${NLOHMANN_JSON_VENDORED}>,iceberg::nlohmann_json,$<IF:$<TARGET_EXISTS:nlohmann_json::nlohmann_json>,nlohmann_json::nlohmann_json,nlohmann_json::nlohmann_json>>"
- "$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>"
"$<IF:$<BOOL:${UTF8PROC_VENDORED}>,iceberg::utf8proc,utf8proc::utf8proc>")
list(APPEND
ICEBERG_SHARED_INSTALL_INTERFACE_LIBS
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,iceberg::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_shared>,nanoarrow::nanoarrow_shared,nanoarrow::nanoarrow_static>>"
"$<IF:$<BOOL:${NLOHMANN_JSON_VENDORED}>,iceberg::nlohmann_json,$<IF:$<TARGET_EXISTS:nlohmann_json::nlohmann_json>,nlohmann_json::nlohmann_json,nlohmann_json::nlohmann_json>>"
- "$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>"
"$<IF:$<BOOL:${UTF8PROC_VENDORED}>,iceberg::utf8proc,utf8proc::utf8proc>")
+if(ICEBERG_SPDLOG)
+ set(ICEBERG_HAS_SPDLOG ON)
+ list(APPEND ICEBERG_STATIC_BUILD_INTERFACE_LIBS spdlog::spdlog)
+ list(APPEND ICEBERG_SHARED_BUILD_INTERFACE_LIBS spdlog::spdlog)
+ list(APPEND ICEBERG_STATIC_INSTALL_INTERFACE_LIBS
+ "$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>")
+ list(APPEND ICEBERG_SHARED_INSTALL_INTERFACE_LIBS
+ "$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>")
+endif()
+
+configure_file("${CMAKE_CURRENT_SOURCE_DIR}/logging/config.h.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/logging/config.h")
+
add_iceberg_lib(iceberg
SOURCES
${ICEBERG_SOURCES}
diff --git a/src/iceberg/logging/config.h.in b/src/iceberg/logging/config.h.in
new file mode 100644
index 00000000..6933230a
--- /dev/null
+++ b/src/iceberg/logging/config.h.in
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+// Internal, build-generated configuration; not installed.
+// ICEBERG_HAS_SPDLOG is defined when the spdlog backend is available.
+
+#cmakedefine ICEBERG_HAS_SPDLOG
diff --git a/src/iceberg/logging/log_macros.h b/src/iceberg/logging/log_macros.h
index cbf33dc3..0dc510f0 100644
--- a/src/iceberg/logging/log_macros.h
+++ b/src/iceberg/logging/log_macros.h
@@ -79,65 +79,61 @@ void LogToCurrent(LogLevel level, const std::source_location& location,
}
}
-/// \brief Runtime-level variant against the current logger: emit if enabled, then
-/// flush + abort when level == kFatal (using the same acquired logger).
+/// \brief Format once, emit if enabled, flush, run the fatal handler, and abort.
+///
+/// Formatting is independent of ShouldLog so the handler always receives the
+/// message. \p logger may be null.
+template <typename MakeMessage>
+[[noreturn]] void DispatchFatal(Logger* logger, const std::source_location& location,
+ MakeMessage&& make_message) noexcept {
+ std::string message;
+ try {
+ message = std::forward<MakeMessage>(make_message)();
+ } catch (...) {
+ message = "<fmt error>";
+ }
+ if (logger != nullptr) {
+ if (logger->ShouldLog(LogLevel::kFatal)) {
+ Emit(*logger, LogLevel::kFatal, location, std::string(message));
+ }
+ logger->Flush();
+ }
+ if (auto handler = GetFatalHandler()) {
+ try {
+ handler(location, message);
+ } catch (...) { // a throwing handler must not prevent the abort
+ }
+ }
+ std::abort();
+}
+
template <typename MakeMessage>
void LogToCurrentRuntime(LogLevel level, const std::source_location& location,
MakeMessage&& make_message) noexcept {
const std::shared_ptr<Logger>& logger = CurrentLogger();
+ if (level == LogLevel::kFatal) {
+ DispatchFatal(logger.get(), location, std::forward<MakeMessage>(make_message));
+ }
if (logger) {
EmitIfEnabled(*logger, level, location, std::forward<MakeMessage>(make_message));
}
- if (level == LogLevel::kFatal) {
- if (logger) logger->Flush();
- std::abort();
- }
}
-/// \brief Runtime-level variant against an explicit logger: emit if enabled, then
-/// flush + abort when level == kFatal.
template <typename MakeMessage>
void LogToExplicitRuntime(Logger& logger, LogLevel level,
const std::source_location& location,
MakeMessage&& make_message) noexcept {
- EmitIfEnabled(logger, level, location, std::forward<MakeMessage>(make_message));
if (level == LogLevel::kFatal) {
- logger.Flush();
- std::abort();
+ DispatchFatal(&logger, location, std::forward<MakeMessage>(make_message));
}
+ EmitIfEnabled(logger, level, location, std::forward<MakeMessage>(make_message));
}
-/// \brief Fatal path: acquire the effective (scoped-or-default) logger ONCE, emit
-/// if enabled, flush that same logger, run any registered FatalHandler, then
-/// abort. Never returns.
-///
-/// The message is always formatted here (independent of ShouldLog) so the handler
-/// receives it even when the fatal record itself is filtered out. The handler runs
-/// after emit+flush and before abort; if it does not itself terminate the process,
-/// std::abort() still runs.
template <typename MakeMessage>
[[noreturn]] void LogFatal(const std::source_location& location,
MakeMessage&& make_message) noexcept {
- std::string message;
- try {
- message = std::forward<MakeMessage>(make_message)();
- } catch (...) {
- message = "<fmt error>";
- }
auto logger = GetCurrentLogger();
- if (logger) {
- if (logger->ShouldLog(LogLevel::kFatal)) {
- Emit(*logger, LogLevel::kFatal, location, std::string(message));
- }
- logger->Flush();
- }
- if (auto handler = GetFatalHandler()) {
- try {
- handler(location, message);
- } catch (...) { // a throwing handler must not prevent the abort
- }
- }
- std::abort();
+ DispatchFatal(logger.get(), location, std::forward<MakeMessage>(make_message));
}
} // namespace iceberg::internal
diff --git a/src/iceberg/logging/logger.cc b/src/iceberg/logging/logger.cc
index 77895e36..f1cb8488 100644
--- a/src/iceberg/logging/logger.cc
+++ b/src/iceberg/logging/logger.cc
@@ -27,6 +27,7 @@
#include <utility>
#include "iceberg/logging/cerr_logger.h"
+#include "iceberg/logging/spdlog_logger_internal.h"
namespace iceberg {
@@ -42,11 +43,14 @@ class NoopLogger final : public Logger {
bool IsNoop() const override { return true; }
};
-/// \brief Construct the process default logger for this build configuration.
-///
-/// Uses the always-available std::cerr sink. The spdlog backend (preferred when
-/// compiled in) is wired into this factory in a later block.
-std::shared_ptr<Logger> MakeDefaultLogger() { return std::make_shared<CerrLogger>(); }
+/// \brief Construct the configured process default logger.
+std::shared_ptr<Logger> MakeDefaultLogger() {
+#ifdef ICEBERG_HAS_SPDLOG
+ return std::make_shared<internal::SpdLogger>();
+#else
+ return std::make_shared<CerrLogger>();
+#endif
+}
/// \brief The process-global default-logger slot.
struct DefaultSlot {
diff --git a/src/iceberg/logging/meson.build b/src/iceberg/logging/meson.build
index 901855a7..04aec7d3 100644
--- a/src/iceberg/logging/meson.build
+++ b/src/iceberg/logging/meson.build
@@ -15,6 +15,14 @@
# specific language governing permissions and limitations
# under the License.
+# Generate the internal, non-installed logging backend config.
+logging_config_data = configuration_data()
+if spdlog_enabled
+ logging_config_data.set('ICEBERG_HAS_SPDLOG', 1)
+endif
+configure_file(output: 'config.h', configuration: logging_config_data)
+
+# Install only public logging headers.
install_headers(
[
'cerr_logger.h',
diff --git a/src/iceberg/logging/spdlog_logger.cc b/src/iceberg/logging/spdlog_logger.cc
new file mode 100644
index 00000000..238e6fed
--- /dev/null
+++ b/src/iceberg/logging/spdlog_logger.cc
@@ -0,0 +1,100 @@
+/*
+ * 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/logging/spdlog_logger_internal.h"
+
+#ifdef ICEBERG_HAS_SPDLOG
+
+# include <memory>
+# include <string>
+# include <unordered_map>
+# include <utility>
+
+# include <spdlog/common.h>
+# include <spdlog/sinks/stdout_color_sinks.h>
+
+namespace iceberg::internal {
+
+namespace {
+
+spdlog::level::level_enum ToSpdLevel(LogLevel level) noexcept {
+ 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:
+ case LogLevel::kFatal:
+ // spdlog has no fatal level; the macro layer handles termination.
+ return spdlog::level::critical;
+ case LogLevel::kOff:
+ return spdlog::level::off;
+ }
+ return spdlog::level::off;
+}
+
+} // namespace
+
+SpdLogger::SpdLogger(LogLevel level)
+ : SpdLogger(spdlog::logger("iceberg",
+ std::make_shared<spdlog::sinks::stderr_color_sink_mt>()),
+ level) {}
+
+SpdLogger::SpdLogger(spdlog::logger logger, LogLevel level)
+ : logger_(std::move(logger)), level_(level) {
+ logger_.set_level(spdlog::level::trace);
+}
+
+Status SpdLogger::Initialize(
+ const std::unordered_map<std::string, std::string>& properties) {
+ if (auto it = properties.find(std::string(kPatternProperty)); it != properties.end()) {
+ logger_.set_pattern(it->second);
+ }
+ return Logger::Initialize(properties);
+}
+
+void SpdLogger::Log(LogMessage&& message) noexcept {
+ try {
+ spdlog::source_loc loc{message.location.file_name(),
+ static_cast<int>(message.location.line()),
+ message.location.function_name()};
+ // LogMessage is already formatted; use spdlog's raw-message overload.
+ logger_.log(loc, ToSpdLevel(message.level),
+ spdlog::string_view_t{message.message.data(), message.message.size()});
+ } catch (...) {
+ // Logging must never throw.
+ }
+}
+
+void SpdLogger::Flush() noexcept {
+ try {
+ logger_.flush();
+ } catch (...) {
+ }
+}
+
+} // namespace iceberg::internal
+
+#endif // ICEBERG_HAS_SPDLOG
diff --git a/src/iceberg/logging/spdlog_logger_internal.h b/src/iceberg/logging/spdlog_logger_internal.h
new file mode 100644
index 00000000..e2aa99c6
--- /dev/null
+++ b/src/iceberg/logging/spdlog_logger_internal.h
@@ -0,0 +1,66 @@
+/*
+ * 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
+
+/// \file iceberg/logging/spdlog_logger_internal.h
+/// \brief Internal spdlog-backed logging sink.
+
+#include "iceberg/logging/config.h"
+
+#ifdef ICEBERG_HAS_SPDLOG
+
+# include <atomic>
+
+# include <spdlog/logger.h>
+
+# include "iceberg/logging/logger.h"
+
+namespace iceberg::internal {
+
+class ICEBERG_EXPORT SpdLogger : public Logger {
+ public:
+ explicit SpdLogger(LogLevel level = LogLevel::kInfo);
+
+ /// \brief Construct with a synchronous logger using thread-safe sinks.
+ explicit SpdLogger(spdlog::logger logger, LogLevel level = LogLevel::kInfo);
+
+ Status Initialize(
+ const std::unordered_map<std::string, std::string>& properties) override;
+
+ bool ShouldLog(LogLevel level) const noexcept override {
+ return level >= level_.load(std::memory_order_relaxed);
+ }
+ void Log(LogMessage&& message) noexcept override;
+ void SetLevel(LogLevel level) noexcept override {
+ level_.store(level, std::memory_order_relaxed);
+ }
+ LogLevel level() const noexcept override {
+ return level_.load(std::memory_order_relaxed);
+ }
+ void Flush() noexcept override;
+
+ private:
+ spdlog::logger logger_;
+ std::atomic<LogLevel> level_;
+};
+
+} // namespace iceberg::internal
+
+#endif // ICEBERG_HAS_SPDLOG
diff --git a/src/iceberg/meson.build b/src/iceberg/meson.build
index 39e8b293..5e742f2d 100644
--- a/src/iceberg/meson.build
+++ b/src/iceberg/meson.build
@@ -63,6 +63,11 @@ configure_file(
install_dir: get_option('includedir') / 'iceberg',
)
+# Resolve spdlog before generating logging/config.h. The feature value controls
+# whether a missing dependency is allowed.
+spdlog_dep = dependency('spdlog', required: get_option('spdlog'))
+spdlog_enabled = spdlog_dep.found()
+
subdir('logging')
iceberg_include_dir = include_directories('..')
@@ -103,6 +108,7 @@ iceberg_sources = files(
'location_provider.cc',
'logging/cerr_logger.cc',
'logging/logger.cc',
+ 'logging/spdlog_logger.cc',
'manifest/manifest_adapter.cc',
'manifest/manifest_entry.cc',
'manifest/manifest_filter_manager.cc',
@@ -228,7 +234,6 @@ croaring_needs_static = (
croaring_dep = dependency('croaring', static: croaring_needs_static)
nanoarrow_dep = dependency('nanoarrow')
nlohmann_json_dep = dependency('nlohmann_json')
-spdlog_dep = dependency('spdlog')
# utf8proc's header declares its functions __declspec(dllimport) on Windows unless
# UTF8PROC_STATIC is defined, and the wrap does not propagate that define to consumers.
# Define it whenever utf8proc is linked statically, so the header's declarations match
@@ -243,13 +248,11 @@ if utf8proc_needs_static
endif
zlib_dep = dependency('zlib')
-iceberg_deps = [
- nanoarrow_dep,
- nlohmann_json_dep,
- spdlog_dep,
- utf8proc_dep,
- zlib_dep,
-]
+iceberg_deps = [nanoarrow_dep, nlohmann_json_dep, utf8proc_dep, zlib_dep]
+
+if spdlog_enabled
+ iceberg_deps += spdlog_dep
+endif
iceberg_lib = library(
'iceberg',
diff --git a/src/iceberg/test/CMakeLists.txt b/src/iceberg/test/CMakeLists.txt
index df820aa4..7cf013c4 100644
--- a/src/iceberg/test/CMakeLists.txt
+++ b/src/iceberg/test/CMakeLists.txt
@@ -97,7 +97,8 @@ add_iceberg_test(logging_test
log_level_test.cc
logger_test.cc
macros_active_level_test.cc
- macros_test.cc)
+ macros_test.cc
+ spdlog_logger_test.cc)
add_iceberg_test(expression_test
SOURCES
diff --git a/src/iceberg/test/logger_test.cc b/src/iceberg/test/logger_test.cc
index 81d8e7f9..8c6c668f 100644
--- a/src/iceberg/test/logger_test.cc
+++ b/src/iceberg/test/logger_test.cc
@@ -27,7 +27,9 @@
#include <gtest/gtest.h>
+#include "iceberg/logging/cerr_logger.h"
#include "iceberg/logging/log_level.h"
+#include "iceberg/logging/spdlog_logger_internal.h"
#include "iceberg/test/logging_test_helpers.h"
#include "iceberg/test/matchers.h"
@@ -43,7 +45,15 @@ TEST(LoggerTest, NoopIsSharedImmortalAndSilent) {
EXPECT_EQ(noop.get(), Logger::Noop().get());
}
-TEST(LoggerTest, DefaultLoggerIsNeverNull) { EXPECT_NE(GetDefaultLogger(), nullptr); }
+TEST(LoggerTest, DefaultLoggerMatchesBuildConfiguration) {
+ auto logger = GetDefaultLogger();
+ ASSERT_NE(logger, nullptr);
+#ifdef ICEBERG_HAS_SPDLOG
+ EXPECT_NE(dynamic_cast<internal::SpdLogger*>(logger.get()), nullptr);
+#else
+ EXPECT_NE(dynamic_cast<CerrLogger*>(logger.get()), nullptr);
+#endif
+}
TEST(LoggerTest, SetAndGetDefaultLogger) {
auto capturing = std::make_shared<CapturingLogger>();
diff --git a/src/iceberg/test/macros_test.cc b/src/iceberg/test/macros_test.cc
index a92e0379..7fc5b8e5 100644
--- a/src/iceberg/test/macros_test.cc
+++ b/src/iceberg/test/macros_test.cc
@@ -136,19 +136,28 @@ TEST(MacrosDeathTest, FatalAbortsEvenWhenRuntimeDisabled) {
"");
}
-TEST(MacrosDeathTest, GenericRuntimeFatalEmitsThenAborts) {
- // ICEBERG_LOG with a runtime kFatal level must also emit then abort.
- EXPECT_DEATH({ ICEBERG_LOG(LogLevel::kFatal, "gfatal {}", 1); }, "gfatal 1");
+TEST(MacrosDeathTest, GenericRuntimeFatalRunsHandlerWhenSuppressed) {
+ EXPECT_DEATH(
+ {
+ SetDefaultLevel(LogLevel::kOff);
+ SetFatalHandler([](const std::source_location&, std::string_view message) {
+ std::cerr << "H[" << message << "]\n";
+ });
+ ICEBERG_LOG(LogLevel::kFatal, "gfatal {}", 1);
+ },
+ "H\\[gfatal 1\\]");
}
-TEST(MacrosDeathTest, LogToFatalEmitsThenAborts) {
- // ICEBERG_LOG_TO with kFatal must emit to the explicit logger then abort.
+TEST(MacrosDeathTest, LogToFatalRunsHandlerWhenSuppressed) {
EXPECT_DEATH(
{
- CerrLogger sink(LogLevel::kTrace);
+ CerrLogger sink(LogLevel::kOff);
+ SetFatalHandler([](const std::source_location&, std::string_view message) {
+ std::cerr << "H[" << message << "]\n";
+ });
ICEBERG_LOG_TO(sink, LogLevel::kFatal, "tofatal {}", 2);
},
- "tofatal 2");
+ "H\\[tofatal 2\\]");
}
// Regression guard: ICEBERG_LOG_FATAL must route through the active ScopedLogger
diff --git a/src/iceberg/test/meson.build b/src/iceberg/test/meson.build
index 09fa1206..f9549eef 100644
--- a/src/iceberg/test/meson.build
+++ b/src/iceberg/test/meson.build
@@ -70,6 +70,7 @@ iceberg_tests = {
'logger_test.cc',
'macros_active_level_test.cc',
'macros_test.cc',
+ 'spdlog_logger_test.cc',
),
},
'expression_test': {
diff --git a/src/iceberg/test/spdlog_logger_test.cc b/src/iceberg/test/spdlog_logger_test.cc
new file mode 100644
index 00000000..6f6b2b57
--- /dev/null
+++ b/src/iceberg/test/spdlog_logger_test.cc
@@ -0,0 +1,139 @@
+/*
+ * 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/logging/spdlog_logger_internal.h"
+
+#ifdef ICEBERG_HAS_SPDLOG
+
+# include <memory>
+# include <source_location>
+# include <sstream>
+# include <string>
+# include <utility>
+
+# include <gtest/gtest.h>
+# include <spdlog/logger.h>
+# include <spdlog/sinks/ostream_sink.h>
+
+# include "iceberg/logging/log_level.h"
+# include "iceberg/logging/logger.h"
+
+namespace iceberg {
+
+namespace {
+
+LogMessage MakeMessage(LogLevel level, std::string text) {
+ return LogMessage{.level = level,
+ .message = std::move(text),
+ .location = std::source_location::current(),
+ .attributes = {}};
+}
+
+internal::SpdLogger MakeCapturing(std::ostringstream& output,
+ LogLevel level = LogLevel::kInfo) {
+ auto sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(output);
+ return internal::SpdLogger(spdlog::logger("test", std::move(sink)), level);
+}
+
+std::string Render(internal::SpdLogger& logger, std::ostringstream& output,
+ LogMessage message) {
+ logger.Log(std::move(message));
+ logger.Flush();
+ return output.str();
+}
+
+} // namespace
+
+TEST(SpdLoggerTest, LevelThreshold) {
+ internal::SpdLogger default_logger;
+ EXPECT_EQ(default_logger.level(), LogLevel::kInfo);
+ EXPECT_FALSE(default_logger.ShouldLog(LogLevel::kDebug));
+ EXPECT_TRUE(default_logger.ShouldLog(LogLevel::kError));
+
+ std::ostringstream output;
+ auto logger = MakeCapturing(output, LogLevel::kError);
+ EXPECT_EQ(logger.level(), LogLevel::kError);
+ EXPECT_FALSE(logger.ShouldLog(LogLevel::kWarn));
+
+ Log(logger, LogLevel::kWarn, "filtered");
+ logger.SetLevel(LogLevel::kTrace);
+ EXPECT_EQ(logger.level(), LogLevel::kTrace);
+ EXPECT_TRUE(logger.ShouldLog(LogLevel::kTrace));
+ Log(logger, LogLevel::kTrace, "visible");
+ logger.Flush();
+ const auto rendered = output.str();
+ EXPECT_EQ(rendered.find("filtered"), std::string::npos);
+ EXPECT_NE(rendered.find("visible"), std::string::npos);
+}
+
+TEST(SpdLoggerTest, ForwardsMessageVerbatim) {
+ std::ostringstream output;
+ auto logger = MakeCapturing(output);
+ const auto rendered =
+ Render(logger, output, MakeMessage(LogLevel::kInfo, "literal {not a placeholder}"));
+ EXPECT_NE(rendered.find("literal {not a placeholder}"), std::string::npos);
+}
+
+TEST(SpdLoggerTest, MapsCriticalAndFatalToCritical) {
+ std::ostringstream output;
+ auto logger = MakeCapturing(output);
+ auto status =
+ logger.Initialize({{std::string(kPatternProperty), std::string("%l %v")}});
+ ASSERT_TRUE(status.has_value());
+ logger.Log(MakeMessage(LogLevel::kCritical, "crit"));
+ const auto rendered = Render(logger, output, MakeMessage(LogLevel::kFatal, "fatal"));
+ EXPECT_NE(rendered.find("critical crit"), std::string::npos);
+ EXPECT_NE(rendered.find("critical fatal"), std::string::npos);
+}
+
+TEST(SpdLoggerTest, InitializeAppliesPatternAndLevel) {
+ std::ostringstream output;
+ auto logger = MakeCapturing(output);
+ auto status = logger.Initialize({{std::string(kPatternProperty), std::string("PFX %v")},
+ {std::string(kLevelProperty), std::string("error")}});
+ ASSERT_TRUE(status.has_value());
+ EXPECT_EQ(logger.level(), LogLevel::kError);
+ const auto rendered = Render(logger, output, MakeMessage(LogLevel::kError, "hello"));
+ EXPECT_NE(rendered.find("PFX hello"), std::string::npos);
+}
+
+TEST(SpdLoggerTest, ForwardsSourceLocationToSink) {
+ std::ostringstream output;
+ auto logger = MakeCapturing(output);
+ // %s, %#, and %! render the source file, line, and function.
+ auto status =
+ logger.Initialize({{std::string(kPatternProperty), std::string("%s:%# %! %v")}});
+ ASSERT_TRUE(status.has_value());
+
+ const auto here = std::source_location::current();
+ const std::string rendered = Render(logger, output,
+ LogMessage{.level = LogLevel::kError,
+ .message = "located",
+ .location = here,
+ .attributes = {}});
+ const std::string expected_location =
+ "spdlog_logger_test.cc:" + std::to_string(here.line());
+ EXPECT_NE(rendered.find(expected_location), std::string::npos) << rendered;
+ EXPECT_NE(rendered.find("TestBody"), std::string::npos) << rendered;
+ EXPECT_NE(rendered.find("located"), std::string::npos) << rendered;
+}
+
+} // namespace iceberg
+
+#endif // ICEBERG_HAS_SPDLOG