mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 22:30:57 +00:00
Merge branch 'pratik/otel-phase10-workload-validation' into pratik/otel-sync-diagnostics
Brings the telemetry startup-ordering fix forward. Two conflicts, both in
the metrics registry.
initSyncInstruments(): this branch had already replaced the registry-owned
state_changes_total with a call-site macro carrying {from,to} labels, and
the arriving branch moves the jq_trans_overflow_total observable out of
this function into registerJqTransOverflowCounter() so it is armed with
the other pull-model instruments. Kept both: the explanatory comment for
state_changes_total stays, the inline overflow block goes. The arriving
stateChangesCounter_ creation is dropped rather than merged -- this branch
removed that member, so keeping the line would not compile.
Test file: kept this branch's fuller header documentation, which records
what the disabled build can and cannot assert for the sync-diagnostics
gauges, and folded in the arriving branch's one new fact, that the
lifecycle is now two-phase. Include lists unioned.
This commit is contained in:
@@ -55,7 +55,7 @@ phases. They will be added as the corresponding subsystems are instrumented:
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
The parser `setupTelemetry()` in `src/libxrpl/telemetry/TelemetryConfig.cpp` reads the `[telemetry]` `Section` and populates a `Telemetry::Setup` struct, applying the defaults listed in Section 5.1.2 via `section.value_or(...)`. It derives `serviceInstanceId` from the node public key when not overridden, selects the exporter endpoint default by exporter type, and leaves the sampling ratio at its fixed 1.0 default (not read from config — see Section 7.4.2).
|
||||
The parser `makeTelemetrySetup()` in `src/libxrpl/telemetry/TelemetryConfig.cpp` reads the `[telemetry]` `Section` and populates a `Telemetry::Setup` struct, applying the defaults listed in Section 5.1.2 via `section.value_or(...)`. It derives `serviceInstanceId` from the node public key when not overridden, selects the exporter endpoint default by exporter type, and leaves the sampling ratio at its fixed 1.0 default (not read from config — see Section 7.4.2).
|
||||
|
||||
---
|
||||
|
||||
@@ -69,7 +69,7 @@ The parser `setupTelemetry()` in `src/libxrpl/telemetry/TelemetryConfig.cpp` rea
|
||||
> constructed with an empty `serviceInstanceId` and patched via
|
||||
> `setServiceInstanceId()` once `setup()` has called `getNodeIdentity()`.
|
||||
|
||||
`ApplicationImp` (in `src/xrpld/app/main/Application.cpp`) owns a `std::unique_ptr<telemetry::Telemetry> telemetry_`. It is built in the member initializer list via `makeTelemetry(setupTelemetry(...))` with an empty `serviceInstanceId`, then patched in `setup()` by calling `setServiceInstanceId()` with the Base58 node public key (unless the user supplied a custom `service_instance_id`). `start()` and `run()` forward to `telemetry_->start()` / `telemetry_->stop()`, and `getTelemetry()` returns the owned instance.
|
||||
`ApplicationImp` (in `src/xrpld/app/main/Application.cpp`) owns a `std::unique_ptr<telemetry::Telemetry> telemetry_`. It is built in the member initializer list via `makeTelemetry(makeTelemetrySetup(...))` with an empty `serviceInstanceId`, then patched in `setup()` by calling `setServiceInstanceId()` with the Base58 node public key (unless the user supplied a custom `service_instance_id`). `start()` and `run()` forward to `telemetry_->start()` / `telemetry_->stop()`, and `getTelemetry()` returns the owned instance.
|
||||
|
||||
### 5.3.2 ServiceRegistry Interface Addition
|
||||
|
||||
@@ -135,7 +135,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph init["Initialization"]
|
||||
parse["setupTelemetry()"]
|
||||
parse["makeTelemetrySetup()"]
|
||||
factory["makeTelemetry()"]
|
||||
end
|
||||
|
||||
@@ -169,7 +169,7 @@ flowchart TB
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Configuration Sources**: `xrpld.cfg` provides runtime settings (endpoint, per-component trace toggles) while the CMake flag controls whether telemetry is compiled in at all. Head sampling is fixed at 1.0 and is not a config option; volume reduction happens via tail sampling in the collector.
|
||||
- **Initialization**: `setupTelemetry()` parses config values, then `makeTelemetry()` constructs the provider, processor, and exporter objects.
|
||||
- **Initialization**: `makeTelemetrySetup()` parses config values, then `makeTelemetry()` constructs the provider, processor, and exporter objects.
|
||||
- **Runtime Components**: The `TracerProvider` creates spans, the `BatchProcessor` buffers them, and the `OTLP Exporter` serializes and sends them over the wire.
|
||||
- **OTLP arrow to Collector**: Trace data leaves the xrpld process via OTLP/HTTP and enters the external Collector pipeline. (OTLP/gRPC is future work — see design decisions §2.2.2.)
|
||||
- **Collector Pipeline**: `Receivers` ingest OTLP data, `Processors` apply sampling/filtering/enrichment, and `Exporters` forward traces to storage backends (Tempo, etc.).
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
- `src/tests/libxrpl/telemetry/TelemetryConfig.cpp`:
|
||||
- Test Setup defaults (all fields have correct initial values)
|
||||
- Test `setupTelemetry` config parser (empty section, full section, edge cases)
|
||||
- Test `makeTelemetrySetup` config parser (empty section, full section, edge cases)
|
||||
- Test `samplingRatio` clamping (values outside 0.0-1.0)
|
||||
|
||||
- `src/tests/libxrpl/telemetry/SpanGuardFactory.cpp`:
|
||||
|
||||
@@ -14,15 +14,17 @@
|
||||
* on the nodestore_state gauge. Also a public static constexpr inline,
|
||||
* so it runs in both builds for the same reason.
|
||||
*
|
||||
* 3. The no-op / telemetry-disabled path — construction, start()/stop()
|
||||
* lifecycle, and the synchronous record*() methods. Guarded, because
|
||||
* 3. The no-op / telemetry-disabled path — construction, the two-phase
|
||||
* start() / startAsyncGauges() / stop() lifecycle, and the synchronous
|
||||
* record*() methods. Guarded, because
|
||||
* when XRPL_ENABLE_TELEMETRY is defined MetricsRegistry.cpp is not
|
||||
* compiled into this binary (see src/tests/libxrpl/CMakeLists.txt) and
|
||||
* its out-of-line symbols are unresolvable here.
|
||||
*
|
||||
* Tests cover:
|
||||
* - Construction with telemetry disabled (no-op behavior).
|
||||
* - start()/stop() lifecycle when disabled.
|
||||
* - The two-phase start() / startAsyncGauges() / stop() lifecycle when
|
||||
* disabled.
|
||||
* - Synchronous instrument recording methods do not crash when disabled.
|
||||
* - Double stop() is safe.
|
||||
* - Destructor handles cleanup without crash.
|
||||
@@ -465,6 +467,7 @@ TEST(MetricsRegistryScaledMean, default_scale_is_one)
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
@@ -472,6 +475,14 @@ using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* OTLP/HTTP endpoint passed to every start() call below. Nothing ever dials
|
||||
* it -- these tests exercise the no-op path -- it just has to be a plausible
|
||||
* URL. start() takes `std::string const&`, so call sites construct one from
|
||||
* this view rather than repeating the literal.
|
||||
*/
|
||||
constexpr std::string_view kTestEndpoint{"http://localhost:4318/v1/metrics"};
|
||||
|
||||
/**
|
||||
* Minimal mock ServiceRegistry for MetricsRegistry testing.
|
||||
*
|
||||
@@ -765,17 +776,108 @@ TEST_F(MetricsRegistryTest, disabled_start_stop)
|
||||
telemetry::MetricsRegistry registry(false, mockApp_, j_);
|
||||
|
||||
// start() and stop() should be no-ops when disabled.
|
||||
registry.start("http://localhost:4318/v1/metrics");
|
||||
registry.start(std::string{kTestEndpoint});
|
||||
registry.stop();
|
||||
|
||||
// Double stop should be safe.
|
||||
registry.stop();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The two-phase startup split: start() then startAsyncGauges().
|
||||
//
|
||||
// Why the split exists: start() reads only config strings, while the
|
||||
// observable-instrument callbacks registered by startAsyncGauges() read live
|
||||
// Application services (getOverlay() asserts overlay_ is non-null). The split
|
||||
// lets the meter go live before the first consensus round records its
|
||||
// mode-transition counter, while the callbacks still wait for the subsystems.
|
||||
//
|
||||
// SCOPE OF THESE TESTS -- read before adding to them. MetricsRegistry.cpp is
|
||||
// compiled into this binary ONLY when telemetry is OFF
|
||||
// (src/tests/libxrpl/CMakeLists.txt:117-126 -- the `else()` branch; when it is
|
||||
// ON the .cpp needs concrete xrpld types such as LedgerMaster, TxQ, NetworkOPs,
|
||||
// Overlay and node_store::Database, which a standalone GTest binary cannot
|
||||
// link). Both start() and startAsyncGauges() therefore compile here to their
|
||||
// `#else` branch, which only (void)-casts its arguments. So these tests pin the
|
||||
// API SURFACE -- that both entry points exist, are callable in either order,
|
||||
// and leave the object usable -- and NOT the gauge behaviour. Real coverage of
|
||||
// "gauges observe values only after startAsyncGauges()" is unreachable from
|
||||
// this target; it needs the enabled path plus an in-memory metric reader.
|
||||
//
|
||||
// Two properties the production code does NOT have, so nothing below asserts
|
||||
// them: startAsyncGauges() has no idempotency guard (a second call on the
|
||||
// enabled path would create a second set of same-named instruments), and
|
||||
// callbacksDetached_ is one-way, so detachCallbacks() followed by
|
||||
// startAsyncGauges() would register permanently-dead instruments.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(MetricsRegistryTest, async_gauges_start_after_start_is_safe)
|
||||
{
|
||||
telemetry::MetricsRegistry registry(false, mockApp_, j_);
|
||||
|
||||
// The documented order: provider/sync instruments first, gauges second.
|
||||
registry.start(std::string{kTestEndpoint});
|
||||
registry.startAsyncGauges();
|
||||
|
||||
// State: the enable flag is untouched by either phase. Exact value, not
|
||||
// merely "falsy" -- a phase that flipped it would be a real defect.
|
||||
EXPECT_EQ(registry.isEnabled(), false);
|
||||
|
||||
// Synchronous recording must work off phase 1 alone. This is the whole
|
||||
// point of the split: nothing here needs the gauges to be registered.
|
||||
registry.recordRpcStarted("server_info");
|
||||
registry.recordRpcFinished("server_info", 1000);
|
||||
|
||||
registry.stop();
|
||||
EXPECT_EQ(registry.isEnabled(), false);
|
||||
}
|
||||
|
||||
TEST_F(MetricsRegistryTest, async_gauges_before_start_does_not_break_start)
|
||||
{
|
||||
telemetry::MetricsRegistry registry(false, mockApp_, j_);
|
||||
|
||||
// Negative path: the mis-ordered call, gauges before the provider exists.
|
||||
// In THIS build it reaches the (void)-cast stub, so what is actually
|
||||
// proven is only that the entry point tolerates being called first and
|
||||
// leaves the object usable -- not that the enabled path's `if (!meter_)`
|
||||
// guard works, since that guard is inside #ifdef XRPL_ENABLE_TELEMETRY and
|
||||
// is not compiled here.
|
||||
registry.startAsyncGauges();
|
||||
EXPECT_EQ(registry.isEnabled(), false);
|
||||
|
||||
// Phase 1 still works afterwards, so the bad call left no state behind.
|
||||
registry.start(std::string{kTestEndpoint});
|
||||
registry.recordJobQueued("ledgerData", "ProcessLData");
|
||||
EXPECT_EQ(registry.isEnabled(), false);
|
||||
|
||||
registry.stop();
|
||||
}
|
||||
|
||||
TEST_F(MetricsRegistryTest, async_gauges_respect_the_compile_time_guard)
|
||||
{
|
||||
// Constructed with enabled=true, which on the enabled path would register
|
||||
// instruments for real. In this build XRPL_ENABLE_TELEMETRY is undefined,
|
||||
// so both phases compile to the (void)-cast stub branch and neither
|
||||
// touches the mock -- every MockServiceRegistry accessor throws, so a
|
||||
// callback that actually ran would surface as a thrown exception here.
|
||||
telemetry::MetricsRegistry registry(true, mockApp_, j_);
|
||||
|
||||
// Cause, not just state: the flag really is true, so the no-op below is
|
||||
// attributable to the compile-time guard and not to an early enabled_
|
||||
// return.
|
||||
EXPECT_EQ(registry.isEnabled(), true);
|
||||
|
||||
EXPECT_NO_THROW(registry.start(std::string{kTestEndpoint}));
|
||||
EXPECT_NO_THROW(registry.startAsyncGauges());
|
||||
EXPECT_NO_THROW(registry.stop());
|
||||
|
||||
EXPECT_EQ(registry.isEnabled(), true);
|
||||
}
|
||||
|
||||
TEST_F(MetricsRegistryTest, disabled_recording_methods)
|
||||
{
|
||||
telemetry::MetricsRegistry registry(false, mockApp_, j_);
|
||||
registry.start("http://localhost:4318/v1/metrics");
|
||||
registry.start(std::string{kTestEndpoint});
|
||||
|
||||
// All recording methods should be no-ops (not crash).
|
||||
registry.recordRpcStarted("server_info");
|
||||
@@ -793,7 +895,7 @@ TEST_F(MetricsRegistryTest, destructor_calls_stop)
|
||||
{
|
||||
// Let the destructor handle cleanup.
|
||||
telemetry::MetricsRegistry registry(false, mockApp_, j_);
|
||||
registry.start("http://localhost:4318/v1/metrics");
|
||||
registry.start(std::string{kTestEndpoint});
|
||||
}
|
||||
// If we get here without crash, the destructor handled stop.
|
||||
}
|
||||
|
||||
@@ -1248,28 +1248,56 @@ private:
|
||||
startGenesisLedger();
|
||||
|
||||
/**
|
||||
* Start the tracing and metrics pipelines.
|
||||
* Start the tracing pipeline and the metrics provider and synchronous
|
||||
* instruments. First of the two telemetry startup phases.
|
||||
*
|
||||
* Called once from setup(), just before the [rpc_startup] loop. Starting
|
||||
* here (rather than in start()) guarantees the OTel MeterProvider is live
|
||||
* before any metric-emitting code runs — including startup RPCs, whose
|
||||
* PerfLog instrumentation records a call-site metric. A call-site metric
|
||||
* macro caches its instrument on first use via std::call_once; if that
|
||||
* first use happens while the meter is still empty, the instrument latches
|
||||
* null for the process lifetime and the metric silently never records.
|
||||
* Called once from setup(), immediately after metricsRegistry_ is
|
||||
* constructed. Starting here (rather than in start()) guarantees the OTel
|
||||
* MeterProvider is live before any metric-emitting code runs — including
|
||||
* the first consensus round, which records a mode-transition counter, and
|
||||
* the startup RPCs, whose PerfLog instrumentation records a call-site
|
||||
* metric. A call-site metric macro caches its instrument on first use via
|
||||
* std::call_once; if that first use happens while the meter is still
|
||||
* empty, the instrument latches null for the process lifetime and the
|
||||
* metric silently never records.
|
||||
*
|
||||
* The call site sits after overlay_ and the other subsystems are
|
||||
* constructed, because the metrics reader thread starts here and its
|
||||
* observable-gauge callbacks read that state (e.g. getOverlay()); starting
|
||||
* earlier would let the reader observe a half-built application.
|
||||
* Rule for keeping this call site valid: only telemetry work that reads
|
||||
* NO application subsystem may run here. That holds today — this phase
|
||||
* uses the config strings and the node identity, and creates only
|
||||
* push-model counters and histograms, which app code records into once
|
||||
* it is ready. Anything that registers a callback reading a subsystem
|
||||
* must go in startTelemetryGauges() instead, because a callback
|
||||
* registered here can fire on the metrics reader thread while the rest of
|
||||
* the application is still being built.
|
||||
*
|
||||
* @pre nodeIdentity_ is populated (needed for the service_instance_id
|
||||
* fallback), metricsRegistry_ is constructed, and overlay_ (and the other
|
||||
* subsystems read by observable-gauge callbacks) are constructed.
|
||||
* fallback) and metricsRegistry_ is constructed.
|
||||
*/
|
||||
void
|
||||
startTelemetry();
|
||||
|
||||
/**
|
||||
* Register the pull-model observable instruments. Second telemetry phase.
|
||||
*
|
||||
* Called once from setup(), immediately after overlay_ is constructed.
|
||||
* Registering an observable instrument arms the metrics reader thread to
|
||||
* invoke its callback, and those callbacks read application services —
|
||||
* getOverlay() asserts overlay_ is non-null, and an assert is not caught
|
||||
* by the callbacks' own try/catch — so this cannot run as early as
|
||||
* startTelemetry().
|
||||
*
|
||||
* @pre startTelemetry() has run, and every service the callbacks read is
|
||||
* constructed. overlay_ is the binding one: the rest (networkOPs_,
|
||||
* ledgerMaster_, openLedger_, txQ_, nodeStore_, nodeFamily_,
|
||||
* validators_, acceptedLedgerCache_, cachedSLEs_, acquireStats_,
|
||||
* timeKeeper_, relationalDatabase_, inboundLedgers_, feeTrack_) are
|
||||
* already live by the time startTelemetry() is callable, and
|
||||
* overlay_ is the only one built after it. See
|
||||
* MetricsRegistry::startAsyncGauges() for the full list.
|
||||
*/
|
||||
void
|
||||
startTelemetryGauges();
|
||||
|
||||
std::shared_ptr<Ledger>
|
||||
getLastFullLedger();
|
||||
|
||||
@@ -1356,6 +1384,39 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
return false;
|
||||
}
|
||||
|
||||
nodeIdentity_ = getNodeIdentity(*this, cmdline);
|
||||
|
||||
// Now that the node identity is known, inject it into the telemetry
|
||||
// resource attributes — but only if the user didn't already set a
|
||||
// custom service_instance_id in [telemetry]. The Telemetry object
|
||||
// was constructed with an empty serviceInstanceId because
|
||||
// nodeIdentity_ is not available in the member initializer list.
|
||||
if (!config_->section("telemetry").exists("service_instance_id"))
|
||||
telemetry_->setServiceInstanceId(toBase58(TokenType::NodePublic, nodeIdentity_->first));
|
||||
|
||||
// Create the OTel MetricsRegistry for gap-fill metrics (counters,
|
||||
// histograms, observable gauges). It must exist before startTelemetry(),
|
||||
// which starts the metrics half of the pipeline.
|
||||
metricsRegistry_ = std::make_unique<telemetry::MetricsRegistry>(
|
||||
telemetry_->isEnabled(), *this, logs_->journal("MetricsRegistry"));
|
||||
|
||||
// Start telemetry here, not in start(). Spans and metrics are both emitted
|
||||
// during the rest of setup() — the first consensus round in
|
||||
// beginConsensus() below emits spans and records the process's only
|
||||
// operating-mode transition — and both are dropped unless the pipeline is
|
||||
// already live.
|
||||
//
|
||||
// The position is bounded on both sides:
|
||||
// - After initRelationalDatabase(): the wallet DB must exist for the node
|
||||
// identity above, and a DB failure aborts setup(), so starting earlier
|
||||
// would export a partial stream for a run that never comes up.
|
||||
// - Before beginConsensus(): that call emits the first consensus spans
|
||||
// and the only mode-transition counter increment.
|
||||
//
|
||||
// Only the observable instruments have to wait for their subsystems; they
|
||||
// are registered separately by startTelemetryGauges() once overlay_ exists.
|
||||
startTelemetry();
|
||||
|
||||
if (validatorKeys_.keys)
|
||||
setMaxDisallowedLedger();
|
||||
|
||||
@@ -1443,22 +1504,6 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
|
||||
orderBookDB_->setup(getLedgerMaster().getCurrentLedger());
|
||||
|
||||
nodeIdentity_ = getNodeIdentity(*this, cmdline);
|
||||
|
||||
// Now that the node identity is known, inject it into the telemetry
|
||||
// resource attributes — but only if the user didn't already set a
|
||||
// custom service_instance_id in [telemetry]. The Telemetry object
|
||||
// was constructed with an empty serviceInstanceId because
|
||||
// nodeIdentity_ is not available in the member initializer list.
|
||||
if (!config_->section("telemetry").exists("service_instance_id"))
|
||||
telemetry_->setServiceInstanceId(toBase58(TokenType::NodePublic, nodeIdentity_->first));
|
||||
|
||||
// Create the OTel MetricsRegistry for gap-fill metrics (counters,
|
||||
// histograms, observable gauges). It is started later, just before the
|
||||
// [rpc_startup] loop (see startTelemetry()).
|
||||
metricsRegistry_ = std::make_unique<telemetry::MetricsRegistry>(
|
||||
telemetry_->isEnabled(), *this, logs_->journal("MetricsRegistry"));
|
||||
|
||||
if (!cluster_->load(config().section(Sections::kClusterNodes)))
|
||||
{
|
||||
JLOG(journal_.fatal()) << "Invalid entry in cluster configuration.";
|
||||
@@ -1534,6 +1579,15 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
collectorManager_->collector());
|
||||
add(*overlay_); // add to PropertyStream
|
||||
|
||||
// Register the observable instruments now that overlay_ exists. This arms
|
||||
// the metrics reader thread to invoke their callbacks, several of which
|
||||
// read getOverlay() — registering earlier would let the reader observe a
|
||||
// half-built application. The reader thread itself already started in
|
||||
// startTelemetry() above; this is as early as the callbacks can safely be
|
||||
// attached, and it is still before beginConsensus() so the gauges cover
|
||||
// the first round.
|
||||
startTelemetryGauges();
|
||||
|
||||
// start first consensus round
|
||||
if (!networkOPs_->beginConsensus(ledgerMaster_->getClosedLedger()->header().hash, {}))
|
||||
{
|
||||
@@ -1600,16 +1654,6 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
JLOG(journal_.warn()) << "*** standalone signing solution as soon as possible.";
|
||||
}
|
||||
|
||||
// Start telemetry and metrics now — before the [rpc_startup] loop below —
|
||||
// so the OTel meter is live before any metric-emitting code runs. Startup
|
||||
// RPCs invoke PerfLog instrumentation that records a call-site metric; a
|
||||
// metric macro caches its instrument on first use, so a first use before
|
||||
// the meter exists would latch null for the process lifetime. Placed here,
|
||||
// after overlay_ and the other subsystems the observable-gauge callbacks
|
||||
// read are constructed, so the metrics reader thread never observes a
|
||||
// half-built application.
|
||||
startTelemetry();
|
||||
|
||||
//
|
||||
// Execute start up rpc commands.
|
||||
//
|
||||
@@ -1708,6 +1752,13 @@ ApplicationImp::startTelemetry()
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ApplicationImp::startTelemetryGauges()
|
||||
{
|
||||
if (metricsRegistry_)
|
||||
metricsRegistry_->startAsyncGauges();
|
||||
}
|
||||
|
||||
void
|
||||
ApplicationImp::run()
|
||||
{
|
||||
|
||||
@@ -65,10 +65,13 @@ inline constexpr auto discover = makeStr("discover");
|
||||
} // namespace op
|
||||
|
||||
// ===== Attribute keys ======================================================
|
||||
//
|
||||
// All pathfind attributes are namespaced under `pathfind_*` (underscore form,
|
||||
// per Phase 1c naming spec rule 5). Avoids collisions with bare keys like
|
||||
// `fast` or `num_paths` that other subsystems may introduce.
|
||||
|
||||
/**
|
||||
* All pathfind attributes are namespaced under `pathfind_*`, in underscore
|
||||
* form, per the span attribute naming convention in CONTRIBUTING.md. Avoids
|
||||
* collisions with bare keys like `fast` or `num_paths` that other subsystems
|
||||
* may introduce.
|
||||
*/
|
||||
|
||||
namespace attr {
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/telemetry/Redaction.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/tx/paths/RippleCalc.h>
|
||||
|
||||
@@ -587,9 +588,13 @@ PathRequest::findPaths(
|
||||
// that a single RPC call produces one discover span instead of N (one per
|
||||
// candidate source asset). Trade-off: per-asset discovery/ranking timing
|
||||
// is no longer split into individual spans — span count and Tempo storage
|
||||
// are bounded per RPC at the cost of per-asset visibility. If per-asset
|
||||
// breakdown is needed in the future, add child spans inside the loop body
|
||||
// (`Pathfinder::findPaths`/`computePathRanks`) parented off this span.
|
||||
// are bounded per RPC at the cost of per-asset visibility.
|
||||
//
|
||||
// This is an unscoped guard: it takes the ambient span as its own parent,
|
||||
// but does not itself become the ambient parent. Adding per-asset child
|
||||
// spans inside the loop body therefore requires either making this a
|
||||
// ScopedSpanGuard or passing its context explicitly via childSpan(); a
|
||||
// child created here today would parent to this span's parent, not to it.
|
||||
using namespace telemetry;
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::discover);
|
||||
@@ -744,7 +749,20 @@ PathRequest::doUpdate(
|
||||
auto span = ScopedSpanGuard(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::compute);
|
||||
span.setAttribute(pathfind_span::attr::fast, fast);
|
||||
span.setAttribute(pathfind_span::attr::destCurrency, to_string(saDstAmount_.asset()).c_str());
|
||||
// to_string(Issue) renders a non-XRP asset as "<issuer>/<currency>" with the
|
||||
// issuer as a plaintext Base58 address, so it cannot be emitted as-is: every
|
||||
// account reaching a span is hashed first. Redact just the issuer and keep
|
||||
// the currency, which is what this attribute is for. An MPT asset renders as
|
||||
// its issuance ID and carries no address, so it needs no redaction.
|
||||
span.setAttribute(
|
||||
pathfind_span::attr::destCurrency,
|
||||
saDstAmount_.asset().visit(
|
||||
[](Issue const& issue) {
|
||||
return isXRP(issue.account)
|
||||
? to_string(issue.currency)
|
||||
: redactAccount(toBase58(issue.account)) + "/" + to_string(issue.currency);
|
||||
},
|
||||
[](MPTIssue const& mpt) { return to_string(mpt.getMptID()); }));
|
||||
|
||||
JLOG(journal_.debug()) << iIdentifier_ << " update " << (fast ? "fast" : "normal");
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -77,15 +78,21 @@ PathRequestManager::updateAll(std::shared_ptr<ReadView const> const& inLedger)
|
||||
using namespace telemetry;
|
||||
// updateAll runs on every ledger close. Skip span emission when there are
|
||||
// no active path subscriptions, to avoid a steady stream of empty spans at
|
||||
// mainnet close cadence. A null guard is used in that case; all other work
|
||||
// still runs unchanged (notably the isNewPathRequest() flag reset below),
|
||||
// so behaviour matches the pre-span code path.
|
||||
auto span = requests.empty()
|
||||
? SpanGuard{}
|
||||
: SpanGuard::span(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::updateAll);
|
||||
span.setAttribute(pathfind_span::attr::ledgerIndex, static_cast<int64_t>(inLedger->seq()));
|
||||
span.setAttribute(pathfind_span::attr::numRequests, static_cast<int64_t>(requests.size()));
|
||||
// mainnet close cadence. All other work still runs unchanged (notably the
|
||||
// isNewPathRequest() flag reset below), so behaviour matches the pre-span
|
||||
// code path.
|
||||
//
|
||||
// Scoped, so the pathfind.compute spans that doUpdate() creates below on
|
||||
// this thread nest under it. std::optional because ScopedSpanGuard is
|
||||
// deliberately non-movable, so it cannot be produced by a ternary.
|
||||
std::optional<ScopedSpanGuard> span;
|
||||
if (!requests.empty())
|
||||
{
|
||||
span.emplace(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::updateAll);
|
||||
span->setAttribute(pathfind_span::attr::ledgerIndex, static_cast<int64_t>(inLedger->seq()));
|
||||
span->setAttribute(pathfind_span::attr::numRequests, static_cast<int64_t>(requests.size()));
|
||||
}
|
||||
|
||||
bool newRequests = app_.getLedgerMaster().isNewPathRequest();
|
||||
bool mustBreak = false;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/rpc/RPCHandler.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
#include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
#include <xrpld/rpc/detail/WSInfoSub.h>
|
||||
@@ -107,6 +108,42 @@ statusRequestResponse(http_request_type const& request, boost::beast::http::stat
|
||||
return handoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the command attribute for a WebSocket message to a bounded value.
|
||||
*
|
||||
* The command/method field is client-supplied and is promoted to a Prometheus
|
||||
* label by the spanmetrics connector, so the raw string must never be emitted:
|
||||
* arbitrary request input would drive unbounded label cardinality. Resolving
|
||||
* against the handler registry keeps per-command attribution for real commands
|
||||
* and collapses everything else to a single "unknown" series.
|
||||
*
|
||||
* A request naming both fields with different values is not a real command
|
||||
* (processSession rejects it below), so it also resolves to "unknown".
|
||||
*
|
||||
* @param jv The parsed WebSocket request object.
|
||||
* @param config Node config, for the beta-RPC-API flag used to look up the
|
||||
* handler for the request's API version.
|
||||
* @return The canonical handler name, or "unknown" for an unrecognized,
|
||||
* missing, non-string, or self-contradictory command.
|
||||
*/
|
||||
static std::string_view
|
||||
resolveWsCommandSpanName(json::Value const& jv, Config const& config)
|
||||
{
|
||||
bool const hasCommand = jv.isMember(jss::command) && jv[jss::command].isString();
|
||||
bool const hasMethod = jv.isMember(jss::method) && jv[jss::method].isString();
|
||||
if (!hasCommand && !hasMethod)
|
||||
return rpc_span::val::unknownCommand;
|
||||
|
||||
std::string const cmd = hasCommand ? jv[jss::command].asString() : jv[jss::method].asString();
|
||||
if (hasCommand && hasMethod && cmd != jv[jss::method].asString())
|
||||
return rpc_span::val::unknownCommand;
|
||||
|
||||
auto const* handler =
|
||||
RPC::getHandler(RPC::getAPIVersionNumber(jv, config.betaRpcApi), config.betaRpcApi, cmd);
|
||||
return (handler != nullptr) ? std::string_view{handler->name}
|
||||
: std::string_view{rpc_span::val::unknownCommand};
|
||||
}
|
||||
|
||||
// VFALCO TODO Rewrite to use boost::beast::http::fields
|
||||
static bool
|
||||
authorized(Port const& port, std::map<std::string, std::string> const& h)
|
||||
@@ -434,14 +471,13 @@ ServerHandler::processSession(
|
||||
// Fresh root so each WS message is its own trace.
|
||||
auto span = ScopedSpanGuard::freshRoot(
|
||||
TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::wsMessage);
|
||||
if (jv.isMember(jss::command) && jv[jss::command].isString())
|
||||
{
|
||||
span.setAttribute(rpc_span::attr::command, jv[jss::command].asString().c_str());
|
||||
}
|
||||
else if (jv.isMember(jss::method) && jv[jss::method].isString())
|
||||
{
|
||||
span.setAttribute(rpc_span::attr::command, jv[jss::method].asString().c_str());
|
||||
}
|
||||
// The command is client-supplied and becomes a Prometheus label via the
|
||||
// spanmetrics connector, so it is resolved against the handler registry
|
||||
// before emission: a recognized command keeps its canonical name, anything
|
||||
// else collapses to "unknown". Emitting the raw string would let request
|
||||
// input drive unbounded label cardinality. Mirrors the HTTP path's
|
||||
// resolveCommandSpanName().
|
||||
span.setAttribute(rpc_span::attr::command, resolveWsCommandSpanName(jv, app_.config()));
|
||||
|
||||
auto is = std::static_pointer_cast<WSInfoSub>(session->appDefined);
|
||||
if (is->getConsumer().disconnect(journal_))
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <xrpl/telemetry/Redaction.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
json::Value
|
||||
@@ -23,10 +25,15 @@ doPathFind(RPC::JsonContext& context)
|
||||
// thread) nest under it. doPathFind does not yield, so scoping is safe.
|
||||
auto span = ScopedSpanGuard(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::request);
|
||||
// Addresses are hashed before emission for privacy.
|
||||
if (auto const& src = context.params[jss::source_account]; src.isString())
|
||||
// Addresses are hashed before emission for privacy. Read through a const
|
||||
// reference: the non-const json::Value::operator[] inserts a null for a
|
||||
// missing key, which would make PathRequest::parseJson's isMember() checks
|
||||
// see an absent field as present and return Malformed instead of Missing.
|
||||
// Reading for telemetry must not alter what the request looks like.
|
||||
auto const& params = std::as_const(context.params);
|
||||
if (auto const& src = params[jss::source_account]; src.isString())
|
||||
span.setAttribute(pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
|
||||
if (auto const& dst = context.params[jss::destination_account]; dst.isString())
|
||||
if (auto const& dst = params[jss::destination_account]; dst.isString())
|
||||
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
|
||||
|
||||
if (context.app.config().pathSearchMax == 0)
|
||||
|
||||
@@ -34,10 +34,15 @@ doRipplePathFind(RPC::JsonContext& context)
|
||||
// span's log lines stay trace-correlated.
|
||||
auto span = ScopedSpanGuard(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::request);
|
||||
// Addresses are hashed before emission for privacy.
|
||||
if (auto const& src = context.params[jss::source_account]; src.isString())
|
||||
// Addresses are hashed before emission for privacy. Read through a const
|
||||
// reference: the non-const json::Value::operator[] inserts a null for a
|
||||
// missing key, which would make PathRequest::parseJson's isMember() checks
|
||||
// see an absent field as present and return Malformed instead of Missing.
|
||||
// Reading for telemetry must not alter what the request looks like.
|
||||
auto const& params = std::as_const(context.params);
|
||||
if (auto const& src = params[jss::source_account]; src.isString())
|
||||
span.setAttribute(pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
|
||||
if (auto const& dst = context.params[jss::destination_account]; dst.isString())
|
||||
if (auto const& dst = params[jss::destination_account]; dst.isString())
|
||||
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
|
||||
|
||||
if (context.app.config().pathSearchMax == 0)
|
||||
|
||||
@@ -255,14 +255,45 @@ MetricsRegistry::start(std::string const& endpoint, std::string const& instanceI
|
||||
JLOG(journal_.info()) << "MetricsRegistry: starting, endpoint=" << endpoint
|
||||
<< ", instanceId=" << instanceId;
|
||||
|
||||
// Rule for anything added below: this phase may create only instruments
|
||||
// whose recording is PUSHED from app code -- counters and histograms. An
|
||||
// instrument registered here is live immediately, and the reader thread
|
||||
// may invoke a registered callback before the rest of the Application is
|
||||
// built, so any observable whose callback reads an Application service
|
||||
// belongs in startAsyncGauges(), not here. That includes observable
|
||||
// COUNTERS, not just gauges: jq_trans_overflow_total was created here and
|
||||
// its callback read getOverlay(), which asserts overlay_ is non-null.
|
||||
initExporterAndProvider(endpoint, instanceId);
|
||||
initSyncInstruments();
|
||||
|
||||
JLOG(journal_.info()) << "MetricsRegistry: provider and instruments ready";
|
||||
#else
|
||||
(void)endpoint;
|
||||
(void)instanceId;
|
||||
(void)enabled_;
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
}
|
||||
|
||||
void
|
||||
MetricsRegistry::startAsyncGauges()
|
||||
{
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
if (!enabled_)
|
||||
return;
|
||||
|
||||
// A mis-ordered call must not crash: without a meter there is nothing to
|
||||
// create instruments on, so registration is skipped entirely.
|
||||
if (!meter_)
|
||||
{
|
||||
JLOG(journal_.warn()) << "MetricsRegistry: startAsyncGauges() called "
|
||||
"before start(); no gauges registered";
|
||||
return;
|
||||
}
|
||||
|
||||
registerAsyncGauges();
|
||||
|
||||
JLOG(journal_.info()) << "MetricsRegistry: started successfully";
|
||||
#else
|
||||
(void)endpoint;
|
||||
(void)instanceId;
|
||||
(void)enabled_;
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
}
|
||||
@@ -428,30 +459,6 @@ MetricsRegistry::initSyncInstruments()
|
||||
// (NetworkOPsImp::setMode) through XRPL_METRIC_COUNTER_INC_LABELED so it
|
||||
// can carry the {from,to} transition labels; a registry-owned instrument
|
||||
// would only give an unlabelled total.
|
||||
// jq_trans_overflow_total is observed from Overlay's existing cumulative
|
||||
// atomic (Overlay::getJqTransOverflow()) rather than pushed. The overlay
|
||||
// owns the only increment site (PeerImp), so an ObservableCounter reads the
|
||||
// live total each collection cycle without threading a push path through
|
||||
// develop-owned overlay code.
|
||||
jqTransOverflowObservable_ = meter_->CreateInt64ObservableCounter(
|
||||
"jq_trans_overflow_total", "Total job queue transaction overflows");
|
||||
jqTransOverflowObservable_->AddCallback(
|
||||
[](opentelemetry::metrics::ObserverResult result, void* state) {
|
||||
auto* self = static_cast<MetricsRegistry*>(state);
|
||||
if (self->callbacksDetached_.load(std::memory_order_acquire))
|
||||
return;
|
||||
try
|
||||
{
|
||||
opentelemetry::nostd::get<opentelemetry::nostd::shared_ptr<
|
||||
opentelemetry::metrics::ObserverResultT<int64_t>>>(result)
|
||||
->Observe(static_cast<int64_t>(self->app_.getOverlay().getJqTransOverflow()));
|
||||
}
|
||||
catch (...) // NOLINT(bugprone-empty-catch)
|
||||
{
|
||||
// Silently skip on error.
|
||||
}
|
||||
},
|
||||
this);
|
||||
ledgerHistoryMismatchCounter_ = meter_->CreateUInt64Counter(
|
||||
metric::ledgerHistoryMismatchTotal, "Total built-vs-validated ledger mismatches by reason");
|
||||
txqExpiredCounter_ = meter_->CreateUInt64Counter(
|
||||
@@ -652,6 +659,7 @@ MetricsRegistry::registerAsyncGauges()
|
||||
// Each helper creates one observable instrument and attaches one
|
||||
// callback. Keeping the registration bodies in separate methods
|
||||
// preserves the 80-line-per-function limit enforced by CLAUDE.md.
|
||||
registerJqTransOverflowCounter();
|
||||
registerCacheHitRateGauge();
|
||||
registerTxqGauge();
|
||||
registerObjectCountGauge();
|
||||
@@ -683,6 +691,40 @@ MetricsRegistry::registerAsyncGauges()
|
||||
registerLedgerQuorumPublishGauge();
|
||||
}
|
||||
|
||||
void
|
||||
MetricsRegistry::registerJqTransOverflowCounter()
|
||||
{
|
||||
// jq_trans_overflow_total is observed from Overlay's existing cumulative
|
||||
// atomic (Overlay::getJqTransOverflow()) rather than pushed. The overlay
|
||||
// owns the only increment site (PeerImp), so an ObservableCounter reads the
|
||||
// live total each collection cycle without threading a push path through
|
||||
// develop-owned overlay code.
|
||||
//
|
||||
// Registered with the gauges, not with the synchronous instruments: the
|
||||
// callback reads getOverlay(), which asserts overlay_ is non-null. Arming
|
||||
// it any earlier would let a reader tick fire before the overlay exists,
|
||||
// and an assert is not caught by the try block below.
|
||||
jqTransOverflowObservable_ = meter_->CreateInt64ObservableCounter(
|
||||
"jq_trans_overflow_total", "Total job queue transaction overflows");
|
||||
jqTransOverflowObservable_->AddCallback(
|
||||
[](opentelemetry::metrics::ObserverResult result, void* state) {
|
||||
auto* self = static_cast<MetricsRegistry*>(state);
|
||||
if (self->callbacksDetached_.load(std::memory_order_acquire))
|
||||
return;
|
||||
try
|
||||
{
|
||||
opentelemetry::nostd::get<opentelemetry::nostd::shared_ptr<
|
||||
opentelemetry::metrics::ObserverResultT<int64_t>>>(result)
|
||||
->Observe(static_cast<int64_t>(self->app_.getOverlay().getJqTransOverflow()));
|
||||
}
|
||||
catch (...) // NOLINT(bugprone-empty-catch)
|
||||
{
|
||||
// Silently skip on error.
|
||||
}
|
||||
},
|
||||
this);
|
||||
}
|
||||
|
||||
void
|
||||
MetricsRegistry::registerCacheHitRateGauge()
|
||||
{
|
||||
@@ -1534,14 +1576,13 @@ MetricsRegistry::registerLedgerEconomyGauge()
|
||||
->Observe(value, {{label::metric, name}});
|
||||
};
|
||||
|
||||
// Local fee (drops).
|
||||
observe("base_fee_xrp", static_cast<double>(app.getFeeTrack().getLocalFee()));
|
||||
|
||||
// Reserve values from the validated ledger.
|
||||
// Fee and reserve values from the validated ledger.
|
||||
auto const ledger = app.getLedgerMaster().getValidatedLedger();
|
||||
if (ledger)
|
||||
{
|
||||
auto const& fees = ledger->fees();
|
||||
// Cost of a reference transaction (drops).
|
||||
observe("base_fee_xrp", static_cast<double>(fees.base.drops()));
|
||||
// Base reserve = one account, zero owned objects:
|
||||
// accountReserve(ownerCount=0, accountCount=1) == reserve.
|
||||
observe(
|
||||
|
||||
@@ -104,11 +104,17 @@
|
||||
* Example usage:
|
||||
*
|
||||
* @code
|
||||
* // In Application::setup(), after telemetry_ is created:
|
||||
* // In Application::setup(), after telemetry_ is created. Phase 1 needs
|
||||
* // only the config strings, so it runs immediately and the meter is live
|
||||
* // before any metric-emitting code:
|
||||
* metricsRegistry_ = std::make_unique<telemetry::MetricsRegistry>(
|
||||
* telemetry_->isEnabled(), app, journal);
|
||||
* metricsRegistry_->start(setup.exporterEndpoint);
|
||||
*
|
||||
* // Later in setup(), once overlay_ exists (the last of the services the
|
||||
* // callbacks read). Phase 2 registers the observable instruments:
|
||||
* metricsRegistry_->startAsyncGauges();
|
||||
*
|
||||
* // In PerfLogImp::rpcStart():
|
||||
* if (auto* mr = app_.getMetricsRegistry())
|
||||
* mr->recordRpcStarted("server_info");
|
||||
@@ -217,9 +223,9 @@ namespace telemetry {
|
||||
* catch-all try block so a transient failure never crashes
|
||||
* the reader thread.
|
||||
* - ValidationTracker protects its rolling windows internally.
|
||||
* - start() and stop() are NOT thread-safe with each other and
|
||||
* must be called from the single Application lifecycle
|
||||
* thread.
|
||||
* - start(), startAsyncGauges() and stop() are NOT thread-safe
|
||||
* with each other and must all be called, in that order, from
|
||||
* the single Application lifecycle thread.
|
||||
*
|
||||
* @note Lifetime:
|
||||
* - Must be constructed AFTER telemetry_ (reads isEnabled()).
|
||||
@@ -264,7 +270,24 @@ public:
|
||||
operator=(MetricsRegistry const&) = delete;
|
||||
|
||||
/**
|
||||
* Initialize the OTel metrics pipeline and register all instruments.
|
||||
* Initialize the OTel metrics pipeline and create the SYNCHRONOUS
|
||||
* instruments (counters and histograms).
|
||||
*
|
||||
* This is the first of two startup phases, and it can be called as soon
|
||||
* as the registry is constructed — which is what makes the meter live
|
||||
* before the first metric-emitting code runs. Startup RPCs and the first
|
||||
* consensus round both record metrics; a call-site metric macro caches
|
||||
* its instrument on first use, so a first use before the meter exists
|
||||
* latches null for the process lifetime.
|
||||
*
|
||||
* @note Invariant for future changes: this phase may create only
|
||||
* instruments with NO Application-reading callback. Push-model
|
||||
* counters and histograms qualify; app code records into them
|
||||
* when it is ready. Any observable instrument whose callback
|
||||
* reads an Application service belongs in `startAsyncGauges()`,
|
||||
* because registering it here arms the reader thread to invoke
|
||||
* that callback against a half-built Application. This applies
|
||||
* to observable COUNTERS as well as gauges.
|
||||
*
|
||||
* @param endpoint OTLP/HTTP endpoint URL for metric export
|
||||
* (e.g. "http://localhost:4318/v1/metrics").
|
||||
@@ -276,6 +299,37 @@ public:
|
||||
void
|
||||
start(std::string const& endpoint, std::string const& instanceId = {});
|
||||
|
||||
/**
|
||||
* Register the pull-model observable instruments — the second startup
|
||||
* phase. Mostly ObservableGauges, plus the ObservableCounters whose
|
||||
* source value is already cumulative.
|
||||
*
|
||||
* Split from `start()` because the two halves have different
|
||||
* prerequisites. `start()` needs only config strings; these callbacks
|
||||
* read live Application services, so this half must run later.
|
||||
* Registering an observable also arms the reader thread to invoke its
|
||||
* callback on the next tick, which is why the split is about ordering
|
||||
* and not just tidiness.
|
||||
*
|
||||
* @pre `start()` has already run (the meter exists). If it has not,
|
||||
* this is a logged no-op rather than a crash.
|
||||
* @pre Every service the callbacks read is constructed. The full set,
|
||||
* from the `app.get*()` calls in the registration helpers, is:
|
||||
* Overlay, OPs (NetworkOPs), LedgerMaster, OpenLedger, TxQ,
|
||||
* NodeStore, NodeFamily, Validators, AcceptedLedgerCache,
|
||||
* CachedSLEs, AcquireStats, TimeKeeper, RelationalDatabase,
|
||||
* InboundLedgers and FeeTrack.
|
||||
* All but Overlay already exist by the time `start()` is
|
||||
* callable, so Overlay is what fixes this call's position:
|
||||
* `ServiceRegistry::getOverlay()` `XRPL_ASSERT`s that
|
||||
* `overlay_` is non-null, and a reader-thread tick before the
|
||||
* overlay exists aborts a Debug build. The callbacks' catch-all
|
||||
* try block does not catch an assert. `getTxQ()` and
|
||||
* `getRelationalDatabase()` assert likewise.
|
||||
*/
|
||||
void
|
||||
startAsyncGauges();
|
||||
|
||||
/**
|
||||
* Detach all ObservableGauge callbacks so they no-op on the next
|
||||
* reader-thread tick.
|
||||
@@ -287,9 +341,15 @@ public:
|
||||
* guarantees that once `detachCallbacks()` returns, no subsequent
|
||||
* callback invocation will dereference an already-stopped service.
|
||||
*
|
||||
* Idempotent. Safe to call multiple times. Safe to call before
|
||||
* `start()` (has no effect). The actual SDK-level provider
|
||||
* shutdown still happens in `stop()`.
|
||||
* Idempotent, and safe to call multiple times: the flag is one-way,
|
||||
* only ever set to true, and nothing clears it. The actual
|
||||
* SDK-level provider shutdown still happens in `stop()`.
|
||||
*
|
||||
* @note One-way means this is a shutdown-only call. Calling it before
|
||||
* `startAsyncGauges()` does not "have no effect" — it
|
||||
* permanently disarms every gauge the later call registers, so
|
||||
* the instruments exist but never observe a value. Only call it
|
||||
* once the process is shutting down.
|
||||
*/
|
||||
void
|
||||
detachCallbacks() noexcept;
|
||||
@@ -1019,15 +1079,22 @@ private:
|
||||
* Register all observable gauge callbacks with the OTel SDK.
|
||||
* Dispatches to one helper per metric domain so that each helper
|
||||
* stays well under the 80-line-per-function limit.
|
||||
*
|
||||
* Called only from `startAsyncGauges()`, which owns the enabled_ and
|
||||
* meter_ guards and the Application-state precondition.
|
||||
*/
|
||||
void
|
||||
registerAsyncGauges();
|
||||
|
||||
// Per-domain gauge registration helpers. Each creates its instrument
|
||||
// and attaches a single ObservableGauge callback that reads current
|
||||
// values from Application services. The callbacks run on the OTel
|
||||
// Per-domain registration helpers for the async (pull-model) phase.
|
||||
// Each creates its instrument -- an ObservableGauge, or an
|
||||
// ObservableCounter where the underlying value is cumulative -- and
|
||||
// attaches a single callback that reads current values from Application
|
||||
// services. The callbacks run on the OTel
|
||||
// PeriodicExportingMetricReader background thread (~10 s tick).
|
||||
void
|
||||
registerJqTransOverflowCounter(); // gap-fill: overlay overflow total
|
||||
void
|
||||
registerCacheHitRateGauge(); // Task 9.2
|
||||
void
|
||||
registerTxqGauge(); // Task 9.3
|
||||
|
||||
Reference in New Issue
Block a user