Skip to content

Commit a8f16eb

Browse files
Add the Logger interface and the process-wide default logger
Second of the logging stack. This defines what a logger is and where the rest of the code finds one, with no concrete backend yet. Logger is a small abstract interface: ShouldLog to test a level, Log to emit an already-formatted record, SetLevel/level, Flush, and a shared no-op instance. Records are passed as LogMessage, which owns its formatted text (so a sink may safely hold onto it) and carries the source location plus a reserved slot for structured key/values we may add later. The interface spells out two rules for implementations: never call back into logging from inside Log/Flush, and keep level() a true lower bound for ShouldLog. There is also one default logger per process, designed to be cheap to reach on the logging path: a lock-free atomic level check drops disabled messages outright, and when a message does need logging the current logger comes from a thread-local cache that only refreshes when the default actually changes, so the steady state has no lock or reference-count traffic. SetDefaultLogger and SetDefaultLevel swap it safely under a mutex. The seed default is the no-op logger; CerrLogger and the spdlog backend arrive in later PRs. Also adds the build-generated, .cc-only config header used later to pick the backend, and tests for the interface and the default-logger lifecycle. Co-authored-by: Isaac
1 parent 9b32a4a commit a8f16eb

10 files changed

Lines changed: 505 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ option(ICEBERG_SQL_MYSQL "Build the MySQL connector for the SQL catalog" OFF)
5555
option(ICEBERG_S3 "Build with S3 support" OFF)
5656
option(ICEBERG_SIGV4 "Build with SigV4 support" OFF)
5757
option(ICEBERG_BUNDLE_AWSSDK "Bundle AWS SDK for S3/SigV4 support" ON)
58+
option(ICEBERG_SPDLOG "Use spdlog as the default logging backend" ON)
5859
option(ICEBERG_ENABLE_ASAN "Enable Address Sanitizer" OFF)
5960
option(ICEBERG_ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF)
6061

src/iceberg/CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@
1717

