commit b0ed20369fd7994e16eaa9572e89c0f62ad83d96 Author: Kurt Sassenrath Date: Mon Sep 4 16:03:58 2023 -0700 Initial commit * Logging ported from layover project with minor tweaks. * Logging test ported over. * Boost libraries are available. diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..a409600 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,2 @@ +build --action_env=BAZEL_CXXOPTS="-std=c++20:-g:-O2" +run --action_env=BAZEL_CXXOPTS="-std=c++20:-g:-O2" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac51a05 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bazel-* diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..f3b23d6 --- /dev/null +++ b/BUILD @@ -0,0 +1,11 @@ +load("@hedron_compile_commands//:refresh_compile_commands.bzl", + "refresh_compile_commands" +) + +refresh_compile_commands( + name = "refresh_compile_commands", + targets = { + "//source:parselinklog": "", + "//tests/...": "", + }, +) diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..a481b77 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,100 @@ +workspace(name = "parselink") + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +#=============================================================================== +# Imported Bazel modules +#=============================================================================== + +#------------------------------------------------------------------------------- +# Boost libraries, needed for asio + beast +#------------------------------------------------------------------------------- +rules_boost_commit = "49dc7d0e697c784f207fb1773b5b371c2511bfb8" +rules_boost_url = "https://github.com/nelhage/rules_boost/archive/" + +http_archive( + name = "com_github_nelhage_rules_boost", + url = rules_boost_url + rules_boost_commit + ".zip", + strip_prefix = "rules_boost-" + rules_boost_commit +) + +load("@com_github_nelhage_rules_boost//:boost/boost.bzl", "boost_deps") +boost_deps() + +#------------------------------------------------------------------------------- +# magic_enum: Used in logging implementation for enum names. +#------------------------------------------------------------------------------- +magic_enum_version = "0.8.2" +magic_enum_base_url = \ + "https://github.com/Neargye/magic_enum/archive/refs/tags/v" +magic_enum_sha256 = \ + "a10fa650307c60950b712a65a33fb46e9ac96ea72585cfa9fcf9ac9f502c01eb" + +http_archive( + name = "magic_enum", + sha256 = magic_enum_sha256, + url = magic_enum_base_url + magic_enum_version + ".zip", + strip_prefix = "magic_enum-" + magic_enum_version, +) + +#------------------------------------------------------------------------------- +# fmt: Used in logging implementation. +#------------------------------------------------------------------------------- +fmt_version = "10.1.1" +fmt_base_url = "https://github.com/fmtlib/fmt/archive/refs/tags/" + +http_archive( + name = "fmt", + url = fmt_base_url + fmt_version + ".zip", + patch_cmds = [ + "mv support/bazel/.bazelversion .bazelbersion", + "mv support/bazel/BUILD.bazel BUILD.bazel", + "mv support/bazel/WORKSPACE.bazel WORKSPACE.bazel", + ], + strip_prefix = "fmt-" + fmt_version, +) + +#------------------------------------------------------------------------------- +# ut: Unit test framework. +# TODO(kss): Only if tests are needed? +#------------------------------------------------------------------------------- +ut_version = "1.1.9" +ut_base_url = "https://github.com/boost-ext/ut/archive/refs/tags/v" +ut_sha256 = "5811d993f88c5ba4916784cef60d1cb529917fb9a3f72236219cb9ee9c1974ca" + +http_archive( + name = "ut", + url = ut_base_url + ut_version + ".zip", + sha256 = ut_sha256, + build_file_content = +""" +cc_library( + name = "ut", + includes = ["include"], + hdrs = glob(["include/**/*.hpp"]), + visibility = ["//visibility:public"], +) +""", + strip_prefix = "ut-" + ut_version, +) + +#------------------------------------------------------------------------------- +# Support compile_commands.json generation for LSP. +#------------------------------------------------------------------------------- +hedron_commit = "3dddf205a1f5cde20faf2444c1757abe0564ff4c" +hedron_sha256 = \ + "a4ce320769ba39a292ae0319eb534599d5114751ec9873e7eaa2aa5b7b7af1b2" +hedron_base_url = \ + "https://github.com/hedronvision/bazel-compile-commands-extractor/archive/" + +http_archive( + name = "hedron_compile_commands", + sha256 = hedron_sha256, + strip_prefix = "bazel-compile-commands-extractor-" + hedron_commit, + url = hedron_base_url + hedron_commit + ".zip", +) + +load("@hedron_compile_commands//:workspace_setup.bzl", + "hedron_compile_commands_setup", +) +hedron_compile_commands_setup() diff --git a/source/BUILD b/source/BUILD new file mode 100644 index 0000000..ef17b82 --- /dev/null +++ b/source/BUILD @@ -0,0 +1,15 @@ +cc_binary( + name = "parselinkd", + srcs = ["main.cpp"], + deps = [ + "@fmt", + "@boost//:beast", + ], +) +cc_binary( + name = "parselinklog", + srcs = ["log.cpp"], + deps = [ + "//source/common" + ], +) diff --git a/source/common/BUILD b/source/common/BUILD new file mode 100644 index 0000000..ae97318 --- /dev/null +++ b/source/common/BUILD @@ -0,0 +1,25 @@ +# parselink + +cc_library( + name = "lib", + srcs = [ + "source/logging.cpp", + ], + hdrs = [ + "include/logging.h", + "include/logging/level.h", + "include/logging/formatters.h", + "include/logging/theme.h", + "include/logging/traits.h", + ], + linkstatic = True, + includes = ["include"], + deps = [ + "@fmt//:fmt", + "@magic_enum//:magic_enum", + ], + visibility = [ + # TODO: Fix visibility + "//visibility:public", + ], +) diff --git a/source/common/include/logging.h b/source/common/include/logging.h new file mode 100644 index 0000000..05aac2d --- /dev/null +++ b/source/common/include/logging.h @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Logging +// +// Logging infrastructure. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- + +#ifndef logging_982a89e400976f59 +#define logging_982a89e400976f59 + +#include +#include +#include +#include + +#include "logging/formatters.h" +#include "logging/theme.h" +#include "logging/traits.h" + +namespace parselink { +namespace logging { + +// Any log levels higher (lower severity) than static_threshold cannot not be +// enabled in the library, but the compiler should be able to optimize away +// some/most of the calls. +constexpr inline auto static_threshold = level::verbose; +constexpr inline auto default_threshold = level::info; + +// Structure for holding a message. Note: message is a view over some buffer, +// not it's own string. It will either be a static buffer supplied by the +// endpoint implementation, or it will be dynamically created by the logger +// before calling endpoint::write() +struct message { + level lvl; + std::chrono::system_clock::time_point time; + std::string_view name; + std::string_view message; +}; + +struct endpoint { + virtual ~endpoint() = default; + virtual std::span buffer() { return {}; } + virtual void write(message const& msg) = 0; + virtual bool colored() const noexcept = 0; + level threshold{default_threshold}; +}; + +class logger { +public: + // This constructor utilizes the default logging endpoint. + logger(std::string_view name); + + // This constructor allows for an arbitrary number of logging endpoints + // to be passed in. + logger(std::string_view name, + std::vector> eps); + + template + explicit logger(std::string_view name, Endpoints&&... eps) + : logger(name, {std::forward(eps)...}) {} + + template + requires (Level <= static_threshold) + void log(fmt::format_string format, Args&&... args) const { + try_write(Level, format.get(), std::forward(args)...); + } + + template + requires (Level > static_threshold) + [[gnu::flatten]] void log(fmt::format_string, Args&&...) const {} + + void set_threshold(level new_threshold) noexcept; + +private: + + template + void write_endpoint(std::shared_ptr const& endpoint, message msg, + fmt::string_view format, Args&&... args) const { + auto buff = endpoint->buffer(); + if (buff.empty()) { + // Allocate memory. + auto buff = fmt::memory_buffer(); + fmt::vformat_to(std::back_inserter(buff), format, + fmt::make_format_args(args...)); + msg.message = std::string_view{buff.data(), buff.size()}; + endpoint->write(msg); + } else { + // Fill the static buffer. + fmt::vformat_to(buff.begin(), format, + fmt::make_format_args(args...)); + msg.message = std::string_view{buff.data(), buff.size()}; + endpoint->write(msg); + } + } + + template + void try_write(level level, fmt::string_view format, + Args&&... args) const { + message msg{level, std::chrono::system_clock::now(), name_}; + for (auto const& ep : endpoints_) { + if (level > ep->threshold) continue; + if (ep->colored()) { + write_endpoint(ep, msg, format, + themed_arg{std::forward(args)}...); + } else { + write_endpoint(ep, msg, format, + format_arg{std::forward(args)}...); + } + } + } + + std::string name_; + std::vector> endpoints_; +}; + +} // namespace logging +} // namespace parselink + +#endif // logging_982a89e400976f59 diff --git a/source/common/include/logging/formatters.h b/source/common/include/logging/formatters.h new file mode 100644 index 0000000..9b33675 --- /dev/null +++ b/source/common/include/logging/formatters.h @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Logging +// +// {fmt} formatters for various types that are used throughout the codebase. +// Some are specific, while others depend on traits. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- + +#ifndef logging_formatters_d22a64b1645a8134 +#define logging_formatters_d22a64b1645a8134 + +#include "traits.h" +#include "theme.h" + +/* #include */ + +#include + +// Simple wrapper for error strings to be formatted like any other string_view. +template <> +struct fmt::formatter + : fmt::formatter { + template + constexpr auto format(auto const& v, FormatContext& ctx) const { + return fmt::formatter::format(v.v, ctx); + } +}; + +// Support enums. By default, enums print the enum type name as well as the +// enum name for the given value. E.g. +// +// enum Color { Red, Green, Blue }; +// auto const color = Color::Green; +// +// Logging `color` will yield "Color::Green" +template +requires std::is_enum_v +struct fmt::formatter : fmt::formatter { + template + auto format(E const v, FormatContext& ctx) const { + auto str = [v]{ + return fmt::format("{}::{}", + magic_enum::enum_type_name(), + magic_enum::enum_name(v)); + }(); + return fmt::formatter::format(str, ctx); + } +}; + +// Support enums that _only_ print the string "name" of the enum value. Using +// the same example above: +// +// enum Color { Red, Green, Blue }; +// auto const color = Color::Green; +// +// Logging `enum_name_only{color}` will yield "Green" +template +requires std::is_enum_v +struct fmt::formatter> + : fmt::formatter { + using enum_name_only = parselink::logging::enum_name_only; + template + auto format(enum_name_only const v, FormatContext& ctx) const { + return fmt::formatter::format( + magic_enum::enum_name(v.v), ctx); + } +}; + +// Support conversion of typical standard error codes into a human-readable +// string. +template <> +struct fmt::formatter : fmt::formatter { + template + auto format(std::errc const& v, FormatContext& ctx) const { + return fmt::formatter::format( + std::make_error_code(v).message(), ctx); + } +}; + +// Support printing raw/smart pointers without needing to wrap them in fmt::ptr +template +struct fmt::formatter : fmt::formatter { + template + auto format(T const& v, FormatContext& ctx) const { + return fmt::formatter::format(fmt::ptr(v), ctx); + } +}; + +#if 0 +// TODO(ksassenrath): Re-enable when expected has been integrated +template +struct fmt::formatter> { + template + constexpr auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { + return ctx.begin(); + } + + template + auto format(parselink::expected const& v, FormatContext& ctx) const { + if (v.has_value()) { + return fmt::format_to(ctx.out(), "{}", parselink::logging::format_arg{v.value()}); + } else { + return fmt::format_to(ctx.out(), "{}", parselink::logging::format_arg{v.error()}); + } + } +}; +#endif + +// Support format_arg wrappers, which will be used to colorize output. +template +struct fmt::formatter> : + fmt::formatter::type> { + using format_arg_type = parselink::logging::format_arg; + using resolved_type = typename format_arg_type::type; + + template + auto format(format_arg_type const& v, FormatContext& ctx) const { + return fmt::formatter::format(v.v, ctx); + } +}; + +#endif // logging_formatters_d22a64b1645a8134 diff --git a/source/common/include/logging/level.h b/source/common/include/logging/level.h new file mode 100644 index 0000000..bf75f47 --- /dev/null +++ b/source/common/include/logging/level.h @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Logging +// +// Level definitions, for indicating the severity or intent of the log message. +// Loggers and sinks can be configured to ignore logs not matching a certain +// threshold. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- + +#ifndef level_9f090ff308e53a57 +#define level_9f090ff308e53a57 + +namespace parselink { +namespace logging { + +enum class level { + silent, // "Virtual" level used to suppress all logging output. + critical, // Indicates a fatal error occurred. Crash likely. + error, // Indicates a non-fatal error occurred. + warning, // Indicates potentially incorrect/unintentional behavior. + info, // Indicates general information. + verbose, // Noisier/potentially unimportant information. + debug, // Information intended for debugging purposes only. + trace // Tracer-like levels of verbosity may impact performance. +}; + +} // namespace logging +} // namespace parselink + +#endif // level_9f090ff308e53a57 diff --git a/source/common/include/logging/theme.h b/source/common/include/logging/theme.h new file mode 100644 index 0000000..5657d7d --- /dev/null +++ b/source/common/include/logging/theme.h @@ -0,0 +1,206 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Logging +// +// The logging facilities within parselink colorize output by default. This +// is achieved by wrapping each formatted argument with an empty structure +// and specializing the theme structure for the type. +// +// If no theme is specified for a type, the text will be rendered with the +// default color. +// +// TODO(ksassenrath): +// 1. Make configurable with a text file. +// 2. Defer colorization for logging endpoints which do not support/desire +// colored output. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- + +#ifndef log_theme_8e9601cc066b20bf +#define log_theme_8e9601cc066b20bf + +#include "level.h" +#include "traits.h" + +/* #include */ + +#include + +#include +#include + +namespace parselink { +namespace logging { + +template +struct format_arg{ + using type = std::decay_t; + constexpr format_arg(type const& t) : v(t) {} + type const& v; +}; + +template T> +struct format_arg { + using type = std::string_view; + constexpr format_arg(T&& t) noexcept : v(t) {} + type v; +}; + +template +struct format_arg { + using type = void const*; + constexpr format_arg(T const& t) noexcept : v(fmt::ptr(t)) {} + void const* v; +}; + +template +struct theme {}; + +template +struct static_theme { + constexpr static auto style = fmt::fg(Color); +}; + +template +struct theme : static_theme {}; + +template T> +struct theme : static_theme {}; + +template +struct theme : static_theme {}; + +template +requires std::is_enum_v +struct theme : static_theme {}; + +template <> +struct theme : static_theme {}; + +template <> +struct theme : static_theme {}; + +template <> +struct theme> { + constexpr static fmt::color colors[] = { + fmt::color::black, + fmt::color::red, + fmt::color::fire_brick, + fmt::color::golden_rod, + fmt::color::light_sky_blue, + fmt::color::lime_green, + fmt::color::pink, + fmt::color::slate_gray + }; + static constexpr auto style(auto l) noexcept { + return fmt::fg(*std::next(std::begin(colors), size_t(l.v))); + } +}; + +template +concept has_static_theme = + std::convertible_to>::style)>; + +template +concept has_dynamic_theme = requires (T const& t) { + { theme>::style(t) } + -> std::convertible_to; +}; + +template +concept has_theme = has_static_theme || has_dynamic_theme; + +static_assert(has_static_theme); + +template +constexpr auto get_theme(T const&) { + return fmt::text_style{}; +} + +template +constexpr auto get_theme(T const&) { + return theme::style; +} + +template +constexpr auto get_theme(T const& value) { + return theme::style(value); +} + +[[gnu::always_inline]] constexpr auto styled(fmt::text_style const& style, + auto out) { + if (style.has_foreground()) { + auto foreground = + fmt::detail::make_foreground_color(style.get_foreground()); + out = std::copy(foreground.begin(), foreground.end(), out); + } + return out; +} + +#if 0 +// TODO(ksassenrath): Enable when expected is supported +template +struct theme> { + static constexpr auto style(auto const& e) noexcept { + if (e.has_value()) { + return get_theme(e.value()); + } else { + return get_theme(e.error()); + } + } +}; +#endif + +template +struct themed_arg : format_arg {}; + +template +themed_arg(T&&) -> themed_arg; + +template +fmt::format_args themed_args(Args&&... args) { + return fmt::format_arg_store( + themed_arg{std::forward(args)}...); +} + +constexpr inline std::string_view reset_theme{"\x1b[0m"}; + +} // namespace logging +} // namespace parselink + +// Colorize the text but forward the actual formatting itself to the original +// formatter. +template +struct fmt::formatter> + : fmt::formatter::type> { + using type = std::remove_cvref_t; + using themed_arg = parselink::logging::themed_arg; + using format_arg = parselink::logging::format_arg; + using resolved_type = typename format_arg::type; + + template + auto format(themed_arg const& v, FormatContext& ctx) const { + auto out = ctx.out(); + out = parselink::logging::styled( + parselink::logging::get_theme(v.v), out); + out = fmt::formatter::format(v.v, ctx); + if constexpr (parselink::logging::has_theme) { + auto reset_color = parselink::logging::reset_theme; + out = std::copy(reset_color.begin(), reset_color.end(), out); + } + return out; + } +}; + +#endif // log_theme_8e9601cc066b20bf diff --git a/source/common/include/logging/traits.h b/source/common/include/logging/traits.h new file mode 100644 index 0000000..37c145d --- /dev/null +++ b/source/common/include/logging/traits.h @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Logging +// +// Type traits for massaging various types into something fmt-printable. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- + +#ifndef logging_traits_34e410874c0179c6 +#define logging_traits_34e410874c0179c6 + +#include +#include + +namespace parselink { +namespace logging { + +// Customization point for printing out the enum name (value) and not the +// full type. The default prints the type name. An example is provided below. +// +// enum class Foo { Bar, Cat }; +// Foo v{Foo::Bar}; +// logger.log<...>("value: {}", v); // logs "value: Foo::Bar" +// logger.log<...>("value: {}", enum_name_only{v}); // logs "value: Bar" +template +requires std::is_enum_v +struct enum_name_only { + E v; +}; + +template +enum_name_only(E) -> enum_name_only; + +// Wrapper for a string that will be colorized as if it's an error, instead of +// a normal string. +struct error_str { + template T> + error_str(T&& t) : v(std::forward(t)) {} + std::string_view v; +}; + +namespace detail { + +// The following concepts aim to describe both raw and smart pointers in a +// way that the log formatter can deduce theme (color/format) as well as +// print the address without libformat's boilerplate (fmt::ptr). +template +struct is_smart_pointer_type : std::false_type {}; + +template +struct is_smart_pointer_type> : std::true_type {}; + +template +struct is_smart_pointer_type> : std::true_type {}; + +template +concept smart_pointer = is_smart_pointer_type>::value; + +template +using underlying_ptr_type = std::remove_cv_t< + std::remove_pointer_t>>>; + +template +concept non_string_pointer = + std::is_pointer_v>> + && !std::is_same_v, void> + && !std::is_same_v, char>; + +template +concept printable_pointer = smart_pointer || non_string_pointer; + +// Some simple assertions to avoid breaking the code above. +static_assert(printable_pointer); +static_assert(printable_pointer); +static_assert(printable_pointer); +static_assert(printable_pointer); +static_assert(printable_pointer); +static_assert(printable_pointer); + +static_assert(!printable_pointer); +static_assert(!printable_pointer); +static_assert(!printable_pointer); +static_assert(!printable_pointer); +static_assert(!printable_pointer); +static_assert(!printable_pointer); + +static_assert(!printable_pointer); +static_assert(!printable_pointer); +static_assert(!printable_pointer); +static_assert(!printable_pointer); + +static_assert(printable_pointer>); +static_assert(printable_pointer&>); +static_assert(printable_pointer const&>); + +static_assert(printable_pointer>); +static_assert(printable_pointer&>); +static_assert(printable_pointer const&>); + +} // namespace detail + +} // namespace logging +} // namespace parselink + +#endif // logging_traits_34e410874c0179c6 diff --git a/source/common/source/logging.cpp b/source/common/source/logging.cpp new file mode 100644 index 0000000..dc4d4f8 --- /dev/null +++ b/source/common/source/logging.cpp @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Logging +// +// Logging implementation. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- + +#include "logging.h" + +#include + +using namespace parselink::logging; + +namespace { + +struct console_endpoint : public endpoint { + static constexpr std::string_view format_string = + "{:%Y-%m-%d %H:%M:%S}.{:03} [{:<8}] {:>20} | {}\n"; + + std::span buffer() noexcept override { return buffer_; } + + bool colored() const noexcept override { return true; } + + void write(message const& msg) override { + using namespace std::chrono_literals; + fmt::print(format_string, fmt::gmtime(msg.time), + (msg.time.time_since_epoch() % 1000ms) / 1ms, + themed_arg{enum_name_only{msg.lvl}}, msg.name, msg.message); + } + + std::array buffer_; +}; + +} + +auto& console() { + static auto console = std::make_shared(); + return console; +} + +logger::logger(std::string_view name) : name_{name} { + endpoints_.emplace_back(console()); +} + +logger::logger(std::string_view name, std::vector> eps) + : name_{name}, endpoints_{std::move(eps)} {} + +void logger::set_threshold(level new_threshold) noexcept { + for (auto& ep : endpoints_) { + ep->threshold = new_threshold; + } +} + diff --git a/source/log.cpp b/source/log.cpp new file mode 100644 index 0000000..55a2291 --- /dev/null +++ b/source/log.cpp @@ -0,0 +1,13 @@ +#include + +using namespace parselink; +using level = parselink::logging::level; + +namespace { + logging::logger logger("parselog"); +} + +int main(int argc, char**) { + logger.log("Found {} arguments", argc); + return 0; +} diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..53feb35 --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,444 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +// Return a reasonable mime type based on the extension of a file. +beast::string_view +mime_type(beast::string_view path) +{ + using beast::iequals; + auto const ext = [&path] + { + auto const pos = path.rfind("."); + if(pos == beast::string_view::npos) + return beast::string_view{}; + return path.substr(pos); + }(); + if(iequals(ext, ".htm")) return "text/html"; + if(iequals(ext, ".html")) return "text/html"; + if(iequals(ext, ".php")) return "text/html"; + if(iequals(ext, ".css")) return "text/css"; + if(iequals(ext, ".txt")) return "text/plain"; + if(iequals(ext, ".js")) return "application/javascript"; + if(iequals(ext, ".json")) return "application/json"; + if(iequals(ext, ".xml")) return "application/xml"; + if(iequals(ext, ".swf")) return "application/x-shockwave-flash"; + if(iequals(ext, ".flv")) return "video/x-flv"; + if(iequals(ext, ".png")) return "image/png"; + if(iequals(ext, ".jpe")) return "image/jpeg"; + if(iequals(ext, ".jpeg")) return "image/jpeg"; + if(iequals(ext, ".jpg")) return "image/jpeg"; + if(iequals(ext, ".gif")) return "image/gif"; + if(iequals(ext, ".bmp")) return "image/bmp"; + if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; + if(iequals(ext, ".tiff")) return "image/tiff"; + if(iequals(ext, ".tif")) return "image/tiff"; + if(iequals(ext, ".svg")) return "image/svg+xml"; + if(iequals(ext, ".svgz")) return "image/svg+xml"; + return "application/text"; +} + +// Append an HTTP rel-path to a local filesystem path. +// The returned path is normalized for the platform. +std::string +path_cat( + beast::string_view base, + beast::string_view path) +{ + if(base.empty()) + return std::string(path); + std::string result(base); +#ifdef BOOST_MSVC + char constexpr path_separator = '\\'; + if(result.back() == path_separator) + result.resize(result.size() - 1); + result.append(path.data(), path.size()); + for(auto& c : result) + if(c == '/') + c = path_separator; +#else + char constexpr path_separator = '/'; + if(result.back() == path_separator) + result.resize(result.size() - 1); + result.append(path.data(), path.size()); +#endif + return result; +} + +// Return a response for the given request. +// +// The concrete type of the response message (which depends on the +// request), is type-erased in message_generator. +template +http::message_generator +handle_request( + beast::string_view doc_root, + http::request>&& req) +{ + // Returns a bad request response + auto const bad_request = + [&req](beast::string_view why) + { + http::response res{http::status::bad_request, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = std::string(why); + res.prepare_payload(); + return res; + }; + + // Returns a not found response + auto const not_found = + [&req](beast::string_view target) + { + http::response res{http::status::not_found, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "The resource '" + std::string(target) + "' was not found."; + res.prepare_payload(); + return res; + }; + + // Returns a server error response + auto const server_error = + [&req](beast::string_view what) + { + http::response res{http::status::internal_server_error, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "An error occurred: '" + std::string(what) + "'"; + res.prepare_payload(); + return res; + }; + + // Make sure we can handle the method + if( req.method() != http::verb::get && + req.method() != http::verb::head) + return bad_request("Unknown HTTP-method"); + + // Request path must be absolute and not contain "..". + if( req.target().empty() || + req.target()[0] != '/' || + req.target().find("..") != beast::string_view::npos) + return bad_request("Illegal request-target"); + + // Build the path to the requested file + std::string path = path_cat(doc_root, req.target()); + if(req.target().back() == '/') + path.append("index.html"); + + // Attempt to open the file + beast::error_code ec; + http::file_body::value_type body; + body.open(path.c_str(), beast::file_mode::scan, ec); + + // Handle the case where the file doesn't exist + if(ec == beast::errc::no_such_file_or_directory) + return not_found(req.target()); + + // Handle an unknown error + if(ec) + return server_error(ec.message()); + + // Cache the size since we need it after the move + auto const size = body.size(); + + // Respond to HEAD request + if(req.method() == http::verb::head) + { + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, mime_type(path)); + res.content_length(size); + res.keep_alive(req.keep_alive()); + return res; + } + + // Respond to GET request + http::response res{ + std::piecewise_construct, + std::make_tuple(std::move(body)), + std::make_tuple(http::status::ok, req.version())}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, mime_type(path)); + res.content_length(size); + res.keep_alive(req.keep_alive()); + return res; +} + +//------------------------------------------------------------------------------ + +// Report a failure +void +fail(beast::error_code ec, char const* what) +{ + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Handles an HTTP server connection +class session : public std::enable_shared_from_this +{ + beast::tcp_stream stream_; + beast::flat_buffer buffer_; + std::shared_ptr doc_root_; + http::request req_; + +public: + // Take ownership of the stream + session( + tcp::socket&& socket, + std::shared_ptr const& doc_root) + : stream_(std::move(socket)) + , doc_root_(doc_root) + { + } + + // Start the asynchronous operation + void + run() + { + // We need to be executing within a strand to perform async operations + // on the I/O objects in this session. Although not strictly necessary + // for single-threaded contexts, this example code is written to be + // thread-safe by default. + net::dispatch(stream_.get_executor(), + beast::bind_front_handler( + &session::do_read, + shared_from_this())); + } + + void + do_read() + { + // Make the request empty before reading, + // otherwise the operation behavior is undefined. + req_ = {}; + + // Set the timeout. + stream_.expires_after(std::chrono::seconds(30)); + + // Read a request + http::async_read(stream_, buffer_, req_, + beast::bind_front_handler( + &session::on_read, + shared_from_this())); + } + + void + on_read( + beast::error_code ec, + std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + // This means they closed the connection + if(ec == http::error::end_of_stream) + return do_close(); + + if(ec) + return fail(ec, "read"); + + // Send the response + send_response( + handle_request(*doc_root_, std::move(req_))); + } + + void + send_response(http::message_generator&& msg) + { + bool keep_alive = msg.keep_alive(); + + // Write the response + beast::async_write( + stream_, + std::move(msg), + beast::bind_front_handler( + &session::on_write, shared_from_this(), keep_alive)); + } + + void + on_write( + bool keep_alive, + beast::error_code ec, + std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + if(ec) + return fail(ec, "write"); + + if(! keep_alive) + { + // This means we should close the connection, usually because + // the response indicated the "Connection: close" semantic. + return do_close(); + } + + // Read another request + do_read(); + } + + void + do_close() + { + // Send a TCP shutdown + beast::error_code ec; + stream_.socket().shutdown(tcp::socket::shutdown_send, ec); + + // At this point the connection is closed gracefully + } +}; + +//------------------------------------------------------------------------------ + +// Accepts incoming connections and launches the sessions +class listener : public std::enable_shared_from_this +{ + net::io_context& ioc_; + tcp::acceptor acceptor_; + std::shared_ptr doc_root_; + +public: + listener( + net::io_context& ioc, + tcp::endpoint endpoint, + std::shared_ptr const& doc_root) + : ioc_(ioc) + , acceptor_(net::make_strand(ioc)) + , doc_root_(doc_root) + { + beast::error_code ec; + + // Open the acceptor + acceptor_.open(endpoint.protocol(), ec); + if(ec) + { + fail(ec, "open"); + return; + } + + // Allow address reuse + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if(ec) + { + fail(ec, "set_option"); + return; + } + + // Bind to the server address + acceptor_.bind(endpoint, ec); + if(ec) + { + fail(ec, "bind"); + return; + } + + // Start listening for connections + acceptor_.listen( + net::socket_base::max_listen_connections, ec); + if(ec) + { + fail(ec, "listen"); + return; + } + } + + // Start accepting incoming connections + void + run() + { + do_accept(); + } + +private: + void + do_accept() + { + // The new connection gets its own strand + acceptor_.async_accept( + net::make_strand(ioc_), + beast::bind_front_handler( + &listener::on_accept, + shared_from_this())); + } + + void + on_accept(beast::error_code ec, tcp::socket socket) + { + if(ec) + { + fail(ec, "accept"); + return; // To avoid infinite loop + } + else + { + // Create the session and run it + std::make_shared( + std::move(socket), + doc_root_)->run(); + } + + // Accept another connection + do_accept(); + } +}; + +//------------------------------------------------------------------------------ + +int main(int argc, char* argv[]) +{ + // Check command line arguments. + if (argc != 5) + { + std::cerr << + "Usage: http-server-async
\n" << + "Example:\n" << + " http-server-async 0.0.0.0 8080 . 1\n"; + return EXIT_FAILURE; + } + auto const address = net::ip::make_address(argv[1]); + auto const port = static_cast(std::atoi(argv[2])); + auto const doc_root = std::make_shared(argv[3]); + auto const threads = std::max(1, std::atoi(argv[4])); + + // The io_context is required for all I/O + net::io_context ioc{threads}; + + // Create and launch a listening port + std::make_shared( + ioc, + tcp::endpoint{address, port}, + doc_root)->run(); + + // Run the I/O service on the requested number of threads + std::vector v; + v.reserve(threads - 1); + for(auto i = threads - 1; i > 0; --i) + v.emplace_back( + [&ioc] + { + ioc.run(); + }); + ioc.run(); + + return EXIT_SUCCESS; +} diff --git a/tests/common/BUILD b/tests/common/BUILD new file mode 100644 index 0000000..abfa0b8 --- /dev/null +++ b/tests/common/BUILD @@ -0,0 +1,8 @@ +cc_test( + name = "logging", + srcs = ["logging.cpp"], + deps = [ + "//source/common:lib", + "@ut", + ], +) diff --git a/tests/common/logging.cpp b/tests/common/logging.cpp new file mode 100644 index 0000000..d100019 --- /dev/null +++ b/tests/common/logging.cpp @@ -0,0 +1,380 @@ +#include + +#include + +#include + +using namespace parselink::logging; + +namespace { + +// Simple testing endpoint, writing to a std::string. Colors are not supported. +template +struct test_endpoint_base : public endpoint { + void write(message const& msg) override { + if constexpr (Colored) { + buffer_.append(fmt::format("{} {} | {}", + themed_arg{enum_name_only{msg.lvl}}, msg.name, msg.message)); + } else { + buffer_.append(fmt::format("{} {} | {}", + enum_name_only{msg.lvl}, msg.name, msg.message)); + } + } + + bool colored() const noexcept override { return Colored; } + + void clear() { buffer_.clear(); } + std::string_view contents() const { return buffer_; } + + std::string buffer_; +}; + +template +auto make_test_logger(std::string_view name) { + auto ep = std::make_shared(); + return std::tuple{logger{name, ep}, ep}; +} + +using test_endpoint = test_endpoint_base; +using colored_test_endpoint = test_endpoint_base; + +// In order to test themed (colorized) logging, we must be sure that theming +// gets correctly-styled content. +struct static_theme_test { int value; }; + +// In order to test themed (colorized) logging, we must be sure that theming +// gets correctly-styled content. +struct dynamic_theme_test { int value; }; + +template +auto styled(T&& v) { + if constexpr(has_static_theme) { + return fmt::styled(v, theme>::style); + } else if constexpr (has_dynamic_theme) { + return fmt::styled(v, theme>::style(v)); + } else { + static_assert("Could not find theme, likely broken test!"); + } +} + +} + +template <> +struct fmt::formatter : public fmt::formatter { + template + auto format(static_theme_test const& v, FormatContext& ctx) const { + return fmt::formatter::format(v.value, ctx); + } +}; + +template <> +struct fmt::formatter : public fmt::formatter { + template + auto format(dynamic_theme_test const& v, FormatContext& ctx) const { + return fmt::formatter::format(v.value, ctx); + } +}; + +template <> +struct parselink::logging::theme + : static_theme {}; + +template <> +struct parselink::logging::theme { + static constexpr auto style(auto const& dtt) noexcept { + return fmt::fg(dtt.value % 2 ? fmt::color::red : fmt::color::green); + } +}; + + +// Begin tests! + +using namespace boost::ut; + +suite logging = [] { + "log thresholds by default"_test = [] { + auto [logger, ep] = make_test_logger("log level"); + // Must be handled explicitly as the level is a template parameter. + logger.log("test"); + if (ep->threshold >= level::critical) { + expect(ep->contents() == "critical log level | test"); + } else { + expect(ep->contents().empty()); + } + ep->clear(); + + logger.log("test"); + if (ep->threshold >= level::error) { + expect(ep->contents() == "error log level | test"); + } else { + expect(ep->contents().empty()); + } + ep->clear(); + + logger.log("test"); + if (ep->threshold >= level::warning) { + expect(ep->contents() == "warning log level | test"); + } else { + expect(ep->contents().empty()); + } + ep->clear(); + + logger.log("test"); + if (ep->threshold >= level::info) { + expect(ep->contents() == "info log level | test"); + } else { + expect(ep->contents().empty()); + } + ep->clear(); + + logger.log("test"); + if (ep->threshold >= level::verbose) { + expect(ep->contents() == "verbose log level | test"); + } else { + expect(ep->contents().empty()); + } + ep->clear(); + + logger.log("test"); + if (ep->threshold >= level::debug) { + expect(ep->contents() == "debug log level | test"); + } else { + expect(ep->contents().empty()); + } + ep->clear(); + + logger.log("test"); + if (ep->threshold >= level::trace) { + expect(ep->contents() == "trace log level | test"); + } else { + expect(ep->contents().empty()); + } + ep->clear(); + }; + + "basic log formatting"_test = [] { + auto [logger, ep] = make_test_logger("formatting"); + logger.log("single int: {}", 5); + expect(ep->contents() == "info formatting | single int: 5"); + ep->clear(); + + logger.log("two ints: {} {}", 32, 55); + expect(ep->contents() == "info formatting | two ints: 32 55"); + ep->clear(); + + logger.log("string: {}", std::string{"foo"}); + expect(ep->contents() == "info formatting | string: foo"); + ep->clear(); + + logger.log("string_view: {}", std::string_view{"bar"}); + expect(ep->contents() == "info formatting | string_view: bar"); + ep->clear(); + + logger.log("c-string: {}", "brow"); + expect(ep->contents() == "info formatting | c-string: brow"); + ep->clear(); + + char const* cat = "cat"; + logger.log("c-string: {}", cat); + expect(ep->contents() == "info formatting | c-string: cat"); + ep->clear(); + }; + + "pointer formatting"_test = [] { + auto [logger, ep] = make_test_logger("formatting"); + + // Yeah, this isn't kosher, but test progression is a bit tidier. + int* ptr = new int(5); + auto uniq_ptr = std::unique_ptr(ptr); + auto shr_ptr = std::shared_ptr(new int(10)); + + { + logger.log("int pointer: {}", ptr); + auto expected = fmt::format( + "info formatting | int pointer: {}", fmt::ptr(ptr)); + expect(ep->contents() == expected); + ep->clear(); + } + + { + auto& ptrref = ptr; + logger.log("int pointer&: {}", ptrref); + auto expected = fmt::format( + "info formatting | int pointer&: {}", fmt::ptr(ptrref)); + expect(ep->contents() == expected); + ep->clear(); + } + + { + auto const& cptrref = ptr; + logger.log("int const pointer&: {}", cptrref); + auto expected = fmt::format( + "info formatting | int const pointer&: {}", + fmt::ptr(cptrref)); + expect(ep->contents() == expected); + ep->clear(); + } + + { + logger.log("std::unique_ptr: {}", uniq_ptr); + auto expected = fmt::format( + "info formatting | std::unique_ptr: {}", + fmt::ptr(uniq_ptr)); + expect(ep->contents() == expected); + ep->clear(); + } + + { + auto& uniq_ptrref = uniq_ptr; + logger.log("std::unique_ptr&: {}", uniq_ptrref); + auto expected = fmt::format( + "info formatting | std::unique_ptr&: {}", + fmt::ptr(uniq_ptrref)); + expect(ep->contents() == expected); + ep->clear(); + } + + { + auto const& cuniq_ptrref = uniq_ptr; + logger.log("std::unique_ptr const&: {}", + cuniq_ptrref); + auto expected = fmt::format( + "info formatting | std::unique_ptr const&: {}", + fmt::ptr(cuniq_ptrref)); + expect(ep->contents() == expected); + ep->clear(); + } + + { + logger.log("std::shared_ptr: {}", shr_ptr); + auto expected = fmt::format( + "info formatting | std::shared_ptr: {}", + fmt::ptr(shr_ptr)); + expect(ep->contents() == expected); + ep->clear(); + } + + { + auto& shr_ptrref = shr_ptr; + logger.log("std::shared_ptr&: {}", shr_ptrref); + auto expected = fmt::format( + "info formatting | std::shared_ptr&: {}", + fmt::ptr(shr_ptrref)); + expect(ep->contents() == expected); + ep->clear(); + } + + { + auto const& cshr_ptrref = shr_ptr; + logger.log("std::shared_ptr const&: {}", + cshr_ptrref); + auto expected = fmt::format( + "info formatting | std::shared_ptr const&: {}", + fmt::ptr(cshr_ptrref)); + expect(ep->contents() == expected); + ep->clear(); + } + }; + + "expected formatting"_test = [] { + auto [logger, ep] = make_test_logger("expected"); + { +#if 0 + //TODO(ksassenrath): Enable when expected is added. + layover::expected x{5}; + logger.log("{}", x); + expect(ep->contents() == "info expected | 5"); + x = std::errc::invalid_argument; + ep->clear(); + logger.log("{}", x); + expect(ep->contents() == "info expected | Invalid argument"); + ep->clear(); +#endif + } + { +#if 0 + layover::expected> x{std::make_unique(20)}; + logger.log("{} {}", x, *(x.value())); + auto expected = fmt::format( + "info expected | {} 20", fmt::ptr(x.value())); + expect(ep->contents() == expected); + x = std::errc::invalid_argument; + ep->clear(); + logger.log("{}", x); + expect(ep->contents() == "info expected | Invalid argument"); + ep->clear(); +#endif + } + }; + + "log theme"_test = [] { + static_theme_test stt{42}; + dynamic_theme_test red{21}, green{202}; + + auto formatted = fmt::format("{}", styled(stt)); + auto expected = fmt::format("{}", + fmt::styled(stt.value, fmt::fg(fmt::color::black))); + expect(formatted == expected); + formatted = fmt::format("{}", themed_arg{stt}); + expect(formatted == expected); + + formatted = fmt::format("{} {}", styled(red), styled(green)); + expected = fmt::format("{} {}", + fmt::styled(red.value, fmt::fg(fmt::color::red)), + fmt::styled(green.value, fmt::fg(fmt::color::green))); + expect(formatted == expected); + formatted = fmt::format("{} {}", themed_arg{red}, themed_arg{green}); + expect(formatted == expected); + }; + + "colored logging"_test = [] { + // This unit test should not break if log colors are changed, but + // it also relies on functionality + auto [logger, ep] = make_test_logger("colored"); + + logger.log(""); + auto expected = fmt::format("{} colored | ", + styled(enum_name_only{level::info})); + expect(ep->contents() == expected); + ep->clear(); + + logger.log("integral {}", 5); + expected = fmt::format("{} colored | integral {}", + styled(enum_name_only{level::info}), styled(5)); + expect(ep->contents() == expected); + ep->clear(); + + std::string_view sv = "hello"; + logger.log("string_view {}", sv); + expected = fmt::format("{} colored | string_view {}", + styled(enum_name_only{level::info}), styled(sv)); + expect(ep->contents() == expected); + ep->clear(); + + std::string str = "hello"; + logger.log("string {}", str); + expected = fmt::format("{} colored | string {}", + styled(enum_name_only{level::info}), styled(str)); + expect(ep->contents() == expected); + ep->clear(); + + auto c_str = "hello"; + logger.log("c-string {}", c_str); + expected = fmt::format("{} colored | c-string {}", + styled(enum_name_only{level::info}), styled(c_str)); + expect(ep->contents() == expected); + ep->clear(); + + int int_value = 42; + auto* int_ptr = &int_value; + logger.log("pointer {} {}", int_ptr, *int_ptr); + expected = fmt::format("{} colored | pointer {} {}", + styled(enum_name_only{level::info}), + themed_arg{int_ptr}, themed_arg{*int_ptr}); + expect(ep->contents() == expected); + ep->clear(); + }; +}; + +int main(int, char**) { +}