quill
RotatingSink.h
1 
7 #pragma once
8 
9 #include "quill/core/Attributes.h"
10 #include "quill/core/Common.h"
11 #include "quill/core/Filesystem.h"
12 #include "quill/core/LogLevel.h"
13 #include "quill/core/QuillError.h"
14 #include "quill/core/TimeUtilities.h"
15 #include "quill/sinks/FileSink.h"
16 #include "quill/sinks/StreamSink.h"
17 
18 #include <algorithm>
19 #include <chrono>
20 #include <cstdint>
21 #include <ctime>
22 #include <deque>
23 #include <limits>
24 #include <string>
25 #include <string_view>
26 #include <system_error>
27 #include <thread>
28 #include <utility>
29 #include <vector>
30 
31 QUILL_BEGIN_NAMESPACE
32 
34 class MacroMetadata;
35 
40 {
41 public:
45  enum class RotationFrequency : uint8_t
46  {
47  Disabled,
48  Daily,
49  Hourly,
50  Minutely
51  };
52 
56  enum class RotationNamingScheme : uint8_t
57  {
58  Index,
59  Date,
60  DateAndTime
61  };
62 
66  RotatingFileSinkConfig() : _daily_rotation_time{_disabled_daily_rotation_time()} {}
67 
72  QUILL_ATTRIBUTE_COLD void set_rotation_max_file_size(size_t value)
73  {
74  if (value < 512)
75  {
76  QUILL_THROW(QuillError{"rotation_max_file_size must be greater than or equal to 512 bytes"});
77  }
78 
79  _rotation_max_file_size = value;
80  }
81 
87  QUILL_ATTRIBUTE_COLD void set_rotation_frequency_and_interval(char frequency, uint32_t interval)
88  {
89  if (frequency == 'M' || frequency == 'm')
90  {
91  _rotation_frequency = RotationFrequency::Minutely;
92  }
93  else if (frequency == 'H' || frequency == 'h')
94  {
95  _rotation_frequency = RotationFrequency::Hourly;
96  }
97  else
98  {
99  QUILL_THROW(QuillError{
100  "Invalid frequency. Valid values are 'M' or 'm' for minutes or 'H' or 'h' for hours"});
101  }
102 
103  if (interval == 0)
104  {
105  QUILL_THROW(QuillError{"interval must be set to a value greater than 0"});
106  }
107 
108  _rotation_interval = interval;
109  _daily_rotation_time = _disabled_daily_rotation_time();
110  }
111 
116  QUILL_ATTRIBUTE_COLD void set_rotation_time_daily(std::string const& daily_rotation_time_str)
117  {
118  _rotation_frequency = RotationFrequency::Daily;
119  _rotation_interval = 0;
120  _daily_rotation_time = _parse_daily_rotation_time(daily_rotation_time_str);
121  }
122 
127  QUILL_ATTRIBUTE_COLD void set_max_backup_files(uint32_t value) { _max_backup_files = value; }
128 
135  QUILL_ATTRIBUTE_COLD void set_overwrite_rolled_files(bool value)
136  {
137  _overwrite_rolled_files = value;
138  }
139 
147  QUILL_ATTRIBUTE_COLD void set_remove_old_files(bool value) { _remove_old_files = value; }
148 
154  QUILL_ATTRIBUTE_COLD void set_rotation_naming_scheme(RotationNamingScheme value)
155  {
156  _rotation_naming_scheme = value;
157  }
158 
166  QUILL_ATTRIBUTE_COLD void set_rotation_on_creation(bool value)
167  {
168  _rotation_on_creation = value;
169  }
170 
172  QUILL_NODISCARD size_t rotation_max_file_size() const noexcept { return _rotation_max_file_size; }
173  QUILL_NODISCARD uint32_t max_backup_files() const noexcept { return _max_backup_files; }
174  QUILL_NODISCARD bool overwrite_rolled_files() const noexcept { return _overwrite_rolled_files; }
175  QUILL_NODISCARD bool remove_old_files() const noexcept { return _remove_old_files; }
176  QUILL_NODISCARD RotationFrequency rotation_frequency() const noexcept
177  {
178  return _rotation_frequency;
179  }
180  QUILL_NODISCARD uint32_t rotation_interval() const noexcept { return _rotation_interval; }
181  QUILL_NODISCARD std::pair<std::chrono::hours, std::chrono::minutes> daily_rotation_time() const noexcept
182  {
183  return _daily_rotation_time;
184  }
185  QUILL_NODISCARD RotationNamingScheme rotation_naming_scheme() const noexcept
186  {
187  return _rotation_naming_scheme;
188  }
189  QUILL_NODISCARD bool rotation_on_creation() const noexcept { return _rotation_on_creation; }
190 
191 private:
192  /***/
193  static std::pair<std::chrono::hours, std::chrono::minutes> _disabled_daily_rotation_time() noexcept
194  {
195  return std::make_pair(std::chrono::hours{std::numeric_limits<std::chrono::hours::rep>::max()},
196  std::chrono::minutes{std::numeric_limits<std::chrono::hours::rep>::max()});
197  }
198 
199  /***/
200  static std::pair<std::chrono::hours, std::chrono::minutes> _parse_daily_rotation_time(std::string const& daily_rotation_time_str)
201  {
202  std::vector<std::string> tokens;
203  std::string token;
204  size_t start = 0, end = 0;
205 
206  while ((end = daily_rotation_time_str.find(':', start)) != std::string::npos)
207  {
208  token = daily_rotation_time_str.substr(start, end - start);
209  tokens.push_back(token);
210  start = end + 1;
211  }
212 
213  // Add the last token (or the only token if there's no delimiter)
214  token = daily_rotation_time_str.substr(start);
215  tokens.push_back(token);
216 
217  if (tokens.size() != 2)
218  {
219  QUILL_THROW(
220  QuillError{"Invalid daily_rotation_time_str value format. The format should be `HH:MM`."});
221  }
222 
223  for (auto const& parsed_token : tokens)
224  {
225  if (parsed_token.size() != 2)
226  {
227  QUILL_THROW(QuillError{
228  "Invalid daily_rotation_time_str value format. Each component of the time (HH and MM) "
229  "should be two digits."});
230  }
231  }
232 
233  auto const daily_rotation_time_str_tp = std::make_pair(
234  std::chrono::hours{std::stoi(tokens[0])}, std::chrono::minutes{std::stoi(tokens[1])});
235 
236  if ((daily_rotation_time_str_tp.first > std::chrono::hours{23}) ||
237  (daily_rotation_time_str_tp.second > std::chrono::minutes{59}))
238  {
239  QUILL_THROW(
240  QuillError("Invalid rotation values. The hour value should be between 00 and 23, and the "
241  "minute value should be between 00 and 59."));
242  }
243 
244  return daily_rotation_time_str_tp;
245  }
246 
247 private:
248  std::pair<std::chrono::hours, std::chrono::minutes> _daily_rotation_time;
249  size_t _rotation_max_file_size{0}; // 0 means disabled
250  uint32_t _max_backup_files{std::numeric_limits<uint32_t>::max()}; // max means disabled
251  uint32_t _rotation_interval{0}; // 0 means disabled
252  RotationFrequency _rotation_frequency{RotationFrequency::Disabled};
253  RotationNamingScheme _rotation_naming_scheme{RotationNamingScheme::Index};
254  bool _overwrite_rolled_files{true};
255  bool _remove_old_files{true};
256  bool _rotation_on_creation{false};
257 };
258 
262 template <typename TBase>
263 class RotatingSink : public TBase
264 {
265 public:
266  using base_type = TBase;
267 
278  RotatingSink(fs::path const& filename, RotatingFileSinkConfig const& config,
279  FileEventNotifier file_event_notifier = FileEventNotifier{},
280  std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now())
281  : base_type(filename, static_cast<FileSinkConfig const&>(config),
282  std::move(file_event_notifier), false, start_time),
283  _config(config)
284  {
285  uint64_t const today_timestamp_ns = static_cast<uint64_t>(
286  std::chrono::duration_cast<std::chrono::nanoseconds>(start_time.time_since_epoch()).count());
287 
288  _clean_and_recover_files(this->_filename, _config.open_mode(), today_timestamp_ns);
289 
290  if (_config.rotation_frequency() != RotatingFileSinkConfig::RotationFrequency::Disabled)
291  {
292  // Calculate next rotation time
293  _next_rotation_time = _calculate_initial_rotation_tp(
294  static_cast<uint64_t>(
295  std::chrono::duration_cast<std::chrono::nanoseconds>(start_time.time_since_epoch()).count()),
296  config);
297  }
298 
299  // Open file for logging
300  this->open_file(this->_filename, _config.open_mode());
301  _open_file_timestamp = static_cast<uint64_t>(
302  std::chrono::duration_cast<std::chrono::nanoseconds>(start_time.time_since_epoch()).count());
303 
304  _created_files.emplace_front(this->_filename, 0, std::string{});
305 
306  // Check if we need to rotate on creation
307  if (_config.rotation_on_creation() && !this->is_null() && _get_file_size(this->_filename) > 0)
308  {
309  _rotate_files(today_timestamp_ns);
310  }
311 
312  if (!this->is_null())
313  {
314  this->_file_size = _get_file_size(this->_filename);
315  }
316  }
317 
318  ~RotatingSink() override = default;
319 
335  QUILL_ATTRIBUTE_HOT void write_log(MacroMetadata const* log_metadata, uint64_t log_timestamp,
336  std::string_view thread_id, std::string_view thread_name,
337  std::string const& process_id, std::string_view logger_name,
338  LogLevel log_level, std::string_view log_level_description,
339  std::string_view log_level_short_code,
340  std::vector<std::pair<std::string, std::string>> const* named_args,
341  std::string_view log_message, std::string_view log_statement) override
342  {
343  if (this->is_null())
344  {
345  base_type::write_log(log_metadata, log_timestamp, thread_id, thread_name, process_id,
346  logger_name, log_level, log_level_description, log_level_short_code,
347  named_args, log_message, log_statement);
348  return;
349  }
350 
351  bool time_rotation = false;
352 
353  if (_config.rotation_frequency() != RotatingFileSinkConfig::RotationFrequency::Disabled)
354  {
355  // Check if we need to rotate based on time
356  time_rotation = _time_rotation(log_timestamp);
357  }
358 
359  if (!time_rotation && _config.rotation_max_file_size() != 0)
360  {
361  // Check if we need to rotate based on size
362  _size_rotation(log_statement.size(), log_timestamp);
363  }
364 
365  // write to file
366  base_type::write_log(log_metadata, log_timestamp, thread_id, thread_name, process_id,
367  logger_name, log_level, log_level_description, log_level_short_code,
368  named_args, log_message, log_statement);
369  }
370 
371 private:
372  /***/
373  QUILL_NODISCARD bool _time_rotation(uint64_t record_timestamp_ns)
374  {
375  if (record_timestamp_ns >= _next_rotation_time)
376  {
377  _rotate_files(record_timestamp_ns);
378  _next_rotation_time = _calculate_rotation_tp(record_timestamp_ns, _config);
379  return true;
380  }
381 
382  return false;
383  }
384 
385  /***/
386  void _size_rotation(size_t log_msg_size, uint64_t record_timestamp_ns)
387  {
388  // Calculate the new size of the file
389  if ((this->_file_size + log_msg_size) > _config.rotation_max_file_size())
390  {
391  _rotate_files(record_timestamp_ns);
392  }
393  }
394 
395  /***/
396  void _rotate_files(uint64_t record_timestamp_ns)
397  {
398  if ((_created_files.size() > _config.max_backup_files()) && !_config.overwrite_rolled_files())
399  {
400  // We have reached the max number of backup files, and we are not allowed to overwrite the
401  // oldest file. We will stop rotating
402  return;
403  }
404 
405  if (!this->_file)
406  {
407  this->open_file(this->_filename, _reopen_mode_after_failed_rotation(_config.open_mode()));
408  _open_file_timestamp = record_timestamp_ns;
409  this->_file_size = _get_file_size(this->_filename);
410  return;
411  }
412 
413  // We need to flush and also fsync before actually getting the size of the file
414  base_type::flush_sink();
415  base_type::fsync_file(true);
416 
417  if (_get_file_size(this->_filename) <= 0)
418  {
419  // Also check the file size is > 0 to better deal with full disk
420  return;
421  }
422 
423  this->close_file();
424 
425  // datetime_suffix will be empty if we are using the default naming scheme
426  std::string datetime_suffix;
427  if (_config.rotation_naming_scheme() == RotatingFileSinkConfig::RotationNamingScheme::Date)
428  {
429  datetime_suffix =
430  this->format_datetime_string(_open_file_timestamp, _config.timezone(), "%Y%m%d");
431  }
432  else if (_config.rotation_naming_scheme() == RotatingFileSinkConfig::RotationNamingScheme::DateAndTime)
433  {
434  datetime_suffix =
435  this->format_datetime_string(_open_file_timestamp, _config.timezone(), "%Y%m%d_%H%M%S");
436  }
437 
438  // We need to rotate the files and rename them with an index
439  for (auto it = _created_files.rbegin(); it != _created_files.rend(); ++it)
440  {
441  // Create each existing filename on disk with the existing index.
442  // when the index is 0 we want to rename the latest file
443  fs::path existing_file;
444  fs::path renamed_file;
445 
446  existing_file = _get_filename(it->base_filename, it->index, it->date_time);
447 
448  // increment the index if needed and rename the file
449  uint32_t index_to_use = it->index;
450 
451  if (_config.rotation_naming_scheme() == RotatingFileSinkConfig::RotationNamingScheme::Index ||
452  it->date_time == datetime_suffix)
453  {
454  // we are rotating and incrementing the index, or we have another file with the same date_time suffix
455  index_to_use += 1;
456 
457  renamed_file = _get_filename(it->base_filename, index_to_use, datetime_suffix);
458 
459  it->index = index_to_use;
460  it->date_time = datetime_suffix;
461 
462  _rename_file(existing_file, renamed_file);
463  }
464  else if (it->date_time.empty())
465  {
466  // we are renaming the latest file
467  index_to_use = it->index;
468 
469  renamed_file = _get_filename(it->base_filename, index_to_use, datetime_suffix);
470 
471  it->index = index_to_use;
472  it->date_time = datetime_suffix;
473 
474  _rename_file(existing_file, renamed_file);
475  }
476  }
477 
478  // Check if we have too many files in the queue remove_file the oldest one
479  if (_created_files.size() > _config.max_backup_files())
480  {
481  // remove_file that file from the system and also pop it from the queue
482  fs::path const removed_file = _get_filename(
483  _created_files.back().base_filename, _created_files.back().index, _created_files.back().date_time);
484  _remove_file(removed_file);
485  _created_files.pop_back();
486  }
487 
488  // add the current file back to the list with index 0
489  _created_files.emplace_front(this->_filename, 0, std::string{});
490 
491  // Open file for logging
492  this->open_file(this->_filename, _config.open_mode());
493  _open_file_timestamp = record_timestamp_ns;
494  this->_file_size = 0;
495  }
496 
497  /***/
498  void _clean_and_recover_files(fs::path const& filename, std::string const& open_mode, uint64_t today_timestamp_ns)
499  {
500  if ((_config.rotation_naming_scheme() != RotatingFileSinkConfig::RotationNamingScheme::Index) &&
501  (_config.rotation_naming_scheme() != RotatingFileSinkConfig::RotationNamingScheme::Date))
502  {
503  // clean and recover is only supported for index and date naming scheme, when using
504  // DateAndTime there are no collisions in the filenames
505  return;
506  }
507 
508  // if we are starting in "w" mode, then we also should clean all previous log files of the previous run
509  if (_config.remove_old_files() && (open_mode.find('w') != std::string::npos))
510  {
511  for (auto const& entry : fs::directory_iterator(fs::current_path() / filename.parent_path()))
512  {
513  if (entry.path().extension().string() != filename.extension().string())
514  {
515  // we only check for the files of the same extension to remove
516  continue;
517  }
518 
519  // is_directory() does not exist in std::experimental::filesystem
520  if (entry.path().filename().string().find(filename.stem().string() + ".") != 0)
521  {
522  // expect to find filename.stem().string() exactly at the start of the filename
523  continue;
524  }
525 
526  if (_config.rotation_naming_scheme() == RotatingFileSinkConfig::RotationNamingScheme::Index)
527  {
528  fs::remove(entry);
529  }
530  else if (_config.rotation_naming_scheme() == RotatingFileSinkConfig::RotationNamingScheme::Date)
531  {
532  // Find the first dot in the filename
533  // stem will be something like `logfile.1`
534  if (size_t const pos = entry.path().stem().string().find_last_of('.'); pos != std::string::npos)
535  {
536  // Get the today's date, we won't remove the files of the previous dates as they won't collide
537  std::string const today_date =
538  this->format_datetime_string(today_timestamp_ns, _config.timezone(), "%Y%m%d");
539 
540  if (std::string const index_or_date =
541  entry.path().stem().string().substr(pos + 1, entry.path().stem().string().length());
542  (index_or_date.length() >= 8) && (index_or_date == today_date))
543  {
544  // assume it is a date, no need to find the index
545  if (index_or_date == today_date)
546  {
547  fs::remove(entry);
548  }
549  }
550  else
551  {
552  // assume it is an index
553  // Find the second last dot to get the date
554  std::string const filename_with_date = entry.path().filename().string().substr(0, pos);
555 
556  if (size_t const second_last = filename_with_date.find_last_of('.'); second_last != std::string::npos)
557  {
558  if (std::string const date_part =
559  filename_with_date.substr(second_last + 1, filename_with_date.length());
560  date_part == today_date)
561  {
562  fs::remove(entry);
563  }
564  }
565  }
566  }
567  }
568  }
569  }
570  else if (open_mode.find('a') != std::string::npos)
571  {
572  // we need to recover the index from the existing files
573  for (auto const& entry : fs::directory_iterator(fs::current_path() / filename.parent_path()))
574  {
575  // is_directory() does not exist in std::experimental::filesystem
576  if (entry.path().extension().string() != filename.extension().string())
577  {
578  // we only check for the files of the same extension to remove
579  continue;
580  }
581 
582  // is_directory() does not exist in std::experimental::filesystem
583  if (entry.path().filename().string().find(filename.stem().string() + ".") != 0)
584  {
585  // expect to find filename.stem().string() exactly at the start of the filename
586  continue;
587  }
588 
589  std::string const extension = entry.path().extension().string(); // e.g. ".log"
590 
591  // stem will be something like `logfile.1`
592  if (size_t const pos = entry.path().stem().string().find_last_of('.'); pos != std::string::npos)
593  {
594  if (_config.rotation_naming_scheme() == RotatingFileSinkConfig::RotationNamingScheme::Index)
595  {
596  std::string const index =
597  entry.path().stem().string().substr(pos + 1, entry.path().stem().string().length());
598 
599  std::string const current_filename = entry.path().filename().string().substr(0, pos) + extension;
600  fs::path current_file = entry.path().parent_path();
601  current_file.append(current_filename);
602 
603  // Attempt to convert the index to a number
604  QUILL_TRY
605  {
606  _created_files.emplace_front(current_file, static_cast<uint32_t>(std::stoul(index)),
607  std::string{});
608  }
609  QUILL_CATCH_ALL() { continue; }
610  }
611  else if (_config.rotation_naming_scheme() == RotatingFileSinkConfig::RotationNamingScheme::Date)
612  {
613  // Get the today's date, we won't remove the files of the previous dates as they won't collide
614  std::string const today_date =
615  this->format_datetime_string(today_timestamp_ns, _config.timezone(), "%Y%m%d");
616 
617  if (std::string const index_or_date =
618  entry.path().stem().string().substr(pos + 1, entry.path().stem().string().length());
619  (index_or_date.length() >= 8) && (index_or_date == today_date))
620  {
621  // assume it is a date, no need to find the index
622  std::string const current_filename = entry.path().filename().string().substr(0, pos) + extension;
623  fs::path current_file = entry.path().parent_path();
624  current_file.append(current_filename);
625 
626  _created_files.emplace_front(current_file, 0, index_or_date);
627  }
628  else
629  {
630  // assume it is an index
631  // Find the second last dot to get the date
632  std::string const filename_with_date = entry.path().filename().string().substr(0, pos);
633 
634  if (size_t const second_last = filename_with_date.find_last_of('.'); second_last != std::string::npos)
635  {
636  if (std::string const date_part =
637  filename_with_date.substr(second_last + 1, filename_with_date.length());
638  date_part == today_date)
639  {
640  std::string const current_filename = filename_with_date.substr(0, second_last) + extension;
641  fs::path current_file = entry.path().parent_path();
642  current_file.append(current_filename);
643 
644  // Attempt to convert the index to a number
645  QUILL_TRY
646  {
647  _created_files.emplace_front(
648  current_file, static_cast<uint32_t>(std::stoul(index_or_date)), date_part);
649  }
650  QUILL_CATCH_ALL() { continue; }
651  }
652  }
653  }
654  }
655  }
656  }
657 
658  // finally we need to sort the deque
659  std::sort(_created_files.begin(), _created_files.end(),
660  [](FileInfo const& a, FileInfo const& b) { return a.index < b.index; });
661  }
662  }
663 
664  /***/
665  QUILL_NODISCARD static std::string _reopen_mode_after_failed_rotation(
666  std::string const& open_mode)
667  {
668  if (!open_mode.empty() && (open_mode.front() == 'w'))
669  {
670  std::string reopen_mode = open_mode;
671  reopen_mode.front() = 'a';
672  return reopen_mode;
673  }
674 
675  return open_mode;
676  }
677 
678  /***/
679  QUILL_NODISCARD static size_t _get_file_size(fs::path const& filename)
680  {
681  return static_cast<size_t>(fs::file_size(filename));
682  }
683 
684  /***/
685  static void _remove_file(fs::path const& filename) noexcept
686  {
687  std::error_code ec;
688 
689  fs::file_status const status = fs::status(filename, ec);
690 
691  if (ec || status.type() != fs::file_type::regular)
692  {
693  // File doesn't exist or is not a regular file
694  return;
695  }
696 
697  fs::remove(filename, ec);
698  }
699 
700  /***/
701  bool static _rename_file(fs::path const& previous_file, fs::path const& new_file) noexcept
702  {
703  std::error_code ec;
704  fs::rename(previous_file, new_file, ec);
705 
706  if (ec)
707  {
708  // Retry once after a delay - workaround for Windows antivirus locking files
709  // This is a common issue where antivirus software temporarily locks files during scanning
710  std::this_thread::sleep_for(std::chrono::milliseconds{250});
711 
712  ec.clear();
713  fs::rename(previous_file, new_file, ec);
714 
715  if (ec)
716  {
717  return false;
718  }
719  }
720 
721  return true;
722  }
723 
724  /***/
725  QUILL_NODISCARD static fs::path _append_index_to_filename(fs::path const& filename, uint32_t index) noexcept
726  {
727  if (index == 0u)
728  {
729  return filename;
730  }
731 
732  // Get base file and extension
733  auto const [stem, ext] = base_type::extract_stem_and_extension(filename);
734  return fs::path{stem + "." + std::to_string(index) + ext};
735  }
736 
737  /***/
738  QUILL_NODISCARD static fs::path _append_string_to_filename(fs::path const& filename, std::string const& text) noexcept
739  {
740  if (text.empty())
741  {
742  return filename;
743  }
744 
745  // Get base file and extension
746  auto const [stem, ext] = base_type::extract_stem_and_extension(filename);
747  return fs::path{stem + "." + text + ext};
748  }
749 
750  /***/
751  static uint64_t _calculate_initial_rotation_tp(uint64_t start_time_ns, RotatingFileSinkConfig const& config)
752  {
753 // time_t on i386 is 32 bits so casting out of range number results in zero
754 #if (defined(__i386))
755  time_t const time_now = static_cast<time_t>(start_time_ns / 1000000000);
756 #else
757  time_t const time_now = static_cast<time_t>(start_time_ns) / 1000000000;
758 #endif
759  tm date;
760 
761  // here we do this because of `daily_rotation_time_str` that might have specified the time in UTC
762  if (config.timezone() == Timezone::GmtTime)
763  {
764  detail::gmtime_rs(&time_now, &date);
765  }
766  else
767  {
768  detail::localtime_rs(&time_now, &date);
769  }
770 
771  // update to the desired date
772  if (config.rotation_frequency() == RotatingFileSinkConfig::RotationFrequency::Minutely)
773  {
774  date.tm_min += 1;
775  date.tm_sec = 0;
776  }
777  else if (config.rotation_frequency() == RotatingFileSinkConfig::RotationFrequency::Hourly)
778  {
779  date.tm_hour += 1;
780  date.tm_min = 0;
781  date.tm_sec = 0;
782  }
783  else if (config.rotation_frequency() == RotatingFileSinkConfig::RotationFrequency::Daily)
784  {
785  date.tm_hour = static_cast<decltype(date.tm_hour)>(config.daily_rotation_time().first.count());
786  date.tm_min = static_cast<decltype(date.tm_min)>(config.daily_rotation_time().second.count());
787  date.tm_sec = 0;
788  }
789  else
790  {
791  QUILL_THROW(QuillError{"Invalid rotation frequency"});
792  }
793 
794  // convert back to timestamp
795  time_t const rotation_time =
796  (config.timezone() == Timezone::GmtTime) ? detail::timegm(&date) : std::mktime(&date);
797 
798  uint64_t const rotation_time_seconds = (rotation_time > time_now)
799  ? static_cast<uint64_t>(rotation_time)
800  : static_cast<uint64_t>(rotation_time + std::chrono::seconds{std::chrono::hours{24}}.count());
801 
802  return static_cast<uint64_t>(
803  std::chrono::nanoseconds{std::chrono::seconds{rotation_time_seconds}}.count());
804  }
805 
806  /***/
807  static uint64_t _calculate_rotation_tp(uint64_t rotation_timestamp_ns, RotatingFileSinkConfig const& config)
808  {
809  if (config.rotation_frequency() == RotatingFileSinkConfig::RotationFrequency::Minutely)
810  {
811  return rotation_timestamp_ns +
812  static_cast<uint64_t>(
813  std::chrono::nanoseconds{std::chrono::minutes{config.rotation_interval()}}.count());
814  }
815 
816  if (config.rotation_frequency() == RotatingFileSinkConfig::RotationFrequency::Hourly)
817  {
818  return rotation_timestamp_ns +
819  static_cast<uint64_t>(
820  std::chrono::nanoseconds{std::chrono::hours{config.rotation_interval()}}.count());
821  }
822 
823  if (config.rotation_frequency() == RotatingFileSinkConfig::RotationFrequency::Daily)
824  {
825  return rotation_timestamp_ns + std::chrono::nanoseconds{std::chrono::hours{24}}.count();
826  }
827 
828  QUILL_THROW(QuillError{"Invalid rotation frequency"});
829  }
830 
831  /***/
832  static fs::path _get_filename(fs::path filename, uint32_t index, std::string const& date_time)
833  {
834  if (!date_time.empty())
835  {
836  filename = _append_string_to_filename(filename, date_time);
837  }
838 
839  if (index > 0)
840  {
841  filename = _append_index_to_filename(filename, index);
842  }
843 
844  return filename;
845  }
846 
847 protected:
848  struct FileInfo
849  {
850  FileInfo(fs::path base_filename, uint32_t index, std::string date_time)
851  : base_filename{std::move(base_filename)}, date_time{std::move(date_time)}, index{index}
852  {
853  }
854 
855  fs::path base_filename;
856  std::string date_time;
857  uint32_t index;
858  };
859 
860  FileEventNotifier _file_event_notifier;
861  std::deque<FileInfo> _created_files;
863  uint64_t _open_file_timestamp{0};
864  RotatingFileSinkConfig _config;
865 };
866 
867 QUILL_END_NAMESPACE
QUILL_ATTRIBUTE_COLD void set_max_backup_files(uint32_t value)
Sets the maximum number of log files to keep.
Definition: RotatingSink.h:127
Definition: RotatingSink.h:848
QUILL_ATTRIBUTE_COLD void set_rotation_max_file_size(size_t value)
Sets the maximum file size for rotation.
Definition: RotatingSink.h:72
QUILL_ATTRIBUTE_COLD void set_rotation_naming_scheme(RotationNamingScheme value)
Sets the naming scheme for the rotated files.
Definition: RotatingSink.h:154
std::deque< FileInfo > _created_files
We store in a queue the filenames we created, first: index, second: date/datetime, third: base_filename.
Definition: RotatingSink.h:861
QUILL_NODISCARD size_t rotation_max_file_size() const noexcept
Getter methods.
Definition: RotatingSink.h:172
Captures and stores information about a logging event in compile time.
Definition: MacroMetadata.h:22
QUILL_ATTRIBUTE_HOT void write_log(MacroMetadata const *log_metadata, uint64_t log_timestamp, std::string_view thread_id, std::string_view thread_name, std::string const &process_id, std::string_view logger_name, LogLevel log_level, std::string_view log_level_description, std::string_view log_level_short_code, std::vector< std::pair< std::string, std::string >> const *named_args, std::string_view log_message, std::string_view log_statement) override
Writes a formatted log message to the stream.
Definition: RotatingSink.h:335
tm * localtime_rs(time_t const *timer, tm *buf)
Portable localtime_r or _s per operating system.
Definition: TimeUtilities.h:59
RotationFrequency
The frequency of log file rotation.
Definition: RotatingSink.h:45
RotatingFileSinkConfig()
Constructs a new RotatingFileSinkConfig object.
Definition: RotatingSink.h:66
uint64_t _next_rotation_time
The next rotation time point.
Definition: RotatingSink.h:862
RotationNamingScheme
The naming scheme for rotated log files.
Definition: RotatingSink.h:56
tm * gmtime_rs(time_t const *timer, tm *buf)
Portable gmtime_r or _s per operating system.
Definition: TimeUtilities.h:31
time_t timegm(tm *tm)
inverses of gmtime
Definition: TimeUtilities.h:85
QUILL_ATTRIBUTE_COLD void set_rotation_time_daily(std::string const &daily_rotation_time_str)
Sets the time of day for daily log file rotation.
Definition: RotatingSink.h:116
custom exception
Definition: QuillError.h:45
QUILL_ATTRIBUTE_COLD void set_rotation_on_creation(bool value)
Sets whether to force rotation on file sink creation/startup.
Definition: RotatingSink.h:166
The FileSinkConfig class holds the configuration options for the FileSink.
Definition: FileSink.h:64
QUILL_ATTRIBUTE_COLD void set_remove_old_files(bool value)
Sets whether previous rotated log files should be removed on process start up.
Definition: RotatingSink.h:147
Notifies on file events by calling the appropriate callback, the callback is executed on the backend ...
Definition: StreamSink.h:55
The RotatingSink class.
Definition: RotatingSink.h:263
QUILL_ATTRIBUTE_COLD void set_overwrite_rolled_files(bool value)
Sets whether the oldest rolled logs should be overwritten when the maximum backup count is reached...
Definition: RotatingSink.h:135
RotatingSink(fs::path const &filename, RotatingFileSinkConfig const &config, FileEventNotifier file_event_notifier=FileEventNotifier{}, std::chrono::system_clock::time_point start_time=std::chrono::system_clock::now())
Constructor.
Definition: RotatingSink.h:278
QUILL_ATTRIBUTE_COLD void set_rotation_frequency_and_interval(char frequency, uint32_t interval)
Sets the frequency and interval of file rotation.
Definition: RotatingSink.h:87
The configuration options for the RotatingSink.
Definition: RotatingSink.h:39