mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-22 14:50:54 +00:00
Adversarial validation of the previous commit found one of its two code fixes
was diagnosed wrongly and the other incomplete. Both are corrected here, along
with the layers the first pass missed.
1. The new dial outcome was named for the wrong condition. It was added as
`duplicate` on the belief that PeerFinder had already granted a slot for the
address. It has not: `Logic::onConnected` contains exactly ONE false-returning
path and it is the self-connect check, which logs "Logic dropping as self
connect" (include/xrpl/peerfinder/detail/Logic.h). The duplicate check lives
in `newOutboundSlot`, evaluated before a ConnectAttempt exists, so a real
duplicate can never reach this branch.
That mattered beyond the name: the previous commit told operators the outcome
was benign churn to ignore, when it actually reports a local misconfiguration
-- this node has its own address in [ips_fixed] or behind its advertised
endpoint, and every dial to it is wasted. Renamed to `self_connection`,
reusing the slug `handshake_negotiation_fail_total` already publishes for the
same fault so it reads identically on both signals, and every description
corrected to say so. The fail() string now reads "Self connection" too.
The first pass also missed three enforcement and contract sites: the
ConnectAttempt.h Doxygen state machine (which still mapped the slot branch
onto tls_fail), the LedgerSpanNames unit test (which pinned exactly five
values over a std::array<..., 5> and so left the new member untested), and the
span-derived twin panel plus two reference docs that still published the old
five-value domain.
2. The credential-free site label was incomplete twice over.
- It appended the port, and `Resource::Resource` DEFAULTS that to 443/https
and 80/http when the config omits one. The label would have become
`https://vl.ripple.com:443/` where Grafana Cloud currently holds
`https://vl.ripple.com`, silently renaming the series for every deployment
already scraping this metric. Verified against live label values before and
after; the port is now omitted.
- parseUrl's path group is `(/.*)?`, greedy to end of string, so a query or
fragment lands inside `path`. A list URL authenticated by `?token=...` would
have leaked exactly as userinfo did. The path is now truncated at the first
'?' or '#'.
Also updated the MetricNames.h usage example, which still taught the raw-URI
pattern to the next author, and the 09-doc row that described the label as the
configured URI.
3. Rule J hardening from the same review: `classify_instrument_kind` returns an
`other` sentinel for a non-factory macro, and storing it in the kind set could
render a future conflict as "created as counter and other". The sentinel is
now skipped, keeping it doing what it already did -- matching no shape rule.
Added a second regression test whose input the pre-fix code reported as CLEAN
(gauge-then-histogram on a `_us` name), so the guard is proven by a 0-vs-1
difference and not only by a changed message. Both new tests were run against
a reconstructed last-wins implementation and both fail against it.
Documented the conflict class in the Rule J rows of the checker README and
CONTRIBUTING, which previously described only the suffix conventions.
Verified: naming checker exits 0 with Rule J passing all 40 real names; 140
checker tests pass; 15 dashboards validate; both workload JSON files parse;
clang-tidy over the full compile database reports no finding on any changed line
of ConnectAttempt.cpp or ValidatorSite.cpp; pre-commit passes.
Not verified: not compiled. The label change adds string truncation and the
outcome rename touches a constexpr used across three translation units, so CI's
build remains the first real check on both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
83 lines
12 KiB
Markdown
83 lines
12 KiB
Markdown
# OTel naming-consistency check
|
|
|
|
`check_otel_naming.py` enforces the OpenTelemetry span-attribute naming
|
|
convention documented in
|
|
[CONTRIBUTING.md](../../../CONTRIBUTING.md#telemetry-span-attribute-naming)
|
|
across every layer of the telemetry pipeline. The `*SpanNames.h` constants are
|
|
the single source of truth (L1); every other layer must agree with them.
|
|
|
|
## Running locally
|
|
|
|
```
|
|
python .github/scripts/otel-naming/check_otel_naming.py
|
|
```
|
|
|
|
It takes no arguments, can be run from any directory inside the repo, and uses
|
|
only the Python standard library (no `pip install`, matching the levelization
|
|
check). A non-zero exit code means a violation was found; the output lists each
|
|
violation as `RULE | location | token | expected`.
|
|
|
|
## What it checks
|
|
|
|
The valid key set is **derived dynamically from the OTel code** — there is no
|
|
hardcoded allowlist:
|
|
|
|
- **L1 keys** come from the `namespace attr { ... }` blocks of every
|
|
`*SpanNames.h`, resolving the `makeStr("x")` / `join(seg::a, seg::b)` DSL
|
|
(cross-file, so `join(seg::rpc, ...)` resolves `seg::rpc` from the base
|
|
`SpanNames.h`). Each constant is resolved against **its own** header, so two
|
|
headers that define a same-named constant (e.g. a base `attr::ledgerHash` and
|
|
a domain `attr::ledgerHash`) each contribute their real wire key — a later
|
|
header cannot clobber an earlier one's value in a flat table.
|
|
- **Legitimate dotted keys** = ONLY the keys the code actually sets as resource
|
|
attributes, i.e. the entries inside `Telemetry.cpp`'s `Resource::Create({...})`
|
|
call: the `semconv::service::*` keys (`service.*`) plus any `attr::<name>`
|
|
constants passed there (`xrpl.network.*`). A dotted key that is _declared_ in a
|
|
header but never set as a resource attr is a span attribute in resource
|
|
clothing — a Rule-A violation, even if it lives in the base `SpanNames.h`.
|
|
- **L1-metrics** — instrument names, label keys and bounded label values come
|
|
from the `namespace metric` / `namespace label` / `namespace lval` blocks of
|
|
every `*MetricNames.h`, read as `inline constexpr char NAME[] = "wire";`.
|
|
These headers deliberately do **not** use the `makeStr`/`StaticStr` DSL the
|
|
span headers use: the OTel C++ API takes `nostd::string_view`, which
|
|
constructs from `char const*` but has no constructor from
|
|
`std::string_view`, so a `StaticStr` will not compile in an instrument-name
|
|
or label-key position.
|
|
|
|
### Rules (each fails the build, when its inputs are present)
|
|
|
|
| Rule | Check |
|
|
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| A | No stray dotted span-attribute key (only the derived resource keys may be dotted). |
|
|
| G | Attribute keys are `lower_snake_case` (`^[a-z][a-z0-9_]*$` per dot-segment) — no camelCase, UPPERCASE, or spaces. |
|
|
| F | No string literals as attribute keys or span-name arguments in `setAttribute`/`addEvent`/`span`/`rootSpan`/`childSpan` (`rootSpan` shares `span`'s `(cat, prefix, name)` signature). Attribute _values_ are exempt (runtime data); `*SpanNames.h` definitions and test files are exempt. |
|
|
| B | Every collector `spanmetrics.dimensions` name exists in the L1 key set. |
|
|
| C | Every Tempo span-filter tag exists in the L1 key set. |
|
|
| D | Every dashboard label resolves to an L1 span attribute, a native-metric label (L6, emitted by MetricsRegistry), or a Prometheus/Grafana builtin. TraceQL scope prefixes (`span.`/`resource.`/…) are stripped before the L1 lookup. |
|
|
| E | No dotted `xrpl.<domain>.<field>` attribute key in the runbook (only the L1 resource attrs `xrpl.network.*` may be dotted). Span names, filenames, OTel-standard keys, and metric labels are not flagged. |
|
|
| I | No string literals as **metric** instrument names or label keys — the mirror of Rule F. Applies to the name passed to an `XRPL_METRIC_*` macro or a `meter->Create*` factory and to the label _keys_ in its label set. Label _values_, descriptions, `*MetricNames.h`, `MetricMacros.h` and test files are exempt. Scoped by metric **family** (first underscore segment): declaring a constant opts that family in, so the metric surface can be converted subsystem by subsystem. Unconverted families warn as Rule L. |
|
|
| J | Metric instrument names follow the suffix conventions: `lower_snake_case`, no `xrpld_`/`xrpl_` prefix (the exporter adds it), a counter ends `_total`, a histogram ends `_us`/`_ms`/`_seconds`, a gauge does not end `_total`. The instrument **kind** is read from the emit site, never guessed from words in the name — so a multi-series gauge carrying units in its label values (e.g. `nodestore_state` observing `write_mean_us`) is not a violation. A name created through two different factories is itself reported as a kind conflict, since no suffix can be correct for both. |
|
|
| K | Every metric named in `docker/telemetry/workload/expected_metrics.json` resolves to a declared constant, so a rename in code cannot leave the workload validator asserting a name nothing emits. PromQL selectors (`m{label="v"}`) and exporter-appended histogram suffixes (`_bucket`/`_count`/`_sum`) are normalized away first; groups fed by another emit path (`statsd_gauges`, `statsd_counters`, `spanmetrics`) are out of scope by design. |
|
|
|
|
Rule F runs **unconditionally** (it is a purely syntactic check on the
|
|
call-sites and needs no `*SpanNames.h`), so a code path that calls
|
|
`SpanGuard::span`/`setAttribute` directly without ever defining a header is
|
|
still caught.
|
|
|
|
### Warnings (printed, never fail the build)
|
|
|
|
| Rule | Check |
|
|
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| H | A namespace-qualified constant (e.g. `foo::bar::myKey`) used at a telemetry call-site is not defined in any `*SpanNames.h`. The constant should live in the proper header; defining it in-place bypasses rules A/G/F. Warns rather than fails — the argument may be a legitimately dynamic value, and the header may live on a later branch. Bare locals and `std::` names are not warned. |
|
|
| L | A literal metric name in a family that has no `*MetricNames.h` constants yet. Rule I's ratchet defers these instead of failing the build on the whole pre-existing metric surface at once; the warning keeps the outstanding conversion work visible rather than silently accepted. |
|
|
|
|
## Presence-gated
|
|
|
|
Every rule runs **only when the source files it needs are present** in the tree
|
|
and is otherwise skipped (printed as `SKIP: <rule> — <reason>`), never failed.
|
|
This keeps the check correct no matter how telemetry work is split across PRs —
|
|
a stacked chain, one large PR, or independent per-stage PRs where (for example)
|
|
the collector config lands before the dashboards. The collector/Tempo/dashboard/
|
|
runbook layers are introduced in later phases; on a branch without them, only
|
|
the L1-intrinsic rules (A, G, F) run.
|