Skip to content

Commit 8f7c826

Browse files
refactor(logging): split macros into log_macros.h + short_log_macros.h
Adopt the structure from the apache#824 prototype (keeping our if constexpr gating): - logger.h is now the C++ API only; ICEBERG_LOG_* macros move to log_macros.h, and the bare LOG_* aliases move behind an opt-in short_log_macros.h (replacing the ICEBERG_LOG_SHORT_MACROS define). - Dedup the five macro bodies onto shared internal helpers (EmitIfEnabled + LogToCurrent / LogToCurrentRuntime / LogToExplicitRuntime / LogFatal) that take a lazy [&]()->std::string message thunk invoked only past ShouldLog, so disabled logs still don't evaluate their args. Keeps catch(...), the GetCurrentLogger() fatal path, and if constexpr compile-time floor. - VFormat moves to log_macros.h (only ICEBERG_LOG_RUNTIME_FMT uses it). Co-authored-by: Isaac
1 parent e549ec2 commit 8f7c826

6 files changed

Lines changed: 287 additions & 207 deletions

File tree

src/iceberg/logging/log_macros.h

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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/log_macros.h
23+
/// \brief Iceberg-prefixed logging macros (ICEBERG_LOG_*).
24+
///
25+
/// Kept out of logger.h so consumers of the C++ logging API (Logger, Log(),
26+
/// ScopedLogger) are not forced to pull in these macros or the conforming
27+
/// preprocessor they require. Include this header to use the macros; include
28+
/// short_log_macros.h for the bare LOG_* aliases.
29+
30+
#include <cstdlib>
31+
#include <format>
32+
#include <memory>
33+
#include <source_location>
34+
#include <string>
35+
#include <string_view>
36+
#include <utility>
37+
38+
#include "iceberg/logging/log_level.h"
39+
#include "iceberg/logging/logger.h"
40+
41+
namespace iceberg::internal {
42+
43+
/// \brief Runtime (non-literal) format-string helper for ICEBERG_LOG_RUNTIME_FMT.
44+
///
45+
/// std::format requires a compile-time format string; this routes a runtime
46+
/// string through std::vformat. Args are bound as named lvalues and the
47+
/// arg-store is held in a named variable so it outlives the vformat call
48+
/// (C++23 make_format_args rejects rvalues -- P2905 / LWG3631).
49+
template <typename... Args>
50+
std::string VFormat(std::string_view fmt, Args&&... args) {
51+
auto store = std::make_format_args(args...);
52+
return std::vformat(fmt, store);
53+
}
54+
55+
/// \brief Gate on \p logger.ShouldLog, then format (via \p make_message) and emit.
56+
///
57+
/// \p make_message is a callable returning the formatted std::string; it is
58+
/// invoked only after the level passes ShouldLog, so a disabled log never
59+
/// evaluates its format arguments. Never throws: a std::formatter that throws
60+
/// (any type) routes to EmitFormatError, so the noexcept logging contract holds.
61+
template <typename MakeMessage>
62+
void EmitIfEnabled(Logger& logger, LogLevel level, const std::source_location& location,
63+
MakeMessage&& make_message) noexcept {
64+
if (!logger.ShouldLog(level)) return;
65+
try {
66+
Emit(logger, level, location, std::forward<MakeMessage>(make_message)());
67+
} catch (...) {
68+
EmitFormatError(logger, level, location);
69+
}
70+
}
71+
72+
/// \brief Emit to the current (scoped-or-default) logger if enabled.
73+
template <typename MakeMessage>
74+
void LogToCurrent(LogLevel level, const std::source_location& location,
75+
MakeMessage&& make_message) noexcept {
76+
const std::shared_ptr<Logger>& logger = CurrentLogger();
77+
if (logger) {
78+
EmitIfEnabled(*logger, level, location, std::forward<MakeMessage>(make_message));
79+
}
80+
}
81+
82+
/// \brief Runtime-level variant against the current logger: emit if enabled, then
83+
/// flush + abort when level == kFatal (using the same acquired logger).
84+
template <typename MakeMessage>
85+
void LogToCurrentRuntime(LogLevel level, const std::source_location& location,
86+
MakeMessage&& make_message) noexcept {
87+
const std::shared_ptr<Logger>& logger = CurrentLogger();
88+
if (logger) {
89+
EmitIfEnabled(*logger, level, location, std::forward<MakeMessage>(make_message));
90+
}
91+
if (level == LogLevel::kFatal) {
92+
if (logger) logger->Flush();
93+
std::abort();
94+
}
95+
}
96+
97+
/// \brief Runtime-level variant against an explicit logger: emit if enabled, then
98+
/// flush + abort when level == kFatal.
99+
template <typename MakeMessage>
100+
void LogToExplicitRuntime(Logger& logger, LogLevel level,
101+
const std::source_location& location,
102+
MakeMessage&& make_message) noexcept {
103+
EmitIfEnabled(logger, level, location, std::forward<MakeMessage>(make_message));
104+
if (level == LogLevel::kFatal) {
105+
logger.Flush();
106+
std::abort();
107+
}
108+
}
109+
110+
/// \brief Fatal path: acquire the effective (scoped-or-default) logger ONCE, emit
111+
/// if enabled, flush that same logger, then abort. Never returns.
112+
template <typename MakeMessage>
113+
[[noreturn]] void LogFatal(const std::source_location& location,
114+
MakeMessage&& make_message) noexcept {
115+
auto logger = GetCurrentLogger();
116+
if (logger) {
117+
EmitIfEnabled(*logger, LogLevel::kFatal, location,
118+
std::forward<MakeMessage>(make_message));
119+
logger->Flush();
120+
}
121+
std::abort();
122+
}
123+
124+
} // namespace iceberg::internal
125+
126+
// ---------------------------------------------------------------------------
127+
// Logging macros.
128+
//
129+
// Every macro takes a std::format string followed by its arguments. The
130+
// rendered line depends on the active backend (see cerr_logger.h for the
131+
// std::cerr layout, or the spdlog pattern); the examples below show the call
132+
// site and, for the default CerrLogger, the line it produces.
133+
//
134+
// ICEBERG_LOG_TRACE("entering scan for {}", table);
135+
// 2026-06-16T10:59:41.186Z trace [12345] [table_scan.cc:88] entering scan for db.t
136+
// ICEBERG_LOG_DEBUG("cache miss key={}", key);
137+
// 2026-06-16T10:59:41.186Z debug [12345] [cache.cc:42] cache miss key=manifest-7
138+
// ICEBERG_LOG_INFO("loaded {} manifests in {} ms", n, ms);
139+
// 2026-06-16T10:59:41.186Z info [12345] [table_scan.cc:91] loaded 5 manifests in 12
140+
// ms
141+
// ICEBERG_LOG_WARN("retry {} after {}", attempt, err);
142+
// 2026-06-16T10:59:41.186Z warn [12345] [io.cc:51] retry 2 after timeout
143+
// ICEBERG_LOG_ERROR("commit failed: {}", status);
144+
// 2026-06-16T10:59:41.186Z error [12345] [txn.cc:77] commit failed: conflict
145+
// ICEBERG_LOG_CRITICAL("metadata unreadable at {}", path);
146+
// 2026-06-16T10:59:41.186Z critical [12345] [meta.cc:30] metadata unreadable at
147+
// s3://b/m.json
148+
// ICEBERG_LOG_FATAL("unrecoverable: {}", reason); // emits, flushes, then
149+
// std::abort()
150+
// 2026-06-16T10:59:41.186Z fatal [12345] [boot.cc:19] unrecoverable: bad config
151+
//
152+
// Less common forms:
153+
// ICEBERG_LOG(level, "level chosen at runtime: {}", x); // runtime severity
154+
// ICEBERG_LOG_TO(logger, level, "to an explicit logger {}", y);
155+
// ICEBERG_LOG_RUNTIME_FMT(level, fmt_string, args...); // non-literal format
156+
//
157+
// Include short_log_macros.h for bare aliases (LOG_INFO, ...). A format string is
158+
// mandatory; zero extra args is fine (ICEBERG_LOG_INFO("done")).
159+
// ---------------------------------------------------------------------------
160+
161+
/// \brief Compile-time severity floor: statements below this level are discarded
162+
/// via `if constexpr`, so no emit code runs and no format call / source_location
163+
/// is generated for them (the compiler is free to optimize the dead branch away).
164+
/// The statement must still be well-formed -- a bad format string or a
165+
/// non-formattable argument is a compile error even when the branch is discarded.
166+
/// Defaults to keeping everything. ICEBERG_LOG_FATAL is never gated by this floor
167+
/// -- its abort is always compiled in.
168+
#ifndef ICEBERG_LOG_ACTIVE_LEVEL
169+
# define ICEBERG_LOG_ACTIVE_LEVEL ::iceberg::LogLevel::kTrace
170+
#endif
171+
172+
// A message-builder lambda that formats lazily (only invoked past ShouldLog by the
173+
// EmitIfEnabled helpers), so disabled logs never evaluate their arguments.
174+
#define ICEBERG_INTERNAL_LOG_MESSAGE(FMT_, ...) \
175+
[&]() -> ::std::string { return ::std::format((FMT_)__VA_OPT__(, ) __VA_ARGS__); }
176+
177+
// Fixed-severity emit with a compile-time floor (`if constexpr`) then the shared
178+
// current-logger path. Formatting happens only on the taken path and never throws.
179+
#define ICEBERG_INTERNAL_LOG(level_, FMT_, ...) \
180+
do { \
181+
if constexpr ((level_) >= ICEBERG_LOG_ACTIVE_LEVEL) { \
182+
::iceberg::internal::LogToCurrent( \
183+
(level_), ::std::source_location::current(), \
184+
ICEBERG_INTERNAL_LOG_MESSAGE(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
185+
} \
186+
} while (0)
187+
188+
#define ICEBERG_LOG_TRACE(...) \
189+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kTrace, __VA_ARGS__)
190+
#define ICEBERG_LOG_DEBUG(...) \
191+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kDebug, __VA_ARGS__)
192+
#define ICEBERG_LOG_INFO(...) \
193+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kInfo, __VA_ARGS__)
194+
#define ICEBERG_LOG_WARN(...) \
195+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kWarn, __VA_ARGS__)
196+
#define ICEBERG_LOG_ERROR(...) \
197+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kError, __VA_ARGS__)
198+
#define ICEBERG_LOG_CRITICAL(...) \
199+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kCritical, __VA_ARGS__)
200+
201+
// FATAL: emit if enabled (never compile-stripped), then ALWAYS flush + abort.
202+
// Acquires the effective (scoped-or-default) logger ONCE so a concurrent
203+
// SetDefaultLogger cannot flush a different logger than it emitted to.
204+
#define ICEBERG_LOG_FATAL(FMT_, ...) \
205+
::iceberg::internal::LogFatal( \
206+
::std::source_location::current(), \
207+
ICEBERG_INTERNAL_LOG_MESSAGE(FMT_ __VA_OPT__(, ) __VA_ARGS__))
208+
209+
// Generic, runtime-level form against the default logger. No compile-time floor
210+
// (the level is not a constant). Aborts when level == kFatal.
211+
#define ICEBERG_LOG(level_, FMT_, ...) \
212+
::iceberg::internal::LogToCurrentRuntime( \
213+
(level_), ::std::source_location::current(), \
214+
ICEBERG_INTERNAL_LOG_MESSAGE(FMT_ __VA_OPT__(, ) __VA_ARGS__))
215+
216+
// Generic form targeting an EXPLICIT logger (must be an lvalue Logger&). Honors
217+
// only that logger's ShouldLog. Aborts when level == kFatal.
218+
#define ICEBERG_LOG_TO(logger_, level_, FMT_, ...) \
219+
::iceberg::internal::LogToExplicitRuntime( \
220+
(logger_), (level_), ::std::source_location::current(), \
221+
ICEBERG_INTERNAL_LOG_MESSAGE(FMT_ __VA_OPT__(, ) __VA_ARGS__))
222+
223+
// Runtime (non-literal) format string against the default logger. Aborts when
224+
// level == kFatal.
225+
#define ICEBERG_LOG_RUNTIME_FMT(level_, FMT_, ...) \
226+
::iceberg::internal::LogToCurrentRuntime( \
227+
(level_), ::std::source_location::current(), [&]() -> ::std::string { \
228+
return ::iceberg::internal::VFormat((FMT_)__VA_OPT__(, ) __VA_ARGS__); \
229+
})

0 commit comments

Comments
 (0)