Commit Graph

15836 Commits

Author SHA1 Message Date
Pratik Mankawde
61b101e147 fix(nodestore): sample writer depth over one population
Depth was observed at insert entry but its sample was only counted at
insert exit, so an insert still in flight contributed nothing to the
mean while completed inserts -- disproportionately the fast, shallow
ones -- all did. The reported mean understated queueing exactly when
queueing was worst: with every writer inside its first insert the gauge
was omitted entirely, while writers-in-flight correctly showed them all.

Depth and its sample count are now both folded in at entry, so the mean
is taken over one population. Mean depth is the L in Little's Law, so a
biased L understated the derived queueing share of each insert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:24:07 +01:00
Pratik Mankawde
cfa2fd06ca fix(ledger): count acquisitions satisfied from the local store
recordCompletion() was reached only from done(), but init() can satisfy an
acquisition entirely from the local store and return without ever calling
done(). Those completions went uncounted, so acquire_completions read zero
for a whole class of outcome. On a live clean-database sync it read 0 for
510 seconds while the node reached full, which was misread as the node
completing nothing.

Counted in init() rather than by calling done(), because done() drives the
state machine: it stores the ledger, dispatches AcqDone, updates the
fetch-rate average, and sets signaled_, which would disarm a later genuine
done(). A counter must not change behaviour. A completionCounted_ latch,
separate from signaled_, makes the count idempotent across the two
independent exits so a completion is counted exactly once however it was
reached.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:44:15 +01:00
Pratik Mankawde
6aa59629de fix(tests): make the fetch-duration assertions machine-independent
BEAST_EXPECT(getFetchDurationUs() > 0) failed on the macOS arm64 runner at
DatabaseConfig_test.cpp:879. It is an assertion about the host, not the
code: Database::fetchNodeObject() truncates each fetch to whole
microseconds, and a read served from NuDB's in-memory buckets can
genuinely measure under one microsecond. On a host fast enough for every
read to truncate, the total legitimately stays at zero.

Both fetch-duration checks now compare the accumulator against the total
the scheduler's fetch reports carried, via a CountingScheduler. Each
fetch is measured once and that one value feeds both, so the equality
holds at any host speed while being strictly stronger than '> 0': a
dropped fetch, a double-count, or the write member being returned all
break it. The same reasoning is already recorded in the GTest at
src/tests/libxrpl/nodestore/Database.cpp.

The store-duration '> 0' checks are left alone: those writes reach disk
and were not what failed.
2026-07-28 17:43:59 +01:00
Pratik Mankawde
e271aa2ea7 fix(tests): clear three clang-tidy findings in the nodestore metric tests
All three were rejected by clang-tidy as errors:
- readability-math-missing-parentheses on 2 * kNumStored + kNumMissing
- misc-const-correctness on the never-mutated AcquireStats 'quiet'
- bugprone-unchecked-optional-access on the read_threads_running
  dereferences: branching on BEAST_EXPECT's return value hides the
  has_value() check from the analyser, so the guard is now a plain if and
  the macro asserts separately.
2026-07-28 17:41:57 +01:00
Pratik Mankawde
5e7c1bd02e test(overlay): use designated initializers in the fee-case table
The FeeCase table used positional aggregate initialisation, which
modernize-use-designated-initializers rejects for a five-field aggregate;
the clang-tidy job failed once per row. Naming the fields also makes the
requested/found/derived/literal ordering readable at each row instead of
relying on the reader tracking four ints positionally.
2026-07-28 17:41:17 +01:00
Pratik Mankawde
ea9ffa0ebf fix(telemetry): drop the unused <ranges> include from MetricsRegistry.h
The header was added alongside sanitiseHandler(), which calls
std::ranges::all_of -- but that algorithm lives in <algorithm>, which the
same commit also added and which is still needed. <ranges> itself is never
used, so misc-include-cleaner rejects it and the clang-tidy job fails on
MetricsRegistry.h:146.
2026-07-28 17:41:07 +01:00
Pratik Mankawde
aea1f25fa8 Merge branch 'pratik/otel-phase8-log-correlation' into pratik/otel-phase9-metric-gap-fill 2026-07-28 16:25:00 +01:00
Pratik Mankawde
0a22a512bb Revert the nodestore read-latency histogram
Drops nodestore_read_us and everything added to reach it. read_mean_us already
carries microsecond precision and separated the two sync failure modes cleanly
in live testing -- 8.8 us on a clean store against a 223 us cold-store peak --
so the distribution added no signal that changed a diagnosis.

