blob: 0a9647f66ca347695f9a491ebb57b22226273d94 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#pragma once
/// \file iceberg/logging/logger.h
/// \brief Pluggable logging interface and the process-global default logger.
///
/// This header is backend-agnostic: it never includes the build-generated
/// backend configuration header and never references the spdlog feature macro,
/// so consumers see one stable API regardless of how the backend was configured.
#include <concepts>
#include <cstdlib>
#include <format>
#include <functional>
#include <memory>
#include <source_location>
#include <string>
#include <string_view>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
#include "iceberg/iceberg_export.h"
#include "iceberg/logging/log_level.h"
#include "iceberg/result.h"
namespace iceberg {
/// \brief A structured key/value attribute attached to a log record.
///
/// Both key and value are owned so a sink may retain the record safely. Engine
/// loggers can surface these as discrete fields (query id, task id, table name,
/// snapshot id, file path, ...); see LogMessage::Builder to populate them.
struct ICEBERG_EXPORT LogAttribute {
std::string key;
std::string value;
};
/// \brief A single log record handed to a Logger.
///
/// The formatted message is owned (moved in when emitted), so a sink
/// may safely retain the record beyond the Log() call. The member set must not
/// depend on the build's logging backend (the spdlog backend never appears here).
/// Use LogMessage::Builder for a readable way to assemble one, especially with
/// structured attributes.
struct ICEBERG_EXPORT LogMessage {
LogLevel level = LogLevel::kOff;
std::string message;
std::source_location location = std::source_location::current();
std::vector<LogAttribute> attributes;
class Builder;
};
/// \brief Fluent builder for LogMessage, the easy path to attach structured
/// attributes.
///
/// Example:
/// auto record = LogMessage::Builder(LogLevel::kInfo)
/// .Message("scan finished")
/// .Attribute("table", table_name)
/// .Attribute("snapshot_id", std::to_string(id))
/// .Build();
/// logger->Log(std::move(record));
///
/// The location defaults to the caller's construction site (captured via the
/// constructor's default argument); override it with Location() (e.g. to forward
/// a caller's std::source_location).
class ICEBERG_EXPORT LogMessage::Builder {
public:
explicit Builder(LogLevel level,
std::source_location location = std::source_location::current())
: level_(level), location_(location) {}
/// \brief Set the already-formatted message text.
Builder& Message(std::string message) {
message_ = std::move(message);
return *this;
}
/// \brief Append a structured key/value attribute.
Builder& Attribute(std::string key, std::string value) {
attributes_.push_back(LogAttribute{.key = std::move(key), .value = std::move(value)});
return *this;
}
/// \brief Override the record's source location (defaults to the build site).
Builder& Location(std::source_location location) {
location_ = location;
return *this;
}
/// \brief Materialize the LogMessage, moving the accumulated state out.
LogMessage Build() {
return LogMessage{.level = level_,
.message = std::move(message_),
.location = location_,
.attributes = std::move(attributes_)};
}
private:
LogLevel level_;
std::string message_;
// `location_` is a trivially copyable members no need to move.
std::source_location location_;
std::vector<LogAttribute> attributes_;
};
/// \brief Well-known Logger::Initialize() property keys.
///
/// `level` is honored by the base Logger::Initialize (parsed via
/// LogLevelFromString) on every backend. `pattern` is honored only by the
/// spdlog backend; CerrLogger uses a fixed layout and ignores it.
inline constexpr std::string_view kLevelProperty = "level";
inline constexpr std::string_view kPatternProperty = "pattern";
/// \brief Pluggable logging sink.
///
/// ShouldLog() is the single authority for runtime filtering -- the logging path
/// calls it on every (compile-time-enabled) statement, so level changes by any
/// path take effect immediately. Implementations must be thread-safe and must not
/// throw. They must also obey:
/// - No reentrancy: Log()/Flush() must not call the logging API or
/// GetDefaultLogger() (UB -- deadlock with mutex-based sinks).
/// - level() is an accessor consistent with ShouldLog (used by SetDefaultLevel
/// and introspection); ShouldLog may implement finer logic than a level compare.
class ICEBERG_EXPORT Logger {
public:
virtual ~Logger() = default;
/// \brief Property-based setup, called by Loggers::Load() before first use.
///
/// The base implementation applies the "level" property (parsed via
/// LogLevelFromString); an unrecognized value is an InvalidArgument error.
/// The spdlog backend overrides this to also apply "pattern" and then delegates
/// to this base for "level"; CerrLogger uses the base as-is (fixed layout).
virtual Status Initialize(
const std::unordered_map<std::string, std::string>& properties) {
if (auto it = properties.find(std::string(kLevelProperty)); it != properties.end()) {
auto parsed = LogLevelFromString(it->second);
if (!parsed) return std::unexpected(parsed.error());
SetLevel(*parsed);
}
return {};
}
/// \brief Cheap check whether a record at \p level would be emitted.
virtual bool ShouldLog(LogLevel level) const noexcept = 0;
/// \brief Emit one (already-formatted) record, taking ownership. Must not throw.
virtual void Log(LogMessage&& message) noexcept = 0;
/// \brief Set the minimum level this logger emits.
virtual void SetLevel(LogLevel level) noexcept = 0;
/// \brief Return the minimum level this logger emits.
virtual LogLevel level() const noexcept = 0;
/// \brief Flush any buffered output. Must not throw; best-effort on the fatal path.
virtual void Flush() noexcept {}
/// \brief Return true if this logger is a no-op.
virtual bool IsNoop() const { return false; }
/// \brief Return a shared, immortal no-op logger singleton.
static std::shared_ptr<Logger> Noop();
};
/// \brief Return the process-global default logger (never null).
///
/// Off the hot path -- acquires the slot lock and returns an owning copy. The
/// logging path uses the cheaper internal hot-path accessor instead.
ICEBERG_EXPORT std::shared_ptr<Logger> GetDefaultLogger();
/// \brief Return the effective logger for this thread (never null): the active
/// ScopedLogger binding if any, else the global default.
///
/// Off the hot path -- returns an owning copy, e.g. to capture the current logger
/// and re-bind it on a worker thread (see ScopedLogger). During teardown, prefer
/// the Log(...) overloads over emitting through this handle.
ICEBERG_EXPORT std::shared_ptr<Logger> GetCurrentLogger();
/// \brief Install a new process-global default logger.
///
/// A null argument installs the no-op logger. Thread-safe; intended for
/// occasional (configuration-time) use rather than the hot path.
ICEBERG_EXPORT void SetDefaultLogger(std::shared_ptr<Logger> logger);
/// \brief Set the minimum level of the current default logger.
///
/// Convenience for `GetDefaultLogger()->SetLevel(level)`. Filtering is always
/// decided by the logger's own ShouldLog(), so changing a logger's level by any
/// means (this, SetLevel on a held handle, or Initialize) takes effect immediately.
ICEBERG_EXPORT void SetDefaultLevel(LogLevel level);
/// \brief A hook invoked on the fatal-log path just before std::abort().
///
/// Receives the fatal record's source location and already-formatted message.
/// Runs after the record has been emitted and the logger flushed, so an embedder
/// (JNI, a Python host, a crash reporter) can print a stack trace, flush its own
/// resources, or translate the abort. It must not return normally on the
/// expectation of cancelling the abort -- ICEBERG_LOG_FATAL always terminates; if
/// the handler itself does not exit the process, std::abort() still runs after it.
using FatalHandler =
std::function<void(const std::source_location&, std::string_view message)>;
/// \brief Install (or clear, with nullptr) the process-global fatal handler.
///
/// Thread-safe. Intended to be set once at startup. Replaces any previous handler.
ICEBERG_EXPORT void SetFatalHandler(FatalHandler handler);
/// \brief Return the installed fatal handler (empty if none). Used by the fatal
/// logging path; thread-safe.
ICEBERG_EXPORT FatalHandler GetFatalHandler();
/// \brief Bind a logger for the current thread until this object leaves scope.
///
/// The default logging path on this thread -- CurrentLogger(), Log(level, ...),
/// and the ICEBERG_LOG_* macros -- routes to \p logger instead of the global default;
/// explicit Log(logger, ...) is unaffected. Bindings nest and restore on exit, and
/// nullptr masks any enclosing binding back to the global default. Lets an engine
/// route Iceberg's own logs into a per catalog/session/query/task context with no
/// call-site changes.
///
/// \code
/// auto query_log = std::make_shared<MySink>();
/// iceberg::ScopedLogger bind(query_log); // this thread, this scope
/// iceberg::Log(LogLevel::kInfo, "scan {}", id); // -> query_log
/// \endcode
///
/// Stack-only and same-thread (non-copyable, non-movable). For thread pools,
/// capture on the submitting thread and re-bind on the worker:
/// \code
/// auto captured = iceberg::GetCurrentLogger();
/// pool.submit([captured, work] { iceberg::ScopedLogger bind(captured); work(); });
/// \endcode
class ICEBERG_EXPORT ScopedLogger {
public:
explicit ScopedLogger(std::shared_ptr<Logger> logger) noexcept;
~ScopedLogger();
ScopedLogger(const ScopedLogger&) = delete;
ScopedLogger& operator=(const ScopedLogger&) = delete;
ScopedLogger(ScopedLogger&&) = delete;
ScopedLogger& operator=(ScopedLogger&&) = delete;
private:
std::shared_ptr<Logger> previous_;
};
// ---------------------------------------------------------------------------
// Using the API directly (the ICEBERG_LOG_* macros that wrap this live in
// log_macros.h). Example: a custom sink, installed as the process default.
//
// class MySink : public Logger {
// public:
// bool ShouldLog(LogLevel level) const noexcept override { return level >= level_; }
// void Log(LogMessage&& m) noexcept override { write_line(m.message); }
// void SetLevel(LogLevel level) noexcept override { level_ = level; }
// LogLevel level() const noexcept override { return level_; }
// private:
// std::atomic<LogLevel> level_{LogLevel::kInfo};
// };
//
// SetDefaultLogger(std::make_shared<MySink>()); // install process-wide
// SetDefaultLevel(LogLevel::kDebug); // adjust the threshold
//
// auto logger = GetDefaultLogger(); // borrow the current default
// if (logger->ShouldLog(LogLevel::kInfo)) {
// logger->Log(LogMessage{.level = LogLevel::kInfo, .message = "scan ready"});
// }
//
// // Or configure from catalog-style properties (applies the "level" key):
// auto sink = std::make_shared<MySink>();
// auto status = sink->Initialize({{std::string(kLevelProperty), "warn"}}); // -> kWarn
// ---------------------------------------------------------------------------
namespace internal {
/// \brief Hot-path accessor for the default logger.
///
/// Returns a reference to a thread-local cached shared_ptr that is refreshed
/// only when the default logger has changed (no lock / no refcount churn in
/// steady state). The reference is valid for the duration of the calling
/// statement.
ICEBERG_EXPORT const std::shared_ptr<Logger>& CurrentLogger() noexcept;
/// \brief Build a LogMessage from the already-formatted text and dispatch it.
///
/// Declared ICEBERG_EXPORT because the logging helpers/macros expand into this
/// call in consumer translation units.
ICEBERG_EXPORT void Emit(Logger& logger, LogLevel level,
const std::source_location& location, std::string&& message);
/// \brief Emit a fixed fallback record when formatting threw.
///
/// noexcept, allocation-light (small/SSO literal), performs no std::format, and
/// does not recurse -- so the macro's "logging never throws" guarantee holds
/// even when a format argument throws.
ICEBERG_EXPORT void EmitFormatError(Logger& logger, LogLevel level,
const std::source_location& location) noexcept;
/// \brief A checked format string bundled with the caller's source_location.
///
/// The consteval constructor preserves std::format's compile-time format-string
/// checking while capturing the call site (the std::print/println technique),
/// so the function-style Log() can record an accurate file:line without a macro.
/// Used as a non-deduced parameter so the trailing args drive deduction.
template <typename... Args>
struct FmtWithLoc {
std::format_string<Args...> fmt;
std::source_location loc;
template <typename T>
requires std::convertible_to<const T&, std::format_string<Args...>>
consteval FmtWithLoc( // NOLINT(google-explicit-constructor): mirrors
// std::format_string
const T& s, std::source_location loc = std::source_location::current())
: fmt(s), loc(loc) {}
};
/// \brief Shared gate -> format -> emit body for the function-style Log() API.
///
/// Formats only when the logger is enabled for \p level, and never throws (a
/// formatting failure routes to EmitFormatError, matching the macros).
template <typename... Args>
void FormatAndEmit(Logger& logger, LogLevel level, const std::source_location& loc,
std::format_string<Args...> fmt, Args&&... args) noexcept {
if (!logger.ShouldLog(level)) return;
try {
Emit(logger, level, loc, std::format(fmt, std::forward<Args>(args)...));
} catch (...) {
// Catch-all upholds the noexcept "logging never throws" guarantee: a
// user-defined std::formatter may throw a non-std::exception type, and this
// function is noexcept, so anything escaping here would call std::terminate.
EmitFormatError(logger, level, loc);
}
}
} // namespace internal
/// \brief Log to the process-default logger, std::format style. Formats only if
/// the level is enabled; never throws.
///
/// Example: `iceberg::Log(LogLevel::kInfo, "loaded {} files", n);`
template <typename... Args>
void Log(LogLevel level, internal::FmtWithLoc<std::type_identity_t<Args>...> fmt,
Args&&... args) noexcept {
const std::shared_ptr<Logger>& logger = internal::CurrentLogger();
if (logger) {
internal::FormatAndEmit(*logger, level, fmt.loc, fmt.fmt,
std::forward<Args>(args)...);
}
}
/// \brief Log to an explicit logger, std::format style. Formats only if enabled.
///
/// Example: `iceberg::Log(logger, LogLevel::kWarn, "retry {}", attempt);`
template <typename... Args>
void Log(Logger& logger, LogLevel level,
internal::FmtWithLoc<std::type_identity_t<Args>...> fmt,
Args&&... args) noexcept {
internal::FormatAndEmit(logger, level, fmt.loc, fmt.fmt, std::forward<Args>(args)...);
}
} // namespace iceberg