Branch data Line data Source code
1 : : /*
2 : : * Copyright 2025 Jinwoo Sung
3 : : *
4 : : * Licensed under the Apache License, Version 2.0 (the "License");
5 : : * you may not use this file except in compliance with the License.
6 : : * You may obtain a copy of the License at
7 : : *
8 : : * http://www.apache.org/licenses/LICENSE-2.0
9 : : *
10 : : * Unless required by applicable law or agreed to in writing, software
11 : : * distributed under the License is distributed on an "AS IS" BASIS,
12 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 : : * See the License for the specific language governing permissions and
14 : : * limitations under the License.
15 : : */
16 : :
17 : : #include "logger.hpp"
18 : :
19 : : #include <spdlog/async.h>
20 : : #include <spdlog/details/thread_pool.h>
21 : : #include <spdlog/sinks/base_sink.h>
22 : : #include <spdlog/sinks/dist_sink.h>
23 : : #include <spdlog/sinks/rotating_file_sink.h>
24 : : #include <spdlog/sinks/sink.h>
25 : : #include <spdlog/sinks/stdout_color_sinks.h>
26 : : #include <spdlog/spdlog.h>
27 : :
28 : : #include <algorithm>
29 : : #include <cctype>
30 : : #include <cstdlib>
31 : : #include <ctime>
32 : : #include <future>
33 : : #include <iomanip>
34 : : #include <iostream>
35 : : #include <mutex>
36 : : #include <sstream>
37 : : #include <string_view>
38 : :
39 : : namespace wirestead {
40 : : namespace diagnostics {
41 : :
42 : : /**
43 : : * @brief Custom spdlog sink for LogCallback
44 : : */
45 : : template <typename Mutex>
46 : : class callback_sink : public spdlog::sinks::base_sink<Mutex> {
47 : : public:
48 : 8 : explicit callback_sink(Logger::LogCallback callback) : callback_(std::move(callback)) {}
49 : :
50 : 1 : void set_callback(Logger::LogCallback callback) {
51 : 1 : std::lock_guard<Mutex> lock(spdlog::sinks::base_sink<Mutex>::mutex_);
52 : 1 : callback_ = std::move(callback);
53 : 1 : }
54 : :
55 : : protected:
56 : 16 : void sink_it_(const spdlog::details::log_msg& msg) override {
57 [ - + ]: 16 : if (!callback_) return;
58 : :
59 : 16 : spdlog::memory_buf_t formatted;
60 : 16 : spdlog::sinks::base_sink<Mutex>::formatter_->format(msg, formatted);
61 : : try {
62 : 17 : callback_(from_spdlog_level(msg.level), fmt::to_string(formatted));
63 : 1 : } catch (...) {
64 : : }
65 : 16 : }
66 : :
67 : 14 : void flush_() override {}
68 : :
69 : : private:
70 : 16 : static LogLevel from_spdlog_level(spdlog::level::level_enum level) {
71 [ + + + + : 16 : switch (level) {
+ - ]
72 : 2 : case spdlog::level::debug:
73 : 2 : return LogLevel::DEBUG;
74 : 10 : case spdlog::level::info:
75 : 10 : return LogLevel::INFO;
76 : 2 : case spdlog::level::warn:
77 : 2 : return LogLevel::WARNING;
78 : 1 : case spdlog::level::err:
79 : 1 : return LogLevel::ERROR;
80 : 1 : case spdlog::level::critical:
81 : 1 : return LogLevel::CRITICAL;
82 : 0 : default:
83 : 0 : return LogLevel::INFO;
84 : : }
85 : : }
86 : :
87 : : Logger::LogCallback callback_;
88 : : };
89 : :
90 : : using callback_sink_mt = callback_sink<std::mutex>;
91 : :
92 : : class level_range_sink final : public spdlog::sinks::sink {
93 : : public:
94 : 702 : level_range_sink(std::shared_ptr<spdlog::sinks::sink> sink, spdlog::level::level_enum min_level,
95 : : spdlog::level::level_enum max_level)
96 : 702 : : sink_(std::move(sink)), min_level_(min_level), max_level_(max_level) {}
97 : :
98 : 1662 : void log(const spdlog::details::log_msg& msg) override {
99 [ + + + + ]: 1662 : if (msg.level >= min_level_ && msg.level <= max_level_) {
100 : 831 : sink_->log(msg);
101 : : }
102 : 1662 : }
103 : :
104 : 54 : void flush() override { sink_->flush(); }
105 : :
106 : 0 : void set_pattern(const std::string& pattern) override { sink_->set_pattern(pattern); }
107 : :
108 : 1456 : void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) override {
109 : 1456 : sink_->set_formatter(std::move(sink_formatter));
110 : 1456 : }
111 : :
112 : : private:
113 : : std::shared_ptr<spdlog::sinks::sink> sink_;
114 : : spdlog::level::level_enum min_level_;
115 : : spdlog::level::level_enum max_level_;
116 : : };
117 : :
118 : : struct Logger::Impl {
119 : : mutable std::mutex mutex_;
120 : : std::atomic<LogLevel> current_level_{LogLevel::INFO};
121 : : std::atomic<bool> enabled_{true};
122 : : std::atomic<int> outputs_{static_cast<int>(LogOutput::CONSOLE)};
123 : :
124 : : std::shared_ptr<spdlog::logger> spd_logger_;
125 : : std::shared_ptr<spdlog::sinks::dist_sink_mt> dist_sink_;
126 : : std::shared_ptr<spdlog::sinks::stdout_color_sink_mt> console_stdout_sink_;
127 : : std::shared_ptr<spdlog::sinks::stderr_color_sink_mt> console_stderr_sink_;
128 : : std::shared_ptr<level_range_sink> console_stdout_filter_;
129 : : std::shared_ptr<level_range_sink> console_stderr_filter_;
130 : : std::shared_ptr<spdlog::sinks::rotating_file_sink_mt> file_sink_;
131 : : std::shared_ptr<callback_sink_mt> callback_sink_;
132 : :
133 : : std::string current_log_file_;
134 : : LogRotationConfig rotation_config_;
135 : : std::string format_ = "{timestamp} [{level}] [{component}] [{operation}] [{source}] {message}";
136 : : std::string last_error_;
137 : :
138 : : // Async logging support
139 : : std::atomic<bool> async_enabled_{false};
140 : : AsyncLogConfig async_config_;
141 : : std::future<void> drop_future_;
142 : : std::shared_ptr<spdlog::details::thread_pool> async_thread_pool_;
143 : :
144 : 1023 : Impl() {
145 : 341 : dist_sink_ = std::make_shared<spdlog::sinks::dist_sink_mt>();
146 : :
147 : : // Default to sync logger initially, can be changed to async via set_async_logging
148 : 341 : spd_logger_ = std::make_shared<spdlog::logger>("wirestead", dist_sink_);
149 : 341 : spd_logger_->set_level(to_spdlog_level(LogLevel::DEBUG)); // Allow all, filter via Logger::log or Logger::set_level
150 : :
151 : 341 : add_console_sinks();
152 : :
153 : 341 : apply_spdlog_pattern();
154 : 341 : apply_environment_settings();
155 : 341 : }
156 : :
157 : 0 : ~Impl() {
158 [ # # ]: 0 : if (drop_future_.valid()) {
159 : 0 : drop_future_.wait();
160 : : }
161 : 0 : spdlog::drop("wirestead");
162 : 0 : }
163 : :
164 : 743 : void apply_spdlog_pattern() {
165 [ + - ]: 743 : if (dist_sink_) {
166 : 2229 : dist_sink_->set_pattern("%v");
167 : : }
168 : 743 : }
169 : :
170 : 4 : void set_error(std::string message) { last_error_ = std::move(message); }
171 : :
172 : 231 : void clear_error() { last_error_.clear(); }
173 : :
174 : 5768 : bool has_outputs_unlocked() const { return outputs_.load() != 0; }
175 : :
176 : 367 : void add_console_sinks() {
177 [ + + ]: 367 : if (!console_stdout_sink_) {
178 : 351 : console_stdout_sink_ = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
179 : : console_stdout_filter_ =
180 : 351 : std::make_shared<level_range_sink>(console_stdout_sink_, spdlog::level::trace, spdlog::level::warn);
181 : : }
182 [ + + ]: 367 : if (!console_stderr_sink_) {
183 : 351 : console_stderr_sink_ = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
184 : : console_stderr_filter_ =
185 : 351 : std::make_shared<level_range_sink>(console_stderr_sink_, spdlog::level::err, spdlog::level::critical);
186 : : }
187 : :
188 : 367 : dist_sink_->remove_sink(console_stdout_filter_);
189 : 367 : dist_sink_->remove_sink(console_stderr_filter_);
190 : 367 : dist_sink_->add_sink(console_stdout_filter_);
191 : 367 : dist_sink_->add_sink(console_stderr_filter_);
192 : 367 : apply_spdlog_pattern();
193 : 367 : }
194 : :
195 : 50 : void remove_console_sinks() {
196 [ + + ]: 50 : if (console_stdout_filter_) {
197 : 45 : dist_sink_->remove_sink(console_stdout_filter_);
198 : : }
199 [ + + ]: 50 : if (console_stderr_filter_) {
200 : 45 : dist_sink_->remove_sink(console_stderr_filter_);
201 : : }
202 : 50 : console_stdout_filter_.reset();
203 : 50 : console_stderr_filter_.reset();
204 : 50 : console_stdout_sink_.reset();
205 : 50 : console_stderr_sink_.reset();
206 : 50 : }
207 : :
208 : 37 : void set_format(const std::string& format) { format_ = format; }
209 : :
210 : 8262 : static void replace_all(std::string& str, const std::string& from, const std::string& to) {
211 [ - + ]: 8262 : if (from.empty()) {
212 : 0 : return;
213 : : }
214 : :
215 : 8262 : size_t pos = 0;
216 [ + + ]: 13768 : while ((pos = str.find(from, pos)) != std::string::npos) {
217 : 5506 : str.replace(pos, from.length(), to);
218 : 5506 : pos += to.length();
219 : : }
220 : : }
221 : :
222 : 918 : static std::string level_name(LogLevel level) {
223 [ + + + + : 918 : switch (level) {
+ - ]
224 : 117 : case LogLevel::DEBUG:
225 : 234 : return "debug";
226 : 599 : case LogLevel::INFO:
227 : 1198 : return "info";
228 : 64 : case LogLevel::WARNING:
229 : 128 : return "warning";
230 : 135 : case LogLevel::ERROR:
231 : 270 : return "error";
232 : 3 : case LogLevel::CRITICAL:
233 : 6 : return "critical";
234 : 0 : default:
235 : 0 : return "info";
236 : : }
237 : : }
238 : :
239 : 13 : static std::string normalize_env_value(std::string_view value) {
240 : 13 : std::string normalized(value);
241 [ + - + - ]: 13 : normalized.erase(normalized.begin(), std::find_if(normalized.begin(), normalized.end(),
242 : 14 : [](unsigned char c) { return !std::isspace(c); }));
243 [ + - ]: 26 : normalized.erase(
244 [ + - ]: 27 : std::find_if(normalized.rbegin(), normalized.rend(), [](unsigned char c) { return !std::isspace(c); }).base(),
245 : 13 : normalized.end());
246 : 13 : std::transform(normalized.begin(), normalized.end(), normalized.begin(),
247 : 66 : [](unsigned char c) { return static_cast<char>(std::toupper(c)); });
248 : 13 : return normalized;
249 : 0 : }
250 : :
251 : 13 : static bool parse_log_level(std::string_view value, LogLevel& level, bool& disable_logging) {
252 : 13 : const auto normalized = normalize_env_value(value);
253 : 13 : disable_logging = false;
254 [ + - + - : 13 : if (normalized == "DEBUG" || normalized == "TRACE") {
+ - + + +
+ ]
255 : 1 : level = LogLevel::DEBUG;
256 : 1 : return true;
257 : : }
258 [ + - + + ]: 12 : if (normalized == "INFO") {
259 : 1 : level = LogLevel::INFO;
260 : 1 : return true;
261 : : }
262 [ + - + + : 11 : if (normalized == "WARNING" || normalized == "WARN") {
+ - + + +
+ ]
263 : 4 : level = LogLevel::WARNING;
264 : 4 : return true;
265 : : }
266 [ + - + + : 7 : if (normalized == "ERROR" || normalized == "ERR") {
+ - + + +
+ ]
267 : 3 : level = LogLevel::ERROR;
268 : 3 : return true;
269 : : }
270 [ + - + - : 4 : if (normalized == "CRITICAL" || normalized == "FATAL") {
+ - + + +
+ ]
271 : 1 : level = LogLevel::CRITICAL;
272 : 1 : return true;
273 : : }
274 [ + - + + : 3 : if (normalized == "OFF" || normalized == "NONE" || normalized == "DISABLED") {
+ - + + +
- - + +
+ ]
275 : 2 : disable_logging = true;
276 : 2 : return true;
277 : : }
278 : 1 : return false;
279 : 13 : }
280 : :
281 : 353 : void apply_environment_settings() {
282 : : // WIRESTEAD_LOG_LEVEL takes priority over UNILINK_LOG_LEVEL when both are
283 : : // set (docs/migration-from-unilink.md compatibility policy), with a
284 : : // startup log line so the choice is discoverable rather than silent.
285 : 353 : const char* wirestead_env = std::getenv("WIRESTEAD_LOG_LEVEL");
286 : 353 : const char* unilink_env = std::getenv("UNILINK_LOG_LEVEL");
287 [ + + + - ]: 353 : const bool has_wirestead_env = wirestead_env && *wirestead_env != '\0';
288 [ + + + - ]: 353 : const bool has_unilink_env = unilink_env && *unilink_env != '\0';
289 : :
290 [ + + ]: 353 : const char* env_level = has_wirestead_env ? wirestead_env : unilink_env;
291 [ + + ]: 353 : const char* env_name = has_wirestead_env ? "WIRESTEAD_LOG_LEVEL" : "UNILINK_LOG_LEVEL";
292 [ + + - + ]: 353 : if (!env_level || *env_level == '\0') {
293 : 341 : return;
294 : : }
295 : :
296 [ + + + + : 13 : if (has_wirestead_env && has_unilink_env && spd_logger_) {
+ - + + ]
297 : 1 : spd_logger_->info("WIRESTEAD_LOG_LEVEL is set alongside UNILINK_LOG_LEVEL; WIRESTEAD_LOG_LEVEL takes precedence");
298 : : }
299 : :
300 : 13 : LogLevel parsed_level = current_level_.load();
301 : 13 : bool disable_logging = false;
302 [ + - + + ]: 13 : if (!parse_log_level(env_level, parsed_level, disable_logging)) {
303 : 3 : set_error("Invalid " + std::string(env_name) + ": " + std::string(env_level));
304 : 1 : return;
305 : : }
306 : :
307 : 12 : enabled_.store(!disable_logging);
308 [ + + ]: 12 : if (!disable_logging) {
309 : 10 : current_level_.store(parsed_level);
310 [ + - ]: 10 : if (spd_logger_) {
311 : 10 : spd_logger_->set_level(to_spdlog_level(parsed_level));
312 : : }
313 : : }
314 : 12 : clear_error();
315 : : }
316 : :
317 : 918 : static std::string timestamp_now() {
318 : : using namespace std::chrono;
319 : 918 : const auto now = system_clock::now();
320 : 918 : const auto tt = system_clock::to_time_t(now);
321 : 918 : std::tm tm{};
322 : : #if defined(_WIN32)
323 : : localtime_s(&tm, &tt);
324 : : #else
325 : 918 : localtime_r(&tt, &tm);
326 : : #endif
327 : 918 : const auto ms = duration_cast<milliseconds>(now.time_since_epoch()) % 1000;
328 : :
329 : 918 : std::ostringstream oss;
330 : 918 : oss << std::put_time(&tm, "%F %T") << '.' << std::setw(3) << std::setfill('0') << ms.count();
331 : 1836 : return oss.str();
332 : 918 : }
333 : :
334 : 918 : static std::string format_message(const std::string& format, LogLevel level, std::string_view component,
335 : : std::string_view operation, std::string_view message,
336 : : const std::source_location& loc) {
337 : 918 : const auto file = std::string(loc.file_name());
338 : 918 : const auto line = std::to_string(loc.line());
339 : 918 : const auto function = std::string(loc.function_name());
340 : 918 : const auto source = fmt::format("{}:{}:{}", file, line, function);
341 : :
342 : 918 : std::string formatted = format;
343 : 2754 : replace_all(formatted, "{timestamp}", timestamp_now());
344 : 2754 : replace_all(formatted, "{level}", level_name(level));
345 : 3672 : replace_all(formatted, "{component}", std::string(component));
346 : 3672 : replace_all(formatted, "{operation}", std::string(operation));
347 : 1836 : replace_all(formatted, "{source}", source);
348 : 1836 : replace_all(formatted, "{file}", file);
349 : 1836 : replace_all(formatted, "{line}", line);
350 : 1836 : replace_all(formatted, "{function}", function);
351 : 2754 : replace_all(formatted, "{message}", std::string(message));
352 : 1836 : return formatted;
353 : 918 : }
354 : :
355 : : void flush() {
356 : : auto logger = spd_logger_;
357 : : auto sink = dist_sink_;
358 : : if (logger) logger->flush();
359 : : if (sink) sink->flush();
360 : : }
361 : :
362 : 7 : void setup_async_logging(const AsyncLogConfig& config) {
363 : 7 : async_config_ = config;
364 : :
365 : 7 : spdlog::drop("wirestead"); // Drop existing logger
366 : :
367 [ + - ]: 7 : auto overflow_policy = config.enable_backpressure ? spdlog::async_overflow_policy::block
368 : : : spdlog::async_overflow_policy::overrun_oldest;
369 : :
370 : 7 : async_thread_pool_ = std::make_shared<spdlog::details::thread_pool>(std::max<size_t>(1, config.max_queue_size), 1);
371 : :
372 : 7 : spd_logger_ = std::make_shared<spdlog::async_logger>("wirestead", dist_sink_, async_thread_pool_, overflow_policy);
373 : 7 : spd_logger_->set_level(to_spdlog_level(current_level_.load()));
374 : 7 : apply_spdlog_pattern();
375 : :
376 : 7 : spdlog::register_logger(spd_logger_);
377 : :
378 [ + + ]: 7 : if (config.flush_interval.count() > 0) {
379 : : // spdlog::flush_every takes std::chrono::seconds.
380 : : // Ensure at least 1 second if a positive interval is requested.
381 : : auto secs =
382 : 5 : std::max(std::chrono::seconds(1), std::chrono::duration_cast<std::chrono::seconds>(config.flush_interval));
383 : 5 : spdlog::flush_every(secs);
384 : : }
385 : :
386 : 7 : async_enabled_.store(true);
387 : 7 : }
388 : :
389 : 13 : void teardown_async_logging() {
390 [ + + ]: 13 : if (!async_enabled_.load()) return;
391 : :
392 : 7 : async_enabled_.store(false);
393 : :
394 : : // If there's a previous drop still pending, wait for it now to avoid multiple drop tasks
395 [ + + ]: 7 : if (drop_future_.valid()) {
396 : 1 : drop_future_.wait();
397 : : }
398 : :
399 : : // Use std::async to drop the logger with a timeout
400 : 28 : drop_future_ = std::async(std::launch::async, []() { spdlog::drop("wirestead"); });
401 : :
402 : 7 : bool drop_success = true;
403 [ - + ]: 7 : if (drop_future_.wait_for(async_config_.shutdown_timeout) == std::future_status::timeout) {
404 : 0 : drop_success = false;
405 : : }
406 : :
407 : 7 : spd_logger_.reset();
408 [ + - ]: 7 : if (drop_success) {
409 : 7 : async_thread_pool_.reset();
410 : : }
411 : 7 : dist_sink_->flush();
412 : :
413 : : // Create sync logger
414 : 7 : spd_logger_ = std::make_shared<spdlog::logger>("wirestead", dist_sink_);
415 : 7 : spd_logger_->set_level(to_spdlog_level(current_level_.load()));
416 : 7 : apply_spdlog_pattern();
417 : :
418 : : // Only register globally if the previous drop finished, to avoid race conditions.
419 : : // If it didn't finish, we still have spd_logger_ internally so log() works.
420 [ + - ]: 7 : if (drop_success) {
421 : 7 : spdlog::register_logger(spd_logger_);
422 : : }
423 : : }
424 : :
425 : 1369 : static spdlog::level::level_enum to_spdlog_level(LogLevel level) {
426 [ + + + + : 1369 : switch (level) {
+ - ]
427 : 541 : case LogLevel::DEBUG:
428 : 541 : return spdlog::level::debug;
429 : 612 : case LogLevel::INFO:
430 : 612 : return spdlog::level::info;
431 : 70 : case LogLevel::WARNING:
432 : 70 : return spdlog::level::warn;
433 : 141 : case LogLevel::ERROR:
434 : 141 : return spdlog::level::err;
435 : 5 : case LogLevel::CRITICAL:
436 : 5 : return spdlog::level::critical;
437 : 0 : default:
438 : 0 : return spdlog::level::info;
439 : : }
440 : : }
441 : : };
442 : :
443 : 341 : Logger::Logger() : impl_(std::make_unique<Impl>()) {}
444 : :
445 : 0 : Logger::~Logger() = default;
446 : :
447 : 0 : Logger::Logger(Logger&&) noexcept = default;
448 : 0 : Logger& Logger::operator=(Logger&&) noexcept = default;
449 : :
450 : 3453 : Logger& Logger::instance() {
451 : 3453 : static Logger* inst = new Logger();
452 : 3453 : return *inst;
453 : : }
454 : :
455 : 1 : Logger& Logger::default_logger() { return instance(); }
456 : :
457 : 86 : void Logger::set_level(LogLevel level) {
458 : 86 : std::lock_guard<std::mutex> lock(impl_->mutex_);
459 : 86 : impl_->current_level_.store(level);
460 [ + - ]: 86 : if (impl_->spd_logger_) {
461 : 86 : impl_->spd_logger_->set_level(Impl::to_spdlog_level(level));
462 : : }
463 : 86 : }
464 : :
465 : 11 : LogLevel Logger::level() const { return get_impl()->current_level_.load(); }
466 : :
467 : 2884 : bool Logger::should_log(LogLevel level) const {
468 : 2884 : const auto* impl = get_impl();
469 [ + + + + : 2884 : return impl->enabled_.load() && impl->has_outputs_unlocked() && level >= impl->current_level_.load();
+ + ]
470 : : }
471 : :
472 : 72 : void Logger::set_console_output(bool enable) {
473 : 72 : std::lock_guard<std::mutex> lock(impl_->mutex_);
474 [ + + ]: 72 : if (enable) {
475 : 26 : impl_->add_console_sinks();
476 : 26 : impl_->outputs_.fetch_or(static_cast<int>(LogOutput::CONSOLE));
477 : : } else {
478 : 46 : impl_->remove_console_sinks();
479 : 46 : impl_->outputs_.fetch_and(~static_cast<int>(LogOutput::CONSOLE));
480 : : }
481 : 72 : impl_->clear_error();
482 : 72 : }
483 : :
484 : 51 : void Logger::set_file_output(const std::string& filename) { (void)try_set_file_output_with_rotation(filename); }
485 : :
486 : 1 : bool Logger::try_set_file_output(const std::string& filename) { return try_set_file_output_with_rotation(filename); }
487 : :
488 : 5 : void Logger::set_file_output_with_rotation(const std::string& filename, const LogRotationConfig& config) {
489 : 5 : (void)try_set_file_output_with_rotation(filename, config);
490 : 5 : }
491 : :
492 : 59 : bool Logger::try_set_file_output_with_rotation(const std::string& filename, const LogRotationConfig& config) {
493 : 59 : std::lock_guard<std::mutex> lock(impl_->mutex_);
494 : :
495 [ + + ]: 59 : if (filename.empty()) {
496 [ + + ]: 46 : if (impl_->file_sink_) {
497 : 10 : impl_->dist_sink_->remove_sink(impl_->file_sink_);
498 : 10 : impl_->file_sink_.reset();
499 : : }
500 : 46 : impl_->current_log_file_.clear();
501 : 46 : impl_->outputs_.fetch_and(~static_cast<int>(LogOutput::FILE));
502 : 46 : impl_->clear_error();
503 : 46 : return true;
504 : : } else {
505 : : try {
506 [ + + ]: 13 : if (config.enable_compression) {
507 : 2 : impl_->set_error("Log compression is not supported by the current rotating file sink");
508 : 1 : return false;
509 : : }
510 [ + - + + ]: 12 : if (config.file_pattern != LogRotationConfig{}.file_pattern) {
511 : 2 : impl_->set_error("Custom log rotation file_pattern is not supported by the current rotating file sink");
512 : 1 : return false;
513 : : }
514 [ - + ]: 11 : if (impl_->file_sink_) {
515 : 0 : impl_->dist_sink_->remove_sink(impl_->file_sink_);
516 : : }
517 : :
518 : 11 : impl_->rotation_config_ = config;
519 : 11 : impl_->current_log_file_ = filename;
520 : :
521 : 21 : impl_->file_sink_ = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(filename, config.max_file_size_bytes,
522 : 21 : config.max_files);
523 : :
524 : 10 : impl_->dist_sink_->add_sink(impl_->file_sink_);
525 : 10 : impl_->apply_spdlog_pattern();
526 : 10 : impl_->outputs_.fetch_or(static_cast<int>(LogOutput::FILE));
527 : 10 : impl_->clear_error();
528 : 10 : return true;
529 : 1 : } catch (const spdlog::spdlog_ex& e) {
530 : 1 : impl_->set_error("Failed to open log file: " + filename + " (" + e.what() + ")");
531 : 1 : std::cerr << impl_->last_error_ << std::endl;
532 : 1 : impl_->file_sink_.reset();
533 : 1 : impl_->current_log_file_.clear();
534 : 1 : impl_->outputs_.fetch_and(~static_cast<int>(LogOutput::FILE));
535 : 1 : return false;
536 : 1 : }
537 : : }
538 : 59 : }
539 : :
540 : 19 : void Logger::set_async_logging(bool enable, const AsyncLogConfig& config) {
541 : 19 : std::lock_guard<std::mutex> lock(impl_->mutex_);
542 [ + + ]: 19 : if (enable) {
543 [ + + ]: 7 : if (impl_->async_enabled_.load()) {
544 : 1 : impl_->teardown_async_logging();
545 : : }
546 : 7 : impl_->setup_async_logging(config);
547 : : } else {
548 : 12 : impl_->teardown_async_logging();
549 : : }
550 : 19 : }
551 : :
552 : 9 : bool Logger::async_logging_enabled() const { return get_impl()->async_enabled_.load(); }
553 : :
554 : 12 : void Logger::reload_from_environment() {
555 : 12 : std::lock_guard<std::mutex> lock(impl_->mutex_);
556 : 12 : impl_->apply_environment_settings();
557 : 12 : }
558 : :
559 : 50 : void Logger::set_callback(LogCallback callback) {
560 : 50 : std::lock_guard<std::mutex> lock(impl_->mutex_);
561 : :
562 [ + + ]: 50 : if (!callback) {
563 [ + + ]: 41 : if (impl_->callback_sink_) {
564 : 8 : impl_->dist_sink_->remove_sink(impl_->callback_sink_);
565 : 8 : impl_->callback_sink_.reset();
566 : : }
567 : 41 : impl_->outputs_.fetch_and(~static_cast<int>(LogOutput::CALLBACK));
568 : 41 : impl_->clear_error();
569 : 41 : return;
570 : : }
571 : :
572 [ + + ]: 9 : if (impl_->callback_sink_) {
573 : 1 : impl_->callback_sink_->set_callback(std::move(callback));
574 : 1 : impl_->dist_sink_->remove_sink(impl_->callback_sink_); // Avoid duplicates
575 : 1 : impl_->dist_sink_->add_sink(impl_->callback_sink_);
576 : : } else {
577 : 8 : impl_->callback_sink_ = std::make_shared<callback_sink_mt>(std::move(callback));
578 : 8 : impl_->dist_sink_->add_sink(impl_->callback_sink_);
579 : : }
580 : 9 : impl_->apply_spdlog_pattern();
581 : 9 : impl_->outputs_.fetch_or(static_cast<int>(LogOutput::CALLBACK));
582 : 9 : impl_->clear_error();
583 : 50 : }
584 : :
585 : 4 : void Logger::set_outputs(int outputs) {
586 : 4 : std::lock_guard<std::mutex> lock(impl_->mutex_);
587 : 4 : int effective_outputs = 0;
588 : :
589 : : // Reconcile sinks in dist_sink_ based on the new bitmask
590 : :
591 : : // Console Sink
592 [ - + ]: 4 : if (outputs & static_cast<int>(LogOutput::CONSOLE)) {
593 : 0 : impl_->add_console_sinks();
594 : 0 : effective_outputs |= static_cast<int>(LogOutput::CONSOLE);
595 : : } else {
596 : 4 : impl_->remove_console_sinks();
597 : : }
598 : :
599 : : // File Sink
600 [ + + ]: 4 : if (outputs & static_cast<int>(LogOutput::FILE)) {
601 [ + - + - : 1 : if (!impl_->file_sink_ && !impl_->current_log_file_.empty()) {
+ - ]
602 : 2 : impl_->file_sink_ = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
603 : 2 : impl_->current_log_file_, impl_->rotation_config_.max_file_size_bytes, impl_->rotation_config_.max_files);
604 : : }
605 [ + - ]: 1 : if (impl_->file_sink_) {
606 : 1 : impl_->dist_sink_->remove_sink(impl_->file_sink_); // Ensure no duplicate
607 : 1 : impl_->dist_sink_->add_sink(impl_->file_sink_);
608 : 1 : impl_->apply_spdlog_pattern();
609 : 1 : effective_outputs |= static_cast<int>(LogOutput::FILE);
610 : : }
611 : : } else {
612 [ + + ]: 3 : if (impl_->file_sink_) {
613 : 1 : impl_->dist_sink_->remove_sink(impl_->file_sink_);
614 : 1 : impl_->file_sink_.reset();
615 : : }
616 : : }
617 : :
618 : : // Callback Sink
619 [ + + ]: 4 : if (outputs & static_cast<int>(LogOutput::CALLBACK)) {
620 [ + - ]: 1 : if (impl_->callback_sink_) {
621 : 1 : impl_->dist_sink_->remove_sink(impl_->callback_sink_); // Ensure no duplicate
622 : 1 : impl_->dist_sink_->add_sink(impl_->callback_sink_);
623 : 1 : impl_->apply_spdlog_pattern();
624 : 1 : effective_outputs |= static_cast<int>(LogOutput::CALLBACK);
625 : : }
626 : : } else {
627 [ + + ]: 3 : if (impl_->callback_sink_) {
628 : 2 : impl_->dist_sink_->remove_sink(impl_->callback_sink_);
629 : : }
630 : : }
631 : :
632 : 4 : impl_->outputs_.store(effective_outputs);
633 : 4 : impl_->clear_error();
634 : 4 : }
635 : :
636 : 47 : void Logger::set_enabled(bool enabled) { impl_->enabled_.store(enabled); }
637 : :
638 : 11 : bool Logger::enabled() const { return get_impl()->enabled_.load(); }
639 : :
640 : 2 : bool Logger::has_outputs() const { return get_impl()->has_outputs_unlocked(); }
641 : :
642 : 13 : std::string Logger::last_error() const {
643 : 13 : std::lock_guard<std::mutex> lock(impl_->mutex_);
644 : 26 : return impl_->last_error_;
645 : 13 : }
646 : :
647 : 37 : void Logger::set_format(const std::string& format) {
648 : 37 : std::lock_guard<std::mutex> lock(impl_->mutex_);
649 : 37 : impl_->set_format(format);
650 : 37 : impl_->clear_error();
651 : 37 : }
652 : :
653 : 26 : void Logger::flush() {
654 : 26 : std::shared_ptr<spdlog::logger> logger;
655 : 26 : std::shared_ptr<spdlog::sinks::dist_sink_mt> sink;
656 : : {
657 : 26 : std::lock_guard<std::mutex> lock(impl_->mutex_);
658 : 26 : logger = impl_->spd_logger_;
659 : 26 : sink = impl_->dist_sink_;
660 : 26 : }
661 [ + - + - ]: 26 : if (logger) logger->flush();
662 [ + - + - ]: 26 : if (sink) sink->flush();
663 : 26 : }
664 : :
665 : 918 : void Logger::log(LogLevel level, std::string_view component, std::string_view operation, std::string_view message,
666 : : const std::source_location& loc) {
667 [ - + ]: 918 : if (!should_log(level)) {
668 : 0 : return;
669 : : }
670 : :
671 : 918 : std::shared_ptr<spdlog::logger> logger;
672 : 918 : std::string format;
673 : : {
674 : 918 : std::lock_guard<std::mutex> lock(impl_->mutex_);
675 : 918 : logger = impl_->spd_logger_;
676 : 918 : format = impl_->format_;
677 : 918 : }
678 : :
679 [ - + ]: 918 : if (!logger) {
680 : 0 : return;
681 : : }
682 : :
683 : 918 : const auto payload = Impl::format_message(format, level, component, operation, message, loc);
684 : 918 : logger->log(Impl::to_spdlog_level(level), payload);
685 : 918 : }
686 : :
687 : 1 : void Logger::debug(std::string_view component, std::string_view operation, std::string_view message,
688 : : const std::source_location& loc) {
689 : 1 : log(LogLevel::DEBUG, component, operation, message, loc);
690 : 1 : }
691 : :
692 : 1 : void Logger::info(std::string_view component, std::string_view operation, std::string_view message,
693 : : const std::source_location& loc) {
694 : 1 : log(LogLevel::INFO, component, operation, message, loc);
695 : 1 : }
696 : :
697 : 1 : void Logger::warning(std::string_view component, std::string_view operation, std::string_view message,
698 : : const std::source_location& loc) {
699 : 1 : log(LogLevel::WARNING, component, operation, message, loc);
700 : 1 : }
701 : :
702 : 1 : void Logger::error(std::string_view component, std::string_view operation, std::string_view message,
703 : : const std::source_location& loc) {
704 : 1 : log(LogLevel::ERROR, component, operation, message, loc);
705 : 1 : }
706 : :
707 : 1 : void Logger::critical(std::string_view component, std::string_view operation, std::string_view message,
708 : : const std::source_location& loc) {
709 : 1 : log(LogLevel::CRITICAL, component, operation, message, loc);
710 : 1 : }
711 : :
712 : : } // namespace diagnostics
713 : : } // namespace wirestead
|