The cost of getting it was disproportionate. NodeStoreScheduler had no path to
the metrics registry, so its production constructor grew a ServiceRegistry
parameter: a metric addition changing a production signature. That in turn
forced an edit to a pre-existing test, src/test/app/SHAMapStore_test.cpp, whose
only stake in this is that it constructs a scheduler. Worse, the scheduler is
built in Application's member initializer list, long before metricsRegistry_
exists, so the registry could not be captured once and had to be re-resolved on
every fetch -- a lookup on a path that runs millions of times per sync.

The constructor returns to taking JobQueue& alone and SHAMapStore_test.cpp
returns to the single-argument call, leaving that file differing from its
pre-change form only by the NodeStore:: to node_store:: rename it picked up from
develop.

FetchReport::elapsed stays microseconds and onFetch keeps its explicit
duration_cast to milliseconds for addLoadEvents, which takes milliseconds. That
widening was a separate fix and is what makes read latency measurable at all.

kSubMillisecondBoundaries loses its only consumer and regains [[maybe_unused]],
which is the state the commit that introduced it left it in; without the
attribute an unused constant is an error under wextra with werr.

Also removes the ledger-data-sync panel that charted the histogram and the
fetch_type and found template variables, which filtered on labels no metric
emits any more, plus the runbook and reference-doc sections and the two
instrument and view counts that named it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:24:35 +01:00
Pratik Mankawde
7e2cd62477 Merge branch 'pratik/otel-phase7-native-metrics' into pratik/otel-phase8-log-correlation 2026-07-28 16:24:23 +01:00
Pratik Mankawde
7aa41e3dfd fix(telemetry): show intermediate sync states on Ledger Data & Sync
The Sync State panel only ever showed Connected and Full. Two causes:

1. state_tracking{metric="state_value"} is emitted with values 0-6, where
   5 is FULL+validating and 6 is FULL+proposing, but the panel declared
   max: 4 with value mappings for 0-4 only. Values above 4 were pinned to
   the axis ceiling and rendered unmapped. Most nodes sit at 6, so the
   majority of series were clipped.

2. The gauge samples the instantaneous mode on a 10s export tick, so
   states shorter than one tick fall between samples. A real sync showed
   SYNCING for a single scrape and skipped TRACKING entirely. This is the
   sampling hazard already noted on StateAccounting in NetworkOPs.h.

Extend the panel domain to 0-6 with Validating and Proposing mappings and
matching threshold steps, and add a colour-coded state-timeline panel where
each band's width is the time spent in that state, so brief states appear
as thin slivers rather than disappearing. The timeline reads
server_info{metric="server_state"} (raw OperatingMode 0-4) rather than
state_value, which folds 5 and 6 onto FULL and would split one Full band
into three colours.

Panel layout below the insertion point shifts down by 6 rows.

Note this makes short states legible, not lossless: exporting
StateAccounting's per-state duration accumulators is the sampling-immune
fix and is left as follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:24:16 +01:00
Pratik Mankawde
9f92a938e5 test(telemetry): assert the nodestore_state labels and pin writer depth
The 22 metric label values on the nodestore_state gauge were asserted
nowhere, and the depthSum assertions could not tell the real
fetch_add(depth) from a fetch_add(1) that would silently zero every
derived queueing time.

Make the four observe* helpers and their ObserveFn sink public rather
than private, so the seam the header already claimed is actually
reachable. All four are static and read only their arguments, so this
exposes no object state; a friend declaration would have granted access
to every private member instead. The assertions live in the existing
Beast nodestore suite because src/test compiles into xrpld, which
contains MetricsRegistry.cpp, while xrpl_tests deliberately does not
when telemetry is enabled.

Each helper now has its exact emitted label set asserted, so a typo in
any literal fails instead of silently producing a disjoint series, and
each derived mean is asserted ABSENT on a fresh store -- a refactor to
value_or(0) would draw a believable flat zero on a latency axis and
otherwise pass everything.

