Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ project(
)

cpp = meson.get_compiler('cpp')
args = cpp.get_supported_arguments(['/bigobj'])
# /Zc:preprocessor: MSVC's conforming preprocessor, required for the __VA_OPT__
# used by the logging macros. get_supported_arguments drops it on non-MSVC.
args = cpp.get_supported_arguments(['/bigobj', '/Zc:preprocessor'])
add_project_arguments(args, language: 'cpp')

subdir('src')
Expand Down
2 changes: 2 additions & 0 deletions src/iceberg/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ set(ICEBERG_SOURCES
inheritable_metadata.cc
json_serde.cc
location_provider.cc
logging/cerr_logger.cc
logging/logger.cc
manifest/manifest_adapter.cc
manifest/manifest_entry.cc
manifest/manifest_filter_manager.cc
Expand Down
105 changes: 105 additions & 0 deletions src/iceberg/logging/cerr_logger.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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.
*/

#include "iceberg/logging/cerr_logger.h"

#include <chrono>
#include <cstdint>
#include <format>
#include <iostream>
#include <mutex>
#include <string>
#include <string_view>

#if defined(_WIN32)
# include <windows.h>
#elif defined(__APPLE__)
# include <pthread.h>
#else
# include <unistd.h>

# include <sys/syscall.h>
#endif

namespace iceberg {

namespace {

/// \brief OS-native thread id, cached per thread to avoid a syscall per log.
///
/// Matches the cross-process-correlatable id used by spdlog/glog (not the opaque
/// std::thread::id), and avoids the std::formatter<std::thread::id> (P2693)
/// minimum-toolchain dependency.
uint64_t OsThreadId() noexcept {
static thread_local uint64_t tid = []() -> uint64_t {
#if defined(_WIN32)
return static_cast<uint64_t>(::GetCurrentThreadId());
#elif defined(__APPLE__)
uint64_t id = 0;
pthread_threadid_np(nullptr, &id);
return id;
#else
return static_cast<uint64_t>(::syscall(SYS_gettid));
#endif
}();
return tid;
}

/// \brief Trailing path component of a source file path.
std::string_view Basename(std::string_view path) noexcept {
auto pos = path.find_last_of("/\\");
return pos == std::string_view::npos ? path : path.substr(pos + 1);
}

/// \brief Format a record into a single newline-terminated line.
std::string FormatLine(const LogMessage& message) {
auto now =
std::chrono::floor<std::chrono::milliseconds>(std::chrono::system_clock::now());
return std::format("{:%Y-%m-%dT%H:%M:%S}Z {} [{}] {}:{}] {}\n", now,
ToString(message.level), OsThreadId(),
Basename(message.location.file_name()), message.location.line(),
message.message);
}

} // namespace

void CerrLogger::Log(LogMessage&& message) noexcept {
try {
std::string line = FormatLine(message);
std::lock_guard<std::mutex> lock(mutex_);
std::cerr << line;
} catch (...) {
// Logging must never throw. Best-effort fallback, swallow any failure.
try {
std::lock_guard<std::mutex> lock(mutex_);
std::cerr << "<fmt error>\n";
} catch (...) {
}
}
}

void CerrLogger::Flush() noexcept {
try {
std::lock_guard<std::mutex> lock(mutex_);
std::cerr.flush();
} catch (...) {
}
}

} // namespace iceberg
59 changes: 59 additions & 0 deletions src/iceberg/logging/cerr_logger.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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/cerr_logger.h
/// \brief Always-available std::cerr logging backend.

#include <atomic>
#include <mutex>

#include "iceberg/iceberg_export.h"
#include "iceberg/logging/log_level.h"
#include "iceberg/logging/logger.h"

namespace iceberg {

/// \brief Logger that writes one line per record to std::cerr.
///
/// Line layout: `YYYY-MM-DDThh:mm:ss.mmmZ LEVEL [tid] file:line] message`.
/// The minimum level is held in a lock-free atomic; a mutex serializes the
/// whole-line write so concurrent records never interleave. Pure standard
/// library -- always compiled, regardless of ICEBERG_SPDLOG.
class ICEBERG_EXPORT CerrLogger : public Logger {
public:
explicit CerrLogger(LogLevel level = LogLevel::kInfo) : level_(level) {}

bool ShouldLog(LogLevel level) const override {
return level >= level_.load(std::memory_order_relaxed);
}
void Log(LogMessage&& message) noexcept override;
void SetLevel(LogLevel level) override {
level_.store(level, std::memory_order_relaxed);
}
LogLevel level() const override { return level_.load(std::memory_order_relaxed); }
void Flush() noexcept override;

private:
std::atomic<LogLevel> level_;
std::mutex mutex_;
};

} // namespace iceberg
147 changes: 147 additions & 0 deletions src/iceberg/logging/logger.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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.
*/

#include "iceberg/logging/logger.h"

#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <utility>

#include "iceberg/logging/cerr_logger.h"

namespace iceberg {

namespace {

/// \brief Logger that drops every record.
class NoopLogger final : public Logger {
public:
bool ShouldLog(LogLevel /*level*/) const override { return false; }
void Log(LogMessage&& /*message*/) noexcept override {}
void SetLevel(LogLevel /*level*/) override {}
LogLevel level() const override { return LogLevel::kOff; }
bool IsNoop() const override { return true; }
};

/// \brief Construct the process default logger for this build configuration.
///
/// Uses the always-available std::cerr sink. The spdlog backend (preferred when
/// compiled in) is wired into this factory in a later block.
std::shared_ptr<Logger> MakeDefaultLogger() { return std::make_shared<CerrLogger>(); }

/// \brief The process-global default-logger slot.
struct DefaultSlot {
std::mutex mtx;
std::shared_ptr<Logger> logger;
// Seeded to 1 so a fresh thread (tls_gen == 0) always refreshes on first use.
std::atomic<uint64_t> gen{1};

DefaultSlot() : logger(MakeDefaultLogger()) {}
};

/// \brief Immortal (leaked, hence reachable -> LSan-clean) accessor for the slot.
DefaultSlot& Slot() {
static auto* slot = new DefaultSlot();
return *slot;
}

} // namespace

std::shared_ptr<Logger> Logger::Noop() {
// Intentionally leaked: reachable via the function-local static (LSan-clean)
// and never destroyed, so logging during static teardown stays safe.
static auto* instance = new std::shared_ptr<Logger>(std::make_shared<NoopLogger>());
return *instance;
}

std::shared_ptr<Logger> GetDefaultLogger() {
DefaultSlot& slot = Slot();
std::lock_guard<std::mutex> lock(slot.mtx);
return slot.logger;
}

void SetDefaultLogger(std::shared_ptr<Logger> logger) {
if (!logger) {
logger = Logger::Noop();
}
DefaultSlot& slot = Slot();
std::lock_guard<std::mutex> lock(slot.mtx);
slot.logger = std::move(logger);
// Publish the swap; the mutex provides the happens-before, gen is a detector.
slot.gen.fetch_add(1, std::memory_order_relaxed);
}

void SetDefaultLevel(LogLevel level) {
DefaultSlot& slot = Slot();
std::lock_guard<std::mutex> lock(slot.mtx);
slot.logger->SetLevel(level);
}

namespace detail {

const std::shared_ptr<Logger>& CurrentLogger() noexcept {
static thread_local std::shared_ptr<Logger> tls;
static thread_local uint64_t tls_gen = 0;
// Sentinel whose destructor marks the cache dead at thread exit. It is
// declared after tls/tls_gen, so it is destroyed FIRST (reverse order); once
// dead, a log from any later-destroyed thread_local destructor must not touch
// the (about-to-be / already) destroyed tls slot.
static thread_local struct AliveFlag {
bool value = true;
~AliveFlag() { value = false; }
} alive;
if (!alive.value) {
// Thread teardown: the TLS cache is unsafe. Fall back to an immortal logger
// (leaked, never destroyed) so logging during teardown stays safe.
static const std::shared_ptr<Logger> kFallback = Logger::Noop();
return kFallback;
}
DefaultSlot& slot = Slot();
uint64_t current = slot.gen.load(std::memory_order_relaxed);
if (current != tls_gen) {
std::lock_guard<std::mutex> lock(slot.mtx);
tls = slot.logger;
tls_gen = current;
}
return tls;
}

void Emit(Logger& logger, LogLevel level, const std::source_location& location,
std::string&& message) {
logger.Log(LogMessage{.level = level,
.message = std::move(message),
.location = location,
.attributes = {}});
}

void EmitFormatError(Logger& logger, LogLevel level,
const std::source_location& location) noexcept {
// Fixed short literal (<= 15 bytes, fits SSO on libstdc++/libc++/MSVC -> no heap
// allocation), no std::format, no retry. Cannot throw or recurse.
logger.Log(LogMessage{.level = level,
.message = std::string("<fmt error>"),
.location = location,
.attributes = {}});
}

} // namespace detail

} // namespace iceberg
Loading
Loading