mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 22:30:57 +00:00
The reference docs had drifted from the code in ways that break the reader rather than merely misinform: PromQL examples that return no data, a rollback flag that is a no-op, a sampling knob that does not exist, and two span parents that moved. Code is treated as the truth throughout; where the code is the defective side, the doc now records it as a known issue instead of describing the bug as intent. Renames the docs missed: histogram names gain the exporter's unit suffix (ios_latency_milliseconds_bucket and four siblings), ledger_history_mismatch gains _total, the StatsD-era quantile label gives way to le buckets, rpc.request becomes rpc.http_request, traces_spanmetrics_calls_total becomes span_calls_total, and the nine dotted xrpl.* span attributes are recorded as renamed rather than left as live keys. Re-parenting: consensus.update_positions and consensus.check are children of consensus.establish, not of consensus.round. Units and labels: state_accounting_*_duration is microseconds, not seconds; cache_metrics label values are case-sensitive; object_count carries demangled C++ type names. Nodestore read and write latency stays microseconds -- the nanosecond accumulator change did not move the exported unit. Adds what shipped but was undocumented: the ledger.acquire span, seven consensus.round events, twelve span attributes, node_writes_duration_us, the 7-day validation-agreement window, the TxQ admission and reduce-relay metric families, metrics_endpoint, and the phase-10 validation workflow. Corrects claims that never held: 10% head sampling (it is fixed at 100%), configurable redaction (it is unconditional), -DXRPL_ENABLE_TELEMETRY=OFF (the flag is -Dtelemetry=OFF, default ON), FindOpenTelemetry.cmake and the xrpl_telemetry target (neither exists), Promtail and a StatsD exporter in the pipeline (neither exists), and Loki stream selection on job= (only service_name is a stream label). Phase 9 is marked complete, its provisioned alerting is attributed to the branch that shipped it, and Phase 11 stays at zero except the one prerequisite its code closes. Counts are reconciled repo-wide: 41 emitted span families, 15 dashboards on disk with 14 asserted, 13 alert rules in 5 groups. Hardens the gate that let this drift through: Rule E of the naming check now covers the reference docs, its allow-dotted marker is key-scoped and warns on stale or empty use, a missing checked file is reported instead of silently skipped, the test suite runs in CI, and doc paths trigger the check. C++ and CMake changes are comment-only: three MetricsRegistry instrument names, eight OTelCollector claims of a metric-name prefix that formatName never adds, and the telemetry option's inverted default.
247 lines
11 KiB
Markdown
247 lines
11 KiB
Markdown
# OpenTelemetry Tracing for xrpld
|
|
|
|
This document explains how to build xrpld with OpenTelemetry distributed tracing support, configure the runtime telemetry options, and set up the observability backend to view traces.
|
|
|
|
- [OpenTelemetry Tracing for xrpld](#opentelemetry-tracing-for-xrpld)
|
|
- [Overview](#overview)
|
|
- [Building with Telemetry](#building-with-telemetry)
|
|
- [Summary](#summary)
|
|
- [Build steps](#build-steps)
|
|
- [Install dependencies](#install-dependencies)
|
|
- [Call CMake](#call-cmake)
|
|
- [Build](#build)
|
|
- [Building without telemetry](#building-without-telemetry)
|
|
- [Troubleshooting](#troubleshooting)
|
|
- [Conan lockfile error](#conan-lockfile-error)
|
|
- [CMake target not found](#cmake-target-not-found)
|
|
- [Conditional compilation](#conditional-compilation)
|
|
- [Span lifetime and cross-thread handling](#span-lifetime-and-cross-thread-handling)
|
|
- [`SpanGuard` versus `ScopedSpanGuard`](#spanguard-versus-scopedspanguard)
|
|
- [Coroutine-aware context storage](#coroutine-aware-context-storage)
|
|
- [Handing a span to a job](#handing-a-span-to-a-job)
|
|
- [Why are unrelated spans in my trace?](#why-are-unrelated-spans-in-my-trace)
|
|
|
|
## Overview
|
|
|
|
xrpld supports optional [OpenTelemetry](https://opentelemetry.io/) distributed tracing.
|
|
When enabled, it instruments RPC requests with trace spans that are exported via
|
|
OTLP/HTTP to an OpenTelemetry Collector, which forwards them to a tracing backend
|
|
such as Grafana Tempo.
|
|
|
|
Telemetry is gated twice — once at compile time and once at runtime:
|
|
|
|
- **Compile time**: The Conan option `telemetry` and CMake option `telemetry` decide
|
|
whether the OTel SDK is linked in and `XRPL_ENABLE_TELEMETRY` is defined.
|
|
When off, all `SpanGuard` calls compile to inline no-ops (defined in `SpanGuard.h`)
|
|
with zero overhead — no OTel SDK dependency required.
|
|
The option is currently `True`/`ON` on the telemetry branches so that CI builds and
|
|
exercises the instrumented code; **`False`/`OFF` is the intended default once this
|
|
feature is merged.** Pass the value you want explicitly rather than relying on the
|
|
default.
|
|
- **Runtime**: Telemetry is **off by default** — the `[telemetry]` config section must
|
|
set `enabled=1`. When disabled at runtime, a no-op implementation is used even in a
|
|
build that has the SDK compiled in.
|
|
|
|
## Building with Telemetry
|
|
|
|
### Summary
|
|
|
|
Follow the same instructions as mentioned in [BUILD.md](../../BUILD.md) but with the following changes:
|
|
|
|
1. Pass `-o telemetry=True` to `conan install` to pull the `opentelemetry-cpp` dependency.
|
|
2. CMake will automatically pick up `telemetry=ON` from the Conan-generated toolchain.
|
|
3. Build as usual.
|
|
|
|
---
|
|
|
|
### Build steps
|
|
|
|
```bash
|
|
cd /path/to/xrpld
|
|
rm -rf .build
|
|
mkdir .build
|
|
cd .build
|
|
```
|
|
|
|
#### Install dependencies
|
|
|
|
The `telemetry` option adds `opentelemetry-cpp/1.28.0` as a dependency.
|
|
If the Conan lockfile does not yet include this package, bypass it with `--lockfile=""`.
|
|
|
|
```bash
|
|
conan install .. \
|
|
--output-folder . \
|
|
--build missing \
|
|
--settings build_type=Debug \
|
|
-o telemetry=True \
|
|
-o tests=True \
|
|
-o xrpld=True \
|
|
--lockfile=""
|
|
```
|
|
|
|
> **Note**: The first build with telemetry may take longer as `opentelemetry-cpp`
|
|
> and its transitive dependencies are compiled from source.
|
|
|
|
#### Call CMake
|
|
|
|
The Conan-generated toolchain file sets `telemetry=ON` automatically.
|
|
No additional CMake flags are needed beyond the standard ones.
|
|
|
|
```bash
|
|
cmake .. -G Ninja \
|
|
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
|
|
-DCMAKE_BUILD_TYPE=Debug \
|
|
-Dtests=ON -Dxrpld=ON
|
|
```
|
|
|
|
You should see in the CMake output:
|
|
|
|
```
|
|
-- OpenTelemetry tracing enabled
|
|
```
|
|
|
|
#### Build
|
|
|
|
```bash
|
|
cmake --build . --parallel $(nproc)
|
|
```
|
|
|
|
## Building without telemetry
|
|
|
|
Pass `-o telemetry=False` to `conan install`, and `-Dtelemetry=OFF` to CMake if you
|
|
configure without the Conan-generated toolchain. Do not just omit the option — it then
|
|
resolves to whatever the current default is, and that default is `True` on the
|
|
telemetry branches.
|
|
|
|
The `opentelemetry-cpp` dependency will not be downloaded,
|
|
the `XRPL_ENABLE_TELEMETRY` preprocessor define will not be set,
|
|
and all tracing macros will compile to no-ops.
|
|
The resulting binary is identical to one built before telemetry support was added.
|
|
|
|
> **`-DXRPL_ENABLE_TELEMETRY=OFF` disables nothing.** `XRPL_ENABLE_TELEMETRY` is not a
|
|
> CMake option — it is only a compile definition added when `telemetry` is on. Passing it
|
|
> on the command line leaves telemetry compiled in; CMake merely lists it at the end of
|
|
> configuration under `Manually-specified variables were not used by the project`.
|
|
> Use `-Dtelemetry=OFF`.
|
|
|
|
## Troubleshooting
|
|
|
|
### Conan lockfile error
|
|
|
|
If you see `ERROR: Requirement 'opentelemetry-cpp/1.28.0' not in lockfile 'requires'`,
|
|
the lockfile was generated without the telemetry dependency.
|
|
Pass `--lockfile=""` to bypass the lockfile, or regenerate it with telemetry enabled.
|
|
|
|
### CMake target not found
|
|
|
|
If CMake reports that `opentelemetry-cpp` targets are not found,
|
|
ensure you ran `conan install` with `-o telemetry=True` and that the
|
|
Conan-generated toolchain file is being used.
|
|
The Conan package provides a single umbrella target
|
|
`opentelemetry-cpp::opentelemetry-cpp` (not individual component targets).
|
|
|
|
## Conditional compilation
|
|
|
|
All OpenTelemetry SDK types are hidden behind the pimpl idiom in `SpanGuard.cpp`.
|
|
When `XRPL_ENABLE_TELEMETRY` is not defined, `SpanGuard.h` provides an all-inline
|
|
no-op stub class with zero overhead and zero OTel dependencies.
|
|
At runtime, if `enabled=0` is set in config (or the section is omitted), a
|
|
`NullTelemetry` implementation is used that returns no-op spans.
|
|
This two-layer approach ensures zero overhead when telemetry is not wanted.
|
|
|
|
## Span lifetime and cross-thread handling
|
|
|
|
Telemetry exposes two RAII guards with split responsibilities, plus a
|
|
non-owning activation helper. Picking the right one is what keeps a trace's
|
|
parent/child nesting and its per-line log correlation correct.
|
|
|
|
### `SpanGuard` versus `ScopedSpanGuard`
|
|
|
|
- **`SpanGuard`** owns a span and nothing else. It is _thread-free_: it never
|
|
touches the active-context stack, so it carries no thread affinity and may be
|
|
moved to and ended on any thread. It is movable and move-assignable. Create
|
|
one with `SpanGuard::span(cat, prefix, name)`, or with
|
|
`SpanGuard::freshRoot(...)` to start a fresh trace root that ignores whatever
|
|
span is currently ambient. Reach for a plain `SpanGuard` whenever the span
|
|
must leave the context store that created it — for example when it is handed
|
|
into a job.
|
|
|
|
- **`ScopedSpanGuard`** owns a `SpanGuard` _plus_ an active OTel scope that
|
|
pushes the span onto the current context store. While it lives, the span is
|
|
the ambient parent for child spans created on that store, and log lines
|
|
emitted under it carry its `trace_id`. It is non-copyable and non-movable —
|
|
short-lived stack RAII. It offers the same factories (`freshRoot(...)`,
|
|
`childSpan(...)`). When the span must outlive the scope, convert it with
|
|
`operator SpanGuard() &&`: that pops the scope on the origin store and yields
|
|
the bare, thread-free `SpanGuard`.
|
|
|
|
Rule of thumb: use `ScopedSpanGuard` for same-thread (or same-coroutine)
|
|
nesting and log correlation; use the plain `SpanGuard` whenever the span
|
|
crosses out of the store that created it.
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
SG["SpanGuard<br/>(unscoped, thread-free)<br/>owns span only; movable across threads and coroutines"]
|
|
SSG["ScopedSpanGuard<br/>(scoped, store-bound)<br/>owns a SpanGuard plus an active scope on the current store"]
|
|
SA["ScopedActivation<br/>(non-owning)<br/>activates a borrowed span; never owns or ends it"]
|
|
|
|
SSG -->|"handoff: pop scope, yield bare span"| SG
|
|
SG -->|"activate / activateIfLive"| SA
|
|
SA -.->|"borrows span, no ownership"| SG
|
|
|
|
classDef box fill:#e8f0fe,stroke:#3b5bdb,color:#111827;
|
|
class SG,SSG,SA box;
|
|
```
|
|
|
|
### Coroutine-aware context storage
|
|
|
|
The active-context stack is not a plain `thread_local`. At telemetry start
|
|
xrpld installs `CoroAwareContextStorage`, which keeps the stack in an
|
|
`xrpl::LocalValue`. Because `JobQueue::Coro::resume()` swaps the coroutine's
|
|
`LocalValue` store in and out with the coroutine, the ambient context _follows
|
|
the coroutine_ across every yield and resume — even when it resumes on a
|
|
different worker thread. A `ScopedSpanGuard` held across a coroutine yield is
|
|
therefore safe: its scope rides the coroutine and pops on the same store it was
|
|
pushed onto, so it never pops the wrong stack. Off a coroutine the `LocalValue`
|
|
transparently gives each thread its own store, so behaviour matches OTel's
|
|
default thread-local storage. This is what lets the RPC entry, process, and
|
|
command spans be scoped — for correct nesting and per-line log-trace
|
|
correlation — even though the RPC path yields.
|
|
|
|
### Handing a span to a job
|
|
|
|
The hand-off pattern is: create a thread-free `SpanGuard` at the origin (or
|
|
convert a `ScopedSpanGuard` with `operator SpanGuard() &&`), move it into the
|
|
job closure, and inside the worker body activate it non-destructively with
|
|
`telemetry::activateIfLive(handle)`. That call takes no ownership and returns a
|
|
`ScopedActivation` (a no-op if the handle is empty or the span inactive) which
|
|
makes the span the ambient context so log lines in the worker body carry its
|
|
`trace_id`. The activation neither owns nor ends the span — the owning
|
|
`SpanGuard` still controls its lifetime and ends it when the closure is
|
|
destroyed. Keep the activation confined to a synchronous, non-yielding block.
|
|
There is no detach step: a `SpanGuard` is already thread-free.
|
|
|
|
### Why are unrelated spans in my trace?
|
|
|
|
Historically a scoped guard destroyed off its origin thread popped the wrong
|
|
context stack, leaving a stale ambient span in place that later work inherited.
|
|
The current design removes that failure mode in two ways:
|
|
|
|
- **Coroutine-aware storage** makes a scope held across a coroutine yield pop on
|
|
the same store it was pushed onto, so a coroutine that resumes on another
|
|
worker never pops the wrong stack.
|
|
|
|
- **A same-store assertion** in `ScopedSpanGuard` (and `ScopedActivation`)
|
|
records the `LocalValue` store its scope was pushed onto and checks, in
|
|
debug/test/fuzzing builds, that destruction, hand-off, and discard all happen
|
|
while that same store is active — turning a genuine cross-store misuse into an
|
|
immediate assertion failure rather than a silently corrupted trace.
|
|
|
|
If a trace still shows unrelated spans nested under one operation, the usual
|
|
cause is an inbound entry point that inherited an ambient parent it should not
|
|
have. Start such an operation with `freshRoot()` so it begins a clean trace
|
|
root and never adopts whatever span happened to be active. To move a span
|
|
across a store boundary, keep it in a thread-free `SpanGuard` (or convert via
|
|
`operator SpanGuard() &&`) rather than holding a `ScopedSpanGuard` across the
|
|
boundary.
|