Replace the concurrent write-stats test with one that forces genuine
overlap through a latch. NuDB holds one global mutex for the whole
insert and doInsert reads the depth before entering it, so a blocked
thread records a depth of at least 2; asserting depthSum strictly
exceeds insertCount therefore cannot be satisfied by a constant 1. The
old bounds admitted that bug at their floor.

Also: cover the std::nullopt branch on the two backends that exist in
every build, bound the store-duration accumulator by the wall clock,
drop four assertions that cannot fail, and correct two comments that
claimed coverage the tests do not have -- the duplicate-key test is not
the throwing path, because nudb reports key_exists without throwing,
and no test drives NodeStoreScheduler::onFetch.
2026-07-28 16:08:56 +01:00
Pratik Mankawde
7ef8c07cf9 Rename nudb_bytes metric to stored_object_bytes
The nudb_bytes label value on the storage_detail gauge named something the
code never measured. It observes Database::getStoreSize(), which returns the
storeSz_ accumulator: the cumulative payload bytes of objects this process
handed to the NodeStore. That is not a NuDB file size. It excludes NuDB's
keys, bucket padding and log, and it resets with the process while the files
on disk do not.

The name caused two concrete errors. It invited sizing the store on disk from
a number that cannot do it, and it invited a write-amplification ratio against
node_written_bytes -- which reads the same accessor at MetricsRegistry.cpp:836,
so that ratio is a constant 1.0 and measures nothing.

The nudb_ prefix was wrong too. storeSz_ is written only by
Database::storeStats(), called from DatabaseNodeImp, DatabaseRotatingImp and
Database itself. No backend code touches it, so the value reads the same on
RocksDB. That distinguishes it from the real nudb_* family
(nudb_writers_in_flight and friends), which come from getWriteStats() and are
absent entirely on a non-NuDB backend.

stored_object_bytes says what the value is and claims nothing about the
filesystem. Docs already described the value correctly; they keep that
explanation and now also record the old name, so a query pinned to it can be
traced. Neither Backend nor Database exposes an on-disk size accessor and none
was added -- no metric reports the store's on-disk size today.

Updates the node-health panel title, description and PromQL, and the four docs
that name the label value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:46:24 +01:00
Pratik Mankawde
8ade2b43d9 docs(telemetry): rename node-health found-ratio and stored-bytes panels
node_reads_hit is a found count, not a cache hit. fetchHitCount_ increments
whenever a fetch returned an object, whatever served it, and a node with
online_delete uses DatabaseRotatingImp which has no NodeObject cache at all. The
ratio therefore reads near 100% while every fetch goes to disk, which made the
cold-read failure mode look impossible on the board.

- Phase9_taskList: node_reads_hit is a found count, not cache-served reads.
- node-health: "NodeStore Cache Hit Ratio" -> "NodeStore Read Found Ratio",
  legends "Cache Hit Ratio" -> "Found Ratio" and "Reads Hit (cache)" ->
  "Reads Found"; corrected the ratio panel's axis label, which read
  "Operations / Sec" on a percentunit panel.
- node-health: "NuDB Storage Size" -> "NuDB Stored Bytes". nudb_bytes observes
  getStoreSize(), the accumulator node_written_bytes also reads, so it is
  cumulative object-payload bytes and not a filesystem measurement.
- ledger-data-sync: point the read-latency discriminator at the renamed panel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:23:41 +01:00
Pratik Mankawde
8be97bca62 Merge branch 'pratik/otel-phase8-log-correlation' into pratik/otel-phase9-metric-gap-fill 2026-07-28 15:20:16 +01:00
Pratik Mankawde
f78db4ba0a Merge branch 'pratik/otel-phase7-native-metrics' into pratik/otel-phase8-log-correlation 2026-07-28 15:20:11 +01:00
Pratik Mankawde
7da5ac5992 docs(telemetry): correct nudb_bytes and NuDB found-ratio descriptions
nudb_bytes was documented as a NuDB file size, one place even claiming a
filesystem stat. It observes Database::getStoreSize(), which sums the object
payloads this process has written. It excludes NuDB's keys, bucket padding and
log, and resets with the process. node_written_bytes calls the same accessor, so
the two series are equal by construction and a write-amplification ratio built
from them is a constant 1.0. Neither Backend nor Database exposes a file-size
accessor, so nothing reports on-disk size today.

