Skip to content

Commit 722db19

Browse files
feat(logging): add registerable FatalHandler before abort (apache#725 review ⑤)
Address the extensibility comment: let an embedder (JNI, Python host, crash reporter) run a hook on the fatal path before std::abort() to flush resources or print a stack trace. - Add FatalHandler = std::function<void(const std::source_location&, std::string_view)> plus thread-safe SetFatalHandler/GetFatalHandler (leaked, teardown-safe slot in logger.cc). - Wire it into LogFatal: format the message once, emit-if-enabled + flush, run the handler (message passed even when the record is filtered out), then abort. A throwing handler cannot prevent the abort. - Death tests: handler runs with the formatted message, receives the call-site location, and still fires when the record is suppressed. Co-authored-by: Isaac
1 parent 8f7c826 commit 722db19

4 files changed

Lines changed: 114 additions & 3 deletions

File tree

src/iceberg/logging/log_macros.h

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,35 @@ void LogToExplicitRuntime(Logger& logger, LogLevel level,
108108
}
109109

110110
/// \brief Fatal path: acquire the effective (scoped-or-default) logger ONCE, emit
111-
/// if enabled, flush that same logger, then abort. Never returns.
111+
/// if enabled, flush that same logger, run any registered FatalHandler, then
112+
/// abort. Never returns.
113+
///
114+
/// The message is always formatted here (independent of ShouldLog) so the handler
115+
/// receives it even when the fatal record itself is filtered out. The handler runs
116+
/// after emit+flush and before abort; if it does not itself terminate the process,
117+
/// std::abort() still runs.
112118
template <typename MakeMessage>
113119
[[noreturn]] void LogFatal(const std::source_location& location,
114120
MakeMessage&& make_message) noexcept {
121+
std::string message;
122+
try {
123+
message = std::forward<MakeMessage>(make_message)();
124+
} catch (...) {
125+
message = "<fmt error>";
126+
}
115127
auto logger = GetCurrentLogger();
116128
if (logger) {
117-
EmitIfEnabled(*logger, LogLevel::kFatal, location,
118-
std::forward<MakeMessage>(make_message));
129+
if (logger->ShouldLog(LogLevel::kFatal)) {
130+
Emit(*logger, LogLevel::kFatal, location, std::string(message));
131+
}
119132
logger->Flush();
120133
}
134+
if (auto handler = GetFatalHandler()) {
135+
try {
136+
handler(location, message);
137+
} catch (...) { // a throwing handler must not prevent the abort
138+
}
139+
}
121140
std::abort();
122141
}
123142

src/iceberg/logging/logger.cc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,35 @@ void SetDefaultLevel(LogLevel level) {
106106
slot.logger->SetLevel(level);
107107
}
108108

109+
namespace {
110+
111+
/// \brief Immortal (leaked, hence teardown-safe) home for the fatal handler and
112+
/// its mutex. Leaked like Slot() so GetFatalHandler() is valid even during static
113+
/// teardown / from the fatal path at any time.
114+
struct FatalHandlerSlot {
115+
std::mutex mtx;
116+
FatalHandler handler;
117+
};
118+
119+
FatalHandlerSlot& FatalSlot() {
120+
static auto* slot = new FatalHandlerSlot();
121+
return *slot;
122+
}
123+
124+
} // namespace
125+
126+
void SetFatalHandler(FatalHandler handler) {
127+
FatalHandlerSlot& slot = FatalSlot();
128+
std::lock_guard<std::mutex> lock(slot.mtx);
129+
slot.handler = std::move(handler);
130+
}
131+
132+
FatalHandler GetFatalHandler() {
133+
FatalHandlerSlot& slot = FatalSlot();
134+
std::lock_guard<std::mutex> lock(slot.mtx);
135+
return slot.handler; // copy under lock; safe to invoke without holding the lock
136+
}
137+
109138
namespace internal {
110139

111140
namespace {

src/iceberg/logging/logger.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <concepts>
3030
#include <cstdlib>
3131
#include <format>
32+
#include <functional>
3233
#include <memory>
3334
#include <source_location>
3435
#include <string>
@@ -211,6 +212,26 @@ ICEBERG_EXPORT void SetDefaultLogger(std::shared_ptr<Logger> logger);
211212
/// means (this, SetLevel on a held handle, or Initialize) takes effect immediately.
212213
ICEBERG_EXPORT void SetDefaultLevel(LogLevel level);
213214

215+
/// \brief A hook invoked on the fatal-log path just before std::abort().
216+
///
217+
/// Receives the fatal record's source location and already-formatted message.
218+
/// Runs after the record has been emitted and the logger flushed, so an embedder
219+
/// (JNI, a Python host, a crash reporter) can print a stack trace, flush its own
220+
/// resources, or translate the abort. It must not return normally on the
221+
/// expectation of cancelling the abort -- ICEBERG_LOG_FATAL always terminates; if
222+
/// the handler itself does not exit the process, std::abort() still runs after it.
223+
using FatalHandler =
224+
std::function<void(const std::source_location&, std::string_view message)>;
225+
226+
/// \brief Install (or clear, with nullptr) the process-global fatal handler.
227+
///
228+
/// Thread-safe. Intended to be set once at startup. Replaces any previous handler.
229+
ICEBERG_EXPORT void SetFatalHandler(FatalHandler handler);
230+
231+
/// \brief Return the installed fatal handler (empty if none). Used by the fatal
232+
/// logging path; thread-safe.
233+
ICEBERG_EXPORT FatalHandler GetFatalHandler();
234+
214235
/// \brief Bind a logger for the current thread until this object leaves scope.
215236
///
216237
/// The default logging path on this thread -- CurrentLogger(), Log(level, ...),

src/iceberg/test/macros_test.cc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
* under the License.
1818
*/
1919

20+
#include <iostream>
2021
#include <memory>
22+
#include <source_location>
23+
#include <string_view>
2124

2225
#include <gtest/gtest.h>
2326

@@ -162,4 +165,43 @@ TEST(MacrosDeathTest, FatalRoutesThroughScopedLogger) {
162165
"scopedfatal 9");
163166
}
164167

168+
// A registered FatalHandler runs on the fatal path (after emit+flush) and receives
169+
// the formatted message; the process still aborts afterwards.
170+
TEST(MacrosDeathTest, FatalHandlerRunsWithFormattedMessageBeforeAbort) {
171+
EXPECT_DEATH(
172+
{
173+
SetFatalHandler([](const std::source_location&, std::string_view message) {
174+
std::cerr << "HANDLER[" << message << "]\n";
175+
});
176+
ICEBERG_LOG_FATAL("boom {}", 42);
177+
},
178+
"HANDLER\\[boom 42\\]");
179+
}
180+
181+
// The handler receives the caller's source location.
182+
TEST(MacrosDeathTest, FatalHandlerReceivesCallSiteLocation) {
183+
EXPECT_DEATH(
184+
{
185+
SetFatalHandler([](const std::source_location& loc, std::string_view) {
186+
std::cerr << "LOC:" << (loc.line() > 0 ? "ok" : "bad") << "\n";
187+
});
188+
ICEBERG_LOG_FATAL("x");
189+
},
190+
"LOC:ok");
191+
}
192+
193+
// The handler is a termination hook independent of log filtering: it still fires
194+
// (with the formatted message) when the fatal record itself is suppressed.
195+
TEST(MacrosDeathTest, FatalHandlerRunsEvenWhenRecordSuppressed) {
196+
EXPECT_DEATH(
197+
{
198+
SetDefaultLevel(LogLevel::kOff);
199+
SetFatalHandler([](const std::source_location&, std::string_view message) {
200+
std::cerr << "H[" << message << "]\n";
201+
});
202+
ICEBERG_LOG_FATAL("suppressed {}", 7);
203+
},
204+
"H\\[suppressed 7\\]");
205+
}
206+
165207
} // namespace iceberg

0 commit comments

Comments
 (0)