Simple-Log  alpha-v0.7
FileSink.hpp
Go to the documentation of this file.
1 // Copyright Dominic Koepke 2021 - 2021.
2 // Distributed under the Boost Software License, Version 1.0.
3 // (See accompanying file LICENSE_1_0.txt or copy at
4 // https://www.boost.org/LICENSE_1_0.txt)
5 
6 #ifndef SL_LOG_FILE_SINK_HPP
7 #define SL_LOG_FILE_SINK_HPP
8 
9 #pragma once
10 
11 #include "Record.hpp"
12 #include "OStreamSink.hpp"
13 #include "StringPattern.hpp"
14 
15 #include <cassert>
16 #include <filesystem>
17 #include <fstream>
18 #include <numeric>
19 #include <optional>
20 
21 namespace sl::log
22 {
30  template <class T>
31  concept FileStateHandler = std::is_invocable_r_v<std::string, T>;
32 
78  template <Record TRecord>
79  class FileSink final :
80  public OStreamSink<TRecord>
81  {
83 
84  public:
85  using typename Super::Record_t;
86  using typename Super::Formatter_t;
87  using typename Super::Filter_t;
88  using typename Super::FlushPolicy_t;
89 
93  struct RotationRule
94  {
95  std::optional<std::size_t> fileSize;
96  std::optional<std::chrono::seconds> duration;
97  };
98 
102  struct CleanupRule
103  {
104  std::optional<std::size_t> directorySize;
105  std::optional<std::size_t> fileCount;
106  };
107 
116  explicit FileSink(std::string fileNamePattern, std::filesystem::path directory = std::filesystem::current_path()) :
117  Super{ m_FileStream }
118  {
120  setDirectory(std::move(directory));
121  }
122 
127  ~FileSink() noexcept
128  {
129  try
130  {
131  if (m_FileStream.is_open())
132  {
133  closeFile();
134  }
135  }
136  catch (...)
137  {
138  }
139  }
140 
144  FileSink(const FileSink&) = delete;
148  FileSink& operator =(const FileSink&) = delete;
152  FileSink(FileSink&&) = delete;
157 
162  void setRotationRule(RotationRule rule) noexcept
163  {
164  std::scoped_lock lock{ m_RotationRuleMx };
165  m_RotationRule = rule;
166  }
167 
171  [[nodiscard]]
172  RotationRule rotationRule() const noexcept
173  {
174  return load(m_RotationRule, m_RotationRuleMx);
175  }
176 
181  void rotate()
182  {
183  if (m_FileStream.is_open())
184  {
185  closeFile();
186  openFile();
187  }
188  }
189 
194  void setCleanupRule(CleanupRule rule) noexcept
195  {
196  std::scoped_lock lock{ m_CleanupRuleMx };
197  m_CleanupRule = rule;
198  }
199 
203  [[nodiscard]]
204  CleanupRule cleanupRule() const noexcept
205  {
206  return load(m_CleanupRule, m_CleanupRuleMx);
207  }
208 
215  template <FileStateHandler THandler>
216  void setOpeningHandler(THandler&& handler) noexcept
217  {
218  std::scoped_lock lock{ m_OpeningHandlerMx };
219  m_OpeningHandler = std::forward<THandler>(handler);
220  }
221 
225  void removeOpeningHandler() noexcept
226  {
227  std::scoped_lock lock{ m_OpeningHandlerMx };
228  m_OpeningHandler = nullptr;
229  }
230 
237  template <FileStateHandler THandler>
238  void setClosingHandler(THandler&& handler) noexcept
239  {
240  std::scoped_lock lock{ m_ClosingHandlerMx };
241  m_ClosingHandler = std::forward<THandler>(handler);
242  }
243 
247  void removeClosingHandler() noexcept
248  {
249  std::scoped_lock lock{ m_ClosingHandlerMx };
250  m_ClosingHandler = nullptr;
251  }
252 
258  void setDirectory(std::filesystem::path directory)
259  {
260  std::scoped_lock lock{ m_FilePathNameMx };
261  m_Directory = std::move(directory);
262  create_directories(m_Directory);
263  }
264 
269  [[nodiscard]]
270  std::filesystem::path directory() const noexcept
271  {
272  std::scoped_lock lock{ m_FilePathNameMx };
273  return m_Directory;
274  }
275 
284  {
285  if (std::empty(fileNamePattern))
286  {
287  throw SinkException{ "FileNamePattern must not be empty." };
288  }
289 
290  if (std::filesystem::path(fileNamePattern).has_parent_path())
291  {
292  throw SinkException{ "FileNamePattern must contain any directory information. Use directory property instead." };
293  }
294 
295  std::scoped_lock lock{ m_FilePathNameMx };
296  m_FileNamePattern.setPatternString(std::move(fileNamePattern));
297  }
298 
303  [[nodiscard]]
304  std::string fileNamePattern() const noexcept
305  {
306  std::scoped_lock lock{ m_FilePathNameMx };
307  return std::string{ m_FileNamePattern.patternString() };
308  }
309 
310  private:
311  using FileStateHandler = std::function<std::string()>;
312 
313  mutable std::mutex m_FilePathNameMx;
314  StringPattern m_FileNamePattern;
315  std::filesystem::path m_Directory;
316 
317  std::ofstream m_FileStream;
318  std::optional<std::filesystem::path> m_CurrentFilePath;
319 
320  // rotation related
321  std::atomic<std::chrono::steady_clock::time_point> m_FileOpeningTime;
322  mutable std::mutex m_RotationRuleMx;
323  RotationRule m_RotationRule;
324 
325  // cleanup related
326  mutable std::mutex m_CleanupRuleMx;
327  CleanupRule m_CleanupRule;
328 
329  // File Handler
330  std::mutex m_OpeningHandlerMx;
331  FileStateHandler m_OpeningHandler;
332  std::mutex m_ClosingHandlerMx;
333  FileStateHandler m_ClosingHandler;
334 
335  void openFile()
336  {
337  auto filePath = [&]
338  {
339  std::scoped_lock lock{ m_FilePathNameMx };
340  return m_Directory / m_FileNamePattern.next();
341  }();
342 
343  m_FileStream.open(filePath);
344  if (!m_FileStream.is_open())
345  {
346  throw SinkException{ "FileSink: Attempted opening file \"" + filePath.string() + "\" but failed." };
347  }
348 
349  m_CurrentFilePath = std::move(filePath);
350  m_FileOpeningTime = std::chrono::steady_clock::now();
351 
352  if (std::scoped_lock lock{ m_OpeningHandlerMx }; m_OpeningHandler)
353  {
354  Super::writeToStream(m_OpeningHandler());
355  }
356  }
357 
358  void closeFile()
359  {
360  assert(m_FileStream.is_open() && "FileStream must be open.");
361  assert(m_CurrentFilePath && !std::empty(*m_CurrentFilePath));
362 
363  if (std::scoped_lock lock{ m_ClosingHandlerMx }; m_ClosingHandler)
364  {
365  Super::writeToStream(m_ClosingHandler());
366  }
367 
368  m_FileStream.close();
369  removeFilesIfNecessary();
370  m_CurrentFilePath.reset();
371  m_FileOpeningTime.store({});
372  }
373 
374  void removeFilesIfNecessary()
375  {
376  // ToDo: use c++20 ranges::view
377  auto directoryItr = std::filesystem::directory_iterator(m_CurrentFilePath->parent_path());
378 
379  std::vector<std::filesystem::directory_entry> files;
380  for (const auto& entry : directoryItr)
381  {
382  if (is_regular_file(entry) && entry.path().extension() == m_CurrentFilePath->extension() &&
383  !equivalent(entry.path(), *m_CurrentFilePath))
384  {
385  files.emplace_back(entry);
386  }
387  }
388 
389  std::ranges::sort(files, std::greater(), [](const auto& file) { return last_write_time(file); });
390  fulfillFileCountCleanup(files);
391  fulfillDirectorySizeCleanup(files);
392  }
393 
394  template <class T, class TMutex>
395  static T load(const T& object, TMutex& mutex)
396  {
397  std::scoped_lock lock{ mutex };
398  return object;
399  }
400 
401  void fulfillFileCountCleanup(std::vector<std::filesystem::directory_entry>& files) const
402  {
403  auto cleanupRule = load(m_CleanupRule, m_CleanupRuleMx);
404  if (!cleanupRule.fileCount)
405  return;
406 
407  while (*cleanupRule.fileCount < std::size(files))
408  {
409  auto& file = files.back();
410  remove(file);
411  files.pop_back();
412  }
413  }
414 
415  void fulfillDirectorySizeCleanup(std::vector<std::filesystem::directory_entry>& files) const
416  {
417  auto cleanupRule = load(m_CleanupRule, m_CleanupRuleMx);
419  return;
420 
421  auto size = std::accumulate(
422  std::begin(files),
423  std::end(files),
424  0ull,
425  [](auto value, const auto& file) { return value + file.file_size(); }
426  );
427  while (*cleanupRule.directorySize < size)
428  {
429  auto& file = files.back();
430  size -= file_size(file);
431  remove(file);
432  files.pop_back();
433  }
434  }
435 
436  [[nodiscard]]
437  bool shallRotate() const
438  {
439  assert(m_FileStream.is_open() && m_CurrentFilePath && !std::empty(*m_CurrentFilePath));
440 
441  auto rotationRule = load(m_RotationRule, m_RotationRuleMx);
442  return (rotationRule.fileSize && *rotationRule.fileSize < file_size(*m_CurrentFilePath)) ||
443  (rotationRule.duration && m_FileOpeningTime.load() + *rotationRule.duration < std::chrono::steady_clock::now()
444  );
445  }
446 
447  void beforeMessageWrite(const Record_t& record, std::string_view message) override
448  {
449  if (!m_FileStream.is_open())
450  {
451  openFile();
452  }
453  else if (shallRotate())
454  {
455  closeFile();
456  openFile();
457  }
458  }
459  };
460 
462 }
463 
464 #endif
std::function< bool(const Record_t &)> Filter_t
Definition: BasicSink.hpp:59
std::function< std::string(const Record_t &)> Formatter_t
Definition: BasicSink.hpp:58
Class for logging into files.
Definition: FileSink.hpp:81
void setOpeningHandler(THandler &&handler) noexcept
Applies a new handler for opening files.
Definition: FileSink.hpp:216
FileSink(FileSink &&)=delete
Deleted move constructor.
void setRotationRule(RotationRule rule) noexcept
Applies a new RotationRule configuration.
Definition: FileSink.hpp:162
void setDirectory(std::filesystem::path directory)
Sets the directory in which the log files will be created.
Definition: FileSink.hpp:258
FileSink(std::string fileNamePattern, std::filesystem::path directory=std::filesystem::current_path())
Constructor.
Definition: FileSink.hpp:116
~FileSink() noexcept
Destructor.
Definition: FileSink.hpp:127
CleanupRule cleanupRule() const noexcept
Returns a copy of the current CleanupRule configuration.
Definition: FileSink.hpp:204
void removeOpeningHandler() noexcept
Removes the active opening handler.
Definition: FileSink.hpp:225
std::remove_cvref_t< TRecord > Record_t
Used Record type.
Definition: ISink.hpp:34
FileSink & operator=(const FileSink &)=delete
Deleted copy assign operator.
void removeClosingHandler() noexcept
Removes the active closing handler.
Definition: FileSink.hpp:247
FileSink(const FileSink &)=delete
Deleted copy constructor.
void setCleanupRule(CleanupRule rule) noexcept
Applies a new CleanupRule configuration.
Definition: FileSink.hpp:194
void setFileNamePattern(std::string fileNamePattern)
Sets the file name pattern for generated log files.
Definition: FileSink.hpp:283
std::string fileNamePattern() const noexcept
Getter of the used file name pattern string.
Definition: FileSink.hpp:304
std::filesystem::path directory() const noexcept
Getter of the directory member.
Definition: FileSink.hpp:270
void setClosingHandler(THandler &&handler) noexcept
Applies a new handler for closing files.
Definition: FileSink.hpp:238
RotationRule rotationRule() const noexcept
Returns a copy of the current RotationRule configuration.
Definition: FileSink.hpp:172
void rotate()
Rotates the current file.
Definition: FileSink.hpp:181
Sink interface class.
Definition: ISink.hpp:29
An std::ostream orientated Sink class which extends BasicSink.
Definition: OStreamSink.hpp:35
std::unique_ptr< detail::AbstractFlushPolicyWrapper< Record_t > > FlushPolicy_t
Definition: OStreamSink.hpp:42
void writeToStream(TData &&data)
Writes directly to the internal stream.
Definition: OStreamSink.hpp:129
std::remove_cvref_t< TRecord > Record_t
Used Record type.
Definition: ISink.hpp:34
Definition: ISink.hpp:183
Helper class for generating patterned strings.
Definition: StringPattern.hpp:120
void setPatternString(std::string patternString)
Sets the pattern string.
Definition: StringPattern.hpp:172
std::string_view patternString() const noexcept
Getter of the used pattern string.
Definition: StringPattern.hpp:163
std::string next()
Creates a new string.
Definition: StringPattern.hpp:142
concept FileStateHandler
Concept for invokable file state handler objects.
Definition: FileSink.hpp:31
Definition: BasicSink.hpp:22
Type for configuring FileSink cleanup rules.
Definition: FileSink.hpp:103
std::optional< std::size_t > fileCount
Definition: FileSink.hpp:105
std::optional< std::size_t > directorySize
Definition: FileSink.hpp:104
Type for configuring FileSink rotation rules.
Definition: FileSink.hpp:94
std::optional< std::chrono::seconds > duration
Definition: FileSink.hpp:96
std::optional< std::size_t > fileSize
Definition: FileSink.hpp:95