unilink  0.4.3
A simple C++ library for unified async communication
logger.cc
Go to the documentation of this file.
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 <condition_variable>
20 #include <cstdio>
21 #include <cstring>
22 #include <ctime>
23 #include <filesystem>
24 #include <fstream>
25 #include <iomanip>
26 #include <iostream>
27 #include <mutex>
28 #include <queue>
29 #include <sstream>
30 #include <string_view>
31 #include <thread>
32 
33 namespace unilink {
34 namespace diagnostics {
35 
36 struct Logger::Impl {
37  mutable std::mutex mutex_;
38  std::atomic<LogLevel> current_level_{LogLevel::INFO};
39  std::atomic<bool> enabled_{true};
40  std::atomic<int> outputs_{static_cast<int>(LogOutput::CONSOLE)};
41 
42  struct FormatPart {
45  std::string value; // Only used for LITERAL
46  };
47 
48  struct LogFormat {
49  std::string format_string;
50  std::vector<FormatPart> parsed_format;
51  };
52 
53  std::shared_ptr<LogFormat> log_format_;
54 
55  std::unique_ptr<std::ofstream> file_output_;
57 
58  // Log rotation support
59  std::unique_ptr<LogRotation> log_rotation_;
60  std::string current_log_file_;
61 
62  // Async logging support
63  std::atomic<bool> async_enabled_{false};
66 
67  // Threading for async logging
68  std::thread worker_thread_;
69  std::atomic<bool> running_{false};
70  std::atomic<bool> shutdown_requested_{false};
71 
72  // Queue management
73  std::queue<LogEntry> log_queue_;
74  mutable std::mutex queue_mutex_;
75  std::condition_variable queue_cv_;
76  mutable std::mutex stats_mutex_;
77 
78  struct TimestampBuffer {
79  char data[64];
80  size_t length;
81  std::string_view view() const { return {data, length}; }
82  };
83 
84  Impl() { parse_format("{timestamp} [{level}] [{component}] [{operation}] {message}"); }
85 
86  ~Impl() {
88  flush();
89  }
90 
91  void parse_format(const std::string& format) {
92  static const std::string default_format = "{timestamp} [{level}] [{component}] [{operation}] {message}";
93  static std::shared_ptr<LogFormat> cached_default_format;
94  static std::once_flag flag;
95 
96  std::call_once(flag, []() {
97  auto new_format = std::make_shared<LogFormat>();
98  new_format->format_string = default_format;
99  new_format->parsed_format.reserve(9);
100  new_format->parsed_format.push_back({FormatPart::TIMESTAMP, ""});
101  new_format->parsed_format.push_back({FormatPart::LITERAL, " ["});
102  new_format->parsed_format.push_back({FormatPart::LEVEL, ""});
103  new_format->parsed_format.push_back({FormatPart::LITERAL, "] ["});
104  new_format->parsed_format.push_back({FormatPart::COMPONENT, ""});
105  new_format->parsed_format.push_back({FormatPart::LITERAL, "] ["});
106  new_format->parsed_format.push_back({FormatPart::OPERATION, ""});
107  new_format->parsed_format.push_back({FormatPart::LITERAL, "] "});
108  new_format->parsed_format.push_back({FormatPart::MESSAGE, ""});
109  cached_default_format = new_format;
110  });
111 
112  if (format == default_format) {
113  std::lock_guard<std::mutex> lock(mutex_);
114  log_format_ = cached_default_format;
115  return;
116  }
117 
118  auto new_format = std::make_shared<LogFormat>();
119  new_format->format_string = format;
120 
121  size_t count = 1;
122  for (char c : format) {
123  if (c == '{') count += 2;
124  }
125  new_format->parsed_format.reserve(count);
126 
127  size_t start = 0;
128  size_t pos = 0;
129  std::string_view format_view = format;
130 
131  while ((pos = format_view.find('{', start)) != std::string_view::npos) {
132  if (pos > start) {
133  new_format->parsed_format.push_back({FormatPart::LITERAL, std::string(format_view.substr(start, pos - start))});
134  }
135 
136  size_t end = format_view.find('}', pos);
137  if (end == std::string_view::npos) {
138  new_format->parsed_format.push_back({FormatPart::LITERAL, std::string(format_view.substr(pos))});
139  start = format_view.length();
140  break;
141  }
142 
143  std::string_view placeholder = format_view.substr(pos + 1, end - pos - 1);
144  if (placeholder == "timestamp") {
145  new_format->parsed_format.push_back({FormatPart::TIMESTAMP, ""});
146  } else if (placeholder == "level") {
147  new_format->parsed_format.push_back({FormatPart::LEVEL, ""});
148  } else if (placeholder == "component") {
149  new_format->parsed_format.push_back({FormatPart::COMPONENT, ""});
150  } else if (placeholder == "operation") {
151  new_format->parsed_format.push_back({FormatPart::OPERATION, ""});
152  } else if (placeholder == "message") {
153  new_format->parsed_format.push_back({FormatPart::MESSAGE, ""});
154  } else {
155  new_format->parsed_format.push_back({FormatPart::LITERAL, std::string(format_view.substr(pos, end - pos + 1))});
156  }
157 
158  start = end + 1;
159  }
160 
161  if (start < format_view.length()) {
162  new_format->parsed_format.push_back({FormatPart::LITERAL, std::string(format_view.substr(start))});
163  }
164 
165  std::lock_guard<std::mutex> lock(mutex_);
166  log_format_ = std::move(new_format);
167  }
168 
169  void flush() {
170  std::lock_guard<std::mutex> lock(mutex_);
171  if (file_output_ && file_output_->is_open()) {
172  file_output_->flush();
173  }
174  std::cout.flush();
175  std::cerr.flush();
176  }
177 
178  std::string format_message(std::chrono::system_clock::time_point timestamp_val, LogLevel level,
179  std::string_view component, std::string_view operation, std::string_view message) {
180  TimestampBuffer timestamp = get_timestamp(timestamp_val);
181  std::string_view level_str = level_to_string(level);
182 
183  std::shared_ptr<LogFormat> current_format;
184  {
185  std::lock_guard<std::mutex> lock(mutex_);
186  current_format = log_format_;
187  }
188 
189  std::string result;
190  if (current_format) {
191  result.reserve(current_format->format_string.length() + message.length() + 32);
192 
193  for (const auto& part : current_format->parsed_format) {
194  switch (part.type) {
195  case FormatPart::LITERAL:
196  result.append(part.value);
197  break;
199  result.append(timestamp.view());
200  break;
201  case FormatPart::LEVEL:
202  result.append(level_str);
203  break;
205  result.append(component);
206  break;
208  result.append(operation);
209  break;
210  case FormatPart::MESSAGE:
211  result.append(message);
212  break;
213  }
214  }
215  }
216 
217  return result;
218  }
219 
220  void write_to_sinks(LogLevel level, const std::string& formatted_message) {
221  std::lock_guard<std::mutex> lock(mutex_);
222  int current_outputs = outputs_.load();
223 
224  if (current_outputs & static_cast<int>(LogOutput::CONSOLE)) {
225  write_to_console(formatted_message);
226  }
227 
228  if (current_outputs & static_cast<int>(LogOutput::FILE)) {
230  write_to_file(formatted_message);
231  }
232 
233  if (current_outputs & static_cast<int>(LogOutput::CALLBACK)) {
234  call_callback(level, formatted_message);
235  }
236  }
237 
238  std::string_view level_to_string(LogLevel level) const {
239  switch (level) {
240  case LogLevel::DEBUG:
241  return "DEBUG";
242  case LogLevel::INFO:
243  return "INFO";
244  case LogLevel::WARNING:
245  return "WARNING";
246  case LogLevel::ERROR:
247  return "ERROR";
248  case LogLevel::CRITICAL:
249  return "CRITICAL";
250  }
251  return "UNKNOWN";
252  }
253 
254  TimestampBuffer get_timestamp(std::chrono::system_clock::time_point timestamp) const {
255  auto time_t = std::chrono::system_clock::to_time_t(timestamp);
256  auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(timestamp.time_since_epoch()) % 1000;
257 
258  static thread_local std::time_t last_t = -1;
259  static thread_local char date_buf[32] = {0};
260 
261  if (time_t != last_t) {
262  std::tm time_info{};
263 #if defined(_WIN32)
264  ::localtime_s(&time_info, &time_t);
265 #else
266  ::localtime_r(&time_t, &time_info);
267 #endif
268  std::strftime(date_buf, sizeof(date_buf), "%Y-%m-%d %H:%M:%S", &time_info);
269  last_t = time_t;
270  }
271 
272  TimestampBuffer result;
273  int len = std::snprintf(result.data, sizeof(result.data), "%s.%03d", date_buf, static_cast<int>(ms.count()));
274  if (len > 0) {
275  if (static_cast<size_t>(len) >= sizeof(result.data)) {
276  result.length = sizeof(result.data) - 1;
277  } else {
278  result.length = static_cast<size_t>(len);
279  }
280  } else {
281  result.length = 0;
282  }
283  return result;
284  }
285 
286  void write_to_console(const std::string& message) const {
287  if (message.find("[ERROR]") != std::string::npos || message.find("[CRITICAL]") != std::string::npos) {
288  std::cerr << message << std::endl;
289  } else {
290 #if defined(_WIN32)
291  std::cout << message << std::endl;
292 #else
293  std::cout << message << '\n';
294 #endif
295  }
296  }
297 
298  void write_to_file(const std::string& message) {
299  if (file_output_ && file_output_->is_open()) {
300  *file_output_ << message << '\n';
301  }
302  }
303 
304  void call_callback(LogLevel level, const std::string& message) {
305  if (callback_) {
306  try {
307  callback_(level, message);
308  } catch (const std::exception& e) {
309  std::cerr << "Error in log callback: " << e.what() << std::endl;
310  } catch (...) {
311  std::cerr << "Unknown error in log callback" << std::endl;
312  }
313  }
314  }
315 
317  if (!log_rotation_ || current_log_file_.empty()) {
318  return;
319  }
320 
321  bool should_rotate = false;
322  if (file_output_ && file_output_->is_open()) {
323  std::streampos current_pos = file_output_->tellp();
324  if (current_pos != static_cast<std::streampos>(-1) &&
325  static_cast<size_t>(current_pos) >= log_rotation_->get_config().max_file_size_bytes) {
326  should_rotate = true;
327  }
328  } else {
329  if (log_rotation_->should_rotate(current_log_file_)) {
330  should_rotate = true;
331  }
332  }
333 
334  if (!should_rotate) {
335  return;
336  }
337 
338  if (file_output_) {
339  file_output_->flush();
340  file_output_->close();
341  file_output_.reset();
342  }
343 
346  }
347 
348  void open_log_file(const std::string& filename) {
349  file_output_ = std::make_unique<std::ofstream>(filename, std::ios::app);
350  if (file_output_->is_open()) {
351  outputs_.fetch_or(static_cast<int>(LogOutput::FILE));
352  } else {
353  file_output_.reset();
354  std::cerr << "Failed to open log file: " << filename << std::endl;
355  }
356  }
357 
358  void setup_async_logging(const AsyncLogConfig& config) {
360 
361  {
362  std::lock_guard<std::mutex> lock(mutex_);
363  async_config_ = config;
365  shutdown_requested_.store(false);
366  running_.store(true);
367  async_enabled_.store(true);
368  }
369 
370  worker_thread_ = std::thread(&Impl::worker_loop, this);
371  }
372 
374  std::thread worker_to_join;
375  bool notify_worker = false;
376  {
377  std::lock_guard<std::mutex> lock(mutex_);
378  async_enabled_.store(false);
379 
380  if (running_.load()) {
381  shutdown_requested_.store(true);
382  notify_worker = true;
383  }
384 
385  if (worker_thread_.joinable()) {
386  worker_to_join = std::move(worker_thread_);
387  }
388  }
389 
390  if (notify_worker) {
391  queue_cv_.notify_all();
392  }
393 
394  if (worker_to_join.joinable()) {
395  worker_to_join.join();
396  }
397 
398  running_.store(false);
399  shutdown_requested_.store(false);
400  }
401 
402  void worker_loop() {
403  std::vector<LogEntry> batch;
404  batch.reserve(async_config_.batch_size);
405 
406  auto last_flush = std::chrono::steady_clock::now();
407 
408  while (true) {
409  std::unique_lock<std::mutex> lock(queue_mutex_);
410 
411  bool has_logs = queue_cv_.wait_for(lock, async_config_.flush_interval,
412  [this] { return !log_queue_.empty() || shutdown_requested_.load(); });
413 
414  if (has_logs && !log_queue_.empty()) {
415  size_t batch_size =
417 
418  batch.clear();
419  batch.reserve(batch_size);
420 
421  for (size_t i = 0; i < batch_size && !log_queue_.empty(); ++i) {
422  batch.push_back(std::move(log_queue_.front()));
423  log_queue_.pop();
424  }
425 
426  lock.unlock();
427 
428  if (!batch.empty()) {
429  process_batch(batch);
430  update_stats_on_batch(batch.size());
431  }
432  } else {
433  lock.unlock();
434  }
435 
436  auto now = std::chrono::steady_clock::now();
437  if (now - last_flush >= async_config_.flush_interval) {
438  flush();
440  last_flush = now;
441  }
442 
443  if (shutdown_requested_.load() && log_queue_.empty()) {
444  break;
445  }
446  }
447 
448  flush();
449  running_.store(false);
450  }
451 
452  void process_batch(const std::vector<LogEntry>& batch) {
453  for (const auto& entry : batch) {
454  std::string formatted_message =
455  format_message(entry.timestamp, entry.level, entry.component, entry.operation, entry.message);
456  write_to_sinks(entry.level, formatted_message);
457  }
458  }
459 
460  bool should_drop_log() const {
461  size_t current_size = get_queue_size();
462  return current_size >= async_config_.max_queue_size;
463  }
464 
465  size_t get_queue_size() const {
466  std::lock_guard<std::mutex> lock(queue_mutex_);
467  return log_queue_.size();
468  }
469 
471  std::lock_guard<std::mutex> lock(stats_mutex_);
473  }
474 
476  std::lock_guard<std::mutex> lock(stats_mutex_);
478  }
479 
480  void update_stats_on_batch(size_t /* batch_size */) {
481  std::lock_guard<std::mutex> lock(stats_mutex_);
483  }
484 
486  std::lock_guard<std::mutex> lock(stats_mutex_);
488  }
489 };
490 
491 Logger::Logger() : impl_(std::make_unique<Impl>()) {}
492 
493 Logger::~Logger() = default;
494 
495 Logger::Logger(Logger&&) noexcept = default;
496 Logger& Logger::operator=(Logger&&) noexcept = default;
497 
498 Logger& Logger::default_logger() {
499  static Logger* instance = new Logger();
500  return *instance;
501 }
502 
504 
505 void Logger::set_level(LogLevel level) { impl_->current_level_.store(level); }
506 
507 LogLevel Logger::get_level() const { return get_impl()->current_level_.load(); }
508 
509 void Logger::set_console_output(bool enable) {
510  std::lock_guard<std::mutex> lock(impl_->mutex_);
511  if (enable) {
512  impl_->outputs_.fetch_or(static_cast<int>(LogOutput::CONSOLE));
513  } else {
514  impl_->outputs_.fetch_and(~static_cast<int>(LogOutput::CONSOLE));
515  }
516 }
517 
518 void Logger::set_file_output(const std::string& filename) {
519  std::lock_guard<std::mutex> lock(impl_->mutex_);
520 
521  if (filename.empty()) {
522  impl_->file_output_.reset();
523  impl_->log_rotation_.reset();
524  impl_->current_log_file_.clear();
525  impl_->outputs_.fetch_and(~static_cast<int>(LogOutput::FILE));
526  } else {
527  impl_->open_log_file(filename);
528  }
529 }
530 
531 void Logger::set_file_output_with_rotation(const std::string& filename, const LogRotationConfig& config) {
532  std::lock_guard<std::mutex> lock(impl_->mutex_);
533 
534  if (filename.empty()) {
535  impl_->file_output_.reset();
536  impl_->log_rotation_.reset();
537  impl_->current_log_file_.clear();
538  impl_->outputs_.fetch_and(~static_cast<int>(LogOutput::FILE));
539  } else {
540  impl_->log_rotation_ = std::make_unique<LogRotation>(config);
541  impl_->current_log_file_ = filename;
542  impl_->open_log_file(filename);
543  }
544 }
545 
546 void Logger::set_async_logging(bool enable, const AsyncLogConfig& config) {
547  if (enable) {
548  impl_->setup_async_logging(config);
549  } else {
550  impl_->teardown_async_logging();
551  }
552 }
553 
554 bool Logger::is_async_logging_enabled() const { return get_impl()->async_enabled_.load(); }
555 
557  std::lock_guard<std::mutex> lock(get_impl()->stats_mutex_);
558  AsyncLogStats result = get_impl()->async_stats_;
559  result.queue_size = get_impl()->get_queue_size();
560  result.max_queue_size_reached = std::max(result.max_queue_size_reached, result.queue_size);
561  return result;
562 }
563 
565  std::lock_guard<std::mutex> lock(impl_->mutex_);
566  impl_->callback_ = std::move(callback);
567  if (impl_->callback_) {
568  impl_->outputs_.fetch_or(static_cast<int>(LogOutput::CALLBACK));
569  } else {
570  impl_->outputs_.fetch_and(~static_cast<int>(LogOutput::CALLBACK));
571  }
572 }
573 
574 void Logger::set_outputs(int outputs) { impl_->outputs_.store(outputs); }
575 
576 void Logger::set_enabled(bool enabled) { impl_->enabled_.store(enabled); }
577 
578 bool Logger::is_enabled() const { return get_impl()->enabled_.load(); }
579 
580 void Logger::set_format(const std::string& format) { impl_->parse_format(format); }
581 
582 void Logger::flush() { impl_->flush(); }
583 
584 void Logger::log(LogLevel level, std::string_view component, std::string_view operation, std::string_view message) {
585  if (!get_impl()->enabled_.load() || level < get_impl()->current_level_.load()) {
586  return;
587  }
588 
589  if (get_impl()->async_enabled_.load()) {
590  LogEntry entry(level, component, operation, message);
591  impl_->update_stats_on_enqueue();
592 
593  if (get_impl()->async_config_.enable_backpressure && impl_->should_drop_log()) {
594  impl_->update_stats_on_drop();
595  return;
596  }
597 
598  {
599  std::lock_guard<std::mutex> lock(impl_->queue_mutex_);
600  impl_->log_queue_.push(entry);
601  }
602 
603  impl_->queue_cv_.notify_one();
604  return;
605  }
606 
607  std::string formatted_message =
608  impl_->format_message(std::chrono::system_clock::now(), level, component, operation, message);
609  if (get_impl()->outputs_.load(std::memory_order_relaxed) != 0) {
610  impl_->write_to_sinks(level, formatted_message);
611  }
612 }
613 
614 void Logger::debug(std::string_view component, std::string_view operation, std::string_view message) {
615  log(LogLevel::DEBUG, component, operation, message);
616 }
617 
618 void Logger::info(std::string_view component, std::string_view operation, std::string_view message) {
619  log(LogLevel::INFO, component, operation, message);
620 }
621 
622 void Logger::warning(std::string_view component, std::string_view operation, std::string_view message) {
623  log(LogLevel::WARNING, component, operation, message);
624 }
625 
626 void Logger::error(std::string_view component, std::string_view operation, std::string_view message) {
627  log(LogLevel::ERROR, component, operation, message);
628 }
629 
630 void Logger::critical(std::string_view component, std::string_view operation, std::string_view message) {
631  log(LogLevel::CRITICAL, component, operation, message);
632 }
633 
634 } // namespace diagnostics
635 } // namespace unilink