Skip to content
Open
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion cmake_modules/IcebergThirdpartyToolchain.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,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)
Expand Down
6 changes: 5 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ project(
cpp = meson.get_compiler('cpp')
# Hide two noisy MSVC warnings (C4251/C4275) about standard-library types used in
# our public classes. They're harmless because the whole library ships as one DLL.
args = cpp.get_supported_arguments(['/bigobj', '/wd4251', '/wd4275'])
# /Zc:preprocessor: MSVC's conforming preprocessor, required for the __VA_OPT__
# used by the logging macros. get_supported_arguments drops these on non-MSVC.
args = cpp.get_supported_arguments(
['/bigobj', '/wd4251', '/wd4275', '/Zc:preprocessor'],
)
add_project_arguments(args, language: 'cpp')

subdir('src')
Expand Down
41 changes: 37 additions & 4 deletions src/iceberg/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@

set(ICEBERG_INCLUDES "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/src>"
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>")

# Generate the logging backend config header. ALWAYS generated (not gated by
# ICEBERG_SPDLOG) so logging/logger.cc can include it in both ON and OFF builds;
# only the definedness of ICEBERG_HAS_SPDLOG varies. Generated into the build
# tree (already on ICEBERG_INCLUDES), included as "iceberg/logging/config.h", and
# NOT installed (it must never appear in a public/installed header).
if(ICEBERG_SPDLOG)
set(ICEBERG_HAS_SPDLOG ON)
endif()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/logging/config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/logging/config.h")

set(ICEBERG_SOURCES
arrow_c_data_guard_internal.cc
arrow_c_data_util.cc
Expand Down Expand Up @@ -53,6 +65,7 @@ set(ICEBERG_SOURCES
location_provider.cc
logging/cerr_logger.cc
logging/logger.cc
logging/loggers.cc
manifest/manifest_adapter.cc
manifest/manifest_entry.cc
manifest/manifest_filter_manager.cc
Expand Down Expand Up @@ -154,29 +167,37 @@ 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>")

# spdlog backend: linked and compiled only when ICEBERG_SPDLOG is ON. When OFF,
# the core library has no spdlog dependency and CerrLogger is the default sink.
if(ICEBERG_SPDLOG)
list(APPEND ICEBERG_SOURCES logging/internal/spdlog_logger.cc)
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()

add_iceberg_lib(iceberg
SOURCES
${ICEBERG_SOURCES}
Expand All @@ -193,6 +214,18 @@ add_iceberg_lib(iceberg
OUTPUTS
ICEBERG_LIBRARIES)

# The logging macros in the installed public header log_macros.h use __VA_OPT__,
# which MSVC's traditional preprocessor rejects. Require the conforming
# preprocessor PUBLICly so downstream consumers that include log_macros.h get it
# on MSVC (and any library TU that includes it does too).
if(MSVC_TOOLCHAIN)
foreach(_iceberg_lib iceberg_shared iceberg_static)
if(TARGET ${_iceberg_lib})
target_compile_options(${_iceberg_lib} PUBLIC /Zc:preprocessor)
endif()
endforeach()
endif()

set(ICEBERG_DATA_SOURCES
data/data_writer.cc
data/delete_filter.cc
Expand Down
30 changes: 30 additions & 0 deletions src/iceberg/logging/config.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 for the logging backend.
// This header is NOT installed and must only be included from .cc files
// (logger.cc, internal/spdlog_logger.cc) -- never from a public header.
//
// ICEBERG_HAS_SPDLOG is defined when the project is built with -DICEBERG_SPDLOG=ON
// and left undefined otherwise. Always test it with #ifdef / #ifndef, never #if
// (it carries no value).

#cmakedefine ICEBERG_HAS_SPDLOG
107 changes: 107 additions & 0 deletions src/iceberg/logging/internal/spdlog_logger.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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/internal/spdlog_logger.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>

# include "iceberg/util/macros.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"; the process abort is owned by the macro layer.
return spdlog::level::critical;
case LogLevel::kOff:
return spdlog::level::off;
}
return spdlog::level::off;
}

} // namespace

SpdLogger::SpdLogger(LogLevel level)
: SpdLogger(std::make_shared<spdlog::logger>(
"iceberg", std::make_shared<spdlog::sinks::stderr_color_sink_mt>()),
level) {}

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);
}
// Apply "level" via the base implementation.
return Logger::Initialize(properties);
}

