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 
48 QUILL_BEGIN_EXPORT
49 
51 class MacroMetadata;
52 
53 #if defined(_WIN32)
54 using FileEventNotifierHandle = HANDLE;
55 #else
56 using FileEventNotifierHandle = FILE*;
57 #endif
58 
69 {
70  std::function<void(fs::path const& file_path)> before_open;
71  std::function<void(fs::path const& file_path, FileEventNotifierHandle f)> after_open;
72  std::function<void(fs::path const& file_path, FileEventNotifierHandle f)> before_close;
73  std::function<void(fs::path const& file_path)> after_close;
74  std::function<std::string(std::string_view message)> before_write;
75 };
76 
80 class StreamSink : public Sink
81 {
82 public:
91  explicit StreamSink(fs::path stream, FILE* file = nullptr,
92  std::optional<PatternFormatterOptions> const& override_pattern_formatter_options = std::nullopt,
93  FileEventNotifier file_event_notifier = FileEventNotifier{})
94  : Sink(override_pattern_formatter_options),
95  _filename(std::move(stream)),
96  _file(file),
97  _file_event_notifier(std::move(file_event_notifier))
98  {
99  // reserve stdout and stderr as filenames
100  if (_filename == std::string{"stdout"})
101  {
102  _file = stdout;
103  }
104  else if (_filename == std::string{"stderr"})
105  {
106  _file = stderr;
107  }
108  else if (_filename == std::string{"/dev/null"})
109  {
110  _is_null = true;
111  }
112  else
113  {
114  _filename = detail::normalize_file_sink_path(_filename);
115  }
116  }
117 
118  ~StreamSink() override = default;
119 
123  QUILL_ATTRIBUTE_HOT void write_log(MacroMetadata const* /* log_metadata */,
124  uint64_t /* log_timestamp */, std::string_view /* thread_id */,
125  std::string_view /* thread_name */, std::string const& /* process_id */,
126  std::string_view /* logger_name */, LogLevel /* log_level */,
127  std::string_view /* log_level_description */,
128  std::string_view /* log_level_short_code */,
129  std::vector<std::pair<std::string, std::string>> const* /* named_args */,
130  std::string_view /* log_message */, std::string_view log_statement) override
131  {
132  if (QUILL_UNLIKELY(!_file))
133  {
134  // FileSink::flush() tries to re-open a deleted file and if it fails _file can be null
135  return;
136  }
137 
138  if (_file_event_notifier.before_write)
139  {
140  std::string const user_log_statement = _file_event_notifier.before_write(log_statement);
141  safe_fwrite(user_log_statement.data(), sizeof(char), user_log_statement.size(), _file);
142  _file_size += user_log_statement.size();
143  }
144  else
145  {
146  safe_fwrite(log_statement.data(), sizeof(char), log_statement.size(), _file);
147  _file_size += log_statement.size();
148  }
149 
150  _write_occurred = true;
151  }
152 
156  QUILL_ATTRIBUTE_HOT void flush_sink() override
157  {
158  if (!_write_occurred || !_file)
159  {
160  return;
161  }
162 
163  flush();
164  }
165 
170  QUILL_NODISCARD virtual fs::path const& get_filename() const noexcept { return _filename; }
171 
176  QUILL_NODISCARD bool is_null() const noexcept { return _is_null; }
177 
185  QUILL_ATTRIBUTE_HOT static void safe_fwrite(void const* ptr, size_t size, size_t count, FILE* stream)
186  {
187  size_t const total_bytes = size * count;
188  size_t bytes_written = 0;
189 
190  while (bytes_written < total_bytes)
191  {
192  auto const* current_ptr = static_cast<char const*>(ptr) + bytes_written;
193  size_t const remaining = total_bytes - bytes_written;
194 
195 #if defined(_WIN32)
196  // On Windows, using fwrite to non-binary stream (stdout/stderr) results in \r\r\n
197  // Instead, use WriteFile for console streams
198  if ((stream == stdout) || (stream == stderr))
199  {
200  HANDLE const handle = reinterpret_cast<HANDLE>(::_get_osfhandle(::_fileno(stream)));
201 
202  if (QUILL_LIKELY(handle != INVALID_HANDLE_VALUE))
203  {
204  constexpr size_t max_dword = static_cast<size_t>(~DWORD{0});
205  auto const total_bytes_remaining = static_cast<DWORD>(std::min<size_t>(remaining, max_dword));
206  DWORD bytes_written_this_call = 0;
207 
208  if (QUILL_UNLIKELY(::WriteFile(handle, current_ptr, total_bytes_remaining,
209  &bytes_written_this_call, nullptr) == 0))
210  {
211  QUILL_THROW(QuillError{std::string{"WriteFile failed. GetLastError: "} +
212  std::to_string(::GetLastError())});
213  }
214 
215  if (QUILL_UNLIKELY(bytes_written_this_call == 0))
216  {
217  QUILL_THROW(QuillError{"WriteFile returned 0 bytes written without error"});
218  }
219 
220  bytes_written += bytes_written_this_call;
221  continue;
222  }
223 
224  // Fall through to fwrite if the handle is invalid (e.g., no console attached)
225  }
226 #endif
227 
228  size_t const written = std::fwrite(current_ptr, 1, remaining, stream);
229 
230  if (QUILL_UNLIKELY(written < remaining))
231  {
232  // Partial write or error
233  if (ferror(stream))
234  {
235  int const saved_errno = errno;
236  std::clearerr(stream); // Reset error state
237  QUILL_THROW(QuillError{std::string{"fwrite failed errno: "} + std::to_string(saved_errno) +
238  " error: " + std::strerror(saved_errno)});
239  }
240 
241  if (written == 0)
242  {
243  // Zero bytes written without error is unusual - treat as fatal to avoid infinite loop
244  QUILL_THROW(
245  QuillError{std::string{"fwrite returned 0 bytes written without error - stream may be "
246  "at EOF or in invalid state"}});
247  }
248 
249  // Partial write succeeded - continue with remaining bytes
250  }
251 
252  bytes_written += written;
253  }
254  }
255 
256 protected:
257  QUILL_NODISCARD virtual size_t estimate_write_size(
258  MacroMetadata const* /* log_metadata */, uint64_t /* log_timestamp */,
259  std::string_view /* thread_id */, std::string_view /* thread_name */,
260  std::string const& /* process_id */, std::string_view /* logger_name */, LogLevel /* log_level */,
261  std::string_view /* log_level_description */, std::string_view /* log_level_short_code */,
262  std::vector<std::pair<std::string, std::string>> const* /* named_args */,
263  std::string_view /* log_message */, std::string_view log_statement)
264  {
265  return log_statement.size();
266  }
267 
271  QUILL_ATTRIBUTE_HOT void flush()
272  {
273  // Retry on EINTR — a signal delivered during fflush() causes it to fail transiently.
274  int result{0};
275  do
276  {
277  result = std::fflush(_file);
278  } while (result != 0 && errno == EINTR);
279 
280  if (QUILL_LIKELY(result == 0))
281  {
282  _write_occurred = false;
283  }
284  else
285  {
286  int const saved_errno = errno;
287  std::clearerr(_file); // Reset error state
288  QUILL_THROW(QuillError{std::string{"fflush failed errno: "} + std::to_string(saved_errno) +
289  " error: " + std::strerror(saved_errno)});
290  }
291  }
292 
293 protected:
294  fs::path _filename;
295  FILE* _file{nullptr};
296  size_t _file_size{0};
297  FileEventNotifier _file_event_notifier;
298  bool _is_null{false};
299  bool _write_occurred{false};
300 };
301 
302 QUILL_END_EXPORT
303 
304 #if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
305  #pragma warning(pop)
306 #endif
307 
308 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:123
Base class for sinks.
Definition: Sink.h:46
QUILL_NODISCARD bool is_null() const noexcept
Checks if the stream is null.
Definition: StreamSink.h:176
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:91
Captures and stores information about a logging event in compile time.
Definition: MacroMetadata.h:24
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:185
custom exception
Definition: QuillError.h:47
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
StreamSink class for handling log messages.
Definition: StreamSink.h:80
QUILL_ATTRIBUTE_HOT void flush()
Flushes the stream.
Definition: StreamSink.h:271
virtual QUILL_NODISCARD fs::path const & get_filename() const noexcept
Returns the name of the file.
Definition: StreamSink.h:170