#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { bool LogServiceState::isAsync_{true}; Severity LogServiceState::defaultSeverity_{Severity::NFO}; std::vector LogServiceState::sinks_{}; bool LogServiceState::initialized_{false}; bool LogServiceState::jsonMode_{false}; std::optional 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 kLABELS = { "TRC", "DBG", "NFO", "WRN", "ERR", "FTL", }; return kLABELS.at(static_cast(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 wrappedFormatter) : wrappedFormatter_(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) { wrappedFormatter_->format(msg, dest); } } [[nodiscard]] std::unique_ptr clone() const override { return std::make_unique(wrappedFormatter_->clone()); } private: std::unique_ptr wrappedFormatter_; }; /** * @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 kLABELS = { "TRC", "DBG", "NFO", "WRN", "ERR", "FTL", "OFF", }; auto const idx = static_cast(msg.level); auto const label = idx < kLABELS.size() ? kLABELS[idx] : "???"; dest.append(label.data(), label.data() + label.size()); } [[nodiscard]] std::unique_ptr clone() const override { return std::make_unique(); } }; /** * @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 createPatternFormatter(std::string const& pattern) { auto formatter = std::make_unique( pattern, spdlog::pattern_time_type::utc, spdlog::details::os::default_eol); formatter->add_flag('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 kMaxMessageChars = 12 * 1024; if (message.size() > kMaxMessageChars) { message.resize(kMaxMessageChars - 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.contains('"')) 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 createConsoleSinks(bool logToConsole, std::string const& format) { std::vector sinks; if (logToConsole) { auto consoleSink = std::make_shared(); consoleSink->set_level(spdlog::level::trace); consoleSink->set_formatter( std::make_unique(createPatternFormatter(format))); sinks.push_back(std::move(consoleSink)); } // Always add stderr sink for fatal logs auto stderrSink = std::make_shared(); 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 { auto const logPath = (dirPath / "xrpld.log").string(); return std::make_shared(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 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 logger) { logger->set_level(toSpdlogLevel(defaultSeverity_)); }); if (isAsync) { static constexpr size_t kQueueSize = 8192; static constexpr size_t kThreadCount = 1; spdlog::init_thread_pool(kQueueSize, kThreadCount); } } 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 LogServiceState::registerLogger(std::string_view channel, std::optional severity) { if (!initialized_) { throw std::logic_error("LogService is not initialized"); } std::string const channelStr{channel}; std::shared_ptr existingLogger = spdlog::get(channelStr); if (existingLogger != nullptr) { if (severity.has_value()) existingLogger->set_level(toSpdlogLevel(*severity)); return existingLogger; } std::shared_ptr logger; if (isAsync_) { logger = std::make_shared( channelStr, sinks_.begin(), sinks_.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block); } else { logger = std::make_shared(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; } std::expected, std::string> LogService::getSinks(LoggingConfiguration const& config, std::string const& format) { std::vector 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 std::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; } std::expected LogService::init(LoggingConfiguration const& config) { // Format is fully determined by the logging mode. format_ = config.jsonMode ? defaultJsonLogFormat() : std::string(kDefaultLogFormat); auto const sinksMaybe = getSinks(config, format_); if (!sinksMaybe.has_value()) { return std::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(sink.get()) != nullptr) { sink = newFileSink; break; } } // Update all registered loggers with the new sinks spdlog::apply_all([](std::shared_ptr 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> const& sinks) { sinks_ = sinks; spdlog::apply_all([](std::shared_ptr logger) { logger->sinks() = sinks_; }); } std::unique_ptr LogServiceState::makeFormatter(std::string const& pattern) { return createPatternFormatter(pattern); } std::unique_ptr LogServiceState::makeNonCriticalFormatter(std::unique_ptr wrappedFormatter) { return std::make_unique(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 kLastLoggerRefCount = 2; if (logger_ == nullptr) { return; // LCOV_EXCL_LINE } if (logger_.use_count() == kLastLoggerRefCount) { 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(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: "", 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 kValuesOpen = ", \"values\": {"sv; wrapped.append(kValuesOpen); 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 logger) : logger_(std::move(logger)), jsonMode_(LogServiceState::jsonMode_) { } } // namespace xrpl