Skip to content

Commit a9878cb

Browse files
feat(logging): add LOG_* logging macros (4/6)
Fourth block: the application-facing macros, the only part most callers touch. - ICEBERG_LOG_{TRACE,DEBUG,INFO,WARN,ERROR,CRITICAL,FATAL} plus the generic ICEBERG_LOG(level, ...), ICEBERG_LOG_TO(logger, level, ...) for an explicit logger, and ICEBERG_LOG_RUNTIME_FMT for a runtime (non-literal) format string. - ICEBERG_LOG_ACTIVE_LEVEL is a compile-time severity floor: statements below it are removed entirely via `if constexpr` (no format call site, no source location emitted). ICEBERG_LOG_FATAL is never gated by the floor -- its abort is always compiled in; it emits, best-effort Flush()es the same logger it emitted to, then std::abort(). - Filtering is decided solely by Logger::ShouldLog(); formatting is wrapped in try/catch so logging never throws (a format failure routes to EmitFormatError). - Bare Java-style aliases (LOG_INFO, ...) are opt-in via ICEBERG_LOG_SHORT_MACROS to avoid polluting consumers / colliding with glog/abseil. Header-only addition to logger.h. macros_test covers injection, the guard-before-format short-circuit, never-throws, and FATAL aborts; macros_active_level_test verifies compile-time stripping in a kOff translation unit. Co-authored-by: Isaac
1 parent 411c0f8 commit a9878cb

6 files changed

Lines changed: 397 additions & 3 deletions

File tree

meson.build

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ project(
3131
)
3232

3333
cpp = meson.get_compiler('cpp')
34-
args = cpp.get_supported_arguments(['/bigobj'])
34+
# /Zc:preprocessor: MSVC's conforming preprocessor, required for the __VA_OPT__
35+
# used by the logging macros. get_supported_arguments drops it on non-MSVC.
36+
args = cpp.get_supported_arguments(['/bigobj', '/Zc:preprocessor'])
3537
add_project_arguments(args, language: 'cpp')
3638

3739
subdir('src')

src/iceberg/logging/logger.h

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,179 @@ void Log(Logger& logger, LogLevel level,
371371
}
372372

