mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-04 17:27:00 +00:00
Compare commits
28 Commits
copilot/co
...
a1q123456/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09009c2533 | ||
|
|
d0e6c9961b | ||
|
|
bfd2022e98 | ||
|
|
f3df114918 | ||
|
|
13bdcee55d | ||
|
|
319a298b45 | ||
|
|
676bc9fb5b | ||
|
|
09851d906c | ||
|
|
0edd5174a1 | ||
|
|
3f7b2ae1c3 | ||
|
|
c9cf78f421 | ||
|
|
3f1cb056c2 | ||
|
|
b70c1f9e62 | ||
|
|
418f67077c | ||
|
|
2e76861945 | ||
|
|
9135171f74 | ||
|
|
ed15220901 | ||
|
|
ed7a936e1f | ||
|
|
f1305dc629 | ||
|
|
5c01da5244 | ||
|
|
640c90ee35 | ||
|
|
6897a853bb | ||
|
|
a3f8e8f32a | ||
|
|
d1f6d4f339 | ||
|
|
06193bde5d | ||
|
|
cd94d7d99b | ||
|
|
17c7398f5d | ||
|
|
a41704b61c |
@@ -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>
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"zlib/1.3.1#cac0f6daea041b0ccf42934163defb20%1774439233.809",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
|
||||
"sqlite3/3.51.0#66aa11eabd0e34954c5c1c061ad44abe%1774467355.988",
|
||||
"spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1768312128.781",
|
||||
"soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
|
||||
"secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
|
||||
@@ -19,6 +20,7 @@
|
||||
"jemalloc/5.3.0#c671e612af76700db5957c9857978a1c%1776700030.961",
|
||||
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152",
|
||||
"grpc/1.78.1#b1a9e74b145cc471bed4dc64dc6eb2c1%1774467387.342",
|
||||
"fmt/12.1.0#50abab23274d56bb8f42c94b3b9a40c7%1763984116.926",
|
||||
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1765850143.772",
|
||||
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772",
|
||||
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1774439234.681",
|
||||
|
||||
@@ -35,6 +35,7 @@ class Xrpl(ConanFile):
|
||||
"openssl/3.6.2",
|
||||
"secp256k1/0.7.1",
|
||||
"soci/4.0.3",
|
||||
"spdlog/1.17.0",
|
||||
"zlib/1.3.1",
|
||||
]
|
||||
|
||||
@@ -109,6 +110,7 @@ class Xrpl(ConanFile):
|
||||
"secp256k1/*:shared": False,
|
||||
"snappy/*:shared": False,
|
||||
"soci/*:shared": False,
|
||||
"spdlog/*:shared": False,
|
||||
"soci/*:with_sqlite3": True,
|
||||
"soci/*:with_boost": True,
|
||||
"xxhash/*:shared": False,
|
||||
@@ -213,6 +215,7 @@ class Xrpl(ConanFile):
|
||||
"protobuf::libprotobuf",
|
||||
"soci::soci",
|
||||
"secp256k1::secp256k1",
|
||||
"spdlog::spdlog",
|
||||
"sqlite3::sqlite",
|
||||
"xxhash::xxhash",
|
||||
"zlib::zlib",
|
||||
|
||||
587
include/xrpl/basics/Logger.h
Normal file
587
include/xrpl/basics/Logger.h
Normal file
@@ -0,0 +1,587 @@
|
||||
#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
|
||||
|
||||
class LoggerFixture;
|
||||
|
||||
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; !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";
|
||||
|
||||
/**
|
||||
* @brief Build the default JSON log format pattern.
|
||||
*
|
||||
* Contains the same fields as @ref kDEFAULT_LOG_FORMAT (timestamp, channel,
|
||||
* severity) plus the trailing message placeholder:
|
||||
* @code
|
||||
* {"timestamp":"2024-Jan-15 12:34:56.789123 UTC","channel":"General","severity":"NFO",
|
||||
* "message": "hello world" }
|
||||
* @endcode
|
||||
*/
|
||||
inline std::string
|
||||
defaultJsonLogFormat()
|
||||
{
|
||||
using sv = std::string_view;
|
||||
return buildJsonPattern(
|
||||
"",
|
||||
log::param("timestamp", sv("%Y-%b-%d %H:%M:%S.%f UTC")),
|
||||
log::param("channel", sv("%n")),
|
||||
log::param("severity", sv("%K")));
|
||||
}
|
||||
|
||||
struct LoggingConfiguration
|
||||
{
|
||||
bool enableConsole{};
|
||||
std::optional<std::string> directory{std::nullopt};
|
||||
bool isAsync{};
|
||||
Severity defaultSeverity{Severity::DBG};
|
||||
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_;
|
||||
std::string contextParams_; // inherited parameter fragments (JSON or text)
|
||||
bool jsonMode_ = false; // captured at construction from LogServiceState
|
||||
|
||||
friend class LogService; // to expose the Pump interface
|
||||
|
||||
/**
|
||||
* @brief Helper that pumps data into a log record via `operator<<`.
|
||||
*/
|
||||
class Pump final
|
||||
{
|
||||
spdlog::logger* logger_;
|
||||
Severity const severity_;
|
||||
std::source_location const sourceLocation_;
|
||||
fmt::memory_buffer stream_;
|
||||
bool const enabled_;
|
||||
bool const jsonMode_;
|
||||
std::string_view contextParams_; // points into Logger's string (no copy)
|
||||
std::string messageParams_; // per-message params (JSON mode only)
|
||||
|
||||
public:
|
||||
~Pump();
|
||||
|
||||
Pump(
|
||||
spdlog::logger* logger,
|
||||
Severity sev,
|
||||
std::source_location const& loc,
|
||||
bool jsonMode,
|
||||
std::string_view contextParams = {});
|
||||
|
||||
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_)
|
||||
{
|
||||
auto const s = to_string(data);
|
||||
stream_.append(s.data(), s.data() + s.size());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends any fmt-formattable data into the output buffer.
|
||||
*
|
||||
* 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(fmt::appender(stream_), "{}", std::forward<T>(data));
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Captures a structured log parameter.
|
||||
*
|
||||
* The parameter value is always appended to the output buffer.
|
||||
* 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_)
|
||||
{
|
||||
// Append the raw string representation to the output buffer
|
||||
if constexpr (detail::HasToString<T>)
|
||||
{
|
||||
auto const s = to_string(p.value());
|
||||
stream_.append(s.data(), s.data() + s.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
fmt::format_to(fmt::appender(stream_), "{}", std::move(p.value()));
|
||||
}
|
||||
|
||||
if (jsonMode_)
|
||||
{
|
||||
detail::appendJsonField(messageParams_, p.name(), 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);
|
||||
|
||||
/**
|
||||
* @brief Construct a child Logger that inherits context from a parent.
|
||||
*
|
||||
* The child uses a new channel and carries all of the parent's context
|
||||
* parameters plus any additional parameters supplied here. Every log
|
||||
* line produced by the child will automatically include these
|
||||
* parameters in the JSON @c values object.
|
||||
*
|
||||
* @tparam Ts Value types of the extra parameters.
|
||||
* @param parent The parent Logger whose context is inherited.
|
||||
* @param channel The channel name for this child logger.
|
||||
* @param params Extra context parameters to attach.
|
||||
*/
|
||||
template <typename... Ts>
|
||||
Logger(Logger const& parent, std::string_view channel, log::Parameter<Ts> const&... params)
|
||||
: Logger(channel)
|
||||
{
|
||||
contextParams_ = parent.contextParams_;
|
||||
(appendContextParam(params), ...);
|
||||
}
|
||||
|
||||
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 Serialise a single parameter into contextParams_. */
|
||||
template <typename T>
|
||||
void
|
||||
appendContextParam(log::Parameter<T> const& p)
|
||||
{
|
||||
if (jsonMode_)
|
||||
{
|
||||
detail::appendJsonField(contextParams_, p.name(), p.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
detail::appendTextField(contextParams_, p.name(), p.value());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 class ::LoggerFixture;
|
||||
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);
|
||||
|
||||
/**
|
||||
* @brief Creates a formatter that suppresses critical-level messages.
|
||||
*
|
||||
* Wraps the given formatter so that only messages below critical severity
|
||||
* are formatted. Critical messages produce no output through this
|
||||
* formatter (they are routed to a separate stderr sink instead).
|
||||
*
|
||||
* @param wrappedFormatter The underlying formatter to delegate to
|
||||
* @return A unique_ptr to the NonCriticalFormatter
|
||||
*/
|
||||
static std::unique_ptr<spdlog::formatter>
|
||||
makeNonCriticalFormatter(std::unique_ptr<spdlog::formatter> wrappedFormatter);
|
||||
|
||||
/**
|
||||
* @brief Returns the active log format pattern.
|
||||
*
|
||||
* Automatically determined by the logging mode:
|
||||
* text mode uses @ref kDEFAULT_LOG_FORMAT, JSON mode uses
|
||||
* @ref defaultJsonLogFormat().
|
||||
*/
|
||||
static std::string const&
|
||||
format()
|
||||
{
|
||||
return format_;
|
||||
}
|
||||
|
||||
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, std::string const& format);
|
||||
|
||||
struct FileLoggingParams
|
||||
{
|
||||
std::string logDir;
|
||||
};
|
||||
|
||||
friend class ::LoggerFixture;
|
||||
|
||||
[[nodiscard]]
|
||||
static std::shared_ptr<spdlog::sinks::sink>
|
||||
createFileSink(FileLoggingParams const& params, std::string const& format);
|
||||
};
|
||||
|
||||
}; // namespace xrpl
|
||||
320
include/xrpl/basics/StructuredLogging.h
Normal file
320
include/xrpl/basics/StructuredLogging.h
Normal file
@@ -0,0 +1,320 @@
|
||||
#pragma once
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
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 Escape a string for safe embedding in a JSON value.
|
||||
*
|
||||
* Escapes backslash, double-quote, and control characters (U+0000-U+001F)
|
||||
* according to RFC 8259 section 7.
|
||||
*/
|
||||
inline void
|
||||
appendEscapedJsonString(std::string& dest, std::string_view sv)
|
||||
{
|
||||
dest.reserve(dest.size() + sv.size() + 2);
|
||||
dest += '"';
|
||||
for (char const c : sv)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
dest += "\\\"";
|
||||
break;
|
||||
case '\\':
|
||||
dest += "\\\\";
|
||||
break;
|
||||
case '\b':
|
||||
dest += "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
dest += "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
dest += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
dest += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
dest += "\\t";
|
||||
break;
|
||||
default:
|
||||
if (static_cast<unsigned char>(c) < 0x20)
|
||||
{
|
||||
fmt::format_to(
|
||||
std::back_inserter(dest), "\\u{:04x}", static_cast<unsigned int>(c));
|
||||
}
|
||||
else
|
||||
{
|
||||
dest += c;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
dest += '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* @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>)
|
||||
{
|
||||
appendEscapedJsonString(dest, to_string(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
appendEscapedJsonString(dest, fmt::format("{}", value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Append a @c "key":value JSON field to @p dest.
|
||||
*
|
||||
* Inserts a comma separator when @p dest already contains fields.
|
||||
* The value is formatted by @ref appendJsonValue.
|
||||
*/
|
||||
template <typename T>
|
||||
void
|
||||
appendJsonField(std::string& dest, std::string_view key, T const& value)
|
||||
{
|
||||
if (!dest.empty())
|
||||
dest += ',';
|
||||
appendEscapedJsonString(dest, key);
|
||||
dest += ':';
|
||||
appendJsonValue(dest, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Append a @c key=value plain-text field to @p dest.
|
||||
*
|
||||
* Inserts a space separator when @p dest already contains fields.
|
||||
*/
|
||||
template <typename T>
|
||||
void
|
||||
appendTextField(std::string& dest, std::string_view key, T const& value)
|
||||
{
|
||||
if (!dest.empty())
|
||||
dest += ' ';
|
||||
if constexpr (HasToString<T>)
|
||||
{
|
||||
fmt::format_to(std::back_inserter(dest), "{}={}", key, to_string(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
fmt::format_to(std::back_inserter(dest), "{}={}", key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Internal builder used by @ref buildJsonPattern.
|
||||
*
|
||||
* Not part of the public API — use @ref buildJsonPattern instead.
|
||||
*/
|
||||
class JsonLoggingPatternBuilder
|
||||
{
|
||||
std::string fields_;
|
||||
|
||||
static constexpr std::string_view kMESSAGE_FIELD = "\"message\": %v";
|
||||
|
||||
static std::string
|
||||
extractFields(std::string_view pattern)
|
||||
{
|
||||
if (!pattern.empty() && pattern.front() == '{')
|
||||
pattern.remove_prefix(1);
|
||||
// Strip the trailing message field: `"message": %v }` or `"message": %v }`
|
||||
if (auto pos = pattern.rfind(kMESSAGE_FIELD); pos != std::string_view::npos)
|
||||
{
|
||||
// Also strip the leading ", " separator if present
|
||||
auto end = pos;
|
||||
if (end >= 2 && pattern.substr(end - 2, 2) == ", ")
|
||||
end -= 2;
|
||||
pattern = pattern.substr(0, end);
|
||||
}
|
||||
return std::string(pattern);
|
||||
}
|
||||
|
||||
public:
|
||||
JsonLoggingPatternBuilder() = default;
|
||||
|
||||
explicit JsonLoggingPatternBuilder(std::string_view existingPattern)
|
||||
: fields_(extractFields(existingPattern))
|
||||
{
|
||||
}
|
||||
|
||||
/** @brief Add a field. Delegates to @ref appendJsonField. */
|
||||
template <typename T>
|
||||
JsonLoggingPatternBuilder&
|
||||
add(std::string_view key, T const& value)
|
||||
{
|
||||
appendJsonField(fields_, key, value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string
|
||||
build() const
|
||||
{
|
||||
std::string result = "{";
|
||||
result += fields_;
|
||||
if (!fields_.empty())
|
||||
result += ", ";
|
||||
result += kMESSAGE_FIELD;
|
||||
result += " }";
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
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 value) : name_(name), value_(std::move(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 Get the parameter value. */
|
||||
[[nodiscard]] T&
|
||||
value() &
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
/** @brief Get the parameter value. */
|
||||
[[nodiscard]] T&&
|
||||
value() &&
|
||||
{
|
||||
return std::move(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
|
||||
|
||||
/**
|
||||
* @brief Build or extend a JSON log pattern from @ref log::Parameter objects.
|
||||
*
|
||||
* Each parameter's name becomes a JSON key and its value becomes the
|
||||
* corresponding JSON value in the spdlog pattern string. String-like
|
||||
* values are treated as spdlog pattern flags (e.g. @c "%%n", @c "%%l");
|
||||
* numeric and boolean values are embedded as literals.
|
||||
*
|
||||
* @code
|
||||
* // Build from scratch (empty existing pattern)
|
||||
* auto pattern = buildJsonPattern("",
|
||||
* log::param("channel", std::string_view("%n")),
|
||||
* log::param("level", std::string_view("%l")));
|
||||
*
|
||||
* // Extend an existing pattern with extra fields
|
||||
* auto extended = buildJsonPattern(pattern,
|
||||
* log::param("trace_id", traceId));
|
||||
* @endcode
|
||||
*
|
||||
* @tparam Ts Value types of the parameters.
|
||||
* @param existingPattern A pattern previously produced by build() or an
|
||||
* empty string to start fresh.
|
||||
* @param params The parameters to add as JSON fields.
|
||||
* @return The spdlog-compatible JSON pattern string.
|
||||
*/
|
||||
template <typename... Ts>
|
||||
[[nodiscard]] std::string
|
||||
buildJsonPattern(std::string_view existingPattern, log::Parameter<Ts> const&... params)
|
||||
{
|
||||
// NOLINTNEXTLINE(misc-const-correctness)
|
||||
detail::JsonLoggingPatternBuilder builder(existingPattern);
|
||||
(builder.add(params.name(), params.value()), ...);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
683
src/libxrpl/basics/Logger.cpp
Normal file
683
src/libxrpl/basics/Logger.cpp
Normal file
@@ -0,0 +1,683 @@
|
||||
#include <xrpl/basics/Logger.h>
|
||||
|
||||
#include <xrpl/basics/Expected.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <spdlog/async.h>
|
||||
#include <spdlog/async_logger.h>
|
||||
#include <spdlog/common.h>
|
||||
#include <spdlog/details/log_msg.h>
|
||||
#include <spdlog/details/os.h>
|
||||
#include <spdlog/formatter.h>
|
||||
#include <spdlog/logger.h>
|
||||
#include <spdlog/pattern_formatter.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <source_location>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
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(fmt::memory_buffer& message)
|
||||
{
|
||||
static constexpr std::size_t kMAX_MESSAGE_CHARS = 12 * 1024;
|
||||
if (message.size() > kMAX_MESSAGE_CHARS)
|
||||
{
|
||||
message.resize(kMAX_MESSAGE_CHARS - 3);
|
||||
static constexpr char kELLIPSIS[] = "...";
|
||||
message.append(kELLIPSIS, kELLIPSIS + 3);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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(fmt::memory_buffer& output)
|
||||
{
|
||||
// Fast path: if there's no double-quote anywhere in the message,
|
||||
// none of the JSON-like tokens can possibly match.
|
||||
std::string_view const view{output.data(), output.size()};
|
||||
if (view.find('"') == std::string_view::npos)
|
||||
return;
|
||||
|
||||
// We need string operations (find/replace) so convert temporarily.
|
||||
// This is only reached for messages that contain at least one '"'.
|
||||
std::string tmp{view};
|
||||
|
||||
auto scrubber = [&tmp](char const* token) {
|
||||
auto first = tmp.find(token);
|
||||
|
||||
if (first != std::string::npos)
|
||||
{
|
||||
first = tmp.find('\"', first + std::strlen(token));
|
||||
|
||||
if (first != std::string::npos)
|
||||
{
|
||||
auto last = tmp.find('\"', ++first);
|
||||
|
||||
if (last == std::string::npos)
|
||||
last = tmp.size();
|
||||
|
||||
tmp.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\"");
|
||||
|
||||
// Copy the scrubbed result back into the buffer
|
||||
output.clear();
|
||||
output.append(tmp.data(), tmp.data() + tmp.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 !sinks_.empty();
|
||||
}
|
||||
|
||||
void
|
||||
LogServiceState::reset()
|
||||
{
|
||||
if (!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 (!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)
|
||||
{
|
||||
std::vector<spdlog::sink_ptr> allSinks = createConsoleSinks(config.enableConsole, format);
|
||||
|
||||
if (config.directory.has_value())
|
||||
{
|
||||
std::filesystem::path const dirPath{config.directory.value()};
|
||||
if (!std::filesystem::exists(dirPath))
|
||||
{
|
||||
if (std::error_code error; !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)
|
||||
{
|
||||
// Format is fully determined by the logging mode.
|
||||
format_ = config.jsonMode ? defaultJsonLogFormat() : std::string(kDEFAULT_LOG_FORMAT);
|
||||
|
||||
auto const sinksMaybe = getSinks(config, format_);
|
||||
if (!sinksMaybe.has_value())
|
||||
{
|
||||
return Unexpected{sinksMaybe.error()};
|
||||
}
|
||||
|
||||
logDir_ = config.directory;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
std::unique_ptr<spdlog::formatter>
|
||||
LogServiceState::makeNonCriticalFormatter(std::unique_ptr<spdlog::formatter> wrappedFormatter)
|
||||
{
|
||||
return std::make_unique<NonCriticalFormatter>(std::move(wrappedFormatter));
|
||||
}
|
||||
|
||||
Logger::Logger(std::string_view const channel)
|
||||
: logger_(LogServiceState::registerLogger(channel)), jsonMode_(LogServiceState::jsonMode_)
|
||||
{
|
||||
}
|
||||
|
||||
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; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (logger_.use_count() == kLAST_LOGGER_REF_COUNT)
|
||||
{
|
||||
spdlog::drop(logger_->name());
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Pump::Pump(
|
||||
spdlog::logger* logger,
|
||||
Severity sev,
|
||||
std::source_location const& loc,
|
||||
bool jsonMode,
|
||||
std::string_view contextParams)
|
||||
: logger_(logger)
|
||||
, severity_(sev)
|
||||
, sourceLocation_(loc)
|
||||
, enabled_(logger_ != nullptr && logger_->should_log(toSpdlogLevel(sev)))
|
||||
, jsonMode_(jsonMode)
|
||||
, contextParams_(contextParams)
|
||||
{
|
||||
}
|
||||
|
||||
Logger::Pump::~Pump()
|
||||
{
|
||||
using namespace std::literals;
|
||||
if (enabled_)
|
||||
{
|
||||
spdlog::source_loc const sourceLocation{
|
||||
sourceLocation_.file_name(), static_cast<int>(sourceLocation_.line()), nullptr};
|
||||
|
||||
// Apply legacy safeguards on the raw message BEFORE JSON wrapping.
|
||||
// This lets scrubSecrets short-circuit (no '"' in typical messages).
|
||||
truncateMessage(stream_);
|
||||
scrubSecrets(stream_);
|
||||
|
||||
if (jsonMode_)
|
||||
{
|
||||
// Wrap the scrubbed message: "<message>", plus optional values object
|
||||
fmt::memory_buffer wrapped;
|
||||
wrapped.push_back('"');
|
||||
wrapped.append(stream_.data(), stream_.data() + stream_.size());
|
||||
wrapped.push_back('"');
|
||||
|
||||
bool const hasContext = !contextParams_.empty();
|
||||
bool const hasMessage = !messageParams_.empty();
|
||||
if (hasContext || hasMessage)
|
||||
{
|
||||
static constexpr auto kVALUES_OPEN = ", \"values\": {"sv;
|
||||
wrapped.append(kVALUES_OPEN);
|
||||
wrapped.append(
|
||||
contextParams_.data(), contextParams_.data() + contextParams_.size());
|
||||
if (hasContext && hasMessage)
|
||||
wrapped.push_back(',');
|
||||
wrapped.append(
|
||||
messageParams_.data(), messageParams_.data() + messageParams_.size());
|
||||
wrapped.push_back('}');
|
||||
}
|
||||
|
||||
logger_->log(
|
||||
sourceLocation,
|
||||
toSpdlogLevel(severity_),
|
||||
std::string_view{wrapped.data(), wrapped.size()});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!contextParams_.empty())
|
||||
{
|
||||
// Prepend [key=val ...] context to the message
|
||||
fmt::memory_buffer buf;
|
||||
buf.push_back('[');
|
||||
buf.append(contextParams_.data(), contextParams_.data() + contextParams_.size());
|
||||
static constexpr auto kCLOSE = "] "sv;
|
||||
buf.append(kCLOSE);
|
||||
buf.append(stream_.data(), stream_.data() + stream_.size());
|
||||
logger_->log(
|
||||
sourceLocation,
|
||||
toSpdlogLevel(severity_),
|
||||
std::string_view{buf.data(), buf.size()});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger_->log(
|
||||
sourceLocation,
|
||||
toSpdlogLevel(severity_),
|
||||
std::string_view{stream_.data(), stream_.size()});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Pump
|
||||
Logger::trace(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_.get(), Severity::TRC, loc, LogServiceState::jsonMode_, contextParams_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::debug(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_.get(), Severity::DBG, loc, LogServiceState::jsonMode_, contextParams_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::info(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_.get(), Severity::NFO, loc, LogServiceState::jsonMode_, contextParams_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::warn(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_.get(), Severity::WRN, loc, LogServiceState::jsonMode_, contextParams_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::error(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_.get(), Severity::ERR, loc, LogServiceState::jsonMode_, contextParams_};
|
||||
}
|
||||
Logger::Pump
|
||||
Logger::fatal(std::source_location const& loc) const
|
||||
{
|
||||
return {logger_.get(), Severity::FTL, loc, LogServiceState::jsonMode_, contextParams_};
|
||||
}
|
||||
|
||||
Logger::Logger(std::shared_ptr<spdlog::logger> logger)
|
||||
: logger_(std::move(logger)), jsonMode_(LogServiceState::jsonMode_)
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
905
src/tests/libxrpl/basics/Logger.cpp
Normal file
905
src/tests/libxrpl/basics/Logger.cpp
Normal file
@@ -0,0 +1,905 @@
|
||||
#include <xrpl/basics/Logger.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/StructuredLogging.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <spdlog/async_logger.h>
|
||||
#include <spdlog/common.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
#include <spdlog/sinks/ostream_sink.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
class LoggerFixture : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
std::ostringstream output_;
|
||||
|
||||
// Regex fragment matching the UTC timestamp produced by spdlog
|
||||
static constexpr std::string_view kTS_RE =
|
||||
R"(\d{4}-[A-Z][a-z]{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6} UTC)";
|
||||
|
||||
void
|
||||
initLogging(bool jsonMode, Severity severity = Severity::TRC, bool isAsync = false)
|
||||
{
|
||||
LoggingConfiguration const config{
|
||||
.enableConsole = false,
|
||||
.directory = std::nullopt,
|
||||
.isAsync = isAsync,
|
||||
.defaultSeverity = severity,
|
||||
.jsonMode = jsonMode,
|
||||
};
|
||||
auto const result = LogService::init(config);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// Replace the (empty) sinks with our ostream sink using the
|
||||
// effective format chosen by LogService (text or JSON).
|
||||
auto sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(output_);
|
||||
sink->set_level(spdlog::level::trace);
|
||||
sink->set_formatter(LogServiceState::makeFormatter(LogServiceState::format()));
|
||||
LogServiceState::replaceSinks({sink});
|
||||
}
|
||||
|
||||
/// Escape special regex characters in a literal string.
|
||||
static std::string
|
||||
escapeRegex(std::string_view s)
|
||||
{
|
||||
std::string result;
|
||||
for (char const c : s)
|
||||
{
|
||||
if (std::string_view(R"(\^$.|?*+()[]{}-)").find(c) != std::string_view::npos)
|
||||
result += '\\';
|
||||
result += c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Assert that the captured output matches the expected text-mode line.
|
||||
/// Format: <timestamp> channel:sev message\n
|
||||
void
|
||||
expectText(std::string_view channel, std::string_view sev, std::string_view message)
|
||||
{
|
||||
auto const& line = output_.str();
|
||||
auto const re =
|
||||
fmt::format("{} {}:{} {}\r?\n?", kTS_RE, channel, sev, escapeRegex(message));
|
||||
EXPECT_TRUE(std::regex_match(line, std::regex(re))) << "got: " << line;
|
||||
}
|
||||
|
||||
/// Assert that the captured output matches the expected JSON-mode line.
|
||||
/// Format: {"timestamp":"...","channel":"...","severity":"...",
|
||||
/// "message": <msgPart> }\n
|
||||
void
|
||||
expectJson(std::string_view channel, std::string_view sev, std::string_view msgPart)
|
||||
{
|
||||
auto const& line = output_.str();
|
||||
auto const re = fmt::format(
|
||||
R"(\{{"timestamp":"{}","channel":"{}","severity":"{}")"
|
||||
R"(, "message": {} \}}\r?\n?)",
|
||||
kTS_RE,
|
||||
channel,
|
||||
sev,
|
||||
escapeRegex(msgPart));
|
||||
EXPECT_TRUE(std::regex_match(line, std::regex(re))) << "got: " << line;
|
||||
}
|
||||
|
||||
/// Access the current sinks vector (friend access).
|
||||
static std::vector<std::shared_ptr<spdlog::sinks::sink>> const&
|
||||
sinks()
|
||||
{
|
||||
return LogServiceState::sinks_;
|
||||
}
|
||||
|
||||
/// Register a logger with an optional severity override (friend access).
|
||||
static std::shared_ptr<spdlog::logger>
|
||||
registerLogger(std::string_view channel, std::optional<Severity> severity = std::nullopt)
|
||||
{
|
||||
return LogServiceState::registerLogger(channel, severity);
|
||||
}
|
||||
|
||||
/// Whether the LogService has any sinks (friend access).
|
||||
static bool
|
||||
hasSinks()
|
||||
{
|
||||
return LogServiceState::hasSinks();
|
||||
}
|
||||
|
||||
/// Reset the LogService (friend access).
|
||||
static void
|
||||
resetService()
|
||||
{
|
||||
LogServiceState::reset();
|
||||
}
|
||||
|
||||
/// Create a file sink via LogService::createFileSink (friend access).
|
||||
static spdlog::sink_ptr
|
||||
createFileSink(std::string const& logDir)
|
||||
{
|
||||
LogService::FileLoggingParams const params{.logDir = logDir};
|
||||
return LogService::createFileSink(params, LogServiceState::format());
|
||||
}
|
||||
|
||||
/// Install two sinks: one with NonCriticalFormatter, one critical-only.
|
||||
/// Returns {non-critical output, critical output}.
|
||||
static std::pair<std::ostringstream*, std::ostringstream*>
|
||||
installSplitSinks(std::ostringstream& ncOut, std::ostringstream& critOut)
|
||||
{
|
||||
auto ncSink = std::make_shared<spdlog::sinks::ostream_sink_mt>(ncOut);
|
||||
ncSink->set_level(spdlog::level::trace);
|
||||
ncSink->set_formatter(
|
||||
LogServiceState::makeNonCriticalFormatter(
|
||||
LogServiceState::makeFormatter(LogServiceState::format())));
|
||||
|
||||
auto critSink = std::make_shared<spdlog::sinks::ostream_sink_mt>(critOut);
|
||||
critSink->set_level(spdlog::level::critical);
|
||||
critSink->set_formatter(LogServiceState::makeFormatter(LogServiceState::format()));
|
||||
|
||||
LogServiceState::replaceSinks({ncSink, critSink});
|
||||
return {&ncOut, &critOut};
|
||||
}
|
||||
|
||||
/// Install a single sink wrapped with NonCriticalFormatter.
|
||||
static void
|
||||
installNonCriticalSink(std::ostringstream& out)
|
||||
{
|
||||
auto sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(out);
|
||||
sink->set_level(spdlog::level::trace);
|
||||
sink->set_formatter(
|
||||
LogServiceState::makeNonCriticalFormatter(
|
||||
LogServiceState::makeFormatter(LogServiceState::format())));
|
||||
LogServiceState::replaceSinks({sink});
|
||||
}
|
||||
|
||||
/// Install a single sink with a custom formatter.
|
||||
static void
|
||||
installSinkWithFormatter(std::ostringstream& out, std::unique_ptr<spdlog::formatter> fmt)
|
||||
{
|
||||
auto sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(out);
|
||||
sink->set_level(spdlog::level::trace);
|
||||
sink->set_formatter(std::move(fmt));
|
||||
LogServiceState::replaceSinks({sink});
|
||||
}
|
||||
|
||||
/// Clone a NonCriticalFormatter (friend access to makeNonCriticalFormatter).
|
||||
static std::unique_ptr<spdlog::formatter>
|
||||
cloneNonCriticalFormatter()
|
||||
{
|
||||
auto original = LogServiceState::makeNonCriticalFormatter(
|
||||
LogServiceState::makeFormatter(LogServiceState::format()));
|
||||
return original->clone();
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
spdlog::shutdown();
|
||||
if (LogService::initialized())
|
||||
LogService::reset();
|
||||
}
|
||||
};
|
||||
|
||||
// -- Plain text mode ---------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, plain_text_simple_message)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("TestChannel");
|
||||
logger.info() << "hello world";
|
||||
expectText("TestChannel", "NFO", "hello world");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, plain_text_multiple_values)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("TestChannel2");
|
||||
logger.info() << "count=" << 42 << " active=" << true;
|
||||
expectText("TestChannel2", "NFO", "count=42 active=true");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, plain_text_with_parameter)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("TestChannel3");
|
||||
logger.info() << "tx " << log::param("hash", "ABC");
|
||||
// In plain text mode, parameter value is just streamed
|
||||
expectText("TestChannel3", "NFO", "tx ABC");
|
||||
}
|
||||
|
||||
// -- JSON mode ---------------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_simple_message)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("JsonChannel");
|
||||
logger.info() << "hello world";
|
||||
expectJson("JsonChannel", "NFO", "\"hello world\"");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_with_parameters)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("JsonChannel2");
|
||||
logger.info() << "processing " << log::param("tx_hash", std::string("ABC123"))
|
||||
<< " amount=" << log::param("amount", 42);
|
||||
expectJson(
|
||||
"JsonChannel2",
|
||||
"NFO",
|
||||
"\"processing ABC123 amount=42\", "
|
||||
"\"values\": {\"tx_hash\":\"ABC123\",\"amount\":42}");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_no_parameters)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("JsonChannel3");
|
||||
logger.info() << "simple message";
|
||||
expectJson("JsonChannel3", "NFO", "\"simple message\"");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_bool_parameter)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("JsonChannel4");
|
||||
logger.info() << "status " << log::param("active", true);
|
||||
expectJson("JsonChannel4", "NFO", "\"status true\", \"values\": {\"active\":true}");
|
||||
}
|
||||
|
||||
// -- Severity filtering ------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, severity_filtering)
|
||||
{
|
||||
initLogging(false, Severity::WRN);
|
||||
|
||||
Logger const logger("FilterChannel");
|
||||
logger.info() << "should not appear";
|
||||
EXPECT_TRUE(output_.str().empty());
|
||||
|
||||
logger.warn() << "should appear";
|
||||
expectText("FilterChannel", "WRN", "should appear");
|
||||
}
|
||||
|
||||
// -- xrpl::to_string integration --------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, text_mode_xrp_amount)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("AmountChannel");
|
||||
logger.info() << "balance: " << XRPAmount{1000};
|
||||
expectText("AmountChannel", "NFO", "balance: 1000");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_xrp_amount)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("AmountChannel");
|
||||
logger.info() << "balance " << XRPAmount{500};
|
||||
expectJson("AmountChannel", "NFO", "\"balance 500\"");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_xrp_amount_parameter)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("AmountChannel");
|
||||
logger.info() << "tx" << log::param("fee", XRPAmount{10});
|
||||
expectJson("AmountChannel", "NFO", "\"tx10\", \"values\": {\"fee\":\"10\"}");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, text_mode_number)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("NumberChannel");
|
||||
logger.info() << "result: " << Number{42};
|
||||
expectText("NumberChannel", "NFO", "result: 42");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_number)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("NumberChannel");
|
||||
logger.info() << "value " << Number{25, -3};
|
||||
expectJson("NumberChannel", "NFO", "\"value 0.025\"");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, json_mode_number_parameter)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("NumberChannel");
|
||||
logger.info() << "calc" << log::param("rate", Number{100});
|
||||
expectJson("NumberChannel", "NFO", "\"calc100\", \"values\": {\"rate\":\"100\"}");
|
||||
}
|
||||
|
||||
// -- Severity codes -----------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, severity_codes_in_default_format)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("Test");
|
||||
|
||||
logger.trace() << "t";
|
||||
logger.debug() << "d";
|
||||
logger.info() << "i";
|
||||
logger.warn() << "w";
|
||||
logger.error() << "e";
|
||||
logger.fatal() << "f";
|
||||
|
||||
// Each line has the full default format; build a regex for all six.
|
||||
auto const line = [&](std::string_view sev, std::string_view msg) {
|
||||
return fmt::format("{} Test:{} {}\r?\n?", kTS_RE, sev, msg);
|
||||
};
|
||||
std::string re;
|
||||
re += line("TRC", "t");
|
||||
re += line("DBG", "d");
|
||||
re += line("NFO", "i");
|
||||
re += line("WRN", "w");
|
||||
re += line("ERR", "e");
|
||||
re += line("FTL", "f");
|
||||
EXPECT_TRUE(std::regex_match(output_.str(), std::regex(re))) << "got: " << output_.str();
|
||||
}
|
||||
|
||||
// -- NonCriticalFormatter ------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, non_critical_formatter_passes_non_critical)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
std::ostringstream ncOutput;
|
||||
installNonCriticalSink(ncOutput);
|
||||
|
||||
Logger const logger("NCTest");
|
||||
logger.info() << "hello";
|
||||
|
||||
// Non-critical message should appear
|
||||
EXPECT_FALSE(ncOutput.str().empty()) << "Non-critical message was suppressed";
|
||||
EXPECT_NE(ncOutput.str().find("hello"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, non_critical_formatter_suppresses_critical)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
std::ostringstream ncOutput;
|
||||
installNonCriticalSink(ncOutput);
|
||||
|
||||
Logger const logger("NCTest2");
|
||||
logger.fatal() << "critical message";
|
||||
|
||||
// Critical (fatal) message should be suppressed
|
||||
EXPECT_TRUE(ncOutput.str().empty())
|
||||
<< "Critical message was not suppressed: " << ncOutput.str();
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, non_critical_formatter_splits_output)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
std::ostringstream stdoutOutput;
|
||||
std::ostringstream stderrOutput;
|
||||
installSplitSinks(stdoutOutput, stderrOutput);
|
||||
|
||||
Logger const logger("SplitTest");
|
||||
logger.info() << "normal message";
|
||||
logger.fatal() << "fatal message";
|
||||
|
||||
// stdout sink: should have the info message but NOT the fatal message
|
||||
EXPECT_NE(stdoutOutput.str().find("normal message"), std::string::npos)
|
||||
<< "stdout: " << stdoutOutput.str();
|
||||
EXPECT_EQ(stdoutOutput.str().find("fatal message"), std::string::npos)
|
||||
<< "stdout should not have fatal: " << stdoutOutput.str();
|
||||
|
||||
// stderr sink: should have the fatal message but NOT the info message
|
||||
EXPECT_NE(stderrOutput.str().find("fatal message"), std::string::npos)
|
||||
<< "stderr: " << stderrOutput.str();
|
||||
EXPECT_EQ(stderrOutput.str().find("normal message"), std::string::npos)
|
||||
<< "stderr should not have info: " << stderrOutput.str();
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, non_critical_formatter_all_non_critical_levels)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
std::ostringstream ncOutput;
|
||||
installNonCriticalSink(ncOutput);
|
||||
|
||||
Logger const logger("LevelTest");
|
||||
logger.trace() << "t";
|
||||
logger.debug() << "d";
|
||||
logger.info() << "i";
|
||||
logger.warn() << "w";
|
||||
logger.error() << "e";
|
||||
|
||||
// All five non-critical levels should appear
|
||||
auto const& out = ncOutput.str();
|
||||
EXPECT_NE(out.find("TRC"), std::string::npos) << out;
|
||||
EXPECT_NE(out.find("DBG"), std::string::npos) << out;
|
||||
EXPECT_NE(out.find("NFO"), std::string::npos) << out;
|
||||
EXPECT_NE(out.find("WRN"), std::string::npos) << out;
|
||||
EXPECT_NE(out.find("ERR"), std::string::npos) << out;
|
||||
|
||||
// Now log a fatal and verify it does NOT appear
|
||||
logger.fatal() << "f";
|
||||
EXPECT_EQ(out.find("FTL"), std::string::npos)
|
||||
<< "FTL should not appear in NonCriticalFormatter output: " << out;
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, non_critical_formatter_clone_succeeds)
|
||||
{
|
||||
initLogging(false);
|
||||
auto cloned = cloneNonCriticalFormatter();
|
||||
EXPECT_NE(cloned, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, console_enabled_creates_stdout_and_stderr_sinks)
|
||||
{
|
||||
// With enableConsole = true, init should create both a stdout sink
|
||||
// (wrapped in NonCriticalFormatter) and a stderr sink (critical only).
|
||||
LoggingConfiguration const config{
|
||||
.enableConsole = true,
|
||||
.directory = std::nullopt,
|
||||
.isAsync = false,
|
||||
.defaultSeverity = Severity::TRC,
|
||||
.jsonMode = false,
|
||||
};
|
||||
auto const result = LogService::init(config);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// enableConsole=true → stdout sink + stderr sink = 2 sinks
|
||||
EXPECT_EQ(sinks().size(), 2u);
|
||||
|
||||
// First sink should be a stdout_color_sink_mt
|
||||
EXPECT_NE(dynamic_cast<spdlog::sinks::stdout_color_sink_mt*>(sinks()[0].get()), nullptr)
|
||||
<< "First sink should be stdout_color_sink_mt";
|
||||
|
||||
// Second sink should be a stderr_color_sink_mt
|
||||
EXPECT_NE(dynamic_cast<spdlog::sinks::stderr_color_sink_mt*>(sinks()[1].get()), nullptr)
|
||||
<< "Second sink should be stderr_color_sink_mt";
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, console_disabled_creates_only_stderr_sink)
|
||||
{
|
||||
// With enableConsole = false, init should only create the stderr sink.
|
||||
LoggingConfiguration const config{
|
||||
.enableConsole = false,
|
||||
.directory = std::nullopt,
|
||||
.isAsync = false,
|
||||
.defaultSeverity = Severity::TRC,
|
||||
.jsonMode = false,
|
||||
};
|
||||
auto const result = LogService::init(config);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// enableConsole=false → stderr sink only = 1 sink
|
||||
EXPECT_EQ(sinks().size(), 1u);
|
||||
|
||||
EXPECT_NE(dynamic_cast<spdlog::sinks::stderr_color_sink_mt*>(sinks()[0].get()), nullptr)
|
||||
<< "Only sink should be stderr_color_sink_mt";
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, create_file_sink_returns_correct_type_and_path)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
// Use the system temp directory (already exists) so no directory
|
||||
// creation or cleanup is needed.
|
||||
auto const dir = std::filesystem::temp_directory_path();
|
||||
auto sink = createFileSink(dir.string());
|
||||
|
||||
// Should be a basic_file_sink_mt
|
||||
auto* fileSink = dynamic_cast<spdlog::sinks::basic_file_sink_mt*>(sink.get());
|
||||
ASSERT_NE(fileSink, nullptr) << "createFileSink should return a basic_file_sink_mt";
|
||||
|
||||
// The filename should point to <dir>/xrpld.log
|
||||
auto const expectedPath = (dir / "xrpld.log").string();
|
||||
EXPECT_EQ(fileSink->filename(), expectedPath);
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, init_fails_for_invalid_directory)
|
||||
{
|
||||
// __FILE__ is a regular file on every platform, so creating a
|
||||
// subdirectory under it always fails.
|
||||
auto const badDir = std::filesystem::path(__FILE__) / "impossible_log_dir";
|
||||
LoggingConfiguration const config{
|
||||
.enableConsole = false,
|
||||
.directory = badDir.string(),
|
||||
.isAsync = false,
|
||||
.defaultSeverity = Severity::TRC,
|
||||
.jsonMode = false,
|
||||
};
|
||||
auto const result = LogService::init(config);
|
||||
EXPECT_FALSE(result) << "init should fail for an invalid directory";
|
||||
}
|
||||
|
||||
// -- Async logging ------------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, async_mode_creates_async_logger)
|
||||
{
|
||||
initLogging(false, Severity::TRC, true);
|
||||
|
||||
Logger const logger("AsyncTest");
|
||||
auto const spdlogger = spdlog::get("AsyncTest");
|
||||
ASSERT_NE(spdlogger, nullptr);
|
||||
EXPECT_NE(dynamic_cast<spdlog::async_logger*>(spdlogger.get()), nullptr)
|
||||
<< "Logger should be async_logger when isAsync=true";
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, sync_mode_creates_sync_logger)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
Logger const logger("SyncTest");
|
||||
auto const spdlogger = spdlog::get("SyncTest");
|
||||
ASSERT_NE(spdlogger, nullptr);
|
||||
EXPECT_EQ(dynamic_cast<spdlog::async_logger*>(spdlogger.get()), nullptr)
|
||||
<< "Logger should be a sync logger when isAsync=false";
|
||||
}
|
||||
|
||||
// -- Double initialisation guard ----------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, double_init_throws)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
// Second init should throw because LogServiceState is already initialized
|
||||
LoggingConfiguration const config{};
|
||||
EXPECT_THROW(auto _ = LogService::init(config), std::logic_error);
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, reset_before_init_throws)
|
||||
{
|
||||
// reset() without prior init should throw
|
||||
EXPECT_THROW(resetService(), std::logic_error);
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, has_sinks_after_init)
|
||||
{
|
||||
EXPECT_FALSE(hasSinks()) << "No sinks before init";
|
||||
|
||||
initLogging(false);
|
||||
EXPECT_TRUE(hasSinks()) << "Should have sinks after init";
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, has_no_sinks_after_reset)
|
||||
{
|
||||
initLogging(false);
|
||||
EXPECT_TRUE(hasSinks());
|
||||
|
||||
resetService();
|
||||
EXPECT_FALSE(hasSinks()) << "Sinks should be cleared after reset";
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, register_logger_before_init_throws)
|
||||
{
|
||||
// registerLogger without prior init should throw
|
||||
EXPECT_THROW(registerLogger("Uninitialized"), std::logic_error);
|
||||
}
|
||||
|
||||
// -- registerLogger re-registration -------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, register_existing_logger_returns_same_instance)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
// First registration creates the logger
|
||||
auto const first = registerLogger("DuplicateChannel");
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
// Second registration should return the same logger instance
|
||||
auto const second = registerLogger("DuplicateChannel");
|
||||
EXPECT_EQ(first.get(), second.get());
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, register_existing_logger_updates_severity)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
// Create logger at default severity (TRC → spdlog::level::trace)
|
||||
auto const logger = registerLogger("SevChannel");
|
||||
ASSERT_NE(logger, nullptr);
|
||||
EXPECT_EQ(logger->level(), spdlog::level::trace);
|
||||
|
||||
// Re-register with a severity override
|
||||
auto const same = registerLogger("SevChannel", Severity::ERR);
|
||||
EXPECT_EQ(same.get(), logger.get()) << "Should return the same instance";
|
||||
EXPECT_EQ(logger->level(), spdlog::level::err) << "Level should be updated to ERR";
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, register_existing_logger_without_severity_keeps_level)
|
||||
{
|
||||
initLogging(false);
|
||||
|
||||
// Create logger with an explicit severity
|
||||
auto const logger = registerLogger("KeepChannel", Severity::WRN);
|
||||
ASSERT_NE(logger, nullptr);
|
||||
EXPECT_EQ(logger->level(), spdlog::level::warn);
|
||||
|
||||
// Re-register without severity — level should remain unchanged
|
||||
auto const same = registerLogger("KeepChannel");
|
||||
EXPECT_EQ(same.get(), logger.get());
|
||||
EXPECT_EQ(logger->level(), spdlog::level::warn) << "Level should remain WRN";
|
||||
}
|
||||
|
||||
// -- LogService static convenience methods ------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, log_service_trace)
|
||||
{
|
||||
initLogging(false);
|
||||
LogService::trace() << "trace msg";
|
||||
expectText("General", "TRC", "trace msg");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, log_service_debug)
|
||||
{
|
||||
initLogging(false);
|
||||
LogService::debug() << "debug msg";
|
||||
expectText("General", "DBG", "debug msg");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, log_service_info)
|
||||
{
|
||||
initLogging(false);
|
||||
// init already logs "Default log level = TRC" on the General channel,
|
||||
// so clear and re-test.
|
||||
output_.str({});
|
||||
LogService::info() << "info msg";
|
||||
expectText("General", "NFO", "info msg");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, log_service_warn)
|
||||
{
|
||||
initLogging(false);
|
||||
output_.str({});
|
||||
LogService::warn() << "warn msg";
|
||||
expectText("General", "WRN", "warn msg");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, log_service_error)
|
||||
{
|
||||
initLogging(false);
|
||||
output_.str({});
|
||||
LogService::error() << "error msg";
|
||||
expectText("General", "ERR", "error msg");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, log_service_fatal)
|
||||
{
|
||||
initLogging(false);
|
||||
output_.str({});
|
||||
LogService::fatal() << "fatal msg";
|
||||
expectText("General", "FTL", "fatal msg");
|
||||
}
|
||||
|
||||
// -- LogService::rotate -------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, rotate_returns_message_when_no_file_logging)
|
||||
{
|
||||
initLogging(false);
|
||||
auto const msg = LogService::rotate();
|
||||
EXPECT_EQ(msg, "Log file rotation is not possible because file logging is not configured.");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, rotate_replaces_file_sink)
|
||||
{
|
||||
auto const tmpDir = std::filesystem::temp_directory_path();
|
||||
|
||||
LoggingConfiguration const config{
|
||||
.enableConsole = false,
|
||||
.directory = tmpDir.string(),
|
||||
.isAsync = false,
|
||||
.defaultSeverity = Severity::TRC,
|
||||
.jsonMode = false,
|
||||
};
|
||||
auto const result = LogService::init(config);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
auto const msg = LogService::rotate();
|
||||
EXPECT_EQ(msg, "The log file was closed and reopened.");
|
||||
|
||||
// The file sink should still be a basic_file_sink_mt after rotation
|
||||
bool hasFileSink = false;
|
||||
for (auto const& s : sinks())
|
||||
{
|
||||
if (dynamic_cast<spdlog::sinks::basic_file_sink_mt*>(s.get()) != nullptr)
|
||||
{
|
||||
hasFileSink = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(hasFileSink) << "File sink should still exist after rotation";
|
||||
}
|
||||
|
||||
// -- LogService::shutdown -----------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, shutdown_sync_mode_is_noop)
|
||||
{
|
||||
initLogging(false);
|
||||
// shutdown() should not throw or crash in sync mode
|
||||
LogService::shutdown();
|
||||
// We can still log after shutdown in sync mode
|
||||
Logger const logger("ShutdownSync");
|
||||
logger.info() << "still works";
|
||||
EXPECT_NE(output_.str().find("still works"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, shutdown_async_mode_flushes)
|
||||
{
|
||||
initLogging(false, Severity::TRC, true);
|
||||
|
||||
Logger const logger("ShutdownAsync");
|
||||
logger.info() << "before shutdown";
|
||||
|
||||
LogService::shutdown();
|
||||
|
||||
EXPECT_NE(output_.str().find("before shutdown"), std::string::npos)
|
||||
<< "Message should be flushed by shutdown: " << output_.str();
|
||||
}
|
||||
|
||||
// -- Pattern builder function -------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, build_json_pattern_from_scratch)
|
||||
{
|
||||
auto pattern = buildJsonPattern(
|
||||
"",
|
||||
log::param("channel", std::string_view("%n")),
|
||||
log::param("level", std::string_view("%l")));
|
||||
EXPECT_EQ(pattern, R"({"channel":"%n","level":"%l", "message": %v })");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, build_json_pattern_extends_existing)
|
||||
{
|
||||
auto const base = buildJsonPattern("", log::param("channel", std::string_view("%n")));
|
||||
auto const extended = buildJsonPattern(base, log::param("trace_id", std::string("abc123")));
|
||||
EXPECT_EQ(extended, R"({"channel":"%n","trace_id":"abc123", "message": %v })");
|
||||
}
|
||||
|
||||
// -- Logger context inheritance -----------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, child_logger_inherits_context_params)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const parent("Parent");
|
||||
Logger const child(parent, "Child", log::param("peer_id", std::string("abc")));
|
||||
child.info() << "hello";
|
||||
expectJson("Child", "NFO", "\"hello\", \"values\": {\"peer_id\":\"abc\"}");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, child_logger_merges_context_and_message_params)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const parent("Parent");
|
||||
Logger const child(parent, "Child", log::param("peer_id", std::string("abc")));
|
||||
child.info() << "event" << log::param("count", 42);
|
||||
expectJson("Child", "NFO", "\"event42\", \"values\": {\"peer_id\":\"abc\",\"count\":42}");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, grandchild_logger_accumulates_context)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const root("Root");
|
||||
Logger const child(root, "Child", log::param("trace_id", std::string("t1")));
|
||||
Logger const grandchild(child, "Grandchild", log::param("span_id", std::string("s1")));
|
||||
grandchild.info() << "deep";
|
||||
expectJson(
|
||||
"Grandchild", "NFO", "\"deep\", \"values\": {\"trace_id\":\"t1\",\"span_id\":\"s1\"}");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, context_params_in_text_mode)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const parent("Parent");
|
||||
Logger const child(parent, "Child", log::param("peer_id", std::string("abc")));
|
||||
child.info() << "hello";
|
||||
// In text mode, context params are shown as [key=val] prefix
|
||||
expectText("Child", "NFO", "[peer_id=abc] hello");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, context_params_text_mode_multiple)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const root("Root");
|
||||
Logger const child(root, "Child", log::param("trace_id", std::string("t1")));
|
||||
Logger const grandchild(child, "GC", log::param("span_id", std::string("s1")));
|
||||
grandchild.info() << "deep";
|
||||
expectText("GC", "NFO", "[trace_id=t1 span_id=s1] deep");
|
||||
}
|
||||
|
||||
// -- Secret scrubbing ---------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_seed)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("Scrub");
|
||||
logger.info() << R"({"seed":"sEdTM1uX8pu2do5XvTnutH6HsouMaM2"})";
|
||||
// 31 chars in the seed value → 31 asterisks
|
||||
expectText("Scrub", "NFO", R"({"seed":"*******************************"})");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_master_key)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("Scrub2");
|
||||
logger.info() << R"({"master_key":"SOME_SECRET_VALUE"})";
|
||||
expectText("Scrub2", "NFO", R"({"master_key":"*****************"})");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_passphrase)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("Scrub3");
|
||||
logger.info() << R"({"passphrase":"my_secret_pass"})";
|
||||
expectText("Scrub3", "NFO", R"({"passphrase":"**************"})");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_seed_json_mode)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("ScrubJson");
|
||||
logger.info() << R"({"seed":"sEdTM1uX8pu2do5XvTnutH6HsouMaM2"})";
|
||||
// In JSON mode the message is wrapped in quotes, but scrubbing still works
|
||||
expectJson("ScrubJson", "NFO", "\"{\"seed\":\"*******************************\"}\"");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_master_key_json_mode)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("ScrubJson2");
|
||||
logger.info() << R"({"master_key":"SOME_SECRET_VALUE"})";
|
||||
expectJson("ScrubJson2", "NFO", "\"{\"master_key\":\"*****************\"}\"");
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, scrubs_seed_without_closing_quote)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const logger("ScrubNoClose");
|
||||
// The value has an opening quote after "seed": but no closing quote.
|
||||
// The scrubber should treat everything to end-of-string as the secret.
|
||||
logger.info() << R"({"seed":"sEdTM1uX8pu2do5XvTnutH)";
|
||||
// 22 chars in "sEdTM1uX8pu2do5XvTnutH" → 22 asterisks
|
||||
expectText("ScrubNoClose", "NFO", R"({"seed":"**********************)");
|
||||
}
|
||||
|
||||
// -- Message truncation -------------------------------------------------------
|
||||
|
||||
TEST_F(LoggerFixture, truncates_oversized_message)
|
||||
{
|
||||
initLogging(false);
|
||||
Logger const 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;
|
||||
|
||||
// The message body is truncated; full line includes timestamp prefix.
|
||||
std::string truncatedMsg(kMAX - 3, 'x');
|
||||
truncatedMsg += "...";
|
||||
expectText("Trunc", "NFO", truncatedMsg);
|
||||
}
|
||||
|
||||
TEST_F(LoggerFixture, truncates_oversized_message_json)
|
||||
{
|
||||
initLogging(true);
|
||||
Logger const logger("Trunc");
|
||||
static constexpr std::size_t kMAX = 12 * 1024;
|
||||
std::string const bigMessage(13000, 'x');
|
||||
logger.info() << bigMessage << log::param("key", std::string("value"));
|
||||
|
||||
// Message body is truncated; values object is preserved in full.
|
||||
std::string msgPart = "\"";
|
||||
msgPart += std::string(kMAX - 3, 'x');
|
||||
msgPart += "...\", \"values\": {\"key\":\"value\"}";
|
||||
expectJson("Trunc", "NFO", msgPart);
|
||||
}
|
||||
270
src/tests/libxrpl/basics/StructuredLogging.cpp
Normal file
270
src/tests/libxrpl/basics/StructuredLogging.cpp
Normal file
@@ -0,0 +1,270 @@
|
||||
#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 const 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");
|
||||
}
|
||||
|
||||
// -- detail::appendEscapedJsonString ------------------------------------------
|
||||
|
||||
TEST(AppendEscapedJsonString, plain_string)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendEscapedJsonString(dest, "hello");
|
||||
EXPECT_EQ(dest, "\"hello\"");
|
||||
}
|
||||
|
||||
TEST(AppendEscapedJsonString, escapes_double_quote)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendEscapedJsonString(dest, R"(say "hi")");
|
||||
auto const expected = R"("say \"hi\"")";
|
||||
EXPECT_EQ(dest, expected);
|
||||
}
|
||||
|
||||
TEST(AppendEscapedJsonString, escapes_backslash)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendEscapedJsonString(dest, R"(a\b)");
|
||||
EXPECT_EQ(dest, R"("a\\b")");
|
||||
}
|
||||
|
||||
TEST(AppendEscapedJsonString, escapes_control_characters)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendEscapedJsonString(dest, "line1\nline2\ttab");
|
||||
EXPECT_EQ(dest, R"("line1\nline2\ttab")");
|
||||
}
|
||||
|
||||
TEST(AppendEscapedJsonString, escapes_all_named_controls)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendEscapedJsonString(dest, "\b\f\r");
|
||||
EXPECT_EQ(dest, R"("\b\f\r")");
|
||||
}
|
||||
|
||||
TEST(AppendEscapedJsonString, escapes_low_control_char_as_unicode)
|
||||
{
|
||||
std::string dest;
|
||||
std::string const input(1, '\x01');
|
||||
detail::appendEscapedJsonString(dest, input);
|
||||
EXPECT_EQ(dest, R"("\u0001")");
|
||||
}
|
||||
|
||||
TEST(AppendEscapedJsonString, empty_string)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendEscapedJsonString(dest, "");
|
||||
EXPECT_EQ(dest, R"("")");
|
||||
}
|
||||
|
||||
// -- appendJsonValue with escaping -------------------------------------------
|
||||
|
||||
TEST(AppendJsonValue, string_with_quotes_escaped)
|
||||
{
|
||||
std::string dest;
|
||||
std::string const val = R"(say "hi")";
|
||||
detail::appendJsonValue(dest, val);
|
||||
auto const expected = R"("say \"hi\"")";
|
||||
EXPECT_EQ(dest, expected);
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, string_with_backslash_escaped)
|
||||
{
|
||||
std::string dest;
|
||||
std::string const val = R"(path\to\file)";
|
||||
detail::appendJsonValue(dest, val);
|
||||
EXPECT_EQ(dest, R"("path\\to\\file")");
|
||||
}
|
||||
|
||||
TEST(AppendJsonValue, string_with_newline_escaped)
|
||||
{
|
||||
std::string dest;
|
||||
std::string const val = "line1\nline2";
|
||||
detail::appendJsonValue(dest, val);
|
||||
EXPECT_EQ(dest, R"("line1\nline2")");
|
||||
}
|
||||
|
||||
// -- appendJsonField with key escaping ----------------------------------------
|
||||
|
||||
TEST(AppendJsonField, key_with_quotes_escaped)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonField(dest, R"(my"key)", 42);
|
||||
auto const expected = R"("my\"key":42)";
|
||||
EXPECT_EQ(dest, expected);
|
||||
}
|
||||
|
||||
TEST(AppendJsonField, key_with_backslash_escaped)
|
||||
{
|
||||
std::string dest;
|
||||
detail::appendJsonField(dest, R"(a\b)", true);
|
||||
EXPECT_EQ(dest, R"("a\\b":true)");
|
||||
}
|
||||
|
||||
// -- buildJsonPattern --------------------------------------------------------
|
||||
|
||||
TEST(BuildJsonPattern, no_params)
|
||||
{
|
||||
auto const pattern = buildJsonPattern("");
|
||||
EXPECT_EQ(pattern, "{\"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(BuildJsonPattern, single_string_field)
|
||||
{
|
||||
auto const pattern = buildJsonPattern("", log::param("level", std::string_view("%l")));
|
||||
EXPECT_EQ(pattern, "{\"level\":\"%l\", \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(BuildJsonPattern, multiple_string_fields)
|
||||
{
|
||||
auto const pattern = buildJsonPattern(
|
||||
"",
|
||||
log::param("level", std::string_view("%l")),
|
||||
log::param("channel", std::string_view("%n")));
|
||||
EXPECT_EQ(pattern, "{\"level\":\"%l\",\"channel\":\"%n\", \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(BuildJsonPattern, typed_fields)
|
||||
{
|
||||
auto const pattern = buildJsonPattern("", log::param("enabled", true), log::param("count", 5));
|
||||
EXPECT_EQ(pattern, "{\"enabled\":true,\"count\":5, \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(BuildJsonPattern, many_fields)
|
||||
{
|
||||
auto const pattern = buildJsonPattern(
|
||||
"",
|
||||
log::param("a", std::string_view("1")),
|
||||
log::param("b", std::string_view("2")),
|
||||
log::param("c", std::string_view("3")));
|
||||
EXPECT_EQ(pattern, "{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\", \"message\": %v }");
|
||||
}
|
||||
|
||||
TEST(BuildJsonPattern, extends_existing_pattern)
|
||||
{
|
||||
auto const base = buildJsonPattern(
|
||||
"",
|
||||
log::param("level", std::string_view("%l")),
|
||||
log::param("channel", std::string_view("%n")));
|
||||
|
||||
auto const extended = buildJsonPattern(base, log::param("source", std::string_view("%s:%#")));
|
||||
EXPECT_EQ(
|
||||
extended, "{\"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\"");
|
||||
}
|
||||
@@ -432,13 +432,13 @@ TEST(AccountSet, TransferRate)
|
||||
|
||||
// Test data: {rate to set, expected TER, expected stored rate}
|
||||
std::vector<TestCase> const testData = {
|
||||
{1.0, tesSUCCESS, 1.0},
|
||||
{1.1, tesSUCCESS, 1.1},
|
||||
{2.0, tesSUCCESS, 2.0},
|
||||
{2.1, temBAD_TRANSFER_RATE, 2.0}, // > 2.0 is invalid
|
||||
{0.0, tesSUCCESS, 1.0}, // 0 clears the rate (default = 1.0)
|
||||
{2.0, tesSUCCESS, 2.0},
|
||||
{0.9, temBAD_TRANSFER_RATE, 2.0}, // < 1.0 is invalid
|
||||
{.set = 1.0, .code = tesSUCCESS, .get = 1.0},
|
||||
{.set = 1.1, .code = tesSUCCESS, .get = 1.1},
|
||||
{.set = 2.0, .code = tesSUCCESS, .get = 2.0},
|
||||
{.set = 2.1, .code = temBAD_TRANSFER_RATE, .get = 2.0}, // > 2.0 is invalid
|
||||
{.set = 0.0, .code = tesSUCCESS, .get = 1.0}, // 0 clears the rate (default = 1.0)
|
||||
{.set = 2.0, .code = tesSUCCESS, .get = 2.0},
|
||||
{.set = 0.9, .code = temBAD_TRANSFER_RATE, .get = 2.0}, // < 1.0 is invalid
|
||||
};
|
||||
|
||||
TxTest env;
|
||||
|
||||
Reference in New Issue
Block a user