mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 14:20:56 +00:00
fix(telemetry): address the PR review findings
Four defects from the automated review on PR #7875, each verified against the current tree before fixing (one further comment, the row-63 dashboard overlap, was already fixed by an earlier commit and needed nothing). 1. Rule J could not detect an instrument-kind mismatch. instrument_kinds() wrote `kinds[wire] = ...`, so a wire name created through two different factories kept only the kind visited last and whichever emit site the file walk reached last silently decided the verdict. It now collects a set per name and reports the conflict itself -- one name exporting two instruments is the defect, and no suffix can be correct for both. Added a regression test that builds a name as both a counter and an observable gauge and asserts the message names both. 2. A duplicate connection was reported as `tls_fail`. The TLS handshake had in fact succeeded; PeerFinder simply already held a slot for that address, which is ordinary churn on a healthy node. Conflating the two made a rising `tls_fail` unreadable -- it could mean unreachable peers or merely a busy PeerFinder, and those need opposite responses. Added a distinct `duplicate` outcome and carried the widened vocabulary through every place that enumerates it: the panel description, both filter descriptions, the runbook branch table, the runbook outcome list and the expected_spans note. The `dial_outcome` template variable is a label_values() query, so it picks the new value up on its own. 3. ConnectAttempt::onShutdown had no `operation_aborted` guard, unlike the five other handlers in the same file. A clean teardown was therefore counted as `upgrade_fail`, inflating that outcome on any node shutting down with dials in flight. 4. ValidatorSite used the raw configured URI as a Prometheus label. [validator_list_sites] accepts credentials in the URI and ParsedUrl keeps them in username/password, so a configured `https://user:pass@host` would have copied the secret into a metric label and on into the collector, Prometheus and every dashboard. The label is now rebuilt from scheme, host, port and path -- everything needed to tell one site apart, and nothing more. Verified: naming checker exits 0 with Rule J still passing all 40 real instrument names; its unit tests now number 139 and all pass; 15 dashboards validate; both workload JSON files parse; clang-tidy over the full compile database reports no finding on either changed .cpp; pre-commit passes. Not verified: not compiled. Item 4 introduces string concatenation and item 2 a new constexpr, so CI's build is the first real check on both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
30
.github/scripts/otel-naming/check_otel_naming.py
vendored
30
.github/scripts/otel-naming/check_otel_naming.py
vendored
@@ -1293,9 +1293,9 @@ def iter_sources(root: Path) -> List[Path]:
|
||||
]
|
||||
|
||||
|
||||
def instrument_kinds(root: Path, wire_by_symbol: Dict[str, str]) -> Dict[str, str]:
|
||||
"""Map each declared instrument's WIRE name to its OTel instrument kind, by
|
||||
looking at how the emit sites actually create it.
|
||||
def instrument_kinds(root: Path, wire_by_symbol: Dict[str, str]) -> Dict[str, Set[str]]:
|
||||
"""Map each declared instrument's WIRE name to the set of OTel instrument
|
||||
kinds its emit sites actually create it with.
|
||||
|
||||
The kind is what decides which suffix is correct, so it must be read from
|
||||
the emit site rather than guessed from the name -- guessing from words like
|
||||
@@ -1303,10 +1303,15 @@ def instrument_kinds(root: Path, wire_by_symbol: Dict[str, str]) -> Dict[str, st
|
||||
label VALUES (e.g. `nodestore_state` observing `write_mean_us`), which is
|
||||
a legitimate shape, not a violation.
|
||||
|
||||
Returns one of `counter`, `histogram`, `gauge`, `updown` per wire name.
|
||||
A name whose emit site is not found is absent from the result, so Rule J
|
||||
checks only the shape-independent rules for it."""
|
||||
kinds: Dict[str, str] = {}
|
||||
A SET rather than one kind per name, because one wire name created through
|
||||
two different factories is itself the defect worth reporting: the SDK would
|
||||
export two instruments under one name and the collector would see whichever
|
||||
arrived last. Recording only the last kind visited hid exactly that case.
|
||||
|
||||
Values are drawn from `counter`, `histogram`, `gauge`, `updown`. A name whose
|
||||
emit site is not found is absent from the result, so Rule J checks only the
|
||||
shape-independent rules for it."""
|
||||
kinds: Dict[str, Set[str]] = {}
|
||||
for path in iter_sources(root):
|
||||
if path.name == "MetricMacros.h" or path.name.endswith("MetricNames.h"):
|
||||
continue
|
||||
@@ -1330,7 +1335,7 @@ def instrument_kinds(root: Path, wire_by_symbol: Dict[str, str]) -> Dict[str, st
|
||||
)
|
||||
if wire is None:
|
||||
continue
|
||||
kinds[wire] = classify_instrument_kind(kind)
|
||||
kinds.setdefault(wire, set()).add(classify_instrument_kind(kind))
|
||||
return kinds
|
||||
|
||||
|
||||
@@ -1404,7 +1409,14 @@ def run_rule_j_metric_suffixes(root: Path, report: Report) -> None:
|
||||
if name.startswith(("xrpld_", "xrpl_")):
|
||||
flag(name, "drop the prefix; the exporter adds it")
|
||||
continue
|
||||
kind = kinds.get(name)
|
||||
found_kinds = kinds.get(name) or set()
|
||||
if len(found_kinds) > 1:
|
||||
# One wire name created through two factories exports two
|
||||
# instruments under one name; no suffix can be right for both, so
|
||||
# report the conflict itself rather than picking one arbitrarily.
|
||||
flag(name, f"created as {' and '.join(sorted(found_kinds))}; pick one kind")
|
||||
continue
|
||||
kind = next(iter(found_kinds), None)
|
||||
if kind == "counter" and not name.endswith(METRIC_COUNTER_SUFFIX):
|
||||
flag(name, "counter must end in _total")
|
||||
elif kind == "histogram" and not name.endswith(METRIC_DURATION_SUFFIXES):
|
||||
|
||||
@@ -1264,6 +1264,23 @@ class RuleJMetricSuffixes(unittest.TestCase):
|
||||
# shape-independent rules apply -- a bare gauge-ish name is fine.
|
||||
self.assertEqual(self._run(_mc("syncState", "sync_state")), [])
|
||||
|
||||
def test_one_name_created_as_two_kinds_is_flagged(self):
|
||||
# One wire name built through two different factories exports two
|
||||
# instruments under a single name, and no suffix can satisfy both. The
|
||||
# kind map therefore records a SET per name: keeping only the last kind
|
||||
# visited silently hid this, because whichever emit site the walk
|
||||
# reached last decided the verdict.
|
||||
violations = self._run(
|
||||
_mc("dualKind", "dual_kind_total"),
|
||||
'meter_->CreateUInt64Counter(metric::dualKind, "d");\n'
|
||||
'meter_->CreateInt64ObservableGauge(metric::dualKind, "d");\n',
|
||||
)
|
||||
self.assertEqual(len(violations), 1, violations)
|
||||
# The message names both kinds, so the reader sees the conflict rather
|
||||
# than a suffix complaint that would contradict one of the two sites.
|
||||
self.assertIn("counter", violations[0][-1])
|
||||
self.assertIn("gauge", violations[0][-1])
|
||||
|
||||
def test_skip_when_no_header(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user