The Ledger Data & Sync panel plotting node_reads_hit / node_reads_total was
titled "NuDB Cache Hit Ratio" and described as reads served from cache.
fetchHitCount_ increments whenever a fetch returned an object, whatever served
it, so the ratio is a found rate. It reads near 100% while every fetch goes to
disk, which made the cold-read failure mode look impossible. Renamed to
"NuDB Read Found Ratio" and rewrote the guidance to pair it with read latency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:20:02 +01:00
Pratik Mankawde
4d3f9d6f7b fix(tests): clear clang-tidy findings in the new nodestore metric tests
The tests added on this branch tripped six checks under
WarningsAsErrors. All of them are in test code introduced here.

bugprone-unchecked-optional-access: gtest's ASSERT_TRUE returns an
opaque AssertionResult, so the dataflow analysis cannot see that a
following deref is guarded. Replaced with an explicit
`if (!x.has_value()) FAIL()`, which the analysis does follow, or with
a direct optional comparison where no deref is needed. Both keep the
original assertion strength and add a reason string.

readability-use-anyofallof: the two consteval helpers now use
std::ranges::all_of. The static_assert still evaluates at compile
time, verified by inverting the predicate and watching it fail.

misc-const-correctness, misc-include-cleaner,
modernize-use-designated-initializers: const on a never-mutated
local, corrected include sets, and named fields on the Expected
aggregate so its two adjacent bools cannot be transposed silently.

No production code changes, and no NOLINT added.
2026-07-28 14:55:30 +01:00
Pratik Mankawde
8fab4ae507 docs: fix sync-diagnosis decision rule and metric semantics
The decision rule for "slow to reach full" keyed on an absolute read-time
threshold and a found-rate threshold that misclassified the very run they
were written to explain: a populated-store run reading 31.8 us at 88.3%
found fell through both cuts and came out as "disk-bound" rather than the
cold-read case it is.

Replace it with two ordered questions -- is the read cost several times a
warm read, and is the write path queueing -- and demote the found rate to a
splitter that only applies once reads are known to be expensive. A high
found rate on its own is the normal state of a populated store, so it can
never be a trigger. The rule now classifies all four reference datasets
correctly, and the runbook shows the rule applied to each so the "confirm
against the reference points" step agrees with the table.

Also in the runbook:
- name the source of the devnet incident figures at the point of use, and
  point forward to the caveat from the same paragraph
- state the provenance of the measured columns, and split the incident
  figures into their own table marked as not our measurement
- say plainly that the compounding-factor explanation is an unconfirmed
  hypothesis
- correct the deferral gate: it fires at the acquisition's own job limit of
  5, not at the ledgerData lane cap of 3
- note that no read-max gauge exists, so the tie-break uses max_over_time
  of the mean or the read histogram's p99
- split a PromQL block that put two expressions on adjacent lines, which
  parses as one invalid expression

In the data-collection reference:
- node_reads_hit counts fetches that found an object, not cache hits
- nudb_bytes is cumulative payload bytes from the same accessor as
  node_written_bytes, not on-disk size, so their ratio is a constant 1.0
- write_load and nudb_writers_in_flight are the same atomic on NuDB;
  document that and what write_load means on RocksDB
- correct the instrument count to 8 and the view count to 7 after
  nodestore_read_us and its view were added
- scope the Phase 9 query examples to one node with the regex form

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 14:09:01 +01:00
Pratik Mankawde
13d915895a fix(test): clear clang-tidy findings in the new suite code
- JobQueue_test: drop the unused <functional>, and make the two
  read-only GaugeFixture instances const. The other two stay mutable
  because they submit jobs through fixture.queue.
- DatabaseConfig_test: drop the unused SystemParameters.h and include
  ByteUtilities.h for megabytes(), which was reached only transitively.

Regenerate ordering.txt for the resulting edges: test.unit_test arrives
with SuiteJournal.h, and xrpl.protocol leaves with SystemParameters.h.
2026-07-28 12:50:36 +01:00
Pratik Mankawde
e3635f1e31 fix(ci): resolve metric label keys written as named constants
Rule D rejected the `handler` and `result` dashboard filters even though
both labels are emitted, because the L6 extractor missed them twice over:

