logman 0.1.0
Modern C++23 header-only logging manager wrapping spdlog with channels, listeners, and structured events
Loading...
Searching...
No Matches
log_manager.hpp
Go to the documentation of this file.
1#pragma once
2
7
12#include <logman/log_event.hpp>
13
14#if __has_include(<nlohmann/json.hpp>)
17#define LOGMAN_HAS_JSON 1
18#else
19#define LOGMAN_HAS_JSON 0
20#endif
21
22#include <spdlog/pattern_formatter.h>
23#include <spdlog/sinks/sink.h>
24#include <spdlog/sinks/stdout_color_sinks.h>
25#include <spdlog/spdlog.h>
26
27#include <algorithm>
28#include <cctype>
29#include <cstddef>
30#include <cstdlib>
31#include <functional>
32#include <iostream>
33#include <memory>
34#include <mutex>
35#include <optional>
36#include <ranges>
37#include <shared_mutex>
38#include <string>
39#include <string_view>
40#include <unordered_map>
41#include <utility>
42#include <vector>
43
44#if defined(_WIN32)
45#define NOMINMAX
46#include <windows.h>
47#else
48extern "C" char** environ; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
49#endif
50
51namespace logman {
52
53inline constexpr bool has_json_support = LOGMAN_HAS_JSON == 1;
54
55namespace detail {
56
58template <typename Cb>
59inline void enumerate_env(Cb&& cb) {
60#if defined(_WIN32)
61 auto* block = ::GetEnvironmentStringsW();
62 if (block == nullptr) {
63 return;
64 }
65 auto* cursor = block;
66 while (*cursor != L'\0') {
67 const std::wstring_view entry{cursor};
68 const auto eq = entry.find(L'=');
69 if (eq != std::wstring_view::npos && eq != 0) {
70 std::string name;
71 std::string value;
72 name.reserve(eq);
73 value.reserve(entry.size() - eq - 1);
74 for (auto wc : entry.substr(0, eq)) {
75 name.push_back(static_cast<char>(wc));
76 }
77 for (auto wc : entry.substr(eq + 1)) {
78 value.push_back(static_cast<char>(wc));
79 }
80 cb(std::string_view{name}, std::string_view{value});
81 }
82 cursor += entry.size() + 1;
83 }
84 ::FreeEnvironmentStringsW(block);
85#else
86 if (environ == nullptr) {
87 return;
88 }
89 for (char** env = environ; *env != nullptr; ++env) {
90 const std::string_view entry{*env};
91 if (const auto eq = entry.find('='); eq != std::string_view::npos && eq != 0) {
92 cb(entry.substr(0, eq), entry.substr(eq + 1));
93 }
94 }
95#endif
96}
97
98inline std::string ascii_lower(const std::string_view in) {
99 std::string out;
100 out.reserve(in.size());
101 for (const char c : in) {
102 out.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
103 }
104 return out;
105}
106
107} // namespace detail
108
112inline std::optional<spdlog::level::level_enum> parse_level_value(const std::string_view value) {
113 const std::string lower = detail::ascii_lower(value);
114 if (lower == "trace") {
115 return spdlog::level::trace;
116 }
117 if (lower == "debug") {
118 return spdlog::level::debug;
119 }
120 if (lower == "info") {
121 return spdlog::level::info;
122 }
123 if (lower == "warn" || lower == "warning") {
124 return spdlog::level::warn;
125 }
126 if (lower == "err" || lower == "error") {
127 return spdlog::level::err;
128 }
129 if (lower == "critical") {
130 return spdlog::level::critical;
131 }
132 if (lower == "off") {
133 return spdlog::level::off;
134 }
135 return std::nullopt;
136}
137
143inline std::string env_suffix_to_channel_prefix(const std::string_view suffix) {
144 std::string out;
145 out.reserve(suffix.size());
146 for (const char c : suffix) {
147 if (c == '_') {
148 out.push_back('.');
149 } else {
150 out.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
151 }
152 }
153 return out;
154}
155
162public:
163 using Listener = std::function<void(const LogEvent&)>;
164 using ListenerId = std::uint64_t;
165
166 static void initialize(const spdlog::level::level_enum default_level = spdlog::level::info) {
167 InitConfig cfg;
168 cfg.default_level = default_level;
169 instance().init(cfg);
170 }
171
172 static void initialize(const InitConfig& cfg) {
173 instance().init(cfg);
174 }
175
176 static void shutdown() {
177 instance().shutdown_impl();
178 }
179
180 static std::shared_ptr<spdlog::logger> get(const std::string_view channel) {
181 return instance().get_channel(channel);
182 }
183
184 static std::shared_ptr<spdlog::logger> get_or_null(const std::string_view channel) {
185 auto& self = instance();
186 const std::shared_lock lock(self.mutex_);
187 if (const auto it = self.loggers_.find(std::string(channel)); it != self.loggers_.end()) {
188 return it->second;
189 }
190 return nullptr;
191 }
192
193 static std::unordered_map<std::string, spdlog::level::level_enum> channels() {
194 auto& self = instance();
195 const std::shared_lock lock(self.mutex_);
196 std::unordered_map<std::string, spdlog::level::level_enum> out;
197 out.reserve(self.loggers_.size());
198 for (const auto& [name, logger] : self.loggers_) {
199 out.emplace(name, logger->level());
200 }
201 return out;
202 }
203
204 static bool set_level(const std::string_view channel, const spdlog::level::level_enum lvl) {
205 auto& self = instance();
206 const std::shared_lock lock(self.mutex_);
207 const auto it = self.loggers_.find(std::string(channel));
208 if (it == self.loggers_.end()) {
209 return false;
210 }
211 it->second->set_level(lvl);
212 return true;
213 }
214
215 static void set_levels_by_prefix(const std::string_view prefix,
216 const spdlog::level::level_enum lvl) {
217 auto& self = instance();
218 const std::unique_lock lock(self.mutex_);
219 for (auto& [name, lg] : self.loggers_) {
220 if (name.starts_with(prefix)) {
221 lg->set_level(lvl);
222 }
223 }
224 self.prefixed_levels_[std::string(prefix)] = lvl;
225 }
226
227 static void set_all_levels(const spdlog::level::level_enum lvl) {
228 auto& self = instance();
229 const std::unique_lock lock(self.mutex_);
230 for (const auto& lg : self.loggers_ | std::views::values) {
231 lg->set_level(lvl);
232 }
233 self.default_level_ = lvl;
234 spdlog::set_level(lvl);
235 }
236
237 static void set_default_level(const spdlog::level::level_enum lvl) {
238 auto& self = instance();
239 const std::unique_lock lock(self.mutex_);
240 self.default_level_ = lvl;
241 spdlog::set_level(lvl);
242 }
243
244 static spdlog::level::level_enum default_level() {
245 const auto& self = instance();
246 const std::shared_lock lock(self.mutex_);
247 return self.default_level_;
248 }
249
250 static void set_pattern(std::string pattern) {
251 auto& self = instance();
252 const std::unique_lock lock(self.mutex_);
253 self.pattern_ = std::move(pattern);
254 const auto formatter = self.make_console_formatter();
255 if (self.console_sink_) {
256 self.console_sink_->set_formatter(formatter->clone());
257 }
258 for (const auto& lg : self.loggers_ | std::views::values) {
259 lg->set_pattern(self.pattern_);
260 }
261 }
262
263 static void flush() {
264 auto& self = instance();
265 const std::shared_lock lock(self.mutex_);
266 for (const auto& lg : self.loggers_ | std::views::values) {
267 lg->flush();
268 }
269 }
270
271 static void set_flush_on(spdlog::level::level_enum lvl) {
272 auto& self = instance();
273 const std::shared_lock lock(self.mutex_);
274 for (const auto& lg : self.loggers_ | std::views::values) {
275 lg->flush_on(lvl);
276 }
277 self.flush_on_ = lvl;
278 }
279
280 static void add_sink(const spdlog::sink_ptr& sink) {
281 auto& self = instance();
282 const std::unique_lock lock(self.mutex_);
283 self.sinks_.push_back(sink);
284 for (const auto& lg : self.loggers_ | std::views::values) {
285 lg->sinks().push_back(sink);
286 }
287 }
288
289 static bool remove_sink(const spdlog::sink_ptr& sink) {
290 auto& self = instance();
291 const std::unique_lock lock(self.mutex_);
292 const auto before = self.sinks_.size();
293 std::erase(self.sinks_, sink);
294 if (self.sinks_.size() == before) {
295 return false;
296 }
297 for (const auto& lg : self.loggers_ | std::views::values) {
298 auto& s = lg->sinks();
299 std::erase(s, sink);
300 }
301 return true;
302 }
303
304 static ListenerId add_listener(Listener l) {
305 const auto& self = instance();
306 return self.listener_sink_->add_listener(std::move(l));
307 }
308
309 static bool remove_listener(const ListenerId id) {
310 return instance().listener_sink_->remove_listener(id);
311 }
312
313 static void clear_listeners() {
314 instance().listener_sink_->clear_listeners();
315 }
316
317 static std::shared_ptr<ListenerSink> listener_sink() {
318 return instance().listener_sink_;
319 }
320
321private:
322 LogManager() = default;
323
324 static LogManager& instance() {
325 static LogManager inst;
326 return inst;
327 }
328
329 friend void detail_reset_for_testing();
330
331 std::unique_ptr<spdlog::pattern_formatter> make_console_formatter() const {
332 auto formatter = std::make_unique<spdlog::pattern_formatter>();
333 formatter->add_flag<UpperLevelFormatter>('L');
334 formatter->add_flag<ChannelNameFormatter>('n');
335 formatter->set_pattern(pattern_);
336 return formatter;
337 }
338
339 void apply_env(InitConfig& cfg) {
340 if (!cfg.read_env) {
341 return;
342 }
343 std::vector<std::string> prefixes = cfg.env_prefixes;
344 if (prefixes.empty()) {
345 for (auto p : default_env_prefixes) {
346 prefixes.emplace_back(p);
347 }
348 }
349 for (const auto& prefix : prefixes) {
350 detail::enumerate_env([&](const std::string_view name, const std::string_view value) {
351 if (!name.starts_with(prefix)) {
352 return;
353 }
354 if (const std::string_view rest = name.substr(prefix.size()); rest == "LEVEL") {
355 if (const auto lvl = parse_level_value(value)) {
356 cfg.default_level = *lvl;
357 } else {
358 std::cerr << "logman: ignoring " << name << '=' << value
359 << " (unknown level)\n";
360 }
361 } else if (rest.starts_with("LEVEL_")) {
362 const std::string_view ns = rest.substr(6);
363 if (const auto lvl = parse_level_value(value)) {
364 env_prefix_overrides_[env_suffix_to_channel_prefix(ns)] = *lvl;
365 } else {
366 std::cerr << "logman: ignoring " << name << '=' << value
367 << " (unknown level)\n";
368 }
369 } else if (rest == "FORMAT") {
370 if (const std::string lower = detail::ascii_lower(value); lower == "text") {
371 cfg.structured_json = false;
372 } else if (lower == "json") {
373 cfg.structured_json = true;
374 } else {
375 std::cerr << "logman: ignoring " << name << '=' << value
376 << " (unknown format; want text|json)\n";
377 }
378 }
379 });
380 }
381 }
382
383 void init(InitConfig cfg) {
384 std::call_once(init_flag_, [&] {
385 apply_env(cfg);
386 pattern_ = cfg.pattern.empty() ? std::string(default_pattern) : cfg.pattern;
387 default_level_ = cfg.default_level;
388
389 listener_sink_ = std::make_shared<ListenerSink>();
390 listener_sink_->set_level(spdlog::level::trace);
391 listener_sink_->set_formatter(std::make_unique<spdlog::pattern_formatter>(
392 "%v", spdlog::pattern_time_type::local, std::string{}));
393
394 if (cfg.enable_console) {
395 console_sink_ = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
396 console_sink_->set_level(spdlog::level::trace);
397
398 if (cfg.structured_json) {
399#if LOGMAN_HAS_JSON
400 console_sink_->set_formatter(std::make_unique<JsonFormatter>());
401#else
402 std::cerr << "logman: structured_json requested but library compiled "
403 "without <nlohmann/json.hpp>; falling back to text\n";
404 console_sink_->set_formatter(make_console_formatter());
405#endif
406 } else {
407 console_sink_->set_formatter(make_console_formatter());
408 }
409 sinks_.push_back(console_sink_);
410 }
411 sinks_.push_back(listener_sink_);
412
413 for (const auto& [pfx, lvl] : env_prefix_overrides_) {
414 prefixed_levels_[pfx] = lvl;
415 }
416
417 auto main = std::make_shared<spdlog::logger>("main", sinks_.begin(), sinks_.end());
418 main->set_level(default_level_);
419 for (const auto& [pfx, lvl] : prefixed_levels_) {
420 if (std::string_view("main").starts_with(pfx)) {
421 main->set_level(lvl);
422 }
423 }
424 spdlog::register_logger(main);
425 loggers_.emplace("main", main);
426 if (cfg.set_as_spdlog_default) {
427 spdlog::set_default_logger(main);
428 }
429 spdlog::set_level(default_level_);
430
431 initialized_ = true;
432 });
433 }
434
435 void shutdown_impl() {
436 const std::unique_lock lock(mutex_);
437 loggers_.clear();
438 sinks_.clear();
439 prefixed_levels_.clear();
440 env_prefix_overrides_.clear();
441 listener_sink_.reset();
442 console_sink_.reset();
443 initialized_ = false;
444 spdlog::shutdown();
445 }
446
447 std::shared_ptr<spdlog::logger> get_channel(const std::string_view channel) {
448 if (!initialized_) {
449 initialize();
450 }
451
452 const std::string name(channel);
453 {
454 const std::shared_lock lock(mutex_);
455 if (const auto it = loggers_.find(name); it != loggers_.end()) {
456 return it->second;
457 }
458 }
459
460 const std::unique_lock lock(mutex_);
461 if (const auto it = loggers_.find(name); it != loggers_.end()) {
462 return it->second;
463 }
464
465 auto logger = std::make_shared<spdlog::logger>(name, sinks_.begin(), sinks_.end());
466 logger->set_level(default_level_);
467 if (flush_on_) {
468 logger->flush_on(*flush_on_);
469 }
470 for (const auto& [pfx, lvl] : prefixed_levels_) {
471 if (name.starts_with(pfx)) {
472 logger->set_level(lvl);
473 }
474 }
475 spdlog::register_logger(logger);
476 loggers_.emplace(name, logger);
477 return logger;
478 }
479
480 mutable std::shared_mutex mutex_;
481 std::once_flag init_flag_;
482 bool initialized_ = false;
483 std::string pattern_;
484 std::shared_ptr<ListenerSink> listener_sink_;
485 std::shared_ptr<spdlog::sinks::stdout_color_sink_mt> console_sink_;
486 std::vector<spdlog::sink_ptr> sinks_;
487 std::unordered_map<std::string, std::shared_ptr<spdlog::logger>> loggers_;
488 std::unordered_map<std::string, spdlog::level::level_enum> prefixed_levels_;
489 std::unordered_map<std::string, spdlog::level::level_enum> env_prefix_overrides_;
490 std::optional<spdlog::level::level_enum> flush_on_;
491 spdlog::level::level_enum default_level_ = spdlog::level::info;
492};
493
494// -----------------------------------------------------------------------------
495// Free-function shortcuts. Each forwards to the corresponding LogManager
496// static method — same arguments, same return type. Lets consumers write
497// `logman::get("net")` instead of `logman::LogManager::get("net")`.
498// -----------------------------------------------------------------------------
499
500inline void initialize(const spdlog::level::level_enum lvl = spdlog::level::info) {
501 LogManager::initialize(lvl);
502}
503
504inline void initialize(const InitConfig& cfg) {
505 LogManager::initialize(cfg);
506}
507
508inline void shutdown() {
509 LogManager::shutdown();
510}
511
512inline std::shared_ptr<spdlog::logger> get(const std::string_view channel) {
513 return LogManager::get(channel);
514}
515
516inline std::shared_ptr<spdlog::logger> get_or_null(const std::string_view channel) {
517 return LogManager::get_or_null(channel);
518}
519
520inline std::unordered_map<std::string, spdlog::level::level_enum> channels() {
521 return LogManager::channels();
522}
523
524inline bool set_level(const std::string_view channel, const spdlog::level::level_enum lvl) {
525 return LogManager::set_level(channel, lvl);
526}
527
528inline void set_levels_by_prefix(const std::string_view prefix,
529 const spdlog::level::level_enum lvl) {
530 LogManager::set_levels_by_prefix(prefix, lvl);
531}
532
533inline void set_all_levels(const spdlog::level::level_enum lvl) {
534 LogManager::set_all_levels(lvl);
535}
536
537inline void set_default_level(const spdlog::level::level_enum lvl) {
538 LogManager::set_default_level(lvl);
539}
540
541inline spdlog::level::level_enum default_level() {
542 return LogManager::default_level();
543}
544
545inline void set_pattern(std::string pattern) {
546 LogManager::set_pattern(std::move(pattern));
547}
548
549inline void flush() {
550 LogManager::flush();
551}
552
553inline void set_flush_on(const spdlog::level::level_enum lvl) {
554 LogManager::set_flush_on(lvl);
555}
556
557inline void add_sink(const spdlog::sink_ptr& sink) {
558 LogManager::add_sink(sink);
559}
560
561inline bool remove_sink(const spdlog::sink_ptr& sink) {
562 return LogManager::remove_sink(sink);
563}
564
565inline LogManager::ListenerId add_listener(LogManager::Listener l) {
566 return LogManager::add_listener(std::move(l));
567}
568
569inline bool remove_listener(const LogManager::ListenerId id) {
570 return LogManager::remove_listener(id);
571}
572
573inline void clear_listeners() {
574 LogManager::clear_listeners();
575}
576
577inline std::shared_ptr<ListenerSink> listener_sink() {
578 return LogManager::listener_sink();
579}
580
581namespace detail {
582
586inline void reset_log_manager_for_testing() {
587 LogManager::shutdown();
588 spdlog::drop_all();
589}
590
591} // namespace detail
592
593} // namespace logman
n — channel name abbreviated/padded to a fixed width (20 chars).
Definition formatters.hpp:48
Central registry of named spdlog channels.
Definition log_manager.hpp:161
L — uppercase level name, right-aligned to 8 characters.
Definition formatters.hpp:24
Compile-time list of env-var prefixes the runtime scans.
constexpr std::span< const std::string_view > default_env_prefixes
Prefixes (including the trailing underscore) the runtime scans for env vars during LogManager::initia...
Definition env_prefixes.hpp:59
Custom spdlog pattern flag formatters used by the default logman pattern.
constexpr std::string_view default_pattern
Default Ghostframe-style pattern.
Definition formatters.hpp:130
Configuration struct consumed by LogManager::initialize().
spdlog formatter that emits one JSON object per log record.
ListenerSink — spdlog sink that dispatches each log record to registered listener callbacks as LogEve...
Plain-old-data structure describing one log record.
nlohmann/json adapter for LogEvent.
std::optional< spdlog::level::level_enum > parse_level_value(const std::string_view value)
Parse a level name (case-insensitive).
Definition log_manager.hpp:112
void enumerate_env(Cb &&cb)
Iterate the process environment, calling cb(name, value) for each entry.
Definition log_manager.hpp:59
std::string env_suffix_to_channel_prefix(const std::string_view suffix)
Convert the <NAMESPACE> part of an env var name (e.g.
Definition log_manager.hpp:143
Settings applied during LogManager::initialize(InitConfig).
Definition init_config.hpp:16
std::vector< std::string > env_prefixes
Prefixes to scan when read_env is true.
Definition init_config.hpp:46
bool read_env
When true, scan the process environment under env_prefixes for <PREFIX>LEVEL, <PREFIX>LEVEL_<NAMESPAC...
Definition init_config.hpp:40
bool structured_json
Emit one JSON object per line via JsonFormatter.
Definition init_config.hpp:51
bool enable_console
Install the colour console sink.
Definition init_config.hpp:25
std::string pattern
spdlog pattern string.
Definition init_config.hpp:31
spdlog::level::level_enum default_level
Default level applied to the root logger and every channel created without an explicit prefix rule.
Definition init_config.hpp:20
bool set_as_spdlog_default
Install the "main" logger as spdlog::default_logger().
Definition init_config.hpp:36
One log record, captured by ListenerSink and (optionally) emitted as JSON by JsonFormatter.
Definition log_event.hpp:16