Skip to content

Commit 5a1845f

Browse files
feat(logging): add CerrLogger std::cerr backend (3/6)
Third block: the first concrete sink, and the process default until the spdlog backend lands. - CerrLogger writes to std::cerr with a fixed line layout `YYYY-MM-DDThh:mm:ss.mmmZ LEVEL [tid] file:line] message`. Timestamps use UTC std::chrono floored to milliseconds (no gmtime/localtime -- thread-unsafe); the thread id is the OS-native id, cached per thread. - Level is a std::atomic<LogLevel>; a mutex guards the whole formatted-line write so concurrent lines never interleave. Log()/Flush() wrap stream ops in try/catch so the noexcept contract holds even if the stream throws. - MakeDefaultLogger() now returns CerrLogger. Wired into both builds and installed; cerr_logger_test covers layout, level filtering, and concurrent-write safety. Co-authored-by: Isaac
1 parent a38e0cd commit 5a1845f

9 files changed

Lines changed: 399 additions & 7 deletions

File tree

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ set(ICEBERG_SOURCES
4949
inheritable_metadata.cc
5050
json_serde.cc
5151
location_provider.cc
52+
logging/cerr_logger.cc
5253
logging/logger.cc
5354
manifest/manifest_adapter.cc
5455
manifest/manifest_entry.cc

src/iceberg/logging/cerr_logger.cc

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 "iceberg/logging/cerr_logger.h"
21+
22+
#include <chrono>
23+
#include <cstdint>
24+
#include <format>
25+
#include <iostream>
26+
#include <mutex>
27+
#include <string>
28+
#include <string_view>
29+
30+
#if defined(_WIN32)
31+
# include <windows.h>
32+
#elif defined(__APPLE__)
33+
# include <pthread.h>
34+
#else
35+
# include <unistd.h>
36+
37+
# include <sys/syscall.h>
38+
#endif
39+
40+
namespace iceberg {
41+
42+
namespace {
43+
44+
/// \brief OS-native thread id, cached per thread to avoid a syscall per log.
45+
///
46+
/// Matches the cross-process-correlatable id used by spdlog/glog (not the opaque
47+
/// std::thread::id), and avoids the std::formatter<std::thread::id> (P2693)
48+
/// minimum-toolchain dependency.
49+
uint64_t OsThreadId() noexcept {
50+
static thread_local uint64_t tid = []() -> uint64_t {
51+
#if defined(_WIN32)
52+
return static_cast<uint64_t>(::GetCurrentThreadId());
53+
#elif defined(__APPLE__)
54+
uint64_t id = 0;
55+
pthread_threadid_np(nullptr, &id);
56+
return id;
57+
#else
58+
return static_cast<uint64_t>(::syscall(SYS_gettid));
59+
#endif
60+
}();
61+
return tid;
62+
}
63+
64+
/// \brief Trailing path component of a source file path.
65+
std::string_view Basename(std::string_view path) noexcept {
66+
auto pos = path.find_last_of("/\\");
67+
return pos == std::string_view::npos ? path : path.substr(pos + 1);
68+
}
69+
70+
/// \brief Format a record into a single newline-terminated line.
71+
std::string FormatLine(const LogMessage& message) {
72+
auto now =
73+
std::chrono::floor<std::chrono::milliseconds>(std::chrono::system_clock::now());
74+
return std::format("{:%Y-%m-%dT%H:%M:%S}Z {} [{}] {}:{}] {}\n", now,
75+
ToString(message.level), OsThreadId(),
76+
Basename(message.location.file_name()), message.location.line(),
77+
message.message);
78+
}
79+
80+
} // namespace
81+
82+
void CerrLogger::Log(LogMessage&& message) noexcept {
83+
try {
84+
std::string line = FormatLine(message);
85+
std::lock_guard<std::mutex> lock(mutex_);
86+
std::cerr << line;
87+
} catch (...) {
88+
// Logging must never throw. Reached if either formatting or the write fails;
89+
// emit a short marker best-effort and swallow anything further.
90+
try {
91+
std::lock_guard<std::mutex> lock(mutex_);
92+
std::cerr << "<fmt error>\n";
93+
} catch (...) {
94+
}
95+
}
96+
}
97+
98+
void CerrLogger::Flush() noexcept {
99+
try {
100+
std::lock_guard<std::mutex> lock(mutex_);
101+
std::cerr.flush();
102+
} catch (...) {
103+
}
104+
}
105+
106+
} // namespace iceberg

src/iceberg/logging/cerr_logger.h

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
#pragma once
21+
22+
/// \file iceberg/logging/cerr_logger.h
23+
/// \brief Always-available std::cerr logging backend.
24+
25+
#include <atomic>
26+
#include <mutex>
27+
28+
#include "iceberg/iceberg_export.h"
29+
#include "iceberg/logging/log_level.h"
30+
#include "iceberg/logging/logger.h"
31+
32+
namespace iceberg {
33+
34+
/// \brief Logger that writes one line per record to std::cerr.
35+
///
36+
/// Line layout: `YYYY-MM-DDThh:mm:ss.mmmZ LEVEL [tid] file:line] message`.
37+
/// The minimum level is held in a lock-free atomic; a mutex serializes the
38+
/// whole-line write so concurrent records never interleave. Pure standard
39+
/// library -- always compiled, regardless of ICEBERG_SPDLOG.
40+
class ICEBERG_EXPORT CerrLogger : public Logger {
41+
public:
42+
explicit CerrLogger(LogLevel level = LogLevel::kInfo) : level_(level) {}
43+
44+
bool ShouldLog(LogLevel level) const noexcept override {
45+
return level >= level_.load(std::memory_order_relaxed);
46+
}
47+
void Log(LogMessage&& message) noexcept override;
48+
void SetLevel(LogLevel level) noexcept override {
49+
level_.store(level, std::memory_order_relaxed);
50+
}
51+
LogLevel level() const noexcept override {
52+
return level_.load(std::memory_order_relaxed);
53+
}
54+
void Flush() noexcept override;
55+
56+
private:
57+
std::atomic<LogLevel> level_;
58+
std::mutex mutex_;
59+
};
60+
61+
} // namespace iceberg

src/iceberg/logging/logger.cc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
#include <tuple>
2727
#include <utility>
2828

29+
#include "iceberg/logging/cerr_logger.h"
30+
2931
namespace iceberg {
3032

3133
namespace {
@@ -42,10 +44,9 @@ class NoopLogger final : public Logger {
4244

4345
/// \brief Construct the process default logger for this build configuration.
4446
///
45-
/// This block ships only the interface and the no-op logger; the concrete
46-
/// std::cerr and spdlog sinks (and the build-config selection between them)
47-
/// arrive in later blocks, which update this factory.
48-
std::shared_ptr<Logger> MakeDefaultLogger() { return std::make_shared<NoopLogger>(); }
47+
/// Uses the always-available std::cerr sink. The spdlog backend (preferred when
48+
/// compiled in) is wired into this factory in a later block.
49+
std::shared_ptr<Logger> MakeDefaultLogger() { return std::make_shared<CerrLogger>(); }
4950

5051
/// \brief The process-global default-logger slot.
5152
struct DefaultSlot {

src/iceberg/logging/meson.build

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
install_headers(['log_level.h', 'logger.h'], subdir: 'iceberg/logging')
18+
install_headers(
19+
['cerr_logger.h', 'log_level.h', 'logger.h'],
20+
subdir: 'iceberg/logging',
21+
)

src/iceberg/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ iceberg_sources = files(
7474
'inspect/snapshots_table.cc',
7575
'json_serde.cc',
7676
'location_provider.cc',
77+
'logging/cerr_logger.cc',
7778
'logging/logger.cc',
7879
'manifest/manifest_adapter.cc',
7980
'manifest/manifest_entry.cc',

src/iceberg/test/CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@ add_iceberg_test(table_test
102102
table_test.cc
103103
table_update_test.cc)
104104

105-
add_iceberg_test(logging_test SOURCES log_level_test.cc logger_test.cc)
105+
add_iceberg_test(logging_test
106+
SOURCES
107+
cerr_logger_test.cc
108+
log_level_test.cc
109+
logger_test.cc)
106110

107111
add_iceberg_test(expression_test
108112
SOURCES

0 commit comments

Comments
 (0)