mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-26 13:37:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16389dcb2e | ||
|
|
97a780b29f | ||
|
|
654edc9a08 | ||
|
|
658d04f694 | ||
|
|
93498952b1 | ||
|
|
269f79e568 | ||
|
|
e7a8e0336a | ||
|
|
1d7e40777a | ||
|
|
7bb43a243b | ||
|
|
d697b7843c | ||
|
|
2de5140f20 | ||
|
|
edf085a697 | ||
|
|
c35ac1339b | ||
|
|
dc1cfbc474 | ||
|
|
3044073401 |
No files matched your search
@@ -58,15 +58,3 @@ ignore:
|
||||
- "src/tests/"
|
||||
- "include/xrpl/beast/test/"
|
||||
- "include/xrpl/beast/unit_test/"
|
||||
# Telemetry modules. Telemetry compiles in by default (conanfile.py,
|
||||
# CMakeLists.txt), but the unit-test suite never starts an exporter, so these
|
||||
# files record no coverage. This block belongs on the earliest branch that
|
||||
# adds telemetry code — codecov config only flows child-ward, so an ignore
|
||||
# added on a later branch can never cover the branches before it.
|
||||
- "src/xrpld/telemetry/"
|
||||
- "src/libxrpl/telemetry/"
|
||||
- "include/xrpl/telemetry/"
|
||||
# Per-module span-name and span-label constant headers: compile-time
|
||||
# constants only, colocated with their subsystem rather than under telemetry/.
|
||||
- "**/*SpanNames.h"
|
||||
- "**/*SpanLabels.h"
|
||||
+4
-12
@@ -69,7 +69,6 @@ words:
|
||||
- Btrfs
|
||||
- Buildx
|
||||
- canonicality
|
||||
- CGNAT
|
||||
- canonicalised
|
||||
- cctools
|
||||
- changespq
|
||||
@@ -130,8 +129,6 @@ words:
|
||||
- fsanitize
|
||||
- funclets
|
||||
- Gamal
|
||||
- gantt
|
||||
- Gantt
|
||||
- gcov
|
||||
- gcovr
|
||||
- ghead
|
||||
@@ -182,10 +179,11 @@ words:
|
||||
- mathbunnyru
|
||||
- mcmodel
|
||||
- MEMORYSTATUSEX
|
||||
- MPTAMM
|
||||
- MPTDEX
|
||||
- Merkle
|
||||
- misprediction
|
||||
- missingok
|
||||
- MPTAMM
|
||||
- mptbalance
|
||||
- MPTDEX
|
||||
- mptflags
|
||||
@@ -220,7 +218,6 @@ words:
|
||||
- nonxrp
|
||||
- noreplace
|
||||
- noripple
|
||||
- nostd
|
||||
- nostdinc
|
||||
- notifempty
|
||||
- nudb
|
||||
@@ -229,7 +226,6 @@ words:
|
||||
- Nyffenegger
|
||||
- onlatest
|
||||
- ostr
|
||||
- otelc
|
||||
- otool
|
||||
- oxalica
|
||||
- pargs
|
||||
@@ -240,7 +236,6 @@ words:
|
||||
- permdex
|
||||
- perminute
|
||||
- permissioned
|
||||
- pimpl
|
||||
- pointee
|
||||
- populator
|
||||
- preauth
|
||||
@@ -333,7 +328,6 @@ words:
|
||||
- TMEndpointv2
|
||||
- toolchain
|
||||
- tparam
|
||||
- traceql
|
||||
- trixie
|
||||
- tx
|
||||
- txid
|
||||
@@ -341,7 +335,6 @@ words:
|
||||
- txjson
|
||||
- txn
|
||||
- txns
|
||||
- txqueue
|
||||
- txs
|
||||
- ubsan
|
||||
- UBSAN
|
||||
@@ -356,7 +349,6 @@ words:
|
||||
- unfindable
|
||||
- unflatten
|
||||
- unfund
|
||||
- ungated
|
||||
- unimpair
|
||||
- unroutable
|
||||
- unscalable
|
||||
@@ -395,6 +387,6 @@ words:
|
||||
- xrplf
|
||||
- xxhash
|
||||
- xxhasher
|
||||
- xychart
|
||||
- zpages
|
||||
- zstdio
|
||||
- CGNAT
|
||||
- ungated
|
||||
@@ -53,9 +53,6 @@ libxrpl.shamap > xrpl.basics
|
||||
libxrpl.shamap > xrpl.nodestore
|
||||
libxrpl.shamap > xrpl.protocol
|
||||
libxrpl.shamap > xrpl.shamap
|
||||
libxrpl.telemetry > xrpl.basics
|
||||
libxrpl.telemetry > xrpl.config
|
||||
libxrpl.telemetry > xrpl.telemetry
|
||||
libxrpl.tx > xrpl.basics
|
||||
libxrpl.tx > xrpl.conditions
|
||||
libxrpl.tx > xrpl.core
|
||||
@@ -196,7 +193,6 @@ tests.libxrpl > xrpl.protocol_autogen
|
||||
tests.libxrpl > xrpl.resource
|
||||
tests.libxrpl > xrpl.server
|
||||
tests.libxrpl > xrpl.shamap
|
||||
tests.libxrpl > xrpl.telemetry
|
||||
tests.libxrpl > xrpl.tx
|
||||
tests.xrpld > xrpld.rpc
|
||||
tests.xrpld > xrpl.json
|
||||
@@ -243,8 +239,6 @@ xrpl.server > xrpl.resource
|
||||
xrpl.shamap > xrpl.basics
|
||||
xrpl.shamap > xrpl.nodestore
|
||||
xrpl.shamap > xrpl.protocol
|
||||
xrpl.telemetry > xrpl.basics
|
||||
xrpl.telemetry > xrpl.config
|
||||
xrpl.tx > xrpl.basics
|
||||
xrpl.tx > xrpl.core
|
||||
xrpl.tx > xrpl.ledger
|
||||
@@ -265,7 +259,6 @@ xrpld.app > xrpl.rdb
|
||||
xrpld.app > xrpl.resource
|
||||
xrpld.app > xrpl.server
|
||||
xrpld.app > xrpl.shamap
|
||||
xrpld.app > xrpl.telemetry
|
||||
xrpld.app > xrpl.tx
|
||||
xrpld.core > xrpl.basics
|
||||
xrpld.core > xrpl.config
|
||||
@@ -313,7 +306,6 @@ xrpld.rpc > xrpl.rdb
|
||||
xrpld.rpc > xrpl.resource
|
||||
xrpld.rpc > xrpl.server
|
||||
xrpld.rpc > xrpl.shamap
|
||||
xrpld.rpc > xrpl.telemetry
|
||||
xrpld.rpc > xrpl.tx
|
||||
xrpld.shamap > xrpl.basics
|
||||
xrpld.shamap > xrpld.core
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# 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`.
|
||||
|
||||
### 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. |
|
||||
|
||||
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. |
|
||||
|
||||
## 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.
|
||||
@@ -1,929 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Usage: check_otel_naming.py
|
||||
This script takes no parameters and can be called from any directory inside the
|
||||
repository (it locates the repo root via `git rev-parse`).
|
||||
|
||||
Enforces the OpenTelemetry span-attribute naming convention documented in
|
||||
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.
|
||||
|
||||
Design principles
|
||||
-----------------
|
||||
1. No hardcoded allowlist. The set of valid attribute keys — including which
|
||||
dotted keys are legitimate resource attributes — is derived dynamically by
|
||||
parsing the repository's own OTel code:
|
||||
* `*SpanNames.h` `namespace attr { ... }` blocks (the underscore/bare keys
|
||||
and the `join(seg::..., ...)` dotted resource compositions), and
|
||||
* the keys the code passes to `Resource::Create({ ... })` in Telemetry.cpp
|
||||
(the standard `semconv::service::*` keys -> service.name/version/...).
|
||||
The one narrow, explicit exception is EXTERNAL_INFRA_LABELS (Rule D):
|
||||
identity labels stamped by infrastructure outside this repo's OTel code
|
||||
(the perf-iac harness), which by definition have no source in-tree to
|
||||
derive from. Kept separate from the generic Prometheus/Grafana builtins
|
||||
set so the exception stays visible rather than blending into "things
|
||||
every OTel setup has".
|
||||
|
||||
2. Presence-gated enforcement. Every rule runs ONLY when the source files it
|
||||
needs are present in the tree, and is otherwise skipped (never failed). This
|
||||
keeps the check correct no matter how work is split across PRs: a stacked
|
||||
chain, one large PR, or independent per-stage PRs where (for example) the
|
||||
collector config lands in a different PR than the dashboards. The check never
|
||||
assumes a file from another phase/PR exists.
|
||||
|
||||
Layers
|
||||
------
|
||||
L1 code : src/**/*SpanNames.h, include/**/*SpanNames.h (ground truth)
|
||||
L1 resource : src/libxrpl/telemetry/Telemetry.cpp (dotted allowlist)
|
||||
L1 callsites : setAttribute/addEvent/span/rootSpan/childSpan in src/**,
|
||||
include/**
|
||||
L2 collector : docker/telemetry/otel-collector-config.yaml (spanmetrics dims)
|
||||
L3 tempo : docker/telemetry/tempo.yaml (span filter tags)
|
||||
L4 dashboards: docker/telemetry/grafana/dashboards/*.json (PromQL labels)
|
||||
L5 runbook : docs/telemetry-runbook.md (attr tables)
|
||||
L6 metrics : MetricsRegistry.cpp instrument labels (native-metric
|
||||
label keys, a valid dashboard-label source besides L1)
|
||||
|
||||
Rules (each FAILS the build, when its inputs are present)
|
||||
---------------------------------------------------------
|
||||
A No stray dotted span-attribute key. A dotted `<a>.<b>` used as a span
|
||||
attribute that is not in the derived resource-key set is a violation.
|
||||
G Attribute keys must be lower_snake_case (^[a-z][a-z0-9_]*$ per segment).
|
||||
Flags camelCase, UPPERCASE, spaces, and other stray characters.
|
||||
F No string literals as attribute keys or span-name arguments. The
|
||||
setAttribute/addEvent key and the span/rootSpan/childSpan prefix/name args
|
||||
must reference a *SpanNames.h constant, never a "literal". Attribute VALUES
|
||||
are exempt (runtime data). Definitions inside *SpanNames.h are exempt, and
|
||||
test files are exempt (they pass arbitrary literals to exercise the API).
|
||||
B Every collector spanmetrics dimension 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, an L6
|
||||
native-metric label, or a builtin. TraceQL `span.`/`resource.` scope
|
||||
prefixes 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.
|
||||
|
||||
Warnings (printed, but do NOT fail the build)
|
||||
----------------------------------------------
|
||||
H A constant referenced at a telemetry call-site is not defined in any
|
||||
*SpanNames.h. Span constants should live in the corresponding
|
||||
*SpanNames.h (single source of truth); defining one in-place bypasses the
|
||||
naming rules. A warning (not a failure) because the argument may instead
|
||||
be a legitimately dynamic local (e.g. a computed span-name leaf).
|
||||
|
||||
Exit code is non-zero if any present-and-enforced rule finds a violation.
|
||||
Warnings never change the exit code.
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repo location
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
"""Return the repository root, so the script works from any CWD.
|
||||
|
||||
Exits with a readable message (not a traceback) if git is unavailable or the
|
||||
CWD is outside a repository."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print(
|
||||
"error: check_otel_naming.py must be run inside the git repository.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
return Path(out.stdout.strip())
|
||||
|
||||
|
||||
def read_source(path: Path) -> str:
|
||||
"""Read a file as UTF-8, tolerating stray non-UTF-8 bytes rather than
|
||||
crashing the whole check on one bad byte."""
|
||||
return path.read_text(encoding="utf-8", errors="ignore")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regexes (compiled once)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# A segment/string constant definition: `inline constexpr auto NAME = <expr>;`
|
||||
CONST_DEF = re.compile(r"inline\s+constexpr\s+auto\s+(\w+)\s*=\s*(.+?);", re.DOTALL)
|
||||
MAKESTR = re.compile(r'makeStr\(\s*"([^"]*)"\s*\)')
|
||||
# A `namespace <name> {` opener, to track which namespace a constant lives in.
|
||||
NS_OPEN = re.compile(r"namespace\s+([\w:]+)\s*\{")
|
||||
# A `using ::a::b::field;` re-export inside an attr block; captures the leaf.
|
||||
USING_DECL = re.compile(r"using\s+(?:::)?[\w:]*::(\w+)\s*;")
|
||||
# Telemetry call-sites whose string arguments must be constants, not literals.
|
||||
# Require a receiver so we match real SpanGuard calls, not std::span / a math
|
||||
# `span(...)` / a bare method declaration:
|
||||
# - `SpanGuard::span(` / `SpanGuard::rootSpan(` / `SpanGuard::childSpan(`
|
||||
# (static factories)
|
||||
# - `<obj>.span(` / `<obj>->setAttribute(` etc. (member call)
|
||||
# `span`/`rootSpan`/`childSpan` additionally require the `SpanGuard`/`.`/`->`
|
||||
# receiver; `setAttribute`/`addEvent` only ever exist on a guard, so a `.`/`->`
|
||||
# suffices. `rootSpan` shares `span`'s (cat, prefix, name) signature.
|
||||
CALLSITE = re.compile(
|
||||
r"(?:SpanGuard::|\.|->)\s*(setAttribute|addEvent|span|rootSpan|childSpan)\s*\("
|
||||
)
|
||||
# A C++ string literal (used to flag literals inside call-site argument lists).
|
||||
STRING_LITERAL = re.compile(r'"((?:[^"\\]|\\.)*)"')
|
||||
# A C++ line comment (`//` ... end of line) and a block comment (`/* ... */`).
|
||||
LINE_COMMENT = re.compile(r"//[^\n]*")
|
||||
BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
||||
# A TraceQL scope prefix on a label (`span.`, `resource.`, `event.`, etc.).
|
||||
# Dashboards reference span attributes in TraceQL as `span.<attr>`; the bare
|
||||
# attribute is what must exist in L1, so strip the scope before validating.
|
||||
TRACEQL_SCOPE = re.compile(r"^(?:span|resource|event|link|instrumentation_scope)\.")
|
||||
# An OTel metric label key as emitted in C++: `Add(.., {{"label", ...}})` /
|
||||
# `{{"label", value}}` instrument calls in MetricsRegistry.
|
||||
METRIC_LABEL = re.compile(r'\{\{\s*"([a-z_][a-z0-9_]*)"\s*,')
|
||||
|
||||
|
||||
def strip_comments(text: str) -> str:
|
||||
"""Remove C/C++ `//` line comments and `/* ... */` block comments.
|
||||
|
||||
Used only for L1 attribute-key extraction so that a commented-out or
|
||||
illustrative `makeStr("...")` inside a `namespace attr` block does not leak
|
||||
into the authoritative key set. Rule F deliberately does NOT strip comments
|
||||
— it must still see `@code` doc-comment examples so their call-site
|
||||
arguments are held to the constant-only convention.
|
||||
|
||||
String literals are not specially handled; a `//` or `/*` appearing inside a
|
||||
string is vanishingly rare in the *SpanNames.h headers and would at worst
|
||||
drop a constant from L1 (a conservative direction).
|
||||
"""
|
||||
text = BLOCK_COMMENT.sub("", text)
|
||||
text = LINE_COMMENT.sub("", text)
|
||||
return text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# L1: parse *SpanNames.h into the authoritative key set
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def find_spanname_headers(root: Path) -> List[Path]:
|
||||
return sorted(
|
||||
p
|
||||
for p in list((root / "src").rglob("*SpanNames.h"))
|
||||
+ list((root / "include").rglob("*SpanNames.h"))
|
||||
if p.is_file()
|
||||
)
|
||||
|
||||
|
||||
def resolve_constants(
|
||||
text: str, symbols: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, str]:
|
||||
"""Resolve `inline constexpr auto NAME = <makeStr/join expr>` to strings.
|
||||
|
||||
Supports the small constexpr DSL used by SpanNames.h:
|
||||
makeStr("x") -> "x"
|
||||
join(a, b) -> resolve(a) + "." + resolve(b)
|
||||
seg::xrpl / attr::foo -> looked up in the symbol table
|
||||
The optional `symbols` argument seeds (and is updated in place with) the
|
||||
table, so a global pass over ALL *SpanNames.h headers can resolve
|
||||
cross-file references such as `join(seg::rpc, ...)` where `seg::rpc` is
|
||||
defined in the base SpanNames.h. Keys are stored by their bare name
|
||||
(last `::` component), so `seg::rpc` and `rpc` both resolve.
|
||||
"""
|
||||
if symbols is None:
|
||||
symbols = {}
|
||||
|
||||
def resolve_expr(expr: str) -> Optional[str]:
|
||||
expr = expr.strip()
|
||||
m = MAKESTR.fullmatch(expr)
|
||||
if m:
|
||||
return m.group(1)
|
||||
if expr.startswith("join(") and expr.endswith(")"):
|
||||
args = split_top_level_args(expr[len("join(") : -1])
|
||||
parts = [resolve_expr(a) for a in args]
|
||||
if any(p is None for p in parts):
|
||||
return None
|
||||
return ".".join(p for p in parts if p is not None)
|
||||
# Bare or qualified symbol reference, e.g. `seg::xrpl` or `networkId`.
|
||||
key = expr.split("::")[-1]
|
||||
return symbols.get(key, symbols.get(expr))
|
||||
|
||||
# Iterate definitions in source order so earlier symbols are available.
|
||||
for m in CONST_DEF.finditer(text):
|
||||
name, expr = m.group(1), m.group(2)
|
||||
val = resolve_expr(expr)
|
||||
if val is not None:
|
||||
symbols[name] = val
|
||||
return symbols
|
||||
|
||||
|
||||
def build_global_symbols(headers: List[Path]) -> Dict[str, str]:
|
||||
"""Resolve constants across ALL headers so cross-file `seg::`/`join`
|
||||
references (e.g. `join(seg::rpc, ...)` in RpcSpanNames.h, where `seg::rpc`
|
||||
lives in the base SpanNames.h) resolve. Base SpanNames.h is processed
|
||||
first so its `seg::` segments seed the table."""
|
||||
symbols: Dict[str, str] = {}
|
||||
ordered = sorted(headers, key=lambda p: (p.name != "SpanNames.h", str(p)))
|
||||
# Two passes: the first seeds segments, the second resolves dependents.
|
||||
# Comments are stripped so a commented-out constant cannot seed the table.
|
||||
for _ in range(2):
|
||||
for h in ordered:
|
||||
resolve_constants(strip_comments(read_source(h)), symbols)
|
||||
return symbols
|
||||
|
||||
|
||||
def split_top_level_args(s: str) -> List[str]:
|
||||
"""Split a comma-separated arg list, respecting nested parentheses and
|
||||
ignoring parens/commas that appear inside a "string literal" (so a value
|
||||
like `setAttribute(k, ",")` does not get mis-split)."""
|
||||
args, depth, cur = [], 0, ""
|
||||
in_str = False
|
||||
escaped = False
|
||||
for ch in s:
|
||||
if in_str:
|
||||
cur += ch
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif ch == "\\":
|
||||
escaped = True
|
||||
elif ch == '"':
|
||||
in_str = False
|
||||
continue
|
||||
if ch == '"':
|
||||
in_str = True
|
||||
cur += ch
|
||||
elif ch == "(":
|
||||
depth += 1
|
||||
cur += ch
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
cur += ch
|
||||
elif ch == "," and depth == 0:
|
||||
args.append(cur)
|
||||
cur = ""
|
||||
else:
|
||||
cur += ch
|
||||
if cur.strip():
|
||||
args.append(cur)
|
||||
return args
|
||||
|
||||
|
||||
def attr_namespace_spans(text: str) -> List[str]:
|
||||
"""Return the source text of each `namespace attr { ... }` block in `text`.
|
||||
|
||||
Brace-matched over the whole (comment-stripped) text, so a definition that
|
||||
wraps across several physical lines is contained in one span. Nested braces
|
||||
inside the block are balanced correctly."""
|
||||
spans: List[str] = []
|
||||
for opener in NS_OPEN.finditer(text):
|
||||
if opener.group(1).split("::")[-1] != "attr":
|
||||
continue
|
||||
# Walk from the opening brace, balancing nesting to the matching close.
|
||||
i = opener.end() # one char past the namespace's `{`
|
||||
depth = 1
|
||||
start = i
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
spans.append(text[start : i - 1])
|
||||
return spans
|
||||
|
||||
|
||||
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, 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 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 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:
|
||||
keys.add(val)
|
||||
return keys
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reporting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Report:
|
||||
def __init__(self) -> None:
|
||||
self.violations: List[Tuple[str, str, str, str]] = []
|
||||
self.warnings: List[Tuple[str, str, str, str]] = []
|
||||
self.skips: List[str] = []
|
||||
self.checked: List[str] = []
|
||||
|
||||
def violation(self, rule: str, loc: str, token: str, expected: str) -> None:
|
||||
self.violations.append((rule, loc, token, expected))
|
||||
|
||||
def warning(self, rule: str, loc: str, token: str, note: str) -> None:
|
||||
"""A non-fatal finding: printed, but does not fail the build. Used where
|
||||
the script cannot be certain a finding is wrong (e.g. a constant used at
|
||||
a call-site that is not defined in any *SpanNames.h — it might be a
|
||||
misplaced constant, or a legitimately dynamic value)."""
|
||||
self.warnings.append((rule, loc, token, note))
|
||||
|
||||
def skip(self, rule: str, reason: str) -> None:
|
||||
self.skips.append(f"SKIP: {rule} — {reason}")
|
||||
|
||||
def ok(self, msg: str) -> None:
|
||||
self.checked.append(f"OK: {msg}")
|
||||
|
||||
def render_and_exit(self) -> None:
|
||||
for line in self.skips:
|
||||
print(line)
|
||||
for line in self.checked:
|
||||
print(line)
|
||||
if self.warnings:
|
||||
print("\nNaming-convention warnings (non-fatal):\n")
|
||||
print(f" {'RULE':<5} {'LOCATION':<48} {'TOKEN':<28} NOTE")
|
||||
print(f" {'-' * 5} {'-' * 48} {'-' * 28} {'-' * 30}")
|
||||
for rule, loc, token, note in self.warnings:
|
||||
print(f" {rule:<5} {loc:<48} {token:<28} {note}")
|
||||
if self.violations:
|
||||
print("\nNaming-convention violations:\n")
|
||||
print(f" {'RULE':<5} {'LOCATION':<48} {'TOKEN':<28} EXPECTED")
|
||||
print(f" {'-' * 5} {'-' * 48} {'-' * 28} {'-' * 30}")
|
||||
for rule, loc, token, expected in self.violations:
|
||||
print(f" {rule:<5} {loc:<48} {token:<28} {expected}")
|
||||
print(
|
||||
"\nSee CONTRIBUTING.md -> 'Telemetry span attribute naming'. "
|
||||
"The *SpanNames.h constants are the single source of truth."
|
||||
)
|
||||
sys.exit(1)
|
||||
print("\nAll present telemetry naming layers are consistent.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = repo_root()
|
||||
report = Report()
|
||||
|
||||
# --- Build the L1 ground-truth key set (presence-gated) ----------------
|
||||
headers = find_spanname_headers(root)
|
||||
l1_keys: Set[str] = set()
|
||||
if headers:
|
||||
symbols = build_global_symbols(headers)
|
||||
# Map each key to the header(s) that declare it, so Rule A can tell a
|
||||
# legitimate resource attr (declared in the base SpanNames.h) from a
|
||||
# stray dotted key declared in a domain header.
|
||||
keys_by_header: Dict[Path, Set[str]] = {}
|
||||
for h in headers:
|
||||
hk = attr_keys_from_header(h, symbols)
|
||||
keys_by_header[h] = hk
|
||||
l1_keys |= hk
|
||||
report.ok(
|
||||
f"L1: {len(l1_keys)} attribute keys from {len(headers)} "
|
||||
f"*SpanNames.h header(s)"
|
||||
)
|
||||
else:
|
||||
report.skip("L1", "no *SpanNames.h present (not a naming-relevant tree)")
|
||||
keys_by_header = {}
|
||||
|
||||
# --- Derive the legitimate dotted (resource) keys dynamically ----------
|
||||
# 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:
|
||||
run_rule_a(keys_by_header, dotted_allow, report)
|
||||
# --- Rule G: keys must be lower_snake_case -----------------------------
|
||||
if l1_keys:
|
||||
run_rule_g(keys_by_header, report)
|
||||
# --- Rule F (+ Rule H): scan telemetry call-sites ----------------------
|
||||
# Runs UNCONDITIONALLY: Rule F is a purely syntactic check (is this argument
|
||||
# a literal?) and does not need the L1 key set, so a code path that uses
|
||||
# SpanGuard::span/setAttribute directly without ever defining a *SpanNames.h
|
||||
# is still caught. Rule H (warning) additionally flags constant references
|
||||
# not defined in any *SpanNames.h.
|
||||
header_symbols = spanname_symbol_names(headers)
|
||||
run_rule_f(root, report, header_symbols)
|
||||
|
||||
# --- Cross-layer rules B/C/D/E (each presence-gated) -------------------
|
||||
# L6 native-metric labels: span attributes are not the only valid dashboard
|
||||
# labels — the MetricsRegistry emits OTel metrics whose label keys are an
|
||||
# additional source of truth. Derive them dynamically (same principle as L1)
|
||||
# so dashboards may reference them without tripping Rule D.
|
||||
metric_labels = metric_label_names(root)
|
||||
run_rule_b_collector(root, l1_keys, report)
|
||||
run_rule_c_tempo(root, l1_keys, report)
|
||||
run_rule_d_dashboards(root, l1_keys, metric_labels, report)
|
||||
run_rule_e_runbook(root, l1_keys, report)
|
||||
|
||||
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, symbols: Dict[str, str], report: Report
|
||||
) -> Set[str]:
|
||||
"""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()
|
||||
tele = root / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp"
|
||||
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
|
||||
|
||||
|
||||
def camel_to_dotsegments(s: str) -> List[str]:
|
||||
"""Split a CamelCase identifier into lowercase dot-segment parts, e.g.
|
||||
`ServiceInstanceId` -> ['service', 'instance', 'id']."""
|
||||
return [w.lower() for w in re.findall(r"[A-Z][a-z0-9]*", s)]
|
||||
|
||||
|
||||
def run_rule_a(
|
||||
keys_by_header: Dict[Path, Set[str]], dotted_allow: Set[str], report: Report
|
||||
) -> None:
|
||||
"""Any dotted attribute key that is not an allowed resource key is a
|
||||
violation, reported against the header that declares it."""
|
||||
found = False
|
||||
for h in sorted(keys_by_header):
|
||||
for key in sorted(keys_by_header[h]):
|
||||
if "." in key and key not in dotted_allow:
|
||||
found = True
|
||||
report.violation("A", h.name, key, "underscore form, not dotted")
|
||||
if not found:
|
||||
report.ok("A: no stray dotted span-attribute keys")
|
||||
|
||||
|
||||
# A lower_snake_case identifier segment: starts lowercase, then lowercase /
|
||||
# digits / underscores. No uppercase, no spaces, no camelCase.
|
||||
SNAKE_SEGMENT = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||
|
||||
|
||||
def run_rule_g(keys_by_header: Dict[Path, Set[str]], report: Report) -> None:
|
||||
"""Every attribute key must be lower_snake_case. Bare/underscore keys must
|
||||
match ^[a-z][a-z0-9_]*$; dotted resource keys must be lowercase
|
||||
dot-separated segments (each segment lower_snake_case). Flags camelCase,
|
||||
UPPERCASE, spaces, and other stray characters."""
|
||||
found = False
|
||||
for h in sorted(keys_by_header):
|
||||
for key in sorted(keys_by_header[h]):
|
||||
segments = key.split(".")
|
||||
if all(SNAKE_SEGMENT.match(seg) for seg in segments):
|
||||
continue
|
||||
found = True
|
||||
report.violation("G", h.name, key, "must be lower_snake_case")
|
||||
if not found:
|
||||
report.ok("G: all attribute keys are lower_snake_case")
|
||||
|
||||
|
||||
# Which argument positions of each call must be a constant (0-based). The
|
||||
# attribute VALUE position is intentionally absent: values are runtime data
|
||||
# (command names, hashes, counts), not naming-convention surface.
|
||||
# setAttribute(key, value) -> check arg 0 (key); value (arg 1) exempt
|
||||
# addEvent(name[, attrs]) -> check arg 0 (event name)
|
||||
# span(category, prefix, name) -> check args 1,2 (prefix + span-name leaf)
|
||||
# rootSpan(category, prefix, name)-> check args 1,2 (same signature as span)
|
||||
# childSpan(name[, parentCtx]) -> check arg 0 (span-name leaf)
|
||||
CONSTANT_ARG_POSITIONS: Dict[str, Set[int]] = {
|
||||
"setAttribute": {0},
|
||||
"addEvent": {0},
|
||||
"span": {1, 2},
|
||||
"rootSpan": {1, 2}, # same signature as span(cat, prefix, name)
|
||||
"childSpan": {0},
|
||||
}
|
||||
|
||||
|
||||
def is_test_path(path: Path, root: Path) -> bool:
|
||||
"""True if the path is test code. Tests legitimately pass arbitrary literal
|
||||
keys/names to exercise the API mechanics, so Rule F does not apply to them.
|
||||
Matches a `test`/`tests` directory anywhere in the path below `root` (e.g.
|
||||
src/test/, src/tests/, .../detail/tests/). Only the part below `root` is
|
||||
read, so a checkout inside a folder named `test` is not all test code."""
|
||||
return any(part in ("test", "tests") for part in path.relative_to(root).parts)
|
||||
|
||||
|
||||
# A constant reference passed at a call-site, e.g. `rpc_span::attr::command`
|
||||
# or a bare `myKey`. We capture the leaf identifier (after the last `::`).
|
||||
IDENTIFIER_ARG = re.compile(r"^[\s&*]*([A-Za-z_][\w:]*)\s*$")
|
||||
|
||||
|
||||
def spanname_symbol_names(headers: List[Path]) -> Set[str]:
|
||||
"""Every `inline constexpr auto NAME = ...;` symbol defined across the
|
||||
*SpanNames.h headers, by bare name. Used by Rule H to tell whether a
|
||||
constant referenced at a call-site actually lives in a SpanNames header."""
|
||||
names: Set[str] = set()
|
||||
for h in headers:
|
||||
for m in CONST_DEF.finditer(strip_comments(read_source(h))):
|
||||
names.add(m.group(1))
|
||||
return names
|
||||
|
||||
|
||||
def run_rule_f(root: Path, report: Report, header_symbols: Set[str]) -> None:
|
||||
"""Walk every telemetry call-site (non-test, non-*SpanNames.h) and check the
|
||||
constant-only argument positions of
|
||||
setAttribute/addEvent/span/rootSpan/childSpan:
|
||||
|
||||
Rule F (FAIL): a string literal in a key / span-name position. Attribute
|
||||
VALUES are exempt (runtime data).
|
||||
Rule H (WARN): a constant reference whose name is not defined in any
|
||||
*SpanNames.h. The constant should live in the corresponding
|
||||
*SpanNames.h (single source of truth); defining it in-place bypasses
|
||||
the naming rules. Warn rather than fail — the argument may instead be a
|
||||
legitimately dynamic local (e.g. a computed span-name leaf)."""
|
||||
found_f = False
|
||||
sources = [
|
||||
p
|
||||
for base in ("src", "include")
|
||||
for ext in ("*.h", "*.cpp")
|
||||
for p in (root / base).rglob(ext)
|
||||
if p.is_file()
|
||||
]
|
||||
for path in sorted(sources):
|
||||
if path.name.endswith("SpanNames.h") or is_test_path(path, root):
|
||||
continue
|
||||
text = read_source(path)
|
||||
rel = path.relative_to(root)
|
||||
for call, arglist, lineno in iter_calls(text):
|
||||
positions = CONSTANT_ARG_POSITIONS.get(call, set())
|
||||
args = split_top_level_args(arglist)
|
||||
for idx in positions:
|
||||
if idx >= len(args):
|
||||
continue
|
||||
arg = args[idx]
|
||||
lit = STRING_LITERAL.search(arg)
|
||||
if lit:
|
||||
found_f = True
|
||||
report.violation(
|
||||
"F",
|
||||
f"{rel}:{lineno}",
|
||||
f'{call} arg{idx} "{lit.group(1)}"',
|
||||
"use a *SpanNames.h constant",
|
||||
)
|
||||
continue
|
||||
# Not a literal: Rule H warns when a NAMESPACE-QUALIFIED constant
|
||||
# reference (e.g. `consensus::span::accept`) is not defined in
|
||||
# any *SpanNames.h — i.e. the constant was defined in-place
|
||||
# instead of in the proper header. We only consider qualified
|
||||
# refs (containing `::`): a bare lowercase identifier is almost
|
||||
# always a legitimately dynamic local (a computed span-name leaf
|
||||
# or attribute value), not a misplaced constant, so warning on it
|
||||
# would be noise. Standard-library types (std::...) are skipped.
|
||||
ident = IDENTIFIER_ARG.match(arg)
|
||||
if not (ident and header_symbols):
|
||||
continue
|
||||
ref = ident.group(1)
|
||||
if "::" not in ref or ref.startswith("std::"):
|
||||
continue
|
||||
leaf = ref.split("::")[-1]
|
||||
if leaf not in header_symbols:
|
||||
report.warning(
|
||||
"H",
|
||||
f"{rel}:{lineno}",
|
||||
f"{call} arg{idx} {ref}",
|
||||
"not defined in any *SpanNames.h",
|
||||
)
|
||||
if not found_f:
|
||||
report.ok("F: no string-literal keys/names at telemetry call-sites")
|
||||
|
||||
|
||||
def iter_calls(text: str):
|
||||
"""Yield (call_name, raw_arglist, lineno) for each setAttribute/addEvent/
|
||||
span/rootSpan/childSpan invocation, spanning multiple physical lines if
|
||||
needed."""
|
||||
for m in CALLSITE.finditer(text):
|
||||
name = m.group(1)
|
||||
# Walk from the opening paren, balancing nesting to find the close.
|
||||
# Parens inside a "string literal" are ignored so a value such as
|
||||
# `setAttribute(k, ")")` does not close the call early.
|
||||
i = m.end() # one char past the '('
|
||||
depth = 1
|
||||
in_str = False
|
||||
escaped = False
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if in_str:
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif c == "\\":
|
||||
escaped = True
|
||||
elif c == '"':
|
||||
in_str = False
|
||||
elif c == '"':
|
||||
in_str = True
|
||||
elif c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
i += 1
|
||||
arglist = text[m.end() : i - 1]
|
||||
lineno = text.count("\n", 0, m.start()) + 1
|
||||
yield name, arglist, lineno
|
||||
|
||||
|
||||
def run_rule_b_collector(root: Path, l1_keys: Set[str], report: Report) -> None:
|
||||
path = root / "docker" / "telemetry" / "otel-collector-config.yaml"
|
||||
if not path.is_file():
|
||||
report.skip("B", "collector config not present")
|
||||
return
|
||||
text = read_source(path)
|
||||
if "spanmetrics" not in text:
|
||||
report.skip("B", "no spanmetrics block in collector config")
|
||||
return
|
||||
dims = extract_spanmetrics_dimensions(text)
|
||||
if not l1_keys:
|
||||
report.skip("B", "no L1 key set to validate against")
|
||||
return
|
||||
miss = [d for d in dims if d not in l1_keys]
|
||||
for d in miss:
|
||||
report.violation("B", str(path.relative_to(root)), d, "must exist in L1")
|
||||
if not miss:
|
||||
report.ok(f"B: {len(dims)} collector dimension(s) all in L1")
|
||||
|
||||
|
||||
def extract_spanmetrics_dimensions(text: str) -> List[str]:
|
||||
"""The `- name: <dim>` entries listed under each `dimensions:` key.
|
||||
|
||||
Blank and comment lines are skipped, so a comment between entries never ends
|
||||
the list, even when it holds a colon. The list ends at the first other line
|
||||
indented no deeper than its `dimensions:` key. A `-` entry at the key's own
|
||||
indentation still belongs to the list, as YAML allows."""
|
||||
dims: List[str] = []
|
||||
key_col: Optional[int] = None
|
||||
for line in text.splitlines():
|
||||
content = line.strip()
|
||||
if not content or content.startswith("#"):
|
||||
continue
|
||||
indent = len(line) - len(line.lstrip())
|
||||
if key_col is not None:
|
||||
is_entry = re.match(r"-(\s|$)", content) is not None
|
||||
if indent > key_col or (indent == key_col and is_entry):
|
||||
m = re.search(r"-\s*name\s*:\s*([A-Za-z0-9_.]+)", line)
|
||||
if m:
|
||||
dims.append(m.group(1))
|
||||
continue
|
||||
key_col = None
|
||||
key = re.match(r"(\s*(?:-\s+)?)dimensions\s*:", line)
|
||||
if key:
|
||||
key_col = len(key.group(1))
|
||||
return dims
|
||||
|
||||
|
||||
def run_rule_c_tempo(root: Path, l1_keys: Set[str], report: Report) -> None:
|
||||
# The trace-search filter tags live in the Grafana Tempo DATASOURCE
|
||||
# provisioning file (search.filters[].{tag,scope}); the Tempo server
|
||||
# tempo.yaml has no such tags. Prefer the datasource file; fall back to the
|
||||
# server file so the rule still does something if the layout changes.
|
||||
candidates = [
|
||||
root / "docker/telemetry/grafana/provisioning/datasources/tempo.yaml",
|
||||
root / "docker/telemetry/tempo.yaml",
|
||||
]
|
||||
path = next((p for p in candidates if p.is_file()), None)
|
||||
if path is None:
|
||||
report.skip("C", "tempo datasource provisioning not present")
|
||||
return
|
||||
if not l1_keys:
|
||||
report.skip("C", "no L1 key set to validate against")
|
||||
return
|
||||
# Pair each filter's `tag:` with its `scope:` (a few lines below it) and
|
||||
# validate only span-scope tags — resource/intrinsic tags (service.*, name,
|
||||
# status, duration) are not span attributes. Strip a TraceQL span. prefix.
|
||||
lines = read_source(path).splitlines()
|
||||
span_tags: List[str] = []
|
||||
for i, line in enumerate(lines):
|
||||
m = re.search(r"^\s*tag:\s*(\S+)", line)
|
||||
if not m:
|
||||
continue
|
||||
scope = next(
|
||||
(
|
||||
sm.group(1)
|
||||
for j in range(i, min(i + 4, len(lines)))
|
||||
for sm in [re.search(r"scope:\s*(\S+)", lines[j])]
|
||||
if sm
|
||||
),
|
||||
"",
|
||||
)
|
||||
if scope == "span":
|
||||
span_tags.append(TRACEQL_SCOPE.sub("", m.group(1)))
|
||||
if not span_tags:
|
||||
report.skip("C", "no span-scope filter tags in tempo datasource")
|
||||
return
|
||||
miss = [t for t in span_tags if t not in l1_keys]
|
||||
for t in sorted(set(miss)):
|
||||
report.violation("C", str(path.relative_to(root)), t, "must exist in L1")
|
||||
if not miss:
|
||||
report.ok(f"C: {len(span_tags)} tempo span-filter tag(s) all in L1")
|
||||
|
||||
|
||||
def metric_label_names(root: Path) -> Set[str]:
|
||||
"""L6: OTel native-metric label keys emitted by the telemetry code, e.g.
|
||||
`counter->Add(1, {{"job_type", value}})` in MetricsRegistry.cpp. These are
|
||||
a valid source of dashboard labels distinct from span attributes (L1)."""
|
||||
labels: Set[str] = set()
|
||||
for base in ("src", "include"):
|
||||
for p in (root / base).rglob("*.cpp"):
|
||||
if not p.is_file():
|
||||
continue
|
||||
text = read_source(p)
|
||||
if "MetricsRegistry" not in p.name and "metric" not in text.lower():
|
||||
continue
|
||||
labels |= set(METRIC_LABEL.findall(text))
|
||||
return labels
|
||||
|
||||
|
||||
# Identity labels stamped by EXTERNAL infrastructure the OTel pipeline in this
|
||||
# repo does not own: the perf-iac harness attaches these to every metric it
|
||||
# scrapes so dashboards can filter by which build/role produced a series. They
|
||||
# have no L1 (*SpanNames.h), L2 (collector config), or L6 (MetricsRegistry.cpp)
|
||||
# source to derive from, so — unlike every other dashboard label — they cannot
|
||||
# be validated dynamically. This is a deliberate, narrow exception to the "no
|
||||
# hardcoded allowlist" design principle, kept separate from the generic
|
||||
# Prometheus/Grafana `builtins` set below so it stays visible and auditable.
|
||||
# Add a label here ONLY if it is genuinely injected by infra outside this
|
||||
# repo's OTel code (never as a workaround for a dashboard querying a label
|
||||
# that nothing actually emits — that is a real Rule D violation).
|
||||
EXTERNAL_INFRA_LABELS = {
|
||||
"xrpl_branch", # perf-iac: git ref of the xrpld build under test
|
||||
"xrpl_node_role", # perf-iac: validator/peer role in the perf cluster
|
||||
}
|
||||
|
||||
|
||||
def run_rule_d_dashboards(
|
||||
root: Path, l1_keys: Set[str], metric_labels: Set[str], report: Report
|
||||
) -> None:
|
||||
dash_dir = root / "docker" / "telemetry" / "grafana" / "dashboards"
|
||||
files = sorted(dash_dir.glob("*.json")) if dash_dir.is_dir() else []
|
||||
if not files:
|
||||
report.skip("D", "no dashboard JSON present")
|
||||
return
|
||||
if not l1_keys:
|
||||
report.skip("D", "no L1 key set to validate against")
|
||||
return
|
||||
builtins = {
|
||||
"__name__", # Prometheus reserved label for the metric name itself
|
||||
"le",
|
||||
"exported_instance",
|
||||
"span_name",
|
||||
"status_code",
|
||||
"service_name",
|
||||
"service_version",
|
||||
"service_instance_id",
|
||||
"job",
|
||||
"instance",
|
||||
}
|
||||
# A dashboard label is valid if it is a span attribute (L1), a native-metric
|
||||
# label (L6), a Prometheus/Grafana builtin, or an external-infra identity
|
||||
# label (EXTERNAL_INFRA_LABELS).
|
||||
valid = l1_keys | metric_labels | builtins | EXTERNAL_INFRA_LABELS
|
||||
found = False
|
||||
for f in files:
|
||||
try:
|
||||
text = read_source(f)
|
||||
except OSError:
|
||||
continue
|
||||
# PromQL `sum by (a, b)` and `{label="..."}` references.
|
||||
labels: Set[str] = set()
|
||||
for m in re.finditer(r"by\s*\(([^)]*)\)", text):
|
||||
labels |= {x.strip() for x in m.group(1).split(",") if x.strip()}
|
||||
for m in re.finditer(r"\b([a-z_][a-z0-9_.]*)\s*(?:=~|!~|!=|=)\s*\"", text):
|
||||
labels.add(m.group(1))
|
||||
for lbl in sorted(labels):
|
||||
# Strip a TraceQL scope prefix (span./resource./...) — the bare
|
||||
# attribute is what must resolve against L1.
|
||||
bare = TRACEQL_SCOPE.sub("", lbl)
|
||||
if bare in valid:
|
||||
continue
|
||||
found = True
|
||||
report.violation(
|
||||
"D",
|
||||
str(f.relative_to(root)),
|
||||
lbl,
|
||||
"must exist in L1, a metric label, or be a builtin",
|
||||
)
|
||||
if not found:
|
||||
report.ok(f"D: dashboard PromQL labels all resolve ({len(files)} file(s))")
|
||||
|
||||
|
||||
def run_rule_e_runbook(root: Path, l1_keys: Set[str], report: Report) -> None:
|
||||
path = root / "docs" / "telemetry-runbook.md"
|
||||
if not path.is_file():
|
||||
report.skip("E", "runbook not present")
|
||||
return
|
||||
if not l1_keys:
|
||||
report.skip("E", "no L1 key set to validate against")
|
||||
return
|
||||
text = read_source(path)
|
||||
found = False
|
||||
# Only the dotted `xrpl.<domain>.<field>` attribute form is a violation. The
|
||||
# `xrpl.`-with-trailing-dot anchor is the discriminator: it matches the old
|
||||
# dotted attribute convention being migrated away from, while everything
|
||||
# else legitimately dotted in the runbook does NOT match it —
|
||||
# * span names (`consensus.round`, `tx.process`) no `xrpl.` prefix
|
||||
# * filenames (`xrpld.cfg`, `RCLConsensus.cpp`) `xrpld.`/`.cpp`, not `xrpl.`
|
||||
# * OTel-standard (`service.name`, `http.method`) no `xrpl.` prefix
|
||||
# * metric labels (`xrpl_rpc_command`) underscore, no dot
|
||||
# Legitimate dotted resource attrs (`xrpl.network.id`/`.type`) are in L1 and
|
||||
# are skipped. A dotted `xrpl.` token absent from L1 is a genuine doc/code
|
||||
# mismatch (e.g. `xrpl.tx.hash` where the code emits `tx_hash`).
|
||||
for m in re.finditer(r"`(xrpl\.[a-z][a-z0-9_.]*)`", text):
|
||||
token = m.group(1)
|
||||
if token in l1_keys: # legitimate dotted resource attr (xrpl.network.*)
|
||||
continue
|
||||
found = True
|
||||
report.violation(
|
||||
"E", str(path.relative_to(root)), token, "underscore, not dotted"
|
||||
)
|
||||
if not found:
|
||||
report.ok("E: runbook attribute references consistent with L1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,961 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Unit tests for check_otel_naming.py.
|
||||
|
||||
Stdlib-only (unittest), matching the dependency-free policy of the check itself.
|
||||
Run from anywhere:
|
||||
|
||||
python .github/scripts/otel-naming/test_check_otel_naming.py
|
||||
|
||||
Each rule is exercised in isolation against a synthetic tree / synthetic L1 key
|
||||
set, covering positive (must flag), negative (must not flag), and boundary
|
||||
cases. Rule E (runbook dotted-attribute detection) has the densest coverage
|
||||
because its discriminator — the `xrpl.<domain>.` prefix vs span names,
|
||||
filenames, OTel-standard keys, and metric labels — is the subtlest.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# Load the check module by path (it is not an importable package).
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"check_otel_naming", str(Path(__file__).with_name("check_otel_naming.py"))
|
||||
)
|
||||
chk = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(chk)
|
||||
|
||||
|
||||
# A controlled L1 set used across tests: the two legitimate dotted resource
|
||||
# attrs plus a handful of underscore span-attribute keys.
|
||||
L1 = {
|
||||
"xrpl.network.id",
|
||||
"xrpl.network.type",
|
||||
"tx_hash",
|
||||
"peer_id",
|
||||
"consensus_mode",
|
||||
"command",
|
||||
"rpc_status",
|
||||
"ledger_seq",
|
||||
}
|
||||
|
||||
|
||||
def _run_rule_e(runbook_text: str):
|
||||
"""Run Rule E against a synthetic runbook; return the flagged tokens."""
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "docs").mkdir()
|
||||
(d / "docs" / "telemetry-runbook.md").write_text(runbook_text)
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, set(L1), report)
|
||||
return sorted(v[2] for v in report.violations)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class RuleERunbook(unittest.TestCase):
|
||||
"""Rule E: only dotted `xrpl.<domain>.<field>` attribute keys are flagged."""
|
||||
|
||||
# ----- positive: genuine dotted attribute-key violations -----
|
||||
def test_single_dotted_attr(self):
|
||||
self.assertEqual(_run_rule_e("`xrpl.tx.hash`"), ["xrpl.tx.hash"])
|
||||
|
||||
def test_multiple_dotted_attrs(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.tx.hash` and `xrpl.consensus.mode`"),
|
||||
["xrpl.consensus.mode", "xrpl.tx.hash"],
|
||||
)
|
||||
|
||||
def test_deep_dotted_three_segments(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.consensus.ledger.seq`"), ["xrpl.consensus.ledger.seq"]
|
||||
)
|
||||
|
||||
def test_dotted_attr_with_underscore_field(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.consensus.round_id`"), ["xrpl.consensus.round_id"]
|
||||
)
|
||||
|
||||
def test_repeated_token_reported_each_occurrence(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.tx.hash` ... `xrpl.tx.hash`"),
|
||||
["xrpl.tx.hash", "xrpl.tx.hash"],
|
||||
)
|
||||
|
||||
def test_resource_attr_not_in_l1_is_flagged(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.network.unknown`"), ["xrpl.network.unknown"]
|
||||
)
|
||||
|
||||
# ----- negative: legitimately-dotted tokens that must NOT be flagged -----
|
||||
def test_span_name_single(self):
|
||||
self.assertEqual(_run_rule_e("`consensus.round`"), [])
|
||||
|
||||
def test_span_name_multi_segment(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`consensus.phase.open` `rpc.command.server_info`"), []
|
||||
)
|
||||
|
||||
def test_filename_cfg(self):
|
||||
self.assertEqual(_run_rule_e("`xrpld.cfg`"), [])
|
||||
|
||||
def test_filename_cpp(self):
|
||||
self.assertEqual(_run_rule_e("`RCLConsensus.cpp`"), [])
|
||||
|
||||
def test_otel_standard_service_name(self):
|
||||
self.assertEqual(_run_rule_e("`service.name`"), [])
|
||||
|
||||
def test_otel_standard_http_method(self):
|
||||
self.assertEqual(_run_rule_e("`http.method`"), [])
|
||||
|
||||
def test_metric_label_underscore(self):
|
||||
self.assertEqual(_run_rule_e("`xrpl_rpc_command`"), [])
|
||||
|
||||
def test_bare_underscore_attrs(self):
|
||||
self.assertEqual(_run_rule_e("`tx_hash` `consensus_mode`"), [])
|
||||
|
||||
def test_legit_dotted_resource_attrs_in_l1(self):
|
||||
self.assertEqual(_run_rule_e("`xrpl.network.id` `xrpl.network.type`"), [])
|
||||
|
||||
def test_prose_word(self):
|
||||
self.assertEqual(_run_rule_e("the `command` attribute"), [])
|
||||
|
||||
def test_plain_prose_no_backticks(self):
|
||||
self.assertEqual(_run_rule_e("xrpl.tx.hash without backticks is prose"), [])
|
||||
|
||||
# ----- boundary -----
|
||||
def test_empty_runbook(self):
|
||||
self.assertEqual(_run_rule_e(""), [])
|
||||
|
||||
def test_lookalike_prefix_xrpld(self):
|
||||
# `xrpld.` is NOT `xrpl.` — must not match.
|
||||
self.assertEqual(_run_rule_e("`xrpld.foo`"), [])
|
||||
|
||||
def test_lookalike_prefix_underscore(self):
|
||||
# `xrpl_rpc.command` starts with `xrpl_`, not `xrpl.`.
|
||||
self.assertEqual(_run_rule_e("`xrpl_rpc.command`"), [])
|
||||
|
||||
def test_uppercase_segment_not_matched(self):
|
||||
# The pattern requires a lowercase char after `xrpl.`; uppercase keys are
|
||||
# caught by Rule G at the L1 layer, not by the runbook text scan.
|
||||
self.assertEqual(_run_rule_e("`xrpl.TX.hash`"), [])
|
||||
|
||||
def test_token_touching_table_pipes(self):
|
||||
self.assertEqual(_run_rule_e("| `xrpl.tx.hash` | desc |"), ["xrpl.tx.hash"])
|
||||
|
||||
def test_mixed_line_only_xrpl_dotted_flagged(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`consensus.round` uses `xrpl.tx.hash` and `service.name`"),
|
||||
["xrpl.tx.hash"],
|
||||
)
|
||||
|
||||
def test_skips_when_runbook_absent(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, set(L1), report)
|
||||
self.assertEqual(report.violations, [])
|
||||
self.assertTrue(any("SKIP: E" in s for s in report.skips))
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_skips_when_l1_empty(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "docs").mkdir()
|
||||
(d / "docs" / "telemetry-runbook.md").write_text("`xrpl.tx.hash`")
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, set(), report)
|
||||
self.assertEqual(report.violations, [])
|
||||
self.assertTrue(any("SKIP: E" in s for s in report.skips))
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class DslParser(unittest.TestCase):
|
||||
"""The makeStr/join/seg:: constexpr DSL resolver — the foundation of the
|
||||
L1 key set. Covers flat, nested, cross-file, alias, and multi-line forms."""
|
||||
|
||||
def test_flat_join(self):
|
||||
syms = chk.resolve_constants(
|
||||
'inline constexpr auto a = makeStr("xrpl");\n'
|
||||
'inline constexpr auto b = makeStr("network");\n'
|
||||
"inline constexpr auto c = join(a, b);\n"
|
||||
)
|
||||
self.assertEqual(syms["c"], "xrpl.network")
|
||||
|
||||
def test_nested_join_three_segments(self):
|
||||
syms = chk.resolve_constants(
|
||||
'inline constexpr auto xrpl = makeStr("xrpl");\n'
|
||||
'inline constexpr auto network = makeStr("network");\n'
|
||||
"inline constexpr auto networkId = "
|
||||
'join(join(xrpl, network), makeStr("id"));\n'
|
||||
)
|
||||
self.assertEqual(syms["networkId"], "xrpl.network.id")
|
||||
|
||||
def test_qualified_seg_reference(self):
|
||||
# `seg::rpc` resolves by its bare leaf `rpc`.
|
||||
syms = chk.resolve_constants('inline constexpr auto rpc = makeStr("rpc");\n')
|
||||
syms2 = chk.resolve_constants(
|
||||
'inline constexpr auto command = join(seg::rpc, makeStr("command"));\n',
|
||||
syms,
|
||||
)
|
||||
self.assertEqual(syms2["command"], "rpc.command")
|
||||
|
||||
def test_alias_reference(self):
|
||||
syms = chk.resolve_constants('inline constexpr auto rpc = makeStr("rpc");\n')
|
||||
chk.resolve_constants("inline constexpr auto alias = seg::rpc;\n", syms)
|
||||
self.assertEqual(syms["alias"], "rpc")
|
||||
|
||||
def test_unresolvable_expr_omitted(self):
|
||||
syms = chk.resolve_constants("inline constexpr auto x = join(unknown, y);\n")
|
||||
self.assertNotIn("x", syms)
|
||||
|
||||
def test_split_top_level_args_respects_nesting(self):
|
||||
self.assertEqual(
|
||||
chk.split_top_level_args("join(seg::a, b), c"),
|
||||
["join(seg::a, b)", " c"],
|
||||
)
|
||||
|
||||
def test_split_top_level_args_ignores_comma_in_string(self):
|
||||
self.assertEqual(
|
||||
chk.split_top_level_args('key, ","'),
|
||||
["key", ' ","'],
|
||||
)
|
||||
|
||||
def test_strip_comments_removes_line_and_block(self):
|
||||
self.assertEqual(
|
||||
chk.strip_comments("a // line\nb /* blk */ c").split(),
|
||||
["a", "b", "c"],
|
||||
)
|
||||
|
||||
|
||||
def _write(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
|
||||
|
||||
def _header(ns_attr_body: str, prefix_seg: str = "") -> str:
|
||||
"""A minimal *SpanNames.h body: optional seg defs + a namespace attr block."""
|
||||
return (
|
||||
"#pragma once\n"
|
||||
+ prefix_seg
|
||||
+ "namespace xrpl::telemetry::demo::span {\n"
|
||||
+ "namespace attr {\n"
|
||||
+ ns_attr_body
|
||||
+ "} // namespace attr\n"
|
||||
+ "}\n"
|
||||
)
|
||||
|
||||
|
||||
class AttrKeyExtraction(unittest.TestCase):
|
||||
"""attr_keys_from_header: comment-stripping + multi-line + using re-export."""
|
||||
|
||||
def _l1(self, header_text):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
h = d / "src" / "DemoSpanNames.h"
|
||||
_write(h, header_text)
|
||||
syms = chk.build_global_symbols([h])
|
||||
return chk.attr_keys_from_header(h, syms)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_single_line_makestr(self):
|
||||
keys = self._l1(_header('inline constexpr auto k = makeStr("tx_hash");\n'))
|
||||
self.assertIn("tx_hash", keys)
|
||||
|
||||
def test_multiline_constexpr_captured(self):
|
||||
keys = self._l1(
|
||||
_header("inline constexpr auto k =\n" ' makeStr("round_time_ms");\n')
|
||||
)
|
||||
self.assertIn("round_time_ms", keys)
|
||||
|
||||
def test_commented_makestr_not_leaked(self):
|
||||
keys = self._l1(
|
||||
_header(
|
||||
'inline constexpr auto k = makeStr("good");\n'
|
||||
'// inline constexpr auto bad = makeStr("old.dotted");\n'
|
||||
)
|
||||
)
|
||||
self.assertIn("good", keys)
|
||||
self.assertNotIn("old.dotted", keys)
|
||||
|
||||
def test_block_commented_makestr_not_leaked(self):
|
||||
keys = self._l1(
|
||||
_header(
|
||||
'inline constexpr auto k = makeStr("good");\n'
|
||||
'/* makeStr("blockbad") */\n'
|
||||
)
|
||||
)
|
||||
self.assertNotIn("blockbad", keys)
|
||||
|
||||
|
||||
class CamelToDotSegments(unittest.TestCase):
|
||||
"""semconv CamelCase -> dotted OTel-standard key derivation."""
|
||||
|
||||
def test_service_instance_id(self):
|
||||
self.assertEqual(
|
||||
chk.camel_to_dotsegments("ServiceInstanceId"),
|
||||
["service", "instance", "id"],
|
||||
)
|
||||
|
||||
def test_service_name(self):
|
||||
self.assertEqual(chk.camel_to_dotsegments("ServiceName"), ["service", "name"])
|
||||
|
||||
def test_derive_keys_from_telemetry_cpp(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
tele = d / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp"
|
||||
_write(
|
||||
tele,
|
||||
"resource::Resource::Create({\n"
|
||||
" {semconv::service::kServiceName, x},\n"
|
||||
" {semconv::service::kServiceInstanceId, y},\n"
|
||||
"});\n",
|
||||
)
|
||||
report = chk.Report()
|
||||
allow = chk.derive_dotted_resource_keys(d, {}, report)
|
||||
self.assertIn("service.name", allow)
|
||||
self.assertIn("service.instance.id", allow)
|
||||
finally:
|
||||
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)
|
||||
return sorted(v[2] for v in report.violations)
|
||||
|
||||
|
||||
class RuleADotted(unittest.TestCase):
|
||||
def test_dotted_attr_not_in_allow_flagged(self):
|
||||
kbh = {Path("src/RpcSpanNames.h"): {"xrpl.tx.hash", "command"}}
|
||||
self.assertEqual(_run_rule_a(kbh, {"xrpl.network.id"}), ["xrpl.tx.hash"])
|
||||
|
||||
def test_resource_attr_in_allow_passes(self):
|
||||
kbh = {Path("src/SpanNames.h"): {"xrpl.network.id"}}
|
||||
self.assertEqual(_run_rule_a(kbh, {"xrpl.network.id"}), [])
|
||||
|
||||
def test_bare_key_never_flagged(self):
|
||||
kbh = {Path("src/TxSpanNames.h"): {"tx_hash", "command"}}
|
||||
self.assertEqual(_run_rule_a(kbh, set()), [])
|
||||
|
||||
|
||||
def _run_rule_g(keys_by_header):
|
||||
report = chk.Report()
|
||||
chk.run_rule_g(keys_by_header, report)
|
||||
return sorted(v[2] for v in report.violations)
|
||||
|
||||
|
||||
class RuleGSnakeCase(unittest.TestCase):
|
||||
def test_camelcase_flagged(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"txHash"}}), ["txHash"])
|
||||
|
||||
def test_uppercase_flagged(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"TX_HASH"}}), ["TX_HASH"])
|
||||
|
||||
def test_space_flagged(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"bad key"}}), ["bad key"])
|
||||
|
||||
def test_snake_case_passes(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"tx_hash", "rpc_status"}}), [])
|
||||
|
||||
def test_dotted_resource_segments_pass(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"xrpl.network.id"}}), [])
|
||||
|
||||
def test_dotted_with_bad_segment_flagged(self):
|
||||
self.assertEqual(
|
||||
_run_rule_g({Path("h"): {"xrpl.Network.id"}}), ["xrpl.Network.id"]
|
||||
)
|
||||
|
||||
|
||||
class RuleFAndH(unittest.TestCase):
|
||||
"""run_rule_f: literal keys/span-names flagged; values & tests exempt.
|
||||
Rule H: qualified constant not in any header warns (non-fatal)."""
|
||||
|
||||
def _run(self, rel_path, source, header_symbols=frozenset()):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(d / rel_path, source)
|
||||
report = chk.Report()
|
||||
chk.run_rule_f(d, report, set(header_symbols))
|
||||
return (
|
||||
sorted(v[2] for v in report.violations),
|
||||
sorted(w[2] for w in report.warnings),
|
||||
)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_literal_key_flagged(self):
|
||||
v, _ = self._run("src/Foo.cpp", 'g.setAttribute("lit_key", v);\n')
|
||||
self.assertEqual(v, ['setAttribute arg0 "lit_key"'])
|
||||
|
||||
def test_literal_value_exempt(self):
|
||||
v, _ = self._run("src/Foo.cpp", 'g.setAttribute(attr::command, "submit");\n')
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_span_name_args_flagged(self):
|
||||
v, _ = self._run("src/Foo.cpp", 'SpanGuard::span(cat, "rpc", "command");\n')
|
||||
self.assertEqual(v, ['span arg1 "rpc"', 'span arg2 "command"'])
|
||||
|
||||
def test_rootspan_literal_flagged_by_rule_f(self):
|
||||
# rootSpan(cat, prefix, name) shares span()'s signature, so a string
|
||||
# literal in the prefix/name position must FAIL rule F exactly as it
|
||||
# does for span() — otherwise a call switched to rootSpan silently
|
||||
# escapes span-name validation.
|
||||
v, _ = self._run(
|
||||
"src/Foo.cpp",
|
||||
'SpanGuard::rootSpan(cat, "peer", "validation.receive");\n',
|
||||
)
|
||||
self.assertEqual(
|
||||
v, ['rootSpan arg1 "peer"', 'rootSpan arg2 "validation.receive"']
|
||||
)
|
||||
|
||||
def test_rootspan_constant_args_accepted(self):
|
||||
# Constant references in the prefix/name position are accepted (no
|
||||
# rule F), mirroring span()'s constant-arg handling.
|
||||
v, _ = self._run(
|
||||
"src/Foo.cpp",
|
||||
"SpanGuard::rootSpan(TraceCategory::Peer, seg::peer, "
|
||||
"peer_span::op::validationReceive);\n",
|
||||
)
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_test_path_exempt(self):
|
||||
v, _ = self._run("src/test/Foo.cpp", 'g.setAttribute("lit_key", v);\n')
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_spannames_header_exempt(self):
|
||||
v, _ = self._run("src/DemoSpanNames.h", 'g.setAttribute("lit_key", v);\n')
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_bare_span_call_not_matched(self):
|
||||
# No SpanGuard/./-> receiver -> not a telemetry call-site.
|
||||
v, _ = self._run("src/Foo.cpp", 'auto s = span("not", "telemetry");\n')
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_multiline_call_reports_first_line(self):
|
||||
v, _ = self._run("src/Foo.cpp", 'g.setAttribute(\n "k",\n v);\n')
|
||||
self.assertEqual(v, ['setAttribute arg0 "k"'])
|
||||
|
||||
def test_paren_in_string_value_does_not_break_parsing(self):
|
||||
# The ")" inside the value must not end the call early; key still seen.
|
||||
v, _ = self._run("src/Foo.cpp", 'g.setAttribute("k", ")");\n')
|
||||
self.assertEqual(v, ['setAttribute arg0 "k"'])
|
||||
|
||||
def test_rule_h_qualified_constant_warns(self):
|
||||
v, w = self._run(
|
||||
"src/Foo.cpp",
|
||||
"g.setAttribute(consensus::span::accept, v);\n",
|
||||
header_symbols={"command"},
|
||||
)
|
||||
self.assertEqual(v, [])
|
||||
self.assertEqual(w, ["setAttribute arg0 consensus::span::accept"])
|
||||
|
||||
def test_rule_h_known_constant_no_warning(self):
|
||||
_, w = self._run(
|
||||
"src/Foo.cpp",
|
||||
"g.setAttribute(rpc_span::attr::command, v);\n",
|
||||
header_symbols={"command"},
|
||||
)
|
||||
self.assertEqual(w, [])
|
||||
|
||||
def test_rule_h_bare_local_no_warning(self):
|
||||
_, w = self._run(
|
||||
"src/Foo.cpp", "g.setAttribute(myLeaf, v);\n", header_symbols={"command"}
|
||||
)
|
||||
self.assertEqual(w, [])
|
||||
|
||||
|
||||
class RuleBCollector(unittest.TestCase):
|
||||
def _run(self, yaml_text, l1):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(d / "docker" / "telemetry" / "otel-collector-config.yaml", yaml_text)
|
||||
report = chk.Report()
|
||||
chk.run_rule_b_collector(d, set(l1), report)
|
||||
return sorted(v[2] for v in report.violations), report.skips
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_dimension_not_in_l1_flagged(self):
|
||||
y = "spanmetrics:\n dimensions:\n - name: bogus_dim\n - name: command\n"
|
||||
v, _ = self._run(y, {"command"})
|
||||
self.assertEqual(v, ["bogus_dim"])
|
||||
|
||||
def test_all_dimensions_in_l1_pass(self):
|
||||
y = "spanmetrics:\n dimensions:\n - name: command\n - name: rpc_status\n"
|
||||
v, _ = self._run(y, {"command", "rpc_status"})
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_comment_does_not_end_list_but_dedent_does(self):
|
||||
# The entries sit at the key's own indentation, which YAML allows. A
|
||||
# comment holding a colon sits between them, so the entry after it must
|
||||
# still be read. The sibling key `other:` ends the list, so the
|
||||
# `- name:` under it is not a dimension.
|
||||
y = (
|
||||
"connectors:\n"
|
||||
" spanmetrics:\n"
|
||||
" dimensions:\n"
|
||||
" - name: command\n"
|
||||
" # Note: a comment with a colon.\n"
|
||||
" - name: bogus_after_comment\n"
|
||||
" other:\n"
|
||||
" - name: bogus_outside_list\n"
|
||||
)
|
||||
v, _ = self._run(y, {"command"})
|
||||
self.assertEqual(v, ["bogus_after_comment"])
|
||||
|
||||
def test_skip_when_no_spanmetrics_block(self):
|
||||
v, skips = self._run("receivers:\n otlp:\n", {"command"})
|
||||
self.assertEqual(v, [])
|
||||
self.assertTrue(any("SKIP: B" in s for s in skips))
|
||||
|
||||
|
||||
class RuleCTempo(unittest.TestCase):
|
||||
"""Rule C reads the Grafana Tempo DATASOURCE file's search.filters and
|
||||
validates only span-scope tags against L1."""
|
||||
|
||||
DS = "docker/telemetry/grafana/provisioning/datasources/tempo.yaml"
|
||||
|
||||
def _run(self, yaml_text, l1):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(d / self.DS, yaml_text)
|
||||
report = chk.Report()
|
||||
chk.run_rule_c_tempo(d, set(l1), report)
|
||||
return sorted(v[2] for v in report.violations), report.skips
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def _filter(self, fid, tag, scope):
|
||||
return (
|
||||
f" - id: {fid}\n"
|
||||
f" tag: {tag}\n"
|
||||
f' operator: "="\n'
|
||||
f" scope: {scope}\n"
|
||||
f" type: static\n"
|
||||
)
|
||||
|
||||
def test_span_tag_not_in_l1_flagged(self):
|
||||
y = "search:\n filters:\n" + self._filter("f1", "bogus_tag", "span")
|
||||
v, _ = self._run(y, {"command"})
|
||||
self.assertEqual(v, ["bogus_tag"])
|
||||
|
||||
def test_span_tags_in_l1_pass(self):
|
||||
y = (
|
||||
"search:\n filters:\n"
|
||||
+ self._filter("f1", "command", "span")
|
||||
+ self._filter("f2", "tx_hash", "span")
|
||||
)
|
||||
v, _ = self._run(y, {"command", "tx_hash"})
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_resource_and_intrinsic_tags_ignored(self):
|
||||
# service.* (resource) and name/status/duration (intrinsic) are not
|
||||
# span attributes — they must not be validated against L1.
|
||||
y = (
|
||||
"search:\n filters:\n"
|
||||
+ self._filter("f1", "service.instance.id", "resource")
|
||||
+ self._filter("f2", "name", "intrinsic")
|
||||
+ self._filter("f3", "duration", "intrinsic")
|
||||
)
|
||||
v, skips = self._run(y, {"command"})
|
||||
self.assertEqual(v, [])
|
||||
self.assertTrue(any("SKIP: C" in s for s in skips))
|
||||
|
||||
def test_skip_when_datasource_absent(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
report = chk.Report()
|
||||
chk.run_rule_c_tempo(d, {"command"}, report)
|
||||
self.assertEqual(report.violations, [])
|
||||
self.assertTrue(any("SKIP: C" in s for s in report.skips))
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class RuleDDashboards(unittest.TestCase):
|
||||
def _run(self, json_text, l1, metric_labels=frozenset()):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(
|
||||
d / "docker" / "telemetry" / "grafana" / "dashboards" / "x.json",
|
||||
json_text,
|
||||
)
|
||||
report = chk.Report()
|
||||
chk.run_rule_d_dashboards(d, set(l1), set(metric_labels), report)
|
||||
return sorted(v[2] for v in report.violations)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_unknown_promql_label_flagged(self):
|
||||
self.assertEqual(
|
||||
self._run('"expr": "sum by (bogus_label) (x)"', {"command"}),
|
||||
["bogus_label"],
|
||||
)
|
||||
|
||||
# Rule D returns early on an empty L1 set, so a test that passes one asserts
|
||||
# nothing. Each test below passes a nonempty L1 set and puts `bogus_label` in
|
||||
# the same expression as the labels under test: the assertion then pins both
|
||||
# halves at once — the accepted labels are absent from the result, and Rule D
|
||||
# demonstrably ran because it flagged the bad one.
|
||||
|
||||
def test_builtin_labels_not_flagged(self):
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "sum by (le, span_name, exported_instance, bogus_label) (x)"',
|
||||
{"command"},
|
||||
),
|
||||
["bogus_label"],
|
||||
)
|
||||
|
||||
def test_external_infra_labels_not_flagged(self):
|
||||
# EXTERNAL_INFRA_LABELS (perf-iac identity labels with no in-tree
|
||||
# source) must be recognized as valid, distinct from `builtins`. The
|
||||
# names are spelled out rather than joined from chk.EXTERNAL_INFRA_LABELS
|
||||
# because building the query from the set that validates it passes for
|
||||
# whatever that set happens to hold — including an empty one.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "sum by (xrpl_branch, xrpl_node_role, bogus_label) (x)"',
|
||||
{"command"},
|
||||
),
|
||||
["bogus_label"],
|
||||
)
|
||||
|
||||
def test_prometheus_name_label_not_flagged(self):
|
||||
# `__name__` is the Prometheus reserved metric-name label; the renamed
|
||||
# system-*.json dashboards use `sum by (le, __name__)`.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "sum by (le, __name__, bogus_label) (rate(x[5m]))"',
|
||||
{"command"},
|
||||
),
|
||||
["bogus_label"],
|
||||
)
|
||||
|
||||
def test_l1_label_passes(self):
|
||||
# `by (...)` form, not a `{command="x"}` selector: a dashboard stores the
|
||||
# query inside a JSON string, so its quotes are escaped on disk and the
|
||||
# selector branch extracts nothing from them here.
|
||||
self.assertEqual(
|
||||
self._run('"expr": "sum by (command, bogus_label) (x)"', {"command"}),
|
||||
["bogus_label"],
|
||||
)
|
||||
|
||||
def test_traceql_span_prefix_stripped(self):
|
||||
# `span.establish_count` must validate against the bare L1 key.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "count_over_time(x) by (span.establish_count)"',
|
||||
{"establish_count"},
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_traceql_resource_prefix_stripped(self):
|
||||
# `resource.service_name` must validate against the bare builtin, same as
|
||||
# the `span.` case above.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "count_over_time(x) by (resource.service_name, bogus_label)"',
|
||||
{"command"},
|
||||
),
|
||||
["bogus_label"],
|
||||
)
|
||||
|
||||
def test_native_metric_label_passes(self):
|
||||
# `job_type` / `reason` are emitted by MetricsRegistry, not span attrs.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "sum by (job_type, reason) (x)"',
|
||||
{"command"},
|
||||
metric_labels={"job_type", "reason"},
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_unknown_label_still_flagged_with_metric_labels(self):
|
||||
# A label that is neither L1, metric label, nor builtin still fails.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "sum by (bogus) (x)"',
|
||||
{"command"},
|
||||
metric_labels={"job_type"},
|
||||
),
|
||||
["bogus"],
|
||||
)
|
||||
|
||||
def test_span_prefixed_unknown_still_flagged(self):
|
||||
# `span.not_a_key` whose bare form is unknown is still a violation.
|
||||
self.assertEqual(
|
||||
self._run('"expr": "x by (span.not_a_key)"', {"command"}),
|
||||
["span.not_a_key"],
|
||||
)
|
||||
|
||||
def test_not_equal_selector_label_flagged(self):
|
||||
# A label used only with `!=` must still be extracted and validated.
|
||||
# The operator class `[=!]~?` matched `=`, `=~` and `!~`, but not
|
||||
# `!=`, so a label that appears only in a `!=` selector slipped past
|
||||
# Rule D -- a false negative, where a bad label name used with `!=`
|
||||
# was never caught. The quote is unescaped, which both the phase-1c
|
||||
# (`\s*\"`) and phase-9 (`\s*\\?\"`) selector forms accept.
|
||||
#
|
||||
# Mutation caught: revert the operator class to `[=!]~?` and this
|
||||
# test fails, because `bogus_label` is never extracted and the
|
||||
# result is empty instead of ["bogus_label"].
|
||||
self.assertEqual(
|
||||
self._run('"expr": "node_metric{bogus_label!="x"}"', {"command"}),
|
||||
["bogus_label"],
|
||||
)
|
||||
|
||||
|
||||
class MetricLabelExtraction(unittest.TestCase):
|
||||
"""L6: native-metric label keys parsed from C++ instrument calls."""
|
||||
|
||||
def test_extracts_add_label(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(
|
||||
d / "src" / "xrpld" / "telemetry" / "MetricsRegistry.cpp",
|
||||
'counter->Add(1, {{"job_type", std::string(jobType)}});\n'
|
||||
'c2->Add(1, {{"reason", std::string(r)}});\n',
|
||||
)
|
||||
self.assertEqual(chk.metric_label_names(d), {"job_type", "reason"})
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_no_metrics_file_empty(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "src").mkdir()
|
||||
self.assertEqual(chk.metric_label_names(d), set())
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class ReportExitContract(unittest.TestCase):
|
||||
@staticmethod
|
||||
def _exit_code(report):
|
||||
"""Call render_and_exit (which prints + raises SystemExit), swallowing
|
||||
its stdout, and return the exit code."""
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
try:
|
||||
report.render_and_exit()
|
||||
except SystemExit as e:
|
||||
return e.code
|
||||
return None # pragma: no cover - render_and_exit always exits
|
||||
|
||||
def test_violation_exits_nonzero(self):
|
||||
r = chk.Report()
|
||||
r.violation("A", "f", "tok", "exp")
|
||||
self.assertEqual(self._exit_code(r), 1)
|
||||
|
||||
def test_clean_exits_zero(self):
|
||||
r = chk.Report()
|
||||
r.ok("all good")
|
||||
self.assertEqual(self._exit_code(r), 0)
|
||||
|
||||
def test_warning_only_exits_zero(self):
|
||||
r = chk.Report()
|
||||
r.warning("H", "f", "tok", "note")
|
||||
self.assertEqual(self._exit_code(r), 0)
|
||||
|
||||
|
||||
class RuleEReportTuple(unittest.TestCase):
|
||||
"""Assert Rule E records the full (rule, expected) tuple, not just token."""
|
||||
|
||||
def test_violation_tuple_fields(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "docs").mkdir()
|
||||
(d / "docs" / "telemetry-runbook.md").write_text("`xrpl.tx.hash`")
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, {"xrpl.network.id"}, report)
|
||||
self.assertEqual(len(report.violations), 1)
|
||||
rule, _loc, token, expected = report.violations[0]
|
||||
self.assertEqual(rule, "E")
|
||||
self.assertEqual(token, "xrpl.tx.hash")
|
||||
self.assertEqual(expected, "underscore, not dotted")
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_clean_runbook_records_ok(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "docs").mkdir()
|
||||
(d / "docs" / "telemetry-runbook.md").write_text(
|
||||
"`tx_hash` `consensus.round`"
|
||||
)
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, {"tx_hash"}, report)
|
||||
self.assertEqual(report.violations, [])
|
||||
self.assertTrue(any("E:" in c for c in report.checked))
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -70,10 +70,8 @@ jobs:
|
||||
files: |
|
||||
# These paths are unique to `on-pr.yml`.
|
||||
.github/scripts/levelization/**
|
||||
.github/scripts/otel-naming/**
|
||||
.github/scripts/rename/**
|
||||
.github/workflows/reusable-check-levelization.yml
|
||||
.github/workflows/reusable-check-otel-naming.yml
|
||||
.github/workflows/reusable-check-rename.yml
|
||||
.github/workflows/on-pr.yml
|
||||
|
||||
@@ -147,11 +145,6 @@ jobs:
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-levelization.yml
|
||||
|
||||
check-otel-naming:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-otel-naming.yml
|
||||
|
||||
check-rename:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
@@ -278,7 +271,6 @@ jobs:
|
||||
needs:
|
||||
- check-autogen
|
||||
- check-levelization
|
||||
- check-otel-naming
|
||||
- check-rename
|
||||
- check-release-commits
|
||||
- clang-tidy
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# This workflow checks that OpenTelemetry span-attribute names stay consistent
|
||||
# across the code (*SpanNames.h), collector, Tempo, dashboards, and docs.
|
||||
# See .github/scripts/otel-naming/check_otel_naming.py and the
|
||||
# "Telemetry span attribute naming" section in CONTRIBUTING.md.
|
||||
name: Check OTel naming
|
||||
|
||||
# This workflow can only be triggered by other workflows.
|
||||
on: workflow_call
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-otel-naming
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
otel-naming:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Check OTel naming
|
||||
# The script is stdlib-only and reads only files already in the tree;
|
||||
# it enforces each rule only when the layer it needs is present, so it
|
||||
# works whether telemetry changes land in one PR or several.
|
||||
run: python .github/scripts/otel-naming/check_otel_naming.py
|
||||
@@ -100,6 +100,3 @@ target/
|
||||
|
||||
# Rust build directory
|
||||
crates/target
|
||||
|
||||
# Env. file carrying environmental setup data for local or cloud runs.
|
||||
.env.*
|
||||
@@ -140,24 +140,6 @@ if(rocksdb)
|
||||
target_link_libraries(xrpl_libs INTERFACE RocksDB::rocksdb)
|
||||
endif()
|
||||
|
||||
# OpenTelemetry distributed tracing (optional).
|
||||
# When on, links against opentelemetry-cpp and defines XRPL_ENABLE_TELEMETRY
|
||||
# so that SpanGuard factory methods produce real OTel spans.
|
||||
# When off, all tracing code compiles to no-ops with zero overhead.
|
||||
#
|
||||
# There is no CMake option. The one switch is `conan install -o telemetry=`,
|
||||
# which decides whether opentelemetry-cpp is fetched and sets the variable read
|
||||
# below through the generated toolchain.
|
||||
#
|
||||
# The define is set for every target in this project, and also PUBLIC on
|
||||
# xrpl.libxrpl.telemetry (cmake/XrplCore.cmake), so a parent project that
|
||||
# adds this one with add_subdirectory() and links that module gets it too.
|
||||
if(telemetry)
|
||||
find_package(opentelemetry-cpp CONFIG REQUIRED)
|
||||
add_compile_definitions(XRPL_ENABLE_TELEMETRY)
|
||||
message(STATUS "OpenTelemetry tracing enabled")
|
||||
endif()
|
||||
|
||||
# Work around changes to Conan recipe for now.
|
||||
if(TARGET nudb::core)
|
||||
set(nudb nudb::core)
|
||||
|
||||
@@ -377,66 +377,6 @@ run-clang-tidy -p build -quiet -fix -format -allow-no-checks src tests
|
||||
|
||||
`-format` reformats the fixed code with [`.clang-format`](./.clang-format); without it the fixes are inserted in LLVM style and the `clang-format` hook rewrites them afterwards.
|
||||
|
||||
## Telemetry span attribute naming
|
||||
|
||||
OpenTelemetry span attribute keys follow these rules so they stay consistent
|
||||
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 — 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_phase`, `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.
|
||||
5. Span names use `<subsystem>[.<component>]` (dotted). Only attribute _keys_
|
||||
follow rules 1–4.
|
||||
|
||||
All attribute keys are `lower_snake_case` (lowercase letters, digits, and
|
||||
underscores; each dot-separated segment of a resource key likewise). No
|
||||
camelCase, uppercase, or spaces.
|
||||
|
||||
Standard OpenTelemetry semantic-convention keys keep their canonical dotted
|
||||
form (e.g. `service.*` resource attributes, `http.*` span attributes); the
|
||||
"no dotted form" rule above applies to xrpl-custom keys, not to OTel-standard
|
||||
conventions.
|
||||
|
||||
Always reference the `*SpanNames.h` constants for attribute keys and span
|
||||
names — never pass a string literal as a key or as a `span`/`childSpan` name
|
||||
argument. (Attribute _values_ may be runtime data.)
|
||||
|
||||
These rules are enforced by `.github/scripts/otel-naming/check_otel_naming.py`,
|
||||
run in CI on every pull request. The check derives the set of valid keys
|
||||
directly from the `*SpanNames.h` constants and the resource attributes the code
|
||||
registers, so there is no separate list to keep in sync. It cross-validates the
|
||||
collector, Tempo, dashboards, and docs against those keys, and each rule runs
|
||||
only when the file it needs is present — so it works whether telemetry changes
|
||||
land in one pull request or several. Run it locally with:
|
||||
|
||||
```
|
||||
python .github/scripts/otel-naming/check_otel_naming.py
|
||||
```
|
||||
|
||||
See [.github/scripts/otel-naming/README.md](.github/scripts/otel-naming/README.md)
|
||||
for the full rule list.
|
||||
|
||||
## Contracts and instrumentation
|
||||
|
||||
We are using [Antithesis](https://antithesis.com/) for continuous fuzzing,
|
||||
|
||||
@@ -1,570 +0,0 @@
|
||||
# Distributed Tracing Fundamentals
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Next**: [Architecture Analysis](./01-architecture-analysis.md)
|
||||
|
||||
---
|
||||
|
||||
## What is Distributed Tracing?
|
||||
|
||||
Distributed tracing is a method for tracking data objects as they flow through distributed systems. In a network like XRP Ledger, a single transaction touches multiple independent nodes—each with no shared memory or logging. Distributed tracing connects these dots.
|
||||
|
||||
**Without tracing:** You see isolated logs on each node with no way to correlate them.
|
||||
|
||||
**With tracing:** You see the complete journey of a transaction or an event across all nodes it touched.
|
||||
|
||||
---
|
||||
|
||||
## Actors and Actions at a Glance
|
||||
|
||||
### Actors
|
||||
|
||||
| Who (Plain English) | Technical Term |
|
||||
| ---------------------------------------------- | --------------- |
|
||||
| A single unit of work being tracked | Span |
|
||||
| The complete journey of a request | Trace |
|
||||
| Data that links spans across services | Trace Context |
|
||||
| Code that creates spans and propagates context | Instrumentation |
|
||||
| Service that receives and processes traces | Collector |
|
||||
| Storage and visualization system | Backend (Tempo) |
|
||||
| Decision logic for which traces to keep | Sampler |
|
||||
|
||||
### Actions
|
||||
|
||||
| What Happens (Plain English) | Technical Term |
|
||||
| --------------------------------------- | ----------------------- |
|
||||
| Start tracking a new operation | Create a Span |
|
||||
| Connect a child operation to its parent | Set `parent_span_id` |
|
||||
| Group all related operations together | Share a `trace_id` |
|
||||
| Pass tracking data between services | Context Propagation |
|
||||
| Decide whether to record a trace | Sampling (Head or Tail) |
|
||||
| Send completed traces to storage | Export (OTLP) |
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Trace
|
||||
|
||||
A **trace** represents the entire journey of a request through the system. It has a unique `trace_id` that stays constant across all nodes.
|
||||
|
||||
```
|
||||
Trace ID: abc123
|
||||
├── Node A: received transaction
|
||||
├── Node B: relayed transaction
|
||||
├── Node C: included in consensus
|
||||
└── Node D: applied to ledger
|
||||
```
|
||||
|
||||
### 2. Span
|
||||
|
||||
A **span** represents a single unit of work within a trace. Each span has:
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| ---------------- | -------------------------------- | -------------------------- |
|
||||
| `trace_id` | Identifies the trace | `event123` |
|
||||
| `span_id` | Unique identifier | `span456` |
|
||||
| `parent_span_id` | Parent span (if any) | `p_span123` |
|
||||
| `name` | Operation name | `rpc.submit` |
|
||||
| `start_time` | When work began (local time) | `2024-01-15T10:30:00Z` |
|
||||
| `end_time` | When work completed (local time) | `2024-01-15T10:30:00.050Z` |
|
||||
| `attributes` | Key-value metadata | `tx_hash=ABC...` |
|
||||
| `status` | OK, ERROR MSG | `OK` |
|
||||
|
||||
### 3. Trace Context
|
||||
|
||||
**Trace context** is the data that propagates between services to link spans together. It contains:
|
||||
|
||||
- `trace_id` - The trace this span belongs to
|
||||
- `span_id` - The current span (becomes parent for child spans)
|
||||
- `trace_flags` - Sampling decisions
|
||||
|
||||
---
|
||||
|
||||
## How Spans Form a Trace
|
||||
|
||||
Spans have parent-child relationships forming a tree structure:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph trace["Trace: abc123"]
|
||||
A["tx.submit<br/>span_id: 001<br/>50ms"] --> B["tx.validate<br/>span_id: 002<br/>5ms"]
|
||||
A --> C["tx.relay<br/>span_id: 003<br/>10ms"]
|
||||
A --> D["tx.apply<br/>span_id: 004<br/>30ms"]
|
||||
D --> E["ledger.update<br/>span_id: 005<br/>20ms"]
|
||||
end
|
||||
|
||||
style A fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style B fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style C fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style D fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style E fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **tx.submit (blue, root)**: The top-level span representing the entire transaction submission; all other spans are its descendants.
|
||||
- **tx.validate, tx.relay, tx.apply (green)**: Direct children of tx.submit, representing the three main stages -- validation, relay to peers, and application to the ledger.
|
||||
- **ledger.update (red)**: A grandchild span nested under tx.apply, representing the actual ledger state mutation triggered by applying the transaction.
|
||||
- **Arrows (parent to child)**: Each arrow indicates a parent-child span relationship where the parent's completion depends on the child finishing.
|
||||
|
||||
The same trace visualized as a **timeline (Gantt chart)**:
|
||||
|
||||
```
|
||||
Time → 0ms 10ms 20ms 30ms 40ms 50ms
|
||||
├───────────────────────────────────────────┤
|
||||
tx.submit│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
|
||||
├─────┤
|
||||
tx.valid │▓▓▓▓▓│
|
||||
│ ├──────────┤
|
||||
tx.relay │ │▓▓▓▓▓▓▓▓▓▓│
|
||||
│ ├────────────────────────────┤
|
||||
tx.apply │ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
|
||||
│ ├──────────────────┤
|
||||
ledger │ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Span Relationships
|
||||
|
||||
Spans don't always form simple parent-child trees. Distributed tracing defines several relationship types to capture different causal patterns:
|
||||
|
||||
### 1. Parent-Child (ChildOf)
|
||||
|
||||
The default relationship. The parent span **depends on** or **contains** the child span. The child runs within the scope of the parent.
|
||||
|
||||
```
|
||||
tx.submit (parent)
|
||||
├── tx.validate (child) ← parent waits for this
|
||||
├── tx.relay (child) ← parent waits for this
|
||||
└── tx.apply (child) ← parent waits for this
|
||||
```
|
||||
|
||||
**When to use:** Synchronous calls, nested operations, any case where the parent's completion depends on the child.
|
||||
|
||||
### 2. Follows-From
|
||||
|
||||
A causal relationship where the first span **triggers** the second, but does **not wait** for it. The originator fires and moves on.
|
||||
|
||||
```
|
||||
Time →
|
||||
|
||||
tx.receive [=======]
|
||||
↓ triggers (follows-from)
|
||||
tx.relay [===========] ← runs independently
|
||||
```
|
||||
|
||||
**When to use:** Asynchronous jobs, queued work, fire-and-forget patterns. For example, a node receives a transaction and queues it for relay — the relay span _follows from_ the receive span but the receiver doesn't wait for relaying to complete.
|
||||
|
||||
> **OpenTracing** defined `FollowsFrom` as a first-class reference type alongside `ChildOf`.
|
||||
> **OpenTelemetry** represents this using **Span Links** with descriptive attributes instead (see below).
|
||||
|
||||
### 3. Span Links (Cross-Trace and Non-Hierarchical)
|
||||
|
||||
Links connect spans that are **causally related but not in a parent-child hierarchy**. Unlike parent-child, links can cross trace boundaries.
|
||||
|
||||
```
|
||||
Trace A Trace B
|
||||
────── ──────
|
||||
batch.schedule batch.execute
|
||||
├─ item.enqueue (span X) ┌──► process.item
|
||||
├─ item.enqueue (span Y) ───┤ (links to X, Y, Z)
|
||||
├─ item.enqueue (span Z) └──►
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
|
||||
| Pattern | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------- |
|
||||
| **Batch processing** | A batch span links back to all individual spans that contributed to it |
|
||||
| **Fan-in** | An aggregation span links to the multiple producer spans it merges |
|
||||
| **Fan-out** | Multiple downstream spans link back to the single span that triggered them |
|
||||
| **Async handoff** | A deferred job links back to the request that queued it (follows-from) |
|
||||
| **Cross-trace** | Correlating spans across independent traces (e.g., retries, related events) |
|
||||
|
||||
**Link structure:** Each link carries the target span's context plus optional attributes:
|
||||
|
||||
```
|
||||
Link {
|
||||
trace_id: <target trace>
|
||||
span_id: <target span>
|
||||
attributes: { "link.description": "triggered by batch scheduler" }
|
||||
}
|
||||
```
|
||||
|
||||
### Relationship Summary
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph parent_child["Parent-Child"]
|
||||
direction TB
|
||||
P["Parent"] --> C["Child"]
|
||||
end
|
||||
|
||||
subgraph follows_from["Follows-From"]
|
||||
direction TB
|
||||
A["Span A"] -.->|triggers| B["Span B"]
|
||||
end
|
||||
|
||||
subgraph links["Span Links"]
|
||||
direction TB
|
||||
X["`Span X
|
||||
(Trace 1)`"] -.-|link| Y["`Span Y
|
||||
(Trace 2)`"]
|
||||
end
|
||||
|
||||
parent_child ~~~ follows_from ~~~ links
|
||||
|
||||
style P fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style C fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style A fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style B fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style X fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
style Y fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
```
|
||||
|
||||
| Relationship | Same Trace? | Dependency? | OTel Mechanism |
|
||||
| ---------------- | ----------- | -------------------------- | ----------------- |
|
||||
| **Parent-Child** | Yes | Parent depends on child | `parent_span_id` |
|
||||
| **Follows-From** | Usually | Causal but no dependency | Link + attributes |
|
||||
| **Span Link** | Either | Correlation, no dependency | Link + attributes |
|
||||
|
||||
---
|
||||
|
||||
## Trace ID Generation
|
||||
|
||||
A `trace_id` is a 128-bit (16-byte) identifier that groups all spans belonging to one logical operation. How it's generated determines how easily you can find and correlate traces later.
|
||||
|
||||
### General Approaches
|
||||
|
||||
#### 1. Random (W3C Default)
|
||||
|
||||
Generate a random 128-bit ID when a trace starts. Standard approach for most services.
|
||||
|
||||
```
|
||||
trace_id = random_128_bits()
|
||||
```
|
||||
|
||||
| Pros | Cons |
|
||||
| --------------------------- | --------------------------------------------- |
|
||||
| Simple, standard | No natural correlation to domain events |
|
||||
| Guaranteed unique per trace | If propagation is lost, trace is broken |
|
||||
| Works with all OTel tooling | "Find trace for TX abc" requires index lookup |
|
||||
|
||||
#### 2. Deterministic (Derived from Domain Data)
|
||||
|
||||
Compute the trace_id from a hash of a natural identifier. Every node independently derives the **same** trace_id for the same event.
|
||||
|
||||
```
|
||||
trace_id = SHA-256(domain_identifier)[0:16] // truncate to 128 bits
|
||||
```
|
||||
|
||||
| Pros | Cons |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| Propagation-resilient — same ID computed everywhere | Same event processed twice (retry) shares trace_id |
|
||||
| Natural search — domain ID maps directly to trace | Non-standard (tooling assumes random) |
|
||||
| No coordination needed between nodes | 256→128 bit truncation (collision risk negligible at ~2⁶⁴) |
|
||||
|
||||
#### 3. Hybrid (Deterministic Prefix + Random Suffix)
|
||||
|
||||
First 8 bytes derived from domain data, last 8 bytes random.
|
||||
|
||||
```
|
||||
trace_id = SHA-256(domain_identifier)[0:8] || random_64_bits()
|
||||
```
|
||||
|
||||
| Pros | Cons |
|
||||
| ------------------------------------------- | ---------------------------------------- |
|
||||
| Prefix search: "find all traces for TX abc" | Must propagate to maintain full trace_id |
|
||||
| Unique per processing instance | More complex generation logic |
|
||||
| Retries get distinct trace_ids | Partial correlation only (prefix match) |
|
||||
|
||||
### XRPL Workflow Analysis
|
||||
|
||||
XRPL has a unique advantage: its core workflows produce **globally unique 256-bit hashes** that are known on every node. This makes deterministic trace_id generation practical in ways most systems can't achieve.
|
||||
|
||||
#### Natural Identifiers by Workflow
|
||||
|
||||
| Workflow | Natural Identifier | Size | Known at Start? | Same on All Nodes? |
|
||||
| ------------------- | --------------------------------- | ---------- | ----------------------------- | -------------------------------- |
|
||||
| **Transaction** | Transaction hash (`tid_`) | 256-bit | Yes — computed before signing | Yes — hash of canonical tx data |
|
||||
| **Consensus round** | Previous ledger hash + ledger seq | 256+32 bit | Yes — known when round opens | Yes — all validators agree |
|
||||
| **Validation** | Ledger hash being validated | 256-bit | Yes — from consensus result | Yes — same closed ledger |
|
||||
| **Ledger catch-up** | Target ledger hash | 256-bit | Yes — we know what to fetch | Yes — identifies ledger globally |
|
||||
|
||||
#### Where These Identifiers Live in Code
|
||||
|
||||
```
|
||||
Transaction: STTx::getTransactionID() → uint256 tid_
|
||||
TMTransaction::rawTransaction → recompute hash from bytes
|
||||
|
||||
Consensus: ConsensusProposal::previousLedger_ → uint256 (previous ledger hash)
|
||||
ConsensusProposal::position_ → uint256 (TxSet hash)
|
||||
LedgerHeader::seq → uint32_t (ledger sequence)
|
||||
|
||||
Validation: STValidation::getLedgerHash() → uint256
|
||||
STValidation::getNodeID() → NodeID (160-bit)
|
||||
|
||||
Ledger fetch: InboundLedger constructor → uint256 hash, uint32_t seq
|
||||
TMGetLedger::ledgerHash → bytes (uint256)
|
||||
```
|
||||
|
||||
### Recommended Strategy: Workflow-Scoped Deterministic
|
||||
|
||||
Each workflow type derives its trace_id from its natural domain identifier:
|
||||
|
||||
```
|
||||
Transaction trace: trace_id = SHA-256("tx" || tx_hash)[0:16]
|
||||
Consensus trace: trace_id = SHA-256("cons" || prev_ledger_hash || ledger_seq)[0:16]
|
||||
Ledger catch-up: trace_id = SHA-256("fetch" || target_ledger_hash)[0:16]
|
||||
```
|
||||
|
||||
The string prefix (`"tx"`, `"cons"`, `"fetch"`) prevents collisions between workflows that might share underlying hashes.
|
||||
|
||||
**Why this works for XRPL:**
|
||||
|
||||
1. **Propagation-resilient** — Even if a P2P message drops trace context, every node independently computes the same trace_id from the same tx_hash or ledger_hash. Spans still correlate.
|
||||
|
||||
2. **Zero-cost search** — "Show me the trace for transaction ABC" becomes a direct lookup: compute `SHA-256("tx" || ABC)[0:16]` and query. No secondary index needed.
|
||||
|
||||
3. **Cross-workflow linking via Span Links** — A consensus trace links to individual transaction traces. A validation span links to the consensus trace. This connects the full picture without forcing everything into one giant trace.
|
||||
|
||||
### Cross-Workflow Correlation
|
||||
|
||||
Each workflow gets its own trace. Span Links tie them together:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph tx_trace["Transaction Trace"]
|
||||
direction LR
|
||||
Tn["trace_id = f(tx_hash)"]:::note --> T1["tx.receive"] --> T2["tx.validate"] --> T3["tx.relay"]
|
||||
end
|
||||
|
||||
subgraph cons_trace["Consensus Trace"]
|
||||
direction LR
|
||||
Cn["trace_id = f(prev_ledger, seq)"]:::note --> C1["cons.open"] --> C2["cons.propose"] --> C3["cons.accept"]
|
||||
end
|
||||
|
||||
subgraph val_trace["Validation"]
|
||||
direction LR
|
||||
Vn["spans within consensus trace"]:::note --> V1["val.create"] --> V2["val.broadcast"]
|
||||
end
|
||||
|
||||
subgraph fetch_trace["Catch-Up Trace"]
|
||||
direction LR
|
||||
Fn["trace_id = f(ledger_hash)"]:::note --> F1["fetch.request"] --> F2["fetch.receive"] --> F3["fetch.apply"]
|
||||
end
|
||||
|
||||
C1 -.-|"`span link
|
||||
(tx traces)`"| T3
|
||||
C3 --> V1
|
||||
F1 -.-|"`span link
|
||||
(target ledger)`"| C3
|
||||
|
||||
classDef note fill:none,stroke:#888,stroke-dasharray:5 5,color:#333,font-style:italic
|
||||
style T1 fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style T2 fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style T3 fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style C1 fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style C2 fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style C3 fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style V1 fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style V2 fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style F1 fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
style F2 fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
style F3 fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Transaction Trace (blue)**: An independent trace whose `trace_id` is deterministically derived from the transaction hash. Contains receive, validate, and relay spans.
|
||||
- **Consensus Trace (green)**: An independent trace whose `trace_id` is derived from the previous ledger hash and sequence number. Covers the open, propose, and accept phases.
|
||||
- **Validation (red)**: Validation spans live within the consensus trace (not a separate trace). They are created after the accept phase completes.
|
||||
- **Catch-Up Trace (purple)**: An independent trace for ledger acquisition, derived from the target ledger hash. Used when a node is behind and fetching missing ledgers.
|
||||
- **Dotted arrows (span links)**: Cross-trace correlations. Consensus links to transaction traces it included; catch-up links to the consensus trace that produced the target ledger.
|
||||
- **Solid arrow (C3 to V1)**: A parent-child relationship -- validation spans are direct children of the consensus accept span within the same trace.
|
||||
|
||||
**How a query flows:**
|
||||
|
||||
```
|
||||
"Why was TX abc slow?"
|
||||
1. Compute trace_id = SHA-256("tx" || abc)[0:16]
|
||||
2. Find transaction trace → see it was included in consensus round N
|
||||
3. Follow span link → consensus trace for round N
|
||||
4. See which phase was slow (propose? accept?)
|
||||
5. If a node was catching up, follow link → catch-up trace
|
||||
```
|
||||
|
||||
### Trade-offs to Consider
|
||||
|
||||
| Concern | Mitigation |
|
||||
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Retries get same trace_id** | Add `attempt` attribute to root span; spans have unique span_ids and timestamps |
|
||||
| **256→128 bit truncation** | Birthday-bound collision at ~2⁶⁴ operations — negligible for XRPL's throughput |
|
||||
| **Non-standard generation** | OTel spec allows any 16-byte non-zero value; tooling works on the hex string |
|
||||
| **Hash computation cost** | SHA-256 is ~0.3μs per call; XRPL already computes these hashes for other purposes |
|
||||
| **Late-binding identifiers** | Ledger hash isn't known until after consensus — validation spans use ledger_seq as fallback, then link to the consensus trace |
|
||||
|
||||
---
|
||||
|
||||
## Distributed Traces Across Nodes
|
||||
|
||||
In distributed systems like xrpld, traces span **multiple independent nodes**. The trace context must be propagated in network messages:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant NodeA as Node A
|
||||
participant NodeB as Node B
|
||||
participant NodeC as Node C
|
||||
|
||||
Client->>NodeA: Submit TX<br/>(no trace context)
|
||||
|
||||
Note over NodeA: Creates new trace<br/>trace_id: abc123<br/>span: tx.receive
|
||||
|
||||
NodeA->>NodeB: Relay TX<br/>(trace_id: abc123, parent: 001)
|
||||
|
||||
Note over NodeB: Creates child span<br/>span: tx.relay<br/>parent_span_id: 001
|
||||
|
||||
NodeA->>NodeC: Relay TX<br/>(trace_id: abc123, parent: 001)
|
||||
|
||||
Note over NodeC: Creates child span<br/>span: tx.relay<br/>parent_span_id: 001
|
||||
|
||||
Note over NodeA,NodeC: All spans share trace_id: abc123<br/>enabling correlation across nodes
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Client**: The external entity that submits a transaction. It does not carry trace context -- the trace originates at the first node.
|
||||
- **Node A**: The entry point that creates a new trace (trace_id: abc123) and the root span `tx.receive`. It relays the transaction to peers with trace context attached.
|
||||
- **Node B and Node C**: Peer nodes that receive the relayed transaction along with the propagated trace context. Each creates a child span under Node A's span, preserving the same `trace_id`.
|
||||
- **Arrows with trace context**: The relay messages carry `trace_id` and `parent_span_id`, allowing each downstream node to link its spans back to the originating span on Node A.
|
||||
|
||||
---
|
||||
|
||||
## Context Propagation
|
||||
|
||||
For traces to work across nodes, **trace context must be propagated** in messages.
|
||||
|
||||
### What's in the Context (~26 bytes)
|
||||
|
||||
| Field | Size | Description |
|
||||
| ------------- | -------- | ------------------------------------------------------- |
|
||||
| `trace_id` | 16 bytes | Identifies the entire trace (constant across all nodes) |
|
||||
| `span_id` | 8 bytes | The sender's current span (becomes parent on receiver) |
|
||||
| `trace_flags` | 1 byte | Sampling decision (bit 0 = sampled; bits 1-7 reserved) |
|
||||
| `trace_state` | variable | Optional vendor-specific data (typically omitted) |
|
||||
|
||||
### How span_id Changes at Each Hop
|
||||
|
||||
Only **one** `span_id` travels in the context - the sender's current span. Each node:
|
||||
|
||||
1. Extracts the received `span_id` and uses it as the `parent_span_id`
|
||||
2. Creates a **new** `span_id` for its own span
|
||||
3. Sends its own `span_id` as the parent when forwarding
|
||||
|
||||
```
|
||||
Node A Node B Node C
|
||||
────── ────── ──────
|
||||
|
||||
Span AAA Span BBB Span CCC
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Context out: Context out: Context out:
|
||||
├─ trace_id: abc123 ├─ trace_id: abc123 ├─ trace_id: abc123
|
||||
├─ span_id: AAA ──────────► ├─ span_id: BBB ──────────► ├─ span_id: CCC ──────►
|
||||
└─ flags: 01 └─ flags: 01 └─ flags: 01
|
||||
│ │
|
||||
parent = AAA parent = BBB
|
||||
```
|
||||
|
||||
The `trace_id` stays constant, but `span_id` **changes at every hop** to maintain the parent-child chain.
|
||||
|
||||
### Propagation Formats
|
||||
|
||||
There are two patterns:
|
||||
|
||||
### HTTP/RPC Headers (W3C Trace Context)
|
||||
|
||||
```
|
||||
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
|
||||
│ │ │ │
|
||||
│ │ │ └── Flags (sampled)
|
||||
│ │ └── Parent span ID (16 hex)
|
||||
│ └── Trace ID (32 hex)
|
||||
└── Version
|
||||
```
|
||||
|
||||
### Protocol Buffers (xrpld P2P messages)
|
||||
|
||||
xrpld P2P messages such as `TMTransaction` carry the trace context in two added byte fields alongside the existing payload: `trace_parent` holds the W3C traceparent (`trace_id`, `span_id`, and `trace_flags`), and `trace_state` holds the optional W3C tracestate. Together they propagate the trace across the P2P boundary so a receiving node can attach its spans to the sender's span.
|
||||
|
||||
---
|
||||
|
||||
## Sampling
|
||||
|
||||
Not every trace needs to be recorded. **Sampling** reduces overhead:
|
||||
|
||||
### Head Sampling (at trace start)
|
||||
|
||||
```
|
||||
Request arrives → Random N% chance → Record or skip entire trace
|
||||
```
|
||||
|
||||
- ✅ Low overhead
|
||||
- ❌ May miss interesting traces
|
||||
|
||||
> **xrpld note**: xrpld intentionally fixes head sampling at 100% (sample
|
||||
> everything) and does not expose a configurable ratio. A per-node ratio
|
||||
> would let different nodes make divergent keep/drop decisions for the same
|
||||
> distributed trace, producing broken/partial traces. A span with a remote
|
||||
> parent is decided by the same trace-id ratio sampler, not by the peer's
|
||||
> sampled flag, so nodes agree on every trace. Volume reduction is
|
||||
> delegated to collector-side tail sampling.
|
||||
|
||||
### Tail Sampling (after trace completes)
|
||||
|
||||
```
|
||||
Trace completes → Collector evaluates:
|
||||
- Error? → KEEP
|
||||
- Slow? → KEEP
|
||||
- Normal? → Sample 10%
|
||||
```
|
||||
|
||||
- ✅ Never loses important traces
|
||||
- ❌ Higher memory usage at collector
|
||||
|
||||
---
|
||||
|
||||
## Key Benefits for xrpld
|
||||
|
||||
| Challenge | How Tracing Helps |
|
||||
| ---------------------------------- | ---------------------------------------- |
|
||||
| "Where is my transaction?" | Follow trace across all nodes it touched |
|
||||
| "Why was consensus slow?" | See timing breakdown of each phase |
|
||||
| "Which node is the bottleneck?" | Compare span durations across nodes |
|
||||
| "What happened during the outage?" | Correlate errors across the network |
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
| Term | Definition |
|
||||
| -------------------- | ------------------------------------------------------------------- |
|
||||
| **Trace** | Complete journey of a request, identified by `trace_id` |
|
||||
| **Span** | Single operation within a trace |
|
||||
| **Parent-Child** | Span relationship where the parent depends on the child |
|
||||
| **Follows-From** | Causal relationship where originator doesn't wait for the result |
|
||||
| **Span Link** | Non-hierarchical connection between spans, possibly across traces |
|
||||
| **Deterministic ID** | Trace ID derived from domain data (e.g., tx_hash) instead of random |
|
||||
| **Context** | Data propagated between services (`trace_id`, `span_id`, flags) |
|
||||
| **Instrumentation** | Code that creates spans and propagates context |
|
||||
| **Collector** | Service that receives, processes, and exports traces |
|
||||
| **Backend** | Storage/visualization system (Tempo) |
|
||||
| **Head Sampling** | Sampling decision at trace start |
|
||||
| **Tail Sampling** | Sampling decision after trace completes |
|
||||
|
||||
---
|
||||
|
||||
_Next: [Architecture Analysis](./01-architecture-analysis.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
@@ -1,467 +0,0 @@
|
||||
# Architecture Analysis
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Design Decisions](./02-design-decisions.md) | [Implementation Strategy](./03-implementation-strategy.md)
|
||||
|
||||
---
|
||||
|
||||
## 1.1 Current xrpld Architecture Overview
|
||||
|
||||
> **WS** = WebSocket | **UNL** = Unique Node List | **TxQ** = Transaction Queue | **StatsD** = Statistics Daemon
|
||||
|
||||
The xrpld node software consists of several interconnected components that need instrumentation for distributed tracing:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Node"]
|
||||
subgraph services["Core Services"]
|
||||
RPC["RPC Server<br/>(HTTP/WS/gRPC)"]
|
||||
Overlay["Overlay<br/>(P2P Network)"]
|
||||
Consensus["Consensus<br/>(RCLConsensus)"]
|
||||
ValidatorList["ValidatorList<br/>(UNL Mgmt)"]
|
||||
end
|
||||
|
||||
JobQueue["JobQueue<br/>(Thread Pool)"]
|
||||
|
||||
subgraph processing["Processing Layer"]
|
||||
NetworkOPs["NetworkOPs<br/>(Tx Processing)"]
|
||||
LedgerMaster["LedgerMaster<br/>(Ledger Mgmt)"]
|
||||
NodeStore["NodeStore<br/>(Database)"]
|
||||
InboundLedgers["InboundLedgers<br/>(Ledger Sync)"]
|
||||
end
|
||||
|
||||
subgraph appservices["Application Services"]
|
||||
PathFind["PathFinding<br/>(Payment Paths)"]
|
||||
TxQ["TxQ<br/>(Fee Escalation)"]
|
||||
LoadMgr["LoadManager<br/>(Fee/Load)"]
|
||||
end
|
||||
|
||||
subgraph observability["Existing Observability"]
|
||||
PerfLog["PerfLog<br/>(JSON)"]
|
||||
Insight["Insight<br/>(StatsD)"]
|
||||
Logging["Logging<br/>(Journal)"]
|
||||
end
|
||||
|
||||
services --> JobQueue
|
||||
JobQueue --> processing
|
||||
JobQueue --> appservices
|
||||
end
|
||||
|
||||
style xrpld fill:#424242,stroke:#212121,color:#ffffff
|
||||
style services fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style processing fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style appservices fill:#6a1b9a,stroke:#4a148c,color:#ffffff
|
||||
style observability fill:#e65100,stroke:#bf360c,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Core Services (blue)**: The entry points into xrpld -- RPC Server handles client requests, Overlay manages peer-to-peer networking, Consensus drives agreement, and ValidatorList manages trusted validators.
|
||||
- **JobQueue (center)**: The asynchronous thread pool that decouples Core Services from the Processing and Application layers. All work flows through it.
|
||||
- **Processing Layer (green)**: Core business logic -- NetworkOPs processes transactions, LedgerMaster manages ledger state, NodeStore handles persistence, and InboundLedgers synchronizes missing data.
|
||||
- **Application Services (purple)**: Higher-level features -- PathFinding computes payment routes, TxQ manages fee-based queuing, and LoadManager tracks server load.
|
||||
- **Existing Observability (orange)**: The current monitoring stack (PerfLog, Insight, Journal logging) that OpenTelemetry will complement, not replace.
|
||||
- **Arrows (Services to JobQueue to layers)**: Work originates at Core Services, is enqueued onto the JobQueue, and dispatched to Processing or Application layers for execution.
|
||||
|
||||
---
|
||||
|
||||
## 1.1.1 Actors and Actions
|
||||
|
||||
### Actors
|
||||
|
||||
| Who (Plain English) | Technical Term |
|
||||
| ----------------------------------------- | -------------------------- |
|
||||
| Network node running XRPL software | xrpld node |
|
||||
| External client submitting requests | RPC Client |
|
||||
| Network neighbor sharing data | Peer (PeerImp) |
|
||||
| Request handler for client queries | RPC Server (ServerHandler) |
|
||||
| Command executor for specific RPC methods | RPCHandler |
|
||||
| Agreement process between nodes | Consensus (RCLConsensus) |
|
||||
| Transaction processing coordinator | NetworkOPs |
|
||||
| Background task scheduler | JobQueue |
|
||||
| Ledger state manager | LedgerMaster |
|
||||
| Payment route calculator | PathFinding (Pathfinder) |
|
||||
| Transaction waiting room | TxQ (Transaction Queue) |
|
||||
| Fee adjustment system | LoadManager |
|
||||
| Trusted validator list manager | ValidatorList |
|
||||
| Protocol upgrade tracker | AmendmentTable |
|
||||
| Ledger state hash tree | SHAMap |
|
||||
| Persistent key-value storage | NodeStore |
|
||||
|
||||
### Actions
|
||||
|
||||
| What Happens (Plain English) | Technical Term |
|
||||
| ---------------------------------------------- | ---------------------- |
|
||||
| Client sends a request to a node | `rpc.request` |
|
||||
| Node executes a specific RPC command | `rpc.command.*` |
|
||||
| Node receives a transaction from a peer | `tx.receive` |
|
||||
| Node checks if a transaction is valid | `tx.validate` |
|
||||
| Node forwards a transaction to neighbors | `tx.relay` |
|
||||
| Nodes agree on which transactions to include | `consensus.round` |
|
||||
| Consensus progresses through phases | `consensus.phase.*` |
|
||||
| Node builds a new confirmed ledger | `ledger.build` |
|
||||
| Node fetches missing ledger data from peers | `ledger.acquire` |
|
||||
| Node computes payment routes | `pathfind.compute` |
|
||||
| Node queues a transaction for later processing | `txq.enqueue` |
|
||||
| Node increases fees due to high load | `fee.escalate` |
|
||||
| Node fetches the latest trusted validator list | `validator.list.fetch` |
|
||||
| Node votes on a protocol amendment | `amendment.vote` |
|
||||
| Node synchronizes state tree data | `shamap.sync` |
|
||||
|
||||
---
|
||||
|
||||
## 1.2 Key Components for Instrumentation
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List
|
||||
|
||||
| Component | Location | Purpose | Trace Value |
|
||||
| ------------------ | ------------------------------------------ | ------------------------ | -------------------------------- |
|
||||
| **Overlay** | `src/xrpld/overlay/` | P2P communication | Message propagation timing |
|
||||
| **PeerImp** | `src/xrpld/overlay/detail/PeerImp.cpp` | Individual peer handling | Per-peer latency |
|
||||
| **RCLConsensus** | `src/xrpld/app/consensus/RCLConsensus.cpp` | Consensus algorithm | Round timing, phase analysis |
|
||||
| **NetworkOPs** | `src/xrpld/app/misc/NetworkOPs.cpp` | Transaction processing | Tx lifecycle tracking |
|
||||
| **ServerHandler** | `src/xrpld/rpc/detail/ServerHandler.cpp` | RPC entry point | Request latency |
|
||||
| **RPCHandler** | `src/xrpld/rpc/detail/RPCHandler.cpp` | Command execution | Per-command timing |
|
||||
| **JobQueue** | `src/xrpl/core/JobQueue.h` | Async task execution | Queue wait times |
|
||||
| **PathFinding** | `src/xrpld/app/paths/` | Payment path computation | Path latency, cache hits |
|
||||
| **TxQ** | `src/xrpld/app/misc/TxQ.cpp` | Transaction queue/fees | Queue depth, eviction rates |
|
||||
| **LoadManager** | `src/xrpld/app/main/LoadManager.cpp` | Fee escalation/load | Fee levels, load factors |
|
||||
| **InboundLedgers** | `src/xrpld/app/ledger/InboundLedgers.cpp` | Ledger acquisition | Sync time, peer reliability |
|
||||
| **ValidatorList** | `src/xrpld/app/misc/ValidatorList.cpp` | UNL management | List freshness, fetch failures |
|
||||
| **AmendmentTable** | `src/xrpld/app/misc/AmendmentTable.cpp` | Protocol amendments | Voting status, activation events |
|
||||
| **SHAMap** | `src/xrpld/shamap/` | State hash tree | Sync speed, missing nodes |
|
||||
|
||||
---
|
||||
|
||||
## 1.3 Transaction Flow Diagram
|
||||
|
||||
Transaction flow spans multiple nodes in the network. Each node creates linked spans to form a distributed trace:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant PeerA as Peer A (Receive)
|
||||
participant PeerB as Peer B (Relay)
|
||||
participant PeerC as Peer C (Validate)
|
||||
|
||||
Client->>PeerA: 1. Submit TX
|
||||
|
||||
rect rgb(230, 245, 255)
|
||||
Note over PeerA: tx.receive SPAN START
|
||||
PeerA->>PeerA: HashRouter Deduplication
|
||||
PeerA->>PeerA: tx.validate (child span)
|
||||
end
|
||||
|
||||
PeerA->>PeerB: 2. Relay TX (with trace ctx)
|
||||
|
||||
rect rgb(230, 245, 255)
|
||||
Note over PeerB: tx.receive (linked span)
|
||||
end
|
||||
|
||||
PeerB->>PeerC: 3. Relay TX
|
||||
|
||||
rect rgb(230, 245, 255)
|
||||
Note over PeerC: tx.receive (linked span)
|
||||
PeerC->>PeerC: tx.process
|
||||
end
|
||||
|
||||
Note over Client,PeerC: DISTRIBUTED TRACE (same trace_id: abc123)
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Client**: The external entity that submits a transaction to Peer A. It has no trace context -- the trace starts at the first node.
|
||||
- **Peer A (Receive)**: The entry node that creates the root span `tx.receive`, runs HashRouter deduplication to avoid processing duplicates, and creates a child `tx.validate` span.
|
||||
- **Peer A to Peer B arrow**: The relay message carries trace context (trace_id + parent span_id), enabling Peer B to create a linked span under the same trace.
|
||||
- **Peer B (Relay)**: Receives the transaction and trace context, creates a `tx.receive` span linked to Peer A's trace, then relays onward.
|
||||
- **Peer C (Validate)**: Final hop in this example. Creates a linked `tx.receive` span and runs `tx.process` to fully process the transaction.
|
||||
- **Blue rectangles**: Highlight the span boundaries on each node, showing where instrumentation creates and closes spans.
|
||||
|
||||
### Trace Structure
|
||||
|
||||
```
|
||||
trace_id: abc123
|
||||
├── span: tx.receive (Peer A)
|
||||
│ ├── span: tx.validate
|
||||
│ └── span: tx.relay
|
||||
├── span: tx.receive (Peer B) [parent: Peer A]
|
||||
│ └── span: tx.relay
|
||||
└── span: tx.receive (Peer C) [parent: Peer B]
|
||||
└── span: tx.process
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.4 Consensus Round Flow
|
||||
|
||||
Consensus rounds are multi-phase operations that benefit significantly from tracing:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph round["consensus.round (root span)"]
|
||||
attrs["Attributes:<br/>ledger_seq = 12345678<br/>consensus_mode = proposing<br/>proposers = 35"]
|
||||
|
||||
subgraph open["consensus.phase.open"]
|
||||
open_desc["Duration: ~3s<br/>Waiting for transactions"]
|
||||
end
|
||||
|
||||
subgraph establish["consensus.phase.establish"]
|
||||
est_attrs["proposals_received = 28<br/>disputes_resolved = 3"]
|
||||
est_children["├── consensus.proposal.receive (×28)<br/>├── consensus.proposal.send (×1)<br/>└── consensus.dispute.resolve (×3)"]
|
||||
end
|
||||
|
||||
subgraph accept["consensus.phase.accept"]
|
||||
acc_attrs["transactions_applied = 150<br/>ledger_hash = DEF456..."]
|
||||
acc_children["├── ledger.build<br/>└── ledger.validate"]
|
||||
end
|
||||
|
||||
attrs --> open
|
||||
open --> establish
|
||||
establish --> accept
|
||||
end
|
||||
|
||||
style round fill:#f57f17,stroke:#e65100,color:#ffffff
|
||||
style open fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style establish fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style accept fill:#c2185b,stroke:#880e4f,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **consensus.round (orange, root span)**: The top-level span encompassing the entire consensus round, with attributes like ledger sequence, mode, and proposer count.
|
||||
- **consensus.phase.open (blue)**: The first phase where the node waits (~3s) to collect incoming transactions before proposing.
|
||||
- **consensus.phase.establish (green)**: The negotiation phase where validators exchange proposals, resolve disputes, and converge on a transaction set. Child spans track each proposal received/sent and each dispute resolved.
|
||||
- **consensus.phase.accept (pink)**: The final phase where the agreed transaction set is applied, a new ledger is built, and the ledger is validated. Child spans cover `ledger.build` and `ledger.validate`.
|
||||
- **Arrows (open to establish to accept)**: The sequential flow through the three consensus phases. Each phase must complete before the next begins.
|
||||
|
||||
---
|
||||
|
||||
## 1.5 RPC Request Flow
|
||||
|
||||
> **WS** = WebSocket
|
||||
|
||||
RPC requests support W3C Trace Context headers for distributed tracing across services:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph request["rpc.request (root span)"]
|
||||
http["HTTP Request — POST /<br/>traceparent:<br/>00-abc123...-def456...-01"]
|
||||
|
||||
attrs["Attributes:<br/>http.method = POST<br/>net.peer.ip = 192.168.1.100<br/>command = submit"]
|
||||
|
||||
subgraph enqueue["jobqueue.enqueue"]
|
||||
job_attr["job_type = jtCLIENT_RPC"]
|
||||
end
|
||||
|
||||
subgraph command["rpc.command.submit"]
|
||||
cmd_attrs["version = 2<br/>rpc_role = user"]
|
||||
cmd_children["├── tx.deserialize<br/>├── tx.validate_local<br/>└── tx.submit_to_network"]
|
||||
end
|
||||
|
||||
response["Response: 200 OK<br/>Duration: 45ms"]
|
||||
|
||||
http --> attrs
|
||||
attrs --> enqueue
|
||||
enqueue --> command
|
||||
command --> response
|
||||
end
|
||||
|
||||
style request fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style enqueue fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style command fill:#e65100,stroke:#bf360c,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **rpc.request (green, root span)**: The outermost span representing the full RPC request lifecycle, from HTTP receipt to response. Carries the W3C `traceparent` header for distributed tracing.
|
||||
- **HTTP Request node**: Shows the incoming POST request with its `traceparent` header and extracted attributes (method, peer IP, command name).
|
||||
- **jobqueue.enqueue (blue)**: The span covering the asynchronous handoff from the RPC thread to the JobQueue worker thread. The trace context is preserved across this async boundary.
|
||||
- **rpc.command.submit (orange)**: The span for the actual command execution, with child spans for deserialization, local validation, and network submission.
|
||||
- **Response node**: The final output with HTTP status and total duration, marking the end of the root span.
|
||||
- **Arrows (top to bottom)**: The sequential processing pipeline -- receive request, extract attributes, enqueue job, execute command, return response.
|
||||
|
||||
---
|
||||
|
||||
## 1.6 Key Trace Points
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
The following table identifies priority instrumentation points across the codebase:
|
||||
|
||||
| Category | Span Name | File | Method | Priority |
|
||||
| --------------- | ---------------------- | ---------------------- | ----------------------- | -------- |
|
||||
| **Transaction** | `tx.receive` | `PeerImp.cpp` | `handleTransaction()` | High |
|
||||
| **Transaction** | `tx.validate` | `NetworkOPs.cpp` | `processTransaction()` | High |
|
||||
| **Transaction** | `tx.process` | `NetworkOPs.cpp` | `doTransactionSync()` | High |
|
||||
| **Transaction** | `tx.relay` | `OverlayImpl.cpp` | `relay()` | Medium |
|
||||
| **Consensus** | `consensus.round` | `RCLConsensus.cpp` | `startRound()` | High |
|
||||
| **Consensus** | `consensus.phase.*` | `Consensus.h` | `timerEntry()` | High |
|
||||
| **Consensus** | `consensus.proposal.*` | `RCLConsensus.cpp` | `peerProposal()` | Medium |
|
||||
| **RPC** | `rpc.request` | `ServerHandler.cpp` | `onRequest()` | High |
|
||||
| **RPC** | `rpc.command.*` | `RPCHandler.cpp` | `doCommand()` | High |
|
||||
| **Peer** | `peer.connect` | `OverlayImpl.cpp` | `onHandoff()` | Low |
|
||||
| **Peer** | `peer.message.*` | `PeerImp.cpp` | `onMessage()` | Low |
|
||||
| **Ledger** | `ledger.acquire` | `InboundLedgers.cpp` | `acquire()` | Medium |
|
||||
| **Ledger** | `ledger.build` | `RCLConsensus.cpp` | `buildLCL()` | High |
|
||||
| **PathFinding** | `pathfind.request` | `PathRequest.cpp` | `doUpdate()` | High |
|
||||
| **PathFinding** | `pathfind.compute` | `Pathfinder.cpp` | `findPaths()` | High |
|
||||
| **TxQ** | `txq.enqueue` | `TxQ.cpp` | `apply()` | High |
|
||||
| **TxQ** | `txq.apply` | `TxQ.cpp` | `processClosedLedger()` | High |
|
||||
| **Fee** | `fee.escalate` | `LoadManager.cpp` | `raiseLocalFee()` | Medium |
|
||||
| **Ledger** | `ledger.replay` | `LedgerReplayer.h` | `replay()` | Medium |
|
||||
| **Ledger** | `ledger.delta` | `LedgerDeltaAcquire.h` | `processData()` | Medium |
|
||||
| **Validator** | `validator.list.fetch` | `ValidatorList.cpp` | `verify()` | Medium |
|
||||
| **Validator** | `validator.manifest` | `Manifest.cpp` | `applyManifest()` | Low |
|
||||
| **Amendment** | `amendment.vote` | `AmendmentTable.cpp` | `doVoting()` | Low |
|
||||
| **SHAMap** | `shamap.sync` | `SHAMap.cpp` | `fetchRoot()` | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 1.7 Instrumentation Priority
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Instrumentation Priority Matrix
|
||||
x-axis Low Complexity --> High Complexity
|
||||
y-axis Low Value --> High Value
|
||||
quadrant-1 Implement First
|
||||
quadrant-2 Plan Carefully
|
||||
quadrant-3 Quick Wins
|
||||
quadrant-4 Consider Later
|
||||
|
||||
RPC Tracing: [0.2, 0.92]
|
||||
Transaction Tracing: [0.55, 0.88]
|
||||
Consensus Tracing: [0.78, 0.82]
|
||||
PathFinding: [0.38, 0.75]
|
||||
TxQ and Fees: [0.25, 0.65]
|
||||
Ledger Sync: [0.62, 0.58]
|
||||
Peer Message Tracing: [0.35, 0.25]
|
||||
JobQueue Tracing: [0.2, 0.48]
|
||||
Validator Mgmt: [0.48, 0.42]
|
||||
Amendment Tracking: [0.15, 0.32]
|
||||
SHAMap Operations: [0.72, 0.45]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.8 Observable Outcomes
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List
|
||||
|
||||
After implementing OpenTelemetry, operators and developers will gain visibility into the following:
|
||||
|
||||
### 1.8.1 What You Will See: Traces
|
||||
|
||||
| Trace Type | Description | Example Query in Grafana/Tempo |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| **Transaction Lifecycle** | Full journey from RPC submission through validation, relay, consensus, and ledger inclusion | `{service.name="xrpld" && tx_hash="ABC123..."}` |
|
||||
| **Cross-Node Propagation** | Transaction path across multiple xrpld nodes with timing | `{relay_count > 0}` |
|
||||
| **Consensus Rounds** | Complete round with all phases (open, establish, accept) | `{span.name=~"consensus.round.*"}` |
|
||||
| **RPC Request Processing** | Individual command execution with timing breakdown | `{command="account_info"}` |
|
||||
| **Ledger Acquisition** | Peer-to-peer ledger data requests and responses | `{span.name="ledger.acquire"}` |
|
||||
| **PathFinding Latency** | Path computation time and cache effectiveness for payment RPCs | `{span.name="pathfind.compute"}` |
|
||||
| **TxQ Behavior** | Queue depth, eviction patterns, fee escalation during congestion | `{span.name=~"txq.*"}` |
|
||||
| **Ledger Sync** | Full acquisition timeline including delta and transaction fetches | `{span.name=~"ledger.acquire.*"}` |
|
||||
| **Validator Health** | UNL fetch success, manifest updates, stale list detection | `{span.name=~"validator.*"}` |
|
||||
|
||||
### 1.8.2 What You Will See: Metrics (Derived from Traces)
|
||||
|
||||
| Metric | Description | Dashboard Panel |
|
||||
| ----------------------------- | --------------------------------------- | --------------------------- |
|
||||
| **RPC Latency (p50/p95/p99)** | Response time distribution per command | Heatmap by command |
|
||||
| **Transaction Throughput** | Transactions processed per second | Time series graph |
|
||||
| **Consensus Round Duration** | Time to complete consensus phases | Histogram |
|
||||
| **Cross-Node Latency** | Time for transaction to reach N nodes | Line chart with percentiles |
|
||||
| **Error Rate** | Failed transactions/RPC calls by type | Stacked bar chart |
|
||||
| **PathFinding Latency** | Path computation time per currency pair | Heatmap by currency |
|
||||
| **TxQ Depth** | Queued transactions over time | Time series with thresholds |
|
||||
| **Fee Escalation Level** | Current fee multiplier | Gauge with alert thresholds |
|
||||
| **Ledger Sync Duration** | Time to acquire missing ledgers | Histogram |
|
||||
|
||||
### 1.8.3 Concrete Dashboard Examples
|
||||
|
||||
**Transaction Trace View (Tempo):**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Trace: abc123... (Transaction Submission) Duration: 847ms │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ├── rpc.request [ServerHandler] ████░░░░░░ 45ms │
|
||||
│ │ └── rpc.command.submit [RPCHandler] ████░░░░░░ 42ms │
|
||||
│ │ └── tx.receive [NetworkOPs] ███░░░░░░░ 35ms │
|
||||
│ │ ├── tx.validate [TxQ] █░░░░░░░░░ 8ms │
|
||||
│ │ └── tx.relay [Overlay] ██░░░░░░░░ 15ms │
|
||||
│ │ ├── tx.receive [Node-B] █████░░░░░ 52ms │
|
||||
│ │ │ └── tx.relay [Node-B] ██░░░░░░░░ 18ms │
|
||||
│ │ └── tx.receive [Node-C] ██████░░░░ 65ms │
|
||||
│ └── consensus.round [RCLConsensus] ████████░░ 720ms │
|
||||
│ ├── consensus.phase.open ██░░░░░░░░ 180ms │
|
||||
│ ├── consensus.phase.establish █████░░░░░ 480ms │
|
||||
│ └── consensus.phase.accept █░░░░░░░░░ 60ms │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**RPC Performance Dashboard Panel:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ RPC Command Latency (Last 1 Hour) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Command │ p50 │ p95 │ p99 │ Errors │ Rate │
|
||||
│──────────────────┼────────┼────────┼────────┼────────┼──────│
|
||||
│ account_info │ 12ms │ 45ms │ 89ms │ 0.1% │ 150/s│
|
||||
│ submit │ 35ms │ 120ms │ 250ms │ 2.3% │ 45/s│
|
||||
│ ledger │ 8ms │ 25ms │ 55ms │ 0.0% │ 80/s│
|
||||
│ tx │ 15ms │ 50ms │ 100ms │ 0.5% │ 60/s│
|
||||
│ server_info │ 5ms │ 12ms │ 20ms │ 0.0% │ 200/s│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Consensus Health Dashboard Panel:**
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 1200
|
||||
height: 400
|
||||
plotReservedSpacePercent: 50
|
||||
chartOrientation: vertical
|
||||
themeVariables:
|
||||
xyChart:
|
||||
plotColorPalette: "#3498db"
|
||||
---
|
||||
xychart-beta
|
||||
title "Consensus Round Duration (Last 24 Hours)"
|
||||
x-axis "Time of Day (Hours)" [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]
|
||||
y-axis "Duration (seconds)" 1 --> 5
|
||||
line [2.1, 2.4, 2.8, 3.2, 3.8, 4.3, 4.5, 5.0, 4.7, 4.0, 3.2, 2.6, 2.0]
|
||||
```
|
||||
|
||||
### 1.8.4 Operator Actionable Insights
|
||||
|
||||
| Scenario | What You'll See | Action |
|
||||
| ------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| **Slow RPC** | Span showing which phase is slow (parsing, execution, serialization) | Optimize specific code path |
|
||||
| **Transaction Stuck** | Trace stops at validation; error attribute shows reason | Fix transaction parameters |
|
||||
| **Consensus Delay** | Phase.establish taking too long; proposer attribute shows missing validators | Investigate network connectivity |
|
||||
| **Memory Spike** | Large batch of spans correlating with memory increase | Tune batch_size or sampling |
|
||||
| **Network Partition** | Traces missing cross-node links for specific peer | Check peer connectivity |
|
||||
| **Path Computation Slow** | pathfind.compute span shows high latency; cache miss rate in attributes | Warm the RippleLineCache, check order book depth |
|
||||
| **TxQ Full** | txq.enqueue spans show evictions; fee.escalate spans increasing | Monitor fee levels, alert operators |
|
||||
| **Ledger Sync Stalled** | ledger.acquire spans timing out; peer reliability attributes show issues | Check peer connectivity, add trusted peers |
|
||||
| **UNL Stale** | validator.list.fetch spans failing; last_update attribute aging | Verify validator site URLs, check DNS |
|
||||
|
||||
### 1.8.5 Developer Debugging Workflow
|
||||
|
||||
1. **Find Transaction**: Query by `tx_hash` to get full trace
|
||||
2. **Identify Bottleneck**: Look at span durations to find slowest component
|
||||
3. **Check Attributes**: Review `validity`, `rpc_status` for errors
|
||||
4. **Correlate Logs**: Use `trace_id` to find related PerfLog entries
|
||||
5. **Compare Nodes**: Filter by `service.instance.id` to compare behavior across nodes
|
||||
|
||||
---
|
||||
|
||||
_Next: [Design Decisions](./02-design-decisions.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
@@ -1,589 +0,0 @@
|
||||
# Design Decisions
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Architecture Analysis](./01-architecture-analysis.md)
|
||||
|
||||
---
|
||||
|
||||
## 2.1 OpenTelemetry Components
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 2.1.1 SDK Selection
|
||||
|
||||
**Primary Choice**: OpenTelemetry C++ SDK (`opentelemetry-cpp`)
|
||||
|
||||
| Component | Purpose | Required |
|
||||
| --------------------------------------- | ---------------------- | ------------------------- |
|
||||
| `opentelemetry-cpp::api` | Tracing API headers | Yes |
|
||||
| `opentelemetry-cpp::sdk` | SDK implementation | Yes |
|
||||
| `opentelemetry-cpp::ext` | Extensions (exporters) | Yes |
|
||||
| `opentelemetry-cpp::otlp_http_exporter` | OTLP/HTTP export | Yes (shipped in Phase 1b) |
|
||||
| `opentelemetry-cpp::otlp_grpc_exporter` | OTLP/gRPC export | Future (not yet wired up) |
|
||||
|
||||
### 2.1.2 Instrumentation Strategy
|
||||
|
||||
**Manual Instrumentation** (recommended):
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
| ---------- | --------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **Manual** | Precise control, optimized placement, xrpld-specific attributes | More development effort |
|
||||
| **Auto** | Less code, automatic coverage | Less control, potential overhead, limited customization |
|
||||
|
||||
---
|
||||
|
||||
## 2.2 Exporter Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph nodes["xrpld Nodes"]
|
||||
node1["xrpld<br/>Node 1"]
|
||||
node2["xrpld<br/>Node 2"]
|
||||
node3["xrpld<br/>Node 3"]
|
||||
end
|
||||
|
||||
collector["OpenTelemetry<br/>Collector<br/>(sidecar or standalone)"]
|
||||
|
||||
subgraph backends["Observability Backends"]
|
||||
tempo["Tempo"]
|
||||
elastic["Elastic<br/>APM"]
|
||||
end
|
||||
|
||||
node1 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
node2 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
node3 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
|
||||
collector --> tempo
|
||||
collector --> elastic
|
||||
|
||||
style nodes fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style backends fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style collector fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Nodes (blue)**: The source of telemetry data. Each xrpld node exports spans via OTLP/HTTP on port 4318 (the only exporter shipped in Phase 1b).
|
||||
- **OpenTelemetry Collector (red)**: The central aggregation point that receives spans from all nodes. Can run as a sidecar (per-node) or standalone (shared). Handles batching, filtering, and routing.
|
||||
- **Observability Backends (green)**: The storage and visualization destinations. Tempo is the recommended backend for both development and production, and Elastic APM is an alternative. The Collector routes to one or more backends.
|
||||
- **Arrows (nodes to collector to backends)**: The data pipeline -- spans flow from nodes to the Collector over HTTP, then the Collector fans out to the configured backends.
|
||||
|
||||
### 2.2.1 OTLP/HTTP (Shipped in Phase 1b)
|
||||
|
||||
OTLP/HTTP is the only exporter wired up in Phase 1b. It is configured via
|
||||
`OtlpHttpExporterOptions` with the collector traces endpoint
|
||||
(`http://localhost:4318/v1/traces` by default) and a JSON content type
|
||||
(binary protobuf is also available).
|
||||
|
||||
### 2.2.2 OTLP/gRPC (Future Work — Planned Upgrade)
|
||||
|
||||
OTLP/gRPC is planned as a future upgrade from the HTTP exporter. The gRPC
|
||||
transport offers lower per-span overhead and tighter back-pressure semantics
|
||||
than HTTP/JSON, making it attractive for production deployments once the HTTP
|
||||
path is validated in earlier phases.
|
||||
|
||||
Required to land this upgrade:
|
||||
|
||||
1. Add `opentelemetry-cpp::otlp_grpc_exporter` to the Conan recipe (the
|
||||
dependency already exists but is not linked in Phase 1b builds).
|
||||
2. Extend `TelemetryConfig.cpp` to parse an `exporter` key (`otlp_http`
|
||||
default, `otlp_grpc` opt-in) and a gRPC endpoint override.
|
||||
3. In `Telemetry::start()` branch on the parsed exporter type and construct
|
||||
either `OtlpHttpExporterFactory::Create(httpOpts)` or
|
||||
`OtlpGrpcExporterFactory::Create(grpcOpts)` accordingly.
|
||||
4. Update the runbook and dashboards to document the alternate port and TLS
|
||||
settings.
|
||||
|
||||
When wired up, the gRPC path will use `OtlpGrpcExporterOptions` configured with
|
||||
the collector endpoint (host on port 4317), TLS credentials enabled, and a CA
|
||||
certificate path.
|
||||
|
||||
Until that work lands, `OtlpGrpcExporterOptions` is **not** used by any code
|
||||
path in Phase 1b through Phase 5.
|
||||
|
||||
---
|
||||
|
||||
## 2.3 Span Naming Conventions
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List | **WS** = WebSocket
|
||||
|
||||
### 2.3.1 Naming Schema
|
||||
|
||||
```
|
||||
<component>.<operation>[.<sub-operation>]
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
- `tx.receive` - Transaction received from peer
|
||||
- `consensus.phase.establish` - Consensus establish phase
|
||||
- `rpc.command.server_info` - server_info RPC command
|
||||
|
||||
### 2.3.2 Complete Span Catalog
|
||||
|
||||
| Span name | Description |
|
||||
| ------------------------------ | --------------------------------------- |
|
||||
| `tx.receive` | Transaction received from network |
|
||||
| `tx.validate` | Transaction signature/format validation |
|
||||
| `tx.process` | Full transaction processing |
|
||||
| `tx.relay` | Transaction relay to peers |
|
||||
| `tx.apply` | Apply transaction to ledger |
|
||||
| `consensus.round` | Complete consensus round |
|
||||
| `consensus.phase.open` | Open phase - collecting transactions |
|
||||
| `consensus.phase.establish` | Establish phase - reaching agreement |
|
||||
| `consensus.phase.accept` | Accept phase - applying consensus |
|
||||
| `consensus.proposal.receive` | Receive peer proposal |
|
||||
| `consensus.proposal.send` | Send our proposal |
|
||||
| `consensus.validation.receive` | Receive peer validation |
|
||||
| `consensus.validation.send` | Send our validation |
|
||||
| `rpc.request` | HTTP/WebSocket request handling |
|
||||
| `rpc.command.*` | Specific RPC command (dynamic) |
|
||||
| `peer.connect` | Peer connection establishment |
|
||||
| `peer.disconnect` | Peer disconnection |
|
||||
| `peer.message.send` | Send protocol message |
|
||||
| `peer.message.receive` | Receive protocol message |
|
||||
| `ledger.acquire` | Ledger acquisition from network |
|
||||
| `ledger.build` | Build new ledger |
|
||||
| `ledger.validate` | Ledger validation |
|
||||
| `ledger.close` | Close ledger |
|
||||
| `ledger.replay` | Ledger replay executed |
|
||||
| `ledger.delta` | Delta-based ledger acquired |
|
||||
| `pathfind.request` | Path request initiated |
|
||||
| `pathfind.compute` | Path computation executed |
|
||||
| `txq.enqueue` | Transaction queued |
|
||||
| `txq.apply` | Queued transaction applied |
|
||||
| `fee.escalate` | Fee escalation triggered |
|
||||
| `validator.list.fetch` | UNL list fetched |
|
||||
| `validator.manifest` | Manifest update processed |
|
||||
| `amendment.vote` | Amendment voting executed |
|
||||
| `shamap.sync` | State tree synchronization |
|
||||
| `job.enqueue` | Job added to queue |
|
||||
| `job.execute` | Job execution |
|
||||
|
||||
### 2.3.3 Attribute Naming Conventions
|
||||
|
||||
Span **names** follow §2.3.1 (dotted `<component>.<operation>`). Span
|
||||
**attribute keys** follow the rules below. The constants in the `*SpanNames.h`
|
||||
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, 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_phase`, `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 —
|
||||
it blurs the resource/span scope boundary and parses awkwardly in TraceQL.
|
||||
5. **Span names** use `<subsystem>[.<component>]` (dotted, per §2.3.1). Only
|
||||
attribute _keys_ follow rules 1–4.
|
||||
|
||||
Standard OpenTelemetry semantic-convention keys keep their canonical dotted
|
||||
form (e.g. `service.*` resource attributes, `http.*` span attributes); the
|
||||
"no dotted form" rule applies to xrpl-custom keys only.
|
||||
|
||||
The same rules are recorded in `CONTRIBUTING.md` (the permanent home, since
|
||||
`OpenTelemetryPlan/` is removed once the rollout completes). The attribute
|
||||
examples in §2.4 below follow these rules.
|
||||
|
||||
---
|
||||
|
||||
## 2.4 Attribute Schema
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List | **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 2.4.1 Resource Attributes (Set Once at Startup)
|
||||
|
||||
Resource attributes identify the process and are set once at startup. They use
|
||||
the standard OpenTelemetry semantic conventions plus custom dotted `xrpl.*`
|
||||
keys (the dotted form is reserved for resource scope per §2.3.3).
|
||||
|
||||
| Key | Type / value | Description |
|
||||
| --------------------- | ------------------------------------------------------- | ------------------------------ |
|
||||
| `service.name` | `"xrpld"` | Standard `SERVICE_NAME` |
|
||||
| `service.version` | `build_info::getVersionString()` | Standard `SERVICE_VERSION` |
|
||||
| `service.instance.id` | node public key (base58) | Standard `SERVICE_INSTANCE_ID` |
|
||||
| `xrpl.network.id` | network id (e.g. 0 for mainnet) | Network identifier |
|
||||
| `xrpl.network.type` | `"mainnet"` \| `"testnet"` \| `"devnet"` \| `"unknown"` | Network kind |
|
||||
| `xrpl.node.type` | `"validator"` \| `"stock"` \| `"reporting"` | Node role |
|
||||
| `xrpl.node.cluster` | cluster name | Cluster name, if clustered |
|
||||
|
||||
### 2.4.2 Span Attributes by Category
|
||||
|
||||
> Span attribute keys use the underscore form from §2.3.3 (shared/qualified
|
||||
> keys are `<domain>_<field>`; per-span unique keys are bare). The dotted form
|
||||
> is reserved for the resource attributes in §2.4.1 above. This catalog lists
|
||||
> the planned attribute set by category; the exact emitted key for each
|
||||
> implemented span is defined by the `*SpanNames.h` constants, which are the
|
||||
> single source of truth where the two differ.
|
||||
|
||||
#### Transaction Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------------- | ------ | ------------------------------------- |
|
||||
| `tx_hash` | string | Transaction hash (hex) |
|
||||
| `tx_type` | string | `"Payment"`, `"OfferCreate"`, etc. |
|
||||
| `tx_account` | string | Source account (redacted in prod) |
|
||||
| `tx_sequence` | int64 | Account sequence number |
|
||||
| `tx_fee` | int64 | Fee in drops |
|
||||
| `tx_result` | string | `"tesSUCCESS"`, `"tecPATH_DRY"`, etc. |
|
||||
| `current_ledger_seq` | int64 | Open ledger the transaction targeted |
|
||||
| `relay_count` | int64 | Peers the transaction was relayed to |
|
||||
|
||||
> **Note:** `current_ledger_seq` and `ledger_seq` are the same concept — a ledger's sequence number — but they name different ledgers, so the design keeps two keys rather than one. `current_ledger_seq` is the open or in-flight ledger a transaction's work was applied into; it is named after the RPC field `ledger_current_index`. `ledger_seq` (see [Ledger & Job Attributes](#ledger--job-attributes)) is a closed or validated ledger, set by the ledger and consensus spans. Neither is spelled `ledger_index`: per rule 2 of [Telemetry span attribute naming](../CONTRIBUTING.md#telemetry-span-attribute-naming), one concept gets one key reused verbatim, and a different referent is disambiguated with a prefix rather than a synonym.
|
||||
|
||||
#### Consensus Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------------- | ------- | ----------------------------------- |
|
||||
| `consensus_round` | int64 | Round number |
|
||||
| `consensus_phase` | string | `"open"`, `"establish"`, `"accept"` |
|
||||
| `consensus_mode` | string | `"proposing"`, `"observing"`, etc. |
|
||||
| `proposers` | int64 | Number of proposers |
|
||||
| `prev_ledger_prefix` | string | Previous ledger hash prefix |
|
||||
| `ledger_seq` | int64 | Ledger sequence |
|
||||
| `tx_count` | int64 | Transactions in consensus set |
|
||||
| `round_time_ms` | float64 | Round duration |
|
||||
|
||||
#### RPC Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------- | ------- | ----------------------------------------------------------------------------- |
|
||||
| `command` | string | Command name (per-span unique on `rpc.command`) |
|
||||
| `version` | int64 | API version |
|
||||
| `rpc_role` | string | `"admin"` or `"user"` (qualified — `role` is generic) |
|
||||
| `params` | string | Sanitized parameters (optional) |
|
||||
| `rpc_status` | string | Response status: `success` \| `error` (qualified — `status` is OTel-reserved) |
|
||||
| `duration_ms` | float64 | Request duration in milliseconds |
|
||||
|
||||
#### Peer & Message Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------------- | ------- | -------------------------- |
|
||||
| `peer_id` | string | Peer public key (base58) |
|
||||
| `peer_address` | string | IP:port |
|
||||
| `peer_latency_ms` | float64 | Measured latency |
|
||||
| `peer_cluster` | string | Cluster name if clustered |
|
||||
| `message_type` | string | Protocol message type name |
|
||||
| `message_size_bytes` | int64 | Message size |
|
||||
| `message_compressed` | bool | Whether compressed |
|
||||
|
||||
#### Ledger & Job Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| --------------------------- | ------- | -------------------------------- |
|
||||
| `ledger_hash` | string | Ledger hash |
|
||||
| `ledger_seq` | int64 | Closed/validated ledger sequence |
|
||||
| `close_time_ripple_epoch_s` | int64 | Close time (XRPL epoch seconds) |
|
||||
| `ledger_tx_count` | int64 | Transaction count |
|
||||
| `job_type` | string | Job type name |
|
||||
| `job_queue_ms` | float64 | Time spent in queue |
|
||||
| `job_worker` | int64 | Worker thread ID |
|
||||
|
||||
#### PathFinding Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------------------- | ------ | ------------------------- |
|
||||
| `pathfind_source_currency` | string | Source currency code |
|
||||
| `pathfind_dest_currency` | string | Destination currency code |
|
||||
| `pathfind_path_count` | int64 | Number of paths found |
|
||||
| `pathfind_cache_hit` | bool | RippleLineCache hit |
|
||||
|
||||
#### TxQ Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| --------------------- | ------ | --------------------------- |
|
||||
| `txq_queue_depth` | int64 | Current queue depth |
|
||||
| `txq_fee_level` | int64 | Fee level of transaction |
|
||||
| `txq_eviction_reason` | string | Why transaction was evicted |
|
||||
|
||||
#### Fee Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ---------------------- | ----- | ------------------------- |
|
||||
| `fee_load_factor` | int64 | Current load factor |
|
||||
| `fee_escalation_level` | int64 | Fee escalation multiplier |
|
||||
|
||||
#### Validator Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------------------ | ----- | ------------------------- |
|
||||
| `validator_list_size` | int64 | UNL size |
|
||||
| `validator_list_age_sec` | int64 | Seconds since last update |
|
||||
|
||||
#### Amendment Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------------ | ------ | -------------------------------------- |
|
||||
| `amendment_name` | string | Amendment name |
|
||||
| `amendment_status` | string | `"enabled"`, `"vetoed"`, `"supported"` |
|
||||
|
||||
#### SHAMap Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ---------------------- | ------- | --------------------------------------------- |
|
||||
| `shamap_type` | string | `"transaction"`, `"state"`, `"account_state"` |
|
||||
| `shamap_missing_nodes` | int64 | Number of missing nodes during sync |
|
||||
| `shamap_duration_ms` | float64 | Sync duration |
|
||||
|
||||
### 2.4.3 Data Collection Summary
|
||||
|
||||
The following table summarizes what data is collected by category:
|
||||
|
||||
| Category | Attributes Collected | Purpose |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- |
|
||||
| **Transaction** | `tx_hash`, `tx_type`, `tx_result`, `tx_fee`, `current_ledger_seq` | Trace transaction lifecycle |
|
||||
| **Consensus** | `consensus_round`, `consensus_phase`, `consensus_mode`, `proposers`, `round_time_ms` | Analyze consensus timing |
|
||||
| **RPC** | `command`, `version`, `rpc_status`, `duration_ms` | Monitor RPC performance |
|
||||
| **Peer** | `peer_id` (public key), `peer_latency_ms`, `message_type`, `message_size_bytes` | Network topology analysis |
|
||||
| **Ledger** | `ledger_hash`, `ledger_seq`, `close_time`, `ledger_tx_count` | Ledger progression tracking |
|
||||
| **Job** | `job_type`, `job_queue_ms`, `job_worker` | JobQueue performance |
|
||||
| **PathFinding** | `pathfind_source_currency`, `pathfind_dest_currency`, `pathfind_path_count`, `pathfind_cache_hit` | Payment path analysis |
|
||||
| **TxQ** | `txq_queue_depth`, `txq_fee_level`, `txq_eviction_reason` | Queue depth and fee tracking |
|
||||
| **Fee** | `fee_load_factor`, `fee_escalation_level` | Fee escalation monitoring |
|
||||
| **Validator** | `validator_list_size`, `validator_list_age_sec` | UNL health monitoring |
|
||||
| **Amendment** | `amendment_name`, `amendment_status` | Protocol upgrade tracking |
|
||||
| **SHAMap** | `shamap_type`, `shamap_missing_nodes`, `shamap_duration_ms` | State tree sync performance |
|
||||
|
||||
### 2.4.4 Privacy & Sensitive Data Policy
|
||||
|
||||
> **PII** = Personally Identifiable Information
|
||||
|
||||
OpenTelemetry instrumentation is designed to collect **operational metadata only**, never sensitive content.
|
||||
|
||||
#### Data NOT Collected
|
||||
|
||||
The following data is explicitly **excluded** from telemetry collection:
|
||||
|
||||
| Excluded Data | Reason |
|
||||
| ----------------------- | ----------------------------------------- |
|
||||
| **Private Keys** | Never exposed; not relevant to tracing |
|
||||
| **Account Balances** | Financial data; privacy sensitive |
|
||||
| **Transaction Amounts** | Financial data; privacy sensitive |
|
||||
| **Raw TX Payloads** | May contain sensitive memo/data fields |
|
||||
| **Personal Data** | No PII collected |
|
||||
| **IP Addresses** | Configurable; excluded by default in prod |
|
||||
|
||||
#### Privacy Protection Mechanisms
|
||||
|
||||
| Mechanism | Description |
|
||||
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Account Hashing** | `tx_account` is hashed at collector level before storage |
|
||||
| **Configurable Redaction** | Sensitive fields can be excluded via `[telemetry]` config section |
|
||||
| **Collector Tail Sampling** | xrpld head sampling is fixed at 1.0 (every span emitted); the collector retains ~10% of non-error traces, reducing stored data exposure |
|
||||
| **Local Control** | Node operators have full control over what gets exported |
|
||||
| **No Raw Payloads** | Transaction content is never recorded, only metadata (hash, type, result) |
|
||||
| **Collector-Level Filtering** | Additional redaction/hashing can be configured at OTel Collector |
|
||||
|
||||
#### Collector-Level Data Protection
|
||||
|
||||
The OpenTelemetry Collector can be configured (via an `attributes` processor)
|
||||
to hash or redact sensitive attributes before export — for example, hashing
|
||||
`tx_account`, deleting `peer_address` to drop IP addresses, and deleting
|
||||
`params` to redact request parameters.
|
||||
|
||||
#### Configuration Options for Privacy
|
||||
|
||||
In `xrpld.cfg`, operators control data collection granularity through the
|
||||
`[telemetry]` section. Besides `enabled`, per-component toggles
|
||||
(`trace_transactions`, `trace_consensus`, `trace_rpc`, `trace_peer` — the last
|
||||
often disabled due to high volume) select which spans are emitted, and
|
||||
redaction flags (`redact_account` to hash account addresses, `redact_peer_address`
|
||||
to remove peer IP addresses) control SDK-level redaction before export.
|
||||
|
||||
> **Note**: The `redact_account` configuration in `xrpld.cfg` controls SDK-level redaction before export, while collector-level filtering (see [Collector-Level Data Protection](#collector-level-data-protection) above) provides an additional defense-in-depth layer. Both can operate independently.
|
||||
|
||||
> **Key Principle**: Telemetry collects **operational metadata** (timing, counts, hashes) — never **sensitive content** (keys, balances, amounts, raw payloads).
|
||||
|
||||
---
|
||||
|
||||
## 2.5 Context Propagation Design
|
||||
|
||||
> **WS** = WebSocket
|
||||
|
||||
### 2.5.1 Propagation Boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph http["HTTP/WebSocket (RPC)"]
|
||||
w3c["W3C Trace Context Headers:<br/>traceparent:<br/>00-trace_id-span_id-flags<br/>tracestate: xrpld=..."]
|
||||
end
|
||||
|
||||
subgraph protobuf["Protocol Buffers (P2P)"]
|
||||
proto["message TraceContext {<br/> bytes trace_id = 1; // 16 bytes<br/> bytes span_id = 2; // 8 bytes<br/> uint32 trace_flags = 3;<br/> reserved 4; // trace_state, later<br/>}"]
|
||||
end
|
||||
|
||||
subgraph jobqueue["JobQueue (Internal Async)"]
|
||||
job["Context captured at job creation,<br/>restored at execution<br/><br/>class Job {<br/> otel::context::Context<br/> traceContext_;<br/>};"]
|
||||
end
|
||||
|
||||
style http fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style protobuf fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style jobqueue fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **HTTP/WebSocket - RPC (blue)**: For client-facing RPC requests, trace context is propagated using the W3C `traceparent` header. This is the standard approach and works with any OTel-compatible client.
|
||||
- **Protocol Buffers - P2P (green)**: For peer-to-peer messages between xrpld nodes, trace context is embedded as a protobuf `TraceContext` message carrying trace_id, span_id, flags, and optional trace_state.
|
||||
- **JobQueue - Internal Async (red)**: For asynchronous work within a single node, the OTel context is captured when a job is created and restored when the job executes on a worker thread. This bridges the async gap so spans remain linked.
|
||||
|
||||
---
|
||||
|
||||
## 2.6 Integration with Existing Observability
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **WS** = WebSocket
|
||||
|
||||
### 2.6.1 Existing Frameworks Comparison
|
||||
|
||||
xrpld already has two observability mechanisms. OpenTelemetry complements (not replaces) them:
|
||||
|
||||
| Aspect | PerfLog | Beast Insight (StatsD) | OpenTelemetry |
|
||||
| --------------------- | ----------------------------- | ---------------------------- | ------------------------- |
|
||||
| **Type** | Logging | Metrics | Distributed Tracing |
|
||||
| **Data** | JSON log entries | Counters, gauges, histograms | Spans with context |
|
||||
| **Scope** | Single node | Single node | **Cross-node** |
|
||||
| **Output** | `perf.log` file | StatsD server | OTLP Collector |
|
||||
| **Question answered** | "What happened on this node?" | "How many? How fast?" | "What was the journey?" |
|
||||
| **Correlation** | By timestamp | By metric name | By `trace_id` |
|
||||
| **Overhead** | Low (file I/O) | Low (UDP packets) | Low-Medium (configurable) |
|
||||
|
||||
### 2.6.2 What Each Framework Does Best
|
||||
|
||||
#### PerfLog
|
||||
|
||||
- **Purpose**: Detailed local event logging for RPC and job execution
|
||||
- **Strengths**:
|
||||
- Rich JSON output with timing data
|
||||
- Already integrated in RPC handlers
|
||||
- File-based, no external dependencies
|
||||
- **Limitations**:
|
||||
- Single-node only (no cross-node correlation)
|
||||
- No parent-child relationships between events
|
||||
- Manual log parsing required
|
||||
|
||||
A PerfLog entry is a JSON object with fields such as `time`, `method`,
|
||||
`duration_us`, and `result`.
|
||||
|
||||
#### Beast Insight (StatsD)
|
||||
|
||||
- **Purpose**: Real-time metrics for monitoring dashboards
|
||||
- **Strengths**:
|
||||
- Aggregated metrics (counters, gauges, histograms)
|
||||
- Low overhead (UDP, fire-and-forget)
|
||||
- Good for alerting thresholds
|
||||
- **Limitations**:
|
||||
- No request-level detail
|
||||
- No causal relationships
|
||||
- Single-node perspective
|
||||
- Aggregation happens on the StatsD server, not in the process
|
||||
|
||||
In xrpld, Beast Insight is used through `increment` (counters), `gauge`
|
||||
(point-in-time values), and `timing` (durations) calls. A `timing` call sends
|
||||
each measured value as its own raw `|ms` sample, so the histogram a dashboard
|
||||
reads is built by the StatsD server from that stream of values.
|
||||
|
||||
#### OpenTelemetry (NEW)
|
||||
|
||||
- **Purpose**: Distributed request tracing across nodes
|
||||
- **Strengths**:
|
||||
- **Cross-node correlation** via `trace_id`
|
||||
- Parent-child span relationships
|
||||
- Rich attributes per span
|
||||
- A `Histogram` instrument that aggregates **at the point of measure**
|
||||
- Industry standard (CNCF)
|
||||
- **Limitations**:
|
||||
- Requires collector infrastructure
|
||||
- Higher complexity than logging
|
||||
|
||||
A span is created via `startSpan` (e.g. `"tx.relay"`), annotated with
|
||||
attributes such as `tx_hash` and `peer_id`, and is automatically linked to its
|
||||
parent through the active context.
|
||||
|
||||
OpenTelemetry is not only spans. The same SDK offers a `Histogram` instrument,
|
||||
and a `Record()` call folds the value straight into bucket counts inside the
|
||||
process — no per-event record is shipped and no server-side aggregation step is
|
||||
needed. That is what makes it affordable in a hot loop where one span per event
|
||||
would not be, and it is the one thing Beast Insight cannot do, because its
|
||||
`timing` path ships raw values and aggregates them on the StatsD server.
|
||||
|
||||
### 2.6.3 When to Use Each
|
||||
|
||||
| Scenario | PerfLog | StatsD | OpenTelemetry |
|
||||
| --------------------------------------- | ---------- | ------ | ------------- |
|
||||
| "How many TXs per second?" | ❌ | ✅ | ✅ |
|
||||
| "What's the p99 RPC latency?" | ❌ | ✅ | ✅ |
|
||||
| "Why was this specific TX slow?" | ⚠️ partial | ❌ | ✅ |
|
||||
| "Which node delayed consensus?" | ❌ | ❌ | ✅ |
|
||||
| "What happened on node X at time T?" | ✅ | ❌ | ✅ |
|
||||
| "Show me the TX journey across 5 nodes" | ❌ | ❌ | ✅ |
|
||||
| "p99 NodeStore fetch latency?" | ❌ | ❌ | ✅ |
|
||||
|
||||
The last row is the case a span cannot answer. One `TMGetObjectByHash` message
|
||||
requests up to `tuning::kHardMaxReplyNodes` objects, so a span per NodeStore
|
||||
fetch is not affordable in that loop. Instead the fetch loop's wall time is
|
||||
recorded once per message into an OpenTelemetry `Histogram`
|
||||
(`getobject_lookup_us`), and the quantile is read off its buckets. StatsD is
|
||||
marked ❌ because that instrument is recorded on the native OpenTelemetry metrics
|
||||
path, not through Beast Insight.
|
||||
|
||||
### 2.6.4 Coexistence Strategy
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Process"]
|
||||
perflog["PerfLog<br/>(JSON to file)"]
|
||||
insight["Beast Insight<br/>(StatsD)"]
|
||||
otel["OpenTelemetry<br/>(Tracing)"]
|
||||
end
|
||||
|
||||
perflog --> perffile["perf.log"]
|
||||
insight --> statsd["StatsD Server"]
|
||||
otel --> collector["OTLP Collector"]
|
||||
|
||||
perffile --> grafana["Grafana<br/>(Unified UI)"]
|
||||
statsd --> grafana
|
||||
collector --> grafana
|
||||
|
||||
style xrpld fill:#212121,stroke:#0a0a0a,color:#ffffff
|
||||
style grafana fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Process (dark gray)**: The single xrpld node running all three observability frameworks side by side. Each framework operates independently with no interference.
|
||||
- **PerfLog to perf.log**: PerfLog writes JSON-formatted event logs to a local file. Grafana can ingest these via Loki or a file-based datasource.
|
||||
- **Beast Insight to StatsD Server**: Insight sends aggregated metrics (counters, gauges) over UDP to a StatsD server. Grafana reads from StatsD-compatible backends like Graphite or Prometheus (via StatsD exporter).
|
||||
- **OpenTelemetry to OTLP Collector**: OTel exports spans over OTLP/HTTP to a Collector, which then forwards to a trace backend (Tempo). (OTLP/gRPC is future work — §2.2.2.)
|
||||
- **Grafana (red, unified UI)**: All three data streams converge in Grafana, enabling operators to correlate logs, metrics, and traces in a single dashboard.
|
||||
|
||||
### 2.6.5 Correlation with PerfLog
|
||||
|
||||
Trace IDs can be correlated with existing PerfLog entries for comprehensive
|
||||
debugging. The design is for `RPCHandler.cpp` to start an `rpc.command.<method>`
|
||||
span alongside the existing PerfLog `rpcStart`/`rpcFinish`/`rpcError` calls,
|
||||
extract the span's `trace_id` (when valid), and eventually stamp it onto the
|
||||
PerfLog entry (a planned `setTraceId` hook) so logs and traces share a key. The
|
||||
span status is set to OK on success or to error (recording the exception) on
|
||||
failure.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Architecture Analysis](./01-architecture-analysis.md)_ | _Next: [Implementation Strategy](./03-implementation-strategy.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
@@ -1,411 +0,0 @@
|
||||
# Implementation Strategy
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Configuration Reference](./05-configuration-reference.md)
|
||||
|
||||
---
|
||||
|
||||
## 3.1 Directory Structure
|
||||
|
||||
The telemetry implementation follows xrpld's existing code organization pattern:
|
||||
|
||||
```
|
||||
include/xrpl/
|
||||
├── telemetry/
|
||||
│ ├── Telemetry.h # Main telemetry interface (global singleton)
|
||||
│ ├── TelemetryConfig.h # Configuration structures
|
||||
│ ├── TraceContext.h # Context propagation utilities
|
||||
│ ├── SpanGuard.h # RAII span management with factory methods + discard()
|
||||
│ ├── DiscardFlag.h # Thread-local discard flag
|
||||
│ └── SpanAttributes.h # Attribute helper functions
|
||||
|
||||
src/libxrpl/
|
||||
├── telemetry/
|
||||
│ ├── Telemetry.cpp # Implementation + FilteringSpanProcessor
|
||||
│ ├── TelemetryConfig.cpp # Config parsing
|
||||
│ ├── TraceContext.cpp # Context serialization
|
||||
│ └── NullTelemetry.cpp # No-op implementation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3.2 Implementation Approach
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 30}}}%%
|
||||
flowchart TB
|
||||
subgraph phase1["Phase 1: Core"]
|
||||
direction LR
|
||||
sdk["SDK Integration"] ~~~ interface["Telemetry Interface"] ~~~ config["Configuration"]
|
||||
end
|
||||
|
||||
subgraph phase2["Phase 2: RPC"]
|
||||
direction LR
|
||||
http["HTTP Context"] ~~~ rpc["RPC Handlers"]
|
||||
end
|
||||
|
||||
subgraph phase3["Phase 3: P2P"]
|
||||
direction LR
|
||||
proto["Protobuf Context"] ~~~ tx["Transaction Relay"]
|
||||
end
|
||||
|
||||
subgraph phase4["Phase 4: Consensus"]
|
||||
direction LR
|
||||
consensus["Consensus Rounds"] ~~~ proposals["Proposals"]
|
||||
end
|
||||
|
||||
phase1 --> phase2 --> phase3 --> phase4
|
||||
|
||||
style phase1 fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style phase2 fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style phase3 fill:#e65100,stroke:#bf360c,color:#ffffff
|
||||
style phase4 fill:#c2185b,stroke:#880e4f,color:#ffffff
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Minimal Intrusion**: Instrumentation should not alter existing control flow
|
||||
2. **Zero-Cost When Disabled**: Use compile-time flags and no-op implementations
|
||||
3. **Backward Compatibility**: Protocol Buffer extensions use high field numbers
|
||||
4. **Graceful Degradation**: Tracing failures must not affect node operation
|
||||
|
||||
---
|
||||
|
||||
## 3.3 Performance Overhead Summary
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
| Metric | Overhead | Notes |
|
||||
| ------------- | ---------- | ------------------------------------------------ |
|
||||
| CPU | 1-3% | Of per-transaction CPU cost (~200μs baseline) |
|
||||
| Memory | ~10 MB | SDK statics + batch buffer + worker thread stack |
|
||||
| Network | 10-50 KB/s | Compressed OTLP export to collector |
|
||||
| Latency (p99) | <2% | With proper sampling configuration |
|
||||
|
||||
---
|
||||
|
||||
## 3.4 Detailed CPU Overhead Analysis
|
||||
|
||||
### 3.4.1 Per-Operation Costs
|
||||
|
||||
> **Note on hardware assumptions**: The costs below are based on the official OTel C++ SDK CI benchmarks
|
||||
> (969 runs on GitHub Actions 2-core shared runners). On production server hardware (3+ GHz Xeon),
|
||||
> expect costs at the **lower end** of each range (~30-50% improvement over CI hardware).
|
||||
|
||||
| Operation | Time (ns) | Frequency | Impact |
|
||||
| --------------------- | --------- | ---------------------- | ---------- |
|
||||
| Span creation | 500-1000 | Every traced operation | Low |
|
||||
| Span end | 100-200 | Every traced operation | Low |
|
||||
| SetAttribute (string) | 80-120 | 3-5 per span | Low |
|
||||
| SetAttribute (int) | 40-60 | 2-3 per span | Negligible |
|
||||
| AddEvent | 100-200 | 0-2 per span | Low |
|
||||
| Context injection | 150-250 | Per outgoing message | Low |
|
||||
| Context extraction | 100-180 | Per incoming message | Low |
|
||||
| GetCurrent context | 10-20 | Thread-local access | Negligible |
|
||||
|
||||
**Source**: Span creation based on OTel C++ SDK `BM_SpanCreation` benchmark (AlwaysOnSampler +
|
||||
SimpleSpanProcessor + InMemoryExporter), median ~1,000 ns on CI hardware. AddEvent includes
|
||||
timestamp read + string copy + vector push + mutex acquisition. Context injection/extraction
|
||||
confirmed by `BM_SpanCreationWithScope` benchmark delta (~160 ns).
|
||||
|
||||
### 3.4.2 Transaction Processing Overhead
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
%%{init: {'pie': {'textPosition': 0.75}}}%%
|
||||
pie showData
|
||||
"tx.receive (1400ns)" : 1400
|
||||
"tx.validate (1200ns)" : 1200
|
||||
"tx.relay (1200ns)" : 1200
|
||||
"Context inject (200ns)" : 200
|
||||
```
|
||||
|
||||
**Transaction Tracing Overhead (~4.0μs total)**
|
||||
|
||||
</div>
|
||||
|
||||
**Overhead percentage**: 4.0 μs / 200 μs (avg tx processing) = **~2.0%**
|
||||
|
||||
> **Breakdown**: Each span (tx.receive, tx.validate, tx.relay) costs ~1,000 ns for creation plus
|
||||
> ~200-400 ns for 3-5 attribute sets. Context injection is ~200 ns (confirmed by benchmarks).
|
||||
> On production hardware, expect ~2.6 μs total (~1.3% overhead) due to faster span creation (~500-600 ns).
|
||||
|
||||
### 3.4.3 Consensus Round Overhead
|
||||
|
||||
| Operation | Count | Cost (ns) | Total |
|
||||
| ---------------------- | ----- | --------- | ---------- |
|
||||
| consensus.round span | 1 | ~1200 | ~1.2 μs |
|
||||
| consensus.phase spans | 3 | ~1100 | ~3.3 μs |
|
||||
| proposal.receive spans | ~20 | ~1100 | ~22 μs |
|
||||
| proposal.send spans | ~3 | ~1100 | ~3.3 μs |
|
||||
| Context operations | ~30 | ~200 | ~6 μs |
|
||||
| **TOTAL** | | | **~36 μs** |
|
||||
|
||||
> **Why higher**: Each span costs ~1,000 ns creation + ~100-200 ns for 1-2 attributes, totaling ~1,100-1,200 ns.
|
||||
> Context operations remain ~200 ns (confirmed by benchmarks). On production hardware, expect ~24 μs total.
|
||||
|
||||
**Overhead percentage**: 36 μs / 3s (typical round) = **~0.001%** (negligible)
|
||||
|
||||
### 3.4.4 RPC Request Overhead
|
||||
|
||||
| Operation | Cost (ns) |
|
||||
| ---------------- | ------------ |
|
||||
| rpc.request span | ~1200 |
|
||||
| rpc.command span | ~1100 |
|
||||
| Context extract | ~250 |
|
||||
| Context inject | ~200 |
|
||||
| **TOTAL** | **~2.75 μs** |
|
||||
|
||||
> **Why higher**: Each span costs ~1,000 ns creation + ~100-200 ns for attributes (command name,
|
||||
> version, role). Context extract/inject costs are confirmed by OTel C++ benchmarks.
|
||||
|
||||
- Fast RPC (1ms): 2.75 μs / 1ms = **~0.275%**
|
||||
- Slow RPC (100ms): 2.75 μs / 100ms = **~0.003%**
|
||||
|
||||
---
|
||||
|
||||
## 3.5 Memory Overhead Analysis
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 3.5.1 Static Memory
|
||||
|
||||
| Component | Size | Allocated |
|
||||
| ------------------------------------ | ----------- | ---------- |
|
||||
| TracerProvider singleton | ~64 KB | At startup |
|
||||
| BatchSpanProcessor (circular buffer) | ~16 KB | At startup |
|
||||
| BatchSpanProcessor (worker thread) | ~8 MB | At startup |
|
||||
| OTLP/HTTP exporter (client init) | ~64 KB | At startup |
|
||||
| Propagator registry | ~8 KB | At startup |
|
||||
| **Total static** | **~8.1 MB** | |
|
||||
|
||||
> **Why higher than earlier estimate**: The BatchSpanProcessor's circular buffer itself is only ~16 KB
|
||||
> (2049 x 8-byte `AtomicUniquePtr` entries), but it spawns a dedicated worker thread whose default
|
||||
> stack size on Linux is ~8 MB. The OTLP/HTTP exporter allocates a small client and TLS
|
||||
> initialization buffer. The worker thread stack dominates the static footprint.
|
||||
|
||||
### 3.5.2 Dynamic Memory
|
||||
|
||||
| Component | Size per unit | Max units | Peak |
|
||||
| -------------------- | -------------- | ---------- | --------------- |
|
||||
| Active span | ~500-800 bytes | 1000 | ~500-800 KB |
|
||||
| Queued span (export) | ~500 bytes | 2048 | ~1 MB |
|
||||
| Attribute storage | ~80 bytes | 5 per span | Included |
|
||||
| Context storage | ~64 bytes | Per thread | ~6.4 KB |
|
||||
| **Total dynamic** | | | **~1.5-1.8 MB** |
|
||||
|
||||
> **Why active spans are larger**: An active `Span` object includes the wrapper (~88 bytes: shared_ptr,
|
||||
> mutex, unique_ptr to Recordable) plus `SpanData` (~250 bytes: SpanContext, timestamps, name, status,
|
||||
> empty containers) plus attribute storage (~200-500 bytes for 3-5 string attributes in a `std::map`).
|
||||
> Source: `sdk/src/trace/span.h` and `sdk/include/opentelemetry/sdk/trace/span_data.h`.
|
||||
> Queued spans release the wrapper, keeping only `SpanData` + attributes (~500 bytes).
|
||||
|
||||
### 3.5.3 Memory Growth Characteristics
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 700
|
||||
height: 400
|
||||
---
|
||||
xychart-beta
|
||||
title "Memory Usage vs Span Rate (bounded by queue limit)"
|
||||
x-axis "Spans/second" [0, 200, 400, 600, 800, 1000]
|
||||
y-axis "Memory (MB)" 0 --> 12
|
||||
line [8.5, 9.2, 9.6, 9.9, 10.0, 10.0]
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
|
||||
- Memory increases with span rate but **plateaus at queue capacity** (default 2048 spans)
|
||||
- Batch export prevents unbounded growth
|
||||
- At queue limit, oldest spans are dropped (not blocked)
|
||||
- Maximum memory is bounded: ~8.3 MB static (dominated by worker thread stack) + 2048 queued spans x ~500 bytes (~1 MB) + active spans (~0.8 MB) ≈ **~10 MB ceiling**
|
||||
- The worker thread stack (~8 MB) is virtual memory; actual RSS depends on stack usage (typically much less)
|
||||
|
||||
> **Measured outcome**: A perf-iac comparison (telemetry compiled-in + enabled vs compiled-out,
|
||||
> 9 nodes — validators and client-handlers — under sustained payment load) recorded **no measurable
|
||||
> RSS increase over the telemetry-off baseline** (~15 GiB mean / ~18–19 GiB peak on both sides),
|
||||
> with no OOM, no swap, and no leak across the run. The ~10 MB ceiling above is therefore a
|
||||
> provisioning safety margin (dominated by virtual thread-stack address space), not an expected
|
||||
> resident-memory increase. Steady-state cost shows up as throughput (~3–4% at head sampling 1.0),
|
||||
> not memory.
|
||||
|
||||
### 3.5.4 Performance Data Sources
|
||||
|
||||
The overhead estimates in Sections 3.3-3.5 are derived from the following sources:
|
||||
|
||||
| Source | What it covers | URL |
|
||||
| ------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| OTel C++ SDK CI benchmarks (969 runs) | Span creation, context activation, sampler overhead | [Benchmark Dashboard](https://open-telemetry.github.io/opentelemetry-cpp/benchmarks/) |
|
||||
| `api/test/trace/span_benchmark.cc` | API-level span creation (~22 ns no-op) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/api/test/trace/span_benchmark.cc) |
|
||||
| `sdk/test/trace/sampler_benchmark.cc` | SDK span creation with samplers (~1,000 ns AlwaysOn) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/test/trace/sampler_benchmark.cc) |
|
||||
| `sdk/include/.../span_data.h` | SpanData memory layout (~250 bytes base) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/trace/span_data.h) |
|
||||
| `sdk/src/trace/span.h` | Span wrapper memory layout (~88 bytes) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/src/trace/span.h) |
|
||||
| `sdk/include/.../batch_span_processor_options.h` | Default queue size (2048), batch size (512) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h) |
|
||||
| `sdk/include/.../circular_buffer.h` | CircularBuffer implementation (AtomicUniquePtr array) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/common/circular_buffer.h) |
|
||||
| OTLP proto definition | Serialized span size estimation | [Proto](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto) |
|
||||
|
||||
---
|
||||
|
||||
## 3.6 Network Overhead Analysis
|
||||
|
||||
### 3.6.1 Export Bandwidth
|
||||
|
||||
> **Bytes per span**: Estimates use ~500 bytes/span (conservative upper bound). OTLP protobuf analysis
|
||||
> shows a typical span with 3-5 string attributes serializes to ~200-300 bytes raw; with gzip
|
||||
> compression (~60-70% of raw) and batching (amortized headers), ~350 bytes/span is more realistic.
|
||||
> The table uses the conservative estimate for capacity planning.
|
||||
|
||||
| Sampling Rate | Spans/sec | Bandwidth | Notes |
|
||||
| ------------- | --------- | --------- | ---------------- |
|
||||
| 100% | ~500 | ~250 KB/s | Development only |
|
||||
| 10% | ~50 | ~25 KB/s | Staging |
|
||||
| 1% | ~5 | ~2.5 KB/s | Production |
|
||||
| Error-only | ~1 | ~0.5 KB/s | Minimal overhead |
|
||||
|
||||
### 3.6.2 Trace Context Propagation
|
||||
|
||||
| Message Type | Context Size | Messages/sec | Overhead |
|
||||
| ---------------------- | ------------ | ------------ | ----------- |
|
||||
| TMTransaction | 25 bytes | ~100 | ~2.5 KB/s |
|
||||
| TMProposeSet | 25 bytes | ~10 | ~250 B/s |
|
||||
| TMValidation | 25 bytes | ~50 | ~1.25 KB/s |
|
||||
| **Total P2P overhead** | | | **~4 KB/s** |
|
||||
|
||||
---
|
||||
|
||||
## 3.7 Optimization Strategies
|
||||
|
||||
### 3.7.1 Sampling Strategies
|
||||
|
||||
#### Tail Sampling
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
trace["New Trace"]
|
||||
|
||||
trace --> errors{"Is Error?"}
|
||||
errors -->|Yes| sample["SAMPLE"]
|
||||
errors -->|No| consensus{"Is Consensus?"}
|
||||
|
||||
consensus -->|Yes| sample
|
||||
consensus -->|No| slow{"Is Slow?"}
|
||||
|
||||
slow -->|Yes| sample
|
||||
slow -->|No| prob{"Random < 10%?"}
|
||||
|
||||
prob -->|Yes| sample
|
||||
prob -->|No| drop["DROP"]
|
||||
|
||||
style sample fill:#4caf50,stroke:#388e3c,color:#fff
|
||||
style drop fill:#f44336,stroke:#c62828,color:#fff
|
||||
```
|
||||
|
||||
### 3.7.2 Batch Tuning Recommendations
|
||||
|
||||
| Environment | Batch Size | Batch Delay | Max Queue |
|
||||
| ------------------ | ---------- | ----------- | --------- |
|
||||
| Low-latency | 128 | 1000ms | 512 |
|
||||
| High-throughput | 1024 | 10000ms | 8192 |
|
||||
| Memory-constrained | 256 | 2000ms | 512 |
|
||||
|
||||
### 3.7.3 Conditional Instrumentation
|
||||
|
||||
Instrumentation is gated on two levels. A compile-time feature flag (`XRPL_ENABLE_TELEMETRY`) reduces the trace macros to no-ops when telemetry is built out, so disabled builds carry zero cost. At runtime, per-component guards (e.g. `shouldTracePeer()`) skip span creation for components whose tracing is turned off, incurring no overhead beyond a single boolean check.
|
||||
|
||||
---
|
||||
|
||||
## 3.8 Links to Detailed Documentation
|
||||
|
||||
- **[Configuration Reference](./05-configuration-reference.md)**: Configuration options and collector setup
|
||||
- **[Implementation Phases](./06-implementation-phases.md)**: Detailed timeline and milestones
|
||||
|
||||
---
|
||||
|
||||
## 3.9 Code Intrusiveness Assessment
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
This section provides a detailed assessment of how intrusive the OpenTelemetry integration is to the existing xrpld codebase.
|
||||
|
||||
### 3.9.3 Risk Assessment by Component
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Do First** ↖ ↗ **Plan Carefully**
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Code Intrusiveness Risk Matrix
|
||||
x-axis Low Risk --> High Risk
|
||||
y-axis Low Value --> High Value
|
||||
|
||||
RPC Tracing: [0.2, 0.55]
|
||||
Transaction Relay: [0.55, 0.85]
|
||||
Consensus Tracing: [0.75, 0.92]
|
||||
Peer Message Tracing: [0.85, 0.35]
|
||||
JobQueue Context: [0.3, 0.42]
|
||||
Ledger Acquisition: [0.48, 0.65]
|
||||
PathFinding: [0.38, 0.72]
|
||||
TxQ and Fees: [0.25, 0.62]
|
||||
Validator Mgmt: [0.15, 0.35]
|
||||
```
|
||||
|
||||
**Optional** ↙ ↘ **Avoid**
|
||||
|
||||
</div>
|
||||
|
||||
#### Risk Level Definitions
|
||||
|
||||
| Risk Level | Definition | Mitigation |
|
||||
| ---------- | ---------------------------------------------------------------- | ---------------------------------- |
|
||||
| **Low** | Additive changes only; no modification to existing logic | Standard code review |
|
||||
| **Medium** | Minor modifications to existing functions; clear boundaries | Comprehensive unit tests |
|
||||
| **High** | Changes to core logic or data structures; potential side effects | Integration tests + staged rollout |
|
||||
|
||||
### 3.9.4 Architectural Impact Assessment
|
||||
|
||||
| Aspect | Impact | Justification |
|
||||
| -------------------- | ------- | -------------------------------------------------------------------------------- |
|
||||
| **Data Flow** | Minimal | Read-only instrumentation; no modification to consensus or transaction data flow |
|
||||
| **Threading Model** | Minimal | Context propagation uses thread-local storage (standard OTel pattern) |
|
||||
| **Memory Model** | Low | Bounded queues prevent unbounded growth; RAII ensures cleanup |
|
||||
| **Network Protocol** | Low | Optional fields in protobuf (high field numbers); backward compatible |
|
||||
| **Configuration** | None | New config section; existing configs unaffected |
|
||||
| **Build System** | Low | Optional CMake flag; builds work without OpenTelemetry |
|
||||
| **Dependencies** | Low | OpenTelemetry SDK is optional; null implementation when disabled |
|
||||
|
||||
### 3.9.5 Backward Compatibility
|
||||
|
||||
| Compatibility | Status | Notes |
|
||||
| --------------- | ------- | ----------------------------------------------------- |
|
||||
| **Config File** | ✅ Full | New `[telemetry]` section is optional |
|
||||
| **Protocol** | ✅ Full | Optional protobuf fields with high field numbers |
|
||||
| **Build** | ✅ Full | `XRPL_ENABLE_TELEMETRY=OFF` produces identical binary |
|
||||
| **Runtime** | ✅ Full | `enabled=0` produces zero overhead |
|
||||
| **API** | ✅ Full | No changes to public RPC or P2P APIs |
|
||||
|
||||
### 3.9.6 Rollback Strategy
|
||||
|
||||
If issues are discovered after deployment:
|
||||
|
||||
1. **Immediate**: Set `enabled=0` in config and restart (zero code change)
|
||||
2. **Quick**: Rebuild with `XRPL_ENABLE_TELEMETRY=OFF`
|
||||
3. **Complete**: Revert telemetry commits (clean separation makes this easy)
|
||||
|
||||
### 3.9.7 Code Change Examples
|
||||
|
||||
**Minimal RPC Instrumentation (Low Intrusiveness):** Instrumenting an RPC handler adds roughly 3-4 lines: one macro to start the span and one or two `setAttribute` calls (command name, status). The span ends automatically via RAII, so the existing control flow — process the request, send the result — is untouched.
|
||||
|
||||
**Consensus Instrumentation (Medium Intrusiveness):** Consensus is slightly more intrusive because child spans in later phase transitions need the round's context. Beyond the span-start and attribute macros, this requires storing the active context in a new member variable (`currentRoundContext_`) at round start. The existing round logic itself remains unchanged.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Design Decisions](./02-design-decisions.md)_ | _Next: [Configuration Reference](./05-configuration-reference.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
@@ -1,265 +0,0 @@
|
||||
# Configuration Reference
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Implementation Phases](./06-implementation-phases.md)
|
||||
|
||||
---
|
||||
|
||||
## 5.1 xrpld Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **TxQ** = Transaction Queue
|
||||
|
||||
### 5.1.1 Configuration File Section
|
||||
|
||||
The authoritative `[telemetry]` example lives in `cfg/xrpld-example.cfg`. Telemetry is disabled by default (`enabled=0`); enabling it turns on distributed tracing for transaction flow, consensus, and RPC calls, with traces exported to an OpenTelemetry Collector over OTLP. Head sampling is intentionally fixed at 1.0 (sample everything) and is not configurable — per-node head-sampling would produce broken/partial distributed traces, so volume reduction is delegated to the collector's tail sampling (see Section 7.4.2). The full option reference follows.
|
||||
|
||||
### 5.1.2 Configuration Options Summary
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --------------------- | ------ | --------------------------------- | ---------------------------------------------------- |
|
||||
| `enabled` | bool | `false` | Enable/disable telemetry |
|
||||
| `traces_endpoint` | string | `http://localhost:4318/v1/traces` | Full OTLP/HTTP URL for spans, used verbatim |
|
||||
| `use_tls` | bool | `false` | Enable TLS for exporter connection |
|
||||
| `tls_ca_cert` | string | `""` | Path to CA certificate file |
|
||||
| `batch_size` | uint | `512` | Spans per export batch |
|
||||
| `batch_delay_ms` | uint | `5000` | Max delay before sending batch (ms) |
|
||||
| `max_queue_size` | uint | `2048` | Maximum queued spans |
|
||||
| `trace_transactions` | 0 or 1 | `1` | Enable transaction tracing |
|
||||
| `trace_consensus` | 0 or 1 | `1` | Enable consensus tracing |
|
||||
| `trace_rpc` | 0 or 1 | `1` | Enable RPC tracing |
|
||||
| `trace_peer` | 0 or 1 | `1` | Enable peer message tracing (high volume) |
|
||||
| `trace_ledger` | 0 or 1 | `1` | Enable ledger tracing |
|
||||
| `service_name` | string | `"xrpld"` | Service name (`service.name`) for traces and metrics |
|
||||
| `service_instance_id` | string | `<node_pubkey>` | Instance identifier |
|
||||
|
||||
**Planned (not yet implemented)**: the following options appear in the design
|
||||
documents but are not parsed by `TelemetryConfig.cpp` in Phase 1b and later
|
||||
phases. They will be added as the corresponding subsystems are instrumented:
|
||||
|
||||
| Option | Planned Phase | Purpose |
|
||||
| -------------------------- | ------------- | ----------------------------------------------------------------------- |
|
||||
| `exporter` | Future | Select between OTLP/HTTP and OTLP/gRPC |
|
||||
| `trace_pathfind` | Phase 2 | Path computation tracing toggle |
|
||||
| `trace_txq` | Phase 3 | Transaction queue tracing toggle |
|
||||
| `trace_validator` | Future | Validator list / manifest update tracing |
|
||||
| `trace_amendment` | Future | Amendment voting tracing |
|
||||
| `consensus_trace_strategy` | Phase 4 | Trace ID strategy for consensus rounds (`deterministic` \| `attribute`) |
|
||||
|
||||
---
|
||||
|
||||
## 5.2 Configuration Parser
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
The parser `makeTelemetrySetup()` in `src/libxrpl/telemetry/TelemetryConfig.cpp` reads the `[telemetry]` `Section` and populates a `Telemetry::Setup` struct, applying the defaults listed in Section 5.1.2 via `section.valueOr(...)`. It derives `serviceInstanceId` from the node public key when not overridden, selects the exporter endpoint default by exporter type, and leaves the sampling ratio at its fixed 1.0 default (not read from config — see Section 7.4.2).
|
||||
|
||||
---
|
||||
|
||||
## 5.3 Application Integration
|
||||
|
||||
### 5.3.1 ApplicationImp Changes
|
||||
|
||||
> **Deferred identity**: The node public key (`nodeIdentity_`) is not
|
||||
> available during `ApplicationImp`'s member initializer list — it is
|
||||
> resolved later in `setup()`. The `Telemetry` object is therefore
|
||||
> constructed with an empty `serviceInstanceId` and patched via
|
||||
> `setServiceInstanceId()` once `setup()` has called `getNodeIdentity()`.
|
||||
|
||||
`ApplicationImp` (in `src/xrpld/app/main/Application.cpp`) owns a `std::unique_ptr<telemetry::Telemetry> telemetry_`. It is built in the member initializer list via `makeTelemetry(makeTelemetrySetup(...))` with an empty `serviceInstanceId`, then patched in `setup()` by calling `setServiceInstanceId()` with the Base58 node public key (unless the user supplied a custom `service_instance_id`). `start()` and `run()` forward to `telemetry_->start()` / `telemetry_->stop()`, and `getTelemetry()` returns the owned instance.
|
||||
|
||||
### 5.3.2 ServiceRegistry Interface Addition
|
||||
|
||||
`include/xrpl/core/ServiceRegistry.h` gains a pure-virtual `telemetry::Telemetry& getTelemetry()` (with a forward declaration of `telemetry::Telemetry`), giving every component a uniform accessor for the tracing subsystem.
|
||||
|
||||
> **Note:** `Application` extends `ServiceRegistry`, so `getTelemetry()` is
|
||||
> available on both. Components that hold a `ServiceRegistry&` (e.g.
|
||||
> `NetworkOPsImp`) call `registry_.get().getTelemetry()`. Components that
|
||||
> still hold an `Application&` (e.g. `ServerHandler`, `PeerImp`,
|
||||
> `RCLConsensus::Adaptor`) call `app_.getTelemetry()` directly.
|
||||
|
||||
---
|
||||
|
||||
## 5.4 CMake Integration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 5.4.1 Find OpenTelemetry Module
|
||||
|
||||
A `cmake/FindOpenTelemetry.cmake` module locates the OpenTelemetry C++ SDK. It first tries `find_package(opentelemetry-cpp CONFIG)`, aliasing the imported targets `OpenTelemetry::api`, `OpenTelemetry::sdk`, and `OpenTelemetry::otlp_grpc_exporter`, and falls back to `pkg-config` when no CMake config package is present.
|
||||
|
||||
### 5.4.2 CMakeLists.txt Changes
|
||||
|
||||
The top-level `CMakeLists.txt` adds an `XRPL_ENABLE_TELEMETRY` option (default `OFF`). When enabled, it runs `find_package(OpenTelemetry REQUIRED)`, defines the `XRPL_ENABLE_TELEMETRY` compile flag, and builds the `xrpl_telemetry` library from the real telemetry sources linked against the OpenTelemetry targets; when disabled, it builds the same target from a no-op `NullTelemetry.cpp` so call sites compile unchanged.
|
||||
|
||||
---
|
||||
|
||||
## 5.5 OpenTelemetry Collector Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring
|
||||
|
||||
The authoritative collector config lives in the repo at `docker/telemetry/otel-collector-config.yaml` (with Tempo backend config in `docker/telemetry/tempo.yaml`). The sections below summarize the development and production shapes of that pipeline.
|
||||
|
||||
### 5.5.1 Development Configuration
|
||||
|
||||
The development collector enables an OTLP receiver on both gRPC (`0.0.0.0:4317`) and HTTP (`0.0.0.0:4318`), a single `batch` processor (1s timeout, batch size 100), and two exporters: a `logging` exporter for console debugging and `otlp_grpc/tempo` (insecure) for trace visualization. The single `traces` pipeline wires receiver → batch → both exporters.
|
||||
|
||||
### 5.5.2 Production Configuration
|
||||
|
||||
The production collector adds TLS on the OTLP gRPC receiver and a richer processor chain: a `memory_limiter` (OOM guard), `batch` (5s timeout, size 512), `tail_sampling`, and an `attributes` processor that hashes sensitive fields (e.g. `tx_account`) and stamps `deployment.environment`. Tail sampling keeps all `ERROR` traces, slow consensus rounds (>5s) and slow RPC requests (>1s), and probabilistically samples the remainder at 10%. Exporters target Grafana Tempo (TLS) and Elastic APM; `health_check` and `zpages` extensions are enabled for operability.
|
||||
|
||||
---
|
||||
|
||||
## 5.6 Docker Compose Development Environment
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
The authoritative development stack lives in the repo at `docker/telemetry/docker-compose.yml`. It brings up four services on a shared `xrpld-telemetry` network: an `otel-collector` (otel/opentelemetry-collector-contrib) exposing OTLP gRPC `4317`, OTLP HTTP `4318`, and health check `13133`; `tempo` for trace storage/visualization; `grafana` with provisioned datasources and dashboards (anonymous admin enabled); and an optional `prometheus` for metric correlation.
|
||||
|
||||
---
|
||||
|
||||
## 5.7 Configuration Architecture
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph config["Configuration Sources"]
|
||||
cfgFile["xrpld.cfg<br/>[telemetry] section"]
|
||||
cmake["CMake<br/>XRPL_ENABLE_TELEMETRY"]
|
||||
end
|
||||
|
||||
subgraph init["Initialization"]
|
||||
parse["makeTelemetrySetup()"]
|
||||
factory["makeTelemetry()"]
|
||||
end
|
||||
|
||||
subgraph runtime["Runtime Components"]
|
||||
tracer["TracerProvider"]
|
||||
exporter["OTLP Exporter"]
|
||||
processor["BatchProcessor"]
|
||||
end
|
||||
|
||||
subgraph collector["Collector Pipeline"]
|
||||
recv["Receivers"]
|
||||
proc["Processors"]
|
||||
exp["Exporters"]
|
||||
end
|
||||
|
||||
cfgFile --> parse
|
||||
cmake -->|"compile flag"| parse
|
||||
parse --> factory
|
||||
factory --> tracer
|
||||
tracer --> processor
|
||||
processor --> exporter
|
||||
exporter -->|"OTLP"| recv
|
||||
recv --> proc
|
||||
proc --> exp
|
||||
|
||||
style config fill:#e3f2fd,stroke:#1976d2
|
||||
style runtime fill:#e8f5e9,stroke:#388e3c
|
||||
style collector fill:#fff3e0,stroke:#ff9800
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Configuration Sources**: `xrpld.cfg` provides runtime settings (endpoint, per-component trace toggles) while the CMake flag controls whether telemetry is compiled in at all. Head sampling is fixed at 1.0 and is not a config option; volume reduction happens via tail sampling in the collector.
|
||||
- **Initialization**: `makeTelemetrySetup()` parses config values, then `makeTelemetry()` constructs the provider, processor, and exporter objects.
|
||||
- **Runtime Components**: The `TracerProvider` creates spans, the `BatchProcessor` buffers them, and the `OTLP Exporter` serializes and sends them over the wire.
|
||||
- **OTLP arrow to Collector**: Trace data leaves the xrpld process via OTLP/HTTP and enters the external Collector pipeline. (OTLP/gRPC is future work — see design decisions §2.2.2.)
|
||||
- **Collector Pipeline**: `Receivers` ingest OTLP data, `Processors` apply sampling/filtering/enrichment, and `Exporters` forward traces to storage backends (Tempo, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 5.8 Grafana Integration
|
||||
|
||||
> **APM** = Application Performance Monitoring
|
||||
|
||||
Step-by-step instructions for integrating xrpld traces with Grafana.
|
||||
|
||||
### 5.8.1 Data Source Configuration
|
||||
|
||||
#### Tempo (Recommended)
|
||||
|
||||
A Tempo datasource (`grafana/provisioning/datasources/tempo.yaml`, provisioned from `docker/telemetry/grafana/`) points at `http://tempo:3200` and enables `tracesToLogs` (linking to Loki on `service.name`/`tx_hash` and mapping `trace_id` → `traceID`), `serviceMap` against Prometheus, the node graph, and Loki search.
|
||||
|
||||
#### Elastic APM
|
||||
|
||||
Alternatively, an Elasticsearch datasource (`grafana/provisioning/datasources/elastic-apm.yaml`) of type `elasticsearch` points at `http://elasticsearch:9200` against the `apm-*` index, using `@timestamp` as the time field and mapping the log message/level fields.
|
||||
|
||||
### 5.8.2 Dashboard Provisioning
|
||||
|
||||
A dashboard provider (`grafana/provisioning/dashboards/dashboards.yaml`) loads the `xrpld` dashboard folder from disk (`/var/lib/grafana/dashboards/rippled`), polling for changes every 30s with deletion disabled.
|
||||
|
||||
### 5.8.3 Example Dashboard: RPC Performance
|
||||
|
||||
An example `xrpld RPC Performance` dashboard (uid `xrpld-rpc-performance`) sourced from Tempo via TraceQL provides four panels: RPC latency by command (heatmap), RPC error rate by command (timeseries), the top 10 slowest RPC commands by average duration (table), and a recent-traces table.
|
||||
|
||||
### 5.8.4 Example Dashboard: Transaction Tracing
|
||||
|
||||
An example `xrpld Transaction Tracing` dashboard (uid `xrpld-tx-tracing`) over Tempo provides three panels: transaction throughput (`tx.receive` rate, stat), cross-node relay count (average `span.relay_count` on `tx.relay`, timeseries), and a table of transaction validation errors (`tx.validate` with `status = error`).
|
||||
|
||||
### 5.8.5 TraceQL Query Examples
|
||||
|
||||
Common queries for xrpld traces:
|
||||
|
||||
```
|
||||
# Find all traces for a specific transaction hash
|
||||
{resource.service.name="xrpld" && span.tx_hash="ABC123..."}
|
||||
|
||||
# Find slow RPC commands (>100ms)
|
||||
{resource.service.name="xrpld" && name=~"rpc.command.*"} | { duration > 100ms }
|
||||
|
||||
# Find consensus rounds taking >5 seconds
|
||||
{resource.service.name="xrpld" && name="consensus.round"} | { duration > 5s }
|
||||
|
||||
# Find failed transactions with error details
|
||||
{resource.service.name="xrpld" && name="tx.validate" && status = error}
|
||||
|
||||
# Find transactions relayed to many peers
|
||||
{resource.service.name="xrpld" && name="tx.relay"} | { span.relay_count > 10 }
|
||||
|
||||
# Compare latency across nodes
|
||||
{resource.service.name="xrpld" && name="rpc.command.account_info"} | avg_over_time(duration) by (resource.service.instance.id)
|
||||
```
|
||||
|
||||
### 5.8.6 Correlation with PerfLog
|
||||
|
||||
To correlate OpenTelemetry traces with existing PerfLog data:
|
||||
|
||||
**Step 1: Configure Loki to ingest PerfLog**
|
||||
|
||||
Configure a Promtail scrape job (`promtail-config.yaml`) that tails `/var/log/rippled/perf*.log`, parses each JSON line, and promotes `trace_id`, `ledger_seq`, and `tx_hash` to Loki labels.
|
||||
|
||||
**Step 2: Add trace_id to PerfLog entries**
|
||||
|
||||
Modify PerfLog so its JSON output includes a `trace_id` field whenever a valid span is active: fetch the current span from the OpenTelemetry runtime context, and if its context is valid, render the trace ID as a 32-character lowercase hex string into the log entry.
|
||||
|
||||
**Step 3: Configure Grafana trace-to-logs link**
|
||||
|
||||
In the Tempo datasource, set the `tracesToLogs` derived field to link to Loki on the `trace_id` and `tx_hash` tags, with `filterByTraceID: true`.
|
||||
|
||||
### 5.8.7 Correlation with Insight/StatsD Metrics
|
||||
|
||||
To correlate traces with existing Beast Insight metrics:
|
||||
|
||||
**Step 1: Export Insight metrics to Prometheus**
|
||||
|
||||
Add a Prometheus scrape job (`prometheus.yaml`) named `xrpld-statsd` targeting the StatsD exporter at `statsd-exporter:9102`.
|
||||
|
||||
**Step 2: Add exemplars to metrics**
|
||||
|
||||
The OpenTelemetry SDK automatically adds exemplars (trace IDs) to metrics when using the Prometheus exporter, linking metric spikes to specific traces.
|
||||
|
||||
**Step 3: Configure Grafana metric-to-trace link**
|
||||
|
||||
In the Prometheus datasource, set `exemplarTraceIdDestinations` to map the `trace_id` exemplar to the Tempo datasource.
|
||||
|
||||
**Step 4: Dashboard panel with exemplars**
|
||||
|
||||
Add a timeseries panel over Prometheus (e.g. `histogram_quantile(0.99, rate(xrpld_rpc_duration_seconds_bucket[5m]))`) with `exemplar: true` enabled.
|
||||
|
||||
This allows clicking on metric data points to jump directly to the related trace.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Implementation Strategy](./03-implementation-strategy.md)_ | _Next: [Implementation Phases](./06-implementation-phases.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
@@ -1,576 +0,0 @@
|
||||
# Implementation Phases
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Configuration Reference](./05-configuration-reference.md) | [Observability Backends](./07-observability-backends.md)
|
||||
|
||||
---
|
||||
|
||||
## 6.1 Phase Overview
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title OpenTelemetry Implementation Timeline
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat Week %W
|
||||
|
||||
section Phase 1
|
||||
Core Infrastructure :p1, 2024-01-01, 2w
|
||||
SDK Integration :p1a, 2024-01-01, 4d
|
||||
Telemetry Interface :p1b, after p1a, 3d
|
||||
Configuration & CMake :p1c, after p1b, 3d
|
||||
Unit Tests :p1d, after p1c, 2d
|
||||
Buffer & Integration :p1e, after p1d, 2d
|
||||
|
||||
section Phase 2
|
||||
RPC Tracing :p2, after p1, 2w
|
||||
HTTP Context Extraction :p2a, after p1, 2d
|
||||
RPC Handler Instrumentation :p2b, after p2a, 4d
|
||||
PathFinding Instrumentation :p2f, after p2b, 2d
|
||||
TxQ Instrumentation :p2g, after p2f, 2d
|
||||
WebSocket Support :p2c, after p2g, 2d
|
||||
Integration Tests :p2d, after p2c, 2d
|
||||
Buffer & Review :p2e, after p2d, 4d
|
||||
|
||||
section Phase 3
|
||||
Transaction Tracing :p3, after p2, 2w
|
||||
Protocol Buffer Extension :p3a, after p2, 2d
|
||||
PeerImp Instrumentation :p3b, after p3a, 3d
|
||||
Fee Escalation Instrumentation :p3f, after p3b, 2d
|
||||
Relay Context Propagation :p3c, after p3f, 3d
|
||||
Multi-node Tests :p3d, after p3c, 2d
|
||||
Buffer & Review :p3e, after p3d, 4d
|
||||
|
||||
section Phase 4
|
||||
Consensus Tracing :p4, after p3, 2w
|
||||
Consensus Round Spans :p4a, after p3, 3d
|
||||
Proposal Handling :p4b, after p4a, 3d
|
||||
Validator List & Manifest Tracing :p4f, after p4b, 2d
|
||||
Amendment Voting Tracing :p4g, after p4f, 2d
|
||||
SHAMap Sync Tracing :p4h, after p4g, 2d
|
||||
Validation Tests :p4c, after p4h, 4d
|
||||
Buffer & Review :p4e, after p4c, 4d
|
||||
|
||||
section Phase 5
|
||||
Documentation & Deploy :p5, after p4, 1w
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.2 Phase 1: Core Infrastructure (Weeks 1-2)
|
||||
|
||||
**Objective**: Establish foundational telemetry infrastructure
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | ----------------------------------------------------- |
|
||||
| 1.1 | Add OpenTelemetry C++ SDK to Conan/CMake |
|
||||
| 1.2 | Implement `Telemetry` interface and factory |
|
||||
| 1.3 | Implement `SpanGuard` RAII wrapper |
|
||||
| 1.4 | Implement configuration parser |
|
||||
| 1.5 | Integrate into `ApplicationImp` |
|
||||
| 1.6 | Add conditional compilation (`XRPL_ENABLE_TELEMETRY`) |
|
||||
| 1.7 | Create `NullTelemetry` no-op implementation |
|
||||
| 1.8 | Unit tests for core infrastructure |
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- [ ] OpenTelemetry SDK compiles and links
|
||||
- [ ] Telemetry can be enabled/disabled via config
|
||||
- [ ] Basic span creation works
|
||||
- [ ] No performance regression when disabled
|
||||
- [ ] Unit tests passing
|
||||
|
||||
---
|
||||
|
||||
## 6.3 Phase 2: RPC Tracing (Weeks 3-4)
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
**Objective**: Complete tracing for all RPC operations
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | -------------------------------------------------------------------------- |
|
||||
| 2.1 | Implement W3C Trace Context HTTP header extraction |
|
||||
| 2.2 | Instrument `ServerHandler::onRequest()` |
|
||||
| 2.3 | Instrument `xrpl::rpc::doCommand()` |
|
||||
| 2.4 | Add RPC-specific attributes |
|
||||
| 2.5 | Instrument WebSocket handler |
|
||||
| 2.6 | PathFinding instrumentation (`pathfind.request`, `pathfind.compute` spans) |
|
||||
| 2.7 | TxQ instrumentation (`txq.enqueue`, `txq.apply` spans) |
|
||||
| 2.8 | Integration tests for RPC tracing |
|
||||
| 2.9 | Performance benchmarks |
|
||||
| 2.10 | Documentation |
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- [ ] All RPC commands traced
|
||||
- [ ] Trace context propagates from HTTP headers
|
||||
- [ ] WebSocket and HTTP both instrumented
|
||||
- [ ] <1ms overhead per RPC call
|
||||
- [ ] Integration tests passing
|
||||
|
||||
---
|
||||
|
||||
## 6.4 Phase 3: Transaction Tracing (Weeks 5-6)
|
||||
|
||||
**Objective**: Trace transaction lifecycle across network
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | ---------------------------------------------------- |
|
||||
| 3.1 | Define `TraceContext` Protocol Buffer message |
|
||||
| 3.2 | Implement protobuf context serialization |
|
||||
| 3.3 | Instrument `PeerImp::handleTransaction()` |
|
||||
| 3.4 | Instrument `NetworkOPs::submitTransaction()` |
|
||||
| 3.5 | Instrument HashRouter integration |
|
||||
| 3.6 | Fee escalation instrumentation (`fee.escalate` span) |
|
||||
| 3.7 | Implement relay context propagation |
|
||||
| 3.8 | Integration tests (multi-node) |
|
||||
| 3.9 | Performance benchmarks |
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- [ ] Transaction traces span across nodes
|
||||
- [ ] Trace context in Protocol Buffer messages
|
||||
- [ ] HashRouter deduplication visible in traces
|
||||
- [ ] Multi-node integration tests passing
|
||||
- [ ] <5% overhead on transaction throughput
|
||||
|
||||
---
|
||||
|
||||
## 6.5 Phase 4: Consensus Tracing (Weeks 7-8)
|
||||
|
||||
**Objective**: Full observability into consensus rounds
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | --------------------------------------- |
|
||||
| 4.1 | Instrument `RCLConsensus::startRound()` |
|
||||
| 4.2 | Instrument phase transitions |
|
||||
| 4.3 | Instrument proposal handling |
|
||||
| 4.4 | Instrument validation handling |
|
||||
| 4.5 | Add consensus-specific attributes |
|
||||
| 4.6 | Correlate with transaction traces |
|
||||
| 4.7 | Validator list and manifest tracing |
|
||||
| 4.8 | Amendment voting tracing |
|
||||
| 4.9 | SHAMap sync tracing |
|
||||
| 4.10 | Multi-validator integration tests |
|
||||
| 4.11 | Performance validation |
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- [ ] Complete consensus round traces
|
||||
- [ ] Phase transitions visible
|
||||
- [ ] Proposals and validations traced
|
||||
- [ ] No impact on consensus timing
|
||||
- [ ] Multi-validator test network validated
|
||||
|
||||
### Implementation Status — Phase 4a Plan
|
||||
|
||||
Phase 4a (establish-phase gap fill & cross-node correlation) will add:
|
||||
|
||||
- **Deterministic trace ID** derived from `previousLedger.id()` so all validators
|
||||
in the same round share the same `trace_id` (switchable via
|
||||
`consensus_trace_strategy` config: `"deterministic"`, or `"random"` which is
|
||||
experimental and not used).
|
||||
See [Configuration Reference](./05-configuration-reference.md) for full
|
||||
configuration options.
|
||||
- **Round lifecycle spans**: `consensus.round` with round-to-round span links.
|
||||
- **Establish phase**: `consensus.establish`, `consensus.update_positions` (with
|
||||
`dispute.resolve` events), `consensus.check` (with threshold tracking).
|
||||
- **Mode changes**: `consensus.mode_change` spans.
|
||||
- **Validation**: `consensus.validation.send` with span link to round span
|
||||
(thread-safe cross-thread access via `roundSpanContext_` snapshot).
|
||||
- **Separation of concerns**: telemetry extracted to private helpers
|
||||
(`startRoundTracing`, `createValidationSpan`, `startEstablishTracing`,
|
||||
`updateEstablishTracing`, `endEstablishTracing`).
|
||||
|
||||
The `Phase4_taskList.md` spec document is introduced in the Phase 2 PR (#6424)
|
||||
and will contain the full task breakdown and implementation notes.
|
||||
|
||||
---
|
||||
|
||||
## 6.6 Phase 5: Documentation & Deployment (Week 9)
|
||||
|
||||
**Objective**: Production readiness
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | ----------------------------- |
|
||||
| 5.1 | Operator runbook |
|
||||
| 5.2 | Grafana dashboards |
|
||||
| 5.3 | Alert definitions |
|
||||
| 5.4 | Collector deployment examples |
|
||||
| 5.5 | Developer documentation |
|
||||
| 5.6 | Training materials |
|
||||
| 5.7 | Final integration testing |
|
||||
|
||||
---
|
||||
|
||||
## 6.7 Risk Assessment
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Risk Assessment Matrix
|
||||
x-axis Low Impact --> High Impact
|
||||
y-axis Low Likelihood --> High Likelihood
|
||||
quadrant-1 Mitigate Immediately
|
||||
quadrant-2 Plan Mitigation
|
||||
quadrant-3 Accept Risk
|
||||
quadrant-4 Monitor Closely
|
||||
|
||||
SDK Compat: [0.2, 0.18]
|
||||
Protocol Chg: [0.75, 0.72]
|
||||
Perf Overhead: [0.58, 0.42]
|
||||
Context Prop: [0.4, 0.55]
|
||||
Memory Leaks: [0.85, 0.25]
|
||||
```
|
||||
|
||||
### Risk Details
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
| ------------------------------------ | ---------- | ------ | --------------------------------------- |
|
||||
| Protocol changes break compatibility | Medium | High | Use high field numbers, optional fields |
|
||||
| Performance overhead unacceptable | Medium | Medium | Sampling, conditional compilation |
|
||||
| Context propagation complexity | Medium | Medium | Phased rollout, extensive testing |
|
||||
| SDK compatibility issues | Low | Medium | Pin SDK version, fallback to no-op |
|
||||
| Memory leaks in long-running nodes | Low | High | Memory profiling, bounded queues |
|
||||
|
||||
---
|
||||
|
||||
## 6.8 Success Metrics
|
||||
|
||||
| Metric | Target | Measurement |
|
||||
| ------------------------ | -------------------------------------------------------------- | --------------------- |
|
||||
| Trace coverage | >95% of transaction code paths (independent of sampling ratio) | Sampling verification |
|
||||
| CPU overhead | <3% | Benchmark tests |
|
||||
| Memory overhead | <10 MB | Memory profiling |
|
||||
| Latency impact (p99) | <2% | Performance tests |
|
||||
| Trace completeness | >99% spans with required attrs | Validation script |
|
||||
| Cross-node trace linkage | >90% of multi-hop transactions | Integration tests |
|
||||
|
||||
---
|
||||
|
||||
## 6.9 Quick Wins and Crawl-Walk-Run Strategy
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
This section outlines a prioritized approach to maximize ROI with minimal initial investment.
|
||||
|
||||
### 6.9.1 Crawl-Walk-Run Overview
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph crawl["🐢 CRAWL (Week 1-2)"]
|
||||
direction LR
|
||||
c1[Core SDK Setup] ~~~ c2[RPC Tracing Only] ~~~ c3[PathFinding + TxQ Tracing] ~~~ c4[Single Node]
|
||||
end
|
||||
|
||||
subgraph walk["🚶 WALK (Week 3-5)"]
|
||||
direction LR
|
||||
w1[Transaction Tracing] ~~~ w2[Fee Escalation Tracing] ~~~ w3[Cross-Node Context] ~~~ w4[Basic Dashboards]
|
||||
end
|
||||
|
||||
subgraph run["🏃 RUN (Week 6-9)"]
|
||||
direction LR
|
||||
r1[Consensus Tracing] ~~~ r2[Validator, Amendment,<br/>SHAMap Tracing] ~~~ r3[Full Correlation] ~~~ r4[Production Deploy]
|
||||
end
|
||||
|
||||
crawl --> walk --> run
|
||||
|
||||
style crawl fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style walk fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style run fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style c1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style c2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style c3 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style c4 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style w1 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style w2 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style w3 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style w4 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style r1 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style r2 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style r3 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style r4 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **CRAWL (Weeks 1-2)**: Minimal investment -- set up the SDK, instrument RPC and PathFinding/TxQ handlers, and verify on a single node. Delivers immediate latency visibility.
|
||||
- **WALK (Weeks 3-5)**: Expand to transaction lifecycle tracing, fee escalation, cross-node context propagation, and basic Grafana dashboards. This is where distributed tracing starts working.
|
||||
- **RUN (Weeks 6-9)**: Full consensus instrumentation, validator/amendment/SHAMap tracing, end-to-end correlation, and production deployment with sampling and alerting.
|
||||
- **Arrows (crawl → walk → run)**: Each phase builds on the prior one; you cannot skip ahead because later phases depend on infrastructure established earlier.
|
||||
|
||||
### 6.9.2 Quick Wins (Immediate Value)
|
||||
|
||||
| Quick Win | Value | When to Deploy |
|
||||
| ------------------------------ | ------ | -------------- |
|
||||
| **RPC Command Tracing** | High | Week 2 |
|
||||
| **RPC Latency Histograms** | High | Week 2 |
|
||||
| **Error Rate Dashboard** | Medium | Week 2 |
|
||||
| **Transaction Submit Tracing** | High | Week 3 |
|
||||
| **Consensus Round Duration** | Medium | Week 6 |
|
||||
|
||||
### 6.9.3 CRAWL Phase (Weeks 1-2)
|
||||
|
||||
**Goal**: Get basic tracing working with minimal code changes.
|
||||
|
||||
**What You Get**:
|
||||
|
||||
- RPC request/response traces for all commands
|
||||
- Latency breakdown per RPC command
|
||||
- PathFinding and TxQ tracing (directly impacts RPC latency)
|
||||
- Error visibility with stack traces
|
||||
- Basic Grafana dashboard
|
||||
|
||||
**Code Changes**: ~15 lines in `ServerHandler.cpp`, ~40 lines in new telemetry module
|
||||
|
||||
**Why Start Here**:
|
||||
|
||||
- RPC is the lowest-risk, highest-visibility component
|
||||
- PathFinding and TxQ are RPC-adjacent and directly affect latency
|
||||
- Immediate value for debugging client issues
|
||||
- No cross-node complexity
|
||||
- Single file modification to existing code
|
||||
|
||||
### 6.9.4 WALK Phase (Weeks 3-5)
|
||||
|
||||
**Goal**: Add transaction lifecycle tracing across nodes.
|
||||
|
||||
**What You Get**:
|
||||
|
||||
- End-to-end transaction traces from submit to relay
|
||||
- Fee escalation tracing within the transaction pipeline
|
||||
- Cross-node correlation (see transaction path)
|
||||
- HashRouter deduplication visibility
|
||||
- Relay latency metrics
|
||||
|
||||
**Code Changes**: ~120 lines across 4 files, plus protobuf extension
|
||||
|
||||
**Why Do This Second**:
|
||||
|
||||
- Builds on RPC tracing (transactions submitted via RPC)
|
||||
- Fee escalation is integral to the transaction processing pipeline
|
||||
- Moderate complexity (requires context propagation)
|
||||
- High value for debugging transaction issues
|
||||
|
||||
### 6.9.5 RUN Phase (Weeks 6-9)
|
||||
|
||||
**Goal**: Full observability including consensus.
|
||||
|
||||
**What You Get**:
|
||||
|
||||
- Complete consensus round visibility
|
||||
- Phase transition timing
|
||||
- Validator proposal tracking
|
||||
- Validator list and manifest tracing
|
||||
- Amendment voting tracing
|
||||
- SHAMap sync tracing
|
||||
- Full end-to-end traces (client → RPC → TX → consensus → ledger)
|
||||
|
||||
**Code Changes**: ~100 lines across 3 consensus files, plus validator/amendment/SHAMap modules
|
||||
|
||||
**Why Do This Last**:
|
||||
|
||||
- Highest complexity (consensus is critical path)
|
||||
- Validator, amendment, and SHAMap components are lower priority
|
||||
- Requires thorough testing
|
||||
- Lower relative value (consensus issues are rarer)
|
||||
|
||||
### 6.9.6 ROI Prioritization Matrix
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Implementation ROI Matrix
|
||||
x-axis Low Effort --> High Effort
|
||||
y-axis Low Value --> High Value
|
||||
quadrant-1 Quick Wins - Do First
|
||||
quadrant-2 Major Projects - Plan Carefully
|
||||
quadrant-3 Nice to Have - Optional
|
||||
quadrant-4 Time Sinks - Avoid
|
||||
|
||||
RPC Tracing: [0.15, 0.92]
|
||||
TX Submit Trace: [0.3, 0.78]
|
||||
TX Relay Trace: [0.5, 0.88]
|
||||
Consensus Trace: [0.72, 0.72]
|
||||
Peer Msg Trace: [0.85, 0.3]
|
||||
Ledger Acquire: [0.55, 0.52]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.10 Definition of Done
|
||||
|
||||
> **TxQ** = Transaction Queue | **HA** = High Availability
|
||||
|
||||
Clear, measurable criteria for each phase.
|
||||
|
||||
### 6.10.1 Phase 1: Core Infrastructure
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| --------------- | ---------------------------------------------------------- | ---------------------------- |
|
||||
| SDK Integration | `cmake --build` succeeds with `-DXRPL_ENABLE_TELEMETRY=ON` | ✅ Compiles |
|
||||
| Runtime Toggle | `enabled=0` produces zero overhead | <0.1% CPU difference |
|
||||
| Span Creation | Unit test creates and exports span | Span appears in Tempo |
|
||||
| Configuration | All config options parsed correctly | Config validation tests pass |
|
||||
| Documentation | Developer guide exists | PR approved |
|
||||
|
||||
**Definition of Done**: All criteria met, PR merged, no regressions in CI.
|
||||
|
||||
### 6.10.2 Phase 2: RPC Tracing
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| ------------------ | ---------------------------------- | -------------------------- |
|
||||
| Coverage | All RPC commands instrumented | 100% of commands |
|
||||
| Context Extraction | traceparent header propagates | Integration test passes |
|
||||
| Attributes | Command, status, duration recorded | Validation script confirms |
|
||||
| Performance | RPC latency overhead | <1ms p99 |
|
||||
| Dashboard | Grafana dashboard deployed | Screenshot in docs |
|
||||
|
||||
**Definition of Done**: RPC traces visible in Tempo for all commands, dashboard shows latency distribution.
|
||||
|
||||
### 6.10.3 Phase 3: Transaction Tracing
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| ---------------- | ------------------------------- | -------------------------- |
|
||||
| Local Trace | Submit → validate → TxQ traced | Single-node test passes |
|
||||
| Cross-Node | Context propagates via protobuf | Multi-node test passes |
|
||||
| Relay Visibility | relay_count attribute correct | Spot check 100 txs |
|
||||
| HashRouter | Deduplication visible | Duplicates produce no span |
|
||||
| Performance | TX throughput overhead | <5% degradation |
|
||||
|
||||
**Definition of Done**: Transaction traces span 3+ nodes in test network, performance within bounds.
|
||||
|
||||
### 6.10.4 Phase 4: Consensus Tracing
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| -------------------- | ----------------------------- | ------------------------- |
|
||||
| Round Tracing | startRound creates root span | Unit test passes |
|
||||
| Phase Visibility | All phases have child spans | Integration test confirms |
|
||||
| Proposer Attribution | Proposer ID in attributes | Spot check 50 rounds |
|
||||
| Timing Accuracy | Phase durations match PerfLog | <5% variance |
|
||||
| No Consensus Impact | Round timing unchanged | Performance test passes |
|
||||
|
||||
**Definition of Done**: Consensus rounds fully traceable, no impact on consensus timing.
|
||||
|
||||
### 6.10.5 Phase 5: Production Deployment
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| ------------ | ---------------------------- | -------------------------- |
|
||||
| Collector HA | Multiple collectors deployed | No single point of failure |
|
||||
| Sampling | Tail sampling configured | 10% base + errors + slow |
|
||||
| Retention | Data retained per policy | 7 days hot, 30 days warm |
|
||||
| Alerting | Alerts configured | Error spike, high latency |
|
||||
| Runbook | Operator documentation | Approved by ops team |
|
||||
| Training | Team trained | Session completed |
|
||||
|
||||
**Definition of Done**: Telemetry running in production, operators trained, alerts active.
|
||||
|
||||
### 6.10.6 Success Metrics Summary
|
||||
|
||||
| Phase | Primary Metric | Secondary Metric | Deadline |
|
||||
| ------- | ---------------------- | --------------------------- | ------------- |
|
||||
| Phase 1 | SDK compiles and runs | Zero overhead when disabled | End of Week 2 |
|
||||
| Phase 2 | 100% RPC coverage | <1ms latency overhead | End of Week 4 |
|
||||
| Phase 3 | Cross-node traces work | <5% throughput impact | End of Week 6 |
|
||||
| Phase 4 | Consensus fully traced | No consensus timing impact | End of Week 8 |
|
||||
| Phase 5 | Production deployment | Operators trained | End of Week 9 |
|
||||
|
||||
---
|
||||
|
||||
## 6.11 Recommended Implementation Order
|
||||
|
||||
Based on ROI analysis, implement in this exact order:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph week1["Week 1"]
|
||||
t1[1. OpenTelemetry SDK<br/>Conan/CMake integration]
|
||||
t2[2. Telemetry interface<br/>SpanGuard, config]
|
||||
end
|
||||
|
||||
subgraph week2["Week 2"]
|
||||
t3[3. RPC ServerHandler<br/>instrumentation]
|
||||
t4[4. Basic Tempo setup<br/>for testing]
|
||||
end
|
||||
|
||||
subgraph week3["Week 3"]
|
||||
t5[5. Transaction submit<br/>tracing]
|
||||
t6[6. Grafana dashboard<br/>v1]
|
||||
end
|
||||
|
||||
subgraph week4["Week 4"]
|
||||
t7[7. Protobuf context<br/>extension]
|
||||
t8[8. PeerImp tx.relay<br/>instrumentation]
|
||||
end
|
||||
|
||||
subgraph week5["Week 5"]
|
||||
t9[9. Multi-node<br/>integration tests]
|
||||
t10[10. Performance<br/>benchmarks]
|
||||
end
|
||||
|
||||
subgraph week6_8["Weeks 6-8"]
|
||||
t11[11. Consensus<br/>instrumentation]
|
||||
t12[12. Full integration<br/>testing]
|
||||
end
|
||||
|
||||
subgraph week9["Week 9"]
|
||||
t13[13. Production<br/>deployment]
|
||||
t14[14. Documentation<br/>& training]
|
||||
end
|
||||
|
||||
t1 --> t2 --> t3 --> t4
|
||||
t4 --> t5 --> t6
|
||||
t6 --> t7 --> t8
|
||||
t8 --> t9 --> t10
|
||||
t10 --> t11 --> t12
|
||||
t12 --> t13 --> t14
|
||||
|
||||
style week1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style week2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style week3 fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style week4 fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style week5 fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style week6_8 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style week9 fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style t1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style t2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style t3 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style t4 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style t5 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t6 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t7 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t8 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t9 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t10 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t11 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style t12 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style t13 fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style t14 fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Week 1 (tasks 1-2)**: Foundation work -- integrate the OpenTelemetry SDK via Conan/CMake and build the `Telemetry` interface with `SpanGuard` and config parsing.
|
||||
- **Week 2 (tasks 3-4)**: First observable output -- instrument `ServerHandler` for RPC tracing and stand up Tempo so developers can see traces immediately.
|
||||
- **Weeks 3-5 (tasks 5-10)**: Transaction lifecycle -- add submit tracing, build the first Grafana dashboard, extend protobuf for cross-node context, instrument `PeerImp` relay, then validate with multi-node integration tests and performance benchmarks.
|
||||
- **Weeks 6-8 (tasks 11-12)**: Consensus deep-dive -- instrument consensus rounds and phases, then run full integration testing across all instrumented paths.
|
||||
- **Week 9 (tasks 13-14)**: Go-live -- deploy to production with sampling/alerting configured, and deliver documentation and operator training.
|
||||
- **Arrow chain (t1 → ... → t14)**: Strict sequential dependency; each task's output is a prerequisite for the next.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Configuration Reference](./05-configuration-reference.md)_ | _Next: [Observability Backends](./07-observability-backends.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
@@ -1,407 +0,0 @@
|
||||
# Observability Backend Recommendations
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Implementation Phases](./06-implementation-phases.md) | [Appendix](./08-appendix.md)
|
||||
|
||||
---
|
||||
|
||||
## 7.1 Development/Testing Backends
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
| Backend | Pros | Cons | Use Case |
|
||||
| ---------- | ----------------------------------- | ---------------------- | ------------------- |
|
||||
| **Tempo** | Cost-effective, Grafana integration | Requires Grafana stack | Local dev, CI, Prod |
|
||||
| **Zipkin** | Simple, lightweight | Basic features | Quick prototyping |
|
||||
|
||||
### Quick Start with Tempo
|
||||
|
||||
```bash
|
||||
# Start Tempo with OTLP support
|
||||
docker run -d --name tempo \
|
||||
-p 3200:3200 \
|
||||
-p 4317:4317 \
|
||||
-p 4318:4318 \
|
||||
grafana/tempo:2.6.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7.2 Production Backends
|
||||
|
||||
> **APM** = Application Performance Monitoring
|
||||
|
||||
| Backend | Pros | Cons | Use Case |
|
||||
| ----------------- | ----------------------------------------- | ---------------------- | --------------------------- |
|
||||
| **Grafana Tempo** | Cost-effective, Grafana integration | Requires Grafana stack | Most production deployments |
|
||||
| **Elastic APM** | Full observability stack, log correlation | Resource intensive | Existing Elastic users |
|
||||
| **Honeycomb** | Excellent query, high cardinality | SaaS cost | Deep debugging needs |
|
||||
| **Datadog APM** | Full platform, easy setup | SaaS cost | Enterprise with budget |
|
||||
|
||||
### Backend Selection Flowchart
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
start[Select Backend] --> budget{Budget<br/>Constraints?}
|
||||
|
||||
budget -->|Yes| oss[Open Source]
|
||||
budget -->|No| saas{Prefer<br/>SaaS?}
|
||||
|
||||
oss --> existing{Existing<br/>Stack?}
|
||||
existing -->|Grafana| tempo[Grafana Tempo]
|
||||
existing -->|Elastic| elastic[Elastic APM]
|
||||
existing -->|None| tempo
|
||||
|
||||
saas -->|Yes| enterprise{Enterprise<br/>Support?}
|
||||
saas -->|No| oss
|
||||
|
||||
enterprise -->|Yes| datadog[Datadog APM]
|
||||
enterprise -->|No| honeycomb[Honeycomb]
|
||||
|
||||
tempo --> final[Configure Collector]
|
||||
elastic --> final
|
||||
honeycomb --> final
|
||||
datadog --> final
|
||||
|
||||
style start fill:#0f172a,stroke:#020617,color:#fff
|
||||
style budget fill:#334155,stroke:#1e293b,color:#fff
|
||||
style oss fill:#1e293b,stroke:#0f172a,color:#fff
|
||||
style existing fill:#334155,stroke:#1e293b,color:#fff
|
||||
style saas fill:#334155,stroke:#1e293b,color:#fff
|
||||
style enterprise fill:#334155,stroke:#1e293b,color:#fff
|
||||
style final fill:#0f172a,stroke:#020617,color:#fff
|
||||
style tempo fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style elastic fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style honeycomb fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style datadog fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Budget Constraints? (Yes)**: Leads to open-source options. If you already run Grafana or Elastic, pick the matching backend; otherwise default to Grafana Tempo.
|
||||
- **Budget Constraints? (No) → Prefer SaaS?**: If you want a managed service, choose between Datadog (enterprise support) and Honeycomb (developer-focused). If not, fall back to open-source.
|
||||
- **Terminal nodes (Tempo / Elastic / Honeycomb / Datadog)**: Each represents a concrete backend choice, all of which feed into the same final step.
|
||||
- **Configure Collector**: Regardless of backend, you always finish by configuring the OTel Collector to export to your chosen destination.
|
||||
|
||||
---
|
||||
|
||||
## 7.3 Recommended Production Architecture
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring | **HA** = High Availability
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph validators["Validator Nodes"]
|
||||
v1[xrpld<br/>Validator 1]
|
||||
v2[xrpld<br/>Validator 2]
|
||||
end
|
||||
|
||||
subgraph stock["Stock Nodes"]
|
||||
s1[xrpld<br/>Stock 1]
|
||||
s2[xrpld<br/>Stock 2]
|
||||
end
|
||||
|
||||
subgraph collector["OTel Collector Cluster"]
|
||||
c1[Collector<br/>DC1]
|
||||
c2[Collector<br/>DC2]
|
||||
end
|
||||
|
||||
subgraph backends["Storage Backends"]
|
||||
tempo[(Grafana<br/>Tempo)]
|
||||
elastic[(Elastic<br/>APM)]
|
||||
archive[(S3/GCS<br/>Archive)]
|
||||
end
|
||||
|
||||
subgraph ui["Visualization"]
|
||||
grafana[Grafana<br/>Dashboards]
|
||||
end
|
||||
|
||||
v1 -->|OTLP| c1
|
||||
v2 -->|OTLP| c1
|
||||
s1 -->|OTLP| c2
|
||||
s2 -->|OTLP| c2
|
||||
|
||||
c1 --> tempo
|
||||
c1 --> elastic
|
||||
c2 --> tempo
|
||||
c2 --> archive
|
||||
|
||||
tempo --> grafana
|
||||
elastic --> grafana
|
||||
|
||||
%% Note: simplified single-collector-per-DC topology shown for clarity
|
||||
|
||||
style validators fill:#b71c1c,stroke:#7f1d1d,color:#ffffff
|
||||
style stock fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style collector fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style backends fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style ui fill:#4a148c,stroke:#2e0d57,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Validator / Stock Nodes**: All xrpld nodes emit trace data via OTLP. Validators and stock nodes are grouped separately because they may reside in different network zones.
|
||||
- **Collector Cluster (DC1, DC2)**: Regional collectors receive OTLP from nodes in their datacenter, apply processing (sampling, enrichment), and fan out to multiple backends. Enrichment includes deployment-tier tagging: each collector stamps `deployment.environment` and (as a fallback) `xrpl.network.type` so one Grafana stack can filter data from many collectors by tier.
|
||||
- **Storage Backends**: Tempo and Elastic provide queryable trace storage; S3/GCS Archive provides long-term cold storage for compliance or post-incident analysis.
|
||||
- **Grafana Dashboards**: The single visualization layer that queries both Tempo and Elastic, giving operators a unified view of all traces.
|
||||
- **Data flow direction**: Nodes → Collectors → Storage → Grafana. Each arrow represents a network hop; minimizing collector-to-backend hops reduces latency.
|
||||
|
||||
> **Note**: Production deployments should use multiple collector instances behind a load balancer for high availability. The diagram shows a simplified single-collector topology for clarity.
|
||||
|
||||
---
|
||||
|
||||
## 7.4 Architecture Considerations
|
||||
|
||||
### 7.4.1 Collector Placement
|
||||
|
||||
| Strategy | Description | Pros | Cons |
|
||||
| ------------- | -------------------- | ------------------------ | ----------------------- |
|
||||
| **Sidecar** | Collector per node | Isolation, simple config | Resource overhead |
|
||||
| **DaemonSet** | Collector per host | Shared resources | Complexity |
|
||||
| **Gateway** | Central collector(s) | Centralized processing | Single point of failure |
|
||||
|
||||
**Recommendation**: Use **Gateway** pattern with regional collectors for xrpld networks:
|
||||
|
||||
- One collector cluster per datacenter/region
|
||||
- Tail-based sampling at collector level
|
||||
- Multiple export destinations for redundancy
|
||||
|
||||
### 7.4.2 Sampling Strategy
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph head["Head Sampling (Node)"]
|
||||
hs[Node-level head sampling<br/>fixed at 100%<br/>not configurable]
|
||||
end
|
||||
|
||||
subgraph tail["Tail Sampling (Collector)"]
|
||||
ts1[Keep all errors]
|
||||
ts2[Keep slow >5s]
|
||||
ts3[Keep 10% rest]
|
||||
end
|
||||
|
||||
head --> tail
|
||||
|
||||
ts1 --> final[Final Traces]
|
||||
ts2 --> final
|
||||
ts3 --> final
|
||||
|
||||
style head fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style tail fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style hs fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style ts1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style ts2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style ts3 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style final fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Head Sampling (Node)**: xrpld pins head sampling at 100% (sample everything) and does not expose a configurable ratio. This is intentional: a per-node ratio would let different nodes make divergent keep/drop decisions for the same distributed trace, producing broken/partial traces. A span inheriting a remote parent is decided by the same ratio sampler, not by the peer's sampled flag. Volume reduction is delegated to the collector's tail sampling.
|
||||
- **Tail Sampling (Collector)**: The second filter -- the collector inspects completed traces and applies rules: keep all errors, keep anything slower than 5 seconds, and keep 10% of the remainder.
|
||||
- **Arrow head → tail**: All head-sampled traces flow to the collector, where tail sampling further reduces volume while preserving the most valuable data.
|
||||
- **Final Traces**: The output after both sampling stages; this is what gets stored and queried. The two-stage approach balances cost with debuggability.
|
||||
|
||||
### 7.4.3 Data Retention
|
||||
|
||||
| Environment | Hot Storage | Warm Storage | Cold Archive |
|
||||
| ----------- | ----------- | ------------ | ------------ |
|
||||
| Development | 24 hours | N/A | N/A |
|
||||
| Staging | 7 days | N/A | N/A |
|
||||
| Production | 7 days | 30 days | many years |
|
||||
|
||||
---
|
||||
|
||||
## 7.5 Integration Checklist
|
||||
|
||||
- [ ] Choose primary backend (Tempo recommended for cost/features)
|
||||
- [ ] Deploy collector cluster with high availability
|
||||
- [ ] Configure tail-based sampling for error/latency traces
|
||||
- [ ] Set up Grafana dashboards for trace visualization
|
||||
- [ ] Configure alerts for trace anomalies
|
||||
- [ ] Establish data retention policies
|
||||
- [ ] Test trace correlation with logs and metrics
|
||||
|
||||
---
|
||||
|
||||
## 7.6 Grafana Dashboard Examples
|
||||
|
||||
Pre-built dashboards for xrpld observability.
|
||||
|
||||
### 7.6.1 Consensus Health Dashboard
|
||||
|
||||
A Tempo-backed dashboard (uid `xrpld-consensus-health`) with four panels, all driven by TraceQL:
|
||||
|
||||
- **Consensus Round Duration** (timeseries, ms): average `consensus.round` span duration per node instance, with yellow/red thresholds at 4s/5s.
|
||||
- **Phase Duration Breakdown** (barchart): average duration of `consensus.phase.*` spans grouped by span name.
|
||||
- **Proposers per Round** (stat): average of the `span.proposers` attribute on `consensus.round` spans.
|
||||
- **Recent Slow Rounds (>5s)** (table): `consensus.round` spans filtered to `duration > 5s`.
|
||||
|
||||
Each panel's TraceQL query is described inline in its bullet above.
|
||||
|
||||
### 7.6.2 Node Overview Dashboard
|
||||
|
||||
A Tempo-backed dashboard (uid `xrpld-node-overview`) with four panels:
|
||||
|
||||
- **Active Nodes** (stat): count of distinct `resource.service.instance.id` values seen for the `xrpld` service.
|
||||
- **Total Transactions (1h)** (stat): count of `tx.receive` spans.
|
||||
- **Error Rate** (gauge, percent): ratio of `status = error` spans to all spans, with yellow/red thresholds at 1%/5%.
|
||||
- **Service Map** (nodeGraph): Tempo-generated service dependency graph.
|
||||
|
||||
### 7.6.3 Alert Rules
|
||||
|
||||
Grafana provisions three TraceQL-based alert rules (group `xrpld-tracing-alerts`, evaluated every 1m) against the Tempo datasource:
|
||||
|
||||
- **Consensus Round Slow** (warning, `for: 5m`): fires when average `consensus.round` duration exceeds 5s.
|
||||
|
||||
```
|
||||
{resource.service.name="xrpld" && name="consensus.round"} | avg(duration) > 5s
|
||||
```
|
||||
|
||||
- **RPC Error Rate Spike** (critical, `for: 2m`): fires when the error rate across `rpc.command.*` spans exceeds 5%. Error _rate_ is a ratio, so it must divide the error-span rate by the total-span rate — a single TraceQL `rate()` returns spans/second, not a percentage, and would fire on traffic volume alone. This uses span metrics emitted by the collector's `spanmetrics` connector (Prometheus datasource), not a TraceQL query:
|
||||
|
||||
```
|
||||
sum(rate(traces_spanmetrics_calls_total{service_name="xrpld", span_name=~"rpc.command.*", status_code="STATUS_CODE_ERROR"}[5m]))
|
||||
/
|
||||
sum(rate(traces_spanmetrics_calls_total{service_name="xrpld", span_name=~"rpc.command.*"}[5m]))
|
||||
> 0.05
|
||||
```
|
||||
|
||||
- **Transaction Throughput Drop** (warning, `for: 10m`): fires when the `tx.receive` span rate falls below 10/s.
|
||||
|
||||
```
|
||||
{resource.service.name="xrpld" && name="tx.receive"} | rate() < 10
|
||||
```
|
||||
|
||||
> **Note**: The Consensus Round Slow and Transaction Throughput Drop rules use TraceQL aggregates (`avg(duration)`, `rate()`), which require Tempo 2.3+ with TraceQL metrics enabled. Verify aggregate query support in your Tempo version before provisioning. The RPC Error Rate Spike rule instead queries Prometheus span metrics (collector `spanmetrics` connector), so it needs that connector enabled in the collector pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 7.7 PerfLog and Insight Correlation
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
How to correlate OpenTelemetry traces with existing xrpld observability.
|
||||
|
||||
### 7.7.1 Correlation Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Node"]
|
||||
otel[OpenTelemetry<br/>Spans]
|
||||
perflog[PerfLog<br/>JSON Logs]
|
||||
insight[Beast Insight<br/>StatsD Metrics]
|
||||
end
|
||||
|
||||
subgraph collectors["Data Collection"]
|
||||
otelc[OTel Collector]
|
||||
promtail[Promtail/Fluentd]
|
||||
statsd[StatsD Exporter]
|
||||
end
|
||||
|
||||
subgraph storage["Storage"]
|
||||
tempo[(Tempo)]
|
||||
loki[(Loki)]
|
||||
prom[(Prometheus)]
|
||||
end
|
||||
|
||||
subgraph grafana["Grafana"]
|
||||
traces[Trace View]
|
||||
logs[Log View]
|
||||
metrics[Metrics View]
|
||||
corr[Correlation<br/>Panel]
|
||||
end
|
||||
|
||||
otel -->|OTLP| otelc --> tempo
|
||||
perflog -->|JSON| promtail --> loki
|
||||
insight -->|StatsD| statsd --> prom
|
||||
|
||||
tempo --> traces
|
||||
loki --> logs
|
||||
prom --> metrics
|
||||
|
||||
traces --> corr
|
||||
logs --> corr
|
||||
metrics --> corr
|
||||
|
||||
style xrpld fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style collectors fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style storage fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style grafana fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style otel fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style perflog fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style insight fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style otelc fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style promtail fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style statsd fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style tempo fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style loki fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style prom fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style traces fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style logs fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style metrics fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style corr fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Node (three sources)**: A single node emits three independent data streams -- OpenTelemetry spans, PerfLog JSON logs, and Beast Insight StatsD metrics.
|
||||
- **Data Collection layer**: Each stream has its own collector -- OTel Collector for spans, Promtail/Fluentd for logs, and a StatsD exporter for metrics. They operate independently.
|
||||
- **Storage layer (Tempo, Loki, Prometheus)**: Each data type lands in a purpose-built store optimized for its query patterns (trace search, log grep, metric aggregation).
|
||||
- **Grafana Correlation Panel**: The key integration point -- Grafana queries all three stores and links them via shared fields (`trace_id`, `tx_hash`, `ledger_seq`), enabling a single-pane debugging experience.
|
||||
|
||||
### 7.7.2 Correlation Fields
|
||||
|
||||
| Source | Field | Link To | Purpose |
|
||||
| ----------- | ------------------- | ------------- | -------------------------- |
|
||||
| **Trace** | `trace_id` | Logs | Find log entries for trace |
|
||||
| **Trace** | `tx_hash` | Logs, Metrics | Find TX-related data |
|
||||
| **Trace** | `ledger_seq` | Logs | Find ledger-related logs |
|
||||
| **PerfLog** | `trace_id` (new) | Traces | Jump to trace from log |
|
||||
| **PerfLog** | `ledger_seq` | Traces | Find consensus trace |
|
||||
| **Insight** | `exemplar.trace_id` | Traces | Jump from metric spike |
|
||||
|
||||
### 7.7.3 Example: Debugging a Slow Transaction
|
||||
|
||||
**Step 1: Find the trace**
|
||||
|
||||
```
|
||||
# In Grafana Explore with Tempo
|
||||
{resource.service.name="xrpld" && span.tx_hash="ABC123..."}
|
||||
```
|
||||
|
||||
**Step 2: Get the trace_id from the trace view**
|
||||
|
||||
```
|
||||
Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736
|
||||
```
|
||||
|
||||
**Step 3: Find related PerfLog entries**
|
||||
|
||||
```
|
||||
# In Grafana Explore with Loki
|
||||
{job="xrpld"} |= "4bf92f3577b34da6a3ce929d0e0e4736"
|
||||
```
|
||||
|
||||
**Step 4: Check Insight metrics for the time window**
|
||||
|
||||
```
|
||||
# In Grafana with Prometheus
|
||||
rate(xrpld_tx_applied_total[1m])
|
||||
@ timestamp_from_trace
|
||||
```
|
||||
|
||||
### 7.7.4 Unified Dashboard Example
|
||||
|
||||
A single dashboard (uid `xrpld-unified`) that ties traces, metrics, and logs together across the Tempo, Prometheus, and Loki datasources:
|
||||
|
||||
- **Transaction Latency (Traces)** (timeseries, Tempo): `histogram_over_time(duration)` of `tx.receive` spans.
|
||||
- **Transaction Rate (Metrics)** (timeseries, Prometheus): `rate(xrpld_tx_received_total[5m])` per instance, with a data link that opens the matching `tx.receive` traces in Tempo.
|
||||
- **Recent Logs** (logs, Loki): `{job="xrpld"} | json`.
|
||||
- **Trace Search** (table, Tempo): all `xrpld` traces, with per-row data links on `traceID` that jump to the trace in Tempo and to the correlated logs in Loki (`{job="xrpld"} |= "<traceID>"`).
|
||||
|
||||
The cross-datasource data links are what make this a single-pane debugging view; the correlation fields they rely on are listed in section 7.7.2.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Implementation Phases](./06-implementation-phases.md)_ | _Next: [Appendix](./08-appendix.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
@@ -1,188 +0,0 @@
|
||||
# Appendix
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Observability Backends](./07-observability-backends.md)
|
||||
|
||||
---
|
||||
|
||||
## 8.1 Glossary
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **TxQ** = Transaction Queue
|
||||
|
||||
| Term | Definition |
|
||||
| --------------------- | ---------------------------------------------------------- |
|
||||
| **Span** | A unit of work with start/end time, name, and attributes |
|
||||
| **Trace** | A collection of spans representing a complete request flow |
|
||||
| **Trace ID** | 128-bit unique identifier for a trace |
|
||||
| **Span ID** | 64-bit unique identifier for a span within a trace |
|
||||
| **Context** | Carrier for trace/span IDs across boundaries |
|
||||
| **Propagator** | Component that injects/extracts context |
|
||||
| **Sampler** | Decides which traces to record |
|
||||
| **Exporter** | Sends spans to backend |
|
||||
| **Collector** | Receives, processes, and forwards telemetry |
|
||||
| **OTLP** | OpenTelemetry Protocol (wire format) |
|
||||
| **W3C Trace Context** | Standard HTTP headers for trace propagation |
|
||||
| **Baggage** | Key-value pairs propagated across service boundaries |
|
||||
| **Resource** | Entity producing telemetry (service, host, etc.) |
|
||||
| **Instrumentation** | Code that creates telemetry data |
|
||||
|
||||
### xrpld-Specific Terms
|
||||
|
||||
| Term | Definition |
|
||||
| ----------------- | ------------------------------------------------------------- |
|
||||
| **Overlay** | P2P network layer managing peer connections |
|
||||
| **Consensus** | XRP Ledger consensus algorithm (RCL) |
|
||||
| **Proposal** | Validator's suggested transaction set for a ledger |
|
||||
| **Validation** | Validator's signature on a closed ledger |
|
||||
| **HashRouter** | Component for transaction deduplication |
|
||||
| **JobQueue** | Thread pool for asynchronous task execution |
|
||||
| **PerfLog** | Existing performance logging system in xrpld |
|
||||
| **Beast Insight** | Existing metrics framework in xrpld |
|
||||
| **PathFinding** | Payment path computation engine for cross-currency payments |
|
||||
| **TxQ** | Transaction queue managing fee-based prioritization |
|
||||
| **LoadManager** | Dynamic fee escalation based on network load |
|
||||
| **SHAMap** | SHA-256 hash-based map (Merkle trie variant) for ledger state |
|
||||
|
||||
---
|
||||
|
||||
## 8.2 Span Hierarchy Visualization
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph trace["Trace: Transaction Lifecycle"]
|
||||
rpc["rpc.request<br/>(entry point)"]
|
||||
validate["tx.validate"]
|
||||
relay["tx.relay<br/>(parent span)"]
|
||||
|
||||
subgraph peers["Peer Spans"]
|
||||
p1["peer.send<br/>Peer A"]
|
||||
p2["peer.send<br/>Peer B"]
|
||||
p3["peer.send<br/>Peer C"]
|
||||
end
|
||||
|
||||
subgraph pathfinding["PathFinding Spans"]
|
||||
pathfind["pathfind.request"]
|
||||
pathcomp["pathfind.compute"]
|
||||
end
|
||||
|
||||
consensus["consensus.round"]
|
||||
apply["tx.apply"]
|
||||
|
||||
subgraph txqueue["TxQ Spans"]
|
||||
txq["txq.enqueue"]
|
||||
txqApply["txq.apply"]
|
||||
end
|
||||
|
||||
feeCalc["fee.escalate"]
|
||||
end
|
||||
|
||||
subgraph validators["Validator Spans"]
|
||||
valFetch["validator.list.fetch"]
|
||||
valManifest["validator.manifest"]
|
||||
end
|
||||
|
||||
rpc --> validate
|
||||
rpc --> pathfind
|
||||
pathfind --> pathcomp
|
||||
validate --> relay
|
||||
relay --> p1
|
||||
relay --> p2
|
||||
relay --> p3
|
||||
p1 -.->|"context propagation"| consensus
|
||||
consensus --> apply
|
||||
apply --> txq
|
||||
txq --> txqApply
|
||||
txq --> feeCalc
|
||||
|
||||
style trace fill:#0f172a,stroke:#020617,color:#fff
|
||||
style peers fill:#1e3a8a,stroke:#172554,color:#fff
|
||||
style pathfinding fill:#134e4a,stroke:#0f766e,color:#fff
|
||||
style txqueue fill:#064e3b,stroke:#047857,color:#fff
|
||||
style validators fill:#4c1d95,stroke:#6d28d9,color:#fff
|
||||
style rpc fill:#1d4ed8,stroke:#1e40af,color:#fff
|
||||
style validate fill:#047857,stroke:#064e3b,color:#fff
|
||||
style relay fill:#047857,stroke:#064e3b,color:#fff
|
||||
style p1 fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style p2 fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style p3 fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style consensus fill:#fef3c7,stroke:#fde68a,color:#1e293b
|
||||
style apply fill:#047857,stroke:#064e3b,color:#fff
|
||||
style pathfind fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style pathcomp fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style txq fill:#047857,stroke:#064e3b,color:#fff
|
||||
style txqApply fill:#047857,stroke:#064e3b,color:#fff
|
||||
style feeCalc fill:#047857,stroke:#064e3b,color:#fff
|
||||
style valFetch fill:#6d28d9,stroke:#4c1d95,color:#fff
|
||||
style valManifest fill:#6d28d9,stroke:#4c1d95,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **rpc.request (blue, top)**: The entry point — every traced transaction starts as an RPC call; this root span is the parent of all downstream work.
|
||||
- **tx.validate and pathfind.request (green/teal, first fork)**: The RPC request fans out into transaction validation and, for cross-currency payments, a PathFinding branch (`pathfind.request` -> `pathfind.compute`).
|
||||
- **tx.relay -> Peer Spans (teal, middle)**: After validation, the transaction is relayed to peers A, B, and C in parallel; each `peer.send` is a sibling child span showing fan-out across the network.
|
||||
- **context propagation (dashed arrow)**: The dotted line from `peer.send Peer A` to `consensus.round` represents the trace context crossing a node boundary — the receiving validator picks up the same `trace_id` and continues the trace.
|
||||
- **consensus.round -> tx.apply -> TxQ Spans (green, lower)**: Once consensus accepts the transaction, it is applied to the ledger; the TxQ spans (`txq.enqueue`, `txq.apply`, `fee.escalate`) capture queue depth and fee escalation behavior.
|
||||
- **Validator Spans (purple, detached)**: `validator.list.fetch` and `validator.manifest` are independent workflows for UNL management — they run on their own traces and are linked to consensus via Span Links, not parent-child relationships.
|
||||
|
||||
---
|
||||
|
||||
## 8.3 References
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### OpenTelemetry Resources
|
||||
|
||||
1. [OpenTelemetry C++ SDK](https://github.com/open-telemetry/opentelemetry-cpp)
|
||||
2. [OpenTelemetry Specification](https://opentelemetry.io/docs/specs/otel/)
|
||||
3. [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/)
|
||||
4. [OTLP Protocol Specification](https://opentelemetry.io/docs/specs/otlp/)
|
||||
|
||||
### Standards
|
||||
|
||||
5. [W3C Trace Context](https://www.w3.org/TR/trace-context/)
|
||||
6. [W3C Baggage](https://www.w3.org/TR/baggage/)
|
||||
7. [Protocol Buffers](https://protobuf.dev/)
|
||||
|
||||
### xrpld Resources
|
||||
|
||||
8. [xrpld Source Code](https://github.com/XRPLF/rippled)
|
||||
9. [XRP Ledger Documentation](https://xrpl.org/docs/)
|
||||
10. [xrpld Overlay README](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/README.md)
|
||||
11. [xrpld RPC README](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/rpc/README.md)
|
||||
12. [xrpld Consensus README](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/app/consensus/README.md)
|
||||
|
||||
---
|
||||
|
||||
## 8.4 Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
| ------- | ---------- | ------ | -------------------------------------------------------------- |
|
||||
| 1.0 | 2026-02-12 | - | Initial implementation plan |
|
||||
| 1.1 | 2026-02-13 | - | Refactored into modular documents |
|
||||
| 1.2 | 2026-03-24 | - | Review fixes: accuracy corrections, cross-document consistency |
|
||||
|
||||
---
|
||||
|
||||
## 8.5 Document Index
|
||||
|
||||
### Plan Documents
|
||||
|
||||
| Document | Description |
|
||||
| ---------------------------------------------------------------- | -------------------------------------------- |
|
||||
| [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) | Master overview and executive summary |
|
||||
| [00-tracing-fundamentals.md](./00-tracing-fundamentals.md) | Distributed tracing concepts and OTel primer |
|
||||
| [01-architecture-analysis.md](./01-architecture-analysis.md) | xrpld architecture and trace points |
|
||||
| [02-design-decisions.md](./02-design-decisions.md) | SDK selection, exporters, span conventions |
|
||||
| [03-implementation-strategy.md](./03-implementation-strategy.md) | Directory structure, performance analysis |
|
||||
| [05-configuration-reference.md](./05-configuration-reference.md) | xrpld config, CMake, Collector configs |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Timeline, tasks, risks, success metrics |
|
||||
| [07-observability-backends.md](./07-observability-backends.md) | Backend selection and architecture |
|
||||
| [08-appendix.md](./08-appendix.md) | Glossary, references, version history |
|
||||
| [presentation.md](./presentation.md) | Slide deck for OTel plan overview |
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Observability Backends](./07-observability-backends.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
@@ -1,199 +0,0 @@
|
||||
# [OpenTelemetry](00-tracing-fundamentals.md) Distributed Tracing Implementation Plan for xrpld
|
||||
|
||||
## Executive Summary
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
This document provides a comprehensive implementation plan for integrating OpenTelemetry distributed tracing into the xrpld XRP Ledger node software. The plan addresses the unique challenges of a decentralized peer-to-peer system where trace context must propagate across network boundaries between independent nodes.
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- **End-to-end transaction visibility**: Track transactions from submission through consensus to ledger inclusion
|
||||
- **Consensus round analysis**: Understand timing and behavior of consensus phases across validators
|
||||
- **RPC performance insights**: Identify slow handlers and optimize response times
|
||||
- **Network topology understanding**: Visualize message propagation patterns between peers
|
||||
- **Incident debugging**: Correlate events across distributed nodes during issues
|
||||
|
||||
### Estimated Performance Overhead
|
||||
|
||||
| Metric | Overhead | Notes |
|
||||
| ------------- | ---------- | ------------------------------------------------ |
|
||||
| CPU | 1-3% | Span creation and attribute setting |
|
||||
| Memory | <10 MB | SDK statics + batch buffer + worker thread stack |
|
||||
| Network | 10-50 KB/s | Compressed OTLP export to collector |
|
||||
| Latency (p99) | <2% | With proper sampling configuration |
|
||||
|
||||
---
|
||||
|
||||
## Document Structure
|
||||
|
||||
This implementation plan is organized into modular documents for easier navigation:
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
overview["📋 OpenTelemetryPlan.md<br/>(This Document)"]
|
||||
|
||||
subgraph fundamentals["Fundamentals"]
|
||||
fund["00-tracing-fundamentals.md"]
|
||||
end
|
||||
|
||||
subgraph analysis["Analysis & Design"]
|
||||
arch["01-architecture-analysis.md"]
|
||||
design["02-design-decisions.md"]
|
||||
end
|
||||
|
||||
subgraph impl["Implementation"]
|
||||
strategy["03-implementation-strategy.md"]
|
||||
config["05-configuration-reference.md"]
|
||||
end
|
||||
|
||||
subgraph deploy["Deployment & Planning"]
|
||||
phases["06-implementation-phases.md"]
|
||||
backends["07-observability-backends.md"]
|
||||
appendix["08-appendix.md"]
|
||||
end
|
||||
|
||||
overview --> fundamentals
|
||||
overview --> analysis
|
||||
overview --> impl
|
||||
overview --> deploy
|
||||
|
||||
fund --> arch
|
||||
arch --> design
|
||||
design --> strategy
|
||||
strategy --> config
|
||||
config --> phases
|
||||
phases --> backends
|
||||
backends --> appendix
|
||||
|
||||
style overview fill:#1b5e20,stroke:#0d3d14,color:#fff,stroke-width:2px
|
||||
style fundamentals fill:#00695c,stroke:#004d40,color:#fff
|
||||
style fund fill:#00695c,stroke:#004d40,color:#fff
|
||||
style analysis fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style impl fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style deploy fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style arch fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style design fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style strategy fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style config fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style phases fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style backends fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style appendix fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
| Section | Document | Description |
|
||||
| ------- | ---------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| **0** | [Tracing Fundamentals](./00-tracing-fundamentals.md) | Distributed tracing concepts, span relationships, context propagation |
|
||||
| **1** | [Architecture Analysis](./01-architecture-analysis.md) | xrpld component analysis, trace points, instrumentation priorities |
|
||||
| **2** | [Design Decisions](./02-design-decisions.md) | SDK selection, exporters, span naming, attributes, context propagation |
|
||||
| **3** | [Implementation Strategy](./03-implementation-strategy.md) | Directory structure, key principles, performance optimization |
|
||||
| **5** | [Configuration Reference](./05-configuration-reference.md) | xrpld config, CMake integration, Collector configurations |
|
||||
| **6** | [Implementation Phases](./06-implementation-phases.md) | 5-phase timeline, tasks, risks, success metrics |
|
||||
| **7** | [Observability Backends](./07-observability-backends.md) | Backend selection guide and production architecture |
|
||||
| **8** | [Appendix](./08-appendix.md) | Glossary, references, version history |
|
||||
|
||||
---
|
||||
|
||||
## 0. Tracing Fundamentals
|
||||
|
||||
This document introduces distributed tracing concepts for readers unfamiliar with the domain. It covers what traces and spans are, how parent-child and follows-from relationships model causality, how context propagates across service boundaries, and how sampling controls data volume. It also maps these concepts to xrpld-specific scenarios like transaction relay and consensus.
|
||||
|
||||
➡️ **[Read Tracing Fundamentals](./00-tracing-fundamentals.md)**
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Analysis
|
||||
|
||||
> **WS** = WebSocket | **TxQ** = Transaction Queue
|
||||
|
||||
The xrpld node consists of several key components that require instrumentation for comprehensive distributed tracing. The main areas include the RPC server (HTTP/WebSocket), Overlay P2P network, Consensus mechanism (RCLConsensus), JobQueue for async task execution, PathFinding, Transaction Queue (TxQ), fee escalation (LoadManager), ledger acquisition, validator management, and existing observability infrastructure (PerfLog, Insight/StatsD, Journal logging).
|
||||
|
||||
Key trace points span across transaction submission via RPC, peer-to-peer message propagation, consensus round execution, ledger building, path computation, transaction queue behavior, fee escalation, and validator health. The implementation prioritizes high-value, low-risk components first: RPC handlers provide immediate value with minimal risk, while consensus tracing requires careful implementation to avoid timing impacts.
|
||||
|
||||
➡️ **[Read full Architecture Analysis](./01-architecture-analysis.md)**
|
||||
|
||||
---
|
||||
|
||||
## 2. Design Decisions
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **CNCF** = Cloud Native Computing Foundation
|
||||
|
||||
The OpenTelemetry C++ SDK is selected for its CNCF backing, active development, and native performance characteristics. Traces are exported via OTLP/HTTP to an OpenTelemetry Collector, which provides flexible routing and sampling. OTLP/gRPC is planned future work (see design decisions §2.2.2).
|
||||
|
||||
Span naming follows a hierarchical `<component>.<operation>` convention (e.g., `rpc.submit`, `tx.relay`, `consensus.round`). Context propagation uses W3C Trace Context headers for HTTP and embedded Protocol Buffer fields for P2P messages. The implementation coexists with existing PerfLog and Insight observability systems through correlation IDs.
|
||||
|
||||
**Data Collection & Privacy**: Telemetry collects only operational metadata (timing, counts, hashes) — never sensitive content (private keys, balances, amounts, raw payloads). Privacy protection includes account hashing, configurable redaction, sampling, and collector-level filtering. Node operators retain full control over telemetry configuration.
|
||||
|
||||
➡️ **[Read full Design Decisions](./02-design-decisions.md)**
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Strategy
|
||||
|
||||
The telemetry code is organized under `include/xrpl/telemetry/` for headers and `src/libxrpl/telemetry/` for implementation. Key principles include RAII-based span management via `SpanGuard` (with `discard()` for dropping unwanted spans), a `FilteringSpanProcessor` that intercepts `OnEnd()` to prevent discarded spans from entering the export pipeline, conditional compilation with `XRPL_ENABLE_TELEMETRY`, and minimal runtime overhead through batch processing and efficient sampling.
|
||||
|
||||
Performance optimization strategies include head sampling fixed at 100% (intentionally not configurable, so trace keep/drop decisions stay coherent across nodes), tail-based sampling at the collector for errors and slow traces to reduce volume, batch export to reduce network overhead, and conditional instrumentation that compiles to no-ops when disabled.
|
||||
|
||||
➡️ **[Read full Implementation Strategy](./03-implementation-strategy.md)**
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuration Reference
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring
|
||||
|
||||
Configuration is handled through the `[telemetry]` section in `xrpld.cfg` with options for enabling/disabling, exporter selection, endpoint configuration, and component-level filtering. Head sampling is fixed at 1.0 (not operator-configurable); volume reduction is done by tail sampling in the collector. CMake integration includes a `XRPL_ENABLE_TELEMETRY` option for compile-time control.
|
||||
|
||||
OpenTelemetry Collector configurations are provided for development and production (with tail-based sampling, Tempo, and Elastic APM). Docker Compose examples enable quick local development environment setup.
|
||||
|
||||
➡️ **[View full Configuration Reference](./05-configuration-reference.md)**
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Phases
|
||||
|
||||
The implementation spans 9 weeks across 5 phases:
|
||||
|
||||
| Phase | Duration | Focus | Key Deliverables |
|
||||
| ----- | --------- | ------------------- | --------------------------------------------------- |
|
||||
| 1 | Weeks 1-2 | Core Infrastructure | SDK integration, Telemetry interface, Configuration |
|
||||
| 2 | Weeks 3-4 | RPC Tracing | HTTP context extraction, Handler instrumentation |
|
||||
| 3 | Weeks 5-6 | Transaction Tracing | Protocol Buffer context, Relay propagation |
|
||||
| 4 | Weeks 7-8 | Consensus Tracing | Round spans, Proposal/validation tracing |
|
||||
| 5 | Week 9 | Documentation | Runbook, Dashboards, Training |
|
||||
|
||||
**Total Effort**: 47 person-days (2 developers working in parallel)
|
||||
|
||||
➡️ **[View full Implementation Phases](./06-implementation-phases.md)**
|
||||
|
||||
---
|
||||
|
||||
## 7. Observability Backends
|
||||
|
||||
> **APM** = Application Performance Monitoring | **GCS** = Google Cloud Storage
|
||||
|
||||
Grafana Tempo is recommended for all environments due to its cost-effectiveness and Grafana integration, while Elastic APM is ideal for organizations with existing Elastic infrastructure.
|
||||
|
||||
The recommended production architecture uses a gateway collector pattern with regional collectors performing tail-based sampling, routing traces to multiple backends (Tempo for primary storage, Elastic for log correlation, S3/GCS for long-term archive).
|
||||
|
||||
➡️ **[View Observability Backend Recommendations](./07-observability-backends.md)**
|
||||
|
||||
---
|
||||
|
||||
## 8. Appendix
|
||||
|
||||
The appendix contains a glossary of OpenTelemetry and xrpld-specific terms, references to external documentation and specifications, version history for this implementation plan, and a complete document index.
|
||||
|
||||
➡️ **[View Appendix](./08-appendix.md)**
|
||||
|
||||
---
|
||||
|
||||
_This document provides a comprehensive implementation plan for integrating OpenTelemetry distributed tracing into the xrpld XRP Ledger node software. For detailed information on any section, follow the links to the corresponding sub-documents._
|
||||
@@ -1683,118 +1683,3 @@ validators.txt
|
||||
# set to ssl_verify to 0.
|
||||
[ssl_verify]
|
||||
1
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 11. Telemetry (OpenTelemetry Tracing)
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Enables distributed tracing via OpenTelemetry. Takes effect only if tracing
|
||||
# was compiled in with the Conan option `-o telemetry=True`; there is no CMake
|
||||
# option for it. See docs/build/telemetry.md.
|
||||
#
|
||||
# [telemetry]
|
||||
#
|
||||
# enabled=0
|
||||
#
|
||||
# Enable or disable telemetry at runtime. Default: 0 (disabled).
|
||||
# This key, use_tls and the trace_* keys take 0, 1, true or false, in
|
||||
# any case. Any other value is a startup error that names the key.
|
||||
#
|
||||
# service_name=xrpld
|
||||
#
|
||||
# OTel resource attribute `service.name`. Default: xrpld.
|
||||
# The node's network ID (from [network_id]) is automatically added
|
||||
# as the `xrpl.network.id` and `xrpl.network.type` resource attributes.
|
||||
#
|
||||
# service_instance_id=n9KabcDEF...
|
||||
#
|
||||
# OTel resource attribute `service.instance.id`, which tells one node's
|
||||
# telemetry apart from another's. Normally left unset: the node identity
|
||||
# is not known when telemetry is constructed, so the server fills this in
|
||||
# with its own Base58 node public key later during startup. Set it only
|
||||
# to pin a stable instance name of your own choosing; doing so suppresses
|
||||
# the node-public-key fallback.
|
||||
# Default: the node's Base58 public key.
|
||||
#
|
||||
# traces_endpoint=http://localhost:4318/v1/traces
|
||||
#
|
||||
# The OTLP/HTTP endpoint spans are exported to. The server sends trace
|
||||
# data as protobuf-encoded HTTP POST requests to this URL. The full URL
|
||||
# including the signal path is used verbatim; no other endpoint is
|
||||
# derived from it.
|
||||
# Default: http://localhost:4318/v1/traces.
|
||||
#
|
||||
# Head sampling is intentionally fixed at 1.0 (sample everything) and is
|
||||
# not configurable. A per-node sampling ratio would let nodes make
|
||||
# divergent keep/drop decisions for the same distributed trace, producing
|
||||
# broken/partial traces. A span with a remote parent is decided by the same
|
||||
# fixed ratio, not by the peer's sampled flag. Reduce volume at the collector
|
||||
# via tail sampling instead; for node-local post-hoc dropping use
|
||||
# SpanGuard::discard() in code.
|
||||
#
|
||||
# use_tls=0
|
||||
#
|
||||
# Whether to hand tls_ca_cert to the exporter as its CA bundle. TLS is
|
||||
# selected by the scheme of traces_endpoint, not by this key. So with
|
||||
# enabled=1, use_tls=1 requires traces_endpoint to start with https://
|
||||
# (lower case). Any other URL, including the default, makes xrpld fail
|
||||
# to start with a message that names traces_endpoint.
|
||||
# Default: 0 (no CA file is passed, so the exporter keeps its own
|
||||
# default trust store).
|
||||
#
|
||||
# tls_ca_cert=/etc/ssl/certs/otel-collector-ca.pem
|
||||
#
|
||||
# Path to a PEM CA bundle used to verify the collector's certificate.
|
||||
# Read only when use_tls=1, and passed to the exporter unchanged. The
|
||||
# path is not checked while the config is parsed, so a missing or
|
||||
# unreadable file surfaces as an export failure at runtime rather than
|
||||
# as a startup error.
|
||||
# Default: empty (no CA file).
|
||||
#
|
||||
# batch_size=512
|
||||
#
|
||||
# Maximum number of spans in one export request. A batch is sent once
|
||||
# this many spans have queued up, or once batch_delay_ms has elapsed,
|
||||
# whichever happens first. Must be at least 1 and must not exceed
|
||||
# max_queue_size. Default: 512.
|
||||
#
|
||||
# batch_delay_ms=5000
|
||||
#
|
||||
# Longest a queued span waits before its batch is exported, in
|
||||
# milliseconds. Lower it for fresher traces at the cost of more
|
||||
# export requests. Must be at least 1. Default: 5000 (5 seconds).
|
||||
#
|
||||
# max_queue_size=2048
|
||||
#
|
||||
# Maximum number of spans held in memory awaiting export. Spans are
|
||||
# dropped once the queue is full, so raise this if the collector is
|
||||
# slow or briefly unreachable. Must be at least 1 and at least as
|
||||
# large as batch_size. Default: 2048.
|
||||
#
|
||||
# trace_rpc=1
|
||||
#
|
||||
# Enable tracing for JSON-RPC and WebSocket API request handling —
|
||||
# command parsing, execution, and response serialization. Default: 1.
|
||||
#
|
||||
# trace_transactions=1
|
||||
#
|
||||
# Enable tracing for the transaction lifecycle — submission, validation,
|
||||
# application to ledgers, and final disposition. Default: 1.
|
||||
#
|
||||
# trace_consensus=1
|
||||
#
|
||||
# Enable tracing for the consensus round lifecycle — proposals,
|
||||
# validations, mode changes, and ledger acceptance. Default: 1.
|
||||
#
|
||||
# trace_peer=1
|
||||
#
|
||||
# Enable tracing for peer-to-peer protocol messages — overlay message
|
||||
# send/receive, peer handshakes, and routing. High volume; enabled
|
||||
# by default. Default: 1.
|
||||
#
|
||||
# trace_ledger=1
|
||||
#
|
||||
# Enable tracing for ledger close and accept operations — ledger
|
||||
# building, state hashing, and write-back to the node store. Default: 1.
|
||||
#
|
||||
@@ -224,31 +224,6 @@ target_link_libraries(
|
||||
xrpl.libxrpl.ledger
|
||||
)
|
||||
|
||||
# Telemetry module — OpenTelemetry distributed tracing support.
|
||||
# Sources: include/xrpl/telemetry/ (headers), src/libxrpl/telemetry/ (impl).
|
||||
# When telemetry=ON, links the Conan-provided umbrella target
|
||||
# opentelemetry-cpp::opentelemetry-cpp (individual component targets like
|
||||
# ::api, ::sdk are not available in the Conan package).
|
||||
add_module(xrpl telemetry)
|
||||
target_link_libraries(
|
||||
xrpl.libxrpl.telemetry
|
||||
PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.beast xrpl.libxrpl.config
|
||||
)
|
||||
if(telemetry)
|
||||
target_link_libraries(
|
||||
xrpl.libxrpl.telemetry
|
||||
PUBLIC opentelemetry-cpp::opentelemetry-cpp
|
||||
)
|
||||
# PUBLIC, so a parent project that adds this one with add_subdirectory()
|
||||
# and links this module sees the same class layouts it was built with.
|
||||
# CMakeLists.txt also sets the define for every target in this project.
|
||||
# Conan consumers get it from conanfile.py instead.
|
||||
target_compile_definitions(
|
||||
xrpl.libxrpl.telemetry
|
||||
PUBLIC XRPL_ENABLE_TELEMETRY
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(xrpl.libxrpl)
|
||||
set_target_properties(xrpl.libxrpl PROPERTIES OUTPUT_NAME xrpl)
|
||||
|
||||
@@ -283,7 +258,6 @@ target_link_modules(
|
||||
resource
|
||||
server
|
||||
shamap
|
||||
telemetry
|
||||
tx
|
||||
)
|
||||
|
||||
|
||||
-12
@@ -11,14 +11,11 @@
|
||||
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713",
|
||||
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
|
||||
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
|
||||
"opentelemetry-cpp/1.28.0#2cbf71db4e0e0535df20be305005cb2f%1785939947.292581",
|
||||
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
|
||||
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
|
||||
"nlohmann_json/3.11.3#45828be26eb619a2e04ca517bb7b828d%1701220705.259",
|
||||
"mpt-crypto/1.0.2#b313cef0c1a493eb970ad185b2e9bab7%1784285108.866483",
|
||||
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
|
||||
"libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744",
|
||||
"libcurl/8.21.0#8c26e59c04891ba3373ea3552e18f67f%1783067699.863",
|
||||
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
|
||||
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
|
||||
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
|
||||
@@ -38,15 +35,9 @@
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1782395692.540639",
|
||||
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
|
||||
"pkgconf/2.5.1#93c2051284cba1279494a43a4fcfeae2%1757684701.089",
|
||||
"opentelemetry-proto/1.7.0#ed6d5bd761bef0afb0ba09676420b9ea%1749461220.268",
|
||||
"ninja/1.13.2#c8c5dc2a52ed6e4e42a66d75b4717ceb%1764096931.974",
|
||||
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1782395690.33162",
|
||||
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
|
||||
"meson/1.10.2#9d2d10681fe7fe61c788c58626c89b25%1775558003.754",
|
||||
"m4/1.4.19#1727f439cf74e83826ec96d0b4904eee%1784541921.659",
|
||||
"libtool/2.4.7#14e7739cc128bc1623d2ed318008e47e%1755679003.847",
|
||||
"gnu-config/cci.20210814#466e9d4d7779e1c142443f7ea44b4284%1762363589.329",
|
||||
"cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091",
|
||||
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
|
||||
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
|
||||
@@ -76,9 +67,6 @@
|
||||
],
|
||||
"lz4/[>=1.9.4 <2]": [
|
||||
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab"
|
||||
],
|
||||
"protobuf/[>=4.25.3 <7]": [
|
||||
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81"
|
||||
]
|
||||
},
|
||||
"config_requires": []
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{# Read SANITIZERS first: the next line rebinds `os` from the module to the OS name. #}
|
||||
{% set sanitizers = os.getenv("SANITIZERS", "") %}
|
||||
{% set os = detect_api.detect_os() %}
|
||||
{% set arch = detect_api.detect_arch() %}
|
||||
{% set compiler, version, compiler_exe = detect_api.detect_default_compiler() %}
|
||||
@@ -51,17 +49,6 @@ tools.build:compiler_executables={'c':'{{ cc_exe }}','cpp':'{{ cxx_exe }}'}
|
||||
user.package:cppstd_version=23
|
||||
tools.info.package_id:confs+=["user.package:cppstd_version"]
|
||||
|
||||
{# opentelemetry-cpp is an external dependency: sanitizer reports from its own #}
|
||||
{# code are not ours to fix, so build it without sanitizer checks. #}
|
||||
{# TSan stays on, because it must see the library's atomics or it reports false races. #}
|
||||
{% if sanitizers and compiler != "msvc" %}
|
||||
{% set otel_sanitizer_flags = ["-fno-sanitize=all"] %}
|
||||
{% if "thread" in sanitizers %}
|
||||
{% set _ = otel_sanitizer_flags.append("-fsanitize=thread") %}
|
||||
{% endif %}
|
||||
opentelemetry-cpp/*:tools.build:cxxflags+={{ otel_sanitizer_flags }}
|
||||
{% endif %}
|
||||
|
||||
{% if os == "Macos" %}
|
||||
[buildenv]
|
||||
{# os.version adds -mmacosx-version-min to compiler command lines, #}
|
||||
@@ -71,15 +58,3 @@ opentelemetry-cpp/*:tools.build:cxxflags+={{ otel_sanitizer_flags }}
|
||||
{# Scoped to boost/* since it is the only gap. #}
|
||||
boost/*:MACOSX_DEPLOYMENT_TARGET={{ min_macos_version }}
|
||||
{% endif %}
|
||||
{% if os == "Windows" %}
|
||||
# opentelemetry-cpp's recipe removes the `shared` option on Windows and never
|
||||
# sets BUILD_SHARED_LIBS, so its upstream CMake defaults the protobuf-generated
|
||||
# `opentelemetry_proto` target to a DLL (opentelemetry_proto.dll). The rest of
|
||||
# the project links statically and nothing deploys that DLL next to the
|
||||
# executables, so the telemetry unit test fails to start with
|
||||
# STATUS_DLL_NOT_FOUND (0xC0000135). Force the dependency to build fully static
|
||||
# so no runtime DLL is produced. The conf is folded into the package id so a
|
||||
# fresh static binary is built instead of reusing a previously cached one.
|
||||
opentelemetry-cpp/*:tools.cmake.cmaketoolchain:extra_variables={"BUILD_SHARED_LIBS": "OFF"}
|
||||
opentelemetry-cpp/*:tools.info.package_id:confs+=["tools.cmake.cmaketoolchain:extra_variables"]
|
||||
{% endif %}
|
||||
@@ -26,7 +26,6 @@ class Xrpl(ConanFile):
|
||||
"rocksdb": [True, False],
|
||||
"shared": [True, False],
|
||||
"static": [True, False],
|
||||
"telemetry": [True, False],
|
||||
"tests": [True, False],
|
||||
"unity": [True, False],
|
||||
"xrpld": [True, False],
|
||||
@@ -62,7 +61,6 @@ class Xrpl(ConanFile):
|
||||
"rocksdb": True,
|
||||
"shared": False,
|
||||
"static": True,
|
||||
"telemetry": True,
|
||||
"tests": False,
|
||||
"unity": False,
|
||||
"xrpld": False,
|
||||
@@ -148,10 +146,6 @@ class Xrpl(ConanFile):
|
||||
self.requires("rocksdb/10.5.1")
|
||||
self.requires("secp256k1/0.7.1", transitive_headers=True)
|
||||
self.requires("sqlite3/3.53.0", force=True)
|
||||
# OpenTelemetry C++ SDK for distributed tracing (optional).
|
||||
# Provides OTLP/HTTP exporter, batch span processor, and trace API.
|
||||
if self.options.telemetry:
|
||||
self.requires("opentelemetry-cpp/1.28.0", transitive_headers=True)
|
||||
self.requires("xxhash/0.8.3", transitive_headers=True)
|
||||
|
||||
exports_sources = (
|
||||
@@ -193,7 +187,6 @@ class Xrpl(ConanFile):
|
||||
tc.variables["rocksdb"] = self.options.rocksdb
|
||||
tc.variables["BUILD_SHARED_LIBS"] = self.options.shared
|
||||
tc.variables["static"] = self.options.static
|
||||
tc.variables["telemetry"] = self.options.telemetry
|
||||
tc.variables["unity"] = self.options.unity
|
||||
tc.variables["xrpld"] = self.options.xrpld
|
||||
tc.generate()
|
||||
@@ -248,9 +241,3 @@ class Xrpl(ConanFile):
|
||||
]
|
||||
if self.options.rocksdb:
|
||||
libxrpl.requires.append("rocksdb::librocksdb")
|
||||
if self.options.telemetry:
|
||||
libxrpl.requires.append("opentelemetry-cpp::opentelemetry-cpp")
|
||||
# The public telemetry headers pick their class layout on this
|
||||
# define, so a consumer that does not see it compiles a different
|
||||
# SpanGuard than the one inside the library it links.
|
||||
libxrpl.defines.append("XRPL_ENABLE_TELEMETRY")
|
||||
@@ -1,86 +0,0 @@
|
||||
# Docker Compose stack for xrpld OpenTelemetry observability.
|
||||
#
|
||||
# Provides services for local development:
|
||||
# - otel-collector: receives OTLP traces from xrpld, batches and
|
||||
# forwards them to Tempo. Listens on ports 4317 (gRPC)
|
||||
# and 4318 (HTTP).
|
||||
# - tempo: Grafana Tempo tracing backend, queryable via Grafana Explore
|
||||
# on port 3000. Recommended for production (S3/GCS storage, TraceQL).
|
||||
# - grafana: dashboards on port 3000, pre-configured with Tempo
|
||||
# datasource.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker/telemetry/docker-compose.yml up -d
|
||||
#
|
||||
# Configure xrpld to export traces by adding to xrpld.cfg:
|
||||
# [telemetry]
|
||||
# enabled=1
|
||||
# traces_endpoint=http://localhost:4318/v1/traces
|
||||
|
||||
services:
|
||||
# OpenTelemetry Collector: receives spans from xrpld via OTLP protocol,
|
||||
# batches them for efficiency, and forwards to Tempo for storage.
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:0.158.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
# Published on the host loopback only. The receivers have no auth and no
|
||||
# TLS, so only processes on this host may reach them. Note this 127.0.0.1
|
||||
# is the HOST interface docker listens on; the container-side bind lives in
|
||||
# the collector config and is a separate choice. Upstream asks for a
|
||||
# specific interface rather than 0.0.0.0 on either side (CWE-1327):
|
||||
# https://opentelemetry.io/docs/security/config-best-practices/
|
||||
ports:
|
||||
- "127.0.0.1:4317:4317" # OTLP gRPC receiver
|
||||
- "127.0.0.1:4318:4318" # OTLP HTTP receiver (xrpld sends traces here)
|
||||
- "127.0.0.1:13133:13133" # Health check endpoint
|
||||
volumes:
|
||||
# Mount collector pipeline config (receivers → processors → exporters)
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
|
||||
depends_on:
|
||||
- tempo
|
||||
networks:
|
||||
- xrpld-telemetry
|
||||
|
||||
# Grafana Tempo: distributed tracing backend that stores and indexes
|
||||
# spans. Queryable via TraceQL in Grafana Explore.
|
||||
tempo:
|
||||
image: grafana/tempo:2.9.4
|
||||
command: ["-config.file=/etc/tempo.yaml"]
|
||||
ports:
|
||||
- "127.0.0.1:3200:3200" # Tempo HTTP API (health check, query)
|
||||
volumes:
|
||||
# Mount Tempo storage and ingestion config
|
||||
- ./tempo.yaml:/etc/tempo.yaml:ro
|
||||
# Persistent volume for trace data (WAL + blocks)
|
||||
- tempo-data:/var/tempo
|
||||
networks:
|
||||
- xrpld-telemetry
|
||||
|
||||
# Grafana: visualization UI with Tempo pre-configured as a datasource.
|
||||
# Anonymous admin access enabled for local development convenience.
|
||||
grafana:
|
||||
image: grafana/grafana:13.1.2
|
||||
environment:
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=true # No login required for local dev
|
||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin # Full access without auth
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000" # Grafana web UI
|
||||
volumes:
|
||||
# Auto-provision Tempo datasource and search filters on startup
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
depends_on:
|
||||
- tempo
|
||||
networks:
|
||||
- xrpld-telemetry
|
||||
|
||||
# Named volume for Tempo trace storage (WAL and compacted blocks).
|
||||
# Data persists across container restarts. Remove with:
|
||||
# docker compose -f docker/telemetry/docker-compose.yml down -v
|
||||
volumes:
|
||||
tempo-data:
|
||||
|
||||
# Isolated bridge network so services communicate by container name
|
||||
# (e.g., the collector reaches Tempo at http://tempo:4317).
|
||||
networks:
|
||||
xrpld-telemetry:
|
||||
driver: bridge
|
||||
@@ -1,91 +0,0 @@
|
||||
# Grafana datasource provisioning for Grafana Tempo.
|
||||
# Auto-configures Tempo as a trace data source on Grafana startup.
|
||||
# Access Grafana at http://localhost:3000, then use Explore -> Tempo
|
||||
# to browse xrpld traces using TraceQL.
|
||||
#
|
||||
# Search filters provide pre-configured dropdowns in the Explore UI.
|
||||
# Each phase adds filters for the span attributes it introduces.
|
||||
# Base filters — node identity, service, span name, status.
|
||||
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Tempo
|
||||
type: tempo
|
||||
access: proxy
|
||||
url: http://tempo:3200
|
||||
uid: tempo
|
||||
jsonData:
|
||||
nodeGraph:
|
||||
enabled: true
|
||||
# Service map and traces-to-metrics require a Prometheus datasource
|
||||
# (not included in this stack). These features are inactive until a
|
||||
# Prometheus service is added to docker-compose.yml.
|
||||
serviceMap:
|
||||
datasourceUid: prometheus
|
||||
tracesToMetrics:
|
||||
datasourceUid: prometheus
|
||||
spanStartTimeShift: "-1h"
|
||||
spanEndTimeShift: "1h"
|
||||
search:
|
||||
filters:
|
||||
# --- Node identification filters ---
|
||||
# service.name: logical service name (default: "xrpld").
|
||||
# Useful when running multiple service types in the same collector.
|
||||
- id: service-name
|
||||
tag: service.name
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# service.instance.id: unique node identifier — defaults to the
|
||||
# node's public key (e.g., nHB1X37...). Distinguishes individual
|
||||
# nodes in a multi-node cluster or network.
|
||||
- id: node-id
|
||||
tag: service.instance.id
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# service.version: xrpld build version (e.g., "2.4.0-b1").
|
||||
# Filter traces from specific software releases.
|
||||
- id: node-version
|
||||
tag: service.version
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# xrpl.network.id: numeric network identifier
|
||||
# (0 = mainnet, 1 = testnet, 2 = devnet, etc.).
|
||||
# Derived from the [network_id] config section.
|
||||
- id: network-id
|
||||
tag: xrpl.network.id
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# xrpl.network.type: human-readable network name derived from
|
||||
# network ID ("mainnet", "testnet", "devnet", "unknown").
|
||||
- id: network-type
|
||||
tag: xrpl.network.type
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# --- Span intrinsic filters ---
|
||||
# name: the span operation name (e.g., "rpc.command.server_info").
|
||||
# Use to find traces for a specific RPC command or subsystem.
|
||||
- id: span-name
|
||||
tag: name
|
||||
operator: "="
|
||||
scope: intrinsic
|
||||
type: dynamic
|
||||
# status: span completion status ("ok", "error", "unset").
|
||||
# Filter for failed operations to diagnose errors.
|
||||
- id: span-status
|
||||
tag: status
|
||||
operator: "="
|
||||
scope: intrinsic
|
||||
type: dynamic
|
||||
# duration: span wall-clock duration. Use with ">" operator
|
||||
# to find slow operations (e.g., duration > 500ms).
|
||||
- id: span-duration
|
||||
tag: duration
|
||||
operator: ">"
|
||||
scope: intrinsic
|
||||
type: dynamic
|
||||
@@ -1,70 +0,0 @@
|
||||
# OpenTelemetry Collector configuration for xrpld development.
|
||||
#
|
||||
# Pipeline: OTLP receiver -> batch processor -> debug + Tempo.
|
||||
# xrpld sends traces via OTLP/HTTP to port 4318. The collector batches
|
||||
# them and forwards to Tempo via OTLP/gRPC on the Docker network. Tempo
|
||||
# is queryable via Grafana Explore using TraceQL.
|
||||
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
|
||||
processors:
|
||||
batch:
|
||||
timeout: 1s
|
||||
send_batch_size: 100
|
||||
# Deployment-tier tagging. Each collector serves ONE environment and ONE
|
||||
# network, so it stamps both onto every signal it forwards. This lets a
|
||||
# single Grafana stack hold data from many collectors and filter by tier.
|
||||
# - deployment.environment: the collector IS the environment (local, ci,
|
||||
# test, prod), so it is authoritative -> upsert (overwrite).
|
||||
# - xrpl.network.type: the xrpld node knows its own chain and already
|
||||
# stamps this, so the collector only fills it when absent -> insert.
|
||||
# This keeps a node's real network (e.g. a local node on mainnet)
|
||||
# from being overwritten by a collector's default.
|
||||
# Replace the placeholder values per collector; see docker/telemetry
|
||||
# tier examples.
|
||||
resource/tier:
|
||||
attributes:
|
||||
- key: deployment.environment
|
||||
value: local
|
||||
action: upsert
|
||||
- key: xrpl.network.type
|
||||
value: mainnet
|
||||
action: insert
|
||||
# Strip SDK-injected resource attributes (telemetry.sdk.language/name/version).
|
||||
# The OpenTelemetry SDK auto-adds these to every Resource; they carry no
|
||||
# operational value and clutter the attribute set on every backend, so drop
|
||||
# them here for all signals.
|
||||
resource/stripsdk:
|
||||
attributes:
|
||||
- key: telemetry.sdk.language
|
||||
action: delete
|
||||
- key: telemetry.sdk.name
|
||||
action: delete
|
||||
- key: telemetry.sdk.version
|
||||
action: delete
|
||||
|
||||
exporters:
|
||||
debug:
|
||||
verbosity: detailed
|
||||
otlp_grpc/tempo:
|
||||
endpoint: tempo:4317
|
||||
tls:
|
||||
insecure: true
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
|
||||
service:
|
||||
extensions: [health_check]
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [resource/tier, resource/stripsdk, batch]
|
||||
exporters: [debug, otlp_grpc/tempo]
|
||||
@@ -1,61 +0,0 @@
|
||||
# Grafana Tempo configuration for xrpld telemetry stack.
|
||||
#
|
||||
# Runs in single-binary mode for local development.
|
||||
# Receives traces via OTLP/gRPC from the OTel Collector and stores
|
||||
# them locally. Queryable via Grafana Explore using the Tempo datasource.
|
||||
#
|
||||
# Search filters are configured on the Grafana datasource side
|
||||
# (grafana/provisioning/datasources/tempo.yaml). Tempo auto-indexes
|
||||
# all span attributes for search in single-binary mode.
|
||||
#
|
||||
# For production, replace local storage with S3/GCS backend and adjust
|
||||
# retention via the compactor settings. See:
|
||||
# https://grafana.com/docs/tempo/latest/configuration/
|
||||
|
||||
stream_over_http_enabled: true
|
||||
|
||||
server:
|
||||
http_listen_port: 3200
|
||||
|
||||
distributor:
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
|
||||
ingester:
|
||||
max_block_duration: 5m
|
||||
|
||||
compactor:
|
||||
compaction:
|
||||
block_retention: 1h
|
||||
|
||||
# Enable metrics generator for service graph and span metrics.
|
||||
# Produces RED metrics (rate, errors, duration) per service/span,
|
||||
# feeding Grafana's service map visualization.
|
||||
metrics_generator:
|
||||
registry:
|
||||
external_labels:
|
||||
source: tempo
|
||||
storage:
|
||||
path: /var/tempo/generator/wal
|
||||
# Uncomment and add a Prometheus service to docker-compose.yml
|
||||
# to enable remote_write for service graph metrics:
|
||||
# remote_write:
|
||||
# - url: http://prometheus:9090/api/v1/write
|
||||
|
||||
overrides:
|
||||
defaults:
|
||||
metrics_generator:
|
||||
processors:
|
||||
- service-graphs
|
||||
- span-metrics
|
||||
|
||||
storage:
|
||||
trace:
|
||||
backend: local
|
||||
wal:
|
||||
path: /var/tempo/wal
|
||||
local:
|
||||
path: /var/tempo/blocks
|
||||
Vendored
-269
@@ -1,269 +0,0 @@
|
||||
# OpenTelemetry Tracing for xrpld
|
||||
|
||||
This document explains how to build xrpld with OpenTelemetry distributed tracing support, configure the runtime telemetry options, and set up the observability backend to view traces.
|
||||
|
||||
- [OpenTelemetry Tracing for xrpld](#opentelemetry-tracing-for-xrpld)
|
||||
- [Overview](#overview)
|
||||
- [Building with Telemetry](#building-with-telemetry)
|
||||
- [Summary](#summary)
|
||||
- [Build steps](#build-steps)
|
||||
- [Install dependencies](#install-dependencies)
|
||||
- [Call CMake](#call-cmake)
|
||||
- [Build](#build)
|
||||
- [Building without telemetry](#building-without-telemetry)
|
||||
- [Viewing traces locally](#viewing-traces-locally)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Conan lockfile error](#conan-lockfile-error)
|
||||
- [CMake target not found](#cmake-target-not-found)
|
||||
- [No traces in Grafana](#no-traces-in-grafana)
|
||||
- [Conditional compilation](#conditional-compilation)
|
||||
- [Span lifetime and cross-thread handling](#span-lifetime-and-cross-thread-handling)
|
||||
- [`SpanGuard` versus `ScopedSpanGuard`](#spanguard-versus-scopedspanguard)
|
||||
- [Coroutine-aware context storage](#coroutine-aware-context-storage)
|
||||
- [Handing a span to a job](#handing-a-span-to-a-job)
|
||||
- [Why are unrelated spans in my trace?](#why-are-unrelated-spans-in-my-trace)
|
||||
|
||||
## Overview
|
||||
|
||||
xrpld supports optional [OpenTelemetry](https://opentelemetry.io/) distributed tracing.
|
||||
When enabled, it instruments RPC requests with trace spans that are exported via
|
||||
OTLP/HTTP to an OpenTelemetry Collector, which forwards them to a tracing backend
|
||||
such as Grafana Tempo.
|
||||
|
||||
Telemetry is gated twice — once at compile time and once at runtime:
|
||||
|
||||
- **Compile time**: The Conan option `telemetry` must be `True`. It is the only switch: there is no CMake option, because `conan install` writes the value into the generated toolchain and CMake reads it from there.
|
||||
When disabled, all `SpanGuard` calls compile to inline no-ops (defined in `SpanGuard.h`)
|
||||
with zero overhead — no OTel SDK dependency required.
|
||||
- **Runtime**: The `[telemetry]` config section must set `enabled=1`.
|
||||
When disabled at runtime, a no-op implementation is used.
|
||||
|
||||
## Building with Telemetry
|
||||
|
||||
### Summary
|
||||
|
||||
Follow the same instructions as mentioned in [BUILD.md](../../BUILD.md) but with the following changes:
|
||||
|
||||
1. Pass `-o telemetry=True` to `conan install` to pull the `opentelemetry-cpp` dependency.
|
||||
2. CMake will automatically pick up `telemetry=ON` from the Conan-generated toolchain.
|
||||
3. Build as usual.
|
||||
|
||||
---
|
||||
|
||||
### Build steps
|
||||
|
||||
```bash
|
||||
cd /path/to/xrpld
|
||||
rm -rf .build
|
||||
mkdir .build
|
||||
cd .build
|
||||
```
|
||||
|
||||
#### Install dependencies
|
||||
|
||||
The `telemetry` option adds `opentelemetry-cpp/1.28.0` as a dependency.
|
||||
If the Conan lockfile does not yet include this package, bypass it with `--lockfile=""`.
|
||||
|
||||
```bash
|
||||
conan install .. \
|
||||
--output-folder . \
|
||||
--build missing \
|
||||
--settings build_type=Debug \
|
||||
-o telemetry=True \
|
||||
-o tests=True \
|
||||
-o xrpld=True \
|
||||
--lockfile=""
|
||||
```
|
||||
|
||||
> **Note**: The first build with telemetry may take longer as `opentelemetry-cpp`
|
||||
> and its transitive dependencies are compiled from source.
|
||||
|
||||
#### Call CMake
|
||||
|
||||
The Conan-generated toolchain file sets `telemetry=ON` automatically.
|
||||
No additional CMake flags are needed beyond the standard ones.
|
||||
|
||||
```bash
|
||||
cmake .. -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-Dtests=ON -Dxrpld=ON
|
||||
```
|
||||
|
||||
You should see in the CMake output:
|
||||
|
||||
```
|
||||
-- OpenTelemetry tracing enabled
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
cmake --build . --parallel $(nproc)
|
||||
```
|
||||
|
||||
## Building without telemetry
|
||||
|
||||
Pass `-o telemetry=False` to `conan install`.
|
||||
Omitting the option is not enough. It then resolves to whatever the recipe's current default is.
|
||||
The `opentelemetry-cpp` dependency will not be downloaded,
|
||||
the `XRPL_ENABLE_TELEMETRY` preprocessor define will not be set,
|
||||
and all tracing macros will compile to no-ops.
|
||||
The resulting binary is identical to one built before telemetry support was added.
|
||||
|
||||
## Viewing traces locally
|
||||
|
||||
[`docker/telemetry/docker-compose.yml`](../../docker/telemetry/docker-compose.yml) runs a local backend: an OpenTelemetry Collector, Grafana Tempo and Grafana. Its header comment lists each service and port.
|
||||
|
||||
1. Build xrpld with telemetry, as in [Building with Telemetry](#building-with-telemetry).
|
||||
2. From the repository root, start the stack: `docker compose -f docker/telemetry/docker-compose.yml up -d`.
|
||||
3. Set `enabled=1` in the `[telemetry]` section of your xrpld config. The default `traces_endpoint` already points at this collector, so no other key is needed. Section 11 of [`cfg/xrpld-example.cfg`](../../cfg/xrpld-example.cfg) documents every key.
|
||||
4. Start xrpld.
|
||||
5. Open Grafana at `http://localhost:3000` (no login), go to **Explore**, pick the **Tempo** data source, and search for `service.name` = `xrpld`.
|
||||
|
||||
Spans are exported in batches, so a span reaches Tempo a few seconds after the work it records.
|
||||
|
||||
To stop the stack, run `docker compose -f docker/telemetry/docker-compose.yml down`. Add `-v` to also delete the stored traces.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Conan lockfile error
|
||||
|
||||
If you see `ERROR: Requirement 'opentelemetry-cpp/1.28.0' not in lockfile 'requires'`,
|
||||
the lockfile was generated without the telemetry dependency.
|
||||
Pass `--lockfile=""` to bypass the lockfile, or regenerate it with telemetry enabled.
|
||||
|
||||
### CMake target not found
|
||||
|
||||
If CMake reports that `opentelemetry-cpp` targets are not found,
|
||||
ensure you ran `conan install` with `-o telemetry=True` and that the
|
||||
Conan-generated toolchain file is being used.
|
||||
The Conan package provides a single umbrella target
|
||||
`opentelemetry-cpp::opentelemetry-cpp` (not individual component targets).
|
||||
|
||||
### No traces in Grafana
|
||||
|
||||
Check each hop in order:
|
||||
|
||||
1. The CMake output shows `-- OpenTelemetry tracing enabled`. If it does not, tracing is not compiled in.
|
||||
2. The xrpld log shows `Telemetry started successfully` from the `Telemetry` partition at `info` level. If it does not, `enabled=1` is not set.
|
||||
3. `docker compose -f docker/telemetry/docker-compose.yml logs otel-collector` prints every span the collector receives. If spans appear there but not in Grafana, the fault is between the collector and Tempo. If they do not appear, xrpld cannot reach `traces_endpoint`.
|
||||
|
||||
## Conditional compilation
|
||||
|
||||
All OpenTelemetry SDK types are hidden behind the pimpl idiom in `SpanGuard.cpp`. When `XRPL_ENABLE_TELEMETRY` is not defined, `SpanGuard.h` provides an all-inline no-op stub class with no OTel dependencies. At runtime, if `enabled=0` is set in config (or the section is omitted), a `NullTelemetry` implementation is used that returns no-op spans.
|
||||
|
||||
Those two layers remove the span, but they do **not** remove the work that computes what you pass to it. The compiled-out guards are ordinary inline functions with ordinary parameters, so every argument is evaluated before the empty body is entered:
|
||||
|
||||
```cpp
|
||||
// to_string() allocates a 64-character string even in a build with telemetry
|
||||
// compiled out. The call then does nothing with it.
|
||||
span.setAttribute(attr::txHash, to_string(txID).c_str());
|
||||
```
|
||||
|
||||
Guard the work, not just the call. Testing the guard is enough: its `operator bool()` is a literal `false` when telemetry is compiled out, so the whole block is eliminated, and when telemetry is compiled in it also skips the work if tracing is switched off in config or the span's category is disabled.
|
||||
|
||||
```cpp
|
||||
if (span)
|
||||
span.setAttribute(attr::txHash, to_string(txID).c_str());
|
||||
```
|
||||
|
||||
A span that exists but was sampled out still pays: there is no `isRecording()` to test.
|
||||
|
||||
The `XRPL_METRIC_*` macros are the opposite case. They expand to `do { } while (false)` and discard their arguments, so anything named only inside a macro argument list disappears on its own and needs no guard.
|
||||
|
||||
## Span lifetime and cross-thread handling
|
||||
|
||||
Telemetry exposes two RAII guards with split responsibilities, plus a
|
||||
non-owning activation helper. Picking the right one is what keeps a trace's
|
||||
parent/child nesting and its per-line log correlation correct.
|
||||
|
||||
### `SpanGuard` versus `ScopedSpanGuard`
|
||||
|
||||
- **`SpanGuard`** owns a span and nothing else. It is _thread-free_: it never
|
||||
touches the active-context stack, so it carries no thread affinity and may be
|
||||
moved to and ended on any thread. It is movable and move-assignable. Create
|
||||
one with `SpanGuard::span(cat, prefix, name)`, or with
|
||||
`SpanGuard::freshRoot(...)` to start a fresh trace root that ignores whatever
|
||||
span is currently ambient. Reach for a plain `SpanGuard` whenever the span
|
||||
must leave the context store that created it — for example when it is handed
|
||||
into a job.
|
||||
|
||||
- **`ScopedSpanGuard`** owns a `SpanGuard` _plus_ an active OTel scope that
|
||||
pushes the span onto the current context store. While it lives, the span is
|
||||
the ambient parent for child spans created on that store, and log lines
|
||||
emitted under it carry its `trace_id`. It is non-copyable and non-movable —
|
||||
short-lived stack RAII. It offers the same factories (`freshRoot(...)`,
|
||||
`childSpan(...)`). When the span must outlive the scope, convert it with
|
||||
`operator SpanGuard() &&`: that pops the scope on the origin store and yields
|
||||
the bare, thread-free `SpanGuard`.
|
||||
|
||||
Rule of thumb: use `ScopedSpanGuard` for same-thread (or same-coroutine)
|
||||
nesting and log correlation; use the plain `SpanGuard` whenever the span
|
||||
crosses out of the store that created it.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
SG["SpanGuard<br/>(unscoped, thread-free)<br/>owns span only; movable across threads and coroutines"]
|
||||
SSG["ScopedSpanGuard<br/>(scoped, store-bound)<br/>owns a SpanGuard plus an active scope on the current store"]
|
||||
SA["ScopedActivation<br/>(non-owning)<br/>activates a borrowed span; never owns or ends it"]
|
||||
|
||||
SSG -->|"handoff: pop scope, yield bare span"| SG
|
||||
SG -->|"activate / activateIfLive"| SA
|
||||
SA -.->|"borrows span, no ownership"| SG
|
||||
|
||||
classDef box fill:#e8f0fe,stroke:#3b5bdb,color:#111827;
|
||||
class SG,SSG,SA box;
|
||||
```
|
||||
|
||||
### Coroutine-aware context storage
|
||||
|
||||
The active-context stack is not a plain `thread_local`. At telemetry start
|
||||
xrpld installs `CoroAwareContextStorage`, which keeps the stack in an
|
||||
`xrpl::LocalValue`. Because `JobQueue::Coro::resume()` swaps the coroutine's
|
||||
`LocalValue` store in and out with the coroutine, the ambient context _follows
|
||||
the coroutine_ across every yield and resume — even when it resumes on a
|
||||
different worker thread. A `ScopedSpanGuard` held across a coroutine yield is
|
||||
therefore safe: its scope rides the coroutine and pops on the same store it was
|
||||
pushed onto, so it never pops the wrong stack. Off a coroutine the `LocalValue`
|
||||
transparently gives each thread its own store, so behaviour matches OTel's
|
||||
default thread-local storage. This is what lets the RPC entry, process, and
|
||||
command spans be scoped — for correct nesting and per-line log-trace
|
||||
correlation — even though the RPC path yields.
|
||||
|
||||
### Handing a span to a job
|
||||
|
||||
The hand-off pattern is: create a thread-free `SpanGuard` at the origin (or
|
||||
convert a `ScopedSpanGuard` with `operator SpanGuard() &&`), move it into the
|
||||
job closure, and inside the worker body activate it non-destructively with
|
||||
`telemetry::activateIfLive(handle)`. That call takes no ownership and returns a
|
||||
`ScopedActivation` (a no-op if the handle is empty or the span inactive) which
|
||||
makes the span the ambient context so log lines in the worker body carry its
|
||||
`trace_id`. The activation neither owns nor ends the span — the owning
|
||||
`SpanGuard` still controls its lifetime and ends it when the closure is
|
||||
destroyed. Keep the activation confined to a synchronous, non-yielding block.
|
||||
There is no detach step: a `SpanGuard` is already thread-free.
|
||||
|
||||
### Why are unrelated spans in my trace?
|
||||
|
||||
Historically a scoped guard destroyed off its origin thread popped the wrong
|
||||
context stack, leaving a stale ambient span in place that later work inherited.
|
||||
The current design removes that failure mode in two ways:
|
||||
|
||||
- **Coroutine-aware storage** makes a scope held across a coroutine yield pop on
|
||||
the same store it was pushed onto, so a coroutine that resumes on another
|
||||
worker never pops the wrong stack.
|
||||
|
||||
- **A same-store assertion** in `ScopedSpanGuard` (and `ScopedActivation`)
|
||||
records the `LocalValue` store its scope was pushed onto and checks, in
|
||||
debug/test/fuzzing builds, that destruction, hand-off, and discard all happen
|
||||
while that same store is active — turning a genuine cross-store misuse into an
|
||||
immediate assertion failure rather than a silently corrupted trace.
|
||||
|
||||
If a trace still shows unrelated spans nested under one operation, the usual
|
||||
cause is an inbound entry point that inherited an ambient parent it should not
|
||||
have. Start such an operation with `freshRoot()` so it begins a clean trace
|
||||
root and never adopts whatever span happened to be active. To move a span
|
||||
across a store boundary, keep it in a thread-free `SpanGuard` (or convert via
|
||||
`operator SpanGuard() &&`) rather than holding a `ScopedSpanGuard` across the
|
||||
boundary.
|
||||
@@ -63,7 +63,6 @@ struct Sections
|
||||
static constexpr auto kSslVerifyDir = "ssl_verify_dir";
|
||||
static constexpr auto kSslVerifyFile = "ssl_verify_file";
|
||||
static constexpr auto kSweepInterval = "sweep_interval";
|
||||
static constexpr auto kTelemetry = "telemetry";
|
||||
static constexpr auto kTransactionQueue = "transaction_queue";
|
||||
static constexpr auto kValidationSeed = "validation_seed";
|
||||
static constexpr auto kValidatorKeys = "validator_keys";
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -211,7 +212,7 @@ canWithdraw(
|
||||
ReadView const& view,
|
||||
AccountID const& from,
|
||||
AccountID const& to,
|
||||
SLE::ConstRef toSle,
|
||||
AccountRootEntryR const& toSle,
|
||||
STAmount const& amount,
|
||||
bool hasDestinationTag,
|
||||
std::optional<std::vector<UInt256>> const& credentialIDs = std::nullopt);
|
||||
|
||||
@@ -7,9 +7,20 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
struct Adjustment
|
||||
{
|
||||
std::int32_t ownerCountDelta = 0;
|
||||
std::int32_t accountCountDelta = 0;
|
||||
};
|
||||
|
||||
template <typename ViewT>
|
||||
class AccountRootEntry : public SLEBase<ViewT, ltACCOUNT_ROOT>
|
||||
{
|
||||
@@ -27,6 +38,123 @@ public:
|
||||
: Base(keylet::account(id), view, j)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account reserve, in drops.
|
||||
*
|
||||
* Actual owner count can be adjusted by delta in ownerCountAdj
|
||||
* Actual reserve count can be adjusted by delta in accountCountAdj
|
||||
* The reserve is calculated as:
|
||||
* (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) *
|
||||
* increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base"
|
||||
*
|
||||
* @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative
|
||||
* to subtract.
|
||||
* @return The account reserve amount in drops
|
||||
*/
|
||||
[[nodiscard]] XRPAmount
|
||||
reserve(Adjustment adj = {}) const;
|
||||
|
||||
/**
|
||||
* Return number of the objects which reserve is covered by the account (so called "owner
|
||||
* count"). Actual owner count can be adjusted by delta in ownerCountAdj.
|
||||
*
|
||||
* @param ownerCountAdj Adjustment to the owner count (default: 0)
|
||||
* @return The adjusted owner count
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
ownerCount(std::int32_t ownerCountAdj = 0) const;
|
||||
|
||||
/**
|
||||
* Check if this account has sufficient reserve.
|
||||
*
|
||||
* @param ctx The apply-view context (view + tx)
|
||||
* @param accBalance The account's balance
|
||||
* @param sponsorSle The sponsor's ledger entry (if applicable)
|
||||
* @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative
|
||||
* to subtract.
|
||||
* @param insufReserveCode The transaction result code to return if the reserve is
|
||||
* insufficient (default: tecINSUFFICIENT_RESERVE).
|
||||
* @return Transaction result code
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkReserve(
|
||||
ApplyViewContext ctx,
|
||||
XRPAmount accBalance,
|
||||
std::optional<AccountRootEntry<ReadView>> const& sponsorSle,
|
||||
Adjustment adj,
|
||||
TER insufReserveCode = tecINSUFFICIENT_RESERVE) const;
|
||||
|
||||
/**
|
||||
* Check if this account has sufficient reserve, deriving the sponsor internally.
|
||||
*
|
||||
* Equivalent to the overload above, but resolves the sponsor via
|
||||
* getEffectiveTxReserveSponsor(ctx, *this) instead of taking it explicitly. Use this
|
||||
* in the common case where the sponsor is simply the transaction's reserve
|
||||
* sponsor for this account. Callers that must force the account's-own-reserve branch
|
||||
* (passing a null sponsor) or supply a different sponsor should use the
|
||||
* explicit overload above.
|
||||
*
|
||||
* @param ctx The apply-view context (view + tx)
|
||||
* @param accBalance The account's balance
|
||||
* @param adj Reserve adjustments (owner/account count deltas)
|
||||
* @return Transaction result code
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkReserve(ApplyViewContext ctx, XRPAmount accBalance, Adjustment adj) const;
|
||||
|
||||
/**
|
||||
* Increase owner-count fields when the caller supplies the sponsor.
|
||||
*
|
||||
* This helper does not create a ledger object. It updates reserve accounting
|
||||
* after the caller has created/updated an object.
|
||||
* If sponsorSle is provided, this also adjusts the account's sponsored count
|
||||
* and the sponsor's sponsoring count.
|
||||
*
|
||||
* @param sponsorSle The sponsor's ledger entry (if applicable)
|
||||
* @param count Amount to add to the owner count
|
||||
*/
|
||||
void
|
||||
increaseOwnerCount(std::optional<AccountRootEntry<ApplyView>>& sponsorSle, std::uint32_t count)
|
||||
requires Base::kIsWritable;
|
||||
|
||||
/**
|
||||
* Increase owner-count fields, deriving the tx reserve sponsor internally.
|
||||
*
|
||||
* Equivalent to the overload above, but resolves the sponsor via
|
||||
* getEffectiveTxReserveSponsor(ctx, *this) instead of taking it explicitly. Use
|
||||
* this when the sponsor is the transaction's reserve sponsor for this account
|
||||
* (the common create path). Deletion paths, which derive the sponsor from an
|
||||
* object's sfSponsor field, should keep using the explicit overload.
|
||||
*
|
||||
* @param ctx The apply-view context (view + tx)
|
||||
* @param count Amount to add to the owner count
|
||||
*/
|
||||
void
|
||||
increaseOwnerCount(ApplyViewContext ctx, std::uint32_t count)
|
||||
requires Base::kIsWritable;
|
||||
|
||||
/**
|
||||
* Decrease owner-count fields when the caller supplies the sponsor.
|
||||
*
|
||||
* This helper does not delete a ledger object. It updates reserve accounting
|
||||
* after the caller has removed an owner-counted reserve, or for special
|
||||
* owner-count changes whose sponsor cannot be derived from an object's
|
||||
* sfSponsor field.
|
||||
*
|
||||
* @param sponsorSle The sponsor's ledger entry (if applicable)
|
||||
* @param count Amount to remove from the owner count
|
||||
*/
|
||||
void
|
||||
decreaseOwnerCount(std::optional<AccountRootEntry<ApplyView>>& sponsorSle, std::uint32_t count)
|
||||
requires Base::kIsWritable;
|
||||
|
||||
private:
|
||||
void
|
||||
adjustOwnerCountSigned(
|
||||
std::optional<AccountRootEntry<ApplyView>>& sponsorSle,
|
||||
std::int32_t adjustment)
|
||||
requires Base::kIsWritable;
|
||||
};
|
||||
|
||||
using AccountRootEntryR = AccountRootEntry<ReadView>;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -46,30 +46,55 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer);
|
||||
[[nodiscard]] XRPAmount
|
||||
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j);
|
||||
|
||||
struct Adjustment
|
||||
{
|
||||
std::int32_t ownerCountDelta = 0;
|
||||
std::int32_t accountCountDelta = 0;
|
||||
};
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Returns the account reserve, in drops.
|
||||
* An owner count cannot be negative. If adjustment would cause a negative
|
||||
* owner count, clamp the owner count at 0. Similarly for overflow. This
|
||||
* adjustment allows the ownerCount to be adjusted up or down in multiple steps.
|
||||
* If id != std::nullopt, then do error reporting.
|
||||
*
|
||||
* Actual owner count can be adjusted by delta in ownerCountAdj
|
||||
* Actual reserve count can be adjusted by delta in accountCountAdj
|
||||
* The reserve is calculated as:
|
||||
* (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) *
|
||||
* increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base"
|
||||
* Shared by AccountRootEntry::ownerCount() and the owner-count helpers.
|
||||
*
|
||||
* @param view The ledger view to read from
|
||||
* @param sle The ledger entry for the account
|
||||
* @param currentOwnerCount The owner count before adjustment
|
||||
* @param ownerCountAdj Positive to add to count, negative to reduce count
|
||||
* @param id The account ID to use for error reporting, or std::nullopt to
|
||||
* skip error reporting
|
||||
* @param j Journal for logging
|
||||
* @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to
|
||||
* subtract.
|
||||
* @return The account reserve amount in drops
|
||||
* @return The adjusted owner count
|
||||
*/
|
||||
[[nodiscard]] XRPAmount
|
||||
accountReserve(ReadView const& view, SLE::ConstRef sle, beast::Journal j, Adjustment adj = {});
|
||||
std::uint32_t
|
||||
confineOwnerCount(
|
||||
std::uint32_t currentOwnerCount,
|
||||
std::int32_t ownerCountAdj,
|
||||
std::optional<AccountID> const& id = std::nullopt,
|
||||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()});
|
||||
|
||||
/**
|
||||
* Clamps and writes an owner-count field of sle, then updates sle in view.
|
||||
*
|
||||
* Shared by AccountRootEntry and adjustLoanBrokerOwnerCount().
|
||||
*
|
||||
* @return The adjusted owner count
|
||||
*/
|
||||
std::uint32_t
|
||||
adjustOwnerCountImpl(
|
||||
ApplyView& view,
|
||||
SLE::Ref sle,
|
||||
SF_UINT32 const& sfield,
|
||||
AccountID const& accID,
|
||||
std::int32_t ownerCountAdj,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Returns the number of account reserves funded by this account: 1 for itself
|
||||
* (0 if sponsored by another account) plus the count of accounts it sponsors.
|
||||
* Shared by AccountRootEntry::reserve() and xrpLiquid().
|
||||
*/
|
||||
std::uint32_t
|
||||
accountCountImpl(AccountRootEntryR const& sle, std::int32_t accountCountAdj, beast::Journal j);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Convenience overload that accepts AccountID instead of SLE.
|
||||
@@ -84,114 +109,9 @@ accountReserve(ReadView const& view, SLE::ConstRef sle, beast::Journal j, Adjust
|
||||
[[nodiscard]] inline XRPAmount
|
||||
accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adjustment adj = {})
|
||||
{
|
||||
return accountReserve(view, view.read(keylet::account(id)), j, adj);
|
||||
return AccountRootEntryR(id, view, j).reserve(adj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an account has sufficient reserve.
|
||||
*
|
||||
* @param view The ledger view to read from
|
||||
* @param tx The transaction being processed
|
||||
* @param accSle The account's ledger entry
|
||||
* @param accBalance The account's balance
|
||||
* @param sponsorSle The sponsor's ledger entry (if applicable)
|
||||
* @param adj Adjustment to the owner/account count (default: 0/0). Positive to add, negative to
|
||||
* subtract.
|
||||
* @param j Journal for logging (default: null sink)
|
||||
* @param insufReserveCode The transaction result code to return if the reserve is insufficient
|
||||
* (default: tecINSUFFICIENT_RESERVE).
|
||||
* @return Transaction result code
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkReserve(
|
||||
ApplyViewContext ctx,
|
||||
SLE::ConstRef accSle,
|
||||
XRPAmount accBalance,
|
||||
SLE::ConstRef sponsorSle,
|
||||
Adjustment adj,
|
||||
beast::Journal j,
|
||||
TER insufReserveCode = tecINSUFFICIENT_RESERVE);
|
||||
|
||||
/**
|
||||
* Check if an account has sufficient reserve, deriving the sponsor internally.
|
||||
*
|
||||
* Equivalent to the overload above, but resolves the sponsor via
|
||||
* getEffectiveTxReserveSponsor(ctx, accSle) instead of taking it explicitly. Use this
|
||||
* in the common case where the sponsor is simply the transaction's reserve
|
||||
* sponsor for accSle. Callers that must force the account's-own-reserve branch
|
||||
* (passing a null sponsor) or supply a different sponsor should use the
|
||||
* explicit overload above.
|
||||
*
|
||||
* @param ctx The apply-view context (view + tx)
|
||||
* @param accSle The account's ledger entry
|
||||
* @param accBalance The account's balance
|
||||
* @param adj Reserve adjustments (owner/account count deltas)
|
||||
* @param j Journal for logging (default: null sink)
|
||||
* @return Transaction result code
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkReserve(
|
||||
ApplyViewContext ctx,
|
||||
SLE::ConstRef accSle,
|
||||
XRPAmount accBalance,
|
||||
Adjustment adj,
|
||||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()});
|
||||
|
||||
/**
|
||||
* Return number of the objects which reserve is covered by the account(sle) (so called "owner
|
||||
* count"). Actual owner count can be adjusted by delta in ownerCountAdj.
|
||||
*
|
||||
* @param sle The account's ledger entry
|
||||
* @param j Journal for logging
|
||||
* @param ownerCountAdj Adjustment to the owner count (default: 0)
|
||||
* @return The adjusted owner count
|
||||
*/
|
||||
std::uint32_t
|
||||
ownerCount(SLE::ConstRef sle, beast::Journal j, std::int32_t ownerCountAdj = 0);
|
||||
|
||||
/**
|
||||
* Increase owner-count fields when the caller supplies the sponsor.
|
||||
*
|
||||
* This helper does not create a ledger object. It updates reserve accounting
|
||||
* after the caller has created/updated an object.
|
||||
* If sponsorSle is provided, this also adjusts the account's sponsored count
|
||||
* and the sponsor's sponsoring count.
|
||||
*
|
||||
* @param view The apply view for making changes
|
||||
* @param accountSle The account's ledger entry
|
||||
* @param sponsorSle The sponsor's ledger entry (if applicable)
|
||||
* @param count Amount to add to the owner count
|
||||
* @param j Journal for logging
|
||||
*/
|
||||
void
|
||||
increaseOwnerCount(
|
||||
ApplyView& view,
|
||||
SLE::Ref accountSle,
|
||||
SLE::Ref sponsorSle,
|
||||
std::uint32_t count,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Increase owner-count fields, deriving the tx reserve sponsor internally.
|
||||
*
|
||||
* Equivalent to the overload above, but resolves the sponsor via
|
||||
* getEffectiveTxReserveSponsor(ctx, accountSle) instead of taking it explicitly. Use
|
||||
* this when the sponsor is the transaction's reserve sponsor for accountSle
|
||||
* (the common create path). Deletion paths, which derive the sponsor from an
|
||||
* object's sfSponsor field, should keep using the explicit overload.
|
||||
*
|
||||
* @param ctx The apply-view context (view + tx)
|
||||
* @param accountSle The account's ledger entry
|
||||
* @param count Amount to add to the owner count
|
||||
* @param j Journal for logging
|
||||
*/
|
||||
void
|
||||
increaseOwnerCount(
|
||||
ApplyViewContext ctx,
|
||||
SLE::Ref accountSle,
|
||||
std::uint32_t count,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Convenience overload that accepts AccountID instead of SLE references.
|
||||
*
|
||||
@@ -209,36 +129,13 @@ increaseOwnerCount(
|
||||
std::uint32_t count,
|
||||
beast::Journal j)
|
||||
{
|
||||
increaseOwnerCount(
|
||||
view,
|
||||
view.peek(keylet::account(account)),
|
||||
sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(),
|
||||
count,
|
||||
j);
|
||||
AccountRootEntryW accountSle(account, view, j);
|
||||
std::optional<AccountRootEntryW> sponsorSle;
|
||||
if (sponsor)
|
||||
sponsorSle.emplace(*sponsor, view);
|
||||
accountSle.increaseOwnerCount(sponsorSle, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease owner-count fields when the caller supplies the sponsor.
|
||||
*
|
||||
* This helper does not delete a ledger object. It updates reserve accounting
|
||||
* after the caller has removed an owner-counted reserve, or for special
|
||||
* owner-count changes whose sponsor cannot be derived from an object's
|
||||
* sfSponsor field.
|
||||
*
|
||||
* @param view The apply view for making changes
|
||||
* @param accountSle The account's ledger entry
|
||||
* @param sponsorSle The sponsor's ledger entry (if applicable)
|
||||
* @param count Amount to remove from the owner count
|
||||
* @param j Journal for logging
|
||||
*/
|
||||
void
|
||||
decreaseOwnerCount(
|
||||
ApplyView& view,
|
||||
SLE::Ref accountSle,
|
||||
SLE::Ref sponsorSle,
|
||||
std::uint32_t count,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Convenience overload that accepts AccountID instead of SLE references.
|
||||
*
|
||||
@@ -256,12 +153,11 @@ decreaseOwnerCount(
|
||||
std::uint32_t count,
|
||||
beast::Journal j)
|
||||
{
|
||||
decreaseOwnerCount(
|
||||
view,
|
||||
view.peek(keylet::account(account)),
|
||||
sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(),
|
||||
count,
|
||||
j);
|
||||
AccountRootEntryW accountSle(account, view, j);
|
||||
std::optional<AccountRootEntryW> sponsorSle;
|
||||
if (sponsor)
|
||||
sponsorSle.emplace(*sponsor, view);
|
||||
accountSle.decreaseOwnerCount(sponsorSle, count);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,7 +177,7 @@ decreaseOwnerCount(
|
||||
void
|
||||
decreaseOwnerCountForObject(
|
||||
ApplyView& view,
|
||||
SLE::Ref accountSle,
|
||||
AccountRootEntryW& accountSle,
|
||||
SLE::Ref objectSle,
|
||||
std::uint32_t count,
|
||||
beast::Journal j);
|
||||
@@ -303,7 +199,7 @@ decreaseOwnerCountForObject(
|
||||
std::uint32_t count,
|
||||
beast::Journal j)
|
||||
{
|
||||
SLE::Ref accountSle = view.peek(keylet::account(account));
|
||||
AccountRootEntryW accountSle(account, view);
|
||||
decreaseOwnerCountForObject(view, accountSle, objectSle, count, j);
|
||||
}
|
||||
|
||||
@@ -367,7 +263,7 @@ getPseudoAccountFields();
|
||||
* - null pointer
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isPseudoAccount(SLE::const_pointer sleAcct);
|
||||
isPseudoAccount(AccountRootEntryR const& sleAcct);
|
||||
|
||||
/**
|
||||
* Convenience overload that reads the account from the view.
|
||||
@@ -375,7 +271,7 @@ isPseudoAccount(SLE::const_pointer sleAcct);
|
||||
[[nodiscard]] inline bool
|
||||
isPseudoAccount(ReadView const& view, AccountID const& accountId)
|
||||
{
|
||||
return isPseudoAccount(view.read(keylet::account(accountId)));
|
||||
return isPseudoAccount(AccountRootEntryR(accountId, view));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,6 +292,6 @@ createPseudoAccount(ApplyView& view, UInt256 const& pseudoOwnerKey, SField const
|
||||
* - If the SLE requires a destination tag, checks that there is a tag.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkDestinationAndTag(SLE::ConstRef toSle, bool hasDestinationTag);
|
||||
checkDestinationAndTag(AccountRootEntryR const& toSle, bool hasDestinationTag);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -27,6 +27,32 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Before fixCleanup3_2_0, EscrowCancel passes the escrow SLE as sleDest.
|
||||
* Return what the old reserve check returned for it, without adopting a
|
||||
* non-AccountRoot SLE into an AccountRootEntry: the tx reserve sponsor's
|
||||
* error if the escrow owner submitted the tx, else tefINTERNAL.
|
||||
*
|
||||
* @param ctx the apply view context for the escrow transaction.
|
||||
* @param sleDest the non-AccountRoot SLE that was passed as the destination.
|
||||
* @return the tx reserve sponsor's error code if the escrow owner submitted
|
||||
* the tx, else tefINTERNAL.
|
||||
*/
|
||||
inline TER
|
||||
escrowDestNotAccountResult(ApplyViewContext ctx, SLE::ConstRef sleDest)
|
||||
{
|
||||
if (sleDest->getAccountID(sfAccount) == ctx.tx[sfAccount])
|
||||
{
|
||||
if (auto const sponsor = getTxReserveSponsor(ctx); !sponsor)
|
||||
return sponsor.error(); // LCOV_EXCL_LINE
|
||||
}
|
||||
return tefINTERNAL;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <ValidIssueType T>
|
||||
TER
|
||||
escrowUnlockApplyHelper(
|
||||
@@ -70,18 +96,17 @@ escrowUnlockApplyHelper<Issue>(
|
||||
if (!ctx.view.exists(trustLineKey) && createAsset)
|
||||
{
|
||||
// Can the account cover the trust line's reserve?
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (sleDest->getType() != ltACCOUNT_ROOT)
|
||||
return detail::escrowDestNotAccountResult(ctx, sleDest);
|
||||
auto const destSle = AccountRootEntryR(sleDest, ctx.view, journal);
|
||||
auto sponsorSle = getEffectiveTxReserveSponsor(ctx, destSle);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx,
|
||||
sleDest,
|
||||
xrpBalance,
|
||||
*sponsorSle,
|
||||
{.ownerCountDelta = 1},
|
||||
journal,
|
||||
tecNO_LINE_INSUF_RESERVE);
|
||||
auto& sponsor = *sponsorSle;
|
||||
|
||||
if (auto const ret = destSle.checkReserve(
|
||||
ctx, xrpBalance, sponsor, {.ownerCountDelta = 1}, tecNO_LINE_INSUF_RESERVE);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
JLOG(journal.trace()) << "Trust line does not exist. "
|
||||
@@ -94,22 +119,22 @@ escrowUnlockApplyHelper<Issue>(
|
||||
initialBalance.get<Issue>().account = noAccount();
|
||||
|
||||
if (TER const ter = trustCreate(
|
||||
ctx.view, // payment sandbox
|
||||
recvLow, // is dest low?
|
||||
issuer, // source
|
||||
receiver, // destination
|
||||
trustLineKey.key, // ledger index
|
||||
sleDest, // Account to add to
|
||||
false, // authorize account
|
||||
!sleDest->isFlag(lsfDefaultRipple), //
|
||||
false, // freeze trust line
|
||||
false, // deep freeze trust line
|
||||
initialBalance, // zero initial balance
|
||||
Issue(currency, receiver), // limit of zero
|
||||
0, // quality in
|
||||
0, // quality out
|
||||
*sponsorSle, // sponsor
|
||||
journal); // journal
|
||||
ctx.view, // payment sandbox
|
||||
recvLow, // is dest low?
|
||||
issuer, // source
|
||||
receiver, // destination
|
||||
trustLineKey.key, // ledger index
|
||||
sleDest, // Account to add to
|
||||
false, // authorize account
|
||||
!sleDest->isFlag(lsfDefaultRipple), //
|
||||
false, // freeze trust line
|
||||
false, // deep freeze trust line
|
||||
initialBalance, // zero initial balance
|
||||
Issue(currency, receiver), // limit of zero
|
||||
0, // quality in
|
||||
0, // quality out
|
||||
sponsor ? sponsor->mutableRawSle() : SLE::pointer(), // sponsor
|
||||
journal); // journal
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
@@ -204,23 +229,29 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver);
|
||||
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
|
||||
{
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (sleDest->getType() != ltACCOUNT_ROOT)
|
||||
return detail::escrowDestNotAccountResult(ctx, sleDest);
|
||||
auto const destSle = AccountRootEntryR(sleDest, ctx.view, journal);
|
||||
auto sponsorSle = getEffectiveTxReserveSponsor(ctx, destSle);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal);
|
||||
auto& sponsor = *sponsorSle;
|
||||
|
||||
if (auto const ret = destSle.checkReserve(ctx, xrpBalance, sponsor, {.ownerCountDelta = 1});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0);
|
||||
if (auto const ter = createMPToken(
|
||||
ctx.view, mptID, receiver, sponsor ? sponsor->mutableRawSle() : SLE::pointer(), 0);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// update owner count.
|
||||
increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal);
|
||||
AccountRootEntryW receiverSle(receiver, ctx.view, journal);
|
||||
receiverSle.increaseOwnerCount(sponsor, 1);
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(mptKeylet) && !receiverIssuer)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -54,11 +55,11 @@ getTxReserveSponsorID(STTx const& tx);
|
||||
* Return a mutable SLE for the transaction's reserve sponsor account.
|
||||
*
|
||||
* @param ctx The apply-view context (view + tx)
|
||||
* @return The sponsor account SLE, a null pointer if the tx is not
|
||||
* @return The sponsor account entry, nullopt if the tx is not
|
||||
* reserve-sponsored, or tecINTERNAL if the sponsor account cannot
|
||||
* be loaded (an already-checked invariant).
|
||||
*/
|
||||
std::expected<SLE::pointer, TER>
|
||||
std::expected<std::optional<AccountRootEntryW>, TER>
|
||||
getTxReserveSponsor(ApplyViewContext ctx);
|
||||
|
||||
/**
|
||||
@@ -66,29 +67,29 @@ getTxReserveSponsor(ApplyViewContext ctx);
|
||||
*
|
||||
* @param view The ledger read view
|
||||
* @param tx The transaction to inspect
|
||||
* @return The sponsor account SLE, a null pointer if the tx is not
|
||||
* @return The sponsor account entry, nullopt if the tx is not
|
||||
* reserve-sponsored, or tecINTERNAL if the sponsor account cannot
|
||||
* be loaded (an already-checked invariant).
|
||||
*/
|
||||
std::expected<SLE::const_pointer, TER>
|
||||
std::expected<std::optional<AccountRootEntryR>, TER>
|
||||
getTxReserveSponsor(ReadView const& view, STTx const& tx);
|
||||
|
||||
/**
|
||||
* The transaction's reserve sponsor for the given account, if applicable.
|
||||
*
|
||||
* A reserve sponsor only covers the transaction submitter's own objects, so
|
||||
* this returns the tx reserve sponsor SLE only when accountSle is the tx's own
|
||||
* (non-pseudo) account; otherwise it returns a null sponsor pointer. This is
|
||||
* this returns the tx reserve sponsor entry only when accountSle is the tx's
|
||||
* own (non-pseudo) account; otherwise it returns nullopt. This is
|
||||
* the single source of truth for the "sponsor applies to tx.Account only" rule
|
||||
* that the sponsor-deriving helper overloads in AccountRootHelpers rely on.
|
||||
*
|
||||
* @param ctx The apply-view context (view + tx)
|
||||
* @param accountSle The account whose sponsor is being resolved
|
||||
* @return The sponsor SLE (nullptr if unsponsored), or tecINTERNAL if the
|
||||
* @return The sponsor entry (nullopt if unsponsored), or tecINTERNAL if the
|
||||
* sponsor account cannot be loaded (an already-checked invariant)
|
||||
*/
|
||||
[[nodiscard]] std::expected<SLE::pointer, TER>
|
||||
getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::ConstRef accountSle);
|
||||
[[nodiscard]] std::expected<std::optional<AccountRootEntryW>, TER>
|
||||
getEffectiveTxReserveSponsor(ApplyViewContext ctx, AccountRootEntryR const& accountSle);
|
||||
|
||||
/**
|
||||
* Return the AccountID stored in the given sponsor field of a ledger entry, or nullopt if absent.
|
||||
@@ -105,9 +106,9 @@ getLedgerEntryReserveSponsorID(SLE::ConstRef sle, SF_ACCOUNT const& field = sfSp
|
||||
* @param view The mutable apply view
|
||||
* @param sle The ledger entry whose sponsor field is inspected
|
||||
* @param field The field that holds the sponsor AccountID (defaults to sfSponsor)
|
||||
* @return The sponsor account SLE, or a null pointer if the entry is unsponsored.
|
||||
* @return The sponsor account entry, or nullopt if the entry is unsponsored.
|
||||
*/
|
||||
SLE::pointer
|
||||
std::optional<AccountRootEntryW>
|
||||
getLedgerEntryReserveSponsor(
|
||||
ApplyView& view,
|
||||
SLE::ConstRef sle,
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* A coroutine-aware OpenTelemetry runtime-context storage.
|
||||
*
|
||||
* OpenTelemetry's default ThreadLocalContextStorage keeps the active-context
|
||||
* stack in a plain static thread_local, so the ambient span does NOT follow a
|
||||
* JobQueue::Coro across yield/resume — a scope pushed on one worker is stranded
|
||||
* when the coroutine resumes on another. This storage instead keeps the stack
|
||||
* in an xrpl::LocalValue, which JobQueue::Coro::resume() swaps in and out with
|
||||
* the coroutine (see Coro.ipp). The active context therefore rides the
|
||||
* coroutine: scopes held across yield are safe, and per-line log-trace
|
||||
* correlation (Log.cpp reads RuntimeContext::GetCurrent()) is retained.
|
||||
*
|
||||
* Off a coroutine, LocalValue transparently provides a per-thread store, so
|
||||
* behaviour is identical to the default thread-local storage.
|
||||
*
|
||||
* +-------------------------------------------------+
|
||||
* | CoroAwareContextStorage |
|
||||
* | (opentelemetry RuntimeContextStorage) |
|
||||
* +-------------------------------------------------+
|
||||
* | - stack_ : LocalValue<vector<Context>> |
|
||||
* +-------------------------------------------------+
|
||||
* | + GetCurrent() : Context |
|
||||
* | + Attach(ctx) : unique_ptr<Token> |
|
||||
* | + Detach(token): bool |
|
||||
* +-------------------------------------------------+
|
||||
* | backed by (coro-aware)
|
||||
* +------------------------+
|
||||
* | xrpl::LocalValue store |
|
||||
* | (swapped by Coro) |
|
||||
* +------------------------+
|
||||
*
|
||||
* Install once at telemetry start via
|
||||
* opentelemetry::context::RuntimeContext::SetRuntimeContextStorage(), BEFORE
|
||||
* any span is created (SDK requirement).
|
||||
*
|
||||
* @note Thread-safety: each thread/coroutine sees its own LocalValue store, so
|
||||
* the stack is never shared across threads — no locking needed. The storage
|
||||
* object itself is stateless apart from the LocalValue handle.
|
||||
* @note Known limitation: install once, before the first span; resetting the
|
||||
* storage while spans exist is undefined behaviour (SDK). A span created on a
|
||||
* raw worker thread and later moved onto a coroutine does not retro-attach to
|
||||
* the coroutine's store — not a pattern here (spans are created inside their
|
||||
* own coro/job body).
|
||||
*
|
||||
* Example 1 — install at telemetry start (primary use):
|
||||
* @code
|
||||
* using opentelemetry::context::RuntimeContext;
|
||||
* RuntimeContext::SetRuntimeContextStorage(
|
||||
* opentelemetry::nostd::shared_ptr<
|
||||
* opentelemetry::context::RuntimeContextStorage>(
|
||||
* new xrpl::telemetry::CoroAwareContextStorage()));
|
||||
* @endcode
|
||||
*
|
||||
* Example 2 — a scope held across a coroutine yield stays correct:
|
||||
* @code
|
||||
* // Inside a JobQueue::Coro body:
|
||||
* auto span = ScopedSpanGuard(TraceCategory::Rpc, "rpc", "process");
|
||||
* context.coro->yield(); // may resume on another worker
|
||||
* // span is still the ambient context here — the storage rode the coro.
|
||||
* @endcode
|
||||
*
|
||||
* Example 3 — edge case: off a coroutine, behaves like thread-local storage:
|
||||
* @code
|
||||
* // On a plain worker thread (no coro): each thread has its own stack,
|
||||
* // identical to opentelemetry's default ThreadLocalContextStorage.
|
||||
* auto span = ScopedSpanGuard(TraceCategory::Ledger, "ledger", "build");
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/basics/LocalValue.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/context/runtime_context.h>
|
||||
#include <opentelemetry/nostd/unique_ptr.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
class CoroAwareContextStorage : public opentelemetry::context::RuntimeContextStorage
|
||||
{
|
||||
public:
|
||||
CoroAwareContextStorage() = default;
|
||||
|
||||
// Non-copyable and non-movable: the LocalValue store is keyed by the
|
||||
// address of stack_, so copying or moving would mis-key the store and
|
||||
// strand its entries. Only ever new-ed once and installed on the SDK, so
|
||||
// these are for intent/safety rather than a live bug.
|
||||
CoroAwareContextStorage(CoroAwareContextStorage const&) = delete;
|
||||
CoroAwareContextStorage&
|
||||
operator=(CoroAwareContextStorage const&) = delete;
|
||||
CoroAwareContextStorage(CoroAwareContextStorage&&) = delete;
|
||||
CoroAwareContextStorage&
|
||||
operator=(CoroAwareContextStorage&&) = delete;
|
||||
|
||||
/**
|
||||
* @return the current (top-of-stack) context for this coro/thread.
|
||||
*/
|
||||
opentelemetry::context::Context
|
||||
GetCurrent() noexcept override;
|
||||
|
||||
/**
|
||||
* Push a context frame onto this coro/thread's stack.
|
||||
* @param context the context to make current.
|
||||
* @return a token that Detach() uses to pop back to the prior frame.
|
||||
*/
|
||||
opentelemetry::nostd::unique_ptr<opentelemetry::context::Token>
|
||||
Attach(opentelemetry::context::Context const& context) noexcept override;
|
||||
|
||||
/**
|
||||
* Pop the stack back through the frame the token refers to.
|
||||
* @param token a token returned by Attach().
|
||||
* @return true if the frame was found and detached.
|
||||
*/
|
||||
bool
|
||||
Detach(opentelemetry::context::Token& token) noexcept override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* The active-context stack, stored coro-locally. LocalValue hands back the
|
||||
* stack for whichever store (coro or thread) is currently installed.
|
||||
*/
|
||||
LocalValue<std::vector<opentelemetry::context::Context>> stack_;
|
||||
};
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,188 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <opentelemetry/sdk/trace/id_generator.h>
|
||||
#include <opentelemetry/trace/span_id.h>
|
||||
#include <opentelemetry/trace/trace_id.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/**
|
||||
* OTel IdGenerator that can mint a deterministic (hash-derived) trace_id.
|
||||
*
|
||||
* By default the OTel SDK generates random trace_ids, so a span derived from
|
||||
* a stable hash (e.g. a transaction id) cannot become a real trace root: the
|
||||
* SDK either inherits the active span as parent or invents a random root id.
|
||||
* This generator lets a caller pin the trace_id of the next forced-root span
|
||||
* to a chosen 16-byte value, so hash-derived spans line up across nodes into
|
||||
* one trace. When no value is pinned it behaves exactly like the default
|
||||
* random generator.
|
||||
*
|
||||
* The pinned value lives in a thread-local slot set by PendingTraceId (below).
|
||||
* Only GenerateTraceId() consults that slot, and only on the SDK's no-parent
|
||||
* (root) branch; span_ids are always random. is_random_ is false so the W3C
|
||||
* random-trace-id flag is not set on these deterministic ids.
|
||||
*
|
||||
* Dependency / data-flow diagram:
|
||||
*
|
||||
* +-----------------------------------------------------------+
|
||||
* | DeterministicIdGenerator |
|
||||
* | (IdGenerator) |
|
||||
* +-----------------------------------------------------------+
|
||||
* | - random_ : unique_ptr<IdGenerator> (random delegate) |
|
||||
* +-----------------------------------------------------------+
|
||||
* | GenerateTraceId(): |
|
||||
* | thread-local pending id set? --yes--> return that id |
|
||||
* | --no --> random_ ... |
|
||||
* | GenerateSpanId(): always ---------------> random_ ... |
|
||||
* +-----------------------------------------------------------+
|
||||
* ^ |
|
||||
* sets/clears| delegates|
|
||||
* | v
|
||||
* +----------------+ +--------------------+
|
||||
* | PendingTraceId | | RandomIdGenerator |
|
||||
* | (RAII guard) | | (delegate) |
|
||||
* +----------------+ +--------------------+
|
||||
*
|
||||
* @note Thread safety: the pending trace_id is a file-local thread_local, so
|
||||
* each thread sees only its own pinned value and no synchronization is needed.
|
||||
* The pending id is consumed ONLY by GenerateTraceId(), which the SDK calls
|
||||
* solely when a span has no valid parent; GenerateSpanId() never reads it and
|
||||
* is always random.
|
||||
* @note Limitation: minting a deterministic root only works when the caller
|
||||
* forces the SDK's root branch (e.g. by starting the span with a
|
||||
* Context{kIsRootSpanKey, true}); otherwise the SDK reuses the parent's
|
||||
* trace_id and never calls GenerateTraceId(). The primary such caller is
|
||||
* SpanGuard::hashSpan(), which forces the root branch to mint deterministic
|
||||
* per-object trace roots.
|
||||
*
|
||||
* Example 1 - primary use, inside a forced-root hash span (shown as
|
||||
* pseudocode):
|
||||
* @code
|
||||
* // std::array<std::uint8_t, 16> id = deriveTraceIdFromHash(txHash);
|
||||
* // PendingTraceId const pending{id}; // pin id for this thread
|
||||
* // auto root = Context{kIsRootSpanKey, true}; // force the no-parent branch
|
||||
* // auto guard = telemetry.startSpan("tx.process", root);
|
||||
* // // GenerateTraceId() returns `id`; the guard's trace_id == id.
|
||||
* @endcode
|
||||
*
|
||||
* Example 2 - edge case, a normal child span never consults the pending id:
|
||||
* @code
|
||||
* // With an active parent span, startSpan() inherits the parent's trace_id
|
||||
* // and the SDK does NOT call GenerateTraceId(), so no PendingTraceId is used.
|
||||
* // auto child = parentGuard.childSpan(rpc_span::prefix::command); // random/parent trace_id
|
||||
* @endcode
|
||||
*/
|
||||
class DeterministicIdGenerator final : public opentelemetry::sdk::trace::IdGenerator
|
||||
{
|
||||
/**
|
||||
* Random generator the deterministic path falls back to. Used for every
|
||||
* span_id and for any trace_id when no PendingTraceId is active.
|
||||
*/
|
||||
std::unique_ptr<opentelemetry::sdk::trace::IdGenerator> random_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Build a generator with is_random_ = false and a random delegate.
|
||||
*/
|
||||
DeterministicIdGenerator();
|
||||
|
||||
/**
|
||||
* @return the thread's pending trace_id if a PendingTraceId is active,
|
||||
* otherwise a fresh random trace_id from the delegate. Consuming the
|
||||
* pending id clears it so it applies to exactly one root span.
|
||||
*/
|
||||
opentelemetry::trace::TraceId
|
||||
GenerateTraceId() noexcept override;
|
||||
|
||||
/**
|
||||
* @return a fresh random span_id. Never uses the pending trace_id.
|
||||
*/
|
||||
opentelemetry::trace::SpanId
|
||||
GenerateSpanId() noexcept override;
|
||||
};
|
||||
|
||||
/**
|
||||
* RAII guard that pins a deterministic trace_id for the next forced-root span
|
||||
* started on this thread.
|
||||
*
|
||||
* Mirrors DiscardScope: it sets a thread-local pending trace_id on
|
||||
* construction and clears it on destruction, so the pinned id stays confined
|
||||
* to the guard's scope and cannot leak onto a later span. On destruction it
|
||||
* asserts that the id was actually consumed by GenerateTraceId() — if it was
|
||||
* not, the SDK took a branch other than the root branch the caller intended,
|
||||
* which is a bug worth catching in debug/test builds.
|
||||
*
|
||||
* Wrap ONLY the single forced-root startSpan() call that must receive the
|
||||
* deterministic id. Non-copyable and non-movable: its sole purpose is the
|
||||
* scoped lifetime of the pending id.
|
||||
*
|
||||
* @note Thread safety: the pending id is thread-local, so a guard on one
|
||||
* thread never affects another. Construct and destroy the guard on the same
|
||||
* thread as the startSpan() call it wraps.
|
||||
* @note Limitation: the consumed-assert only holds when the wrapped span is
|
||||
* started on the SDK root branch (Context{kIsRootSpanKey, true}); wrapping a
|
||||
* child span would leave the id unconsumed and trip the assert. Nesting two
|
||||
* guards on one thread is unsupported: the inner guard consumes/clears first,
|
||||
* so the outer id is silently dropped and its destructor trips the assert.
|
||||
*
|
||||
* Example 1 - primary use, wrapping a forced-root span start (pseudocode):
|
||||
* @code
|
||||
* // {
|
||||
* // PendingTraceId const pending{id}; // pin for this scope
|
||||
* // auto root = Context{kIsRootSpanKey, true};
|
||||
* // auto guard = telemetry.startSpan(name, root);// consumes the pinned id
|
||||
* // } // ~PendingTraceId asserts the id was consumed, then clears it
|
||||
* @endcode
|
||||
*
|
||||
* Example 2 - edge case, misuse detection: wrapping a non-root span leaves the
|
||||
* id unconsumed, so ~PendingTraceId trips XRPL_ASSERT in debug/test builds.
|
||||
*/
|
||||
class PendingTraceId
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Pin @p id as the pending trace_id for this thread.
|
||||
* @param id The 16-byte trace_id the next forced-root span should adopt.
|
||||
*/
|
||||
explicit PendingTraceId(std::array<std::uint8_t, 16> const& id) noexcept;
|
||||
|
||||
/**
|
||||
* Assert the pinned id was consumed by a root span, then clear it so it
|
||||
* never leaks onto the next span (even in release, where the assert is a
|
||||
* no-op).
|
||||
*/
|
||||
~PendingTraceId() noexcept;
|
||||
|
||||
PendingTraceId(PendingTraceId const&) = delete;
|
||||
PendingTraceId&
|
||||
operator=(PendingTraceId const&) = delete;
|
||||
PendingTraceId(PendingTraceId&&) = delete;
|
||||
PendingTraceId&
|
||||
operator=(PendingTraceId&&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* Running count of deterministic trace_ids that were pinned by a
|
||||
* PendingTraceId but never consumed by a forced-root span before the guard was
|
||||
* destroyed. Bumped once per silent drop.
|
||||
*
|
||||
* ~PendingTraceId also asserts on this condition, but the assert is a no-op in
|
||||
* release builds, so this monotonic counter is the only signal that a
|
||||
* deterministic trace root was lost there. It is process-wide (summed across
|
||||
* all threads) and intended for a future diagnostic metric or debugger
|
||||
* inspection; reading it has no side effects.
|
||||
*
|
||||
* @return the total number of unconsumed deterministic trace_id drops so far.
|
||||
*/
|
||||
[[nodiscard]] std::uint64_t
|
||||
unconsumedDeterministicIdDrops() noexcept;
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,95 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Thread-local discard signaling between SpanGuard and the span processor.
|
||||
*
|
||||
* SpanGuard::discard() wants to drop a span without sending it to the
|
||||
* exporter. The OTel SDK calls SpanProcessor::OnEnd() synchronously on the
|
||||
* same thread that calls Span::End(), so a thread-local flag set just before
|
||||
* End() and read inside OnEnd() lets FilteringSpanProcessor drop the span
|
||||
* before it enters the batch export queue.
|
||||
*
|
||||
* This side-channel avoids inspecting the Recordable's internals (which vary
|
||||
* by exporter type — SpanData vs OtlpRecordable).
|
||||
*
|
||||
* The flag is a *private* thread-local member of DiscardScope, mutated only by
|
||||
* its constructor and destructor. This gives real access control rather than a
|
||||
* naming convention: no code that includes this header can flip the flag
|
||||
* directly — it can only enter a DiscardScope (which sets and clears the flag
|
||||
* over its own lifetime) and observe the state via DiscardScope::isActive().
|
||||
* Binding set/clear to a scope also means the flag cannot leak onto the next
|
||||
* span even if End() were to throw.
|
||||
*
|
||||
* Kept in a separate header to avoid transitive include bloat: SpanGuard.h
|
||||
* only needs this signaling, not the full Telemetry.h with BasicConfig/Journal.
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
* // In SpanGuard::discard():
|
||||
* {
|
||||
* DiscardScope discardScope; // flag set for this scope only
|
||||
* span->End(); // OnEnd() runs synchronously, sees flag
|
||||
* } // flag cleared here, unconditionally
|
||||
*
|
||||
* // In FilteringSpanProcessor::OnEnd():
|
||||
* if (DiscardScope::isActive())
|
||||
* return; // drop the span
|
||||
* @endcode
|
||||
*
|
||||
* @note Thread safety: the flag is thread-local, so each thread observes only
|
||||
* its own discard signal — no synchronization is required.
|
||||
*
|
||||
* @see SpanGuard::discard(), FilteringSpanProcessor (FilteringSpanProcessor.h)
|
||||
*/
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/**
|
||||
* RAII guard that marks the current thread's span for discard.
|
||||
*
|
||||
* Sets the thread-local discard flag on construction and clears it on
|
||||
* destruction, so a span ended within the guard's scope is dropped by
|
||||
* FilteringSpanProcessor::OnEnd() while the flag stays confined to that scope.
|
||||
* The flag is private and mutated only here, so no other code can set it.
|
||||
* Non-copyable and non-movable — its sole purpose is the scoped flag lifetime.
|
||||
*/
|
||||
class DiscardScope
|
||||
{
|
||||
public:
|
||||
DiscardScope() noexcept
|
||||
{
|
||||
discarding = true;
|
||||
}
|
||||
|
||||
~DiscardScope()
|
||||
{
|
||||
discarding = false;
|
||||
}
|
||||
|
||||
DiscardScope(DiscardScope const&) = delete;
|
||||
DiscardScope&
|
||||
operator=(DiscardScope const&) = delete;
|
||||
DiscardScope(DiscardScope&&) = delete;
|
||||
DiscardScope&
|
||||
operator=(DiscardScope&&) = delete;
|
||||
|
||||
/**
|
||||
* @return true if the current thread is inside a DiscardScope, i.e. the
|
||||
* span ending now should be dropped rather than exported. Read by
|
||||
* FilteringSpanProcessor::OnEnd().
|
||||
*/
|
||||
[[nodiscard]] static bool
|
||||
isActive() noexcept
|
||||
{
|
||||
return discarding;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* Thread-local discard flag. Private, so only this class's ctor/dtor can
|
||||
* mutate it; observers use isActive(). One instance per thread.
|
||||
*/
|
||||
inline static thread_local bool discarding = false;
|
||||
};
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
@@ -1,138 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <opentelemetry/sdk/trace/processor.h>
|
||||
#include <opentelemetry/sdk/trace/recordable.h>
|
||||
#include <opentelemetry/trace/span_context.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/**
|
||||
* Span processor that sits in front of the batch processor. It drops
|
||||
* discarded spans, and it makes every span export each attribute key once.
|
||||
*
|
||||
* The OTLP recordable appends every SetAttribute() call without looking up
|
||||
* the key. Without this processor a key set twice is exported twice, and a
|
||||
* reader that takes the first value misses the update. So the processor
|
||||
* wraps each recordable it hands to the SDK. The wrapper keeps the last value
|
||||
* for each key and writes each key once, in first-set order, when the span
|
||||
* ends. Every other call goes straight to the delegate's recordable.
|
||||
*
|
||||
* Dependency diagram:
|
||||
*
|
||||
* +------------------------+ delegate_ +--------------------+
|
||||
* | FilteringSpanProcessor |------------->| BatchSpanProcessor |
|
||||
* +------------------------+ +--------------------+
|
||||
* | reads | makes
|
||||
* v v
|
||||
* DiscardScope (DiscardFlag.h) OtlpRecordable
|
||||
*
|
||||
* Data flow for one span:
|
||||
*
|
||||
* MakeRecordable() --> wrapper around the delegate's recordable
|
||||
* SetAttribute() --> wrapper keeps an owned copy, last value wins
|
||||
* other calls --> forwarded to the delegate's recordable at once
|
||||
* OnEnd() --> discarded? drop the span
|
||||
* --> else write each kept key once, then pass the
|
||||
* delegate's recordable to the delegate
|
||||
*
|
||||
* The discard check reads a thread-local flag, not the span's attributes,
|
||||
* because the recordable type depends on the exporter and has no common
|
||||
* getter. The flag works because Span::End() calls OnEnd() on its own thread.
|
||||
*
|
||||
* @code
|
||||
* // Production: wrap the batch processor before building the provider.
|
||||
* auto processor = std::make_unique<FilteringSpanProcessor>(std::move(batchProcessor));
|
||||
*
|
||||
* // A key set twice is exported once, as "accepted".
|
||||
* auto span = processor->MakeRecordable();
|
||||
* span->SetAttribute("consensus_phase", "open");
|
||||
* span->SetAttribute("consensus_phase", "accepted");
|
||||
* processor->OnEnd(std::move(span));
|
||||
*
|
||||
* // Edge case: a span ended inside a DiscardScope never reaches the delegate.
|
||||
* {
|
||||
* DiscardScope discardScope;
|
||||
* processor->OnEnd(processor->MakeRecordable());
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @note Thread safety: OnStart() and OnEnd() may run on many threads at once.
|
||||
* Each wrapper belongs to one span, and the SDK span serializes calls to it.
|
||||
* The discard flag is thread-local.
|
||||
* @note Limitations: every distinct key is kept until the span ends, so memory
|
||||
* grows with the number of distinct keys, and the delegate's attribute count
|
||||
* limit applies when the keys are written. Attributes inside one event or link
|
||||
* are forwarded as given. An attribute a delegate sets on the recordable in its
|
||||
* own OnStart() skips the merge.
|
||||
*/
|
||||
class FilteringSpanProcessor final : public opentelemetry::sdk::trace::SpanProcessor
|
||||
{
|
||||
/**
|
||||
* Receives every span that is not discarded. The batch processor in
|
||||
* production.
|
||||
*/
|
||||
std::unique_ptr<opentelemetry::sdk::trace::SpanProcessor> delegate_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @param delegate Processor that receives every span that is not discarded.
|
||||
*/
|
||||
explicit FilteringSpanProcessor(
|
||||
std::unique_ptr<opentelemetry::sdk::trace::SpanProcessor> delegate);
|
||||
|
||||
/**
|
||||
* Asks the delegate for a recordable and wraps it.
|
||||
*
|
||||
* @return the wrapper, or nullptr when the delegate made none. The SDK then
|
||||
* records nothing for the span.
|
||||
*/
|
||||
std::unique_ptr<opentelemetry::sdk::trace::Recordable>
|
||||
MakeRecordable() noexcept override;
|
||||
|
||||
/**
|
||||
* Tells the delegate a span started, passing the delegate's own recordable.
|
||||
*
|
||||
* @param span Recordable from MakeRecordable(). Any other recordable is
|
||||
* passed on as it is.
|
||||
* @param parentContext Context of the span's parent.
|
||||
*/
|
||||
void
|
||||
OnStart(
|
||||
opentelemetry::sdk::trace::Recordable& span,
|
||||
opentelemetry::trace::SpanContext const& parentContext) noexcept override;
|
||||
|
||||
/**
|
||||
* Drops the span if it was discarded. Otherwise writes each kept attribute
|
||||
* once and passes the delegate's own recordable to the delegate.
|
||||
*
|
||||
* @param span Recordable from MakeRecordable(). Any other recordable is
|
||||
* passed on as it is.
|
||||
*/
|
||||
void
|
||||
OnEnd(std::unique_ptr<opentelemetry::sdk::trace::Recordable>&& span) noexcept override;
|
||||
|
||||
/**
|
||||
* @param timeout Longest time to wait for the delegate.
|
||||
* @return the delegate's result.
|
||||
*/
|
||||
bool
|
||||
ForceFlush(
|
||||
std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override;
|
||||
|
||||
/**
|
||||
* @param timeout Longest time to wait for the delegate.
|
||||
* @return the delegate's result.
|
||||
*/
|
||||
bool
|
||||
Shutdown(
|
||||
std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override;
|
||||
};
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <opentelemetry/sdk/trace/sampler.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/**
|
||||
* Build the head sampler that TelemetryImpl installs on its TracerProvider.
|
||||
*
|
||||
* The result is a ParentBased sampler. A span with no parent or a remote
|
||||
* parent is decided by one TraceIdRatioBasedSampler, so a peer's sampled flag
|
||||
* cannot turn our spans off or on. A span with a local parent follows it.
|
||||
*
|
||||
* parent of the new span delegate
|
||||
* ---------------------- ------------------------
|
||||
* none (root span) --> TraceIdRatioBased(ratio)
|
||||
* remote, sampled --> TraceIdRatioBased(ratio)
|
||||
* remote, not sampled --> TraceIdRatioBased(ratio)
|
||||
* local, sampled --> AlwaysOn
|
||||
* local, not sampled --> AlwaysOff
|
||||
*
|
||||
* The ratio sampler decides from the trace id alone, so nodes that use the
|
||||
* same ratio make the same choice for every trace.
|
||||
*
|
||||
* @note The sampler has no mutable state, so the SDK may call ShouldSample()
|
||||
* on many threads at once.
|
||||
*
|
||||
* @param ratio Fraction of trace ids to sample, from 0.0 (none) to 1.0 (all).
|
||||
* @return A new sampler that owns its delegates.
|
||||
*/
|
||||
[[nodiscard]] std::unique_ptr<opentelemetry::sdk::trace::Sampler>
|
||||
makeHeadSampler(double ratio);
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,128 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Compile-time string concatenation utility and shared telemetry constants.
|
||||
*
|
||||
* Provides StaticStr<N> — a compile-time string buffer that implicitly
|
||||
* converts to std::string_view — and join() for dot-separated concatenation.
|
||||
* Module-specific span names (e.g. RPC, consensus) live in their respective
|
||||
* modules and build upon these shared primitives.
|
||||
*
|
||||
* @note These constants are NOT guarded by XRPL_ENABLE_TELEMETRY because
|
||||
* call sites reference them even when SpanGuard methods are no-ops
|
||||
* (the no-op stubs still accept string_view parameters). The compiler
|
||||
* elides all inline constexpr values whose only uses are in dead code.
|
||||
*
|
||||
* @note Json::StaticString (jss.h) is a pointer wrapper without
|
||||
* concatenation support. boost::static_string is not constexpr.
|
||||
* StaticStr<N> exists specifically for compile-time dot-join composition.
|
||||
*
|
||||
* Naming conventions (see spec 2026-05-13-span-attr-naming-design):
|
||||
* - Per-span attribute keys: bare field name (span name carries the domain).
|
||||
* - Collision qualifier: <domain>_<field> when bare name collides across
|
||||
* domains or with OTel reserved `status` (e.g. rpc_status, grpc_status).
|
||||
* - Shared cross-span attributes: <domain>_<field> (underscore) form
|
||||
* (e.g. tx_hash, peer_id, ledger_seq, consensus_round).
|
||||
* - Resource attribute keys: xrpl.<subsystem>.<field> (dotted) form is
|
||||
* RESERVED for process-identity attributes set once at startup on the
|
||||
* OTel resource (e.g. xrpl.network.id, xrpl.network.type). Do not use
|
||||
* this form for span attributes — it parses awkwardly in TraceQL and
|
||||
* blurs the resource/span scope distinction.
|
||||
* - Span prefixes: <subsystem>[.<component>].
|
||||
*/
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
// ===== Compile-time string utility =========================================
|
||||
|
||||
/**
|
||||
* Fixed-size character buffer for compile-time string operations.
|
||||
* Implicitly converts to std::string_view at zero cost.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
struct StaticStr
|
||||
{
|
||||
char data[N + 1]{};
|
||||
static constexpr std::size_t size = N;
|
||||
|
||||
constexpr StaticStr() = default;
|
||||
|
||||
constexpr explicit StaticStr(char const (&str)[N + 1])
|
||||
{
|
||||
for (std::size_t i = 0; i <= N; ++i)
|
||||
data[i] = str[i];
|
||||
}
|
||||
|
||||
constexpr
|
||||
operator std::string_view() const noexcept
|
||||
{
|
||||
return {data, N};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deduction guide: StaticStr from string literal.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
StaticStr(char const (&)[N]) -> StaticStr<N - 1>;
|
||||
|
||||
/**
|
||||
* Create a StaticStr from a string literal.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
constexpr auto
|
||||
makeStr(char const (&str)[N])
|
||||
{
|
||||
return StaticStr<N - 1>(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate two StaticStr values with a dot separator.
|
||||
*/
|
||||
template <std::size_t A, std::size_t B>
|
||||
constexpr auto
|
||||
join(StaticStr<A> const& lhs, StaticStr<B> const& rhs)
|
||||
{
|
||||
constexpr std::size_t len = A + 1 + B; // lhs + '.' + rhs
|
||||
StaticStr<len> result;
|
||||
std::size_t pos = 0;
|
||||
for (std::size_t i = 0; i < A; ++i)
|
||||
result.data[pos++] = lhs.data[i];
|
||||
result.data[pos++] = '.';
|
||||
for (std::size_t i = 0; i < B; ++i)
|
||||
result.data[pos++] = rhs.data[i];
|
||||
result.data[pos] = '\0';
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== Shared root segments ================================================
|
||||
|
||||
namespace seg {
|
||||
inline constexpr auto xrpl = makeStr("xrpl");
|
||||
inline constexpr auto rpc = makeStr("rpc");
|
||||
inline constexpr auto tx = makeStr("tx");
|
||||
inline constexpr auto consensus = makeStr("consensus");
|
||||
inline constexpr auto peer = makeStr("peer");
|
||||
inline constexpr auto ledger = makeStr("ledger");
|
||||
inline constexpr auto network = makeStr("network");
|
||||
inline constexpr auto link = makeStr("link");
|
||||
} // namespace seg
|
||||
|
||||
// ===== Shared attribute keys (used across modules) =========================
|
||||
|
||||
namespace attr {
|
||||
inline constexpr auto networkId = join(join(seg::xrpl, seg::network), makeStr("id"));
|
||||
inline constexpr auto networkType = join(join(seg::xrpl, seg::network), makeStr("type"));
|
||||
} // namespace attr
|
||||
|
||||
// ===== Shared attribute values =============================================
|
||||
|
||||
namespace attr_val {
|
||||
inline constexpr auto success = makeStr("success");
|
||||
inline constexpr auto error = makeStr("error");
|
||||
} // namespace attr_val
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
@@ -1,418 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Abstract interface for OpenTelemetry distributed tracing.
|
||||
*
|
||||
* Provides the Telemetry base class that all components use to create trace
|
||||
* spans. Three concrete implementations exist, selected at construction time
|
||||
* by makeTelemetry():
|
||||
*
|
||||
* - TelemetryImpl (Telemetry.cpp): real OTel SDK integration, compiled
|
||||
* only when XRPL_ENABLE_TELEMETRY is defined and enabled at runtime.
|
||||
* - NullTelemetry (NullTelemetry.cpp): no-op stub used when telemetry is
|
||||
* disabled at compile time or runtime.
|
||||
* - NullTelemetryOtel (Telemetry.cpp): no-op stub that still depends on
|
||||
* the OTel API (used during transition or for testing).
|
||||
*
|
||||
* Inheritance / dependency diagram:
|
||||
*
|
||||
* +--------------------+
|
||||
* | Telemetry | (abstract, this file)
|
||||
* | <<interface>> |
|
||||
* +---------+----------+
|
||||
* |
|
||||
* +---------+-----------+-------------------+
|
||||
* | | |
|
||||
* +---+------------+ +-----+---------+ +------+----------+
|
||||
* | TelemetryImpl | | NullTelemetry | | NullTelemetryOtel|
|
||||
* | (Telemetry.cpp)| |(NullTelemetry | | (Telemetry.cpp) |
|
||||
* | OTel SDK | | .cpp) | | noop w/ OTel API |
|
||||
* +----------------+ +---------------+ +------------------+
|
||||
*
|
||||
* The Setup struct holds all configuration parsed from the [telemetry]
|
||||
* section of xrpld.cfg. See TelemetryConfig.cpp for the parser and
|
||||
* cfg/xrpld-example.cfg for the available options.
|
||||
*
|
||||
* OTel SDK headers are conditionally included behind XRPL_ENABLE_TELEMETRY
|
||||
* so that builds without telemetry have zero dependency on opentelemetry-cpp.
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
* 1. Root span at a subsystem entry point (typical usage):
|
||||
* @code
|
||||
* #include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
* using namespace xrpl::telemetry;
|
||||
*
|
||||
* // In an RPC handler dispatch:
|
||||
* auto guard = SpanGuard::span(
|
||||
* TraceCategory::Rpc, rpc_span::prefix::command, commandName);
|
||||
* guard.setAttribute(rpc_span::attr::command, commandName);
|
||||
* // ... process request
|
||||
* // guard destructor automatically ends the span on scope exit
|
||||
* @endcode
|
||||
*
|
||||
* 2. Child span for a sub-operation (scoped child):
|
||||
* @code
|
||||
* auto parent = ScopedSpanGuard(
|
||||
* TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process);
|
||||
* {
|
||||
* auto child = parent.childSpan(rpc_span::prefix::command);
|
||||
* child.setAttribute(rpc_span::attr::version, static_cast<int64_t>(apiVersion));
|
||||
* // child ends here
|
||||
* }
|
||||
* @endcode
|
||||
* childSpan() parents to the ambient scope, so the parent must be a
|
||||
* ScopedSpanGuard. A plain SpanGuard is not ambient: pass its spanContext()
|
||||
* to childSpan(name, ctx) instead. childSpan() takes the name verbatim, so
|
||||
* pass a full dotted constant, never a bare op:: suffix.
|
||||
*
|
||||
* 3. Unrelated span (cross-scope, same thread):
|
||||
* @code
|
||||
* // gRPC and RPC handlers can be active simultaneously
|
||||
* auto grpcSpan = SpanGuard::span(
|
||||
* TraceCategory::Rpc, grpc_span::prefix::grpc, grpc_span::attr::method);
|
||||
* auto rpcSpan = SpanGuard::span(
|
||||
* TraceCategory::Rpc, rpc_span::prefix::command, commandName);
|
||||
* // both spans end on scope exit
|
||||
* @endcode
|
||||
*
|
||||
* 4. Cross-thread context propagation:
|
||||
* @code
|
||||
* // Thread A: take a handle to the parent span's own context
|
||||
* auto ctx = parentGuard.spanContext();
|
||||
*
|
||||
* // Thread B: create child span with explicit parent
|
||||
* auto child = SpanGuard::childSpan(rpc_span::prefix::command, ctx);
|
||||
* @endcode
|
||||
*
|
||||
* @note Thread safety: The Telemetry interface is safe for concurrent reads
|
||||
* (isEnabled, shouldTrace*, getTracer, startSpan) after start() completes.
|
||||
* setServiceInstanceId() must be called before start() and is not thread-safe.
|
||||
* The OTel SDK's TracerProvider and Tracer are internally thread-safe.
|
||||
*/
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/tracer.h>
|
||||
|
||||
// std::string_view appears only in the telemetry-enabled declarations below.
|
||||
#include <string_view>
|
||||
#endif
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
/**
|
||||
* OTel instrumentation scope (tracer) name. Identifies this library as the
|
||||
* source of spans; distinct from the `service.name` resource attribute
|
||||
* (Setup::serviceName), which is config-overridable.
|
||||
*/
|
||||
inline constexpr std::string_view kTracerName{"xrpld"};
|
||||
#endif
|
||||
|
||||
class Telemetry
|
||||
{
|
||||
/**
|
||||
* Global singleton pointer, set by start()/stop() in the active
|
||||
* implementation. Allows SpanGuard factory methods to access the
|
||||
* Telemetry instance without callers passing it explicitly.
|
||||
*
|
||||
* Atomic with acquire/release ordering: start()/stop() store on
|
||||
* the initialization thread, factory methods load on worker threads.
|
||||
* @see setInstance(), getInstance()
|
||||
*/
|
||||
inline static std::atomic<Telemetry*> instance{nullptr};
|
||||
|
||||
public:
|
||||
/**
|
||||
* Get the global Telemetry instance.
|
||||
* @return Pointer to the active instance, or nullptr if not started.
|
||||
*/
|
||||
[[nodiscard]] static Telemetry*
|
||||
getInstance()
|
||||
{
|
||||
return instance.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the global Telemetry instance.
|
||||
* Called by start()/stop() in concrete implementations.
|
||||
* Tests can call this with a mock to override the global instance.
|
||||
* @param t Pointer to the Telemetry instance, or nullptr to clear.
|
||||
*/
|
||||
static void
|
||||
setInstance(Telemetry* t)
|
||||
{
|
||||
instance.store(t, std::memory_order_release);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration parsed from the [telemetry] section of xrpld.cfg.
|
||||
*
|
||||
* All fields have sensible defaults so the section can be minimal
|
||||
* or omitted entirely. See TelemetryConfig.cpp for the parser.
|
||||
*/
|
||||
struct Setup
|
||||
{
|
||||
/**
|
||||
* Master switch: true to enable tracing at runtime.
|
||||
*/
|
||||
bool enabled = false;
|
||||
|
||||
/**
|
||||
* OTel resource attribute `service.name`.
|
||||
*/
|
||||
std::string serviceName = "xrpld";
|
||||
|
||||
/**
|
||||
* OTel resource attribute `service.version` (set from BuildInfo).
|
||||
*/
|
||||
std::string serviceVersion;
|
||||
|
||||
/**
|
||||
* OTel resource attribute `service.instance.id` (defaults to node
|
||||
* public key).
|
||||
*/
|
||||
std::string serviceInstanceId;
|
||||
|
||||
/**
|
||||
* Full OTLP/HTTP URL where spans are sent, including the signal path.
|
||||
* Used verbatim: no other endpoint is derived from it.
|
||||
*/
|
||||
std::string tracesEndpoint = "http://localhost:4318/v1/traces";
|
||||
|
||||
/**
|
||||
* Whether to use TLS for the exporter connection.
|
||||
*/
|
||||
bool useTls = false;
|
||||
|
||||
/**
|
||||
* Path to a CA certificate bundle for TLS verification.
|
||||
*/
|
||||
std::string tlsCertPath;
|
||||
|
||||
/**
|
||||
* Head-based sampling ratio. Intentionally fixed at 1.0 (sample
|
||||
* everything) and NOT read from config. A per-node ratio would let
|
||||
* nodes make divergent keep/drop decisions for the same distributed
|
||||
* trace, producing broken/partial traces. makeHeadSampler() applies
|
||||
* this ratio to spans with no parent or a remote parent, so a peer's
|
||||
* sampled flag cannot turn our spans off or on; spans with a local
|
||||
* parent follow it. Volume reduction is delegated to the collector's
|
||||
* tail sampling; for node-local post-hoc dropping see
|
||||
* SpanGuard::discard().
|
||||
*/
|
||||
static constexpr double samplingRatio = 1.0;
|
||||
|
||||
/**
|
||||
* Maximum number of spans per batch export.
|
||||
*/
|
||||
std::uint32_t batchSize = 512;
|
||||
|
||||
/**
|
||||
* Delay between batch exports.
|
||||
*/
|
||||
std::chrono::milliseconds batchDelay = std::chrono::milliseconds{5000};
|
||||
|
||||
/**
|
||||
* Maximum number of spans queued before dropping.
|
||||
*/
|
||||
std::uint32_t maxQueueSize = 2048;
|
||||
|
||||
/**
|
||||
* Network identifier, added as an OTel resource attribute.
|
||||
*/
|
||||
std::uint32_t networkId = 0;
|
||||
|
||||
/**
|
||||
* Network type label (e.g. "mainnet", "testnet", "devnet").
|
||||
*/
|
||||
std::string networkType = "mainnet";
|
||||
|
||||
/**
|
||||
* Enable tracing for transaction processing.
|
||||
*/
|
||||
bool traceTransactions = true;
|
||||
|
||||
/**
|
||||
* Enable tracing for consensus rounds.
|
||||
*/
|
||||
bool traceConsensus = true;
|
||||
|
||||
/**
|
||||
* Enable tracing for RPC request handling.
|
||||
*/
|
||||
bool traceRpc = true;
|
||||
|
||||
/**
|
||||
* Enable tracing for peer-to-peer messages (enabled by default;
|
||||
* high volume).
|
||||
*/
|
||||
bool tracePeer = true;
|
||||
|
||||
/**
|
||||
* Enable tracing for ledger close/accept.
|
||||
*/
|
||||
bool traceLedger = true;
|
||||
};
|
||||
|
||||
virtual ~Telemetry() = default;
|
||||
|
||||
/**
|
||||
* Update the service instance ID (OTel resource attribute
|
||||
* `service.instance.id`).
|
||||
*
|
||||
* Must be called before start(). The node public key is not available
|
||||
* when Telemetry is constructed (during the ApplicationImp member
|
||||
* initializer list), so this setter allows Application::setup() to
|
||||
* inject the identity once nodeIdentity_ is known.
|
||||
*
|
||||
* @param id The node's base58-encoded public key or custom identifier.
|
||||
*/
|
||||
virtual void
|
||||
setServiceInstanceId([[maybe_unused]] std::string const& id)
|
||||
{
|
||||
// Default no-op for NullTelemetry implementations.
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the tracing pipeline (exporter, processor, provider).
|
||||
* Call after construction.
|
||||
*/
|
||||
virtual void
|
||||
start() = 0;
|
||||
|
||||
/**
|
||||
* Flush pending spans and shut down the tracing pipeline.
|
||||
* Call before destruction.
|
||||
*/
|
||||
virtual void
|
||||
stop() = 0;
|
||||
|
||||
/**
|
||||
* @return true if this instance is actively exporting spans.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
isEnabled() const = 0;
|
||||
|
||||
/**
|
||||
* @return true if transaction processing should be traced.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTraceTransactions() const = 0;
|
||||
|
||||
/**
|
||||
* @return true if consensus rounds should be traced.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTraceConsensus() const = 0;
|
||||
|
||||
/**
|
||||
* @return true if RPC request handling should be traced.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTraceRpc() const = 0;
|
||||
|
||||
/**
|
||||
* @return true if peer-to-peer messages should be traced.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTracePeer() const = 0;
|
||||
|
||||
/**
|
||||
* @return true if ledger close/accept should be traced.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTraceLedger() const = 0;
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
/**
|
||||
* Get or create a named tracer instance.
|
||||
*
|
||||
* @param name Tracer name used to identify the instrumentation library.
|
||||
* @return A shared pointer to the Tracer.
|
||||
*/
|
||||
[[nodiscard]] virtual opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer>
|
||||
getTracer(std::string_view name = kTracerName) = 0;
|
||||
|
||||
/**
|
||||
* Start a new span on the current thread's context.
|
||||
*
|
||||
* The span becomes a child of the current active span (if any) via
|
||||
* OpenTelemetry's context propagation.
|
||||
*
|
||||
* @param name Span name (typically "rpc.command.<cmd>").
|
||||
* @param kind The span kind (defaults to kInternal). Possible values:
|
||||
* - kInternal: default, in-process operation
|
||||
* - kServer: incoming synchronous request (e.g. RPC)
|
||||
* - kClient: outgoing synchronous request
|
||||
* - kProducer: async message send (e.g. peer broadcast)
|
||||
* - kConsumer: async message receive
|
||||
* @return A shared pointer to the new Span.
|
||||
*/
|
||||
[[nodiscard]] virtual opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
startSpan(
|
||||
std::string_view name,
|
||||
opentelemetry::trace::SpanKind kind = opentelemetry::trace::SpanKind::kInternal) = 0;
|
||||
|
||||
/**
|
||||
* Start a new span with an explicit parent context.
|
||||
*
|
||||
* Use this overload when the parent span is not on the current
|
||||
* thread's context stack (e.g. cross-thread trace propagation).
|
||||
*
|
||||
* @param name Span name.
|
||||
* @param parentContext The parent span's context.
|
||||
* @param kind The span kind (defaults to kInternal).
|
||||
* @return A shared pointer to the new Span.
|
||||
*/
|
||||
[[nodiscard]] virtual opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
startSpan(
|
||||
std::string_view name,
|
||||
opentelemetry::context::Context const& parentContext,
|
||||
opentelemetry::trace::SpanKind kind = opentelemetry::trace::SpanKind::kInternal) = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a Telemetry instance.
|
||||
*
|
||||
* Returns a TelemetryImpl when setup.enabled is true, or a
|
||||
* NullTelemetry no-op stub otherwise.
|
||||
*
|
||||
* @param setup Configuration from the [telemetry] config section.
|
||||
* @param journal Journal for log output during initialization.
|
||||
*/
|
||||
std::unique_ptr<Telemetry>
|
||||
makeTelemetry(Telemetry::Setup const& setup, beast::Journal journal);
|
||||
|
||||
/**
|
||||
* Parse the [telemetry] config section into a Setup struct.
|
||||
*
|
||||
* @param section The [telemetry] config section.
|
||||
* @param nodePublicKey Node public key, used as default instance ID.
|
||||
* @param version Build version string.
|
||||
* @param networkId Network identifier from [network_id] config
|
||||
* (0 = mainnet, 1 = testnet, 2 = devnet).
|
||||
* @return A populated Setup struct with defaults for missing values.
|
||||
*/
|
||||
Telemetry::Setup
|
||||
makeTelemetrySetup(
|
||||
Section const& section,
|
||||
std::string const& nodePublicKey,
|
||||
std::string const& version,
|
||||
std::uint32_t networkId);
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
@@ -27,6 +27,18 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Returns true if sleAcct is an AccountRoot carrying a pseudo-account field.
|
||||
*
|
||||
* isPseudoAccount() takes a view-bound AccountRootEntry, which is not
|
||||
* available while visiting entries, so invariant checks use this instead.
|
||||
*
|
||||
* @param sleAcct the SLE to check.
|
||||
* @return true if sleAcct is an AccountRoot carrying a pseudo-account field.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isPseudoAccountSle(SLE::ConstRef sleAcct);
|
||||
|
||||
#if GENERATING_DOCS
|
||||
/**
|
||||
* @brief Prototype for invariant check implementations.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -25,7 +26,7 @@ checkFreeze(
|
||||
XRPL_ASSERT(src != dst, "xrpl::checkFreeze : unequal input accounts");
|
||||
|
||||
// check freeze
|
||||
if (auto sle = view.read(keylet::account(dst)))
|
||||
if (auto sle = AccountRootEntryR(dst, view))
|
||||
{
|
||||
if (sle->isFlag(lsfGlobalFreeze))
|
||||
{
|
||||
@@ -49,7 +50,7 @@ checkFreeze(
|
||||
|
||||
if (view.rules().enabled(fixFrozenLPTokenTransfer))
|
||||
{
|
||||
if (auto const sleDst = view.read(keylet::account(dst));
|
||||
if (auto const sleDst = AccountRootEntryR(dst, view);
|
||||
sleDst && sleDst->isFieldPresent(sfAMMID))
|
||||
{
|
||||
auto const sleAmm = view.read(keylet::amm((*sleDst)[sfAMMID]));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -12,6 +13,7 @@
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -56,8 +58,8 @@ private:
|
||||
Keylet const& sponsorshipKeylet,
|
||||
AccountID const& sponsorID,
|
||||
AccountID const& sponseeID,
|
||||
SLE::Ref sponsorAccSle,
|
||||
SLE::Ref reserveSponsorAccSle);
|
||||
AccountRootEntryW& sponsorAccSle,
|
||||
std::optional<AccountRootEntryW>& reserveSponsorAccSle);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
@@ -115,8 +116,8 @@ isVaultPseudoAccountFrozenForIssuance(
|
||||
view, {issuer, account}, assetOfHolding(issuanceSle, *sleHolding), depth + 1);
|
||||
}
|
||||
|
||||
auto const mptIssuer = view.read(keylet::account(issuer));
|
||||
if (mptIssuer == nullptr)
|
||||
auto const mptIssuer = AccountRootEntryR(issuer, view);
|
||||
if (!mptIssuer.exists())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::isVaultPseudoAccountFrozen : null MPToken issuer");
|
||||
@@ -189,7 +190,7 @@ canTransferLPToken(
|
||||
{
|
||||
// Only AMM-issued LPTokens are subject to this check. The LPToken's issuer
|
||||
// is the AMM account; if it is not an AMM, this is not an LPToken.
|
||||
auto const sleIssuer = view.read(keylet::account(lpTokenIssuer));
|
||||
auto const sleIssuer = AccountRootEntryR(lpTokenIssuer, view);
|
||||
if (!sleIssuer || !sleIssuer->isFieldPresent(sfAMMID))
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -466,7 +467,7 @@ canWithdraw(
|
||||
ReadView const& view,
|
||||
AccountID const& from,
|
||||
AccountID const& to,
|
||||
SLE::ConstRef toSle,
|
||||
AccountRootEntryR const& toSle,
|
||||
STAmount const& amount,
|
||||
bool hasDestinationTag,
|
||||
std::optional<std::vector<UInt256>> const& credentialIDs)
|
||||
@@ -516,7 +517,7 @@ canWithdraw(
|
||||
bool hasDestinationTag,
|
||||
std::optional<std::vector<UInt256>> const& credentialIDs)
|
||||
{
|
||||
auto const toSle = view.read(keylet::account(to));
|
||||
auto const toSle = AccountRootEntryR(to, view);
|
||||
|
||||
return canWithdraw(view, from, to, toSle, amount, hasDestinationTag, credentialIDs);
|
||||
}
|
||||
@@ -541,7 +542,7 @@ doWithdraw(
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
|
||||
auto dstSle = AccountRootEntryW(dstAcct, ctx.view);
|
||||
|
||||
// Create a trust line or MPToken for a self-destination only when there
|
||||
// is a payout to credit. Post-fixCleanup3_4_0, a zero-value withdraw
|
||||
@@ -559,7 +560,8 @@ doWithdraw(
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto err = verifyDepositPreauth(ctx.tx, ctx.view, senderAcct, dstAcct, dstSle, j))
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx.tx, ctx.view, senderAcct, dstAcct, dstSle.rawSle(), j))
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -582,14 +584,21 @@ doWithdraw(
|
||||
// sponsor against the destination. accountSend can auto-create a holding
|
||||
// for dstAcct; keying on the destination ensures a third-party destination's
|
||||
// holding is never stamped with the tx's reserve sponsor.
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, dstSle);
|
||||
auto sponsorSle = getEffectiveTxReserveSponsor(ctx, dstSle);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
// Move the funds directly from the broker's pseudo-account to the
|
||||
// dstAcct
|
||||
auto& sponsor = *sponsorSle;
|
||||
return accountSend(
|
||||
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
ctx.view,
|
||||
sourceAcct,
|
||||
dstAcct,
|
||||
amount,
|
||||
j,
|
||||
sponsor ? sponsor->mutableRawSle() : SLE::pointer{},
|
||||
WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OwnerCounts.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
template <typename ViewT>
|
||||
XRPAmount
|
||||
AccountRootEntry<ViewT>::reserve(Adjustment adj) const
|
||||
{
|
||||
auto const& view = this->readView();
|
||||
auto const j = this->journal();
|
||||
|
||||
XRPL_ASSERT(
|
||||
this->exists() && (*this)->getType() == ltACCOUNT_ROOT, "xrpl::accountReserve : valid sle");
|
||||
|
||||
if (!view.rules().enabled(featureSponsor))
|
||||
{
|
||||
XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::accountReserve : no account count delta");
|
||||
return view.fees().accountReserve(
|
||||
(*this)->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1);
|
||||
}
|
||||
std::uint32_t const currentOwnerCount = ownerCount(adj.ownerCountDelta);
|
||||
std::uint32_t const currentAccountCount =
|
||||
detail::accountCountImpl(*this, adj.accountCountDelta, j);
|
||||
|
||||
return view.fees().accountReserve(currentOwnerCount, currentAccountCount);
|
||||
}
|
||||
|
||||
template <typename ViewT>
|
||||
std::uint32_t
|
||||
AccountRootEntry<ViewT>::ownerCount(std::int32_t ownerCountAdj) const
|
||||
{
|
||||
auto const j = this->journal();
|
||||
|
||||
XRPL_ASSERT(
|
||||
this->exists() && (*this)->getType() == ltACCOUNT_ROOT,
|
||||
"xrpl::ownerCount : sle is account root");
|
||||
|
||||
AccountID const id = (*this)->getAccountID(sfAccount);
|
||||
std::uint32_t const currentOwnerCount = (*this)->at(sfOwnerCount);
|
||||
std::uint32_t const sponsoredOwnerCount = (*this)->at(sfSponsoredOwnerCount);
|
||||
std::uint32_t const sponsoringOwnerCount = (*this)->at(sfSponsoringOwnerCount);
|
||||
|
||||
XRPL_ASSERT(
|
||||
currentOwnerCount >= sponsoredOwnerCount,
|
||||
"xrpl::ownerCount : OwnerCount must be greater than or equal to SponsoredOwnerCount");
|
||||
|
||||
std::int64_t deltaCount =
|
||||
static_cast<std::int64_t>(ownerCountAdj) - sponsoredOwnerCount + sponsoringOwnerCount;
|
||||
|
||||
if (deltaCount > std::numeric_limits<std::int32_t>::max())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
deltaCount = std::numeric_limits<std::int32_t>::max();
|
||||
JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, "
|
||||
<< "adjustment: " << ownerCountAdj
|
||||
<< ", sponsoredCount: " << sponsoredOwnerCount
|
||||
<< ", sponsoringOwnerCount: " << sponsoringOwnerCount;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
else if (deltaCount < std::numeric_limits<std::int32_t>::min())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
deltaCount = std::numeric_limits<std::int32_t>::min();
|
||||
JLOG(j.fatal()) << "Account " << id << " delta count is below min, "
|
||||
<< "adjustment: " << ownerCountAdj
|
||||
<< ", sponsoredCount: " << sponsoredOwnerCount
|
||||
<< ", sponsoringCount: " << sponsoringOwnerCount;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return detail::confineOwnerCount(currentOwnerCount, deltaCount);
|
||||
}
|
||||
|
||||
template <typename ViewT>
|
||||
TER
|
||||
AccountRootEntry<ViewT>::checkReserve(
|
||||
ApplyViewContext ctx,
|
||||
XRPAmount accBalance,
|
||||
std::optional<AccountRootEntry<ReadView>> const& sponsorSle,
|
||||
Adjustment adj,
|
||||
TER insufReserveCode) const
|
||||
{
|
||||
// TODO: swap to assert after fixCleanup3_2_0 is retired
|
||||
if (!this->exists() || (*this)->getType() != ltACCOUNT_ROOT)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
XRPL_ASSERT(
|
||||
!isTesSuccess(insufReserveCode), "xrpl::checkReserve : insufReserveCode is not tesSUCCESS");
|
||||
if (ctx.view.rules().enabled(featureSponsor))
|
||||
{
|
||||
if (sponsorSle)
|
||||
{
|
||||
if ((*sponsorSle)->getType() != ltACCOUNT_ROOT)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sle = ctx.view.read(
|
||||
keylet::sponsorship(
|
||||
(*sponsorSle)->getAccountID(sfAccount), (*this)->getAccountID(sfAccount)));
|
||||
|
||||
// A reserve-sponsored tx must carry a sponsor signature
|
||||
// (cosigning path) and/or have a pre-existing sponsorship SLE
|
||||
// (prefunded path). Absence of both is an internal invariant break.
|
||||
if (isReserveSponsored(ctx.tx) && !sle && !ctx.tx.isFieldPresent(sfSponsorSignature))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (sle)
|
||||
{
|
||||
auto const ownerCountAllowed = sle->getFieldU32(sfRemainingOwnerCount);
|
||||
if (adj.ownerCountDelta > 0 &&
|
||||
ownerCountAllowed < static_cast<std::uint32_t>(adj.ownerCountDelta))
|
||||
return insufReserveCode;
|
||||
}
|
||||
|
||||
auto const sponsorBalance = (*sponsorSle)->getFieldAmount(sfBalance).xrp();
|
||||
XRPAmount const sponsorReserve = sponsorSle->reserve(adj);
|
||||
|
||||
if (sponsorBalance < sponsorReserve)
|
||||
return insufReserveCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPAmount const reserve = this->reserve(adj);
|
||||
if (accBalance < reserve)
|
||||
return insufReserveCode;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
!sponsorSle,
|
||||
"xrpl::checkReserve : featureSponsor disabled and sponsorSle not provided");
|
||||
XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::checkReserve : accountCountDelta is 0");
|
||||
auto const reserve = ctx.view.fees().accountReserve(
|
||||
(*this)->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1);
|
||||
if (accBalance < reserve)
|
||||
return insufReserveCode;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <typename ViewT>
|
||||
TER
|
||||
AccountRootEntry<ViewT>::checkReserve(ApplyViewContext ctx, XRPAmount accBalance, Adjustment adj)
|
||||
const
|
||||
{
|
||||
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, *this);
|
||||
if (!sponsorExp)
|
||||
return sponsorExp.error(); // LCOV_EXCL_LINE
|
||||
|
||||
std::optional<AccountRootEntry<ReadView>> sponsorSle;
|
||||
if (*sponsorExp)
|
||||
sponsorSle.emplace(**sponsorExp);
|
||||
return checkReserve(ctx, accBalance, sponsorSle, adj);
|
||||
}
|
||||
|
||||
template <typename ViewT>
|
||||
void
|
||||
AccountRootEntry<ViewT>::adjustOwnerCountSigned(
|
||||
std::optional<AccountRootEntry<ApplyView>>& sponsorSle,
|
||||
std::int32_t adjustment)
|
||||
requires Base::kIsWritable
|
||||
{
|
||||
auto& view = this->applyView();
|
||||
auto const j = this->journal();
|
||||
|
||||
if (view.rules().enabled(featureSponsor))
|
||||
{
|
||||
XRPL_ASSERT(this->exists(), "xrpl::adjustOwnerCountSigned : valid account sle");
|
||||
if (!this->exists())
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
auto const accountID = (*this)->getAccountID(sfAccount);
|
||||
bool const validType = (*this)->getType() == ltACCOUNT_ROOT;
|
||||
XRPL_ASSERT(validType, "xrpl::adjustOwnerCountSigned : valid account sle type");
|
||||
if (!validType)
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCountSigned : nonzero adjustment input");
|
||||
|
||||
OwnerCounts const currentOwnerCount(this->rawSle());
|
||||
OwnerCounts totalOwnerCount(currentOwnerCount);
|
||||
|
||||
if (sponsorSle)
|
||||
{
|
||||
bool const validSponsorType = (*sponsorSle)->getType() == ltACCOUNT_ROOT;
|
||||
XRPL_ASSERT(validSponsorType, "xrpl::adjustOwnerCountSigned : valid sponsor sle type");
|
||||
if (!validSponsorType)
|
||||
return; // LCOV_EXCL_LINE
|
||||
auto const sponsorID = (*sponsorSle)->getAccountID(sfAccount);
|
||||
|
||||
totalOwnerCount.sponsored = detail::adjustOwnerCountImpl(
|
||||
view, this->mutableRawSle(), sfSponsoredOwnerCount, accountID, adjustment, j);
|
||||
|
||||
{
|
||||
OwnerCounts const sponsorCurrent(sponsorSle->rawSle());
|
||||
OwnerCounts sponsorAdjustment(sponsorCurrent);
|
||||
sponsorAdjustment.sponsoring = detail::adjustOwnerCountImpl(
|
||||
view,
|
||||
sponsorSle->mutableRawSle(),
|
||||
sfSponsoringOwnerCount,
|
||||
sponsorID,
|
||||
adjustment,
|
||||
j);
|
||||
view.adjustOwnerCountHook(sponsorID, sponsorCurrent, sponsorAdjustment);
|
||||
}
|
||||
|
||||
auto sponsorshipSle = view.peek(keylet::sponsorship(sponsorID, accountID));
|
||||
if (sponsorshipSle && adjustment > 0)
|
||||
{
|
||||
// Only decrease the pre-funded ReserveCount on Sponsorship if we assign new
|
||||
// objects. Removing/reassigning ownership of the object doesn't increase
|
||||
// RemainingOwnerCount back. Don't call hook because this counter is not something
|
||||
// that requires reserve (like other sf...OwnerCounts do).
|
||||
detail::adjustOwnerCountImpl(
|
||||
view, sponsorshipSle, sfRemainingOwnerCount, sponsorID, -adjustment, j);
|
||||
}
|
||||
}
|
||||
|
||||
totalOwnerCount.owner = detail::adjustOwnerCountImpl(
|
||||
view, this->mutableRawSle(), sfOwnerCount, accountID, adjustment, j);
|
||||
view.adjustOwnerCountHook(accountID, currentOwnerCount, totalOwnerCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(this->exists(), "xrpl::adjustOwnerCountSigned : valid account sle");
|
||||
if (!this->exists())
|
||||
return;
|
||||
// the remaining are only asserts to preserve existing behavior
|
||||
XRPL_ASSERT(!sponsorSle, "xrpl::adjustOwnerCountSigned : sponsor not enabled");
|
||||
XRPL_ASSERT(
|
||||
(*this)->getType() == ltACCOUNT_ROOT,
|
||||
"xrpl::adjustOwnerCountSigned : valid account sle type");
|
||||
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input");
|
||||
std::uint32_t const current{(*this)->getFieldU32(sfOwnerCount)};
|
||||
AccountID const id = (**this)[sfAccount];
|
||||
std::uint32_t const adjusted = detail::confineOwnerCount(current, adjustment, id, j);
|
||||
|
||||
OwnerCounts const currentOwnerCount(this->rawSle());
|
||||
OwnerCounts finalOwnerCount(currentOwnerCount);
|
||||
finalOwnerCount.owner = adjusted;
|
||||
|
||||
view.adjustOwnerCountHook(id, currentOwnerCount, finalOwnerCount);
|
||||
(*this)->at(sfOwnerCount) = adjusted;
|
||||
this->update();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename ViewT>
|
||||
void
|
||||
AccountRootEntry<ViewT>::increaseOwnerCount(
|
||||
std::optional<AccountRootEntry<ApplyView>>& sponsorSle,
|
||||
std::uint32_t count)
|
||||
requires Base::kIsWritable
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
count != 0 && count <= std::numeric_limits<std::int32_t>::max(),
|
||||
"xrpl::increaseOwnerCount : count in signed delta range");
|
||||
if (count == 0 || count > std::numeric_limits<std::int32_t>::max())
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
adjustOwnerCountSigned(sponsorSle, static_cast<std::int32_t>(count));
|
||||
}
|
||||
|
||||
template <typename ViewT>
|
||||
void
|
||||
AccountRootEntry<ViewT>::increaseOwnerCount(ApplyViewContext ctx, std::uint32_t count)
|
||||
requires Base::kIsWritable
|
||||
{
|
||||
auto sponsorExp = getEffectiveTxReserveSponsor(ctx, *this);
|
||||
|
||||
// The sponsor's existence is validated by checkReserve/checkSponsor before
|
||||
// any owner-count mutation, so loading it here cannot fail.
|
||||
XRPL_ASSERT(
|
||||
sponsorExp.has_value(), "xrpl::increaseOwnerCount : sponsor validated before mutation");
|
||||
|
||||
std::optional<AccountRootEntry<ApplyView>> sponsorSle;
|
||||
if (sponsorExp && *sponsorExp)
|
||||
sponsorSle.emplace(std::move(**sponsorExp));
|
||||
|
||||
increaseOwnerCount(sponsorSle, count);
|
||||
}
|
||||
|
||||
template <typename ViewT>
|
||||
void
|
||||
AccountRootEntry<ViewT>::decreaseOwnerCount(
|
||||
std::optional<AccountRootEntry<ApplyView>>& sponsorSle,
|
||||
std::uint32_t count)
|
||||
requires Base::kIsWritable
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
count != 0 && count <= std::numeric_limits<std::int32_t>::max(),
|
||||
"xrpl::decreaseOwnerCount : count in signed delta range");
|
||||
if (count == 0 || count > std::numeric_limits<std::int32_t>::max())
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
adjustOwnerCountSigned(sponsorSle, -static_cast<std::int32_t>(count));
|
||||
}
|
||||
|
||||
template class AccountRootEntry<ReadView>;
|
||||
template class AccountRootEntry<ApplyView>;
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -641,7 +642,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
|
||||
[&](Issue const& issue) {
|
||||
if (isXRP(issue))
|
||||
{
|
||||
if (auto const sle = view.read(keylet::account(ammAccountID)))
|
||||
if (auto const sle = AccountRootEntryR(ammAccountID, view))
|
||||
return (*sle)[sfBalance];
|
||||
}
|
||||
else if (
|
||||
@@ -757,7 +758,7 @@ deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Jo
|
||||
}
|
||||
|
||||
auto const ammAccountID = (*ammSle)[sfAccount];
|
||||
auto sleAMMRoot = sb.peek(keylet::account(ammAccountID));
|
||||
auto sleAMMRoot = AccountRootEntryW(ammAccountID, sb);
|
||||
if (!sleAMMRoot)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -795,7 +796,7 @@ deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Jo
|
||||
}
|
||||
|
||||
sb.erase(ammSle);
|
||||
sb.erase(sleAMMRoot);
|
||||
sleAMMRoot.erase();
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OwnerCounts.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -17,7 +18,6 @@
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
@@ -38,25 +38,19 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer)
|
||||
{
|
||||
if (isXRP(issuer))
|
||||
return false;
|
||||
if (auto const sle = view.read(keylet::account(issuer)))
|
||||
if (auto const sle = AccountRootEntryR(issuer, view))
|
||||
return sle->isFlag(lsfGlobalFreeze);
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
namespace detail {
|
||||
|
||||
// An owner count cannot be negative. If adjustment would cause a negative
|
||||
// owner count, clamp the owner count at 0. Similarly for overflow. This
|
||||
// adjustment allows the ownerCount to be adjusted up or down in multiple steps.
|
||||
// If id != std::nullopt, then do error reporting.
|
||||
//
|
||||
// Returns adjusted owner count.
|
||||
std::uint32_t
|
||||
confineOwnerCount(
|
||||
std::uint32_t currentOwnerCount,
|
||||
std::int32_t ownerCountAdj,
|
||||
std::optional<AccountID> const& id = std::nullopt,
|
||||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
|
||||
std::optional<AccountID> const& id,
|
||||
beast::Journal j)
|
||||
{
|
||||
std::uint32_t totalOwnerCount{currentOwnerCount + ownerCountAdj};
|
||||
if (ownerCountAdj > 0)
|
||||
@@ -91,10 +85,8 @@ confineOwnerCount(
|
||||
return totalOwnerCount;
|
||||
}
|
||||
|
||||
// Returns the number of account reserves funded by this account: 1 for itself (0 if sponsored by
|
||||
// another account) plus the count of accounts it sponsors.
|
||||
std::uint32_t
|
||||
accountCountImpl(SLE::ConstRef sle, std::int32_t accountCountAdj, beast::Journal j)
|
||||
accountCountImpl(AccountRootEntryR const& sle, std::int32_t accountCountAdj, beast::Journal j)
|
||||
{
|
||||
bool const isSponsored = sle->isFieldPresent(sfSponsor);
|
||||
std::int64_t const sponsoringAccountCount = sle->getFieldU32(sfSponsoringAccountCount);
|
||||
@@ -131,151 +123,25 @@ adjustOwnerCountImpl(
|
||||
{
|
||||
std::uint32_t const currentOwnerCount = sle->at(sfield);
|
||||
std::uint32_t const totalOwnerCount =
|
||||
confineOwnerCount(currentOwnerCount, ownerCountAdj, accID, j);
|
||||
detail::confineOwnerCount(currentOwnerCount, ownerCountAdj, accID, j);
|
||||
sle->at(sfield) = totalOwnerCount;
|
||||
view.update(sle);
|
||||
return totalOwnerCount;
|
||||
}
|
||||
|
||||
void
|
||||
adjustOwnerCountSigned(
|
||||
ApplyView& view,
|
||||
SLE::Ref accountSle,
|
||||
SLE::Ref sponsorSle,
|
||||
std::int32_t adjustment,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (view.rules().enabled(featureSponsor))
|
||||
{
|
||||
XRPL_ASSERT(accountSle, "xrpl::adjustOwnerCountSigned : valid account sle");
|
||||
if (!accountSle)
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
auto const accountID = accountSle->getAccountID(sfAccount);
|
||||
bool const validType = accountSle->getType() == ltACCOUNT_ROOT;
|
||||
XRPL_ASSERT(validType, "xrpl::adjustOwnerCountSigned : valid account sle type");
|
||||
if (!validType)
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCountSigned : nonzero adjustment input");
|
||||
|
||||
OwnerCounts const currentOwnerCount(accountSle);
|
||||
OwnerCounts totalOwnerCount(currentOwnerCount);
|
||||
|
||||
if (sponsorSle)
|
||||
{
|
||||
bool const validSponsorType = sponsorSle->getType() == ltACCOUNT_ROOT;
|
||||
XRPL_ASSERT(validSponsorType, "xrpl::adjustOwnerCountSigned : valid sponsor sle type");
|
||||
if (!validSponsorType)
|
||||
return; // LCOV_EXCL_LINE
|
||||
auto const sponsorID = sponsorSle->getAccountID(sfAccount);
|
||||
|
||||
totalOwnerCount.sponsored = adjustOwnerCountImpl(
|
||||
view, accountSle, sfSponsoredOwnerCount, accountID, adjustment, j);
|
||||
|
||||
{
|
||||
OwnerCounts const sponsorCurrent(sponsorSle);
|
||||
OwnerCounts sponsorAdjustment(sponsorCurrent);
|
||||
sponsorAdjustment.sponsoring = adjustOwnerCountImpl(
|
||||
view, sponsorSle, sfSponsoringOwnerCount, sponsorID, adjustment, j);
|
||||
view.adjustOwnerCountHook(sponsorID, sponsorCurrent, sponsorAdjustment);
|
||||
}
|
||||
|
||||
auto sponsorshipSle = view.peek(keylet::sponsorship(sponsorID, accountID));
|
||||
if (sponsorshipSle && adjustment > 0)
|
||||
{
|
||||
// Only decrease the pre-funded ReserveCount on Sponsorship if we assign new
|
||||
// objects. Removing/reassigning ownership of the object doesn't increase
|
||||
// RemainingOwnerCount back. Don't call hook because this counter is not something
|
||||
// that requires reserve (like other sf...OwnerCounts do).
|
||||
adjustOwnerCountImpl(
|
||||
view, sponsorshipSle, sfRemainingOwnerCount, sponsorID, -adjustment, j);
|
||||
}
|
||||
}
|
||||
|
||||
totalOwnerCount.owner =
|
||||
adjustOwnerCountImpl(view, accountSle, sfOwnerCount, accountID, adjustment, j);
|
||||
view.adjustOwnerCountHook(accountID, currentOwnerCount, totalOwnerCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(accountSle, "xrpl::adjustOwnerCountSigned : valid account sle");
|
||||
if (!accountSle)
|
||||
return;
|
||||
// the remaining are only asserts to preserve existing behavior
|
||||
XRPL_ASSERT(sponsorSle == nullptr, "xrpl::adjustOwnerCountSigned : sponsor not enabled");
|
||||
XRPL_ASSERT(
|
||||
accountSle->getType() == ltACCOUNT_ROOT,
|
||||
"xrpl::adjustOwnerCountSigned : valid account sle type");
|
||||
XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input");
|
||||
std::uint32_t const current{accountSle->getFieldU32(sfOwnerCount)};
|
||||
AccountID const id = (*accountSle)[sfAccount];
|
||||
std::uint32_t const adjusted = confineOwnerCount(current, adjustment, id, j);
|
||||
|
||||
OwnerCounts const currentOwnerCount(accountSle);
|
||||
OwnerCounts finalOwnerCount(currentOwnerCount);
|
||||
finalOwnerCount.owner = adjusted;
|
||||
|
||||
view.adjustOwnerCountHook(id, currentOwnerCount, finalOwnerCount);
|
||||
accountSle->at(sfOwnerCount) = adjusted;
|
||||
view.update(accountSle);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::uint32_t
|
||||
ownerCount(SLE::ConstRef sle, beast::Journal j, std::int32_t ownerCountAdj)
|
||||
{
|
||||
XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::ownerCount : sle is account root");
|
||||
|
||||
AccountID const id = sle->getAccountID(sfAccount);
|
||||
std::uint32_t const currentOwnerCount = sle->at(sfOwnerCount);
|
||||
std::uint32_t const sponsoredOwnerCount = sle->at(sfSponsoredOwnerCount);
|
||||
std::uint32_t const sponsoringOwnerCount = sle->at(sfSponsoringOwnerCount);
|
||||
|
||||
XRPL_ASSERT(
|
||||
currentOwnerCount >= sponsoredOwnerCount,
|
||||
"xrpl::ownerCount : OwnerCount must be greater than or equal to SponsoredOwnerCount");
|
||||
|
||||
std::int64_t deltaCount =
|
||||
static_cast<std::int64_t>(ownerCountAdj) - sponsoredOwnerCount + sponsoringOwnerCount;
|
||||
|
||||
if (deltaCount > std::numeric_limits<std::int32_t>::max())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
deltaCount = std::numeric_limits<std::int32_t>::max();
|
||||
JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, "
|
||||
<< "adjustment: " << ownerCountAdj
|
||||
<< ", sponsoredCount: " << sponsoredOwnerCount
|
||||
<< ", sponsoringOwnerCount: " << sponsoringOwnerCount;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
else if (deltaCount < std::numeric_limits<std::int32_t>::min())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
deltaCount = std::numeric_limits<std::int32_t>::min();
|
||||
JLOG(j.fatal()) << "Account " << id << " delta count is below min, "
|
||||
<< "adjustment: " << ownerCountAdj
|
||||
<< ", sponsoredCount: " << sponsoredOwnerCount
|
||||
<< ", sponsoringCount: " << sponsoringOwnerCount;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return confineOwnerCount(currentOwnerCount, deltaCount);
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
XRPAmount
|
||||
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j)
|
||||
{
|
||||
auto const sle = view.read(keylet::account(id));
|
||||
if (sle == nullptr)
|
||||
auto const sle = AccountRootEntryR(id, view);
|
||||
if (!sle.exists())
|
||||
return beast::kZero;
|
||||
|
||||
// Return balance minus reserve
|
||||
std::uint32_t const currentOwnerCount =
|
||||
confineOwnerCount(view.ownerCountHook(id, OwnerCounts(sle)).count(), ownerCountAdj);
|
||||
std::uint32_t const currentAccountCount = accountCountImpl(sle, 0, j);
|
||||
std::uint32_t const currentOwnerCount = detail::confineOwnerCount(
|
||||
view.ownerCountHook(id, OwnerCounts(sle.rawSle())).count(), ownerCountAdj);
|
||||
std::uint32_t const currentAccountCount = detail::accountCountImpl(sle, 0, j);
|
||||
|
||||
// Pseudo-accounts have no reserve requirement
|
||||
auto const reserve = isPseudoAccount(sle)
|
||||
@@ -300,7 +166,7 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj,
|
||||
Rate
|
||||
transferRate(ReadView const& view, AccountID const& issuer)
|
||||
{
|
||||
auto const sle = view.read(keylet::account(issuer));
|
||||
auto const sle = AccountRootEntryR(issuer, view);
|
||||
|
||||
if (sle && sle->isFieldPresent(sfTransferRate))
|
||||
return Rate{sle->getFieldU32(sfTransferRate)};
|
||||
@@ -308,57 +174,10 @@ transferRate(ReadView const& view, AccountID const& issuer)
|
||||
return kParityRate;
|
||||
}
|
||||
|
||||
void
|
||||
increaseOwnerCount(
|
||||
ApplyView& view,
|
||||
SLE::Ref accountSle,
|
||||
SLE::Ref sponsorSle,
|
||||
std::uint32_t count,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
count != 0 && count <= std::numeric_limits<std::int32_t>::max(),
|
||||
"xrpl::increaseOwnerCount : count in signed delta range");
|
||||
if (count == 0 || count > std::numeric_limits<std::int32_t>::max())
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
adjustOwnerCountSigned(view, accountSle, sponsorSle, static_cast<std::int32_t>(count), j);
|
||||
}
|
||||
|
||||
void
|
||||
increaseOwnerCount(ApplyViewContext ctx, SLE::Ref accountSle, std::uint32_t count, beast::Journal j)
|
||||
{
|
||||
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, accountSle);
|
||||
|
||||
// The sponsor's existence is validated by checkReserve/checkSponsor before
|
||||
// any owner-count mutation, so loading it here cannot fail.
|
||||
XRPL_ASSERT(
|
||||
sponsorExp.has_value(), "xrpl::increaseOwnerCount : sponsor validated before mutation");
|
||||
|
||||
increaseOwnerCount(ctx.view, accountSle, sponsorExp ? *sponsorExp : SLE::pointer(), count, j);
|
||||
}
|
||||
|
||||
void
|
||||
decreaseOwnerCount(
|
||||
ApplyView& view,
|
||||
SLE::Ref accountSle,
|
||||
SLE::Ref sponsorSle,
|
||||
std::uint32_t count,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
count != 0 && count <= std::numeric_limits<std::int32_t>::max(),
|
||||
"xrpl::decreaseOwnerCount : count in signed delta range");
|
||||
if (count == 0 || count > std::numeric_limits<std::int32_t>::max())
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
adjustOwnerCountSigned(view, accountSle, sponsorSle, -static_cast<std::int32_t>(count), j);
|
||||
}
|
||||
|
||||
void
|
||||
decreaseOwnerCountForObject(
|
||||
ApplyView& view,
|
||||
SLE::Ref accountSle,
|
||||
AccountRootEntryW& accountSle,
|
||||
SLE::Ref objectSle,
|
||||
std::uint32_t count,
|
||||
beast::Journal j)
|
||||
@@ -372,8 +191,8 @@ decreaseOwnerCountForObject(
|
||||
if (!validObjectType)
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
SLE::Ref sponsorSle = getLedgerEntryReserveSponsor(view, objectSle);
|
||||
decreaseOwnerCount(view, accountSle, sponsorSle, count, j);
|
||||
auto sponsorSle = getLedgerEntryReserveSponsor(view, objectSle);
|
||||
accountSle.decreaseOwnerCount(sponsorSle, count);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -393,107 +212,10 @@ adjustLoanBrokerOwnerCount(
|
||||
if (delta == 0)
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
adjustOwnerCountImpl(
|
||||
detail::adjustOwnerCountImpl(
|
||||
view, brokerSle, sfOwnerCount, brokerSle->getAccountID(sfAccount), delta, j);
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
accountReserve(ReadView const& view, SLE::ConstRef sle, beast::Journal j, Adjustment adj)
|
||||
{
|
||||
XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::accountReserve : valid sle");
|
||||
|
||||
if (!view.rules().enabled(featureSponsor))
|
||||
{
|
||||
XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::accountReserve : no account count delta");
|
||||
return view.fees().accountReserve(sle->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1);
|
||||
}
|
||||
std::uint32_t const currentOwnerCount = ownerCount(sle, j, adj.ownerCountDelta);
|
||||
std::uint32_t const currentAccountCount = accountCountImpl(sle, adj.accountCountDelta, j);
|
||||
|
||||
return view.fees().accountReserve(currentOwnerCount, currentAccountCount);
|
||||
}
|
||||
|
||||
TER
|
||||
checkReserve(
|
||||
ApplyViewContext ctx,
|
||||
SLE::ConstRef accSle,
|
||||
XRPAmount accBalance,
|
||||
SLE::ConstRef sponsorSle,
|
||||
Adjustment adj,
|
||||
beast::Journal j,
|
||||
TER insufReserveCode)
|
||||
{
|
||||
// TODO: swap to assert after fixCleanup3_2_0 is retired
|
||||
if (!accSle || accSle->getType() != ltACCOUNT_ROOT)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
XRPL_ASSERT(
|
||||
!isTesSuccess(insufReserveCode), "xrpl::checkReserve : insufReserveCode is not tesSUCCESS");
|
||||
if (ctx.view.rules().enabled(featureSponsor))
|
||||
{
|
||||
if (sponsorSle)
|
||||
{
|
||||
if (sponsorSle->getType() != ltACCOUNT_ROOT)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sle = ctx.view.read(
|
||||
keylet::sponsorship(
|
||||
sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount)));
|
||||
|
||||
// A reserve-sponsored tx must carry a sponsor signature
|
||||
// (cosigning path) and/or have a pre-existing sponsorship SLE
|
||||
// (prefunded path). Absence of both is an internal invariant break.
|
||||
if (isReserveSponsored(ctx.tx) && !sle && !ctx.tx.isFieldPresent(sfSponsorSignature))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (sle)
|
||||
{
|
||||
auto const ownerCountAllowed = sle->getFieldU32(sfRemainingOwnerCount);
|
||||
if (adj.ownerCountDelta > 0 &&
|
||||
ownerCountAllowed < static_cast<std::uint32_t>(adj.ownerCountDelta))
|
||||
return insufReserveCode;
|
||||
}
|
||||
|
||||
auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance).xrp();
|
||||
XRPAmount const sponsorReserve = accountReserve(ctx.view, sponsorSle, j, adj);
|
||||
|
||||
if (sponsorBalance < sponsorReserve)
|
||||
return insufReserveCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPAmount const reserve = accountReserve(ctx.view, accSle, j, adj);
|
||||
if (accBalance < reserve)
|
||||
return insufReserveCode;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
!sponsorSle,
|
||||
"xrpl::checkReserve : featureSponsor disabled and sponsorSle not provided");
|
||||
XRPL_ASSERT(adj.accountCountDelta == 0, "xrpl::checkReserve : accountCountDelta is 0");
|
||||
auto const reserve = ctx.view.fees().accountReserve(
|
||||
accSle->getFieldU32(sfOwnerCount) + adj.ownerCountDelta, 1);
|
||||
if (accBalance < reserve)
|
||||
return insufReserveCode;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
checkReserve(
|
||||
ApplyViewContext ctx,
|
||||
SLE::ConstRef accSle,
|
||||
XRPAmount accBalance,
|
||||
Adjustment adj,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, accSle);
|
||||
if (!sponsorExp)
|
||||
return sponsorExp.error(); // LCOV_EXCL_LINE
|
||||
return checkReserve(ctx, accSle, accBalance, *sponsorExp, adj, j);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
AccountID
|
||||
@@ -507,7 +229,7 @@ pseudoAccountAddress(ReadView const& view, UInt256 const& pseudoOwnerKey)
|
||||
auto const hash = sha512Half(i, view.header().parentHash, pseudoOwnerKey);
|
||||
rsh(hash.data(), hash.size());
|
||||
AccountID const ret = AccountID::fromRaw(static_cast<RipeshaHasher::result_type>(rsh));
|
||||
if (!view.read(keylet::account(ret)))
|
||||
if (!AccountRootEntryR(ret, view).exists())
|
||||
return ret;
|
||||
}
|
||||
return beast::kZero;
|
||||
@@ -546,7 +268,7 @@ getPseudoAccountFields()
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isPseudoAccount(SLE::const_pointer sleAcct)
|
||||
isPseudoAccount(AccountRootEntryR const& sleAcct)
|
||||
{
|
||||
// Intentionally use defensive coding here because it's cheap and makes the
|
||||
// semantics of true return value clean.
|
||||
@@ -599,9 +321,9 @@ createPseudoAccount(ApplyView& view, UInt256 const& pseudoOwnerKey, SField const
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkDestinationAndTag(SLE::ConstRef toSle, bool hasDestinationTag)
|
||||
checkDestinationAndTag(AccountRootEntryR const& toSle, bool hasDestinationTag)
|
||||
{
|
||||
if (toSle == nullptr)
|
||||
if (!toSle.exists())
|
||||
return tecNO_DST;
|
||||
|
||||
// The tag is basically account-specific information we don't
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -81,7 +82,7 @@ deleteSLE(ApplyView& view, SLE::Ref sleCredential, beast::Journal j)
|
||||
|
||||
auto delSLE = [&view, &sleCredential, j](
|
||||
AccountID const& account, SField const& node, bool isOwner) -> TER {
|
||||
auto const sleAccount = view.peek(keylet::account(account));
|
||||
auto sleAccount = AccountRootEntryW(account, view);
|
||||
if (!sleAccount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
@@ -206,7 +207,7 @@ authorizeMPToken(
|
||||
std::uint32_t flags,
|
||||
std::optional<AccountID> holderID)
|
||||
{
|
||||
auto const sleAcct = ctx.view.peek(keylet::account(account));
|
||||
auto sleAcct = AccountRootEntryW(account, ctx.view, journal);
|
||||
if (!sleAcct)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -242,10 +243,10 @@ authorizeMPToken(
|
||||
// - create the MPToken object for the holder
|
||||
|
||||
// A reserve sponsor only covers tx.Account's own objects.
|
||||
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, sleAcct);
|
||||
auto sponsorExp = getEffectiveTxReserveSponsor(ctx, sleAcct);
|
||||
if (!sponsorExp)
|
||||
return sponsorExp.error(); // LCOV_EXCL_LINE
|
||||
auto const sponsorSle = *sponsorExp;
|
||||
auto& sponsorSle = *sponsorExp;
|
||||
|
||||
// The reserve that is required to create the MPToken. Note
|
||||
// that although the reserve increases with every item
|
||||
@@ -255,10 +256,10 @@ authorizeMPToken(
|
||||
// The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on
|
||||
// the tx — the sponsor must always cover the reserve (via balance or prefunded
|
||||
// budget), so this check always runs for sponsored transactions.
|
||||
if (sponsorSle || ownerCount(sleAcct, journal) >= 2)
|
||||
if (sponsorSle || sleAcct.ownerCount() >= 2)
|
||||
{
|
||||
if (auto const ret = checkReserve(
|
||||
ctx, sleAcct, priorBalance, sponsorSle, {.ownerCountDelta = 1}, journal);
|
||||
if (auto const ret =
|
||||
sleAcct.checkReserve(ctx, priorBalance, sponsorSle, {.ownerCountDelta = 1});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -285,8 +286,8 @@ authorizeMPToken(
|
||||
ctx.view.insert(mptoken);
|
||||
|
||||
// Update owner count.
|
||||
increaseOwnerCount(ctx.view, sleAcct, sponsorSle, 1, journal);
|
||||
addSponsorToLedgerEntry(mptoken, sponsorSle);
|
||||
sleAcct.increaseOwnerCount(sponsorSle, 1);
|
||||
addSponsorToLedgerEntry(mptoken, sponsorSle ? sponsorSle->rawSle() : SLE::const_pointer{});
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -415,7 +416,7 @@ requireAuth(
|
||||
}
|
||||
|
||||
// requireAuth is recursive if the issuer is a vault pseudo-account
|
||||
auto const sleIssuer = view.read(keylet::account(mptIssuer));
|
||||
auto const sleIssuer = AccountRootEntryR(mptIssuer, view);
|
||||
if (!sleIssuer)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -1020,13 +1021,16 @@ checkCreateMPT(
|
||||
{
|
||||
return err;
|
||||
}
|
||||
auto const sleAcct = view.peek(keylet::account(holder));
|
||||
auto sleAcct = AccountRootEntryW(holder, view, j);
|
||||
if (!sleAcct)
|
||||
{
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
increaseOwnerCount(view, sleAcct, sponsorSle, 1, j);
|
||||
std::optional<AccountRootEntryW> sponsorAccSle;
|
||||
if (sponsorSle)
|
||||
sponsorAccSle.emplace(sponsorSle->getAccountID(sfAccount), view);
|
||||
sleAcct.increaseOwnerCount(sponsorAccSle, 1);
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
@@ -855,7 +856,7 @@ tokenOfferCreatePreclaim(
|
||||
|
||||
if (nftIssuer != acctID && ((nftFlags & nft::kFlagTransferable) == 0))
|
||||
{
|
||||
auto const root = view.read(keylet::account(nftIssuer));
|
||||
auto const root = AccountRootEntryR(nftIssuer, view);
|
||||
XRPL_ASSERT(root, "xrpl::nft::tokenOfferCreatePreclaim : non-null account");
|
||||
|
||||
if (auto minter = (*root)[~sfNFTokenMinter]; minter != acctID)
|
||||
@@ -886,7 +887,7 @@ tokenOfferCreatePreclaim(
|
||||
{
|
||||
// If a destination is specified, the destination must already be in
|
||||
// the ledger.
|
||||
auto const sleDst = view.read(keylet::account(*dest));
|
||||
auto const sleDst = AccountRootEntryR(*dest, view);
|
||||
|
||||
if (!sleDst)
|
||||
return tecNO_DST;
|
||||
@@ -898,7 +899,7 @@ tokenOfferCreatePreclaim(
|
||||
|
||||
if (owner)
|
||||
{
|
||||
auto const sleOwner = view.read(keylet::account(*owner));
|
||||
auto const sleOwner = AccountRootEntryR(*owner, view);
|
||||
|
||||
// defensively check
|
||||
// it should not be possible to specify owner that doesn't exist
|
||||
@@ -937,9 +938,8 @@ tokenOfferCreateApply(
|
||||
beast::Journal j,
|
||||
std::uint32_t txFlags)
|
||||
{
|
||||
Keylet const acctKeylet = keylet::account(acctID);
|
||||
if (auto const acct = view.read(acctKeylet);
|
||||
priorBalance < accountReserve(view, acct, j, {.ownerCountDelta = 1}))
|
||||
if (auto const acct = AccountRootEntryW(acctID, view, j);
|
||||
priorBalance < acct.reserve({.ownerCountDelta = 1}))
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
auto const offerID = keylet::nftokenOffer(acctID, seqProxy);
|
||||
@@ -1008,7 +1008,7 @@ checkTrustlineAuthorized(
|
||||
|
||||
if (view.rules().enabled(fixEnforceNFTokenTrustlineV2))
|
||||
{
|
||||
auto const issuerAccount = view.read(keylet::account(issue.account));
|
||||
auto const issuerAccount = AccountRootEntryR(issue.account, view);
|
||||
if (!issuerAccount)
|
||||
{
|
||||
JLOG(j.debug()) << "xrpl::nft::checkTrustlineAuthorized: can't "
|
||||
@@ -1060,7 +1060,7 @@ checkTrustlineDeepFrozen(
|
||||
|
||||
if (view.rules().enabled(featureDeepFreeze))
|
||||
{
|
||||
auto const issuerAccount = view.read(keylet::account(issue.account));
|
||||
auto const issuerAccount = AccountRootEntryR(issue.account, view);
|
||||
if (!issuerAccount)
|
||||
{
|
||||
JLOG(j.debug()) << "xrpl::nft::checkTrustlineDeepFrozen: can't "
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -51,7 +52,7 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
|
||||
}
|
||||
|
||||
// Transfer amount back to owner, decrement owner count
|
||||
auto const sle = view.peek(keylet::account(src));
|
||||
auto sle = AccountRootEntryW(src, view);
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -59,7 +60,7 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
|
||||
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
|
||||
decreaseOwnerCountForObject(view, sle, slep, 1, j);
|
||||
view.update(sle);
|
||||
sle.update();
|
||||
|
||||
// Remove PayChan from ledger
|
||||
view.erase(slep);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -133,14 +134,14 @@ isFrozen(
|
||||
{
|
||||
if (isXRP(currency))
|
||||
return false;
|
||||
auto sle = view.read(keylet::account(issuer));
|
||||
if (sle && sle->isFlag(lsfGlobalFreeze))
|
||||
auto const sleIssuer = AccountRootEntryR(issuer, view);
|
||||
if (sleIssuer && sleIssuer->isFlag(lsfGlobalFreeze))
|
||||
return true;
|
||||
if (issuer != account)
|
||||
{
|
||||
// Check if the issuer froze the line
|
||||
sle = view.read(keylet::trustLine(account, issuer, currency));
|
||||
if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
|
||||
auto const sleLine = view.read(keylet::trustLine(account, issuer, currency));
|
||||
if (sleLine && sleLine->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -238,7 +239,7 @@ trustCreate(
|
||||
XRPL_ASSERT(
|
||||
sleAccount->getAccountID(sfAccount) == (bSetHigh ? uHighAccountID : uLowAccountID),
|
||||
"xrpl::trustCreate : matching account ID");
|
||||
auto const slePeer = view.peek(keylet::account(bSetHigh ? uLowAccountID : uHighAccountID));
|
||||
auto const slePeer = AccountRootEntryW(bSetHigh ? uLowAccountID : uHighAccountID, view);
|
||||
if (!slePeer)
|
||||
return tecNO_TARGET;
|
||||
|
||||
@@ -283,7 +284,11 @@ trustCreate(
|
||||
}
|
||||
|
||||
sleRippleState->setFieldU32(sfFlags, uFlags);
|
||||
increaseOwnerCount(view, sleAccount, sponsorSle, 1, j);
|
||||
AccountRootEntryW accountSle(bSetHigh ? uHighAccountID : uLowAccountID, view, j);
|
||||
std::optional<AccountRootEntryW> sponsorAccSle;
|
||||
if (sponsorSle)
|
||||
sponsorAccSle.emplace(sponsorSle->getAccountID(sfAccount), view);
|
||||
accountSle.increaseOwnerCount(sponsorAccSle, 1);
|
||||
|
||||
addSponsorToLedgerEntry(sleRippleState, sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor);
|
||||
|
||||
@@ -349,7 +354,7 @@ updateTrustLine(
|
||||
if (!state)
|
||||
return false;
|
||||
|
||||
auto sle = view.peek(keylet::account(sender));
|
||||
auto sle = AccountRootEntryW(sender, view, j);
|
||||
if (!sle)
|
||||
return false;
|
||||
|
||||
@@ -376,9 +381,9 @@ updateTrustLine(
|
||||
{
|
||||
// VFALCO Where is the line being deleted?
|
||||
// Clear the reserve of the sender, possibly delete the line!
|
||||
auto const currentSponsor =
|
||||
auto currentSponsor =
|
||||
getLedgerEntryReserveSponsor(view, state, bSenderHigh ? sfHighSponsor : sfLowSponsor);
|
||||
decreaseOwnerCount(view, sle, currentSponsor, 1, j);
|
||||
sle.decreaseOwnerCount(currentSponsor, 1);
|
||||
|
||||
// Clear reserve flag.
|
||||
state->clearFlag(senderReserveFlag);
|
||||
@@ -464,7 +469,7 @@ issueIOU(
|
||||
|
||||
finalBalance.get<Issue>().account = noAccount();
|
||||
|
||||
auto const receiverAccount = view.peek(keylet::account(account));
|
||||
auto receiverAccount = AccountRootEntryW(account, view);
|
||||
if (!receiverAccount)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -476,7 +481,7 @@ issueIOU(
|
||||
issue.account,
|
||||
account,
|
||||
index.key,
|
||||
receiverAccount,
|
||||
receiverAccount.mutableRawSle(),
|
||||
false,
|
||||
noRipple,
|
||||
false,
|
||||
@@ -579,7 +584,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
|
||||
|
||||
// If this is a weak or legacy check, or if the account has a line, fail if
|
||||
// auth is required and not set on the line
|
||||
if (auto const issuerAccount = view.read(keylet::account(issue.account));
|
||||
if (auto const issuerAccount = AccountRootEntryR(issue.account, view);
|
||||
issuerAccount && issuerAccount->isFlag(lsfRequireAuth))
|
||||
{
|
||||
if (trustLine)
|
||||
@@ -609,8 +614,8 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
|
||||
auto const& issuerId = issue.getIssuer();
|
||||
if (issuerId == from || issuerId == to)
|
||||
return tesSUCCESS;
|
||||
auto const sleIssuer = view.read(keylet::account(issuerId));
|
||||
if (sleIssuer == nullptr)
|
||||
auto const sleIssuer = AccountRootEntryR(issuerId, view);
|
||||
if (!sleIssuer.exists())
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const isRippleDisabled = [&](AccountID account) -> bool {
|
||||
@@ -665,8 +670,8 @@ addEmptyHolding(
|
||||
if (isGlobalFrozen(ctx.view, issuerId))
|
||||
return tecFROZEN; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sleSrc = ctx.view.peek(keylet::account(srcId));
|
||||
auto const sleDst = ctx.view.peek(keylet::account(dstId));
|
||||
auto const sleSrc = AccountRootEntryW(srcId, ctx.view);
|
||||
auto sleDst = AccountRootEntryW(dstId, ctx.view, journal);
|
||||
if (!sleDst || !sleSrc)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
// Create path: DefaultRipple is still required. terNO_RIPPLE is
|
||||
@@ -681,20 +686,14 @@ addEmptyHolding(
|
||||
return tecDUPLICATE;
|
||||
|
||||
// A reserve sponsor only covers tx.Account's own objects.
|
||||
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, sleDst);
|
||||
auto sponsorExp = getEffectiveTxReserveSponsor(ctx, sleDst);
|
||||
if (!sponsorExp)
|
||||
return sponsorExp.error(); // LCOV_EXCL_LINE
|
||||
auto const sponsorSle = *sponsorExp;
|
||||
auto& sponsorSle = *sponsorExp;
|
||||
|
||||
// Can the account cover the trust line reserve ?
|
||||
if (auto const ret = checkReserve(
|
||||
ctx,
|
||||
sleDst,
|
||||
priorBalance,
|
||||
sponsorSle,
|
||||
{.ownerCountDelta = 1},
|
||||
journal,
|
||||
tecNO_LINE_INSUF_RESERVE);
|
||||
if (auto const ret = sleDst.checkReserve(
|
||||
ctx, priorBalance, sponsorSle, {.ownerCountDelta = 1}, tecNO_LINE_INSUF_RESERVE);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
return ret;
|
||||
@@ -706,7 +705,7 @@ addEmptyHolding(
|
||||
srcId,
|
||||
dstId,
|
||||
index.key,
|
||||
sleDst,
|
||||
sleDst.mutableRawSle(),
|
||||
/*bAuth=*/false,
|
||||
/*bNoRipple=*/true,
|
||||
/*bFreeze=*/false,
|
||||
@@ -715,7 +714,7 @@ addEmptyHolding(
|
||||
/*saLimit=*/STAmount{Issue{currency, dstId}},
|
||||
/*uQualityIn=*/0,
|
||||
/*uQualityOut=*/0,
|
||||
sponsorSle,
|
||||
sponsorSle ? sponsorSle->mutableRawSle() : SLE::pointer{},
|
||||
journal);
|
||||
}
|
||||
|
||||
@@ -728,7 +727,7 @@ removeEmptyHolding(
|
||||
{
|
||||
if (issue.native())
|
||||
{
|
||||
auto const sle = ctx.view.read(keylet::account(accountID));
|
||||
auto const sle = AccountRootEntryW(accountID, ctx.view);
|
||||
if (!sle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -753,13 +752,14 @@ removeEmptyHolding(
|
||||
if (line->isFlag(lsfLowReserve))
|
||||
{
|
||||
// Clear reserve for low account.
|
||||
auto sleLowAccount = ctx.view.peek(keylet::account(line->at(sfLowLimit)->getIssuer()));
|
||||
auto sleLowAccount =
|
||||
AccountRootEntryW(line->at(sfLowLimit)->getIssuer(), ctx.view, journal);
|
||||
if (!sleLowAccount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const currentLowSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfLowSponsor);
|
||||
auto currentLowSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfLowSponsor);
|
||||
|
||||
decreaseOwnerCount(ctx.view, sleLowAccount, currentLowSponsor, 1, journal);
|
||||
sleLowAccount.decreaseOwnerCount(currentLowSponsor, 1);
|
||||
// It's not really necessary to clear the reserve flag, since the line
|
||||
// is about to be deleted, but this will make the metadata reflect an
|
||||
// accurate state at the time of deletion.
|
||||
@@ -770,13 +770,14 @@ removeEmptyHolding(
|
||||
if (line->isFlag(lsfHighReserve))
|
||||
{
|
||||
// Clear reserve for high account.
|
||||
auto sleHighAccount = ctx.view.peek(keylet::account(line->at(sfHighLimit)->getIssuer()));
|
||||
auto sleHighAccount =
|
||||
AccountRootEntryW(line->at(sfHighLimit)->getIssuer(), ctx.view, journal);
|
||||
if (!sleHighAccount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const currentHighSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfHighSponsor);
|
||||
auto currentHighSponsor = getLedgerEntryReserveSponsor(ctx.view, line, sfHighSponsor);
|
||||
|
||||
decreaseOwnerCount(ctx.view, sleHighAccount, currentHighSponsor, 1, journal);
|
||||
sleHighAccount.decreaseOwnerCount(currentHighSponsor, 1);
|
||||
// It's not really necessary to clear the reserve flag, since the line
|
||||
// is about to be deleted, but this will make the metadata reflect an
|
||||
// accurate state at the time of deletion.
|
||||
@@ -805,8 +806,8 @@ deleteAMMTrustLine(
|
||||
auto const& [low, high] = std::minmax(
|
||||
sleState->getFieldAmount(sfLowLimit).getIssuer(),
|
||||
sleState->getFieldAmount(sfHighLimit).getIssuer());
|
||||
auto sleLow = view.peek(keylet::account(low));
|
||||
auto sleHigh = view.peek(keylet::account(high));
|
||||
auto sleLow = AccountRootEntryW(low, view, j);
|
||||
auto sleHigh = AccountRootEntryW(high, view, j);
|
||||
if (!sleLow || !sleHigh)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -825,7 +826,7 @@ deleteAMMTrustLine(
|
||||
if (ammAccountID && (low != *ammAccountID && high != *ammAccountID))
|
||||
return terNO_AMM;
|
||||
|
||||
auto const sponsorSle =
|
||||
auto sponsorSle =
|
||||
getLedgerEntryReserveSponsor(view, sleState, !ammLow ? sfLowSponsor : sfHighSponsor);
|
||||
|
||||
if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter))
|
||||
@@ -838,7 +839,7 @@ deleteAMMTrustLine(
|
||||
if (!sleState->isFlag(uFlags))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
decreaseOwnerCount(view, !ammLow ? sleLow : sleHigh, sponsorSle, 1, j);
|
||||
(!ammLow ? sleLow : sleHigh).decreaseOwnerCount(sponsorSle, 1);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OracleHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
@@ -76,7 +77,7 @@ getTxReserveSponsorID(STTx const& tx)
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<SLE::pointer, TER>
|
||||
std::expected<std::optional<AccountRootEntryW>, TER>
|
||||
getTxReserveSponsor(ApplyViewContext ctx)
|
||||
{
|
||||
auto const sponsorID = getTxReserveSponsorID(ctx.tx);
|
||||
@@ -85,17 +86,17 @@ getTxReserveSponsor(ApplyViewContext ctx)
|
||||
XRPL_ASSERT(
|
||||
ctx.view.rules().enabled(featureSponsor),
|
||||
"xrpl::getTxReserveSponsor : sponsor exists + Sponsor enabled");
|
||||
auto sle = ctx.view.peek(keylet::account(*sponsorID));
|
||||
auto sle = AccountRootEntryW(*sponsorID, ctx.view);
|
||||
|
||||
// already checked in Transactor::checkSponsor
|
||||
if (!sle)
|
||||
if (!sle.exists())
|
||||
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
return sle;
|
||||
}
|
||||
return SLE::pointer();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::expected<SLE::const_pointer, TER>
|
||||
std::expected<std::optional<AccountRootEntryR>, TER>
|
||||
getTxReserveSponsor(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const sponsorID = getTxReserveSponsorID(tx);
|
||||
@@ -104,18 +105,18 @@ getTxReserveSponsor(ReadView const& view, STTx const& tx)
|
||||
XRPL_ASSERT(
|
||||
view.rules().enabled(featureSponsor),
|
||||
"xrpl::getTxReserveSponsor : sponsor exists + Sponsor enabled");
|
||||
auto sle = view.read(keylet::account(*sponsorID));
|
||||
auto sle = AccountRootEntryR(*sponsorID, view);
|
||||
|
||||
// already checked in Transactor::checkSponsor
|
||||
if (!sle)
|
||||
if (!sle.exists())
|
||||
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
return sle;
|
||||
}
|
||||
return SLE::pointer();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::expected<SLE::pointer, TER>
|
||||
getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::ConstRef accountSle)
|
||||
std::expected<std::optional<AccountRootEntryW>, TER>
|
||||
getEffectiveTxReserveSponsor(ApplyViewContext ctx, AccountRootEntryR const& accountSle)
|
||||
{
|
||||
// A reserve sponsor only covers tx.Account's own objects.
|
||||
if (ctx.view.rules().enabled(fixCleanup3_2_0))
|
||||
@@ -133,7 +134,7 @@ getEffectiveTxReserveSponsor(ApplyViewContext ctx, SLE::ConstRef accountSle)
|
||||
}
|
||||
|
||||
if (isPseudoAccount(accountSle) || accountSle->getAccountID(sfAccount) != ctx.tx[sfAccount])
|
||||
return SLE::pointer();
|
||||
return std::nullopt;
|
||||
return getTxReserveSponsor(ctx);
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ getLedgerEntryReserveSponsorID(SLE::ConstRef sle, SF_ACCOUNT const& field)
|
||||
return {};
|
||||
}
|
||||
|
||||
SLE::pointer
|
||||
std::optional<AccountRootEntryW>
|
||||
getLedgerEntryReserveSponsor(ApplyView& view, SLE::ConstRef sle, SF_ACCOUNT const& field)
|
||||
{
|
||||
auto const sponsorID = getLedgerEntryReserveSponsorID(sle, field);
|
||||
@@ -160,9 +161,9 @@ getLedgerEntryReserveSponsor(ApplyView& view, SLE::ConstRef sle, SF_ACCOUNT cons
|
||||
XRPL_ASSERT(
|
||||
view.rules().enabled(featureSponsor),
|
||||
"xrpl::getLedgerEntryReserveSponsor : sponsor exists + Sponsor enabled");
|
||||
return view.peek(keylet::account(*sponsorID));
|
||||
return AccountRootEntryW(*sponsorID, view);
|
||||
}
|
||||
return {};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -185,16 +186,16 @@ addSponsorToLedgerEntry(SLE::Ref sle, SLE::ConstRef sponsorSle, SF_ACCOUNT const
|
||||
void
|
||||
addSponsorToLedgerEntry(ApplyViewContext ctx, SLE::Ref sle, SF_ACCOUNT const& field)
|
||||
{
|
||||
// getTxReserveSponsor yields a null pointer when the tx is not
|
||||
// reserve-sponsored, so addSponsorToLedgerEntry becomes a no-op then. The
|
||||
// error case (tecINTERNAL) is an already-checked invariant; skip stamping.
|
||||
// getTxReserveSponsor yields nullopt when the tx is not reserve-sponsored,
|
||||
// so addSponsorToLedgerEntry becomes a no-op then. The error case
|
||||
// (tecINTERNAL) is an already-checked invariant; skip stamping.
|
||||
auto const sponsorSle = getTxReserveSponsor(ctx);
|
||||
if (sponsorSle && *sponsorSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
ctx.view.rules().enabled(featureSponsor),
|
||||
"xrpl::addSponsorToLedgerEntry : sponsor exists + Sponsor enabled");
|
||||
addSponsorToLedgerEntry(sle, *sponsorSle, field);
|
||||
addSponsorToLedgerEntry(sle, (**sponsorSle).rawSle(), field);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
@@ -293,7 +294,7 @@ getLineIfUsable(
|
||||
// we need to check if the associated assets have been frozen
|
||||
if (view.rules().enabled(fixFrozenLPTokenTransfer))
|
||||
{
|
||||
auto const sleIssuer = view.read(keylet::account(issuer));
|
||||
auto const sleIssuer = AccountRootEntryR(issuer, view);
|
||||
if (!sleIssuer)
|
||||
{
|
||||
return nullptr; // LCOV_EXCL_LINE
|
||||
@@ -562,7 +563,7 @@ canAddHolding(ReadView const& view, Issue const& issue)
|
||||
return tesSUCCESS; // No special checks for XRP
|
||||
}
|
||||
|
||||
auto const issuer = view.read(keylet::account(issue.getIssuer()));
|
||||
auto const issuer = AccountRootEntryR(issue.getIssuer(), view);
|
||||
if (!issuer)
|
||||
{
|
||||
return terNO_ACCOUNT;
|
||||
@@ -738,6 +739,8 @@ directSendNoFeeIOU(
|
||||
auto const senderFreezeFlag = bSenderHigh ? lsfHighFreeze : lsfLowFreeze;
|
||||
auto const receiverReserveFlag = bSenderHigh ? lsfLowReserve : lsfHighReserve;
|
||||
|
||||
AccountRootEntryW senderSle(uSenderID, view, j);
|
||||
|
||||
// FIXME This NEEDS to be cleaned up and simplified. It's impossible
|
||||
// for anyone to understand.
|
||||
if (saBefore > beast::kZero
|
||||
@@ -746,8 +749,7 @@ directSendNoFeeIOU(
|
||||
// Sender is zero or negative.
|
||||
&& sleRippleState->isFlag(senderReserveFlag)
|
||||
// Sender reserve is set.
|
||||
&& sleRippleState->isFlag(senderNoRippleFlag) !=
|
||||
view.read(keylet::account(uSenderID))->isFlag(lsfDefaultRipple) &&
|
||||
&& sleRippleState->isFlag(senderNoRippleFlag) != senderSle->isFlag(lsfDefaultRipple) &&
|
||||
!sleRippleState->isFlag(senderFreezeFlag) &&
|
||||
!sleRippleState->getFieldAmount(bSenderHigh ? sfHighLimit : sfLowLimit)
|
||||
// Sender trust limit is 0.
|
||||
@@ -758,9 +760,9 @@ directSendNoFeeIOU(
|
||||
// Sender quality out is 0.
|
||||
{
|
||||
// Clear the reserve of the sender, possibly delete the line!
|
||||
auto const currentSponsor = getLedgerEntryReserveSponsor(
|
||||
auto currentSponsor = getLedgerEntryReserveSponsor(
|
||||
view, sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
|
||||
decreaseOwnerCount(view, view.peek(keylet::account(uSenderID)), currentSponsor, 1, j);
|
||||
senderSle.decreaseOwnerCount(currentSponsor, 1);
|
||||
|
||||
removeSponsorFromLedgerEntry(
|
||||
sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor);
|
||||
@@ -805,7 +807,7 @@ directSendNoFeeIOU(
|
||||
<< to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : "
|
||||
<< saAmount.getFullText();
|
||||
|
||||
auto const sleAccount = view.peek(keylet::account(uReceiverID));
|
||||
auto sleAccount = AccountRootEntryW(uReceiverID, view);
|
||||
if (!sleAccount)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -817,7 +819,7 @@ directSendNoFeeIOU(
|
||||
uSenderID,
|
||||
uReceiverID,
|
||||
index.key,
|
||||
sleAccount,
|
||||
sleAccount.mutableRawSle(),
|
||||
false,
|
||||
noRipple,
|
||||
false,
|
||||
@@ -1010,10 +1012,12 @@ accountSendIOU(
|
||||
*/
|
||||
TER terResult(tesSUCCESS);
|
||||
|
||||
SLE::pointer const sender =
|
||||
uSenderID != beast::kZero ? view.peek(keylet::account(uSenderID)) : SLE::pointer();
|
||||
SLE::pointer const receiver =
|
||||
uReceiverID != beast::kZero ? view.peek(keylet::account(uReceiverID)) : SLE::pointer();
|
||||
SLE::pointer const sender = uSenderID != beast::kZero
|
||||
? AccountRootEntryW(uSenderID, view).mutableRawSle()
|
||||
: SLE::pointer();
|
||||
SLE::pointer const receiver = uReceiverID != beast::kZero
|
||||
? AccountRootEntryW(uReceiverID, view).mutableRawSle()
|
||||
: SLE::pointer();
|
||||
|
||||
if (auto stream = j.trace())
|
||||
{
|
||||
@@ -1106,8 +1110,9 @@ accountSendMultiIOU(
|
||||
* ensure that transfers are balanced.
|
||||
*/
|
||||
|
||||
SLE::pointer const sender =
|
||||
senderID != beast::kZero ? view.peek(keylet::account(senderID)) : SLE::pointer();
|
||||
SLE::pointer const sender = senderID != beast::kZero
|
||||
? AccountRootEntryW(senderID, view).mutableRawSle()
|
||||
: SLE::pointer();
|
||||
|
||||
if (auto stream = j.trace())
|
||||
{
|
||||
@@ -1138,8 +1143,9 @@ accountSendMultiIOU(
|
||||
if (!amount || (senderID == receiverID))
|
||||
continue;
|
||||
|
||||
SLE::pointer const receiver =
|
||||
receiverID != beast::kZero ? view.peek(keylet::account(receiverID)) : SLE::pointer();
|
||||
SLE::pointer const receiver = receiverID != beast::kZero
|
||||
? AccountRootEntryW(receiverID, view).mutableRawSle()
|
||||
: SLE::pointer();
|
||||
|
||||
if (auto stream = j.trace())
|
||||
{
|
||||
@@ -1591,8 +1597,8 @@ transferXRP(
|
||||
XRPL_ASSERT(from != to, "xrpl::transferXRP : sender is not receiver");
|
||||
XRPL_ASSERT(amount.native(), "xrpl::transferXRP : amount is XRP");
|
||||
|
||||
SLE::pointer const sender = view.peek(keylet::account(from));
|
||||
SLE::pointer const receiver = view.peek(keylet::account(to));
|
||||
SLE::pointer const sender = AccountRootEntryW(from, view).mutableRawSle();
|
||||
SLE::pointer const receiver = AccountRootEntryW(to, view).mutableRawSle();
|
||||
if (!sender || !receiver)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Implementation of CoroAwareContextStorage.
|
||||
*
|
||||
* Mirrors opentelemetry's ThreadLocalContextStorage stack semantics, but the
|
||||
* stack lives in an xrpl::LocalValue so it follows a JobQueue::Coro across
|
||||
* yield/resume. All OpenTelemetry types stay confined to this translation unit.
|
||||
*
|
||||
* @see CoroAwareContextStorage (CoroAwareContextStorage.h)
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/telemetry/CoroAwareContextStorage.h>
|
||||
|
||||
#include <xrpl/basics/LocalValue.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/context/runtime_context.h>
|
||||
#include <opentelemetry/nostd/unique_ptr.h>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
opentelemetry::context::Context
|
||||
CoroAwareContextStorage::GetCurrent() noexcept
|
||||
{
|
||||
// Hot path: Log.cpp reads the current context on EVERY log line on EVERY
|
||||
// thread. Peek the raw per-thread/coro store WITHOUT materializing it:
|
||||
// getLocalValues().get() returns nullptr when this thread has no store yet
|
||||
// and never allocates. Dereferencing stack_ (a LocalValue) would instead
|
||||
// heap-allocate the store + a map entry + the vector on first touch, so we
|
||||
// avoid that on the no-span path.
|
||||
//
|
||||
// Behaviour is identical to touching *stack_: a thread with no store has no
|
||||
// active span, and a store whose stack was never pushed yields an empty
|
||||
// vector — both cases map to an empty Context, matching stock
|
||||
// ThreadLocalContextStorage (Top() returns Context() when the stack is
|
||||
// empty). Only when a store already exists do we read the top frame.
|
||||
if (detail::getLocalValues().get() == nullptr)
|
||||
return opentelemetry::context::Context{};
|
||||
auto& stack = *stack_;
|
||||
return stack.empty() ? opentelemetry::context::Context{} : stack.back();
|
||||
}
|
||||
|
||||
opentelemetry::nostd::unique_ptr<opentelemetry::context::Token>
|
||||
CoroAwareContextStorage::Attach(opentelemetry::context::Context const& context) noexcept
|
||||
{
|
||||
stack_->push_back(context);
|
||||
return CreateToken(context);
|
||||
}
|
||||
|
||||
bool
|
||||
CoroAwareContextStorage::Detach(opentelemetry::context::Token& token) noexcept
|
||||
{
|
||||
auto& stack = *stack_;
|
||||
// Fast path: the token's frame is on top (the common LIFO case).
|
||||
if (!stack.empty() && token == stack.back())
|
||||
{
|
||||
stack.pop_back();
|
||||
return true;
|
||||
}
|
||||
// Fallback: token not on top — verify it is present, then pop down to and
|
||||
// including it (mirrors ThreadLocalContextStorage::Detach; also detaches
|
||||
// any child frames left above it).
|
||||
bool found = false;
|
||||
for (auto const& frame : stack)
|
||||
{
|
||||
if (token == frame)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
return false;
|
||||
while (!stack.empty() && !(token == stack.back()))
|
||||
stack.pop_back();
|
||||
if (!stack.empty())
|
||||
stack.pop_back();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* Implementation of DeterministicIdGenerator and PendingTraceId.
|
||||
*
|
||||
* The pending trace_id lives in file-local thread_locals shared between the
|
||||
* generator and the RAII guard. GenerateTraceId() consumes the pending id on
|
||||
* the SDK's no-parent branch; PendingTraceId sets it and asserts consumption.
|
||||
* All OpenTelemetry SDK types stay confined to telemetry translation units.
|
||||
*
|
||||
* @see DeterministicIdGenerator, PendingTraceId (DeterministicIdGenerator.h)
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/telemetry/DeterministicIdGenerator.h>
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <opentelemetry/nostd/span.h>
|
||||
#include <opentelemetry/sdk/trace/id_generator.h>
|
||||
#include <opentelemetry/sdk/trace/random_id_generator_factory.h>
|
||||
#include <opentelemetry/trace/span_id.h>
|
||||
#include <opentelemetry/trace/trace_id.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Trace_id pinned by PendingTraceId, consumed by GenerateTraceId(). File-local
|
||||
* and thread-local, so it is set and read on the same thread with no locking.
|
||||
*/
|
||||
thread_local std::optional<std::array<std::uint8_t, 16>> gTlsPendingTraceId;
|
||||
|
||||
/**
|
||||
* True once GenerateTraceId() has consumed the pending id. ~PendingTraceId
|
||||
* asserts on it to catch a forced-root span that never reached the SDK root
|
||||
* branch.
|
||||
*/
|
||||
thread_local bool gTlsPendingConsumed = false;
|
||||
|
||||
/**
|
||||
* Process-wide count of deterministic trace_ids dropped without being consumed.
|
||||
* Bumped in ~PendingTraceId when the pending id was never taken by a root span.
|
||||
* Atomic because it is summed across all threads; relaxed ordering is enough
|
||||
* for a diagnostic tally. Exposed via unconsumedDeterministicIdDrops().
|
||||
*/
|
||||
std::atomic<std::uint64_t> gUnconsumedDeterministicIdDrops{0};
|
||||
|
||||
} // namespace
|
||||
|
||||
DeterministicIdGenerator::DeterministicIdGenerator()
|
||||
: opentelemetry::sdk::trace::IdGenerator(/*is_random=*/false)
|
||||
, random_(opentelemetry::sdk::trace::RandomIdGeneratorFactory::Create())
|
||||
{
|
||||
}
|
||||
|
||||
opentelemetry::trace::TraceId
|
||||
DeterministicIdGenerator::GenerateTraceId() noexcept
|
||||
{
|
||||
if (gTlsPendingTraceId)
|
||||
{
|
||||
auto const id = *gTlsPendingTraceId;
|
||||
gTlsPendingTraceId.reset();
|
||||
gTlsPendingConsumed = true;
|
||||
return opentelemetry::trace::TraceId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 16>(id.data(), 16));
|
||||
}
|
||||
return random_->GenerateTraceId();
|
||||
}
|
||||
|
||||
opentelemetry::trace::SpanId
|
||||
DeterministicIdGenerator::GenerateSpanId() noexcept
|
||||
{
|
||||
return random_->GenerateSpanId(); // ALWAYS random — never uses the pending id
|
||||
}
|
||||
|
||||
PendingTraceId::PendingTraceId(std::array<std::uint8_t, 16> const& id) noexcept
|
||||
{
|
||||
gTlsPendingTraceId = id;
|
||||
gTlsPendingConsumed = false;
|
||||
}
|
||||
|
||||
PendingTraceId::~PendingTraceId() noexcept
|
||||
{
|
||||
// The forced no-parent (root) span-start MUST have consumed the pending id
|
||||
// via GenerateTraceId(). If not, the SDK took a different branch than the
|
||||
// caller forced — a bug — so fail loudly in debug/test builds.
|
||||
XRPL_ASSERT(
|
||||
gTlsPendingConsumed,
|
||||
"xrpl::telemetry::PendingTraceId : deterministic trace_id was not consumed");
|
||||
if (!gTlsPendingConsumed)
|
||||
{
|
||||
// The assert above is stripped in release, so bump a counter too: a
|
||||
// process-wide tally of dropped deterministic trace roots. It is
|
||||
// exposed via unconsumedDeterministicIdDrops() as an extension point
|
||||
// for a future metric or diagnostic to read; nothing wires it to a log
|
||||
// or metric yet. Relaxed: this is a plain diagnostic tally.
|
||||
gUnconsumedDeterministicIdDrops.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
gTlsPendingTraceId.reset(); // never leak, even in release
|
||||
gTlsPendingConsumed = false;
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
unconsumedDeterministicIdDrops() noexcept
|
||||
{
|
||||
return gUnconsumedDeterministicIdDrops.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,360 +0,0 @@
|
||||
/**
|
||||
* Implementation of FilteringSpanProcessor.
|
||||
*
|
||||
* The processor gives the SDK a UniqueAttributeRecordable in place of the
|
||||
* delegate's recordable. The wrapper keeps one owned value per attribute key
|
||||
* and writes the keys once, in first-set order, when the span ends.
|
||||
*
|
||||
* @see FilteringSpanProcessor (FilteringSpanProcessor.h)
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/telemetry/FilteringSpanProcessor.h>
|
||||
|
||||
#include <xrpl/telemetry/DiscardFlag.h>
|
||||
|
||||
#include <opentelemetry/common/attribute_value.h>
|
||||
#include <opentelemetry/common/key_value_iterable.h>
|
||||
#include <opentelemetry/common/timestamp.h>
|
||||
#include <opentelemetry/nostd/span.h>
|
||||
#include <opentelemetry/nostd/string_view.h>
|
||||
#include <opentelemetry/nostd/variant.h>
|
||||
#include <opentelemetry/sdk/common/attribute_utils.h>
|
||||
#include <opentelemetry/sdk/instrumentationscope/instrumentation_scope.h>
|
||||
#include <opentelemetry/sdk/resource/resource.h>
|
||||
#include <opentelemetry/sdk/trace/processor.h>
|
||||
#include <opentelemetry/sdk/trace/recordable.h>
|
||||
#include <opentelemetry/sdk/trace/span_limits.h>
|
||||
#include <opentelemetry/trace/span_context.h>
|
||||
#include <opentelemetry/trace/span_id.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/trace_flags.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace trace_api = opentelemetry::trace;
|
||||
namespace trace_sdk = opentelemetry::sdk::trace;
|
||||
|
||||
/**
|
||||
* Writes one owned attribute value to a recordable, as the view type the SDK
|
||||
* takes. The view lasts only for the call, and the recordable copies it.
|
||||
*
|
||||
* @code
|
||||
* opentelemetry::nostd::visit(ForwardOwnedValue{.target = recordable, .key = key}, ownedValue);
|
||||
* @endcode
|
||||
*
|
||||
* @note The key and the owned value must outlive the call.
|
||||
*/
|
||||
struct ForwardOwnedValue
|
||||
{
|
||||
/**
|
||||
* Recordable that receives the attribute.
|
||||
*/
|
||||
trace_sdk::Recordable& target;
|
||||
|
||||
/**
|
||||
* Attribute key, pointing into the caller's owned copy.
|
||||
*/
|
||||
opentelemetry::nostd::string_view key;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_arithmetic_v<T>
|
||||
void
|
||||
operator()(T value) const noexcept
|
||||
{
|
||||
target.SetAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
operator()(std::string const& value) const noexcept
|
||||
{
|
||||
target.SetAttribute(key, opentelemetry::nostd::string_view{value.data(), value.size()});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
operator()(std::vector<T> const& values) const noexcept
|
||||
{
|
||||
target.SetAttribute(key, opentelemetry::nostd::span<T const>{values.data(), values.size()});
|
||||
}
|
||||
|
||||
// std::vector<bool> packs its bits and has no data(), so copy to plain bools.
|
||||
void
|
||||
operator()(std::vector<bool> const& values) const noexcept
|
||||
{
|
||||
auto const flags = std::make_unique<bool[]>(values.size());
|
||||
std::ranges::copy(values, flags.get());
|
||||
target.SetAttribute(
|
||||
key, opentelemetry::nostd::span<bool const>{flags.get(), values.size()});
|
||||
}
|
||||
|
||||
// A string array is sent as views into the owned strings.
|
||||
void
|
||||
operator()(std::vector<std::string> const& values) const noexcept
|
||||
{
|
||||
std::vector<opentelemetry::nostd::string_view> views;
|
||||
views.reserve(values.size());
|
||||
for (auto const& value : values)
|
||||
{
|
||||
views.emplace_back(value.data(), value.size());
|
||||
}
|
||||
target.SetAttribute(
|
||||
key,
|
||||
opentelemetry::nostd::span<opentelemetry::nostd::string_view const>{
|
||||
views.data(), views.size()});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Recordable the processor gives the SDK in place of the delegate's.
|
||||
*
|
||||
* SetAttribute() keeps an owned copy of each value and replaces any earlier
|
||||
* value for the same key. The first set fixes the key's position. Every other
|
||||
* call goes to the delegate's recordable at once.
|
||||
*
|
||||
* SDK span --SetAttribute()--> attributes_ (one entry per key)
|
||||
* --other calls-----> inner_ (the delegate's recordable)
|
||||
* flushAndRelease(): attributes_ --> inner_, then inner_ is handed back
|
||||
*
|
||||
* @code
|
||||
* UniqueAttributeRecordable wrapper{delegate.MakeRecordable()};
|
||||
* wrapper.SetAttribute("txq_status", "rejected");
|
||||
* wrapper.SetAttribute("txq_status", "queued");
|
||||
* delegate.OnEnd(wrapper.flushAndRelease()); // one txq_status, "queued"
|
||||
* @endcode
|
||||
*
|
||||
* @note Not thread-safe. The SDK span serializes calls to its recordable.
|
||||
* @note operator SpanData*() keeps its default of nullptr, because the
|
||||
* delegate's recordable does not hold the kept attributes until the span ends.
|
||||
*/
|
||||
class UniqueAttributeRecordable final : public trace_sdk::Recordable
|
||||
{
|
||||
/**
|
||||
* One kept attribute: an owned key and an owned copy of its last value.
|
||||
*/
|
||||
using Attribute = std::pair<std::string, opentelemetry::sdk::common::OwnedAttributeValue>;
|
||||
|
||||
/**
|
||||
* The delegate's recordable. Null only after flushAndRelease().
|
||||
*/
|
||||
std::unique_ptr<trace_sdk::Recordable> inner_;
|
||||
|
||||
/**
|
||||
* Kept attributes, in the order each key was first set.
|
||||
*/
|
||||
std::vector<Attribute> attributes_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @param inner The delegate's recordable. Must not be null.
|
||||
*/
|
||||
explicit UniqueAttributeRecordable(std::unique_ptr<trace_sdk::Recordable> inner) noexcept
|
||||
: inner_(std::move(inner))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the delegate's recordable.
|
||||
*/
|
||||
[[nodiscard]] trace_sdk::Recordable&
|
||||
inner() const noexcept
|
||||
{
|
||||
return *inner_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes each kept attribute to the delegate's recordable once, in
|
||||
* first-set order. The delegate's recordable applies its own limits here.
|
||||
*
|
||||
* @return the delegate's recordable. Do not use this wrapper afterwards.
|
||||
*/
|
||||
[[nodiscard]] std::unique_ptr<trace_sdk::Recordable>
|
||||
flushAndRelease() noexcept
|
||||
{
|
||||
for (auto const& [key, value] : attributes_)
|
||||
{
|
||||
opentelemetry::nostd::visit(
|
||||
ForwardOwnedValue{.target = *inner_, .key = {key.data(), key.size()}}, value);
|
||||
}
|
||||
return std::move(inner_);
|
||||
}
|
||||
|
||||
void
|
||||
SetAttribute(
|
||||
opentelemetry::nostd::string_view key,
|
||||
opentelemetry::common::AttributeValue const& value) noexcept override
|
||||
{
|
||||
auto owned = opentelemetry::sdk::common::VisitVariant(
|
||||
opentelemetry::sdk::common::AttributeConverter{}, value);
|
||||
if (!owned.second)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::string_view const name{key.data(), key.size()};
|
||||
auto const found = std::ranges::find_if(
|
||||
attributes_, [name](Attribute const& attribute) { return attribute.first == name; });
|
||||
if (found != attributes_.end())
|
||||
{
|
||||
found->second = std::move(owned.first);
|
||||
}
|
||||
else
|
||||
{
|
||||
attributes_.emplace_back(std::string{name}, std::move(owned.first));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SetIdentity(trace_api::SpanContext const& spanContext, trace_api::SpanId parentSpanId) noexcept
|
||||
override
|
||||
{
|
||||
inner_->SetIdentity(spanContext, parentSpanId);
|
||||
}
|
||||
|
||||
void
|
||||
AddEvent(
|
||||
opentelemetry::nostd::string_view name,
|
||||
opentelemetry::common::SystemTimestamp timestamp,
|
||||
opentelemetry::common::KeyValueIterable const& attributes) noexcept override
|
||||
{
|
||||
inner_->AddEvent(name, timestamp, attributes);
|
||||
}
|
||||
|
||||
void
|
||||
AddLink(
|
||||
trace_api::SpanContext const& spanContext,
|
||||
opentelemetry::common::KeyValueIterable const& attributes) noexcept override
|
||||
{
|
||||
inner_->AddLink(spanContext, attributes);
|
||||
}
|
||||
|
||||
void
|
||||
SetStatus(trace_api::StatusCode code, opentelemetry::nostd::string_view description) noexcept
|
||||
override
|
||||
{
|
||||
inner_->SetStatus(code, description);
|
||||
}
|
||||
|
||||
void
|
||||
SetName(opentelemetry::nostd::string_view name) noexcept override
|
||||
{
|
||||
inner_->SetName(name);
|
||||
}
|
||||
|
||||
void
|
||||
SetTraceFlags(trace_api::TraceFlags flags) noexcept override
|
||||
{
|
||||
inner_->SetTraceFlags(flags);
|
||||
}
|
||||
|
||||
void
|
||||
SetSpanKind(trace_api::SpanKind spanKind) noexcept override
|
||||
{
|
||||
inner_->SetSpanKind(spanKind);
|
||||
}
|
||||
|
||||
void
|
||||
SetResource(opentelemetry::sdk::resource::Resource const& resource) noexcept override
|
||||
{
|
||||
inner_->SetResource(resource);
|
||||
}
|
||||
|
||||
void
|
||||
SetStartTime(opentelemetry::common::SystemTimestamp startTime) noexcept override
|
||||
{
|
||||
inner_->SetStartTime(startTime);
|
||||
}
|
||||
|
||||
void
|
||||
SetDuration(std::chrono::nanoseconds duration) noexcept override
|
||||
{
|
||||
inner_->SetDuration(duration);
|
||||
}
|
||||
|
||||
void
|
||||
SetSpanLimits(trace_sdk::SpanLimits const& limits) noexcept override
|
||||
{
|
||||
inner_->SetSpanLimits(limits);
|
||||
}
|
||||
|
||||
void
|
||||
SetInstrumentationScope(
|
||||
opentelemetry::sdk::instrumentationscope::InstrumentationScope const& scope) noexcept
|
||||
override
|
||||
{
|
||||
inner_->SetInstrumentationScope(scope);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
FilteringSpanProcessor::FilteringSpanProcessor(std::unique_ptr<trace_sdk::SpanProcessor> delegate)
|
||||
: delegate_(std::move(delegate))
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<trace_sdk::Recordable>
|
||||
FilteringSpanProcessor::MakeRecordable() noexcept
|
||||
{
|
||||
auto inner = delegate_->MakeRecordable();
|
||||
if (inner == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_unique<UniqueAttributeRecordable>(std::move(inner));
|
||||
}
|
||||
|
||||
void
|
||||
FilteringSpanProcessor::OnStart(
|
||||
trace_sdk::Recordable& span,
|
||||
trace_api::SpanContext const& parentContext) noexcept
|
||||
{
|
||||
auto* const wrapper = dynamic_cast<UniqueAttributeRecordable*>(&span);
|
||||
delegate_->OnStart(wrapper != nullptr ? wrapper->inner() : span, parentContext);
|
||||
}
|
||||
|
||||
void
|
||||
FilteringSpanProcessor::OnEnd(std::unique_ptr<trace_sdk::Recordable>&& span) noexcept
|
||||
{
|
||||
if (DiscardScope::isActive())
|
||||
{
|
||||
// SpanGuard::discard() is inside a DiscardScope on this thread,
|
||||
// which it entered just before calling Span::End() — and End()
|
||||
// invokes OnEnd() synchronously. Drop the span.
|
||||
return;
|
||||
}
|
||||
auto* const wrapper = dynamic_cast<UniqueAttributeRecordable*>(span.get());
|
||||
if (wrapper == nullptr)
|
||||
{
|
||||
delegate_->OnEnd(std::move(span));
|
||||
return;
|
||||
}
|
||||
delegate_->OnEnd(wrapper->flushAndRelease());
|
||||
}
|
||||
|
||||
bool
|
||||
FilteringSpanProcessor::ForceFlush(std::chrono::microseconds timeout) noexcept
|
||||
{
|
||||
return delegate_->ForceFlush(timeout);
|
||||
}
|
||||
|
||||
bool
|
||||
FilteringSpanProcessor::Shutdown(std::chrono::microseconds timeout) noexcept
|
||||
{
|
||||
return delegate_->Shutdown(timeout);
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,154 +0,0 @@
|
||||
/**
|
||||
* No-op implementation of the Telemetry interface.
|
||||
*
|
||||
* Always compiled (regardless of XRPL_ENABLE_TELEMETRY). Provides the
|
||||
* makeTelemetry() factory when telemetry is compiled out (#ifndef), which
|
||||
* unconditionally returns a NullTelemetry that does nothing.
|
||||
*
|
||||
* When XRPL_ENABLE_TELEMETRY IS defined, the OTel virtual methods
|
||||
* (getTracer, startSpan) return noop tracers/spans. The makeTelemetry()
|
||||
* factory in this file is not used in that case -- Telemetry.cpp provides
|
||||
* its own factory that can return the real TelemetryImpl.
|
||||
*/
|
||||
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#ifndef XRPL_ENABLE_TELEMETRY
|
||||
// beast::Journal is named only by the compiled-out makeTelemetry() below, so
|
||||
// this include belongs to that configuration and would be unused in the other.
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/trace/noop.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/tracer.h>
|
||||
|
||||
#include <string_view>
|
||||
#endif
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* No-op Telemetry that returns immediately from every method.
|
||||
*
|
||||
* Used as the sole implementation when XRPL_ENABLE_TELEMETRY is not
|
||||
* defined, or as a fallback when it is defined but enabled=0.
|
||||
*/
|
||||
class NullTelemetry : public Telemetry
|
||||
{
|
||||
/**
|
||||
* Retained configuration (unused, kept for diagnostic access).
|
||||
*/
|
||||
Setup const setup_;
|
||||
|
||||
public:
|
||||
explicit NullTelemetry(Setup setup) : setup_{std::move(setup)}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
start() override
|
||||
{
|
||||
Telemetry::setInstance(this);
|
||||
}
|
||||
|
||||
void
|
||||
stop() override
|
||||
{
|
||||
// Clear the global instance only if this object is the one that
|
||||
// published it. A process with two of these, as the test binary has,
|
||||
// would otherwise let one unregister the other.
|
||||
if (Telemetry::getInstance() == this)
|
||||
{
|
||||
Telemetry::setInstance(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isEnabled() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceTransactions() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceConsensus() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceRpc() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTracePeer() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceLedger() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer>
|
||||
getTracer(std::string_view) override
|
||||
{
|
||||
static auto noopTracer = opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer>(
|
||||
new opentelemetry::trace::NoopTracer());
|
||||
return noopTracer;
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
startSpan(std::string_view, opentelemetry::trace::SpanKind) override
|
||||
{
|
||||
return opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>(
|
||||
new opentelemetry::trace::NoopSpan(nullptr));
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
startSpan(
|
||||
std::string_view,
|
||||
opentelemetry::context::Context const&,
|
||||
opentelemetry::trace::SpanKind) override
|
||||
{
|
||||
return opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>(
|
||||
new opentelemetry::trace::NoopSpan(nullptr));
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* Factory used when XRPL_ENABLE_TELEMETRY is not defined.
|
||||
* Unconditionally returns a NullTelemetry instance.
|
||||
*/
|
||||
#ifndef XRPL_ENABLE_TELEMETRY
|
||||
std::unique_ptr<Telemetry>
|
||||
makeTelemetry(Telemetry::Setup const& setup, beast::Journal)
|
||||
{
|
||||
return std::make_unique<NullTelemetry>(setup);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
@@ -1,772 +0,0 @@
|
||||
/**
|
||||
* Pimpl implementation for SpanGuard, ScopedSpanGuard and SpanContext.
|
||||
*
|
||||
* All OpenTelemetry SDK types are confined to this translation unit.
|
||||
* The public SpanGuard.h header contains only standard-library types
|
||||
* and forward-declares the Impl / ScopedImpl structs.
|
||||
*
|
||||
* Two guards with split responsibilities live here:
|
||||
* - SpanGuard::Impl holds ONLY the OTel Span (shared_ptr). It never
|
||||
* touches the thread-local context stack, so a SpanGuard is
|
||||
* thread-free and may be moved to and destroyed on any thread.
|
||||
* - ScopedSpanGuard holds a SpanGuard inline plus a ScopedImpl, which
|
||||
* holds the Scope and is allocated only for a live span. The Scope
|
||||
* pushes the span onto the constructing context store's stack; member
|
||||
* order (guard_ first, impl_ second) ensures the Scope pops BEFORE the
|
||||
* span ends on destruction. It is store-bound: construct and destroy
|
||||
* while the same LocalValue context store is active (the same thread,
|
||||
* or the same JobQueue coroutine even if it resumes on another worker).
|
||||
*
|
||||
* Static factory methods access the global Telemetry instance via
|
||||
* Telemetry::getInstance(), check whether the requested TraceCategory
|
||||
* is enabled, and return either an active guard with a real Span or a
|
||||
* null guard whose methods are all no-ops.
|
||||
*
|
||||
* @see SpanGuard, ScopedSpanGuard (SpanGuard.h), Telemetry (Telemetry.h),
|
||||
* FilteringSpanProcessor (FilteringSpanProcessor.h)
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <xrpl/basics/LocalValue.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/telemetry/DiscardFlag.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/context/runtime_context.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/semconv/exception_attributes.h>
|
||||
#include <opentelemetry/trace/context.h>
|
||||
#include <opentelemetry/trace/scope.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/span_startoptions.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <typeinfo>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace otel_trace = opentelemetry::trace;
|
||||
|
||||
// ===== SpanContext::Impl ===================================================
|
||||
|
||||
struct SpanContext::Impl
|
||||
{
|
||||
opentelemetry::context::Context ctx;
|
||||
|
||||
explicit Impl(opentelemetry::context::Context c) : ctx(std::move(c))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
SpanContext::SpanContext(std::shared_ptr<Impl> impl) : impl_(std::move(impl))
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
SpanContext::isValid() const noexcept
|
||||
{
|
||||
// Holding a Context is not proof of holding a span. GetCurrent() hands back
|
||||
// an empty Context on a thread with no active span, and threadLocalContext()
|
||||
// wraps that too. Ask the Context for its span instead of trusting impl_.
|
||||
return impl_ != nullptr && otel_trace::GetSpan(impl_->ctx)->GetContext().IsValid();
|
||||
}
|
||||
|
||||
// ===== SpanGuard::Impl ====================================================
|
||||
|
||||
struct SpanGuard::Impl
|
||||
{
|
||||
/**
|
||||
* The OTel span being guarded. Set to nullptr after discard() so
|
||||
* ~Impl skips End().
|
||||
*/
|
||||
opentelemetry::nostd::shared_ptr<otel_trace::Span> span;
|
||||
|
||||
/**
|
||||
* Construct an unscoped guard: the span is owned but NOT pushed
|
||||
* onto any thread's context stack. The guard is thread-free.
|
||||
* @param s The span to guard.
|
||||
*/
|
||||
explicit Impl(opentelemetry::nostd::shared_ptr<otel_trace::Span> s) : span(std::move(s))
|
||||
{
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
if (span)
|
||||
span->End();
|
||||
}
|
||||
|
||||
Impl(Impl const&) = delete;
|
||||
Impl&
|
||||
operator=(Impl const&) = delete;
|
||||
Impl(Impl&&) = delete;
|
||||
Impl&
|
||||
operator=(Impl&&) = delete;
|
||||
};
|
||||
|
||||
// ===== SpanGuard core lifecycle ============================================
|
||||
|
||||
SpanGuard::SpanGuard() = default;
|
||||
SpanGuard::~SpanGuard() = default;
|
||||
SpanGuard::SpanGuard(SpanGuard&&) noexcept = default;
|
||||
SpanGuard&
|
||||
SpanGuard::operator=(SpanGuard&&) noexcept = default;
|
||||
|
||||
SpanGuard::SpanGuard(std::unique_ptr<Impl> impl) noexcept : impl_(std::move(impl))
|
||||
{
|
||||
}
|
||||
|
||||
SpanGuard::
|
||||
operator bool() const noexcept
|
||||
{
|
||||
return impl_ != nullptr;
|
||||
}
|
||||
|
||||
// ===== Static factory methods ==============================================
|
||||
|
||||
/**
|
||||
* Check whether the given TraceCategory is enabled on the Telemetry instance.
|
||||
* @return true if the category's shouldTrace*() flag is on.
|
||||
*/
|
||||
static bool
|
||||
isCategoryEnabled(Telemetry const& tel, TraceCategory cat)
|
||||
{
|
||||
switch (cat)
|
||||
{
|
||||
case TraceCategory::Rpc:
|
||||
return tel.shouldTraceRpc();
|
||||
case TraceCategory::Transactions:
|
||||
return tel.shouldTraceTransactions();
|
||||
case TraceCategory::Consensus:
|
||||
return tel.shouldTraceConsensus();
|
||||
case TraceCategory::Peer:
|
||||
return tel.shouldTracePeer();
|
||||
case TraceCategory::Ledger:
|
||||
return tel.shouldTraceLedger();
|
||||
}
|
||||
return false; // unreachable, silences compiler warning
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Span-link attribute marking a "follows-from" (causal, non-parent) link,
|
||||
// emitted by both linkedSpan() overloads. Custom xrpl attribute — not part
|
||||
// of the OTel semantic conventions, so defined here rather than pulled from
|
||||
// <opentelemetry/semconv/...>.
|
||||
constexpr char const* kLinkTypeKey = "link_type";
|
||||
constexpr char const* kLinkTypeFollowsFrom = "follows_from";
|
||||
|
||||
// Per-category default OTel SpanKind, used when a call site passes no
|
||||
// SpanRole. A category cannot tell an inbound entry point from the
|
||||
// internal work under it, so RPC and Peer default to the entry-point
|
||||
// kind and any call site below the entry point passes SpanRole::Internal
|
||||
// instead. Transactions / Consensus / Ledger are internal throughout.
|
||||
// The kind drives direction in Tempo's service-graph / RED metrics,
|
||||
// which pair kServer with kClient and kConsumer with kProducer.
|
||||
otel_trace::SpanKind
|
||||
categoryToSpanKind(TraceCategory cat)
|
||||
{
|
||||
switch (cat)
|
||||
{
|
||||
case TraceCategory::Rpc:
|
||||
return otel_trace::SpanKind::kServer;
|
||||
case TraceCategory::Peer:
|
||||
return otel_trace::SpanKind::kConsumer;
|
||||
case TraceCategory::Transactions:
|
||||
case TraceCategory::Consensus:
|
||||
case TraceCategory::Ledger:
|
||||
return otel_trace::SpanKind::kInternal;
|
||||
}
|
||||
return otel_trace::SpanKind::kInternal; // unreachable
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the span kind to start a span with.
|
||||
*
|
||||
* An explicit SpanRole wins; SpanRole::FromCategory falls back to the
|
||||
* category default above. Role and category are separate axes, so a single
|
||||
* category can emit both an inbound handler and the internal work under it.
|
||||
*
|
||||
* @param cat Trace subsystem category. Read only for SpanRole::FromCategory.
|
||||
* @param role Role the caller asked for.
|
||||
* @return The OTel span kind for this span.
|
||||
*/
|
||||
[[nodiscard]] otel_trace::SpanKind
|
||||
resolveSpanKind(TraceCategory cat, SpanRole role)
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case SpanRole::FromCategory:
|
||||
return categoryToSpanKind(cat);
|
||||
case SpanRole::Internal:
|
||||
return otel_trace::SpanKind::kInternal;
|
||||
case SpanRole::Server:
|
||||
return otel_trace::SpanKind::kServer;
|
||||
case SpanRole::Client:
|
||||
return otel_trace::SpanKind::kClient;
|
||||
case SpanRole::Producer:
|
||||
return otel_trace::SpanKind::kProducer;
|
||||
case SpanRole::Consumer:
|
||||
return otel_trace::SpanKind::kConsumer;
|
||||
}
|
||||
return categoryToSpanKind(cat); // unreachable
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a span-name prefix and suffix into the dotted full name.
|
||||
*
|
||||
* Wraps std::format because the callers are noexcept: std::format can throw
|
||||
* (std::bad_alloc, or std::format_error on a malformed spec) and an escaping
|
||||
* exception would terminate the process. Telemetry must never take the node
|
||||
* down, so a failure yields std::nullopt and the caller returns a null guard —
|
||||
* the same degrade-to-no-op path already used when telemetry is disabled.
|
||||
*
|
||||
* @param prefix Segment before the dot (e.g. "consensus").
|
||||
* @param name Segment after the dot (e.g. "round").
|
||||
* @return The joined name, or std::nullopt if formatting failed.
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::string>
|
||||
joinSpanName(std::string_view prefix, std::string_view name) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
return std::format("{}.{}", prefix, name);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// Out of memory or a bad format spec. Drop the span rather than
|
||||
// propagate out of a noexcept factory.
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::span(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role) noexcept
|
||||
{
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
return {};
|
||||
auto const fullName = joinSpanName(prefix, name);
|
||||
if (!fullName)
|
||||
return {};
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(*fullName, resolveSpanKind(cat, role))));
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::freshRoot(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role) noexcept
|
||||
{
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
return {};
|
||||
auto const fullName = joinSpanName(prefix, name);
|
||||
if (!fullName)
|
||||
return {};
|
||||
// Force a fresh trace root: do NOT inherit this thread's active span.
|
||||
auto rootCtx = opentelemetry::context::Context{otel_trace::kIsRootSpanKey, true};
|
||||
return SpanGuard(
|
||||
std::make_unique<Impl>(tel->startSpan(*fullName, rootCtx, resolveSpanKind(cat, role))));
|
||||
}
|
||||
|
||||
// ===== Child / linked span creation ========================================
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::childSpan(std::string_view name) const noexcept
|
||||
{
|
||||
if (!impl_)
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled())
|
||||
return {};
|
||||
auto ctx = opentelemetry::context::RuntimeContext::GetCurrent();
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(name, ctx)));
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::childSpan(std::string_view name, SpanContext const& parentCtx) noexcept
|
||||
{
|
||||
if (!parentCtx.isValid())
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled())
|
||||
return {};
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(name, parentCtx.impl_->ctx)));
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::linkedSpan(std::string_view name) const noexcept
|
||||
{
|
||||
if (!impl_)
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled())
|
||||
return {};
|
||||
|
||||
auto tracer = tel->getTracer();
|
||||
auto spanCtx = impl_->span->GetContext();
|
||||
|
||||
// Mark as root span so it starts a new trace sub-tree rather than
|
||||
// inheriting the current thread's active span as parent.
|
||||
otel_trace::StartSpanOptions opts;
|
||||
opts.parent = opentelemetry::context::Context{otel_trace::kIsRootSpanKey, true};
|
||||
|
||||
// LCOV_EXCL_START
|
||||
return SpanGuard(
|
||||
std::make_unique<Impl>(tracer->StartSpan(
|
||||
std::string(name), {}, {{spanCtx, {{kLinkTypeKey, kLinkTypeFollowsFrom}}}}, opts)));
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::linkedSpan(std::string_view name, SpanContext const& linkCtx) noexcept
|
||||
{
|
||||
if (!linkCtx.isValid())
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled())
|
||||
return {};
|
||||
|
||||
auto tracer = tel->getTracer();
|
||||
|
||||
// Extract the span from the captured context to get its SpanContext.
|
||||
auto linkSpan = otel_trace::GetSpan(linkCtx.impl_->ctx);
|
||||
if (!linkSpan || !linkSpan->GetContext().IsValid())
|
||||
return {};
|
||||
|
||||
// Mark as root span so it starts a new trace sub-tree rather than
|
||||
// inheriting the current thread's active span as parent.
|
||||
otel_trace::StartSpanOptions opts;
|
||||
opts.parent = opentelemetry::context::Context{otel_trace::kIsRootSpanKey, true};
|
||||
|
||||
// LCOV_EXCL_START
|
||||
return SpanGuard(
|
||||
std::make_unique<Impl>(tracer->StartSpan(
|
||||
std::string(name),
|
||||
{},
|
||||
{{linkSpan->GetContext(), {{kLinkTypeKey, kLinkTypeFollowsFrom}}}},
|
||||
opts)));
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// ===== Context capture =====================================================
|
||||
|
||||
SpanContext
|
||||
SpanGuard::spanContext() const noexcept
|
||||
{
|
||||
if (!impl_ || !impl_->span)
|
||||
return {};
|
||||
// Refer to THIS guard's own span, not the thread's ambient span. A
|
||||
// SpanGuard is unscoped (never pushed onto the thread-local stack), so
|
||||
// RuntimeContext::GetCurrent() would return an unrelated ambient span.
|
||||
// Building the context from impl_->span makes spanContext() correct
|
||||
// for every caller regardless of scoping, and lets childSpan(name, ctx)
|
||||
// parent explicitly to this span across threads.
|
||||
auto ctx = opentelemetry::context::Context{}.SetValue(otel_trace::kSpanKey, impl_->span);
|
||||
return SpanContext(std::make_shared<SpanContext::Impl>(std::move(ctx)));
|
||||
}
|
||||
|
||||
SpanContext
|
||||
SpanGuard::threadLocalContext() noexcept
|
||||
{
|
||||
// Snapshot whatever span is currently active on THIS thread's context
|
||||
// stack (the ambient context), independent of any guard. This is the
|
||||
// OTel "capture current context" operation used for propagation.
|
||||
auto ctx = opentelemetry::context::RuntimeContext::GetCurrent();
|
||||
return SpanContext(std::make_shared<SpanContext::Impl>(std::move(ctx)));
|
||||
}
|
||||
|
||||
// ===== Attribute setters ===================================================
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, std::string_view value) noexcept
|
||||
{
|
||||
if (impl_)
|
||||
{
|
||||
impl_->span->SetAttribute(
|
||||
opentelemetry::nostd::string_view(key.data(), key.size()),
|
||||
opentelemetry::nostd::string_view(value.data(), value.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, char const* value) noexcept
|
||||
{
|
||||
// A std::string_view built from a pointer reads that pointer to find its
|
||||
// length, so a null one is undefined behaviour. A null pointer carries no
|
||||
// text, and an empty value already means something here, so record nothing.
|
||||
if (value != nullptr)
|
||||
setAttribute(key, std::string_view(value));
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, std::int64_t value) noexcept
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetAttribute(opentelemetry::nostd::string_view(key.data(), key.size()), value);
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, double value) noexcept
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetAttribute(opentelemetry::nostd::string_view(key.data(), key.size()), value);
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, bool value) noexcept
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetAttribute(opentelemetry::nostd::string_view(key.data(), key.size()), value);
|
||||
}
|
||||
|
||||
// ===== Status / events =====================================================
|
||||
|
||||
void
|
||||
SpanGuard::setOk() noexcept
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetStatus(otel_trace::StatusCode::kOk);
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setError(std::string_view description) noexcept
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetStatus(otel_trace::StatusCode::kError, std::string(description));
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::addEvent(std::string_view name) noexcept
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->AddEvent(std::string(name));
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::recordException(std::exception const& e) noexcept
|
||||
{
|
||||
if (!impl_)
|
||||
return;
|
||||
namespace semconv_exc = opentelemetry::semconv::exception;
|
||||
// Event name "exception" and the attribute keys follow the OTel semantic
|
||||
// conventions; the keys come from semconv constants rather than literals.
|
||||
impl_->span->AddEvent(
|
||||
"exception",
|
||||
{{semconv_exc::kExceptionType, typeid(e).name()},
|
||||
{semconv_exc::kExceptionMessage, std::string(e.what())}});
|
||||
impl_->span->SetStatus(otel_trace::StatusCode::kError, e.what());
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::discard() noexcept
|
||||
{
|
||||
if (impl_)
|
||||
{
|
||||
{
|
||||
// DiscardScope owns the flag's whole lifetime: it sets the flag,
|
||||
// and clears it on scope exit — even if End() were to throw. The
|
||||
// SDK invokes FilteringSpanProcessor::OnEnd() synchronously from
|
||||
// End() on this thread, so the flag is observed while still set.
|
||||
// Today every valid guard wraps a recording span (head sampling is
|
||||
// 1.0), so OnEnd() always runs — but scoping set/clear keeps the
|
||||
// flag leak-proof if a later phase can hand back a non-recording
|
||||
// span (e.g. a head ratio below 1.0), so it can never spill onto
|
||||
// the next span.
|
||||
DiscardScope const discardScope;
|
||||
impl_->span->End();
|
||||
}
|
||||
impl_->span = nullptr; // prevent ~Impl from calling End() again
|
||||
impl_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard::ScopedImpl =========================================
|
||||
|
||||
struct ScopedSpanGuard::ScopedImpl
|
||||
{
|
||||
/**
|
||||
* Active OTel Scope binding the guard's span to the active context
|
||||
* store's stack. Destroying this ScopedImpl pops it; operator
|
||||
* SpanGuard() and discard() do that eagerly under the constructing
|
||||
* store.
|
||||
*/
|
||||
otel_trace::Scope scope;
|
||||
|
||||
/**
|
||||
* Identity of the LocalValue store the Scope was pushed onto (the
|
||||
* coroutine's store on a coro, else the thread's own store). Popping
|
||||
* the Scope must happen while the SAME store is active, else a
|
||||
* different stack is corrupted. Coroutine-aware storage lets a coro
|
||||
* legitimately resume on another worker while keeping the same store,
|
||||
* so store-identity — not thread-id — is the correct invariant.
|
||||
* Checked in the destructor, operator SpanGuard() and discard() to
|
||||
* turn a silent cross-store pop into an assertion failure in
|
||||
* debug/test/fuzzing builds.
|
||||
*
|
||||
* Initialized AFTER `scope` in the member init list: members
|
||||
* initialize in declaration order, so the Scope's push runs first. On a
|
||||
* thread with no store yet, the push creates it; GetCurrent() only peeks
|
||||
* and never creates one. Capturing before the push would record nullptr
|
||||
* while the destructor sees the new store.
|
||||
*/
|
||||
void const* owner;
|
||||
|
||||
/**
|
||||
* Push a live guard's span onto the current context store.
|
||||
* @param span The span to activate. The owning SpanGuard keeps it alive.
|
||||
*/
|
||||
explicit ScopedImpl(opentelemetry::nostd::shared_ptr<otel_trace::Span> const& span)
|
||||
: scope(span), owner(detail::getLocalValues().get())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// ===== ScopedSpanGuard core lifecycle ======================================
|
||||
|
||||
ScopedSpanGuard::ScopedSpanGuard(SpanGuard&& guard) noexcept : guard_(std::move(guard))
|
||||
{
|
||||
if (guard_)
|
||||
impl_ = std::make_unique<ScopedImpl>(guard_.impl_->span);
|
||||
}
|
||||
|
||||
ScopedSpanGuard::~ScopedSpanGuard()
|
||||
{
|
||||
// A live Scope must be popped while its constructing context store is
|
||||
// active; destroying the guard under a different store corrupts that
|
||||
// store's context stack. A null guard holds no Scope, so the check is
|
||||
// skipped.
|
||||
XRPL_ASSERT(
|
||||
!impl_ || impl_->owner == detail::getLocalValues().get(),
|
||||
"xrpl::telemetry::ScopedSpanGuard::~ScopedSpanGuard : destroyed on the "
|
||||
"constructing context store");
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard factory methods =====================================
|
||||
|
||||
ScopedSpanGuard::ScopedSpanGuard(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role) noexcept
|
||||
: ScopedSpanGuard(SpanGuard::span(cat, prefix, name, role))
|
||||
{
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::freshRoot(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role) noexcept
|
||||
{
|
||||
return ScopedSpanGuard(SpanGuard::freshRoot(cat, prefix, name, role));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::childSpan(std::string_view name) const noexcept
|
||||
{
|
||||
return ScopedSpanGuard(guard_.childSpan(name));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::childSpan(std::string_view name, SpanContext const& parentCtx) noexcept
|
||||
{
|
||||
return ScopedSpanGuard(SpanGuard::childSpan(name, parentCtx));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::linkedSpan(std::string_view name) const noexcept
|
||||
{
|
||||
return ScopedSpanGuard(guard_.linkedSpan(name));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::linkedSpan(std::string_view name, SpanContext const& linkCtx) noexcept
|
||||
{
|
||||
return ScopedSpanGuard(SpanGuard::linkedSpan(name, linkCtx));
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard handoff bridge ======================================
|
||||
|
||||
ScopedSpanGuard::
|
||||
operator SpanGuard() && noexcept
|
||||
{
|
||||
// The scope must be popped while its constructing context store is
|
||||
// active; handing off under a different store would pop the wrong stack.
|
||||
// A null guard holds no Scope, so the check is skipped.
|
||||
XRPL_ASSERT(
|
||||
!impl_ || impl_->owner == detail::getLocalValues().get(),
|
||||
"xrpl::telemetry::ScopedSpanGuard::operator SpanGuard : handoff on the "
|
||||
"constructing context store");
|
||||
impl_.reset(); // eager pop, on the origin store
|
||||
return std::move(guard_);
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard context capture =====================================
|
||||
|
||||
SpanContext
|
||||
ScopedSpanGuard::spanContext() const noexcept
|
||||
{
|
||||
return guard_.spanContext();
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard forwarding methods ==================================
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, std::string_view value) noexcept
|
||||
{
|
||||
guard_.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, char const* value) noexcept
|
||||
{
|
||||
guard_.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, std::int64_t value) noexcept
|
||||
{
|
||||
guard_.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, double value) noexcept
|
||||
{
|
||||
guard_.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, bool value) noexcept
|
||||
{
|
||||
guard_.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setOk() noexcept
|
||||
{
|
||||
guard_.setOk();
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setError(std::string_view description) noexcept
|
||||
{
|
||||
guard_.setError(description);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::addEvent(std::string_view name) noexcept
|
||||
{
|
||||
guard_.addEvent(name);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::recordException(std::exception const& e) noexcept
|
||||
{
|
||||
guard_.recordException(e);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::discard() noexcept
|
||||
{
|
||||
// A live Scope must be popped while its constructing context store is
|
||||
// active; discarding under a different store corrupts that store's
|
||||
// context stack. A null guard holds no Scope, so the check is skipped.
|
||||
XRPL_ASSERT(
|
||||
!impl_ || impl_->owner == detail::getLocalValues().get(),
|
||||
"xrpl::telemetry::ScopedSpanGuard::discard : discarded on the "
|
||||
"constructing context store");
|
||||
// Pop the scope first (under the constructing store) so no span stays
|
||||
// active on the stack, then discard the span via the owned guard.
|
||||
impl_.reset();
|
||||
guard_.discard();
|
||||
}
|
||||
|
||||
ScopedSpanGuard::
|
||||
operator bool() const noexcept
|
||||
{
|
||||
return static_cast<bool>(guard_);
|
||||
}
|
||||
|
||||
// ===== ScopedActivation ====================================================
|
||||
|
||||
struct ScopedActivation::Impl
|
||||
{
|
||||
/**
|
||||
* Active OTel Scope binding an externally-owned span to the current
|
||||
* context store. Popped on destruction; never ends the span.
|
||||
*/
|
||||
otel_trace::Scope scope;
|
||||
|
||||
/**
|
||||
* Identity of the LocalValue store the Scope was pushed onto. The Scope
|
||||
* must pop while the SAME store is active (see
|
||||
* ScopedSpanGuard::ScopedImpl::owner). Initialized AFTER `scope` in the
|
||||
* member init list: members initialize in declaration order, so the
|
||||
* Scope's push (the first LocalValue touch on a fresh worker, which
|
||||
* materializes the store) runs first, then this captures the
|
||||
* now-materialized store. Capturing before the push would record
|
||||
* nullptr while the destructor sees the materialized store.
|
||||
*/
|
||||
void const* owner;
|
||||
|
||||
/**
|
||||
* Push an externally-owned span onto the current context store.
|
||||
* @param span The already-owned span to activate (not owned here).
|
||||
*/
|
||||
explicit Impl(opentelemetry::nostd::shared_ptr<otel_trace::Span> const& span)
|
||||
: scope(span), owner(detail::getLocalValues().get())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
ScopedActivation::ScopedActivation() = default;
|
||||
|
||||
ScopedActivation::ScopedActivation(std::unique_ptr<Impl> impl) noexcept : impl_(std::move(impl))
|
||||
{
|
||||
}
|
||||
|
||||
ScopedActivation::~ScopedActivation()
|
||||
{
|
||||
// A live Scope must be popped while its constructing context store is
|
||||
// active; destroying the activation under a different store corrupts
|
||||
// that store's context stack. A null activation holds no Scope, so the
|
||||
// check is skipped.
|
||||
XRPL_ASSERT(
|
||||
!impl_ || impl_->owner == detail::getLocalValues().get(),
|
||||
"xrpl::telemetry::ScopedActivation::~ScopedActivation : destroyed on the "
|
||||
"constructing context store");
|
||||
}
|
||||
|
||||
ScopedActivation
|
||||
SpanGuard::activate() const noexcept
|
||||
{
|
||||
if (!impl_ || !impl_->span)
|
||||
return {};
|
||||
return ScopedActivation(std::make_unique<ScopedActivation::Impl>(impl_->span));
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,440 +0,0 @@
|
||||
/**
|
||||
* OpenTelemetry SDK implementation of the Telemetry interface.
|
||||
*
|
||||
* Compiled only when XRPL_ENABLE_TELEMETRY is defined (via CMake
|
||||
* telemetry=ON). Contains:
|
||||
*
|
||||
* - TelemetryImpl: configures the OTel SDK with an OTLP/HTTP exporter,
|
||||
* FilteringSpanProcessor wrapping a batch span processor, the head
|
||||
* sampler from makeHeadSampler(), and resource attributes.
|
||||
* - makeHeadSampler(): builds the head sampler (see HeadSampler.h).
|
||||
* - NullTelemetryOtel: no-op fallback used when telemetry is compiled in
|
||||
* but disabled at runtime (enabled=0 in config).
|
||||
* - makeTelemetry(): factory that selects the appropriate implementation.
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/telemetry/CoroAwareContextStorage.h>
|
||||
#include <xrpl/telemetry/DeterministicIdGenerator.h>
|
||||
#include <xrpl/telemetry/FilteringSpanProcessor.h>
|
||||
#include <xrpl/telemetry/HeadSampler.h>
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/context/runtime_context.h>
|
||||
#include <opentelemetry/exporters/otlp/otlp_http_exporter_factory.h>
|
||||
#include <opentelemetry/exporters/otlp/otlp_http_exporter_options.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/nostd/string_view.h>
|
||||
#include <opentelemetry/sdk/resource/resource.h>
|
||||
#include <opentelemetry/sdk/trace/batch_span_processor_factory.h>
|
||||
#include <opentelemetry/sdk/trace/batch_span_processor_options.h>
|
||||
#include <opentelemetry/sdk/trace/sampler.h>
|
||||
#include <opentelemetry/sdk/trace/samplers/always_off.h>
|
||||
#include <opentelemetry/sdk/trace/samplers/always_on.h>
|
||||
#include <opentelemetry/sdk/trace/samplers/parent_factory.h>
|
||||
#include <opentelemetry/sdk/trace/samplers/trace_id_ratio.h>
|
||||
#include <opentelemetry/sdk/trace/tracer_provider.h>
|
||||
#include <opentelemetry/sdk/trace/tracer_provider_factory.h>
|
||||
#include <opentelemetry/semconv/incubating/service_attributes.h>
|
||||
#include <opentelemetry/trace/noop.h>
|
||||
#include <opentelemetry/trace/provider.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/span_startoptions.h>
|
||||
#include <opentelemetry/trace/tracer.h>
|
||||
#include <opentelemetry/trace/tracer_provider.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace trace_api = opentelemetry::trace;
|
||||
namespace trace_sdk = opentelemetry::sdk::trace;
|
||||
namespace otlp_http = opentelemetry::exporter::otlp;
|
||||
namespace resource = opentelemetry::sdk::resource;
|
||||
|
||||
/**
|
||||
* View a std::string_view as the SDK's string view, without a copy.
|
||||
*
|
||||
* Without the SDK's STL option, nostd::string_view is a separate type with
|
||||
* no constructor from std::string_view.
|
||||
*
|
||||
* @param text Characters that must outlive the returned view.
|
||||
* @return A view of the same characters.
|
||||
*/
|
||||
[[nodiscard]] opentelemetry::nostd::string_view
|
||||
toOtelView(std::string_view text) noexcept
|
||||
{
|
||||
return {text.data(), text.size()};
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op implementation used when XRPL_ENABLE_TELEMETRY is defined but
|
||||
* setup.enabled is false at runtime.
|
||||
*
|
||||
* Lives in the anonymous namespace so there is no ODR conflict with the
|
||||
* NullTelemetry in NullTelemetry.cpp.
|
||||
*/
|
||||
class NullTelemetryOtel : public Telemetry
|
||||
{
|
||||
/**
|
||||
* Retained configuration (unused, kept for diagnostic access).
|
||||
*/
|
||||
Setup const setup_;
|
||||
|
||||
public:
|
||||
explicit NullTelemetryOtel(Setup setup) : setup_(std::move(setup))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
start() override
|
||||
{
|
||||
Telemetry::setInstance(this);
|
||||
}
|
||||
|
||||
void
|
||||
stop() override
|
||||
{
|
||||
// Clear the global instance only if this object is the one that
|
||||
// published it. A process with two of these, as the test binary has,
|
||||
// would otherwise let one unregister the other.
|
||||
if (Telemetry::getInstance() == this)
|
||||
{
|
||||
Telemetry::setInstance(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isEnabled() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceTransactions() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceConsensus() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceRpc() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTracePeer() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceLedger() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<trace_api::Tracer>
|
||||
getTracer(std::string_view) override
|
||||
{
|
||||
static auto noopTracer =
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Tracer>(new trace_api::NoopTracer());
|
||||
return noopTracer;
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
startSpan(std::string_view, trace_api::SpanKind) override
|
||||
{
|
||||
return opentelemetry::nostd::shared_ptr<trace_api::Span>(new trace_api::NoopSpan(nullptr));
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
startSpan(std::string_view, opentelemetry::context::Context const&, trace_api::SpanKind)
|
||||
override
|
||||
{
|
||||
return opentelemetry::nostd::shared_ptr<trace_api::Span>(new trace_api::NoopSpan(nullptr));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Full OTel SDK implementation that exports trace spans via OTLP/HTTP.
|
||||
*
|
||||
* Configures an OTLP/HTTP exporter, batch span processor,
|
||||
* TraceIdRatioBasedSampler, and resource attributes on start().
|
||||
*/
|
||||
class TelemetryImpl : public Telemetry
|
||||
{
|
||||
/**
|
||||
* Configuration from the [telemetry] config section.
|
||||
* Non-const so setServiceInstanceId() can update the instance ID
|
||||
* before start() creates the OTel resource.
|
||||
*/
|
||||
Setup setup_;
|
||||
|
||||
/**
|
||||
* Journal used for log output during start/stop.
|
||||
*/
|
||||
beast::Journal const journal_;
|
||||
|
||||
/**
|
||||
* The SDK TracerProvider that owns the export pipeline.
|
||||
*
|
||||
* Held as std::shared_ptr so we can call ForceFlush() on shutdown.
|
||||
* Wrapped in a nostd::shared_ptr when registered as the global provider.
|
||||
*/
|
||||
std::shared_ptr<trace_sdk::TracerProvider> sdkProvider_;
|
||||
|
||||
/**
|
||||
* Tracer for kTracerName, cached so a span does not take the provider's
|
||||
* lock.
|
||||
*
|
||||
* Starts as the global provider's tracer. start() sets it to the SDK
|
||||
* tracer before setInstance() publishes this object, and stop() never
|
||||
* writes it, so readers need no lock.
|
||||
*/
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Tracer> tracer_ =
|
||||
trace_api::Provider::GetTracerProvider()->GetTracer(toOtelView(kTracerName));
|
||||
|
||||
/**
|
||||
* Coroutine-aware runtime-context storage, installed globally so the OTel
|
||||
* ambient context follows JobQueue coroutines. Held for the process
|
||||
* lifetime because it must outlive every span (SDK requirement).
|
||||
*/
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::context::RuntimeContextStorage> contextStorage_;
|
||||
|
||||
public:
|
||||
TelemetryImpl(Setup setup, beast::Journal journal) : setup_(std::move(setup)), journal_(journal)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
setServiceInstanceId(std::string const& id) override
|
||||
{
|
||||
setup_.serviceInstanceId = id;
|
||||
}
|
||||
|
||||
void
|
||||
start() override
|
||||
{
|
||||
JLOG(journal_.info()) << "Telemetry starting: traces_endpoint=" << setup_.tracesEndpoint
|
||||
<< " sampling=" << setup_.samplingRatio;
|
||||
|
||||
// Configure OTLP HTTP exporter
|
||||
otlp_http::OtlpHttpExporterOptions exporterOpts;
|
||||
exporterOpts.url = setup_.tracesEndpoint;
|
||||
if (setup_.useTls)
|
||||
{
|
||||
exporterOpts.ssl_ca_cert_path = setup_.tlsCertPath;
|
||||
}
|
||||
|
||||
auto exporter = otlp_http::OtlpHttpExporterFactory::Create(exporterOpts);
|
||||
|
||||
// Configure batch processor
|
||||
trace_sdk::BatchSpanProcessorOptions processorOpts;
|
||||
processorOpts.max_queue_size = setup_.maxQueueSize;
|
||||
processorOpts.schedule_delay_millis = std::chrono::milliseconds(setup_.batchDelay);
|
||||
processorOpts.max_export_batch_size = setup_.batchSize;
|
||||
|
||||
auto batchProcessor =
|
||||
trace_sdk::BatchSpanProcessorFactory::Create(std::move(exporter), processorOpts);
|
||||
|
||||
// Wrap the batch processor. FilteringSpanProcessor drops discarded
|
||||
// spans and exports each attribute key once, with its last value.
|
||||
auto processor = std::make_unique<FilteringSpanProcessor>(std::move(batchProcessor));
|
||||
|
||||
// Configure resource attributes
|
||||
auto resourceAttrs = resource::Resource::Create({
|
||||
{opentelemetry::semconv::service::kServiceName, setup_.serviceName},
|
||||
{opentelemetry::semconv::service::kServiceVersion, setup_.serviceVersion},
|
||||
{opentelemetry::semconv::service::kServiceInstanceId, setup_.serviceInstanceId},
|
||||
{std::string(attr::networkId),
|
||||
static_cast<int64_t>(setup_.networkId)}, // LCOV_EXCL_LINE
|
||||
{std::string(attr::networkType), setup_.networkType}, // LCOV_EXCL_LINE
|
||||
});
|
||||
|
||||
// Head sampling is fixed at 1.0. Spans with no parent or a remote parent use the
|
||||
// trace-id ratio sampler, so a peer's sampled flag cannot turn our spans off or on.
|
||||
// Spans with a local parent follow it. The ratio sampler reads only the trace id,
|
||||
// so nodes agree on every trace. Collector tail sampling reduces volume.
|
||||
auto sampler = makeHeadSampler(setup_.samplingRatio);
|
||||
|
||||
// Create TracerProvider with a DeterministicIdGenerator. It returns a
|
||||
// deterministic trace_id when a PendingTraceId is active on the thread,
|
||||
// else a random one — letting hash-derived roots (introduced on a later
|
||||
// branch) become true trace roots. Dormant until such a caller exists.
|
||||
sdkProvider_ = trace_sdk::TracerProviderFactory::Create(
|
||||
std::move(processor),
|
||||
resourceAttrs,
|
||||
std::move(sampler),
|
||||
std::make_unique<DeterministicIdGenerator>());
|
||||
|
||||
// Install coroutine-aware context storage BEFORE any span is created
|
||||
// so the OTel ambient context follows JobQueue coroutines across
|
||||
// yield/resume (fixes wrong-thread scope pop; keeps log-trace
|
||||
// correlation). Must precede SetTracerProvider and the first span.
|
||||
// Not reset in stop(): resetting the storage while spans may still
|
||||
// exist is undefined behaviour (SDK), and by stop() all spans are
|
||||
// gone, so the storage is simply left installed for process lifetime.
|
||||
contextStorage_ =
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::context::RuntimeContextStorage>(
|
||||
new CoroAwareContextStorage());
|
||||
opentelemetry::context::RuntimeContext::SetRuntimeContextStorage(contextStorage_);
|
||||
|
||||
// Set as global provider
|
||||
trace_api::Provider::SetTracerProvider(
|
||||
opentelemetry::nostd::shared_ptr<trace_api::TracerProvider>(sdkProvider_));
|
||||
|
||||
// GetTracer() locks the provider, so fetch the tracer once here rather
|
||||
// than per span. It must be set before setInstance() publishes it.
|
||||
tracer_ = sdkProvider_->GetTracer(toOtelView(kTracerName));
|
||||
|
||||
// Register as the global Telemetry instance so SpanGuard factory
|
||||
// methods can access it without callers passing a reference.
|
||||
Telemetry::setInstance(this);
|
||||
|
||||
JLOG(journal_.info()) << "Telemetry started successfully";
|
||||
}
|
||||
|
||||
void
|
||||
stop() override
|
||||
{
|
||||
JLOG(journal_.info()) << "Telemetry stopping";
|
||||
|
||||
// Unregister global instance before tearing down the pipeline, but only
|
||||
// if this object is the one that published it.
|
||||
if (Telemetry::getInstance() == this)
|
||||
{
|
||||
Telemetry::setInstance(nullptr);
|
||||
}
|
||||
|
||||
if (sdkProvider_)
|
||||
{
|
||||
// Force flush with timeout to avoid blocking indefinitely
|
||||
// when the OTLP endpoint is unreachable.
|
||||
sdkProvider_->ForceFlush(std::chrono::milliseconds(5000));
|
||||
// startSpan() and getTracer() never read sdkProvider_, so this
|
||||
// reset() cannot race with them. A span started after stop()
|
||||
// still goes through tracer_, into the shut-down pipeline, and is
|
||||
// dropped rather than exported.
|
||||
sdkProvider_.reset();
|
||||
trace_api::Provider::SetTracerProvider(
|
||||
opentelemetry::nostd::shared_ptr<trace_api::TracerProvider>(
|
||||
new trace_api::NoopTracerProvider()));
|
||||
}
|
||||
JLOG(journal_.info()) << "Telemetry stopped";
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isEnabled() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceTransactions() const override
|
||||
{
|
||||
return setup_.traceTransactions;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceConsensus() const override
|
||||
{
|
||||
return setup_.traceConsensus;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceRpc() const override
|
||||
{
|
||||
return setup_.traceRpc;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTracePeer() const override
|
||||
{
|
||||
return setup_.tracePeer;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceLedger() const override
|
||||
{
|
||||
return setup_.traceLedger;
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<trace_api::Tracer>
|
||||
getTracer(std::string_view name = kTracerName) override
|
||||
{
|
||||
if (name == kTracerName)
|
||||
return tracer_;
|
||||
return trace_api::Provider::GetTracerProvider()->GetTracer(toOtelView(name));
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
startSpan(std::string_view name, trace_api::SpanKind kind) override
|
||||
{
|
||||
trace_api::StartSpanOptions opts;
|
||||
opts.kind = kind;
|
||||
return tracer_->StartSpan(toOtelView(name), opts);
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
startSpan(
|
||||
std::string_view name,
|
||||
opentelemetry::context::Context const& parentContext,
|
||||
trace_api::SpanKind kind) override
|
||||
{
|
||||
trace_api::StartSpanOptions opts;
|
||||
opts.kind = kind;
|
||||
opts.parent = parentContext;
|
||||
return tracer_->StartSpan(toOtelView(name), opts);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<trace_sdk::Sampler>
|
||||
makeHeadSampler(double ratio)
|
||||
{
|
||||
// One ratio sampler serves the root case and both remote-parent cases.
|
||||
std::shared_ptr<trace_sdk::Sampler> const ratioSampler =
|
||||
std::make_shared<trace_sdk::TraceIdRatioBasedSampler>(ratio);
|
||||
return trace_sdk::ParentBasedSamplerFactory::Create(
|
||||
ratioSampler,
|
||||
ratioSampler,
|
||||
ratioSampler,
|
||||
std::make_shared<trace_sdk::AlwaysOnSampler>(),
|
||||
std::make_shared<trace_sdk::AlwaysOffSampler>());
|
||||
}
|
||||
|
||||
std::unique_ptr<Telemetry>
|
||||
makeTelemetry(Telemetry::Setup const& setup, beast::Journal journal)
|
||||
{
|
||||
if (setup.enabled)
|
||||
{
|
||||
return std::make_unique<TelemetryImpl>(setup, journal);
|
||||
}
|
||||
return std::make_unique<NullTelemetryOtel>(setup);
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,278 +0,0 @@
|
||||
/**
|
||||
* Parser for the [telemetry] section of xrpld.cfg.
|
||||
*
|
||||
* Reads configuration values from the config file and populates a
|
||||
* Telemetry::Setup struct. All options have sensible defaults so the
|
||||
* section can be minimal or omitted entirely.
|
||||
*
|
||||
* See cfg/xrpld-example.cfg for the full list of available options.
|
||||
*/
|
||||
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Config key names for the [telemetry] section.
|
||||
*
|
||||
* Each must match the corresponding option documented in
|
||||
* cfg/xrpld-example.cfg verbatim. Defined as `char const*` so they
|
||||
* pass to Section::valueOr() (which takes `std::string const&`)
|
||||
* without an explicit conversion, exactly as a literal would.
|
||||
*/
|
||||
namespace key {
|
||||
constexpr char const* enabled = "enabled";
|
||||
constexpr char const* serviceName = "service_name";
|
||||
constexpr char const* serviceInstanceId = "service_instance_id";
|
||||
constexpr char const* tracesEndpoint = "traces_endpoint";
|
||||
constexpr char const* useTls = "use_tls";
|
||||
constexpr char const* tlsCaCert = "tls_ca_cert";
|
||||
constexpr char const* batchSize = "batch_size";
|
||||
constexpr char const* batchDelayMs = "batch_delay_ms";
|
||||
constexpr char const* maxQueueSize = "max_queue_size";
|
||||
constexpr char const* traceTransactions = "trace_transactions";
|
||||
constexpr char const* traceConsensus = "trace_consensus";
|
||||
constexpr char const* traceRpc = "trace_rpc";
|
||||
constexpr char const* tracePeer = "trace_peer";
|
||||
constexpr char const* traceLedger = "trace_ledger";
|
||||
} // namespace key
|
||||
|
||||
/**
|
||||
* Default values applied when a key is absent from the config.
|
||||
*
|
||||
* @note serviceName mirrors SystemParameters' systemName() ("xrpld") but
|
||||
* is duplicated here as a literal: the telemetry module deliberately does
|
||||
* not link xrpl.libxrpl.protocol, so including SystemParameters.h would
|
||||
* introduce an undeclared cross-module dependency.
|
||||
*/
|
||||
namespace dflt {
|
||||
constexpr char const* serviceName = "xrpld";
|
||||
constexpr char const* tracesEndpoint = "http://localhost:4318/v1/traces";
|
||||
constexpr std::uint32_t batchSize = 512u;
|
||||
constexpr std::uint32_t batchDelayMs = 5000u;
|
||||
constexpr std::uint32_t maxQueueSize = 2048u;
|
||||
} // namespace dflt
|
||||
|
||||
/**
|
||||
* Smallest accepted value for the three batch settings.
|
||||
*
|
||||
* All three size a queue or a timer, so zero is meaningless for every one of
|
||||
* them. The OTel BatchSpanProcessor takes them as given and does not validate,
|
||||
* so the config parser is the only place a nonsense value can be rejected.
|
||||
*/
|
||||
constexpr std::uint32_t kMinBatchSetting = 1u;
|
||||
|
||||
/**
|
||||
* Section name used in error messages, so the operator knows where to look.
|
||||
*/
|
||||
constexpr char const* kSectionLabel = "[telemetry]";
|
||||
|
||||
/**
|
||||
* Read a config value and reject anything outside minValue..UINT32_MAX.
|
||||
*
|
||||
* Section::get() lets boost::bad_lexical_cast escape. That derives from
|
||||
* std::bad_cast, not std::runtime_error, so a mistyped value gives the operator
|
||||
* a bare "bad cast" naming no key. Wrap it and rethrow with the key name.
|
||||
*
|
||||
* @param section The [telemetry] section to read from.
|
||||
* @param name Key to read, as documented in cfg/xrpld-example.cfg.
|
||||
* @param absentValue Value returned when the key is absent.
|
||||
* @param minValue Smallest accepted value.
|
||||
* @return The configured value, or absentValue if the key is absent.
|
||||
* @note Throws std::runtime_error for a value that is not a whole number, and
|
||||
* for one out of range, with a different message for each.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
readBounded(
|
||||
Section const& section,
|
||||
char const* name,
|
||||
std::uint32_t absentValue,
|
||||
std::uint32_t minValue)
|
||||
{
|
||||
// Read as signed. boost::lexical_cast to an unsigned type wraps a leading
|
||||
// minus instead of failing ("-1" yields 4294967295), so reading signed is
|
||||
// the only way to see a negative value and reject it below.
|
||||
std::optional<std::int64_t> parsed;
|
||||
try
|
||||
{
|
||||
parsed = section.get<std::int64_t>(name);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Invalid value '") + name + "' in " + kSectionLabel +
|
||||
": must be a whole number.");
|
||||
}
|
||||
|
||||
if (!parsed)
|
||||
return absentValue;
|
||||
|
||||
constexpr auto maxValue = static_cast<std::int64_t>(std::numeric_limits<std::uint32_t>::max());
|
||||
if (*parsed < static_cast<std::int64_t>(minValue) || *parsed > maxValue)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Invalid value '") + name + "' in " + kSectionLabel + ": must be between " +
|
||||
std::to_string(minValue) + " and " + std::to_string(maxValue) + ".");
|
||||
}
|
||||
|
||||
return static_cast<std::uint32_t>(*parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an on/off key. Accepts 0, 1, true or false, in any case.
|
||||
*
|
||||
* Reading it as a number would throw boost::bad_lexical_cast for "true",
|
||||
* naming no key. This reads the text and names the key instead.
|
||||
*
|
||||
* @param section The [telemetry] section to read from.
|
||||
* @param name Key to read, as documented in cfg/xrpld-example.cfg.
|
||||
* @param absentValue Value returned when the key is absent.
|
||||
* @return The configured value, or absentValue if the key is absent.
|
||||
* @throws std::runtime_error For any other value, with a message naming the
|
||||
* key.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
readFlag(Section const& section, std::string_view name, bool absentValue)
|
||||
{
|
||||
std::string const keyName{name};
|
||||
auto const text = section.get(keyName);
|
||||
if (!text)
|
||||
return absentValue;
|
||||
|
||||
bool flag = false;
|
||||
if (!beast::lexicalCastChecked(flag, *text))
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid value '" + keyName + "' in " + kSectionLabel +
|
||||
": must be 0, 1, true or false.");
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a human-readable network type label from the numeric network ID.
|
||||
* @param networkId The network identifier from [network_id] config.
|
||||
* @return "mainnet", "testnet", "devnet", or "unknown" for other values.
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
networkTypeFromId(std::uint32_t networkId)
|
||||
{
|
||||
switch (networkId)
|
||||
{
|
||||
case 0:
|
||||
return "mainnet";
|
||||
case 1:
|
||||
return "testnet";
|
||||
case 2:
|
||||
return "devnet";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw unless an endpoint URL is one TLS can actually be used on.
|
||||
*
|
||||
* The OTLP/HTTP exporter turns TLS on from the URL scheme alone, and matches
|
||||
* "https:" exactly and case-sensitively. So the certificate settings only mean
|
||||
* anything on an https endpoint, and this check is what holds that invariant:
|
||||
* with TLS asked for, the endpoint is an https URL. "https://" is required in
|
||||
* full, which is stricter than the exporter's own test, so anything this
|
||||
* accepts the exporter also treats as TLS.
|
||||
*
|
||||
* @param endpoint Endpoint URL from the config, or the built-in default.
|
||||
* @param configKey Config key the URL came from, named in the message.
|
||||
* @throws std::runtime_error If the URL does not begin with "https://".
|
||||
*/
|
||||
void
|
||||
requireHttpsEndpoint(std::string const& endpoint, char const* configKey)
|
||||
{
|
||||
constexpr std::string_view kHttpsPrefix{"https://"};
|
||||
|
||||
if (std::string_view{endpoint}.starts_with(kHttpsPrefix))
|
||||
return;
|
||||
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Invalid value '") + configKey + "' in " + kSectionLabel +
|
||||
": must start with '" + std::string{kHttpsPrefix} + "' when " + key::useTls +
|
||||
"=1, but is '" + endpoint + "'.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Telemetry::Setup
|
||||
makeTelemetrySetup(
|
||||
Section const& section,
|
||||
std::string const& nodePublicKey,
|
||||
std::string const& version,
|
||||
std::uint32_t networkId)
|
||||
{
|
||||
Telemetry::Setup setup;
|
||||
|
||||
setup.enabled = readFlag(section, key::enabled, false);
|
||||
setup.serviceName = section.valueOr<std::string>(key::serviceName, dflt::serviceName);
|
||||
setup.serviceVersion = version;
|
||||
setup.serviceInstanceId = section.valueOr<std::string>(key::serviceInstanceId, nodePublicKey);
|
||||
|
||||
setup.tracesEndpoint = section.valueOr<std::string>(key::tracesEndpoint, dflt::tracesEndpoint);
|
||||
|
||||
setup.useTls = readFlag(section, key::useTls, false);
|
||||
setup.tlsCertPath = section.valueOr<std::string>(key::tlsCaCert, "");
|
||||
// Checked only when `enabled` is 1, so a leftover TLS line never stops a
|
||||
// node with telemetry off from booting. The exporter reads TLS off the
|
||||
// endpoint scheme alone, so use_tls=1 takes effect only on an https
|
||||
// endpoint. An operator who asks for TLS gets TLS or a startup error.
|
||||
if (setup.enabled && setup.useTls)
|
||||
{
|
||||
requireHttpsEndpoint(setup.tracesEndpoint, key::tracesEndpoint);
|
||||
}
|
||||
|
||||
// Head sampling is intentionally fixed at 1.0 (sample everything) and is
|
||||
// not read from config. A per-node ratio would let nodes make divergent
|
||||
// keep/drop decisions for the same distributed trace, producing broken
|
||||
// traces; volume reduction is delegated to the collector's tail sampling.
|
||||
// setup.samplingRatio is a const member fixed at 1.0; nothing to parse.
|
||||
|
||||
setup.batchSize = readBounded(section, key::batchSize, dflt::batchSize, kMinBatchSetting);
|
||||
setup.batchDelay = std::chrono::milliseconds{
|
||||
readBounded(section, key::batchDelayMs, dflt::batchDelayMs, kMinBatchSetting)};
|
||||
setup.maxQueueSize =
|
||||
readBounded(section, key::maxQueueSize, dflt::maxQueueSize, kMinBatchSetting);
|
||||
|
||||
// The OTel SDK documents max_export_batch_size <= max_queue_size as a
|
||||
// precondition of BatchSpanProcessorOptions and does not enforce it, so
|
||||
// reject the pair here rather than hand the SDK a state it forbids.
|
||||
if (setup.batchSize > setup.maxQueueSize)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Invalid value '") + key::batchSize + "' in " + kSectionLabel +
|
||||
": must not exceed '" + key::maxQueueSize + "' (" + std::to_string(setup.maxQueueSize) +
|
||||
").");
|
||||
}
|
||||
|
||||
setup.networkId = networkId;
|
||||
setup.networkType = networkTypeFromId(networkId);
|
||||
|
||||
setup.traceTransactions = readFlag(section, key::traceTransactions, true);
|
||||
setup.traceConsensus = readFlag(section, key::traceConsensus, true);
|
||||
setup.traceRpc = readFlag(section, key::traceRpc, true);
|
||||
setup.tracePeer = readFlag(section, key::tracePeer, true);
|
||||
setup.traceLedger = readFlag(section, key::traceLedger, true);
|
||||
|
||||
return setup;
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <xrpl/json/to_string.h> // IWYU pragma: keep
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DelegateHelpers.h>
|
||||
@@ -586,7 +587,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
|
||||
|
||||
if (feePayer.type == FeePayerType::SponsorCoSigned)
|
||||
{
|
||||
auto const sponsorReserve = accountReserve(ctx.view, payerSle, ctx.j);
|
||||
auto const sponsorReserve = accountReserve(ctx.view, feePayer.id, ctx.j);
|
||||
maxSpendable = payerSle->getFieldAmount(sfBalance).xrp() - sponsorReserve;
|
||||
}
|
||||
else
|
||||
@@ -652,7 +653,7 @@ Transactor::payFee()
|
||||
XRPAmount spendable = balance;
|
||||
if (feePayer.type == FeePayerType::SponsorCoSigned)
|
||||
{
|
||||
auto const sponsorReserve = accountReserve(view(), sle, j_);
|
||||
auto const sponsorReserve = accountReserve(view(), feePayer.id, j_);
|
||||
// max(balance - reserve, 0) with overflow handling
|
||||
spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero;
|
||||
}
|
||||
@@ -700,7 +701,7 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
|
||||
{
|
||||
auto const id = tx.getAccountID(sfAccount);
|
||||
|
||||
auto const sle = view.read(keylet::account(id));
|
||||
auto const sle = AccountRootEntryR(id, view);
|
||||
|
||||
if (!sle)
|
||||
{
|
||||
@@ -765,7 +766,7 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const id = ctx.tx.getAccountID(sfAccount);
|
||||
|
||||
auto const sle = ctx.view.read(keylet::account(id));
|
||||
auto const sle = AccountRootEntryR(id, ctx.view);
|
||||
|
||||
if (!sle)
|
||||
{
|
||||
@@ -835,7 +836,7 @@ Transactor::ticketDelete(
|
||||
|
||||
// Update the account root's TicketCount. If the ticket count drops to
|
||||
// zero remove the (optional) field.
|
||||
auto sleAccount = view.peek(keylet::account(account));
|
||||
auto sleAccount = AccountRootEntryW(account, view);
|
||||
if (!sleAccount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -885,19 +886,19 @@ Transactor::apply()
|
||||
|
||||
// If the transactor requires a valid account and the transaction doesn't
|
||||
// list one, preflight will have already a flagged a failure.
|
||||
auto const sle = view().peek(keylet::account(accountID_));
|
||||
auto sle = AccountRootEntryW(accountID_, view());
|
||||
|
||||
// sle must exist except for transactions
|
||||
// that allow zero account.
|
||||
XRPL_ASSERT(
|
||||
sle != nullptr || accountID_ == beast::kZero,
|
||||
sle.exists() || accountID_ == beast::kZero,
|
||||
"xrpl::Transactor::apply : non-null SLE or zero account");
|
||||
|
||||
if (sle)
|
||||
{
|
||||
preFeeBalance_ = STAmount{(*sle)[sfBalance]}.xrp();
|
||||
|
||||
TER result = consumeSeqProxy(sle);
|
||||
TER result = consumeSeqProxy(sle.mutableRawSle());
|
||||
if (!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
@@ -908,7 +909,7 @@ Transactor::apply()
|
||||
if (sle->isFieldPresent(sfAccountTxnID))
|
||||
sle->setFieldH256(sfAccountTxnID, ctx_.tx.getTransactionID());
|
||||
|
||||
view().update(sle);
|
||||
sle.update();
|
||||
}
|
||||
|
||||
return doApply();
|
||||
@@ -925,7 +926,7 @@ Transactor::checkSign(
|
||||
bool permitUncreatedAccount)
|
||||
{
|
||||
{
|
||||
auto const sle = view.read(keylet::account(idAccount));
|
||||
auto const sle = AccountRootEntryR(idAccount, view);
|
||||
|
||||
if ((view.rules().enabled(featureLendingProtocol) ||
|
||||
view.rules().enabled(featureBatchV1_1) || view.rules().enabled(fixCleanup3_3_0)) &&
|
||||
@@ -991,7 +992,7 @@ Transactor::checkSign(
|
||||
|
||||
// Look up the account.
|
||||
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
|
||||
auto const sleAccount = view.read(keylet::account(idAccount));
|
||||
auto const sleAccount = AccountRootEntryR(idAccount, view);
|
||||
if (!sleAccount)
|
||||
{
|
||||
// An account that does not exist yet can only be authorized by its own
|
||||
@@ -1004,7 +1005,7 @@ Transactor::checkSign(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
return checkSingleSign(view, idSigner, idAccount, sleAccount, j);
|
||||
return checkSingleSign(view, idSigner, idAccount, sleAccount.rawSle(), j);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
@@ -1153,7 +1154,7 @@ Transactor::checkMultiSign(
|
||||
|
||||
// In any of these cases we need to know whether the account is in
|
||||
// the ledger. Determine that now.
|
||||
auto const sleTxSignerRoot = view.read(keylet::account(txSignerAcctID));
|
||||
auto const sleTxSignerRoot = AccountRootEntryR(txSignerAcctID, view);
|
||||
|
||||
if (signingAcctIDFromPubKey == txSignerAcctID)
|
||||
{
|
||||
@@ -1296,7 +1297,7 @@ Transactor::reset(XRPAmount fee)
|
||||
{
|
||||
ctx_.discard();
|
||||
|
||||
auto const txnAcct = view().peek(keylet::account(ctx_.tx.getAccountID(sfAccount)));
|
||||
auto txnAcct = AccountRootEntryW(ctx_.tx.getAccountID(sfAccount), view());
|
||||
|
||||
// The account should never be missing from the ledger. But if it
|
||||
// is missing then we can't very well charge it a fee, can we?
|
||||
@@ -1331,7 +1332,7 @@ Transactor::reset(XRPAmount fee)
|
||||
XRPAmount spendable = balance;
|
||||
if (feePayer.type == FeePayerType::SponsorCoSigned)
|
||||
{
|
||||
auto const sponsorReserve = accountReserve(view(), payerSle, j_);
|
||||
auto const sponsorReserve = accountReserve(view(), feePayer.id, j_);
|
||||
// max(balance - reserve, 0) with overflow handling
|
||||
spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero;
|
||||
}
|
||||
@@ -1364,13 +1365,13 @@ Transactor::reset(XRPAmount fee)
|
||||
payerSle->setFieldAmount(feePayer.balanceField, feeAmountAfter);
|
||||
}
|
||||
|
||||
TER const ter{consumeSeqProxy(txnAcct)};
|
||||
TER const ter{consumeSeqProxy(txnAcct.mutableRawSle())};
|
||||
XRPL_ASSERT(isTesSuccess(ter), "xrpl::Transactor::reset : result is tesSUCCESS");
|
||||
|
||||
if (isTesSuccess(ter))
|
||||
{
|
||||
view().update(txnAcct);
|
||||
if (payerSle != txnAcct)
|
||||
txnAcct.update();
|
||||
if (payerSle != txnAcct.mutableRawSle())
|
||||
view().update(payerSle);
|
||||
}
|
||||
|
||||
|
||||
@@ -735,6 +735,15 @@ NoDeepFreezeTrustLinesWithoutFreeze::finalize(
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
bool
|
||||
isPseudoAccountSle(SLE::ConstRef sleAcct)
|
||||
{
|
||||
auto const& fields = getPseudoAccountFields();
|
||||
return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT &&
|
||||
std::ranges::any_of(
|
||||
fields, [&sleAcct](SField const* sf) { return sleAcct->isFieldPresent(*sf); });
|
||||
}
|
||||
|
||||
void
|
||||
ValidNewAccountRoot::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
|
||||
{
|
||||
@@ -742,7 +751,7 @@ ValidNewAccountRoot::visitEntry(bool, SLE::ConstRef before, SLE::ConstRef after)
|
||||
{
|
||||
accountsCreated_++;
|
||||
accountSeq_ = (*after)[sfSequence];
|
||||
pseudoAccount_ = isPseudoAccount(after);
|
||||
pseudoAccount_ = isPseudoAccountSle(after);
|
||||
flags_ = after->getFlags();
|
||||
}
|
||||
}
|
||||
@@ -1020,9 +1029,9 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstR
|
||||
if (after && after->getType() == ltACCOUNT_ROOT)
|
||||
{
|
||||
bool const isPseudo = [&]() {
|
||||
// isPseudoAccount checks that any of the pseudo-account fields are
|
||||
// set.
|
||||
if (isPseudoAccount(after))
|
||||
// isPseudoAccountSle checks that any of the pseudo-account fields
|
||||
// are set.
|
||||
if (isPseudoAccountSle(after))
|
||||
return true;
|
||||
// Not all pseudo-accounts have a zero sequence, but all accounts
|
||||
// with a zero sequence had better be pseudo-accounts.
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheck.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -893,7 +894,7 @@ ValidMPTTransfer::visitEntry(
|
||||
// no trace of its pseudo-account status in the post-transaction view
|
||||
// isAuthorized() sees at finalize() time.
|
||||
if (before && before->getType() == ltACCOUNT_ROOT)
|
||||
pseudoAccountsBefore_[before->at(sfAccount)] = isPseudoAccount(before);
|
||||
pseudoAccountsBefore_[before->at(sfAccount)] = isPseudoAccountSle(before);
|
||||
}
|
||||
|
||||
bool
|
||||
|
||||
@@ -740,7 +740,7 @@ ValidVault::finalize(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPseudoAccount(sleSharesIssuer))
|
||||
if (!isPseudoAccount(AccountRootEntryR(sleSharesIssuer, view)))
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: shares issuer must be a "
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -836,7 +837,7 @@ DirectStepI<TDerived>::check(StrandContext const& ctx) const
|
||||
return temBAD_PATH;
|
||||
}
|
||||
|
||||
auto const sleSrc = ctx.view.read(keylet::account(src_));
|
||||
auto const sleSrc = AccountRootEntryR(src_, ctx.view);
|
||||
if (!sleSrc)
|
||||
{
|
||||
JLOG(j_.warn()) << "DirectStepI: can't receive IOUs from non-existent issuer: " << src_;
|
||||
@@ -902,7 +903,7 @@ DirectStepI<TDerived>::check(StrandContext const& ctx) const
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<TDerived const*>(this)->check(ctx, sleSrc);
|
||||
return static_cast<TDerived const*>(this)->check(ctx, sleSrc.rawSle());
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -865,7 +866,7 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
|
||||
return temBAD_PATH;
|
||||
}
|
||||
|
||||
auto const sleSrc = ctx.view.read(keylet::account(src_));
|
||||
auto const sleSrc = AccountRootEntryR(src_, ctx.view);
|
||||
if (!sleSrc)
|
||||
{
|
||||
JLOG(j_.warn()) << "MPTEndpointStep: can't receive MPT from non-existent issuer: " << src_;
|
||||
@@ -928,7 +929,7 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
|
||||
return temBAD_PATH;
|
||||
}
|
||||
|
||||
return static_cast<TDerived const*>(this)->check(ctx, sleSrc);
|
||||
return static_cast<TDerived const*>(this)->check(ctx, sleSrc.rawSle());
|
||||
}
|
||||
|
||||
template <class TDerived>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -342,7 +343,7 @@ XRPEndpointStep<TDerived>::check(StrandContext const& ctx) const
|
||||
return temBAD_PATH;
|
||||
}
|
||||
|
||||
auto sleAcc = ctx.view.read(keylet::account(acc_));
|
||||
auto sleAcc = AccountRootEntryR(acc_, ctx.view);
|
||||
if (!sleAcc)
|
||||
{
|
||||
JLOG(j_.warn()) << "XRPEndpointStep: can't send or receive XRP from "
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
|
||||
@@ -224,7 +225,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
|
||||
AccountID const account{ctx.tx[sfAccount]};
|
||||
AccountID const dst{ctx.tx[sfDestination]};
|
||||
|
||||
auto sleDst = ctx.view.read(keylet::account(dst));
|
||||
auto sleDst = AccountRootEntryR(dst, ctx.view);
|
||||
|
||||
if (!sleDst)
|
||||
return tecNO_DST;
|
||||
@@ -250,7 +251,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
auto sleAccount = ctx.view.read(keylet::account(account));
|
||||
auto sleAccount = AccountRootEntryR(account, ctx.view);
|
||||
XRPL_ASSERT(sleAccount, "xrpl::AccountDelete::preclaim : non-null account");
|
||||
if (!sleAccount)
|
||||
return terNO_ACCOUNT;
|
||||
@@ -353,11 +354,11 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
|
||||
TER
|
||||
AccountDelete::doApply()
|
||||
{
|
||||
auto src = view().peek(keylet::account(accountID_));
|
||||
auto src = AccountRootEntryW(accountID_, view());
|
||||
XRPL_ASSERT(src, "xrpl::AccountDelete::doApply : non-null source account");
|
||||
|
||||
auto const dstID = ctx_.tx[sfDestination];
|
||||
auto dst = view().peek(keylet::account(dstID));
|
||||
auto dst = AccountRootEntryW(dstID, view());
|
||||
XRPL_ASSERT(dst, "xrpl::AccountDelete::doApply : non-null destination account");
|
||||
|
||||
if (!src || !dst)
|
||||
@@ -365,8 +366,8 @@ AccountDelete::doApply()
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfCredentialIDs))
|
||||
{
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, dstID, dst, ctx_.journal);
|
||||
if (auto err = verifyDepositPreauth(
|
||||
ctx_.tx, ctx_.view(), accountID_, dstID, dst.rawSle(), ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
@@ -407,7 +408,7 @@ AccountDelete::doApply()
|
||||
if (src->isFieldPresent(sfSponsor))
|
||||
{
|
||||
auto const sponsorID = src->getAccountID(sfSponsor);
|
||||
auto sponsorSle = view().peek(keylet::account(sponsorID));
|
||||
auto sponsorSle = AccountRootEntryW(sponsorID, view());
|
||||
|
||||
if (!sponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -425,7 +426,7 @@ AccountDelete::doApply()
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
sponsorSle->at(sfSponsoringAccountCount) = sponsoringAccountCount - 1;
|
||||
view().update(sponsorSle);
|
||||
sponsorSle.update();
|
||||
|
||||
// Following line might look redundant, but without it, sfSponsor
|
||||
// would end up remaining in after-ltAccountRoot during the
|
||||
@@ -448,8 +449,8 @@ AccountDelete::doApply()
|
||||
if (remainingBalance > XRPAmount(0) && dst->isFlag(lsfPasswordSpent))
|
||||
dst->clearFlag(lsfPasswordSpent);
|
||||
|
||||
view().update(dst);
|
||||
view().erase(src);
|
||||
dst.update();
|
||||
src.erase();
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -170,7 +171,7 @@ AccountSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const id = ctx.tx[sfAccount];
|
||||
|
||||
auto const sle = ctx.view.read(keylet::account(id));
|
||||
auto const sle = AccountRootEntryR(id, ctx.view);
|
||||
if (!sle)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
@@ -224,7 +225,7 @@ AccountSet::preclaim(PreclaimContext const& ctx)
|
||||
TER
|
||||
AccountSet::doApply()
|
||||
{
|
||||
auto const sle = view().peek(keylet::account(accountID_));
|
||||
auto sle = AccountRootEntryW(accountID_, view());
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -582,7 +583,7 @@ AccountSet::doApply()
|
||||
if (uFlagsIn != uFlagsOut)
|
||||
sle->setFieldU32(sfFlags, uFlagsOut);
|
||||
|
||||
ctx_.view().update(sle);
|
||||
sle.update();
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
@@ -25,7 +26,7 @@ SetRegularKey::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
if (calcAccountID(PublicKey(makeSlice(spk))) == id)
|
||||
{
|
||||
auto const sle = view.read(keylet::account(id));
|
||||
auto const sle = AccountRootEntryR(id, view);
|
||||
|
||||
if (sle && !sle->isFlag(lsfPasswordSpent))
|
||||
{
|
||||
@@ -53,7 +54,7 @@ SetRegularKey::preflight(PreflightContext const& ctx)
|
||||
TER
|
||||
SetRegularKey::doApply()
|
||||
{
|
||||
auto const sle = view().peek(keylet::account(accountID_));
|
||||
auto sle = AccountRootEntryW(accountID_, view());
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -73,7 +74,7 @@ SetRegularKey::doApply()
|
||||
sle->makeFieldAbsent(sfRegularKey);
|
||||
}
|
||||
|
||||
ctx_.view().update(sle);
|
||||
sle.update();
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -213,8 +214,9 @@ removeSignersFromLedger(
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
AccountRootEntryW accountSle(accountKeylet, view);
|
||||
decreaseOwnerCountForObject(
|
||||
view, view.peek(accountKeylet), signers, removeFromOwnerCount, registry.getJournal("View"));
|
||||
view, accountSle, signers, removeFromOwnerCount, registry.getJournal("View"));
|
||||
|
||||
view.erase(signers);
|
||||
|
||||
@@ -309,7 +311,7 @@ SignerListSet::replaceSignerList()
|
||||
ctx_.registry, view(), accountKeylet, ownerDirKeylet, signerListKeylet, j_))
|
||||
return ter;
|
||||
|
||||
auto const sle = view().peek(accountKeylet);
|
||||
auto sle = AccountRootEntryW(accountKeylet, view(), ctx_.journal);
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -319,12 +321,8 @@ SignerListSet::replaceSignerList()
|
||||
// We check the reserve against the starting balance because we want to
|
||||
// allow dipping into the reserve to pay fees. This behavior is consistent
|
||||
// with TicketCreate.
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sle,
|
||||
preFeeBalance_,
|
||||
{.ownerCountDelta = kAddedOwnerCount},
|
||||
ctx_.journal);
|
||||
if (auto const ret = sle.checkReserve(
|
||||
ctx_.getApplyViewContext(), preFeeBalance_, {.ownerCountDelta = kAddedOwnerCount});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -333,7 +331,6 @@ SignerListSet::replaceSignerList()
|
||||
view().insert(signerList);
|
||||
writeSignersToSLE(signerList, flags);
|
||||
|
||||
auto viewJ = ctx_.registry.get().getJournal("View");
|
||||
// Add the signer list to the account's directory.
|
||||
auto const page =
|
||||
ctx_.view().dirInsert(ownerDirKeylet, signerListKeylet, describeOwnerDir(accountID_));
|
||||
@@ -348,7 +345,7 @@ SignerListSet::replaceSignerList()
|
||||
|
||||
// If we succeeded, the new entry counts against the
|
||||
// creator's reserve.
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, kAddedOwnerCount, viewJ);
|
||||
sle.increaseOwnerCount(ctx_.getApplyViewContext(), kAddedOwnerCount);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), signerList);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -359,7 +356,7 @@ SignerListSet::destroySignerList()
|
||||
auto const accountKeylet = keylet::account(accountID_);
|
||||
// Destroying the signer list is only allowed if either the master key
|
||||
// is enabled or there is a regular key.
|
||||
SLE::pointer const ledgerEntry = view().peek(accountKeylet);
|
||||
auto const ledgerEntry = AccountRootEntryW(accountKeylet, view());
|
||||
if (!ledgerEntry)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/RawView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -125,8 +126,7 @@ checkAttestationPublicKey(
|
||||
|
||||
AccountID const accountFromPK = calcAccountID(pk);
|
||||
|
||||
if (auto const sleAttestationSigningAccount =
|
||||
view.read(keylet::account(attestationSignerAccount)))
|
||||
if (auto const sleAttestationSigningAccount = AccountRootEntryR(attestationSignerAccount, view))
|
||||
{
|
||||
if (accountFromPK == attestationSignerAccount)
|
||||
{
|
||||
@@ -404,7 +404,7 @@ transferHelper(
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const dstK = keylet::account(dst);
|
||||
if (auto sleDst = psb.read(dstK))
|
||||
if (auto sleDst = AccountRootEntryR(dstK, psb))
|
||||
{
|
||||
// Check dst tag and deposit auth
|
||||
|
||||
@@ -430,13 +430,13 @@ transferHelper(
|
||||
|
||||
if (amt.native())
|
||||
{
|
||||
auto const sleSrc = psb.peek(keylet::account(src));
|
||||
auto sleSrc = AccountRootEntryW(src, psb, j);
|
||||
XRPL_ASSERT(sleSrc, "xrpl::transferHelper : non-null source account");
|
||||
if (!sleSrc)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
{
|
||||
auto const reserve = accountReserve(psb, sleSrc, j);
|
||||
auto const reserve = sleSrc.reserve();
|
||||
|
||||
auto const availableBalance = [&]() -> STAmount {
|
||||
STAmount curBal = (*sleSrc)[sfBalance];
|
||||
@@ -455,7 +455,7 @@ transferHelper(
|
||||
}
|
||||
}
|
||||
|
||||
auto sleDst = psb.peek(dstK);
|
||||
auto sleDst = AccountRootEntryW(dstK, psb);
|
||||
if (!sleDst)
|
||||
{
|
||||
if (canCreate == CanCreateDstPolicy::No)
|
||||
@@ -470,17 +470,17 @@ transferHelper(
|
||||
}
|
||||
|
||||
// Create the account.
|
||||
sleDst = std::make_shared<SLE>(dstK);
|
||||
sleDst.newSLE();
|
||||
sleDst->setAccountID(sfAccount, dst);
|
||||
sleDst->setFieldU32(sfSequence, psb.seq());
|
||||
|
||||
psb.insert(sleDst);
|
||||
sleDst.insert();
|
||||
}
|
||||
|
||||
(*sleSrc)[sfBalance] = (*sleSrc)[sfBalance] - amt;
|
||||
(*sleDst)[sfBalance] = (*sleDst)[sfBalance] + amt;
|
||||
psb.update(sleSrc);
|
||||
psb.update(sleDst);
|
||||
sleSrc.update();
|
||||
sleDst.update();
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -724,7 +724,7 @@ finalizeClaimHelper(
|
||||
auto const cidOwner = (*sleClaimID)[sfAccount];
|
||||
{
|
||||
// Remove the claim id
|
||||
auto const sleOwner = outerSb.peek(keylet::account(cidOwner));
|
||||
auto sleOwner = AccountRootEntryW(cidOwner, outerSb);
|
||||
auto const page = (*sleClaimID)[sfOwnerNode];
|
||||
if (!outerSb.dirRemove(keylet::ownerDir(cidOwner), page, sleClaimID->key(), true))
|
||||
{
|
||||
@@ -759,7 +759,7 @@ getSignersListAndQuorum(ReadView const& view, SLE const& sleBridge, beast::Journ
|
||||
std::uint32_t q = std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
AccountID const thisDoor = sleBridge[sfAccount];
|
||||
auto const sleDoor = [&] { return view.read(keylet::account(thisDoor)); }();
|
||||
auto const sleDoor = [&] { return AccountRootEntryR(thisDoor, view); }();
|
||||
|
||||
if (!sleDoor)
|
||||
{
|
||||
@@ -1036,7 +1036,7 @@ applyCreateAccountAttestations(
|
||||
|
||||
// Check reserve
|
||||
auto const balance = (*sleDoor)[sfBalance];
|
||||
auto const reserve = accountReserve(psb, sleDoor, j, {.ownerCountDelta = 1});
|
||||
auto const reserve = AccountRootEntryR(sleDoor, psb, j).reserve({.ownerCountDelta = 1});
|
||||
|
||||
if (balance < reserve)
|
||||
return std::unexpected(tecINSUFFICIENT_RESERVE);
|
||||
@@ -1145,7 +1145,9 @@ applyCreateAccountAttestations(
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Reserve was already checked
|
||||
increaseOwnerCount(psb, sleDoor, {}, 1, j);
|
||||
AccountRootEntryW doorSle(doorK, psb, j);
|
||||
std::optional<AccountRootEntryW> noSponsor;
|
||||
doorSle.increaseOwnerCount(noSponsor, 1);
|
||||
psb.insert(createdSleClaimID);
|
||||
psb.update(sleDoor);
|
||||
}
|
||||
@@ -1429,7 +1431,7 @@ XChainCreateBridge::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (!isXRP(bridgeSpec.issue(chainType)))
|
||||
{
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(bridgeSpec.issue(chainType).account));
|
||||
auto const sleIssuer = AccountRootEntryR(bridgeSpec.issue(chainType).account, ctx.view);
|
||||
|
||||
if (!sleIssuer)
|
||||
return tecNO_ISSUER;
|
||||
@@ -1442,12 +1444,12 @@ XChainCreateBridge::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
{
|
||||
// Check reserve
|
||||
auto const sleAcc = ctx.view.read(keylet::account(account));
|
||||
auto const sleAcc = AccountRootEntryR(account, ctx.view, ctx.j);
|
||||
if (!sleAcc)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
auto const balance = (*sleAcc)[sfBalance];
|
||||
auto const reserve = accountReserve(ctx.view, sleAcc, ctx.j, {.ownerCountDelta = 1});
|
||||
auto const reserve = sleAcc.reserve({.ownerCountDelta = 1});
|
||||
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
@@ -1464,7 +1466,7 @@ XChainCreateBridge::doApply()
|
||||
auto const reward = ctx_.tx[sfSignatureReward];
|
||||
auto const minAccountCreate = ctx_.tx[~sfMinAccountCreateAmount];
|
||||
|
||||
auto const sleAcct = ctx_.view().peek(keylet::account(account));
|
||||
auto sleAcct = AccountRootEntryW(account, ctx_.view(), ctx_.journal);
|
||||
if (!sleAcct)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -1492,10 +1494,11 @@ XChainCreateBridge::doApply()
|
||||
(*sleBridge)[sfOwnerNode] = *page;
|
||||
}
|
||||
|
||||
increaseOwnerCount(ctx_.view(), sleAcct, {}, 1, ctx_.journal);
|
||||
std::optional<AccountRootEntryW> noSponsor;
|
||||
sleAcct.increaseOwnerCount(noSponsor, 1);
|
||||
|
||||
ctx_.view().insert(sleBridge);
|
||||
ctx_.view().update(sleAcct);
|
||||
sleAcct.update();
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -1575,7 +1578,7 @@ BridgeModify::doApply()
|
||||
auto const minAccountCreate = ctx_.tx[~sfMinAccountCreateAmount];
|
||||
bool const clearAccountCreate = ctx_.tx.isFlag(tfClearAccountCreateAmount);
|
||||
|
||||
auto const sleAcct = ctx_.view().peek(keylet::account(account));
|
||||
auto const sleAcct = AccountRootEntryW(account, ctx_.view());
|
||||
if (!sleAcct)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -1633,7 +1636,7 @@ XChainClaim::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
|
||||
if (!ctx.view.read(keylet::account(ctx.tx[sfDestination])))
|
||||
if (!AccountRootEntryR(ctx.tx[sfDestination], ctx.view))
|
||||
{
|
||||
return tecNO_DST;
|
||||
}
|
||||
@@ -1738,7 +1741,7 @@ XChainClaim::doApply()
|
||||
// `finalizeClaimHelper`. Since `finalizeClaimHelper` can create child
|
||||
// views, it's important that the sle's lifetime doesn't overlap.
|
||||
|
||||
auto const sleAcct = psb.peek(keylet::account(account));
|
||||
auto const sleAcct = AccountRootEntryW(account, psb);
|
||||
auto const sleBridge = peekBridge(psb, bridgeSpec);
|
||||
auto const sleClaimID = psb.peek(claimIDKeylet);
|
||||
|
||||
@@ -1919,7 +1922,7 @@ XChainCommit::doApply()
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
auto const bridgeSpec = ctx_.tx[sfXChainBridge];
|
||||
|
||||
auto const sleAccount = psb.read(keylet::account(account));
|
||||
auto const sleAccount = AccountRootEntryR(account, psb);
|
||||
if (!sleAccount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -1990,12 +1993,12 @@ XChainCreateClaimID::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
{
|
||||
// Check reserve
|
||||
auto const sleAcc = ctx.view.read(keylet::account(account));
|
||||
auto const sleAcc = AccountRootEntryR(account, ctx.view, ctx.j);
|
||||
if (!sleAcc)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
auto const balance = (*sleAcc)[sfBalance];
|
||||
auto const reserve = accountReserve(ctx.view, sleAcc, ctx.j, {.ownerCountDelta = 1});
|
||||
auto const reserve = sleAcc.reserve({.ownerCountDelta = 1});
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
@@ -2011,7 +2014,7 @@ XChainCreateClaimID::doApply()
|
||||
auto const reward = ctx_.tx[sfSignatureReward];
|
||||
auto const otherChainSrc = ctx_.tx[sfOtherChainSource];
|
||||
|
||||
auto const sleAcct = ctx_.view().peek(keylet::account(account));
|
||||
auto sleAcct = AccountRootEntryW(account, ctx_.view(), ctx_.journal);
|
||||
if (!sleAcct)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -2053,11 +2056,12 @@ XChainCreateClaimID::doApply()
|
||||
(*sleClaimID)[sfOwnerNode] = *page;
|
||||
}
|
||||
|
||||
increaseOwnerCount(ctx_.view(), sleAcct, {}, 1, ctx_.journal);
|
||||
std::optional<AccountRootEntryW> noSponsor;
|
||||
sleAcct.increaseOwnerCount(noSponsor, 1);
|
||||
|
||||
ctx_.view().insert(sleClaimID);
|
||||
ctx_.view().update(sleBridge);
|
||||
ctx_.view().update(sleAcct);
|
||||
sleAcct.update();
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -2195,7 +2199,7 @@ XChainCreateAccountCommit::doApply()
|
||||
STAmount const reward = ctx_.tx[sfSignatureReward];
|
||||
STXChainBridge const bridge = ctx_.tx[sfXChainBridge];
|
||||
|
||||
auto const sle = psb.peek(keylet::account(account));
|
||||
auto const sle = AccountRootEntryW(account, psb);
|
||||
if (!sle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
@@ -112,8 +113,8 @@ CheckCash::preclaim(PreclaimContext const& ctx)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
{
|
||||
auto const sleSrc = ctx.view.read(keylet::account(srcId));
|
||||
auto const sleDst = ctx.view.read(keylet::account(dstId));
|
||||
auto const sleSrc = AccountRootEntryR(srcId, ctx.view);
|
||||
auto const sleDst = AccountRootEntryR(dstId, ctx.view);
|
||||
if (!sleSrc || !sleDst)
|
||||
{
|
||||
// If the check exists this should never occur.
|
||||
@@ -201,7 +202,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
|
||||
auto const sleTrustLine =
|
||||
ctx.view.read(keylet::trustLine(dstId, issuerId, currency));
|
||||
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
|
||||
auto const sleIssuer = AccountRootEntryR(issuerId, ctx.view);
|
||||
if (!sleIssuer)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Can't receive IOUs from "
|
||||
@@ -253,7 +254,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
},
|
||||
[&](MPTIssue const& issue) -> TER {
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
|
||||
auto const sleIssuer = AccountRootEntryR(issuerId, ctx.view);
|
||||
if (!sleIssuer)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Can't receive MPTs from "
|
||||
@@ -406,23 +407,18 @@ CheckCash::doApply()
|
||||
optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)};
|
||||
|
||||
auto applyViewContext = ApplyViewContext({.view = psb, .tx = ctx_.tx});
|
||||
auto const sponsorSle = getTxReserveSponsor(applyViewContext);
|
||||
auto sponsorSle = getTxReserveSponsor(applyViewContext);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
// Check reserve. Return destination account SLE if enough reserve,
|
||||
// otherwise return nullptr.
|
||||
auto checkDstReserve = [&]() -> SLE::pointer {
|
||||
auto sleDst = psb.peek(keylet::account(accountID_));
|
||||
auto sleDst = AccountRootEntryW(accountID_, psb, j_);
|
||||
|
||||
// Can the account cover the trust line's or MPT reserve?
|
||||
if (auto const ret = checkReserve(
|
||||
applyViewContext,
|
||||
sleDst,
|
||||
preFeeBalance_,
|
||||
*sponsorSle,
|
||||
{.ownerCountDelta = 1},
|
||||
j_);
|
||||
if (auto const ret = sleDst.checkReserve(
|
||||
applyViewContext, preFeeBalance_, *sponsorSle, {.ownerCountDelta = 1});
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
JLOG(j_.trace()) << "Trust line does not exist. "
|
||||
@@ -430,7 +426,7 @@ CheckCash::doApply()
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
return sleDst;
|
||||
return sleDst.mutableRawSle();
|
||||
};
|
||||
|
||||
std::optional<Keylet> trustLineKey;
|
||||
@@ -486,8 +482,10 @@ CheckCash::doApply()
|
||||
Issue(currency, accountID_), // limit of zero
|
||||
0, // quality in
|
||||
0, // quality out
|
||||
*sponsorSle, // sponsor
|
||||
viewJ); // journal
|
||||
sponsorSle->has_value() // sponsor
|
||||
? (*sponsorSle)->mutableRawSle()
|
||||
: SLE::pointer{},
|
||||
viewJ); // journal
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter;
|
||||
@@ -533,8 +531,14 @@ CheckCash::doApply()
|
||||
if (sleDst == nullptr)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
if (auto const err =
|
||||
checkCreateMPT(psb, mptID, accountID_, *sponsorSle, 0, j_);
|
||||
if (auto const err = checkCreateMPT(
|
||||
psb,
|
||||
mptID,
|
||||
accountID_,
|
||||
sponsorSle->has_value() ? (*sponsorSle)->mutableRawSle()
|
||||
: SLE::pointer{},
|
||||
0,
|
||||
j_);
|
||||
!isTesSuccess(err))
|
||||
{
|
||||
return err;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -79,7 +80,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
AccountID const dstId{ctx.tx[sfDestination]};
|
||||
AccountID const srcId{ctx.tx[sfAccount]};
|
||||
auto const sleDst = ctx.view.read(keylet::account(dstId));
|
||||
auto const sleDst = AccountRootEntryR(dstId, ctx.view);
|
||||
if (!sleDst)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Destination account does not exist.";
|
||||
@@ -187,15 +188,15 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
|
||||
TER
|
||||
CheckCreate::doApply()
|
||||
{
|
||||
auto const sle = view().peek(keylet::account(accountID_));
|
||||
auto sle = AccountRootEntryW(accountID_, view(), ctx_.journal);
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// A check counts against the reserve of the issuing account, but we
|
||||
// check the starting balance because we want to allow dipping into the
|
||||
// reserve to pay fees.
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sle, preFeeBalance_, {.ownerCountDelta = 1}, ctx_.journal);
|
||||
if (auto const ret =
|
||||
sle.checkReserve(ctx_.getApplyViewContext(), preFeeBalance_, {.ownerCountDelta = 1});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
// Note that we use the value from the sequence or ticket as the
|
||||
@@ -220,7 +221,6 @@ CheckCreate::doApply()
|
||||
|
||||
view().insert(sleCheck);
|
||||
|
||||
auto viewJ = ctx_.registry.get().getJournal("View");
|
||||
// If it's not a self-send (and it shouldn't be), add Check to the
|
||||
// destination's owner directory.
|
||||
if (dstAccountId != accountID_)
|
||||
@@ -251,7 +251,7 @@ CheckCreate::doApply()
|
||||
}
|
||||
// If we succeeded, the new entry counts against the creator's reserve.
|
||||
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, viewJ);
|
||||
sle.increaseOwnerCount(ctx_.getApplyViewContext(), 1);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCheck);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -87,8 +88,8 @@ CredentialAccept::doApply()
|
||||
AccountID const issuer{ctx_.tx[sfIssuer]};
|
||||
|
||||
// Both exist as credential object exist itself (checked in preclaim)
|
||||
auto const sleSubject = view().peek(keylet::account(accountID_));
|
||||
auto const sleIssuer = view().peek(keylet::account(issuer));
|
||||
auto sleSubject = AccountRootEntryW(accountID_, view(), j_);
|
||||
auto sleIssuer = AccountRootEntryW(issuer, view());
|
||||
|
||||
if (!sleSubject || !sleIssuer)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -97,13 +98,8 @@ CredentialAccept::doApply()
|
||||
if (!txSponsorSle)
|
||||
return txSponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sleSubject,
|
||||
preFeeBalance_,
|
||||
*txSponsorSle,
|
||||
{.ownerCountDelta = 1},
|
||||
j_);
|
||||
if (auto const ret = sleSubject.checkReserve(
|
||||
ctx_.getApplyViewContext(), preFeeBalance_, *txSponsorSle, {.ownerCountDelta = 1});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -130,7 +126,7 @@ CredentialAccept::doApply()
|
||||
removeSponsorFromLedgerEntry(sleCred);
|
||||
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCred);
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sleSubject, 1, j_);
|
||||
sleSubject.increaseOwnerCount(ctx_.getApplyViewContext(), 1);
|
||||
view().update(sleCred);
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h> // IWYU pragma: keep
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
@@ -84,7 +85,7 @@ CredentialCreate::preclaim(PreclaimContext const& ctx)
|
||||
auto const credType(ctx.tx[sfCredentialType]);
|
||||
auto const subject = ctx.tx[sfSubject];
|
||||
|
||||
auto const subjectSle = ctx.view.read(keylet::account(subject));
|
||||
auto const subjectSle = AccountRootEntryR(subject, ctx.view);
|
||||
|
||||
if (!subjectSle)
|
||||
{
|
||||
@@ -134,12 +135,12 @@ CredentialCreate::doApply()
|
||||
sleCred->setFieldU32(sfExpiration, *optExp);
|
||||
}
|
||||
|
||||
auto const sleIssuer = view().peek(keylet::account(accountID_));
|
||||
auto sleIssuer = AccountRootEntryW(accountID_, view(), j_);
|
||||
if (!sleIssuer)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sleIssuer, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
if (auto const ret = sleIssuer.checkReserve(
|
||||
ctx_.getApplyViewContext(), preFeeBalance_, {.ownerCountDelta = 1});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -159,7 +160,7 @@ CredentialCreate::doApply()
|
||||
return tecDIR_FULL;
|
||||
sleCred->setFieldU64(sfIssuerNode, *page);
|
||||
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sleIssuer, 1, j_);
|
||||
sleIssuer.increaseOwnerCount(ctx_.getApplyViewContext(), 1);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCred);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -52,7 +53,7 @@ DelegateSet::preclaim(PreclaimContext const& ctx)
|
||||
if (!ctx.view.exists(keylet::account(ctx.tx[sfAccount])))
|
||||
return terNO_ACCOUNT; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sleAuthorize = ctx.view.read(keylet::account(ctx.tx[sfAuthorize]));
|
||||
auto const sleAuthorize = AccountRootEntryR(ctx.tx[sfAuthorize], ctx.view);
|
||||
if (!sleAuthorize)
|
||||
return tecNO_TARGET;
|
||||
|
||||
@@ -72,7 +73,7 @@ DelegateSet::preclaim(PreclaimContext const& ctx)
|
||||
TER
|
||||
DelegateSet::doApply()
|
||||
{
|
||||
auto const sleOwner = ctx_.view().peek(keylet::account(accountID_));
|
||||
auto sleOwner = AccountRootEntryW(accountID_, ctx_.view(), ctx_.journal);
|
||||
if (!sleOwner)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -98,12 +99,8 @@ DelegateSet::doApply()
|
||||
if (permissions.empty())
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sleOwner,
|
||||
preFeeBalance_,
|
||||
{.ownerCountDelta = 1},
|
||||
ctx_.journal);
|
||||
if (auto const ret = sleOwner.checkReserve(
|
||||
ctx_.getApplyViewContext(), preFeeBalance_, {.ownerCountDelta = 1});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -133,7 +130,7 @@ DelegateSet::doApply()
|
||||
(*sle)[sfDestinationNode] = *destPage;
|
||||
|
||||
ctx_.view().insert(sle);
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sleOwner, 1, ctx_.journal);
|
||||
sleOwner.increaseOwnerCount(ctx_.getApplyViewContext(), 1);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -170,7 +167,7 @@ DelegateSet::deleteDelegate(ApplyView& view, SLE::Ref sle, beast::Journal j)
|
||||
}
|
||||
|
||||
// Only the delegating account's owner count was incremented on creation
|
||||
auto const sleOwner = view.peek(keylet::account(delegator));
|
||||
auto sleOwner = AccountRootEntryW(delegator, view);
|
||||
if (!sleOwner)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AMMHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -119,7 +120,7 @@ AMMBid::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
for (auto const& account : ctx.tx.getFieldArray(sfAuthAccounts))
|
||||
{
|
||||
if (!ctx.view.read(keylet::account(account[sfAccount])))
|
||||
if (!AccountRootEntryR(account[sfAccount], ctx.view))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "AMM Bid: Invalid Account.";
|
||||
return terNO_ACCOUNT;
|
||||
@@ -208,7 +209,7 @@ applyBid(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Journa
|
||||
auto validOwner = [&](AccountID const& account) {
|
||||
// Valid range is 0-19 but the tailing slot pays MinSlotPrice
|
||||
// and doesn't refund so the check is < instead of <= to optimize.
|
||||
return timeSlot && *timeSlot < kTailingSlot && sb.read(keylet::account(account));
|
||||
return timeSlot && *timeSlot < kTailingSlot && AccountRootEntryR(account, sb);
|
||||
};
|
||||
|
||||
auto updateSlot = [&](std::uint32_t fee, Number const& minPrice, Number const& burn) -> TER {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AMMHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -102,11 +103,11 @@ AMMClawback::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const asset = ctx.tx[sfAsset];
|
||||
auto const asset2 = ctx.tx[sfAsset2];
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(ctx.tx[sfAccount]));
|
||||
auto const sleIssuer = AccountRootEntryR(ctx.tx[sfAccount], ctx.view);
|
||||
if (!sleIssuer)
|
||||
return terNO_ACCOUNT; // LCOV_EXCL_LINE
|
||||
|
||||
if (!ctx.view.read(keylet::account(ctx.tx[sfHolder])))
|
||||
if (!AccountRootEntryR(ctx.tx[sfHolder], ctx.view))
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
auto const ammSle = ctx.view.read(keylet::amm(asset, asset2));
|
||||
@@ -178,7 +179,7 @@ AMMClawback::applyGuts(Sandbox& sb)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ammAccount = (*ammSle)[sfAccount];
|
||||
auto const accountSle = sb.read(keylet::account(ammAccount));
|
||||
auto const accountSle = AccountRootEntryR(ammAccount, sb);
|
||||
if (!accountSle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AMMHelpers.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -133,7 +134,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (asset.holds<MPTIssue>() || isXRP(asset))
|
||||
return false;
|
||||
|
||||
if (auto const issuerAccount = view.read(keylet::account(asset.getIssuer())))
|
||||
if (auto const issuerAccount = AccountRootEntryR(asset.getIssuer(), view))
|
||||
return !issuerAccount->isFlag(lsfDefaultRipple);
|
||||
|
||||
return false;
|
||||
@@ -173,7 +174,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
|
||||
auto isLPToken = [&](STAmount const& amount) -> bool {
|
||||
if (auto const sle = ctx.view.read(keylet::account(amount.asset().getIssuer())))
|
||||
if (auto const sle = AccountRootEntryR(amount.asset().getIssuer(), ctx.view))
|
||||
return sle->isFieldPresent(sfAMMID);
|
||||
return false;
|
||||
};
|
||||
@@ -237,7 +238,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
|
||||
[&](Issue const& issue) -> TER {
|
||||
if (isXRP(issue))
|
||||
return tesSUCCESS;
|
||||
auto const sle = ctx.view.read(keylet::account(issue.account));
|
||||
auto const sle = AccountRootEntryR(issue.account, ctx.view);
|
||||
if (!sle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
if (sle->isFlag(lsfAllowTrustLineClawback))
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AMMHelpers.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -344,7 +344,7 @@ AMMWithdraw::applyGuts(Sandbox& sb)
|
||||
if (!ammSle)
|
||||
return {tecINTERNAL, false}; // LCOV_EXCL_LINE
|
||||
auto const ammAccountID = (*ammSle)[sfAccount];
|
||||
auto const accountSle = sb.read(keylet::account(ammAccountID));
|
||||
auto const accountSle = AccountRootEntryR(ammAccountID, sb);
|
||||
if (!accountSle)
|
||||
return {tecINTERNAL, false}; // LCOV_EXCL_LINE
|
||||
auto const lpTokens = ammLPHolds(ctx_.view(), *ammSle, ctx_.tx[sfAccount], ctx_.journal);
|
||||
@@ -691,16 +691,15 @@ AMMWithdraw::withdraw(
|
||||
reserveHandling == ReserveHandling::IgnoreReserve)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto sleAccount = view.peek(keylet::account(account));
|
||||
auto sleAccount = AccountRootEntryW(account, view, journal);
|
||||
if (!sleAccount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const balance = (*sleAccount)[sfBalance]->xrp();
|
||||
// See also TrustSet::doApply() and MPTokenAuthorize::authorize()
|
||||
XRPAmount const reserve(
|
||||
(ownerCount(sleAccount, journal) < 2)
|
||||
? XRPAmount(beast::kZero)
|
||||
: accountReserve(view, sleAccount, journal, {.ownerCountDelta = 1}));
|
||||
(sleAccount.ownerCount() < 2) ? XRPAmount(beast::kZero)
|
||||
: sleAccount.reserve({.ownerCountDelta = 1}));
|
||||
|
||||
auto const balanceAdj = std::max(priorBalance, balance);
|
||||
if (balanceAdj < reserve)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <xrpl/tx/transactors/dex/OfferCancel.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/OfferHelpers.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -33,7 +35,7 @@ OfferCancel::preclaim(PreclaimContext const& ctx)
|
||||
auto const id = ctx.tx[sfAccount];
|
||||
auto const offerSequence = ctx.tx[sfOfferSequence];
|
||||
|
||||
auto const sle = ctx.view.read(keylet::account(id));
|
||||
auto const sle = AccountRootEntryR(id, ctx.view);
|
||||
if (!sle)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
@@ -54,7 +56,7 @@ OfferCancel::doApply()
|
||||
{
|
||||
auto const offerSequence = ctx_.tx[sfOfferSequence];
|
||||
|
||||
auto const sle = view().read(keylet::account(accountID_));
|
||||
auto const sle = AccountRootEntryW(accountID_, view());
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -181,7 +181,7 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
auto const cancelSequence = ctx.tx[~sfOfferSequence];
|
||||
|
||||
auto const sleCreator = ctx.view.read(keylet::account(id));
|
||||
auto const sleCreator = AccountRootEntryR(id, ctx.view);
|
||||
if (!sleCreator)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
@@ -288,7 +288,7 @@ OfferCreate::checkAcceptAsset(
|
||||
// Only valid for custom currencies
|
||||
XRPL_ASSERT(!isXRP(asset), "xrpl::OfferCreate::checkAcceptAsset : input is not XRP");
|
||||
|
||||
auto const issuerAccount = view.read(keylet::account(asset.getIssuer()));
|
||||
auto const issuerAccount = AccountRootEntryR(asset.getIssuer(), view);
|
||||
|
||||
if (!issuerAccount)
|
||||
{
|
||||
@@ -707,14 +707,14 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
// Not XRP or MPT
|
||||
if (!saTakerPays.integral())
|
||||
{
|
||||
auto const sle = sb.read(keylet::account(uPaysIssuerID));
|
||||
auto const sle = AccountRootEntryR(uPaysIssuerID, sb);
|
||||
if (sle && sle->isFieldPresent(sfTickSize))
|
||||
uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
|
||||
}
|
||||
// Not XRP or MPT
|
||||
if (!saTakerGets.integral())
|
||||
{
|
||||
auto const sle = sb.read(keylet::account(uGetsIssuerID));
|
||||
auto const sle = AccountRootEntryR(uGetsIssuerID, sb);
|
||||
if (sle && sle->isFieldPresent(sfTickSize))
|
||||
uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
|
||||
}
|
||||
@@ -884,12 +884,12 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
return {tesSUCCESS, true};
|
||||
}
|
||||
|
||||
auto const sleCreator = sb.peek(keylet::account(accountID_));
|
||||
auto sleCreator = AccountRootEntryW(accountID_, sb, viewJ);
|
||||
if (!sleCreator)
|
||||
return {tefINTERNAL, false};
|
||||
|
||||
{
|
||||
XRPAmount const reserve = accountReserve(sb, sleCreator, viewJ, {.ownerCountDelta = 1});
|
||||
XRPAmount const reserve = sleCreator.reserve({.ownerCountDelta = 1});
|
||||
if (preFeeBalance_ < reserve)
|
||||
{
|
||||
// If we are here, the signing account had an insufficient reserve
|
||||
@@ -923,7 +923,8 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
}
|
||||
|
||||
// Update owner count.
|
||||
increaseOwnerCount(sb, sleCreator, {}, 1, viewJ);
|
||||
std::optional<AccountRootEntryW> noSponsor;
|
||||
sleCreator.increaseOwnerCount(noSponsor, 1);
|
||||
|
||||
JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : "
|
||||
<< to_string(saTakerGets.asset())
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -46,7 +47,7 @@ DIDDelete::deleteSLE(ApplyView& view, SLE::pointer sle, AccountID const owner, b
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleOwner = view.peek(keylet::account(owner));
|
||||
auto sleOwner = AccountRootEntryW(owner, view);
|
||||
if (!sleOwner)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -65,15 +66,14 @@ DIDSet::preflight(PreflightContext const& ctx)
|
||||
static TER
|
||||
addSLE(ApplyContext& ctx, SLE::Ref sle, AccountID const& owner)
|
||||
{
|
||||
auto const sleAccount = ctx.view().peek(keylet::account(owner));
|
||||
auto sleAccount = AccountRootEntryW(owner, ctx.view(), ctx.journal);
|
||||
if (!sleAccount)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Check reserve availability for new object creation
|
||||
{
|
||||
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
|
||||
auto const reserve =
|
||||
accountReserve(ctx.view(), sleAccount, ctx.journal, {.ownerCountDelta = 1});
|
||||
auto const reserve = sleAccount.reserve({.ownerCountDelta = 1});
|
||||
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
@@ -90,8 +90,9 @@ addSLE(ApplyContext& ctx, SLE::Ref sle, AccountID const& owner)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
(*sle)[sfOwnerNode] = *page;
|
||||
}
|
||||
increaseOwnerCount(ctx.view(), sleAccount, {}, 1, ctx.journal);
|
||||
ctx.view().update(sleAccount);
|
||||
std::optional<AccountRootEntryW> noSponsor;
|
||||
sleAccount.increaseOwnerCount(noSponsor, 1);
|
||||
sleAccount.update();
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -166,7 +167,7 @@ EscrowCancel::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
auto const sle = ctx_.view().peek(keylet::account(account));
|
||||
auto sle = AccountRootEntryW(account, ctx_.view());
|
||||
STAmount const amount = slep->getFieldAmount(sfAmount);
|
||||
|
||||
// The return can re-create a holding the owner deleted while the escrow
|
||||
@@ -192,7 +193,7 @@ EscrowCancel::doApply()
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
kParityRate,
|
||||
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : slep,
|
||||
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle.mutableRawSle() : slep,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -207,7 +208,7 @@ escrowCreatePreclaimHelper<Issue>(
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
|
||||
auto const sleIssuer = AccountRootEntryR(issuer, ctx.view);
|
||||
if (!sleIssuer)
|
||||
return tecNO_ISSUER;
|
||||
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
|
||||
@@ -347,7 +348,7 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
|
||||
AccountID const account{ctx.tx[sfAccount]};
|
||||
AccountID const dest{ctx.tx[sfDestination]};
|
||||
|
||||
auto const sled = ctx.view.read(keylet::account(dest));
|
||||
auto const sled = AccountRootEntryR(dest, ctx.view);
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
@@ -433,7 +434,7 @@ EscrowCreate::doApply()
|
||||
if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter]))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const sle = ctx_.view().peek(keylet::account(accountID_));
|
||||
auto sle = AccountRootEntryW(accountID_, ctx_.view(), j_);
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -447,7 +448,7 @@ EscrowCreate::doApply()
|
||||
// unsponsored this hits the source branch and validates the
|
||||
// source's pre-lock balance against base + (currentOC+1)*increment.
|
||||
if (auto const ret =
|
||||
checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
|
||||
sle.checkReserve(ctx_.getApplyViewContext(), balance, {.ownerCountDelta = 1});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -464,15 +465,15 @@ EscrowCreate::doApply()
|
||||
// - sponsored: 0 — sponsor covers the new owner increment, so the
|
||||
// source only owes reserve for its current owners.
|
||||
// - unsponsored: 1 — source owes reserve including the new increment.
|
||||
auto const sourceReserve = accountReserve(
|
||||
ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1});
|
||||
auto const sourceReserve =
|
||||
sle.reserve({.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1});
|
||||
if (balance - STAmount(amount).xrp() < sourceReserve)
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
|
||||
// Check destination account
|
||||
{
|
||||
auto const sled = ctx_.view().read(keylet::account(ctx_.tx[sfDestination]));
|
||||
auto const sled = AccountRootEntryW(ctx_.tx[sfDestination], ctx_.view());
|
||||
if (!sled)
|
||||
return tecNO_DST; // LCOV_EXCL_LINE
|
||||
if (sled->isFlag(lsfRequireDestTag) && !ctx_.tx[~sfDestinationTag])
|
||||
@@ -558,9 +559,9 @@ EscrowCreate::doApply()
|
||||
}
|
||||
|
||||
// increment owner count
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
|
||||
sle.increaseOwnerCount(ctx_.getApplyViewContext(), 1);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep);
|
||||
ctx_.view().update(sle);
|
||||
sle.update();
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
@@ -314,12 +315,12 @@ EscrowFinish::doApply()
|
||||
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
AccountID const destID = (*slep)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
auto sled = AccountRootEntryW(destID, ctx_.view());
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
if (auto err = verifyDepositPreauth(
|
||||
ctx_.tx, ctx_.view(), accountID_, destID, sled.rawSle(), ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
@@ -378,7 +379,7 @@ EscrowFinish::doApply()
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
lockedRate,
|
||||
sled,
|
||||
sled.mutableRawSle(),
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
@@ -404,7 +405,7 @@ EscrowFinish::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
ctx_.view().update(sled);
|
||||
sled.update();
|
||||
|
||||
if (!recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -108,7 +109,7 @@ determineBrokerID(ReadView const& view, STTx const& tx)
|
||||
// Thus, Amount.issuer _should_ be the loan broker's
|
||||
// pseudo-account, but we don't know yet whether it is.
|
||||
auto const maybePseudo = dstAmount->getIssuer();
|
||||
auto const sle = view.read(keylet::account(maybePseudo));
|
||||
auto const sle = AccountRootEntryR(maybePseudo, view);
|
||||
|
||||
// If the account was not found, the transaction can't go further.
|
||||
if (!sle)
|
||||
@@ -319,7 +320,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
|
||||
return tecINTERNAL; // tecINSUFFICIENT_FUNDS; LCOV_EXCL_LINE
|
||||
|
||||
// Check if the vault asset issuer has the correct flags
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(vaultAsset.getIssuer()));
|
||||
auto const sleIssuer = AccountRootEntryR(vaultAsset.getIssuer(), ctx.view);
|
||||
if (!sleIssuer)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -161,7 +162,7 @@ LoanBrokerDelete::doApply()
|
||||
if (auto ter = removeEmptyHolding(ctx_.getApplyViewContext(), brokerPseudoID, vaultAsset, j_))
|
||||
return ter;
|
||||
|
||||
auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID));
|
||||
auto brokerPseudoSLE = AccountRootEntryW(brokerPseudoID, view());
|
||||
if (!brokerPseudoSLE)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -183,10 +184,10 @@ LoanBrokerDelete::doApply()
|
||||
return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
view().erase(brokerPseudoSLE);
|
||||
brokerPseudoSLE.erase();
|
||||
|
||||
{
|
||||
auto owner = view().peek(keylet::account(accountID_));
|
||||
auto owner = AccountRootEntryW(accountID_, view());
|
||||
if (!owner)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -23,6 +24,7 @@
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -236,7 +238,7 @@ LoanBrokerSet::doApply()
|
||||
auto const vaultAsset = sleVault->at(sfAsset);
|
||||
auto const sequence = tx.getSeqProxy();
|
||||
|
||||
auto owner = view.peek(keylet::account(accountID_));
|
||||
auto owner = AccountRootEntryW(accountID_, view, j_);
|
||||
if (!owner)
|
||||
{
|
||||
// This should be impossible
|
||||
@@ -254,8 +256,9 @@ LoanBrokerSet::doApply()
|
||||
|
||||
// Increases the owner count by two: one for the LoanBroker object, and
|
||||
// one for the pseudo-account.
|
||||
increaseOwnerCount(view, owner, {}, 2, j_);
|
||||
if (preFeeBalance_ < accountReserve(view, owner, j_))
|
||||
std::optional<AccountRootEntryW> noSponsor;
|
||||
owner.increaseOwnerCount(noSponsor, 2);
|
||||
if (preFeeBalance_ < owner.reserve())
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
auto maybePseudo = createPseudoAccount(view, broker->key(), sfLoanBrokerID);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/basics/Number.h> // IWYU pragma: keep
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -80,7 +81,7 @@ LoanDelete::doApply()
|
||||
if (!loanSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
auto const borrower = loanSle->at(sfBorrower);
|
||||
auto const borrowerSle = view.peek(keylet::account(borrower));
|
||||
auto borrowerSle = AccountRootEntryW(borrower, view);
|
||||
if (!borrowerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -56,7 +57,7 @@ conservationBalance(ReadView const& view, AccountID const& id, Asset const& asse
|
||||
{
|
||||
if (isXRP(asset))
|
||||
{
|
||||
auto const sle = view.read(keylet::account(id));
|
||||
auto const sle = AccountRootEntryR(id, view);
|
||||
if (!sle)
|
||||
return STAmount{asset}; // LCOV_EXCL_LINE
|
||||
return view.balanceHookIOU(id, xrpAccount(), sle->getFieldAmount(sfBalance));
|
||||
@@ -391,7 +392,7 @@ LoanPay::doApply()
|
||||
}();
|
||||
|
||||
auto const brokerPayee = sendBrokerFeeToOwner ? brokerOwner : brokerPseudoAccount;
|
||||
auto const brokerPayeeSle = view.peek(keylet::account(brokerPayee));
|
||||
auto const brokerPayeeSle = AccountRootEntryW(brokerPayee, view);
|
||||
if (!sendBrokerFeeToOwner)
|
||||
{
|
||||
// If we can't send the fee to the owner, and the pseudo-account is
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -305,7 +306,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
auto const brokerPseudo = brokerSle->at(sfAccount);
|
||||
|
||||
auto const borrower = counterparty == brokerOwner ? account : counterparty;
|
||||
if (auto const borrowerSle = ctx.view.read(keylet::account(borrower)); !borrowerSle)
|
||||
if (auto const borrowerSle = AccountRootEntryR(borrower, ctx.view); !borrowerSle)
|
||||
{
|
||||
// It may not be possible to hit this case, because it'll fail the
|
||||
// signature check with terNO_ACCOUNT.
|
||||
@@ -443,7 +444,7 @@ LoanSet::doApply()
|
||||
if (!brokerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
auto const brokerOwner = brokerSle->at(sfOwner);
|
||||
auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
|
||||
auto const brokerOwnerSle = AccountRootEntryW(brokerOwner, view);
|
||||
if (!brokerOwnerSle)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -455,14 +456,14 @@ LoanSet::doApply()
|
||||
|
||||
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
|
||||
auto const borrower = counterparty == brokerOwner ? accountID_ : counterparty;
|
||||
auto const borrowerSle = view.peek(keylet::account(borrower));
|
||||
auto borrowerSle = AccountRootEntryW(borrower, view, j_);
|
||||
if (!borrowerSle)
|
||||
{
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
auto const brokerPseudo = brokerSle->at(sfAccount);
|
||||
auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo));
|
||||
auto const brokerPseudoSle = AccountRootEntryW(brokerPseudo, view);
|
||||
if (!brokerPseudoSle)
|
||||
{
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
@@ -581,12 +582,13 @@ LoanSet::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
increaseOwnerCount(view, borrowerSle, {}, 1, j_);
|
||||
std::optional<AccountRootEntryW> noSponsor;
|
||||
borrowerSle.increaseOwnerCount(noSponsor, 1);
|
||||
|
||||
{
|
||||
auto const balance =
|
||||
accountID_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp();
|
||||
if (balance < accountReserve(view, borrowerSle, j_))
|
||||
if (balance < borrowerSle.reserve())
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/entries/AccountRootEntry.h>
|
||||
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
@@ -376,7 +376,7 @@ NFTokenAcceptOffer::transferNFToken(
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
auto const sleBuyer = view().read(keylet::account(buyer));
|
||||
auto const sleBuyer = AccountRootEntryW(buyer, view(), j_);
|
||||
if (!sleBuyer)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -397,7 +397,7 @@ NFTokenAcceptOffer::transferNFToken(
|
||||
auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount);
|
||||
if (buyerOwnerCountAfter > buyerOwnerCountBefore)
|
||||
{
|
||||
if (buyerBalance < accountReserve(view(), sleBuyer, j_))
|
||||
if (buyerBalance < sleBuyer.reserve())
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user