Skip to content

Commit be37451

Browse files
feat(logging): add Loggers registry (#737)
Part 6 of the logging stack (builds on #726). Lets applications pick a logging backend from configuration and install it as the process default — the same pattern as `MetricsReporters`. **How it works** Choose a backend with the `logger-impl` property; built-ins are `noop`, `cerr`, and `spdlog` (when compiled in). ```cpp // Build a logger from properties and install it as the process default. auto logger = iceberg::Loggers::Load({{"logger-impl", "spdlog"}, {"level", "info"}}); if (logger) { iceberg::SetDefaultLogger( std::shared_ptr<iceberg::Logger>(std::move(*logger))); } // ...then anywhere in the code: ICEBERG_LOG_INFO("scan planned: {} files", n); ``` **API** - `Loggers::Load(properties)` — build a logger, choosing the type from `logger-impl`. - `Loggers::Register(type, factory)` — register a custom backend under a new name. With no `logger-impl` set, the default is `spdlog` when compiled in, otherwise `cerr` — so logging works out of the box. **Tests** — `loggers_test` drives the full path an app uses: configure a backend + level via properties, install it, log through the macros, and check the real output. Covers configured level handling and a macro reaching a real spdlog sink, with spdlog ON and OFF. This pull request and its description were written by Isaac.
1 parent 53f6a3e commit be37451

10 files changed

Lines changed: 417 additions & 1 deletion

File tree

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ set(ICEBERG_SOURCES
5757
location_provider.cc
5858
logging/cerr_logger.cc
5959
logging/logger.cc
60+
logging/loggers.cc
6061
logging/spdlog_logger.cc
6162
manifest/manifest_adapter.cc
6263
manifest/manifest_entry.cc

src/iceberg/logging/logger.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ struct ThreadCache {
8383
std::shared_ptr<Logger> Logger::Noop() {
8484
// Intentionally leaked: reachable via the function-local static (LSan-clean)
8585
// and never destroyed, so logging during static teardown stays safe.
86-
static auto* instance = new std::shared_ptr<Logger>(std::make_shared<NoopLogger>());
86+
static auto* instance = new std::shared_ptr<Logger>(internal::MakeNoopLogger());
8787
return *instance;
8888
}
8989

@@ -141,6 +141,8 @@ FatalHandler GetFatalHandler() {
141141

142142
namespace internal {
143143

144+
std::unique_ptr<Logger> MakeNoopLogger() { return std::make_unique<NoopLogger>(); }
145+
144146
namespace {
145147

146148
/// \brief The one place the per-thread cache's lifetime is managed; shared by

src/iceberg/logging/logger.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ class ICEBERG_EXPORT ScopedLogger {
296296

297297
namespace internal {
298298

299+
/// \brief Construct a fresh no-op logger. Shared by Logger::Noop() (which caches a
300+
/// single instance) and the "noop" registry factory (which needs an owned one).
301+
ICEBERG_EXPORT std::unique_ptr<Logger> MakeNoopLogger();
302+
299303
/// \brief Hot-path accessor for the default logger.
300304
///
301305
/// Returns a reference to a thread-local cached shared_ptr that is refreshed

src/iceberg/logging/loggers.cc

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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/loggers.h"
21+
22+
#include <exception>
23+
#include <memory>
24+
#include <mutex>
25+
#include <shared_mutex>
26+
#include <string>
27+
#include <unordered_map>
28+
#include <utility>
29+
30+
// Build-generated, .cc-only. Defines ICEBERG_HAS_SPDLOG; tested with #ifdef.
31+
#include "iceberg/logging/cerr_logger.h"
32+
#include "iceberg/logging/config.h"
33+
#include "iceberg/util/macros.h"
34+
#ifdef ICEBERG_HAS_SPDLOG
35+
# include "iceberg/logging/spdlog_logger_internal.h"
36+
#endif
37+
38+
namespace iceberg {
39+
40+
namespace {
41+
42+
/// \brief Extract the logger type, defaulting to the compiled-in backend.
43+
std::string InferLoggerType(
44+
const std::unordered_map<std::string, std::string>& properties) {
45+
auto it = properties.find(std::string(kLoggerImpl));
46+
if (it != properties.end() && !it->second.empty()) {
47+
return it->second;
48+
}
49+
#ifdef ICEBERG_HAS_SPDLOG
50+
return std::string(kLoggerTypeSpdlog);
51+
#else
52+
return std::string(kLoggerTypeCerr);
53+
#endif
54+
}
55+
56+
struct LoggerRegistryState {
57+
std::shared_mutex mtx;
58+
std::unordered_map<std::string, LoggerFactory> map;
59+
};
60+
61+
LoggerRegistryState& GetRegistry() {
62+
static auto* state = new LoggerRegistryState{
63+
.map = {
64+
{std::string(kLoggerTypeNoop),
65+
[](const std::unordered_map<std::string, std::string>&)
66+
-> Result<std::unique_ptr<Logger>> { return internal::MakeNoopLogger(); }},
67+
{std::string(kLoggerTypeCerr),
68+
[](const std::unordered_map<std::string, std::string>&)
69+
-> Result<std::unique_ptr<Logger>> {
70+
return std::make_unique<CerrLogger>();
71+
}},
72+
#ifdef ICEBERG_HAS_SPDLOG
73+
{std::string(kLoggerTypeSpdlog),
74+
[](const std::unordered_map<std::string, std::string>&)
75+
-> Result<std::unique_ptr<Logger>> {
76+
return std::make_unique<internal::SpdLogger>();
77+
}},
78+
#endif
79+
}};
80+
return *state;
81+
}
82+
83+
} // namespace
84+
85+
Status Loggers::Register(std::string_view logger_type, LoggerFactory factory) {
86+
if (!factory) {
87+
return InvalidArgument("Logger factory for '{}' must not be empty", logger_type);
88+
}
89+
auto& registry = GetRegistry();
90+
std::unique_lock lock(registry.mtx);
91+
registry.map[std::string(logger_type)] = std::move(factory);
92+
return {};
93+
}
94+
95+
Result<std::unique_ptr<Logger>> Loggers::Load(
96+
const std::unordered_map<std::string, std::string>& properties) {
97+
std::string logger_type = InferLoggerType(properties);
98+
99+
LoggerFactory factory;
100+
{
101+
auto& registry = GetRegistry();
102+
std::shared_lock lock(registry.mtx);
103+
auto it = registry.map.find(logger_type);
104+
if (it == registry.map.end()) {
105+
return InvalidArgument(
106+
"Unknown logger type '{}'. Register a factory with Loggers::Register() "
107+
"before using this type.",
108+
logger_type);
109+
}
110+
factory = it->second;
111+
}
112+
113+
try {
114+
// Run the (user-supplied) factory outside the registry lock so it cannot
115+
// deadlock or re-enter the registry; the try/catch turns a throwing factory
116+
// into an error instead of propagating.
117+
ICEBERG_ASSIGN_OR_RAISE(auto logger, factory(properties));
118+
if (!logger) {
119+
return InvalidArgument("Logger factory for '{}' returned null", logger_type);
120+
}
121+
ICEBERG_RETURN_UNEXPECTED(logger->Initialize(properties));
122+
return logger;
123+
} catch (const std::exception& ex) {
124+
return InvalidArgument("Logger factory for '{}' failed: {}", logger_type, ex.what());
125+
} catch (...) {
126+
return InvalidArgument("Logger factory for '{}' failed with unknown exception",
127+
logger_type);
128+
}
129+
}
130+
131+
} // namespace iceberg

src/iceberg/logging/loggers.h

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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/loggers.h
23+
/// \brief Property-driven registry/factory for Logger backends.
24+
25+
#include <functional>
26+
#include <memory>
27+
#include <string>
28+
#include <string_view>
29+
#include <unordered_map>
30+
31+
#include "iceberg/iceberg_export.h"
32+
#include "iceberg/logging/logger.h"
33+
#include "iceberg/result.h"
34+
35+
namespace iceberg {
36+
37+
/// \brief Property key selecting the logger implementation.
38+
constexpr std::string_view kLoggerImpl = "logger-impl";
39+
/// \brief Built-in logger type identifiers.
40+
constexpr std::string_view kLoggerTypeNoop = "noop";
41+
constexpr std::string_view kLoggerTypeCerr = "cerr";
42+
constexpr std::string_view kLoggerTypeSpdlog = "spdlog";
43+
44+
/// \brief Factory constructing a Logger from catalog-style properties.
45+
using LoggerFactory = std::function<Result<std::unique_ptr<Logger>>(
46+
const std::unordered_map<std::string, std::string>& properties)>;
47+
48+
/// \brief Registry of logger factories, mirroring MetricsReporters.
49+
///
50+
/// Built-in factories: "noop", "cerr", and (only when built with ICEBERG_SPDLOG)
51+
/// "spdlog". When the "logger-impl" property is absent, the default is "spdlog"
52+
/// if compiled in, otherwise "cerr" -- an intentional divergence from the metrics
53+
/// registry's noop default (we want logs by default).
54+
class ICEBERG_EXPORT Loggers {
55+
public:
56+
/// \brief Construct and initialize a logger from properties.
57+
static Result<std::unique_ptr<Logger>> Load(
58+
const std::unordered_map<std::string, std::string>& properties);
59+
60+
/// \brief Register a factory for \p logger_type (overwrites any existing).
61+
static Status Register(std::string_view logger_type, LoggerFactory factory);
62+
};
63+
64+
} // namespace iceberg

src/iceberg/logging/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ install_headers(
2929
'log_level.h',
3030
'log_macros.h',
3131
'logger.h',
32+
'loggers.h',
3233
'short_log_macros.h',
3334
],
3435
subdir: 'iceberg/logging',

src/iceberg/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ iceberg_sources = files(
109109
'location_provider.cc',
110110
'logging/cerr_logger.cc',
111111
'logging/logger.cc',
112+
'logging/loggers.cc',
112113
'logging/spdlog_logger.cc',
113114
'manifest/manifest_adapter.cc',
114115
'manifest/manifest_entry.cc',

src/iceberg/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ add_iceberg_test(logging_test
9696
cerr_logger_test.cc
9797
log_level_test.cc
9898
logger_test.cc
99+
loggers_test.cc
99100
macros_active_level_test.cc
100101
macros_test.cc
101102
spdlog_logger_test.cc)

0 commit comments

Comments
 (0)