- It matched only a key written as an inline literal directly after `{{`.
  A label map is a braced list of braced pairs, so in
  `Add(1, {{"job_type", a}, {"handler", b}})` only the first key follows
  `{{` -- every later one was dropped. A key hoisted into a constant
  (`{{kHandlerLabel, v}}`) was invisible in any position.
- It walked *.cpp only, so a constant declared in a header, as
  kLabelResult is, could never be found.

Match the label map first and scan its pairs, resolve `k...Label`
constants through their `constexpr char k...[] = "..."` definitions, and
read headers too. Matching the map rather than any `{"key",` in the file
keeps ordinary brace initializers, such as the `{"http", "https"}` scheme
array, out of the label set -- they are not labels and must not license a
dashboard filter. Test code is skipped for the same reason Rule F skips
it: fixtures pass arbitrary literal pairs.
2026-07-28 12:50:27 +01:00
Pratik Mankawde
bae6514667 fix(docs): match the dashboard node filter convention in the runbook
The new PromQL used an exact match on service_instance_id. The $node
template variable is multi-value, so an exact match returns nothing as
soon as more than one node is selected. Every one of the 442 filters
across the dashboards uses the regex form; the runbook now agrees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:21:35 +01:00
Pratik Mankawde
8b14176609 feat(telemetry): add sync bottleneck discrimination dashboard row
A stalled ledger sync has two distinct causes that look identical from the
job queue: in both, the ledgerData lane sits at its concurrency limit of 3
with work waiting. Lane occupancy therefore discriminates nothing, and the
dashboard had no panel that separated the two.

Adds a Sync Bottleneck Discrimination row to Ledger Data & Sync with seven
panels built on the nodestore_state write-path and acquire counters:

- NodeStore Read Latency, lifetime and windowed, on a log axis with
  threshold lines at 10 and 100 us. This is the discriminator: fast reads
  mean the cost is on the write side, slow reads with a high hit ratio
  mean every access is paying disk latency.
- NuDB Writer Queue Depth, the confirming half. NuDB takes one global
  mutex per insert, so depth above 1 is queueing at that mutex.
- NuDB Insert Time, mean against true max, which quantifies how much of
  each insert is wait rather than work.
- Acquire Deferrals vs Timeouts on one panel, because the livelock
  fingerprint is deferrals rising while timeouts stay flat, and neither
  counter shows that alone.
- Acquisition Progress and Discarded Acquire Work, the outcome side: no
  completions while the lane is busy means acquisitions resolve neither
  way.
- NodeStore Read Latency Distribution split by fetch_type and found, from
  the nodestore_read_us histogram, since a mean cannot tell a uniformly
  warm store from a mostly-warm one with a cold tail.

Every expression carries the existing service_instance_id and tier filters.
The histogram introduces two new label dimensions, so fetch_type and found
template variables are added to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:20:47 +01:00
Pratik Mankawde
819abf8ee1 docs(telemetry): document the sync bottleneck diagnosis
Two different bottlenecks both present as the ledgerData job lane pinned
at its concurrency cap of 3, so lane occupancy diagnoses neither. One is
write-serialized (NuDB takes one global mutex per insert, so inserts
queue), the other is cold-read-bound on a populated store. Telling them
apart needs the storage-side signals, not the lane.

Adds to docs/telemetry-runbook.md a "Slow to reach full" procedure: a
Mermaid diagram of the two modes, a decision table keyed on whether
acquisitions are completing, the measured reference values from both
runs, and the deferral/timeout pair that fingerprints the disarmed
give-up path. States plainly that node_reads_hit is a found count rather
than a cache-hit rate, which is why a ~100% "hit rate" at 113 us per
read is the cold-read signature and not a contradiction.

Records honestly that the populated-store run was twice as fast despite
slower reads, so cold reads alone do not explain the long incident.

