Files
rippled/include/xrpl/beast/insight/Unit.h
Pratik Mankawde 24094e427b fix(telemetry): give each histogram unit its own bucket ladder
This is the change that actually lifts the 5 s ceiling. Until now the
millisecond ladder and the Unit type existed but nothing consumed them.

Telemetry.cpp registered ONE histogram view: instrument name pattern "*",
unit exactly "ms", boundaries {1, 5, ..., 1000, 5000}. Verified against the
installed SDK, "*" matches every name and "ms" matches exactly, so that view
governed every beast::insight Event -- all 54 of them, whatever they measure.
Measured on devnet: 24.9% of rpc_size samples and 100% of jobq_updatepaths
samples fell above 5000. A quantile landing in the `+Inf` bucket reads back
as the second-highest edge, so those p95s reported a flat 5000 rather than a
measurement, and the 1 s to 5 s span was a single four-second-wide bucket
that any quantile inside it had to interpolate across.

Replaces it with one view per unit, keyed on the unit an instrument declares:

- `ms` gets kMillisecondBuckets: every representable edge of the collector's
  spanmetrics ladder, plus 60 s and 120 s. The extensions are deliberate --
  jobq_updatepaths was measured averaging 59,956 ms, which no span
  approaches, so parity alone would still censor it.
- `By` gets kByteBuckets, placed from the measured response distribution
  (mean 2131 B, half under 1 kB, tail mean bounded at 7538 B).

OTelEventImpl now derives its declared unit AND its description from unit()
instead of hardcoding "Duration in ms"/"ms", so rpc_size exports as
rpc_size_bytes on the byte ladder. rpc-pathfinding's "RPC Response Size"
panel follows the rename; its unit was already decbytes and is now truthful.

Also corrects Phase7_taskList.md, which still specified the 5000 ladder as
"matching SpanMetrics". That was true when written and became false when the
collector ladder was extended on its own -- implementing the plan as written
reproduced the bug, so the spec is where the defect had come to live. The
edges now have exactly one owner and the plan points at it.
2026-08-21 12:30:38 +01:00

95 lines
2.9 KiB
C++

#pragma once
#include <cstdint>
namespace beast::insight {
/**
* @brief What an Event's samples measure.
*
* `Event` documents itself as carrying "a millisecond time, or other integral
* value", but both backends used to assume the first case: the OTel bridge
* declared every instrument with unit `ms`, and StatsD tagged every sample
* `|ms`. A size metric therefore exported under a `_milliseconds` name and
* inherited a latency bucket ladder, which censored a quarter of its samples
* and pinned its p95 to a constant.
*
* Naming the unit at creation time is what lets the OTel bridge pick both the
* instrument unit and the matching bucket ladder:
*
* makeEvent("time", Unit::Millis) --> OTel unit "ms" --> millisecond ladder
* makeEvent("size", Unit::Bytes) --> OTel unit "By" --> byte ladder
*
* The StatsD backend deliberately ignores this and keeps emitting `|ms` for
* every Event. That path is retired here -- its UDP port is commented out of
* the compose file and the integration test fails if anything is listening on
* 8125 -- so changing its wire format would alter a legacy contract for no
* local benefit and with no way to verify it.
*
* @note Adding a member requires extending otelUnitCode(), which switches
* exhaustively so a new member is a compile error rather than a silent
* fallthrough to milliseconds.
*/
enum class Unit : std::uint8_t {
/**
* Whole milliseconds. The default, and what every duration Event uses.
*/
Millis,
/**
* A byte count, such as a serialized response size.
*/
Bytes
};
/**
* @brief The OTel (UCUM) unit code for a Unit.
*
* The collector's Prometheus exporter derives the exported metric-name suffix
* from this code, so `ms` yields `_milliseconds` and `By` yields `_bytes`. It
* is also the key the histogram views match on, which is how each unit gets
* its own bucket ladder.
*
* @param unit The unit to translate.
* @return A static, null-terminated UCUM code.
*/
constexpr char const*
otelUnitCode(Unit unit) noexcept
{
switch (unit)
{
case Unit::Bytes:
return "By";
case Unit::Millis:
break;
}
return "ms";
}
/**
* @brief Human-readable description for an instrument of this unit.
*
* Exported alongside the metric, so this is the text an operator reads in a
* metric catalogue. A byte-valued instrument that describes itself as a
* duration is exactly the confusion this whole type exists to remove, so the
* description is derived from the unit rather than written out at each
* instrument site.
*
* @param unit The unit to describe.
* @return A static, null-terminated description.
*/
constexpr char const*
otelUnitDescription(Unit unit) noexcept
{
switch (unit)
{
case Unit::Bytes:
return "Size in bytes";
case Unit::Millis:
break;
}
return "Duration in ms";
}
} // namespace beast::insight