Initial commit

* Logging ported from layover project with minor tweaks.
    * Logging test ported over.
    * Boost libraries are available.
This commit is contained in:
Kurt Sassenrath 2023-09-04 16:03:58 -07:00
commit b0ed20369f
16 changed files with 1688 additions and 0 deletions

2
.bazelrc Normal file
View File

@ -0,0 +1,2 @@
build --action_env=BAZEL_CXXOPTS="-std=c++20:-g:-O2"
run --action_env=BAZEL_CXXOPTS="-std=c++20:-g:-O2"

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
bazel-*

11
BUILD Normal file
View File

@ -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/...": "",
},
)

100
WORKSPACE Normal file
View File

@ -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()

15
source/BUILD Normal file
View File

@ -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"
],
)

25
source/common/BUILD Normal file
View File

@ -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",
],
)

View File

@ -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 <chrono>
#include <memory>
#include <string_view>
#include <vector>
#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<char> 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<std::shared_ptr<endpoint>> eps);
template <typename... Endpoints>
explicit logger(std::string_view name, Endpoints&&... eps)
: logger(name, {std::forward<Endpoints>(eps)...}) {}
template<level Level, typename... Args>
requires (Level <= static_threshold)
void log(fmt::format_string<Args...> format, Args&&... args) const {
try_write(Level, format.get(), std::forward<Args>(args)...);
}
template<level Level, typename... Args>
requires (Level > static_threshold)
[[gnu::flatten]] void log(fmt::format_string<Args...>, Args&&...) const {}
void set_threshold(level new_threshold) noexcept;
private:
template<typename... Args>
void write_endpoint(std::shared_ptr<endpoint> 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<typename... Args>
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<Args>{std::forward<Args>(args)}...);
} else {
write_endpoint(ep, msg, format,
format_arg<Args>{std::forward<Args>(args)}...);
}
}
}
std::string name_;
std::vector<std::shared_ptr<endpoint>> endpoints_;
};
} // namespace logging
} // namespace parselink
#endif // logging_982a89e400976f59

View File