SpdLogger::SpdLogger(std::shared_ptr<spdlog::logger> logger, LogLevel level)
: logger_(std::move(logger)), level_(level) {
// logger_ is a hard precondition: SpdLogger is not consumer-constructible (it is
// obtained via the default logger or the "spdlog" registry factory, both of which
// pass a real logger), so Initialize/Log/Flush may dereference it unconditionally.
ICEBERG_DCHECK(logger_ != nullptr, "SpdLogger requires a non-null spdlog::logger");
logger_->set_level(spdlog::level::trace); // filtering is done by ShouldLog
}

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()};
// Pass the pre-formatted text as an argument ("{}") so any braces in the
// message are not re-interpreted as a format string.
logger_->log(loc, ToSpdLevel(message.level), "{}", message.message);
} catch (...) {
// Logging must never throw.
}
}

void SpdLogger::Flush() noexcept {
try {
logger_->flush();
} catch (...) {
}
}

} // namespace iceberg::internal

#endif // ICEBERG_HAS_SPDLOG
90 changes: 90 additions & 0 deletions src/iceberg/logging/internal/spdlog_logger.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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/internal/spdlog_logger.h
/// \brief spdlog-backed logging sink.
///
/// INTERNAL, NOT INSTALLED. Included only from .cc files (logger.cc and
/// spdlog_logger.cc). It pulls in the build-generated config.h itself and gates
/// its entire body on ICEBERG_HAS_SPDLOG, so it compiles to nothing unless the
/// project was built with ICEBERG_SPDLOG=ON. SpdLogger is not a
/// consumer-constructible public type -- applications obtain it via the default
/// logger or the "logger-impl"="spdlog" registry factory.

#include "iceberg/logging/config.h"

#ifdef ICEBERG_HAS_SPDLOG

# include <atomic>
# include <memory>

# include <spdlog/logger.h>

# include "iceberg/logging/log_level.h"
# include "iceberg/logging/logger.h"

namespace iceberg::internal {

/// \brief Logger backed by spdlog (synchronous only in v1).
///
/// Synchronous because spdlog::source_loc holds non-owning const char* that are
/// unsafe to forward into an async logger (spdlog #3227).
/// ICEBERG_EXPORT so the symbol is linkable from in-tree tests (and any
/// internal consumer) under -fvisibility=hidden / MSVC DLL builds. The header
/// is still not installed -- this is a binary-visibility detail, not public API.
class ICEBERG_EXPORT SpdLogger : public Logger {
public:
/// \brief Construct over a default stderr-backed spdlog logger.
explicit SpdLogger(LogLevel level = LogLevel::kInfo);

/// \brief Construct over a caller-provided spdlog logger.
///
/// The logger MUST be synchronous. Log() forwards spdlog::source_loc, which
/// borrows the std::source_location's const char* pointers; an async spdlog
/// logger would queue them past their lifetime (spdlog #3227 -> UB). This is a
/// caller contract -- spdlog exposes no reliable sync/async query to assert on.
explicit SpdLogger(std::shared_ptr<spdlog::logger> logger,
LogLevel level = LogLevel::kInfo);

/// \brief Apply the "pattern" property (spdlog set_pattern), then "level".
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:
std::shared_ptr<spdlog::logger> logger_;
std::atomic<LogLevel> level_;
};

} // namespace iceberg::internal

#endif // ICEBERG_HAS_SPDLOG
Loading