Adds reference rows for the 13 new nodestore_state label values and the
nodestore_read_us histogram to
OpenTelemetryPlan/09-data-collection-reference.md, in the authoritative
Phase 9 OTel SDK section alongside the existing NodeStore I/O table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:19:22 +01:00
Pratik Mankawde
51f6544f7b feat(telemetry): record nodestore read latency as a histogram
kSubMillisecondBoundaries existed but nothing used it, so per-fetch read
latency never reached Grafana -- only the coarse read_mean_us gauge did,
which cannot separate "every read took 9us" from "most took 2 and a few
took 900".

Add a nodestore_read_us histogram, register its view against the sub-
millisecond ladder rather than kMicrosecondBoundaries (whose first edge
is 100us, above the entire range a warm read occupies), and record into
it from NodeStoreScheduler::onFetch using FetchReport::elapsed, which a
previous change widened to microseconds for exactly this purpose.

The name and its labels live in a new include/xrpl/telemetry header
because the view registration (xrpld.telemetry) and the record site
(xrpld.app) sit in different levelization modules; a copy-pasted literal
would let them drift and silently drop the bucket override. Same reason
and same placement as GetObjectMetricNames.h. No new levelization edge:
xrpld.app > xrpl.telemetry already exists.

NodeStoreScheduler had no registry access, so it now takes a
ServiceRegistry and resolves the registry per call. It is constructed in
Application's initializer list, long before metricsRegistry_ is assigned
in setup() and started in startTelemetry(), so capturing a pointer at
construction would capture nullptr forever; the metric macros null-check
the registry, the meter and the instrument, so early fetches are simply
not recorded.

Labels are fetch_type and found, both already carried on the report --
4 series, fixed at compile time. A slow async read delays prefetch while
a slow sync read blocks a caller, and a miss can cost a read of every
backend, so neither dimension can be collapsed.

Negative elapsed times are skipped: the SDK rejects them and logs a
warning on every call, which on a per-fetch path is a log flood. Zero is
still recorded, since a page-cache-served read genuinely rounds to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:04:37 +01:00
Pratik Mankawde
a7f98d93f9 fix(test): use SuiteJournal and drop an unused local
beast::Journal has no default constructor, so holding one as a plain
member deleted the suite's own default constructor and the Beast
registration macro could not instantiate it. SuiteJournal takes the
suite, converts implicitly where a journal is expected, and routes log
output into the test report.

Also removes a leftover unused local in run(), which is a hard error
under the warnings-as-errors build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e3c2f8279a)
2026-07-28 11:30:45 +01:00
Pratik Mankawde
332bedac89 fix(test): restore the helpers DatabaseConfig_test lost in the GTest migration
The nodestore suites moved from Beast to GTest upstream, which deleted
src/test/nodestore/TestBase.h. DatabaseConfig_test stayed on Beast and
still derived its journal and batch helpers from that base, so once both
sides met in a merge it referenced three symbols that no longer existed.

It now carries its own copies, matching the current API: node object
types are NodeObjectType::Ledger rather than the old hotLEDGER spelling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 05d01de81a)
2026-07-28 11:30:45 +01:00
Pratik Mankawde
064f79e5e5 fix(nodestore): report fetch latency in microseconds
FetchReport::elapsed was milliseconds, so every nodestore read rounded to
zero: a warm store answers in single-digit microseconds and a cold one in
low hundreds, and both became 0 ms. That difference is the whole signal
separating a cold-read stall from a healthy node, and it was being
discarded at the type. Database::fetchNodeObject now measures once and
uses that one value for both the cumulative counter and the report, so
the two can never disagree. The job-queue call still takes milliseconds
and now casts explicitly.

BatchWriteReport::elapsed stays milliseconds and is documented as such:
a batch write covers many objects and reaches the disk, so it belongs in
that range.

Also adds a sub-millisecond histogram ladder, because the existing bucket
edges start at 100 microseconds and put the entire warm range in bucket
0. It is not wired to a view yet: no sub-millisecond instrument exists to
name, so the edges wait for the instrument that records read latency.

