Files
rippled/src/libxrpl/basics/Log.cpp
Pratik Mankawde b6da558f00 Fix codecov: exclude telemetry ifdef block in Log.cpp from coverage
The trace context injection block is compiled out when
XRPL_ENABLE_TELEMETRY is not defined (coverage builds). codecov still
counts preprocessor-excluded lines as uncovered in the source diff.
Wrap with LCOV_EXCL_START/STOP.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:12:28 +00:00

490 lines
11 KiB
C++

#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem/path.hpp>
// Phase 8: OTel trace context headers for log-trace correlation.
// GetSpan() and RuntimeContext::GetCurrent() are thread-local reads
// with no locking — measured at <10ns per call.
#ifdef XRPL_ENABLE_TELEMETRY
#include <opentelemetry/context/runtime_context.h>
#include <opentelemetry/trace/context.h>
#include <opentelemetry/trace/provider.h>
#endif // XRPL_ENABLE_TELEMETRY
#include <chrono>
#include <cstring>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
namespace xrpl {
Logs::Sink::Sink(std::string const& partition, beast::severities::Severity thresh, Logs& logs)
: beast::Journal::Sink(thresh, false), logs_(logs), partition_(partition)
{
}
void
Logs::Sink::write(beast::severities::Severity level, std::string const& text)
{
if (level < threshold())
return;
logs_.write(level, partition_, text, console());
}
void
Logs::Sink::writeAlways(beast::severities::Severity level, std::string const& text)
{
logs_.write(level, partition_, text, console());
}
//------------------------------------------------------------------------------
Logs::File::File() : m_stream(nullptr)
{
}
bool
Logs::File::isOpen() const noexcept
{
return m_stream != nullptr;
}
bool
Logs::File::open(boost::filesystem::path const& path)
{
close();
bool wasOpened = false;
// VFALCO TODO Make this work with Unicode file paths
std::unique_ptr<std::ofstream> stream(new std::ofstream(path.c_str(), std::fstream::app));
if (stream->good())
{
m_path = path;
m_stream = std::move(stream);
wasOpened = true;
}
return wasOpened;
}
bool
Logs::File::closeAndReopen()
{
close();
return open(m_path);
}
void
Logs::File::close()
{
m_stream = nullptr;
}
void
Logs::File::write(char const* text)
{
if (m_stream != nullptr)
(*m_stream) << text;
}
void
Logs::File::writeln(char const* text)
{
if (m_stream != nullptr)
{
(*m_stream) << text;
(*m_stream) << std::endl;
}
}
//------------------------------------------------------------------------------
Logs::Logs(beast::severities::Severity thresh) : thresh_(thresh) // default severity
{
}
bool
Logs::open(boost::filesystem::path const& pathToLogFile)
{
return file_.open(pathToLogFile);
}
beast::Journal::Sink&
Logs::get(std::string const& name)
{
std::lock_guard lock(mutex_);
auto const result = sinks_.emplace(name, makeSink(name, thresh_));
return *result.first->second;
}
beast::Journal::Sink&
Logs::operator[](std::string const& name)
{
return get(name);
}
beast::Journal
Logs::journal(std::string const& name)
{
return beast::Journal(get(name));
}
beast::severities::Severity
Logs::threshold() const
{
return thresh_;
}
void
Logs::threshold(beast::severities::Severity thresh)
{
std::lock_guard lock(mutex_);
thresh_ = thresh;
for (auto& sink : sinks_)
sink.second->threshold(thresh);
}
std::vector<std::pair<std::string, std::string>>
Logs::partition_severities() const
{
std::vector<std::pair<std::string, std::string>> list;
std::lock_guard lock(mutex_);
list.reserve(sinks_.size());
for (auto const& [name, sink] : sinks_)
list.emplace_back(name, toString(fromSeverity(sink->threshold())));
return list;
}
void
Logs::write(
beast::severities::Severity level,
std::string const& partition,
std::string const& text,
bool console)
{
std::string s;
format(s, text, level, partition);
std::lock_guard lock(mutex_);
file_.writeln(s);
if (!silent_)
std::cerr << s << '\n';
// VFALCO TODO Fix console output
// if (console)
// out_.write_console(s);
}
std::string
Logs::rotate()
{
std::lock_guard lock(mutex_);
bool const wasOpened = file_.closeAndReopen();
if (wasOpened)
return "The log file was closed and reopened.";
return "The log file could not be closed and reopened.";
}
std::unique_ptr<beast::Journal::Sink>
Logs::makeSink(std::string const& name, beast::severities::Severity threshold)
{
return std::make_unique<Sink>(name, threshold, *this);
}
LogSeverity
Logs::fromSeverity(beast::severities::Severity level)
{
using namespace beast::severities;
switch (level)
{
case kTrace:
return lsTRACE;
case kDebug:
return lsDEBUG;
case kInfo:
return lsINFO;
case kWarning:
return lsWARNING;
case kError:
return lsERROR;
// LCOV_EXCL_START
default:
UNREACHABLE("xrpl::Logs::fromSeverity : invalid severity");
[[fallthrough]];
// LCOV_EXCL_STOP
case kFatal:
break;
}
return lsFATAL;
}
beast::severities::Severity
Logs::toSeverity(LogSeverity level)
{
using namespace beast::severities;
switch (level)
{
case lsTRACE:
return kTrace;
case lsDEBUG:
return kDebug;
case lsINFO:
return kInfo;
case lsWARNING:
return kWarning;
case lsERROR:
return kError;
// LCOV_EXCL_START
default:
UNREACHABLE("xrpl::Logs::toSeverity : invalid severity");
[[fallthrough]];
// LCOV_EXCL_STOP
case lsFATAL:
break;
}
return kFatal;
}
std::string
Logs::toString(LogSeverity s)
{
switch (s)
{
case lsTRACE:
return "Trace";
case lsDEBUG:
return "Debug";
case lsINFO:
return "Info";
case lsWARNING:
return "Warning";
case lsERROR:
return "Error";
case lsFATAL:
return "Fatal";
// LCOV_EXCL_START
default:
UNREACHABLE("xrpl::Logs::toString : invalid severity");
return "Unknown";
// LCOV_EXCL_STOP
}
}
LogSeverity
Logs::fromString(std::string const& s)
{
if (boost::iequals(s, "trace"))
return lsTRACE;
if (boost::iequals(s, "debug"))
return lsDEBUG;
if (boost::iequals(s, "info") || boost::iequals(s, "information"))
return lsINFO;
if (boost::iequals(s, "warn") || boost::iequals(s, "warning") || boost::iequals(s, "warnings"))
return lsWARNING;
if (boost::iequals(s, "error") || boost::iequals(s, "errors"))
return lsERROR;
if (boost::iequals(s, "fatal") || boost::iequals(s, "fatals"))
return lsFATAL;
return lsINVALID;
}
void
Logs::format(
std::string& output,
std::string const& message,
beast::severities::Severity severity,
std::string const& partition)
{
output.reserve(message.size() + partition.size() + 100);
output = to_string(std::chrono::system_clock::now());
output += " ";
if (!partition.empty())
output += partition + ":";
using namespace beast::severities;
switch (severity)
{
case kTrace:
output += "TRC ";
break;
case kDebug:
output += "DBG ";
break;
case kInfo:
output += "NFO ";
break;
case kWarning:
output += "WRN ";
break;
case kError:
output += "ERR ";
break;
// LCOV_EXCL_START
default:
UNREACHABLE("xrpl::Logs::format : invalid severity");
[[fallthrough]];
// LCOV_EXCL_STOP
case kFatal:
output += "FTL ";
break;
}
// Phase 8: Inject OTel trace context (trace_id, span_id) into log lines
// for log-trace correlation. Only appended when an active span exists.
// GetSpan() reads thread-local storage — no locks, <10ns overhead.
// LCOV_EXCL_START -- compiled out when XRPL_ENABLE_TELEMETRY is not defined
#ifdef XRPL_ENABLE_TELEMETRY
{
auto span =
opentelemetry::trace::GetSpan(opentelemetry::context::RuntimeContext::GetCurrent());
auto ctx = span->GetContext();
if (ctx.IsValid())
{
// Append trace context as structured key=value fields that the
// OTel Collector filelog receiver regex_parser can extract.
char traceId[32], spanId[16];
ctx.trace_id().ToLowerBase16(opentelemetry::nostd::span<char, 32>{traceId});
ctx.span_id().ToLowerBase16(opentelemetry::nostd::span<char, 16>{spanId});
output += "trace_id=";
output.append(traceId, 32);
output += " span_id=";
output.append(spanId, 16);
output += ' ';
}
}
#endif // XRPL_ENABLE_TELEMETRY
// LCOV_EXCL_STOP
output += message;
// Limit the maximum length of the output
if (output.size() > maximumMessageCharacters)
{
output.resize(maximumMessageCharacters - 3);
output += "...";
}
// Attempt to prevent sensitive information from appearing in log files by
// redacting it with asterisks.
auto scrubber = [&output](char const* token) {
auto first = output.find(token);
// If we have found the specified token, then attempt to isolate the
// sensitive data (it's enclosed by double quotes) and mask it off:
if (first != std::string::npos)
{
first = output.find('\"', first + std::strlen(token));
if (first != std::string::npos)
{
auto last = output.find('\"', ++first);
if (last == std::string::npos)
last = output.size();
output.replace(first, last - first, last - first, '*');
}
}
};
scrubber("\"seed\"");
scrubber("\"seed_hex\"");
scrubber("\"secret\"");
scrubber("\"master_key\"");
scrubber("\"master_seed\"");
scrubber("\"master_seed_hex\"");
scrubber("\"passphrase\"");
}
//------------------------------------------------------------------------------
class DebugSink
{
private:
std::reference_wrapper<beast::Journal::Sink> sink_;
std::unique_ptr<beast::Journal::Sink> holder_;
std::mutex m_;
public:
DebugSink() : sink_(beast::Journal::getNullSink())
{
}
DebugSink(DebugSink const&) = delete;
DebugSink&
operator=(DebugSink const&) = delete;
DebugSink(DebugSink&&) = delete;
DebugSink&
operator=(DebugSink&&) = delete;
std::unique_ptr<beast::Journal::Sink>
set(std::unique_ptr<beast::Journal::Sink> sink)
{
std::lock_guard _(m_);
using std::swap;
swap(holder_, sink);
if (holder_)
sink_ = *holder_;
else
sink_ = beast::Journal::getNullSink();
return sink;
}
beast::Journal::Sink&
get()
{
std::lock_guard _(m_);
return sink_.get();
}
};
static DebugSink&
debugSink()
{
static DebugSink _;
return _;
}
std::unique_ptr<beast::Journal::Sink>
setDebugLogSink(std::unique_ptr<beast::Journal::Sink> sink)
{
return debugSink().set(std::move(sink));
}
beast::Journal
debugLog()
{
return beast::Journal(debugSink().get());
}
} // namespace xrpl