mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 15:28:03 +00:00
These comments dated themselves against this branch or against an earlier revision of the same change, neither of which survives a squash merge. - 'as of this branch' in the pricing-case doc and the pass-through static_assert becomes a statement about what the constants currently produce. - 'Extracted from processGetObjectByHash()' and 'Split from start()' describe edits internal to this change; both now say why the method stands alone. - Recording.h: the mock-abstract mismatch is a standing consequence of a member set that differs between builds, not something that 'has previously' happened. - MetricsRegistry.cpp: describe the loops.txt entry as recording two cycles rather than as what ordering.txt 'previously had'. - InboundLedger.h: the acquire span is the only signal for back-fill cost; it did not 'previously emit' nothing. - check_bucket_parity.py: replace the eleven-phase drift story with the reason the check exists, and point the failure message at HistogramBuckets.h and the collector config instead of OpenTelemetryPlan/, which does not reach develop. Comments and one error message only, no behaviour change.
131 lines
4.9 KiB
Python
Executable File
131 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Assert the C++ millisecond ladder agrees with the collector's spanmetrics ladder.
|
|
|
|
The two are specified to match so a span-derived latency panel and a native
|
|
histogram panel can be read on the same scale. Nothing else couples them, so
|
|
extending one ladder alone -- sub-millisecond edges below 1ms, second-scale
|
|
edges up to 30s -- silently leaves the other short. That failure is quiet:
|
|
Prometheus returns the second-highest edge for a quantile landing in the
|
|
`+Inf` bucket, so every quantile above a too-low ceiling reads back as a flat
|
|
number that looks like a measurement rather than an error. This check is what
|
|
makes the drift loud.
|
|
|
|
The rule is containment, not equality:
|
|
|
|
* every representable collector edge MUST appear in the C++ ladder, so the
|
|
shared range reads identically;
|
|
* the C++ ladder MAY carry extra edges ABOVE the collector's highest edge,
|
|
because jobs outlive spans -- the updatepaths job type was measured
|
|
averaging ~60s, which no span approaches. Demanding equality would force a
|
|
ceiling that censors it, recreating the failure this guards against;
|
|
* collector edges below 1ms are expected to be ABSENT rather than missing:
|
|
beast::insight::Event rounds every duration up to a whole millisecond
|
|
before it reaches the histogram, so those edges could never collect a
|
|
sample.
|
|
|
|
Exit 0 when the ladders agree, 1 with a diff when they do not.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HEADER = Path("include/xrpl/telemetry/HistogramBuckets.h")
|
|
COLLECTOR = Path("docker/telemetry/otel-collector-config.yaml")
|
|
|
|
# beast::insight::Event applies ceil<milliseconds>, so anything below 1ms
|
|
# collapses onto the 1ms edge.
|
|
REPRESENTABLE_FLOOR_MS = 1.0
|
|
|
|
UNIT_TO_MS = {"ms": 1.0, "s": 1000.0}
|
|
|
|
|
|
def collector_edges_ms():
|
|
"""Parse the spanmetrics bucket list, normalising each edge to milliseconds."""
|
|
text = COLLECTOR.read_text()
|
|
match = re.search(r"buckets:\s*\[(.*?)\]", text, re.S)
|
|
if not match:
|
|
sys.exit(f"{COLLECTOR}: no 'buckets:' list found")
|
|
|
|
edges = []
|
|
for raw in match.group(1).split(","):
|
|
token = raw.strip()
|
|
if not token:
|
|
continue
|
|
parsed = re.fullmatch(r"([0-9.]+)(ms|s)", token)
|
|
if not parsed:
|
|
sys.exit(f"{COLLECTOR}: cannot parse bucket edge {token!r}")
|
|
edges.append(float(parsed.group(1)) * UNIT_TO_MS[parsed.group(2)])
|
|
return edges
|
|
|
|
|
|
def cpp_edges_ms():
|
|
"""Parse kMillisecondBuckets out of the header that owns every ladder."""
|
|
text = HEADER.read_text()
|
|
match = re.search(r"kMillisecondBuckets\{(.*?)\};", text, re.S)
|
|
if not match:
|
|
sys.exit(f"{HEADER}: kMillisecondBuckets not found")
|
|
return [
|
|
float(token.strip().replace("'", ""))
|
|
for token in match.group(1).split(",")
|
|
if token.strip()
|
|
]
|
|
|
|
|
|
def main():
|
|
collector = collector_edges_ms()
|
|
cpp = cpp_edges_ms()
|
|
required = [edge for edge in collector if edge >= REPRESENTABLE_FLOOR_MS]
|
|
if not required:
|
|
sys.exit(f"{COLLECTOR}: no edges at or above {REPRESENTABLE_FLOOR_MS} ms")
|
|
collector_top = max(required)
|
|
|
|
missing = [edge for edge in required if edge not in cpp]
|
|
# An extra C++ edge inside the collector's range means the two scales
|
|
# disagree where they overlap. Above the collector's top it is a deliberate
|
|
# extension.
|
|
inside_range = [e for e in cpp if e not in required and e < collector_top]
|
|
|
|
if not missing and not inside_range:
|
|
extensions = [e for e in cpp if e > collector_top]
|
|
summary = f"OK: all {len(required)} representable collector edges present"
|
|
if extensions:
|
|
pretty = ", ".join(f"{e:g}" for e in extensions)
|
|
summary += (
|
|
f"; {len(extensions)} extension edge(s) above "
|
|
f"{collector_top:g} ms: [{pretty}]"
|
|
)
|
|
print(summary)
|
|
return 0
|
|
|
|
print("Bucket ladder parity violated.", file=sys.stderr)
|
|
print(
|
|
f" collector (>= {REPRESENTABLE_FLOOR_MS:g} ms): "
|
|
f"{[f'{e:g}' for e in required]}",
|
|
file=sys.stderr,
|
|
)
|
|
print(
|
|
f" HistogramBuckets.h : {[f'{e:g}' for e in cpp]}", file=sys.stderr
|
|
)
|
|
for edge in missing:
|
|
print(f" MISSING from the C++ ladder: {edge:g} ms", file=sys.stderr)
|
|
for edge in inside_range:
|
|
print(
|
|
f" C++ edge {edge:g} ms lies inside the collector's range but is not "
|
|
"a collector edge -- add it to the collector or drop it here",
|
|
file=sys.stderr,
|
|
)
|
|
print(
|
|
"\nThe two ladders must agree over their shared range. Extra C++ edges are\n"
|
|
"permitted only ABOVE the collector's highest edge. To re-price the shared\n"
|
|
f"range, edit the ladder in {HEADER} and the\n"
|
|
f"spanmetrics 'buckets:' list in {COLLECTOR}\n"
|
|
"in the same change, so both sides stay in step.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|