Skip to content

Commit d828ab2

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 77702cc commit d828ab2

5 files changed

Lines changed: 347 additions & 1 deletion

File tree

src/iceberg/logging/logger.h

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,142 @@ std::string VFormat(std::string_view fmt, Args&&... args) {
169169
} // namespace detail
170170

171171
} // namespace iceberg
172+
173+
/// \brief Compile-time severity floor: statements below this level are removed
174+
/// entirely from the build (their format call sites and source_location literals
175+
/// are never emitted). Defaults to keeping everything. ICEBERG_LOG_FATAL is never
176+
/// gated by this floor -- its abort is always compiled in.
177+
#ifndef ICEBERG_LOG_ACTIVE_LEVEL
178+
# define ICEBERG_LOG_ACTIVE_LEVEL ::iceberg::LogLevel::kTrace
179+
#endif
180+
181+
// Internal: fixed-severity emit with compile-time floor then the authoritative
182+
// Logger::ShouldLog (the single source of truth for runtime filtering), with
183+
// formatting only on the taken path, never throwing.
184+
#define ICEBERG_INTERNAL_LOG(level_, FMT_, ...) \
185+
do { \
186+
if constexpr ((level_) >= ICEBERG_LOG_ACTIVE_LEVEL) { \
187+
const auto& _ib_logger = ::iceberg::detail::CurrentLogger(); \
188+
if (_ib_logger && _ib_logger->ShouldLog(level_)) { \
189+
try { \
190+
::iceberg::detail::Emit(*_ib_logger, (level_), \
191+
::std::source_location::current(), \
192+
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
193+
} catch (...) { \
194+
::iceberg::detail::EmitFormatError(*_ib_logger, (level_), \
195+
::std::source_location::current()); \
196+
} \
197+
} \
198+
} \
199+
} while (0)
200+
201+
#define ICEBERG_LOG_TRACE(...) \
202+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kTrace, __VA_ARGS__)
203+
#define ICEBERG_LOG_DEBUG(...) \
204+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kDebug, __VA_ARGS__)
205+
#define ICEBERG_LOG_INFO(...) \
206+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kInfo, __VA_ARGS__)
207+
#define ICEBERG_LOG_WARN(...) \
208+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kWarn, __VA_ARGS__)
209+
#define ICEBERG_LOG_ERROR(...) \
210+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kError, __VA_ARGS__)
211+
#define ICEBERG_LOG_CRITICAL(...) \
212+
ICEBERG_INTERNAL_LOG(::iceberg::LogLevel::kCritical, __VA_ARGS__)
213+
214+
// FATAL: emit if enabled (never compile-stripped), then ALWAYS flush + abort.
215+
// Acquires the default logger ONCE and uses the same instance for emit and flush
216+
// so a concurrent SetDefaultLogger cannot flush a different logger than it emitted to.
217+
#define ICEBERG_LOG_FATAL(FMT_, ...) \
218+
do { \
219+
auto _ib_logger = ::iceberg::GetDefaultLogger(); \
220+
if (_ib_logger && _ib_logger->ShouldLog(::iceberg::LogLevel::kFatal)) { \
221+
try { \
222+
::iceberg::detail::Emit(*_ib_logger, ::iceberg::LogLevel::kFatal, \
223+
::std::source_location::current(), \
224+
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
225+
} catch (...) { \
226+
::iceberg::detail::EmitFormatError(*_ib_logger, ::iceberg::LogLevel::kFatal, \
227+
::std::source_location::current()); \
228+
} \
229+
} \
230+
if (_ib_logger) _ib_logger->Flush(); \
231+
::std::abort(); \
232+
} while (0)
233+
234+
// Generic, runtime-level form against the default logger. No compile-time floor
235+
// (the level is not a constant). Acquires the logger once; aborts when level == kFatal
236+
// (flushing that same logger first).
237+
#define ICEBERG_LOG(level_, FMT_, ...) \
238+
do { \
239+
const ::iceberg::LogLevel _ib_lvl = (level_); \
240+
auto _ib_logger = ::iceberg::GetDefaultLogger(); \
241+
if (_ib_logger && _ib_logger->ShouldLog(_ib_lvl)) { \
242+
try { \
243+
::iceberg::detail::Emit(*_ib_logger, _ib_lvl, ::std::source_location::current(), \
244+
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
245+
} catch (...) { \
246+
::iceberg::detail::EmitFormatError(*_ib_logger, _ib_lvl, \
247+
::std::source_location::current()); \
248+
} \
249+
} \
250+
if (_ib_lvl == ::iceberg::LogLevel::kFatal) { \
251+
if (_ib_logger) _ib_logger->Flush(); \
252+
::std::abort(); \
253+
} \
254+
} while (0)
255+
256+
// Generic form targeting an EXPLICIT logger (must be an lvalue Logger&). Honors
257+
// only that logger's ShouldLog. Aborts when level == kFatal.
258+
#define ICEBERG_LOG_TO(logger_, level_, FMT_, ...) \
259+
do { \
260+
::iceberg::Logger& _ib_logger = (logger_); \
261+
const ::iceberg::LogLevel _ib_lvl = (level_); \
262+
if (_ib_logger.ShouldLog(_ib_lvl)) { \
263+
try { \
264+
::iceberg::detail::Emit(_ib_logger, _ib_lvl, ::std::source_location::current(), \
265+
::std::format(FMT_ __VA_OPT__(, ) __VA_ARGS__)); \
266+
} catch (...) { \
267+
::iceberg::detail::EmitFormatError(_ib_logger, _ib_lvl, \
268+
::std::source_location::current()); \
269+
} \
270+
} \
271+
if (_ib_lvl == ::iceberg::LogLevel::kFatal) { \
272+
_ib_logger.Flush(); \
273+
::std::abort(); \
274+
} \
275+
} while (0)
276+
277+
// Runtime (non-literal) format string against the default logger. Acquires the
278+
// logger once; aborts when level == kFatal (flushing that same logger first).
279+
#define ICEBERG_LOG_RUNTIME_FMT(level_, FMT_, ...) \
280+
do { \
281+
const ::iceberg::LogLevel _ib_lvl = (level_); \
282+
auto _ib_logger = ::iceberg::GetDefaultLogger(); \
283+
if (_ib_logger && _ib_logger->ShouldLog(_ib_lvl)) { \
284+
try { \
285+
::iceberg::detail::Emit( \
286+
*_ib_logger, _ib_lvl, ::std::source_location::current(), \
287+
::iceberg::detail::VFormat((FMT_)__VA_OPT__(, ) __VA_ARGS__)); \
288+
} catch (...) { \
289+
::iceberg::detail::EmitFormatError(*_ib_logger, _ib_lvl, \
290+
::std::source_location::current()); \
291+
} \
292+
} \
293+
if (_ib_lvl == ::iceberg::LogLevel::kFatal) { \
294+
if (_ib_logger) _ib_logger->Flush(); \
295+
::std::abort(); \
296+
} \
297+
} while (0)
298+
299+
// Bare, Java-style aliases. Opt-IN only (define ICEBERG_LOG_SHORT_MACROS before
300+
// including this header) to avoid colliding with glog/abseil/windows.h in
301+
// consumer translation units. No bare LOG(level) is provided.
302+
#ifdef ICEBERG_LOG_SHORT_MACROS
303+
# define LOG_TRACE(...) ICEBERG_LOG_TRACE(__VA_ARGS__)
304+
# define LOG_DEBUG(...) ICEBERG_LOG_DEBUG(__VA_ARGS__)
305+
# define LOG_INFO(...) ICEBERG_LOG_INFO(__VA_ARGS__)
306+
# define LOG_WARN(...) ICEBERG_LOG_WARN(__VA_ARGS__)
307+
# define LOG_ERROR(...) ICEBERG_LOG_ERROR(__VA_ARGS__)
308+
# define LOG_CRITICAL(...) ICEBERG_LOG_CRITICAL(__VA_ARGS__)
309+
# define LOG_FATAL(...) ICEBERG_LOG_FATAL(__VA_ARGS__)
310+
#endif // ICEBERG_LOG_SHORT_MACROS

src/iceberg/test/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ add_iceberg_test(logging_test
105105
SOURCES
106106
cerr_logger_test.cc
107107
log_level_test.cc
108-
logger_test.cc)
108+
logger_test.cc
109+
macros_active_level_test.cc
110+
macros_test.cc)
109111

110112
add_iceberg_test(expression_test
111113
SOURCES
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
auto counted = [&calls]() {
42+
++calls;
43+
return 1;
44+
};
45+
// Stripped at compile time -> arguments never evaluated, nothing emitted,
46+
// even though the runtime logger would accept these levels.
47+
ICEBERG_LOG_INFO("{}", counted());
48+
ICEBERG_LOG_CRITICAL("{}", counted());
49+
EXPECT_EQ(calls, 0);
50+
EXPECT_EQ(logger->count(), 0u);
51+
}
52+
53+
TEST(MacrosActiveLevelDeathTest, FatalStillAbortsWhenEverythingElseStripped) {
54+
EXPECT_DEATH({ ICEBERG_LOG_FATAL("still fatal"); }, "");
55+
}
56+
57+
} // namespace iceberg

