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/TimeUtilities.h"
14 #include "quill/sinks/StreamSink.h"
15 
16 #include <cerrno>
17 #include <chrono>
18 #include <cstdint>
19 #include <cstdio>
20 #include <cstring>
21 #include <ctime>
22 #include <memory>
23 #include <string>
24 #include <string_view>
25 #include <utility>
26 #include <thread>
27 
28 #if defined(_WIN32)
29  #if !defined(WIN32_LEAN_AND_MEAN)
30  #define WIN32_LEAN_AND_MEAN
31  #endif
32 
33  #if !defined(NOMINMAX)
34  // Mingw already defines this, so no need to redefine
35  #define NOMINMAX
36  #endif
37 
38  #include <io.h>
39  #include <share.h>
40  #include <windows.h>
41 #else
42  #include <fcntl.h>
43  #include <unistd.h>
44 #endif
45 
46 QUILL_BEGIN_NAMESPACE
47 
48 #if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
49  #pragma warning(push)
50  #pragma warning(disable : 4996)
51 #endif
52 
53 enum class FilenameAppendOption : uint8_t
54 {
55  None,
56  StartDate,
57  StartDateTime,
58  StartCustomTimestampFormat
59 };
60 
65 {
66 public:
81  QUILL_ATTRIBUTE_COLD void set_filename_append_option(
82  FilenameAppendOption value, std::string_view append_filename_format_pattern = std::string_view{})
83  {
84  _filename_append_option = value;
85 
86  if (_filename_append_option == FilenameAppendOption::StartCustomTimestampFormat)
87  {
88  if (append_filename_format_pattern.empty())
89  {
90  QUILL_THROW(QuillError{
91  "The 'CustomDateTimeFormat' option was specified, but no format pattern was provided. "
92  "Please set a valid strftime format pattern"});
93  }
94 
95  _append_filename_format_pattern = append_filename_format_pattern;
96  }
97  else if (_filename_append_option == FilenameAppendOption::StartDateTime)
98  {
99  _append_filename_format_pattern = "_%Y%m%d_%H%M%S";
100  }
101  else if (_filename_append_option == FilenameAppendOption::StartDate)
102  {
103  _append_filename_format_pattern = "_%Y%m%d";
104  }
105  }
106 
114  QUILL_ATTRIBUTE_COLD void set_timezone(Timezone time_zone) { _time_zone = time_zone; }
115 
121  QUILL_ATTRIBUTE_COLD void set_fsync_enabled(bool value) { _fsync_enabled = value; }
122 
127  QUILL_ATTRIBUTE_COLD void set_open_mode(char open_mode) { _open_mode = open_mode; }
128 
133  QUILL_ATTRIBUTE_COLD void set_open_mode(std::string_view open_mode) { _open_mode = open_mode; }
134 
146  QUILL_ATTRIBUTE_COLD void set_write_buffer_size(size_t value)
147  {
148  _write_buffer_size = (value == 0) ? 0 : ((value < 4096) ? 4096 : value);
149  }
150 
170  QUILL_ATTRIBUTE_COLD void set_minimum_fsync_interval(std::chrono::milliseconds value)
171  {
172  _minimum_fsync_interval = value;
173  }
174 
184  QUILL_ATTRIBUTE_COLD void set_override_pattern_formatter_options(std::optional<PatternFormatterOptions> const& options)
185  {
186  _override_pattern_formatter_options = options;
187  }
188 
190  QUILL_NODISCARD bool fsync_enabled() const noexcept { return _fsync_enabled; }
191  QUILL_NODISCARD Timezone timezone() const noexcept { return _time_zone; }
192  QUILL_NODISCARD FilenameAppendOption filename_append_option() const noexcept
193  {
194  return _filename_append_option;
195  }
196  QUILL_NODISCARD std::string const& append_filename_format_pattern() const noexcept
197  {
198  return _append_filename_format_pattern;
199  }
200  QUILL_NODISCARD std::string const& open_mode() const noexcept { return _open_mode; }
201  QUILL_NODISCARD size_t write_buffer_size() const noexcept { return _write_buffer_size; }
202  QUILL_NODISCARD std::chrono::milliseconds minimum_fsync_interval() const noexcept
203  {
204  return _minimum_fsync_interval;
205  }
206  QUILL_NODISCARD std::optional<PatternFormatterOptions> const& override_pattern_formatter_options() const noexcept
207  {
208  return _override_pattern_formatter_options;
209  }
210 
211 private:
212  std::string _open_mode{'a'};
213  std::string _append_filename_format_pattern;
214  size_t _write_buffer_size{64 * 1024}; // Default size 64k
215  std::chrono::milliseconds _minimum_fsync_interval{0};
216  std::optional<PatternFormatterOptions> _override_pattern_formatter_options;
217  Timezone _time_zone{Timezone::LocalTime};
218  FilenameAppendOption _filename_append_option{FilenameAppendOption::None};
219  bool _fsync_enabled{false};
220 };
221 
226 class FileSink : public StreamSink
227 {
228 public:
238  explicit FileSink(fs::path const& filename, FileSinkConfig const& config = FileSinkConfig{},
239  FileEventNotifier file_event_notifier = FileEventNotifier{}, bool do_fopen = true,
240  std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now())
241  : StreamSink(_get_updated_filename_with_appended_datetime(filename, config.filename_append_option(),
242  config.append_filename_format_pattern(),
243  config.timezone(), start_time),
244  nullptr, config.override_pattern_formatter_options(), std::move(file_event_notifier)),
245  _config(config)
246  {
247  if (!_config.fsync_enabled() && (_config.minimum_fsync_interval().count() != 0))
248  {
249  QUILL_THROW(
250  QuillError{"Cannot set a non-zero minimum fsync interval when fsync is disabled."});
251  }
252 
253  if (do_fopen)
254  {
255  open_file(_filename, _config.open_mode());
256  }
257  }
258 
259  ~FileSink() override { close_file(); }
260 
264  QUILL_ATTRIBUTE_HOT void flush_sink() override
265  {
266  if (!_write_occurred || !_file)
267  {
268  // Check here because StreamSink::flush() will set _write_occurred to false
269  return;
270  }
271 
273 
274  if (_config.fsync_enabled())
275  {
276  fsync_file();
277  }
278 
279  if (!fs::exists(_filename))
280  {
281  // after flushing the file we can check if the file still exists. If not we reopen it.
282  // This can happen if a user deletes a file while the application is running
283  close_file();
284 
285  // now reopen the file for writing again, it will be a new file
286  open_file(_filename, "w");
287  }
288  }
289 
290 protected:
298  QUILL_NODISCARD static std::string format_datetime_string(uint64_t timestamp_ns, Timezone time_zone,
299  std::string const& append_format_pattern)
300  {
301  // convert to seconds
302  auto const time_now = static_cast<time_t>(timestamp_ns / 1000000000);
303  tm now_tm;
304 
305  if (time_zone == Timezone::GmtTime)
306  {
307  detail::gmtime_rs(&time_now, &now_tm);
308  }
309  else
310  {
311  detail::localtime_rs(&time_now, &now_tm);
312  }
313 
314  // Construct the string
315  static constexpr size_t buffer_size{128};
316  char buffer[buffer_size];
317  std::strftime(buffer, buffer_size, append_format_pattern.data(), &now_tm);
318 
319  return std::string{buffer};
320  }
321 
327  QUILL_NODISCARD static std::pair<std::string, std::string> extract_stem_and_extension(fs::path const& filename) noexcept
328  {
329  // filename and extension
330  return std::make_pair((filename.parent_path() / filename.stem()).string(), filename.extension().string());
331  }
332 
341  QUILL_NODISCARD static fs::path append_datetime_to_filename(fs::path const& filename,
342  std::string const& append_filename_format_pattern,
343  Timezone time_zone,
344  std::chrono::system_clock::time_point timestamp) noexcept
345  {
346  // Get base file and extension
347  auto const [stem, ext] = extract_stem_and_extension(filename);
348 
349  // Get the time now as tm from user or default to now
350  uint64_t const timestamp_ns = static_cast<uint64_t>(
351  std::chrono::duration_cast<std::chrono::nanoseconds>(timestamp.time_since_epoch()).count());
352 
353  // Construct a filename
354  return stem + format_datetime_string(timestamp_ns, time_zone, append_filename_format_pattern) + ext;
355  }
356 
362  void open_file(fs::path const& filename, std::string const& mode)
363  {
364  if (_file_event_notifier.before_open)
365  {
366  _file_event_notifier.before_open(filename);
367  }
368 
369  // Retry file open to handle transient failures (e.g., antivirus locking on Windows)
370  constexpr int max_retries = 3;
371  constexpr int retry_delay_ms = 200;
372 
373  for (int attempt = 0; attempt < max_retries; ++attempt)
374  {
375 #if defined(_WIN32)
376  // Use _fsopen with _SH_DENYNO to allow other processes to read the file
377  // while we're writing (e.g., tail, monitoring tools)
378  _file = ::_fsopen(filename.string().data(), mode.data(), _SH_DENYNO);
379 
380  if (_file)
381  {
382  // Prevent child processes from inheriting this file handle
383  auto file_handle = reinterpret_cast<HANDLE>(::_get_osfhandle(::_fileno(_file)));
384  if (!::SetHandleInformation(file_handle, HANDLE_FLAG_INHERIT, 0))
385  {
386  ::fclose(_file);
387  _file = nullptr;
388  }
389  }
390 #else
391  // Unix: use open() with O_CLOEXEC to prevent child processes from inheriting the FD
392  int flags = O_CREAT | O_WRONLY | O_CLOEXEC;
393  flags |= (mode == "w") ? O_TRUNC : O_APPEND;
394 
395  int fd = ::open(filename.string().data(), flags, 0644);
396  if (fd != -1)
397  {
398  _file = ::fdopen(fd, mode.data());
399  if (!_file)
400  {
401  ::close(fd);
402  }
403  }
404 #endif
405 
406  if (_file)
407  {
408  break; // Success
409  }
410 
411  // Retry after delay if not the last attempt
412  if (attempt < max_retries - 1)
413  {
414  std::this_thread::sleep_for(std::chrono::milliseconds{retry_delay_ms});
415  }
416  }
417 
418  if (!_file)
419  {
420  QUILL_THROW(QuillError{std::string{"fopen failed after "} + std::to_string(max_retries) +
421  " attempts, path: " + filename.string() + " mode: " + mode +
422  " errno: " + std::to_string(errno) + " error: " + std::strerror(errno)});
423  }
424 
425  if (_config.write_buffer_size() != 0)
426  {
427  _write_buffer = std::make_unique<char[]>(_config.write_buffer_size());
428 
429  if (setvbuf(_file, _write_buffer.get(), _IOFBF, _config.write_buffer_size()) != 0)
430  {
431  QUILL_THROW(QuillError{std::string{"setvbuf failed error: "} + std::strerror(errno)});
432  }
433  }
434 
435  if (_file_event_notifier.after_open)
436  {
437  _file_event_notifier.after_open(filename, _file);
438  }
439  }
440 
444  void close_file()
445  {
446  if (!_file)
447  {
448  return;
449  }
450 
451  if (_file_event_notifier.before_close)
452  {
453  _file_event_notifier.before_close(_filename, _file);
454  }
455 
456  fclose(_file);
457  _file = nullptr;
458 
459  if (_file_event_notifier.after_close)
460  {
461  _file_event_notifier.after_close(_filename);
462  }
463  }
464 
468  void fsync_file(bool force_fsync = false) noexcept
469  {
470  if (!force_fsync)
471  {
472  auto const now = std::chrono::steady_clock::now();
473  if ((now - _last_fsync_timestamp) < _config.minimum_fsync_interval())
474  {
475  return;
476  }
477  _last_fsync_timestamp = now;
478  }
479 
480 #ifdef _WIN32
481  FlushFileBuffers(reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(_file))));
482 #else
483  ::fsync(fileno(_file));
484 #endif
485  }
486 
487 private:
497  QUILL_NODISCARD static fs::path _get_updated_filename_with_appended_datetime(
498  fs::path const& filename, FilenameAppendOption append_to_filename_option,
499  std::string const& append_filename_format_pattern, Timezone time_zone,
500  std::chrono::system_clock::time_point timestamp)
501  {
502  if ((append_to_filename_option == FilenameAppendOption::None) || (filename == "/dev/null"))
503  {
504  return filename;
505  }
506 
507  if ((append_to_filename_option == FilenameAppendOption::StartCustomTimestampFormat) ||
508  (append_to_filename_option == FilenameAppendOption::StartDate) ||
509  (append_to_filename_option == FilenameAppendOption::StartDateTime))
510  {
511  return append_datetime_to_filename(filename, append_filename_format_pattern, time_zone, timestamp);
512  }
513 
514  return fs::path{};
515  }
516 
517 protected:
518  FileSinkConfig _config;
519  std::chrono::steady_clock::time_point _last_fsync_timestamp{};
520  std::unique_ptr<char[]> _write_buffer;
521 };
522 
523 #if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
524 #pragma warning(pop)
525 #endif
526 
527 QUILL_END_NAMESPACE
static QUILL_NODISCARD std::string format_datetime_string(uint64_t timestamp_ns, Timezone time_zone, std::string const &append_format_pattern)
Format a datetime string.
Definition: FileSink.h:298
FileSink Writes the log messages to a file.
Definition: FileSink.h:226
QUILL_NODISCARD bool fsync_enabled() const noexcept
Getters.
Definition: FileSink.h:190
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:184
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:81
QUILL_ATTRIBUTE_COLD void set_fsync_enabled(bool value)
Sets whether fsync should be performed when flushing.
Definition: FileSink.h:121
tm * localtime_rs(time_t const *timer, tm *buf)
Portable localtime_r or _s per operating system.
Definition: TimeUtilities.h:59
void fsync_file(bool force_fsync=false) noexcept
Fsync the file descriptor.
Definition: FileSink.h:468
static QUILL_NODISCARD std::pair< std::string, std::string > extract_stem_and_extension(fs::path const &filename) noexcept
Extract stem and extension from a filename.
Definition: FileSink.h:327
QUILL_ATTRIBUTE_COLD void set_write_buffer_size(size_t value)
Sets the user-defined buffer size for fwrite operations.
Definition: FileSink.h:146
void open_file(fs::path const &filename, std::string const &mode)
Open a file.
Definition: FileSink.h:362
QUILL_ATTRIBUTE_HOT void flush_sink() override
Flushes the stream and optionally fsyncs it.
Definition: FileSink.h:264
tm * gmtime_rs(time_t const *timer, tm *buf)
Portable gmtime_r or _s per operating system.
Definition: TimeUtilities.h:31
void close_file()
Close the file.
Definition: FileSink.h:444
FileSink(fs::path const &filename, FileSinkConfig const &config=FileSinkConfig{}, FileEventNotifier file_event_notifier=FileEventNotifier{}, bool do_fopen=true, std::chrono::system_clock::time_point start_time=std::chrono::system_clock::now())
Construct a FileSink object.
Definition: FileSink.h:238
static QUILL_NODISCARD fs::path append_datetime_to_filename(fs::path const &filename, std::string const &append_filename_format_pattern, Timezone time_zone, std::chrono::system_clock::time_point timestamp) noexcept
Append date and/or time to a filename.
Definition: FileSink.h:341
custom exception
Definition: QuillError.h:45
QUILL_ATTRIBUTE_COLD void set_timezone(Timezone time_zone)
Sets the timezone to use for time-based operations e.g.
Definition: FileSink.h:114
The FileSinkConfig class holds the configuration options for the FileSink.
Definition: FileSink.h:64
QUILL_ATTRIBUTE_COLD void set_open_mode(std::string_view open_mode)
Sets the open mode for the file.
Definition: FileSink.h:133
QUILL_ATTRIBUTE_HOT void flush_sink() override
Flushes the stream.
Definition: StreamSink.h:185
Notifies on file events by calling the appropriate callback, the callback is executed on the backend ...
Definition: StreamSink.h:55
QUILL_ATTRIBUTE_COLD void set_minimum_fsync_interval(std::chrono::milliseconds value)
Sets the minimum interval between fsync calls.
Definition: FileSink.h:170
StreamSink class for handling log messages.
Definition: StreamSink.h:67
QUILL_ATTRIBUTE_COLD void set_open_mode(char open_mode)
Sets the open mode for the file.
Definition: FileSink.h:127