The new test captures what the nodestore reports and asserts the reported
total equals the internal microsecond accumulator exactly, plus that at
least one report is not a whole number of milliseconds -- which a
millisecond-typed field can never satisfy on any hardware.
2026-07-27 20:22:49 +01:00
Pratik Mankawde
ab026eaea8 Merge branch 'pratik/otel-phase7-native-metrics' into pratik/otel-phase8-log-correlation 2026-07-27 19:48:39 +01:00
Pratik Mankawde
b9f1c19851 Merge branch 'pratik/otel-phase6-statsd' into pratik/otel-phase7-native-metrics 2026-07-27 19:48:39 +01:00
Pratik Mankawde
b7d25c43e6 Merge branch 'pratik/otel-phase5-docs-deployment' into pratik/otel-phase6-statsd 2026-07-27 19:48:39 +01:00
Pratik Mankawde
f7c7b906ec Merge branch 'pratik/otel-phase4-consensus-tracing' into pratik/otel-phase5-docs-deployment 2026-07-27 19:48:39 +01:00
Pratik Mankawde
b50aa17aee fix(consensus): include ConsensusSpanNames.h where its symbols are used
Both files use telemetry::consensus::span::* but reached the declarations
only transitively, which misc-include-cleaner rejects. Add the direct
include now that the header lives under xrpl/consensus/.
2026-07-27 19:48:26 +01:00
Pratik Mankawde
e931433962 fix(tests): use the renamed node_store namespace in the registry mock
The nodestore namespace became xrpl::node_store when develop was merged
in, but one mock override still named the old spelling, so it did not
match the ServiceRegistry signature it overrides. The sibling mock in
TestServiceRegistry.h was already correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:48:25 +01:00
Pratik Mankawde
2f5f0944ab feat(telemetry): observe the write queue, read latency and stalls
Adds the derived read and write means, the NuDB writer depth and insert
timings, and the seven acquisition counters to the existing
nodestore_state gauge. Every value multiplexes onto that one instrument
through its `metric` label, so no new instrument is created.

Means are omitted rather than reported as zero when their denominator is
zero, so a dashboard shows a gap instead of a plausible wrong number. All
four go through one new scaledMean() helper so the guard cannot be
forgotten at a future call site; it also saturates instead of wrapping,
because a wrapped gauge reads as a healthy-looking dip. A zero total over
real samples still reports zero, since a store fast enough to truncate
every sample must not look dead.

The NuDB write-path block is skipped entirely when getWriteStats() is
nullopt, which is every backend but NuDB, so absent labels distinguish
"not measured" from "measured, and idle". Writer depth is scaled by 100
and named accordingly, because it sits just above 1.0 and an integral
gauge would truncate the whole signal away.

The gauge callback body is split into four static helpers to stay inside
the per-function line budget and to make each domain testable with a
recording sink.

Also corrects nudb_bytes, which called getStoreSize() exactly as
node_written_bytes does, so the obvious write-amplification ratio was a
constant 1.0 and the old "on-disk size" comment was wrong. No file-size
accessor exists on Backend or Database, so the value is unchanged and the
comment now states what it really is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:46:48 +01:00
Pratik Mankawde
c06a6586a9 Merge branch 'pratik/otel-phase8-log-correlation' into pratik/otel-phase9-metric-gap-fill
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
2026-07-27 19:27:25 +01:00
Pratik Mankawde
c98c8dad85 Merge branch 'pratik/otel-phase7-native-metrics' into pratik/otel-phase8-log-correlation 2026-07-27 19:14:30 +01:00
Pratik Mankawde
00c262e1ac Merge branch 'pratik/otel-phase6-statsd' into pratik/otel-phase7-native-metrics 2026-07-27 19:14:30 +01:00
Pratik Mankawde
3e3f59b7bd Merge branch 'pratik/otel-phase5-docs-deployment' into pratik/otel-phase6-statsd 2026-07-27 19:14:29 +01:00
Pratik Mankawde
1df4e80907 Merge branch 'pratik/otel-phase4-consensus-tracing' into pratik/otel-phase5-docs-deployment 2026-07-27 19:14:29 +01:00
Pratik Mankawde
496ac21259 Merge branch 'pratik/otel-phase3-tx-tracing' into pratik/otel-phase4-consensus-tracing 2026-07-27 19:14:29 +01:00
Pratik Mankawde
c2aad445c1 fix(tx): satisfy clang-tidy in the tx apply stage span helper
clang-tidy had been skipped on this branch while CMake configure was
failing, so these findings in makeStageSpan surfaced only now:

- brace the three single-statement if bodies (readability-braces-around-statements)
- compare the pointer parameter explicitly against nullptr
  (readability-implicit-bool-conversion)
- include xrpl/protocol/Protocol.h for LedgerIndex (misc-include-cleaner)
2026-07-27 19:14:19 +01:00
Pratik Mankawde
6eec363dae Merge branch 'pratik/otel-phase7-native-metrics' into pratik/otel-phase8-log-correlation 2026-07-27 19:04:19 +01:00
Pratik Mankawde
c319159b77 Merge branch 'pratik/otel-phase6-statsd' into pratik/otel-phase7-native-metrics
# Conflicts:
#	.github/scripts/levelization/results/ordering.txt
2026-07-27 19:04:08 +01:00
Pratik Mankawde
17bab7289f feat(ledger): count acquisition stalls instead of only logging them
A saturated ledgerData lane makes TimeoutCounter re-arm its timer
without running the timer body, so timeouts_ never advances and the
six-timeout give-up can never fire. Acquisitions then neither finish
nor fail until the one-minute sweep destroys their partial maps, and
the work restarts. Every step of that chain was debug-log-only, so a
node at warning level could not be diagnosed after the fact.

The counters are separate on purpose: deferrals rising while timeouts
stay flat is the signature, and no single counter shows it.

Completions are recorded in done() rather than at the "Done: complete"
log line, because that line also fires for failures and misses the
checkLocal and receiveNode paths; done() is the one funnel every
outcome passes through and its signaled_ guard makes it idempotent.

AcquireStats is only forward-declared in ServiceRegistry so libxrpl
still includes nothing from xrpld. The src/ include path for the test
binary moves out of the telemetry guard, since a header-only type
under src/xrpld/ is testable in every build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:04:04 +01:00
Pratik Mankawde
8bc5285983 Merge branch 'pratik/otel-phase5-docs-deployment' into pratik/otel-phase6-statsd 2026-07-27 19:03:29 +01:00
Pratik Mankawde
1aa8521357 Merge branch 'pratik/otel-phase4-consensus-tracing' into pratik/otel-phase5-docs-deployment 2026-07-27 19:03:29 +01:00
Pratik Mankawde
9edb1ce60b fix(build): link telemetry into the consensus module
The consensus headers moved into the isolated xrpl/consensus module and
took a dependency on xrpl/telemetry for the tracing span constants, but
two things were left behind:

- Four includes still pointed at the old src/xrpld/consensus/ location,
  which no longer exists, so the build failed with
  "fatal error: 'xrpld/consensus/ConsensusParms.h' file not found".
- xrpl.libxrpl.consensus never linked xrpl.libxrpl.telemetry. add_module
  isolates each module's headers, so xrpl/telemetry/SpanNames.h was not
  on the include path even once the include was repointed.

Repoint the stale includes at xrpl/consensus/, and declare the telemetry
module before consensus so consensus can link it. Regenerate ordering.txt
for the resulting edge.
2026-07-27 19:03:13 +01:00
Pratik Mankawde
216d75e2e5 feat(nodestore): measure the NuDB write queue
NuDB serializes every insert behind one global mutex held for the whole
call, so a caller cannot see how long it waited. Record instead the
writer depth joined at and the wall time spent; with mean depth L and
mean insert time W, Little's Law gives service time W/L and queueing
W - W/L. That distinguishes a serialized write path from a saturated
disk: measured on a dev box the device sat 89 percent idle while
throughput stayed flat at 42k inserts per second.

The accounting runs from a ScopeExit guard because the insert can
allocate and therefore throw; leaking the depth would strand the gauge
above zero for the life of the process.

getWriteLoad also stops returning a hardcoded zero. It now reports
writer depth, which is bounded by the writing-thread count and so stays
far below the kMaxWriteLoadAcquire cutoff that gates history
acquisition, where returning bytes or microseconds would have silently
suppressed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:37:00 +01:00
Pratik Mankawde
23c93cd927 Merge branch 'pratik/otel-phase7-native-metrics' into pratik/otel-phase8-log-correlation 2026-07-27 18:32:34 +01:00
Pratik Mankawde
7126137522 Merge branch 'pratik/otel-phase6-statsd' into pratik/otel-phase7-native-metrics 2026-07-27 18:32:34 +01:00