src/iceberg/test/macros_test.cc

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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 <memory>
21+
22+
#include <gtest/gtest.h>
23+
24+
#include "iceberg/logging/cerr_logger.h"
25+
#include "iceberg/logging/log_level.h"
26+
#include "iceberg/logging/logger.h"
27+
#include "iceberg/test/logging_test_helpers.h"
28+
29+
namespace iceberg {
30+
31+
namespace {
32+
33+
std::shared_ptr<CapturingLogger> InstallCapturing(LogLevel level = LogLevel::kTrace) {
34+
auto logger = std::make_shared<CapturingLogger>();
35+
logger->SetLevel(level);
36+
return logger;
37+
}
38+
39+
} // namespace
40+
41+
TEST(MacrosTest, InfoFormatsAndCapturesLocation) {
42+
auto logger = InstallCapturing();
43+
ScopedDefaultLogger guard(logger);
44+
ICEBERG_LOG_INFO("x={}", 42);
45+
auto records = logger->records();
46+
ASSERT_EQ(records.size(), 1u);
47+
EXPECT_EQ(records[0].level, LogLevel::kInfo);
48+
EXPECT_EQ(records[0].message, "x=42");
49+
EXPECT_NE(records[0].location.line(), 0u);
50+
}
51+
52+
TEST(MacrosTest, RuntimeLevelFiltersBelowThreshold) {
53+
auto logger = InstallCapturing();
54+
ScopedDefaultLogger guard(logger);
55+
SetDefaultLevel(LogLevel::kError);
56+
ICEBERG_LOG_INFO("dropped");
57+
ICEBERG_LOG_ERROR("kept");
58+
auto records = logger->records();
59+
ASSERT_EQ(records.size(), 1u);
60+
EXPECT_EQ(records[0].message, "kept");
61+
}
62+
63+
TEST(MacrosTest, DisabledLevelDoesNotEvaluateArguments) {
64+
auto logger = InstallCapturing();
65+
ScopedDefaultLogger guard(logger);
66+
SetDefaultLevel(LogLevel::kError);
67+
int calls = 0;
68+
auto counted = [&calls]() {
69+
++calls;
70+
return 1;
71+
};
72+
ICEBERG_LOG_INFO("{}", counted());
73+
EXPECT_EQ(calls, 0);
74+
}
75+
76+
TEST(MacrosTest, DanglingElseBindsCorrectly) {
77+
auto logger = InstallCapturing();
78+
ScopedDefaultLogger guard(logger);
79+
bool took_else = false;
80+
if (false)
81+
ICEBERG_LOG_INFO("if-branch");
82+
else
83+
took_else = true;
84+
EXPECT_TRUE(took_else);
85+
EXPECT_EQ(logger->count(), 0u);
86+
}
87+
88+
TEST(MacrosTest, GenericRuntimeLevelMacroCompilesAndLogs) {
89+
auto logger = InstallCapturing();
90+
ScopedDefaultLogger guard(logger);
91+
LogLevel level = LogLevel::kWarn;
92+
ICEBERG_LOG(level, "n={}", 7);
93+
auto records = logger->records();
94+
ASSERT_EQ(records.size(), 1u);
95+
EXPECT_EQ(records[0].message, "n=7");
96+
EXPECT_EQ(records[0].level, LogLevel::kWarn);
97+
}
98+
99+
TEST(MacrosTest, LogToHonorsOnlyExplicitLoggerNotDefaultGate) {
100+
auto sink = InstallCapturing();
101+
ScopedDefaultLogger guard(InstallCapturing());
102+
SetDefaultLevel(LogLevel::kOff); // default gate would block everything
103+
ICEBERG_LOG_TO(*sink, LogLevel::kInfo, "explicit {}", 1);
104+
EXPECT_EQ(sink->count(), 1u);
105+
}
106+
107+
TEST(MacrosTest, NeverThrowsOnBadRuntimeFormat) {
108+
auto logger = InstallCapturing();
109+
ScopedDefaultLogger guard(logger);
110+
// Invalid runtime format string -> std::vformat throws -> swallowed -> fallback.
111+
EXPECT_NO_THROW(ICEBERG_LOG_RUNTIME_FMT(LogLevel::kInfo, "{"));
112+
auto records = logger->records();
113+
ASSERT_EQ(records.size(), 1u);
114+
EXPECT_EQ(records[0].message, "<fmt error>");
115+
}
116+
117+
TEST(MacrosDeathTest, FatalEmitsThenAborts) {
118+
// Default logger writes to std::cerr; the message must appear before abort.
119+
EXPECT_DEATH({ ICEBERG_LOG_FATAL("fatalmsg {}", 7); }, "fatalmsg 7");
120+
}
121+
122+
TEST(MacrosDeathTest, FatalAbortsEvenWhenRuntimeDisabled) {
123+
EXPECT_DEATH(
124+
{
125+
SetDefaultLevel(LogLevel::kOff);
126+
ICEBERG_LOG_FATAL("suppressed");
127+
},
128+
"");
129+
}
130+
131+
TEST(MacrosDeathTest, GenericRuntimeFatalEmitsThenAborts) {
132+
// ICEBERG_LOG with a runtime kFatal level must also emit then abort.
133+
EXPECT_DEATH({ ICEBERG_LOG(LogLevel::kFatal, "gfatal {}", 1); }, "gfatal 1");
134+
}
135+
136+
TEST(MacrosDeathTest, LogToFatalEmitsThenAborts) {
137+
// ICEBERG_LOG_TO with kFatal must emit to the explicit logger then abort.
138+
EXPECT_DEATH(
139+
{
140+
CerrLogger sink(LogLevel::kTrace);
141+
ICEBERG_LOG_TO(sink, LogLevel::kFatal, "tofatal {}", 2);
142+
},
143+
"tofatal 2");
144+
}
145+
146+
} // namespace iceberg

src/iceberg/test/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ iceberg_tests = {
6565
'cerr_logger_test.cc',
6666
'log_level_test.cc',
6767
'logger_test.cc',
68+
'macros_active_level_test.cc',
69+
'macros_test.cc',
6870
),
6971
},
7072
'expression_test': {

0 commit comments

Comments
 (0)