Skip to content

Commit 31274f5

Browse files
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.
1 parent 6dff5ac commit 31274f5

17 files changed

Lines changed: 450 additions & 65 deletions

‎CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ option(ICEBERG_S3 "Build with S3 support" OFF)
5454
option(ICEBERG_SIGV4 "Build with SigV4 support" OFF)
5555
option(ICEBERG_BUNDLE_AWSSDK "Bundle AWS SDK for S3/SigV4 support" ON)
5656
option(ICEBERG_BUNDLE_THRIFT "Bundle Thrift (from Arrow) for Hive catalog" ON)
57+
option(ICEBERG_SPDLOG "Use spdlog as the default logging backend" ON)
5758
option(ICEBERG_ENABLE_ASAN "Enable Address Sanitizer" OFF)
5859
option(ICEBERG_ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF)
5960

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ cmake --install build
9696
| `ICEBERG_BUILD_BUNDLE` | `ON` | Build the battery-included library |
9797
| `ICEBERG_BUILD_REST` | `ON` | Build REST catalog client |
9898
| `ICEBERG_BUILD_REST_INTEGRATION_TESTS` | `OFF` | Build REST catalog integration tests |
99+
| `ICEBERG_SPDLOG` | `ON` | Use spdlog as the default logging backend |
99100
| `ICEBERG_BUILD_HIVE` | `OFF` | Build Hive (HMS) catalog client |
100101
| `ICEBERG_BUILD_SQL_CATALOG` | `OFF` | Build SQL catalog client |
101102
| `ICEBERG_SQL_SQLITE` | `OFF` | Build the SQLite connector for the SQL catalog |
@@ -124,6 +125,7 @@ Meson-specific options (configured via `-D<option>=<value>`):
124125
|--------|---------|-------------|
125126
| `rest` | `enabled` | Build REST catalog client |
126127
| `rest_integration_test` | `disabled` | Build integration test for REST catalog |
128+
| `spdlog` | `enabled` | Use spdlog as the default logging backend |
127129
| `tests` | `enabled` | Build tests |
128130

129131
### Build Examples

‎cmake_modules/IcebergThirdpartyToolchain.cmake‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,9 @@ resolve_nanoarrow_dependency()
869869
resolve_croaring_dependency()
870870
resolve_utf8proc_dependency()
871871
resolve_nlohmann_json_dependency()
872-
resolve_spdlog_dependency()
872+
if(ICEBERG_SPDLOG)
873+
resolve_spdlog_dependency()
874+
endif()
873875

874876
if(ICEBERG_S3 OR ICEBERG_SIGV4)
875877
if(ICEBERG_SIGV4 AND NOT ICEBERG_BUILD_REST)

‎meson.options‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ option(
5151
value: 'disabled',
5252
)
5353

54+
option(
55+
'spdlog',
56+
type: 'feature',
57+
description: 'Use spdlog as the default logging backend',
58+
value: 'enabled',
59+
)
60+
5461
option('tests', type: 'feature', description: 'Build tests', value: 'enabled')
5562

