Add Logger from clio and implement json logging

This commit is contained in:
JCW
2026-04-29 15:44:36 +01:00
parent 17c7398f5d
commit cd94d7d99b
7 changed files with 1853 additions and 0 deletions

View File

@@ -0,0 +1,507 @@
#pragma once
#include <xrpl/basics/Expected.h>
#include <xrpl/basics/StructuredLogging.h>
#include <fmt/format.h>
#include <cstddef>
#include <iterator>
#include <memory>
#include <optional>
#include <source_location>
#include <string>
#include <string_view>
#include <vector>
// We forward declare spdlog types to avoid including the spdlog headers in this header file.
namespace spdlog {
class logger; // NOLINT(readability-identifier-naming)
class formatter; // NOLINT(readability-identifier-naming)
namespace sinks {
class sink; // NOLINT(readability-identifier-naming)
} // namespace sinks
} // namespace spdlog
struct BenchmarkLoggingInitializer;
class LoggerFixture;
struct LogServiceInitTests;
struct LogFileRotationTests;
namespace xrpl {
/**
* @brief Skips evaluation of expensive argument lists if the given logger is disabled for the
* required severity level.
*
* Note: Currently this introduces potential shadowing (unlikely).
*/
#ifndef COVERAGE_ENABLED
#define LOG(x) \
if (auto xrpl_pump__ = x; not xrpl_pump__) \
{ \
} \
else \
xrpl_pump__
#else
#define LOG(x) x
#endif
/**
* @brief Custom severity levels for @ref util::Logger.
*/
enum class Severity {
TRC,
DBG,
NFO,
WRN,
ERR,
FTL,
};
/**
* @brief The default log format pattern for non-JSON mode.
*
* Matches the legacy Logs::format output:
* 2024-Jan-15 12:34:56.789123 UTC General:NFO hello world
*
* '%K' is a custom flag that outputs the three-letter severity code
* (TRC, DBG, NFO, WRN, ERR, FTL).
*/
inline constexpr char const* kDEFAULT_LOG_FORMAT = "%Y-%b-%d %H:%M:%S.%f UTC %n:%K %v";
struct LoggingConfiguration
{
std::string format{kDEFAULT_LOG_FORMAT};
bool enableConsole;
std::optional<std::string> directory;
bool isAsync;
Severity defaultSeverity;
bool jsonMode{false};
};
/**
* @brief A simple thread-safe logger for the channel specified
* in the constructor.
*
* This is cheap to copy and move. Designed to be used as a member variable or
* otherwise. See @ref LogService::init() for setup of the logging core and
* severity levels for each channel.
*/
class Logger
{
std::shared_ptr<spdlog::logger> logger_;
friend class LogService; // to expose the Pump interface
/**
* @brief Helper that pumps data into a log record via `operator<<`.
*/
class Pump final
{
std::shared_ptr<spdlog::logger> logger_;
Severity const severity_;
std::source_location const sourceLocation_;
std::string stream_;
bool const enabled_;
bool const jsonMode_;
std::string parameters_; // accumulated JSON parameter fragments
public:
~Pump();
Pump(
std::shared_ptr<spdlog::logger> logger,
Severity sev,
std::source_location const& loc,
bool jsonMode);
Pump(Pump&&) = delete;
Pump(Pump const&) = delete;
Pump&
operator=(Pump const&) = delete;
Pump&
operator=(Pump&&) = delete;
/**
* @brief Appends data that has an @c xrpl::to_string overload.
*
* This covers all XRP Ledger domain types such as @c Number,
* @c Currency, @c AccountID, @c XRPAmount, @c IOUAmount, @c Asset, etc.
*
* @tparam T Type of data to pump (must satisfy detail::HasToString)
* @param data The data to pump
* @return Reference to itself for chaining
*/
template <typename T>
requires(detail::HasToString<T>)
[[maybe_unused]] Pump&
operator<<(T&& data)
{
if (enabled_)
stream_ += to_string(data);
return *this;
}
/**
* @brief Appends any fmt-formattable data into the output string.
*
* Fallback for types that do not have an @c xrpl::to_string overload
* but can be formatted by @c fmt::format (e.g. arithmetic types,
* @c std::string, @c std::string_view).
*
* @tparam T Type of data to pump
* @param data The data to pump
* @return Reference to itself for chaining
*/
template <typename T>
requires(!detail::HasToString<T>)
[[maybe_unused]] Pump&
operator<<(T&& data)
{
if (enabled_)
fmt::format_to(std::back_inserter(stream_), "{}", std::forward<T>(data));
return *this;
}
/**
* @brief Captures a structured log parameter.
*
* The parameter value is always appended to the output string.
* In JSON mode, the parameter is also accumulated into the
* parameters string for the "values" object emitted in the
* destructor.
*
* @tparam T Type of the parameter value
* @param p The parameter to capture
* @return Reference to itself for chaining
*/
template <typename T>
[[maybe_unused]] Pump&
operator<<(xrpl::log::Parameter<T>&& p)
{
if (!enabled_)
return *this;
// Append the raw string representation to the output stream
if constexpr (detail::HasToString<T>)
{
stream_ += to_string(p.value());
}
else
{
fmt::format_to(std::back_inserter(stream_), "{}", p.value());
}
if (jsonMode_)
{
// Also build up the parameters for the "values" object
if (!parameters_.empty())
parameters_ += ',';
fmt::format_to(std::back_inserter(parameters_), "\"{}\":", p.name());
detail::appendJsonValue(parameters_, p.value());
}
return *this;
}
/**
* @return true if logger is enabled; false otherwise
*/
operator bool() const
{
return enabled_;
}
};
public:
/**
* @brief Construct a new Logger object that produces loglines for the
* specified channel.
*
* See @ref LogService::init() for general setup and configuration of
* severity levels per channel.
*
* @param channel The channel this logger will report into.
*/
Logger(std::string_view const channel);
Logger(Logger const&) = default;
~Logger();
Logger(Logger&&) = default;
Logger&
operator=(Logger const&) = default;
Logger&
operator=(Logger&&) = default;
/**
* @brief Interface for logging at Severity::TRC severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] Pump
trace(std::source_location const& loc = std::source_location::current()) const;
/**
* @brief Interface for logging at Severity::DBG severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] Pump
debug(std::source_location const& loc = std::source_location::current()) const;
/**
* @brief Interface for logging at Severity::NFO severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] Pump
info(std::source_location const& loc = std::source_location::current()) const;
/**
* @brief Interface for logging at Severity::WRN severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] Pump
warn(std::source_location const& loc = std::source_location::current()) const;
/**
* @brief Interface for logging at Severity::ERR severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] Pump
error(std::source_location const& loc = std::source_location::current()) const;
/**
* @brief Interface for logging at Severity::FTL severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] Pump
fatal(std::source_location const& loc = std::source_location::current()) const;
private:
Logger(std::shared_ptr<spdlog::logger> logger);
};
/**
* @brief Base state management class for the logging service.
*
* This class manages the global state and core functionality for the logging system,
* including initialization, sink management, and logger registration.
*/
class LogServiceState
{
protected:
friend struct ::LogServiceInitTests;
friend class ::LoggerFixture;
friend struct ::LogFileRotationTests;
friend class Logger;
/**
* @brief Initialize the logging core with specified parameters.
*
* @param isAsync Whether logging should be asynchronous
* @param defaultSeverity The default severity level for new loggers
* @param sinks Vector of spdlog sinks to use for output
*/
static void
init(
bool isAsync,
Severity defaultSeverity,
std::vector<std::shared_ptr<spdlog::sinks::sink>> const& sinks,
bool jsonMode = false);
/**
* @brief Whether the LogService is initialized or not
*
* @return true if the LogService is initialized
*/
[[nodiscard]] static bool
initialized();
/**
* @brief Whether the LogService has any sink. If there is no sink, logger will not log messages
* anywhere.
*
* @return true if the LogService has at least one sink
*/
[[nodiscard]] static bool
hasSinks();
/**
* @brief Reset the logging service to uninitialized state.
*/
static void
reset();
/**
* @brief Register a new logger for the specified channel.
*
* Creates and registers a new spdlog logger instance for the given channel
* with the specified or default severity level.
*
* @param channel The name of the logging channel
* @param severity Optional severity level override; uses default if not specified
* @return Shared pointer to the registered spdlog logger
*/
static std::shared_ptr<spdlog::logger>
registerLogger(std::string_view channel, std::optional<Severity> severity = std::nullopt);
/**
* @brief Replace the current sinks with a new set of sinks.
*
* @param sinks Vector of new spdlog sinks to replace the current ones
*/
static void
replaceSinks(std::vector<std::shared_ptr<spdlog::sinks::sink>> const& sinks);
/**
* @brief Creates a pattern formatter with custom flags (e.g. '%K' for
* severity) and UTC timestamps.
*
* @param pattern The spdlog pattern string
* @return A unique_ptr to the configured pattern_formatter
*/
static std::unique_ptr<spdlog::formatter>
makeFormatter(std::string const& pattern);
protected:
static bool isAsync_; // NOLINT(readability-identifier-naming)
static Severity defaultSeverity_; // NOLINT(readability-identifier-naming)
static std::vector<std::shared_ptr<spdlog::sinks::sink>>
sinks_; // NOLINT(readability-identifier-naming)
static bool initialized_; // NOLINT(readability-identifier-naming)
static bool jsonMode_; // NOLINT(readability-identifier-naming)
static std::optional<std::string> logDir_; // NOLINT(readability-identifier-naming)
static std::string format_; // NOLINT(readability-identifier-naming)
};
/**
* @brief A global logging service.
*
* Used to initialize and setup the logging core as well as a globally available
* entrypoint for logging into the `General` channel as well as raising alerts.
*/
class LogService : public LogServiceState
{
public:
LogService() = delete;
/**
* @brief Global log core initialization from a @ref LoggingConfiguration
*
* @param config The configuration to use
* @return Void on success, error message on failure
*/
[[nodiscard]] static Expected<void, std::string>
init(LoggingConfiguration const& config);
/**
* @brief Shutdown spdlog to guarantee output is not lost
*/
static void
shutdown();
/**
* @brief Close and reopen the log file.
*
* This assists in interoperating with external log management tools
* such as logrotate(8). If no file logging is configured, this is a no-op.
*
* @return A human-readable status message
*/
[[nodiscard]] static std::string
rotate();
/**
* @brief Globally accessible General logger at Severity::TRC severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] static Logger::Pump
trace(std::source_location const& loc = std::source_location::current());
/**
* @brief Globally accessible General logger at Severity::DBG severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] static Logger::Pump
debug(std::source_location const& loc = std::source_location::current());
/**
* @brief Globally accessible General logger at Severity::NFO severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] static Logger::Pump
info(std::source_location const& loc = std::source_location::current());
/**
* @brief Globally accessible General logger at Severity::WRN severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] static Logger::Pump
warn(std::source_location const& loc = std::source_location::current());
/**
* @brief Globally accessible General logger at Severity::ERR severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] static Logger::Pump
error(std::source_location const& loc = std::source_location::current());
/**
* @brief Globally accessible General logger at Severity::FTL severity
*
* @param loc The source location of the log message
* @return The pump to use for logging
*/
[[nodiscard]] static Logger::Pump
fatal(std::source_location const& loc = std::source_location::current());
private:
/**
* @brief Parses the sinks from a @ref LoggingConfiguration
*
* @param config The configuration to parse sinks from
* @return A vector of sinks on success, error message on failure
*/
[[nodiscard]] static Expected<std::vector<std::shared_ptr<spdlog::sinks::sink>>, std::string>
getSinks(LoggingConfiguration const& config);
struct FileLoggingParams
{
std::string logDir;
};
friend struct ::BenchmarkLoggingInitializer;
friend class ::LoggerFixture;
[[nodiscard]]
static std::shared_ptr<spdlog::sinks::sink>
createFileSink(FileLoggingParams const& params, std::string const& format);
};
}; // namespace xrpl

View File

@@ -0,0 +1,271 @@
#pragma once
#include <fmt/format.h>
#include <concepts>
#include <iterator>
#include <string>
#include <string_view>
#include <type_traits>
namespace xrpl {
namespace detail {
/**
* @brief Concept that detects whether @c to_string(t) is a valid expression
* returning something convertible to @c std::string.
*
* Because this concept lives inside @c namespace @c xrpl, unqualified lookup
* will find every @c xrpl::to_string overload (e.g. for @c Number,
* @c Currency, @c AccountID, @c XRPAmount, @c IOUAmount, @c Asset, etc.).
* ADL further extends the search to any namespace associated with @p T.
*/
template <typename T>
concept HasToString = requires(std::remove_cvref_t<T> const& t) {
{ to_string(t) } -> std::convertible_to<std::string>;
};
/**
* @brief Append a value formatted as a JSON fragment to @p dest.
*
* - @c bool \u2192 unquoted @c true / @c false
* - integral (non-bool) \u2192 unquoted number
* - floating-point \u2192 unquoted number
* - types with @c to_string \u2192 quoted string via @c to_string
* - everything else \u2192 quoted string obtained via @c fmt::format
*
* @tparam T The value type.
* @param dest The string to append the JSON fragment to.
* @param value The value to format.
*/
template <typename T>
void
appendJsonValue(std::string& dest, T const& value)
{
if constexpr (std::is_same_v<T, bool>)
{
dest += value ? "true" : "false";
}
else if constexpr (std::is_integral_v<T>)
{
dest += std::to_string(value);
}
else if constexpr (std::is_floating_point_v<T>)
{
dest += std::to_string(value);
}
else if constexpr (HasToString<T>)
{
fmt::format_to(std::back_inserter(dest), "\"{}\"", to_string(value));
}
else
{
fmt::format_to(std::back_inserter(dest), "\"{}\"", value);
}
}
} // namespace detail
/**
* @brief Builds a JSON-structured spdlog format pattern string.
*
* This class produces a pattern string suitable for passing to
* @c spdlog::pattern_formatter. Each field maps a JSON key to an spdlog
* pattern flag (e.g. @c %t for thread ID, @c %n for channel name).
*
* The pattern ends with @c %%v so that the log message (a partial JSON
* fragment) is spliced in at the end.
*
* Composability is lightweight: construct from an existing pattern string
* (e.g. obtained from another logger's pattern) and append more fields.
* No map copying — just string concatenation.
*
* Common spdlog pattern flags:
* - @c %%v — the log message text
* - @c %%t — thread ID
* - @c %%n — logger/channel name
* - @c %%l — log level (e.g. "info", "warning")
* - @c %%Y-%%m-%%d %%H:%%M:%%S.%%e — date/time with milliseconds
* - @c %%s — source file name
* - @c %%# — source line number
* - @c %%P — process ID
*
* Example usage:
* @code
* // Build a fresh pattern
* JsonLoggingPatternBuilder builder;
* builder.add("timestamp", "%Y-%m-%d %H:%M:%S.%e");
* builder.add("channel", "%n");
* builder.add("level", "%l");
* std::string pattern = builder.build();
* // Produces: {"timestamp":"%Y-%m-%d %H:%M:%S.%e","channel":"%n","level":"%l" %v }
*
* // Extend an existing pattern from another logger
* JsonLoggingPatternBuilder extended(pattern);
* extended.add("source", "%s:%#");
* std::string newPattern = extended.build();
* // Produces: {"timestamp":"%Y-%m-%d %H:%M:%S.%e","channel":"%n","level":"%l","source":"%s:%#"
* %v }
* @endcode
*/
class JsonLoggingPatternBuilder
{
// Accumulated "key":"value" fragments, comma-separated.
// e.g. "\"timestamp\":\"%Y-%m-%d\",\"channel\":\"%n\""
std::string fields_;
static constexpr std::string_view kSUFFIX = ", \"message\": %v }";
/**
* @brief Extract the fields portion from an existing pattern string.
*
* Strips the leading '{' and trailing ' %%v }' suffix, leaving just
* the comma-separated field fragments.
*/
static std::string
extractFields(std::string_view pattern)
{
// Strip leading '{'
if (!pattern.empty() && pattern.front() == '{')
pattern.remove_prefix(1);
// Strip trailing ' %v }'
if (auto pos = pattern.rfind(kSUFFIX); pos != std::string_view::npos)
pattern = pattern.substr(0, pos);
return std::string(pattern);
}
public:
/** @brief Construct an empty builder. */
JsonLoggingPatternBuilder() = default;
/**
* @brief Construct from an existing pattern string.
*
* Extracts the fields from the pattern so that new fields can be
* appended. This is a lightweight string copy — no map involved.
*
* @param existingPattern A pattern previously produced by build().
*/
explicit JsonLoggingPatternBuilder(std::string_view existingPattern)
: fields_(extractFields(existingPattern))
{
}
/**
* @brief Add a field to the JSON pattern.
*
* The value is an spdlog pattern flag or any literal text that spdlog
* will expand at log time.
*
* @param key The JSON field name (e.g. "timestamp", "thread").
* @param value The spdlog pattern string (e.g. "%%Y-%%m-%%d", "%%t").
* @return Reference to this builder for chaining.
*/
JsonLoggingPatternBuilder&
add(std::string_view key, std::string_view value)
{
if (!fields_.empty())
fields_ += ',';
fmt::format_to(std::back_inserter(fields_), "\"{}\":\"{}\"", key, value);
return *this;
}
/**
* @brief Add a typed field to the JSON pattern.
*
* Uses @ref detail::appendJsonValue to format the value as a JSON
* fragment (unquoted for numbers/bools, quoted for everything else).
*
* @tparam T The value type (bool, integral, floating-point, or streamable).
* @param key The JSON field name.
* @param value The value to embed.
* @return Reference to this builder for chaining.
*/
template <typename T>
requires(!std::is_convertible_v<T, std::string_view>)
JsonLoggingPatternBuilder&
add(std::string_view key, T const& value)
{
if (!fields_.empty())
fields_ += ',';
fmt::format_to(std::back_inserter(fields_), "\"{}\":", key);
detail::appendJsonValue(fields_, value);
return *this;
}
/**
* @brief Build the final spdlog-compatible pattern string.
*
* Produces a compact JSON object where each value contains the raw
* spdlog pattern flags, terminated with %%v for the message body:
* @code
* {"level":"%l","channel":"%n" %v }
* @endcode
*
* @return The pattern string to pass to spdlog::pattern_formatter or
* LoggingConfiguration::format.
*/
[[nodiscard]] std::string
build() const
{
return "{" + fields_ + std::string(kSUFFIX);
}
};
namespace log {
/**
* @brief Holds a named log parameter with its value.
*
* Constructed via the @ref param helper function. Designed to be passed
* into Logger::Pump (the integration with Pump will be added later).
*
* @tparam T The value type.
*/
template <typename T>
class Parameter
{
std::string name_;
T value_;
public:
Parameter(std::string_view name, T const& value) : name_(name), value_(value)
{
}
/** @brief Get the parameter name. */
[[nodiscard]] std::string_view
name() const
{
return name_;
}
/** @brief Get the parameter value. */
[[nodiscard]] T const&
value() const
{
return value_;
}
};
/**
* @brief Create a named log parameter.
*
* @tparam T The value type (deduced).
* @param name The parameter name (JSON key).
* @param value The parameter value.
* @return A Parameter holding the name and value.
*/
template <typename T>
[[nodiscard]] Parameter<std::decay_t<T>>
param(std::string_view name, T&& value)
{
return Parameter<std::decay_t<T>>{name, value};
}
} // namespace log
} // namespace xrpl