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 <cassert>
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 
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 <windows.h>
40 #else
41  #include <unistd.h>
42 #endif
43 
44 QUILL_BEGIN_NAMESPACE
45 
46 enum class FilenameAppendOption : uint8_t
47 {
48  None,
49  StartDate,
50  StartDateTime,
51  StartCustomTimestampFormat
52 };
53 
58 {
59 public:
74  QUILL_ATTRIBUTE_COLD void set_filename_append_option(
75  FilenameAppendOption value, std::string_view append_filename_format_pattern = std::string_view{})
76  {
77  _filename_append_option = value;
78 
79  if (_filename_append_option == FilenameAppendOption::StartCustomTimestampFormat)
80  {
81  if (append_filename_format_pattern.empty())
82  {
83  QUILL_THROW(QuillError{
84  "The 'CustomDateTimeFormat' option was specified, but no format pattern was provided. "
85  "Please set a valid strftime format pattern"});
86  }
87 
88  _append_filename_format_pattern = append_filename_format_pattern;
89  }
90  else if (_filename_append_option == FilenameAppendOption::StartDateTime)
91  {
92  _append_filename_format_pattern = "_%Y%m%d_%H%M%S";
93  }
94  else if (_filename_append_option == FilenameAppendOption::StartDate)
95  {
96  _append_filename_format_pattern = "_%Y%m%d";
97  }
98  }
99 
107  QUILL_ATTRIBUTE_COLD void set_timezone(Timezone time_zone) { _time_zone = time_zone; }
108 
114  QUILL_ATTRIBUTE_COLD void set_fsync_enabled(bool value) { _fsync_enabled = value; }
115 
121  QUILL_ATTRIBUTE_COLD void set_open_mode(char open_mode) { _open_mode = open_mode; }
122 
134  QUILL_ATTRIBUTE_COLD void set_write_buffer_size(size_t value)
135  {
136  _write_buffer_size = (value == 0) ? 0 : ((value < 4096) ? 4096 : value);
137  }
138 
158  QUILL_ATTRIBUTE_COLD void set_minimum_fsync_interval(std::chrono::milliseconds value)
159  {
160  _minimum_fsync_interval = value;
161  }
162 
172  QUILL_ATTRIBUTE_COLD void set_override_pattern_formatter_options(std::optional<PatternFormatterOptions> const& options)
173  {
174  _override_pattern_formatter_options = options;
175  }
176 
178  QUILL_NODISCARD bool fsync_enabled() const noexcept { return _fsync_enabled; }
179  QUILL_NODISCARD Timezone timezone() const noexcept { return _time_zone; }
180  QUILL_NODISCARD FilenameAppendOption filename_append_option() const noexcept
181  {
182  return _filename_append_option;
183  }
184  QUILL_NODISCARD std::string const& append_filename_format_pattern() const noexcept
185  {
186  return _append_filename_format_pattern;
187  }
188  QUILL_NODISCARD std::string const& open_mode() const noexcept { return _open_mode; }
189  QUILL_NODISCARD size_t write_buffer_size() const noexcept { return _write_buffer_size; }
190  QUILL_NODISCARD std::chrono::milliseconds minimum_fsync_interval() const noexcept
191  {
192  return _minimum_fsync_interval;
193  }
194  QUILL_NODISCARD std::optional<PatternFormatterOptions> const& override_pattern_formatter_options() const noexcept
195  {
196  return _override_pattern_formatter_options;
197  }
198 
199 private:
200  std::string _open_mode{'a'};
201  std::string _append_filename_format_pattern;
202  size_t _write_buffer_size{64 * 1024}; // Default size 64k
203  std::chrono::milliseconds _minimum_fsync_interval{0};
204  std::optional<PatternFormatterOptions> _override_pattern_formatter_options;
205  Timezone _time_zone{Timezone::LocalTime};
206  FilenameAppendOption _filename_append_option{FilenameAppendOption::None};
207  bool _fsync_enabled{false};
208 };
209 
214 class FileSink : public StreamSink
215 {
216 public:
226  explicit FileSink(fs::path const& filename, FileSinkConfig const& config = FileSinkConfig{},
227  FileEventNotifier file_event_notifier = FileEventNotifier{}, bool do_fopen = true,
228  std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now())
229  : StreamSink(_get_updated_filename_with_appended_datetime(filename, config.filename_append_option(),
230  config.append_filename_format_pattern(),
231  config.timezone(), start_time),
232  nullptr, config.override_pattern_formatter_options(), std::move(file_event_notifier)),
233  _config(config)
234  {
235  if (!_config.fsync_enabled() && (_config.minimum_fsync_interval().count() != 0))
236  {
237  QUILL_THROW(
238  QuillError{"Cannot set a non-zero minimum fsync interval when fsync is disabled."});
239  }
240 
241  if (do_fopen)
242  {
243  open_file(_filename, _config.open_mode());
244  }
245  }
246 
247  ~FileSink() override { close_file(); }
248 
252  QUILL_ATTRIBUTE_HOT void flush_sink() override
253  {
254  if (!_write_occurred || !_file)
255  {
256  // Check here because StreamSink::flush() will set _write_occurred to false
257  return;
258  }
259 
261 
262  if (_config.fsync_enabled())
263  {
264  fsync_file();
265  }
266 
267  if (!fs::exists(_filename))
268  {
269  // after flushing the file we can check if the file still exists. If not we reopen it.
270  // This can happen if a user deletes a file while the application is running
271  close_file();
272 
273  // now reopen the file for writing again, it will be a new file
274  open_file(_filename, "w");
275  }
276  }
277 
278 protected:
286  QUILL_NODISCARD static std::string format_datetime_string(uint64_t timestamp_ns, Timezone time_zone,
287  std::string const& append_format_pattern)
288  {
289  // convert to seconds
290  auto const time_now = static_cast<time_t>(timestamp_ns / 1000000000);
291  tm now_tm;
292 
293  if (time_zone == Timezone::GmtTime)
294  {
295  detail::gmtime_rs(&time_now, &now_tm);
296  }
297  else
298  {
299  detail::localtime_rs(&time_now, &now_tm);
300  }
301 
302  // Construct the string
303  static constexpr size_t buffer_size{128};
304  char buffer[buffer_size];
305  std::strftime(buffer, buffer_size, append_format_pattern.data(), &now_tm);
306 
307  return std::string{buffer};
308  }
309 
315  QUILL_NODISCARD static std::pair<std::string, std::string> extract_stem_and_extension(fs::path const& filename) noexcept
316  {
317  // filename and extension
318  return std::make_pair((filename.parent_path() / filename.stem()).string(), filename.extension().string());
319  }
320 
329  QUILL_NODISCARD static fs::path append_datetime_to_filename(fs::path const& filename,
330  std::string const& append_filename_format_pattern,
331  Timezone time_zone,
332  std::chrono::system_clock::time_point timestamp) noexcept
333  {
334  // Get base file and extension
335  auto const [stem, ext] = extract_stem_and_extension(filename);
336 
337  // Get the time now as tm from user or default to now
338  uint64_t const timestamp_ns = static_cast<uint64_t>(
339  std::chrono::duration_cast<std::chrono::nanoseconds>(timestamp.time_since_epoch()).count());
340 
341  // Construct a filename
342  return stem + format_datetime_string(timestamp_ns, time_zone, append_filename_format_pattern) + ext;
343  }
344 
350  void open_file(fs::path const& filename, std::string const& mode)
351  {
352  if (_file_event_notifier.before_open)
353  {
354  _file_event_notifier.before_open(filename);
355  }
356 
357  _file = fopen(filename.string().data(), mode.data());
358 
359  if (!_file)
360  {
361  QUILL_THROW(QuillError{std::string{"fopen failed failed path: "} + filename.string() + " mode: " + mode +
362  " errno: " + std::to_string(errno) + " error: " + strerror(errno)});
363  }
364 
365  if (_config.write_buffer_size() != 0)
366  {
367  _write_buffer = std::make_unique<char[]>(_config.write_buffer_size());
368 
369  if (setvbuf(_file, _write_buffer.get(), _IOFBF, _config.write_buffer_size()) != 0)
370  {
371  QUILL_THROW(QuillError{std::string{"setvbuf failed error: "} + strerror(errno)});
372  }
373  }
374 
375  if (_file_event_notifier.after_open)
376  {
377  _file_event_notifier.after_open(filename, _file);
378  }
379  }
380 
384  void close_file()
385  {
386  if (!_file)
387  {
388  return;
389  }
390 
391  if (_file_event_notifier.before_close)
392  {
393  _file_event_notifier.before_close(_filename, _file);
394  }
395 
396  fclose(_file);
397  _file = nullptr;
398 
399  if (_file_event_notifier.after_close)
400  {
401  _file_event_notifier.after_close(_filename);
402  }
403  }
404 
408  void fsync_file(bool force_fsync = false) noexcept
409  {
410  if (!force_fsync)
411  {
412  auto const now = std::chrono::steady_clock::now();
413  if ((now - _last_fsync_timestamp) < _config.minimum_fsync_interval())
414  {
415  return;
416  }
417  _last_fsync_timestamp = now;
418  }
419 
420 #ifdef _WIN32
421  FlushFileBuffers(reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(_file))));
422 #else
423  ::fsync(fileno(_file));
424 #endif
425  }
426 
427 private:
437  QUILL_NODISCARD static fs::path _get_updated_filename_with_appended_datetime(
438  fs::path const& filename, FilenameAppendOption append_to_filename_option,
439  std::string const& append_filename_format_pattern, Timezone time_zone,
440  std::chrono::system_clock::time_point timestamp)
441  {
442  if ((append_to_filename_option == FilenameAppendOption::None) || (filename == "/dev/null"))
443  {
444  return filename;
445  }
446 
447  if ((append_to_filename_option == FilenameAppendOption::StartCustomTimestampFormat) ||
448  (append_to_filename_option == FilenameAppendOption::StartDate) ||
449  (append_to_filename_option == FilenameAppendOption::StartDateTime))
450  {
451  return append_datetime_to_filename(filename, append_filename_format_pattern, time_zone, timestamp);
452  }
453 
454  return fs::path{};
455  }
456 
457 protected:
458  FileSinkConfig _config;
459  std::chrono::steady_clock::time_point _last_fsync_timestamp{};
460  std::unique_ptr<char[]> _write_buffer;
461 };
462 
463 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:286
FileSink Writes the log messages to a file.
Definition: FileSink.h:214
QUILL_NODISCARD bool fsync_enabled() const noexcept
Getters.
Definition: FileSink.h:178
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:172
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:74
QUILL_ATTRIBUTE_COLD void set_fsync_enabled(bool value)
Sets whether fsync should be performed when flushing.
Definition: FileSink.h:114
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:408
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:315
QUILL_ATTRIBUTE_COLD void set_write_buffer_size(size_t value)
Sets the user-defined buffer size for fwrite operations.
Definition: FileSink.h:134
void open_file(fs::path const &filename, std::string const &mode)
Open a file.
Definition: FileSink.h:350
QUILL_ATTRIBUTE_HOT void flush_sink() override
Flushes the stream and optionally fsyncs it.
Definition: FileSink.h:252
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:384
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:226
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:329
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:107
The FileSinkConfig class holds the configuration options for the FileSink.
Definition: FileSink.h:57
QUILL_ATTRIBUTE_HOT void flush_sink() override
Flushes the stream.
Definition: StreamSink.h:166
Notifies on file events by calling the appropriate callback, the callback is executed on the backend ...
Definition: StreamSink.h:36
QUILL_ATTRIBUTE_COLD void set_minimum_fsync_interval(std::chrono::milliseconds value)
Sets the minimum interval between fsync calls.
Definition: FileSink.h:158
StreamSink class for handling log messages.
Definition: StreamSink.h:48
QUILL_ATTRIBUTE_COLD void set_open_mode(char open_mode)
Sets the open mode for the file.
Definition: FileSink.h:121