Merge branch 'pratik/otel-phase7-native-metrics' into pratik/otel-phase8-log-correlation

This commit is contained in:
Pratik Mankawde
2026-06-11 23:18:36 +01:00
13 changed files with 323 additions and 82 deletions

View File

@@ -25,11 +25,16 @@ 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`).
- **Legitimate dotted keys** = the resource attrs declared in the base
`SpanNames.h` (`xrpl.network.*`) plus the `semconv::service::*` keys the code
passes to `Resource::Create()` in `Telemetry.cpp` (`service.*`). A dotted key
declared in any other header is a violation.
`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`.
### Rules (each fails the build, when its inputs are present)

View File

@@ -297,22 +297,39 @@ def attr_namespace_spans(text: str) -> List[str]:
def attr_keys_from_header(path: Path, symbols: Dict[str, str]) -> Set[str]:
"""Return the set of attribute-key strings declared in a header's
`namespace attr { ... }` block(s). `symbols` is the global cross-file
table so `join(seg::rpc, ...)` style dotted attrs resolve correctly.
table, used ONLY to seed `seg::`/segment references for `join(...)`
resolution — never to look up an attr constant's value.
A constant DEFINED in this header is resolved against this header's OWN
text, so two headers that each define a same-named constant (e.g. the base
`attr::ledgerHash = xrpl.ledger.hash` and consensus
`attr::ledgerHash = ledger_hash`) each report their real wire key. The
global table is keyed by bare name and would otherwise let a later header
clobber an earlier one, erasing the real key from L1 (a Rule-A blind spot).
A `using`-re-export, by contrast, imports a constant defined elsewhere, so
it is resolved against the global table.
Comments are stripped first (a commented constant must not enter L1), and
each attr block is brace-matched over the whole text so multi-line
`inline constexpr auto NAME = join(\\n ...);` definitions are captured."""
text = strip_comments(read_source(path))
# Local table: the global segments/symbols seed cross-file `join` parts,
# then this header's own definitions overwrite any same-named global entry
# so a locally-defined attr resolves to ITS value, not another header's.
local = dict(symbols)
resolve_constants(text, local)
keys: Set[str] = set()
for block in attr_namespace_spans(text):
for md in CONST_DEF.finditer(block):
# Resolve the named constant against the global symbol table; this
# captures makeStr("x"), join(seg::y, ...), and `using ::a::b` forms.
val = symbols.get(md.group(1))
# Resolve a locally-defined constant against the LOCAL table; this
# captures makeStr("x") and join(seg::y, ...) with the header's own
# value, immune to cross-header bare-name collisions.
val = local.get(md.group(1))
if val is not None:
keys.add(val)
# `using ::ns::attr::field;` re-exports a base constant into this attr
# block (e.g. RpcSpanNames imports txHash). Resolve the imported name.
# `using ::ns::attr::field;` re-exports a constant defined in ANOTHER
# header (e.g. PeerSpanNames imports the base ledgerHash). Resolve the
# imported name against the global table.
for um in USING_DECL.finditer(block):
val = symbols.get(um.group(1))
if val is not None:
@@ -400,10 +417,12 @@ def main() -> None:
keys_by_header = {}
# --- Derive the legitimate dotted (resource) keys dynamically ----------
# ONLY the base SpanNames.h resource attrs + the semconv keys Telemetry.cpp
# passes to Resource::Create(). A dotted key from any other header is a
# violation, not an allowlist entry.
dotted_allow = derive_dotted_resource_keys(root, keys_by_header, report)
# ONLY the keys actually passed to Resource::Create() in Telemetry.cpp
# (semconv service.* + the attr:: constants set there, e.g. xrpl.network.*).
# A dotted key declared in a header but NOT set as a resource attr is a
# Rule-A violation, not an allowlist entry.
resource_symbols = symbols if headers else {}
dotted_allow = derive_dotted_resource_keys(root, resource_symbols, report)
# --- Rule A: no stray dotted span-attribute keys -----------------------
if l1_keys:
@@ -434,35 +453,63 @@ def main() -> None:
report.render_and_exit()
def resource_create_block(text: str) -> str:
"""Return the text inside the first `Resource::Create({ ... })` argument
list, brace-matched so nested `{key, value}` initializers are contained.
Empty string if the call is absent."""
m = re.search(r"Resource::Create\(\s*\{", text)
if not m:
return ""
i = m.end() # one char past the opening `{`
depth, start = 1, i
while i < len(text) and depth > 0:
c = text[i]
if c == "{":
depth += 1
elif c == "}":
depth -= 1
i += 1
return text[start : i - 1]
def derive_dotted_resource_keys(
root: Path, keys_by_header: Dict[Path, Set[str]], report: Report
root: Path, symbols: Dict[str, str], report: Report
) -> Set[str]:
"""Legitimate dotted keys = dotted attrs declared ONLY in the base
SpanNames.h (xrpl.network.*) plus the standard semconv keys the code
passes to Resource::Create() in Telemetry.cpp (service.*). Dotted keys
declared in any OTHER header are NOT allowlisted they are Rule-A
violations."""
"""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 standard semconv keys (`service.*`) plus any `attr::<name>`
constants passed there (resolved to their wire key via the global symbol
table, e.g. `attr::networkId` -> `xrpl.network.id`).
A dotted key DECLARED in a `*SpanNames.h` header but NOT passed to
Resource::Create() is a span attribute wearing the resource form — a Rule-A
violation, never allowlisted. Deriving the allowlist from the actual
resource call (not from "any dotted key in the base header") is what lets
Rule A catch a stray dotted span attr such as `xrpl.ledger.hash`."""
allow: Set[str] = set()
for h, keys in keys_by_header.items():
if h.name == "SpanNames.h": # the base header holds resource attrs
allow |= {k for k in keys if "." in k}
tele = root / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp"
if tele.is_file():
text = read_source(tele)
# semconv::<group>::k<CamelKey> -> the dotted OTel-standard key. The
# CamelKey already embeds the group, e.g. service::kServiceInstanceId
# -> service.instance.id. Split the CamelCase name into dotted lowercase
# segments; if it does not lead with the group, prepend the group.
for m in re.finditer(r"semconv::(\w+)::k(\w+)", text):
group, camel = m.group(1), m.group(2)
segments = camel_to_dotsegments(camel)
if segments and segments[0] == group:
allow.add(".".join(segments))
else:
allow.add(group + "." + ".".join(segments))
report.ok(f"resource dotted-key allowlist derived: {sorted(allow)}")
else:
if not tele.is_file():
report.skip("resource-derive", "Telemetry.cpp not present")
return allow
block = resource_create_block(read_source(tele))
# semconv::<group>::k<CamelKey> -> the dotted OTel-standard key. The
# CamelKey already embeds the group, e.g. service::kServiceInstanceId
# -> service.instance.id. Split the CamelCase name into dotted lowercase
# segments; if it does not lead with the group, prepend the group.
for m in re.finditer(r"semconv::(\w+)::k(\w+)", block):
group, camel = m.group(1), m.group(2)
segments = camel_to_dotsegments(camel)
if segments and segments[0] == group:
allow.add(".".join(segments))
else:
allow.add(group + "." + ".".join(segments))
# attr::<name> constants set as resource attrs (e.g. networkId/networkType);
# resolve each to its wire key and allowlist only the dotted ones.
for m in re.finditer(r"attr::(\w+)", block):
val = symbols.get(m.group(1))
if val is not None and "." in val:
allow.add(val)
report.ok(f"resource dotted-key allowlist derived: {sorted(allow)}")
return allow

View File

@@ -326,6 +326,162 @@ class CamelToDotSegments(unittest.TestCase):
shutil.rmtree(d)
class SymbolCollision(unittest.TestCase):
"""attr_keys_from_header must resolve a constant against ITS OWN header, so
two headers defining a same-named constant each report their real wire key.
Regression for the flat-symbol-table collision that let a later header
clobber an earlier one and erased a dotted key from L1 (a Rule-A blind
spot)."""
def _build(self, files):
d = Path(tempfile.mkdtemp())
paths = {}
for rel, text in files.items():
p = d / rel
_write(p, text)
paths[rel] = p
return d, paths
def test_same_named_const_not_clobbered_across_headers(self):
base = (
"#pragma once\n"
"namespace xrpl::telemetry {\n"
'namespace seg { inline constexpr auto xrpl = makeStr("xrpl");\n'
'inline constexpr auto ledger = makeStr("ledger"); }\n'
"namespace attr {\n"
"inline constexpr auto ledgerHash = "
'join(join(seg::xrpl, seg::ledger), makeStr("hash"));\n'
"}\n}\n"
)
cons = (
"#pragma once\n"
"namespace xrpl::telemetry::consensus::span {\n"
"namespace attr { inline constexpr auto ledgerHash = "
'makeStr("ledger_hash"); }\n}\n'
)
d, paths = self._build(
{
"include/xrpl/telemetry/SpanNames.h": base,
"src/xrpld/consensus/ConsensusSpanNames.h": cons,
}
)
try:
headers = chk.find_spanname_headers(d)
syms = chk.build_global_symbols(headers)
by_name = {p.name: chk.attr_keys_from_header(p, syms) for p in headers}
# The base header keeps its dotted key; consensus keeps the bare one.
self.assertIn("xrpl.ledger.hash", by_name["SpanNames.h"])
self.assertEqual(by_name["ConsensusSpanNames.h"], {"ledger_hash"})
finally:
shutil.rmtree(d)
def test_using_reexport_still_resolves_globally(self):
# A `using`-re-export imports a constant defined elsewhere; it must
# resolve against the global table, not the local header.
base = (
"#pragma once\n"
"namespace xrpl::telemetry {\n"
"namespace attr { inline constexpr auto txHash = "
'makeStr("tx_hash"); }\n}\n'
)
dom = (
"#pragma once\n"
"namespace xrpl::telemetry::tx::span {\n"
"namespace attr { using ::xrpl::telemetry::attr::txHash; }\n}\n"
)
d, paths = self._build(
{
"include/xrpl/telemetry/SpanNames.h": base,
"src/xrpld/app/misc/TxSpanNames.h": dom,
}
)
try:
headers = chk.find_spanname_headers(d)
syms = chk.build_global_symbols(headers)
keys = chk.attr_keys_from_header(
paths["src/xrpld/app/misc/TxSpanNames.h"], syms
)
self.assertEqual(keys, {"tx_hash"})
finally:
shutil.rmtree(d)
class ResourceAllowlistScope(unittest.TestCase):
"""derive_dotted_resource_keys must allowlist ONLY the dotted keys actually
passed to Resource::Create() — not every dotted key in the base header. A
dotted attr declared in a header but not set as a resource attr is a Rule-A
violation."""
def _derive(self, tele_text, span_text):
d = Path(tempfile.mkdtemp())
try:
_write(d / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp", tele_text)
_write(d / "include" / "xrpl" / "telemetry" / "SpanNames.h", span_text)
headers = chk.find_spanname_headers(d)
syms = chk.build_global_symbols(headers)
allow = chk.derive_dotted_resource_keys(d, syms, chk.Report())
return allow, syms, headers, d
except Exception:
shutil.rmtree(d)
raise
def test_dotted_span_attr_not_allowlisted_and_flagged(self):
span = (
"#pragma once\n"
"namespace xrpl::telemetry {\n"
'namespace seg { inline constexpr auto xrpl = makeStr("xrpl");\n'
'inline constexpr auto ledger = makeStr("ledger");\n'
'inline constexpr auto network = makeStr("network"); }\n'
"namespace attr {\n"
"inline constexpr auto networkId = "
'join(join(seg::xrpl, seg::network), makeStr("id"));\n'
"inline constexpr auto ledgerHash = "
'join(join(seg::xrpl, seg::ledger), makeStr("hash"));\n'
"}\n}\n"
)
tele = (
"auto r = resource::Resource::Create({\n"
" {semconv::service::kServiceName, x},\n"
" {std::string(attr::networkId), n},\n"
"});\n"
)
allow, syms, headers, d = self._derive(tele, span)
try:
# networkId IS a resource attr; ledgerHash is NOT, despite living in
# the base header.
self.assertIn("xrpl.network.id", allow)
self.assertNotIn("xrpl.ledger.hash", allow)
kbh = {h: chk.attr_keys_from_header(h, syms) for h in headers}
report = chk.Report()
chk.run_rule_a(kbh, allow, report)
self.assertEqual([v[2] for v in report.violations], ["xrpl.ledger.hash"])
finally:
shutil.rmtree(d)
def test_resource_block_brace_matched(self):
# A nested {key,value} initializer must not truncate the block scan.
tele = (
"auto r = resource::Resource::Create({\n"
" {semconv::service::kServiceName, x},\n"
" {std::string(attr::networkType), t},\n"
"});\n"
)
span = (
"#pragma once\n"
"namespace xrpl::telemetry {\n"
'namespace seg { inline constexpr auto xrpl = makeStr("xrpl");\n'
'inline constexpr auto network = makeStr("network"); }\n'
"namespace attr { inline constexpr auto networkType = "
'join(join(seg::xrpl, seg::network), makeStr("type")); }\n}\n'
)
allow, _syms, _headers, d = self._derive(tele, span)
try:
self.assertIn("xrpl.network.type", allow)
self.assertIn("service.name", allow)
finally:
shutil.rmtree(d)
def _run_rule_a(keys_by_header, allow):
report = chk.Report()
chk.run_rule_a(keys_by_header, allow, report)

View File

@@ -307,14 +307,25 @@ across the code, the OTel collector, Tempo, Grafana dashboards, and docs. The
constants in the `*SpanNames.h` headers are the single source of truth; every
other layer must match them. A CI check enforces this end to end.
1. Per-span unique attribute: bare field name — the span name already carries
the domain (e.g. `command`, `local`, `version` on `rpc.command` / `tx.process`).
2. Collision qualifier: `<domain>_<field>` when a bare name would collide across
domains (in the shared spanmetrics label space) or with the OTel-reserved
`status` key (e.g. `rpc_status`, `grpc_status`, `proposal_trusted`,
`validation_trusted`).
3. Shared cross-span attribute: `<domain>_<field>` underscore form
(e.g. `tx_hash`, `peer_id`, `ledger_seq`, `consensus_round`).
1. Per-span unique attribute: bare field name — allowed when the field is
recorded by a single span/workflow, so the span name already supplies the
domain (e.g. `command`, `local`, `version` on `rpc.command` / `tx.process`).
2. Shared attribute (same concept on more than one span): ONE key, reused
verbatim on every span that records it — the span name tells the occurrences
apart, so no per-emitter prefix is added. Pick the name by the field's
meaning: a property of a domain object keeps that object's bare field name
(`ledger_hash`, `ledger_seq`, `tx_hash`, `peer_id`, `full_validation`); a
field already qualified by a sub-kind keeps that qualifier on every emitter
(`proposal_trusted` on both `consensus.proposal.receive` and
`peer.proposal.receive`; `validation_trusted` likewise). Define it once in
the base `SpanNames.h` `namespace attr` block and re-export (`using`) it from
each domain header, so all emitters share the exact string.
3. Collision qualifier: `<domain>_<field>` — only when a bare name would collide
with a DIFFERENT concept in the shared spanmetrics label space, or with the
OTel-reserved `status` key (e.g. `rpc_status`, `grpc_status`,
`consensus_state`, `consensus_round`). This disambiguates distinct concepts
that share a word; it is NOT used to tag the same concept with the workflow
that emitted it — that is rule 2 (one shared name).
4. Resource attribute: dotted `xrpl.<subsystem>.<field>` — reserved ONLY for
process/network identity set once at startup (`xrpl.network.id`,
`xrpl.network.type`). Never use the dotted `xrpl.` form for span attributes.

View File

@@ -170,16 +170,24 @@ headers are the single source of truth; the collector, Tempo, the Grafana
dashboards, and the runbook all consume these exact keys, so every layer must
agree with the code. A CI check enforces this end to end.
1. **Per-span unique attribute** → bare field name. The span name already
carries the domain, so no prefix is needed (e.g. `command`, `version`,
`local` on `rpc.command`).
2. **Collision qualifier**`<domain>_<field>` when a bare name would collide
across domains in the shared spanmetrics label space, or with the
OTel-reserved `status` key (e.g. `rpc_status`, `grpc_status`,
`proposal_trusted`, `validation_trusted`).
3. **Shared cross-span attribute**`<domain>_<field>` underscore form, used
wherever the same field appears on more than one span (e.g. `tx_hash`,
`peer_id`, `ledger_seq`, `consensus_round`, `consensus_mode`).
1. **Per-span unique attribute** → bare field name, allowed when the field is
recorded by a single span/workflow so the span name already supplies the
domain (e.g. `command`, `version`, `local` on `rpc.command`).
2. **Shared attribute (same concept on more than one span)** → ONE key, reused
verbatim on every span that records it; the span name tells the occurrences
apart, so no per-emitter prefix is added. Name it by the field's meaning: a
property of a domain object keeps that object's bare field name (`ledger_hash`,
`ledger_seq`, `tx_hash`, `peer_id`, `full_validation`); a field already
qualified by a sub-kind keeps that qualifier on every emitter (`proposal_trusted`
on both `consensus.proposal.receive` and `peer.proposal.receive`;
`validation_trusted` likewise). Defined once in the base `SpanNames.h`
`namespace attr` block and re-exported (`using`) by each domain header.
3. **Collision qualifier**`<domain>_<field>`, only when a bare name would
collide with a DIFFERENT concept in the shared spanmetrics label space or with
the OTel-reserved `status` key (e.g. `rpc_status`, `grpc_status`,
`consensus_state`, `consensus_round`, `consensus_mode`). This disambiguates
distinct concepts that share a word; it is NOT used to tag the same concept
with its emitting workflow — that is rule 2 (one shared name).
4. **Resource attribute** → dotted `xrpl.<subsystem>.<field>`, reserved ONLY
for process/network identity set once at startup (`xrpl.network.id`,
`xrpl.network.type`). Span attributes are never dotted in the `xrpl.` form —

View File

@@ -239,14 +239,14 @@ aggregation. Per the 2026-05-13 naming redesign, span-attribute keys use the
`<domain>_<field>` underscore form where a bare name would collide (e.g.
`rpc_status`, `grpc_status`, `tx_status`, `txq_status`).
> **Dotted exceptions** (do not confuse with span attributes):
> **Dotted keys are resource attributes, never span attributes:**
>
> - `xrpl.ledger.hash` is the **only** dotted span attribute. It is a shared
> constant set on `peer.validation.receive`. Note that `consensus.validation.send`
> uses the **bare** `ledger_hash` instead.
> - `xrpl.network.id` and `xrpl.network.type` are **resource** attributes set
> once at startup on the OTel resource — not span attributes. They appear on
> every span's resource scope, queried as `{resource.xrpl.network.id=...}`.
> - The ledger hash uses the bare `ledger_hash` key on every span that records
> it (both `consensus.validation.send` and `peer.validation.receive`) — there
> is no dotted span attribute.
#### RPC Attributes
@@ -359,7 +359,7 @@ aggregation. Per the 2026-05-13 naming redesign, span-attribute keys use the
| `close_time_vote_bins` | string | `consensus.accept.apply` | Distribution of close-time votes |
| `resolution_direction` | string | `consensus.accept.apply` | Whether close resolution increased/decreased/unchanged |
| `tx_count` | int64 | `consensus.accept.apply` | Transactions in the accepted set |
| `ledger_hash` | string | `consensus.validation.send` | Full hash of the validated ledger (**bare**, not dotted) |
| `ledger_hash` | string | `consensus.validation.send` | Full hash of the validated ledger (shared with peer) |
| `full_validation` | boolean | `consensus.validation.send` | Whether this is a full validation |
| `validation_sign_time` | int64 | `consensus.validation.send` | Validation signing time |
| `mode_old` | string | `consensus.mode_change` | Operating mode before the transition |
@@ -393,8 +393,8 @@ the parent `ledger.build` carries `ledger_seq` and the close-time attributes.
| `peer_id` | int64 | `tx.receive`, `peer.proposal.receive`, `peer.validation.receive` | Peer identifier |
| `proposal_trusted` | boolean | `peer.proposal.receive` | Whether the proposal came from a trusted validator |
| `validation_trusted` | boolean | `peer.validation.receive` | Whether the validation came from a trusted validator |
| `validation_full` | boolean | `peer.validation.receive` | Whether the validation is a full validation |
| `xrpl.ledger.hash` | string | `peer.validation.receive` | Validated ledger hash (**dotted** — shared constant) |
| `full_validation` | boolean | `peer.validation.receive` | Whether the validation is a full validation |
| `ledger_hash` | string | `peer.validation.receive` | Validated ledger hash (shared with consensus spans) |
**Prometheus labels**: `proposal_trusted`, `validation_trusted` (SpanMetrics dimensions).

View File

@@ -121,8 +121,8 @@ RED metrics on the _Transaction Overview_ dashboard.
| `consensus.accept.apply` | RCLConsensus.cpp | `ledger_seq`, `close_time`, `close_time_correct`, `close_resolution_ms`, `consensus_state`, `proposing`, `round_time_ms`, `parent_close_time`, `close_time_self`, `close_time_vote_bins`, `resolution_direction`, `tx_count` | Ledger application with close time details (see Events below) |
| `consensus.validation.send` | RCLConsensus.cpp | `ledger_seq`, `proposing`, `ledger_hash`, `full_validation`, `validation_sign_time` | Validation sent after accept (follows-from link) |
| `consensus.mode_change` | RCLConsensus.cpp | `mode_old`, `mode_new` | Consensus mode transition |
| `consensus.proposal.receive` | PeerImp.cpp | `trusted`, `consensus_round` | Proposal received from peer (extracts parent context from TraceContext when present; falls back to standalone span for older peers) |
| `consensus.validation.receive` | PeerImp.cpp | `trusted`, `ledger_seq` | Validation received from peer (extracts parent context from TraceContext when present; falls back to standalone span for older peers) |
| `consensus.proposal.receive` | PeerImp.cpp | `proposal_trusted`, `consensus_round` | Proposal received from peer (extracts parent context from TraceContext when present; falls back to standalone span for older peers) |
| `consensus.validation.receive` | PeerImp.cpp | `validation_trusted`, `ledger_seq` | Validation received from peer (extracts parent context from TraceContext when present; falls back to standalone span for older peers) |
#### Consensus Span Events

View File

@@ -125,7 +125,13 @@ inline constexpr auto ledgerSeq = makeStr("ledger_seq");
inline constexpr auto closeTime = makeStr("close_time");
inline constexpr auto closeTimeCorrect = makeStr("close_time_correct");
inline constexpr auto closeResolutionMs = makeStr("close_resolution_ms");
inline constexpr auto ledgerHash = join(join(seg::xrpl, seg::ledger), makeStr("hash"));
/// Shared validation attrs — reused by the consensus and peer validation
/// spans. Same concept, same key on every span; the span name tells them
/// apart, so neither is emitter-prefixed. `ledgerHash` is a ledger-object
/// property (bare, like ledgerSeq); `fullValidation` is the is-full-validation
/// flag. Never the dotted xrpl. form (reserved for resource attrs).
inline constexpr auto ledgerHash = makeStr("ledger_hash");
inline constexpr auto fullValidation = makeStr("full_validation");
} // namespace attr
// ===== Shared attribute values =============================================

View File

@@ -34,7 +34,7 @@
* | attrs: queue_size, ledger_changed |
* | |
* | +--------------------------------------------------+ |
* | | txq.accept.tx (per queued transaction) | |
* | | txq.accept_tx (per queued transaction) | |
* | | attrs: tx_hash, ter_code, retries_remaining | |
* | +--------------------------------------------------+ |
* +-------------------------------------------------------+

View File

@@ -1856,7 +1856,7 @@ Consensus<Adaptor>::haveConsensus(std::unique_ptr<std::stringstream> const& clog
consensus::span::attr::proposersFinished, static_cast<int64_t>(currentFinished));
span.setAttribute(consensus::span::attr::consensusStalled, stalled);
span.setAttribute(
consensus::span::attr::establishCounter, static_cast<int64_t>(establishCounter_));
consensus::span::attr::establishCount, static_cast<int64_t>(establishCounter_));
std::string_view stateStr = consensus::span::val::no;
if (result_->state == ConsensusState::Yes)

View File

@@ -125,10 +125,14 @@ inline constexpr auto phaseOpen = join(seg::consensus, op::phaseOpen);
// ===== Attribute keys ========================================================
namespace attr {
/// Canonical shared constants (defined in SpanNames.h).
/// Canonical shared constants (defined in SpanNames.h). `ledgerHash` and
/// `fullValidation` are shared with the peer.validation.receive span — same
/// concept, same key, distinguished by span name (not an emitter prefix).
using ::xrpl::telemetry::attr::closeResolutionMs;
using ::xrpl::telemetry::attr::closeTime;
using ::xrpl::telemetry::attr::closeTimeCorrect;
using ::xrpl::telemetry::attr::fullValidation;
using ::xrpl::telemetry::attr::ledgerHash;
using ::xrpl::telemetry::attr::ledgerSeq;
/// Domain-qualified attrs (rule 5 — bare name ambiguous across domains).
@@ -159,11 +163,10 @@ inline constexpr auto peerPositionsAtClose = makeStr("peer_positions_at_close");
inline constexpr auto txCountOpen = makeStr("tx_count_open");
/// Establish/check additional state.
inline constexpr auto proposersFinished = makeStr("proposers_finished");
inline constexpr auto establishCounter = makeStr("establish_counter");
/// Accept/apply enrichment.
inline constexpr auto disputesResolvedCount = makeStr("disputes_resolved_count");
/// Validation send/receive enrichment.
inline constexpr auto fullValidation = makeStr("full_validation");
/// Validation send/receive enrichment. (`full_validation` is shared — see the
/// `using` re-export above.)
inline constexpr auto validationSignTime = makeStr("validation_sign_time");
/// Receive-side hash prefixes for cross-peer correlation.
inline constexpr auto prevLedgerPrefix = makeStr("prev_ledger_prefix");
@@ -191,8 +194,6 @@ inline constexpr auto modeNew = makeStr("mode_new");
/// "is_bow_out" — whether this proposal is a bow-out (resigning from round).
inline constexpr auto isBowOut = makeStr("is_bow_out");
/// "ledger_hash" — full hash of the ledger being validated/accepted.
inline constexpr auto ledgerHash = makeStr("ledger_hash");
/// Transaction/dispute attrs used in consensus accept spans.
inline constexpr auto txId = makeStr("tx_id");
@@ -201,7 +202,12 @@ inline constexpr auto disputeYays = makeStr("dispute_yays");
inline constexpr auto disputeNays = makeStr("dispute_nays");
inline constexpr auto txCount = makeStr("tx_count");
inline constexpr auto disputesCount = makeStr("disputes_count");
inline constexpr auto trusted = makeStr("trusted");
/// Trust flag (is the message origin a trusted UNL validator). Qualified by
/// message type, shared with the peer.{proposal,validation}.receive spans:
/// consensus.proposal.receive uses `proposal_trusted`, consensus.validation.
/// receive uses `validation_trusted`. Same concept on both emitters → same key.
inline constexpr auto proposalTrusted = makeStr("proposal_trusted");
inline constexpr auto validationTrusted = makeStr("validation_trusted");
} // namespace attr
// ===== Event names ===========================================================

View File

@@ -1869,7 +1869,7 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
calcNodeID(app_.getValidatorManifests().getMasterKey(publicKey))});
auto consSpan = std::make_shared<telemetry::SpanGuard>(telemetry::proposalReceiveSpan(set));
consSpan->setAttribute(telemetry::consensus::span::attr::trusted, isTrusted);
consSpan->setAttribute(telemetry::consensus::span::attr::proposalTrusted, isTrusted);
consSpan->setAttribute(
telemetry::consensus::span::attr::round, static_cast<int64_t>(set.proposeseq()));
// First 16 hex chars (8 bytes) of each hash — enough to disambiguate
@@ -2413,7 +2413,7 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMValidation> const& m)
val->setSeen(closeTime);
}
valSpan.setAttribute(peer_span::attr::ledgerHash, to_string(val->getLedgerHash()).c_str());
valSpan.setAttribute(peer_span::attr::validationFull, val->isFull());
valSpan.setAttribute(peer_span::attr::fullValidation, val->isFull());
if (!isCurrent(
app_.getValidations().parms(),
@@ -2470,7 +2470,7 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMValidation> const& m)
auto consSpan =
std::make_shared<telemetry::SpanGuard>(telemetry::validationReceiveSpan(*m));
consSpan->setAttribute(telemetry::consensus::span::attr::trusted, isTrusted);
consSpan->setAttribute(telemetry::consensus::span::attr::validationTrusted, isTrusted);
if (val->isFieldPresent(sfLedgerSequence))
{
consSpan->setAttribute(

View File

@@ -25,13 +25,15 @@ inline constexpr auto validationReceive = makeStr("validation.receive");
// ===== Attribute keys ========================================================
namespace attr {
/// Canonical shared constants (defined in SpanNames.h).
/// Canonical shared constants (defined in SpanNames.h). `ledgerHash` and
/// `fullValidation` are shared with the consensus validation spans — same
/// concept, same key, told apart by span name.
using ::xrpl::telemetry::attr::fullValidation;
using ::xrpl::telemetry::attr::ledgerHash;
using ::xrpl::telemetry::attr::peerId;
/// Domain-owned bare attrs.
/// Trust flag qualified by message type, shared with consensus.*.receive.
inline constexpr auto proposalTrusted = makeStr("proposal_trusted");
inline constexpr auto validationFull = makeStr("validation_full");
inline constexpr auto validationTrusted = makeStr("validation_trusted");
} // namespace attr