diff --git a/include/log/format.h b/include/log/format.h index 908bb25..414da53 100644 --- a/include/log/format.h +++ b/include/log/format.h @@ -15,12 +15,12 @@ #include // IWYU pragma: keep #include // IWYU pragma: keep #else +#include #include #endif #include "location.h" #include "util/buffer.h" -#include "util/types.h" #include #include @@ -43,6 +43,12 @@ template using FormatString = fmt::basic_format_string; /** @brief Alias for \a fmt::format_error. */ using FormatError = fmt::format_error; +/** @brief Alias for \a fmt::formatter. */ +template +using Formatter = fmt::formatter; +/** @brief Alias for \a fmt::basic_format_parse_context. */ +template +using FormatParseContext = fmt::basic_format_parse_context; #else /** * @brief Alias for std::basic_format_string. @@ -54,6 +60,12 @@ template using FormatString = std::basic_format_string; /** @brief Alias for \a std::format_error. */ using FormatError = std::format_error; +/** @brief Alias for \a std::formatter. */ +template +using Formatter = std::formatter; +/** @brief Alias for \a std::basic_format_parse_context. */ +template +using FormatParseContext = std::basic_format_parse_context; #endif /** @@ -128,6 +140,121 @@ class Format final { Location m_loc; }; +template +class CachedFormatter; + +/** + * @brief Wrapper for formatting a value with a cached formatter. + * + * Stores the value and reference to the cached formatter (see CachedFormatter). + * Used in combination with `std::formatter, Char>` specialization. + * + * @tparam T Value type. + * @tparam Char Output character type. + */ +template +class FormatValue final { +public: + /** + * @brief Constructs a new FormatValue object from a reference to the formatter and value. + * + * @param formatter Reference to the formatter. + * @param value Value to be formatted. + */ + constexpr FormatValue(const CachedFormatter& formatter, T value) + : m_formatter(formatter) + , m_value(std::move(value)) + { + } + + ~FormatValue() = default; + + FormatValue(const FormatValue&) = delete; + FormatValue(FormatValue&&) = delete; + auto operator=(const FormatValue&) -> FormatValue& = delete; + auto operator=(FormatValue&&) noexcept -> FormatValue& = delete; + + /** + * @brief Formats the value with the cached formatter. + * + * @tparam Context Format context type (`std::format_context`). + * @param context Format context. + * @return Output iterator to the resulting buffer. + */ + template + auto format(Context& context) const + { + return m_formatter.format(m_value, context); + } + +private: + const CachedFormatter& m_formatter; + T m_value; +}; + +/** + * @brief Wrapper to parse the format string and store the format context. + * + * Used to parse format string only once and then cache the formatter. + * + * @tparam T Value type. + * @tparam Char Output character type. + */ +template +class CachedFormatter final : Formatter { +public: + using Formatter::format; + + /** + * @brief Constructs a new CachedFormatter object from a format string. + * + * @param fmt Format string. + */ + constexpr explicit CachedFormatter(std::basic_string_view fmt) +#ifdef ENABLE_FMTLIB + : m_empty(fmt.empty()) +#endif + { + FormatParseContext parse_context(std::move(fmt)); + Formatter::parse(parse_context); + } + + /** + * @brief Formats the value and writes to the output buffer. + * + * @tparam Out Output buffer type (see MemoryBuffer). + * @param out Output buffer. + * @param value Value to be formatted. + */ + template + void format(Out& out, T value) const + { +#ifdef ENABLE_FMTLIB + if constexpr (std::is_arithmetic_v) { + if (m_empty) [[likely]] { + out.append(fmt::format_int(value)); + return; + } + } + + using Appender = std::conditional_t< + std::is_same_v, + fmt::appender, + std::back_insert_iterator>>; + fmt::basic_format_context fmt_context(Appender(out), {}); + Formatter::format(std::move(value), fmt_context); +#else + static constexpr std::array Fmt{'{', '}', '\0'}; + out.format(Fmt.data(), FormatValue(*this, std::move(value))); +#endif + } + +#ifdef ENABLE_FMTLIB +private: + bool m_empty; +#endif +}; + /** * @brief Buffer used for log message formatting. * @@ -140,6 +267,35 @@ class FormatBuffer final : public MemoryBuffer { public: using MemoryBuffer::MemoryBuffer; + /** + * @brief Appends data to the end of the buffer. + * + * @tparam ContiguousRange Type of the source object. + * + * @param range Source object containing data to be added to the buffer. + */ + template + void append(const ContiguousRange& range) // cppcheck-suppress duplInheritedMember + { + append(range.data(), std::next(range.data(), range.size())); + } + + /** + * @brief Appends data to the end of the buffer. + * + * @tparam U Input data type. + * @param begin Begin input iterator. + * @param end End input iterator. + */ + template + void append(const U* begin, const U* end) // cppcheck-suppress duplInheritedMember + { + const auto buf_size = this->size(); + const auto count = static_cast>(end - begin); + this->resize(buf_size + count); + std::uninitialized_copy_n(begin, count, std::next(this->begin(), buf_size)); + } + /** * @brief Formats a log message with compile-time argument checks. * @@ -154,60 +310,37 @@ class FormatBuffer final : public MemoryBuffer { if constexpr (std::is_same_v) { fmt::format_to(fmt::appender(*this), std::move(fmt), std::forward(args)...); } else { -#if FMT_VERSION < 110000 fmt::format_to( std::back_inserter(*this), +#if FMT_VERSION < 110000 static_cast>(std::move(fmt)), - std::forward(args)...); #else - fmt::format_to(std::back_inserter(*this), std::move(fmt), std::forward(args)...); + std::move(fmt), #endif + std::forward(args)...); } #else std::format_to(std::back_inserter(*this), std::move(fmt), std::forward(args)...); #endif } +}; - /** - * @brief Formats a log message. - * - * @tparam Args Format argument types. Deduced from arguments. - * @param fmt Format string. - * @param args Format arguments. - * @throws FormatError if fmt is not a valid format string for the provided arguments. - */ - template - auto format_runtime(std::basic_string_view fmt, Args&... args) -> void +} // namespace PlainCloud::Log + +#ifndef ENABLE_FMTLIB +/** @cond */ +template +struct std::formatter, Char> { // NOLINT (cert-dcl58-cpp) + constexpr auto parse(PlainCloud::Log::FormatParseContext& context) { -#ifdef ENABLE_FMTLIB - if constexpr (std::is_same_v) { - fmt::vformat_to(fmt::appender(*this), std::move(fmt), fmt::make_format_args(args...)); - } else { + return context.end(); + } - fmt::vformat_to( - std::back_inserter(*this), - std::move(fmt), -#if FMT_VERSION < 110000 - fmt::make_format_args>(args...) -#else - fmt::make_format_args>(args...) -#endif - ); - } -#else - if constexpr (std::is_same_v) { - std::vformat_to( - std::back_inserter(*this), std::move(fmt), std::make_format_args(args...)); - } else if constexpr (std::is_same_v) { - std::vformat_to( - std::back_inserter(*this), std::move(fmt), std::make_wformat_args(args...)); - } else { - static_assert( - Util::Types::AlwaysFalse{}, - "std::vformat_to() supports only `char` or `wchar_t` character types"); - } -#endif + template + auto format(const PlainCloud::Log::FormatValue& wrapper, Context& context) const + { + return wrapper.format(context); } }; - -} // namespace PlainCloud::Log +/** @endcond */ +#endif diff --git a/include/log/location.h b/include/log/location.h index dceecea..b6389c7 100644 --- a/include/log/location.h +++ b/include/log/location.h @@ -5,16 +5,8 @@ #pragma once -#if __has_include() -#include -#endif - namespace PlainCloud::Log { -#ifdef __cpp_lib_source_location -/** @brief Alias for std::source_location. */ -using Location = std::source_location; -#else /** * @brief Represents a specific location in the source code. * @@ -43,7 +35,7 @@ class Location { #if __has_builtin(__builtin_FILE) and __has_builtin(__builtin_FUNCTION) \ and __has_builtin(__builtin_LINE) \ or defined(_MSC_VER) and _MSC_VER > 192 - const char* file = __builtin_FILE(), + const char* file = extract_file_name(__builtin_FILE()), const char* function = __builtin_FUNCTION(), int line = __builtin_LINE() #else @@ -89,9 +81,27 @@ class Location { } private: + consteval static auto extract_file_name(const char* path) -> const char* + { + const char* file = path; + const char sep = +#ifdef _WIN32 + '\\'; +#else + '/'; +#endif + while (*path != '\0') { + // NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (*path++ == sep) { + file = path; + } + } + return file; + } + const char* m_file{""}; const char* m_function{""}; int m_line{}; }; -#endif + } // namespace PlainCloud::Log diff --git a/include/log/pattern.h b/include/log/pattern.h index 99532f0..ddc73fd 100644 --- a/include/log/pattern.h +++ b/include/log/pattern.h @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -67,7 +68,16 @@ using namespace std; * @return A `std::basic_string_view` of the same character type as the input string. */ template -[[maybe_unused]] constexpr auto to_string_view(const String& str) -> std::basic_string_view; +struct ConvertString { + auto operator()(const String&) const -> std::basic_string_view = delete; +}; + +namespace Detail { +template +concept HasConvertString = requires(StringType value) { + { ConvertString{}(value) } -> std::same_as>; +}; +} // namespace Detail /** * @brief Represents a log message pattern specifying the message format. @@ -174,14 +184,17 @@ class Pattern { Message, Function, Time, + Msec, + Usec, + Nsec, Thread }; - /** @brief Field alignment options. */ - enum class Align : std::uint8_t { None, Left, Right, Center }; - /** @brief Parameters for string fields. */ struct StringSpecs { + /** @brief Field alignment options. */ + enum class Align : std::uint8_t { None, Left, Right, Center }; + std::size_t width = 0; ///< Field width. Align align = Align::None; ///< Field alignment. StringViewType fill = StringSpecs::DefaultFill.data(); ///< Fill character. @@ -190,34 +203,13 @@ class Pattern { static constexpr std::array DefaultFill{' ', '\0'}; }; - /** - * @brief Checks if the placeholder is for a string. - * - * @param type Placeholder type. - * @return \b true if the placeholder is a string. - * @return \b false if the placeholder is not a string. - */ - static auto is_string(Type type) -> bool - { - switch (type) { - case Placeholder::Type::Category: - [[fallthrough]]; - case Placeholder::Type::Level: - [[fallthrough]]; - case Placeholder::Type::File: - [[fallthrough]]; - case Placeholder::Type::Function: - [[fallthrough]]; - case Placeholder::Type::Message: - return true; - default: - break; - } - return false; - } - Type type = Type::None; ///< Placeholder type. - std::variant value = StringViewType{}; ///< Placeholder value. + std::variant< + StringViewType, + StringSpecs, + CachedFormatter, + CachedFormatter> + value = StringViewType{}; ///< Placeholder value. }; /** @@ -231,18 +223,24 @@ class Pattern { static constexpr std::array Line{'l', 'i', 'n', 'e', '\0'}; static constexpr std::array Function{'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', '\0'}; static constexpr std::array Time{'t', 'i', 'm', 'e', '\0'}; + static constexpr std::array Msec{'m', 's', 'e', 'c', '\0'}; + static constexpr std::array Usec{'u', 's', 'e', 'c', '\0'}; + static constexpr std::array Nsec{'n', 's', 'e', 'c', '\0'}; static constexpr std::array Thread{'t', 'h', 'r', 'e', 'a', 'd', '\0'}; static constexpr std::array Message{'m', 'e', 's', 's', 'a', 'g', 'e', '\0'}; public: /** @brief List of placeholder names. */ - static constexpr std::array List{ + static constexpr std::array, 11> List{ {{Placeholder::Type::Category, Placeholders::Category.data()}, {Placeholder::Type::Level, Placeholders::Level.data()}, {Placeholder::Type::File, Placeholders::File.data()}, {Placeholder::Type::Line, Placeholders::Line.data()}, {Placeholder::Type::Function, Placeholders::Function.data()}, {Placeholder::Type::Time, Placeholders::Time.data()}, + {Placeholder::Type::Msec, Placeholders::Msec.data()}, + {Placeholder::Type::Usec, Placeholders::Usec.data()}, + {Placeholder::Type::Nsec, Placeholders::Nsec.data()}, {Placeholder::Type::Thread, Placeholders::Thread.data()}, {Placeholder::Type::Message, Placeholders::Message.data()}}}; }; @@ -293,83 +291,72 @@ class Pattern { * This function formats a log message based on the specified pattern. * * @tparam StringType %Logger string type. - * @param result Buffer storing the raw message to be overwritten with the result. + * @param out Buffer storing the raw message to be overwritten with the result. * @param record Log record. */ template - auto format(auto& result, Record& record) -> void + auto format(auto& out, Record& record) -> void { - if (empty()) { - // Empty format, just append message - std::visit( - Util::Types::Overloaded{ - [&result](std::reference_wrapper arg) { - if constexpr (std::is_convertible_v) { - result.append(arg.get()); - } else { - result.append(to_string_view(arg.get())); - } - }, - [&result](auto&& arg) { result.append(std::forward(arg)); }}, - record.message); - } else { - using StringSpecs = typename Placeholder::StringSpecs; - - // Process format placeholders - for (auto& item : m_placeholders) { - switch (item.type) { - case Placeholder::Type::None: - result.append(std::get(item.value)); - break; - case Placeholder::Type::Category: - format_string(result, std::get(item.value), record.category); - break; - case Placeholder::Type::Level: - format_string( - result, std::get(item.value), m_levels.get(record.level)); - break; - case Placeholder::Type::File: - format_string( - result, std::get(item.value), record.location.filename); - break; - case Placeholder::Type::Function: - format_string( - result, std::get(item.value), record.location.function); - break; - case Placeholder::Type::Line: - result.format_runtime( - std::get(item.value), record.location.line); - break; - case Placeholder::Type::Time: - result.format_runtime(std::get(item.value), record.time); - break; - case Placeholder::Type::Thread: - result.format_runtime(std::get(item.value), record.thread_id); - break; - case Placeholder::Type::Message: - std::visit( - Util::Types::Overloaded{ - [this, &result, &specs = std::get(item.value)]( - std::reference_wrapper arg) { - if constexpr (std::is_convertible_v) { - RecordStringView message{StringViewType{arg.get()}}; - this->format_string(result, specs, message); - } else { - RecordStringView message{to_string_view(arg.get())}; - this->format_string(result, specs, message); - } - }, - [this, &result, &specs = std::get(item.value)]( - auto&& arg) { - this->format_string( - result, specs, std::forward(arg)); - }, + constexpr std::size_t MsecInNsec = 1000000; + constexpr std::size_t UsecInNsec = 1000; + + // Process format placeholders + for (auto& item : m_placeholders) { + switch (item.type) { + case Placeholder::Type::None: + out.append(std::get(item.value)); + break; + case Placeholder::Type::Category: + format_string(out, item.value, record.category); + break; + case Placeholder::Type::Level: + format_string(out, item.value, m_levels.get(record.level)); + break; + case Placeholder::Type::File: + format_string(out, item.value, record.location.filename); + break; + case Placeholder::Type::Function: + format_string(out, item.value, record.location.function); + break; + case Placeholder::Type::Line: + format_generic(out, item.value, record.location.line); + break; + case Placeholder::Type::Time: + format_generic(out, item.value, record.time.local); + break; + case Placeholder::Type::Msec: + format_generic(out, item.value, record.time.nsec / MsecInNsec); + break; + case Placeholder::Type::Usec: + format_generic(out, item.value, record.time.nsec / UsecInNsec); + break; + case Placeholder::Type::Nsec: + format_generic(out, item.value, record.time.nsec); + break; + case Placeholder::Type::Thread: + format_generic(out, item.value, record.thread_id); + break; + case Placeholder::Type::Message: + std::visit( + Util::Types::Overloaded{ + [&out, &value = item.value](std::reference_wrapper arg) { + if constexpr (Detail::HasConvertString) { + format_string( + out, value, ConvertString{}(arg.get())); + } else { + (void)out; + (void)value; + (void)arg; + throw FormatError( + "No corresponding Log::ConvertString<> specialization found"); + } }, - record.message); - break; - default: - break; - } + [&out, &value = item.value](auto&& arg) { + format_string(out, value, std::forward(arg)); + }, + }, + record.message); + break; } } } @@ -465,22 +452,22 @@ class Pattern { inside_placeholder = true; } else if (inside_placeholder && chr == '}') { // Leave the placeholder - Placeholder placeholder; + std::pair placeholder; for (const auto& item : Placeholders::List) { - if (pattern.starts_with(std::get(item.value))) { + if (pattern.starts_with(item.second)) { placeholder = item; break; } } - auto delta = std::get(placeholder.value).size(); - auto type = placeholder.type; + const auto type = placeholder.first; + const auto delta = placeholder.second.size(); m_pattern.append(pattern.substr(delta, pos + 1 - delta)); pattern = pattern.substr(pos + 1); // Save empty string view instead of {} to mark unformatted placeholder - append_placeholder(type, pos - delta + 2); + append_placeholder(type, pos - delta); inside_placeholder = false; } else { @@ -490,6 +477,12 @@ class Pattern { break; } } + + // If no placeholders found, just add message as a default + /*if (m_placeholders.empty()) { + m_placeholders.emplace_back( + Placeholder::Type::Message, typename Placeholder::StringSpecs{}); + }*/ } /** @@ -499,15 +492,16 @@ class Pattern { * the provided destination stream buffer. * * @tparam T Character type of the source string. - * @param result Destination stream buffer where the converted string will be appended. + * @param out Destination stream buffer where the converted string will be appended. * @param data Source multi-byte string to be converted. */ template - static void from_multibyte(auto& result, std::basic_string_view data) + static void from_multibyte(auto& out, std::basic_string_view data) { Char wchr; auto state = std::mbstate_t{}; std::size_t (*towc_func)(Char*, const T*, std::size_t, mbstate_t*) = nullptr; + if constexpr (std::is_same_v) { towc_func = std::mbrtowc; #ifdef __cpp_lib_char8_t @@ -523,41 +517,55 @@ class Pattern { for (int ret{}; (ret = static_cast(towc_func(&wchr, data.data(), data.size(), &state))) > 0; data = data.substr(ret)) { - result.push_back(wchr); + out.push_back(wchr); } } /** - * @brief Formats a string according to the specified specifications. + * @brief Formats a string according to the specifications. * * This function formats the source string based on the provided specifications, * including alignment and fill character, and appends the result to the given buffer. * - * @tparam T Character type for the string. - * @param result Buffer where the formatted string will be appended. - * @param specs Specifications for the string formatting (alignment and fill character). + * @tparam StringView Source string type (can be std::string_view or RecordStringView). + * @param out Buffer where the formatted string will be appended. + * @param item Variant holding either StringSpecs or RecordStringView. * @param data Source string to be formatted. */ - template - auto format_string( - auto& result, const Placeholder::StringSpecs& specs, RecordStringView& data) -> void + template + static void format_string(auto& out, const auto& item, StringView&& data) { - if (specs.width > 0) { - this->write_padded(result, data, specs); + if (auto& specs = std::get(item); specs.width > 0) + [[unlikely]] { + write_padded(out, std::forward(data), specs); } else { - if constexpr (std::is_same_v && !std::is_same_v) { - this->from_multibyte(result, data); // NOLINT(cppcoreguidelines-slicing) + using DataChar = typename std::remove_cvref_t::value_type; + if constexpr (std::is_same_v && !std::is_same_v) { + // NOLINTNEXTLINE (cppcoreguidelines-slicing) + from_multibyte(out, std::forward(data)); } else { - // Special case: since formatted message is stored - // at the beginning of the buffer, we have to reserve - // capacity first to prevent changing buffer address - // while re-allocating later. - result.reserve(result.size() + data.size()); - result.append(data); + out.append(std::forward(data)); } } } + /** + * @brief Formats data according to the cached format context. + * + * This function formats the source data based on the format context from CachedFormatter, + * including alignment and fill character, and appends the out to the given buffer. + * + * @tparam T Data type. + * @param out Buffer where the formatted data will be appended. + * @param item Variant holding CachedFormatter, where T is the data type. + * @param data Source data to be formatted. + */ + template + static void format_generic(auto& out, const auto& item, T data) + { + std::get>(item).format(out, data); + } + private: /** * @brief Converts a string to a non-negative integer. @@ -570,7 +578,7 @@ class Pattern { * @param error_value The default value to return in case of a conversion error. * @return The parsed integer, or error_value if the conversion fails. */ - constexpr auto + static constexpr auto parse_nonnegative_int(const Char*& begin, const Char* end, int error_value) noexcept -> int { unsigned value = 0; @@ -609,10 +617,10 @@ class Pattern { * @param specs Reference to the output specs structure to be updated. * @return Pointer to the past-the-end of the processed characters. */ - constexpr auto + static constexpr auto parse_align(const Char* begin, const Char* end, Placeholder::StringSpecs& specs) -> const Char* { - auto align = Placeholder::Align::None; + auto align = Placeholder::StringSpecs::Align::None; auto* ptr = std::next(begin, Util::Unicode::code_point_length(begin)); if (end - ptr <= 0) { ptr = begin; @@ -621,16 +629,16 @@ class Pattern { for (;;) { switch (Util::Unicode::to_ascii(*ptr)) { case '<': - align = Placeholder::Align::Left; + align = Placeholder::StringSpecs::Align::Left; break; case '>': - align = Placeholder::Align::Right; + align = Placeholder::StringSpecs::Align::Right; break; case '^': - align = Placeholder::Align::Center; + align = Placeholder::StringSpecs::Align::Center; break; } - if (align != Placeholder::Align::None) { + if (align != Placeholder::StringSpecs::Align::None) { if (ptr != begin) { // Actually this check is redundant, cause using '{' or '}' // as a fill character will cause parsing failure earlier. @@ -662,8 +670,8 @@ class Pattern { /** * @brief Append a pattern placeholder to the list of placeholders. * - * This function parses a placeholder from the end of the string and appends it to the list of - * placeholders. + * This function parses a placeholder from the end of the string and appends it to the list + * of placeholders. * * @param type Placeholder type. * @param count Placeholder length. @@ -672,16 +680,34 @@ class Pattern { void append_placeholder(Placeholder::Type type, std::size_t count, std::size_t shift = 0) { auto data = StringViewType{m_pattern}.substr(m_pattern.size() - count - shift, count); - if (type == Placeholder::Type::None && !m_placeholders.empty() - && m_placeholders.back().type == type) { - // In case of raw text, we can safely merge current chunk with the last one - auto& format = std::get(m_placeholders.back().value); - format = StringViewType{format.data(), format.size() + data.size()}; - } else if (Placeholder::is_string(type)) { - // Calculate formatted width for string fields + switch (type) { + case Placeholder::Type::None: + if (!m_placeholders.empty() && m_placeholders.back().type == type) { + // In case of raw text, we can safely merge current chunk with the last one + auto& format = std::get(m_placeholders.back().value); + format = StringViewType{format.data(), format.size() + data.size()}; + } else { + // Otherwise just add new string placeholder + m_placeholders.emplace_back(type, data); + } + break; + case Placeholder::Type::Category: + case Placeholder::Type::Level: + case Placeholder::Type::File: + case Placeholder::Type::Function: + case Placeholder::Type::Message: m_placeholders.emplace_back(type, get_string_specs(data)); - } else { - m_placeholders.emplace_back(type, data); + break; + case Placeholder::Type::Line: + case Placeholder::Type::Thread: + case Placeholder::Type::Msec: + case Placeholder::Type::Usec: + case Placeholder::Type::Nsec: + m_placeholders.emplace_back(type, CachedFormatter(data)); + break; + case Placeholder::Type::Time: + m_placeholders.emplace_back(type, CachedFormatter(data)); + break; } }; @@ -694,13 +720,13 @@ class Pattern { * @param value The string value of the placeholder field. * @return A Placeholder::StringSpecs object containing the parsed specifications. */ - auto get_string_specs(StringViewType value) -> Placeholder::StringSpecs + static auto get_string_specs(StringViewType value) -> Placeholder::StringSpecs { typename Placeholder::StringSpecs specs = {}; - if (value.size() > 2) { + if (!value.empty()) { const auto* begin = value.data(); const auto* end = std::next(begin, value.size()); - const auto* fmt = parse_align(std::next(begin, 2), end, specs); + const auto* fmt = parse_align(begin, end, specs); if (auto chr = Util::Unicode::to_ascii(*fmt); chr != '}') { const int width = parse_nonnegative_int(fmt, std::prev(end), -1); if (width == -1) { @@ -736,13 +762,21 @@ class Pattern { * @param src Source string view to be written. * @param specs String specifications, including alignment and fill character. */ - template - constexpr void - write_padded(auto& dst, RecordStringView& src, const Placeholder::StringSpecs& specs) + template + constexpr static void + write_padded(auto& dst, StringView&& src, const Placeholder::StringSpecs& specs) { + constexpr auto CountCodepoints = [](StringView& src) { + if constexpr (std::is_same_v) { + return Util::Unicode::count_codepoints(src.data(), src.size()); + } else { + return src.codepoints(); + } + }; + const auto spec_width = static_cast>(specs.width); - const auto width = src.codepoints(); + const auto width = CountCodepoints(src); const auto padding = spec_width > width ? spec_width - width : 0; // Shifts are encoded as string literals because static constexpr is not @@ -756,45 +790,52 @@ class Pattern { dst.reserve(dst.size() + src.size() + padding * specs.fill.size()); // Lambda for filling with single character or multibyte pattern - auto fill_pattern = [&dst, &fill = specs.fill](std::size_t fill_len) { - const auto* src = fill.data(); - auto block_size = fill.size(); - auto* dest = std::prev(dst.end(), fill_len * block_size); - - if (block_size > 1) { - // Copy first block - std::copy_n(src, block_size, dest); - - // Copy other blocks recursively via O(n*log(N)) calls - const auto* start = dest; - const auto* end = std::next(start, fill_len * block_size); - auto* current = std::next(dest, block_size); - while (std::next(current, block_size) < end) { - std::copy_n(start, block_size, current); - std::advance(current, block_size); - block_size *= 2; - } - - // Copy the rest - std::copy_n(start, end - current, current); - } else { - std::fill_n(dest, fill_len, *src); - } - }; + constexpr auto FillPattern + = [](auto& dst, std::basic_string_view fill, std::size_t fill_len) { + const auto* src = fill.data(); + auto block_size = fill.size(); + auto* dest = std::prev(dst.end(), fill_len * block_size); + + if (block_size > 1) { + // Copy first block + std::copy_n(src, block_size, dest); + + // Copy other blocks recursively via O(n*log(N)) calls + const auto* start = dest; + const auto* end = std::next(start, fill_len * block_size); + auto* current = std::next(dest, block_size); + while (std::next(current, block_size) < end) { + std::copy_n(start, block_size, current); + std::advance(current, block_size); + block_size *= 2; + } + + // Copy the rest + std::copy_n(start, end - current, current); + } else { + std::fill_n(dest, fill_len, *src); + } + }; // Fill left padding if (left_padding != 0) { dst.resize(dst.size() + left_padding * specs.fill.size()); - fill_pattern(left_padding); + FillPattern(dst, specs.fill, left_padding); } // Fill data - dst.append(src); + using DataChar = typename std::remove_cvref_t::value_type; + if constexpr (std::is_same_v && !std::is_same_v) { + // NOLINTNEXTLINE (cppcoreguidelines-slicing) + from_multibyte(dst, std::forward(src)); + } else { + dst.append(std::forward(src)); + } // Fill right padding if (right_padding != 0) { dst.resize(dst.size() + right_padding * specs.fill.size()); - fill_pattern(right_padding); + FillPattern(dst, specs.fill, right_padding); } } diff --git a/include/log/record.h b/include/log/record.h index ac0cb74..6a184f0 100644 --- a/include/log/record.h +++ b/include/log/record.h @@ -8,14 +8,18 @@ #include "util/unicode.h" #include -#include -#include #include #include -#include #include #include +#ifdef ENABLE_FMTLIB +#include // IWYU pragma: no_forward_declare tm +#else +#include +#include +#endif + namespace PlainCloud::Log { enum class Level : std::uint8_t; @@ -134,18 +138,9 @@ class RecordStringView : public std::basic_string_view { */ auto codepoints() -> std::size_t { - auto codepoints = m_codepoints.load(std::memory_order_consume); + auto codepoints = m_codepoints.load(std::memory_order_acquire); if (codepoints == std::string_view::npos) { - codepoints = 0; - if constexpr (sizeof(T) != 1) { - codepoints = this->size(); - } else { - const auto size = this->size(); - const auto data = this->data(); - for (std::size_t idx = 0; idx < size; codepoints++) { - idx += Util::Unicode::code_point_length(std::next(data, idx)); - } - } + codepoints = Util::Unicode::count_codepoints(this->data(), this->size()); m_codepoints.store(codepoints, std::memory_order_release); } return codepoints; @@ -163,6 +158,30 @@ class RecordStringView : public std::basic_string_view { template RecordStringView(const Char*, std::size_t) -> RecordStringView; +/** + * @brief Time tag of the log record. + */ +struct RecordTime { +#ifdef ENABLE_FMTLIB + /** @brief Alias for \a std::tm. */ + using TimePoint = std::tm; +#else + /** @brief Alias for \a std::chrono::sys_seconds. */ + using TimePoint = std::chrono::sys_seconds; +#endif + TimePoint local; ///< Local time (seconds precision). + std::size_t nsec = {}; ///< Event time (nsec part). +}; + +/** + * @brief Source code location. + */ +struct RecordLocation { + RecordStringView filename = {}; ///< File name. + RecordStringView function = {}; ///< Function name. + std::size_t line = {}; ///< Line number. +}; + /** * @brief Log record. * @@ -171,20 +190,11 @@ RecordStringView(const Char*, std::size_t) -> RecordStringView; */ template struct Record { - /** - * @brief Source code location. - */ - struct Location { - RecordStringView filename = {}; ///< File name. - RecordStringView function = {}; ///< Function name. - std::size_t line = {}; ///< Line number. - }; - Level level = {}; ///< Log level. - Location location = {}; ///< Source code location. + RecordLocation location = {}; ///< Source code location. RecordStringView category = {}; ///< Log category. - std::chrono::system_clock::time_point time; ///< Event time. std::size_t thread_id = {}; ///< Thread ID. + RecordTime time = {}; ///< Record time. std::variant, RecordStringView> message = RecordStringView{}; ///< Log message. }; diff --git a/include/log/sink.h b/include/log/sink.h index a0447d4..3dded21 100644 --- a/include/log/sink.h +++ b/include/log/sink.h @@ -9,14 +9,14 @@ #include "location.h" #include "pattern.h" #include "record.h" +#include "util/os.h" -#include #include #include #include #include #include -#include +#include #include #include #include @@ -135,11 +135,9 @@ class Sink : public std::enable_shared_from_this> { * The former type is used for direct passing custom string type, while latter is for * types convertible to `std::basic_string_view`. * - * @param buffer Buffer where the final message should be put. - * It already contains a message, feel free to overwrite it. * @param record Log record (message, category, etc.). */ - virtual auto message(FormatBufferType& buffer, RecordType& record) -> void = 0; + virtual auto message(RecordType& record) -> void = 0; /** * @brief Flush message cache, if any. @@ -331,51 +329,47 @@ class SinkDriver final { Location location = Location::current(), // cppcheck-suppress passedByValue Args&&... args) const -> void { - FormatBufferType buffer; + FormatBufferType buffer; // NOLINT(misc-const-correctness) RecordType record = {level, - {location.file_name(), location.function_name(), location.line()}, + {location.file_name(), + location.function_name(), + static_cast(location.line())}, std::move(category), - std::chrono::system_clock::now(), - std::hash{}(std::this_thread::get_id())}; + Util::OS::thread_id()}; + std::tie(record.time.local, record.time.nsec) + = Util::OS::local_time(); // Flag to check that message has been evaluated bool evaluated = false; const typename ThreadingPolicy::ReadLock lock(m_mutex); for (const auto& [sink, logger] : m_effective_sinks) { - if (!logger->level_enabled(level)) { + if (!logger->level_enabled(level)) [[unlikely]] { continue; } - if (!evaluated) { + if (!evaluated) [[unlikely]] { evaluated = true; using BufferRefType = std::add_lvalue_reference_t; if constexpr (std::is_invocable_v) { // Callable with buffer argument: message will be stored in buffer. callback(buffer, std::forward(args)...); - record.message = RecordStringView(buffer.data(), buffer.size()); - // Update pointer to message on every buffer re-allocation - buffer.on_grow( - [](const CharType* data, std::size_t, void* message) { - static_cast*>(message)->update_data_ptr( - data); - }, - &std::get>(record.message)); + record.message = RecordStringView{buffer.data(), buffer.size()}; } else if constexpr (std::is_invocable_v) { - if constexpr (std::is_void_v>) { + using RetType = typename std::invoke_result_t; + if constexpr (std::is_void_v) { // Void callable without arguments: there is no message, just a callback callback(std::forward(args)...); break; } else { // Non-void callable without arguments: message is the return value auto message = callback(std::forward(args)...); - using Ret = std::invoke_result_t; - if constexpr (std::is_convertible_v>) { + if constexpr (std::is_convertible_v>) { record.message = RecordStringView{std::move(message)}; } else { - record.message = std::move(message); + record.message = message; } } } else if constexpr (std::is_convertible_v>) { @@ -384,11 +378,11 @@ class SinkDriver final { // NOLINTNEXTLINE(*-array-to-pointer-decay,*-no-array-decay) record.message = RecordStringView{std::forward(callback)}; } else { - record.message = std::forward(callback); + record.message = callback; } } - sink->message(buffer, record); + sink->message(record); } } @@ -480,6 +474,6 @@ class SinkDriver final { std::unordered_map*, const Logger*> m_effective_sinks; std::unordered_map>, bool> m_sinks; mutable ThreadingPolicy::Mutex m_mutex; -}; +}; // namespace PlainCloud::Log } // namespace PlainCloud::Log diff --git a/include/log/sinks/dummy_sink.h b/include/log/sinks/dummy_sink.h index 16a1139..d57618b 100644 --- a/include/log/sinks/dummy_sink.h +++ b/include/log/sinks/dummy_sink.h @@ -36,12 +36,11 @@ class DummySink : public Sink { { } - auto message(FormatBufferType& buffer, RecordType& record) -> void override + auto message(RecordType& record) -> void override { - const auto orig_size = buffer.size(); - this->format(buffer, record); + FormatBufferType buffer; + Sink::format(buffer, record); buffer.push_back('\n'); - buffer.resize(orig_size); } auto flush() -> void override diff --git a/include/log/sinks/file_sink.h b/include/log/sinks/file_sink.h new file mode 100644 index 0000000..541c1c2 --- /dev/null +++ b/include/log/sinks/file_sink.h @@ -0,0 +1,72 @@ +/** + * @file ostream_sink.h + * @brief Contains definition of FileSink class. + */ + +#pragma once + +#include "log/sink.h" // IWYU pragma: export + +#include +#include +#include +#include +#include +#include + +namespace PlainCloud::Log { + +/** + * @brief File-based sink + * + * @tparam Logger %Logger class type intended for the sink to be used with. + */ +template +class FileSink : public Sink { +public: + using typename Sink::CharType; + using typename Sink::FormatBufferType; + using typename Sink::RecordType; + + /** + * @brief Construct a new FileSink object + * + * @tparam Args Argument types for the pattern and log levels. + * @param filename Output file name. + * @param args Optional pattern and list of log levels. + */ + template + explicit FileSink(std::string_view filename, Args&&... args) + : Sink(std::forward(args)...) + { + fp = std::fopen(filename.data(), "w+"); + if (!fp) { + throw std::system_error({errno, std::system_category()}, std::strerror(errno)); + } + } + + virtual ~FileSink() + { + std::fclose(fp); + } + + auto message(RecordType& record) -> void override + { + // const auto orig_size = buffer.size(); + FormatBufferType buffer; + Sink::format(buffer, record); + buffer.push_back('\n'); + // std::fwrite(std::next(buffer.begin(), orig_size), buffer.size() - orig_size, 1, fp); + std::fwrite(buffer.data(), buffer.size(), 1, fp); + // buffer.resize(orig_size); + } + + auto flush() -> void override + { + std::fflush(fp); + } + +private: + FILE* fp = nullptr; +}; +} // namespace PlainCloud::Log diff --git a/include/log/sinks/ostream_sink.h b/include/log/sinks/ostream_sink.h index 3c27066..23909a5 100644 --- a/include/log/sinks/ostream_sink.h +++ b/include/log/sinks/ostream_sink.h @@ -7,7 +7,6 @@ #include "log/sink.h" // IWYU pragma: export -#include #include #include @@ -53,13 +52,12 @@ class OStreamSink : public Sink { { } - auto message(FormatBufferType& buffer, RecordType& record) -> void override + auto message(RecordType& record) -> void override { - const auto orig_size = buffer.size(); - this->format(buffer, record); + FormatBufferType buffer; + Sink::format(buffer, record); buffer.push_back('\n'); - m_ostream.write(std::next(buffer.begin(), orig_size), buffer.size() - orig_size); - buffer.resize(orig_size); + m_ostream.write(buffer.begin(), buffer.size()); } auto flush() -> void override diff --git a/include/util/buffer.h b/include/util/buffer.h index 5b6d939..9417b43 100644 --- a/include/util/buffer.h +++ b/include/util/buffer.h @@ -7,17 +7,20 @@ #include // IWYU pragma: keep #include -#include #include #include #ifdef ENABLE_FMTLIB +#include + /** - * @brief Defines an alias for `fmt::detail::buffer`. + * @brief Defines an alias for `fmt::detail::buffer`. */ template using Buffer = fmt::detail::buffer; #else +#include + /** * @brief Represents a contiguous memory buffer with an optional growing ability. */ @@ -173,11 +176,11 @@ class Buffer { auto count = static_cast>(end - begin); try_reserve(m_size + count); - auto free_cap = m_capacity - m_size; + const auto free_cap = m_capacity - m_size; if (free_cap < count) { count = free_cap; } - if (std::is_same::value) { + if constexpr (std::is_same_v) { std::uninitialized_copy_n(begin, count, std::next(m_ptr, m_size)); } else { T* out = std::next(m_ptr, m_size); @@ -314,15 +317,6 @@ class MemoryBuffer : public Buffer { using const_reference = const T&; // NOLINT(readability-identifier-naming) /// @endcond - /** - * @brief Defines a callback to be called after the buffer grows. - * - * @param data Pointer to the data. - * @param size Updated size. - * @param userdata Pointer to the user data. - */ - using OnGrowCallback = void (*)(const T* data, std::size_t size, void* userdata); - /** * @brief Constructs a new MemoryBuffer object. * @@ -418,15 +412,16 @@ class MemoryBuffer : public Buffer { } /** - * @brief Sets the callback to be called after the buffer grows. + * @brief Appends data to the end of the buffer. * - * @param callback Pointer to the callback function. - * @param userdata Pointer to the user data passed to the callback. + * @tparam U Input data type. + * @param begin Begin iterator of the source data. + * @param end End iterator of the source data. */ - void on_grow(OnGrowCallback callback, void* userdata = nullptr) + template + void append(const U* begin, const U* end) { - m_on_grow = callback; - m_on_grow_userdata = userdata; + Buffer::append(begin, end); } protected: @@ -468,9 +463,6 @@ class MemoryBuffer : public Buffer { if (old_data != static_cast(self.m_store)) { self.m_allocator.deallocate(old_data, old_capacity); } - if (self.m_on_grow) { - self.m_on_grow(new_data, new_capacity, self.m_on_grow_userdata); - } } private: @@ -512,6 +504,4 @@ class MemoryBuffer : public Buffer { T m_store[Size]; // NOLINT(*-avoid-c-arrays) Allocator m_allocator; - OnGrowCallback m_on_grow = nullptr; - void* m_on_grow_userdata = nullptr; }; diff --git a/include/util/os.h b/include/util/os.h new file mode 100644 index 0000000..efa2a44 --- /dev/null +++ b/include/util/os.h @@ -0,0 +1,149 @@ +/** + * @file os.h + * @brief Provides OS-specific functions. + */ + +#pragma once + +#include +#include // IWYU pragma: no_forward_declare tm +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include // for GetCurrentThreadId +#else +#include +#ifdef __linux__ +#include // use gettid() syscall under linux to get thread id +#elif defined(_AIX) +#include // for pthread_getthrds_np +#elif defined(__DragonFly__) || defined(__FreeBSD__) +#include // for pthread_getthreadid_np +#elif defined(__NetBSD__) +#include // for _lwp_self +#elif defined(__sun) +#include // for thr_self +#elif defined(__APPLE__) +#include // for MAC_OS_X_VERSION_MAX_ALLOWED +#include // for pthread_threadid_np +#endif +#endif + +namespace PlainCloud::Util::OS { + +/** + * @brief Retrieves current thread ID in a cross-platform way. + * + * Uses `GetCurrentThreadId()()` on Windows, `gettid` syscall on Linux, + * `pthread` on other UNIX-based systems. + * Falls back to `std::this_thread::get_id()` in other cases. + * + * @return Current thread ID. + */ +inline auto thread_id() noexcept -> std::size_t +{ + static thread_local std::size_t cached_tid; + + if (cached_tid == 0) { +#ifdef _WIN32 + cached_tid = static_cast(::GetCurrentThreadId()); +#elif defined(__linux__) +#if defined(__ANDROID__) && defined(__ANDROID_API__) && (__ANDROID_API__ < 21) +#define SYS_gettid __NR_gettid +#endif + // NOLINTNEXTLINE (*-vararg) + cached_tid = static_cast(::syscall(SYS_gettid)); +#elif defined(_AIX) + struct __pthrdsinfo buf; + int reg_size = 0; + pthread_t pt = pthread_self(); + int retval + = pthread_getthrds_np(&pt, PTHRDSINFO_QUERY_TID, &buf, sizeof(buf), NULL, ®_size); + int tid = (!retval) ? buf.__pi_tid : 0; + cached_tid = static_cast(tid); +#elif defined(__DragonFly__) || defined(__FreeBSD__) + cached_tid = static_cast(::pthread_getthreadid_np()); +#elif defined(__NetBSD__) + cached_tid = static_cast(::_lwp_self()); +#elif defined(__OpenBSD__) + cached_tid = static_cast(::getthrid()); +#elif defined(__sun) + cached_tid = static_cast(::thr_self()); +#elif defined(__APPLE__) + uint64_t tid; +// There is no pthread_threadid_np prior to Mac OS X 10.6, and it is not supported on any PPC, +// including 10.6.8 Rosetta. __POWERPC__ is Apple-specific define encompassing ppc and ppc64. +#ifdef MAC_OS_X_VERSION_MAX_ALLOWED + { +#if (MAC_OS_X_VERSION_MAX_ALLOWED < 1060) || defined(__POWERPC__) + tid = pthread_mach_thread_np(pthread_self()); +#elif MAC_OS_X_VERSION_MIN_REQUIRED < 1060 + if (&pthread_threadid_np) { + pthread_threadid_np(nullptr, &tid); + } else { + tid = pthread_mach_thread_np(pthread_self()); + } +#else + pthread_threadid_np(nullptr, &tid); +#endif + } +#else + pthread_threadid_np(nullptr, &tid); +#endif + cached_tid = static_cast(tid); +#else // Default to standard C++11 (other Unix) + cached_tid + = static_cast(std::hash()(std::this_thread::get_id())); +#endif + } + + return cached_tid; +} + +/** + * @brief Retrieves local time in a cross-platform way. + * + * Uses `localtime_s` on Windows and `localtime_r` on other systems. + * + * @tparam TimePoint type of resulting time point. Can be `std::chrono::time_point<>` or `std::tm`. + * @return Pair of local time in seconds and additional nanoseconds. + */ +template +inline auto local_time() noexcept -> std::pair +{ + static thread_local TimePoint cached_local; + static thread_local std::time_t cached_time; + + std::timespec curtime{}; +#ifdef __linux__ + ::clock_gettime(CLOCK_REALTIME_COARSE, &curtime); +#else + std::timespec_get(&curtime, TIME_UTC); +#endif + + if (curtime.tv_sec != cached_time) { + cached_time = curtime.tv_sec; + if constexpr (std::is_same_v) { +#ifdef _WIN32 + ::localtime_s(&cached_local, &curtime.tv_sec); +#else + ::localtime_r(&curtime.tv_sec, &cached_local); +#endif + } else { + cached_local = TimePoint(std::chrono::duration_cast( + std::chrono::current_zone() + ->to_local(std::chrono::sys_seconds(std::chrono::seconds(curtime.tv_sec))) + .time_since_epoch())); + } + } + + return std::make_pair(cached_local, static_cast(curtime.tv_nsec)); +} + +} // namespace PlainCloud::Util::OS \ No newline at end of file diff --git a/include/util/unicode.h b/include/util/unicode.h index 802fa03..80c75ae 100644 --- a/include/util/unicode.h +++ b/include/util/unicode.h @@ -5,6 +5,8 @@ #pragma once +#include +#include #include namespace PlainCloud::Util::Unicode { @@ -50,6 +52,27 @@ constexpr auto code_point_length(const Char* begin) -> int } } +/** + * @brief Calculates number of Unicode code points in a source data. + * + * @tparam Char Character type. + * @param begin Pointer to the start of the Unicode sequence. + * @param len Number of bytes in a source data. + */ +template +constexpr auto count_codepoints(const Char* begin, std::size_t len) -> std::size_t +{ + if constexpr (sizeof(Char) != 1) { + return len; + } else { + std::size_t codepoints = 0; + for (const auto* end = std::next(begin, len); begin != end; ++codepoints) { + std::advance(begin, Util::Unicode::code_point_length(begin)); + } + return codepoints; + } +} + /** * @brief Converts a character code to ASCII. *