Skip to content

Commit 6829cfc

Browse files
fix(logging): address #725 review (catch-all, /Zc:preprocessor, docs, FATAL test)
- Revert the 6 catch sites to catch(...) so the noexcept "logging never throws" guarantee holds even when a user-defined std::formatter throws a non-std type (catch(const std::exception&) would let it reach std::terminate). - Apply /Zc:preprocessor to the iceberg library targets PUBLICly on MSVC, not only to tests: logger.h is public and uses __VA_OPT__, so the library build and consumers need the conforming preprocessor too. - Soften the compile-time-floor doc: `if constexpr` discards the emit but the statement must still be well-formed (bad format string is still a compile error). - Add the missing opening '[' to the logger.h example output lines to match CerrLogger's [file:line] layout. - Add MacrosDeathTest.FatalRoutesThroughScopedLogger locking in that ICEBERG_LOG_FATAL routes through the active ScopedLogger (GetCurrentLogger). Co-authored-by: Isaac
1 parent d7754cb commit 6829cfc

3 files changed

Lines changed: 53 additions & 24 deletions

File tree

src/iceberg/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ add_iceberg_lib(iceberg
186186
OUTPUTS
187187
ICEBERG_LIBRARIES)
188188

189+
# The logging macros in the public logger.h use __VA_OPT__, which MSVC's
190+
# traditional preprocessor rejects. Require the conforming preprocessor PUBLICly
191+
# so both these library targets (logger.cc includes logger.h) and downstream
192+
# consumers of the installed header get it on MSVC.
193+
if(MSVC_TOOLCHAIN)
194+
foreach(_iceberg_lib iceberg_shared iceberg_static)
195+
if(TARGET ${_iceberg_lib})
196+
target_compile_options(${_iceberg_lib} PUBLIC /Zc:preprocessor)
197+
endif()
198+
endforeach()
199+
endif()
200+
189201
set(ICEBERG_DATA_SOURCES
190202
data/data_writer.cc
191203
data/delete_filter.cc

src/iceberg/logging/logger.h

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
#include <concepts>
3030
#include <cstdlib>
31-
#include <exception>
3231
#include <format>
3332
#include <memory>
3433
#include <source_location>
@@ -340,10 +339,10 @@ void FormatAndEmit(Logger& logger, LogLevel level, const std::source_location& l
340339
if (!logger.ShouldLog(level)) return;
341340
try {
342341
Emit(logger, level, loc, std::format(fmt, std::forward<Args>(args)...));
343-
} catch (const std::exception&) {
344-
// The only throws here are std::format_error / std::bad_alloc -- same recovery
345-
// for both (drop, emit a fixed marker). Catch std::exception, not (...), so a
346-
// non-std unwind like abi::__forced_unwind (thread cancellation) propagates.
342+
} catch (...) {
343+
// Catch-all upholds the noexcept "logging never throws" guarantee: a
344+
// user-defined std::formatter may throw a non-std::exception type, and this
345+
// function is noexcept, so anything escaping here would call std::terminate.
347346
EmitFormatError(logger, level, loc);
348347
}
349348
}
@@ -385,21 +384,22 @@ void Log(Logger& logger, LogLevel level,
385384
// site and, for the default CerrLogger, the line it produces.
386385
//
387386
// ICEBERG_LOG_TRACE("entering scan for {}", table);
388-
// 2026-06-16T10:59:41.186Z trace [12345] table_scan.cc:88] entering scan for db.t
387+
// 2026-06-16T10:59:41.186Z trace [12345] [table_scan.cc:88] entering scan for db.t
389388
// ICEBERG_LOG_DEBUG("cache miss key={}", key);
390-
// 2026-06-16T10:59:41.186Z debug [12345] cache.cc:42] cache miss key=manifest-7
389+
// 2026-06-16T10:59:41.186Z debug [12345] [cache.cc:42] cache miss key=manifest-7
391390
// ICEBERG_LOG_INFO("loaded {} manifests in {} ms", n, ms);
392-
// 2026-06-16T10:59:41.186Z info [12345] table_scan.cc:91] loaded 5 manifests in 12 ms
391+
// 2026-06-16T10:59:41.186Z info [12345] [table_scan.cc:91] loaded 5 manifests in 12
392+
// ms
393393
// ICEBERG_LOG_WARN("retry {} after {}", attempt, err);
394-
// 2026-06-16T10:59:41.186Z warn [12345] io.cc:51] retry 2 after timeout
394+
// 2026-06-16T10:59:41.186Z warn [12345] [io.cc:51] retry 2 after timeout
395395
// ICEBERG_LOG_ERROR("commit failed: {}", status);
396-
// 2026-06-16T10:59:41.186Z error [12345] txn.cc:77] commit failed: conflict
396+
// 2026-06-16T10:59:41.186Z error [12345] [txn.cc:77] commit failed: conflict
397397
// ICEBERG_LOG_CRITICAL("metadata unreadable at {}", path);
398-
// 2026-06-16T10:59:41.186Z critical [12345] meta.cc:30] metadata unreadable at
398+
// 2026-06-16T10:59:41.186Z critical [12345] [meta.cc:30] metadata unreadable at
399399
// s3://b/m.json
400400
// ICEBERG_LOG_FATAL("unrecoverable: {}", reason); // emits, flushes, then
401401
// std::abort()
402-
// 2026-06-16T10:59:41.186Z fatal [12345] boot.cc:19] unrecoverable: bad config
402+
// 2026-06-16T10:59:41.186Z fatal [12345] [boot.cc:19] unrecoverable: bad config
403403
//
404404
// Less common forms:
405405
// ICEBERG_LOG(level, "level chosen at runtime: {}", x); // runtime severity
@@ -411,19 +411,22 @@ void Log(Logger& logger, LogLevel level,
411411
// (ICEBERG_LOG_INFO("done")).
412412
// ---------------------------------------------------------------------------
413413

414-
/// \brief Compile-time severity floor: statements below this level are removed
415-
/// entirely from the build (their format call sites and source_location literals
416-
/// are never emitted). Defaults to keeping everything. ICEBERG_LOG_FATAL is never
417-
/// gated by this floor -- its abort is always compiled in.
414+
/// \brief Compile-time severity floor: statements below this level are discarded
415+
/// via `if constexpr`, so no emit code runs and no format call / source_location
416+
/// is generated for them (the compiler is free to optimize the dead branch away).
417+
/// The statement must still be well-formed -- a bad format string or a
418+
/// non-formattable argument is a compile error even when the branch is discarded.
419+
/// Defaults to keeping everything. ICEBERG_LOG_FATAL is never gated by this floor
420+
/// -- its abort is always compiled in.
418421
#ifndef ICEBERG_LOG_ACTIVE_LEVEL
419422
# define ICEBERG_LOG_ACTIVE_LEVEL ::iceberg::LogLevel::kTrace
420423
#endif
421424

422425
// Internal: fixed-severity emit with compile-time floor then the authoritative
423426
// Logger::ShouldLog (the single source of truth for runtime filtering), with
424-
// formatting only on the taken path, never throwing. The catch handles
425-
// std::exception (std::format_error / std::bad_alloc) -- not (...) -- so a non-std
426-
// unwind such as abi::__forced_unwind (thread cancellation) still propagates.
427+
// formatting only on the taken path, never throwing. The catch-all upholds the
428+
// "logging never throws" guarantee even when a user-defined std::formatter throws
429+
// a non-std::exception type (a std::format_error/bad_alloc route to EmitFormatError).
427430
#define ICEBERG_INTERNAL_LOG(level_, FMT_, ...) \
428431
do { \
429432
if constexpr ((level_) >= ICEBERG_LOG_ACTIVE_LEVEL) { \
@@ -433,7 +436,7 @@ void Log(Logger& logger, LogLevel level,
433436
::iceberg::internal::Emit(*_ib_logger, (level_), \
434437
::std::source_location::current(), \
435438
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
436-
} catch (const std::exception&) { \
439+
} catch (...) { \
437440
::iceberg::internal::EmitFormatError(*_ib_logger, (level_), \
438441
::std::source_location::current()); \
439442
} \
@@ -466,7 +469,7 @@ void Log(Logger& logger, LogLevel level,
466469
::iceberg::internal::Emit(*_ib_logger, ::iceberg::LogLevel::kFatal, \
467470
::std::source_location::current(), \
468471
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
469-
} catch (const std::exception&) { \
472+
} catch (...) { \
470473
::iceberg::internal::EmitFormatError(*_ib_logger, ::iceberg::LogLevel::kFatal, \
471474
::std::source_location::current()); \
472475
} \
@@ -487,7 +490,7 @@ void Log(Logger& logger, LogLevel level,
487490
::iceberg::internal::Emit(*_ib_logger, _ib_lvl, \
488491
::std::source_location::current(), \
489492
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
490-
} catch (const std::exception&) { \
493+
} catch (...) { \
491494
::iceberg::internal::EmitFormatError(*_ib_logger, _ib_lvl, \
492495
::std::source_location::current()); \
493496
} \
@@ -509,7 +512,7 @@ void Log(Logger& logger, LogLevel level,
509512
::iceberg::internal::Emit(_ib_logger, _ib_lvl, \
510513
::std::source_location::current(), \
511514
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
512-
} catch (const std::exception&) { \
515+
} catch (...) { \
513516
::iceberg::internal::EmitFormatError(_ib_logger, _ib_lvl, \
514517
::std::source_location::current()); \
515518
} \
@@ -531,7 +534,7 @@ void Log(Logger& logger, LogLevel level,
531534
::iceberg::internal::Emit( \
532535
*_ib_logger, _ib_lvl, ::std::source_location::current(), \
533536
::iceberg::internal::VFormat((FMT_)__VA_OPT__(, ) __VA_ARGS__)); \
534-
} catch (const std::exception&) { \
537+
} catch (...) { \
535538
::iceberg::internal::EmitFormatError(*_ib_logger, _ib_lvl, \
536539
::std::source_location::current()); \
537540
} \

src/iceberg/test/macros_test.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,18 @@ TEST(MacrosDeathTest, LogToFatalEmitsThenAborts) {
147147
"tofatal 2");
148148
}
149149

150+
// Regression guard: ICEBERG_LOG_FATAL must route through the active ScopedLogger
151+
// binding (GetCurrentLogger), not the process default (GetDefaultLogger). A scoped
152+
// CerrLogger emits the record before the abort; the message must reach that sink.
153+
TEST(MacrosDeathTest, FatalRoutesThroughScopedLogger) {
154+
EXPECT_DEATH(
155+
{
156+
SetDefaultLevel(LogLevel::kOff); // default gate off -> only the scope emits
157+
auto scoped = std::make_shared<CerrLogger>(LogLevel::kTrace);
158+
ScopedLogger bind(scoped);
159+
ICEBERG_LOG_FATAL("scopedfatal {}", 9);
160+
},
161+
"scopedfatal 9");
162+
}
163+
150164
} // namespace iceberg

0 commit comments

Comments
 (0)