5663
option(

‎src/iceberg/CMakeLists.txt‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
set(ICEBERG_INCLUDES "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/src>"
1919
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>")
20+
2021
set(ICEBERG_SOURCES
2122
arrow_c_data_guard_internal.cc
2223
arrow_c_data_util.cc
@@ -55,6 +56,7 @@ set(ICEBERG_SOURCES
5556
location_provider.cc
5657
logging/cerr_logger.cc
5758
logging/logger.cc
59+
logging/spdlog_logger.cc
5860
manifest/manifest_adapter.cc
5961
manifest/manifest_entry.cc
6062
manifest/manifest_filter_manager.cc
@@ -157,29 +159,38 @@ list(APPEND
157159
ICEBERG_STATIC_BUILD_INTERFACE_LIBS
158160
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,nanoarrow::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_static>,nanoarrow::nanoarrow_static,nanoarrow::nanoarrow_shared>>"
159161
nlohmann_json::nlohmann_json
160-
spdlog::spdlog
161162
utf8proc::utf8proc
162163
ZLIB::ZLIB)
163164
list(APPEND
164165
ICEBERG_SHARED_BUILD_INTERFACE_LIBS
165166
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,nanoarrow::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_shared>,nanoarrow::nanoarrow_shared,nanoarrow::nanoarrow_static>>"
166167
nlohmann_json::nlohmann_json
167-
spdlog::spdlog
168168
utf8proc::utf8proc
169169
ZLIB::ZLIB)
170170
list(APPEND
171171
ICEBERG_STATIC_INSTALL_INTERFACE_LIBS
172172
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,iceberg::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_static>,nanoarrow::nanoarrow_static,nanoarrow::nanoarrow_shared>>"
173173
"$<IF:$<BOOL:${NLOHMANN_JSON_VENDORED}>,iceberg::nlohmann_json,$<IF:$<TARGET_EXISTS:nlohmann_json::nlohmann_json>,nlohmann_json::nlohmann_json,nlohmann_json::nlohmann_json>>"
174-
"$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>"
175174
"$<IF:$<BOOL:${UTF8PROC_VENDORED}>,iceberg::utf8proc,utf8proc::utf8proc>")
176175
list(APPEND
177176
ICEBERG_SHARED_INSTALL_INTERFACE_LIBS
178177
"$<IF:$<BOOL:${NANOARROW_VENDORED}>,iceberg::nanoarrow_static,$<IF:$<TARGET_EXISTS:nanoarrow::nanoarrow_shared>,nanoarrow::nanoarrow_shared,nanoarrow::nanoarrow_static>>"
179178
"$<IF:$<BOOL:${NLOHMANN_JSON_VENDORED}>,iceberg::nlohmann_json,$<IF:$<TARGET_EXISTS:nlohmann_json::nlohmann_json>,nlohmann_json::nlohmann_json,nlohmann_json::nlohmann_json>>"
180-
"$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>"
181179
"$<IF:$<BOOL:${UTF8PROC_VENDORED}>,iceberg::utf8proc,utf8proc::utf8proc>")
182180

181+
if(ICEBERG_SPDLOG)
182+
set(ICEBERG_HAS_SPDLOG ON)
183+
list(APPEND ICEBERG_STATIC_BUILD_INTERFACE_LIBS spdlog::spdlog)
184+
list(APPEND ICEBERG_SHARED_BUILD_INTERFACE_LIBS spdlog::spdlog)
185+
list(APPEND ICEBERG_STATIC_INSTALL_INTERFACE_LIBS
186+
"$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>")
187+
list(APPEND ICEBERG_SHARED_INSTALL_INTERFACE_LIBS
188+
"$<IF:$<BOOL:${SPDLOG_VENDORED}>,iceberg::spdlog,spdlog::spdlog>")
189+
endif()
190+
191+
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/logging/config.h.in"
192+
"${CMAKE_CURRENT_BINARY_DIR}/logging/config.h")
193+
183194
add_iceberg_lib(iceberg
184195
SOURCES
185196
${ICEBERG_SOURCES}

‎src/iceberg/logging/config.h.in‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
// Internal, build-generated configuration; not installed.
23+
// ICEBERG_HAS_SPDLOG is defined when the spdlog backend is available.
24+
25+
#cmakedefine ICEBERG_HAS_SPDLOG

‎src/iceberg/logging/log_macros.h‎

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -79,65 +79,61 @@ void LogToCurrent(LogLevel level, const std::source_location& location,
7979
}
8080
}
8181

82-
/// \brief Runtime-level variant against the current logger: emit if enabled, then
83-
/// flush + abort when level == kFatal (using the same acquired logger).
82+
/// \brief Format once, emit if enabled, flush, run the fatal handler, and abort.
83+
///
84+
/// Formatting is independent of ShouldLog so the handler always receives the
85+
/// message. \p logger may be null.
86+
template <typename MakeMessage>
87+
[[noreturn]] void DispatchFatal(Logger* logger, const std::source_location& location,
88+
MakeMessage&& make_message) noexcept {
89+
std::string message;
90+
try {
91+
message = std::forward<MakeMessage>(make_message)();
92+
} catch (...) {
93+
message = "<fmt error>";
94+
}
95+
if (logger != nullptr) {
96+
if (logger->ShouldLog(LogLevel::kFatal)) {
97+
Emit(*logger, LogLevel::kFatal, location, std::string(message));
98+
}
99+
logger->Flush();
100+
}
101+
if (auto handler = GetFatalHandler()) {
102+
try {
103+
handler(location, message);
104+
} catch (...) { // a throwing handler must not prevent the abort
105+
}
106+
}
107+
std::abort();
108+
}
109+
84110
template <typename MakeMessage>
85111
void LogToCurrentRuntime(LogLevel level, const std::source_location& location,
86112
MakeMessage&& make_message) noexcept {
87113
const std::shared_ptr<Logger>& logger = CurrentLogger();
114+
if (level == LogLevel::kFatal) {
115+
DispatchFatal(logger.get(), location, std::forward<MakeMessage>(make_message));
116+
}
88117
if (logger) {
89118
EmitIfEnabled(*logger, level, location, std::forward<MakeMessage>(make_message));
90119
}
91-
if (level == LogLevel::kFatal) {
92-
if (logger) logger->Flush();
93-
std::abort();
94-
}
95120
}
96121

97-
/// \brief Runtime-level variant against an explicit logger: emit if enabled, then
98-
/// flush + abort when level == kFatal.
99122
template <typename MakeMessage>
100123
void LogToExplicitRuntime(Logger& logger, LogLevel level,
101124
const std::source_location& location,
102125
MakeMessage&& make_message) noexcept {
103-
EmitIfEnabled(logger, level, location, std::forward<MakeMessage>(make_message));
104126
if (level == LogLevel::kFatal) {
105-
logger.Flush();
106-
std::abort();
127+
DispatchFatal(&logger, location, std::forward<MakeMessage>(make_message));
107128
}
129+
EmitIfEnabled(logger, level, location, std::forward<MakeMessage>(make_message));
108130
}
109131

110-
/// \brief Fatal path: acquire the effective (scoped-or-default) logger ONCE, emit
111-
/// if enabled, flush that same logger, run any registered FatalHandler, then
112-
/// abort. Never returns.
113-
///
114-
/// The message is always formatted here (independent of ShouldLog) so the handler
115-
/// receives it even when the fatal record itself is filtered out. The handler runs
116-
/// after emit+flush and before abort; if it does not itself terminate the process,
117-
/// std::abort() still runs.
118132
template <typename MakeMessage>
119133
[[noreturn]] void LogFatal(const std::source_location& location,
120134
MakeMessage&& make_message) noexcept {
121-
std::string message;
122-
try {
123-
message = std::forward<MakeMessage>(make_message)();
124-
} catch (...) {
125-
message = "<fmt error>";
126-
}
127135
auto logger = GetCurrentLogger();
128-
if (logger) {
129-
if (logger->ShouldLog(LogLevel::kFatal)) {
130-
Emit(*logger, LogLevel::kFatal, location, std::string(message));
131-
}
132-
logger->Flush();
133-
}
134-
if (auto handler = GetFatalHandler()) {
135-
try {
136-
handler(location, message);
137-
} catch (...) { // a throwing handler must not prevent the abort
138-
}
139-
}
140-
std::abort();
136+
DispatchFatal(logger.get(), location, std::forward<MakeMessage>(make_message));
141137
}
142138

143139
} // namespace iceberg::internal

‎src/iceberg/logging/logger.cc‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <utility>
2828

2929
#include "iceberg/logging/cerr_logger.h"
30+
#include "iceberg/logging/spdlog_logger_internal.h"
3031

3132
namespace iceberg {
3233

@@ -42,11 +43,14 @@ class NoopLogger final : public Logger {
4243
bool IsNoop() const override { return true; }
4344
};
4445

45-
/// \brief Construct the process default logger for this build configuration.
46-
///
47-
/// Uses the always-available std::cerr sink. The spdlog backend (preferred when
48-
/// compiled in) is wired into this factory in a later block.
49-
std::shared_ptr<Logger> MakeDefaultLogger() { return std::make_shared<CerrLogger>(); }
46+
/// \brief Construct the configured process default logger.
47+
std::shared_ptr<Logger> MakeDefaultLogger() {
48+
#ifdef ICEBERG_HAS_SPDLOG
49+
return std::make_shared<internal::SpdLogger>();
50+
#else
51+
return std::make_shared<CerrLogger>();
52+
#endif
53+
}
5054

5155
/// \brief The process-global default-logger slot.
5256
struct DefaultSlot {

‎src/iceberg/logging/meson.build‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
# Generate the internal, non-installed logging backend config.
19+
logging_config_data = configuration_data()
20+
if spdlog_enabled
21+
logging_config_data.set('ICEBERG_HAS_SPDLOG', 1)
22+
endif
23+
configure_file(output: 'config.h', configuration: logging_config_data)
24+
25+
# Install only public logging headers.
1826
install_headers(
1927
[
2028
'cerr_logger.h',
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/logging/spdlog_logger_internal.h"
21+
22+
#ifdef ICEBERG_HAS_SPDLOG
23+
24+
# include <memory>
25+
# include <string>
26+
# include <unordered_map>
27+
# include <utility>
28+
29+
# include <spdlog/common.h>
30+
# include <spdlog/sinks/stdout_color_sinks.h>
31+
32+
namespace iceberg::internal {
33+
34+
namespace {
35+
36+
spdlog::level::level_enum ToSpdLevel(LogLevel level) noexcept {
37+
switch (level) {
38+
case LogLevel::kTrace:
39+
return spdlog::level::trace;
40+
case LogLevel::kDebug:
41+
return spdlog::level::debug;
42+
case LogLevel::kInfo:
43+
return spdlog::level::info;
44+
case LogLevel::kWarn:
45+
return spdlog::level::warn;
46+
case LogLevel::kError:
47+
return spdlog::level::err;
48+
case LogLevel::kCritical:
49+
case LogLevel::kFatal:
50+
// spdlog has no fatal level; the macro layer handles termination.
51+
return spdlog::level::critical;
52+
case LogLevel::kOff:
53+
return spdlog::level::off;
54+
}
55+
return spdlog::level::off;
56+
}
57+
58+
} // namespace
59+
60+
SpdLogger::SpdLogger(LogLevel level)
61+
: SpdLogger(spdlog::logger("iceberg",
62+
std::make_shared<spdlog::sinks::stderr_color_sink_mt>()),
63+
level) {}
64+
65+
SpdLogger::SpdLogger(spdlog::logger logger, LogLevel level)
66+
: logger_(std::move(logger)), level_(level) {
67+
logger_.set_level(spdlog::level::trace);
68+
}
69+
70+
Status SpdLogger::Initialize(
71+
const std::unordered_map<std::string, std::string>& properties) {
72+
if (auto it = properties.find(std::string(kPatternProperty)); it != properties.end()) {
73+
logger_.set_pattern(it->second);
74+
}
75+
return Logger::Initialize(properties);
76+
}
77+
78+
void SpdLogger::Log(LogMessage&& message) noexcept {
79+
try {
80+
spdlog::source_loc loc{message.location.file_name(),
81+
static_cast<int>(message.location.line()),
82+
message.location.function_name()};
83+
// LogMessage is already formatted; use spdlog's raw-message overload.
84+
logger_.log(loc, ToSpdLevel(message.level),
85+
spdlog::string_view_t{message.message.data(), message.message.size()});
86+
} catch (...) {
87+
// Logging must never throw.
88+
}
89+
}
90+
91+
void SpdLogger::Flush() noexcept {
92+
try {
93+
logger_.flush();
94+
} catch (...) {
95+
}
96+
}
97+
98+
} // namespace iceberg::internal
99+
100+
#endif // ICEBERG_HAS_SPDLOG

0 commit comments

Comments
 (0)