373373
} // namespace iceberg
374+
375+
// ---------------------------------------------------------------------------
376+
// Logging macros.
377+
//
378+
// Every macro takes a std::format string followed by its arguments. The
379+
// rendered line depends on the active backend (see cerr_logger.h for the
380+
// std::cerr layout, or the spdlog pattern); the examples below show the call
381+
// site and, for the default CerrLogger, the line it produces.
382+
//
383+
// ICEBERG_LOG_TRACE("entering scan for {}", table);
384+
// 2026-06-16T10:59:41.186Z trace [12345] table_scan.cc:88] entering scan for db.t
385+
// ICEBERG_LOG_DEBUG("cache miss key={}", key);
386+
// 2026-06-16T10:59:41.186Z debug [12345] cache.cc:42] cache miss key=manifest-7
387+
// ICEBERG_LOG_INFO("loaded {} manifests in {} ms", n, ms);
388+
// 2026-06-16T10:59:41.186Z info [12345] table_scan.cc:91] loaded 5 manifests in 12 ms
389+
// ICEBERG_LOG_WARN("retry {} after {}", attempt, err);
390+
// 2026-06-16T10:59:41.186Z warn [12345] io.cc:51] retry 2 after timeout
391+
// ICEBERG_LOG_ERROR("commit failed: {}", status);
392+
// 2026-06-16T10:59:41.186Z error [12345] txn.cc:77] commit failed: conflict
393+
// ICEBERG_LOG_CRITICAL("metadata unreadable at {}", path);
394+
// 2026-06-16T10:59:41.186Z critical [12345] meta.cc:30] metadata unreadable at
395+
// s3://b/m.json
396+
// ICEBERG_LOG_FATAL("unrecoverable: {}", reason); // emits, flushes, then
397+
// std::abort()
398+
// 2026-06-16T10:59:41.186Z fatal [12345] boot.cc:19] unrecoverable: bad config
399+
//
400+
// Less common forms:
401+
// ICEBERG_LOG(level, "level chosen at runtime: {}", x); // runtime severity
402+
// ICEBERG_LOG_TO(logger, level, "to an explicit logger {}", y);
403+
// ICEBERG_LOG_RUNTIME_FMT(level, fmt_string, args...); // non-literal format
404+
//
405+
// With ICEBERG_LOG_SHORT_MACROS defined, bare aliases (LOG_INFO, ...) are also
406+
// available. A format string is mandatory; zero extra args is fine
407+
// (ICEBERG_LOG_INFO("done")).
408+
// ---------------------------------------------------------------------------
409+
410+
/// \brief Compile-time severity floor: statements below this level are removed
411+
/// entirely from the build (their format call sites and source_location literals
412+
/// are never emitted). Defaults to keeping everything. ICEBERG_LOG_FATAL is never
413+
/// gated by this floor -- its abort is always compiled in.
414+
#ifndef ICEBERG_LOG_ACTIVE_LEVEL
415+
# define ICEBERG_LOG_ACTIVE_LEVEL ::iceberg::LogLevel::kTrace
416+
#endif
417+
418+
// Internal: fixed-severity emit with compile-time floor then the authoritative
419+
// Logger::ShouldLog (the single source of truth for runtime filtering), with
420+
// formatting only on the taken path, never throwing.
421+
#define ICEBERG_INTERNAL_LOG(level_, FMT_, ...) \
422+
do { \
423+
if constexpr ((level_) >= ICEBERG_LOG_ACTIVE_LEVEL) { \
424+
const auto& _ib_logger = ::iceberg::internal::CurrentLogger(); \
425+
if (_ib_logger && _ib_logger->ShouldLog(level_)) { \
426+
try { \
427+
::iceberg::internal::Emit(*_ib_logger, (level_), \
428+
::std::source_location::current(), \
429+
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
430+
} catch (...) { \
431+
::iceberg::internal::EmitFormatError(*_ib_logger, (level_), \
432+
::std::source_location::current()); \
433+
} \
434+
} \
435+
} \
436+
} while (0)
437+
438+
#define ICEBERG_LOG_TRACE(...) \
439+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kTrace, __VA_ARGS__)
440+
#define ICEBERG_LOG_DEBUG(...) \
441+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kDebug, __VA_ARGS__)
442+
#define ICEBERG_LOG_INFO(...) \
443+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kInfo, __VA_ARGS__)
444+
#define ICEBERG_LOG_WARN(...) \
445+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kWarn, __VA_ARGS__)
446+
#define ICEBERG_LOG_ERROR(...) \
447+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kError, __VA_ARGS__)
448+
#define ICEBERG_LOG_CRITICAL(...) \
449+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kCritical, __VA_ARGS__)
450+
451+
// FATAL: emit if enabled (never compile-stripped), then ALWAYS flush + abort.
452+
// Acquires the default logger ONCE and uses the same instance for emit and flush
453+
// so a concurrent SetDefaultLogger cannot flush a different logger than it emitted to.
454+
#define ICEBERG_LOG_FATAL(FMT_, ...) \
455+
do { \
456+
auto _ib_logger = ::iceberg::GetDefaultLogger(); \
457+
if (_ib_logger && _ib_logger->ShouldLog(::iceberg::LogLevel::kFatal)) { \
458+
try { \
459+
::iceberg::internal::Emit(*_ib_logger, ::iceberg::LogLevel::kFatal, \
460+
::std::source_location::current(), \
461+
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
462+
} catch (...) { \
463+
::iceberg::internal::EmitFormatError(*_ib_logger, ::iceberg::LogLevel::kFatal, \
464+
::std::source_location::current()); \
465+
} \
466+
} \
467+
if (_ib_logger) _ib_logger->Flush(); \
468+
::std::abort(); \
469+
} while (0)
470+
471+
// Generic, runtime-level form against the default logger. No compile-time floor
472+
// (the level is not a constant). Acquires the logger once; aborts when level == kFatal
473+
// (flushing that same logger first).
474+
#define ICEBERG_LOG(level_, FMT_, ...) \
475+
do { \
476+
const ::iceberg::LogLevel _ib_lvl = (level_); \
477+
const auto& _ib_logger = ::iceberg::internal::CurrentLogger(); \
478+
if (_ib_logger && _ib_logger->ShouldLog(_ib_lvl)) { \
479+
try { \
480+
::iceberg::internal::Emit(*_ib_logger, _ib_lvl, \
481+
::std::source_location::current(), \
482+
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
483+
} catch (...) { \
484+
::iceberg::internal::EmitFormatError(*_ib_logger, _ib_lvl, \
485+
::std::source_location::current()); \
486+
} \
487+
} \
488+
if (_ib_lvl == ::iceberg::LogLevel::kFatal) { \
489+
if (_ib_logger) _ib_logger->Flush(); \
490+
::std::abort(); \
491+
} \
492+
} while (0)
493+
494+
// Generic form targeting an EXPLICIT logger (must be an lvalue Logger&). Honors
495+
// only that logger's ShouldLog. Aborts when level == kFatal.
496+
#define ICEBERG_LOG_TO(logger_, level_, FMT_, ...) \
497+
do { \
498+
::iceberg::Logger& _ib_logger = (logger_); \
499+
const ::iceberg::LogLevel _ib_lvl = (level_); \
500+
if (_ib_logger.ShouldLog(_ib_lvl)) { \
501+
try { \
502+
::iceberg::internal::Emit(_ib_logger, _ib_lvl, \
503+
::std::source_location::current(), \
504+
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
505+
} catch (...) { \
506+
::iceberg::internal::EmitFormatError(_ib_logger, _ib_lvl, \
507+
::std::source_location::current()); \
508+
} \
509+
} \
510+
if (_ib_lvl == ::iceberg::LogLevel::kFatal) { \
511+
_ib_logger.Flush(); \
512+
::std::abort(); \
513+
} \
514+
} while (0)
515+
516+
// Runtime (non-literal) format string against the default logger. Acquires the
517+
// logger once; aborts when level == kFatal (flushing that same logger first).
518+
#define ICEBERG_LOG_RUNTIME_FMT(level_, FMT_, ...) \
519+
do { \
520+
const ::iceberg::LogLevel _ib_lvl = (level_); \
521+
const auto& _ib_logger = ::iceberg::internal::CurrentLogger(); \
522+
if (_ib_logger && _ib_logger->ShouldLog(_ib_lvl)) { \
523+
try { \
524+
::iceberg::internal::Emit( \
525+
*_ib_logger, _ib_lvl, ::std::source_location::current(), \
526+
::iceberg::internal::VFormat((FMT_)__VA_OPT__(, ) __VA_ARGS__)); \
527+
} catch (...) { \
528+
::iceberg::internal::EmitFormatError(*_ib_logger, _ib_lvl, \
529+
::std::source_location::current()); \
530+
} \
531+
} \
532+
if (_ib_lvl == ::iceberg::LogLevel::kFatal) { \
533+
if (_ib_logger) _ib_logger->Flush(); \
534+
::std::abort(); \
535+
} \
536+
} while (0)
537+
538+
// Bare, Java-style aliases. Opt-IN only (define ICEBERG_LOG_SHORT_MACROS before
539+
// including this header) to avoid colliding with glog/abseil/windows.h in
540+
// consumer translation units. No bare LOG(level) is provided.
541+
#ifdef ICEBERG_LOG_SHORT_MACROS
542+
# define LOG_TRACE(...) ICEBERG_LOG_TRACE(__VA_ARGS__)
543+
# define LOG_DEBUG(...) ICEBERG_LOG_DEBUG(__VA_ARGS__)
544+
# define LOG_INFO(...) ICEBERG_LOG_INFO(__VA_ARGS__)
545+
# define LOG_WARN(...) ICEBERG_LOG_WARN(__VA_ARGS__)
546+
# define LOG_ERROR(...) ICEBERG_LOG_ERROR(__VA_ARGS__)
547+
# define LOG_CRITICAL(...) ICEBERG_LOG_CRITICAL(__VA_ARGS__)
548+
# define LOG_FATAL(...) ICEBERG_LOG_FATAL(__VA_ARGS__)
549+
#endif // ICEBERG_LOG_SHORT_MACROS

src/iceberg/test/CMakeLists.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ function(add_iceberg_test test_name)
6464
endif()
6565

6666
if(MSVC_TOOLCHAIN)
67-
target_compile_options(${test_name} PRIVATE /bigobj)
67+
# /Zc:preprocessor: conforming preprocessor for the __VA_OPT__ in the logging
68+
# macros (MSVC's traditional preprocessor rejects it).
69+
target_compile_options(${test_name} PRIVATE /bigobj /Zc:preprocessor)
6870
endif()
6971

7072
add_test(NAME ${test_name} COMMAND ${test_name})
@@ -106,7 +108,9 @@ add_iceberg_test(logging_test
106108
SOURCES
107109
cerr_logger_test.cc
108110
log_level_test.cc
109-
logger_test.cc)
111+
logger_test.cc
112+
macros_active_level_test.cc
113+
macros_test.cc)
110114

111115
add_iceberg_test(expression_test
112116
SOURCES
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
// Compile-time floor set to kOff for this translation unit: every fixed-severity
21+
// macro below kFatal must be stripped to nothing, while ICEBERG_LOG_FATAL must
22+
// still abort (its abort is never gated by the compile-time floor).
23+
#define ICEBERG_LOG_ACTIVE_LEVEL ::iceberg::LogLevel::kOff
24+
25+
#include <memory>
26+
27+
#include <gtest/gtest.h>
28+
29+
#include "iceberg/logging/log_level.h"
30+
#include "iceberg/logging/logger.h"
31+
#include "iceberg/test/logging_test_helpers.h"
32+
33+
namespace iceberg {
34+
35+
TEST(MacrosActiveLevelTest, BelowFloorStatementsAreCompiledOut) {
36+
auto logger = std::make_shared<CapturingLogger>();
37+
logger->SetLevel(LogLevel::kTrace);
38+
ScopedDefaultLogger guard(logger);
39+
40+
int calls = 0;
41+
// counted() is only "called" from the compile-time-stripped macros below, so the
42+
// analyzer sees its init as a dead store -- which is exactly what this verifies.
43+
// NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
44+
auto counted = [&calls]() {
45+
++calls;
46+
return 1;
47+
};
48+
// Stripped at compile time -> arguments never evaluated, nothing emitted,
49+
// even though the runtime logger would accept these levels.
50+
ICEBERG_LOG_INFO("{}", counted());
51+
ICEBERG_LOG_CRITICAL("{}", counted());
52+
EXPECT_EQ(calls, 0);
53+
EXPECT_EQ(logger->count(), 0u);
54+
}
55+
56+
TEST(MacrosActiveLevelDeathTest, FatalStillAbortsWhenEverythingElseStripped) {
57+
EXPECT_DEATH({ ICEBERG_LOG_FATAL("still fatal"); }, "");
58+
}
59+
60+
} // namespace iceberg

0 commit comments

Comments
 (0)