quill
StreamSink.h
1 
7 #pragma once
8 
9 #include "quill/core/Attributes.h"
10 #include "quill/core/Filesystem.h"
11 #include "quill/core/LogLevel.h"
12 #include "quill/core/QuillError.h"
13 #include "quill/sinks/Sink.h"
14 
15 #include <cerrno>
16 #include <cstdint>
17 #include <cstdio>
18 #include <cstring>
19 #include <functional>
20 #include <optional>
21 #include <string>
22 #include <string_view>
23 #include <system_error>
24 #include <utility>
25 #include <vector>
26 
27 #if defined(_WIN32)
28  #if !defined(WIN32_LEAN_AND_MEAN)
29  #define WIN32_LEAN_AND_MEAN
30  #endif
31 
32  #if !defined(NOMINMAX)
33  // Mingw already defines this, so no need to redefine
34  #define NOMINMAX
35  #endif
36 
37  #include <io.h>
38  #include <windows.h>
39 #endif
40 
41 QUILL_BEGIN_NAMESPACE
42 
43 #if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
44  #pragma warning(push)
45  #pragma warning(disable : 4996)
46 #endif
47 
49 class MacroMetadata;
50 
56 {
57  std::function<void(fs::path const& file_path)> before_open;
58  std::function<void(fs::path const& file_path, FILE* f)> after_open;
59  std::function<void(fs::path const& file_path, FILE* f)> before_close;
60  std::function<void(fs::path const& file_path)> after_close;
61  std::function<std::string(std::string_view message)> before_write;
62 };
63 
67 class StreamSink : public Sink
68 {
69 public:
78  explicit StreamSink(fs::path stream, FILE* file = nullptr,
79  std::optional<PatternFormatterOptions> const& override_pattern_formatter_options = std::nullopt,
80  FileEventNotifier file_event_notifier = FileEventNotifier{})
81  : Sink(override_pattern_formatter_options),
82  _filename(std::move(stream)),
83  _file(file),
84  _file_event_notifier(std::move(file_event_notifier))
85  {
86  // reserve stdout and stderr as filenames
87  if (_filename == std::string{"stdout"})
88  {
89  _file = stdout;
90  }
91  else if (_filename == std::string{"stderr"})
92  {
93  _file = stderr;
94  }
95  else if (_filename == std::string{"/dev/null"})
96  {
97  _is_null = true;
98  }
99  else
100  {
101  // first attempt to create any non-existing directories
102  std::error_code ec;
103  fs::path parent_path;
104 
105  if (!_filename.parent_path().empty())
106  {
107  parent_path = _filename.parent_path();
108 
109  // The call to fs::status is necessary due to a known issue in GCC versions 8.3.0 to 9.4.0.
110  // In these versions, fs::create_directories(path, ec) internally uses
111  // fs::symlink_status(path, ec) instead of fs::status(path, ec) for checking the path.
112  // This causes a problem because fs::symlink_status does not follow the symlink to the
113  // target directory. As a result, it fails the is_directory() check but still indicates
114  // that the path exists, leading to a not_a_directory exception being set in the error code
115  auto const st = fs::status(parent_path, ec);
116  if (!is_directory(st))
117  {
118  fs::create_directories(parent_path, ec);
119  if (ec)
120  {
121  // use .string() to also support experimental fs
122  QUILL_THROW(QuillError{std::string{"cannot create directories for path "} +
123  parent_path.string() + std::string{" - error: "} + ec.message()});
124  }
125  }
126  }
127  else
128  {
129  parent_path = fs::current_path();
130  }
131 
132  // convert the parent path to an absolute path
133  fs::path const canonical_path = fs::canonical(parent_path, ec);
134 
135  if (ec)
136  {
137  // use .string() to also support experimental fs
138  QUILL_THROW(QuillError{std::string{"cannot create canonical path for path "} +
139  parent_path.string() + std::string{" - error: "} + ec.message()});
140  }
141 
142  // finally replace the given filename's parent_path with the equivalent canonical path
143  _filename = canonical_path / _filename.filename();
144  }
145  }
146 
147  ~StreamSink() override = default;
148 
152  QUILL_ATTRIBUTE_HOT void write_log(MacroMetadata const* /* log_metadata */,
153  uint64_t /* log_timestamp */, std::string_view /* thread_id */,
154  std::string_view /* thread_name */, std::string const& /* process_id */,
155  std::string_view /* logger_name */, LogLevel /* log_level */,
156  std::string_view /* log_level_description */,
157  std::string_view /* log_level_short_code */,
158  std::vector<std::pair<std::string, std::string>> const* /* named_args */,
159  std::string_view /* log_message */, std::string_view log_statement) override
160  {
161  if (QUILL_UNLIKELY(!_file))
162  {
163  // FileSink::flush() tries to re-open a deleted file and if it fails _file can be null
164  return;
165  }
166 
167  if (_file_event_notifier.before_write)
168  {
169  std::string const user_log_statement = _file_event_notifier.before_write(log_statement);
170  safe_fwrite(user_log_statement.data(), sizeof(char), user_log_statement.size(), _file);
171  _file_size += user_log_statement.size();
172  }
173  else
174  {
175  safe_fwrite(log_statement.data(), sizeof(char), log_statement.size(), _file);
176  _file_size += log_statement.size();
177  }
178 
179  _write_occurred = true;
180  }
181 
185  QUILL_ATTRIBUTE_HOT void flush_sink() override
186  {
187  if (!_write_occurred || !_file)
188  {
189  return;
190  }
191 
192  flush();
193  }
194 
199  QUILL_NODISCARD virtual fs::path const& get_filename() const noexcept { return _filename; }
200 
205  QUILL_NODISCARD bool is_null() const noexcept { return _is_null; }
206 
214  QUILL_ATTRIBUTE_HOT static void safe_fwrite(void const* ptr, size_t size, size_t count, FILE* stream)
215  {
216  size_t const total_bytes = size * count;
217  size_t bytes_written = 0;
218 
219  while (bytes_written < total_bytes)
220  {
221  auto const* current_ptr = static_cast<char const*>(ptr) + bytes_written;
222  size_t const remaining = total_bytes - bytes_written;
223 
224 #if defined(_WIN32)
225  // On Windows, using fwrite to non-binary stream (stdout/stderr) results in \r\r\n
226  // Instead, use WriteFile for console streams
227  if ((stream == stdout) || (stream == stderr))
228  {
229  HANDLE const handle = reinterpret_cast<HANDLE>(::_get_osfhandle(::_fileno(stream)));
230 
231  if (QUILL_LIKELY(handle != INVALID_HANDLE_VALUE))
232  {
233  auto const total_bytes_remaining = static_cast<DWORD>(remaining);
234  DWORD bytes_written_this_call = 0;
235 
236  if (QUILL_UNLIKELY((::WriteFile(handle, current_ptr, total_bytes_remaining,
237  &bytes_written_this_call, nullptr) == 0) ||
238  (bytes_written_this_call != total_bytes_remaining)))
239  {
240  QUILL_THROW(QuillError{std::string{"WriteFile failed. GetLastError: "} +
241  std::to_string(::GetLastError())});
242  }
243 
244  bytes_written += bytes_written_this_call;
245  continue;
246  }
247 
248  // Fall through to fwrite if the handle is invalid (e.g., no console attached)
249  }
250 #endif
251 
252  size_t const written = std::fwrite(current_ptr, 1, remaining, stream);
253 
254  if (QUILL_UNLIKELY(written < remaining))
255  {
256  // Partial write or error
257  if (ferror(stream))
258  {
259  int const saved_errno = errno;
260  std::clearerr(stream); // Reset error state
261  QUILL_THROW(QuillError{std::string{"fwrite failed errno: "} + std::to_string(saved_errno) +
262  " error: " + std::strerror(saved_errno)});
263  }
264 
265  if (written == 0)
266  {
267  // Zero bytes written without error is unusual - treat as fatal to avoid infinite loop
268  QUILL_THROW(
269  QuillError{std::string{"fwrite returned 0 bytes written without error - stream may be "
270  "at EOF or in invalid state"}});
271  }
272 
273  // Partial write succeeded - continue with remaining bytes
274  }
275 
276  bytes_written += written;
277  }
278  }
279 
280 protected:
284  QUILL_ATTRIBUTE_HOT void flush()
285  {
286  int const result = std::fflush(_file);
287 
288  if (QUILL_LIKELY(result == 0))
289  {
290  _write_occurred = false;
291  }
292  else
293  {
294  int const saved_errno = errno;
295  std::clearerr(_file); // Reset error state
296  QUILL_THROW(QuillError{std::string{"fflush failed errno: "} + std::to_string(saved_errno) +
297  " error: " + std::strerror(saved_errno)});
298  }
299  }
300 
301 protected:
302  fs::path _filename;
303  FILE* _file{nullptr};
304  size_t _file_size{0};
305  FileEventNotifier _file_event_notifier;
306  bool _is_null{false};
307  bool _write_occurred{false};
308 };
309 
310 #if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
311  #pragma warning(pop)
312 #endif
313 
314 QUILL_END_NAMESPACE
QUILL_ATTRIBUTE_HOT void write_log(MacroMetadata const *, uint64_t, std::string_view, std::string_view, std::string const &, std::string_view, LogLevel, std::string_view, std::string_view, std::vector< std::pair< std::string, std::string >> const *, std::string_view, std::string_view log_statement) override
Writes a formatted log message to the stream.
Definition: StreamSink.h:152
Base class for sinks.
Definition: Sink.h:40
QUILL_NODISCARD bool is_null() const noexcept
Checks if the stream is null.
Definition: StreamSink.h:205
StreamSink(fs::path stream, FILE *file=nullptr, std::optional< PatternFormatterOptions > const &override_pattern_formatter_options=std::nullopt, FileEventNotifier file_event_notifier=FileEventNotifier{})
Constructor for StreamSink.
Definition: StreamSink.h:78
Captures and stores information about a logging event in compile time.
Definition: MacroMetadata.h:22
static QUILL_ATTRIBUTE_HOT void safe_fwrite(void const *ptr, size_t size, size_t count, FILE *stream)
Writes data safely to the stream.
Definition: StreamSink.h:214
custom exception
Definition: QuillError.h:45
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
StreamSink class for handling log messages.
Definition: StreamSink.h:67
QUILL_ATTRIBUTE_HOT void flush()
Flushes the stream.
Definition: StreamSink.h:284
virtual QUILL_NODISCARD fs::path const & get_filename() const noexcept
Returns the name of the file.
Definition: StreamSink.h:199