@ -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 <parselink/util/expected.h> */
#include <magic_enum.hpp>
// Simple wrapper for error strings to be formatted like any other string_view.
template <>
struct fmt::formatter<parselink::logging::error_str>
: fmt::formatter<std::string_view> {
template<typename FormatContext>
constexpr auto format(auto const& v, FormatContext& ctx) const {
return fmt::formatter<std::string_view>::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 <typename E>
requires std::is_enum_v<E>
struct fmt::formatter<E> : fmt::formatter<std::string_view> {
template<typename FormatContext>
auto format(E const v, FormatContext& ctx) const {
auto str = [v]{
return fmt::format("{}::{}",
magic_enum::enum_type_name<E>(),
magic_enum::enum_name(v));
}();
return fmt::formatter<std::string_view>::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 <typename E>
requires std::is_enum_v<E>
struct fmt::formatter<parselink::logging::enum_name_only<E>>
: fmt::formatter<std::string_view> {
using enum_name_only = parselink::logging::enum_name_only<E>;
template<typename FormatContext>
auto format(enum_name_only const v, FormatContext& ctx) const {
return fmt::formatter<std::string_view>::format(
magic_enum::enum_name(v.v), ctx);
}
};
// Support conversion of typical standard error codes into a human-readable
// string.
template <>
struct fmt::formatter<std::errc> : fmt::formatter<std::string_view> {
template<typename FormatContext>
auto format(std::errc const& v, FormatContext& ctx) const {
return fmt::formatter<std::string_view>::format(
std::make_error_code(v).message(), ctx);
}
};
// Support printing raw/smart pointers without needing to wrap them in fmt::ptr
template <parselink::logging::detail::printable_pointer T>
struct fmt::formatter<T> : fmt::formatter<void const*> {
template<typename FormatContext>
auto format(T const& v, FormatContext& ctx) const {
return fmt::formatter<void const*>::format(fmt::ptr(v), ctx);
}
};
#if 0
// TODO(ksassenrath): Re-enable when expected has been integrated
template <typename T, typename Err>
struct fmt::formatter<parselink::expected<T, Err>> {
template <typename ParseContext>
constexpr auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
return ctx.begin();
}
template<typename FormatContext>
auto format(parselink::expected<T, Err> const& v, FormatContext& ctx) const {
if (v.has_value()) {
return fmt::format_to(ctx.out(), "{}", parselink::logging::format_arg<T>{v.value()});
} else {
return fmt::format_to(ctx.out(), "{}", parselink::logging::format_arg<Err>{v.error()});
}
}
};
#endif
// Support format_arg wrappers, which will be used to colorize output.
template <typename T>
struct fmt::formatter<parselink::logging::format_arg<T>> :
fmt::formatter<typename parselink::logging::format_arg<T>::type> {
using format_arg_type = parselink::logging::format_arg<T>;
using resolved_type = typename format_arg_type::type;
template <typename FormatContext>
auto format(format_arg_type const& v, FormatContext& ctx) const {
return fmt::formatter<resolved_type>::format(v.v, ctx);
}
};
#endif // logging_formatters_d22a64b1645a8134

View File

@ -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

View File

@ -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 <parselink/util/expected.h> */
#include <fmt/color.h>
#include <system_error>
#include <type_traits>
namespace parselink {
namespace logging {
template <typename T>
struct format_arg{
using type = std::decay_t<T>;
constexpr format_arg(type const& t) : v(t) {}
type const& v;
};
template <std::convertible_to<std::string_view> T>
struct format_arg<T> {
using type = std::string_view;
constexpr format_arg(T&& t) noexcept : v(t) {}
type v;
};
template <detail::smart_pointer T>
struct format_arg<T> {
using type = void const*;
constexpr format_arg(T const& t) noexcept : v(fmt::ptr(t)) {}
void const* v;
};
template <typename>
struct theme {};
template <fmt::color Color>
struct static_theme {
constexpr static auto style = fmt::fg(Color);
};
template <std::integral T>
struct theme<T> : static_theme<fmt::color::light_sky_blue> {};
template <std::convertible_to<std::string_view> T>
struct theme<T> : static_theme<fmt::color::pale_green> {};
template <detail::printable_pointer T>
struct theme<T> : static_theme<fmt::color::pale_violet_red> {};
template <typename E>
requires std::is_enum_v<E>
struct theme<E> : static_theme<fmt::color::gray> {};
template <>
struct theme<std::errc> : static_theme<fmt::color::fire_brick> {};
template <>
struct theme<error_str> : static_theme<fmt::color::fire_brick> {};
template <>
struct theme<enum_name_only<logging::level>> {
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 <typename T>
concept has_static_theme =
std::convertible_to<fmt::text_style,
decltype(theme<std::remove_cvref_t<T>>::style)>;
template <typename T>
concept has_dynamic_theme = requires (T const& t) {
{ theme<std::remove_cvref_t<T>>::style(t) }
-> std::convertible_to<fmt::text_style>;
};
template <typename T>
concept has_theme = has_static_theme<T> || has_dynamic_theme<T>;
static_assert(has_static_theme<std::errc>);
template <typename T>
constexpr auto get_theme(T const&) {
return fmt::text_style{};
}
template <has_static_theme T>
constexpr auto get_theme(T const&) {
return theme<T>::style;
}
template <has_dynamic_theme T>
constexpr auto get_theme(T const& value) {
return theme<T>::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<char>(style.get_foreground());
out = std::copy(foreground.begin(), foreground.end(), out);
}
return out;
}
#if 0
// TODO(ksassenrath): Enable when expected is supported
template <typename T, typename Err>
struct theme<expected<T, Err>> {
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 <typename T>
struct themed_arg : format_arg<T> {};
template <typename T>
themed_arg(T&&) -> themed_arg<T>;
template <typename... Args>
fmt::format_args themed_args(Args&&... args) {
return fmt::format_arg_store<fmt::format_context>(
themed_arg<Args>{std::forward<Args>(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 <typename T>
struct fmt::formatter<parselink::logging::themed_arg<T>>
: fmt::formatter<typename parselink::logging::format_arg<T>::type> {
using type = std::remove_cvref_t<T>;
using themed_arg = parselink::logging::themed_arg<T>;
using format_arg = parselink::logging::format_arg<T>;
using resolved_type = typename format_arg::type;
template <typename FormatContext>
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<resolved_type>::format(v.v, ctx);
if constexpr (parselink::logging::has_theme<type>) {
auto reset_color = parselink::logging::reset_theme;
out = std::copy(reset_color.begin(), reset_color.end(), out);
}
return out;
}
};
#endif // log_theme_8e9601cc066b20bf

View File

@ -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 <type_traits>
#include <memory>
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 <typename E>
requires std::is_enum_v<E>
struct enum_name_only {
E v;
};
template <typename E>
enum_name_only(E) -> enum_name_only<E>;
// Wrapper for a string that will be colorized as if it's an error, instead of
// a normal string.
struct error_str {
template <std::convertible_to<std::string_view> T>
error_str(T&& t) : v(std::forward<T>(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 <typename T>
struct is_smart_pointer_type : std::false_type {};
template <typename T, typename D>
struct is_smart_pointer_type<std::unique_ptr<T, D>> : std::true_type {};
template <typename T>
struct is_smart_pointer_type<std::shared_ptr<T>> : std::true_type {};
template <typename T>
concept smart_pointer = is_smart_pointer_type<std::remove_cvref_t<T>>::value;
template <typename T>
using underlying_ptr_type = std::remove_cv_t<
std::remove_pointer_t<std::decay_t<std::remove_cvref_t<T>>>>;
template <typename T>
concept non_string_pointer =
std::is_pointer_v<std::decay_t<std::remove_cvref_t<T>>>
&& !std::is_same_v<underlying_ptr_type<T>, void>
&& !std::is_same_v<underlying_ptr_type<T>, char>;
template <typename T>
concept printable_pointer = smart_pointer<T> || non_string_pointer<T>;
// Some simple assertions to avoid breaking the code above.
static_assert(printable_pointer<int*>);
static_assert(printable_pointer<int*&>);
static_assert(printable_pointer<int const*&>);
static_assert(printable_pointer<int* const&>);
static_assert(printable_pointer<int (&)[4]>);
static_assert(printable_pointer<int const (&)[4]>);
static_assert(!printable_pointer<char*>);
static_assert(!printable_pointer<char*&>);
static_assert(!printable_pointer<char const*>);
static_assert(!printable_pointer<char const*&>);
static_assert(!printable_pointer<char (&)[4]>);
static_assert(!printable_pointer<char const (&)[4]>);
static_assert(!printable_pointer<void*>);
static_assert(!printable_pointer<void*&>);
static_assert(!printable_pointer<void const*>);
static_assert(!printable_pointer<void const*&>);
static_assert(printable_pointer<std::unique_ptr<int>>);
static_assert(printable_pointer<std::unique_ptr<int>&>);
static_assert(printable_pointer<std::unique_ptr<int> const&>);
static_assert(printable_pointer<std::unique_ptr<int const>>);
static_assert(printable_pointer<std::unique_ptr<int const>&>);
static_assert(printable_pointer<std::unique_ptr<int const> const&>);
} // namespace detail
} // namespace logging
} // namespace parselink
#endif // logging_traits_34e410874c0179c6

View File

@ -0,0 +1,64 @@
//-----------------------------------------------------------------------------
// ___ __ _ _
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
// / ___/ (_| | | \__ \ __/ /__| | | | | <
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
//
//-----------------------------------------------------------------------------
// Author: Kurt Sassenrath
// Module: Logging
//
// Logging implementation.
//
// Copyright (c) 2023 Kurt Sassenrath.
//
// License TBD.
//-----------------------------------------------------------------------------
#include "logging.h"
#include <fmt/chrono.h>
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<char> 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<char, 4096> buffer_;
};
}
auto& console() {
static auto console = std::make_shared<console_endpoint>();
return console;
}
logger::logger(std::string_view name) : name_{name} {
endpoints_.emplace_back(console());
}
logger::logger(std::string_view name, std::vector<std::shared_ptr<endpoint>> eps)
: name_{name}, endpoints_{std::move(eps)} {}
void logger::set_threshold(level new_threshold) noexcept {
for (auto& ep : endpoints_) {
ep->threshold = new_threshold;
}
}

13
source/log.cpp Normal file
View File

@ -0,0 +1,13 @@
#include <logging.h>
using namespace parselink;
using level = parselink::logging::level;
namespace {
logging::logger logger("parselog");
}
int main(int argc, char**) {
logger.log<level::critical>("Found {} arguments", argc);
return 0;
}

444
source/main.cpp Normal file
View File

@ -0,0 +1,444 @@
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/strand.hpp>
#include <boost/config.hpp>
#include <algorithm>
#include <cstdlib>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <vector>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
// 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 <class Body, class Allocator>
http::message_generator
handle_request(
beast::string_view doc_root,
http::request<Body, http::basic_fields<Allocator>>&& req)
{
// Returns a bad request response
auto const bad_request =
[&req](beast::string_view why)
{
http::response<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::empty_body> 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<http::file_body> 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<session>
{
beast::tcp_stream stream_;
beast::flat_buffer buffer_;
std::shared_ptr<std::string const> doc_root_;
http::request<http::string_body> req_;
public:
// Take ownership of the stream
session(
tcp::socket&& socket,
std::shared_ptr<std::string const> 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<listener>
{
net::io_context& ioc_;
tcp::acceptor acceptor_;
std::shared_ptr<std::string const> doc_root_;
public:
listener(
net::io_context& ioc,
tcp::endpoint endpoint,
std::shared_ptr<std::string const> 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<session>(
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 <address> <port> <doc_root> <threads>\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<unsigned short>(std::atoi(argv[2]));
auto const doc_root = std::make_shared<std::string>(argv[3]);
auto const threads = std::max<int>(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<listener>(
ioc,
tcp::endpoint{address, port},
doc_root)->run();
// Run the I/O service on the requested number of threads
std::vector<std::thread> v;
v.reserve(threads - 1);
for(auto i = threads - 1; i > 0; --i)
v.emplace_back(
[&ioc]
{
ioc.run();
});
ioc.run();
return EXIT_SUCCESS;
}

8
tests/common/BUILD Normal file
View File

@ -0,0 +1,8 @@
cc_test(
name = "logging",
srcs = ["logging.cpp"],
deps = [
"//source/common:lib",
"@ut",
],
)

380
tests/common/logging.cpp Normal file
View File

@ -0,0 +1,380 @@
#include <logging.h>
#include <boost/ut.hpp>
#include <fmt/core.h>
using namespace parselink::logging;
namespace {
// Simple testing endpoint, writing to a std::string. Colors are not supported.
template <bool Colored>
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 <typename Endpoint>
auto make_test_logger(std::string_view name) {
auto ep = std::make_shared<Endpoint>();
return std::tuple{logger{name, ep}, ep};
}
using test_endpoint = test_endpoint_base<false>;
using colored_test_endpoint = test_endpoint_base<true>;
// 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 <has_theme T>
auto styled(T&& v) {
if constexpr(has_static_theme<T>) {
return fmt::styled(v, theme<std::remove_cvref_t<T>>::style);
} else if constexpr (has_dynamic_theme<T>) {
return fmt::styled(v, theme<std::remove_cvref_t<T>>::style(v));
} else {
static_assert("Could not find theme, likely broken test!");
}
}
}
template <>
struct fmt::formatter<static_theme_test> : public fmt::formatter<int> {
template <typename FormatContext>
auto format(static_theme_test const& v, FormatContext& ctx) const {
return fmt::formatter<int>::format(v.value, ctx);
}
};
template <>
struct fmt::formatter<dynamic_theme_test> : public fmt::formatter<int> {
template <typename FormatContext>
auto format(dynamic_theme_test const& v, FormatContext& ctx) const {
return fmt::formatter<int>::format(v.value, ctx);
}
};
template <>
struct parselink::logging::theme<static_theme_test>
: static_theme<fmt::color::black> {};
template <>
struct parselink::logging::theme<dynamic_theme_test> {
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<test_endpoint>("log level");
// Must be handled explicitly as the level is a template parameter.
logger.log<level::critical>("test");
if (ep->threshold >= level::critical) {
expect(ep->contents() == "critical log level | test");
} else {
expect(ep->contents().empty());
}
ep->clear();
logger.log<level::error>("test");
if (ep->threshold >= level::error) {
expect(ep->contents() == "error log level | test");
} else {
expect(ep->contents().empty());
}
ep->clear();
logger.log<level::warning>("test");
if (ep->threshold >= level::warning) {
expect(ep->contents() == "warning log level | test");
} else {
expect(ep->contents().empty());
}
ep->clear();
logger.log<level::info>("test");
if (ep->threshold >= level::info) {
expect(ep->contents() == "info log level | test");
} else {
expect(ep->contents().empty());
}
ep->clear();
logger.log<level::verbose>("test");
if (ep->threshold >= level::verbose) {
expect(ep->contents() == "verbose log level | test");
} else {
expect(ep->contents().empty());
}
ep->clear();
logger.log<level::debug>("test");
if (ep->threshold >= level::debug) {
expect(ep->contents() == "debug log level | test");
} else {
expect(ep->contents().empty());
}
ep->clear();
logger.log<level::trace>("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<test_endpoint>("formatting");
logger.log<level::info>("single int: {}", 5);
expect(ep->contents() == "info formatting | single int: 5");
ep->clear();
logger.log<level::info>("two ints: {} {}", 32, 55);
expect(ep->contents() == "info formatting | two ints: 32 55");
ep->clear();
logger.log<level::info>("string: {}", std::string{"foo"});
expect(ep->contents() == "info formatting | string: foo");
ep->clear();
logger.log<level::info>("string_view: {}", std::string_view{"bar"});
expect(ep->contents() == "info formatting | string_view: bar");
ep->clear();
logger.log<level::info>("c-string: {}", "brow");
expect(ep->contents() == "info formatting | c-string: brow");
ep->clear();
char const* cat = "cat";
logger.log<level::info>("c-string: {}", cat);
expect(ep->contents() == "info formatting | c-string: cat");
ep->clear();
};
"pointer formatting"_test = [] {
auto [logger, ep] = make_test_logger<test_endpoint>("formatting");
// Yeah, this isn't kosher, but test progression is a bit tidier.
int* ptr = new int(5);
auto uniq_ptr = std::unique_ptr<int>(ptr);
auto shr_ptr = std::shared_ptr<int>(new int(10));
{
logger.log<level::info>("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<level::info>("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<level::info>("int const pointer&: {}", cptrref);
auto expected = fmt::format(
"info formatting | int const pointer&: {}",
fmt::ptr(cptrref));
expect(ep->contents() == expected);
ep->clear();
}
{
logger.log<level::info>("std::unique_ptr<int>: {}", uniq_ptr);
auto expected = fmt::format(
"info formatting | std::unique_ptr<int>: {}",
fmt::ptr(uniq_ptr));
expect(ep->contents() == expected);
ep->clear();
}
{
auto& uniq_ptrref = uniq_ptr;
logger.log<level::info>("std::unique_ptr<int>&: {}", uniq_ptrref);
auto expected = fmt::format(
"info formatting | std::unique_ptr<int>&: {}",
fmt::ptr(uniq_ptrref));
expect(ep->contents() == expected);
ep->clear();
}
{
auto const& cuniq_ptrref = uniq_ptr;
logger.log<level::info>("std::unique_ptr<int> const&: {}",
cuniq_ptrref);
auto expected = fmt::format(
"info formatting | std::unique_ptr<int> const&: {}",
fmt::ptr(cuniq_ptrref));
expect(ep->contents() == expected);
ep->clear();
}
{
logger.log<level::info>("std::shared_ptr<int>: {}", shr_ptr);
auto expected = fmt::format(
"info formatting | std::shared_ptr<int>: {}",
fmt::ptr(shr_ptr));
expect(ep->contents() == expected);
ep->clear();
}
{
auto& shr_ptrref = shr_ptr;
logger.log<level::info>("std::shared_ptr<int>&: {}", shr_ptrref);
auto expected = fmt::format(
"info formatting | std::shared_ptr<int>&: {}",
fmt::ptr(shr_ptrref));
expect(ep->contents() == expected);
ep->clear();
}
{
auto const& cshr_ptrref = shr_ptr;
logger.log<level::info>("std::shared_ptr<int> const&: {}",
cshr_ptrref);
auto expected = fmt::format(
"info formatting | std::shared_ptr<int> const&: {}",
fmt::ptr(cshr_ptrref));
expect(ep->contents() == expected);
ep->clear();
}
};
"expected<T> formatting"_test = [] {
auto [logger, ep] = make_test_logger<test_endpoint>("expected");
{
#if 0
//TODO(ksassenrath): Enable when expected is added.
layover::expected<int> x{5};
logger.log<level::info>("{}", x);
expect(ep->contents() == "info expected | 5");
x = std::errc::invalid_argument;
ep->clear();
logger.log<level::info>("{}", x);
expect(ep->contents() == "info expected | Invalid argument");
ep->clear();
#endif
}
{
#if 0
layover::expected<std::unique_ptr<int>> x{std::make_unique<int>(20)};
logger.log<level::info>("{} {}", 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<level::info>("{}", 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_test_endpoint>("colored");
logger.log<level::info>("");
auto expected = fmt::format("{} colored | ",
styled(enum_name_only{level::info}));
expect(ep->contents() == expected);
ep->clear();
logger.log<level::info>("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<level::info>("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<level::info>("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<level::info>("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<level::info>("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**) {
}