1818
set(ICEBERG_INCLUDES "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/src>"
1919
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>")
20+
21+
# Generate the logging backend config header. ALWAYS generated (not gated by
22+
# ICEBERG_SPDLOG) so logging/logger.cc can include it in both ON and OFF builds;
23+
# only the definedness of ICEBERG_HAS_SPDLOG varies. Generated into the build
24+
# tree (already on ICEBERG_INCLUDES), included as "iceberg/logging/config.h", and
25+
# NOT installed (it must never appear in a public/installed header).
26+
if(ICEBERG_SPDLOG)
27+
set(ICEBERG_HAS_SPDLOG ON)
28+
endif()
29+
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/logging/config.h.in"
30+
"${CMAKE_CURRENT_BINARY_DIR}/logging/config.h")
31+
2032
set(ICEBERG_SOURCES
2133
arrow_c_data_util.cc
2234
arrow_c_data_guard_internal.cc
@@ -44,6 +56,7 @@ set(ICEBERG_SOURCES
4456
inheritable_metadata.cc
4557
json_serde.cc
4658
location_provider.cc
59+
logging/logger.cc
4760
manifest/manifest_adapter.cc
4861
manifest/manifest_entry.cc
4962
manifest/manifest_filter_manager.cc

src/iceberg/logging/config.h.in

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 for the logging backend.
23+
// This header is NOT installed and must only be included from .cc files
24+
// (logger.cc, internal/spdlog_logger.cc) -- never from a public header.
25+
//
26+
// ICEBERG_HAS_SPDLOG is defined when the project is built with -DICEBERG_SPDLOG=ON
27+
// and left undefined otherwise. Always test it with #ifdef / #ifndef, never #if
28+
// (it carries no value).
29+
30+
#cmakedefine ICEBERG_HAS_SPDLOG

src/iceberg/logging/logger.cc

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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/logger.h"
21+
22+
#include <atomic>
23+
#include <cstdint>
24+
#include <memory>
25+
#include <mutex>
26+
#include <utility>
27+
28+
namespace iceberg {
29+
30+
namespace {
31+
32+
/// \brief Logger that drops every record.
33+
class NoopLogger final : public Logger {
34+
public:
35+
bool ShouldLog(LogLevel /*level*/) const override { return false; }
36+
void Log(LogMessage&& /*message*/) noexcept override {}
37+
void SetLevel(LogLevel /*level*/) override {}
38+
LogLevel level() const override { return LogLevel::kOff; }
39+
bool IsNoop() const override { return true; }
40+
};
41+
42+
/// \brief Construct the process default logger for this build configuration.
43+
///
44+
/// Block 2 defaults to the no-op logger; Block 3 switches this to CerrLogger and
45+
/// Block 5 wraps the selection in `#ifdef ICEBERG_HAS_SPDLOG` to prefer SpdLogger.
46+
std::shared_ptr<Logger> MakeDefaultLogger() { return Logger::Noop(); }
47+
48+
/// \brief The process-global default-logger slot.
49+
struct DefaultSlot {
50+
std::mutex mtx;
51+
std::shared_ptr<Logger> logger;
52+
// Seeded to 1 so a fresh thread (tls_gen == 0) always refreshes on first use.
53+
std::atomic<uint64_t> gen{1};
54+
55+
DefaultSlot() : logger(MakeDefaultLogger()) {}
56+
};
57+
58+
/// \brief Immortal (leaked, hence reachable -> LSan-clean) accessor for the slot.
59+
DefaultSlot& Slot() {
60+
static auto* slot = new DefaultSlot();
61+
return *slot;
62+
}
63+
64+
/// \brief Cached effective minimum level (lock-free fast-path gate).
65+
///
66+
/// Constant-initialized permissive (kTrace); seeded to the default logger's
67+
/// level() on first slot use and on every Set*. As a lower bound it may only
68+
/// admit extra calls through to the authoritative Logger::ShouldLog.
69+
std::atomic<LogLevel> g_effective_level{LogLevel::kTrace};
70+
71+
} // namespace
72+
73+
std::shared_ptr<Logger> Logger::Noop() {
74+
// Intentionally leaked: reachable via the function-local static (LSan-clean)
75+
// and never destroyed, so logging during static teardown stays safe.
76+
static auto* instance = new std::shared_ptr<Logger>(std::make_shared<NoopLogger>());
77+
return *instance;
78+
}
79+
80+
std::shared_ptr<Logger> GetDefaultLogger() {
81+
DefaultSlot& slot = Slot();
82+
std::lock_guard<std::mutex> lock(slot.mtx);
83+
return slot.logger;
84+
}
85+
86+
void SetDefaultLogger(std::shared_ptr<Logger> logger) {
87+
if (!logger) {
88+
logger = Logger::Noop();
89+
}
90+
DefaultSlot& slot = Slot();
91+
std::lock_guard<std::mutex> lock(slot.mtx);
92+
g_effective_level.store(logger->level(), std::memory_order_relaxed);
93+
slot.logger = std::move(logger);
94+
// Publish the swap; the mutex provides the happens-before, gen is a detector.
95+
slot.gen.fetch_add(1, std::memory_order_relaxed);
96+
}
97+
98+
void SetDefaultLevel(LogLevel level) {
99+
DefaultSlot& slot = Slot();
100+
std::lock_guard<std::mutex> lock(slot.mtx);
101+
slot.logger->SetLevel(level);
102+
g_effective_level.store(level, std::memory_order_relaxed);
103+
}
104+
105+
namespace detail {
106+
107+
LogLevel EffectiveLevel() noexcept {
108+
return g_effective_level.load(std::memory_order_relaxed);
109+
}
110+
111+
const std::shared_ptr<Logger>& CurrentLogger() noexcept {
112+
static thread_local std::shared_ptr<Logger> tls;
113+
static thread_local uint64_t tls_gen = 0;
114+
DefaultSlot& slot = Slot();
115+
uint64_t current = slot.gen.load(std::memory_order_relaxed);
116+
if (current != tls_gen) {
117+
std::lock_guard<std::mutex> lock(slot.mtx);
118+
tls = slot.logger;
119+
tls_gen = current;
120+
}
121+
return tls;
122+
}
123+
124+
} // namespace detail
125+
126+
} // namespace iceberg

src/iceberg/logging/logger.h

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
/// \file iceberg/logging/logger.h
23+
/// \brief Pluggable logging interface and the process-global default logger.
24+
///
25+
/// This header is backend-agnostic: it never includes the build-generated
26+
/// backend configuration header and never references the spdlog feature macro,
27+
/// so consumers see one stable API regardless of how the backend was configured.
28+
29+
#include <memory>
30+
#include <source_location>
31+
#include <string>
32+
#include <string_view>
33+
#include <unordered_map>
34+
#include <vector>
35+
36+
#include "iceberg/iceberg_export.h"
37+
#include "iceberg/logging/log_level.h"
38+
#include "iceberg/result.h"
39+
40+
namespace iceberg {
41+
42+
/// \brief A structured key/value attribute attached to a log record.
43+
///
44+
/// Both key and value are owned so a sink may retain the record safely.
45+
/// Unused in v1; reserved so structured logging can be added without an ABI
46+
/// break to LogMessage.
47+
struct LogAttribute {
48+
std::string key;
49+
std::string value;
50+
};
51+
52+
/// \brief A single log record handed to a Logger.
53+
///
54+
/// The formatted message is owned (moved in by the logging macros), so a sink
55+
/// may safely retain the record beyond the Log() call. The member set must not
56+
/// depend on the build's logging backend (the spdlog backend never appears here).
57+
struct LogMessage {
58+
LogLevel level;
59+
std::string message;
60+
std::source_location location;
61+
std::vector<LogAttribute> attributes;
62+
};
63+
64+
/// \brief Pluggable logging sink.
65+
///
66+
/// Implementations must be thread-safe and must not throw. They must also obey:
67+
/// - No reentrancy: Log()/Flush() must not call the logging macros or
68+
/// GetDefaultLogger() (UB -- deadlock with mutex-based sinks).
69+
/// - Lower-bound level: ShouldLog(l) must imply l >= level(), i.e. level()
70+
/// summarizes ShouldLog(), because the global fast-path gate reflects only
71+
/// level(). A logger needing finer logic must report its most permissive
72+
/// threshold from level().
73+
class ICEBERG_EXPORT Logger {
74+
public:
75+
virtual ~Logger() = default;
76+
77+
/// \brief Property-based setup, called by Loggers::Load() before first use.
78+
///
79+
/// The default is a no-op. Recognized properties include "level" (parsed via
80+
/// LogLevelFromString) and, for formatting sinks, "pattern".
81+
virtual Status Initialize(
82+
[[maybe_unused]] const std::unordered_map<std::string, std::string>& properties) {
83+
return {};
84+
}
85+
86+
/// \brief Cheap check whether a record at \p level would be emitted.
87+
virtual bool ShouldLog(LogLevel level) const = 0;
88+
89+
/// \brief Emit one (already-formatted) record, taking ownership. Must not throw.
90+
virtual void Log(LogMessage&& message) noexcept = 0;
91+
92+
/// \brief Set the minimum level this logger emits.
93+
virtual void SetLevel(LogLevel level) = 0;
94+
95+
/// \brief Return the minimum level this logger emits.
96+
virtual LogLevel level() const = 0;
97+
98+
/// \brief Flush any buffered output. Must not throw; best-effort on the fatal path.
99+
virtual void Flush() noexcept {}
100+
101+
/// \brief Return true if this logger is a no-op.
102+
virtual bool IsNoop() const { return false; }
103+
104+
/// \brief Return a shared, immortal no-op logger singleton.
105+
static std::shared_ptr<Logger> Noop();
106+
};
107+
108+
/// \brief Return the process-global default logger (never null).
109+
///
110+
/// Off the hot path -- acquires the slot lock and returns an owning copy. The
111+
/// logging macros use the cheaper internal hot-path accessor instead.
112+
ICEBERG_EXPORT std::shared_ptr<Logger> GetDefaultLogger();
113+
114+
/// \brief Install a new process-global default logger.
115+
///
116+
/// A null argument installs the no-op logger. Thread-safe; intended for
117+
/// occasional (configuration-time) use rather than the hot path.
118+
ICEBERG_EXPORT void SetDefaultLogger(std::shared_ptr<Logger> logger);
119+
120+
/// \brief Set the minimum level of the current default logger.
121+
///
122+
/// Updates both the logger and the lock-free fast-path gate atomically.
123+
ICEBERG_EXPORT void SetDefaultLevel(LogLevel level);
124+
125+
namespace detail {
126+
127+
/// \brief Lock-free read of the cached effective minimum level.
128+
///
129+
/// This is a non-authoritative lower-bound early-out: it may admit extra calls
130+
/// through to the authoritative Logger::ShouldLog, but never wrongly rejects.
131+
ICEBERG_EXPORT LogLevel EffectiveLevel() noexcept;
132+
133+
/// \brief Hot-path accessor for the default logger.
134+
///
135+
/// Returns a reference to a thread-local cached shared_ptr that is refreshed
136+
/// only when the default logger has changed (no lock / no refcount churn in
137+
/// steady state). The reference is valid for the duration of the calling
138+
/// statement.
139+
ICEBERG_EXPORT const std::shared_ptr<Logger>& CurrentLogger() noexcept;
140+
141+
} // namespace detail
142+
143+
} // namespace iceberg

src/iceberg/logging/meson.build

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# Generate the .cc-only logging backend config header. The meson build always
19+
# links spdlog, so ICEBERG_HAS_SPDLOG is always defined here. Generated into
20+
# build/src/iceberg/logging/config.h (resolved via include_directories('..'),
21+
# which exposes both the source and build trees); not installed.
22+
logging_config_data = configuration_data()
23+
logging_config_data.set('ICEBERG_HAS_SPDLOG', 1)
24+
configure_file(output: 'config.h', configuration: logging_config_data)
25+
26+
# Public logging headers. The build-generated config.h and the internal
27+
# SpdLogger header are intentionally NOT installed.
28+
install_headers(['log_level.h', 'logger.h'], subdir: 'iceberg/logging')

src/iceberg/meson.build

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ configure_file(
3838
install_dir: get_option('includedir') / 'iceberg',
3939
)
4040

41+
# Generate iceberg/logging/config.h (must precede the library() that compiles
42+
# the logging sources which include it).
43+
subdir('logging')
44+
4145
iceberg_include_dir = include_directories('..')
4246
iceberg_sources = files(
4347
'arrow_c_data_guard_internal.cc',
@@ -66,6 +70,7 @@ iceberg_sources = files(
6670
'inheritable_metadata.cc',
6771
'json_serde.cc',
6872
'location_provider.cc',
73+
'logging/logger.cc',
6974
'manifest/manifest_adapter.cc',
7075
'manifest/manifest_entry.cc',
7176
'manifest/manifest_filter_manager.cc',

src/iceberg/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ add_iceberg_test(table_test
101101
table_test.cc
102102
table_update_test.cc)
103103

104-
add_iceberg_test(logging_test SOURCES log_level_test.cc)
104+
add_iceberg_test(logging_test SOURCES log_level_test.cc logger_test.cc)
105105

106106
add_iceberg_test(expression_test
107107
SOURCES

0 commit comments

Comments
 (0)