diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index d147755260..1f15c6b754 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -47,6 +47,7 @@ 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 @@ -251,8 +252,7 @@ xrpl.server > xrpl.shamap xrpl.shamap > xrpl.basics xrpl.shamap > xrpl.nodestore xrpl.shamap > xrpl.protocol -xrpl.telemetry > xrpl.basics -xrpl.telemetry > xrpld.consensus +xrpl.telemetry > xrpl.config xrpl.tx > xrpl.basics xrpl.tx > xrpl.core xrpl.tx > xrpl.ledger @@ -331,4 +331,5 @@ xrpld.shamap > xrpld.core xrpld.shamap > xrpl.protocol xrpld.shamap > xrpl.shamap xrpld.telemetry > xrpl.basics +xrpld.telemetry > xrpld.consensus xrpld.telemetry > xrpl.telemetry diff --git a/.github/scripts/otel-naming/README.md b/.github/scripts/otel-naming/README.md new file mode 100644 index 0000000000..421a108931 --- /dev/null +++ b/.github/scripts/otel-naming/README.md @@ -0,0 +1,65 @@ +# 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`). +- **Legitimate dotted keys** = the resource attrs declared in the base + `SpanNames.h` (`xrpl.network.*`) plus the `semconv::service::*` keys the code + passes to `Resource::Create()` in `Telemetry.cpp` (`service.*`). A dotted key + declared in any other header is a violation. + +### 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`/`childSpan`. 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 PromQL label (non-builtin) exists in the L1 key set. | +| E | Every runbook attribute reference exists in the L1 key set. | + +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: `), 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, F) run. diff --git a/.github/scripts/otel-naming/check_otel_naming.py b/.github/scripts/otel-naming/check_otel_naming.py new file mode 100644 index 0000000000..13b79237dc --- /dev/null +++ b/.github/scripts/otel-naming/check_otel_naming.py @@ -0,0 +1,683 @@ +#!/usr/bin/env python3 + +""" +Usage: check_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/...). + +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/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) + +Rules (each FAILS the build, when its inputs are present) +--------------------------------------------------------- + A No stray dotted span-attribute key. A dotted `.` 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/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 PromQL label (non-builtin) exists in the L1 key set. + E Every attribute named in the runbook tables exists in the L1 key set. + +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 json +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.""" + out = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ) + return Path(out.stdout.strip()) + + +# --------------------------------------------------------------------------- +# Regexes (compiled once) +# --------------------------------------------------------------------------- + +# A segment/string constant definition: `inline constexpr auto NAME = ;` +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 {` opener, to track which namespace a constant lives in. +NS_OPEN = re.compile(r"namespace\s+([\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::childSpan(` (static factory) +# - `.span(` / `->setAttribute(` etc. (member call) +# `span`/`childSpan` additionally require the `SpanGuard`/`.`/`->` receiver; +# `setAttribute`/`addEvent` only ever exist on a guard, so a `.`/`->` suffices. +CALLSITE = re.compile( + r"(?:SpanGuard::|\.|->)\s*(setAttribute|addEvent|span|childSpan)\s*\(" +) +# A C++ string literal (used to flag literals inside call-site argument lists). +STRING_LITERAL = re.compile(r'"((?:[^"\\]|\\.)*)"') +# Dotted token that looks like `a.b` or `a.b.c` (lowercase + underscore words). +DOTTED_TOKEN = re.compile(r"\b([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+)\b") + + +def strip_block_comments(text: str) -> str: + """Remove /* ... */ comments but KEEP /// and // line comments. + + Doxygen @code examples live in /** ... */ blocks, and Rule F must still see + the call-sites inside them, so we do NOT strip /** */ wholesale. Instead we + only strip C-style comments that are clearly not doc blocks is unsafe, so we + keep all comments and rely on call-site detection. This function currently + returns text unchanged and exists as a single, documented control point. + """ + 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 = ` 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. + for _ in range(2): + for h in ordered: + resolve_constants(h.read_text(), symbols) + return symbols + + +def split_top_level_args(s: str) -> List[str]: + """Split a comma-separated arg list, respecting nested parentheses.""" + args, depth, cur = [], 0, "" + for ch in s: + if 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_keys_from_header(path: Path, symbols: Dict[str, str]) -> Set[str]: + """Return the set of attribute-key strings declared in a header's + `namespace attr { ... }` block(s). `symbols` is the global cross-file + table so `join(seg::rpc, ...)` style dotted attrs resolve correctly.""" + text = path.read_text() + keys: Set[str] = set() + # Walk the file tracking namespace nesting to find `namespace attr` blocks. + depth = 0 + attr_depth: Optional[int] = None + for line in text.splitlines(): + opener = NS_OPEN.search(line) + if opener: + if opener.group(1).split("::")[-1] == "attr": + attr_depth = depth + depth += 1 + continue + # Count brace nesting crudely for namespace close detection. + depth += line.count("{") - line.count("}") + if attr_depth is not None and depth <= attr_depth: + attr_depth = None + continue + if attr_depth is not None: + md = CONST_DEF.search(line) + if md: + # Resolve the named constant against the global symbol table; + # this captures both makeStr("x") and join(seg::y, ...) forms. + val = symbols.get(md.group(1)) + if val is not None: + keys.add(val) + else: + for ms in MAKESTR.finditer(line): + keys.add(ms.group(1)) + 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 base SpanNames.h resource attrs + the semconv keys Telemetry.cpp + # passes to Resource::Create(). A dotted key from any other header is a + # violation, not an allowlist entry. + dotted_allow = derive_dotted_resource_keys(root, keys_by_header, report) + + # --- 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) ------------------- + run_rule_b_collector(root, l1_keys, report) + run_rule_c_tempo(root, l1_keys, report) + run_rule_d_dashboards(root, l1_keys, report) + run_rule_e_runbook(root, l1_keys, report) + + report.render_and_exit() + + +def derive_dotted_resource_keys( + root: Path, keys_by_header: Dict[Path, Set[str]], report: Report +) -> Set[str]: + """Legitimate dotted keys = dotted attrs declared ONLY in the base + SpanNames.h (xrpl.network.*) plus the standard semconv keys the code + passes to Resource::Create() in Telemetry.cpp (service.*). Dotted keys + declared in any OTHER header are NOT allowlisted — they are Rule-A + violations.""" + allow: Set[str] = set() + for h, keys in keys_by_header.items(): + if h.name == "SpanNames.h": # the base header holds resource attrs + allow |= {k for k in keys if "." in k} + tele = root / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp" + if tele.is_file(): + text = tele.read_text() + # semconv::service::kServiceName -> service.name, etc. + for m in re.finditer(r"semconv::(\w+)::k(\w+)", text): + group = m.group(1) # e.g. "service" + field = camel_to_snake(m.group(2)).replace("service_", "", 1) + allow.add(f"{group}.{field.replace('_', '.')}") + report.ok(f"resource dotted-key allowlist derived: {sorted(allow)}") + else: + report.skip("resource-derive", "Telemetry.cpp not present") + return allow + + +def camel_to_snake(s: str) -> str: + return re.sub(r"(? 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) +# childSpan(name[, parentCtx]) -> check arg 0 (span-name leaf) +CONSTANT_ARG_POSITIONS: Dict[str, Set[int]] = { + "setAttribute": {0}, + "addEvent": {0}, + "span": {1, 2}, + "childSpan": {0}, +} + + +def is_test_path(path: 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 (e.g. src/test/, + src/tests/, .../detail/tests/).""" + return any(part in ("test", "tests") for part in path.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(h.read_text(errors="ignore")): + 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/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): + continue + text = path.read_text(errors="ignore") + 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/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. + i = m.end() # one char past the '(' + depth = 1 + while i < len(text) and depth > 0: + c = text[i] + if 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 = path.read_text() + 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]: + dims: List[str] = [] + in_dims = False + for line in text.splitlines(): + if re.search(r"\bdimensions\s*:", line): + in_dims = True + continue + if in_dims: + m = re.search(r"-\s*name\s*:\s*([A-Za-z0-9_.]+)", line) + if m: + dims.append(m.group(1)) + elif line.strip() and not line.lstrip().startswith("-") and ":" in line: + in_dims = False + return dims + + +def run_rule_c_tempo(root: Path, l1_keys: Set[str], report: Report) -> None: + path = root / "docker" / "telemetry" / "tempo.yaml" + if not path.is_file(): + report.skip("C", "tempo.yaml not present") + return + text = path.read_text() + tags = re.findall(r"(?:tag|attribute)\s*:\s*([A-Za-z0-9_.]+)", text) + span_tags = [t for t in tags if not t.startswith(("service.", "span."))] + if not span_tags: + report.skip("C", "no span-attribute tags in tempo.yaml") + return + if not l1_keys: + report.skip("C", "no L1 key set to validate against") + return + miss = [t for t in span_tags if t not in l1_keys] + for t in 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 tag(s) all in L1") + + +def run_rule_d_dashboards(root: Path, l1_keys: 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 = { + "le", + "exported_instance", + "span_name", + "status_code", + "service_name", + "service_version", + "service_instance_id", + "job", + "instance", + } + found = False + for f in files: + try: + text = f.read_text() + 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): + if lbl in builtins or lbl in l1_keys: + continue + found = True + report.violation( + "D", str(f.relative_to(root)), lbl, "must exist in L1 or builtin" + ) + if not found: + report.ok(f"D: dashboard PromQL labels all in L1 ({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 = path.read_text() + found = False + # Attribute names appear as `code` spans in the runbook's reference tables. + for m in re.finditer(r"`([a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*)`", text): + token = m.group(1) + # Only consider tokens that look like attribute keys (underscore form or + # a dotted form), and skip obvious span names / prose words. + if "_" in token or "." in token: + if token in l1_keys: + continue + if "." in token: # dotted: must be a resource key in L1 + 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() diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index aaf84a51d0..6353567f27 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -27,6 +27,19 @@ def get_cmake_args(build_type: str, extra_args: str) -> str: return " ".join(args) +def runs_on_event(exclude_event_types: list[str], event: str | None) -> bool: + """Whether a config should run for the current event. + + 'exclude_event_types' is a list of GitHub event names (e.g. + ["pull_request"]) on which the config should NOT run; an empty list means + the config runs on every event. When no event is given (event is None), no + filtering is applied. + """ + if event is None: + return True + return event not in exclude_event_types + + # --------------------------------------------------------------------------- # Input types — shapes of the JSON config files # --------------------------------------------------------------------------- @@ -43,6 +56,9 @@ class LinuxConfig: suffix: str = "" extra_cmake_args: str = "" image: str = "" # only used by package_configs entries + # List of GitHub event names (e.g. "pull_request") on which this config + # should NOT run. Empty means it runs on every event. + exclude_event_types: list[str] = dataclasses.field(default_factory=list) @dataclasses.dataclass @@ -77,6 +93,9 @@ class PlatformConfig: build_type: list[str] build_only: bool = False # if true, skip tests (e.g. macos/Windows Debug) extra_cmake_args: str = "" + # List of GitHub event names (e.g. "pull_request") on which this config + # should NOT run. Empty means it runs on every event. + exclude_event_types: list[str] = dataclasses.field(default_factory=list) def __post_init__(self) -> None: if isinstance(self.build_type, str): @@ -151,16 +170,21 @@ _ARCHS: dict[str, Architecture] = { } -def expand_linux_matrix(linux: LinuxFile) -> list[MatrixEntry]: +def expand_linux_matrix( + linux: LinuxFile, event: str | None = None +) -> list[MatrixEntry]: """Expand a LinuxFile into a flat list of matrix entries. Each config entry is expanded over the cross-product of its - compiler, build_type, sanitizers, and architecture lists. + compiler, build_type, sanitizers, and architecture lists. Configs that + exclude the current event are skipped. """ entries: list[MatrixEntry] = [] for distro, configs in linux.configs.items(): for cfg in configs: + if not runs_on_event(cfg.exclude_event_types, event): + continue # An empty sanitizers list means "one entry with no sanitizer". effective_sanitizers = cfg.sanitizers or [""] effective_archs = {arch: _ARCHS[arch] for arch in cfg.arch} @@ -218,13 +242,20 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]: return entries -def expand_platform_matrix(pf: PlatformFile) -> list[MatrixEntry]: - """Expand a PlatformFile (macOS or Windows) into matrix entries.""" +def expand_platform_matrix( + pf: PlatformFile, event: str | None = None +) -> list[MatrixEntry]: + """Expand a PlatformFile (macOS or Windows) into matrix entries. + + Configs that exclude the current event are skipped. + """ platform_name, arch = pf.platform.split("/") is_windows = platform_name == "windows" entries: list[MatrixEntry] = [] for cfg in pf.configs: + if not runs_on_event(cfg.exclude_event_types, event): + continue for build_type in cfg.build_type: entries.append( MatrixEntry( @@ -262,6 +293,14 @@ if __name__ == "__main__": help="Emit the Linux packaging matrix instead of the build/test matrix.", action="store_true", ) + parser.add_argument( + "-e", + "--event", + help="The GitHub event name that triggered the workflow (e.g. 'push', " + "'pull_request'). Configs are filtered by their 'event_type'. If " + "omitted, no filtering is applied.", + default=None, + ) args = parser.parse_args() matrix: list[MatrixEntry] | list[PackagingEntry] = [] @@ -270,12 +309,16 @@ if __name__ == "__main__": matrix = expand_linux_packaging(LinuxFile.load(THIS_DIR / "linux.json")) else: if args.config in ("linux", None): - matrix += expand_linux_matrix(LinuxFile.load(THIS_DIR / "linux.json")) + matrix += expand_linux_matrix( + LinuxFile.load(THIS_DIR / "linux.json"), args.event + ) if args.config in ("macos", None): - matrix += expand_platform_matrix(PlatformFile.load(THIS_DIR / "macos.json")) + matrix += expand_platform_matrix( + PlatformFile.load(THIS_DIR / "macos.json"), args.event + ) if args.config in ("windows", None): matrix += expand_platform_matrix( - PlatformFile.load(THIS_DIR / "windows.json") + PlatformFile.load(THIS_DIR / "windows.json"), args.event ) print(f"matrix={json.dumps({'include': [dataclasses.asdict(e) for e in matrix]})}") diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index edacdbde4c..e6c807ac95 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -41,7 +41,8 @@ "build_type": ["Debug"], "arch": ["amd64"], "suffix": "unity", - "extra_cmake_args": "-Dunity=ON" + "extra_cmake_args": "-Dunity=ON", + "exclude_event_types": ["pull_request"] } ], diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json index 5b9e32f88e..66d7a55a43 100644 --- a/.github/scripts/strategy-matrix/macos.json +++ b/.github/scripts/strategy-matrix/macos.json @@ -9,7 +9,8 @@ { "build_type": "Debug", "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", - "build_only": true + "build_only": true, + "exclude_event_types": ["pull_request"] } ] } diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index e4678b60db..e25f9ad131 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -3,6 +3,10 @@ "runner": ["self-hosted", "Windows", "devbox"], "configs": [ { "build_type": "Release" }, - { "build_type": "Debug", "build_only": true } + { + "build_type": "Debug", + "build_only": true, + "exclude_event_types": ["pull_request"] + } ] } diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 4b2edeb93d..59e78354dd 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -51,8 +51,10 @@ 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 @@ -108,6 +110,11 @@ 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' }} @@ -176,6 +183,7 @@ jobs: if: failure() || cancelled() needs: - check-levelization + - check-otel-naming - check-rename - clang-tidy - build-test diff --git a/.github/workflows/reusable-check-otel-naming.yml b/.github/workflows/reusable-check-otel-naming.yml new file mode 100644 index 0000000000..a7af2da8cd --- /dev/null +++ b/.github/workflows/reusable-check-otel-naming.yml @@ -0,0 +1,28 @@ +# 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 diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index ea134b43b2..4518a8ffef 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -35,4 +35,5 @@ jobs: id: generate env: GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}', inputs.os) || '' }} - run: ./generate.py ${GENERATE_CONFIG} >>"${GITHUB_OUTPUT}" + GENERATE_EVENT: ${{ github.event_name }} + run: ./generate.py ${GENERATE_CONFIG} --event="${GENERATE_EVENT}" >>"${GITHUB_OUTPUT}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25dd7ac059..d09b5f504e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -300,6 +300,55 @@ If you wish to automatically fix whatever clang-tidy finds _and_ is capable of f run-clang-tidy -p build -quiet -fix -allow-no-checks src tests ``` +## 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 — the span name already carries + the domain (e.g. `command`, `local`, `version` on `rpc.command` / `tx.process`). +2. Collision qualifier: `_` when a bare name would collide across + domains (in the shared spanmetrics label space) or with the OTel-reserved + `status` key (e.g. `rpc_status`, `grpc_status`, `proposal_trusted`, + `validation_trusted`). +3. Shared cross-span attribute: `_` underscore form + (e.g. `tx_hash`, `peer_id`, `ledger_seq`, `consensus_round`). +4. Resource attribute: dotted `xrpl..` — 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 `[.]` (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, diff --git a/OpenTelemetryPlan/00-tracing-fundamentals.md b/OpenTelemetryPlan/00-tracing-fundamentals.md index d7cc40c5ac..9c6f96d7af 100644 --- a/OpenTelemetryPlan/00-tracing-fundamentals.md +++ b/OpenTelemetryPlan/00-tracing-fundamentals.md @@ -68,7 +68,7 @@ A **span** represents a single unit of work within a trace. Each span has: | `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...` | +| `attributes` | Key-value metadata | `tx_hash=ABC...` | | `status` | OK, ERROR MSG | `OK` | ### 3. Trace Context @@ -494,16 +494,7 @@ traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 ### Protocol Buffers (xrpld P2P messages) -```protobuf -message TMTransaction { - bytes rawTransaction = 1; - // ... existing fields ... - - // Trace context extension - bytes trace_parent = 100; // W3C traceparent - bytes trace_state = 101; // W3C tracestate -} -``` +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. --- diff --git a/OpenTelemetryPlan/01-architecture-analysis.md b/OpenTelemetryPlan/01-architecture-analysis.md index fde1833349..1161a1645b 100644 --- a/OpenTelemetryPlan/01-architecture-analysis.md +++ b/OpenTelemetryPlan/01-architecture-analysis.md @@ -199,7 +199,7 @@ Consensus rounds are multi-phase operations that benefit significantly from trac ```mermaid flowchart TB subgraph round["consensus.round (root span)"] - attrs["Attributes:
xrpl.consensus.ledger.seq = 12345678
xrpl.consensus.mode = proposing
xrpl.consensus.proposers = 35"] + attrs["Attributes:
ledger_seq = 12345678
consensus_mode = proposing
proposers = 35"] subgraph open["consensus.phase.open"] open_desc["Duration: ~3s
Waiting for transactions"] @@ -211,7 +211,7 @@ flowchart TB end subgraph accept["consensus.phase.accept"] - acc_attrs["transactions_applied = 150
ledger.hash = DEF456..."] + acc_attrs["transactions_applied = 150
ledger_hash = DEF456..."] acc_children["├── ledger.build
└── ledger.validate"] end @@ -250,7 +250,7 @@ flowchart TB attrs["Attributes:
http.method = POST
net.peer.ip = 192.168.1.100
command = submit"] subgraph enqueue["jobqueue.enqueue"] - job_attr["xrpl.job.type = jtCLIENT_RPC"] + job_attr["job_type = jtCLIENT_RPC"] end subgraph command["rpc.command.submit"] @@ -354,17 +354,17 @@ After implementing OpenTelemetry, operators and developers will gain visibility ### 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" && xrpl.tx.hash="ABC123..."}` | -| **Cross-Node Propagation** | Transaction path across multiple xrpld nodes with timing | `{xrpl.tx.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.*"}` | +| 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) @@ -456,9 +456,9 @@ xychart-beta ### 1.8.5 Developer Debugging Workflow -1. **Find Transaction**: Query by `xrpl.tx.hash` to get full trace +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 `xrpl.tx.validity`, `rpc_status` for errors +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 diff --git a/OpenTelemetryPlan/02-design-decisions.md b/OpenTelemetryPlan/02-design-decisions.md index f7d4e71c2b..a03551b9fa 100644 --- a/OpenTelemetryPlan/02-design-decisions.md +++ b/OpenTelemetryPlan/02-design-decisions.md @@ -1,7 +1,7 @@ # Design Decisions > **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) -> **Related**: [Architecture Analysis](./01-architecture-analysis.md) | [Code Samples](./04-code-samples.md) +> **Related**: [Architecture Analysis](./01-architecture-analysis.md) --- @@ -72,14 +72,10 @@ flowchart TB ### 2.2.1 OTLP/HTTP (Shipped in Phase 1b) -```cpp -// Configuration for OTLP over HTTP (the only exporter currently wired up). -namespace otlp = opentelemetry::exporter::otlp; - -otlp::OtlpHttpExporterOptions opts; -opts.url = "http://localhost:4318/v1/traces"; -opts.content_type = otlp::HttpRequestContentType::kJson; // or kBinary -``` +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) @@ -100,17 +96,9 @@ Required to land this upgrade: 4. Update the runbook and dashboards to document the alternate port and TLS settings. -Example Phase 1b+ gRPC configuration (when wired up): - -```cpp -// Configuration for OTLP over gRPC (future work). -namespace otlp = opentelemetry::exporter::otlp; - -otlp::OtlpGrpcExporterOptions opts; -opts.endpoint = ":4317"; -opts.use_ssl_credentials = true; -opts.ssl_credentials_cacert_path = "/path/to/ca.crt"; -``` +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. @@ -135,85 +123,77 @@ path in Phase 1b through Phase 5. ### 2.3.2 Complete Span Catalog -```yaml -# Transaction Spans -tx: - receive: "Transaction received from network" - validate: "Transaction signature/format validation" - process: "Full transaction processing" - relay: "Transaction relay to peers" - apply: "Apply transaction to ledger" +| 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 | -# Consensus Spans -consensus: - round: "Complete consensus round" - phase: - open: "Open phase - collecting transactions" - establish: "Establish phase - reaching agreement" - accept: "Accept phase - applying consensus" - proposal: - receive: "Receive peer proposal" - send: "Send our proposal" - validation: - receive: "Receive peer validation" - send: "Send our validation" +### 2.3.3 Attribute Naming Conventions -# RPC Spans -rpc: - request: "HTTP/WebSocket request handling" - command: - "*": "Specific RPC command (dynamic)" +Span **names** follow §2.3.1 (dotted `.`). 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. -# Peer Spans -peer: - connect: "Peer connection establishment" - disconnect: "Peer disconnection" - message: - send: "Send protocol message" - receive: "Receive protocol message" +1. **Per-span unique attribute** → bare field name. The span name already + carries the domain, so no prefix is needed (e.g. `command`, `version`, + `local` on `rpc.command`). +2. **Collision qualifier** → `_` when a bare name would collide + across domains in the shared spanmetrics label space, or with the + OTel-reserved `status` key (e.g. `rpc_status`, `grpc_status`, + `proposal_trusted`, `validation_trusted`). +3. **Shared cross-span attribute** → `_` underscore form, used + wherever the same field appears on more than one span (e.g. `tx_hash`, + `peer_id`, `ledger_seq`, `consensus_round`, `consensus_mode`). +4. **Resource attribute** → dotted `xrpl..`, 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 `[.]` (dotted, per §2.3.1). Only + attribute _keys_ follow rules 1–4. -# Ledger Spans -ledger: - acquire: "Ledger acquisition from network" - build: "Build new ledger" - validate: "Ledger validation" - close: "Close ledger" - replay: "Ledger replay executed" - delta: "Delta-based ledger acquired" +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. -# PathFinding Spans -pathfind: - request: "Path request initiated" - compute: "Path computation executed" - -# TxQ Spans -txq: - enqueue: "Transaction queued" - apply: "Queued transaction applied" - -# Fee/Load Spans -fee: - escalate: "Fee escalation triggered" - -# Validator Spans -validator: - list: - fetch: "UNL list fetched" - manifest: "Manifest update processed" - -# Amendment Spans -amendment: - vote: "Amendment voting executed" - -# SHAMap Spans -shamap: - sync: "State tree synchronization" - -# Job Spans -job: - enqueue: "Job added to queue" - execute: "Job execution" -``` +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. --- @@ -223,140 +203,149 @@ job: ### 2.4.1 Resource Attributes (Set Once at Startup) -```cpp -// Standard OpenTelemetry semantic conventions -resource::SemanticConventions::SERVICE_NAME = "xrpld" -resource::SemanticConventions::SERVICE_VERSION = BuildInfo::getVersionString() -resource::SemanticConventions::SERVICE_INSTANCE_ID = +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). -// Custom xrpld attributes -"xrpl.network.id" = // e.g., 0 for mainnet -"xrpl.network.type" = "mainnet" | "testnet" | "devnet" | "standalone" -"xrpl.node.type" = "validator" | "stock" | "reporting" -"xrpl.node.cluster" = // If clustered -``` +| Key | Type / value | Description | +| --------------------- | ---------------------------------------------------------- | ------------------------------ | +| `service.name` | `"xrpld"` | Standard `SERVICE_NAME` | +| `service.version` | `BuildInfo::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"` \| `"standalone"` | 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 `_`; 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 -```cpp -"xrpl.tx.hash" = string // Transaction hash (hex) -"xrpl.tx.type" = string // "Payment", "OfferCreate", etc. -"xrpl.tx.account" = string // Source account (redacted in prod) -"xrpl.tx.sequence" = int64 // Account sequence number -"xrpl.tx.fee" = int64 // Fee in drops -"xrpl.tx.result" = string // "tesSUCCESS", "tecPATH_DRY", etc. -"xrpl.tx.ledger_index" = int64 // Ledger containing transaction -``` +| 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. | +| `ledger_index` | int64 | Ledger containing transaction | #### Consensus Attributes -```cpp -"xrpl.consensus.round" = int64 // Round number -"xrpl.consensus.phase" = string // "open", "establish", "accept" -"xrpl.consensus.mode" = string // "proposing", "observing", etc. -"xrpl.consensus.proposers" = int64 // Number of proposers -"xrpl.consensus.ledger.prev" = string // Previous ledger hash -"xrpl.consensus.ledger.seq" = int64 // Ledger sequence -"xrpl.consensus.tx_count" = int64 // Transactions in consensus set -"xrpl.consensus.duration_ms" = float64 // Round duration +| 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 | -// Phase 4a: Establish-phase gap fill & cross-node correlation -"xrpl.consensus.round_id" = int64 // Consensus round number -"xrpl.consensus.ledger_id" = string // previousLedger.id() — shared across nodes -"xrpl.consensus.trace_strategy" = string // "deterministic" or "attribute" -"xrpl.consensus.converge_percent" = int64 // Convergence % (0-100+) -"xrpl.consensus.establish_count" = int64 // Number of establish iterations -"xrpl.consensus.disputes_count" = int64 // Active disputed transactions -"xrpl.consensus.proposers_agreed" = int64 // Peers agreeing with our position -"xrpl.consensus.proposers_total" = int64 // Total peer positions -"xrpl.consensus.agree_count" = int64 // Peers that agree (haveConsensus) -"xrpl.consensus.disagree_count" = int64 // Peers that disagree -"xrpl.consensus.threshold_percent" = int64 // Close-time consensus threshold (avCT_CONSENSUS_PCT = 75%) -"xrpl.consensus.result" = string // "yes", "no", "moved_on", "expired" -"xrpl.consensus.mode.old" = string // Previous consensus mode -"xrpl.consensus.mode.new" = string // New consensus mode -``` +Establish-phase gap fill and cross-node correlation attributes (Phase 4a): + +| Key | Type | Description | +| --------------------- | ------ | --------------------------------------------------------- | +| `consensus_round_id` | int64 | Consensus round number | +| `consensus_ledger_id` | string | `previousLedger.id()` — shared across nodes | +| `trace_strategy` | string | `"deterministic"` or `"attribute"` | +| `converge_percent` | int64 | Convergence % (0-100+) | +| `establish_count` | int64 | Number of establish iterations | +| `disputes_count` | int64 | Active disputed transactions | +| `agree_count` | int64 | Peers that agree (haveConsensus) | +| `disagree_count` | int64 | Peers that disagree | +| `threshold_percent` | int64 | Close-time consensus threshold (`avCT_CONSENSUS_PCT`=75%) | +| `consensus_result` | string | `"yes"`, `"no"`, `"moved_on"`, `"expired"` | +| `mode_old` | string | Previous consensus mode | +| `mode_new` | string | New consensus mode | #### RPC Attributes -```cpp -"command" = string // Command name -"version" = int64 // API version -"rpc_role" = string // "admin" or "user" -"xrpl.rpc.params" = string // Sanitized parameters (optional, planned) -``` +| 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) | #### Peer & Message Attributes -```cpp -"xrpl.peer.id" = string // Peer public key (base58) -"xrpl.peer.address" = string // IP:port -"xrpl.peer.latency_ms" = float64 // Measured latency -"xrpl.peer.cluster" = string // Cluster name if clustered -"xrpl.message.type" = string // Protocol message type name -"xrpl.message.size_bytes" = int64 // Message size -"xrpl.message.compressed" = bool // Whether compressed -``` +| 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 -```cpp -"xrpl.ledger.hash" = string // Ledger hash -"xrpl.ledger.index" = int64 // Ledger sequence/index -"xrpl.ledger.close_time" = int64 // Close time (epoch) -"xrpl.ledger.tx_count" = int64 // Transaction count -"xrpl.job.type" = string // Job type name -"xrpl.job.queue_ms" = float64 // Time spent in queue -"xrpl.job.worker" = int64 // Worker thread ID -``` +| Key | Type | Description | +| ----------------- | ------- | --------------------- | +| `ledger_hash` | string | Ledger hash | +| `ledger_index` | int64 | Ledger sequence/index | +| `close_time` | int64 | Close time (epoch) | +| `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 -```cpp -"source_currency" = string // Source currency code (planned, not yet implemented) -"dest_currency" = string // Destination currency code (planned, not yet implemented) -"path_count" = int64 // Number of paths found (planned, not yet implemented) -"cache_hit" = bool // RippleLineCache hit (planned, not yet implemented) -``` +| 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 -```cpp -"xrpl.txq.queue_depth" = int64 // Current queue depth -"xrpl.txq.fee_level" = int64 // Fee level of transaction -"xrpl.txq.eviction_reason" = string // Why transaction was evicted -``` +| 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 -```cpp -"xrpl.fee.load_factor" = int64 // Current load factor -"xrpl.fee.escalation_level" = int64 // Fee escalation multiplier -``` +| Key | Type | Description | +| ---------------------- | ----- | ------------------------- | +| `fee_load_factor` | int64 | Current load factor | +| `fee_escalation_level` | int64 | Fee escalation multiplier | #### Validator Attributes -```cpp -"xrpl.validator.list_size" = int64 // UNL size -"xrpl.validator.list_age_sec" = int64 // Seconds since last update -``` +| Key | Type | Description | +| ------------------------ | ----- | ------------------------- | +| `validator_list_size` | int64 | UNL size | +| `validator_list_age_sec` | int64 | Seconds since last update | #### Amendment Attributes -```cpp -"xrpl.amendment.name" = string // Amendment name -"xrpl.amendment.status" = string // "enabled", "vetoed", "supported" -``` +| Key | Type | Description | +| ------------------ | ------ | -------------------------------------- | +| `amendment_name` | string | Amendment name | +| `amendment_status` | string | `"enabled"`, `"vetoed"`, `"supported"` | #### SHAMap Attributes -```cpp -"xrpl.shamap.type" = string // "transaction", "state", "account_state" -"xrpl.shamap.missing_nodes" = int64 // Number of missing nodes during sync -"xrpl.shamap.duration_ms" = float64 // Sync duration -``` +| 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 @@ -364,18 +353,18 @@ The following table summarizes what data is collected by category: | Category | Attributes Collected | Purpose | | --------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------- | -| **Transaction** | `tx.hash`, `tx.type`, `tx.result`, `tx.fee`, `ledger_index` | Trace transaction lifecycle | -| **Consensus** | `round`, `phase`, `mode`, `proposers` (public keys), `duration_ms` | Analyze consensus timing | -| **RPC** | `command`, `version`, `status`, `duration_ms` | Monitor RPC performance | -| **Peer** | `peer.id` (public key), `latency_ms`, `message.type`, `message.size` | Network topology analysis | -| **Ledger** | `ledger.hash`, `ledger.index`, `close_time`, `tx_count` | Ledger progression tracking | -| **Job** | `job.type`, `queue_ms`, `worker` | JobQueue performance | +| **Transaction** | `tx_hash`, `tx_type`, `tx_result`, `tx_fee`, `ledger_index` | 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_index`, `close_time`, `ledger_tx_count` | Ledger progression tracking | +| **Job** | `job_type`, `job_queue_ms`, `job_worker` | JobQueue performance | | **PathFinding** | `pathfind_fast`, `pathfind_search_level`, `pathfind_num_paths`, `pathfind_ledger_index`, `pathfind_num_requests` | Payment path analysis | -| **TxQ** | `txq.queue_depth`, `fee_level`, `eviction_reason` | Queue depth and fee tracking | -| **Fee** | `fee.load_factor`, `escalation_level` | Fee escalation monitoring | -| **Validator** | `validator.list_size`, `list_age_sec` | UNL health monitoring | -| **Amendment** | `amendment.name`, `status` | Protocol upgrade tracking | -| **SHAMap** | `shamap.type`, `missing_nodes`, `duration_ms` | State tree sync performance | +| **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 @@ -400,7 +389,7 @@ The following data is explicitly **excluded** from telemetry collection: | Mechanism | Description | | ----------------------------- | ------------------------------------------------------------------------- | -| **Account Hashing** | `xrpl.tx.account` is hashed at collector level before storage | +| **Account Hashing** | `tx_account` is hashed at collector level before storage | | **Configurable Redaction** | Sensitive fields can be excluded via `[telemetry]` config section | | **Sampling** | Only 10% of traces recorded by default, reducing data exposure | | **Local Control** | Node operators have full control over what gets exported | @@ -409,41 +398,19 @@ The following data is explicitly **excluded** from telemetry collection: #### Collector-Level Data Protection -The OpenTelemetry Collector can be configured to hash or redact sensitive attributes before export: - -```yaml -processors: - attributes: - actions: - # Hash account addresses before storage - - key: xrpl.tx.account - action: hash - # Remove IP addresses entirely - - key: xrpl.peer.address - action: delete - # Redact specific fields - - key: xrpl.rpc.params - action: delete -``` +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 can control data collection granularity: - -```ini -[telemetry] -enabled=1 - -# Disable collection of specific components -trace_transactions=1 -trace_consensus=1 -trace_rpc=1 -trace_peer=0 # Disable peer tracing (high volume) - -# Redact specific attributes -redact_account=1 # Hash account addresses before export -redact_peer_address=1 # Remove peer IP addresses -``` +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. @@ -597,15 +564,8 @@ xrpld already has two observability mechanisms. OpenTelemetry complements (not r - No parent-child relationships between events - Manual log parsing required -```json -// Example PerfLog entry -{ - "time": "2024-01-15T10:30:00.123Z", - "method": "submit", - "duration_us": 1523, - "result": "tesSUCCESS" -} -``` +A PerfLog entry is a JSON object with fields such as `time`, `method`, +`duration_us`, and `result`. #### Beast Insight (StatsD) @@ -619,12 +579,8 @@ xrpld already has two observability mechanisms. OpenTelemetry complements (not r - No causal relationships - Single-node perspective -```cpp -// Example StatsD usage in xrpld -insight.increment("rpc.submit.count"); -insight.gauge("ledger.age", age); -insight.timing("consensus.round", duration); -``` +In xrpld, Beast Insight is used through `increment` (counters), `gauge` +(point-in-time values), and `timing` (durations) calls. #### OpenTelemetry (NEW) @@ -638,13 +594,9 @@ insight.timing("consensus.round", duration); - Requires collector infrastructure - Higher complexity than logging -```cpp -// Example OpenTelemetry span -auto span = telemetry.startSpan("tx.relay"); -span->SetAttribute("tx.hash", hash); -span->SetAttribute("peer.id", peerId); -// Span automatically linked to parent via context -``` +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. ### 2.6.3 When to Use Each @@ -689,41 +641,13 @@ flowchart TB ### 2.6.5 Correlation with PerfLog -Trace IDs can be correlated with existing PerfLog entries for comprehensive debugging: - -```cpp -// In RPCHandler.cpp - correlate trace with PerfLog -Status doCommand(RPC::JsonContext& context, Json::Value& result) -{ - // Start OpenTelemetry span - auto span = context.app.getTelemetry().startSpan( - "rpc.command." + context.method); - - // Get trace ID for correlation - auto traceId = span->GetContext().trace_id().IsValid() - ? toHex(span->GetContext().trace_id()) - : ""; - - // Use existing PerfLog with trace correlation - auto const curId = context.app.getPerfLog().currentId(); - context.app.getPerfLog().rpcStart(context.method, curId); - - // Future: Add trace ID to PerfLog entry - // context.app.getPerfLog().setTraceId(curId, traceId); - - try { - auto ret = handler(context, result); - context.app.getPerfLog().rpcFinish(context.method, curId); - span->SetStatus(opentelemetry::trace::StatusCode::kOk); - return ret; - } catch (std::exception const& e) { - context.app.getPerfLog().rpcError(context.method, curId); - span->RecordException(e); - span->SetStatus(opentelemetry::trace::StatusCode::kError, e.what()); - throw; - } -} -``` +Trace IDs can be correlated with existing PerfLog entries for comprehensive +debugging. The design is for `RPCHandler.cpp` to start an `rpc.command.` +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. --- diff --git a/OpenTelemetryPlan/03-implementation-strategy.md b/OpenTelemetryPlan/03-implementation-strategy.md index 61e522719b..9e80db5d30 100644 --- a/OpenTelemetryPlan/03-implementation-strategy.md +++ b/OpenTelemetryPlan/03-implementation-strategy.md @@ -1,7 +1,7 @@ # Implementation Strategy > **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) -> **Related**: [Code Samples](./04-code-samples.md) | [Configuration Reference](./05-configuration-reference.md) +> **Related**: [Configuration Reference](./05-configuration-reference.md) --- @@ -310,27 +310,12 @@ flowchart TD ### 3.7.3 Conditional Instrumentation -SpanGuard's static factory methods handle both compile-time and runtime -checks internally. When `XRPL_ENABLE_TELEMETRY` is not defined, the -entire SpanGuard class compiles to a no-op stub with empty method bodies. -When it is defined, the factory methods check the global Telemetry -instance and the relevant component filter before creating a span: - -```cpp -// SpanGuard factory methods handle all conditional logic internally. -// When XRPL_ENABLE_TELEMETRY is not defined, these are no-ops. -// When defined, they check Telemetry::getInstance() and the -// component filter (e.g. shouldTracePeer()) at runtime. -auto span = telemetry::SpanGuard::peerSpan("peer.message.receive"); -span.setAttribute("xrpl.peer.id", peerId); -// No overhead when telemetry is disabled at compile time or runtime -``` +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 -- **[Code Samples](./04-code-samples.md)**: Complete implementation code for all components - **[Configuration Reference](./05-configuration-reference.md)**: Configuration options and collector setup - **[Implementation Phases](./06-implementation-phases.md)**: Detailed timeline and milestones @@ -478,53 +463,10 @@ If issues are discovered after deployment: ### 3.9.7 Code Change Examples -**Minimal RPC Instrumentation (Low Intrusiveness):** +**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. -```cpp -// Before -void ServerHandler::onRequest(...) { - auto result = processRequest(req); - send(result); -} - -// After (only ~4 lines added) -void ServerHandler::onRequest(...) { - auto span = telemetry::SpanGuard::rpcSpan("rpc.request"); // +1 line - span.setAttribute("command", command); // +1 line - - auto result = processRequest(req); - - span.setAttribute("rpc_status", status); // +1 line - send(result); -} -``` - -SpanGuard factory methods (`rpcSpan`, `txSpan`, `consensusSpan`, etc.) -access the global `Telemetry` instance internally and check the relevant -component filter (`shouldTraceRpc()`, etc.) before creating a span. The -public SpanGuard header has zero `opentelemetry/` includes -- all OTel -types are hidden behind the pimpl idiom. - -**Consensus Instrumentation (Medium Intrusiveness):** - -```cpp -// Before -void RCLConsensusAdaptor::startRound(...) { - // ... existing logic -} - -// After (context storage required) -void RCLConsensusAdaptor::startRound(...) { - auto span = telemetry::SpanGuard::consensusSpan("consensus.round"); - span.setAttribute("xrpl.consensus.ledger.seq", seq); - - // Store context for child spans in phase transitions - currentRoundContext_ = span.context(); // New member variable - - // ... existing logic unchanged -} -``` +**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: [Code Samples](./04-code-samples.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ +_Previous: [Design Decisions](./02-design-decisions.md)_ | _Next: [Configuration Reference](./05-configuration-reference.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ diff --git a/OpenTelemetryPlan/04-code-samples.md b/OpenTelemetryPlan/04-code-samples.md deleted file mode 100644 index cd5c811f7b..0000000000 --- a/OpenTelemetryPlan/04-code-samples.md +++ /dev/null @@ -1,1018 +0,0 @@ -# Code Samples - -> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) -> **Related**: [Implementation Strategy](./03-implementation-strategy.md) | [Configuration Reference](./05-configuration-reference.md) - ---- - -## 4.1 Core Interfaces - -> **OTLP** = OpenTelemetry Protocol - -### 4.1.1 Main Telemetry Interface - -```cpp -// include/xrpl/telemetry/Telemetry.h -#pragma once - -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl { -namespace telemetry { - -/** - * Main telemetry interface for OpenTelemetry integration. - * - * This class provides the primary API for distributed tracing in xrpld. - * It manages the OpenTelemetry SDK lifecycle and provides convenience - * methods for creating spans and propagating context. - */ -class Telemetry -{ -public: - /** - * Configuration for the telemetry system. - */ - struct Setup - { - bool enabled = false; - std::string serviceName = "xrpld"; - std::string serviceVersion; - std::string serviceInstanceId; // Node public key - - // Exporter configuration - std::string exporterType = "otlp_grpc"; // "otlp_grpc", "otlp_http", "none" - std::string exporterEndpoint = "localhost:4317"; - bool useTls = false; - std::string tlsCertPath; - - // Head sampling: fixed at 1.0 (sample everything), not config-driven. - // Keeps trace keep/drop decisions coherent across nodes; volume - // reduction is delegated to the collector's tail sampling. - double samplingRatio = 1.0; - - // Batch processor settings - std::uint32_t batchSize = 512; - std::chrono::milliseconds batchDelay{5000}; - std::uint32_t maxQueueSize = 2048; - - // Network attributes - std::uint32_t networkId = 0; - std::string networkType = "mainnet"; - - // Component filtering - bool traceTransactions = true; - bool traceConsensus = true; - bool traceRpc = true; - bool tracePeer = true; // High volume, enabled by default - bool traceLedger = true; - bool tracePathfind = true; - bool traceTxQ = true; - bool traceValidator = false; // Low volume, disabled by default - bool traceAmendment = false; // Very low volume, disabled by default - }; - - virtual ~Telemetry() = default; - - // ═══════════════════════════════════════════════════════════════════════ - // LIFECYCLE - // ═══════════════════════════════════════════════════════════════════════ - - /** Start the telemetry system (call after configuration) */ - virtual void start() = 0; - - /** Stop the telemetry system (flushes pending spans) */ - virtual void stop() = 0; - - /** Check if telemetry is enabled */ - virtual bool isEnabled() const = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // TRACER ACCESS - // ═══════════════════════════════════════════════════════════════════════ - - /** Get the tracer for creating spans */ - virtual opentelemetry::nostd::shared_ptr - getTracer(std::string_view name = "xrpld") = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // SPAN CREATION (Convenience Methods) - // ═══════════════════════════════════════════════════════════════════════ - - /** Start a new span with default options */ - virtual opentelemetry::nostd::shared_ptr - startSpan( - std::string_view name, - opentelemetry::trace::SpanKind kind = - opentelemetry::trace::SpanKind::kInternal) = 0; - - /** Start a span as child of given context */ - virtual opentelemetry::nostd::shared_ptr - startSpan( - std::string_view name, - opentelemetry::context::Context const& parentContext, - opentelemetry::trace::SpanKind kind = - opentelemetry::trace::SpanKind::kInternal) = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // CONTEXT PROPAGATION - // ═══════════════════════════════════════════════════════════════════════ - - /** Serialize context for network transmission */ - virtual std::string serializeContext( - opentelemetry::context::Context const& ctx) = 0; - - /** Deserialize context from network data */ - virtual opentelemetry::context::Context deserializeContext( - std::string const& serialized) = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // COMPONENT FILTERING - // ═══════════════════════════════════════════════════════════════════════ - - /** Check if transaction tracing is enabled */ - virtual bool shouldTraceTransactions() const = 0; - - /** Check if consensus tracing is enabled */ - virtual bool shouldTraceConsensus() const = 0; - - /** Check if RPC tracing is enabled */ - virtual bool shouldTraceRpc() const = 0; - - /** Check if peer message tracing is enabled */ - virtual bool shouldTracePeer() const = 0; - - /** Check if ledger tracing is enabled */ - virtual bool shouldTraceLedger() const = 0; - - /** Check if path finding tracing is enabled */ - virtual bool shouldTracePathfind() const = 0; - - /** Check if transaction queue tracing is enabled */ - virtual bool shouldTraceTxQ() const = 0; - - /** Check if validator list/manifest tracing is enabled */ - virtual bool shouldTraceValidator() const = 0; - - /** Check if amendment voting tracing is enabled */ - virtual bool shouldTraceAmendment() const = 0; -}; - -// Factory functions -std::unique_ptr -makeTelemetry( - Telemetry::Setup const& setup, - beast::Journal journal); - -Telemetry::Setup -setupTelemetry( - Section const& section, - std::string const& nodePublicKey, - std::string const& version); - -} // namespace telemetry -} // namespace xrpl -``` - ---- - -## 4.2 RAII Span Guard with Factory Methods - -SpanGuard is a self-contained RAII wrapper that creates, activates, and -ends trace spans. It uses the pimpl idiom to hide all OpenTelemetry -types -- the public header has **zero `opentelemetry/` includes**. -Callers never interact with OTel SDK types directly. - -SpanGuard provides static factory methods (`rpcSpan()`, `txSpan()`, -`consensusSpan()`, etc.) that access the global `Telemetry` singleton -internally. Each factory checks both the runtime enable flag and the -relevant component filter before creating a span. - -When `XRPL_ENABLE_TELEMETRY` is **not** defined, the entire SpanGuard -class compiles to a no-op stub with empty inline method bodies, giving -zero compile-time and runtime cost. - -```cpp -// include/xrpl/telemetry/SpanGuard.h -// -// Public API -- no opentelemetry/ includes. -// OTel types are hidden behind the pimpl (Impl struct, defined in the -// #ifdef XRPL_ENABLE_TELEMETRY section at the bottom of the header). -#pragma once - -#include -#include - -namespace xrpl { -namespace telemetry { - -#ifdef XRPL_ENABLE_TELEMETRY - -class SpanGuard -{ - struct Impl; // pimpl -- defined in .cpp or - std::unique_ptr impl_; // in the guarded section below - -public: - // ═══════════════════════════════════════════════════════════════════ - // FACTORY METHODS (access global Telemetry internally) - // ═══════════════════════════════════════════════════════════════════ - - /** Create a span for RPC request handling. - * Returns a no-op guard if telemetry is disabled or - * shouldTraceRpc() is false. - */ - static SpanGuard rpcSpan(std::string_view name); - - /** Create a span for transaction processing. */ - static SpanGuard txSpan(std::string_view name); - - /** Create a span for consensus rounds. */ - static SpanGuard consensusSpan(std::string_view name); - - /** Create a span for peer-to-peer messages. */ - static SpanGuard peerSpan(std::string_view name); - - /** Create a span for ledger operations. */ - static SpanGuard ledgerSpan(std::string_view name); - - /** Create an uncategorized span (always created when enabled). */ - static SpanGuard span(std::string_view name); - - // ═══════════════════════════════════════════════════════════════════ - // INSTANCE METHODS - // ═══════════════════════════════════════════════════════════════════ - - SpanGuard(); // constructs a no-op guard - ~SpanGuard(); - SpanGuard(SpanGuard&& other) noexcept; - SpanGuard& operator=(SpanGuard&&) = delete; - SpanGuard(SpanGuard const&) = delete; - SpanGuard& operator=(SpanGuard const&) = delete; - - /** Mark the span status as OK. */ - void setOk(); - - /** Set an explicit status code. */ - void setStatus(int code, std::string_view description = ""); - - /** Set a key-value attribute on the span. */ - template - void setAttribute(std::string_view key, T&& value); - - /** Add an event to the span timeline. */ - void addEvent(std::string_view name); - - /** Record an exception and set error status. */ - void recordException(std::exception const& e); - - /** Get the current trace context (for cross-thread propagation). */ - // Returns an opaque context handle. - auto context() const; - - /** Discard this span -- dropped before export. */ - void discard(); -}; - -#else // XRPL_ENABLE_TELEMETRY not defined -- zero-cost stub - -class SpanGuard -{ -public: - // Factory methods -- all return no-op guards - static SpanGuard rpcSpan(std::string_view) { return {}; } - static SpanGuard txSpan(std::string_view) { return {}; } - static SpanGuard consensusSpan(std::string_view) { return {}; } - static SpanGuard peerSpan(std::string_view) { return {}; } - static SpanGuard ledgerSpan(std::string_view) { return {}; } - static SpanGuard span(std::string_view) { return {}; } - - // Instance methods -- all no-ops - void setOk() {} - void setStatus(int, std::string_view = "") {} - - template - void setAttribute(std::string_view, T&&) {} - - void addEvent(std::string_view) {} - void recordException(std::exception const&) {} - void discard() {} -}; - -#endif // XRPL_ENABLE_TELEMETRY - -} // namespace telemetry -} // namespace xrpl -``` - ---- - -## 4.3 SpanGuard API Reference - -The previous macro-based approach (`TracingInstrumentation.h` with -`XRPL_TRACE_*` macros) has been replaced by SpanGuard's static factory -methods. This eliminates preprocessor macros from instrumentation call -sites and provides a cleaner, type-safe API. - -### 4.3.1 Factory Methods - -Each factory method accesses the global `Telemetry::getInstance()` -singleton internally and checks the corresponding component filter. -If telemetry is disabled (compile-time or runtime) or the component -filter is off, the factory returns a no-op guard whose methods are -all empty inlines. - -| Factory Method | Component Filter | Typical Span Names | -| -------------------------------- | --------------------------- | ------------------------------------ | -| `SpanGuard::rpcSpan(name)` | `shouldTraceRpc()` | `rpc.request`, `rpc.command.submit` | -| `SpanGuard::txSpan(name)` | `shouldTraceTransactions()` | `tx.receive`, `tx.validate` | -| `SpanGuard::consensusSpan(name)` | `shouldTraceConsensus()` | `consensus.round`, `consensus.phase` | -| `SpanGuard::peerSpan(name)` | `shouldTracePeer()` | `peer.message.receive` | -| `SpanGuard::ledgerSpan(name)` | `shouldTraceLedger()` | `ledger.close`, `ledger.accept` | -| `SpanGuard::span(name)` | (always, if enabled) | `job.execute`, custom spans | - -### 4.3.2 Usage Pattern - -```cpp -#include - -void ServerHandler::onRequest(...) -{ - // Factory creates a span if RPC tracing is enabled, no-op otherwise. - // No Telemetry& reference needed -- accessed via global singleton. - auto span = telemetry::SpanGuard::rpcSpan("rpc.request"); - span.setAttribute("command", command); - - auto result = processRequest(req); - - span.setAttribute("rpc_status", result.status()); - span.setOk(); - // span ended automatically when it goes out of scope -} -``` - -### 4.3.3 Compile-Time Disabled Behavior - -When `XRPL_ENABLE_TELEMETRY` is **not** defined, SpanGuard compiles to -a zero-cost no-op stub. All factory methods return a default-constructed -guard, and all instance methods have empty bodies: - -```cpp -// When XRPL_ENABLE_TELEMETRY is not defined: -class SpanGuard -{ -public: - static SpanGuard rpcSpan(std::string_view) { return {}; } - static SpanGuard txSpan(std::string_view) { return {}; } - static SpanGuard consensusSpan(std::string_view) { return {}; } - static SpanGuard peerSpan(std::string_view) { return {}; } - static SpanGuard ledgerSpan(std::string_view) { return {}; } - static SpanGuard span(std::string_view) { return {}; } - - void setOk() {} - void setStatus(int, std::string_view = "") {} - template - void setAttribute(std::string_view, T&&) {} - void addEvent(std::string_view) {} - void recordException(std::exception const&) {} - void discard() {} -}; -``` - -The compiler optimizes away all calls to these empty methods, producing -the same binary as if no instrumentation code were present. - -### 4.3.4 Discard Support - -SpanGuard supports discarding a span before it is exported. This is -useful for filtering out uninteresting spans (e.g. successful -preflight checks) after the span has been started: - -```cpp -auto span = telemetry::SpanGuard::txSpan("tx.process"); -auto result = preflight(tx); -if (result != tesSUCCESS) -{ - // Span is dropped before entering the batch export queue. - span.discard(); - return result; -} -``` - ---- - -## 4.4 Protocol Buffer Extensions - -### 4.4.1 TraceContext Message Definition - -Add to `src/xrpld/overlay/detail/ripple.proto`: - -```protobuf -// Note: xrpld uses proto2 syntax. The 'optional' keyword below is valid -// in proto2 (it is the default field rule) and is included for clarity. - -// Trace context for distributed tracing across nodes -// Uses W3C Trace Context format internally -message TraceContext { - // 16-byte trace identifier (required for valid context) - bytes trace_id = 1; - - // 8-byte span identifier of parent span - bytes span_id = 2; - - // Trace flags (bit 0 = sampled) - uint32 trace_flags = 3; - - // W3C tracestate header value for vendor-specific data - string trace_state = 4; -} - -// Extend existing messages with optional trace context -// High field numbers (1000+) to avoid conflicts - -message TMTransaction { - // ... existing fields ... - - // Optional trace context for distributed tracing - optional TraceContext trace_context = 1001; -} - -message TMProposeSet { - // ... existing fields ... - optional TraceContext trace_context = 1001; -} - -message TMValidation { - // ... existing fields ... - optional TraceContext trace_context = 1001; -} - -message TMGetLedger { - // ... existing fields ... - optional TraceContext trace_context = 1001; -} - -message TMLedgerData { - // ... existing fields ... - optional TraceContext trace_context = 1001; -} -``` - -### 4.4.2 Context Serialization/Deserialization - -```cpp -// include/xrpl/telemetry/TraceContext.h -#pragma once - -#include -#include -#include -#include -#include // Generated protobuf - -#include -#include - -namespace xrpl { -namespace telemetry { - -/** - * Utilities for trace context serialization and propagation. - */ -class TraceContextPropagator -{ -public: - /** - * Extract trace context from Protocol Buffer message. - * Returns empty context if no trace info present. - */ - static opentelemetry::context::Context - extract(protocol::TraceContext const& proto); - - /** - * Inject current trace context into Protocol Buffer message. - */ - static void - inject( - opentelemetry::context::Context const& ctx, - protocol::TraceContext& proto); - - /** - * Extract trace context from HTTP headers (for RPC). - * Supports W3C Trace Context (traceparent, tracestate). - */ - static opentelemetry::context::Context - extractFromHeaders( - std::function(std::string_view)> headerGetter); - - /** - * Inject trace context into HTTP headers (for RPC responses). - */ - static void - injectToHeaders( - opentelemetry::context::Context const& ctx, - std::function headerSetter); -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// IMPLEMENTATION -// ═══════════════════════════════════════════════════════════════════════════ - -inline opentelemetry::context::Context -TraceContextPropagator::extract(protocol::TraceContext const& proto) -{ - using namespace opentelemetry::trace; - - if (proto.trace_id().size() != 16 || proto.span_id().size() != 8) - { - // Log malformed trace context for debugging. Silent failures in - // context extraction make distributed tracing issues hard to diagnose. - JLOG(j_.warn()) << "Malformed trace context: trace_id size=" - << proto.trace_id().size() - << " span_id size=" << proto.span_id().size(); - return opentelemetry::context::Context{}; - } - - // Construct TraceId and SpanId from bytes - TraceId traceId(reinterpret_cast(proto.trace_id().data())); - SpanId spanId(reinterpret_cast(proto.span_id().data())); - TraceFlags flags(static_cast(proto.trace_flags())); - - // Create SpanContext from extracted data - SpanContext spanContext(traceId, spanId, flags, /* remote = */ true); - - // DefaultSpan wraps SpanContext for use as a non-recording parent. - // This is the standard OTel C++ pattern for remote context propagation. - // DefaultSpan carries the remote SpanContext without recording any data. - auto parentCtx = opentelemetry::trace::SetSpan( - opentelemetry::context::Context{}, - opentelemetry::nostd::shared_ptr( - new DefaultSpan(spanContext))); - - return parentCtx; -} - -inline void -TraceContextPropagator::inject( - opentelemetry::context::Context const& ctx, - protocol::TraceContext& proto) -{ - using namespace opentelemetry::trace; - - // Get current span from context - auto span = GetSpan(ctx); - if (!span) - return; - - auto const& spanCtx = span->GetContext(); - if (!spanCtx.IsValid()) - return; - - // Serialize trace_id (16 bytes) - auto const& traceId = spanCtx.trace_id(); - proto.set_trace_id(traceId.Id().data(), TraceId::kSize); - - // Serialize span_id (8 bytes) - auto const& spanId = spanCtx.span_id(); - proto.set_span_id(spanId.Id().data(), SpanId::kSize); - - // Serialize flags - proto.set_trace_flags(spanCtx.trace_flags().flags()); - - // Note: tracestate not implemented yet -} - -} // namespace telemetry -} // namespace xrpl -``` - ---- - -## 4.5 Module-Specific Instrumentation - -### 4.5.1 Transaction Relay Instrumentation - -```cpp -// src/xrpld/overlay/detail/PeerImp.cpp (modified) - -#include - -void -PeerImp::handleTransaction( - std::shared_ptr const& m) -{ - // Extract trace context from incoming message - opentelemetry::context::Context parentCtx; - if (m->has_trace_context()) - { - parentCtx = telemetry::TraceContextPropagator::extract( - m->trace_context()); - } - - // Start span as child of remote span (cross-node link) - auto span = app_.getTelemetry().startSpan( - "tx.receive", - parentCtx, - opentelemetry::trace::SpanKind::kServer); - telemetry::SpanGuard guard(span); - - try - { - // Parse and validate transaction - SerialIter sit(makeSlice(m->rawtransaction())); - auto stx = std::make_shared(sit); - - // Add transaction attributes - guard.setAttribute("xrpl.tx.hash", to_string(stx->getTransactionID())); - guard.setAttribute("xrpl.tx.type", stx->getTxnType()); - guard.setAttribute("xrpl.peer.id", remote_address_.to_string()); - - // Check if we've seen this transaction (HashRouter) - auto const [flags, suppressed] = - app_.getHashRouter().addSuppressionPeer( - stx->getTransactionID(), - id_); - - if (suppressed) - { - guard.setAttribute("xrpl.tx.suppressed", true); - guard.addEvent("tx.duplicate"); - return; // Already processing this transaction - } - - // Create child span for validation - { - auto validateSpan = app_.getTelemetry().startSpan("tx.validate"); - telemetry::SpanGuard validateGuard(validateSpan); - - auto [validity, reason] = checkTransaction(stx); - validateGuard.setAttribute("xrpl.tx.validity", - validity == Validity::Valid ? "valid" : "invalid"); - - if (validity != Validity::Valid) - { - validateGuard.setStatus( - opentelemetry::trace::StatusCode::kError, - reason); - return; - } - } - - // Relay to other peers (capture context for propagation) - auto ctx = guard.context(); - - // Create child span for relay - auto relaySpan = app_.getTelemetry().startSpan( - "tx.relay", - ctx, - opentelemetry::trace::SpanKind::kClient); - { - telemetry::SpanGuard relayGuard(relaySpan); - - // Inject context into outgoing message - protocol::TraceContext protoCtx; - telemetry::TraceContextPropagator::inject( - relayGuard.context(), protoCtx); - - // Relay to other peers - app_.getOverlay().relay( - stx->getTransactionID(), - *m, - protoCtx, // Pass trace context - exclusions); - - relayGuard.setAttribute("xrpl.tx.relay_count", - static_cast(relayCount)); - } - - guard.setOk(); - } - catch (std::exception const& e) - { - guard.recordException(e); - JLOG(journal_.warn()) << "Transaction handling failed: " << e.what(); - } -} -``` - -### 4.5.2 Consensus Instrumentation - -```cpp -// src/xrpld/app/consensus/RCLConsensus.cpp (modified) - -#include - -void -RCLConsensusAdaptor::startRound( - NetClock::time_point const& now, - RCLCxLedger::ID const& prevLedgerHash, - RCLCxLedger const& prevLedger, - hash_set const& peers, - bool proposing) -{ - auto span = telemetry::SpanGuard::consensusSpan("consensus.round"); - - span.setAttribute("xrpl.consensus.ledger.prev", to_string(prevLedgerHash)); - span.setAttribute("xrpl.consensus.ledger.seq", - static_cast(prevLedger.seq() + 1)); - span.setAttribute("xrpl.consensus.proposers", - static_cast(peers.size())); - span.setAttribute("xrpl.consensus.mode", - proposing ? "proposing" : "observing"); - - // Store trace context for use in phase transitions - currentRoundContext_ = span.context(); - - // ... existing implementation ... -} - -ConsensusPhase -RCLConsensusAdaptor::phaseTransition(ConsensusPhase newPhase) -{ - // Create span for phase transition - auto span = app_.getTelemetry().startSpan( - "consensus.phase." + to_string(newPhase), - currentRoundContext_); - telemetry::SpanGuard guard(span); - - guard.setAttribute("xrpl.consensus.phase", to_string(newPhase)); - guard.addEvent("phase.enter"); - - auto const startTime = std::chrono::steady_clock::now(); - - try - { - auto result = doPhaseTransition(newPhase); - - auto const duration = std::chrono::steady_clock::now() - startTime; - guard.setAttribute("xrpl.consensus.phase_duration_ms", - std::chrono::duration(duration).count()); - - guard.setOk(); - return result; - } - catch (std::exception const& e) - { - guard.recordException(e); - throw; - } -} - -void -RCLConsensusAdaptor::peerProposal( - NetClock::time_point const& now, - RCLCxPeerPos const& proposal) -{ - // Extract trace context from proposal message - opentelemetry::context::Context parentCtx; - if (proposal.hasTraceContext()) - { - parentCtx = telemetry::TraceContextPropagator::extract( - proposal.traceContext()); - } - - auto span = app_.getTelemetry().startSpan( - "consensus.proposal.receive", - parentCtx, - opentelemetry::trace::SpanKind::kServer); - telemetry::SpanGuard guard(span); - - guard.setAttribute("xrpl.consensus.proposer", - toBase58(TokenType::NodePublic, proposal.nodeId())); - guard.setAttribute("xrpl.consensus.round", - static_cast(proposal.proposal().proposeSeq())); - - // ... existing implementation ... - - guard.setOk(); -} -``` - -### 4.5.3 RPC Handler Instrumentation - -```cpp -// src/xrpld/rpc/detail/ServerHandler.cpp (modified) - -#include - -void -ServerHandler::onRequest( - http_request_type&& req, - std::function&& send) -{ - // SpanGuard::rpcSpan() accesses the global Telemetry instance - // and checks shouldTraceRpc() internally. Returns a no-op guard - // if tracing is disabled. - auto span = telemetry::SpanGuard::rpcSpan("rpc.request"); - - // Add HTTP attributes - span.setAttribute("http.method", std::string(req.method_string())); - span.setAttribute("http.target", std::string(req.target())); - span.setAttribute("http.user_agent", - std::string(req[boost::beast::http::field::user_agent])); - - auto const startTime = std::chrono::steady_clock::now(); - - try - { - // Parse and process request - auto const& body = req.body(); - Json::Value jv; - Json::Reader reader; - - if (!reader.parse(body, jv)) - { - span.setStatus( - /* kError */ 2, - "Invalid JSON"); - sendError(send, "Invalid JSON"); - return; - } - - // Extract command name - std::string command = jv.isMember("command") - ? jv["command"].asString() - : jv.isMember("method") - ? jv["method"].asString() - : "unknown"; - - span.setAttribute("command", command); - - // Create child span for command execution - { - auto cmdSpan = telemetry::SpanGuard::rpcSpan( - "rpc.command." + command); - - // Execute RPC command - auto result = processRequest(jv); - - // Record result attributes - if (result.isMember("status")) - { - cmdSpan.setAttribute("rpc_status", - result["status"].asString()); - } - - if (result["status"].asString() == "error") - { - cmdSpan.setStatus( - /* kError */ 2, - result.isMember("error_message") - ? result["error_message"].asString() - : "RPC error"); - } - else - { - cmdSpan.setOk(); - } - } - - auto const duration = std::chrono::steady_clock::now() - startTime; - span.setAttribute("http.duration_ms", - std::chrono::duration(duration).count()); - - // Inject trace context into response headers - http_response_type resp; - telemetry::TraceContextPropagator::injectToHeaders( - span.context(), - [&resp](std::string_view name, std::string_view value) { - resp.set(std::string(name), std::string(value)); - }); - - span.setOk(); - send(std::move(resp)); - } - catch (std::exception const& e) - { - span.recordException(e); - JLOG(journal_.error()) << "RPC request failed: " << e.what(); - sendError(send, e.what()); - } -} -``` - -### 4.5.4 JobQueue Context Propagation - -> **Architecture note**: `JobQueue` and its inner `Workers` class do not -> hold an `Application&` or `ServiceRegistry&`. They receive a -> `perf::PerfLog*` at construction. Because SpanGuard's factory methods -> access the global `Telemetry` instance directly, no `Telemetry&` -> reference needs to be threaded into `JobQueue`. -> -> The approach below captures trace context at job-creation time and -> restores it when the job executes, so that any spans created _inside_ -> the job body automatically become children of the original caller's -> trace. - -```cpp -// src/libxrpl/core/detail/JobQueue.cpp (modified -- processTask) - -#include - -void -JobQueue::processTask(int instance) -{ - // ... existing job dequeue logic ... - - // SpanGuard::span() uses the global Telemetry instance -- - // no Telemetry& member needed on JobQueue. - auto span = telemetry::SpanGuard::span("job.execute"); - span.setAttribute("xrpl.job.type", to_string(job.type())); - span.setAttribute("xrpl.job.worker", - static_cast(instance)); - - try - { - job.execute(); - span.setOk(); - } - catch (std::exception const& e) - { - span.recordException(e); - JLOG(journal_.error()) << "Job execution failed: " << e.what(); - } -} -``` - ---- - -## 4.6 Span Flow Visualization - -
- -```mermaid -flowchart TB - subgraph Client["External Client"] - submit["Submit TX"] - end - - subgraph NodeA["xrpld Node A"] - rpcA["rpc.request"] - cmdA["rpc.command.submit"] - txRecvA["tx.receive"] - txValA["tx.validate"] - txRelayA["tx.relay"] - end - - subgraph NodeB["xrpld Node B"] - txRecvB["tx.receive"] - txValB["tx.validate"] - txRelayB["tx.relay"] - end - - subgraph NodeC["xrpld Node C"] - txRecvC["tx.receive"] - consensusC["consensus.round"] - phaseC["consensus.phase.establish"] - end - - submit --> rpcA - rpcA --> cmdA - cmdA --> txRecvA - txRecvA --> txValA - txValA --> txRelayA - txRelayA -.->|"TraceContext"| txRecvB - txRecvB --> txValB - txValB --> txRelayB - txRelayB -.->|"TraceContext"| txRecvC - txRecvC --> consensusC - consensusC --> phaseC - - style Client fill:#334155,stroke:#1e293b,color:#fff - style NodeA fill:#1e3a8a,stroke:#172554,color:#fff - style NodeB fill:#064e3b,stroke:#022c22,color:#fff - style NodeC fill:#78350f,stroke:#451a03,color:#fff - style submit fill:#e2e8f0,stroke:#cbd5e1,color:#1e293b - style rpcA fill:#1d4ed8,stroke:#1e40af,color:#fff - style cmdA fill:#1d4ed8,stroke:#1e40af,color:#fff - style txRecvA fill:#047857,stroke:#064e3b,color:#fff - style txValA fill:#047857,stroke:#064e3b,color:#fff - style txRelayA fill:#047857,stroke:#064e3b,color:#fff - style txRecvB fill:#047857,stroke:#064e3b,color:#fff - style txValB fill:#047857,stroke:#064e3b,color:#fff - style txRelayB fill:#047857,stroke:#064e3b,color:#fff - style txRecvC fill:#047857,stroke:#064e3b,color:#fff - style consensusC fill:#fef3c7,stroke:#fde68a,color:#1e293b - style phaseC fill:#fef3c7,stroke:#fde68a,color:#1e293b -``` - -
- -**Reading the diagram:** - -- **Client / Submit TX**: An external client submits a transaction, creating the root span that initiates the trace. -- **Node A (RPC layer)**: The receiving node processes the submission through `rpc.request` and `rpc.command.submit`, then hands off to the transaction pipeline (`tx.receive` → `tx.validate` → `tx.relay`). -- **Dashed arrows (TraceContext)**: Cross-node boundaries where trace context is propagated via the protobuf protocol extension, linking spans across independent processes. -- **Node B (relay hop)**: A peer node that receives, validates, and relays the transaction further, demonstrating multi-hop propagation. -- **Node C (consensus)**: The final node where the transaction enters consensus (`consensus.round` → `consensus.phase.establish`), showing how a single client action produces an end-to-end distributed trace. - ---- - -_Previous: [Implementation Strategy](./03-implementation-strategy.md)_ | _Next: [Configuration Reference](./05-configuration-reference.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ diff --git a/OpenTelemetryPlan/05-configuration-reference.md b/OpenTelemetryPlan/05-configuration-reference.md index 822dedbb41..fca02058cd 100644 --- a/OpenTelemetryPlan/05-configuration-reference.md +++ b/OpenTelemetryPlan/05-configuration-reference.md @@ -1,7 +1,7 @@ # Configuration Reference > **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) -> **Related**: [Code Samples](./04-code-samples.md) | [Implementation Phases](./06-implementation-phases.md) +> **Related**: [Implementation Phases](./06-implementation-phases.md) --- @@ -11,71 +11,7 @@ ### 5.1.1 Configuration File Section -Add to `cfg/xrpld-example.cfg`: - -```ini -# ═══════════════════════════════════════════════════════════════════════════════ -# TELEMETRY (OpenTelemetry Distributed Tracing) -# ═══════════════════════════════════════════════════════════════════════════════ -# -# Enables distributed tracing for transaction flow, consensus, and RPC calls. -# Traces are exported to an OpenTelemetry Collector using OTLP protocol. -# -# [telemetry] -# -# # Enable/disable telemetry (default: 0 = disabled) -# enabled=1 -# -# # OTLP endpoint (default: http://localhost:4318/v1/traces - OTLP/HTTP) -# # Note: only OTLP/HTTP is shipped in Phase 1b. OTLP/gRPC support is -# # planned as future work and is not yet parsed by TelemetryConfig.cpp. -# endpoint=http://localhost:4318/v1/traces -# -# # Use TLS for exporter connection (default: 0) -# use_tls=0 -# -# # Path to CA certificate for TLS (optional) -# # tls_ca_cert=/path/to/ca.crt -# -# # Head sampling is intentionally fixed at 1.0 (sample everything) and is -# # NOT configurable. A per-node head-sampling ratio would let nodes make -# # divergent keep/drop decisions for the same distributed trace, producing -# # broken/partial traces across the network. Volume reduction is delegated -# # to the collector's tail sampling instead. See Section 7.4.2. -# -# # Batch processor settings -# batch_size=512 # Spans per batch (default: 512) -# batch_delay_ms=5000 # Max delay before sending batch (default: 5000) -# max_queue_size=2048 # Max queued spans (default: 2048) -# -# # Component-specific tracing (default: all enabled except peer) -# trace_transactions=1 # Transaction relay and processing -# trace_consensus=1 # Consensus rounds and proposals -# trace_rpc=1 # RPC request handling -# trace_peer=1 # Peer messages (high volume, enabled by default) -# trace_ledger=1 # Ledger acquisition and building -# -# # Planned (not yet parsed by TelemetryConfig.cpp): -# # trace_pathfind=1 # Path computation (Phase 2) -# # trace_txq=1 # Transaction queue (Phase 3) -# # trace_validator=0 # Validator list / manifest (future) -# # trace_amendment=0 # Amendment voting (future) -# -# # Trace ID strategies for cross-node correlation -# # "deterministic" (default) derives trace_id from a workflow hash -# # (txHash for transactions, prevLedgerHash for consensus) so all nodes -# # produce spans under the same trace_id for the same workflow. -# # "attribute" uses random trace_id; correlation via attribute queries. -# tx_trace_strategy=deterministic -# consensus_trace_strategy=deterministic -# -# # Service identification (automatically detected if not specified) -# # service_name=xrpld -# # service_instance_id= - -[telemetry] -enabled=0 -``` +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 @@ -118,69 +54,7 @@ phases. They will be added as the corresponding subsystems are instrumented: > **TxQ** = Transaction Queue -```cpp -// src/libxrpl/telemetry/TelemetryConfig.cpp - -#include -#include - -namespace xrpl { -namespace telemetry { - -Telemetry::Setup -setupTelemetry( - Section const& section, - std::string const& nodePublicKey, - std::string const& version) -{ - Telemetry::Setup setup; - - // Basic settings - setup.enabled = section.value_or("enabled", false); - setup.serviceName = section.value_or("service_name", "xrpld"); - setup.serviceVersion = version; - setup.serviceInstanceId = section.value_or( - "service_instance_id", nodePublicKey); - - // Exporter settings - setup.exporterType = section.value_or("exporter", "otlp_grpc"); - - if (setup.exporterType == "otlp_grpc") - setup.exporterEndpoint = section.value_or("endpoint", "localhost:4317"); - else if (setup.exporterType == "otlp_http") - setup.exporterEndpoint = section.value_or("endpoint", "localhost:4318"); - - setup.useTls = section.value_or("use_tls", false); - setup.tlsCertPath = section.value_or("tls_ca_cert", ""); - setup.tlsClientCertPath = section.value_or("tls_client_cert", ""); - setup.tlsClientKeyPath = section.value_or("tls_client_key", ""); - - // Head sampling is fixed at 1.0 (sample everything) and is not read from - // config — see Section 7.4.2. setup.samplingRatio stays at its 1.0 default. - - // Batch processor - setup.batchSize = section.value_or("batch_size", 512u); - setup.batchDelay = std::chrono::milliseconds{ - section.value_or("batch_delay_ms", 5000u)}; - setup.maxQueueSize = section.value_or("max_queue_size", 2048u); - - // Component filtering - setup.traceTransactions = section.value_or("trace_transactions", true); - setup.traceConsensus = section.value_or("trace_consensus", true); - setup.traceRpc = section.value_or("trace_rpc", true); - setup.tracePeer = section.value_or("trace_peer", true); - setup.traceLedger = section.value_or("trace_ledger", true); - setup.tracePathfind = section.value_or("trace_pathfind", true); - setup.traceTxQ = section.value_or("trace_txq", true); - setup.traceValidator = section.value_or("trace_validator", false); - setup.traceAmendment = section.value_or("trace_amendment", false); - - return setup; -} - -} // namespace telemetry -} // namespace xrpl -``` +The parser `setupTelemetry()` 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.value_or(...)`. 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). --- @@ -194,84 +68,11 @@ setupTelemetry( > constructed with an empty `serviceInstanceId` and patched via > `setServiceInstanceId()` once `setup()` has called `getNodeIdentity()`. -```cpp -// src/xrpld/app/main/Application.cpp (modified) - -#include - -class ApplicationImp : public Application, public BasicApp -{ - // ... existing members (perfLog_, etc.) ... - - // Telemetry — constructed in the member initializer list with - // an empty serviceInstanceId, patched in setup(). - std::unique_ptr telemetry_; - - // Member initializer list (excerpt): - // ... - // , telemetry_( - // telemetry::makeTelemetry( - // telemetry::setupTelemetry( - // config_->section("telemetry"), - // "", // Updated later via setServiceInstanceId() - // BuildInfo::getVersionString()), - // logs_->journal("Telemetry"))) - // ... - - bool setup(...) override - { - // ... existing setup code ... - - nodeIdentity_ = getNodeIdentity(*this, cmdline); - - // Inject node identity into telemetry resource attributes, - // unless the user already set a custom service_instance_id. - if (!config_->section("telemetry").exists("service_instance_id")) - telemetry_->setServiceInstanceId( - toBase58(TokenType::NodePublic, nodeIdentity_->first)); - - // ... rest of setup ... - } - - void start(bool withTimers) override - { - // ... existing start code ... - telemetry_->start(); - } - - void run() override - { - // ... existing run/shutdown code ... - telemetry_->stop(); - } - - telemetry::Telemetry& - getTelemetry() override - { - return *telemetry_; - } -}; -``` +`ApplicationImp` (in `src/xrpld/app/main/Application.cpp`) owns a `std::unique_ptr telemetry_`. It is built in the member initializer list via `makeTelemetry(setupTelemetry(...))` 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 -```cpp -// include/xrpl/core/ServiceRegistry.h (modified) - -namespace telemetry { -class Telemetry; -} // namespace telemetry - -class ServiceRegistry -{ -public: - // ... existing virtual methods ... - - /** Get the telemetry system for distributed tracing. */ - virtual telemetry::Telemetry& - getTelemetry() = 0; -}; -``` +`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. @@ -287,114 +88,11 @@ public: ### 5.4.1 Find OpenTelemetry Module -```cmake -# cmake/FindOpenTelemetry.cmake - -# Find OpenTelemetry C++ SDK -# -# This module defines: -# OpenTelemetry_FOUND - System has OpenTelemetry -# OpenTelemetry::api - API library target -# OpenTelemetry::sdk - SDK library target -# OpenTelemetry::otlp_grpc_exporter - OTLP gRPC exporter target -# OpenTelemetry::otlp_http_exporter - OTLP HTTP exporter target - -find_package(opentelemetry-cpp CONFIG QUIET) - -if(opentelemetry-cpp_FOUND) - set(OpenTelemetry_FOUND TRUE) - - # Create imported targets if not already created by config - if(NOT TARGET OpenTelemetry::api) - add_library(OpenTelemetry::api ALIAS opentelemetry-cpp::api) - endif() - if(NOT TARGET OpenTelemetry::sdk) - add_library(OpenTelemetry::sdk ALIAS opentelemetry-cpp::sdk) - endif() - if(NOT TARGET OpenTelemetry::otlp_grpc_exporter) - add_library(OpenTelemetry::otlp_grpc_exporter ALIAS - opentelemetry-cpp::otlp_grpc_exporter) - endif() -else() - # Try pkg-config fallback - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(OTEL opentelemetry-cpp QUIET) - if(OTEL_FOUND) - set(OpenTelemetry_FOUND TRUE) - # Create imported targets from pkg-config - add_library(OpenTelemetry::api INTERFACE IMPORTED) - target_include_directories(OpenTelemetry::api INTERFACE - ${OTEL_INCLUDE_DIRS}) - endif() - endif() -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(OpenTelemetry - REQUIRED_VARS OpenTelemetry_FOUND) -``` +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 -```cmake -# CMakeLists.txt (additions) - -# ═══════════════════════════════════════════════════════════════════════════════ -# TELEMETRY OPTIONS -# ═══════════════════════════════════════════════════════════════════════════════ - -option(XRPL_ENABLE_TELEMETRY - "Enable OpenTelemetry distributed tracing support" OFF) - -if(XRPL_ENABLE_TELEMETRY) - find_package(OpenTelemetry REQUIRED) - - # Define compile-time flag - add_compile_definitions(XRPL_ENABLE_TELEMETRY) - - message(STATUS "OpenTelemetry tracing: ENABLED") -else() - message(STATUS "OpenTelemetry tracing: DISABLED") -endif() - -# ═══════════════════════════════════════════════════════════════════════════════ -# TELEMETRY LIBRARY -# ═══════════════════════════════════════════════════════════════════════════════ - -if(XRPL_ENABLE_TELEMETRY) - add_library(xrpl_telemetry - src/libxrpl/telemetry/Telemetry.cpp - src/libxrpl/telemetry/TelemetryConfig.cpp - src/libxrpl/telemetry/TraceContext.cpp - ) - - target_include_directories(xrpl_telemetry - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - ) - - target_link_libraries(xrpl_telemetry - PUBLIC - OpenTelemetry::api - OpenTelemetry::sdk - OpenTelemetry::otlp_grpc_exporter - PRIVATE - xrpl_basics - ) - - # Add to main library dependencies - target_link_libraries(xrpld PRIVATE xrpl_telemetry) -else() - # Create null implementation library - add_library(xrpl_telemetry - src/libxrpl/telemetry/NullTelemetry.cpp - ) - target_include_directories(xrpl_telemetry - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include - ) -endif() -``` +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. --- @@ -404,151 +102,15 @@ endif() > **Production hardening**: The configurations in this section are starting points. For production deployments where xrpld ships telemetry across a network to a centrally-hosted collector, see [Securing the OTel Pipeline](./secure-OTel.md) for the required mTLS receiver config, NetworkPolicy, and peer trace-context validation. +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 -```yaml -# otel-collector-dev.yaml -# Minimal configuration for local development - -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 - -exporters: - # Console output for debugging - logging: - verbosity: detailed - sampling_initial: 5 - sampling_thereafter: 200 - - # Tempo for trace storage - otlp/tempo: - endpoint: tempo:4317 - tls: - insecure: true - -service: - pipelines: - traces: - receivers: [otlp] - processors: [batch] - exporters: [logging, otlp/tempo] -``` +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/tempo` (insecure) for trace visualization. The single `traces` pipeline wires receiver → batch → both exporters. ### 5.5.2 Production Configuration -```yaml -# otel-collector-prod.yaml -# Production configuration with filtering, sampling, and multiple backends - -receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - tls: - cert_file: /etc/otel/server.crt - key_file: /etc/otel/server.key - ca_file: /etc/otel/ca.crt - -processors: - # Memory limiter to prevent OOM - memory_limiter: - check_interval: 1s - limit_mib: 1000 - spike_limit_mib: 200 - - # Batch processing for efficiency - batch: - timeout: 5s - send_batch_size: 512 - send_batch_max_size: 1024 - - # Tail-based sampling (keep errors and slow traces) - tail_sampling: - decision_wait: 10s - num_traces: 100000 - expected_new_traces_per_sec: 1000 - policies: - # Always keep error traces - - name: errors - type: status_code - status_code: - status_codes: [ERROR] - # Keep slow consensus rounds (>5s) - - name: slow-consensus - type: latency - latency: - threshold_ms: 5000 - # Keep slow RPC requests (>1s) - - name: slow-rpc - type: and - and: - and_sub_policy: - - name: rpc-spans - type: string_attribute - string_attribute: - key: command - values: [".*"] - enabled_regex_matching: true - - name: latency - type: latency - latency: - threshold_ms: 1000 - # Probabilistic sampling for the rest - - name: probabilistic - type: probabilistic - probabilistic: - sampling_percentage: 10 - - # Attribute processing - attributes: - actions: - # Hash sensitive data - - key: xrpl.tx.account - action: hash - # Add deployment info - - key: deployment.environment - value: production - action: upsert - -exporters: - # Grafana Tempo for long-term storage - otlp/tempo: - endpoint: tempo.monitoring:4317 - tls: - insecure: false - ca_file: /etc/otel/tempo-ca.crt - - # Elastic APM for correlation with logs - otlp/elastic: - endpoint: apm.elastic:8200 - headers: - Authorization: "Bearer ${ELASTIC_APM_TOKEN}" - -extensions: - health_check: - endpoint: 0.0.0.0:13133 - zpages: - endpoint: 0.0.0.0:55679 - -service: - extensions: [health_check, zpages] - pipelines: - traces: - receivers: [otlp] - processors: [memory_limiter, tail_sampling, attributes, batch] - exporters: [otlp/tempo, otlp/elastic] -``` +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. --- @@ -556,61 +118,7 @@ service: > **OTLP** = OpenTelemetry Protocol -```yaml -# docker-compose-telemetry.yaml -version: "3.8" - -services: - # OpenTelemetry Collector - otel-collector: - image: otel/opentelemetry-collector-contrib:0.92.0 - container_name: otel-collector - command: ["--config=/etc/otel-collector-config.yaml"] - volumes: - - ./otel-collector-dev.yaml:/etc/otel-collector-config.yaml:ro - ports: - - "4317:4317" # OTLP gRPC - - "4318:4318" # OTLP HTTP - - "13133:13133" # Health check - depends_on: - - tempo - - # Tempo for trace storage - tempo: - image: grafana/tempo:2.6.1 - container_name: tempo - ports: - - "3200:3200" # Tempo HTTP API - - "4317" # OTLP gRPC (internal) - - # Grafana for dashboards - grafana: - image: grafana/grafana:10.2.3 - container_name: grafana - environment: - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - volumes: - - ./grafana/provisioning:/etc/grafana/provisioning:ro - - ./grafana/dashboards:/var/lib/grafana/dashboards:ro - ports: - - "3000:3000" - depends_on: - - tempo - - # Prometheus for metrics (optional, for correlation) - prometheus: - image: prom/prometheus:v2.48.1 - container_name: prometheus - volumes: - - ./prometheus.yaml:/etc/prometheus/prometheus.yml:ro - ports: - - "9090:9090" - -networks: - default: - name: xrpld-telemetry -``` +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. --- @@ -677,175 +185,23 @@ Step-by-step instructions for integrating xrpld traces with Grafana. #### Tempo (Recommended) -```yaml -# grafana/provisioning/datasources/tempo.yaml -apiVersion: 1 - -datasources: - - name: Tempo - type: tempo - access: proxy - url: http://tempo:3200 - jsonData: - httpMethod: GET - tracesToLogs: - datasourceUid: loki - tags: ["service.name", "xrpl.tx.hash"] - mappedTags: [{ key: "trace_id", value: "traceID" }] - mapTagNamesEnabled: true - filterByTraceID: true - serviceMap: - datasourceUid: prometheus - nodeGraph: - enabled: true - search: - hide: false - lokiSearch: - datasourceUid: loki -``` +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 -```yaml -# grafana/provisioning/datasources/elastic-apm.yaml -apiVersion: 1 - -datasources: - - name: Elasticsearch-APM - type: elasticsearch - access: proxy - url: http://elasticsearch:9200 - database: "apm-*" - jsonData: - esVersion: "8.0.0" - timeField: "@timestamp" - logMessageField: message - logLevelField: log.level -``` +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 -```yaml -# grafana/provisioning/dashboards/dashboards.yaml -apiVersion: 1 - -providers: - - name: "xrpld-dashboards" - orgId: 1 - folder: "xrpld" - folderUid: "xrpld" - type: file - disableDeletion: false - updateIntervalSeconds: 30 - options: - path: /var/lib/grafana/dashboards/rippled -``` +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 -```json -{ - "title": "xrpld RPC Performance", - "uid": "xrpld-rpc-performance", - "panels": [ - { - "title": "RPC Latency by Command", - "type": "heatmap", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && span.command != \"\"} | histogram_over_time(duration) by (span.command)" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 } - }, - { - "title": "RPC Error Rate", - "type": "timeseries", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && status.code=error} | rate() by (span.command)" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 } - }, - { - "title": "Top 10 Slowest RPC Commands", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && span.command != \"\"} | avg(duration) by (span.command) | topk(10)" - } - ], - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 8 } - }, - { - "title": "Recent Traces", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\"}" - } - ], - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 } - } - ] -} -``` +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 -```json -{ - "title": "xrpld Transaction Tracing", - "uid": "xrpld-tx-tracing", - "panels": [ - { - "title": "Transaction Throughput", - "type": "stat", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.receive\"} | rate()" - } - ], - "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 } - }, - { - "title": "Cross-Node Relay Count", - "type": "timeseries", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.relay\"} | avg(span.xrpl.tx.relay_count)" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 } - }, - { - "title": "Transaction Validation Errors", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.validate\" && status.code=error}" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 } - } - ] -} -``` +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.code=error`). ### 5.8.5 TraceQL Query Examples @@ -853,7 +209,7 @@ Common queries for xrpld traces: ``` # Find all traces for a specific transaction hash -{resource.service.name="xrpld" && span.xrpl.tx.hash="ABC123..."} +{resource.service.name="xrpld" && span.tx_hash="ABC123..."} # Find slow RPC commands (>100ms) {resource.service.name="xrpld" && name=~"rpc.command.*"} | duration > 100ms @@ -865,7 +221,7 @@ Common queries for xrpld traces: {resource.service.name="xrpld" && name="tx.validate" && status.code=error} # Find transactions relayed to many peers -{resource.service.name="xrpld" && name="tx.relay"} | span.xrpl.tx.relay_count > 10 +{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(duration) by (resource.service.instance.id) @@ -877,58 +233,15 @@ To correlate OpenTelemetry traces with existing PerfLog data: **Step 1: Configure Loki to ingest PerfLog** -```yaml -# promtail-config.yaml -scrape_configs: - - job_name: xrpld-perflog - static_configs: - - targets: - - localhost - labels: - job: xrpld - __path__: /var/log/rippled/perf*.log - pipeline_stages: - - json: - expressions: - trace_id: trace_id - ledger_seq: ledger_seq - tx_hash: tx_hash - - labels: - trace_id: - ledger_seq: - tx_hash: -``` +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 to include trace_id when available: - -```cpp -// In PerfLog output, add trace_id from current span context -void logPerf(Json::Value& entry) { - auto span = opentelemetry::trace::GetSpan( - opentelemetry::context::RuntimeContext::GetCurrent()); - if (span && span->GetContext().IsValid()) { - char traceIdHex[33]; - span->GetContext().trace_id().ToLowerBase16(traceIdHex); - entry["trace_id"] = std::string(traceIdHex, 32); - } - // ... existing logging -} -``` +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 Tempo data source configuration, set up the derived field: - -```yaml -jsonData: - tracesToLogs: - datasourceUid: loki - tags: ["trace_id", "xrpl.tx.hash"] - filterByTraceID: true - filterBySpanID: false -``` +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 @@ -936,46 +249,22 @@ To correlate traces with existing Beast Insight metrics: **Step 1: Export Insight metrics to Prometheus** -```yaml -# prometheus.yaml -scrape_configs: - - job_name: "xrpld-statsd" - static_configs: - - targets: ["statsd-exporter:9102"] -``` +Add a Prometheus scrape job (`prometheus.yaml`) named `xrpld-statsd` targeting the StatsD exporter at `statsd-exporter:9102`. **Step 2: Add exemplars to metrics** -OpenTelemetry SDK automatically adds exemplars (trace IDs) to metrics when using the Prometheus exporter. This links metrics spikes to specific traces. +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** -```yaml -# In Prometheus data source -jsonData: - exemplarTraceIdDestinations: - - name: trace_id - datasourceUid: tempo -``` +In the Prometheus datasource, set `exemplarTraceIdDestinations` to map the `trace_id` exemplar to the Tempo datasource. **Step 4: Dashboard panel with exemplars** -```json -{ - "title": "RPC Latency with Trace Links", - "type": "timeseries", - "datasource": "Prometheus", - "targets": [ - { - "expr": "histogram_quantile(0.99, rate(xrpld_rpc_duration_seconds_bucket[5m]))", - "exemplar": true - } - ] -} -``` +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: [Code Samples](./04-code-samples.md)_ | _Next: [Implementation Phases](./06-implementation-phases.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ +_Previous: [Implementation Strategy](./03-implementation-strategy.md)_ | _Next: [Implementation Phases](./06-implementation-phases.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ diff --git a/OpenTelemetryPlan/06-implementation-phases.md b/OpenTelemetryPlan/06-implementation-phases.md index 9936165e0a..14b8938b31 100644 --- a/OpenTelemetryPlan/06-implementation-phases.md +++ b/OpenTelemetryPlan/06-implementation-phases.md @@ -179,14 +179,14 @@ SHAMap tracing are not implemented. ### Spans Produced -| Span Name | Location | Attributes | -| --------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `consensus.phase.open` | `Consensus.h` | _(none)_ | -| `consensus.proposal.send` | `RCLConsensus.cpp` | `xrpl.consensus.round` | -| `consensus.ledger_close` | `RCLConsensus.cpp` | `xrpl.consensus.ledger.seq`, `xrpl.consensus.mode` | -| `consensus.accept` | `RCLConsensus.cpp` | `xrpl.consensus.proposers`, `xrpl.consensus.round_time_ms`, `xrpl.consensus.quorum` | -| `consensus.accept.apply` | `RCLConsensus.cpp` | `xrpl.consensus.close_time`, `close_time_correct`, `close_resolution_ms`, `state`, `proposing`, `round_time_ms`, `ledger.seq`, `parent_close_time`, `close_time_self`, `close_time_vote_bins`, `resolution_direction` | -| `consensus.validation.send` | `RCLConsensus.cpp` | `xrpl.consensus.ledger.seq`, `xrpl.consensus.proposing` | +| Span Name | Location | Attributes | +| --------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `consensus.phase.open` | `Consensus.h` | _(none)_ | +| `consensus.proposal.send` | `RCLConsensus.cpp` | `consensus_round` | +| `consensus.ledger_close` | `RCLConsensus.cpp` | `ledger_seq`, `consensus_mode` | +| `consensus.accept` | `RCLConsensus.cpp` | `proposers`, `round_time_ms`, `quorum` | +| `consensus.accept.apply` | `RCLConsensus.cpp` | `close_time`, `close_time_correct`, `close_resolution_ms`, `consensus_state`, `proposing`, `round_time_ms`, `ledger_seq`, `parent_close_time`, `close_time_self`, `close_time_vote_bins`, `resolution_direction` | +| `consensus.validation.send` | `RCLConsensus.cpp` | `ledger_seq`, `proposing` | ### Exit Criteria @@ -253,11 +253,11 @@ with `TraceCategory::Consensus` gating. No macros used — all tracing via direc | Span Name | Location | Key Attributes (actually set) | | ---------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------- | -| `consensus.round` | `RCLConsensus.cpp` | `round_id`, `ledger_id`, `ledger.seq`, `mode`, `trace_strategy` | +| `consensus.round` | `RCLConsensus.cpp` | `consensus_round_id`, `consensus_ledger_id`, `ledger_seq`, `consensus_mode`, `trace_strategy` | | `consensus.establish` | `Consensus.h` | `converge_percent`, `establish_count`, `proposers` | | `consensus.update_positions` | `Consensus.h` | `converge_percent`, `proposers`, `have_close_time_consensus`, `close_time_threshold`, `disputes_count`, `avalanche_threshold` | -| `consensus.check` | `Consensus.h` | `agree/disagree_count`, `converge_percent`, `have_close_time_consensus`, `threshold_percent`, `result` | -| `consensus.mode_change` | `RCLConsensus.cpp` | `mode.old`, `mode.new` | +| `consensus.check` | `Consensus.h` | `agree_count`, `disagree_count`, `converge_percent`, `have_close_time_consensus`, `threshold_percent`, `consensus_result` | +| `consensus.mode_change` | `RCLConsensus.cpp` | `mode_old`, `mode_new` | ### Exit Criteria diff --git a/OpenTelemetryPlan/07-observability-backends.md b/OpenTelemetryPlan/07-observability-backends.md index 5d1638670a..a8bd897723 100644 --- a/OpenTelemetryPlan/07-observability-backends.md +++ b/OpenTelemetryPlan/07-observability-backends.md @@ -230,205 +230,47 @@ Pre-built dashboards for xrpld observability. ### 7.6.1 Consensus Health Dashboard -```json -{ - "title": "xrpld Consensus Health", - "uid": "xrpld-consensus-health", - "tags": ["xrpld", "consensus", "tracing"], - "panels": [ - { - "title": "Consensus Round Duration", - "type": "timeseries", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | avg(duration) by (resource.service.instance.id)" - } - ], - "fieldConfig": { - "defaults": { - "unit": "ms", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 4000 }, - { "color": "red", "value": 5000 } - ] - } - } - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 } - }, - { - "title": "Phase Duration Breakdown", - "type": "barchart", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=~\"consensus.phase.*\"} | avg(duration) by (name)" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 } - }, - { - "title": "Proposers per Round", - "type": "stat", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | avg(span.xrpl.consensus.proposers)" - } - ], - "gridPos": { "h": 4, "w": 6, "x": 0, "y": 8 } - }, - { - "title": "Recent Slow Rounds (>5s)", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | duration > 5s" - } - ], - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 12 } - } - ] -} -``` +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`. + +The underlying TraceQL queries are listed in section 7.7.3 and used throughout this doc. ### 7.6.2 Node Overview Dashboard -```json -{ - "title": "xrpld Node Overview", - "uid": "xrpld-node-overview", - "panels": [ - { - "title": "Active Nodes", - "type": "stat", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\"} | count_over_time() by (resource.service.instance.id) | count()" - } - ], - "gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 } - }, - { - "title": "Total Transactions (1h)", - "type": "stat", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.receive\"} | count()" - } - ], - "gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 } - }, - { - "title": "Error Rate", - "type": "gauge", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && status.code=error} | rate() / {resource.service.name=\"xrpld\"} | rate() * 100" - } - ], - "fieldConfig": { - "defaults": { - "unit": "percent", - "max": 10, - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 1 }, - { "color": "red", "value": 5 } - ] - } - } - }, - "gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 } - }, - { - "title": "Service Map", - "type": "nodeGraph", - "datasource": "Tempo", - "gridPos": { "h": 12, "w": 12, "x": 12, "y": 0 } - } - ] -} -``` +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.code=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 -```yaml -# grafana/provisioning/alerting/rippled-alerts.yaml -apiVersion: 1 +Grafana provisions three TraceQL-based alert rules (group `xrpld-tracing-alerts`, evaluated every 1m) against the Tempo datasource: -groups: - - name: xrpld-tracing-alerts - folder: xrpld - interval: 1m - rules: - - uid: consensus-slow - title: Consensus Round Slow - condition: A - data: - - refId: A - datasourceUid: tempo - model: - queryType: traceql - query: '{resource.service.name="xrpld" && name="consensus.round"} | avg(duration) > 5s' - # Note: Verify TraceQL aggregate queries are supported by your - # Tempo version. Aggregate alerting (e.g., avg(duration)) requires - # Tempo 2.3+ with TraceQL metrics enabled. - for: 5m - annotations: - summary: Consensus rounds taking >5 seconds - description: "Consensus duration: {{ $value }}ms" - labels: - severity: warning +- **Consensus Round Slow** (warning, `for: 5m`): fires when average `consensus.round` duration exceeds 5s. - - uid: rpc-error-spike - title: RPC Error Rate Spike - condition: B - data: - - refId: B - datasourceUid: tempo - model: - queryType: traceql - query: '{resource.service.name="xrpld" && name=~"rpc.command.*" && status.code=error} | rate() > 0.05' - # Note: Verify TraceQL aggregate queries are supported by your - # Tempo version. Aggregate alerting (e.g., rate()) requires - # Tempo 2.3+ with TraceQL metrics enabled. - for: 2m - annotations: - summary: RPC error rate >5% - labels: - severity: critical + ``` + {resource.service.name="xrpld" && name="consensus.round"} | avg(duration) > 5s + ``` - - uid: tx-throughput-drop - title: Transaction Throughput Drop - condition: C - data: - - refId: C - datasourceUid: tempo - model: - queryType: traceql - query: '{resource.service.name="xrpld" && name="tx.receive"} | rate() < 10' - for: 10m - annotations: - summary: Transaction throughput below threshold - labels: - severity: warning -``` +- **RPC Error Rate Spike** (critical, `for: 2m`): fires when the error rate across `rpc.command.*` spans exceeds 5%. + + ``` + {resource.service.name="xrpld" && name=~"rpc.command.*" && status.code=error} | rate() > 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 first two 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. --- @@ -503,18 +345,18 @@ flowchart TB - **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`, `xrpl.tx.hash`, `ledger_seq`), enabling a single-pane debugging experience. +- **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** | `xrpl.tx.hash` | Logs, Metrics | Find TX-related data | -| **Trace** | `xrpl.consensus.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 | +| 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 @@ -522,7 +364,7 @@ flowchart TB ``` # In Grafana Explore with Tempo -{resource.service.name="xrpld" && span.xrpl.tx.hash="ABC123..."} +{resource.service.name="xrpld" && span.tx_hash="ABC123..."} ``` **Step 2: Get the trace_id from the trace view** @@ -548,93 +390,14 @@ rate(xrpld_tx_applied_total[1m]) ### 7.7.4 Unified Dashboard Example -```json -{ - "title": "xrpld Unified Observability", - "uid": "xrpld-unified", - "panels": [ - { - "title": "Transaction Latency (Traces)", - "type": "timeseries", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.receive\"} | histogram_over_time(duration)" - } - ], - "gridPos": { "h": 6, "w": 8, "x": 0, "y": 0 } - }, - { - "title": "Transaction Rate (Metrics)", - "type": "timeseries", - "datasource": "Prometheus", - "targets": [ - { - "expr": "rate(xrpld_tx_received_total[5m])", - "legendFormat": "{{ instance }}" - } - ], - "fieldConfig": { - "defaults": { - "links": [ - { - "title": "View traces", - "url": "/explore?left={\"datasource\":\"Tempo\",\"query\":\"{resource.service.name=\\\"xrpld\\\" && name=\\\"tx.receive\\\"}\"}" - } - ] - } - }, - "gridPos": { "h": 6, "w": 8, "x": 8, "y": 0 } - }, - { - "title": "Recent Logs", - "type": "logs", - "datasource": "Loki", - "targets": [ - { - "expr": "{job=\"xrpld\"} | json" - } - ], - "gridPos": { "h": 6, "w": 8, "x": 16, "y": 0 } - }, - { - "title": "Trace Search", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\"}" - } - ], - "fieldConfig": { - "overrides": [ - { - "matcher": { "id": "byName", "options": "traceID" }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "View trace", - "url": "/explore?left={\"datasource\":\"Tempo\",\"query\":\"${__value.raw}\"}" - }, - { - "title": "View logs", - "url": "/explore?left={\"datasource\":\"Loki\",\"query\":\"{job=\\\"xrpld\\\"} |= \\\"${__value.raw}\\\"\"}" - } - ] - } - ] - } - ] - }, - "gridPos": { "h": 12, "w": 24, "x": 0, "y": 6 } - } - ] -} -``` +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"} |= ""`). + +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. --- diff --git a/OpenTelemetryPlan/08-appendix.md b/OpenTelemetryPlan/08-appendix.md index ad281dba99..204098764b 100644 --- a/OpenTelemetryPlan/08-appendix.md +++ b/OpenTelemetryPlan/08-appendix.md @@ -177,20 +177,17 @@ flowchart TB | [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 | -| [04-code-samples.md](./04-code-samples.md) | C++ code examples for all components | | [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 | | [secure-OTel.md](./secure-OTel.md) | Threat model and hardening (mTLS, peer validation) | | [09-data-collection-reference.md](./09-data-collection-reference.md) | Span/metric/dashboard inventory | -| [presentation.md](./presentation.md) | Slide deck for OTel plan overview | ### Task Lists | Document | Description | | -------------------------------------------------------------------------- | --------------------------------------------------- | -| [POC_taskList.md](./POC_taskList.md) | Proof-of-concept telemetry integration | | [Phase2_taskList.md](./Phase2_taskList.md) | RPC layer trace instrumentation | | [Phase3_taskList.md](./Phase3_taskList.md) | Peer overlay & consensus tracing | | [Phase4_taskList.md](./Phase4_taskList.md) | Transaction lifecycle tracing | diff --git a/OpenTelemetryPlan/OpenTelemetryPlan.md b/OpenTelemetryPlan/OpenTelemetryPlan.md index 3a221da573..dde94c5760 100644 --- a/OpenTelemetryPlan/OpenTelemetryPlan.md +++ b/OpenTelemetryPlan/OpenTelemetryPlan.md @@ -46,7 +46,6 @@ flowchart TB subgraph impl["Implementation"] strategy["03-implementation-strategy.md"] - code["04-code-samples.md"] config["05-configuration-reference.md"] end @@ -55,7 +54,6 @@ flowchart TB backends["07-observability-backends.md"] appendix["08-appendix.md"] secure["secure-OTel.md"] - poc["POC_taskList.md"] dataref["09-data-collection-reference.md"] end @@ -67,13 +65,11 @@ flowchart TB fund --> arch arch --> design design --> strategy - strategy --> code - code --> config + strategy --> config config --> phases phases --> backends backends --> appendix backends --> secure - phases --> poc appendix --> dataref style overview fill:#1b5e20,stroke:#0d3d14,color:#fff,stroke-width:2px @@ -85,13 +81,11 @@ flowchart TB 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 code 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 style secure fill:#4a148c,stroke:#2e0d57,color:#fff - style poc fill:#4a148c,stroke:#2e0d57,color:#fff style dataref fill:#4a148c,stroke:#2e0d57,color:#fff ``` @@ -107,14 +101,12 @@ flowchart TB | **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 | -| **4** | [Code Samples](./04-code-samples.md) | C++ implementation examples for core infrastructure and key modules | | **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 | | **9** | [Data Collection Reference](./09-data-collection-reference.md) | Complete inventory of spans, attributes, metrics, and dashboards | | **Sec** | [Securing the OTel Pipeline](./secure-OTel.md) | Threat model and hardening (mTLS, peer trace-context validation) | -| **POC** | [POC Task List](./POC_taskList.md) | Proof of concept tasks for RPC tracing end-to-end demo | --- @@ -162,22 +154,6 @@ Performance optimization strategies include head sampling fixed at 100% (intenti --- -## 4. Code Samples - -C++ implementation examples are provided for the core telemetry infrastructure and key modules: - -- `Telemetry.h` - Core interface for tracer access and span creation -- `SpanGuard.h` - RAII wrapper for automatic span lifecycle management with `discard()` support -- `DiscardFlag.h` - Thread-local flag for span discard signaling between SpanGuard and FilteringSpanProcessor -- `SpanGuard.cpp` - Pimpl implementation confining all OTel SDK types -- Protocol Buffer extensions for trace context propagation -- Module-specific instrumentation (RPC, Consensus, P2P, JobQueue) -- Remaining modules (PathFinding, TxQ, Validator, etc.) follow the same patterns - -➡️ **[View all Code Samples](./04-code-samples.md)** - ---- - ## 5. Configuration Reference > **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring @@ -244,12 +220,4 @@ Threat model and hardening guidance for production deployments where xrpld nodes --- -## POC Task List - -A step-by-step task list for building a minimal end-to-end proof of concept that demonstrates distributed tracing in xrpld. The POC scope is limited to RPC tracing — showing request traces flowing from xrpld through an OpenTelemetry Collector into Tempo, viewable in Grafana. - -➡️ **[View POC Task List](./POC_taskList.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._ diff --git a/OpenTelemetryPlan/POC_taskList.md b/OpenTelemetryPlan/POC_taskList.md deleted file mode 100644 index 112c0359fe..0000000000 --- a/OpenTelemetryPlan/POC_taskList.md +++ /dev/null @@ -1,628 +0,0 @@ -# OpenTelemetry POC Task List - -> **Goal**: Build a minimal end-to-end proof of concept that demonstrates distributed tracing in xrpld. A successful POC will show RPC request traces flowing from xrpld through an OTel Collector into Tempo, viewable in Grafana. -> -> **Scope**: RPC tracing only (highest value, lowest risk per the [CRAWL phase](./06-implementation-phases.md#6102-quick-wins-immediate-value) in the implementation phases). No cross-node P2P context propagation or consensus tracing in the POC. - -### Related Plan Documents - -| Document | Relevance to POC | -| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [00-tracing-fundamentals.md](./00-tracing-fundamentals.md) | Core concepts: traces, spans, context propagation, sampling | -| [01-architecture-analysis.md](./01-architecture-analysis.md) | RPC request flow (§1.5), key trace points (§1.6), instrumentation priority (§1.7) | -| [02-design-decisions.md](./02-design-decisions.md) | SDK selection (§2.1), exporter config (§2.2), span naming (§2.3), attribute schema (§2.4), coexistence with PerfLog/Insight (§2.6) | -| [03-implementation-strategy.md](./03-implementation-strategy.md) | Directory structure (§3.1), key principles (§3.2), performance overhead (§3.3-3.6), conditional compilation (§3.7.3), code intrusiveness (§3.9) | -| [04-code-samples.md](./04-code-samples.md) | Telemetry interface (§4.1), SpanGuard factory methods (§4.2-4.3), RPC instrumentation (§4.5.3) | -| [05-configuration-reference.md](./05-configuration-reference.md) | xrpld config (§5.1), config parser (§5.2), Application integration (§5.3), CMake (§5.4), Collector config (§5.5), Docker Compose (§5.6), Grafana (§5.8) | -| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 1 core tasks (§6.2), Phase 2 RPC tasks (§6.3), quick wins (§6.10), definition of done (§6.11) | -| [07-observability-backends.md](./07-observability-backends.md) | Tempo dev setup (§7.1), Grafana dashboards (§7.6), alert rules (§7.6.3) | - ---- - -## Task 0: Docker Observability Stack Setup - -> **OTLP** = OpenTelemetry Protocol - -**Objective**: Stand up the backend infrastructure to receive, store, and display traces. - -**What to do**: - -- Create `docker/telemetry/docker-compose.yml` in the repo with three services: - 1. **OpenTelemetry Collector** (`otel/opentelemetry-collector-contrib:0.92.0`) - - Expose ports `4317` (OTLP gRPC) and `4318` (OTLP HTTP) - - Expose port `13133` (health check) - - Mount a config file `docker/telemetry/otel-collector-config.yaml` - 2. **Tempo** (`grafana/tempo:2.6.1`) - - Expose port `3200` (HTTP API) and `4317` (OTLP gRPC, internal) - 3. **Grafana** (`grafana/grafana:latest`) — optional but useful - - Expose port `3000` - - Enable anonymous admin access for local dev (`GF_AUTH_ANONYMOUS_ENABLED=true`, `GF_AUTH_ANONYMOUS_ORG_ROLE=Admin`) - - Provision Tempo as a data source via `docker/telemetry/grafana/provisioning/datasources/tempo.yaml` - -- Create `docker/telemetry/otel-collector-config.yaml`: - - ```yaml - 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 - - exporters: - logging: - verbosity: detailed - otlp/tempo: - endpoint: tempo:4317 - tls: - insecure: true - - service: - pipelines: - traces: - receivers: [otlp] - processors: [batch] - exporters: [logging, otlp/tempo] - ``` - -- Create Grafana Tempo datasource provisioning file at `docker/telemetry/grafana/provisioning/datasources/tempo.yaml`: - ```yaml - apiVersion: 1 - datasources: - - name: Tempo - type: tempo - access: proxy - url: http://tempo:3200 - ``` - -**Verification**: Run `docker compose -f docker/telemetry/docker-compose.yml up -d`, then: - -- `curl http://localhost:13133` returns healthy (Collector) -- `http://localhost:3000` opens Grafana (Tempo datasource available, no traces yet) - -**Reference**: - -- [05-configuration-reference.md §5.5](./05-configuration-reference.md) — Collector config (dev YAML with Tempo exporter) -- [05-configuration-reference.md §5.6](./05-configuration-reference.md) — Docker Compose development environment -- [07-observability-backends.md §7.1](./07-observability-backends.md) — Tempo quick start and backend selection -- [05-configuration-reference.md §5.8](./05-configuration-reference.md) — Grafana datasource provisioning and dashboards - ---- - -## Task 1: Add OpenTelemetry C++ SDK Dependency - -**Objective**: Make `opentelemetry-cpp` available to the build system. - -**What to do**: - -- Edit `conanfile.py` to add `opentelemetry-cpp` as an **optional** dependency. The gRPC otel plugin flag (`"grpc/*:otel_plugin": False`) in the existing conanfile may need to remain false — we pull the OTel SDK separately. - - Add a Conan option: `with_telemetry = [True, False]` defaulting to `False` - - When `with_telemetry` is `True`, add `opentelemetry-cpp` to `self.requires()` - - Required OTel Conan components: `opentelemetry-cpp` (which bundles api, sdk, and exporters). If the package isn't in Conan Center, consider using `FetchContent` in CMake or building from source as a fallback. -- Edit `CMakeLists.txt`: - - Add option: `option(XRPL_ENABLE_TELEMETRY "Enable OpenTelemetry tracing" OFF)` - - When ON, `find_package(opentelemetry-cpp CONFIG REQUIRED)` and add compile definition `XRPL_ENABLE_TELEMETRY` - - When OFF, do nothing (zero build impact) -- Verify the build succeeds with `-DXRPL_ENABLE_TELEMETRY=OFF` (no regressions) and with `-DXRPL_ENABLE_TELEMETRY=ON` (SDK links successfully). - -**Key files**: - -- `conanfile.py` -- `CMakeLists.txt` - -**Reference**: - -- [05-configuration-reference.md §5.4](./05-configuration-reference.md) — CMake integration, `FindOpenTelemetry.cmake`, `XRPL_ENABLE_TELEMETRY` option -- [03-implementation-strategy.md §3.2](./03-implementation-strategy.md) — Key principle: zero-cost when disabled via compile-time flags -- [02-design-decisions.md §2.1](./02-design-decisions.md) — SDK selection rationale and required OTel components - ---- - -## Task 2: Create Core Telemetry Interface and NullTelemetry - -**Objective**: Define the `Telemetry` abstract interface and a no-op implementation so the rest of the codebase can reference telemetry without hard-depending on the OTel SDK. - -**What to do**: - -- Create `include/xrpl/telemetry/Telemetry.h`: - - Define `namespace xrpl::telemetry` - - Define `struct Telemetry::Setup` holding: `enabled`, `exporterEndpoint`, `samplingRatio`, `serviceName`, `serviceVersion`, `serviceInstanceId`, `traceRpc`, `traceTransactions`, `traceConsensus`, `tracePeer` - - Define abstract `class Telemetry` with: - - `virtual void start() = 0;` - - `virtual void stop() = 0;` - - `virtual bool isEnabled() const = 0;` - - `virtual nostd::shared_ptr getTracer(string_view name = "xrpld") = 0;` - - `virtual nostd::shared_ptr startSpan(string_view name, SpanKind kind = kInternal) = 0;` - - `virtual nostd::shared_ptr startSpan(string_view name, Context const& parentContext, SpanKind kind = kInternal) = 0;` - - `virtual bool shouldTraceRpc() const = 0;` - - `virtual bool shouldTraceTransactions() const = 0;` - - `virtual bool shouldTraceConsensus() const = 0;` - - Factory: `std::unique_ptr makeTelemetry(Setup const&, beast::Journal);` - - Config parser: `Telemetry::Setup setupTelemetry(Section const&, std::string const& nodePublicKey, std::string const& version);` - -- Create `include/xrpl/telemetry/SpanGuard.h`: - - RAII guard with static factory methods (`rpcSpan()`, `txSpan()`, `consensusSpan()`, etc.) that access the global `Telemetry::getInstance()` singleton internally. - - Uses pimpl idiom to hide all OTel types -- the public header has zero `opentelemetry/` includes. - - Convenience instance methods: `setAttribute()`, `setOk()`, `setStatus()`, `addEvent()`, `recordException()`, `context()`, `discard()` - - When `XRPL_ENABLE_TELEMETRY` is not defined, the entire class compiles to a no-op stub. - - See [04-code-samples.md](./04-code-samples.md) §4.2-4.3 for the full API reference. - -- Create `src/libxrpl/telemetry/NullTelemetry.cpp`: - - Implements `Telemetry` with all no-ops. - - `isEnabled()` returns `false`, `startSpan()` returns a noop span. - - This is used when `XRPL_ENABLE_TELEMETRY` is OFF or `enabled=0` in config. - -- Guard all OTel SDK headers behind `#ifdef XRPL_ENABLE_TELEMETRY`. The `NullTelemetry` implementation should compile without the OTel SDK present. - -**Key new files**: - -- `include/xrpl/telemetry/Telemetry.h` -- `include/xrpl/telemetry/SpanGuard.h` -- `src/libxrpl/telemetry/NullTelemetry.cpp` - -**Reference**: - -- [04-code-samples.md §4.1](./04-code-samples.md) — Full `Telemetry` interface with `Setup` struct, lifecycle, tracer access, span creation, and component filtering methods -- [04-code-samples.md §4.2-4.3](./04-code-samples.md) — SpanGuard with factory methods, pimpl design, no-op stub, and discard support -- [03-implementation-strategy.md §3.1](./03-implementation-strategy.md) — Directory structure: `include/xrpl/telemetry/` for headers, `src/libxrpl/telemetry/` for implementation -- [03-implementation-strategy.md §3.7.3](./03-implementation-strategy.md) — Conditional instrumentation and zero-cost compile-time disabled pattern - ---- - -## Task 3: Implement OTel-Backed Telemetry - -> **OTLP** = OpenTelemetry Protocol - -**Objective**: Implement the real `Telemetry` class that initializes the OTel SDK, configures the OTLP exporter and batch processor, and creates tracers/spans. - -**What to do**: - -- Create `src/libxrpl/telemetry/Telemetry.cpp` (compiled only when `XRPL_ENABLE_TELEMETRY=ON`): - - `class TelemetryImpl : public Telemetry` that: - - In `start()`: creates a `TracerProvider` with: - - Resource attributes: `service.name`, `service.version`, `service.instance.id` - - An `OtlpHttpExporter` pointed at `setup.exporterEndpoint` (default `localhost:4318`) - - A `BatchSpanProcessor` with configurable batch size and delay - - A `TraceIdRatioBasedSampler` using `setup.samplingRatio` - - Sets the global `TracerProvider` - - In `stop()`: calls `ForceFlush()` then shuts down the provider - - In `startSpan()`: delegates to `getTracer()->StartSpan(name, ...)` - - `shouldTraceRpc()` etc. read from `Setup` fields - -- Create `src/libxrpl/telemetry/TelemetryConfig.cpp`: - - `setupTelemetry()` parses the `[telemetry]` config section from `xrpld.cfg` - - Maps config keys: `enabled`, `exporter`, `endpoint`, `sampling_ratio`, `trace_rpc`, `trace_transactions`, `trace_consensus`, `trace_peer` - -- Wire `makeTelemetry()` factory: - - If `setup.enabled` is true AND `XRPL_ENABLE_TELEMETRY` is defined: return `TelemetryImpl` - - Otherwise: return `NullTelemetry` - -- Add telemetry source files to CMake. When `XRPL_ENABLE_TELEMETRY=ON`, compile `Telemetry.cpp` and `TelemetryConfig.cpp` and link against `opentelemetry-cpp::api`, `opentelemetry-cpp::sdk`, `opentelemetry-cpp::otlp_grpc_exporter`. When OFF, compile only `NullTelemetry.cpp`. - -**Key new files**: - -- `src/libxrpl/telemetry/Telemetry.cpp` -- `src/libxrpl/telemetry/TelemetryConfig.cpp` - -**Key modified files**: - -- `CMakeLists.txt` (add telemetry library target) - -**Reference**: - -- [04-code-samples.md §4.1](./04-code-samples.md) — `Telemetry` interface that `TelemetryImpl` must implement -- [05-configuration-reference.md §5.2](./05-configuration-reference.md) — `setupTelemetry()` config parser implementation -- [02-design-decisions.md §2.2](./02-design-decisions.md) — OTLP/gRPC exporter config (endpoint, TLS options) -- [02-design-decisions.md §2.4.1](./02-design-decisions.md) — Resource attributes: `service.name`, `service.version`, `service.instance.id`, `xrpl.network.id` -- [03-implementation-strategy.md §3.4](./03-implementation-strategy.md) — Per-operation CPU costs and overhead budget for span creation -- [03-implementation-strategy.md §3.5](./03-implementation-strategy.md) — Memory overhead: static (~456 KB) and dynamic (~1.2 MB) budgets - ---- - -## Task 4: Integrate Telemetry into Application Lifecycle - -**Objective**: Wire the `Telemetry` object into the `ServiceRegistry` / `Application` so all components can access it. - -**What to do**: - -- Edit `include/xrpl/core/ServiceRegistry.h`: - - Forward-declare `namespace telemetry { class Telemetry; }` inside `namespace xrpl` - - Add pure virtual method: `virtual telemetry::Telemetry& getTelemetry() = 0;` - - (`Application` extends `ServiceRegistry`, so this is automatically available on `Application` too) - -- Edit `src/xrpld/app/main/Application.cpp` (the `ApplicationImp` class): - - Add member: `std::unique_ptr telemetry_;` - - In the member initializer list, construct telemetry with an empty - `serviceInstanceId` (node identity is not yet known): - ```cpp - , telemetry_( - telemetry::makeTelemetry( - telemetry::setupTelemetry( - config_->section("telemetry"), - "", // Updated later via setServiceInstanceId() - BuildInfo::getVersionString()), - logs_->journal("Telemetry"))) - ``` - - In `setup()`, after `nodeIdentity_` is resolved, inject the node - public key as the service instance ID: - ```cpp - if (!config_->section("telemetry").exists("service_instance_id")) - telemetry_->setServiceInstanceId( - toBase58(TokenType::NodePublic, nodeIdentity_->first)); - ``` - - In `start()`: call `telemetry_->start()` - - In `run()` (shutdown path): call `telemetry_->stop()` (to flush pending spans) - - Implement `getTelemetry()` override: return `*telemetry_` - -- Add `[telemetry]` section to the example config `cfg/xrpld-example.cfg`: - ```ini - # [telemetry] - # enabled=1 - # endpoint=http://localhost:4318/v1/traces - # sampling_ratio=1.0 - # trace_rpc=1 - ``` - -> **Access patterns**: Components holding `ServiceRegistry&` (e.g. -> `NetworkOPsImp`) call `registry_.get().getTelemetry()`. Components -> holding `Application&` (e.g. `ServerHandler`, `PeerImp`, -> `RCLConsensusAdaptor`) call `app_.getTelemetry()` directly. Both -> resolve to the same `Telemetry` instance. - -**Key modified files**: - -- `include/xrpl/core/ServiceRegistry.h` -- `src/xrpld/app/main/Application.cpp` -- `cfg/xrpld-example.cfg` (example config) - -**Reference**: - -- [05-configuration-reference.md §5.3](./05-configuration-reference.md) — `ApplicationImp` changes: member declaration, constructor init, `start()`/`stop()` wiring, `getTelemetry()` override -- [05-configuration-reference.md §5.1](./05-configuration-reference.md) — `[telemetry]` config section format and all option defaults -- [03-implementation-strategy.md §3.9.2](./03-implementation-strategy.md) — File impact assessment: `Application.cpp` ~15 lines added, ~3 changed (Low risk) - ---- - -## Task 5: Add SpanGuard Factory Methods - -**Objective**: Add static factory methods to SpanGuard that provide type-safe, one-liner instrumentation and compile to zero-cost no-ops when telemetry is disabled. This replaces the earlier macro-based approach (`TracingInstrumentation.h` has been removed). - -**What to do**: - -- Update `include/xrpl/telemetry/SpanGuard.h`: - - Add static factory methods that access the global `Telemetry::getInstance()` singleton and check the relevant component filter before creating a span: - - ```cpp - // Each factory checks the global Telemetry instance internally. - // No Telemetry& reference needed at the call site. - auto span = telemetry::SpanGuard::rpcSpan("rpc.request"); - span.setAttribute("command", command); - span.setAttribute("rpc_status", status); - ``` - - - Factory methods: `rpcSpan()`, `txSpan()`, `consensusSpan()`, `peerSpan()`, `ledgerSpan()`, `span()` - - Use the pimpl idiom to hide all OTel types from the public header (zero `opentelemetry/` includes) - - When `XRPL_ENABLE_TELEMETRY` is NOT defined, the entire class compiles to a no-op stub with empty inline method bodies - -- No separate `TracingInstrumentation.h` file is needed. All instrumentation call sites use `#include ` directly. - -**Key modified file**: - -- `include/xrpl/telemetry/SpanGuard.h` - -**Reference**: - -- [04-code-samples.md §4.3](./04-code-samples.md) — SpanGuard API reference: factory methods, usage patterns, compile-time disabled behavior, and discard support -- [03-implementation-strategy.md §3.7.3](./03-implementation-strategy.md) — Conditional instrumentation pattern: factory methods handle compile-time and runtime checks internally -- [03-implementation-strategy.md §3.9.7](./03-implementation-strategy.md) — Before/after code examples showing minimal intrusiveness (~1-3 lines per instrumentation point) - ---- - -## Task 6: Instrument RPC ServerHandler - -> **WS** = WebSocket - -**Objective**: Add tracing to the HTTP RPC entry point so every incoming RPC request creates a span. - -**What to do**: - -- Edit `src/xrpld/rpc/detail/ServerHandler.cpp`: - - `#include ` - - In `ServerHandler::onRequest(Session& session)`: - - At the top of the method, add: `auto span = telemetry::SpanGuard::rpcSpan("rpc.request");` - - After the RPC command name is extracted, set attribute: `span.setAttribute("command", command);` - - After the response status is known, set: `span.setAttribute("http.status_code", static_cast(statusCode));` - - Wrap error paths with: `span.recordException(e);` - - In `ServerHandler::processRequest(...)`: - - Add a child span: `auto span = telemetry::SpanGuard::rpcSpan("rpc.process");` - - Set method attribute: `span.setAttribute("method", request_method);` - - In `ServerHandler::onWSMessage(...)` (WebSocket path): - - Add: `auto span = telemetry::SpanGuard::rpcSpan("rpc.ws.message");` - -- The goal is to see spans like: - ``` - rpc.request - └── rpc.process - ``` - in Tempo/Grafana for every HTTP RPC call. - -**Key modified file**: - -- `src/xrpld/rpc/detail/ServerHandler.cpp` (~15-25 lines added) - -**Reference**: - -- [04-code-samples.md §4.5.3](./04-code-samples.md) — Complete `ServerHandler::onRequest()` instrumented code sample using SpanGuard factory methods -- [01-architecture-analysis.md §1.5](./01-architecture-analysis.md) — RPC request flow diagram: HTTP request -> attributes -> jobqueue.enqueue -> rpc.command -> response -- [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — Key trace points table: `rpc.request` in `ServerHandler.cpp::onRequest()` (Priority: High) -- [02-design-decisions.md §2.3](./02-design-decisions.md) — Span naming convention: `rpc.request`, `rpc.command.*` -- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC span attributes: `command`, `version`, `rpc_role`, `xrpl.rpc.params` -- [03-implementation-strategy.md §3.9.2](./03-implementation-strategy.md) — File impact: `ServerHandler.cpp` ~40 lines added, ~10 changed (Low risk) - ---- - -## Task 7: Instrument RPC Command Execution - -**Objective**: Add per-command tracing inside the RPC handler so each command (e.g., `submit`, `account_info`, `server_info`) gets its own child span. - -**What to do**: - -- Edit `src/xrpld/rpc/detail/RPCHandler.cpp`: - - `#include ` - - In `doCommand(RPC::JsonContext& context, Json::Value& result)`: - - At the top: `auto span = telemetry::SpanGuard::rpcSpan("rpc.command." + context.method);` - - Set attributes: - - `span.setAttribute("command", context.method);` - - `span.setAttribute("version", static_cast(context.apiVersion));` - - `span.setAttribute("rpc_role", (context.role == Role::ADMIN) ? "admin" : "user");` - - On success: `span.setAttribute("rpc_status", "success");` - - On error: `span.setAttribute("rpc_status", "error");` and set the error message - -- After this, traces in Tempo/Grafana should look like: - ``` - rpc.request (command=account_info) - └── rpc.process - └── rpc.command.account_info (version=2, rpc_role=user, rpc_status=success) - ``` - -**Key modified file**: - -- `src/xrpld/rpc/detail/RPCHandler.cpp` (~15-20 lines added) - -**Reference**: - -- [04-code-samples.md §4.5.3](./04-code-samples.md) — `ServerHandler::onRequest()` code sample (includes child span pattern for `rpc.command.*`) -- [02-design-decisions.md §2.3](./02-design-decisions.md) — Span naming: `rpc.command.*` pattern with dynamic command name (e.g., `rpc.command.server_info`) -- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC attribute schema: `command`, `version`, `rpc_role`, `rpc_status` -- [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — Key trace points table: `rpc.command.*` in `RPCHandler.cpp::doCommand()` (Priority: High) -- [02-design-decisions.md §2.6.5](./02-design-decisions.md) — Correlation with PerfLog: how `doCommand()` can link trace_id with existing PerfLog entries -- [03-implementation-strategy.md §3.4.4](./03-implementation-strategy.md) — RPC request overhead budget: ~1.75 μs total per request - ---- - -## Task 8: Build, Run, and Verify End-to-End - -> **OTLP** = OpenTelemetry Protocol - -**Objective**: Prove the full pipeline works: xrpld emits traces -> OTel Collector receives them -> Tempo stores them for Grafana visualization. - -**What to do**: - -1. **Start the Docker stack**: - - ```bash - docker compose -f docker/telemetry/docker-compose.yml up -d - ``` - - Verify Collector health: `curl http://localhost:13133` - -2. **Build xrpld with telemetry**: - - ```bash - # Adjust for your actual build workflow - conan install . --build=missing -o with_telemetry=True - cmake --preset default -DXRPL_ENABLE_TELEMETRY=ON - cmake --build --preset default - ``` - -3. **Configure xrpld**: - Add to `xrpld.cfg` (or your local test config): - - ```ini - [telemetry] - enabled=1 - endpoint=localhost:4317 - sampling_ratio=1.0 - trace_rpc=1 - ``` - -4. **Start xrpld** in standalone mode: - - ```bash - ./rippled --conf xrpld.cfg -a --start - ``` - -5. **Generate RPC traffic**: - - ```bash - # server_info - curl -s -X POST http://localhost:5005 \ - -H "Content-Type: application/json" \ - -d '{"method":"server_info","params":[{}]}' - - # ledger - curl -s -X POST http://localhost:5005 \ - -H "Content-Type: application/json" \ - -d '{"method":"ledger","params":[{"ledger_index":"current"}]}' - - # account_info (will error in standalone, that's fine — we trace errors too) - curl -s -X POST http://localhost:5005 \ - -H "Content-Type: application/json" \ - -d '{"method":"account_info","params":[{"account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"}]}' - ``` - -6. **Verify in Grafana (Tempo)**: - - Open `http://localhost:3000` - - Navigate to Explore → select Tempo datasource - - Search for service `xrpld` - - Confirm you see traces with spans: `rpc.request` -> `rpc.process` -> `rpc.command.server_info` - - Click into a trace and verify attributes: `command`, `rpc_status`, `version` - -7. **Verify zero-overhead when disabled**: - - Rebuild with `XRPL_ENABLE_TELEMETRY=OFF`, or set `enabled=0` in config - - Run the same RPC calls - - Confirm no new traces appear and no errors in xrpld logs - -**Verification Checklist**: - -- [ ] Docker stack starts without errors -- [ ] xrpld builds with `-DXRPL_ENABLE_TELEMETRY=ON` -- [ ] xrpld starts and connects to OTel Collector (check xrpld logs for telemetry messages) -- [ ] Traces appear in Grafana/Tempo under service "xrpld" -- [ ] Span hierarchy is correct (parent-child relationships) -- [ ] Span attributes are populated (`command`, `rpc_status`, etc.) -- [ ] Error spans show error status and message -- [ ] Building with `XRPL_ENABLE_TELEMETRY=OFF` produces no regressions -- [ ] Setting `enabled=0` at runtime produces no traces and no errors - -**Reference**: - -- [06-implementation-phases.md §6.11.1](./06-implementation-phases.md) — Phase 1 definition of done: SDK compiles, runtime toggle works, span creation verified in Tempo, config validation passes -- [06-implementation-phases.md §6.11.2](./06-implementation-phases.md#6112-phase-2-rpc-tracing) — Phase 2 definition of done: 100% RPC coverage, traceparent propagation, <1ms p99 overhead, dashboard deployed -- [06-implementation-phases.md §6.8](./06-implementation-phases.md) — Success metrics: trace coverage >95%, CPU overhead <3%, memory <5 MB, latency impact <2% -- [03-implementation-strategy.md §3.9.5](./03-implementation-strategy.md) — Backward compatibility: config optional, protocol unchanged, `XRPL_ENABLE_TELEMETRY=OFF` produces identical binary -- [01-architecture-analysis.md §1.8](./01-architecture-analysis.md) — Observable outcomes: what traces, metrics, and dashboards to expect - ---- - -## Task 9: Document POC Results and Next Steps - -> **OTLP** = OpenTelemetry Protocol | **WS** = WebSocket - -**Objective**: Capture findings, screenshots, and remaining work for the team. - -**What to do**: - -- Take screenshots of Grafana/Tempo showing: - - The service list with "xrpld" - - A trace with the full span tree - - Span detail view showing attributes -- Document any issues encountered (build issues, SDK quirks, missing attributes) -- Note performance observations (build time impact, any noticeable runtime overhead) -- Write a short summary of what the POC proves and what it doesn't cover yet: - - **Proves**: OTel SDK integrates with xrpld, OTLP export works, RPC traces visible - - **Doesn't cover**: Cross-node P2P context propagation, consensus tracing, protobuf trace context, W3C traceparent header extraction, tail-based sampling, production deployment -- Outline next steps (mapping to the full plan phases): - - [Phase 2](./06-implementation-phases.md) completion: [W3C header extraction](./02-design-decisions.md) (§2.5), WebSocket tracing, all [RPC handlers](./01-architecture-analysis.md) (§1.6) - - [Phase 3](./06-implementation-phases.md): [Protobuf `TraceContext` message](./04-code-samples.md) (§4.4), [transaction relay tracing](./04-code-samples.md) (§4.5.1) across nodes - - [Phase 4](./06-implementation-phases.md): [Consensus round and phase tracing](./04-code-samples.md) (§4.5.2) - - [Phase 5](./06-implementation-phases.md): [Production collector config](./05-configuration-reference.md) (§5.5.2), [Grafana dashboards](./07-observability-backends.md) (§7.6), [alerting](./07-observability-backends.md) (§7.6.3) - -**Reference**: - -- [06-implementation-phases.md §6.1](./06-implementation-phases.md) — Full 5-phase timeline overview and Gantt chart -- [06-implementation-phases.md §6.10](./06-implementation-phases.md) — Crawl-Walk-Run strategy: POC is the CRAWL phase, next steps are WALK and RUN -- [06-implementation-phases.md §6.12](./06-implementation-phases.md) — Recommended implementation order (14 steps across 9 weeks) -- [03-implementation-strategy.md §3.9](./03-implementation-strategy.md) — Code intrusiveness assessment and risk matrix for each remaining component -- [07-observability-backends.md §7.2](./07-observability-backends.md) — Production backend selection (Tempo, Elastic APM, Honeycomb, Datadog) -- [02-design-decisions.md §2.5](./02-design-decisions.md) — Context propagation design: W3C HTTP headers, protobuf P2P, JobQueue internal -- [00-tracing-fundamentals.md](./00-tracing-fundamentals.md) — Reference for team onboarding on distributed tracing concepts - ---- - -## Summary - -| Task | Description | New Files | Modified Files | Depends On | -| ---- | ------------------------------------ | --------- | -------------- | ---------- | -| 0 | Docker observability stack | 4 | 0 | — | -| 1 | OTel C++ SDK dependency | 0 | 2 | — | -| 2 | Core Telemetry interface + NullImpl | 3 | 0 | 1 | -| 3 | OTel-backed Telemetry implementation | 2 | 1 | 1, 2 | -| 4 | Application lifecycle integration | 0 | 3 | 2, 3 | -| 5 | SpanGuard factory methods | 0 | 1 | 2 | -| 6 | Instrument RPC ServerHandler | 0 | 1 | 4, 5 | -| 7 | Instrument RPC command execution | 0 | 1 | 4, 5 | -| 8 | End-to-end verification | 0 | 0 | 0-7 | -| 9 | Document results and next steps | 1 | 0 | 8 | - -**Parallel work**: Tasks 0 and 1 can run in parallel. Tasks 2 and 5 have no dependency on each other. Tasks 6 and 7 can be done in parallel once Tasks 4 and 5 are complete. - ---- - -## Next Steps (Post-POC) - -> **OTLP** = OpenTelemetry Protocol | **WS** = WebSocket - -### Metrics Pipeline for Grafana Dashboards - -The current POC exports **traces only**. Grafana's Explore view can query Tempo for individual traces, but time-series charts (latency histograms, request throughput, error rates) require a **metrics pipeline**. To enable this: - -1. **Add a `spanmetrics` connector** to the OTel Collector config that derives RED metrics (Rate, Errors, Duration) from trace spans automatically: - - ```yaml - connectors: - spanmetrics: - histogram: - explicit: - buckets: [1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 5s] - dimensions: - - name: command - - name: rpc_status - - exporters: - prometheus: - endpoint: 0.0.0.0:8889 - - service: - pipelines: - traces: - receivers: [otlp] - processors: [batch] - exporters: [debug, otlp/tempo, spanmetrics] - metrics: - receivers: [spanmetrics] - exporters: [prometheus] - ``` - -2. **Add Prometheus** to the Docker Compose stack to scrape the collector's metrics endpoint. - -3. **Add Prometheus as a Grafana datasource** and build dashboards for: - - RPC request latency (p50/p95/p99) by command - - RPC throughput (requests/sec) by command - - Error rate by command - - Span duration distribution - -### Additional Instrumentation - -- **W3C `traceparent` header extraction** in `ServerHandler` to support cross-service context propagation from external callers -- **WebSocket RPC tracing** in `ServerHandler::onWSMessage()` -- **Transaction relay tracing** across nodes using protobuf `TraceContext` messages -- **Consensus round and phase tracing** for validator coordination visibility -- **Ledger close tracing** to measure close-to-validated latency - -### Production Hardening - -- **Tail-based sampling** in the OTel Collector to reduce volume while retaining error/slow traces -- **TLS configuration** for the OTLP exporter in production deployments -- **Resource limits** on the batch processor queue to prevent unbounded memory growth -- **Health monitoring** for the telemetry pipeline itself (collector lag, export failures) - -### POC Lessons Learned - -Issues encountered during POC implementation that inform future work: - -| Issue | Resolution | Impact on Future Work | -| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| Conan lockfile rejected `opentelemetry-cpp/1.18.0` | Used `--lockfile=""` to bypass | Lockfile must be regenerated when adding new dependencies | -| Conan package only builds OTLP HTTP exporter, not gRPC | Switched from gRPC to HTTP exporter (`localhost:4318/v1/traces`) | HTTP exporter is the default; gRPC requires custom Conan profile | -| CMake target `opentelemetry-cpp::api` etc. don't exist in Conan package | Use umbrella target `opentelemetry-cpp::opentelemetry-cpp` | Conan targets differ from upstream CMake targets | -| OTel Collector `logging` exporter deprecated | Renamed to `debug` exporter | Use `debug` in all collector configs going forward | -| Macro parameter `telemetry` collided with `::xrpl::telemetry::` namespace | Replaced macros with SpanGuard factory methods (no macros needed) | Factory methods avoid macro hygiene issues entirely | -| `opentelemetry::trace::Scope` creates new context on move | Store scope as member, create once in constructor | SpanGuard move semantics need care with Scope lifecycle | -| `TracerProviderFactory::Create` returns `unique_ptr`, not `nostd::shared_ptr` | Use `std::shared_ptr` member, wrap in `nostd::shared_ptr` for global provider | OTel SDK factory return types don't match API provider types | diff --git a/OpenTelemetryPlan/Phase4_taskList.md b/OpenTelemetryPlan/Phase4_taskList.md index 6d0f169546..9973331651 100644 --- a/OpenTelemetryPlan/Phase4_taskList.md +++ b/OpenTelemetryPlan/Phase4_taskList.md @@ -6,6 +6,13 @@ > > **Branch**: `pratik/otel-phase4-consensus-tracing` (from `pratik/otel-phase3-tx-tracing`) +> **Note on attribute names**: the `xrpl..` keys shown below are +> written in the older dotted form for readability — it mirrors how the fully +> qualified attribute reads in a Tempo trace view. The implemented keys follow +> the convention in [CONTRIBUTING.md](../CONTRIBUTING.md#telemetry-span-attribute-naming) +> (underscore form, e.g. `consensus_round`, `consensus_mode`); the +> `*SpanNames.h` constants are the single source of truth. + ### Related Plan Documents | Document | Relevance | @@ -82,7 +89,7 @@ - In `Adaptor::propose()`: - Creates `consensus.proposal.send` span via `SpanGuard::span()` - - Sets `xrpl.consensus.round` attribute (kept — rule 5) + - Sets `xrpl.consensus.round` attribute - In `PeerImp::onMessage(TMProposeSet)`: - Creates `consensus.proposal.receive` span @@ -815,8 +822,8 @@ and OFF, and don't affect consensus timing. ```cpp // Round-level (on consensus.round) — ALL IMPLEMENTED -"xrpl.consensus.round_id" = int64 // Consensus round number (kept — rule 5) -"xrpl.consensus.ledger_id" = string // previousLedger.id() hash (kept — rule 5) +"xrpl.consensus.round_id" = int64 // Consensus round number +"xrpl.consensus.ledger_id" = string // previousLedger.id() hash "trace_strategy" = string // "deterministic" or "attribute" // Establish-level — IMPLEMENTED diff --git a/OpenTelemetryPlan/Phase5_taskList.md b/OpenTelemetryPlan/Phase5_taskList.md index b573f666a7..ed5dacc305 100644 --- a/OpenTelemetryPlan/Phase5_taskList.md +++ b/OpenTelemetryPlan/Phase5_taskList.md @@ -6,6 +6,15 @@ > > **Branch**: `pratik/otel-phase5-docs-deployment` (from `pratik/otel-phase4-consensus-tracing`) +> **Note on attribute names**: the `xrpl..` keys shown below +> (including the collector spanmetrics dimension examples) are written in the +> older dotted form for readability — it mirrors how the fully qualified +> attribute reads in a Tempo trace view. The implemented keys follow the +> convention in [CONTRIBUTING.md](../CONTRIBUTING.md#telemetry-span-attribute-naming) +> (underscore form, e.g. `command`, `rpc_status`); the `*SpanNames.h` constants +> are the single source of truth, and the real collector dimensions must use +> those exact underscore keys (the CI naming check enforces this). + ### Related Plan Documents | Document | Relevance | diff --git a/OpenTelemetryPlan/presentation.md b/OpenTelemetryPlan/presentation.md index 479aa8fa55..535807a96e 100644 --- a/OpenTelemetryPlan/presentation.md +++ b/OpenTelemetryPlan/presentation.md @@ -618,14 +618,14 @@ flowchart LR ### What Data is Collected -| Category | Attributes Collected | Purpose | -| --------------- | ------------------------------------------------------------------------------------ | --------------------------- | -| **Transaction** | `tx.hash`, `tx.type`, `tx.result`, `tx.fee`, `ledger_index` | Trace transaction lifecycle | -| **Consensus** | `round`, `phase`, `mode`, `proposers` (count of proposing validators), `duration_ms` | Analyze consensus timing | -| **RPC** | `command`, `version`, `status`, `duration_ms` | Monitor RPC performance | -| **Peer** | `peer.id`(public key), `latency_ms`, `message.type`, `message.size` | Network topology analysis | -| **Ledger** | `ledger.hash`, `ledger.index`, `close_time`, `tx_count` | Ledger progression tracking | -| **Job** | `job.type`, `queue_ms`, `worker` | JobQueue performance | +| Category | Attributes Collected | Purpose | +| --------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| **Transaction** | `tx_hash`, `tx_type`, `tx_result`, `tx_fee`, `ledger_index` | Trace transaction lifecycle | +| **Consensus** | `consensus_round`, `consensus_phase`, `consensus_mode`, `proposers` (count of proposing validators), `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_index`, `close_time`, `ledger_tx_count` | Ledger progression tracking | +| **Job** | `job_type`, `job_queue_ms`, `job_worker` | JobQueue performance | ### What is NOT Collected (Privacy Guarantees) @@ -658,13 +658,13 @@ flowchart LR ### Privacy Protection Mechanisms -| Mechanism | Description | -| -------------------------- | ------------------------------------------------------------- | -| **Account Hashing** | `xrpl.tx.account` is hashed at collector level before storage | -| **Configurable Redaction** | Sensitive fields can be excluded via config | -| **Sampling** | Only 10% of traces recorded by default (reduces exposure) | -| **Local Control** | Node operators control what gets exported | -| **No Raw Payloads** | Transaction content is never recorded, only metadata | +| Mechanism | Description | +| -------------------------- | --------------------------------------------------------- | +| **Account Hashing** | `tx_account` is hashed at collector level before storage | +| **Configurable Redaction** | Sensitive fields can be excluded via config | +| **Sampling** | Only 10% of traces recorded by default (reduces exposure) | +| **Local Control** | Node operators control what gets exported | +| **No Raw Payloads** | Transaction content is never recorded, only metadata | > **Key Principle**: Telemetry collects **operational metadata** (timing, counts, hashes) — never **sensitive content** (keys, balances, amounts). diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index d123d3388a..84c6a95853 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -206,7 +206,7 @@ target_link_libraries( add_module(xrpl telemetry) target_link_libraries( xrpl.libxrpl.telemetry - PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.beast + PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.beast xrpl.libxrpl.config ) if(telemetry) target_link_libraries( diff --git a/conan.lock b/conan.lock index f83d3eccc6..ca5927801c 100644 --- a/conan.lock +++ b/conan.lock @@ -16,7 +16,7 @@ "nlohmann_json/3.11.3#45828be26eb619a2e04ca517bb7b828d%1701220705.259", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914", "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492", - "libcurl/8.20.0#465ac276192c197ddc6a9f4494004278%1779353234.048", + "libcurl/8.20.0#c90b0c91a33d9a79b519c1c70bafc823%1780907438.587", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03", "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1778091117.848", "jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228", diff --git a/conan/profiles/sanitizers b/conan/profiles/sanitizers index 4a05fda734..083807ea9e 100644 --- a/conan/profiles/sanitizers +++ b/conan/profiles/sanitizers @@ -52,52 +52,50 @@ include(default) {% endif %} {# Frame pointer required for meaningful stack traces; -O1 for reasonable performance #} -{% set compile_flags = ["-fno-omit-frame-pointer", "-O1"] %} +{% set sanitizer_compiler_flags = ["-fno-omit-frame-pointer", "-O1"] %} {% if compiler == "gcc" %} {# Suppress false positive warnings with GCC #} - {% set _ = compile_flags.append("-Wno-stringop-overflow") %} + {% set _ = sanitizer_compiler_flags.append("-Wno-stringop-overflow") %} {% set relocation_flags = [] %} {% if arch == "x86_64" and enable_asan %} {# Large code model prevents relocation errors in instrumented ASAN binaries #} - {% set _ = compile_flags.append("-mcmodel=large") %} + {% set _ = sanitizer_compiler_flags.append("-mcmodel=large") %} {% set _ = relocation_flags.append("-mcmodel=large") %} {% elif enable_tsan %} {# GCC doesn't support atomic_thread_fence with TSAN; suppress warnings #} - {% set _ = compile_flags.append("-Wno-tsan") %} + {% set _ = sanitizer_compiler_flags.append("-Wno-tsan") %} {% if arch == "x86_64" %} {# Medium code model for TSAN; large is incompatible #} - {% set _ = compile_flags.append("-mcmodel=medium") %} + {% set _ = sanitizer_compiler_flags.append("-mcmodel=medium") %} {% set _ = relocation_flags.append("-mcmodel=medium") %} {% endif %} {% endif %} {% set fsanitize = "-fsanitize=" ~ ",".join(sanitizer_types) %} - {% set _ = compile_flags.append(fsanitize) %} + {% set _ = sanitizer_compiler_flags.append(fsanitize) %} {% set _ = relocation_flags.append(fsanitize) %} - {% set sanitizer_compiler_flags = " ".join(compile_flags) %} - {% set sanitizer_linker_flags = " ".join(relocation_flags) %} + {% set sanitizer_linker_flags = relocation_flags %} {% elif compiler == "clang" or compiler == "apple-clang" %} {% set fsanitize = "-fsanitize=" ~ ",".join(sanitizer_types) %} - {% set _ = compile_flags.append(fsanitize) %} + {% set _ = sanitizer_compiler_flags.append(fsanitize) %} - {% set sanitizer_compiler_flags = " ".join(compile_flags) %} - {% set sanitizer_linker_flags = fsanitize %} + {% set sanitizer_linker_flags = [fsanitize] %} {% endif %} [conf] tools.build:defines+={{defines}} -tools.build:cxxflags+=['{{sanitizer_compiler_flags}}'] -tools.build:sharedlinkflags+=['{{sanitizer_linker_flags}}'] -tools.build:exelinkflags+=['{{sanitizer_linker_flags}}'] +tools.build:cxxflags+={{sanitizer_compiler_flags}} +tools.build:sharedlinkflags+={{sanitizer_linker_flags}} +tools.build:exelinkflags+={{sanitizer_linker_flags}} tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags", "tools.build:defines"] # &: means "apply only to the consumer/root package" -&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{sanitizers}}", "SANITIZERS_COMPILER_FLAGS": "{{sanitizer_compiler_flags}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags}}"} +&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{sanitizers}}", "SANITIZERS_COMPILER_FLAGS": "{{sanitizer_compiler_flags | join(' ')}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags | join(' ')}}"} [options] {% if enable_asan %} diff --git a/cspell.config.yaml b/cspell.config.yaml index f7b1a7e987..8e5cb10ef4 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -85,6 +85,7 @@ words: - coro - coros - cowid + - cpack - cryptocondition - cryptoconditional - cryptoconditions diff --git a/docker/telemetry/grafana/provisioning/datasources/tempo.yaml b/docker/telemetry/grafana/provisioning/datasources/tempo.yaml index 8c65e82fee..e98e83cacf 100644 --- a/docker/telemetry/grafana/provisioning/datasources/tempo.yaml +++ b/docker/telemetry/grafana/provisioning/datasources/tempo.yaml @@ -97,7 +97,7 @@ datasources: tag: command operator: "=" scope: span - type: static + type: dynamic - id: rpc-status tag: rpc_status operator: "=" @@ -126,17 +126,17 @@ datasources: type: dynamic # Phase 4: Consensus tracing filters - id: consensus-mode - tag: xrpl.consensus.mode + tag: consensus_mode operator: "=" scope: span type: static - id: consensus-round - tag: xrpl.consensus.round + tag: consensus_round operator: "=" scope: span type: dynamic - id: consensus-ledger-seq - tag: xrpl.ledger.seq + tag: ledger_seq operator: "=" scope: span type: static diff --git a/flake.lock b/flake.lock index 2013cfabd4..f8553af703 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1780243769, - "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=", + "lastModified": 1780749050, + "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c", + "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", "type": "github" }, "original": { diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index ced84c4c87..a83c8bfa84 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -65,7 +65,7 @@ invalidAMMAssetPair( std::optional ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot); -/** Return true if required AMM amendments are enabled +/** Return true if required AMM amendment is enabled */ bool ammEnabled(Rules const&); diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index 9e0fbe38eb..b057f1c245 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -179,36 +178,4 @@ to_string(IOUAmount const& amount); IOUAmount mulRatio(IOUAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp); -// Since many uses of the number class do not have access to a ledger, -// getSTNumberSwitchover needs to be globally accessible. - -bool -getSTNumberSwitchover(); - -void -setSTNumberSwitchover(bool v); - -/** RAII class to set and restore the Number switchover. - */ - -class NumberSO -{ - bool saved_; - -public: - ~NumberSO() - { - setSTNumberSwitchover(saved_); - } - - NumberSO(NumberSO const&) = delete; - NumberSO& - operator=(NumberSO const&) = delete; - - explicit NumberSO(bool v) : saved_(getSTNumberSwitchover()) - { - setSTNumberSwitchover(v); - } -}; - } // namespace xrpl diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 9c17ff2391..47b20756db 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -122,7 +122,6 @@ private: std::optional saved_; }; -class NumberSO; class NumberMantissaScaleGuard; bool @@ -131,7 +130,6 @@ useRulesGuards(Rules const& rules); void createGuards( Rules const& rules, - std::optional& stNumberSO, std::optional& rulesGuard, std::optional& mantissaScaleGuard); diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index c99c1e5ce8..d3500ab144 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes) @@ -64,7 +65,6 @@ XRPL_FIX (DisallowIncomingV1, Supported::Yes, VoteBehavior::DefaultNo XRPL_FEATURE(XChainBridge, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(AMM, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Clawback, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FIX (UniversalNumber, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(XRPFees, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (RemoveNFTokenAutoTrustLine, Supported::Yes, VoteBehavior::DefaultYes) @@ -112,6 +112,7 @@ XRPL_RETIRE_FIX(RmSmallIncreasedQOffers) XRPL_RETIRE_FIX(STAmountCanonicalize) XRPL_RETIRE_FIX(TakerDryOfferRemoval) XRPL_RETIRE_FIX(TrustLinesToSelf) +XRPL_RETIRE_FIX(UniversalNumber) XRPL_RETIRE_FEATURE(Checks) XRPL_RETIRE_FEATURE(CheckCashMakesTrustLine) diff --git a/include/xrpl/telemetry/SpanGuard.h b/include/xrpl/telemetry/SpanGuard.h index ea4dde41d8..bd3e705daa 100644 --- a/include/xrpl/telemetry/SpanGuard.h +++ b/include/xrpl/telemetry/SpanGuard.h @@ -56,8 +56,8 @@ using namespace xrpl::telemetry; auto span = SpanGuard::span( - TraceCategory::Rpc, rpc_span::prefix::command, "submit"); - span.setAttribute(rpc_span::attr::command, "submit"); + TraceCategory::Rpc, rpc_span::prefix::command, commandName); + span.setAttribute(rpc_span::attr::command, commandName); span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::success); // span ended automatically on scope exit @endcode @@ -65,7 +65,7 @@ 2. Error recording: @code auto span = SpanGuard::span( - TraceCategory::Rpc, rpc_span::prefix::command, "submit"); + TraceCategory::Rpc, rpc_span::prefix::command, commandName); try { doWork(); span.setOk(); @@ -76,16 +76,16 @@ 3. Cross-thread context propagation: @code - #include + #include using namespace xrpl::telemetry; // Thread A: create span and capture context auto span = SpanGuard::span( - TraceCategory::Consensus, seg::consensus, consensus::span::op::round); + TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process); auto ctx = span.captureContext(); // Thread B: create child with captured context - auto child = SpanGuard::childSpan(consensus::span::accept, ctx); + auto child = SpanGuard::childSpan(rpc_span::op::process, ctx); @endcode 4. Conditional check (rarely needed — methods are no-ops on null): diff --git a/include/xrpl/telemetry/Telemetry.h b/include/xrpl/telemetry/Telemetry.h index fae8a71076..21edaaa53d 100644 --- a/include/xrpl/telemetry/Telemetry.h +++ b/include/xrpl/telemetry/Telemetry.h @@ -52,19 +52,22 @@ 2. Child span for a sub-operation (scoped child): @code - auto parent = SpanGuard::span(TraceCategory::Transactions, "tx", "process"); + auto parent = SpanGuard::span( + TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process); { - auto child = parent.childSpan("tx.apply"); - child.setAttribute("tx_type", txType); + auto child = parent.childSpan(rpc_span::op::process); + child.setAttribute(rpc_span::attr::version, apiVersion); // child ends here } @endcode 3. Unrelated span (cross-scope, same thread): @code - // Transactions and RPC can be active simultaneously - auto txSpan = SpanGuard::span(TraceCategory::Transactions, "tx", "process"); - auto rpcSpan = SpanGuard::span(TraceCategory::Rpc, "rpc", "info"); + // 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 @@ -74,7 +77,7 @@ auto ctx = parentGuard.captureContext(); // Thread B: create child span with explicit parent - auto child = SpanGuard::childSpan("async.work", ctx); + auto child = SpanGuard::childSpan(rpc_span::op::process, ctx); @endcode @note Thread safety: The Telemetry interface is safe for concurrent reads @@ -83,8 +86,8 @@ The OTel SDK's TracerProvider and Tracer are internally thread-safe. */ -#include #include +#include #include #include @@ -148,11 +151,11 @@ public: std::string serviceName = "xrpld"; /** OTel resource attribute `service.version` (set from BuildInfo). */ - std::string serviceVersion{}; + std::string serviceVersion; /** OTel resource attribute `service.instance.id` (defaults to node public key). */ - std::string serviceInstanceId{}; + std::string serviceInstanceId; /** OTLP/HTTP endpoint URL where spans are sent. */ std::string exporterEndpoint = "http://localhost:4318/v1/traces"; @@ -161,7 +164,7 @@ public: bool useTls = false; /** Path to a CA certificate bundle for TLS verification. */ - std::string tlsCertPath{}; + std::string tlsCertPath; /** Path to this node's client certificate (PEM), presented to the collector for mutual TLS. Empty disables client-side auth, in diff --git a/nix/docker/check-tools.sh b/nix/docker/check-tools.sh index 67bcdff8a9..276e5977ff 100755 --- a/nix/docker/check-tools.sh +++ b/nix/docker/check-tools.sh @@ -10,10 +10,12 @@ cmake --version conan --version curl --version doxygen --version +file --version g++ --version gcc --version gcov --version gcovr --version +gh --version git --version git-cliff --version gpg --version diff --git a/nix/packages.nix b/nix/packages.nix index d40472634b..fc4eff679e 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -13,7 +13,9 @@ in conan curlMinimal # needed for codecov/codecov-action doxygen + file # needed for cpack in Clio gcovr + gh git git-cliff gnumake diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp index 676b473132..f7ec8a8bc3 100644 --- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp +++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp @@ -813,7 +813,7 @@ doOverpayment( // 3. The overpayment's penalty interest part (= untrackedInterest // for the overpayment path; see computeOverpaymentComponents): // trackedInterestPart() - bool const fix320Enabled = rules.enabled(fixCleanup3_2_0); + [[maybe_unused]] bool const fix320Enabled = rules.enabled(fixCleanup3_2_0); XRPL_ASSERT_IF( fix320Enabled, overpaymentComponents.trackedPrincipalDelta == diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index eccb581c6d..e58ab29257 100644 --- a/src/libxrpl/protocol/AMMCore.cpp +++ b/src/libxrpl/protocol/AMMCore.cpp @@ -127,7 +127,7 @@ ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot) bool ammEnabled(Rules const& rules) { - return rules.enabled(featureAMM) && rules.enabled(fixUniversalNumber); + return rules.enabled(featureAMM); } } // namespace xrpl diff --git a/src/libxrpl/protocol/IOUAmount.cpp b/src/libxrpl/protocol/IOUAmount.cpp index d214995809..acbf6724e1 100644 --- a/src/libxrpl/protocol/IOUAmount.cpp +++ b/src/libxrpl/protocol/IOUAmount.cpp @@ -1,6 +1,5 @@ #include -#include #include #include #include @@ -17,29 +16,6 @@ namespace xrpl { -namespace { - -// Use a static inside a function to help prevent order-of-initialization issues -LocalValue& -getStaticSTNumberSwitchover() -{ - static LocalValue kR{true}; - return kR; -} -} // namespace - -bool -getSTNumberSwitchover() -{ - return *getStaticSTNumberSwitchover(); -} - -void -setSTNumberSwitchover(bool v) -{ - *getStaticSTNumberSwitchover() = v; -} - /* The range for the mantissa when normalized */ // log(2^63,10) ~ 18.96 // @@ -75,56 +51,20 @@ IOUAmount::normalize() return; } - if (getSTNumberSwitchover()) - { - Number const v{mantissa_, exponent_}; - *this = fromNumber(v); - if (exponent_ > kMaxExponent) - Throw("value overflow"); - if (exponent_ < kMinExponent) - *this = beast::kZero; - return; - } - - bool const negative = (mantissa_ < 0); - - if (negative) - mantissa_ = -mantissa_; - - while ((mantissa_ < kMinMantissa) && (exponent_ > kMinExponent)) - { - mantissa_ *= 10; - --exponent_; - } - - while (mantissa_ > kMaxMantissa) - { - if (exponent_ >= kMaxExponent) - Throw("IOUAmount::normalize"); - - mantissa_ /= 10; - ++exponent_; - } - - if ((exponent_ < kMinExponent) || (mantissa_ < kMinMantissa)) - { - *this = beast::kZero; - return; - } - - if (exponent_ > kMaxExponent) - Throw("value overflow"); - - if (negative) - mantissa_ = -mantissa_; + Number const v{mantissa_, exponent_}; + *this = IOUAmount(v); } IOUAmount::IOUAmount(Number const& other) : IOUAmount(fromNumber(other)) { if (exponent_ > kMaxExponent) + { Throw("value overflow"); + } if (exponent_ < kMinExponent) + { *this = beast::kZero; + } } IOUAmount& @@ -139,37 +79,7 @@ IOUAmount::operator+=(IOUAmount const& other) return *this; } - if (getSTNumberSwitchover()) - { - *this = IOUAmount{Number{*this} + Number{other}}; - return *this; - } - auto m = other.mantissa_; - auto e = other.exponent_; - - while (exponent_ < e) - { - mantissa_ /= 10; - ++exponent_; - } - - while (e < exponent_) - { - m /= 10; - ++e; - } - - // This addition cannot overflow an std::int64_t but we may throw from - // normalize if the result isn't representable. - mantissa_ += m; - - if (mantissa_ >= -10 && mantissa_ <= 10) - { - *this = beast::kZero; - return *this; - } - - normalize(); + *this = IOUAmount{Number{*this} + Number{other}}; return *this; } diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 08a95145eb..e0968ea868 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -83,15 +82,12 @@ useRulesGuards(Rules const& rules) void createGuards( Rules const& rules, - std::optional& stNumberSO, std::optional& rulesGuard, std::optional& mantissaScaleGuard) { if (useRulesGuards(rules)) { // raii classes for the current ledger rules. - // fixUniversalNumber predates the rulesGuard and should be replaced. - stNumberSO.emplace(rules.enabled(fixUniversalNumber)); rulesGuard.emplace(rules); } else diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 1ba9cd042f..748d00f25a 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -388,47 +388,9 @@ operator+(STAmount const& v1, STAmount const& v2) if (v1.holds()) return {v1.asset_, v1.mpt().value() + v2.mpt().value()}; - if (getSTNumberSwitchover()) - { - auto x = v1; - x = v1.iou() + v2.iou(); - return x; - } - - int ov1 = v1.exponent(), ov2 = v2.exponent(); - std::int64_t vv1 = static_cast(v1.mantissa()); - std::int64_t vv2 = static_cast(v2.mantissa()); - - if (v1.negative()) - vv1 = -vv1; - - if (v2.negative()) - vv2 = -vv2; - - while (ov1 < ov2) - { - vv1 /= 10; - ++ov1; - } - - while (ov2 < ov1) - { - vv2 /= 10; - ++ov2; - } - - // This addition cannot overflow an std::int64_t. It can overflow an - // STAmount and the constructor will throw. - - std::int64_t const fv = vv1 + vv2; - - if ((fv >= -10) && (fv <= 10)) - return {v1.getFName(), v1.asset()}; - - if (fv >= 0) - return STAmount{v1.getFName(), v1.asset(), static_cast(fv), ov1, false}; - - return STAmount{v1.getFName(), v1.asset(), static_cast(-fv), ov1, true}; + auto x = v1; + x = v1.iou() + v2.iou(); + return x; } STAmount @@ -877,53 +839,25 @@ STAmount::canonicalize() if (asset_.holds() && offset_ > 18) Throw("MPT amount out of range"); - if (getSTNumberSwitchover()) + Number const num(isNegative_, value_, offset_, Number::Unchecked{}); + auto set = [&](auto const& val) { + auto const value = val.value(); + isNegative_ = value < 0; + value_ = isNegative_ ? -value : value; + }; + if (native()) { - Number const num(isNegative_, value_, offset_, Number::Unchecked{}); - auto set = [&](auto const& val) { - auto const value = val.value(); - isNegative_ = value < 0; - value_ = isNegative_ ? -value : value; - }; - if (native()) - { - set(XRPAmount{num}); - } - else if (asset_.holds()) - { - set(MPTAmount{num}); - } - else - { - Throw("Unknown integral asset type"); - } - offset_ = 0; + set(XRPAmount{num}); + } + else if (asset_.holds()) + { + set(MPTAmount{num}); } else { - while (offset_ < 0) - { - value_ /= 10; - ++offset_; - } - - while (offset_ > 0) - { - // N.B. do not move the overflow check to after the - // multiplication - if (native() && value_ > kMaxNativeN) - { - Throw("Native currency amount out of range"); - } - else if (!native() && value_ > kMaxMpTokenAmount) - { - Throw("MPT amount out of range"); - } - - value_ *= 10; - --offset_; - } + Throw("Unknown integral asset type"); // LCOV_EXCL_LINE } + offset_ = 0; if (native() && value_ > kMaxNativeN) { @@ -937,53 +871,7 @@ STAmount::canonicalize() return; } - if (getSTNumberSwitchover()) - { - *this = iou(); - return; - } - - if (value_ == 0) - { - offset_ = -100; - isNegative_ = false; - return; - } - - while ((value_ < kMinValue) && (offset_ > kMinOffset)) - { - value_ *= 10; - --offset_; - } - - while (value_ > kMaxValue) - { - if (offset_ >= kMaxOffset) - Throw("value overflow"); - - value_ /= 10; - ++offset_; - } - - if ((offset_ < kMinOffset) || (value_ < kMinValue)) - { - value_ = 0; - isNegative_ = false; - offset_ = -100; - return; - } - - if (offset_ > kMaxOffset) - Throw("value overflow"); - - XRPL_ASSERT( - (value_ == 0) || ((value_ >= kMinValue) && (value_ <= kMaxValue)), - "xrpl::STAmount::canonicalize : value inside range"); - XRPL_ASSERT( - (value_ == 0) || ((offset_ >= kMinOffset) && (offset_ <= kMaxOffset)), - "xrpl::STAmount::canonicalize : offset inside range"); - XRPL_ASSERT( - (value_ != 0) || (offset_ != -100), "xrpl::STAmount::canonicalize : value or offset set"); + *this = iou(); } void @@ -1250,16 +1138,34 @@ hasInvalidAmount(STBase const& field, int depth, beast::Journal j) return true; } - if (auto const amount = dynamic_cast(&field)) - return !isLegalMPT(*amount) || !isLegalNet(*amount); + // Dispatch on the serialized type tag rather than RTTI: this is on the invariant-checking path + // and a dynamic_cast chain over every field of every modified entry is measurably expensive. + // The object-like tags below all denote STObject subclasses (STLedgerEntry, STTx), so the + // downcast is sound; nested fields are only ever plain STI_OBJECT / STI_ARRAY containers. + // safeDowncast keeps a dynamic_cast validity assert in debug builds while compiling to + // static_cast in release. + switch (field.getSType()) + { + case STI_AMOUNT: { + auto const& amount = safeDowncast(field); + return !isLegalMPT(amount) || !isLegalNet(amount); + } - if (auto const object = dynamic_cast(&field)) - return hasInvalidAmount(*object, depth + 1, j); + case STI_OBJECT: + case STI_LEDGERENTRY: + case STI_TRANSACTION: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - if (auto const array = dynamic_cast(&field)) - return hasInvalidAmount(*array, depth + 1, j); + case STI_ARRAY: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - return false; + default: { + XRPL_ASSERT( + dynamic_cast(&field) == nullptr, + "xrpl::hasInvalidAmount : unhandled STObject type"); + return false; + } + } } bool @@ -1395,44 +1301,8 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) return STAmount(asset, minV * maxV); } - if (getSTNumberSwitchover()) - { - auto const r = Number{v1} * Number{v2}; - return STAmount{asset, r}; - } - - std::uint64_t value1 = v1.mantissa(); - std::uint64_t value2 = v2.mantissa(); - int offset1 = v1.exponent(); - int offset2 = v2.exponent(); - - if (v1.integral()) - { - while (value1 < STAmount::kMinValue) - { - value1 *= 10; - --offset1; - } - } - - if (v2.integral()) - { - while (value2 < STAmount::kMinValue) - { - value2 *= 10; - --offset2; - } - } - - // We multiply the two mantissas (each is between 10^15 - // and 10^16), so their product is in the 10^30 to 10^32 - // range. Dividing their product by 10^14 maintains the - // precision, by scaling the result to 10^16 to 10^18. - return STAmount( - asset, - muldiv(value1, value2, kTenTO14) + 7, - offset1 + offset2 + 14, - v1.negative() != v2.negative()); + auto const r = Number{v1} * Number{v2}; + return STAmount{asset, r}; } // This is the legacy version of canonicalizeRound. It's been in use diff --git a/src/libxrpl/telemetry/SpanGuard.cpp b/src/libxrpl/telemetry/SpanGuard.cpp index 403099e864..97fc2582dd 100644 --- a/src/libxrpl/telemetry/SpanGuard.cpp +++ b/src/libxrpl/telemetry/SpanGuard.cpp @@ -504,6 +504,14 @@ SpanGuard::discard() { gTlDiscardCurrentSpan = true; impl_->span->End(); + // Clear here so discard() owns the flag's whole lifetime + // (set -> End -> clear) in one scope, rather than relying on + // FilteringSpanProcessor::OnEnd() to clear it. Today every valid guard + // wraps a recording span (head sampling is 1.0), so OnEnd() always runs + // and clearing here is equivalent — but colocating set and clear keeps + // the flag leak-proof if a later phase can hand back a non-recording + // span (e.g. honoring a non-sampled remote parent during propagation). + gTlDiscardCurrentSpan = false; impl_->span = nullptr; // prevent ~Impl from calling End() again impl_.reset(); } diff --git a/src/libxrpl/telemetry/Telemetry.cpp b/src/libxrpl/telemetry/Telemetry.cpp index f27800c5d9..47ff915b87 100644 --- a/src/libxrpl/telemetry/Telemetry.cpp +++ b/src/libxrpl/telemetry/Telemetry.cpp @@ -123,8 +123,7 @@ public: { // SpanGuard::discard() set the flag on this thread just before // calling Span::End(), which invokes OnEnd() synchronously. - // Clear the flag and drop the span. - gTlDiscardCurrentSpan = false; + // Drop the span. return; } delegate_->OnEnd(std::move(span)); diff --git a/src/libxrpl/telemetry/TelemetryConfig.cpp b/src/libxrpl/telemetry/TelemetryConfig.cpp index b81f915deb..da6f340b50 100644 --- a/src/libxrpl/telemetry/TelemetryConfig.cpp +++ b/src/libxrpl/telemetry/TelemetryConfig.cpp @@ -7,11 +7,13 @@ See cfg/xrpld-example.cfg for the full list of available options. */ -#include +#include +#include #include #include #include +#include #include namespace xrpl::telemetry { @@ -102,6 +104,16 @@ setupTelemetry( setup.tlsClientCertPath = section.valueOr(key::tlsClientCert, ""); setup.tlsClientKeyPath = section.valueOr(key::tlsClientKey, ""); + // Mutual TLS needs both the client certificate and its private key. + // Supplying only one fails later with a cryptic SSL handshake error, so + // reject the partial configuration here with an actionable message. + if (setup.tlsClientCertPath.empty() != setup.tlsClientKeyPath.empty()) + { + Throw( + "[telemetry] tls_client_cert and tls_client_key must be set together " + "(set both for mutual TLS, or neither for one-way TLS)."); + } + // 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 diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index aa3c39696c..7f17d97c7c 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -1176,21 +1175,17 @@ Transactor::checkTransactionInvariants(TER result, XRPAmount fee) [[nodiscard]] TER Transactor::checkInvariants(TER result, XRPAmount fee) { - // Transaction invariants first (more specific). These check post-conditions of the specific - // transaction. If these fail, the transaction's core logic is wrong. - auto const txResult = checkTransactionInvariants(result, fee); - - // Protocol invariants second (broader). These check properties that must hold regardless of - // transaction type. - auto const protoResult = ctx_.checkInvariants(result, fee); - - // Fail if either check failed. tef (fatal) takes priority over tec. - if (protoResult == tefINVARIANT_FAILED) - return tefINVARIANT_FAILED; - if (txResult == tecINVARIANT_FAILED || protoResult == tecINVARIANT_FAILED) - return tecINVARIANT_FAILED; - - return result; + /* + * DISABLED for 3.2.0 — Must be re-introduced for 3.3.0 + * + * Transaction invariants are disabled due to a performance regression: + * the two-pass design (transaction-specific invariants + protocol invariants) + * iterates over modified ledger entries twice per transaction. + * + * Until resolved, only protocol invariants are checked (delegated to ctx_). + * This is safe because all transaction invariants in 3.2.0 are no-ops. + */ + return ctx_.checkInvariants(result, fee); } //------------------------------------------------------------------------------ ApplyResult @@ -1212,8 +1207,6 @@ Transactor::operator()() // with_txn_type(). // // raii classes for the current ledger rules. - // fixUniversalNumber predate the rulesGuard and should be replaced. - NumberSO const stNumberSO{view().rules().enabled(fixUniversalNumber)}; CurrentTransactionRulesGuard const currentTransactionRulesGuard(view().rules()); #ifdef DEBUG diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp index 0fc0275eb0..b70cb0d345 100644 --- a/src/libxrpl/tx/apply.cpp +++ b/src/libxrpl/tx/apply.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -133,7 +132,6 @@ template ApplyResult apply(ServiceRegistry& registry, OpenView& view, PreflightChecks&& preflightChecks) { - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; return doApply(preclaim(preflightChecks(), registry, view), registry, view); } diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index fa2021a215..2b5b99e564 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -114,10 +113,9 @@ withTxnType(Rules const& rules, TxType txnType, F&& f) // // See also Transactor::operator(). // - std::optional stNumberSO; std::optional rulesGuard; std::optional mantissaScaleGuard; - createGuards(rules, stNumberSO, rulesGuard, mantissaScaleGuard); + createGuards(rules, rulesGuard, mantissaScaleGuard); switch (txnType) { diff --git a/src/libxrpl/tx/paths/OfferStream.cpp b/src/libxrpl/tx/paths/OfferStream.cpp index b7defb4df8..ecc8416a2b 100644 --- a/src/libxrpl/tx/paths/OfferStream.cpp +++ b/src/libxrpl/tx/paths/OfferStream.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -249,7 +250,13 @@ TOfferStreamBase::step() continue; } - if (entry->isFieldPresent(sfDomainID) && + // Pre-fixCleanup3_3_0: validate domain membership for any book. + // Post-fixCleanup3_3_0: only validate when walking a domain book. + // Hybrid offers carry sfDomainID but also participate in the open + // book; expiry of the owner's domain credential should not evict + // the offer from the open book. + if ((!view_.rules().enabled(fixCleanup3_3_0) || book_.domain.has_value()) && + entry->isFieldPresent(sfDomainID) && !permissioned_dex::offerInDomain( view_, entry->key(), entry->getFieldH256(sfDomainID), j_)) { diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 64972c24ab..e3a1cc935f 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -4333,15 +4333,10 @@ private: testAmendment() { testcase("Amendment"); - FeatureBitset const all{testableAmendments()}; - FeatureBitset const noAMM{all - featureAMM}; - FeatureBitset const noNumber{all - fixUniversalNumber}; - FeatureBitset const noAMMAndNumber{all - featureAMM - fixUniversalNumber}; using namespace jtx; + Env env{*this, testableAmendments() - featureAMM}; - for (auto const& feature : {noAMM, noNumber, noAMMAndNumber}) { - Env env{*this, feature}; fund(env, gw_, {alice_}, {USD(1'000)}, Fund::All); AMM amm(env, alice_, XRP(1'000), USD(1'000), Ter(temDISABLED)); diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 269bc72c53..ba8f09c449 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -2236,17 +2236,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // See the impact of rounding when the nft is sold for small amounts // of drops. - for (auto numberSwitchOver : {true}) { - if (numberSwitchOver) - { - env.enableFeature(fixUniversalNumber); - } - else - { - env.disableFeature(fixUniversalNumber); - } - // An nft with a transfer fee of 1 basis point. uint256 const nftID = token::getNextID(env, alice, 0u, tfTransferable, 1); env(token::mint(alice), Txflags(tfTransferable), token::XferFee(1)); @@ -2268,7 +2258,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // minter sells to carol. The payment is just small enough that // alice does not get any transfer fee. - auto pmt = numberSwitchOver ? drops(50000) : drops(99999); + auto pmt = drops(50000); STAmount carolBalance = env.balance(carol); uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key; env(token::createOffer(minter, nftID, pmt), Txflags(tfSellNFToken)); @@ -2285,7 +2275,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // transfer that enables a transfer fee of 1 basis point. STAmount beckyBalance = env.balance(becky); uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key; - pmt = numberSwitchOver ? drops(50001) : drops(100000); + pmt = drops(50001); env(token::createOffer(becky, nftID, pmt), token::Owner(carol)); env.close(); env(token::acceptBuyOffer(carol, beckyBuyOfferIndex)); diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index e0f2f4eab0..ed0b2ffbe3 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -1827,7 +1827,6 @@ public: using namespace jtx; Env env{*this, features}; - env.enableFeature(fixUniversalNumber); auto const gw = Account{"gateway"}; auto const alice = Account{"alice"}; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 83c58884e0..7382f4f090 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -1973,54 +1973,36 @@ public: using namespace jtx; - for (auto numberSwitchOver : {false, true}) - { - Env env{*this, features}; - if (numberSwitchOver) - { - env.enableFeature(fixUniversalNumber); - } - else - { - env.disableFeature(fixUniversalNumber); - } + Env env{*this, features}; - auto const gw = Account{"gateway"}; - auto const alice = Account{"alice"}; - auto const bob = Account{"bob"}; - auto const usd = gw["USD"]; + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const usd = gw["USD"]; - env.fund(XRP(10000), gw, alice, bob); - env.close(); + env.fund(XRP(10000), gw, alice, bob); + env.close(); - env(rate(gw, 1.005)); + env(rate(gw, 1.005)); - env(trust(alice, usd(1000))); - env(trust(bob, usd(1000))); - env(trust(gw, alice["USD"](50))); + env(trust(alice, usd(1000))); + env(trust(bob, usd(1000))); + env(trust(gw, alice["USD"](50))); - env(pay(gw, bob, bob["USD"](1))); - env(pay(alice, gw, usd(50))); + env(pay(gw, bob, bob["USD"](1))); + env(pay(alice, gw, usd(50))); - env(trust(gw, alice["USD"](0))); + env(trust(gw, alice["USD"](0))); - env(offer(alice, usd(50), XRP(150000))); - env(offer(bob, XRP(100), usd(0.1))); + env(offer(alice, usd(50), XRP(150000))); + env(offer(bob, XRP(100), usd(0.1))); - auto jrr = ledgerEntryState(env, alice, gw, "USD"); - BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667"); + auto jrr = ledgerEntryState(env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667"); - jrr = ledgerEntryState(env, bob, gw, "USD"); - json::Value const bobUSD = jrr[jss::node][sfBalance.fieldName][jss::value]; - if (!numberSwitchOver) - { - BEAST_EXPECT(bobUSD == "-0.966500000033334"); - } - else - { - BEAST_EXPECT(bobUSD == "-0.9665000000333333"); - } - } + jrr = ledgerEntryState(env, bob, gw, "USD"); + json::Value const bobUSD = jrr[jss::node][sfBalance.fieldName][jss::value]; + BEAST_EXPECT(bobUSD == "-0.9665000000333333"); } void diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index d534f20248..99e69ce482 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -185,15 +185,15 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID), Ter(temDISABLED)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID), Ter(temDISABLED)); env.close(); env.enableFeature(featurePermissionedDEX); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); } @@ -214,7 +214,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - someone outside of the domain cannot create domain offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -223,7 +223,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(tecNO_PERMISSION)); @@ -247,7 +247,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - someone with expired cred cannot create domain offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -256,7 +256,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto jv = credentials::create(devin, domainOwner, credType); @@ -282,13 +282,13 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - cannot create an offer in a non existent domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); uint256 const badDomain{ "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" "E5"}; - env(offer(bob_, XRP(10), USD(10)), Domain(badDomain), Ter(tecNO_PERMISSION)); + env(offer(bob, XRP(10), USD(10)), Domain(badDomain), Ter(tecNO_PERMISSION)); env.close(); } @@ -296,68 +296,68 @@ class PermissionedDEX_test : public beast::unit_test::Suite // domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); } // apply - offer can be created even if takerpays issuer is not in // domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), XRP(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, USD(10), XRP(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, USD(10), XRP(10), 0, true)); } // apply - two domain offers cross with each other { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // a non domain offer cannot cross with domain offer - env(offer(carol_, USD(10), XRP(10))); + env(offer(carol, USD(10), XRP(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - create lots of domain offers { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); std::vector offerSeqs; @@ -365,19 +365,19 @@ class PermissionedDEX_test : public beast::unit_test::Suite for (size_t i = 0; i <= 100; i++) { - auto const bobOfferSeq{env.seq(bob_)}; + auto const bobOfferSeq{env.seq(bob)}; offerSeqs.emplace_back(bobOfferSeq); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); } for (auto const offerSeq : offerSeqs) { - env(offerCancel(bob_, offerSeq)); + env(offerCancel(bob, offerSeq)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, offerSeq)); + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); } } } @@ -390,10 +390,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight - without enabling featurePermissionedDEX amendment { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(pay(bob_, alice_, USD(10)), + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -403,10 +403,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.enableFeature(featurePermissionedDEX); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - env(pay(bob_, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } @@ -431,13 +431,13 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - cannot send payment with non existent domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); uint256 const badDomain{ "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" "E5"}; - env(pay(bob_, alice_, USD(10)), + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(badDomain), @@ -448,10 +448,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - payment with non-domain destination fails { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create devin account who is not part of the domain @@ -460,11 +460,11 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); // devin is not part of domain - env(pay(alice_, devin, USD(10)), + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -476,7 +476,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin has not yet accepted cred - env(pay(alice_, devin, USD(10)), + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -487,17 +487,17 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin can now receive payment after he is in domain - env(pay(alice_, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // preclaim - non-domain sender cannot send payment { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create devin account who is not part of the domain @@ -506,11 +506,11 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); // devin tries to send domain payment - env(pay(devin, alice_, USD(10)), + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -522,7 +522,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin has not yet accepted cred - env(pay(devin, alice_, USD(10)), + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -533,28 +533,28 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin can now send payment after he is in domain - env(pay(devin, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // apply - domain owner can always send and receive domain payment { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // domain owner can always be destination - env(pay(alice_, domainOwner, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, domainOwner, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // domain owner can send - env(pay(domainOwner, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(domainOwner, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } } @@ -567,22 +567,22 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test domain cross currency payment consuming one offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create a regular offer without domain - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); - auto const regularDirKey = getDefaultOfferDirKey(env, bob_, regularOfferSeq); + auto const regularDirKey = getDefaultOfferDirKey(env, bob, regularOfferSeq); BEAST_EXPECT(regularDirKey); BEAST_EXPECT(checkDirectorySize( env, *regularDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) // a domain payment cannot consume regular offers - env(pay(alice_, carol_, USD(10)), + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -590,23 +590,23 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // create a domain offer - auto const domainOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const domainOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, domainOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, domainOfferSeq, XRP(10), USD(10), 0, true)); - auto const domainDirKey = getDefaultOfferDirKey(env, bob_, domainOfferSeq); + auto const domainDirKey = getDefaultOfferDirKey(env, bob, domainOfferSeq); BEAST_EXPECT(domainDirKey); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) // cross-currency permissioned payment consumed // domain offer instead of regular offer - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, domainOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(!offerExists(env, bob, domainOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); // domain directory is empty BEAST_EXPECT(checkDirectorySize( @@ -618,79 +618,79 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test domain payment consuming two offers in the path { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); // create XRP/USD domain offer - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); // payment fail because there isn't eur offer - env(pay(alice_, carol_, eur(10)), + env(pay(alice, carol, eur(10)), Path(~USD, ~eur), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); - // bob_ creates a regular USD/EUR offer - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10))); + // bob creates a regular USD/EUR offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, USD(10), eur(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, USD(10), eur(10))); - // alice_ tries to pay again, but still fails because the regular + // alice tries to pay again, but still fails because the regular // offer cannot be consumed - env(pay(alice_, carol_, eur(10)), + env(pay(alice, carol, eur(10)), Path(~USD, ~eur), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - // bob_ creates a domain USD/EUR offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID)); + // bob creates a domain USD/EUR offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), 0, true)); - // alice_ successfully consume two domain offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Sendmax(XRP(5)), Domain(domainID), Path(~USD, ~eur)); + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Sendmax(XRP(5)), Domain(domainID), Path(~USD, ~eur)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), 0, true)); - // alice_ successfully consume two domain offers and deletes them + // alice successfully consume two domain offers and deletes them // we compute path this time using `paths` - env(pay(alice_, carol_, eur(5)), Sendmax(XRP(5)), Domain(domainID), Paths(XRP)); + env(pay(alice, carol, eur(5)), Sendmax(XRP(5)), Domain(domainID), Paths(XRP)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, usdOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, eurOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, usdOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, eurOfferSeq)); // regular offer is not consumed - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, USD(10), eur(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, USD(10), eur(10))); } // domain payment cannot consume offer from another domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // Fund devin and create USD trustline @@ -700,7 +700,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto const badCredType = "badCred"; @@ -720,24 +720,24 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // domain payment can't consume an offer from another domain - env(pay(alice_, carol_, USD(10)), + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - // bob_ creates an offer under the right domain - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + // bob creates an offer under the right domain + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); // domain payment now consumes from the right domain - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); } // sanity check: devin, who is part of the domain but doesn't have a @@ -745,10 +745,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // fund devin but don't create a USD trustline with gateway @@ -764,14 +764,14 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // successful payment because offer is consumed - env(pay(devin, alice_, USD(10)), Sendmax(XRP(10)), Domain(domainID)); + env(pay(devin, alice, USD(10)), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // offer becomes unfunded when offer owner's cred expires { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -780,7 +780,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto jv = credentials::create(devin, domainOwner, credType); @@ -797,7 +797,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin's offer can still be consumed while his cred isn't expired - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); BEAST_EXPECT(checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true)); @@ -805,7 +805,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(std::chrono::seconds(20)); // devin's offer is unfunded now due to expired cred - env(pay(alice_, carol_, USD(5)), + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), @@ -817,30 +817,30 @@ class PermissionedDEX_test : public beast::unit_test::Suite // offer becomes unfunded when offer owner's cred is removed { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const offerSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - // bob_'s offer can still be consumed while his cred exists - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + // bob's offer can still be consumed while his cred exists + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); - // remove bob_'s cred - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob's cred + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // bob_'s offer is unfunded now due to expired cred - env(pay(alice_, carol_, USD(5)), + // bob's offer is unfunded now due to expired cred + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); } } @@ -853,34 +853,34 @@ class PermissionedDEX_test : public beast::unit_test::Suite // payment. If the domain wishes to control who is allowed to ripple // through, they should set the rippling individually Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eura = alice_["EUR"]; - auto const eurb = bob_["EUR"]; + auto const eura = alice["EUR"]; + auto const eurb = bob["EUR"]; - env.trust(eura(100), bob_); - env.trust(eurb(100), carol_); + env.trust(eura(100), bob); + env.trust(eurb(100), carol); env.close(); - // remove bob_ from domain - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // alice_ can still ripple through bob_ even though he's not part + // alice can still ripple through bob even though he's not part // of the domain, this is intentional - env(pay(alice_, carol_, eurb(10)), Paths(eura), Domain(domainID)); + env(pay(alice, carol, eurb(10)), Paths(eura), Domain(domainID)); env.close(); - env.require(Balance(bob_, eura(10)), Balance(carol_, eurb(10))); + env.require(Balance(bob, eura(10)), Balance(carol, eurb(10))); - // carol_ sets no ripple on bob_ - env(trust(carol_, bob_["EUR"](0), bob_, tfSetNoRipple)); + // carol sets no ripple on bob + env(trust(carol, bob["EUR"](0), bob, tfSetNoRipple)); env.close(); - // payment no longer works because carol_ has no ripple on bob_ - env(pay(alice_, carol_, eurb(5)), Paths(eura), Domain(domainID), Ter(tecPATH_DRY)); + // payment no longer works because carol has no ripple on bob + env(pay(alice, carol, eurb(5)), Paths(eura), Domain(domainID), Ter(tecPATH_DRY)); env.close(); - env.require(Balance(bob_, eura(10)), Balance(carol_, eurb(10))); + env.require(Balance(bob, eura(10)), Balance(carol, eurb(10))); } void @@ -891,37 +891,37 @@ class PermissionedDEX_test : public beast::unit_test::Suite // whether the issuer is in the domain should NOT affect whether an // offer can be consumed in domain payment Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create an xrp/usd offer with usd as takergets - auto const bobOffer1Seq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOffer1Seq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create an usd/xrp offer with usd as takerpays - auto const bobOffer2Seq{env.seq(bob_)}; - env(offer(bob_, USD(10), XRP(10)), Domain(domainID), Txflags(tfPassive)); + auto const bobOffer2Seq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), Domain(domainID), Txflags(tfPassive)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOffer1Seq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOffer1Seq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true)); // remove gateway from domain - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); // payment succeeds even if issuer is not in domain // xrp/usd offer is consumed - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOffer1Seq)); + BEAST_EXPECT(!offerExists(env, bob, bobOffer1Seq)); // payment succeeds even if issuer is not in domain // usd/xrp offer is consumed - env(pay(alice_, carol_, XRP(10)), Path(~XRP), Sendmax(USD(10)), Domain(domainID)); + env(pay(alice, carol, XRP(10)), Path(~XRP), Sendmax(USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOffer2Seq)); + BEAST_EXPECT(!offerExists(env, bob, bobOffer2Seq)); } void @@ -932,36 +932,36 @@ class PermissionedDEX_test : public beast::unit_test::Suite // checking that an unfunded offer will be implicitly removed by a // successful payment tx Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, XRP(100), USD(100)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, XRP(100), USD(100)), Domain(domainID)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(20), USD(20)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(20), USD(20)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(20), USD(20), 0, true)); - BEAST_EXPECT(checkOffer(env, alice_, aliceOfferSeq, XRP(100), USD(100), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(20), USD(20), 0, true)); + BEAST_EXPECT(checkOffer(env, alice, aliceOfferSeq, XRP(100), USD(100), 0, true)); - auto const domainDirKey = getDefaultOfferDirKey(env, bob_, bobOfferSeq); + auto const domainDirKey = getDefaultOfferDirKey(env, bob, bobOfferSeq); BEAST_EXPECT(domainDirKey); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 2)); // NOLINT(bugprone-unchecked-optional-access) - // remove alice_ from domain and thus alice_'s offer becomes unfunded - env(credentials::deleteCred(domainOwner, alice_, domainOwner, credType)); + // remove alice from domain and thus alice's offer becomes unfunded + env(credentials::deleteCred(domainOwner, alice, domainOwner, credType)); env.close(); - env(pay(gw_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(gw, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); - // alice_'s unfunded offer is removed implicitly - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); + // alice's unfunded offer is removed implicitly + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) } @@ -972,12 +972,12 @@ class PermissionedDEX_test : public beast::unit_test::Suite testcase("AMM not used"); Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - AMM const amm(env, alice_, XRP(10), USD(50)); + AMM const amm(env, alice, XRP(10), USD(50)); // a domain payment isn't able to consume AMM - env(pay(bob_, carol_, USD(5)), + env(pay(bob, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), @@ -985,7 +985,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // a non domain payment can use AMM - env(pay(bob_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + env(pay(bob, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); // USD amount in AMM is changed @@ -1001,126 +1001,126 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight - invalid hybrid flag { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), + env(offer(bob, XRP(10), USD(10)), Domain(domainID), Txflags(tfHybrid), Ter(temDISABLED)); env.close(); - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); env.close(); env.enableFeature(featurePermissionedDEX); env.close(); // hybrid offer must have domainID - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); env.close(); // hybrid offer must have domainID - auto const offerSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(10), USD(10), lsfHybrid, true)); } // apply - domain offer can cross with hybrid { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - open offer can cross with hybrid { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10))); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10))); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - by default, hybrid offer tries to cross with offers in the // domain book { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // hybrid offer auto crosses with domain offer - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - hybrid offer does not automatically cross with open offers // because by default, it only tries to cross domain offers { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, false)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // hybrid offer auto crosses with domain offer - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, false)); - BEAST_EXPECT(checkOffer(env, alice_, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true)); - BEAST_EXPECT(ownerCount(env, alice_) == 3); + BEAST_EXPECT(offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(checkOffer(env, alice, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true)); + BEAST_EXPECT(ownerCount(env, alice) == 3); } } @@ -1129,58 +1129,97 @@ class PermissionedDEX_test : public beast::unit_test::Suite { testcase("Hybrid invalid offer"); - // bob_ has a hybrid offer and then he is removed from domain. - // in this case, the hybrid offer will be considered as unfunded even in - // a regular payment + // bob has a hybrid offer and then he is removed from the domain. + // Domain payments must not consume the offer; regular open-book + // payments follow the fixCleanup3_3_0 behavior checked below. Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(50), USD(50)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(50), USD(50)), Txflags(tfHybrid), Domain(domainID)); env.close(); - // remove bob_ from domain - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // bob_'s hybrid offer is unfunded and can not be consumed in a domain + // bob's hybrid offer is unfunded and can not be consumed in a domain // payment - env(pay(alice_, carol_, USD(5)), + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); - // bob_'s unfunded hybrid offer can't be consumed even with a regular - // payment - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Ter(tecPATH_PARTIAL)); - env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + if (features[fixCleanup3_3_0]) + { + // Post-fixCleanup3_3_0: hybrid offer can still be consumed via a regular + // open-book payment even though the domain credential was revoked. + auto const carolBalBefore = env.balance(carol, USD); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalBefore == USD(5)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(45), USD(45), lsfHybrid, true)); - // create a regular offer - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); - env.close(); - BEAST_EXPECT(offerExists(env, bob_, regularOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + // create a regular offer alongside the hybrid one + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); - auto const sleHybridOffer = env.le(keylet::offer(bob_.id(), hybridOfferSeq)); - BEAST_EXPECT(sleHybridOffer); - auto const openDir = - sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); - BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + auto const sleHybridOffer = env.le(keylet::offer(bob.id(), hybridOfferSeq)); + if (!BEAST_EXPECT(sleHybridOffer)) + return; + auto const openDir = + sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); + // both offers are in the open book directory + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); - // this normal payment should consume the regular offer and remove the - // unfunded hybrid offer - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); - env.close(); + // A regular payment crosses the hybrid offer first (FIFO, older + // offer), then stops; the regular offer is untouched. + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(5), USD(5))); - BEAST_EXPECT(checkDirectorySize(env, openDir, 1)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(40), USD(40), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + } + else + { + // Pre-fixCleanup3_3_0: the open-book traversal + // also runs the offerInDomain eviction check, so the hybrid offer + // is treated as unfunded and the regular payment fails. + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + + // create a regular offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT(offerExists(env, bob, regularOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + auto const sleHybridOffer = env.le(keylet::offer(bob.id(), hybridOfferSeq)); + if (!BEAST_EXPECT(sleHybridOffer)) + return; + auto const openDir = + sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + + // This payment crosses the regular offer and permanently evicts the + // hybrid offer from the open book (since the payment succeeds, the + // sandbox, including the hybrid eviction, is committed). + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(5), USD(5))); + BEAST_EXPECT(checkDirectorySize(env, openDir, 1)); + } } void @@ -1191,29 +1230,29 @@ class PermissionedDEX_test : public beast::unit_test::Suite // both non domain and domain payments can consume hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); - // hybrid offer can't be consumed since bob_ is not in domain anymore - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); } // someone from another domain can't cross hybrid if they specified // wrong domainID { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // Fund accounts @@ -1235,8 +1274,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite env(credentials::accept(devin, badDomainOwner, badCredType)); env.close(); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); // other domains can't consume the offer @@ -1246,107 +1285,197 @@ class PermissionedDEX_test : public beast::unit_test::Suite Domain(badDomainID), Ter(tecPATH_DRY)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); - // hybrid offer can't be consumed since bob_ is not in domain anymore - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); } // test domain payment consuming two offers w/ hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); // payment fail because there isn't eur offer - env(pay(alice_, carol_, eur(5)), + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); - // bob_ creates a hybrid eur offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); - // alice_ successfully consume two domain offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID)); + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); } // test regular payment using a regular offer and a hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); - // bob_ creates a regular usd offer - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + // bob creates a regular usd offer + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, false)); - // bob_ creates a hybrid eur offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); - // alice_ successfully consume two offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5))); + // alice successfully consume two offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, false)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, false)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); } } + // Test that a hybrid offer remains crossable in the open book after the + // owner's domain credential expires. A domain payment after expiry should + // fail (domain book evicts the offer in its sandbox), but the open book + // remains usable. + void + testHybridOpenBookAfterCredentialExpiry(FeatureBitset features) + { + testcase("Hybrid open book after credential expiry"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + Account const devin("devin"); + env.fund(XRP(100000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + // Give devin a credential that expires far enough in the future to + // survive the setup env.close() calls. + auto jv = credentials::create(devin, domainOwner, credType); + uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count(); + jv[sfExpiration.jsonName] = t + 100; + env(jv); + env.close(); + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // Devin creates a hybrid offer: sell USD(10) for XRP(10). + // The offer is placed in both the domain book and the open book. + auto const hybridOfferSeq{env.seq(devin)}; + env(offer(devin, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + env.close(); + + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + + // A non-domain open-book payment partially crosses the offer while + // devin's credential is still valid. + auto carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(5)); + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + + // Advance time so that devin's credential expires. + env.close(std::chrono::seconds(100)); + + // Confirm devin can no longer create domain offers. + env(offer(devin, XRP(1), USD(1)), Domain(domainID), Ter(tecNO_PERMISSION)); + env.close(); + + // The hybrid offer must still exist in the open book after expiry. + BEAST_EXPECT(offerExists(env, devin, hybridOfferSeq)); + + // A non-domain open-book payment must cross (not evict) the + // remaining portion of devin's hybrid offer. + carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(2)), Path(~USD), Sendmax(XRP(2))); + env.close(); + + // Carol received USD; the offer was crossed, not evicted. + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(2)); + // Offer still exists with 3 USD / 3 XRP remaining. + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(3), USD(3), lsfHybrid, true)); + + // A domain payment now fails because the domain book evicts devin's + // offer (his credential has expired). The eviction is rolled back with + // the failed sandbox, so the offer is NOT permanently removed. + env(pay(alice, carol, USD(1)), + Path(~USD), + Sendmax(XRP(1)), + Domain(domainID), + Ter(tecPATH_PARTIAL)); + env.close(); + + // Offer still intact in the open book; domain payment did not + // permanently delete it. + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(3), USD(3), lsfHybrid, true)); + + // The open book can still fully consume the remaining portion. + carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(3)), Path(~USD), Sendmax(XRP(3))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(3)); + BEAST_EXPECT(!offerExists(env, devin, hybridOfferSeq)); + } + void testHybridOfferDirectories(FeatureBitset features) { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); std::vector offerSeqs; @@ -1362,12 +1491,12 @@ class PermissionedDEX_test : public beast::unit_test::Suite for (size_t i = 1; i <= dirCnt; i++) { - auto const bobOfferSeq{env.seq(bob_)}; + auto const bobOfferSeq{env.seq(bob)}; offerSeqs.emplace_back(bobOfferSeq); - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - auto const sleOffer = env.le(keylet::offer(bob_.id(), bobOfferSeq)); + auto const sleOffer = env.le(keylet::offer(bob.id(), bobOfferSeq)); BEAST_EXPECT(sleOffer); BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir); BEAST_EXPECT(sleOffer->getFieldArray(sfAdditionalBooks).size() == 1); @@ -1375,17 +1504,17 @@ class PermissionedDEX_test : public beast::unit_test::Suite sleOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory) == openDir); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); BEAST_EXPECT(checkDirectorySize(env, domainDir, i)); BEAST_EXPECT(checkDirectorySize(env, openDir, i)); } for (auto const offerSeq : offerSeqs) { - env(offerCancel(bob_, offerSeq)); + env(offerCancel(bob, offerSeq)); env.close(); dirCnt--; - BEAST_EXPECT(!offerExists(env, bob_, offerSeq)); + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); BEAST_EXPECT(checkDirectorySize(env, domainDir, dirCnt)); BEAST_EXPECT(checkDirectorySize(env, openDir, dirCnt)); } @@ -1397,34 +1526,34 @@ class PermissionedDEX_test : public beast::unit_test::Suite testcase("Auto bridge"); Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; + auto const eur = gw["EUR"]; - for (auto const& account : {alice_, bob_, carol_}) + for (auto const& account : {alice, bob, carol}) { env(trust(account, eur(10000))); env.close(); } - env(pay(gw_, carol_, eur(1))); + env(pay(gw, carol, eur(1))); env.close(); - auto const aliceOfferSeq{env.seq(alice_)}; - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(alice_, XRP(100), USD(1)), Domain(domainID)); - env(offer(bob_, eur(1), XRP(100)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + auto const bobOfferSeq{env.seq(bob)}; + env(offer(alice, XRP(100), USD(1)), Domain(domainID)); + env(offer(bob, eur(1), XRP(100)), Domain(domainID)); env.close(); - // carol_'s offer should cross bob_ and alice_'s offers due to auto + // carol's offer should cross bob and alice's offers due to auto // bridging - auto const carolOfferSeq{env.seq(carol_)}; - env(offer(carol_, USD(1), eur(1)), Domain(domainID)); + auto const carolOfferSeq{env.seq(carol)}; + env(offer(carol, USD(1), eur(1)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, carolOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq)); } void @@ -1819,7 +1948,9 @@ public: // Test hybrid offers testHybridOfferCreate(all); testHybridBookStep(all); + testHybridInvalidOffer(all - fixCleanup3_3_0); testHybridInvalidOffer(all); + testHybridOpenBookAfterCredentialExpiry(all); testHybridOfferDirectories(all); testHybridMalformedOffer(all); testHybridMalformedOffer(all - fixCleanup3_1_3); diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index f4bd1c9d66..2d2745de14 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1514,7 +1514,6 @@ public: void testToStAmount() { - NumberSO const stNumberSO{true}; Issue const issue; Number const n{7'518'783'80596, -5}; SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; diff --git a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp index aa935b2151..0d3802df8b 100644 --- a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp +++ b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp @@ -30,8 +30,12 @@ TEST(SpanGuardFactory, category_span_returns_null_when_disabled) auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "test"); EXPECT_FALSE(span); - span.setAttribute("xrpl.rpc.command", "test"); - span.setAttribute("xrpl.rpc.status", "success"); + // Attribute keys use the underscore convention for span attributes (the + // dotted xrpl.. form is reserved for resource attributes). The + // canonical constants live in the xrpld-level *SpanNames.h headers, which a + // libxrpl test cannot include, so the keys are written as literals here. + span.setAttribute("command", "test"); + span.setAttribute("rpc_status", "success"); } TEST(SpanGuardFactory, child_span_null_when_no_parent) @@ -85,23 +89,26 @@ TEST(SpanGuardFactory, discard_safe_on_null) TEST(SpanGuardFactory, consensus_close_time_attributes) { - // Verify the consensus attribute pattern compiles and - // doesn't crash with null SpanGuard. + // Verify the consensus attribute pattern compiles and doesn't crash with a + // null SpanGuard. Attribute keys/values use the underscore convention; the + // canonical consensus::span constants are defined in the xrpld-level + // ConsensusSpanNames.h, which a libxrpl test cannot include, so the keys are + // written as literals here. { auto span = telemetry::SpanGuard::span( telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "accept.apply"); - span.setAttribute("xrpl.consensus.ledger.seq", static_cast(42)); - span.setAttribute("xrpl.consensus.close_time", static_cast(780000000)); - span.setAttribute("xrpl.consensus.close_time_correct", true); - span.setAttribute("xrpl.consensus.close_resolution_ms", static_cast(30000)); - span.setAttribute("xrpl.consensus.state", std::string("finished")); - span.setAttribute("xrpl.consensus.proposing", true); - span.setAttribute("xrpl.consensus.round_time_ms", static_cast(3500)); + span.setAttribute("ledger_seq", static_cast(42)); + span.setAttribute("close_time", static_cast(780000000)); + span.setAttribute("close_time_correct", true); + span.setAttribute("close_resolution_ms", static_cast(30000)); + span.setAttribute("consensus_state", std::string("finished")); + span.setAttribute("proposing", true); + span.setAttribute("round_time_ms", static_cast(3500)); } { auto span = telemetry::SpanGuard::span( telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "accept.apply"); - span.setAttribute("xrpl.consensus.close_time_correct", false); - span.setAttribute("xrpl.consensus.state", std::string("moved_on")); + span.setAttribute("close_time_correct", false); + span.setAttribute("consensus_state", std::string("moved_on")); } } diff --git a/src/tests/libxrpl/telemetry/TelemetryConfig.cpp b/src/tests/libxrpl/telemetry/TelemetryConfig.cpp index 137b30a62f..b9458b66ac 100644 --- a/src/tests/libxrpl/telemetry/TelemetryConfig.cpp +++ b/src/tests/libxrpl/telemetry/TelemetryConfig.cpp @@ -1,9 +1,11 @@ -#include #include +#include #include #include +#include + using namespace xrpl; TEST(TelemetryConfig, setup_defaults) @@ -83,6 +85,43 @@ TEST(TelemetryConfig, parse_full_section) EXPECT_FALSE(setup.traceLedger); } +TEST(TelemetryConfig, mtls_cert_and_key_both_set) +{ + Section section; + section.set("use_tls", "1"); + section.set("tls_client_cert", "/etc/ssl/client.pem"); + section.set("tls_client_key", "/etc/ssl/client.key"); + + auto setup = telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0); + EXPECT_EQ(setup.tlsClientCertPath, "/etc/ssl/client.pem"); + EXPECT_EQ(setup.tlsClientKeyPath, "/etc/ssl/client.key"); +} + +TEST(TelemetryConfig, mtls_cert_without_key_throws) +{ + Section section; + section.set("tls_client_cert", "/etc/ssl/client.pem"); + EXPECT_THROW(telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0), std::runtime_error); +} + +TEST(TelemetryConfig, mtls_key_without_cert_throws) +{ + Section section; + section.set("tls_client_key", "/etc/ssl/client.key"); + EXPECT_THROW(telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0), std::runtime_error); +} + +TEST(TelemetryConfig, mtls_neither_set_is_one_way_tls) +{ + Section section; + section.set("use_tls", "1"); + section.set("tls_ca_cert", "/etc/ssl/ca.pem"); + + auto setup = telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0); + EXPECT_TRUE(setup.tlsClientCertPath.empty()); + EXPECT_TRUE(setup.tlsClientKeyPath.empty()); +} + TEST(TelemetryConfig, null_telemetry_factory) { telemetry::Telemetry::Setup setup; diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index db8a44675e..277b39938f 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -588,7 +588,8 @@ RCLConsensus::Adaptor::doAccept( static_cast( std::chrono::duration_cast(closeResolution).count())); doAcceptSpan.setAttribute( - cs::attr::consensusState, std::string(consensusFail ? "moved_on" : "finished")); + cs::attr::consensusState, + consensusFail ? std::string_view{cs::val::movedOn} : std::string_view{cs::val::finished}); doAcceptSpan.setAttribute(cs::attr::proposing, proposing); doAcceptSpan.setAttribute( cs::attr::roundTimeMs, static_cast(result.roundTime.read().count())); @@ -1284,7 +1285,7 @@ RCLConsensus::Adaptor::startRoundTracing(RCLCxLedger const& prevLgr) roundSpan_->setAttribute(cs::attr::previousProposers, static_cast(prevProposers_)); roundSpan_->setAttribute( cs::attr::previousRoundTimeMs, static_cast(prevRoundTime_.load().count())); - roundSpan_->setAttribute(cs::attr::consensusPhase, "open"); + roundSpan_->setAttribute(cs::attr::consensusPhase, cs::val::phaseOpen); roundSpan_->addEvent(cs::event::phaseOpen); diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 7259c233e4..33bbf1c613 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -387,8 +387,12 @@ void SHAMapStoreImp::dbPaths() { Section const section{app_.config().section(Sections::kNodeDatabase)}; - boost::filesystem::path dbPath = get(section, Keys::kPath); + // Skip creating the directory when an in-memory database is used. + if (boost::iequals(get(section, Keys::kType), "memory")) + return; + + boost::filesystem::path dbPath = get(section, Keys::kPath); if (boost::filesystem::exists(dbPath)) { if (!boost::filesystem::is_directory(dbPath)) diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 5edd73acf7..37b66c1d90 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -18,8 +18,6 @@ #include #include #include -#include -#include #include #include #include @@ -310,7 +308,6 @@ TxQ::MaybeTx::apply(Application& app, OpenView& view, beast::Journal j) { // If the rules or flags change, preflight again XRPL_ASSERT(pfResult, "xrpl::TxQ::MaybeTx::apply : preflight result is set"); - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; // NOLINTBEGIN(bugprone-unchecked-optional-access) assert above if (pfResult->rules != view.rules() || pfResult->flags != flags) @@ -750,8 +747,6 @@ TxQ::apply( // Every other early return leaves the tx rejected from the queue. span.setAttribute(txq_span::attr::txqStatus, txq_span::val::rejected); - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; - // See if the transaction is valid, properly formed, // etc. before doing potentially expensive queue // replace and multi-transaction operations. diff --git a/src/xrpld/consensus/Consensus.h b/src/xrpld/consensus/Consensus.h index dbbcc8a0a9..ecd1b5fc60 100644 --- a/src/xrpld/consensus/Consensus.h +++ b/src/xrpld/consensus/Consensus.h @@ -744,7 +744,9 @@ Consensus::startRoundInternal( // after the new round span is in place. if (reason == StartRoundReason::Recovered) { - adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseOpen, "open"); + adaptor_.onPhaseEvent( + telemetry::consensus::span::event::phaseOpen, + telemetry::consensus::span::val::phaseOpen); } mode_.set(mode, adaptor_); now_ = now; @@ -994,7 +996,9 @@ Consensus::simulate( result_->proposers = prevProposers_ = currPeerPositions_.size(); prevRoundTime_ = result_->roundTime.read(); phase_ = ConsensusPhase::Accepted; - adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseAccepted, "accepted"); + adaptor_.onPhaseEvent( + telemetry::consensus::span::event::phaseAccepted, + telemetry::consensus::span::val::phaseAccepted); adaptor_.onForceAccept( *result_, previousLedger_, closeResolution_, rawCloseTimes_, mode_.get(), getJson(true)); // NOLINTEND(bugprone-unchecked-optional-access) @@ -1474,7 +1478,9 @@ Consensus::phaseEstablish(std::unique_ptr const& clo } } phase_ = ConsensusPhase::Accepted; - adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseAccepted, "accepted"); + adaptor_.onPhaseEvent( + telemetry::consensus::span::event::phaseAccepted, + telemetry::consensus::span::val::phaseAccepted); JLOG(j_.debug()) << "transitioned to ConsensusPhase::Accepted"; adaptor_.onAccept( *result_, @@ -1508,7 +1514,9 @@ Consensus::closeLedger(std::unique_ptr const& clog) } openSpan_.reset(); phase_ = ConsensusPhase::Establish; - adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseEstablish, "establish"); + adaptor_.onPhaseEvent( + telemetry::consensus::span::event::phaseEstablish, + telemetry::consensus::span::val::phaseEstablish); JLOG(j_.debug()) << "transitioned to ConsensusPhase::Establish"; rawCloseTimes_.self = now_; peerUnchangedCounter_ = 0; @@ -1638,7 +1646,9 @@ Consensus::updateOurPositions(std::unique_ptr const& span.addEvent( consensus::span::event::disputeResolve, {{consensus::span::attr::txId, to_string(txId)}, - {consensus::span::attr::disputeOurVote, dispute.getOurVote() ? "yes" : "no"}, + {consensus::span::attr::disputeOurVote, + dispute.getOurVote() ? std::string_view{consensus::span::val::yes} + : std::string_view{consensus::span::val::no}}, {consensus::span::attr::disputeYays, yaysStr}, {consensus::span::attr::disputeNays, naysStr}}); } @@ -1831,6 +1841,38 @@ Consensus::haveConsensus(std::unique_ptr const& clog j_, clog); + // Set span attributes before the early-return branches below so the + // consensus.check span carries diagnostic data even when consensus is + // not reached (the No / Expired paths return early). + span.setAttribute(consensus::span::attr::agreeCount, static_cast(agree)); + span.setAttribute(consensus::span::attr::disagreeCount, static_cast(disagree)); + span.setAttribute( + consensus::span::attr::convergePercent, static_cast(convergePercent_)); + span.setAttribute(consensus::span::attr::haveCloseTimeConsensus, haveCloseTimeConsensus_); + span.setAttribute( + consensus::span::attr::thresholdPercent, + static_cast(adaptor_.parms().avCtConsensusPct)); + span.setAttribute( + consensus::span::attr::proposersFinished, static_cast(currentFinished)); + span.setAttribute(consensus::span::attr::consensusStalled, stalled); + span.setAttribute( + consensus::span::attr::establishCounter, static_cast(establishCounter_)); + + std::string_view stateStr = consensus::span::val::no; + if (result_->state == ConsensusState::Yes) + { + stateStr = consensus::span::val::yes; + } + else if (result_->state == ConsensusState::MovedOn) + { + stateStr = consensus::span::val::movedOn; + } + else if (result_->state == ConsensusState::Expired) + { + stateStr = consensus::span::val::expired; + } + span.setAttribute(consensus::span::attr::consensusResult, stateStr); + if (result_->state == ConsensusState::No) { CLOG(clog) << "No consensus. "; @@ -1872,35 +1914,6 @@ Consensus::haveConsensus(std::unique_ptr const& clog CLOG(clog) << "Unable to reach consensus " << json::Compact{getJson(true)} << ". "; } - span.setAttribute(consensus::span::attr::agreeCount, static_cast(agree)); - span.setAttribute(consensus::span::attr::disagreeCount, static_cast(disagree)); - span.setAttribute( - consensus::span::attr::convergePercent, static_cast(convergePercent_)); - span.setAttribute(consensus::span::attr::haveCloseTimeConsensus, haveCloseTimeConsensus_); - span.setAttribute( - consensus::span::attr::thresholdPercent, - static_cast(adaptor_.parms().avCtConsensusPct)); - span.setAttribute( - consensus::span::attr::proposersFinished, static_cast(currentFinished)); - span.setAttribute(consensus::span::attr::consensusStalled, stalled); - span.setAttribute( - consensus::span::attr::establishCounter, static_cast(establishCounter_)); - - char const* stateStr = "no"; - if (result_->state == ConsensusState::Yes) - { - stateStr = "yes"; - } - else if (result_->state == ConsensusState::MovedOn) - { - stateStr = "moved_on"; - } - else if (result_->state == ConsensusState::Expired) - { - stateStr = "expired"; - } - span.setAttribute(consensus::span::attr::consensusResult, stateStr); - CLOG(clog) << "Consensus has been reached. "; // NOLINTEND(bugprone-unchecked-optional-access) return true; diff --git a/src/xrpld/consensus/ConsensusSpanNames.h b/src/xrpld/consensus/ConsensusSpanNames.h index e9b08b8439..7aa362adb1 100644 --- a/src/xrpld/consensus/ConsensusSpanNames.h +++ b/src/xrpld/consensus/ConsensusSpanNames.h @@ -238,6 +238,10 @@ inline constexpr auto expired = makeStr("expired"); inline constexpr auto increased = makeStr("increased"); inline constexpr auto decreased = makeStr("decreased"); inline constexpr auto unchanged = makeStr("unchanged"); +// consensus_phase attribute values (the phase the round is entering). +inline constexpr auto phaseOpen = makeStr("open"); +inline constexpr auto phaseEstablish = makeStr("establish"); +inline constexpr auto phaseAccepted = makeStr("accepted"); } // namespace val } // namespace xrpl::telemetry::consensus::span diff --git a/src/xrpld/rpc/ServerHandler.h b/src/xrpld/rpc/ServerHandler.h index 0aac05ea44..b1ecf71f82 100644 --- a/src/xrpld/rpc/ServerHandler.h +++ b/src/xrpld/rpc/ServerHandler.h @@ -177,7 +177,11 @@ private: void processSession(std::shared_ptr const&, std::shared_ptr coro); - void + /** Process an RPC request and write the reply to `output`. + @return false if the request resulted in an error response, true + otherwise. Lets the caller's enclosing span reflect the outcome. + */ + bool processRequest( Port const& port, std::string const& request, diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp b/src/xrpld/rpc/detail/RPCHandler.cpp index 7d2a47c49e..2b322b1c24 100644 --- a/src/xrpld/rpc/detail/RPCHandler.cpp +++ b/src/xrpld/rpc/detail/RPCHandler.cpp @@ -192,8 +192,17 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object& rpc_span::attr::rpcStatus, ret ? std::string_view{rpc_span::val::error} : std::string_view{rpc_span::val::success}); - if (!ret) + // Reflect the result in the OTel span status, not just the attribute, + // so non-exception RPC errors (rpcTOO_BUSY, rpcNO_PERMISSION, ...) are + // visible to {status.code=error} queries. + if (ret) + { + span.setError(rpc_span::val::error); + } + else + { span.setOk(); + } return ret; } catch (std::exception& e) @@ -238,6 +247,7 @@ doCommand(RPC::JsonContext& context, json::Value& result) // "unknown" name only when the request truly omits both fields. auto span = SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::command, cmdName); span.setAttribute(rpc_span::attr::command, cmdName.c_str()); + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); span.setError(getErrorInfo(error).token.cStr()); injectError(error, result); diff --git a/src/xrpld/rpc/detail/RpcSpanNames.h b/src/xrpld/rpc/detail/RpcSpanNames.h index e7bae84c2f..88fd299f29 100644 --- a/src/xrpld/rpc/detail/RpcSpanNames.h +++ b/src/xrpld/rpc/detail/RpcSpanNames.h @@ -159,7 +159,7 @@ using telemetry::attr_val::error; using telemetry::attr_val::success; inline constexpr auto admin = makeStr("admin"); inline constexpr auto user = makeStr("user"); -inline constexpr auto unknownCommand = makeStr("unknown_command"); +inline constexpr auto unknownCommand = makeStr("unknown"); /// "invalid_json" — WS message parse failure or oversize. inline constexpr auto invalidJson = makeStr("invalid_json"); } // namespace val diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index 69f197646a..657f76e1de 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -444,6 +444,8 @@ ServerHandler::processSession( session->close({boost::beast::websocket::policy_error, "threshold exceeded"}); // FIX: This rpcError is not delivered since the session // was just closed. + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + span.setError("resource threshold exceeded"); return rpcError(RpcSlowDown); } @@ -475,6 +477,8 @@ ServerHandler::processSession( jr[jss::api_version] = jv[jss::api_version]; is->getConsumer().charge(Resource::kFeeMalformedRpc); + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + span.setError(jr[jss::error].asString()); return jr; } @@ -555,12 +559,19 @@ ServerHandler::processSession( } jr[jss::request] = rq; + // Mark the span according to the final result. Doing it here (rather + // than an unconditional setOk later) ensures error responses — from + // doCommand, a FORBID role, or the catch block above — are not + // overwritten as OK. + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + span.setError(jr[jss::error].asString()); } else { if (jr[jss::result].isMember("forwarded") && jr[jss::result]["forwarded"]) jr = jr[jss::result]; jr[jss::status] = jss::success; + span.setOk(); } if (jv.isMember(jss::id)) @@ -573,7 +584,6 @@ ServerHandler::processSession( jr[jss::api_version] = jv[jss::api_version]; jr[jss::type] = jss::response; - span.setOk(); return jr; } @@ -589,7 +599,7 @@ ServerHandler::processSession( auto const requestBody = ::xrpl::buffersToString(session->request().body().data()); span.setAttribute(rpc_span::attr::requestPayloadSize, static_cast(requestBody.size())); - processRequest( + bool const ok = processRequest( session->port(), requestBody, session->remoteAddress().atPort(0), @@ -611,7 +621,15 @@ ServerHandler::processSession( { session->close(true); } - span.setOk(); + // Reflect the request outcome on the wrapper span instead of always OK. + if (ok) + { + span.setOk(); + } + else + { + span.setError(rpc_span::val::error); + } } static json::Value @@ -630,7 +648,7 @@ constexpr json::Int kServerOverloaded = -32604; constexpr json::Int kForbidden = -32605; constexpr json::Int kWrongVersion = -32606; -void +bool ServerHandler::processRequest( Port const& port, std::string const& request, @@ -643,18 +661,30 @@ ServerHandler::processRequest( auto span = SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process); auto rpcJ = app_.getJournal("RPC"); + // Tracks whether any failure occurred. Set on every error path (early + // returns, the catch block, and per-request error replies) and used at the + // end to mark the span status. The HTTP status code alone is insufficient: + // it stays 200 for batch responses and for ripplerpc < 3.0, so relying on + // it would let payload-level errors end the span as successful. + bool spanHadError = false; + + // Marks the span as failed before sending an error reply, so the + // early-return validation paths below are not later seen as successful + // (the span would otherwise end UNSET, invisible to {status.code=error}). + auto httpReplyError = [&](int status, std::string const& message) { + spanHadError = true; + span.setError(message); + httpReply(status, message, output, rpcJ); + }; + json::Value jsonOrig; { json::Reader reader; if ((request.size() > RPC::Tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) || !jsonOrig || !jsonOrig.isObject()) { - httpReply( - 400, - "Unable to parse request: " + reader.getFormattedErrorMessages(), - output, - rpcJ); - return; + httpReplyError(400, "Unable to parse request: " + reader.getFormattedErrorMessages()); + return false; } } @@ -665,8 +695,8 @@ ServerHandler::processRequest( batch = true; if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray()) { - httpReply(400, "Malformed batch request", output, rpcJ); - return; + httpReplyError(400, "Malformed batch request"); + return false; } size = jsonOrig[jss::params].size(); } @@ -707,8 +737,8 @@ ServerHandler::processRequest( { if (!batch) { - httpReply(400, jss::invalid_API_version.cStr(), output, rpcJ); - return; + httpReplyError(400, jss::invalid_API_version.cStr()); + return false; } json::Value r(json::ValueType::Object); r[jss::request] = jsonRPC; @@ -750,8 +780,8 @@ ServerHandler::processRequest( { if (!batch) { - httpReply(503, "Server is overloaded", output, rpcJ); - return; + httpReplyError(503, "Server is overloaded"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kServerOverloaded, "Server is overloaded"); @@ -765,8 +795,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(403, "Forbidden", output, rpcJ); - return; + httpReplyError(403, "Forbidden"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kForbidden, "Forbidden"); @@ -779,8 +809,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(400, "Null method", output, rpcJ); - return; + httpReplyError(400, "Null method"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kMethodNotFound, "Null method"); @@ -794,8 +824,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(400, "method is not string", output, rpcJ); - return; + httpReplyError(400, "method is not string"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kMethodNotFound, "method is not string"); @@ -809,8 +839,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(400, "method is empty", output, rpcJ); - return; + httpReplyError(400, "method is empty"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kMethodNotFound, "method is empty"); @@ -835,8 +865,8 @@ ServerHandler::processRequest( else if (!params.isArray() || params.size() != 1) { usage.charge(Resource::kFeeMalformedRpc); - httpReply(400, "params unparsable", output, rpcJ); - return; + httpReplyError(400, "params unparsable"); + return false; } else { @@ -844,8 +874,8 @@ ServerHandler::processRequest( if (!params.isObjectOrNull()) { usage.charge(Resource::kFeeMalformedRpc); - httpReply(400, "params unparsable", output, rpcJ); - return; + httpReplyError(400, "params unparsable"); + return false; } } } @@ -862,8 +892,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(400, "ripplerpc is not a string", output, rpcJ); - return; + httpReplyError(400, "ripplerpc is not a string"); + return false; } json::Value r = jsonRPC; @@ -922,6 +952,7 @@ ServerHandler::processRequest( << " when processing request: " << json::Compact{json::Value{params}}; span.recordException(ex); span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + spanHadError = true; // LCOV_EXCL_STOP } @@ -938,6 +969,7 @@ ServerHandler::processRequest( { if (result.isMember(jss::error)) { + spanHadError = true; result[jss::status] = jss::error; result["code"] = result[jss::error_code]; result["message"] = result[jss::error_message]; @@ -958,6 +990,7 @@ ServerHandler::processRequest( // received. if (result.isMember(jss::error)) { + spanHadError = true; auto rq = params; if (rq.isObject()) @@ -1053,8 +1086,20 @@ ServerHandler::processRequest( } } - span.setOk(); + // Mark the span error if any request failed or the HTTP status is an error. + // spanHadError catches payload-level errors that httpStatus misses (batch + // responses and ripplerpc < 3.0 always return HTTP 200). + if (spanHadError || httpStatus >= 400) + { + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + span.setError(rpc_span::val::error); + } + else + { + span.setOk(); + } httpReply(httpStatus, response, output, rpcJ); + return !spanHadError; } //------------------------------------------------------------------------------ diff --git a/src/xrpld/telemetry/ConsensusReceiveTracing.h b/src/xrpld/telemetry/ConsensusReceiveTracing.h index 63bc34b243..3c7ea42ca6 100644 --- a/src/xrpld/telemetry/ConsensusReceiveTracing.h +++ b/src/xrpld/telemetry/ConsensusReceiveTracing.h @@ -31,25 +31,19 @@ * span.setAttribute(...); * @endcode * - * @note These span names use inline string_view literals. When - * ConsensusSpanNames.h (from Phase 4) is available, callers should - * migrate to using the constexpr constants defined there. + * @note Span names come from the canonical constants in + * ConsensusSpanNames.h (consensus::span::proposalReceive / + * validationReceive) so they stay in sync with the rest of Phase 4. */ +#include + #include #include #include namespace xrpl::telemetry { -// Inline span name constants for consensus receive spans. -// Phase 4 will provide these via ConsensusSpanNames.h; these are -// temporary definitions for the propagation infrastructure. -namespace detail { -inline constexpr std::string_view proposalReceiveName = "consensus.proposal.receive"; -inline constexpr std::string_view validationReceiveName = "consensus.validation.receive"; -} // namespace detail - /** Create a "consensus.proposal.receive" span for an incoming proposal. * * If the message carries a TraceContext with a valid span_id, the @@ -75,7 +69,7 @@ proposalReceiveSpan([[maybe_unused]] protocol::TMProposeSet const& msg) // trace_id so the receiving span shares the same trace. return SpanGuard::hashSpan( TraceCategory::Consensus, - detail::proposalReceiveName, + consensus::span::proposalReceive, reinterpret_cast(tc.trace_id().data()), tc.trace_id().size(), reinterpret_cast(tc.span_id().data()), @@ -86,7 +80,8 @@ proposalReceiveSpan([[maybe_unused]] protocol::TMProposeSet const& msg) } #endif // No propagated context — create a standalone span. - return SpanGuard::span(TraceCategory::Consensus, "consensus", "proposal.receive"); + return SpanGuard::span( + TraceCategory::Consensus, seg::consensus, consensus::span::op::proposalReceive); } /** Create a "consensus.validation.receive" span for an incoming validation. @@ -111,7 +106,7 @@ validationReceiveSpan([[maybe_unused]] protocol::TMValidation const& msg) { return SpanGuard::hashSpan( TraceCategory::Consensus, - detail::validationReceiveName, + consensus::span::validationReceive, reinterpret_cast(tc.trace_id().data()), tc.trace_id().size(), reinterpret_cast(tc.span_id().data()), @@ -122,7 +117,8 @@ validationReceiveSpan([[maybe_unused]] protocol::TMValidation const& msg) } #endif // No propagated context — create a standalone span. - return SpanGuard::span(TraceCategory::Consensus, "consensus", "validation.receive"); + return SpanGuard::span( + TraceCategory::Consensus, seg::consensus, consensus::span::op::validationReceive); } } // namespace xrpl::telemetry