mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-04 17:27:00 +00:00
Add Logger from clio and implement json logging
This commit is contained in:
@@ -94,6 +94,7 @@ find_package(secp256k1 REQUIRED)
|
||||
find_package(SOCI REQUIRED)
|
||||
find_package(SQLite3 REQUIRED)
|
||||
find_package(xxHash REQUIRED)
|
||||
find_package(spdlog REQUIRED)
|
||||
|
||||
target_link_libraries(
|
||||
xrpl_libs
|
||||
|
||||
@@ -69,6 +69,7 @@ target_link_libraries(
|
||||
secp256k1::secp256k1
|
||||
xrpl.libpb
|
||||
xxHash::xxhash
|
||||
spdlog::spdlog
|
||||
$<$<BOOL:${voidstar}>:antithesis-sdk-cpp>
|
||||
)
|
||||
|
||||
|
||||
507
include/xrpl/basics/Logger.h
Normal file
507
include/xrpl/basics/Logger.h
Normal 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
|
||||
271
include/xrpl/basics/StructuredLogging.h
Normal file
271
include/xrpl/basics/StructuredLogging.h
Normal 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
|
||||
622
src/libxrpl/basics/Logger.cpp
Normal file
622
src/libxrpl/basics/Logger.cpp
Normal file
@@ -0,0 +1,622 @@
|
||||
#include <xrpl/basics/Logger.h>
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <spdlog/async.h>
|
||||
#include <spdlog/async_logger.h>
|
||||
#include <spdlog/common.h>
|
||||
#include <spdlog/details/log_msg.h>
|
||||
#include <spdlog/formatter.h>
|
||||
#include <spdlog/logger.h>
|
||||
#include <spdlog/pattern_formatter.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
#include <spdlog/sinks/rotating_file_sink.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <source_location>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool LogServiceState::isAsync_{true};
|
||||
Severity LogServiceState::defaultSeverity_{Severity::NFO};
|
||||
std::vector<spdlog::sink_ptr> LogServiceState::sinks_{};
|
||||
bool LogServiceState::initialized_{false};
|
||||
bool LogServiceState::jsonMode_{false};
|
||||
std::optional<std::string> LogServiceState::logDir_{};
|
||||
std::string LogServiceState::format_{};
|
||||
|
||||
namespace {
|
||||
|
||||
spdlog::level::level_enum
|
||||
toSpdlogLevel(Severity sev)
|
||||
{
|
||||
switch (sev)
|
||||
{
|
||||
case Severity::TRC:
|
||||
return spdlog::level::trace;
|
||||
case Severity::DBG:
|
||||
return spdlog::level::debug;
|
||||
case Severity::NFO:
|
||||
return spdlog::level::info;
|
||||
case Severity::WRN:
|
||||
return spdlog::level::warn;
|
||||
case Severity::ERR:
|
||||
return spdlog::level::err;
|
||||
case Severity::FTL:
|
||||
return spdlog::level::critical;
|
||||
}
|
||||
return spdlog::level::info;
|
||||
}
|
||||
|
||||
std::string_view
|
||||
toString(Severity sev)
|
||||
{
|
||||
static constexpr std::array<std::string_view, 6> kLABELS = {
|
||||
"TRC",
|
||||
"DBG",
|
||||
"NFO",
|
||||
"WRN",
|
||||
"ERR",
|
||||
"FTL",
|
||||
};
|
||||
|
||||
return kLABELS.at(static_cast<int>(sev));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief Custom formatter that filters out critical messages
|
||||
*
|
||||
* This formatter only processes and formats messages with severity level less than critical.
|
||||
* Critical messages will be handled separately.
|
||||
*/
|
||||
class NonCriticalFormatter : public spdlog::formatter
|
||||
{
|
||||
public:
|
||||
NonCriticalFormatter(std::unique_ptr<spdlog::formatter> wrappedFormatter)
|
||||
: wrapped_formatter_(std::move(wrappedFormatter))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
format(spdlog::details::log_msg const& msg, spdlog::memory_buf_t& dest) override
|
||||
{
|
||||
// Only format messages with severity less than critical
|
||||
if (msg.level != spdlog::level::critical)
|
||||
{
|
||||
wrapped_formatter_->format(msg, dest);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::unique_ptr<formatter>
|
||||
clone() const override
|
||||
{
|
||||
return std::make_unique<NonCriticalFormatter>(wrapped_formatter_->clone());
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<spdlog::formatter> wrapped_formatter_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Custom spdlog flag formatter that outputs the three-letter severity
|
||||
* codes used by the legacy logging system: TRC, DBG, NFO, WRN, ERR, FTL.
|
||||
*
|
||||
* Registered as the '%K' custom flag in pattern formatters created by
|
||||
* @ref createPatternFormatter.
|
||||
*/
|
||||
class SeverityFlagFormatter final : public spdlog::custom_flag_formatter
|
||||
{
|
||||
public:
|
||||
void
|
||||
format(
|
||||
spdlog::details::log_msg const& msg,
|
||||
std::tm const& /*tm_time*/,
|
||||
spdlog::memory_buf_t& dest) override
|
||||
{
|
||||
static constexpr std::array<std::string_view, 7> kLABELS = {
|
||||
"TRC",
|
||||
"DBG",
|
||||
"NFO",
|
||||
"WRN",
|
||||
"ERR",
|
||||
"FTL",
|
||||
"OFF",
|
||||
};
|
||||
|
||||
auto const idx = static_cast<std::size_t>(msg.level);
|
||||
auto const label = idx < kLABELS.size() ? kLABELS[idx] : "???";
|
||||
dest.append(label.data(), label.data() + label.size());
|
||||
}
|
||||
|
||||
[[nodiscard]] std::unique_ptr<custom_flag_formatter>
|
||||
clone() const override
|
||||
{
|
||||
return std::make_unique<SeverityFlagFormatter>();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Creates a spdlog pattern_formatter with our custom '%K' severity flag
|
||||
* and UTC timestamps.
|
||||
*
|
||||
* @param pattern The spdlog pattern string (should use '%K' for severity)
|
||||
* @return A unique_ptr to the configured pattern_formatter
|
||||
*/
|
||||
static std::unique_ptr<spdlog::pattern_formatter>
|
||||
createPatternFormatter(std::string const& pattern)
|
||||
{
|
||||
auto formatter = std::make_unique<spdlog::pattern_formatter>(
|
||||
pattern, spdlog::pattern_time_type::utc, spdlog::details::os::default_eol);
|
||||
formatter->add_flag<SeverityFlagFormatter>('K');
|
||||
formatter->set_pattern(pattern);
|
||||
return formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Truncates a log message to the maximum allowed length, appending
|
||||
* an ellipsis if truncation occurs.
|
||||
*
|
||||
* Matches the legacy Logs::format behaviour with maximumMessageCharacters = 12 * 1024.
|
||||
*
|
||||
* @param message The message to potentially truncate (modified in-place)
|
||||
*/
|
||||
static void
|
||||
truncateMessage(std::string& message)
|
||||
{
|
||||
static constexpr std::size_t kMAX_MESSAGE_CHARS = 12 * 1024;
|
||||
if (message.size() > kMAX_MESSAGE_CHARS)
|
||||
{
|
||||
message.resize(kMAX_MESSAGE_CHARS - 3);
|
||||
message += "...";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scrubs sensitive fields from a log message by replacing their values
|
||||
* with asterisks.
|
||||
*
|
||||
* Ported from the legacy Logs::format scrubber. Looks for JSON-like tokens
|
||||
* such as "seed", "secret", "master_key", etc., finds the value enclosed in
|
||||
* double quotes immediately after the token, and replaces the value characters
|
||||
* with '*'.
|
||||
*
|
||||
* @param output The log message to scrub (modified in-place)
|
||||
*/
|
||||
static void
|
||||
scrubSecrets(std::string& output)
|
||||
{
|
||||
auto scrubber = [&output](char const* token) {
|
||||
auto first = output.find(token);
|
||||
|
||||
// If we have found the specified token, then attempt to isolate the
|
||||
// sensitive data (it's enclosed by double quotes) and mask it off:
|
||||
if (first != std::string::npos)
|
||||
{
|
||||
first = output.find('\"', first + std::strlen(token));
|
||||
|
||||
if (first != std::string::npos)
|
||||
{
|
||||
auto last = output.find('\"', ++first);
|
||||
|
||||
if (last == std::string::npos)
|
||||
last = output.size();
|
||||
|
||||
output.replace(first, last - first, last - first, '*');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scrubber("\"seed\"");
|
||||
scrubber("\"seed_hex\"");
|
||||
scrubber("\"secret\"");
|
||||
scrubber("\"master_key\"");
|
||||
scrubber("\"master_seed\"");
|
||||
scrubber("\"master_seed_hex\"");
|
||||
scrubber("\"passphrase\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initializes console logging.
|
||||
*
|
||||
* @param logToConsole A boolean indicating whether to log to console.
|
||||
* @param format A string representing the log format.
|
||||
* @return Vector of sinks for console logging.
|
||||
*/
|
||||
static std::vector<spdlog::sink_ptr>
|
||||
createConsoleSinks(bool logToConsole, std::string const& format)
|
||||
{
|
||||
std::vector<spdlog::sink_ptr> sinks;
|
||||
|
||||
if (logToConsole)
|
||||
{
|
||||
auto consoleSink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
|
||||
consoleSink->set_level(spdlog::level::trace);
|
||||
consoleSink->set_formatter(
|
||||
std::make_unique<NonCriticalFormatter>(createPatternFormatter(format)));
|
||||
sinks.push_back(std::move(consoleSink));
|
||||
}
|
||||
|
||||
// Always add stderr sink for fatal logs
|
||||
auto stderrSink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
|
||||
stderrSink->set_level(spdlog::level::critical);
|
||||
stderrSink->set_formatter(createPatternFormatter(format));
|
||||
sinks.push_back(std::move(stderrSink));
|
||||
|
||||
return sinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initializes file logging.
|
||||
*
|
||||
* @param config The configuration object containing log settings.
|
||||
* @param dirPath The directory path where log files will be stored.
|
||||
* @return File sink for logging.
|
||||
*/
|
||||
spdlog::sink_ptr
|
||||
LogService::createFileSink(FileLoggingParams const& params, std::string const& format)
|
||||
{
|
||||
std::filesystem::path const dirPath(params.logDir);
|
||||
auto fileSink = [&]() -> std::shared_ptr<spdlog::sinks::sink> {
|
||||
auto const logPath = (dirPath / "xrpld.log").string();
|
||||
return std::make_shared<spdlog::sinks::basic_file_sink_mt>(logPath, /*truncate=*/false);
|
||||
}();
|
||||
|
||||
fileSink->set_level(spdlog::level::trace);
|
||||
fileSink->set_formatter(createPatternFormatter(format));
|
||||
|
||||
return fileSink;
|
||||
}
|
||||
|
||||
void
|
||||
LogServiceState::init(
|
||||
bool isAsync,
|
||||
Severity defaultSeverity,
|
||||
std::vector<spdlog::sink_ptr> const& sinks,
|
||||
bool jsonMode)
|
||||
{
|
||||
if (initialized_)
|
||||
{
|
||||
throw std::logic_error("LogServiceState is already initialized");
|
||||
}
|
||||
|
||||
isAsync_ = isAsync;
|
||||
defaultSeverity_ = defaultSeverity;
|
||||
sinks_ = sinks;
|
||||
jsonMode_ = jsonMode;
|
||||
initialized_ = true;
|
||||
|
||||
spdlog::apply_all([](std::shared_ptr<spdlog::logger> logger) {
|
||||
logger->set_level(toSpdlogLevel(defaultSeverity_));
|
||||
});
|
||||
|
||||
if (isAsync)
|
||||
{
|
||||
static constexpr size_t kQUEUE_SIZE = 8192;
|
||||
static constexpr size_t kTHREAD_COUNT = 1;
|
||||
spdlog::init_thread_pool(kQUEUE_SIZE, kTHREAD_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
LogServiceState::initialized()
|
||||
{
|
||||
return initialized_;
|
||||
}
|
||||
|
||||
bool
|
||||
LogServiceState::hasSinks()
|
||||
{
|
||||
return not sinks_.empty();
|
||||
}
|
||||
|
||||
void
|
||||
LogServiceState::reset()
|
||||
{
|
||||
if (not initialized())
|
||||
{
|
||||
throw std::logic_error("LogService is not initialized");
|
||||
}
|
||||
isAsync_ = true;
|
||||
defaultSeverity_ = Severity::NFO;
|
||||
sinks_.clear();
|
||||
jsonMode_ = false;
|
||||
logDir_.reset();
|
||||
format_.clear();
|
||||
initialized_ = false;
|
||||
}
|
||||
|
||||
std::shared_ptr<spdlog::logger>
|
||||
LogServiceState::registerLogger(std::string_view channel, std::optional<Severity> severity)
|
||||
{
|
||||
if (not initialized_)
|
||||
{
|
||||
throw std::logic_error("LogService is not initialized");
|
||||
}
|
||||
|
||||
std::string const channelStr{channel};
|
||||
|
||||
std::shared_ptr<spdlog::logger> existingLogger = spdlog::get(channelStr);
|
||||
if (existingLogger != nullptr)
|
||||
{
|
||||
if (severity.has_value())
|
||||
existingLogger->set_level(toSpdlogLevel(*severity));
|
||||
return existingLogger;
|
||||
}
|
||||
|
||||
std::shared_ptr<spdlog::logger> logger;
|
||||
if (isAsync_)
|
||||
{
|
||||
logger = std::make_shared<spdlog::async_logger>(
|
||||
channelStr,
|
||||
sinks_.begin(),
|
||||
sinks_.end(),
|
||||
spdlog::thread_pool(),
|
||||
spdlog::async_overflow_policy::block);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger = std::make_shared<spdlog::logger>(channelStr, sinks_.begin(), sinks_.end());
|
||||
}
|
||||
|
||||
logger->set_level(toSpdlogLevel(severity.value_or(defaultSeverity_)));
|
||||
logger->flush_on(spdlog::level::err);
|
||||
|
||||
spdlog::register_logger(logger);
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
Expected<std::vector<spdlog::sink_ptr>, std::string>
|
||||
LogService::getSinks(LoggingConfiguration const& config)
|
||||
{
|
||||
std::string const format = config.format;
|
||||
|
||||
std::vector<spdlog::sink_ptr> allSinks = createConsoleSinks(config.enableConsole, format);
|
||||
|
||||
if (config.directory.has_value())
|
||||
{
|
||||
std::filesystem::path const dirPath{config.directory.value()};
|
||||
if (not std::filesystem::exists(dirPath))
|
||||
{
|
||||
if (std::error_code error; not std::filesystem::create_directories(dirPath, error))
|
||||
{
|
||||
return Unexpected{fmt::format(
|
||||
"Couldn't create logs directory '{}': {}", dirPath.string(), error.message())};
|
||||
}
|
||||
}
|
||||
|
||||
FileLoggingParams const params{
|
||||
.logDir = config.directory.value(),
|
||||
};
|
||||
allSinks.push_back(createFileSink(params, format));
|
||||
}
|
||||
return allSinks;
|
||||
}
|
||||
|
||||
Expected<void, std::string>
|
||||
LogService::init(LoggingConfiguration const& config)
|
||||
{
|
||||
auto const sinksMaybe = getSinks(config);
|
||||
if (!sinksMaybe.has_value())
|
||||
{
|
||||
return Unexpected{sinksMaybe.error()};
|
||||
}
|
||||
|
||||
logDir_ = config.directory;
|
||||
format_ = config.format;
|
||||
|
||||
LogServiceState::init(
|
||||
config.isAsync, config.defaultSeverity, std::move(sinksMaybe).value(), config.jsonMode);
|
||||
|
||||
// Register and set the "General" channel as the default logger.
|
||||
// All other channels are created dynamically at runtime via Logger(channel).
|
||||
spdlog::set_default_logger(registerLogger("General"));
|
||||
|
||||
LOG(LogService::info()) << "Default log level = " << toString(defaultSeverity_);
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
LogService::shutdown()
|
||||
{
|
||||
if (initialized_ && isAsync_)
|
||||
{
|
||||
// We run in async mode in production, so we need to make sure all logs are flushed before
|
||||
// shutting down
|
||||
spdlog::shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
std::string
|
||||
LogService::rotate()
|
||||
{
|
||||
if (!logDir_.has_value())
|
||||
{
|
||||
return "Log file rotation is not possible because file logging is not configured.";
|
||||
}
|
||||
|
||||
FileLoggingParams const params{.logDir = logDir_.value()};
|
||||
|
||||
auto newFileSink = createFileSink(params, format_);
|
||||
|
||||
// Replace any existing file sink with the new one
|
||||
for (auto& sink : sinks_)
|
||||
{
|
||||
if (dynamic_cast<spdlog::sinks::basic_file_sink_mt*>(sink.get()) != nullptr)
|
||||
{
|
||||
sink = newFileSink;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all registered loggers with the new sinks
|
||||
spdlog::apply_all([](std::shared_ptr<spdlog::logger> logger) { logger->sinks() = sinks_; });
|
||||
|
||||
return "The log file was closed and reopened.";
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
LogService::trace(std::source_location const& loc)
|
||||
{
|
||||
return Logger(spdlog::default_logger()).trace(loc);
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
LogService::debug(std::source_location const& loc)
|
||||
{
|
||||
return Logger(spdlog::default_logger()).debug(loc);
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
LogService::info(std::source_location const& loc)
|
||||
{
|
||||
return Logger(spdlog::default_logger()).info(loc);
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
LogService::warn(std::source_location const& loc)
|
||||
{
|
||||
return Logger(spdlog::default_logger()).warn(loc);
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
LogService::error(std::source_location const& loc)
|
||||
{
|
||||
return Logger(spdlog::default_logger()).error(loc);
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
LogService::fatal(std::source_location const& loc)
|
||||
{
|
||||
return Logger(spdlog::default_logger()).fatal(loc);
|
||||
}
|
||||
|
||||
void
|
||||
LogServiceState::replaceSinks(std::vector<std::shared_ptr<spdlog::sinks::sink>> const& sinks)
|
||||
{
|
||||
sinks_ = sinks;
|
||||
spdlog::apply_all([](std::shared_ptr<spdlog::logger> logger) { logger->sinks() = sinks_; });
|
||||
}
|
||||
|
||||
std::unique_ptr<spdlog::formatter>
|
||||
LogServiceState::makeFormatter(std::string const& pattern)
|
||||
{
|
||||
return createPatternFormatter(pattern);
|
||||
}
|
||||
|
||||
Logger::Logger(std::string_view const channel) : logger_(LogServiceState::registerLogger(channel))
|
||||
{
|
||||
}
|
||||
|
||||
Logger::~Logger()
|
||||
{
|
||||
// One reference is held by logger_ and the other by spdlog registry
|
||||
static constexpr size_t kLAST_LOGGER_REF_COUNT = 2;
|
||||
|
||||
if (logger_ == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger_.use_count() == kLAST_LOGGER_REF_COUNT)
|
||||
{
|
||||
spdlog::drop(logger_->name());
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Pump::Pump(
|
||||
std::shared_ptr<spdlog::logger> logger,
|
||||
Severity sev,
|
||||
std::source_location const& loc,
|
||||
bool jsonMode)
|
||||
: logger_(std::move(logger))
|
||||
, severity_(sev)
|
||||
, sourceLocation_(loc)
|
||||
, enabled_(logger_ != nullptr && logger_->should_log(toSpdlogLevel(sev)))
|
||||
, jsonMode_(jsonMode)
|
||||
{
|
||||
if (enabled_ && jsonMode_)
|
||||
{
|
||||
// Open the quoted message value
|
||||
stream_ += "\"";
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Pump::~Pump()
|
||||
{
|
||||
if (enabled_)
|
||||
{
|
||||
spdlog::source_loc const sourceLocation{
|
||||
sourceLocation_.file_name(), static_cast<int>(sourceLocation_.line()), nullptr};
|
||||
|
||||
if (jsonMode_)
|
||||
{
|
||||
// Close the quoted message value
|
||||
stream_ += "\"";
|
||||
|
||||
// Append the parameters object if any were captured
|
||||
if (!parameters_.empty())
|
||||
{
|
||||
stream_ += ", \"values\": {";
|
||||
stream_ += parameters_;
|
||||
stream_ += "}";
|
||||
}
|
||||
}
|
||||
|
||||
// Apply legacy safeguards: truncate oversized messages and scrub secrets
|
||||
truncateMessage(stream_);
|
||||
scrubSecrets(stream_);
|
||||
|
||||
logger_->log(sourceLocation, toSpdlogLevel(severity_), stream_);
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
Logger::trace(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_, Severity::TRC, loc, LogServiceState::jsonMode_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::debug(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_, Severity::DBG, loc, LogServiceState::jsonMode_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::info(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_, Severity::NFO, loc, LogServiceState::jsonMode_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::warn(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_, Severity::WRN, loc, LogServiceState::jsonMode_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::error(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_, Severity::ERR, loc, LogServiceState::jsonMode_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::fatal(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_, Severity::FTL, loc, LogServiceState::jsonMode_};
|
||||
}
|
||||
|
||||
Logger::Logger(std::shared_ptr<spdlog::logger> logger) : logger_(std::move(logger))
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
278
src/tests/libxrpl/basics/Logger.cpp
Normal file
278
src/tests/libxrpl/basics/Logger.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#include <xrpl/basics/Logger.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <spdlog/sinks/ostream_sink.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
class LoggerFixture : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
std::ostringstream output_;
|
||||
|
||||
void
|
||||
initLogging(bool jsonMode, std::string const& pattern = "%v", Severity severity = Severity::TRC)
|
||||
{
|
||||
LoggingConfiguration config{
|
||||
.format = pattern,
|
||||
.enableConsole = false,
|
||||
.directory = std::nullopt,
|
||||
.isAsync = false,
|
||||
.defaultSeverity = severity,
|
||||
.jsonMode = jsonMode,
|
||||
};
|
||||
auto const result = LogService::init(config);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// Replace the (empty) sinks with our ostream sink so we can
|
||||
// capture output in tests.
|
||||
auto sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(output_);
|
||||
sink->set_level(spdlog::level::trace);
|
||||
sink->set_formatter(LogServiceState::makeFormatter(pattern));
|
||||
LogServiceState::replaceSinks({sink});
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
spdlog::drop_all();
|
||||
if (LogService::initialized())
|
||||
LogService::reset();
|
||||
}
|
||||
};
|
||||
|
||||
// -- Plain text mode ---------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, plain_text_simple_message)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("TestChannel");
|
||||
logger.info() << "hello world";
|
||||
EXPECT_EQ(output_.str(), "hello world\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, plain_text_multiple_values)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("TestChannel2");
|
||||
logger.info() << "count=" << 42 << " active=" << true;
|
||||
EXPECT_EQ(output_.str(), "count=42 active=true\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, plain_text_with_parameter)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("TestChannel3");
|
||||
logger.info() << "tx " << log::param("hash", "ABC");
|
||||
// In plain text mode, parameter value is just streamed
|
||||
EXPECT_EQ(output_.str(), "tx ABC\n");
|
||||
}
|
||||
|
||||
// -- JSON mode ---------------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_simple_message)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger logger("JsonChannel");
|
||||
logger.info() << "hello world";
|
||||
// In JSON mode, message is quoted
|
||||
EXPECT_EQ(output_.str(), "\"hello world\"\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_with_parameters)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger logger("JsonChannel2");
|
||||
logger.info() << "processing " << log::param("tx_hash", std::string("ABC123"))
|
||||
<< " amount=" << log::param("amount", 42);
|
||||
// Message body is quoted, then values object appended
|
||||
EXPECT_EQ(
|
||||
output_.str(),
|
||||
"\"processing ABC123 amount=42\", "
|
||||
"\"values\": {\"tx_hash\":\"ABC123\",\"amount\":42}\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_no_parameters)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger logger("JsonChannel3");
|
||||
logger.info() << "simple message";
|
||||
// No values object when no parameters
|
||||
EXPECT_EQ(output_.str(), "\"simple message\"\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_with_pattern)
|
||||
{
|
||||
std::string pattern =
|
||||
JsonLoggingPatternBuilder().add("level", "%l").add("channel", "%n").build();
|
||||
initLogging(true, pattern);
|
||||
Logger logger("Overlay");
|
||||
logger.info() << "peer connected";
|
||||
EXPECT_EQ(
|
||||
output_.str(),
|
||||
"{\"level\":\"info\",\"channel\":\"Overlay\","
|
||||
" \"message\": \"peer connected\" }\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_bool_parameter)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger logger("JsonChannel4");
|
||||
logger.info() << "status " << log::param("active", true);
|
||||
EXPECT_EQ(output_.str(), "\"status true\", \"values\": {\"active\":true}\n");
|
||||
}
|
||||
|
||||
// -- Severity filtering ------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, severity_filtering)
|
||||
{
|
||||
initLogging(false, "%v", Severity::WRN);
|
||||
|
||||
Logger logger("FilterChannel");
|
||||
logger.info() << "should not appear";
|
||||
EXPECT_TRUE(output_.str().empty());
|
||||
|
||||
logger.warn() << "should appear";
|
||||
EXPECT_EQ(output_.str(), "should appear\n");
|
||||
}
|
||||
|
||||
// -- xrpl::to_string integration --------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, text_mode_xrp_amount)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("AmountChannel");
|
||||
logger.info() << "balance: " << XRPAmount{1000};
|
||||
EXPECT_EQ(output_.str(), "balance: 1000\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_xrp_amount)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger logger("AmountChannel");
|
||||
logger.info() << "balance " << XRPAmount{500};
|
||||
EXPECT_EQ(output_.str(), "\"balance 500\"\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_xrp_amount_parameter)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger logger("AmountChannel");
|
||||
logger.info() << "tx" << log::param("fee", XRPAmount{10});
|
||||
EXPECT_EQ(output_.str(), "\"tx10\", \"values\": {\"fee\":\"10\"}\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, text_mode_number)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("NumberChannel");
|
||||
logger.info() << "result: " << Number{42};
|
||||
EXPECT_EQ(output_.str(), "result: 42\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_number)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger logger("NumberChannel");
|
||||
logger.info() << "value " << Number{25, -3};
|
||||
EXPECT_EQ(output_.str(), "\"value 0.025\"\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_number_parameter)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger logger("NumberChannel");
|
||||
logger.info() << "calc" << log::param("rate", Number{100});
|
||||
EXPECT_EQ(output_.str(), "\"calc100\", \"values\": {\"rate\":\"100\"}\n");
|
||||
}
|
||||
|
||||
// -- Legacy format matching ---------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, default_format_matches_legacy)
|
||||
{
|
||||
// Use the default format (kDEFAULT_LOG_FORMAT) which includes %K for severity
|
||||
initLogging(false, kDEFAULT_LOG_FORMAT);
|
||||
Logger logger("General");
|
||||
logger.info() << "hello world";
|
||||
auto const line = output_.str();
|
||||
|
||||
// The full output must be:
|
||||
// YYYY-Mon-DD HH:MM:SS.ffffff UTC General:NFO hello world\n
|
||||
std::regex const expected(
|
||||
R"(\d{4}-[A-Z][a-z]{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6} UTC General:NFO hello world\n)");
|
||||
EXPECT_TRUE(std::regex_match(line, expected)) << "got: " << line;
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, severity_codes_in_default_format)
|
||||
{
|
||||
initLogging(false, "%n:%K %v");
|
||||
Logger logger("Test");
|
||||
|
||||
logger.trace() << "t";
|
||||
logger.debug() << "d";
|
||||
logger.info() << "i";
|
||||
logger.warn() << "w";
|
||||
logger.error() << "e";
|
||||
logger.fatal() << "f";
|
||||
|
||||
EXPECT_EQ(
|
||||
output_.str(),
|
||||
"Test:TRC t\n"
|
||||
"Test:DBG d\n"
|
||||
"Test:NFO i\n"
|
||||
"Test:WRN w\n"
|
||||
"Test:ERR e\n"
|
||||
"Test:FTL f\n");
|
||||
}
|
||||
|
||||
// -- Secret scrubbing ---------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_seed)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("Scrub");
|
||||
logger.info() << R"({"seed":"sEdTM1uX8pu2do5XvTnutH6HsouMaM2"})";
|
||||
// 31 chars in the seed value → 31 asterisks
|
||||
EXPECT_EQ(output_.str(), "{\"seed\":\"*******************************\"}\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_master_key)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("Scrub2");
|
||||
logger.info() << R"({"master_key":"SOME_SECRET_VALUE"})";
|
||||
EXPECT_EQ(output_.str(), "{\"master_key\":\"*****************\"}\n");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_passphrase)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("Scrub3");
|
||||
logger.info() << R"({"passphrase":"my_secret_pass"})";
|
||||
EXPECT_EQ(output_.str(), "{\"passphrase\":\"**************\"}\n");
|
||||
}
|
||||
|
||||
// -- Message truncation -------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, truncates_oversized_message)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger logger("Trunc");
|
||||
// 12 * 1024 = 12288 max chars; create a message larger than that
|
||||
static constexpr std::size_t kMAX = 12 * 1024;
|
||||
std::string const bigMessage(13000, 'x');
|
||||
logger.info() << bigMessage;
|
||||
|
||||
// Expected: (kMAX - 3) 'x' chars + "..." + newline
|
||||
std::string expected(kMAX - 3, 'x');
|
||||
expected += "...\n";
|
||||
EXPECT_EQ(output_.str(), expected);
|
||||
}
|
||||
173
src/tests/libxrpl/basics/StructuredLogging.cpp
Normal file
173
src/tests/libxrpl/basics/StructuredLogging.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include <xrpl/basics/StructuredLogging.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
// -- detail::appendJsonValue -------------------------------------------------
|
||||
|
||||
TEST(AppendJsonValue, bool_true)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonValue(dest, true);
|
||||
EXPECT_EQ(dest, "true");
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, bool_false)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonValue(dest, false);
|
||||
EXPECT_EQ(dest, "false");
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, integral_positive)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonValue(dest, 42);
|
||||
EXPECT_EQ(dest, "42");
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, integral_negative)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonValue(dest, -7);
|
||||
EXPECT_EQ(dest, "-7");
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, integral_zero)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonValue(dest, 0);
|
||||
EXPECT_EQ(dest, "0");
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, string_quoted)
|
||||
{
|
||||
std::string dest;
|
||||
std::string val = "hello";
|
||||
detail::appendJsonValue(dest, val);
|
||||
EXPECT_EQ(dest, "\"hello\"");
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, appends_to_existing)
|
||||
{
|
||||
std::string dest = "prefix:";
|
||||
detail::appendJsonValue(dest, 99);
|
||||
EXPECT_EQ(dest, "prefix:99");
|
||||
}
|
||||
|
||||
// -- JsonLoggingPatternBuilder -----------------------------------------------
|
||||
|
||||
TEST(JsonLoggingPatternBuilder, empty_build)
|
||||
{
|
||||
JsonLoggingPatternBuilder builder;
|
||||
EXPECT_EQ(builder.build(), "{, \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(JsonLoggingPatternBuilder, single_string_field)
|
||||
{
|
||||
JsonLoggingPatternBuilder builder;
|
||||
builder.add("level", "%l");
|
||||
EXPECT_EQ(builder.build(), "{\"level\":\"%l\", \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(JsonLoggingPatternBuilder, multiple_string_fields)
|
||||
{
|
||||
JsonLoggingPatternBuilder builder;
|
||||
builder.add("level", "%l");
|
||||
builder.add("channel", "%n");
|
||||
EXPECT_EQ(builder.build(), "{\"level\":\"%l\",\"channel\":\"%n\", \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(JsonLoggingPatternBuilder, typed_fields)
|
||||
{
|
||||
JsonLoggingPatternBuilder builder;
|
||||
builder.add("enabled", true);
|
||||
builder.add("count", 5);
|
||||
EXPECT_EQ(builder.build(), "{\"enabled\":true,\"count\":5, \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(JsonLoggingPatternBuilder, chaining)
|
||||
{
|
||||
JsonLoggingPatternBuilder builder;
|
||||
builder.add("a", "1").add("b", "2").add("c", "3");
|
||||
EXPECT_EQ(builder.build(), "{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\", \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(JsonLoggingPatternBuilder, from_existing_pattern)
|
||||
{
|
||||
JsonLoggingPatternBuilder original;
|
||||
original.add("level", "%l").add("channel", "%n");
|
||||
auto const pattern = original.build();
|
||||
|
||||
JsonLoggingPatternBuilder extended(pattern);
|
||||
extended.add("source", "%s:%#");
|
||||
EXPECT_EQ(
|
||||
extended.build(),
|
||||
"{\"level\":\"%l\",\"channel\":\"%n\",\"source\":\"%s:%#\", \"message\": %v }");
|
||||
}
|
||||
|
||||
// -- log::Parameter / log::param ---------------------------------------------
|
||||
|
||||
TEST(LogParameter, string_param)
|
||||
{
|
||||
auto const p = log::param("tx_hash", std::string("ABC123"));
|
||||
EXPECT_EQ(p.name(), "tx_hash");
|
||||
EXPECT_EQ(p.value(), "ABC123");
|
||||
}
|
||||
|
||||
TEST(LogParameter, int_param)
|
||||
{
|
||||
auto const p = log::param("count", 42);
|
||||
EXPECT_EQ(p.name(), "count");
|
||||
EXPECT_EQ(p.value(), 42);
|
||||
}
|
||||
|
||||
TEST(LogParameter, bool_param)
|
||||
{
|
||||
auto const p = log::param("active", true);
|
||||
EXPECT_EQ(p.name(), "active");
|
||||
EXPECT_EQ(p.value(), true);
|
||||
}
|
||||
|
||||
// -- detail::HasToString concept ---------------------------------------------
|
||||
|
||||
TEST(HasToString, xrp_amount_satisfies_concept)
|
||||
{
|
||||
static_assert(detail::HasToString<XRPAmount>);
|
||||
}
|
||||
|
||||
TEST(HasToString, number_satisfies_concept)
|
||||
{
|
||||
static_assert(detail::HasToString<Number>);
|
||||
}
|
||||
|
||||
TEST(HasToString, builtin_types_without_adl)
|
||||
{
|
||||
// Built-in types have no associated namespace for ADL, so unless
|
||||
// ToString.h is explicitly included they do not satisfy HasToString.
|
||||
// They are handled by the fmt::format fallback path instead.
|
||||
static_assert(!detail::HasToString<int>);
|
||||
static_assert(!detail::HasToString<double>);
|
||||
}
|
||||
|
||||
// -- appendJsonValue with to_string types ------------------------------------
|
||||
|
||||
TEST(AppendJsonValue, xrp_amount_quoted)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonValue(dest, XRPAmount{1000});
|
||||
EXPECT_EQ(dest, "\"1000\"");
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, number_quoted)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonValue(dest, Number{25, -3});
|
||||
EXPECT_EQ(dest, "\"0.025\"");
|
||||
}
|
||||
Reference in New Issue
Block a user