| /* | |
| * 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 ::iceberg::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 Construct a fresh no-op logger. Shared by Logger::Noop() (which caches a | |
| /// single instance) and the "noop" registry factory (which needs an owned one). | |
| ICEBERG_EXPORT std::unique_ptr<Logger> MakeNoopLogger(); | |
| /// \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 |