quill
FileSink.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/QuillError.h"
13 #include "quill/core/ThreadPrimitives.h"
14 #include "quill/core/TimeUtilities.h"
15 #include "quill/sinks/StreamSink.h"
16 
17 #include <cerrno>
18 #include <chrono>
19 #include <cstdint>
20 #include <cstdio>
21 #include <cstring>
22 #include <ctime>
23 #include <memory>
24 #include <string>
25 #include <string_view>
26 #include <utility>
27 #include <vector>
28 
29 #if defined(_WIN32)
30  #if !defined(WIN32_LEAN_AND_MEAN)
31  #define WIN32_LEAN_AND_MEAN
32  #endif
33 
34  #if !defined(NOMINMAX)
35  // Mingw already defines this, so no need to redefine
36  #define NOMINMAX
37  #endif
38 
39  #include <io.h>
40  #include <share.h>
41  #include <windows.h>
42 #else
43  #include <fcntl.h>
44  #include <unistd.h>
45 #endif
46 
47 QUILL_BEGIN_NAMESPACE
48 
49 #if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
50  #pragma warning(push)
51  #pragma warning(disable : 4996)
52 #endif
53 
54 QUILL_BEGIN_EXPORT
55 
56 enum class FilenameAppendOption : uint8_t
57 {
58  None,
59  StartDate,
60  StartDateTime,
61  StartCustomTimestampFormat
62 };
63 
68 {
69 public:
84  QUILL_ATTRIBUTE_COLD void set_filename_append_option(
85  FilenameAppendOption value, std::string_view append_filename_format_pattern = std::string_view{})
86  {
87  _filename_append_option = value;
88 
89  if (_filename_append_option == FilenameAppendOption::StartCustomTimestampFormat)
90  {
91  if (append_filename_format_pattern.empty())
92  {
93  QUILL_THROW(
94  QuillError{"The 'StartCustomTimestampFormat' option was specified, but no format pattern "
95  "was provided. "
96  "Please set a valid strftime format pattern"});
97  }
98 
99  _append_filename_format_pattern = append_filename_format_pattern;
100  }
101  else if (_filename_append_option == FilenameAppendOption::StartDateTime)
102  {
103  _append_filename_format_pattern = "_%Y%m%d_%H%M%S";
104  }
105  else if (_filename_append_option == FilenameAppendOption::StartDate)
106  {
107  _append_filename_format_pattern = "_%Y%m%d";
108  }
109  }
110 
118  QUILL_ATTRIBUTE_COLD void set_timezone(Timezone time_zone) { _time_zone = time_zone; }
119 
129  QUILL_ATTRIBUTE_COLD void set_fsync_enabled(bool value) { _fsync_enabled = value; }
130 
135  QUILL_ATTRIBUTE_COLD void set_open_mode(char open_mode) { _open_mode = open_mode; }
136 
141  QUILL_ATTRIBUTE_COLD void set_open_mode(std::string_view open_mode) { _open_mode = open_mode; }
142 
154  QUILL_ATTRIBUTE_COLD void set_write_buffer_size(size_t value)
155  {
156  _write_buffer_size = (value == 0) ? 0 : ((value < 4096) ? 4096 : value);
157  }
158 
178  QUILL_ATTRIBUTE_COLD void set_minimum_fsync_interval(std::chrono::milliseconds value)
179  {
180  _minimum_fsync_interval = value;
181  }
182 
192  QUILL_ATTRIBUTE_COLD void set_override_pattern_formatter_options(std::optional<PatternFormatterOptions> const& options)
193  {
194  _override_pattern_formatter_options = options;
195  }
196 
198  QUILL_NODISCARD bool fsync_enabled() const noexcept { return _fsync_enabled; }
199  QUILL_NODISCARD Timezone timezone() const noexcept { return _time_zone; }
200  QUILL_NODISCARD FilenameAppendOption filename_append_option() const noexcept
201  {
202  return _filename_append_option;
203  }
204  QUILL_NODISCARD std::string const& append_filename_format_pattern() const noexcept
205  {
206  return _append_filename_format_pattern;
207  }
208  QUILL_NODISCARD std::string const& open_mode() const noexcept { return _open_mode; }
209  QUILL_NODISCARD size_t write_buffer_size() const noexcept { return _write_buffer_size; }
210  QUILL_NODISCARD std::chrono::milliseconds minimum_fsync_interval() const noexcept
211  {
212  return _minimum_fsync_interval;
213  }
214  QUILL_NODISCARD std::optional<PatternFormatterOptions> const& override_pattern_formatter_options() const noexcept
215  {
216  return _override_pattern_formatter_options;
217  }
218 
219 private:
220  std::string _open_mode{'a'};
221  std::string _append_filename_format_pattern;
222  size_t _write_buffer_size{64 * 1024}; // Default size 64k
223  std::chrono::milliseconds _minimum_fsync_interval{0};
224  std::optional<PatternFormatterOptions> _override_pattern_formatter_options;
225  Timezone _time_zone{Timezone::LocalTime};
226  FilenameAppendOption _filename_append_option{FilenameAppendOption::None};
227  bool _fsync_enabled{false};
228 };
229 
241 #if defined(_WIN32)
242 class FileSinkBase : public StreamSink
243 {
244 public:
245  explicit FileSinkBase(fs::path const& filename, FileSinkConfig const& config, FileEventNotifier file_event_notifier,
246  bool /* do_fopen */, std::chrono::system_clock::time_point start_time)
247  : StreamSink(_get_updated_filename_with_appended_datetime(filename, config.filename_append_option(),
248  config.append_filename_format_pattern(),
249  config.timezone(), start_time),
250  nullptr, config.override_pattern_formatter_options(), std::move(file_event_notifier)),
251  _config(config)
252  {
253  if (!_config.fsync_enabled() && (_config.minimum_fsync_interval().count() != 0))
254  {
255  QUILL_THROW(
256  QuillError{"Cannot set a non-zero minimum fsync interval when fsync is disabled."});
257  }
258  }
259 
260  ~FileSinkBase() override = default;
261 
262 protected:
263  QUILL_NODISCARD static std::string format_datetime_string(uint64_t timestamp_ns, Timezone time_zone,
264  std::string const& append_format_pattern)
265  {
266  auto const time_now = static_cast<time_t>(timestamp_ns / 1000000000);
267  tm now_tm;
268 
269  if (time_zone == Timezone::GmtTime)
270  {
271  detail::gmtime_rs(&time_now, &now_tm);
272  }
273  else
274  {
275  detail::localtime_rs(&time_now, &now_tm);
276  }
277 
278  static constexpr size_t buffer_size{128};
279  static constexpr size_t max_buffer_size{64 * 1024};
280  std::vector<char> buffer(buffer_size);
281 
282  while (true)
283  {
284  size_t const len = std::strftime(buffer.data(), buffer.size(), append_format_pattern.data(), &now_tm);
285  if (len != 0)
286  {
287  return std::string{buffer.data(), len};
288  }
289 
290  if (buffer.size() >= max_buffer_size)
291  {
292  QUILL_THROW(
293  QuillError{"strftime failed to format filename timestamp. The filename "
294  "timestamp pattern may contain an unsupported format specifier."});
295  }
296 
297  buffer.resize(buffer.size() * 2);
298  }
299  }
300 
301  QUILL_NODISCARD static std::pair<std::string, std::string> extract_stem_and_extension(fs::path const& filename)
302  {
303  return std::make_pair((filename.parent_path() / filename.stem()).string(), filename.extension().string());
304  }
305 
306  QUILL_NODISCARD static fs::path append_datetime_to_filename(fs::path const& filename,
307  std::string const& append_filename_format_pattern,
308  Timezone time_zone,
309  std::chrono::system_clock::time_point timestamp)
310  {
311  auto const [stem, ext] = extract_stem_and_extension(filename);
312 
313  uint64_t const timestamp_ns = static_cast<uint64_t>(
314  std::chrono::duration_cast<std::chrono::nanoseconds>(timestamp.time_since_epoch()).count());
315 
316  return stem + format_datetime_string(timestamp_ns, time_zone, append_filename_format_pattern) + ext;
317  }
318 
319 private:
320  QUILL_NODISCARD static fs::path _get_updated_filename_with_appended_datetime(
321  fs::path const& filename, FilenameAppendOption append_to_filename_option,
322  std::string const& append_filename_format_pattern, Timezone time_zone,
323  std::chrono::system_clock::time_point timestamp)
324  {
325  if ((append_to_filename_option == FilenameAppendOption::None) || (filename == "/dev/null"))
326  {
327  return filename;
328  }
329 
330  if ((append_to_filename_option == FilenameAppendOption::StartCustomTimestampFormat) ||
331  (append_to_filename_option == FilenameAppendOption::StartDate) ||
332  (append_to_filename_option == FilenameAppendOption::StartDateTime))
333  {
334  return append_datetime_to_filename(filename, append_filename_format_pattern, time_zone, timestamp);
335  }
336 
337  QUILL_THROW(QuillError{"Unexpected FilenameAppendOption value"});
338  }
339 
340 protected:
341  FileSinkConfig _config;
342  std::chrono::steady_clock::time_point _last_fsync_timestamp{};
343 };
344 
350 class FileSink : public FileSinkBase
351 {
352 public:
353  explicit FileSink(fs::path const& filename, FileSinkConfig const& config = FileSinkConfig{},
354  FileEventNotifier file_event_notifier = FileEventNotifier{}, bool do_fopen = true,
355  std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now())
356  : FileSinkBase(filename, config, std::move(file_event_notifier), do_fopen, start_time)
357  {
358  if (do_fopen && !is_null())
359  {
360  open_file(_filename, _config.open_mode());
361  }
362  }
363 
364  ~FileSink() override { _close_file_noexcept(); }
365 
366  QUILL_ATTRIBUTE_HOT void flush_sink() override
367  {
368  if (!_write_occurred)
369  {
370  return;
371  }
372 
373  _flush_native_write_buffer();
374  _write_occurred = false;
375 
376  if (_config.fsync_enabled())
377  {
378  fsync_file();
379  }
380 
381  std::error_code ec;
382  if (!fs::exists(_filename, ec))
383  {
384  close_file();
385  open_file(_filename, _config.open_mode());
386  }
387  }
388 
389  QUILL_ATTRIBUTE_HOT void write_log(MacroMetadata const* /* log_metadata */,
390  uint64_t /* log_timestamp */, std::string_view /* thread_id */,
391  std::string_view /* thread_name */, std::string const& /* process_id */,
392  std::string_view /* logger_name */, LogLevel /* log_level */,
393  std::string_view /* log_level_description */,
394  std::string_view /* log_level_short_code */,
395  std::vector<std::pair<std::string, std::string>> const* /* named_args */,
396  std::string_view /* log_message */, std::string_view log_statement) override
397  {
398  if (QUILL_UNLIKELY(_native_file_handle == INVALID_HANDLE_VALUE))
399  {
400  return;
401  }
402 
403  std::string_view statement = log_statement;
404  std::string user_log_statement;
405 
406  if (_file_event_notifier.before_write)
407  {
408  user_log_statement = _file_event_notifier.before_write(log_statement);
409  statement = user_log_statement;
410  }
411 
412  auto const stmt_size = statement.size();
413 
414  if (_native_write_pos + stmt_size > _native_write_buffer_cap)
415  {
416  _flush_native_write_buffer();
417  }
418 
419  if (QUILL_LIKELY(stmt_size <= _native_write_buffer_cap))
420  {
421  std::memcpy(_native_write_buffer.get() + _native_write_pos, statement.data(), stmt_size);
422  _native_write_pos += stmt_size;
423  }
424  else
425  {
426  _write_to_file(statement.data(), stmt_size);
427  }
428 
429  _file_size += stmt_size;
430  _write_occurred = true;
431  }
432 
433 protected:
434  void open_file(fs::path const& filename, std::string const& mode)
435  {
436  if (_file_event_notifier.before_open)
437  {
438  _file_event_notifier.before_open(filename);
439  }
440 
441  constexpr int max_retries = 3;
442  constexpr int retry_delay_ms = 200;
443  HANDLE native_file_handle = INVALID_HANDLE_VALUE;
444  DWORD last_error = 0;
445 
446  for (int attempt = 0; attempt < max_retries; ++attempt)
447  {
448  DWORD creation_disposition = OPEN_ALWAYS;
449  if (!mode.empty() && mode[0] == 'w')
450  {
451  creation_disposition = CREATE_ALWAYS;
452  }
453 
454  native_file_handle = ::CreateFileW(filename.c_str(), GENERIC_WRITE,
455  FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
456  nullptr, creation_disposition, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
457 
458  if (native_file_handle == INVALID_HANDLE_VALUE)
459  {
460  last_error = ::GetLastError();
461  }
462  else
463  {
464  if (!::SetHandleInformation(native_file_handle, HANDLE_FLAG_INHERIT, 0))
465  {
466  last_error = ::GetLastError();
467  ::CloseHandle(native_file_handle);
468  native_file_handle = INVALID_HANDLE_VALUE;
469  }
470  }
471 
472  if (native_file_handle != INVALID_HANDLE_VALUE)
473  {
474  break;
475  }
476 
477  if (attempt < max_retries - 1)
478  {
479  detail::sleep_for_ns(static_cast<uint64_t>(retry_delay_ms) * 1'000'000ull);
480  }
481  }
482 
483  if (native_file_handle == INVALID_HANDLE_VALUE)
484  {
485  QUILL_THROW(QuillError{std::string{"CreateFileW failed after "} + std::to_string(max_retries) +
486  " attempts, path: " + filename.string() + " mode: " + mode +
487  " GetLastError: " + std::to_string(last_error)});
488  }
489 
490  _native_write_buffer_cap = _native_write_buffer_capacity();
491  _native_write_buffer = std::make_unique<char[]>(_native_write_buffer_cap);
492  _native_write_pos = 0;
493 
494  if (_file_event_notifier.after_open)
495  {
496  QUILL_TRY { _file_event_notifier.after_open(filename, native_file_handle); }
497  #if !defined(QUILL_NO_EXCEPTIONS)
498  QUILL_CATCH(...)
499  {
500  ::CloseHandle(native_file_handle);
501  throw;
502  }
503  #endif
504  }
505 
506  _append_mode = !mode.empty() && mode[0] == 'a';
507  _native_file_handle = native_file_handle;
508  }
509 
510  void close_file()
511  {
512  if (_native_file_handle == INVALID_HANDLE_VALUE)
513  {
514  return;
515  }
516 
517  if (_file_event_notifier.before_close)
518  {
519  QUILL_TRY { _file_event_notifier.before_close(_filename, _native_file_handle); }
520  #if !defined(QUILL_NO_EXCEPTIONS)
521  QUILL_CATCH_ALL()
522  {
523  HANDLE native_file_handle = _native_file_handle;
524  QUILL_TRY { _flush_native_write_buffer(); }
525  QUILL_CATCH_ALL() {}
526  _native_file_handle = INVALID_HANDLE_VALUE;
527  _append_mode = false;
528  _native_write_pos = 0;
529  ::CloseHandle(native_file_handle);
530  throw;
531  }
532  #endif
533  }
534 
535  _flush_native_write_buffer();
536  ::CloseHandle(_native_file_handle);
537  _native_file_handle = INVALID_HANDLE_VALUE;
538  _append_mode = false;
539  _native_write_pos = 0;
540 
541  if (_file_event_notifier.after_close)
542  {
543  _file_event_notifier.after_close(_filename);
544  }
545  }
546 
547  void _close_file_noexcept() noexcept
548  {
549  QUILL_TRY { close_file(); }
550  #if !defined(QUILL_NO_EXCEPTIONS)
551  QUILL_CATCH_ALL() {}
552  #endif
553  }
554 
555  void fsync_file(bool force_fsync = false)
556  {
557  if (_native_file_handle == INVALID_HANDLE_VALUE)
558  {
559  return;
560  }
561 
562  std::chrono::steady_clock::time_point fsync_timestamp{};
563 
564  if (!force_fsync)
565  {
566  fsync_timestamp = std::chrono::steady_clock::now();
567  if ((fsync_timestamp - _last_fsync_timestamp) < _config.minimum_fsync_interval())
568  {
569  return;
570  }
571  }
572 
573  if (!::FlushFileBuffers(_native_file_handle))
574  {
575  _write_occurred = true;
576  QUILL_THROW(QuillError{std::string{"FlushFileBuffers failed. GetLastError: "} +
577  std::to_string(::GetLastError())});
578  }
579 
580  if (!force_fsync)
581  {
582  _last_fsync_timestamp = fsync_timestamp;
583  }
584  }
585 
586  QUILL_NODISCARD bool is_open() const noexcept
587  {
588  return _native_file_handle != INVALID_HANDLE_VALUE;
589  }
590 
591  void _write_to_file(char const* data, size_t size)
592  {
593  while (size != 0)
594  {
595  DWORD bytes_written = 0;
596  constexpr size_t max_dword = static_cast<size_t>(~DWORD{0});
597  DWORD const chunk = static_cast<DWORD>(std::min<size_t>(size, max_dword));
598  OVERLAPPED overlapped{};
599  OVERLAPPED* overlapped_ptr{nullptr};
600 
601  if (_append_mode)
602  {
603  overlapped.Offset = 0xFFFFFFFF;
604  overlapped.OffsetHigh = 0xFFFFFFFF;
605  overlapped_ptr = &overlapped;
606  }
607 
608  if (!::WriteFile(_native_file_handle, data, chunk, &bytes_written, overlapped_ptr) ||
609  (bytes_written == 0))
610  {
611  QUILL_THROW(QuillError{std::string{"WriteFile failed. GetLastError: "} +
612  std::to_string(::GetLastError())});
613  }
614 
615  data += bytes_written;
616  size -= bytes_written;
617  }
618  }
619 
620  void _flush_native_write_buffer()
621  {
622  if ((_native_file_handle == INVALID_HANDLE_VALUE) || (_native_write_pos == 0))
623  {
624  return;
625  }
626 
627  _write_to_file(_native_write_buffer.get(), _native_write_pos);
628  _native_write_pos = 0;
629  }
630 
631  QUILL_NODISCARD size_t _native_write_buffer_capacity() const noexcept
632  {
633  return _config.write_buffer_size() == 0 ? default_write_buffer_size : _config.write_buffer_size();
634  }
635 
636 protected:
637  static constexpr size_t default_write_buffer_size{64 * 1024};
638  HANDLE _native_file_handle{INVALID_HANDLE_VALUE};
639  std::unique_ptr<char[]> _native_write_buffer;
640  size_t _native_write_pos{0};
641  size_t _native_write_buffer_cap{0};
642  bool _append_mode{false};
643 };
644 #else
645 class FileSink : public StreamSink
646 {
647 public:
648  explicit FileSink(fs::path const& filename, FileSinkConfig const& config = FileSinkConfig{},
649  FileEventNotifier file_event_notifier = FileEventNotifier{}, bool do_fopen = true,
650  std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now())
651  : StreamSink(_get_updated_filename_with_appended_datetime(filename, config.filename_append_option(),
652  config.append_filename_format_pattern(),
653  config.timezone(), start_time),
654  nullptr, config.override_pattern_formatter_options(), std::move(file_event_notifier)),
655  _config(config)
656  {
657  if (!_config.fsync_enabled() && (_config.minimum_fsync_interval().count() != 0))
658  {
659  QUILL_THROW(
660  QuillError{"Cannot set a non-zero minimum fsync interval when fsync is disabled."});
661  }
662 
663  if (do_fopen)
664  {
665  open_file(_filename, _config.open_mode());
666  }
667  }
668 
669  ~FileSink() override { _close_file_noexcept(); }
670 
671  QUILL_ATTRIBUTE_HOT void flush_sink() override
672  {
673  if (!_write_occurred || !_file)
674  {
675  return;
676  }
677 
679 
680  if (_config.fsync_enabled())
681  {
682  fsync_file();
683  }
684 
685  std::error_code ec;
686  if (!fs::exists(_filename, ec))
687  {
688  close_file();
689  open_file(_filename, _config.open_mode());
690  }
691  }
692 
693 private:
694  struct OpenedFileGuard
695  {
696  ~OpenedFileGuard()
697  {
698  if (file)
699  {
700  std::fclose(file);
701  }
702  }
703 
704  QUILL_NODISCARD FILE* release() noexcept
705  {
706  FILE* const released_file = file;
707  file = nullptr;
708  return released_file;
709  }
710 
711  FILE* file{nullptr};
712  };
713 
714 protected:
715  QUILL_NODISCARD static std::string format_datetime_string(uint64_t timestamp_ns, Timezone time_zone,
716  std::string const& append_format_pattern)
717  {
718  auto const time_now = static_cast<time_t>(timestamp_ns / 1000000000);
719  tm now_tm;
720 
721  if (time_zone == Timezone::GmtTime)
722  {
723  detail::gmtime_rs(&time_now, &now_tm);
724  }
725  else
726  {
727  detail::localtime_rs(&time_now, &now_tm);
728  }
729 
730  static constexpr size_t buffer_size{128};
731  static constexpr size_t max_buffer_size{64 * 1024};
732  std::vector<char> buffer(buffer_size);
733 
734  while (true)
735  {
736  size_t const len = std::strftime(buffer.data(), buffer.size(), append_format_pattern.data(), &now_tm);
737  if (len != 0)
738  {
739  return std::string{buffer.data(), len};
740  }
741 
742  if (buffer.size() >= max_buffer_size)
743  {
744  QUILL_THROW(
745  QuillError{"strftime failed to format filename timestamp. The filename "
746  "timestamp pattern may contain an unsupported format specifier."});
747  }
748 
749  buffer.resize(buffer.size() * 2);
750  }
751  }
752 
753  QUILL_NODISCARD static std::pair<std::string, std::string> extract_stem_and_extension(fs::path const& filename)
754  {
755  return std::make_pair((filename.parent_path() / filename.stem()).string(), filename.extension().string());
756  }
757 
758  QUILL_NODISCARD static fs::path append_datetime_to_filename(fs::path const& filename,
759  std::string const& append_filename_format_pattern,
760  Timezone time_zone,
761  std::chrono::system_clock::time_point timestamp)
762  {
763  auto const [stem, ext] = extract_stem_and_extension(filename);
764 
765  uint64_t const timestamp_ns = static_cast<uint64_t>(
766  std::chrono::duration_cast<std::chrono::nanoseconds>(timestamp.time_since_epoch()).count());
767 
768  return stem + format_datetime_string(timestamp_ns, time_zone, append_filename_format_pattern) + ext;
769  }
770 
771  void open_file(fs::path const& filename, std::string const& mode)
772  {
773  if (_file_event_notifier.before_open)
774  {
775  _file_event_notifier.before_open(filename);
776  }
777 
778  constexpr int max_retries = 3;
779  constexpr int retry_delay_ms = 200;
780  std::unique_ptr<char[]> write_buffer;
781  OpenedFileGuard opened_file_guard;
782 
783  for (int attempt = 0; attempt < max_retries; ++attempt)
784  {
785  int flags = O_CREAT | O_WRONLY | O_CLOEXEC;
786  flags |= (!mode.empty() && mode[0] == 'w') ? O_TRUNC : O_APPEND;
787 
788  // Retry on EINTR — a signal delivered during open() causes it to fail transiently.
789  int fd{-1};
790  do
791  {
792  fd = ::open(filename.string().data(), flags, 0644);
793  } while (fd == -1 && errno == EINTR);
794 
795  if (fd != -1)
796  {
797  opened_file_guard.file = ::fdopen(fd, mode.data());
798  if (!opened_file_guard.file)
799  {
800  ::close(fd);
801  }
802  }
803 
804  if (opened_file_guard.file)
805  {
806  break;
807  }
808 
809  if (attempt < max_retries - 1)
810  {
811  detail::sleep_for_ns(static_cast<uint64_t>(retry_delay_ms) * 1'000'000ull);
812  }
813  }
814 
815  if (!opened_file_guard.file)
816  {
817  QUILL_THROW(QuillError{std::string{"fopen failed after "} + std::to_string(max_retries) +
818  " attempts, path: " + filename.string() + " mode: " + mode +
819  " errno: " + std::to_string(errno) + " error: " + std::strerror(errno)});
820  }
821 
822  if (_config.write_buffer_size() != 0)
823  {
824  write_buffer = std::make_unique<char[]>(_config.write_buffer_size());
825 
826  if (setvbuf(opened_file_guard.file, write_buffer.get(), _IOFBF, _config.write_buffer_size()) != 0)
827  {
828  QUILL_THROW(QuillError{std::string{"setvbuf failed error: "} + std::strerror(errno)});
829  }
830  }
831 
832  if (_file_event_notifier.after_open)
833  {
834  _file_event_notifier.after_open(filename, opened_file_guard.file);
835  }
836 
837  _file = opened_file_guard.release();
838  _write_buffer = std::move(write_buffer);
839  }
840 
841  void close_file()
842  {
843  if (!_file)
844  {
845  return;
846  }
847 
848  if (_file_event_notifier.before_close)
849  {
850  QUILL_TRY { _file_event_notifier.before_close(_filename, _file); }
851  #if !defined(QUILL_NO_EXCEPTIONS)
852  QUILL_CATCH_ALL()
853  {
854  FILE* file = _file;
855  _file = nullptr;
856  std::fclose(file);
857  throw;
858  }
859  #endif
860  }
861 
862  FILE* file = _file;
863  _file = nullptr;
864 
865  if (std::fclose(file) != 0)
866  {
867  int const saved_errno = errno;
868  QUILL_THROW(QuillError{std::string{"fclose failed errno: "} + std::to_string(saved_errno) +
869  " error: " + std::strerror(saved_errno)});
870  }
871 
872  if (_file_event_notifier.after_close)
873  {
874  _file_event_notifier.after_close(_filename);
875  }
876  }
877 
878  void _close_file_noexcept() noexcept
879  {
880  QUILL_TRY { close_file(); }
881  #if !defined(QUILL_NO_EXCEPTIONS)
882  QUILL_CATCH_ALL() {}
883  #endif
884  }
885 
886  void fsync_file(bool force_fsync = false)
887  {
888  if (!_file)
889  {
890  return;
891  }
892 
893  std::chrono::steady_clock::time_point fsync_timestamp{};
894 
895  if (!force_fsync)
896  {
897  fsync_timestamp = std::chrono::steady_clock::now();
898  if ((fsync_timestamp - _last_fsync_timestamp) < _config.minimum_fsync_interval())
899  {
900  return;
901  }
902  }
903 
904  // Retry on EINTR — a signal delivered during fsync() causes it to fail transiently.
905  int ret{0};
906  do
907  {
908  ret = ::fsync(fileno(_file));
909  } while (ret != 0 && errno == EINTR);
910 
911  if (QUILL_UNLIKELY(ret != 0))
912  {
913  int const saved_errno = errno;
914  _write_occurred = true;
915  QUILL_THROW(QuillError{std::string{"fsync failed errno: "} + std::to_string(saved_errno) +
916  " error: " + std::strerror(saved_errno)});
917  }
918 
919  if (!force_fsync)
920  {
921  _last_fsync_timestamp = fsync_timestamp;
922  }
923  }
924 
925  QUILL_NODISCARD bool is_open() const noexcept { return _file != nullptr; }
926 
927 private:
928  QUILL_NODISCARD static fs::path _get_updated_filename_with_appended_datetime(
929  fs::path const& filename, FilenameAppendOption append_to_filename_option,
930  std::string const& append_filename_format_pattern, Timezone time_zone,
931  std::chrono::system_clock::time_point timestamp)
932  {
933  if ((append_to_filename_option == FilenameAppendOption::None) || (filename == "/dev/null"))
934  {
935  return filename;
936  }
937 
938  if ((append_to_filename_option == FilenameAppendOption::StartCustomTimestampFormat) ||
939  (append_to_filename_option == FilenameAppendOption::StartDate) ||
940  (append_to_filename_option == FilenameAppendOption::StartDateTime))
941  {
942  return append_datetime_to_filename(filename, append_filename_format_pattern, time_zone, timestamp);
943  }
944 
945  QUILL_THROW(QuillError{"Unexpected FilenameAppendOption value"});
946  }
947 
948 protected:
949  FileSinkConfig _config;
950  std::chrono::steady_clock::time_point _last_fsync_timestamp{};
951  std::unique_ptr<char[]> _write_buffer;
952 };
953 #endif
954 
955 QUILL_END_EXPORT
956 
957 #if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
958  #pragma warning(pop)
959 #endif
960 
961 QUILL_END_NAMESPACE
QUILL_ATTRIBUTE_HOT void sleep_for_ns(uint64_t ns) noexcept
Mirrors std::this_thread::sleep_for(std::chrono::nanoseconds{ns}).
Definition: ThreadPrimitives.h:43
Keep the Windows-specific FileSink split isolated under _WIN32.
Definition: FileSink.h:645
QUILL_NODISCARD bool fsync_enabled() const noexcept
Getters.
Definition: FileSink.h:198
QUILL_ATTRIBUTE_COLD void set_override_pattern_formatter_options(std::optional< PatternFormatterOptions > const &options)
Sets custom pattern formatter options for this sink.
Definition: FileSink.h:192
QUILL_ATTRIBUTE_COLD void set_filename_append_option(FilenameAppendOption value, std::string_view append_filename_format_pattern=std::string_view{})
Sets the append type for the file name.
Definition: FileSink.h:84
Captures and stores information about a logging event in compile time.
Definition: MacroMetadata.h:24
QUILL_ATTRIBUTE_COLD void set_fsync_enabled(bool value)
Sets whether fsync should be performed when flushing.
Definition: FileSink.h:129
tm * localtime_rs(time_t const *timer, tm *buf)
Portable localtime_r or _s per operating system.
Definition: TimeUtilities.h:56
QUILL_ATTRIBUTE_COLD void set_write_buffer_size(size_t value)
Sets the user-defined buffer size for fwrite operations.
Definition: FileSink.h:154
QUILL_ATTRIBUTE_HOT void flush_sink() override
Flushes the sink, synchronizing the associated sink with its controlled output sequence.
Definition: FileSink.h:671
tm * gmtime_rs(time_t const *timer, tm *buf)
Portable gmtime_r or _s per operating system.
Definition: TimeUtilities.h:28
custom exception
Definition: QuillError.h:47
QUILL_ATTRIBUTE_COLD void set_timezone(Timezone time_zone)
Sets the timezone to use for time-based operations e.g.
Definition: FileSink.h:118
The FileSinkConfig class holds the configuration options for the FileSink.
Definition: FileSink.h:67
QUILL_ATTRIBUTE_COLD void set_open_mode(std::string_view open_mode)
Sets the open mode for the file.
Definition: FileSink.h:141
QUILL_ATTRIBUTE_HOT void flush_sink() override
Flushes the stream.
Definition: StreamSink.h:156
Notifies on file events by calling the appropriate callback.
Definition: StreamSink.h:68
QUILL_ATTRIBUTE_COLD void set_minimum_fsync_interval(std::chrono::milliseconds value)
Sets the minimum interval between fsync calls.
Definition: FileSink.h:178
StreamSink class for handling log messages.
Definition: StreamSink.h:80
QUILL_ATTRIBUTE_COLD void set_open_mode(char open_mode)
Sets the open mode for the file.
Definition: FileSink.h:135