quill
PatternFormatter.h
1 
7 #pragma once
8 
9 #include "quill/backend/TimestampFormatter.h"
10 #include "quill/bundled/fmt/base.h"
11 #include "quill/bundled/fmt/format.h"
12 #include "quill/core/Attributes.h"
13 #include "quill/core/Common.h"
14 #include "quill/core/MacroMetadata.h"
15 #include "quill/core/PatternFormatterOptions.h"
16 #include "quill/core/QuillError.h"
17 
18 #include <array>
19 #include <bitset>
20 #include <chrono>
21 #include <cstddef>
22 #include <cstdint>
23 #include <iterator>
24 #include <string>
25 #include <string_view>
26 #include <tuple>
27 #include <unordered_map>
28 #include <utility>
29 #include <vector>
30 
31 QUILL_BEGIN_NAMESPACE
32 
34 {
36 public:
40  enum class TimestampPrecision : uint8_t
41  {
42  None,
43  MilliSeconds,
44  MicroSeconds,
45  NanoSeconds
46  };
47 
48  enum Attribute : uint8_t
49  {
50  Time = 0,
51  FileName,
52  CallerFunction,
53  LogLevel,
54  LogLevelShortCode,
55  LineNumber,
56  Logger,
57  FullPath,
58  ThreadId,
59  ThreadName,
60  ProcessId,
62  ShortSourceLocation,
63  Message,
64  Tags,
65  NamedArgs,
66  ATTR_NR_ITEMS
67  };
68 
70 public:
80  : _options(std::move(options)),
81  _timestamp_formatter(_options.timestamp_pattern, _options.timestamp_timezone)
82  {
83  _set_pattern();
84  }
85 
86  /***/
87  PatternFormatter(PatternFormatter const& other) = delete;
88  PatternFormatter& operator=(PatternFormatter const& other) = delete;
89 
90  /***/
91  PatternFormatter& operator=(PatternFormatter&& other) noexcept = default;
92  PatternFormatter(PatternFormatter&& other) noexcept = default;
93 
94  /***/
95  ~PatternFormatter() = default;
96 
97  QUILL_NODISCARD QUILL_ATTRIBUTE_HOT std::string_view format(
98  uint64_t timestamp, std::string_view thread_id, std::string_view thread_name,
99  std::string_view process_id, std::string_view logger, std::string_view log_level_description,
100  std::string_view log_level_short_code, MacroMetadata const& log_statement_metadata,
101  std::vector<std::pair<std::string, std::string>> const* named_args, std::string_view log_msg)
102  {
103  if (_options.format_pattern.empty())
104  {
105  // No formatting is needed when the format pattern is empty.
106  // For example, in JsonFileSink, we can retrieve the MacroMetadata and the named arguments as
107  // key-value pairs, but we do not need to format the log statement.
108  return std::string_view{};
109  }
110 
111  // clear out the existing buffer
112  _formatted_log_message_buffer.clear();
113 
114  if (QUILL_UNLIKELY(log_msg.empty()))
115  {
116  // Process an empty message
117  return _format(timestamp, thread_id, thread_name, process_id, logger, log_level_description,
118  log_level_short_code, log_statement_metadata, named_args, log_msg);
119  }
120 
121  std::string_view formatted_log_msg;
122 
123  // Check if we need to handle multi-line formatting
124  if (_options.add_metadata_to_multi_line_logs && (!named_args || named_args->empty()))
125  {
126  // multi line metadata only supported when named_args are not used
127  size_t start = 0;
128 
129  while (start < log_msg.size())
130  {
131  size_t const end = log_msg.find_first_of('\n', start);
132 
133  if (end == std::string_view::npos)
134  {
135  // Handle the last line or a single line without a newline
136  formatted_log_msg =
137  _format(timestamp, thread_id, thread_name, process_id, logger, log_level_description,
138  log_level_short_code, log_statement_metadata, named_args,
139  std::string_view(log_msg.data() + start, log_msg.size() - start));
140  break;
141  }
142 
143  // Write the current line
144  size_t line_length = end - start;
145  if (_options.pattern_suffix != '\n')
146  {
147  // When suffix is not '\n', include the newline character in the message
148  line_length++;
149  }
150 
151  formatted_log_msg = _format(timestamp, thread_id, thread_name, process_id, logger,
152  log_level_description, log_level_short_code, log_statement_metadata,
153  named_args, std::string_view(log_msg.data() + start, line_length));
154  start = end + 1;
155  }
156  }
157  else
158  {
159  // Use the regular format method for single-line messages
160  QUILL_ASSERT(
161  !log_msg.empty(),
162  "log_msg should not be empty, already checked earlier in PatternFormatter::format()");
163  size_t log_message_size = log_msg.size();
164 
165  if (_options.pattern_suffix == '\n' && log_msg[log_msg.size() - 1] == '\n')
166  {
167  // if the log_message ends with \n we exclude it (only when using newline suffix)
168  log_message_size = log_msg.size() - 1;
169  }
170 
171  formatted_log_msg = _format(timestamp, thread_id, thread_name, process_id, logger,
172  log_level_description, log_level_short_code, log_statement_metadata,
173  named_args, std::string_view{log_msg.data(), log_message_size});
174  }
175 
176  return formatted_log_msg;
177  }
178 
179  /***/
180  QUILL_NODISCARD PatternFormatterOptions const& get_options() const noexcept { return _options; }
181 
182 protected:
183  /***/
184  QUILL_NODISCARD static std::string_view _process_source_location_path(std::string_view source_location,
185  std::string const& strip_prefix,
186  bool remove_relative_paths)
187  {
188  std::string_view result = source_location;
189 
190  // First, handle removal of relative paths if requested
191  if (remove_relative_paths)
192  {
193  // Remove any relative paths (e.g., relative paths can appear when using a mounted volume under docker)
194 
195 #if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
196  static constexpr std::string_view relative_path = "..\\";
197 #else
198  static constexpr std::string_view relative_path = "../";
199 #endif
200 
201  if (size_t n = result.rfind(relative_path); n != std::string_view::npos)
202  {
203  result = result.substr(n + relative_path.size());
204  }
205  }
206 
207  // Then handle prefix stripping
208  if (!strip_prefix.empty())
209  {
210  // Find the last occurrence of the prefix in the path
211  size_t prefix_pos = result.rfind(strip_prefix);
212 
213  if (prefix_pos != std::string_view::npos)
214  {
215  size_t after_prefix_pos = prefix_pos + strip_prefix.size();
216 
217  // If the prefix doesn't end with a separator and there is a character after the prefix
218  // and that character is a separator, skip it as well
219  if (after_prefix_pos < result.length() && result[after_prefix_pos] == detail::PATH_PREFERRED_SEPARATOR)
220  {
221  after_prefix_pos++;
222  }
223 
224  return result.substr(after_prefix_pos);
225  }
226  // Prefix not found, use the full path
227  }
228 
229  // No prefix set or prefix not found, use the full path
230  return result;
231  }
232 
233 private:
234  void _set_pattern()
235  {
236  // the order we pass the arguments here must match with the order of Attribute enum
237  using namespace fmtquill::literals;
238  std::tie(_fmt_format, _order_index) = _generate_fmt_format_string(
239  _is_set_in_pattern, _options.format_pattern, "time"_a = "", "file_name"_a = "",
240  "caller_function"_a = "", "log_level"_a = "", "log_level_short_code"_a = "",
241  "line_number"_a = "", "logger"_a = "", "full_path"_a = "", "thread_id"_a = "",
242  "thread_name"_a = "", "process_id"_a = "", "source_location"_a = "",
243  "short_source_location"_a = "", "message"_a = "", "tags"_a = "", "named_args"_a = "");
244 
245  _set_arg<Attribute::Time>(std::string_view("time"));
246  _set_arg<Attribute::FileName>(std::string_view("file_name"));
247  _set_arg<Attribute::CallerFunction>(std::string_view("caller_function"));
248  _set_arg<Attribute::LogLevel>(std::string_view("log_level"));
249  _set_arg<Attribute::LogLevelShortCode>(std::string_view("log_level_short_code"));
250  _set_arg<Attribute::LineNumber>("line_number");
251  _set_arg<Attribute::Logger>(std::string_view("logger"));
252  _set_arg<Attribute::FullPath>(std::string_view("full_path"));
253  _set_arg<Attribute::ThreadId>(std::string_view("thread_id"));
254  _set_arg<Attribute::ThreadName>(std::string_view("thread_name"));
255  _set_arg<Attribute::ProcessId>(std::string_view("process_id"));
256  _set_arg<Attribute::SourceLocation>("source_location");
257  _set_arg<Attribute::ShortSourceLocation>("short_source_location");
258  _set_arg<Attribute::Message>(std::string_view("message"));
259  _set_arg<Attribute::Tags>(std::string_view("tags"));
260  _set_arg<Attribute::NamedArgs>(std::string_view("named_args"));
261  }
262 
263  /***/
264  template <size_t I, typename T>
265  void _set_arg(T const& arg)
266  {
267  _args[_order_index[I]] = arg;
268  }
269 
270  template <size_t I, typename T>
271  void _set_arg_val(T const& arg)
272  {
273  fmtquill::detail::value<fmtquill::format_context>& value_ =
274  *(reinterpret_cast<fmtquill::detail::value<fmtquill::format_context>*>(
275  std::addressof(_args[_order_index[I]])));
276 
277  value_ = fmtquill::detail::value<fmtquill::format_context>(arg);
278  }
279 
280  /***/
281  PatternFormatter::Attribute static _attribute_from_string(std::string const& attribute_name)
282  {
283  // don't make this static as it breaks on windows with atexit when backend worker stops
284  std::unordered_map<std::string, PatternFormatter::Attribute> const attr_map = {
285  {"time", PatternFormatter::Attribute::Time},
286  {"file_name", PatternFormatter::Attribute::FileName},
287  {"caller_function", PatternFormatter::Attribute::CallerFunction},
288  {"log_level", PatternFormatter::Attribute::LogLevel},
289  {"log_level_short_code", PatternFormatter::Attribute::LogLevelShortCode},
290  {"line_number", PatternFormatter::Attribute::LineNumber},
291  {"logger", PatternFormatter::Attribute::Logger},
292  {"full_path", PatternFormatter::Attribute::FullPath},
293  {"thread_id", PatternFormatter::Attribute::ThreadId},
294  {"thread_name", PatternFormatter::Attribute::ThreadName},
295  {"process_id", PatternFormatter::Attribute::ProcessId},
296  {"source_location", PatternFormatter::Attribute::SourceLocation},
297  {"short_source_location", PatternFormatter::Attribute::ShortSourceLocation},
298  {"message", PatternFormatter::Attribute::Message},
299  {"tags", PatternFormatter::Attribute::Tags},
300  {"named_args", PatternFormatter::Attribute::NamedArgs}};
301 
302  auto const search = attr_map.find(attribute_name);
303 
304  if (QUILL_UNLIKELY(search == attr_map.cend()))
305  {
306  QUILL_THROW(QuillError{
307  std::string{"Attribute enum value does not exist for attribute with name " + attribute_name}});
308  }
309 
310  return search->second;
311  }
312 
313  /***/
314  template <size_t, size_t>
315  constexpr void _store_named_args(std::array<fmtquill::detail::named_arg_info<char>, PatternFormatter::Attribute::ATTR_NR_ITEMS>&)
316  {
317  }
318 
319  /***/
320  template <size_t Idx, size_t NamedIdx, typename Arg, typename... Args>
321  constexpr void _store_named_args(
322  std::array<fmtquill::detail::named_arg_info<char>, PatternFormatter::Attribute::ATTR_NR_ITEMS>& named_args_store,
323  Arg const& arg, Args const&... args)
324  {
325  named_args_store[NamedIdx] = {arg.name, Idx};
326  _store_named_args<Idx + 1, NamedIdx + 1>(named_args_store, args...);
327  }
328 
355  template <typename... Args>
356  QUILL_NODISCARD std::pair<std::string, std::array<size_t, PatternFormatter::Attribute::ATTR_NR_ITEMS>> _generate_fmt_format_string(
357  std::bitset<PatternFormatter::Attribute::ATTR_NR_ITEMS>& is_set_in_pattern, std::string pattern,
358  Args const&... args)
359  {
360  // Attribute enum and the args we are passing here must be in sync
361  static_assert(PatternFormatter::Attribute::ATTR_NR_ITEMS == sizeof...(Args));
362 
363  if (_options.pattern_suffix != PatternFormatterOptions::NO_SUFFIX)
364  {
365  pattern += _options.pattern_suffix;
366  }
367 
368  std::array<size_t, PatternFormatter::Attribute::ATTR_NR_ITEMS> order_index{};
369  order_index.fill(PatternFormatter::Attribute::ATTR_NR_ITEMS - 1);
370 
371  std::array<fmtquill::detail::named_arg_info<char>, PatternFormatter::Attribute::ATTR_NR_ITEMS> named_args{};
372  _store_named_args<0, 0>(named_args, args...);
373  uint8_t arg_idx = 0;
374 
375  // we will replace all %(....) with {} to construct a string to pass to fmt library
376  size_t arg_identifier_pos = pattern.find_first_of('%');
377  while (arg_identifier_pos != std::string::npos)
378  {
379  if (size_t const open_paren_pos = pattern.find_first_of('(', arg_identifier_pos);
380  open_paren_pos != std::string::npos && (open_paren_pos - arg_identifier_pos) == 1)
381  {
382  // if we found '%(' we have a matching pattern and we now need to get the closed paren
383  size_t const closed_paren_pos = pattern.find_first_of(')', open_paren_pos);
384 
385  if (closed_paren_pos == std::string::npos)
386  {
387  QUILL_THROW(QuillError{"Invalid format pattern"});
388  }
389 
390  // We have everything, get the substring, this time including '%( )'
391  std::string attr = pattern.substr(arg_identifier_pos, (closed_paren_pos + 1) - arg_identifier_pos);
392 
393  // find any user format specifiers
394  size_t const pos = attr.find(':');
395  std::string attr_name;
396 
397  if (pos != std::string::npos)
398  {
399  // we found user format specifiers that we want to keep.
400  // e.g. %(short_source_location:<32)
401 
402  // Get only the format specifier
403  // e.g. :<32
404  std::string custom_format_specifier = attr.substr(pos);
405  custom_format_specifier.pop_back(); // remove the ")"
406 
407  // replace with the pattern with the correct value
408  std::string value;
409  value += "{";
410  value += custom_format_specifier;
411  value += "}";
412 
413  // e.g. {:<32}
414  pattern.replace(arg_identifier_pos, attr.length(), value);
415 
416  // Get the part that is the named argument
417  // e.g. short_source_location
418  attr_name = attr.substr(2, pos - 2);
419  }
420  else
421  {
422  // Make the replacement.
423  pattern.replace(arg_identifier_pos, attr.length(), "{}");
424 
425  // Get the part that is the named argument
426  // e.g. short_source_location
427  attr.pop_back(); // remove the ")"
428 
429  attr_name = attr.substr(2, attr.size());
430  }
431 
432  // reorder
433  int id = -1;
434 
435  for (size_t i = 0; i < PatternFormatter::Attribute::ATTR_NR_ITEMS; ++i)
436  {
437  if (named_args[i].name == attr_name)
438  {
439  id = named_args[i].id;
440  break;
441  }
442  }
443 
444  if (id < 0)
445  {
446  QUILL_THROW(QuillError{"Invalid format pattern, attribute with name \"" + attr_name + "\" is invalid"});
447  }
448 
449  order_index[static_cast<size_t>(id)] = arg_idx++;
450 
451  // Also set the value as used in the pattern in our bitset for lazy evaluation
452  PatternFormatter::Attribute const attr_enum_value = _attribute_from_string(attr_name);
453  is_set_in_pattern.set(attr_enum_value);
454 
455  // Look for the next pattern to replace
456  arg_identifier_pos = pattern.find_first_of('%');
457  }
458  else
459  {
460  // search for the next '%'
461  arg_identifier_pos = pattern.find_first_of('%', arg_identifier_pos + 1);
462  }
463  }
464 
465  return std::make_pair(pattern, order_index);
466  }
467 
468  /***/
469  QUILL_ATTRIBUTE_HOT std::string_view _format(
470  uint64_t timestamp, std::string_view thread_id, std::string_view thread_name,
471  std::string_view process_id, std::string_view logger, std::string_view log_level_description,
472  std::string_view log_level_short_code, MacroMetadata const& log_statement_metadata,
473  std::vector<std::pair<std::string, std::string>> const* named_args, std::string_view log_msg)
474  {
475  if (_is_set_in_pattern[Attribute::Time])
476  {
477  _set_arg_val<Attribute::Time>(_timestamp_formatter.format_timestamp(std::chrono::nanoseconds{timestamp}));
478  }
479 
480  if (_is_set_in_pattern[Attribute::FileName])
481  {
482  _set_arg_val<Attribute::FileName>(log_statement_metadata.file_name());
483  }
484 
485  if (_is_set_in_pattern[Attribute::CallerFunction])
486  {
487  std::string_view const function_name = _options.process_function_name
488  ? _options.process_function_name(log_statement_metadata.caller_function())
489  : std::string_view{log_statement_metadata.caller_function()};
490 
491  _set_arg_val<Attribute::CallerFunction>(function_name);
492  }
493 
494  if (_is_set_in_pattern[Attribute::LogLevel])
495  {
496  _set_arg_val<Attribute::LogLevel>(log_level_description);
497  }
498 
499  if (_is_set_in_pattern[Attribute::LogLevelShortCode])
500  {
501  _set_arg_val<Attribute::LogLevelShortCode>(log_level_short_code);
502  }
503 
504  if (_is_set_in_pattern[Attribute::LineNumber])
505  {
506  _set_arg_val<Attribute::LineNumber>(log_statement_metadata.line());
507  }
508 
509  if (_is_set_in_pattern[Attribute::Logger])
510  {
511  _set_arg_val<Attribute::Logger>(logger);
512  }
513 
514  if (_is_set_in_pattern[Attribute::FullPath])
515  {
516  _set_arg_val<Attribute::FullPath>(log_statement_metadata.full_path());
517  }
518 
519  if (_is_set_in_pattern[Attribute::ThreadId])
520  {
521  _set_arg_val<Attribute::ThreadId>(thread_id);
522  }
523 
524  if (_is_set_in_pattern[Attribute::ThreadName])
525  {
526  _set_arg_val<Attribute::ThreadName>(thread_name);
527  }
528 
529  if (_is_set_in_pattern[Attribute::ProcessId])
530  {
531  _set_arg_val<Attribute::ProcessId>(process_id);
532  }
533 
534  if (_is_set_in_pattern[Attribute::SourceLocation])
535  {
536  _set_arg_val<Attribute::SourceLocation>(_process_source_location_path(
537  log_statement_metadata.source_location(), _options.source_location_path_strip_prefix,
538  _options.source_location_remove_relative_paths));
539  ;
540  }
541 
542  if (_is_set_in_pattern[Attribute::ShortSourceLocation])
543  {
544  _set_arg_val<Attribute::ShortSourceLocation>(log_statement_metadata.short_source_location());
545  }
546 
547  if (_is_set_in_pattern[Attribute::NamedArgs])
548  {
549  _formatted_named_args_buffer.clear();
550 
551  if (named_args)
552  {
553  for (size_t i = 0; i < named_args->size(); ++i)
554  {
555  _formatted_named_args_buffer.append((*named_args)[i].first);
556  _formatted_named_args_buffer.append(std::string_view{": "});
557  _formatted_named_args_buffer.append((*named_args)[i].second);
558 
559  if (i != named_args->size() - 1)
560  {
561  _formatted_named_args_buffer.append(std::string_view{", "});
562  }
563  }
564  }
565 
566  _set_arg_val<Attribute::NamedArgs>(
567  std::string_view{_formatted_named_args_buffer.data(), _formatted_named_args_buffer.size()});
568  }
569 
570  if (_is_set_in_pattern[Attribute::Tags])
571  {
572  if (log_statement_metadata.tags())
573  {
574  _set_arg_val<Attribute::Tags>(std::string_view{log_statement_metadata.tags()});
575  }
576  else
577  {
578  _set_arg_val<Attribute::Tags>(std::string_view{});
579  }
580  }
581 
582  _set_arg_val<Attribute::Message>(log_msg);
583 
584  fmtquill::vformat_to(std::back_inserter(_formatted_log_message_buffer), _fmt_format,
585  fmtquill::basic_format_args(_args.data(), static_cast<int>(_args.size())));
586 
587  return std::string_view{_formatted_log_message_buffer.data(), _formatted_log_message_buffer.size()};
588  }
589 
590 private:
591  PatternFormatterOptions _options;
592  std::string _fmt_format;
593 
595  std::array<size_t, Attribute::ATTR_NR_ITEMS> _order_index{};
596  std::array<fmtquill::basic_format_arg<fmtquill::format_context>, Attribute::ATTR_NR_ITEMS> _args{};
597  std::bitset<Attribute::ATTR_NR_ITEMS> _is_set_in_pattern;
598 
600  detail::TimestampFormatter _timestamp_formatter;
601 
604  fmtquill::basic_memory_buffer<char, 512> _formatted_log_message_buffer;
605  fmtquill::basic_memory_buffer<char, 512> _formatted_named_args_buffer;
606 };
607 
608 QUILL_END_NAMESPACE
static constexpr char NO_SUFFIX
Special value to indicate no pattern suffix should be appended Using -1 cast to char ensures this val...
Definition: PatternFormatterOptions.h:153
Definition: UserDefinedDirectFormatFuzzer.cpp:81
Captures and stores information about a logging event in compile time.
Definition: MacroMetadata.h:22
Formats a timestamp given a format string as a pattern.
Definition: TimestampFormatter.h:38
Definition: PatternFormatter.h:33
Configuration options for the PatternFormatter.
Definition: PatternFormatterOptions.h:23
PatternFormatter(PatternFormatterOptions options)
Main PatternFormatter class.
Definition: PatternFormatter.h:79
custom exception
Definition: QuillError.h:45
Definition: SourceLocation.h:40
TimestampPrecision
Public classes.
Definition: PatternFormatter.h:40
Macro-Free Logging Interface
Definition: LogFunctions.h:53