mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-11 12:46:44 +00:00
Compare commits
214 Commits
develop
...
pratik/ote
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f37589b1f5 | ||
|
|
4ed409f35a | ||
|
|
d126868a25 | ||
|
|
56f2e2c4cf | ||
|
|
34d7280df4 | ||
|
|
8908036b11 | ||
|
|
e205d0ef8e | ||
|
|
38fbab1d18 | ||
|
|
c128625857 | ||
|
|
e11bf35691 | ||
|
|
331d9d55b1 | ||
|
|
4ce882965a | ||
|
|
385c3cd91c | ||
|
|
7a4baab7cb | ||
|
|
3cb9a2bf51 | ||
|
|
b32db4ceeb | ||
|
|
207864a98b | ||
|
|
57d382ceda | ||
|
|
848cbcbfbe | ||
|
|
4f53291fe8 | ||
|
|
f3fc85fd2b | ||
|
|
8fe3f06999 | ||
|
|
d3c09fd3f4 | ||
|
|
142e8c5b36 | ||
|
|
e5f890e195 | ||
|
|
c712968890 | ||
|
|
fa71280795 | ||
|
|
bb8f7f0e9a | ||
|
|
5598b0eac7 | ||
|
|
0628dc172e | ||
|
|
be67ad25e7 | ||
|
|
3ee8f900ec | ||
|
|
a119efc478 | ||
|
|
a946ce7458 | ||
|
|
57a54ad0fe | ||
|
|
fe13359024 | ||
|
|
ea5991fb0a | ||
|
|
f5f13df5ff | ||
|
|
f12fa8d3a9 | ||
|
|
3df7e9cba6 | ||
|
|
6a16dfa823 | ||
|
|
6428c9f13c | ||
|
|
dfd67b8124 | ||
|
|
a13a858112 | ||
|
|
8dd5ac55e8 | ||
|
|
507828edde | ||
|
|
aca6623f14 | ||
|
|
dd9cde88f3 | ||
|
|
1a2f9a71f5 | ||
|
|
ebf107e73c | ||
|
|
d5f9242f84 | ||
|
|
84fc829be3 | ||
|
|
bdd7dea4b4 | ||
|
|
b46ee12a19 | ||
|
|
994e425804 | ||
|
|
967f0082c3 | ||
|
|
b8602b7821 | ||
|
|
7bc6c65bb2 | ||
|
|
d6fefe2468 | ||
|
|
781e08a6a6 | ||
|
|
1fd0ee5999 | ||
|
|
771eb3d66c | ||
|
|
b24456abb4 | ||
|
|
ded9847eaf | ||
|
|
9918803333 | ||
|
|
bf527a41dd | ||
|
|
154d441ff2 | ||
|
|
046e2e2b85 | ||
|
|
9f81e770eb | ||
|
|
670b6ef3d5 | ||
|
|
5d1e3f207c | ||
|
|
e321f294e5 | ||
|
|
1a0780fd3e | ||
|
|
5dd5e765ae | ||
|
|
c157253372 | ||
|
|
c187a62353 | ||
|
|
c848e51e13 | ||
|
|
8395e69e94 | ||
|
|
8f9057729c | ||
|
|
f031befc6e | ||
|
|
4e8d37facf | ||
|
|
071ad45d31 | ||
|
|
c9901595f7 | ||
|
|
3a1f22583f | ||
|
|
e1163f7180 | ||
|
|
f66a53cfc9 | ||
|
|
68a69d9064 | ||
|
|
53e8c4d54e | ||
|
|
d6b101069e | ||
|
|
c9521b97fe | ||
|
|
954223958f | ||
|
|
c6c019ed8b | ||
|
|
43258e8dc0 | ||
|
|
9e89d74d2f | ||
|
|
8b790ebac9 | ||
|
|
4bd1176df5 | ||
|
|
4c4c6f5de2 | ||
|
|
9498b2865f | ||
|
|
64ffcffe32 | ||
|
|
bfdcd3da87 | ||
|
|
f6f0cb1a5f | ||
|
|
6aa8570d6c | ||
|
|
824f63216a | ||
|
|
a104140a51 | ||
|
|
45e1c15d24 | ||
|
|
865ab65a07 | ||
|
|
009c63e7db | ||
|
|
5d70a5fffd | ||
|
|
f3a095ab65 | ||
|
|
3e894f8e93 | ||
|
|
cb7dc5c52e | ||
|
|
9cfb43d8d0 | ||
|
|
7ada57e2a8 | ||
|
|
b392035544 | ||
|
|
450004ebd8 | ||
|
|
6f403fdd1b | ||
|
|
9bc2e4abb3 | ||
|
|
7b9e0bc300 | ||
|
|
5c14b57462 | ||
|
|
c875944e05 | ||
|
|
2430032e3a | ||
|
|
0f63d14999 | ||
|
|
faaec003f4 | ||
|
|
e339ba1f6b | ||
|
|
ac1b01b4c7 | ||
|
|
497dd007d9 | ||
|
|
0d845149ec | ||
|
|
7a854ccad2 | ||
|
|
937d11d7c3 | ||
|
|
cf18032e7f | ||
|
|
bb3732c22f | ||
|
|
87a8780b73 | ||
|
|
78522ba18e | ||
|
|
79fbb9c303 | ||
|
|
e21e7b0d51 | ||
|
|
61cb1faf8f | ||
|
|
5cbb349efa | ||
|
|
93bed03d8d | ||
|
|
581ab8f552 | ||
|
|
6154357daa | ||
|
|
3a1e462bef | ||
|
|
30af98200f | ||
|
|
ff27e62e1f | ||
|
|
7e93e75d8e | ||
|
|
ecd02134fa | ||
|
|
3c0eec0209 | ||
|
|
7b9e2cf91f | ||
|
|
312dec2baa | ||
|
|
be812b8d21 | ||
|
|
e63a5f72be | ||
|
|
1e2287e6e1 | ||
|
|
3508917f17 | ||
|
|
3ed22580fe | ||
|
|
20fabbc0ec | ||
|
|
831be14fd9 | ||
|
|
019e84f0d2 | ||
|
|
694abe2004 | ||
|
|
e07391fe78 | ||
|
|
9515177e29 | ||
|
|
bd6e58a20e | ||
|
|
e8c826c816 | ||
|
|
d753191d20 | ||
|
|
d4e91b462e | ||
|
|
fa2736277f | ||
|
|
196c309d1d | ||
|
|
d46d015fd5 | ||
|
|
999bf83f15 | ||
|
|
96470e0c8d | ||
|
|
ed8164d502 | ||
|
|
682d7a8d76 | ||
|
|
eb51457e69 | ||
|
|
65817c4c57 | ||
|
|
9bc8cc6b4e | ||
|
|
832648c351 | ||
|
|
21b58a8885 | ||
|
|
a9ee819ea1 | ||
|
|
736579e473 | ||
|
|
3b93e2d4d9 | ||
|
|
ac9bd2c055 | ||
|
|
4124762343 | ||
|
|
ea8600e204 | ||
|
|
895e9167b0 | ||
|
|
d15d2d2df6 | ||
|
|
75bcd4ff53 | ||
|
|
a73117ddd0 | ||
|
|
9e4d943c69 | ||
|
|
025a8a344b | ||
|
|
9ee9e566d4 | ||
|
|
0de807b1be | ||
|
|
59ee027d8a | ||
|
|
7aa4486741 | ||
|
|
5e8277f36a | ||
|
|
573593ae31 | ||
|
|
a5c405f4be | ||
|
|
e9c5c3520e | ||
|
|
26947267b1 | ||
|
|
4bb2030315 | ||
|
|
3852b5ae4b | ||
|
|
ea921d3a02 | ||
|
|
ca2d616277 | ||
|
|
88686af850 | ||
|
|
7f4ef83df0 | ||
|
|
1fd971b78b | ||
|
|
d6c8dec451 | ||
|
|
30ecb32a6f | ||
|
|
a01b274352 | ||
|
|
193f5b39cb | ||
|
|
db8111ef7c | ||
|
|
913a4b794c | ||
|
|
accea17e9d | ||
|
|
c6fa00fbe3 | ||
|
|
bfb8f4f01a | ||
|
|
4b745a86b7 | ||
|
|
ddf894dcb0 |
@@ -4,6 +4,9 @@ Loop: test.jtx test.toplevel
|
||||
Loop: test.jtx test.unit_test
|
||||
test.unit_test ~= test.jtx
|
||||
|
||||
Loop: xrpl.telemetry xrpld.rpc
|
||||
xrpld.rpc > xrpl.telemetry
|
||||
|
||||
Loop: xrpld.app xrpld.overlay
|
||||
xrpld.app > xrpld.overlay
|
||||
|
||||
@@ -16,6 +19,9 @@ Loop: xrpld.app xrpld.rpc
|
||||
Loop: xrpld.app xrpld.shamap
|
||||
xrpld.shamap > xrpld.app
|
||||
|
||||
Loop: xrpld.app xrpld.telemetry
|
||||
xrpld.telemetry ~= xrpld.app
|
||||
|
||||
Loop: xrpld.overlay xrpld.rpc
|
||||
xrpld.rpc ~= xrpld.overlay
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ libxrpl.shamap > xrpl.basics
|
||||
libxrpl.shamap > xrpl.nodestore
|
||||
libxrpl.shamap > xrpl.protocol
|
||||
libxrpl.shamap > xrpl.shamap
|
||||
libxrpl.telemetry > xrpl.basics
|
||||
libxrpl.telemetry > xrpl.config
|
||||
libxrpl.telemetry > xrpl.telemetry
|
||||
libxrpl.tx > xrpl.basics
|
||||
libxrpl.tx > xrpl.conditions
|
||||
libxrpl.tx > xrpl.core
|
||||
@@ -53,6 +56,7 @@ libxrpl.tx > xrpl.json
|
||||
libxrpl.tx > xrpl.ledger
|
||||
libxrpl.tx > xrpl.protocol
|
||||
libxrpl.tx > xrpl.server
|
||||
libxrpl.tx > xrpl.telemetry
|
||||
libxrpl.tx > xrpl.tx
|
||||
test.app > test.jtx
|
||||
test.app > test.unit_test
|
||||
@@ -211,6 +215,7 @@ tests.libxrpl > xrpl.protocol
|
||||
tests.libxrpl > xrpl.protocol_autogen
|
||||
tests.libxrpl > xrpl.server
|
||||
tests.libxrpl > xrpl.shamap
|
||||
tests.libxrpl > xrpl.telemetry
|
||||
tests.libxrpl > xrpl.tx
|
||||
xrpl.conditions > xrpl.basics
|
||||
xrpl.conditions > xrpl.protocol
|
||||
@@ -247,10 +252,13 @@ xrpl.server > xrpl.shamap
|
||||
xrpl.shamap > xrpl.basics
|
||||
xrpl.shamap > xrpl.nodestore
|
||||
xrpl.shamap > xrpl.protocol
|
||||
xrpl.telemetry > xrpl.config
|
||||
xrpl.telemetry > xrpld.consensus
|
||||
xrpl.tx > xrpl.basics
|
||||
xrpl.tx > xrpl.core
|
||||
xrpl.tx > xrpl.ledger
|
||||
xrpl.tx > xrpl.protocol
|
||||
xrpl.tx > xrpl.telemetry
|
||||
xrpld.app > test.unit_test
|
||||
xrpld.app > xrpl.basics
|
||||
xrpld.app > xrpl.config
|
||||
@@ -266,6 +274,7 @@ xrpld.app > xrpl.rdb
|
||||
xrpld.app > xrpl.resource
|
||||
xrpld.app > xrpl.server
|
||||
xrpld.app > xrpl.shamap
|
||||
xrpld.app > xrpl.telemetry
|
||||
xrpld.app > xrpl.tx
|
||||
xrpld.consensus > xrpl.basics
|
||||
xrpld.consensus > xrpl.json
|
||||
@@ -283,12 +292,14 @@ xrpld.overlay > xrpl.core
|
||||
xrpld.overlay > xrpld.consensus
|
||||
xrpld.overlay > xrpld.core
|
||||
xrpld.overlay > xrpld.peerfinder
|
||||
xrpld.overlay > xrpld.telemetry
|
||||
xrpld.overlay > xrpl.json
|
||||
xrpld.overlay > xrpl.ledger
|
||||
xrpld.overlay > xrpl.protocol
|
||||
xrpld.overlay > xrpl.resource
|
||||
xrpld.overlay > xrpl.server
|
||||
xrpld.overlay > xrpl.shamap
|
||||
xrpld.overlay > xrpl.telemetry
|
||||
xrpld.overlay > xrpl.tx
|
||||
xrpld.peerfinder > xrpl.basics
|
||||
xrpld.peerfinder > xrpl.config
|
||||
@@ -319,3 +330,5 @@ xrpld.shamap > xrpl.basics
|
||||
xrpld.shamap > xrpld.core
|
||||
xrpld.shamap > xrpl.protocol
|
||||
xrpld.shamap > xrpl.shamap
|
||||
xrpld.telemetry > xrpl.basics
|
||||
xrpld.telemetry > xrpl.telemetry
|
||||
|
||||
@@ -117,6 +117,18 @@ if(rocksdb)
|
||||
target_link_libraries(xrpl_libs INTERFACE RocksDB::rocksdb)
|
||||
endif()
|
||||
|
||||
# OpenTelemetry distributed tracing (optional).
|
||||
# When ON, links against opentelemetry-cpp and defines XRPL_ENABLE_TELEMETRY
|
||||
# so that SpanGuard factory methods produce real OTel spans.
|
||||
# When OFF (default), all tracing code compiles to no-ops with zero overhead.
|
||||
# Enable via: conan install -o telemetry=True, or cmake -Dtelemetry=ON.
|
||||
option(telemetry "Enable OpenTelemetry tracing" ON)
|
||||
if(telemetry)
|
||||
find_package(opentelemetry-cpp CONFIG REQUIRED)
|
||||
add_compile_definitions(XRPL_ENABLE_TELEMETRY)
|
||||
message(STATUS "OpenTelemetry tracing enabled")
|
||||
endif()
|
||||
|
||||
# Work around changes to Conan recipe for now.
|
||||
if(TARGET nudb::core)
|
||||
set(nudb nudb::core)
|
||||
|
||||
574
OpenTelemetryPlan/00-tracing-fundamentals.md
Normal file
574
OpenTelemetryPlan/00-tracing-fundamentals.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# Distributed Tracing Fundamentals
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Next**: [Architecture Analysis](./01-architecture-analysis.md)
|
||||
|
||||
---
|
||||
|
||||
## What is Distributed Tracing?
|
||||
|
||||
Distributed tracing is a method for tracking data objects as they flow through distributed systems. In a network like XRP Ledger, a single transaction touches multiple independent nodes—each with no shared memory or logging. Distributed tracing connects these dots.
|
||||
|
||||
**Without tracing:** You see isolated logs on each node with no way to correlate them.
|
||||
|
||||
**With tracing:** You see the complete journey of a transaction or an event across all nodes it touched.
|
||||
|
||||
---
|
||||
|
||||
## Actors and Actions at a Glance
|
||||
|
||||
### Actors
|
||||
|
||||
| Who (Plain English) | Technical Term |
|
||||
| ---------------------------------------------- | --------------- |
|
||||
| A single unit of work being tracked | Span |
|
||||
| The complete journey of a request | Trace |
|
||||
| Data that links spans across services | Trace Context |
|
||||
| Code that creates spans and propagates context | Instrumentation |
|
||||
| Service that receives and processes traces | Collector |
|
||||
| Storage and visualization system | Backend (Tempo) |
|
||||
| Decision logic for which traces to keep | Sampler |
|
||||
|
||||
### Actions
|
||||
|
||||
| What Happens (Plain English) | Technical Term |
|
||||
| --------------------------------------- | ----------------------- |
|
||||
| Start tracking a new operation | Create a Span |
|
||||
| Connect a child operation to its parent | Set `parent_span_id` |
|
||||
| Group all related operations together | Share a `trace_id` |
|
||||
| Pass tracking data between services | Context Propagation |
|
||||
| Decide whether to record a trace | Sampling (Head or Tail) |
|
||||
| Send completed traces to storage | Export (OTLP) |
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Trace
|
||||
|
||||
A **trace** represents the entire journey of a request through the system. It has a unique `trace_id` that stays constant across all nodes.
|
||||
|
||||
```
|
||||
Trace ID: abc123
|
||||
├── Node A: received transaction
|
||||
├── Node B: relayed transaction
|
||||
├── Node C: included in consensus
|
||||
└── Node D: applied to ledger
|
||||
```
|
||||
|
||||
### 2. Span
|
||||
|
||||
A **span** represents a single unit of work within a trace. Each span has:
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| ---------------- | -------------------------------- | -------------------------- |
|
||||
| `trace_id` | Identifies the trace | `event123` |
|
||||
| `span_id` | Unique identifier | `span456` |
|
||||
| `parent_span_id` | Parent span (if any) | `p_span123` |
|
||||
| `name` | Operation name | `rpc.submit` |
|
||||
| `start_time` | When work began (local time) | `2024-01-15T10:30:00Z` |
|
||||
| `end_time` | When work completed (local time) | `2024-01-15T10:30:00.050Z` |
|
||||
| `attributes` | Key-value metadata | `tx.hash=ABC...` |
|
||||
| `status` | OK, ERROR MSG | `OK` |
|
||||
|
||||
### 3. Trace Context
|
||||
|
||||
**Trace context** is the data that propagates between services to link spans together. It contains:
|
||||
|
||||
- `trace_id` - The trace this span belongs to
|
||||
- `span_id` - The current span (becomes parent for child spans)
|
||||
- `trace_flags` - Sampling decisions
|
||||
|
||||
---
|
||||
|
||||
## How Spans Form a Trace
|
||||
|
||||
Spans have parent-child relationships forming a tree structure:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph trace["Trace: abc123"]
|
||||
A["tx.submit<br/>span_id: 001<br/>50ms"] --> B["tx.validate<br/>span_id: 002<br/>5ms"]
|
||||
A --> C["tx.relay<br/>span_id: 003<br/>10ms"]
|
||||
A --> D["tx.apply<br/>span_id: 004<br/>30ms"]
|
||||
D --> E["ledger.update<br/>span_id: 005<br/>20ms"]
|
||||
end
|
||||
|
||||
style A fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style B fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style C fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style D fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style E fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **tx.submit (blue, root)**: The top-level span representing the entire transaction submission; all other spans are its descendants.
|
||||
- **tx.validate, tx.relay, tx.apply (green)**: Direct children of tx.submit, representing the three main stages -- validation, relay to peers, and application to the ledger.
|
||||
- **ledger.update (red)**: A grandchild span nested under tx.apply, representing the actual ledger state mutation triggered by applying the transaction.
|
||||
- **Arrows (parent to child)**: Each arrow indicates a parent-child span relationship where the parent's completion depends on the child finishing.
|
||||
|
||||
The same trace visualized as a **timeline (Gantt chart)**:
|
||||
|
||||
```
|
||||
Time → 0ms 10ms 20ms 30ms 40ms 50ms
|
||||
├───────────────────────────────────────────┤
|
||||
tx.submit│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
|
||||
├─────┤
|
||||
tx.valid │▓▓▓▓▓│
|
||||
│ ├──────────┤
|
||||
tx.relay │ │▓▓▓▓▓▓▓▓▓▓│
|
||||
│ ├────────────────────────────┤
|
||||
tx.apply │ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
|
||||
│ ├──────────────────┤
|
||||
ledger │ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Span Relationships
|
||||
|
||||
Spans don't always form simple parent-child trees. Distributed tracing defines several relationship types to capture different causal patterns:
|
||||
|
||||
### 1. Parent-Child (ChildOf)
|
||||
|
||||
The default relationship. The parent span **depends on** or **contains** the child span. The child runs within the scope of the parent.
|
||||
|
||||
```
|
||||
tx.submit (parent)
|
||||
├── tx.validate (child) ← parent waits for this
|
||||
├── tx.relay (child) ← parent waits for this
|
||||
└── tx.apply (child) ← parent waits for this
|
||||
```
|
||||
|
||||
**When to use:** Synchronous calls, nested operations, any case where the parent's completion depends on the child.
|
||||
|
||||
### 2. Follows-From
|
||||
|
||||
A causal relationship where the first span **triggers** the second, but does **not wait** for it. The originator fires and moves on.
|
||||
|
||||
```
|
||||
Time →
|
||||
|
||||
tx.receive [=======]
|
||||
↓ triggers (follows-from)
|
||||
tx.relay [===========] ← runs independently
|
||||
```
|
||||
|
||||
**When to use:** Asynchronous jobs, queued work, fire-and-forget patterns. For example, a node receives a transaction and queues it for relay — the relay span _follows from_ the receive span but the receiver doesn't wait for relaying to complete.
|
||||
|
||||
> **OpenTracing** defined `FollowsFrom` as a first-class reference type alongside `ChildOf`.
|
||||
> **OpenTelemetry** represents this using **Span Links** with descriptive attributes instead (see below).
|
||||
|
||||
### 3. Span Links (Cross-Trace and Non-Hierarchical)
|
||||
|
||||
Links connect spans that are **causally related but not in a parent-child hierarchy**. Unlike parent-child, links can cross trace boundaries.
|
||||
|
||||
```
|
||||
Trace A Trace B
|
||||
────── ──────
|
||||
batch.schedule batch.execute
|
||||
├─ item.enqueue (span X) ┌──► process.item
|
||||
├─ item.enqueue (span Y) ───┤ (links to X, Y, Z)
|
||||
├─ item.enqueue (span Z) └──►
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
|
||||
| Pattern | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------- |
|
||||
| **Batch processing** | A batch span links back to all individual spans that contributed to it |
|
||||
| **Fan-in** | An aggregation span links to the multiple producer spans it merges |
|
||||
| **Fan-out** | Multiple downstream spans link back to the single span that triggered them |
|
||||
| **Async handoff** | A deferred job links back to the request that queued it (follows-from) |
|
||||
| **Cross-trace** | Correlating spans across independent traces (e.g., retries, related events) |
|
||||
|
||||
**Link structure:** Each link carries the target span's context plus optional attributes:
|
||||
|
||||
```
|
||||
Link {
|
||||
trace_id: <target trace>
|
||||
span_id: <target span>
|
||||
attributes: { "link.description": "triggered by batch scheduler" }
|
||||
}
|
||||
```
|
||||
|
||||
### Relationship Summary
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph parent_child["Parent-Child"]
|
||||
direction TB
|
||||
P["Parent"] --> C["Child"]
|
||||
end
|
||||
|
||||
subgraph follows_from["Follows-From"]
|
||||
direction TB
|
||||
A["Span A"] -.->|triggers| B["Span B"]
|
||||
end
|
||||
|
||||
subgraph links["Span Links"]
|
||||
direction TB
|
||||
X["Span X\n(Trace 1)"] -.-|link| Y["Span Y\n(Trace 2)"]
|
||||
end
|
||||
|
||||
parent_child ~~~ follows_from ~~~ links
|
||||
|
||||
style P fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style C fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style A fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style B fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style X fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
style Y fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
```
|
||||
|
||||
| Relationship | Same Trace? | Dependency? | OTel Mechanism |
|
||||
| ---------------- | ----------- | -------------------------- | ----------------- |
|
||||
| **Parent-Child** | Yes | Parent depends on child | `parent_span_id` |
|
||||
| **Follows-From** | Usually | Causal but no dependency | Link + attributes |
|
||||
| **Span Link** | Either | Correlation, no dependency | Link + attributes |
|
||||
|
||||
---
|
||||
|
||||
## Trace ID Generation
|
||||
|
||||
A `trace_id` is a 128-bit (16-byte) identifier that groups all spans belonging to one logical operation. How it's generated determines how easily you can find and correlate traces later.
|
||||
|
||||
### General Approaches
|
||||
|
||||
#### 1. Random (W3C Default)
|
||||
|
||||
Generate a random 128-bit ID when a trace starts. Standard approach for most services.
|
||||
|
||||
```
|
||||
trace_id = random_128_bits()
|
||||
```
|
||||
|
||||
| Pros | Cons |
|
||||
| --------------------------- | --------------------------------------------- |
|
||||
| Simple, standard | No natural correlation to domain events |
|
||||
| Guaranteed unique per trace | If propagation is lost, trace is broken |
|
||||
| Works with all OTel tooling | "Find trace for TX abc" requires index lookup |
|
||||
|
||||
#### 2. Deterministic (Derived from Domain Data)
|
||||
|
||||
Compute the trace_id from a hash of a natural identifier. Every node independently derives the **same** trace_id for the same event.
|
||||
|
||||
```
|
||||
trace_id = SHA-256(domain_identifier)[0:16] // truncate to 128 bits
|
||||
```
|
||||
|
||||
| Pros | Cons |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| Propagation-resilient — same ID computed everywhere | Same event processed twice (retry) shares trace_id |
|
||||
| Natural search — domain ID maps directly to trace | Non-standard (tooling assumes random) |
|
||||
| No coordination needed between nodes | 256→128 bit truncation (collision risk negligible at ~2⁶⁴) |
|
||||
|
||||
#### 3. Hybrid (Deterministic Prefix + Random Suffix)
|
||||
|
||||
First 8 bytes derived from domain data, last 8 bytes random.
|
||||
|
||||
```
|
||||
trace_id = SHA-256(domain_identifier)[0:8] || random_64_bits()
|
||||
```
|
||||
|
||||
| Pros | Cons |
|
||||
| ------------------------------------------- | ---------------------------------------- |
|
||||
| Prefix search: "find all traces for TX abc" | Must propagate to maintain full trace_id |
|
||||
| Unique per processing instance | More complex generation logic |
|
||||
| Retries get distinct trace_ids | Partial correlation only (prefix match) |
|
||||
|
||||
### XRPL Workflow Analysis
|
||||
|
||||
XRPL has a unique advantage: its core workflows produce **globally unique 256-bit hashes** that are known on every node. This makes deterministic trace_id generation practical in ways most systems can't achieve.
|
||||
|
||||
#### Natural Identifiers by Workflow
|
||||
|
||||
| Workflow | Natural Identifier | Size | Known at Start? | Same on All Nodes? |
|
||||
| ------------------- | --------------------------------- | ---------- | ----------------------------- | -------------------------------- |
|
||||
| **Transaction** | Transaction hash (`tid_`) | 256-bit | Yes — computed before signing | Yes — hash of canonical tx data |
|
||||
| **Consensus round** | Previous ledger hash + ledger seq | 256+32 bit | Yes — known when round opens | Yes — all validators agree |
|
||||
| **Validation** | Ledger hash being validated | 256-bit | Yes — from consensus result | Yes — same closed ledger |
|
||||
| **Ledger catch-up** | Target ledger hash | 256-bit | Yes — we know what to fetch | Yes — identifies ledger globally |
|
||||
|
||||
#### Where These Identifiers Live in Code
|
||||
|
||||
```
|
||||
Transaction: STTx::getTransactionID() → uint256 tid_
|
||||
TMTransaction::rawTransaction → recompute hash from bytes
|
||||
|
||||
Consensus: ConsensusProposal::prevLedger_ → uint256 (previous ledger hash)
|
||||
ConsensusProposal::position_ → uint256 (TxSet hash)
|
||||
LedgerHeader::seq → uint32_t (ledger sequence)
|
||||
|
||||
Validation: STValidation::getLedgerHash() → uint256
|
||||
STValidation::getNodeID() → NodeID (160-bit)
|
||||
|
||||
Ledger fetch: InboundLedger constructor → uint256 hash, uint32_t seq
|
||||
TMGetLedger::ledgerHash → bytes (uint256)
|
||||
```
|
||||
|
||||
### Recommended Strategy: Workflow-Scoped Deterministic
|
||||
|
||||
Each workflow type derives its trace_id from its natural domain identifier:
|
||||
|
||||
```
|
||||
Transaction trace: trace_id = SHA-256("tx" || tx_hash)[0:16]
|
||||
Consensus trace: trace_id = SHA-256("cons" || prev_ledger_hash || ledger_seq)[0:16]
|
||||
Ledger catch-up: trace_id = SHA-256("fetch" || target_ledger_hash)[0:16]
|
||||
```
|
||||
|
||||
The string prefix (`"tx"`, `"cons"`, `"fetch"`) prevents collisions between workflows that might share underlying hashes.
|
||||
|
||||
**Why this works for XRPL:**
|
||||
|
||||
1. **Propagation-resilient** — Even if a P2P message drops trace context, every node independently computes the same trace_id from the same tx_hash or ledger_hash. Spans still correlate.
|
||||
|
||||
2. **Zero-cost search** — "Show me the trace for transaction ABC" becomes a direct lookup: compute `SHA-256("tx" || ABC)[0:16]` and query. No secondary index needed.
|
||||
|
||||
3. **Cross-workflow linking via Span Links** — A consensus trace links to individual transaction traces. A validation span links to the consensus trace. This connects the full picture without forcing everything into one giant trace.
|
||||
|
||||
### Cross-Workflow Correlation
|
||||
|
||||
Each workflow gets its own trace. Span Links tie them together:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph tx_trace["Transaction Trace"]
|
||||
direction LR
|
||||
Tn["trace_id = f(tx_hash)"]:::note --> T1["tx.receive"] --> T2["tx.validate"] --> T3["tx.relay"]
|
||||
end
|
||||
|
||||
subgraph cons_trace["Consensus Trace"]
|
||||
direction LR
|
||||
Cn["trace_id = f(prev_ledger, seq)"]:::note --> C1["cons.open"] --> C2["cons.propose"] --> C3["cons.accept"]
|
||||
end
|
||||
|
||||
subgraph val_trace["Validation"]
|
||||
direction LR
|
||||
Vn["spans within consensus trace"]:::note --> V1["val.create"] --> V2["val.broadcast"]
|
||||
end
|
||||
|
||||
subgraph fetch_trace["Catch-Up Trace"]
|
||||
direction LR
|
||||
Fn["trace_id = f(ledger_hash)"]:::note --> F1["fetch.request"] --> F2["fetch.receive"] --> F3["fetch.apply"]
|
||||
end
|
||||
|
||||
C1 -.-|"span link\n(tx traces)"| T3
|
||||
C3 --> V1
|
||||
F1 -.-|"span link\n(target ledger)"| C3
|
||||
|
||||
classDef note fill:none,stroke:#888,stroke-dasharray:5 5,color:#333,font-style:italic
|
||||
style T1 fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style T2 fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style T3 fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style C1 fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style C2 fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style C3 fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style V1 fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style V2 fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style F1 fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
style F2 fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
style F3 fill:#4a148c,stroke:#38006b,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Transaction Trace (blue)**: An independent trace whose `trace_id` is deterministically derived from the transaction hash. Contains receive, validate, and relay spans.
|
||||
- **Consensus Trace (green)**: An independent trace whose `trace_id` is derived from the previous ledger hash and sequence number. Covers the open, propose, and accept phases.
|
||||
- **Validation (red)**: Validation spans live within the consensus trace (not a separate trace). They are created after the accept phase completes.
|
||||
- **Catch-Up Trace (purple)**: An independent trace for ledger acquisition, derived from the target ledger hash. Used when a node is behind and fetching missing ledgers.
|
||||
- **Dotted arrows (span links)**: Cross-trace correlations. Consensus links to transaction traces it included; catch-up links to the consensus trace that produced the target ledger.
|
||||
- **Solid arrow (C3 to V1)**: A parent-child relationship -- validation spans are direct children of the consensus accept span within the same trace.
|
||||
|
||||
**How a query flows:**
|
||||
|
||||
```
|
||||
"Why was TX abc slow?"
|
||||
1. Compute trace_id = SHA-256("tx" || abc)[0:16]
|
||||
2. Find transaction trace → see it was included in consensus round N
|
||||
3. Follow span link → consensus trace for round N
|
||||
4. See which phase was slow (propose? accept?)
|
||||
5. If a node was catching up, follow link → catch-up trace
|
||||
```
|
||||
|
||||
### Trade-offs to Consider
|
||||
|
||||
| Concern | Mitigation |
|
||||
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Retries get same trace_id** | Add `attempt` attribute to root span; spans have unique span_ids and timestamps |
|
||||
| **256→128 bit truncation** | Birthday-bound collision at ~2⁶⁴ operations — negligible for XRPL's throughput |
|
||||
| **Non-standard generation** | OTel spec allows any 16-byte non-zero value; tooling works on the hex string |
|
||||
| **Hash computation cost** | SHA-256 is ~0.3μs per call; XRPL already computes these hashes for other purposes |
|
||||
| **Late-binding identifiers** | Ledger hash isn't known until after consensus — validation spans use ledger_seq as fallback, then link to the consensus trace |
|
||||
|
||||
---
|
||||
|
||||
## Distributed Traces Across Nodes
|
||||
|
||||
In distributed systems like xrpld, traces span **multiple independent nodes**. The trace context must be propagated in network messages:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant NodeA as Node A
|
||||
participant NodeB as Node B
|
||||
participant NodeC as Node C
|
||||
|
||||
Client->>NodeA: Submit TX<br/>(no trace context)
|
||||
|
||||
Note over NodeA: Creates new trace<br/>trace_id: abc123<br/>span: tx.receive
|
||||
|
||||
NodeA->>NodeB: Relay TX<br/>(trace_id: abc123, parent: 001)
|
||||
|
||||
Note over NodeB: Creates child span<br/>span: tx.relay<br/>parent_span_id: 001
|
||||
|
||||
NodeA->>NodeC: Relay TX<br/>(trace_id: abc123, parent: 001)
|
||||
|
||||
Note over NodeC: Creates child span<br/>span: tx.relay<br/>parent_span_id: 001
|
||||
|
||||
Note over NodeA,NodeC: All spans share trace_id: abc123<br/>enabling correlation across nodes
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Client**: The external entity that submits a transaction. It does not carry trace context -- the trace originates at the first node.
|
||||
- **Node A**: The entry point that creates a new trace (trace_id: abc123) and the root span `tx.receive`. It relays the transaction to peers with trace context attached.
|
||||
- **Node B and Node C**: Peer nodes that receive the relayed transaction along with the propagated trace context. Each creates a child span under Node A's span, preserving the same `trace_id`.
|
||||
- **Arrows with trace context**: The relay messages carry `trace_id` and `parent_span_id`, allowing each downstream node to link its spans back to the originating span on Node A.
|
||||
|
||||
---
|
||||
|
||||
## Context Propagation
|
||||
|
||||
For traces to work across nodes, **trace context must be propagated** in messages.
|
||||
|
||||
### What's in the Context (~26 bytes)
|
||||
|
||||
| Field | Size | Description |
|
||||
| ------------- | -------- | ------------------------------------------------------- |
|
||||
| `trace_id` | 16 bytes | Identifies the entire trace (constant across all nodes) |
|
||||
| `span_id` | 8 bytes | The sender's current span (becomes parent on receiver) |
|
||||
| `trace_flags` | 1 byte | Sampling decision (bit 0 = sampled; bits 1-7 reserved) |
|
||||
| `trace_state` | variable | Optional vendor-specific data (typically omitted) |
|
||||
|
||||
### How span_id Changes at Each Hop
|
||||
|
||||
Only **one** `span_id` travels in the context - the sender's current span. Each node:
|
||||
|
||||
1. Extracts the received `span_id` and uses it as the `parent_span_id`
|
||||
2. Creates a **new** `span_id` for its own span
|
||||
3. Sends its own `span_id` as the parent when forwarding
|
||||
|
||||
```
|
||||
Node A Node B Node C
|
||||
────── ────── ──────
|
||||
|
||||
Span AAA Span BBB Span CCC
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Context out: Context out: Context out:
|
||||
├─ trace_id: abc123 ├─ trace_id: abc123 ├─ trace_id: abc123
|
||||
├─ span_id: AAA ──────────► ├─ span_id: BBB ──────────► ├─ span_id: CCC ──────►
|
||||
└─ flags: 01 └─ flags: 01 └─ flags: 01
|
||||
│ │
|
||||
parent = AAA parent = BBB
|
||||
```
|
||||
|
||||
The `trace_id` stays constant, but `span_id` **changes at every hop** to maintain the parent-child chain.
|
||||
|
||||
### Propagation Formats
|
||||
|
||||
There are two patterns:
|
||||
|
||||
### HTTP/RPC Headers (W3C Trace Context)
|
||||
|
||||
```
|
||||
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
|
||||
│ │ │ │
|
||||
│ │ │ └── Flags (sampled)
|
||||
│ │ └── Parent span ID (16 hex)
|
||||
│ └── Trace ID (32 hex)
|
||||
└── Version
|
||||
```
|
||||
|
||||
### Protocol Buffers (xrpld P2P messages)
|
||||
|
||||
```protobuf
|
||||
message TMTransaction {
|
||||
bytes rawTransaction = 1;
|
||||
// ... existing fields ...
|
||||
|
||||
// Trace context extension
|
||||
bytes trace_parent = 100; // W3C traceparent
|
||||
bytes trace_state = 101; // W3C tracestate
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sampling
|
||||
|
||||
Not every trace needs to be recorded. **Sampling** reduces overhead:
|
||||
|
||||
### Head Sampling (at trace start)
|
||||
|
||||
```
|
||||
Request arrives → Random N% chance → Record or skip entire trace
|
||||
```
|
||||
|
||||
- ✅ Low overhead
|
||||
- ❌ May miss interesting traces
|
||||
|
||||
> **xrpld note**: xrpld intentionally fixes head sampling at 100% (sample
|
||||
> everything) and does not expose a configurable ratio. A per-node ratio
|
||||
> would let different nodes make divergent keep/drop decisions for the same
|
||||
> distributed trace, producing broken/partial traces. xrpld uses a
|
||||
> `ParentBased` sampler so spans with a remote parent honor the upstream
|
||||
> decision. Volume reduction is delegated to collector-side tail sampling.
|
||||
|
||||
### Tail Sampling (after trace completes)
|
||||
|
||||
```
|
||||
Trace completes → Collector evaluates:
|
||||
- Error? → KEEP
|
||||
- Slow? → KEEP
|
||||
- Normal? → Sample 10%
|
||||
```
|
||||
|
||||
- ✅ Never loses important traces
|
||||
- ❌ Higher memory usage at collector
|
||||
|
||||
---
|
||||
|
||||
## Key Benefits for xrpld
|
||||
|
||||
| Challenge | How Tracing Helps |
|
||||
| ---------------------------------- | ---------------------------------------- |
|
||||
| "Where is my transaction?" | Follow trace across all nodes it touched |
|
||||
| "Why was consensus slow?" | See timing breakdown of each phase |
|
||||
| "Which node is the bottleneck?" | Compare span durations across nodes |
|
||||
| "What happened during the outage?" | Correlate errors across the network |
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
| Term | Definition |
|
||||
| -------------------- | ------------------------------------------------------------------- |
|
||||
| **Trace** | Complete journey of a request, identified by `trace_id` |
|
||||
| **Span** | Single operation within a trace |
|
||||
| **Parent-Child** | Span relationship where the parent depends on the child |
|
||||
| **Follows-From** | Causal relationship where originator doesn't wait for the result |
|
||||
| **Span Link** | Non-hierarchical connection between spans, possibly across traces |
|
||||
| **Deterministic ID** | Trace ID derived from domain data (e.g., tx_hash) instead of random |
|
||||
| **Context** | Data propagated between services (`trace_id`, `span_id`, flags) |
|
||||
| **Instrumentation** | Code that creates spans and propagates context |
|
||||
| **Collector** | Service that receives, processes, and exports traces |
|
||||
| **Backend** | Storage/visualization system (Tempo) |
|
||||
| **Head Sampling** | Sampling decision at trace start |
|
||||
| **Tail Sampling** | Sampling decision after trace completes |
|
||||
|
||||
---
|
||||
|
||||
_Next: [Architecture Analysis](./01-architecture-analysis.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
467
OpenTelemetryPlan/01-architecture-analysis.md
Normal file
467
OpenTelemetryPlan/01-architecture-analysis.md
Normal file
@@ -0,0 +1,467 @@
|
||||
# Architecture Analysis
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Design Decisions](./02-design-decisions.md) | [Implementation Strategy](./03-implementation-strategy.md)
|
||||
|
||||
---
|
||||
|
||||
## 1.1 Current xrpld Architecture Overview
|
||||
|
||||
> **WS** = WebSocket | **UNL** = Unique Node List | **TxQ** = Transaction Queue | **StatsD** = Statistics Daemon
|
||||
|
||||
The xrpld node software consists of several interconnected components that need instrumentation for distributed tracing:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Node"]
|
||||
subgraph services["Core Services"]
|
||||
RPC["RPC Server<br/>(HTTP/WS/gRPC)"]
|
||||
Overlay["Overlay<br/>(P2P Network)"]
|
||||
Consensus["Consensus<br/>(RCLConsensus)"]
|
||||
ValidatorList["ValidatorList<br/>(UNL Mgmt)"]
|
||||
end
|
||||
|
||||
JobQueue["JobQueue<br/>(Thread Pool)"]
|
||||
|
||||
subgraph processing["Processing Layer"]
|
||||
NetworkOPs["NetworkOPs<br/>(Tx Processing)"]
|
||||
LedgerMaster["LedgerMaster<br/>(Ledger Mgmt)"]
|
||||
NodeStore["NodeStore<br/>(Database)"]
|
||||
InboundLedgers["InboundLedgers<br/>(Ledger Sync)"]
|
||||
end
|
||||
|
||||
subgraph appservices["Application Services"]
|
||||
PathFind["PathFinding<br/>(Payment Paths)"]
|
||||
TxQ["TxQ<br/>(Fee Escalation)"]
|
||||
LoadMgr["LoadManager<br/>(Fee/Load)"]
|
||||
end
|
||||
|
||||
subgraph observability["Existing Observability"]
|
||||
PerfLog["PerfLog<br/>(JSON)"]
|
||||
Insight["Insight<br/>(StatsD)"]
|
||||
Logging["Logging<br/>(Journal)"]
|
||||
end
|
||||
|
||||
services --> JobQueue
|
||||
JobQueue --> processing
|
||||
JobQueue --> appservices
|
||||
end
|
||||
|
||||
style xrpld fill:#424242,stroke:#212121,color:#ffffff
|
||||
style services fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style processing fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style appservices fill:#6a1b9a,stroke:#4a148c,color:#ffffff
|
||||
style observability fill:#e65100,stroke:#bf360c,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Core Services (blue)**: The entry points into xrpld -- RPC Server handles client requests, Overlay manages peer-to-peer networking, Consensus drives agreement, and ValidatorList manages trusted validators.
|
||||
- **JobQueue (center)**: The asynchronous thread pool that decouples Core Services from the Processing and Application layers. All work flows through it.
|
||||
- **Processing Layer (green)**: Core business logic -- NetworkOPs processes transactions, LedgerMaster manages ledger state, NodeStore handles persistence, and InboundLedgers synchronizes missing data.
|
||||
- **Application Services (purple)**: Higher-level features -- PathFinding computes payment routes, TxQ manages fee-based queuing, and LoadManager tracks server load.
|
||||
- **Existing Observability (orange)**: The current monitoring stack (PerfLog, Insight, Journal logging) that OpenTelemetry will complement, not replace.
|
||||
- **Arrows (Services to JobQueue to layers)**: Work originates at Core Services, is enqueued onto the JobQueue, and dispatched to Processing or Application layers for execution.
|
||||
|
||||
---
|
||||
|
||||
## 1.1.1 Actors and Actions
|
||||
|
||||
### Actors
|
||||
|
||||
| Who (Plain English) | Technical Term |
|
||||
| ----------------------------------------- | -------------------------- |
|
||||
| Network node running XRPL software | xrpld node |
|
||||
| External client submitting requests | RPC Client |
|
||||
| Network neighbor sharing data | Peer (PeerImp) |
|
||||
| Request handler for client queries | RPC Server (ServerHandler) |
|
||||
| Command executor for specific RPC methods | RPCHandler |
|
||||
| Agreement process between nodes | Consensus (RCLConsensus) |
|
||||
| Transaction processing coordinator | NetworkOPs |
|
||||
| Background task scheduler | JobQueue |
|
||||
| Ledger state manager | LedgerMaster |
|
||||
| Payment route calculator | PathFinding (Pathfinder) |
|
||||
| Transaction waiting room | TxQ (Transaction Queue) |
|
||||
| Fee adjustment system | LoadManager |
|
||||
| Trusted validator list manager | ValidatorList |
|
||||
| Protocol upgrade tracker | AmendmentTable |
|
||||
| Ledger state hash tree | SHAMap |
|
||||
| Persistent key-value storage | NodeStore |
|
||||
|
||||
### Actions
|
||||
|
||||
| What Happens (Plain English) | Technical Term |
|
||||
| ---------------------------------------------- | ---------------------- |
|
||||
| Client sends a request to a node | `rpc.request` |
|
||||
| Node executes a specific RPC command | `rpc.command.*` |
|
||||
| Node receives a transaction from a peer | `tx.receive` |
|
||||
| Node checks if a transaction is valid | `tx.validate` |
|
||||
| Node forwards a transaction to neighbors | `tx.relay` |
|
||||
| Nodes agree on which transactions to include | `consensus.round` |
|
||||
| Consensus progresses through phases | `consensus.phase.*` |
|
||||
| Node builds a new confirmed ledger | `ledger.build` |
|
||||
| Node fetches missing ledger data from peers | `ledger.acquire` |
|
||||
| Node computes payment routes | `pathfind.compute` |
|
||||
| Node queues a transaction for later processing | `txq.enqueue` |
|
||||
| Node increases fees due to high load | `fee.escalate` |
|
||||
| Node fetches the latest trusted validator list | `validator.list.fetch` |
|
||||
| Node votes on a protocol amendment | `amendment.vote` |
|
||||
| Node synchronizes state tree data | `shamap.sync` |
|
||||
|
||||
---
|
||||
|
||||
## 1.2 Key Components for Instrumentation
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List
|
||||
|
||||
| Component | Location | Purpose | Trace Value |
|
||||
| ------------------ | ------------------------------------------ | ------------------------ | -------------------------------- |
|
||||
| **Overlay** | `src/xrpld/overlay/` | P2P communication | Message propagation timing |
|
||||
| **PeerImp** | `src/xrpld/overlay/detail/PeerImp.cpp` | Individual peer handling | Per-peer latency |
|
||||
| **RCLConsensus** | `src/xrpld/app/consensus/RCLConsensus.cpp` | Consensus algorithm | Round timing, phase analysis |
|
||||
| **NetworkOPs** | `src/xrpld/app/misc/NetworkOPs.cpp` | Transaction processing | Tx lifecycle tracking |
|
||||
| **ServerHandler** | `src/xrpld/rpc/detail/ServerHandler.cpp` | RPC entry point | Request latency |
|
||||
| **RPCHandler** | `src/xrpld/rpc/detail/RPCHandler.cpp` | Command execution | Per-command timing |
|
||||
| **JobQueue** | `src/xrpl/core/JobQueue.h` | Async task execution | Queue wait times |
|
||||
| **PathFinding** | `src/xrpld/app/paths/` | Payment path computation | Path latency, cache hits |
|
||||
| **TxQ** | `src/xrpld/app/misc/TxQ.cpp` | Transaction queue/fees | Queue depth, eviction rates |
|
||||
| **LoadManager** | `src/xrpld/app/main/LoadManager.cpp` | Fee escalation/load | Fee levels, load factors |
|
||||
| **InboundLedgers** | `src/xrpld/app/ledger/InboundLedgers.cpp` | Ledger acquisition | Sync time, peer reliability |
|
||||
| **ValidatorList** | `src/xrpld/app/misc/ValidatorList.cpp` | UNL management | List freshness, fetch failures |
|
||||
| **AmendmentTable** | `src/xrpld/app/misc/AmendmentTable.cpp` | Protocol amendments | Voting status, activation events |
|
||||
| **SHAMap** | `src/xrpld/shamap/` | State hash tree | Sync speed, missing nodes |
|
||||
|
||||
---
|
||||
|
||||
## 1.3 Transaction Flow Diagram
|
||||
|
||||
Transaction flow spans multiple nodes in the network. Each node creates linked spans to form a distributed trace:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant PeerA as Peer A (Receive)
|
||||
participant PeerB as Peer B (Relay)
|
||||
participant PeerC as Peer C (Validate)
|
||||
|
||||
Client->>PeerA: 1. Submit TX
|
||||
|
||||
rect rgb(230, 245, 255)
|
||||
Note over PeerA: tx.receive SPAN START
|
||||
PeerA->>PeerA: HashRouter Deduplication
|
||||
PeerA->>PeerA: tx.validate (child span)
|
||||
end
|
||||
|
||||
PeerA->>PeerB: 2. Relay TX (with trace ctx)
|
||||
|
||||
rect rgb(230, 245, 255)
|
||||
Note over PeerB: tx.receive (linked span)
|
||||
end
|
||||
|
||||
PeerB->>PeerC: 3. Relay TX
|
||||
|
||||
rect rgb(230, 245, 255)
|
||||
Note over PeerC: tx.receive (linked span)
|
||||
PeerC->>PeerC: tx.process
|
||||
end
|
||||
|
||||
Note over Client,PeerC: DISTRIBUTED TRACE (same trace_id: abc123)
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Client**: The external entity that submits a transaction to Peer A. It has no trace context -- the trace starts at the first node.
|
||||
- **Peer A (Receive)**: The entry node that creates the root span `tx.receive`, runs HashRouter deduplication to avoid processing duplicates, and creates a child `tx.validate` span.
|
||||
- **Peer A to Peer B arrow**: The relay message carries trace context (trace_id + parent span_id), enabling Peer B to create a linked span under the same trace.
|
||||
- **Peer B (Relay)**: Receives the transaction and trace context, creates a `tx.receive` span linked to Peer A's trace, then relays onward.
|
||||
- **Peer C (Validate)**: Final hop in this example. Creates a linked `tx.receive` span and runs `tx.process` to fully process the transaction.
|
||||
- **Blue rectangles**: Highlight the span boundaries on each node, showing where instrumentation creates and closes spans.
|
||||
|
||||
### Trace Structure
|
||||
|
||||
```
|
||||
trace_id: abc123
|
||||
├── span: tx.receive (Peer A)
|
||||
│ ├── span: tx.validate
|
||||
│ └── span: tx.relay
|
||||
├── span: tx.receive (Peer B) [parent: Peer A]
|
||||
│ └── span: tx.relay
|
||||
└── span: tx.receive (Peer C) [parent: Peer B]
|
||||
└── span: tx.process
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.4 Consensus Round Flow
|
||||
|
||||
Consensus rounds are multi-phase operations that benefit significantly from tracing:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph round["consensus.round (root span)"]
|
||||
attrs["Attributes:<br/>xrpl.consensus.ledger.seq = 12345678<br/>xrpl.consensus.mode = proposing<br/>xrpl.consensus.proposers = 35"]
|
||||
|
||||
subgraph open["consensus.phase.open"]
|
||||
open_desc["Duration: ~3s<br/>Waiting for transactions"]
|
||||
end
|
||||
|
||||
subgraph establish["consensus.phase.establish"]
|
||||
est_attrs["proposals_received = 28<br/>disputes_resolved = 3"]
|
||||
est_children["├── consensus.proposal.receive (×28)<br/>├── consensus.proposal.send (×1)<br/>└── consensus.dispute.resolve (×3)"]
|
||||
end
|
||||
|
||||
subgraph accept["consensus.phase.accept"]
|
||||
acc_attrs["transactions_applied = 150<br/>ledger.hash = DEF456..."]
|
||||
acc_children["├── ledger.build<br/>└── ledger.validate"]
|
||||
end
|
||||
|
||||
attrs --> open
|
||||
open --> establish
|
||||
establish --> accept
|
||||
end
|
||||
|
||||
style round fill:#f57f17,stroke:#e65100,color:#ffffff
|
||||
style open fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style establish fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style accept fill:#c2185b,stroke:#880e4f,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **consensus.round (orange, root span)**: The top-level span encompassing the entire consensus round, with attributes like ledger sequence, mode, and proposer count.
|
||||
- **consensus.phase.open (blue)**: The first phase where the node waits (~3s) to collect incoming transactions before proposing.
|
||||
- **consensus.phase.establish (green)**: The negotiation phase where validators exchange proposals, resolve disputes, and converge on a transaction set. Child spans track each proposal received/sent and each dispute resolved.
|
||||
- **consensus.phase.accept (pink)**: The final phase where the agreed transaction set is applied, a new ledger is built, and the ledger is validated. Child spans cover `ledger.build` and `ledger.validate`.
|
||||
- **Arrows (open to establish to accept)**: The sequential flow through the three consensus phases. Each phase must complete before the next begins.
|
||||
|
||||
---
|
||||
|
||||
## 1.5 RPC Request Flow
|
||||
|
||||
> **WS** = WebSocket
|
||||
|
||||
RPC requests support W3C Trace Context headers for distributed tracing across services:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph request["rpc.request (root span)"]
|
||||
http["HTTP Request — POST /<br/>traceparent:<br/>00-abc123...-def456...-01"]
|
||||
|
||||
attrs["Attributes:<br/>http.method = POST<br/>net.peer.ip = 192.168.1.100<br/>command = submit"]
|
||||
|
||||
subgraph enqueue["jobqueue.enqueue"]
|
||||
job_attr["xrpl.job.type = jtCLIENT_RPC"]
|
||||
end
|
||||
|
||||
subgraph command["rpc.command.submit"]
|
||||
cmd_attrs["version = 2<br/>rpc_role = user"]
|
||||
cmd_children["├── tx.deserialize<br/>├── tx.validate_local<br/>└── tx.submit_to_network"]
|
||||
end
|
||||
|
||||
response["Response: 200 OK<br/>Duration: 45ms"]
|
||||
|
||||
http --> attrs
|
||||
attrs --> enqueue
|
||||
enqueue --> command
|
||||
command --> response
|
||||
end
|
||||
|
||||
style request fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style enqueue fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style command fill:#e65100,stroke:#bf360c,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **rpc.request (green, root span)**: The outermost span representing the full RPC request lifecycle, from HTTP receipt to response. Carries the W3C `traceparent` header for distributed tracing.
|
||||
- **HTTP Request node**: Shows the incoming POST request with its `traceparent` header and extracted attributes (method, peer IP, command name).
|
||||
- **jobqueue.enqueue (blue)**: The span covering the asynchronous handoff from the RPC thread to the JobQueue worker thread. The trace context is preserved across this async boundary.
|
||||
- **rpc.command.submit (orange)**: The span for the actual command execution, with child spans for deserialization, local validation, and network submission.
|
||||
- **Response node**: The final output with HTTP status and total duration, marking the end of the root span.
|
||||
- **Arrows (top to bottom)**: The sequential processing pipeline -- receive request, extract attributes, enqueue job, execute command, return response.
|
||||
|
||||
---
|
||||
|
||||
## 1.6 Key Trace Points
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
The following table identifies priority instrumentation points across the codebase:
|
||||
|
||||
| Category | Span Name | File | Method | Priority |
|
||||
| --------------- | ---------------------- | ---------------------- | ----------------------- | -------- |
|
||||
| **Transaction** | `tx.receive` | `PeerImp.cpp` | `handleTransaction()` | High |
|
||||
| **Transaction** | `tx.validate` | `NetworkOPs.cpp` | `processTransaction()` | High |
|
||||
| **Transaction** | `tx.process` | `NetworkOPs.cpp` | `doTransactionSync()` | High |
|
||||
| **Transaction** | `tx.relay` | `OverlayImpl.cpp` | `relay()` | Medium |
|
||||
| **Consensus** | `consensus.round` | `RCLConsensus.cpp` | `startRound()` | High |
|
||||
| **Consensus** | `consensus.phase.*` | `Consensus.h` | `timerEntry()` | High |
|
||||
| **Consensus** | `consensus.proposal.*` | `RCLConsensus.cpp` | `peerProposal()` | Medium |
|
||||
| **RPC** | `rpc.request` | `ServerHandler.cpp` | `onRequest()` | High |
|
||||
| **RPC** | `rpc.command.*` | `RPCHandler.cpp` | `doCommand()` | High |
|
||||
| **Peer** | `peer.connect` | `OverlayImpl.cpp` | `onHandoff()` | Low |
|
||||
| **Peer** | `peer.message.*` | `PeerImp.cpp` | `onMessage()` | Low |
|
||||
| **Ledger** | `ledger.acquire` | `InboundLedgers.cpp` | `acquire()` | Medium |
|
||||
| **Ledger** | `ledger.build` | `RCLConsensus.cpp` | `buildLCL()` | High |
|
||||
| **PathFinding** | `pathfind.request` | `PathRequest.cpp` | `doUpdate()` | High |
|
||||
| **PathFinding** | `pathfind.compute` | `Pathfinder.cpp` | `findPaths()` | High |
|
||||
| **TxQ** | `txq.enqueue` | `TxQ.cpp` | `apply()` | High |
|
||||
| **TxQ** | `txq.apply` | `TxQ.cpp` | `processClosedLedger()` | High |
|
||||
| **Fee** | `fee.escalate` | `LoadManager.cpp` | `raiseLocalFee()` | Medium |
|
||||
| **Ledger** | `ledger.replay` | `LedgerReplayer.h` | `replay()` | Medium |
|
||||
| **Ledger** | `ledger.delta` | `LedgerDeltaAcquire.h` | `processData()` | Medium |
|
||||
| **Validator** | `validator.list.fetch` | `ValidatorList.cpp` | `verify()` | Medium |
|
||||
| **Validator** | `validator.manifest` | `Manifest.cpp` | `applyManifest()` | Low |
|
||||
| **Amendment** | `amendment.vote` | `AmendmentTable.cpp` | `doVoting()` | Low |
|
||||
| **SHAMap** | `shamap.sync` | `SHAMap.cpp` | `fetchRoot()` | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 1.7 Instrumentation Priority
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Instrumentation Priority Matrix
|
||||
x-axis Low Complexity --> High Complexity
|
||||
y-axis Low Value --> High Value
|
||||
quadrant-1 Implement First
|
||||
quadrant-2 Plan Carefully
|
||||
quadrant-3 Quick Wins
|
||||
quadrant-4 Consider Later
|
||||
|
||||
RPC Tracing: [0.2, 0.92]
|
||||
Transaction Tracing: [0.55, 0.88]
|
||||
Consensus Tracing: [0.78, 0.82]
|
||||
PathFinding: [0.38, 0.75]
|
||||
TxQ and Fees: [0.25, 0.65]
|
||||
Ledger Sync: [0.62, 0.58]
|
||||
Peer Message Tracing: [0.35, 0.25]
|
||||
JobQueue Tracing: [0.2, 0.48]
|
||||
Validator Mgmt: [0.48, 0.42]
|
||||
Amendment Tracking: [0.15, 0.32]
|
||||
SHAMap Operations: [0.72, 0.45]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.8 Observable Outcomes
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List
|
||||
|
||||
After implementing OpenTelemetry, operators and developers will gain visibility into the following:
|
||||
|
||||
### 1.8.1 What You Will See: Traces
|
||||
|
||||
| Trace Type | Description | Example Query in Grafana/Tempo |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| **Transaction Lifecycle** | Full journey from RPC submission through validation, relay, consensus, and ledger inclusion | `{service.name="xrpld" && 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.*"}` |
|
||||
|
||||
### 1.8.2 What You Will See: Metrics (Derived from Traces)
|
||||
|
||||
| Metric | Description | Dashboard Panel |
|
||||
| ----------------------------- | --------------------------------------- | --------------------------- |
|
||||
| **RPC Latency (p50/p95/p99)** | Response time distribution per command | Heatmap by command |
|
||||
| **Transaction Throughput** | Transactions processed per second | Time series graph |
|
||||
| **Consensus Round Duration** | Time to complete consensus phases | Histogram |
|
||||
| **Cross-Node Latency** | Time for transaction to reach N nodes | Line chart with percentiles |
|
||||
| **Error Rate** | Failed transactions/RPC calls by type | Stacked bar chart |
|
||||
| **PathFinding Latency** | Path computation time per currency pair | Heatmap by currency |
|
||||
| **TxQ Depth** | Queued transactions over time | Time series with thresholds |
|
||||
| **Fee Escalation Level** | Current fee multiplier | Gauge with alert thresholds |
|
||||
| **Ledger Sync Duration** | Time to acquire missing ledgers | Histogram |
|
||||
|
||||
### 1.8.3 Concrete Dashboard Examples
|
||||
|
||||
**Transaction Trace View (Tempo):**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Trace: abc123... (Transaction Submission) Duration: 847ms │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ├── rpc.request [ServerHandler] ████░░░░░░ 45ms │
|
||||
│ │ └── rpc.command.submit [RPCHandler] ████░░░░░░ 42ms │
|
||||
│ │ └── tx.receive [NetworkOPs] ███░░░░░░░ 35ms │
|
||||
│ │ ├── tx.validate [TxQ] █░░░░░░░░░ 8ms │
|
||||
│ │ └── tx.relay [Overlay] ██░░░░░░░░ 15ms │
|
||||
│ │ ├── tx.receive [Node-B] █████░░░░░ 52ms │
|
||||
│ │ │ └── tx.relay [Node-B] ██░░░░░░░░ 18ms │
|
||||
│ │ └── tx.receive [Node-C] ██████░░░░ 65ms │
|
||||
│ └── consensus.round [RCLConsensus] ████████░░ 720ms │
|
||||
│ ├── consensus.phase.open ██░░░░░░░░ 180ms │
|
||||
│ ├── consensus.phase.establish █████░░░░░ 480ms │
|
||||
│ └── consensus.phase.accept █░░░░░░░░░ 60ms │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**RPC Performance Dashboard Panel:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ RPC Command Latency (Last 1 Hour) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Command │ p50 │ p95 │ p99 │ Errors │ Rate │
|
||||
│──────────────────┼────────┼────────┼────────┼────────┼──────│
|
||||
│ account_info │ 12ms │ 45ms │ 89ms │ 0.1% │ 150/s│
|
||||
│ submit │ 35ms │ 120ms │ 250ms │ 2.3% │ 45/s│
|
||||
│ ledger │ 8ms │ 25ms │ 55ms │ 0.0% │ 80/s│
|
||||
│ tx │ 15ms │ 50ms │ 100ms │ 0.5% │ 60/s│
|
||||
│ server_info │ 5ms │ 12ms │ 20ms │ 0.0% │ 200/s│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Consensus Health Dashboard Panel:**
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 1200
|
||||
height: 400
|
||||
plotReservedSpacePercent: 50
|
||||
chartOrientation: vertical
|
||||
themeVariables:
|
||||
xyChart:
|
||||
plotColorPalette: "#3498db"
|
||||
---
|
||||
xychart-beta
|
||||
title "Consensus Round Duration (Last 24 Hours)"
|
||||
x-axis "Time of Day (Hours)" [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]
|
||||
y-axis "Duration (seconds)" 1 --> 5
|
||||
line [2.1, 2.4, 2.8, 3.2, 3.8, 4.3, 4.5, 5.0, 4.7, 4.0, 3.2, 2.6, 2.0]
|
||||
```
|
||||
|
||||
### 1.8.4 Operator Actionable Insights
|
||||
|
||||
| Scenario | What You'll See | Action |
|
||||
| ------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| **Slow RPC** | Span showing which phase is slow (parsing, execution, serialization) | Optimize specific code path |
|
||||
| **Transaction Stuck** | Trace stops at validation; error attribute shows reason | Fix transaction parameters |
|
||||
| **Consensus Delay** | Phase.establish taking too long; proposer attribute shows missing validators | Investigate network connectivity |
|
||||
| **Memory Spike** | Large batch of spans correlating with memory increase | Tune batch_size or sampling |
|
||||
| **Network Partition** | Traces missing cross-node links for specific peer | Check peer connectivity |
|
||||
| **Path Computation Slow** | pathfind.compute span shows high latency; cache miss rate in attributes | Warm the RippleLineCache, check order book depth |
|
||||
| **TxQ Full** | txq.enqueue spans show evictions; fee.escalate spans increasing | Monitor fee levels, alert operators |
|
||||
| **Ledger Sync Stalled** | ledger.acquire spans timing out; peer reliability attributes show issues | Check peer connectivity, add trusted peers |
|
||||
| **UNL Stale** | validator.list.fetch spans failing; last_update attribute aging | Verify validator site URLs, check DNS |
|
||||
|
||||
### 1.8.5 Developer Debugging Workflow
|
||||
|
||||
1. **Find Transaction**: Query by `xrpl.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
|
||||
4. **Correlate Logs**: Use `trace_id` to find related PerfLog entries
|
||||
5. **Compare Nodes**: Filter by `service.instance.id` to compare behavior across nodes
|
||||
|
||||
---
|
||||
|
||||
_Next: [Design Decisions](./02-design-decisions.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
714
OpenTelemetryPlan/02-design-decisions.md
Normal file
714
OpenTelemetryPlan/02-design-decisions.md
Normal file
@@ -0,0 +1,714 @@
|
||||
# Design Decisions
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Architecture Analysis](./01-architecture-analysis.md) | [Code Samples](./04-code-samples.md)
|
||||
|
||||
---
|
||||
|
||||
## 2.1 OpenTelemetry Components
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 2.1.1 SDK Selection
|
||||
|
||||
**Primary Choice**: OpenTelemetry C++ SDK (`opentelemetry-cpp`)
|
||||
|
||||
| Component | Purpose | Required |
|
||||
| --------------------------------------- | ---------------------- | ------------------------- |
|
||||
| `opentelemetry-cpp::api` | Tracing API headers | Yes |
|
||||
| `opentelemetry-cpp::sdk` | SDK implementation | Yes |
|
||||
| `opentelemetry-cpp::ext` | Extensions (exporters) | Yes |
|
||||
| `opentelemetry-cpp::otlp_http_exporter` | OTLP/HTTP export | Yes (shipped in Phase 1b) |
|
||||
| `opentelemetry-cpp::otlp_grpc_exporter` | OTLP/gRPC export | Future (not yet wired up) |
|
||||
|
||||
### 2.1.2 Instrumentation Strategy
|
||||
|
||||
**Manual Instrumentation** (recommended):
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
| ---------- | --------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **Manual** | Precise control, optimized placement, xrpld-specific attributes | More development effort |
|
||||
| **Auto** | Less code, automatic coverage | Less control, potential overhead, limited customization |
|
||||
|
||||
---
|
||||
|
||||
## 2.2 Exporter Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph nodes["xrpld Nodes"]
|
||||
node1["xrpld<br/>Node 1"]
|
||||
node2["xrpld<br/>Node 2"]
|
||||
node3["xrpld<br/>Node 3"]
|
||||
end
|
||||
|
||||
collector["OpenTelemetry<br/>Collector<br/>(sidecar or standalone)"]
|
||||
|
||||
subgraph backends["Observability Backends"]
|
||||
tempo["Tempo"]
|
||||
elastic["Elastic<br/>APM"]
|
||||
end
|
||||
|
||||
node1 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
node2 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
node3 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
|
||||
collector --> tempo
|
||||
collector --> elastic
|
||||
|
||||
style nodes fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style backends fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style collector fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Nodes (blue)**: The source of telemetry data. Each xrpld node exports spans via OTLP/HTTP on port 4318 (the only exporter shipped in Phase 1b).
|
||||
- **OpenTelemetry Collector (red)**: The central aggregation point that receives spans from all nodes. Can run as a sidecar (per-node) or standalone (shared). Handles batching, filtering, and routing.
|
||||
- **Observability Backends (green)**: The storage and visualization destinations. Tempo is the recommended backend for both development and production, and Elastic APM is an alternative. The Collector routes to one or more backends.
|
||||
- **Arrows (nodes to collector to backends)**: The data pipeline -- spans flow from nodes to the Collector over HTTP, then the Collector fans out to the configured backends.
|
||||
|
||||
### 2.2.1 OTLP/HTTP (Shipped in Phase 1b)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 2.2.2 OTLP/gRPC (Future Work — Planned Upgrade)
|
||||
|
||||
OTLP/gRPC is planned as a future upgrade from the HTTP exporter. The gRPC
|
||||
transport offers lower per-span overhead and tighter back-pressure semantics
|
||||
than HTTP/JSON, making it attractive for production deployments once the HTTP
|
||||
path is validated in earlier phases.
|
||||
|
||||
Required to land this upgrade:
|
||||
|
||||
1. Add `opentelemetry-cpp::otlp_grpc_exporter` to the Conan recipe (the
|
||||
dependency already exists but is not linked in Phase 1b builds).
|
||||
2. Extend `TelemetryConfig.cpp` to parse an `exporter` key (`otlp_http`
|
||||
default, `otlp_grpc` opt-in) and a gRPC endpoint override.
|
||||
3. In `Telemetry::start()` branch on the parsed exporter type and construct
|
||||
either `OtlpHttpExporterFactory::Create(httpOpts)` or
|
||||
`OtlpGrpcExporterFactory::Create(grpcOpts)` accordingly.
|
||||
4. Update the runbook and dashboards to document the alternate port and TLS
|
||||
settings.
|
||||
|
||||
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 = "<otel-collector-host>:4317";
|
||||
opts.use_ssl_credentials = true;
|
||||
opts.ssl_credentials_cacert_path = "/path/to/ca.crt";
|
||||
```
|
||||
|
||||
Until that work lands, `OtlpGrpcExporterOptions` is **not** used by any code
|
||||
path in Phase 1b through Phase 5.
|
||||
|
||||
---
|
||||
|
||||
## 2.3 Span Naming Conventions
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List | **WS** = WebSocket
|
||||
|
||||
### 2.3.1 Naming Schema
|
||||
|
||||
```
|
||||
<component>.<operation>[.<sub-operation>]
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
- `tx.receive` - Transaction received from peer
|
||||
- `consensus.phase.establish` - Consensus establish phase
|
||||
- `rpc.command.server_info` - server_info RPC command
|
||||
|
||||
### 2.3.2 Complete Span Catalog
|
||||
|
||||
```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"
|
||||
|
||||
# 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"
|
||||
|
||||
# RPC Spans
|
||||
rpc:
|
||||
request: "HTTP/WebSocket request handling"
|
||||
command:
|
||||
"*": "Specific RPC command (dynamic)"
|
||||
|
||||
# Peer Spans
|
||||
peer:
|
||||
connect: "Peer connection establishment"
|
||||
disconnect: "Peer disconnection"
|
||||
message:
|
||||
send: "Send protocol message"
|
||||
receive: "Receive protocol message"
|
||||
|
||||
# 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"
|
||||
|
||||
# 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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2.4 Attribute Schema
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List | **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 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 = <node_public_key_base58>
|
||||
|
||||
// Custom xrpld attributes
|
||||
"xrpl.network.id" = <network_id> // e.g., 0 for mainnet
|
||||
"xrpl.network.type" = "mainnet" | "testnet" | "devnet" | "standalone"
|
||||
"xrpl.node.type" = "validator" | "stock" | "reporting"
|
||||
"xrpl.node.cluster" = <cluster_name> // If clustered
|
||||
```
|
||||
|
||||
### 2.4.2 Span Attributes by Category
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
#### 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)
|
||||
```
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
#### 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)
|
||||
```
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
#### Fee Attributes
|
||||
|
||||
```cpp
|
||||
"xrpl.fee.load_factor" = int64 // Current load factor
|
||||
"xrpl.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
|
||||
```
|
||||
|
||||
#### Amendment Attributes
|
||||
|
||||
```cpp
|
||||
"xrpl.amendment.name" = string // Amendment name
|
||||
"xrpl.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
|
||||
```
|
||||
|
||||
### 2.4.3 Data Collection Summary
|
||||
|
||||
The following table summarizes what data is collected by category:
|
||||
|
||||
| Category | Attributes Collected | Purpose |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------- |
|
||||
| **Transaction** | `tx.hash`, `tx.type`, `tx.result`, `tx.fee`, `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 |
|
||||
| **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 |
|
||||
|
||||
### 2.4.4 Privacy & Sensitive Data Policy
|
||||
|
||||
> **PII** = Personally Identifiable Information
|
||||
|
||||
OpenTelemetry instrumentation is designed to collect **operational metadata only**, never sensitive content.
|
||||
|
||||
#### Data NOT Collected
|
||||
|
||||
The following data is explicitly **excluded** from telemetry collection:
|
||||
|
||||
| Excluded Data | Reason |
|
||||
| ----------------------- | ----------------------------------------- |
|
||||
| **Private Keys** | Never exposed; not relevant to tracing |
|
||||
| **Account Balances** | Financial data; privacy sensitive |
|
||||
| **Transaction Amounts** | Financial data; privacy sensitive |
|
||||
| **Raw TX Payloads** | May contain sensitive memo/data fields |
|
||||
| **Personal Data** | No PII collected |
|
||||
| **IP Addresses** | Configurable; excluded by default in prod |
|
||||
|
||||
#### Privacy Protection Mechanisms
|
||||
|
||||
| Mechanism | Description |
|
||||
| ----------------------------- | ------------------------------------------------------------------------- |
|
||||
| **Account Hashing** | `xrpl.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 |
|
||||
| **No Raw Payloads** | Transaction content is never recorded, only metadata (hash, type, result) |
|
||||
| **Collector-Level Filtering** | Additional redaction/hashing can be configured at OTel Collector |
|
||||
|
||||
#### Collector-Level Data Protection
|
||||
|
||||
The OpenTelemetry Collector can be configured 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
|
||||
```
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
> **Note**: The `redact_account` configuration in `xrpld.cfg` controls SDK-level redaction before export, while collector-level filtering (see [Collector-Level Data Protection](#collector-level-data-protection) above) provides an additional defense-in-depth layer. Both can operate independently.
|
||||
|
||||
> **Key Principle**: Telemetry collects **operational metadata** (timing, counts, hashes) — never **sensitive content** (keys, balances, amounts, raw payloads).
|
||||
|
||||
> **See also**: [Securing the OTel Pipeline](./secure-OTel.md) covers transport-level protection for telemetry leaving the node — mTLS to the collector and validation of incoming peer trace context. Privacy controls in this section keep sensitive data out of spans; the security doc keeps the spans themselves out of untrusted hands.
|
||||
|
||||
---
|
||||
|
||||
## 2.5 Context Propagation Design
|
||||
|
||||
> **WS** = WebSocket
|
||||
|
||||
### 2.5.0 Deterministic Trace ID Strategy
|
||||
|
||||
Both transaction and consensus tracing use **deterministic trace IDs** derived from
|
||||
a globally known hash, so all nodes handling the same workflow independently produce
|
||||
spans under the same `trace_id`. This is combined with protobuf `span_id` propagation
|
||||
for parent-child relay ordering when available.
|
||||
|
||||
#### Transactions — `trace_id = txHash[0:16]`
|
||||
|
||||
Every node that handles a transaction knows its `txID` (the `uint256` transaction
|
||||
hash). The first 16 bytes of this hash are used as the OTel `trace_id`:
|
||||
|
||||
```
|
||||
uint256 txHash: A1B2C3D4 E5F6A7B8 C9D0E1F2 A3B4C5D6 E7F8A9B0 C1D2E3F4 A5B6C7D8 E9F0A1B2
|
||||
|---------- trace_id (16 bytes) ---------| (remaining 16 bytes unused)
|
||||
```
|
||||
|
||||
Each node generates a **random 8-byte `span_id`** so its span is unique within the
|
||||
shared trace. When protobuf `TraceContext` is present in the incoming `TMTransaction`,
|
||||
the sender's `span_id` is extracted and used as the parent — preserving the relay
|
||||
chain as a parent-child tree. When absent (older peers, first hop from client), the
|
||||
span appears as a root in the same trace — correlation is preserved, only the tree
|
||||
structure degrades.
|
||||
|
||||
```
|
||||
Node A (submitter) Node B (relay) Node C (relay)
|
||||
trace_id: A1B2... trace_id: A1B2... trace_id: A1B2...
|
||||
span_id: 1234 (random) span_id: 5678 (random) span_id: 9ABC (random)
|
||||
parent: (none) parent: 1234 (proto) parent: 5678 (proto)
|
||||
↑ ↑
|
||||
protobuf propagation protobuf propagation
|
||||
```
|
||||
|
||||
If protobuf propagation fails at Node B (old peer):
|
||||
|
||||
```
|
||||
Node A Node B (old peer) Node C
|
||||
trace_id: A1B2... trace_id: A1B2... trace_id: A1B2...
|
||||
span_id: 1234 span_id: 5678 span_id: 9ABC
|
||||
parent: (none) parent: (none) parent: 5678 (proto)
|
||||
↑ no parent, but same trace_id — still grouped
|
||||
```
|
||||
|
||||
#### Consensus — `trace_id = prevLedgerHash[0:16]`
|
||||
|
||||
All validators in the same consensus round share the same `previousLedger.id()`.
|
||||
The first 16 bytes are used as trace_id. See [Phase 4a implementation status](./06-implementation-phases.md)
|
||||
and `createDeterministicContext()` in `RCLConsensus.cpp` for the implementation.
|
||||
|
||||
Switchable via `consensus_trace_strategy` config:
|
||||
`"deterministic"` (default) or `"attribute"` (random trace_id, correlation via attribute queries).
|
||||
|
||||
#### Why Not Random IDs with Propagation Only?
|
||||
|
||||
Random trace IDs require **unbroken context propagation** across every hop. In a
|
||||
mixed-version network (common during upgrades), older peers silently drop the
|
||||
`trace_context` protobuf field. The trace splits and downstream spans become
|
||||
impossible to find. Deterministic IDs make correlation **propagation-resilient** — the trace
|
||||
backend groups all spans for the same transaction/round regardless of whether
|
||||
propagation succeeded.
|
||||
|
||||
#### Why Keep Protobuf Propagation?
|
||||
|
||||
Deterministic trace IDs alone provide correlation (all spans grouped) but not
|
||||
**causality** (which node relayed to which). Protobuf `span_id` propagation adds
|
||||
parent-child ordering that shows the exact relay path. The two mechanisms complement
|
||||
each other:
|
||||
|
||||
| Mechanism | Provides | Fails when |
|
||||
| ---------------------------- | --------------------------- | -------------------------------------- |
|
||||
| Deterministic trace_id | Cross-node correlation | Never (hash is always known) |
|
||||
| Protobuf span_id propagation | Parent-child relay ordering | Older peer drops `trace_context` field |
|
||||
|
||||
#### Implementation Reference
|
||||
|
||||
The utility function `createDeterministicTxContext(uint256 const& txHash)` follows
|
||||
the same pattern as `createDeterministicContext(uint256 const& ledgerId)` in
|
||||
`RCLConsensus.cpp`. See [Phase 3 Task 3.9](./Phase3_taskList.md) for the full spec.
|
||||
|
||||
### 2.5.1 Propagation Boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph http["HTTP/WebSocket (RPC)"]
|
||||
w3c["W3C Trace Context Headers:<br/>traceparent:<br/>00-trace_id-span_id-flags<br/>tracestate: xrpld=..."]
|
||||
end
|
||||
|
||||
subgraph protobuf["Protocol Buffers (P2P)"]
|
||||
proto["message TraceContext {<br/> bytes trace_id = 1; // 16 bytes<br/> bytes span_id = 2; // 8 bytes<br/> uint32 trace_flags = 3;<br/> string trace_state = 4;<br/>}"]
|
||||
end
|
||||
|
||||
subgraph jobqueue["JobQueue (Internal Async)"]
|
||||
job["Context captured at job creation,<br/>restored at execution<br/><br/>class Job {<br/> otel::context::Context<br/> traceContext_;<br/>};"]
|
||||
end
|
||||
|
||||
style http fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style protobuf fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style jobqueue fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **HTTP/WebSocket - RPC (blue)**: For client-facing RPC requests, trace context is propagated using the W3C `traceparent` header. This is the standard approach and works with any OTel-compatible client.
|
||||
- **Protocol Buffers - P2P (green)**: For peer-to-peer messages between xrpld nodes, trace context is embedded as a protobuf `TraceContext` message carrying trace_id, span_id, flags, and optional trace_state.
|
||||
- **JobQueue - Internal Async (red)**: For asynchronous work within a single node, the OTel context is captured when a job is created and restored when the job executes on a worker thread. This bridges the async gap so spans remain linked.
|
||||
|
||||
---
|
||||
|
||||
## 2.6 Integration with Existing Observability
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **WS** = WebSocket
|
||||
|
||||
### 2.6.1 Existing Frameworks Comparison
|
||||
|
||||
xrpld already has two observability mechanisms. OpenTelemetry complements (not replaces) them:
|
||||
|
||||
| Aspect | PerfLog | Beast Insight (StatsD) | OpenTelemetry |
|
||||
| --------------------- | ----------------------------- | ---------------------------- | ------------------------- |
|
||||
| **Type** | Logging | Metrics | Distributed Tracing |
|
||||
| **Data** | JSON log entries | Counters, gauges, histograms | Spans with context |
|
||||
| **Scope** | Single node | Single node | **Cross-node** |
|
||||
| **Output** | `perf.log` file | StatsD server | OTLP Collector |
|
||||
| **Question answered** | "What happened on this node?" | "How many? How fast?" | "What was the journey?" |
|
||||
| **Correlation** | By timestamp | By metric name | By `trace_id` |
|
||||
| **Overhead** | Low (file I/O) | Low (UDP packets) | Low-Medium (configurable) |
|
||||
|
||||
### 2.6.2 What Each Framework Does Best
|
||||
|
||||
#### PerfLog
|
||||
|
||||
- **Purpose**: Detailed local event logging for RPC and job execution
|
||||
- **Strengths**:
|
||||
- Rich JSON output with timing data
|
||||
- Already integrated in RPC handlers
|
||||
- File-based, no external dependencies
|
||||
- **Limitations**:
|
||||
- Single-node only (no cross-node correlation)
|
||||
- No parent-child relationships between events
|
||||
- Manual log parsing required
|
||||
|
||||
```json
|
||||
// Example PerfLog entry
|
||||
{
|
||||
"time": "2024-01-15T10:30:00.123Z",
|
||||
"method": "submit",
|
||||
"duration_us": 1523,
|
||||
"result": "tesSUCCESS"
|
||||
}
|
||||
```
|
||||
|
||||
#### Beast Insight (StatsD)
|
||||
|
||||
- **Purpose**: Real-time metrics for monitoring dashboards
|
||||
- **Strengths**:
|
||||
- Aggregated metrics (counters, gauges, histograms)
|
||||
- Low overhead (UDP, fire-and-forget)
|
||||
- Good for alerting thresholds
|
||||
- **Limitations**:
|
||||
- No request-level detail
|
||||
- No causal relationships
|
||||
- Single-node perspective
|
||||
|
||||
```cpp
|
||||
// Example StatsD usage in xrpld
|
||||
insight.increment("rpc.submit.count");
|
||||
insight.gauge("ledger.age", age);
|
||||
insight.timing("consensus.round", duration);
|
||||
```
|
||||
|
||||
#### OpenTelemetry (NEW)
|
||||
|
||||
- **Purpose**: Distributed request tracing across nodes
|
||||
- **Strengths**:
|
||||
- **Cross-node correlation** via `trace_id`
|
||||
- Parent-child span relationships
|
||||
- Rich attributes per span
|
||||
- Industry standard (CNCF)
|
||||
- **Limitations**:
|
||||
- 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
|
||||
```
|
||||
|
||||
### 2.6.3 When to Use Each
|
||||
|
||||
| Scenario | PerfLog | StatsD | OpenTelemetry |
|
||||
| --------------------------------------- | ---------- | ------ | ------------- |
|
||||
| "How many TXs per second?" | ❌ | ✅ | ✅ |
|
||||
| "What's the p99 RPC latency?" | ❌ | ✅ | ✅ |
|
||||
| "Why was this specific TX slow?" | ⚠️ partial | ❌ | ✅ |
|
||||
| "Which node delayed consensus?" | ❌ | ❌ | ✅ |
|
||||
| "What happened on node X at time T?" | ✅ | ❌ | ✅ |
|
||||
| "Show me the TX journey across 5 nodes" | ❌ | ❌ | ✅ |
|
||||
|
||||
### 2.6.4 Coexistence Strategy
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Process"]
|
||||
perflog["PerfLog<br/>(JSON to file)"]
|
||||
insight["Beast Insight<br/>(StatsD)"]
|
||||
otel["OpenTelemetry<br/>(Tracing)"]
|
||||
end
|
||||
|
||||
perflog --> perffile["perf.log"]
|
||||
insight --> statsd["StatsD Server"]
|
||||
otel --> collector["OTLP Collector"]
|
||||
|
||||
perffile --> grafana["Grafana<br/>(Unified UI)"]
|
||||
statsd --> grafana
|
||||
collector --> grafana
|
||||
|
||||
style xrpld fill:#212121,stroke:#0a0a0a,color:#ffffff
|
||||
style grafana fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Process (dark gray)**: The single xrpld node running all three observability frameworks side by side. Each framework operates independently with no interference.
|
||||
- **PerfLog to perf.log**: PerfLog writes JSON-formatted event logs to a local file. Grafana can ingest these via Loki or a file-based datasource.
|
||||
- **Beast Insight to StatsD Server**: Insight sends aggregated metrics (counters, gauges) over UDP to a StatsD server. Grafana reads from StatsD-compatible backends like Graphite or Prometheus (via StatsD exporter).
|
||||
- **OpenTelemetry to OTLP Collector**: OTel exports spans over OTLP/gRPC to a Collector, which then forwards to a trace backend (Tempo).
|
||||
- **Grafana (red, unified UI)**: All three data streams converge in Grafana, enabling operators to correlate logs, metrics, and traces in a single dashboard.
|
||||
|
||||
### 2.6.5 Correlation with PerfLog
|
||||
|
||||
Trace IDs can be correlated with existing PerfLog entries for comprehensive debugging:
|
||||
|
||||
```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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Architecture Analysis](./01-architecture-analysis.md)_ | _Next: [Implementation Strategy](./03-implementation-strategy.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
530
OpenTelemetryPlan/03-implementation-strategy.md
Normal file
530
OpenTelemetryPlan/03-implementation-strategy.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# Implementation Strategy
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Code Samples](./04-code-samples.md) | [Configuration Reference](./05-configuration-reference.md)
|
||||
|
||||
---
|
||||
|
||||
## 3.1 Directory Structure
|
||||
|
||||
The telemetry implementation follows xrpld's existing code organization pattern:
|
||||
|
||||
```
|
||||
include/xrpl/
|
||||
├── telemetry/
|
||||
│ ├── Telemetry.h # Main telemetry interface (global singleton)
|
||||
│ ├── TelemetryConfig.h # Configuration structures
|
||||
│ ├── TraceContext.h # Context propagation utilities
|
||||
│ ├── SpanGuard.h # RAII span management with factory methods + discard()
|
||||
│ ├── DiscardFlag.h # Thread-local discard flag
|
||||
│ └── SpanAttributes.h # Attribute helper functions
|
||||
|
||||
src/libxrpl/
|
||||
├── telemetry/
|
||||
│ ├── Telemetry.cpp # Implementation + FilteringSpanProcessor
|
||||
│ ├── TelemetryConfig.cpp # Config parsing
|
||||
│ ├── TraceContext.cpp # Context serialization
|
||||
│ └── NullTelemetry.cpp # No-op implementation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3.2 Implementation Approach
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 30}}}%%
|
||||
flowchart TB
|
||||
subgraph phase1["Phase 1: Core"]
|
||||
direction LR
|
||||
sdk["SDK Integration"] ~~~ interface["Telemetry Interface"] ~~~ config["Configuration"]
|
||||
end
|
||||
|
||||
subgraph phase2["Phase 2: RPC"]
|
||||
direction LR
|
||||
http["HTTP Context"] ~~~ rpc["RPC Handlers"]
|
||||
end
|
||||
|
||||
subgraph phase3["Phase 3: P2P"]
|
||||
direction LR
|
||||
proto["Protobuf Context"] ~~~ tx["Transaction Relay"]
|
||||
end
|
||||
|
||||
subgraph phase4["Phase 4: Consensus"]
|
||||
direction LR
|
||||
consensus["Consensus Rounds"] ~~~ proposals["Proposals"]
|
||||
end
|
||||
|
||||
phase1 --> phase2 --> phase3 --> phase4
|
||||
|
||||
style phase1 fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style phase2 fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style phase3 fill:#e65100,stroke:#bf360c,color:#ffffff
|
||||
style phase4 fill:#c2185b,stroke:#880e4f,color:#ffffff
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Minimal Intrusion**: Instrumentation should not alter existing control flow
|
||||
2. **Zero-Cost When Disabled**: Use compile-time flags and no-op implementations
|
||||
3. **Backward Compatibility**: Protocol Buffer extensions use high field numbers
|
||||
4. **Graceful Degradation**: Tracing failures must not affect node operation
|
||||
|
||||
---
|
||||
|
||||
## 3.3 Performance Overhead Summary
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
| Metric | Overhead | Notes |
|
||||
| ------------- | ---------- | ------------------------------------------------ |
|
||||
| CPU | 1-3% | Of per-transaction CPU cost (~200μs baseline) |
|
||||
| Memory | ~10 MB | SDK statics + batch buffer + worker thread stack |
|
||||
| Network | 10-50 KB/s | Compressed OTLP export to collector |
|
||||
| Latency (p99) | <2% | With proper sampling configuration |
|
||||
|
||||
---
|
||||
|
||||
## 3.4 Detailed CPU Overhead Analysis
|
||||
|
||||
### 3.4.1 Per-Operation Costs
|
||||
|
||||
> **Note on hardware assumptions**: The costs below are based on the official OTel C++ SDK CI benchmarks
|
||||
> (969 runs on GitHub Actions 2-core shared runners). On production server hardware (3+ GHz Xeon),
|
||||
> expect costs at the **lower end** of each range (~30-50% improvement over CI hardware).
|
||||
|
||||
| Operation | Time (ns) | Frequency | Impact |
|
||||
| --------------------- | --------- | ---------------------- | ---------- |
|
||||
| Span creation | 500-1000 | Every traced operation | Low |
|
||||
| Span end | 100-200 | Every traced operation | Low |
|
||||
| SetAttribute (string) | 80-120 | 3-5 per span | Low |
|
||||
| SetAttribute (int) | 40-60 | 2-3 per span | Negligible |
|
||||
| AddEvent | 100-200 | 0-2 per span | Low |
|
||||
| Context injection | 150-250 | Per outgoing message | Low |
|
||||
| Context extraction | 100-180 | Per incoming message | Low |
|
||||
| GetCurrent context | 10-20 | Thread-local access | Negligible |
|
||||
|
||||
**Source**: Span creation based on OTel C++ SDK `BM_SpanCreation` benchmark (AlwaysOnSampler +
|
||||
SimpleSpanProcessor + InMemoryExporter), median ~1,000 ns on CI hardware. AddEvent includes
|
||||
timestamp read + string copy + vector push + mutex acquisition. Context injection/extraction
|
||||
confirmed by `BM_SpanCreationWithScope` benchmark delta (~160 ns).
|
||||
|
||||
### 3.4.2 Transaction Processing Overhead
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
%%{init: {'pie': {'textPosition': 0.75}}}%%
|
||||
pie showData
|
||||
"tx.receive (1400ns)" : 1400
|
||||
"tx.validate (1200ns)" : 1200
|
||||
"tx.relay (1200ns)" : 1200
|
||||
"Context inject (200ns)" : 200
|
||||
```
|
||||
|
||||
**Transaction Tracing Overhead (~4.0μs total)**
|
||||
|
||||
</div>
|
||||
|
||||
**Overhead percentage**: 4.0 μs / 200 μs (avg tx processing) = **~2.0%**
|
||||
|
||||
> **Breakdown**: Each span (tx.receive, tx.validate, tx.relay) costs ~1,000 ns for creation plus
|
||||
> ~200-400 ns for 3-5 attribute sets. Context injection is ~200 ns (confirmed by benchmarks).
|
||||
> On production hardware, expect ~2.6 μs total (~1.3% overhead) due to faster span creation (~500-600 ns).
|
||||
|
||||
### 3.4.3 Consensus Round Overhead
|
||||
|
||||
| Operation | Count | Cost (ns) | Total |
|
||||
| ---------------------- | ----- | --------- | ---------- |
|
||||
| consensus.round span | 1 | ~1200 | ~1.2 μs |
|
||||
| consensus.phase spans | 3 | ~1100 | ~3.3 μs |
|
||||
| proposal.receive spans | ~20 | ~1100 | ~22 μs |
|
||||
| proposal.send spans | ~3 | ~1100 | ~3.3 μs |
|
||||
| Context operations | ~30 | ~200 | ~6 μs |
|
||||
| **TOTAL** | | | **~36 μs** |
|
||||
|
||||
> **Why higher**: Each span costs ~1,000 ns creation + ~100-200 ns for 1-2 attributes, totaling ~1,100-1,200 ns.
|
||||
> Context operations remain ~200 ns (confirmed by benchmarks). On production hardware, expect ~24 μs total.
|
||||
|
||||
**Overhead percentage**: 36 μs / 3s (typical round) = **~0.001%** (negligible)
|
||||
|
||||
### 3.4.4 RPC Request Overhead
|
||||
|
||||
| Operation | Cost (ns) |
|
||||
| ---------------- | ------------ |
|
||||
| rpc.request span | ~1200 |
|
||||
| rpc.command span | ~1100 |
|
||||
| Context extract | ~250 |
|
||||
| Context inject | ~200 |
|
||||
| **TOTAL** | **~2.75 μs** |
|
||||
|
||||
> **Why higher**: Each span costs ~1,000 ns creation + ~100-200 ns for attributes (command name,
|
||||
> version, role). Context extract/inject costs are confirmed by OTel C++ benchmarks.
|
||||
|
||||
- Fast RPC (1ms): 2.75 μs / 1ms = **~0.275%**
|
||||
- Slow RPC (100ms): 2.75 μs / 100ms = **~0.003%**
|
||||
|
||||
---
|
||||
|
||||
## 3.5 Memory Overhead Analysis
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 3.5.1 Static Memory
|
||||
|
||||
| Component | Size | Allocated |
|
||||
| ------------------------------------ | ----------- | ---------- |
|
||||
| TracerProvider singleton | ~64 KB | At startup |
|
||||
| BatchSpanProcessor (circular buffer) | ~16 KB | At startup |
|
||||
| BatchSpanProcessor (worker thread) | ~8 MB | At startup |
|
||||
| OTLP exporter (gRPC channel init) | ~256 KB | At startup |
|
||||
| Propagator registry | ~8 KB | At startup |
|
||||
| **Total static** | **~8.3 MB** | |
|
||||
|
||||
> **Why higher than earlier estimate**: The BatchSpanProcessor's circular buffer itself is only ~16 KB
|
||||
> (2049 x 8-byte `AtomicUniquePtr` entries), but it spawns a dedicated worker thread whose default
|
||||
> stack size on Linux is ~8 MB. The OTLP gRPC exporter allocates memory for channel stubs and TLS
|
||||
> initialization. The worker thread stack dominates the static footprint.
|
||||
|
||||
### 3.5.2 Dynamic Memory
|
||||
|
||||
| Component | Size per unit | Max units | Peak |
|
||||
| -------------------- | -------------- | ---------- | --------------- |
|
||||
| Active span | ~500-800 bytes | 1000 | ~500-800 KB |
|
||||
| Queued span (export) | ~500 bytes | 2048 | ~1 MB |
|
||||
| Attribute storage | ~80 bytes | 5 per span | Included |
|
||||
| Context storage | ~64 bytes | Per thread | ~6.4 KB |
|
||||
| **Total dynamic** | | | **~1.5-1.8 MB** |
|
||||
|
||||
> **Why active spans are larger**: An active `Span` object includes the wrapper (~88 bytes: shared_ptr,
|
||||
> mutex, unique_ptr to Recordable) plus `SpanData` (~250 bytes: SpanContext, timestamps, name, status,
|
||||
> empty containers) plus attribute storage (~200-500 bytes for 3-5 string attributes in a `std::map`).
|
||||
> Source: `sdk/src/trace/span.h` and `sdk/include/opentelemetry/sdk/trace/span_data.h`.
|
||||
> Queued spans release the wrapper, keeping only `SpanData` + attributes (~500 bytes).
|
||||
|
||||
### 3.5.3 Memory Growth Characteristics
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 700
|
||||
height: 400
|
||||
---
|
||||
xychart-beta
|
||||
title "Memory Usage vs Span Rate (bounded by queue limit)"
|
||||
x-axis "Spans/second" [0, 200, 400, 600, 800, 1000]
|
||||
y-axis "Memory (MB)" 0 --> 12
|
||||
line [8.5, 9.2, 9.6, 9.9, 10.0, 10.0]
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
|
||||
- Memory increases with span rate but **plateaus at queue capacity** (default 2048 spans)
|
||||
- Batch export prevents unbounded growth
|
||||
- At queue limit, oldest spans are dropped (not blocked)
|
||||
- Maximum memory is bounded: ~8.3 MB static (dominated by worker thread stack) + 2048 queued spans x ~500 bytes (~1 MB) + active spans (~0.8 MB) ≈ **~10 MB ceiling**
|
||||
- The worker thread stack (~8 MB) is virtual memory; actual RSS depends on stack usage (typically much less)
|
||||
|
||||
### 3.5.4 Performance Data Sources
|
||||
|
||||
The overhead estimates in Sections 3.3-3.5 are derived from the following sources:
|
||||
|
||||
| Source | What it covers | URL |
|
||||
| ------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| OTel C++ SDK CI benchmarks (969 runs) | Span creation, context activation, sampler overhead | [Benchmark Dashboard](https://open-telemetry.github.io/opentelemetry-cpp/benchmarks/) |
|
||||
| `api/test/trace/span_benchmark.cc` | API-level span creation (~22 ns no-op) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/api/test/trace/span_benchmark.cc) |
|
||||
| `sdk/test/trace/sampler_benchmark.cc` | SDK span creation with samplers (~1,000 ns AlwaysOn) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/test/trace/sampler_benchmark.cc) |
|
||||
| `sdk/include/.../span_data.h` | SpanData memory layout (~250 bytes base) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/trace/span_data.h) |
|
||||
| `sdk/src/trace/span.h` | Span wrapper memory layout (~88 bytes) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/src/trace/span.h) |
|
||||
| `sdk/include/.../batch_span_processor_options.h` | Default queue size (2048), batch size (512) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h) |
|
||||
| `sdk/include/.../circular_buffer.h` | CircularBuffer implementation (AtomicUniquePtr array) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/common/circular_buffer.h) |
|
||||
| OTLP proto definition | Serialized span size estimation | [Proto](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto) |
|
||||
|
||||
---
|
||||
|
||||
## 3.6 Network Overhead Analysis
|
||||
|
||||
### 3.6.1 Export Bandwidth
|
||||
|
||||
> **Bytes per span**: Estimates use ~500 bytes/span (conservative upper bound). OTLP protobuf analysis
|
||||
> shows a typical span with 3-5 string attributes serializes to ~200-300 bytes raw; with gzip
|
||||
> compression (~60-70% of raw) and batching (amortized headers), ~350 bytes/span is more realistic.
|
||||
> The table uses the conservative estimate for capacity planning.
|
||||
|
||||
| Sampling Rate | Spans/sec | Bandwidth | Notes |
|
||||
| ------------- | --------- | --------- | ---------------- |
|
||||
| 100% | ~500 | ~250 KB/s | Development only |
|
||||
| 10% | ~50 | ~25 KB/s | Staging |
|
||||
| 1% | ~5 | ~2.5 KB/s | Production |
|
||||
| Error-only | ~1 | ~0.5 KB/s | Minimal overhead |
|
||||
|
||||
### 3.6.2 Trace Context Propagation
|
||||
|
||||
| Message Type | Context Size | Messages/sec | Overhead |
|
||||
| ---------------------- | ------------ | ------------ | ----------- |
|
||||
| TMTransaction | 25 bytes | ~100 | ~2.5 KB/s |
|
||||
| TMProposeSet | 25 bytes | ~10 | ~250 B/s |
|
||||
| TMValidation | 25 bytes | ~50 | ~1.25 KB/s |
|
||||
| **Total P2P overhead** | | | **~4 KB/s** |
|
||||
|
||||
---
|
||||
|
||||
## 3.7 Optimization Strategies
|
||||
|
||||
### 3.7.1 Sampling Strategies
|
||||
|
||||
#### Tail Sampling
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
trace["New Trace"]
|
||||
|
||||
trace --> errors{"Is Error?"}
|
||||
errors -->|Yes| sample["SAMPLE"]
|
||||
errors -->|No| consensus{"Is Consensus?"}
|
||||
|
||||
consensus -->|Yes| sample
|
||||
consensus -->|No| slow{"Is Slow?"}
|
||||
|
||||
slow -->|Yes| sample
|
||||
slow -->|No| prob{"Random < 10%?"}
|
||||
|
||||
prob -->|Yes| sample
|
||||
prob -->|No| drop["DROP"]
|
||||
|
||||
style sample fill:#4caf50,stroke:#388e3c,color:#fff
|
||||
style drop fill:#f44336,stroke:#c62828,color:#fff
|
||||
```
|
||||
|
||||
### 3.7.2 Batch Tuning Recommendations
|
||||
|
||||
| Environment | Batch Size | Batch Delay | Max Queue |
|
||||
| ------------------ | ---------- | ----------- | --------- |
|
||||
| Low-latency | 128 | 1000ms | 512 |
|
||||
| High-throughput | 1024 | 10000ms | 8192 |
|
||||
| Memory-constrained | 256 | 2000ms | 512 |
|
||||
|
||||
### 3.7.3 Conditional Instrumentation
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
## 3.9 Code Intrusiveness Assessment
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
This section provides a detailed assessment of how intrusive the OpenTelemetry integration is to the existing xrpld codebase.
|
||||
|
||||
### 3.9.1 Files Modified Summary
|
||||
|
||||
| Component | Files Modified | Lines Added | Lines Changed | Architectural Impact |
|
||||
| --------------------- | -------------- | ----------- | ------------- | -------------------- |
|
||||
| **Core Telemetry** | 7 new files | ~800 | 0 | None (new module) |
|
||||
| **Application Init** | 2 files | ~30 | ~5 | Minimal |
|
||||
| **RPC Layer** | 3 files | ~80 | ~20 | Minimal |
|
||||
| **Transaction Relay** | 4 files | ~120 | ~40 | Low |
|
||||
| **Consensus** | 3 files | ~100 | ~30 | Low-Medium |
|
||||
| **Protocol Buffers** | 1 file | ~25 | 0 | Low |
|
||||
| **CMake/Build** | 3 files | ~50 | ~10 | Minimal |
|
||||
| **PathFinding** | 2 | ~80 | ~5 | Minimal |
|
||||
| **TxQ/Fee** | 2 | ~60 | ~5 | Minimal |
|
||||
| **Validator/Amend** | 3 | ~40 | ~5 | Minimal |
|
||||
| **Total** | **~27 files** | **~1,490** | **~120** | **Low** |
|
||||
|
||||
### 3.9.2 Detailed File Impact
|
||||
|
||||
```mermaid
|
||||
pie title Code Changes by Component
|
||||
"New Telemetry Module" : 800
|
||||
"Transaction Relay" : 160
|
||||
"Consensus" : 130
|
||||
"RPC Layer" : 100
|
||||
"PathFinding" : 80
|
||||
"TxQ/Fee" : 60
|
||||
"Validator/Amendment" : 40
|
||||
"Application Init" : 35
|
||||
"Protocol Buffers" : 25
|
||||
"Build System" : 60
|
||||
```
|
||||
|
||||
#### New Files (No Impact on Existing Code)
|
||||
|
||||
| File | Lines | Purpose |
|
||||
| ------------------------------------------- | ----- | ----------------------------------------------------- |
|
||||
| `include/xrpl/telemetry/Telemetry.h` | ~160 | Main interface (global singleton) |
|
||||
| `include/xrpl/telemetry/SpanGuard.h` | ~250 | RAII wrapper + factory methods + discard + no-op stub |
|
||||
| `include/xrpl/telemetry/DiscardFlag.h` | ~28 | Thread-local discard flag |
|
||||
| `include/xrpl/telemetry/TraceContext.h` | ~80 | Context propagation |
|
||||
| `src/libxrpl/telemetry/Telemetry.cpp` | ~400 | Implementation + FilteringSpanProcessor |
|
||||
| `src/libxrpl/telemetry/TelemetryConfig.cpp` | ~60 | Config parsing |
|
||||
| `src/libxrpl/telemetry/NullTelemetry.cpp` | ~40 | No-op implementation |
|
||||
|
||||
#### Modified Files (Existing Xrpld Code)
|
||||
|
||||
| File | Lines Added | Lines Changed | Risk Level |
|
||||
| ------------------------------------------------- | ----------- | ------------- | ---------- |
|
||||
| `src/xrpld/app/main/Application.cpp` | ~15 | ~3 | Low |
|
||||
| `include/xrpl/core/ServiceRegistry.h` | ~5 | ~2 | Low |
|
||||
| `src/xrpld/rpc/detail/ServerHandler.cpp` | ~40 | ~10 | Low |
|
||||
| `src/xrpld/rpc/handlers/*.cpp` | ~30 | ~8 | Low |
|
||||
| `src/xrpld/overlay/detail/PeerImp.cpp` | ~60 | ~15 | Medium |
|
||||
| `src/xrpld/overlay/detail/OverlayImpl.cpp` | ~30 | ~10 | Medium |
|
||||
| `src/xrpld/app/consensus/RCLConsensus.cpp` | ~50 | ~15 | Medium |
|
||||
| `src/xrpld/app/consensus/RCLConsensusAdaptor.cpp` | ~40 | ~12 | Medium |
|
||||
| `src/xrpld/core/JobQueue.cpp` | ~20 | ~5 | Low |
|
||||
| `src/xrpld/app/paths/PathRequest.cpp` | ~40 | ~3 | Low |
|
||||
| `src/xrpld/app/paths/Pathfinder.cpp` | ~40 | ~2 | Low |
|
||||
| `src/xrpld/app/misc/TxQ.cpp` | ~40 | ~3 | Low |
|
||||
| `src/xrpld/app/main/LoadManager.cpp` | ~20 | ~2 | Low |
|
||||
| `src/xrpld/app/misc/ValidatorList.cpp` | ~20 | ~2 | Low |
|
||||
| `src/xrpld/app/misc/AmendmentTable.cpp` | ~10 | ~2 | Low |
|
||||
| `src/xrpld/app/misc/Manifest.cpp` | ~10 | ~1 | Low |
|
||||
| `src/xrpld/shamap/SHAMap.cpp` | ~20 | ~3 | Low |
|
||||
| `src/xrpld/overlay/detail/ripple.proto` | ~25 | 0 | Low |
|
||||
| `CMakeLists.txt` | ~40 | ~8 | Low |
|
||||
| `cmake/FindOpenTelemetry.cmake` | ~50 | 0 | None (new) |
|
||||
|
||||
### 3.9.3 Risk Assessment by Component
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Do First** ↖ ↗ **Plan Carefully**
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Code Intrusiveness Risk Matrix
|
||||
x-axis Low Risk --> High Risk
|
||||
y-axis Low Value --> High Value
|
||||
|
||||
RPC Tracing: [0.2, 0.55]
|
||||
Transaction Relay: [0.55, 0.85]
|
||||
Consensus Tracing: [0.75, 0.92]
|
||||
Peer Message Tracing: [0.85, 0.35]
|
||||
JobQueue Context: [0.3, 0.42]
|
||||
Ledger Acquisition: [0.48, 0.65]
|
||||
PathFinding: [0.38, 0.72]
|
||||
TxQ and Fees: [0.25, 0.62]
|
||||
Validator Mgmt: [0.15, 0.35]
|
||||
```
|
||||
|
||||
**Optional** ↙ ↘ **Avoid**
|
||||
|
||||
</div>
|
||||
|
||||
#### Risk Level Definitions
|
||||
|
||||
| Risk Level | Definition | Mitigation |
|
||||
| ---------- | ---------------------------------------------------------------- | ---------------------------------- |
|
||||
| **Low** | Additive changes only; no modification to existing logic | Standard code review |
|
||||
| **Medium** | Minor modifications to existing functions; clear boundaries | Comprehensive unit tests |
|
||||
| **High** | Changes to core logic or data structures; potential side effects | Integration tests + staged rollout |
|
||||
|
||||
### 3.9.4 Architectural Impact Assessment
|
||||
|
||||
| Aspect | Impact | Justification |
|
||||
| -------------------- | ------- | -------------------------------------------------------------------------------- |
|
||||
| **Data Flow** | Minimal | Read-only instrumentation; no modification to consensus or transaction data flow |
|
||||
| **Threading Model** | Minimal | Context propagation uses thread-local storage (standard OTel pattern) |
|
||||
| **Memory Model** | Low | Bounded queues prevent unbounded growth; RAII ensures cleanup |
|
||||
| **Network Protocol** | Low | Optional fields in protobuf (high field numbers); backward compatible |
|
||||
| **Configuration** | None | New config section; existing configs unaffected |
|
||||
| **Build System** | Low | Optional CMake flag; builds work without OpenTelemetry |
|
||||
| **Dependencies** | Low | OpenTelemetry SDK is optional; null implementation when disabled |
|
||||
|
||||
### 3.9.5 Backward Compatibility
|
||||
|
||||
| Compatibility | Status | Notes |
|
||||
| --------------- | ------- | ----------------------------------------------------- |
|
||||
| **Config File** | ✅ Full | New `[telemetry]` section is optional |
|
||||
| **Protocol** | ✅ Full | Optional protobuf fields with high field numbers |
|
||||
| **Build** | ✅ Full | `XRPL_ENABLE_TELEMETRY=OFF` produces identical binary |
|
||||
| **Runtime** | ✅ Full | `enabled=0` produces zero overhead |
|
||||
| **API** | ✅ Full | No changes to public RPC or P2P APIs |
|
||||
|
||||
### 3.9.6 Rollback Strategy
|
||||
|
||||
If issues are discovered after deployment:
|
||||
|
||||
1. **Immediate**: Set `enabled=0` in config and restart (zero code change)
|
||||
2. **Quick**: Rebuild with `XRPL_ENABLE_TELEMETRY=OFF`
|
||||
3. **Complete**: Revert telemetry commits (clean separation makes this easy)
|
||||
|
||||
### 3.9.7 Code Change Examples
|
||||
|
||||
**Minimal RPC Instrumentation (Low Intrusiveness):**
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Design Decisions](./02-design-decisions.md)_ | _Next: [Code Samples](./04-code-samples.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
1018
OpenTelemetryPlan/04-code-samples.md
Normal file
1018
OpenTelemetryPlan/04-code-samples.md
Normal file
File diff suppressed because it is too large
Load Diff
977
OpenTelemetryPlan/05-configuration-reference.md
Normal file
977
OpenTelemetryPlan/05-configuration-reference.md
Normal file
@@ -0,0 +1,977 @@
|
||||
# Configuration Reference
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Code Samples](./04-code-samples.md) | [Implementation Phases](./06-implementation-phases.md)
|
||||
|
||||
---
|
||||
|
||||
## 5.1 xrpld Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **TxQ** = Transaction Queue
|
||||
|
||||
### 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=<node_public_key>
|
||||
|
||||
[telemetry]
|
||||
enabled=0
|
||||
```
|
||||
|
||||
### 5.1.2 Configuration Options Summary
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| -------------------------- | ------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | bool | `false` | Enable/disable telemetry |
|
||||
| `endpoint` | string | `http://localhost:4318/v1/traces` | OTLP/HTTP collector endpoint |
|
||||
| `use_tls` | bool | `false` | Enable TLS for exporter connection |
|
||||
| `tls_ca_cert` | string | `""` | Path to CA certificate file |
|
||||
| `batch_size` | uint | `512` | Spans per export batch |
|
||||
| `batch_delay_ms` | uint | `5000` | Max delay before sending batch (ms) |
|
||||
| `max_queue_size` | uint | `2048` | Maximum queued spans |
|
||||
| `trace_transactions` | bool | `true` | Enable transaction tracing |
|
||||
| `trace_consensus` | bool | `true` | Enable consensus tracing |
|
||||
| `trace_rpc` | bool | `true` | Enable RPC tracing |
|
||||
| `trace_peer` | bool | `true` | Enable peer message tracing (high volume) |
|
||||
| `trace_ledger` | bool | `true` | Enable ledger tracing |
|
||||
| `tx_trace_strategy` | string | `"deterministic"` | TX trace ID strategy: `"deterministic"` (trace_id = txHash[0:16]) or `"attribute"` (random) |
|
||||
| `consensus_trace_strategy` | string | `"deterministic"` | Consensus trace ID strategy: `"deterministic"` (trace_id = prevLedgerHash[0:16]) or `"attribute"` (random) |
|
||||
| `service_name` | string | `"xrpld"` | Service name for traces |
|
||||
| `service_instance_id` | string | `<node_pubkey>` | Instance identifier |
|
||||
|
||||
**Planned (not yet implemented)**: the following options appear in the design
|
||||
documents but are not parsed by `TelemetryConfig.cpp` in Phase 1b and later
|
||||
phases. They will be added as the corresponding subsystems are instrumented:
|
||||
|
||||
| Option | Planned Phase | Purpose |
|
||||
| ----------------- | ------------- | ---------------------------------------- |
|
||||
| `exporter` | Future | Select between OTLP/HTTP and OTLP/gRPC |
|
||||
| `trace_pathfind` | Phase 2 | Path computation tracing toggle |
|
||||
| `trace_txq` | Phase 3 | Transaction queue tracing toggle |
|
||||
| `trace_validator` | Future | Validator list / manifest update tracing |
|
||||
| `trace_amendment` | Future | Amendment voting tracing |
|
||||
|
||||
---
|
||||
|
||||
## 5.2 Configuration Parser
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
```cpp
|
||||
// src/libxrpl/telemetry/TelemetryConfig.cpp
|
||||
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
|
||||
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", "");
|
||||
|
||||
// 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.3 Application Integration
|
||||
|
||||
### 5.3.1 ApplicationImp Changes
|
||||
|
||||
> **Deferred identity**: The node public key (`nodeIdentity_`) is not
|
||||
> available during `ApplicationImp`'s member initializer list — it is
|
||||
> resolved later in `setup()`. The `Telemetry` object is therefore
|
||||
> constructed with an empty `serviceInstanceId` and patched via
|
||||
> `setServiceInstanceId()` once `setup()` has called `getNodeIdentity()`.
|
||||
|
||||
```cpp
|
||||
// src/xrpld/app/main/Application.cpp (modified)
|
||||
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
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::Telemetry> 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_;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 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;
|
||||
};
|
||||
```
|
||||
|
||||
> **Note:** `Application` extends `ServiceRegistry`, so `getTelemetry()` is
|
||||
> available on both. Components that hold a `ServiceRegistry&` (e.g.
|
||||
> `NetworkOPsImp`) call `registry_.get().getTelemetry()`. Components that
|
||||
> still hold an `Application&` (e.g. `ServerHandler`, `PeerImp`,
|
||||
> `RCLConsensusAdaptor`) call `app_.getTelemetry()` directly.
|
||||
|
||||
---
|
||||
|
||||
## 5.4 CMake Integration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 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)
|
||||
```
|
||||
|
||||
### 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()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.5 OpenTelemetry Collector Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring
|
||||
|
||||
> **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.
|
||||
|
||||
### 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]
|
||||
```
|
||||
|
||||
### 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]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.6 Docker Compose Development Environment
|
||||
|
||||
> **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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.7 Configuration Architecture
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph config["Configuration Sources"]
|
||||
cfgFile["xrpld.cfg<br/>[telemetry] section"]
|
||||
cmake["CMake<br/>XRPL_ENABLE_TELEMETRY"]
|
||||
end
|
||||
|
||||
subgraph init["Initialization"]
|
||||
parse["setupTelemetry()"]
|
||||
factory["makeTelemetry()"]
|
||||
end
|
||||
|
||||
subgraph runtime["Runtime Components"]
|
||||
tracer["TracerProvider"]
|
||||
exporter["OTLP Exporter"]
|
||||
processor["BatchProcessor"]
|
||||
end
|
||||
|
||||
subgraph collector["Collector Pipeline"]
|
||||
recv["Receivers"]
|
||||
proc["Processors"]
|
||||
exp["Exporters"]
|
||||
end
|
||||
|
||||
cfgFile --> parse
|
||||
cmake -->|"compile flag"| parse
|
||||
parse --> factory
|
||||
factory --> tracer
|
||||
tracer --> processor
|
||||
processor --> exporter
|
||||
exporter -->|"OTLP"| recv
|
||||
recv --> proc
|
||||
proc --> exp
|
||||
|
||||
style config fill:#e3f2fd,stroke:#1976d2
|
||||
style runtime fill:#e8f5e9,stroke:#388e3c
|
||||
style collector fill:#fff3e0,stroke:#ff9800
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Configuration Sources**: `xrpld.cfg` provides runtime settings (endpoint, sampling) while the CMake flag controls whether telemetry is compiled in at all.
|
||||
- **Initialization**: `setupTelemetry()` parses config values, then `makeTelemetry()` constructs the provider, processor, and exporter objects.
|
||||
- **Runtime Components**: The `TracerProvider` creates spans, the `BatchProcessor` buffers them, and the `OTLP Exporter` serializes and sends them over the wire.
|
||||
- **OTLP arrow to Collector**: Trace data leaves the xrpld process via OTLP (gRPC or HTTP) and enters the external Collector pipeline.
|
||||
- **Collector Pipeline**: `Receivers` ingest OTLP data, `Processors` apply sampling/filtering/enrichment, and `Exporters` forward traces to storage backends (Tempo, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 5.8 Grafana Integration
|
||||
|
||||
> **APM** = Application Performance Monitoring
|
||||
|
||||
Step-by-step instructions for integrating xrpld traces with Grafana.
|
||||
|
||||
### 5.8.1 Data Source Configuration
|
||||
|
||||
#### Tempo (Recommended)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### 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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.8.5 TraceQL Query Examples
|
||||
|
||||
Common queries for xrpld traces:
|
||||
|
||||
```
|
||||
# Find all traces for a specific transaction hash
|
||||
{resource.service.name="xrpld" && span.xrpl.tx.hash="ABC123..."}
|
||||
|
||||
# Find slow RPC commands (>100ms)
|
||||
{resource.service.name="xrpld" && name=~"rpc.command.*"} | duration > 100ms
|
||||
|
||||
# Find consensus rounds taking >5 seconds
|
||||
{resource.service.name="xrpld" && name="consensus.round"} | duration > 5s
|
||||
|
||||
# Find failed transactions with error details
|
||||
{resource.service.name="xrpld" && name="tx.validate" && status.code=error}
|
||||
|
||||
# Find transactions relayed to many peers
|
||||
{resource.service.name="xrpld" && name="tx.relay"} | span.xrpl.tx.relay_count > 10
|
||||
|
||||
# Compare latency across nodes
|
||||
{resource.service.name="xrpld" && name="rpc.command.account_info"} | avg(duration) by (resource.service.instance.id)
|
||||
```
|
||||
|
||||
### 5.8.6 Correlation with PerfLog
|
||||
|
||||
To correlate OpenTelemetry traces with existing PerfLog data:
|
||||
|
||||
**Step 1: Configure Loki to ingest PerfLog**
|
||||
|
||||
```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:
|
||||
```
|
||||
|
||||
**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
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
### 5.8.7 Correlation with Insight/StatsD Metrics
|
||||
|
||||
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"]
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
**Step 3: Configure Grafana metric-to-trace link**
|
||||
|
||||
```yaml
|
||||
# In Prometheus data source
|
||||
jsonData:
|
||||
exemplarTraceIdDestinations:
|
||||
- name: trace_id
|
||||
datasourceUid: tempo
|
||||
```
|
||||
|
||||
**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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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)_
|
||||
590
OpenTelemetryPlan/06-implementation-phases.md
Normal file
590
OpenTelemetryPlan/06-implementation-phases.md
Normal file
@@ -0,0 +1,590 @@
|
||||
# Implementation Phases
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Configuration Reference](./05-configuration-reference.md) | [Observability Backends](./07-observability-backends.md)
|
||||
|
||||
---
|
||||
|
||||
## 6.1 Phase Overview
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title OpenTelemetry Implementation Timeline
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat Week %W
|
||||
|
||||
section Phase 1
|
||||
Core Infrastructure :p1, 2024-01-01, 2w
|
||||
SDK Integration :p1a, 2024-01-01, 4d
|
||||
Telemetry Interface :p1b, after p1a, 3d
|
||||
Configuration & CMake :p1c, after p1b, 3d
|
||||
Unit Tests :p1d, after p1c, 2d
|
||||
Buffer & Integration :p1e, after p1d, 2d
|
||||
|
||||
section Phase 2
|
||||
RPC Tracing :p2, after p1, 2w
|
||||
HTTP Context Extraction :p2a, after p1, 2d
|
||||
RPC Handler Instrumentation :p2b, after p2a, 4d
|
||||
PathFinding Instrumentation :p2f, after p2b, 2d
|
||||
TxQ Instrumentation :p2g, after p2f, 2d
|
||||
WebSocket Support :p2c, after p2g, 2d
|
||||
Integration Tests :p2d, after p2c, 2d
|
||||
Buffer & Review :p2e, after p2d, 4d
|
||||
|
||||
section Phase 3
|
||||
Transaction Tracing :p3, after p2, 2w
|
||||
Protocol Buffer Extension :p3a, after p2, 2d
|
||||
PeerImp Instrumentation :p3b, after p3a, 3d
|
||||
Fee Escalation Instrumentation :p3f, after p3b, 2d
|
||||
Relay Context Propagation :p3c, after p3f, 3d
|
||||
Multi-node Tests :p3d, after p3c, 2d
|
||||
Buffer & Review :p3e, after p3d, 4d
|
||||
|
||||
section Phase 4
|
||||
Consensus Tracing :p4, after p3, 2w
|
||||
Consensus Round Spans :p4a, after p3, 3d
|
||||
Proposal Handling :p4b, after p4a, 3d
|
||||
Validator List & Manifest Tracing :p4f, after p4b, 2d
|
||||
Amendment Voting Tracing :p4g, after p4f, 2d
|
||||
SHAMap Sync Tracing :p4h, after p4g, 2d
|
||||
Validation Tests :p4c, after p4h, 4d
|
||||
Buffer & Review :p4e, after p4c, 4d
|
||||
|
||||
section Phase 5
|
||||
Documentation & Deploy :p5, after p4, 1w
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.2 Phase 1: Core Infrastructure (Weeks 1-2)
|
||||
|
||||
**Objective**: Establish foundational telemetry infrastructure
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | ----------------------------------------------------- |
|
||||
| 1.1 | Add OpenTelemetry C++ SDK to Conan/CMake |
|
||||
| 1.2 | Implement `Telemetry` interface and factory |
|
||||
| 1.3 | Implement `SpanGuard` RAII wrapper |
|
||||
| 1.4 | Implement configuration parser |
|
||||
| 1.5 | Integrate into `ApplicationImp` |
|
||||
| 1.6 | Add conditional compilation (`XRPL_ENABLE_TELEMETRY`) |
|
||||
| 1.7 | Create `NullTelemetry` no-op implementation |
|
||||
| 1.8 | Unit tests for core infrastructure |
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- [ ] OpenTelemetry SDK compiles and links
|
||||
- [ ] Telemetry can be enabled/disabled via config
|
||||
- [ ] Basic span creation works
|
||||
- [ ] No performance regression when disabled
|
||||
- [ ] Unit tests passing
|
||||
|
||||
---
|
||||
|
||||
## 6.3 Phase 2: RPC Tracing (Weeks 3-4)
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
**Objective**: Complete tracing for all RPC operations
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | -------------------------------------------------------------------------- |
|
||||
| 2.1 | Implement W3C Trace Context HTTP header extraction |
|
||||
| 2.2 | Instrument `ServerHandler::onRequest()` |
|
||||
| 2.3 | Instrument `RPCHandler::doCommand()` |
|
||||
| 2.4 | Add RPC-specific attributes |
|
||||
| 2.5 | Instrument WebSocket handler |
|
||||
| 2.6 | PathFinding instrumentation (`pathfind.request`, `pathfind.compute` spans) |
|
||||
| 2.7 | TxQ instrumentation (`txq.enqueue`, `txq.apply` spans) |
|
||||
| 2.8 | Integration tests for RPC tracing |
|
||||
| 2.9 | Performance benchmarks |
|
||||
| 2.10 | Documentation |
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- [ ] All RPC commands traced
|
||||
- [ ] Trace context propagates from HTTP headers
|
||||
- [ ] WebSocket and HTTP both instrumented
|
||||
- [ ] <1ms overhead per RPC call
|
||||
- [ ] Integration tests passing
|
||||
|
||||
---
|
||||
|
||||
## 6.4 Phase 3: Transaction Tracing (Weeks 5-6)
|
||||
|
||||
**Objective**: Trace transaction lifecycle across network with deterministic cross-node correlation
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | -------------------------------------------------------------- |
|
||||
| 3.1 | Define `TraceContext` Protocol Buffer message |
|
||||
| 3.2 | Implement protobuf context serialization |
|
||||
| 3.3 | Instrument `PeerImp::handleTransaction()` |
|
||||
| 3.4 | Instrument `NetworkOPs::submitTransaction()` |
|
||||
| 3.5 | Instrument HashRouter integration |
|
||||
| 3.6 | Fee escalation instrumentation (`fee.escalate` span) |
|
||||
| 3.7 | Implement relay context propagation |
|
||||
| 3.8 | Integration tests (multi-node) |
|
||||
| 3.9 | Deterministic transaction trace ID (`trace_id = txHash[0:16]`) |
|
||||
| 3.10 | Performance benchmarks |
|
||||
|
||||
### Deterministic Trace ID (Task 3.9)
|
||||
|
||||
Transaction spans use **deterministic trace IDs** derived from the transaction hash:
|
||||
`trace_id = txHash[0:16]`. All nodes handling the same transaction independently
|
||||
produce spans under the same trace_id. Protobuf `span_id` propagation (Task 3.7)
|
||||
additionally provides parent-child relay ordering when available. See
|
||||
[02-design-decisions.md §2.5.0](./02-design-decisions.md) for the design rationale
|
||||
and [Phase3_taskList.md Task 3.9](./Phase3_taskList.md) for the full implementation spec.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- [ ] Transaction traces span across nodes
|
||||
- [ ] Trace context in Protocol Buffer messages
|
||||
- [ ] HashRouter deduplication visible in traces
|
||||
- [ ] Multi-node integration tests passing
|
||||
- [ ] <5% overhead on transaction throughput
|
||||
- [ ] Deterministic trace_id: all nodes produce same trace_id for same transaction
|
||||
- [ ] Protobuf span_id propagation preserves parent-child ordering when available
|
||||
|
||||
---
|
||||
|
||||
## 6.5 Phase 4: Consensus Tracing (Weeks 7-8)
|
||||
|
||||
**Objective**: Full observability into consensus rounds
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | ---------------------------------------------- |
|
||||
| 4.1 | Instrument `RCLConsensusAdaptor::startRound()` |
|
||||
| 4.2 | Instrument phase transitions |
|
||||
| 4.3 | Instrument proposal handling |
|
||||
| 4.4 | Instrument validation handling |
|
||||
| 4.5 | Add consensus-specific attributes |
|
||||
| 4.6 | Correlate with transaction traces |
|
||||
| 4.7 | Validator list and manifest tracing |
|
||||
| 4.8 | Amendment voting tracing |
|
||||
| 4.9 | SHAMap sync tracing |
|
||||
| 4.10 | Multi-validator integration tests |
|
||||
| 4.11 | Performance validation |
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- [ ] Complete consensus round traces
|
||||
- [ ] Phase transitions visible
|
||||
- [ ] Proposals and validations traced
|
||||
- [ ] No impact on consensus timing
|
||||
- [ ] Multi-validator test network validated
|
||||
|
||||
### Implementation Status — Phase 4a Plan
|
||||
|
||||
Phase 4a (establish-phase gap fill & cross-node correlation) will add:
|
||||
|
||||
- **Deterministic trace ID** derived from `previousLedger.id()` so all validators
|
||||
in the same round share the same `trace_id` (switchable via
|
||||
`consensus_trace_strategy` config: `"deterministic"` or `"attribute"`).
|
||||
See [Configuration Reference](./05-configuration-reference.md) for full
|
||||
configuration options.
|
||||
- **Round lifecycle spans**: `consensus.round` with round-to-round span links.
|
||||
- **Establish phase**: `consensus.establish`, `consensus.update_positions` (with
|
||||
`dispute.resolve` events), `consensus.check` (with threshold tracking).
|
||||
- **Mode changes**: `consensus.mode_change` spans.
|
||||
- **Validation**: `consensus.validation.send` with span link to round span
|
||||
(thread-safe cross-thread access via `roundSpanContext_` snapshot).
|
||||
- **Separation of concerns**: telemetry extracted to private helpers
|
||||
(`startRoundTracing`, `createValidationSpan`, `startEstablishTracing`,
|
||||
`updateEstablishTracing`, `endEstablishTracing`).
|
||||
|
||||
The `Phase4_taskList.md` spec document is introduced in the Phase 2 PR (#6424)
|
||||
and will contain the full task breakdown and implementation notes.
|
||||
|
||||
---
|
||||
|
||||
## 6.6 Phase 5: Documentation & Deployment (Week 9)
|
||||
|
||||
**Objective**: Production readiness
|
||||
|
||||
### Tasks
|
||||
|
||||
| Task | Description |
|
||||
| ---- | ----------------------------- |
|
||||
| 5.1 | Operator runbook |
|
||||
| 5.2 | Grafana dashboards |
|
||||
| 5.3 | Alert definitions |
|
||||
| 5.4 | Collector deployment examples |
|
||||
| 5.5 | Developer documentation |
|
||||
| 5.6 | Training materials |
|
||||
| 5.7 | Final integration testing |
|
||||
|
||||
---
|
||||
|
||||
## 6.7 Risk Assessment
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Risk Assessment Matrix
|
||||
x-axis Low Impact --> High Impact
|
||||
y-axis Low Likelihood --> High Likelihood
|
||||
quadrant-1 Mitigate Immediately
|
||||
quadrant-2 Plan Mitigation
|
||||
quadrant-3 Accept Risk
|
||||
quadrant-4 Monitor Closely
|
||||
|
||||
SDK Compat: [0.2, 0.18]
|
||||
Protocol Chg: [0.75, 0.72]
|
||||
Perf Overhead: [0.58, 0.42]
|
||||
Context Prop: [0.4, 0.55]
|
||||
Memory Leaks: [0.85, 0.25]
|
||||
```
|
||||
|
||||
### Risk Details
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
| ------------------------------------ | ---------- | ------ | --------------------------------------- |
|
||||
| Protocol changes break compatibility | Medium | High | Use high field numbers, optional fields |
|
||||
| Performance overhead unacceptable | Medium | Medium | Sampling, conditional compilation |
|
||||
| Context propagation complexity | Medium | Medium | Phased rollout, extensive testing |
|
||||
| SDK compatibility issues | Low | Medium | Pin SDK version, fallback to no-op |
|
||||
| Memory leaks in long-running nodes | Low | High | Memory profiling, bounded queues |
|
||||
|
||||
---
|
||||
|
||||
## 6.8 Success Metrics
|
||||
|
||||
| Metric | Target | Measurement |
|
||||
| ------------------------ | -------------------------------------------------------------- | --------------------- |
|
||||
| Trace coverage | >95% of transaction code paths (independent of sampling ratio) | Sampling verification |
|
||||
| CPU overhead | <3% | Benchmark tests |
|
||||
| Memory overhead | <10 MB | Memory profiling |
|
||||
| Latency impact (p99) | <2% | Performance tests |
|
||||
| Trace completeness | >99% spans with required attrs | Validation script |
|
||||
| Cross-node trace linkage | >90% of multi-hop transactions | Integration tests |
|
||||
|
||||
---
|
||||
|
||||
## 6.9 Quick Wins and Crawl-Walk-Run Strategy
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
This section outlines a prioritized approach to maximize ROI with minimal initial investment.
|
||||
|
||||
### 6.9.1 Crawl-Walk-Run Overview
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph crawl["🐢 CRAWL (Week 1-2)"]
|
||||
direction LR
|
||||
c1[Core SDK Setup] ~~~ c2[RPC Tracing Only] ~~~ c3[PathFinding + TxQ Tracing] ~~~ c4[Single Node]
|
||||
end
|
||||
|
||||
subgraph walk["🚶 WALK (Week 3-5)"]
|
||||
direction LR
|
||||
w1[Transaction Tracing] ~~~ w2[Fee Escalation Tracing] ~~~ w3[Cross-Node Context] ~~~ w4[Basic Dashboards]
|
||||
end
|
||||
|
||||
subgraph run["🏃 RUN (Week 6-9)"]
|
||||
direction LR
|
||||
r1[Consensus Tracing] ~~~ r2[Validator, Amendment,<br/>SHAMap Tracing] ~~~ r3[Full Correlation] ~~~ r4[Production Deploy]
|
||||
end
|
||||
|
||||
crawl --> walk --> run
|
||||
|
||||
style crawl fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style walk fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style run fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style c1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style c2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style c3 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style c4 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style w1 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style w2 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style w3 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style w4 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style r1 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style r2 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style r3 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style r4 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **CRAWL (Weeks 1-2)**: Minimal investment -- set up the SDK, instrument RPC and PathFinding/TxQ handlers, and verify on a single node. Delivers immediate latency visibility.
|
||||
- **WALK (Weeks 3-5)**: Expand to transaction lifecycle tracing, fee escalation, cross-node context propagation, and basic Grafana dashboards. This is where distributed tracing starts working.
|
||||
- **RUN (Weeks 6-9)**: Full consensus instrumentation, validator/amendment/SHAMap tracing, end-to-end correlation, and production deployment with sampling and alerting.
|
||||
- **Arrows (crawl → walk → run)**: Each phase builds on the prior one; you cannot skip ahead because later phases depend on infrastructure established earlier.
|
||||
|
||||
### 6.9.2 Quick Wins (Immediate Value)
|
||||
|
||||
| Quick Win | Value | When to Deploy |
|
||||
| ------------------------------ | ------ | -------------- |
|
||||
| **RPC Command Tracing** | High | Week 2 |
|
||||
| **RPC Latency Histograms** | High | Week 2 |
|
||||
| **Error Rate Dashboard** | Medium | Week 2 |
|
||||
| **Transaction Submit Tracing** | High | Week 3 |
|
||||
| **Consensus Round Duration** | Medium | Week 6 |
|
||||
|
||||
### 6.9.3 CRAWL Phase (Weeks 1-2)
|
||||
|
||||
**Goal**: Get basic tracing working with minimal code changes.
|
||||
|
||||
**What You Get**:
|
||||
|
||||
- RPC request/response traces for all commands
|
||||
- Latency breakdown per RPC command
|
||||
- PathFinding and TxQ tracing (directly impacts RPC latency)
|
||||
- Error visibility with stack traces
|
||||
- Basic Grafana dashboard
|
||||
|
||||
**Code Changes**: ~15 lines in `ServerHandler.cpp`, ~40 lines in new telemetry module
|
||||
|
||||
**Why Start Here**:
|
||||
|
||||
- RPC is the lowest-risk, highest-visibility component
|
||||
- PathFinding and TxQ are RPC-adjacent and directly affect latency
|
||||
- Immediate value for debugging client issues
|
||||
- No cross-node complexity
|
||||
- Single file modification to existing code
|
||||
|
||||
### 6.9.4 WALK Phase (Weeks 3-5)
|
||||
|
||||
**Goal**: Add transaction lifecycle tracing across nodes.
|
||||
|
||||
**What You Get**:
|
||||
|
||||
- End-to-end transaction traces from submit to relay
|
||||
- Fee escalation tracing within the transaction pipeline
|
||||
- Cross-node correlation (see transaction path)
|
||||
- HashRouter deduplication visibility
|
||||
- Relay latency metrics
|
||||
|
||||
**Code Changes**: ~120 lines across 4 files, plus protobuf extension
|
||||
|
||||
**Why Do This Second**:
|
||||
|
||||
- Builds on RPC tracing (transactions submitted via RPC)
|
||||
- Fee escalation is integral to the transaction processing pipeline
|
||||
- Moderate complexity (requires context propagation)
|
||||
- High value for debugging transaction issues
|
||||
|
||||
### 6.9.5 RUN Phase (Weeks 6-9)
|
||||
|
||||
**Goal**: Full observability including consensus.
|
||||
|
||||
**What You Get**:
|
||||
|
||||
- Complete consensus round visibility
|
||||
- Phase transition timing
|
||||
- Validator proposal tracking
|
||||
- Validator list and manifest tracing
|
||||
- Amendment voting tracing
|
||||
- SHAMap sync tracing
|
||||
- Full end-to-end traces (client → RPC → TX → consensus → ledger)
|
||||
|
||||
**Code Changes**: ~100 lines across 3 consensus files, plus validator/amendment/SHAMap modules
|
||||
|
||||
**Why Do This Last**:
|
||||
|
||||
- Highest complexity (consensus is critical path)
|
||||
- Validator, amendment, and SHAMap components are lower priority
|
||||
- Requires thorough testing
|
||||
- Lower relative value (consensus issues are rarer)
|
||||
|
||||
### 6.9.6 ROI Prioritization Matrix
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Implementation ROI Matrix
|
||||
x-axis Low Effort --> High Effort
|
||||
y-axis Low Value --> High Value
|
||||
quadrant-1 Quick Wins - Do First
|
||||
quadrant-2 Major Projects - Plan Carefully
|
||||
quadrant-3 Nice to Have - Optional
|
||||
quadrant-4 Time Sinks - Avoid
|
||||
|
||||
RPC Tracing: [0.15, 0.92]
|
||||
TX Submit Trace: [0.3, 0.78]
|
||||
TX Relay Trace: [0.5, 0.88]
|
||||
Consensus Trace: [0.72, 0.72]
|
||||
Peer Msg Trace: [0.85, 0.3]
|
||||
Ledger Acquire: [0.55, 0.52]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.10 Definition of Done
|
||||
|
||||
> **TxQ** = Transaction Queue | **HA** = High Availability
|
||||
|
||||
Clear, measurable criteria for each phase.
|
||||
|
||||
### 6.10.1 Phase 1: Core Infrastructure
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| --------------- | ---------------------------------------------------------- | ---------------------------- |
|
||||
| SDK Integration | `cmake --build` succeeds with `-DXRPL_ENABLE_TELEMETRY=ON` | ✅ Compiles |
|
||||
| Runtime Toggle | `enabled=0` produces zero overhead | <0.1% CPU difference |
|
||||
| Span Creation | Unit test creates and exports span | Span appears in Tempo |
|
||||
| Configuration | All config options parsed correctly | Config validation tests pass |
|
||||
| Documentation | Developer guide exists | PR approved |
|
||||
|
||||
**Definition of Done**: All criteria met, PR merged, no regressions in CI.
|
||||
|
||||
### 6.10.2 Phase 2: RPC Tracing
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| ------------------ | ---------------------------------- | -------------------------- |
|
||||
| Coverage | All RPC commands instrumented | 100% of commands |
|
||||
| Context Extraction | traceparent header propagates | Integration test passes |
|
||||
| Attributes | Command, status, duration recorded | Validation script confirms |
|
||||
| Performance | RPC latency overhead | <1ms p99 |
|
||||
| Dashboard | Grafana dashboard deployed | Screenshot in docs |
|
||||
|
||||
**Definition of Done**: RPC traces visible in Tempo for all commands, dashboard shows latency distribution.
|
||||
|
||||
### 6.10.3 Phase 3: Transaction Tracing
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| --------------------- | ------------------------------------------------- | -------------------------------------------------------- |
|
||||
| Local Trace | Submit → validate → TxQ traced | Single-node test passes |
|
||||
| Cross-Node | Context propagates via protobuf | Multi-node test passes |
|
||||
| Deterministic TraceID | Same trace_id on all nodes for same tx | Multi-node test: query by txHash[0:16] returns all spans |
|
||||
| Relay Ordering | Protobuf span_id propagation creates parent-child | Tempo trace tree shows relay chain |
|
||||
| Graceful Degradation | Old peer drops trace_context | Spans still grouped by deterministic trace_id |
|
||||
| Relay Visibility | relay_count attribute correct | Spot check 100 txs |
|
||||
| HashRouter | Deduplication visible in trace | Duplicate txs show suppressed=true |
|
||||
| Performance | TX throughput overhead | <5% degradation |
|
||||
|
||||
**Definition of Done**: Transaction traces span 3+ nodes in test network with deterministic trace_id correlation, parent-child ordering via protobuf propagation, and performance within bounds.
|
||||
|
||||
### 6.10.4 Phase 4: Consensus Tracing
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| -------------------- | ----------------------------- | ------------------------- |
|
||||
| Round Tracing | startRound creates root span | Unit test passes |
|
||||
| Phase Visibility | All phases have child spans | Integration test confirms |
|
||||
| Proposer Attribution | Proposer ID in attributes | Spot check 50 rounds |
|
||||
| Timing Accuracy | Phase durations match PerfLog | <5% variance |
|
||||
| No Consensus Impact | Round timing unchanged | Performance test passes |
|
||||
|
||||
**Definition of Done**: Consensus rounds fully traceable, no impact on consensus timing.
|
||||
|
||||
### 6.10.5 Phase 5: Production Deployment
|
||||
|
||||
| Criterion | Measurement | Target |
|
||||
| ------------ | ---------------------------- | -------------------------- |
|
||||
| Collector HA | Multiple collectors deployed | No single point of failure |
|
||||
| Sampling | Tail sampling configured | 10% base + errors + slow |
|
||||
| Retention | Data retained per policy | 7 days hot, 30 days warm |
|
||||
| Alerting | Alerts configured | Error spike, high latency |
|
||||
| Runbook | Operator documentation | Approved by ops team |
|
||||
| Training | Team trained | Session completed |
|
||||
|
||||
**Definition of Done**: Telemetry running in production, operators trained, alerts active.
|
||||
|
||||
### 6.10.6 Success Metrics Summary
|
||||
|
||||
| Phase | Primary Metric | Secondary Metric | Deadline |
|
||||
| ------- | ---------------------- | --------------------------- | ------------- |
|
||||
| Phase 1 | SDK compiles and runs | Zero overhead when disabled | End of Week 2 |
|
||||
| Phase 2 | 100% RPC coverage | <1ms latency overhead | End of Week 4 |
|
||||
| Phase 3 | Cross-node traces work | <5% throughput impact | End of Week 6 |
|
||||
| Phase 4 | Consensus fully traced | No consensus timing impact | End of Week 8 |
|
||||
| Phase 5 | Production deployment | Operators trained | End of Week 9 |
|
||||
|
||||
---
|
||||
|
||||
## 6.11 Recommended Implementation Order
|
||||
|
||||
Based on ROI analysis, implement in this exact order:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph week1["Week 1"]
|
||||
t1[1. OpenTelemetry SDK<br/>Conan/CMake integration]
|
||||
t2[2. Telemetry interface<br/>SpanGuard, config]
|
||||
end
|
||||
|
||||
subgraph week2["Week 2"]
|
||||
t3[3. RPC ServerHandler<br/>instrumentation]
|
||||
t4[4. Basic Tempo setup<br/>for testing]
|
||||
end
|
||||
|
||||
subgraph week3["Week 3"]
|
||||
t5[5. Transaction submit<br/>tracing]
|
||||
t6[6. Grafana dashboard<br/>v1]
|
||||
end
|
||||
|
||||
subgraph week4["Week 4"]
|
||||
t7[7. Protobuf context<br/>extension]
|
||||
t8[8. PeerImp tx.relay<br/>instrumentation]
|
||||
end
|
||||
|
||||
subgraph week5["Week 5"]
|
||||
t9[9. Multi-node<br/>integration tests]
|
||||
t10[10. Performance<br/>benchmarks]
|
||||
end
|
||||
|
||||
subgraph week6_8["Weeks 6-8"]
|
||||
t11[11. Consensus<br/>instrumentation]
|
||||
t12[12. Full integration<br/>testing]
|
||||
end
|
||||
|
||||
subgraph week9["Week 9"]
|
||||
t13[13. Production<br/>deployment]
|
||||
t14[14. Documentation<br/>& training]
|
||||
end
|
||||
|
||||
t1 --> t2 --> t3 --> t4
|
||||
t4 --> t5 --> t6
|
||||
t6 --> t7 --> t8
|
||||
t8 --> t9 --> t10
|
||||
t10 --> t11 --> t12
|
||||
t12 --> t13 --> t14
|
||||
|
||||
style week1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style week2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style week3 fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style week4 fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style week5 fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style week6_8 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style week9 fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style t1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style t2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style t3 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style t4 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style t5 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t6 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t7 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t8 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t9 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t10 fill:#ffe0b2,stroke:#ffcc80,color:#1e293b
|
||||
style t11 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style t12 fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style t13 fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style t14 fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Week 1 (tasks 1-2)**: Foundation work -- integrate the OpenTelemetry SDK via Conan/CMake and build the `Telemetry` interface with `SpanGuard` and config parsing.
|
||||
- **Week 2 (tasks 3-4)**: First observable output -- instrument `ServerHandler` for RPC tracing and stand up Tempo so developers can see traces immediately.
|
||||
- **Weeks 3-5 (tasks 5-10)**: Transaction lifecycle -- add submit tracing, build the first Grafana dashboard, extend protobuf for cross-node context, instrument `PeerImp` relay, then validate with multi-node integration tests and performance benchmarks.
|
||||
- **Weeks 6-8 (tasks 11-12)**: Consensus deep-dive -- instrument consensus rounds and phases, then run full integration testing across all instrumented paths.
|
||||
- **Week 9 (tasks 13-14)**: Go-live -- deploy to production with sampling/alerting configured, and deliver documentation and operator training.
|
||||
- **Arrow chain (t1 → ... → t14)**: Strict sequential dependency; each task's output is a prerequisite for the next.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Configuration Reference](./05-configuration-reference.md)_ | _Next: [Observability Backends](./07-observability-backends.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
641
OpenTelemetryPlan/07-observability-backends.md
Normal file
641
OpenTelemetryPlan/07-observability-backends.md
Normal file
@@ -0,0 +1,641 @@
|
||||
# Observability Backend Recommendations
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Implementation Phases](./06-implementation-phases.md) | [Appendix](./08-appendix.md)
|
||||
|
||||
---
|
||||
|
||||
## 7.1 Development/Testing Backends
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
| Backend | Pros | Cons | Use Case |
|
||||
| ---------- | ----------------------------------- | ---------------------- | ------------------- |
|
||||
| **Tempo** | Cost-effective, Grafana integration | Requires Grafana stack | Local dev, CI, Prod |
|
||||
| **Zipkin** | Simple, lightweight | Basic features | Quick prototyping |
|
||||
|
||||
### Quick Start with Tempo
|
||||
|
||||
```bash
|
||||
# Start Tempo with OTLP support
|
||||
docker run -d --name tempo \
|
||||
-p 3200:3200 \
|
||||
-p 4317:4317 \
|
||||
-p 4318:4318 \
|
||||
grafana/tempo:2.6.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7.2 Production Backends
|
||||
|
||||
> **APM** = Application Performance Monitoring
|
||||
|
||||
| Backend | Pros | Cons | Use Case |
|
||||
| ----------------- | ----------------------------------------- | ---------------------- | --------------------------- |
|
||||
| **Grafana Tempo** | Cost-effective, Grafana integration | Requires Grafana stack | Most production deployments |
|
||||
| **Elastic APM** | Full observability stack, log correlation | Resource intensive | Existing Elastic users |
|
||||
| **Honeycomb** | Excellent query, high cardinality | SaaS cost | Deep debugging needs |
|
||||
| **Datadog APM** | Full platform, easy setup | SaaS cost | Enterprise with budget |
|
||||
|
||||
### Backend Selection Flowchart
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
start[Select Backend] --> budget{Budget<br/>Constraints?}
|
||||
|
||||
budget -->|Yes| oss[Open Source]
|
||||
budget -->|No| saas{Prefer<br/>SaaS?}
|
||||
|
||||
oss --> existing{Existing<br/>Stack?}
|
||||
existing -->|Grafana| tempo[Grafana Tempo]
|
||||
existing -->|Elastic| elastic[Elastic APM]
|
||||
existing -->|None| tempo
|
||||
|
||||
saas -->|Yes| enterprise{Enterprise<br/>Support?}
|
||||
saas -->|No| oss
|
||||
|
||||
enterprise -->|Yes| datadog[Datadog APM]
|
||||
enterprise -->|No| honeycomb[Honeycomb]
|
||||
|
||||
tempo --> final[Configure Collector]
|
||||
elastic --> final
|
||||
honeycomb --> final
|
||||
datadog --> final
|
||||
|
||||
style start fill:#0f172a,stroke:#020617,color:#fff
|
||||
style budget fill:#334155,stroke:#1e293b,color:#fff
|
||||
style oss fill:#1e293b,stroke:#0f172a,color:#fff
|
||||
style existing fill:#334155,stroke:#1e293b,color:#fff
|
||||
style saas fill:#334155,stroke:#1e293b,color:#fff
|
||||
style enterprise fill:#334155,stroke:#1e293b,color:#fff
|
||||
style final fill:#0f172a,stroke:#020617,color:#fff
|
||||
style tempo fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style elastic fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style honeycomb fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style datadog fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Budget Constraints? (Yes)**: Leads to open-source options. If you already run Grafana or Elastic, pick the matching backend; otherwise default to Grafana Tempo.
|
||||
- **Budget Constraints? (No) → Prefer SaaS?**: If you want a managed service, choose between Datadog (enterprise support) and Honeycomb (developer-focused). If not, fall back to open-source.
|
||||
- **Terminal nodes (Tempo / Elastic / Honeycomb / Datadog)**: Each represents a concrete backend choice, all of which feed into the same final step.
|
||||
- **Configure Collector**: Regardless of backend, you always finish by configuring the OTel Collector to export to your chosen destination.
|
||||
|
||||
---
|
||||
|
||||
## 7.3 Recommended Production Architecture
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring | **HA** = High Availability
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph validators["Validator Nodes"]
|
||||
v1[xrpld<br/>Validator 1]
|
||||
v2[xrpld<br/>Validator 2]
|
||||
end
|
||||
|
||||
subgraph stock["Stock Nodes"]
|
||||
s1[xrpld<br/>Stock 1]
|
||||
s2[xrpld<br/>Stock 2]
|
||||
end
|
||||
|
||||
subgraph collector["OTel Collector Cluster"]
|
||||
c1[Collector<br/>DC1]
|
||||
c2[Collector<br/>DC2]
|
||||
end
|
||||
|
||||
subgraph backends["Storage Backends"]
|
||||
tempo[(Grafana<br/>Tempo)]
|
||||
elastic[(Elastic<br/>APM)]
|
||||
archive[(S3/GCS<br/>Archive)]
|
||||
end
|
||||
|
||||
subgraph ui["Visualization"]
|
||||
grafana[Grafana<br/>Dashboards]
|
||||
end
|
||||
|
||||
v1 -->|OTLP| c1
|
||||
v2 -->|OTLP| c1
|
||||
s1 -->|OTLP| c2
|
||||
s2 -->|OTLP| c2
|
||||
|
||||
c1 --> tempo
|
||||
c1 --> elastic
|
||||
c2 --> tempo
|
||||
c2 --> archive
|
||||
|
||||
tempo --> grafana
|
||||
elastic --> grafana
|
||||
|
||||
%% Note: simplified single-collector-per-DC topology shown for clarity
|
||||
|
||||
style validators fill:#b71c1c,stroke:#7f1d1d,color:#ffffff
|
||||
style stock fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style collector fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style backends fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style ui fill:#4a148c,stroke:#2e0d57,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Validator / Stock Nodes**: All xrpld nodes emit trace data via OTLP. Validators and stock nodes are grouped separately because they may reside in different network zones.
|
||||
- **Collector Cluster (DC1, DC2)**: Regional collectors receive OTLP from nodes in their datacenter, apply processing (sampling, enrichment), and fan out to multiple backends.
|
||||
- **Storage Backends**: Tempo and Elastic provide queryable trace storage; S3/GCS Archive provides long-term cold storage for compliance or post-incident analysis.
|
||||
- **Grafana Dashboards**: The single visualization layer that queries both Tempo and Elastic, giving operators a unified view of all traces.
|
||||
- **Data flow direction**: Nodes → Collectors → Storage → Grafana. Each arrow represents a network hop; minimizing collector-to-backend hops reduces latency.
|
||||
|
||||
> **Note**: Production deployments should use multiple collector instances behind a load balancer for high availability. The diagram shows a simplified single-collector topology for clarity.
|
||||
|
||||
---
|
||||
|
||||
## 7.4 Architecture Considerations
|
||||
|
||||
### 7.4.1 Collector Placement
|
||||
|
||||
| Strategy | Description | Pros | Cons |
|
||||
| ------------- | -------------------- | ------------------------ | ----------------------- |
|
||||
| **Sidecar** | Collector per node | Isolation, simple config | Resource overhead |
|
||||
| **DaemonSet** | Collector per host | Shared resources | Complexity |
|
||||
| **Gateway** | Central collector(s) | Centralized processing | Single point of failure |
|
||||
|
||||
**Recommendation**: Use **Gateway** pattern with regional collectors for xrpld networks:
|
||||
|
||||
- One collector cluster per datacenter/region
|
||||
- Tail-based sampling at collector level
|
||||
- Multiple export destinations for redundancy
|
||||
|
||||
### 7.4.2 Sampling Strategy
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph head["Head Sampling (Node)"]
|
||||
hs[Node-level head sampling<br/>fixed at 100%<br/>not configurable]
|
||||
end
|
||||
|
||||
subgraph tail["Tail Sampling (Collector)"]
|
||||
ts1[Keep all errors]
|
||||
ts2[Keep slow >5s]
|
||||
ts3[Keep 10% rest]
|
||||
end
|
||||
|
||||
head --> tail
|
||||
|
||||
ts1 --> final[Final Traces]
|
||||
ts2 --> final
|
||||
ts3 --> final
|
||||
|
||||
style head fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style tail fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style hs fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style ts1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style ts2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style ts3 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style final fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Head Sampling (Node)**: xrpld pins head sampling at 100% (sample everything) and does not expose a configurable ratio. This is intentional: a per-node ratio would let different nodes make divergent keep/drop decisions for the same distributed trace, producing broken/partial traces. xrpld uses a `ParentBased` sampler so spans inheriting a remote parent honor the upstream decision. Volume reduction is delegated to the collector's tail sampling.
|
||||
- **Tail Sampling (Collector)**: The second filter -- the collector inspects completed traces and applies rules: keep all errors, keep anything slower than 5 seconds, and keep 10% of the remainder.
|
||||
- **Arrow head → tail**: All head-sampled traces flow to the collector, where tail sampling further reduces volume while preserving the most valuable data.
|
||||
- **Final Traces**: The output after both sampling stages; this is what gets stored and queried. The two-stage approach balances cost with debuggability.
|
||||
|
||||
### 7.4.3 Data Retention
|
||||
|
||||
| Environment | Hot Storage | Warm Storage | Cold Archive |
|
||||
| ----------- | ----------- | ------------ | ------------ |
|
||||
| Development | 24 hours | N/A | N/A |
|
||||
| Staging | 7 days | N/A | N/A |
|
||||
| Production | 7 days | 30 days | many years |
|
||||
|
||||
---
|
||||
|
||||
## 7.5 Integration Checklist
|
||||
|
||||
- [ ] Choose primary backend (Tempo recommended for cost/features)
|
||||
- [ ] Deploy collector cluster with high availability
|
||||
- [ ] Configure tail-based sampling for error/latency traces
|
||||
- [ ] Set up Grafana dashboards for trace visualization
|
||||
- [ ] Configure alerts for trace anomalies
|
||||
- [ ] Establish data retention policies
|
||||
- [ ] Test trace correlation with logs and metrics
|
||||
|
||||
---
|
||||
|
||||
## 7.6 Grafana Dashboard Examples
|
||||
|
||||
Pre-built dashboards for xrpld observability.
|
||||
|
||||
### 7.6.1 Consensus Health Dashboard
|
||||
|
||||
```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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.6.3 Alert Rules
|
||||
|
||||
```yaml
|
||||
# grafana/provisioning/alerting/rippled-alerts.yaml
|
||||
apiVersion: 1
|
||||
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7.7 PerfLog and Insight Correlation
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
How to correlate OpenTelemetry traces with existing xrpld observability.
|
||||
|
||||
### 7.7.1 Correlation Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Node"]
|
||||
otel[OpenTelemetry<br/>Spans]
|
||||
perflog[PerfLog<br/>JSON Logs]
|
||||
insight[Beast Insight<br/>StatsD Metrics]
|
||||
end
|
||||
|
||||
subgraph collectors["Data Collection"]
|
||||
otelc[OTel Collector]
|
||||
promtail[Promtail/Fluentd]
|
||||
statsd[StatsD Exporter]
|
||||
end
|
||||
|
||||
subgraph storage["Storage"]
|
||||
tempo[(Tempo)]
|
||||
loki[(Loki)]
|
||||
prom[(Prometheus)]
|
||||
end
|
||||
|
||||
subgraph grafana["Grafana"]
|
||||
traces[Trace View]
|
||||
logs[Log View]
|
||||
metrics[Metrics View]
|
||||
corr[Correlation<br/>Panel]
|
||||
end
|
||||
|
||||
otel -->|OTLP| otelc --> tempo
|
||||
perflog -->|JSON| promtail --> loki
|
||||
insight -->|StatsD| statsd --> prom
|
||||
|
||||
tempo --> traces
|
||||
loki --> logs
|
||||
prom --> metrics
|
||||
|
||||
traces --> corr
|
||||
logs --> corr
|
||||
metrics --> corr
|
||||
|
||||
style xrpld fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style collectors fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style storage fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style grafana fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style otel fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style perflog fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style insight fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style otelc fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style promtail fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style statsd fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style tempo fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style loki fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style prom fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style traces fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style logs fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style metrics fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style corr fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Node (three sources)**: A single node emits three independent data streams -- OpenTelemetry spans, PerfLog JSON logs, and Beast Insight StatsD metrics.
|
||||
- **Data Collection layer**: Each stream has its own collector -- OTel Collector for spans, Promtail/Fluentd for logs, and a StatsD exporter for metrics. They operate independently.
|
||||
- **Storage layer (Tempo, Loki, Prometheus)**: Each data type lands in a purpose-built store optimized for its query patterns (trace search, log grep, metric aggregation).
|
||||
- **Grafana Correlation Panel**: The key integration point -- Grafana queries all three stores and links them via shared fields (`trace_id`, `xrpl.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 |
|
||||
|
||||
### 7.7.3 Example: Debugging a Slow Transaction
|
||||
|
||||
**Step 1: Find the trace**
|
||||
|
||||
```
|
||||
# In Grafana Explore with Tempo
|
||||
{resource.service.name="xrpld" && span.xrpl.tx.hash="ABC123..."}
|
||||
```
|
||||
|
||||
**Step 2: Get the trace_id from the trace view**
|
||||
|
||||
```
|
||||
Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736
|
||||
```
|
||||
|
||||
**Step 3: Find related PerfLog entries**
|
||||
|
||||
```
|
||||
# In Grafana Explore with Loki
|
||||
{job="xrpld"} |= "4bf92f3577b34da6a3ce929d0e0e4736"
|
||||
```
|
||||
|
||||
**Step 4: Check Insight metrics for the time window**
|
||||
|
||||
```
|
||||
# In Grafana with Prometheus
|
||||
rate(xrpld_tx_applied_total[1m])
|
||||
@ timestamp_from_trace
|
||||
```
|
||||
|
||||
### 7.7.4 Unified Dashboard Example
|
||||
|
||||
```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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Implementation Phases](./06-implementation-phases.md)_ | _Next: [Appendix](./08-appendix.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
201
OpenTelemetryPlan/08-appendix.md
Normal file
201
OpenTelemetryPlan/08-appendix.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Appendix
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Observability Backends](./07-observability-backends.md)
|
||||
|
||||
---
|
||||
|
||||
## 8.1 Glossary
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **TxQ** = Transaction Queue
|
||||
|
||||
| Term | Definition |
|
||||
| --------------------- | ---------------------------------------------------------- |
|
||||
| **Span** | A unit of work with start/end time, name, and attributes |
|
||||
| **Trace** | A collection of spans representing a complete request flow |
|
||||
| **Trace ID** | 128-bit unique identifier for a trace |
|
||||
| **Span ID** | 64-bit unique identifier for a span within a trace |
|
||||
| **Context** | Carrier for trace/span IDs across boundaries |
|
||||
| **Propagator** | Component that injects/extracts context |
|
||||
| **Sampler** | Decides which traces to record |
|
||||
| **Exporter** | Sends spans to backend |
|
||||
| **Collector** | Receives, processes, and forwards telemetry |
|
||||
| **OTLP** | OpenTelemetry Protocol (wire format) |
|
||||
| **W3C Trace Context** | Standard HTTP headers for trace propagation |
|
||||
| **Baggage** | Key-value pairs propagated across service boundaries |
|
||||
| **Resource** | Entity producing telemetry (service, host, etc.) |
|
||||
| **Instrumentation** | Code that creates telemetry data |
|
||||
|
||||
### xrpld-Specific Terms
|
||||
|
||||
| Term | Definition |
|
||||
| ----------------- | ------------------------------------------------------------- |
|
||||
| **Overlay** | P2P network layer managing peer connections |
|
||||
| **Consensus** | XRP Ledger consensus algorithm (RCL) |
|
||||
| **Proposal** | Validator's suggested transaction set for a ledger |
|
||||
| **Validation** | Validator's signature on a closed ledger |
|
||||
| **HashRouter** | Component for transaction deduplication |
|
||||
| **JobQueue** | Thread pool for asynchronous task execution |
|
||||
| **PerfLog** | Existing performance logging system in xrpld |
|
||||
| **Beast Insight** | Existing metrics framework in xrpld |
|
||||
| **PathFinding** | Payment path computation engine for cross-currency payments |
|
||||
| **TxQ** | Transaction queue managing fee-based prioritization |
|
||||
| **LoadManager** | Dynamic fee escalation based on network load |
|
||||
| **SHAMap** | SHA-256 hash-based map (Merkle trie variant) for ledger state |
|
||||
|
||||
---
|
||||
|
||||
## 8.2 Span Hierarchy Visualization
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph trace["Trace: Transaction Lifecycle"]
|
||||
rpc["rpc.request<br/>(entry point)"]
|
||||
validate["tx.validate"]
|
||||
relay["tx.relay<br/>(parent span)"]
|
||||
|
||||
subgraph peers["Peer Spans"]
|
||||
p1["peer.send<br/>Peer A"]
|
||||
p2["peer.send<br/>Peer B"]
|
||||
p3["peer.send<br/>Peer C"]
|
||||
end
|
||||
|
||||
subgraph pathfinding["PathFinding Spans"]
|
||||
pathfind["pathfind.request"]
|
||||
pathcomp["pathfind.compute"]
|
||||
end
|
||||
|
||||
consensus["consensus.round"]
|
||||
apply["tx.apply"]
|
||||
|
||||
subgraph txqueue["TxQ Spans"]
|
||||
txq["txq.enqueue"]
|
||||
txqApply["txq.apply"]
|
||||
end
|
||||
|
||||
feeCalc["fee.escalate"]
|
||||
end
|
||||
|
||||
subgraph validators["Validator Spans"]
|
||||
valFetch["validator.list.fetch"]
|
||||
valManifest["validator.manifest"]
|
||||
end
|
||||
|
||||
rpc --> validate
|
||||
rpc --> pathfind
|
||||
pathfind --> pathcomp
|
||||
validate --> relay
|
||||
relay --> p1
|
||||
relay --> p2
|
||||
relay --> p3
|
||||
p1 -.->|"context propagation"| consensus
|
||||
consensus --> apply
|
||||
apply --> txq
|
||||
txq --> txqApply
|
||||
txq --> feeCalc
|
||||
|
||||
style trace fill:#0f172a,stroke:#020617,color:#fff
|
||||
style peers fill:#1e3a8a,stroke:#172554,color:#fff
|
||||
style pathfinding fill:#134e4a,stroke:#0f766e,color:#fff
|
||||
style txqueue fill:#064e3b,stroke:#047857,color:#fff
|
||||
style validators fill:#4c1d95,stroke:#6d28d9,color:#fff
|
||||
style rpc fill:#1d4ed8,stroke:#1e40af,color:#fff
|
||||
style validate fill:#047857,stroke:#064e3b,color:#fff
|
||||
style relay fill:#047857,stroke:#064e3b,color:#fff
|
||||
style p1 fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style p2 fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style p3 fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style consensus fill:#fef3c7,stroke:#fde68a,color:#1e293b
|
||||
style apply fill:#047857,stroke:#064e3b,color:#fff
|
||||
style pathfind fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style pathcomp fill:#0e7490,stroke:#155e75,color:#fff
|
||||
style txq fill:#047857,stroke:#064e3b,color:#fff
|
||||
style txqApply fill:#047857,stroke:#064e3b,color:#fff
|
||||
style feeCalc fill:#047857,stroke:#064e3b,color:#fff
|
||||
style valFetch fill:#6d28d9,stroke:#4c1d95,color:#fff
|
||||
style valManifest fill:#6d28d9,stroke:#4c1d95,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **rpc.request (blue, top)**: The entry point — every traced transaction starts as an RPC call; this root span is the parent of all downstream work.
|
||||
- **tx.validate and pathfind.request (green/teal, first fork)**: The RPC request fans out into transaction validation and, for cross-currency payments, a PathFinding branch (`pathfind.request` -> `pathfind.compute`).
|
||||
- **tx.relay -> Peer Spans (teal, middle)**: After validation, the transaction is relayed to peers A, B, and C in parallel; each `peer.send` is a sibling child span showing fan-out across the network.
|
||||
- **context propagation (dashed arrow)**: The dotted line from `peer.send Peer A` to `consensus.round` represents the trace context crossing a node boundary — the receiving validator picks up the same `trace_id` and continues the trace.
|
||||
- **consensus.round -> tx.apply -> TxQ Spans (green, lower)**: Once consensus accepts the transaction, it is applied to the ledger; the TxQ spans (`txq.enqueue`, `txq.apply`, `fee.escalate`) capture queue depth and fee escalation behavior.
|
||||
- **Validator Spans (purple, detached)**: `validator.list.fetch` and `validator.manifest` are independent workflows for UNL management — they run on their own traces and are linked to consensus via Span Links, not parent-child relationships.
|
||||
|
||||
---
|
||||
|
||||
## 8.3 References
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### OpenTelemetry Resources
|
||||
|
||||
1. [OpenTelemetry C++ SDK](https://github.com/open-telemetry/opentelemetry-cpp)
|
||||
2. [OpenTelemetry Specification](https://opentelemetry.io/docs/specs/otel/)
|
||||
3. [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/)
|
||||
4. [OTLP Protocol Specification](https://opentelemetry.io/docs/specs/otlp/)
|
||||
|
||||
### Standards
|
||||
|
||||
5. [W3C Trace Context](https://www.w3.org/TR/trace-context/)
|
||||
6. [W3C Baggage](https://www.w3.org/TR/baggage/)
|
||||
7. [Protocol Buffers](https://protobuf.dev/)
|
||||
|
||||
### xrpld Resources
|
||||
|
||||
8. [xrpld Source Code](https://github.com/XRPLF/rippled)
|
||||
9. [XRP Ledger Documentation](https://xrpl.org/docs/)
|
||||
10. [xrpld Overlay README](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/README.md)
|
||||
11. [xrpld RPC README](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/rpc/README.md)
|
||||
12. [xrpld Consensus README](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/app/consensus/README.md)
|
||||
|
||||
---
|
||||
|
||||
## 8.4 Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
| ------- | ---------- | ------ | -------------------------------------------------------------- |
|
||||
| 1.0 | 2026-02-12 | - | Initial implementation plan |
|
||||
| 1.1 | 2026-02-13 | - | Refactored into modular documents |
|
||||
| 1.2 | 2026-03-24 | - | Review fixes: accuracy corrections, cross-document consistency |
|
||||
|
||||
---
|
||||
|
||||
## 8.5 Document Index
|
||||
|
||||
### Plan Documents
|
||||
|
||||
| Document | Description |
|
||||
| ---------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) | Master overview and executive summary |
|
||||
| [00-tracing-fundamentals.md](./00-tracing-fundamentals.md) | Distributed tracing concepts and OTel primer |
|
||||
| [01-architecture-analysis.md](./01-architecture-analysis.md) | xrpld architecture and trace points |
|
||||
| [02-design-decisions.md](./02-design-decisions.md) | SDK selection, exporters, span conventions |
|
||||
| [03-implementation-strategy.md](./03-implementation-strategy.md) | Directory structure, performance analysis |
|
||||
| [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) |
|
||||
| [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 |
|
||||
| [Phase5_taskList.md](./Phase5_taskList.md) | Ledger processing & advanced tracing |
|
||||
| [presentation.md](./presentation.md) | Presentation slides for OpenTelemetry plan overview |
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Observability Backends](./07-observability-backends.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
243
OpenTelemetryPlan/OpenTelemetryPlan.md
Normal file
243
OpenTelemetryPlan/OpenTelemetryPlan.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# [OpenTelemetry](00-tracing-fundamentals.md) Distributed Tracing Implementation Plan for xrpld (xrpld)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
This document provides a comprehensive implementation plan for integrating OpenTelemetry distributed tracing into the xrpld XRP Ledger node software. The plan addresses the unique challenges of a decentralized peer-to-peer system where trace context must propagate across network boundaries between independent nodes.
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- **End-to-end transaction visibility**: Track transactions from submission through consensus to ledger inclusion
|
||||
- **Consensus round analysis**: Understand timing and behavior of consensus phases across validators
|
||||
- **RPC performance insights**: Identify slow handlers and optimize response times
|
||||
- **Network topology understanding**: Visualize message propagation patterns between peers
|
||||
- **Incident debugging**: Correlate events across distributed nodes during issues
|
||||
|
||||
### Estimated Performance Overhead
|
||||
|
||||
| Metric | Overhead | Notes |
|
||||
| ------------- | ---------- | ----------------------------------- |
|
||||
| CPU | 1-3% | Span creation and attribute setting |
|
||||
| Memory | 2-5 MB | Batch buffer for pending spans |
|
||||
| Network | 10-50 KB/s | Compressed OTLP export to collector |
|
||||
| Latency (p99) | <2% | With proper sampling configuration |
|
||||
|
||||
---
|
||||
|
||||
## Document Structure
|
||||
|
||||
This implementation plan is organized into modular documents for easier navigation:
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
overview["📋 OpenTelemetryPlan.md<br/>(This Document)"]
|
||||
|
||||
subgraph fundamentals["Fundamentals"]
|
||||
fund["00-tracing-fundamentals.md"]
|
||||
end
|
||||
|
||||
subgraph analysis["Analysis & Design"]
|
||||
arch["01-architecture-analysis.md"]
|
||||
design["02-design-decisions.md"]
|
||||
end
|
||||
|
||||
subgraph impl["Implementation"]
|
||||
strategy["03-implementation-strategy.md"]
|
||||
code["04-code-samples.md"]
|
||||
config["05-configuration-reference.md"]
|
||||
end
|
||||
|
||||
subgraph deploy["Deployment & Planning"]
|
||||
phases["06-implementation-phases.md"]
|
||||
backends["07-observability-backends.md"]
|
||||
appendix["08-appendix.md"]
|
||||
secure["secure-OTel.md"]
|
||||
poc["POC_taskList.md"]
|
||||
end
|
||||
|
||||
overview --> fundamentals
|
||||
overview --> analysis
|
||||
overview --> impl
|
||||
overview --> deploy
|
||||
|
||||
fund --> arch
|
||||
arch --> design
|
||||
design --> strategy
|
||||
strategy --> code
|
||||
code --> config
|
||||
config --> phases
|
||||
phases --> backends
|
||||
backends --> appendix
|
||||
backends --> secure
|
||||
phases --> poc
|
||||
|
||||
style overview fill:#1b5e20,stroke:#0d3d14,color:#fff,stroke-width:2px
|
||||
style fundamentals fill:#00695c,stroke:#004d40,color:#fff
|
||||
style fund fill:#00695c,stroke:#004d40,color:#fff
|
||||
style analysis fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style impl fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style deploy fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style arch fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style design fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style strategy fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style 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
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
| Section | Document | Description |
|
||||
| ------- | ---------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| **0** | [Tracing Fundamentals](./00-tracing-fundamentals.md) | Distributed tracing concepts, span relationships, context propagation |
|
||||
| **1** | [Architecture Analysis](./01-architecture-analysis.md) | xrpld component analysis, trace points, instrumentation priorities |
|
||||
| **2** | [Design Decisions](./02-design-decisions.md) | SDK selection, exporters, span naming, attributes, context propagation |
|
||||
| **3** | [Implementation Strategy](./03-implementation-strategy.md) | Directory structure, key principles, performance optimization |
|
||||
| **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 |
|
||||
| **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 |
|
||||
|
||||
---
|
||||
|
||||
## 0. Tracing Fundamentals
|
||||
|
||||
This document introduces distributed tracing concepts for readers unfamiliar with the domain. It covers what traces and spans are, how parent-child and follows-from relationships model causality, how context propagates across service boundaries, and how sampling controls data volume. It also maps these concepts to xrpld-specific scenarios like transaction relay and consensus.
|
||||
|
||||
➡️ **[Read Tracing Fundamentals](./00-tracing-fundamentals.md)**
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Analysis
|
||||
|
||||
> **WS** = WebSocket | **TxQ** = Transaction Queue
|
||||
|
||||
The xrpld node consists of several key components that require instrumentation for comprehensive distributed tracing. The main areas include the RPC server (HTTP/WebSocket), Overlay P2P network, Consensus mechanism (RCLConsensus), JobQueue for async task execution, PathFinding, Transaction Queue (TxQ), fee escalation (LoadManager), ledger acquisition, validator management, and existing observability infrastructure (PerfLog, Insight/StatsD, Journal logging).
|
||||
|
||||
Key trace points span across transaction submission via RPC, peer-to-peer message propagation, consensus round execution, ledger building, path computation, transaction queue behavior, fee escalation, and validator health. The implementation prioritizes high-value, low-risk components first: RPC handlers provide immediate value with minimal risk, while consensus tracing requires careful implementation to avoid timing impacts.
|
||||
|
||||
➡️ **[Read full Architecture Analysis](./01-architecture-analysis.md)**
|
||||
|
||||
---
|
||||
|
||||
## 2. Design Decisions
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **CNCF** = Cloud Native Computing Foundation
|
||||
|
||||
The OpenTelemetry C++ SDK is selected for its CNCF backing, active development, and native performance characteristics. Traces are exported via OTLP/gRPC (primary) or OTLP/HTTP (fallback) to an OpenTelemetry Collector, which provides flexible routing and sampling.
|
||||
|
||||
Span naming follows a hierarchical `<component>.<operation>` convention (e.g., `rpc.submit`, `tx.relay`, `consensus.round`). Context propagation uses W3C Trace Context headers for HTTP and embedded Protocol Buffer fields for P2P messages. The implementation coexists with existing PerfLog and Insight observability systems through correlation IDs.
|
||||
|
||||
**Data Collection & Privacy**: Telemetry collects only operational metadata (timing, counts, hashes) — never sensitive content (private keys, balances, amounts, raw payloads). Privacy protection includes account hashing, configurable redaction, sampling, and collector-level filtering. Node operators retain full control over telemetry configuration.
|
||||
|
||||
➡️ **[Read full Design Decisions](./02-design-decisions.md)**
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Strategy
|
||||
|
||||
The telemetry code is organized under `include/xrpl/telemetry/` for headers and `src/libxrpl/telemetry/` for implementation. Key principles include RAII-based span management via `SpanGuard` (with `discard()` for dropping unwanted spans), a `FilteringSpanProcessor` that intercepts `OnEnd()` to prevent discarded spans from entering the export pipeline, conditional compilation with `XRPL_ENABLE_TELEMETRY`, and minimal runtime overhead through batch processing and efficient sampling.
|
||||
|
||||
Performance optimization strategies include head sampling fixed at 100% (intentionally not configurable, so trace keep/drop decisions stay coherent across nodes), tail-based sampling at the collector for errors and slow traces to reduce volume, batch export to reduce network overhead, and conditional instrumentation that compiles to no-ops when disabled.
|
||||
|
||||
➡️ **[Read full Implementation Strategy](./03-implementation-strategy.md)**
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
Configuration is handled through the `[telemetry]` section in `xrpld.cfg` with options for enabling/disabling, exporter selection, endpoint configuration, sampling ratios, and component-level filtering. CMake integration includes a `XRPL_ENABLE_TELEMETRY` option for compile-time control.
|
||||
|
||||
OpenTelemetry Collector configurations are provided for development and production (with tail-based sampling, Tempo, and Elastic APM). Docker Compose examples enable quick local development environment setup.
|
||||
|
||||
➡️ **[View full Configuration Reference](./05-configuration-reference.md)**
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Phases
|
||||
|
||||
The implementation spans 9 weeks across 5 phases:
|
||||
|
||||
| Phase | Duration | Focus | Key Deliverables |
|
||||
| ----- | --------- | ------------------- | --------------------------------------------------- |
|
||||
| 1 | Weeks 1-2 | Core Infrastructure | SDK integration, Telemetry interface, Configuration |
|
||||
| 2 | Weeks 3-4 | RPC Tracing | HTTP context extraction, Handler instrumentation |
|
||||
| 3 | Weeks 5-6 | Transaction Tracing | Protocol Buffer context, Relay propagation |
|
||||
| 4 | Weeks 7-8 | Consensus Tracing | Round spans, Proposal/validation tracing |
|
||||
| 5 | Week 9 | Documentation | Runbook, Dashboards, Training |
|
||||
|
||||
**Total Effort**: 47 person-days (2 developers working in parallel)
|
||||
|
||||
➡️ **[View full Implementation Phases](./06-implementation-phases.md)**
|
||||
|
||||
---
|
||||
|
||||
## 7. Observability Backends
|
||||
|
||||
> **APM** = Application Performance Monitoring | **GCS** = Google Cloud Storage
|
||||
|
||||
Grafana Tempo is recommended for all environments due to its cost-effectiveness and Grafana integration, while Elastic APM is ideal for organizations with existing Elastic infrastructure.
|
||||
|
||||
The recommended production architecture uses a gateway collector pattern with regional collectors performing tail-based sampling, routing traces to multiple backends (Tempo for primary storage, Elastic for log correlation, S3/GCS for long-term archive).
|
||||
|
||||
➡️ **[View Observability Backend Recommendations](./07-observability-backends.md)**
|
||||
|
||||
---
|
||||
|
||||
## 8. Appendix
|
||||
|
||||
The appendix contains a glossary of OpenTelemetry and xrpld-specific terms, references to external documentation and specifications, version history for this implementation plan, and a complete document index.
|
||||
|
||||
➡️ **[View Appendix](./08-appendix.md)**
|
||||
|
||||
---
|
||||
|
||||
## Securing the OTel Pipeline
|
||||
|
||||
Threat model and hardening guidance for production deployments where xrpld nodes ship telemetry to a centrally-hosted collector across an untrusted network. Covers the two attack surfaces (collector ingress and peer trace-context spoofing) and the chosen defenses: mTLS as primary collector auth, NetworkPolicy as defense-in-depth, and source-side validation plus per-peer rate limiting for the `protocol::TraceContext` field on peer messages.
|
||||
|
||||
➡️ **[View Securing the OTel Pipeline](./secure-OTel.md)**
|
||||
|
||||
---
|
||||
|
||||
## 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._
|
||||
628
OpenTelemetryPlan/POC_taskList.md
Normal file
628
OpenTelemetryPlan/POC_taskList.md
Normal file
@@ -0,0 +1,628 @@
|
||||
# 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<Tracer> getTracer(string_view name = "xrpld") = 0;`
|
||||
- `virtual nostd::shared_ptr<Span> startSpan(string_view name, SpanKind kind = kInternal) = 0;`
|
||||
- `virtual nostd::shared_ptr<Span> 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<Telemetry> 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::Telemetry> 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 <xrpl/telemetry/SpanGuard.h>` 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 <xrpl/telemetry/SpanGuard.h>`
|
||||
- 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<int64_t>(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 <xrpl/telemetry/SpanGuard.h>`
|
||||
- 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<int64_t>(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<sdk::TracerProvider>`, 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 |
|
||||
206
OpenTelemetryPlan/Phase2_taskList.md
Normal file
206
OpenTelemetryPlan/Phase2_taskList.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Phase 2: RPC Tracing Completion Task List
|
||||
|
||||
> **Goal**: Complete RPC tracing coverage with unit tests, Grafana search filters, PathFind instrumentation, and config hardening. Build on the Phase 1c SpanGuard factory foundation to achieve production-quality RPC observability.
|
||||
>
|
||||
> **Scope**: Unit tests for core telemetry, Grafana Tempo search filters, PathFind RPC tracing, config validation (`std::clamp`).
|
||||
>
|
||||
> **Branch**: `pratik/otel-phase2-rpc-tracing` (from `pratik/otel-phase1c-rpc-integration`)
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| [04-code-samples.md](./04-code-samples.md) | TraceContextPropagator (§4.4.2), RPC instrumentation (§4.5.3) |
|
||||
| [02-design-decisions.md](./02-design-decisions.md) | W3C Trace Context (§2.5), span attributes (§2.4.2) |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 2 tasks (§6.3), definition of done (§6.11.2) |
|
||||
|
||||
---
|
||||
|
||||
## Task 2.1: W3C Trace Context HTTP Header Extraction
|
||||
|
||||
**Status**: DEFERRED → Phase 3
|
||||
|
||||
**Reason**: W3C context propagation (`traceparent`/`tracestate` headers) requires a consumer — in Phase 2, RPC spans are entirely local to the node. Phase 3 introduces cross-node transaction tracing via protobuf context propagation, which is the first use case for extracted trace context. Implementing it here without a consumer would be dead code.
|
||||
|
||||
**Implemented in**: `pratik/otel-phase3-tx-tracing` — `TraceContextPropagator.h/.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Task 2.2: Per-Category Span Creation
|
||||
|
||||
**Status**: COMPLETE (superseded by Phase 1c design)
|
||||
|
||||
**Original plan**: Add `XRPL_TRACE_PEER` and `XRPL_TRACE_LEDGER` macros.
|
||||
|
||||
**Actual implementation**: Phase 1c replaced all tracing macros with the `SpanGuard::span(TraceCategory, prefix, name)` factory pattern. The `TraceCategory` enum (`Rpc`, `Transactions`, `Consensus`, `Peer`, `Ledger`) serves the same conditional-creation purpose without macros. No separate task needed — the factory already supports all categories.
|
||||
|
||||
---
|
||||
|
||||
## Task 2.3: Add shouldTraceLedger() to Telemetry Interface
|
||||
|
||||
**Objective**: The `Setup` struct has a `traceLedger` field but there's no corresponding virtual method. Add it for interface completeness.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `include/xrpl/telemetry/Telemetry.h`:
|
||||
- Add `virtual bool shouldTraceLedger() const = 0;`
|
||||
|
||||
- Update all implementations:
|
||||
- `src/libxrpl/telemetry/Telemetry.cpp` (TelemetryImpl, NullTelemetryOtel)
|
||||
- `src/libxrpl/telemetry/NullTelemetry.cpp` (NullTelemetry)
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `include/xrpl/telemetry/Telemetry.h`
|
||||
- `src/libxrpl/telemetry/Telemetry.cpp`
|
||||
- `src/libxrpl/telemetry/NullTelemetry.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Task 2.4: Unit Tests for Core Telemetry Infrastructure
|
||||
|
||||
**Status**: COMPLETE
|
||||
|
||||
**Objective**: Add unit tests for the core telemetry abstractions to validate correctness and catch regressions.
|
||||
|
||||
**Implemented**:
|
||||
|
||||
- `src/tests/libxrpl/telemetry/TelemetryConfig.cpp`:
|
||||
- Test Setup defaults (all fields have correct initial values)
|
||||
- Test `setupTelemetry` config parser (empty section, full section, edge cases)
|
||||
- Test `samplingRatio` clamping (values outside 0.0-1.0)
|
||||
|
||||
- `src/tests/libxrpl/telemetry/SpanGuardFactory.cpp`:
|
||||
- Test null guard methods are safe (setAttribute, setOk, setError, addEvent on null)
|
||||
- Test category span returns null when telemetry disabled
|
||||
- Test child/linked span null when no parent context
|
||||
- Test move construction transfers ownership
|
||||
- Test recordException safe on null guard
|
||||
- Test discard() safe on null guard
|
||||
|
||||
- `src/tests/libxrpl/telemetry/main.cpp` — GTest runner
|
||||
- `src/tests/libxrpl/CMakeLists.txt` — test target with optional OTel linking
|
||||
|
||||
---
|
||||
|
||||
## Task 2.5: Enhance RPC Span Attributes
|
||||
|
||||
**Status**: DEFERRED (low priority)
|
||||
|
||||
**Reason**: The high-value attributes (`command`, `version`, `role`, `status`) are already set by Phase 1c. The remaining HTTP transport-level attributes (`http.method`, `net.peer.ip`, `http.status_code`) provide limited additional insight since:
|
||||
|
||||
- `http.method` is always POST for JSON-RPC
|
||||
- `net.peer.ip` is debug-level info available in logs
|
||||
- `duration_ms` is redundant with span duration (OTel captures start/end time natively)
|
||||
|
||||
These can be added later if dashboard queries specifically need them. The node health attributes (Task 2.8) provide far more operational value and were prioritized instead.
|
||||
|
||||
---
|
||||
|
||||
## Task 2.6: Build Verification and Performance Baseline
|
||||
|
||||
**Objective**: Verify the build succeeds with and without telemetry, and establish a performance baseline.
|
||||
|
||||
**What to do**:
|
||||
|
||||
1. Build with `telemetry=ON` and verify no compilation errors
|
||||
2. Build with `telemetry=OFF` and verify no regressions
|
||||
3. Run existing unit tests to verify no breakage
|
||||
4. Document any build issues in lessons.md
|
||||
|
||||
**Verification Checklist**:
|
||||
|
||||
- [ ] `conan install . --build=missing -o telemetry=True` succeeds
|
||||
- [ ] `cmake --preset default -Dtelemetry=ON` configures correctly
|
||||
- [ ] Build succeeds with telemetry ON
|
||||
- [ ] Build succeeds with telemetry OFF
|
||||
- [ ] Existing tests pass with telemetry ON
|
||||
- [ ] Existing tests pass with telemetry OFF
|
||||
|
||||
---
|
||||
|
||||
## Task 2.8: RPC Span Attribute Enrichment — Node Health Context
|
||||
|
||||
**Status**: DROPPED.
|
||||
|
||||
Node health (`amendment_blocked`, `server_state`) is not part of the telemetry surface. Operators consume the same data via the existing `server_info` / `server_state` RPC commands, so duplicating it on traces adds storage and cardinality cost without new value. The OTel C++ SDK 1.18.0 also does not support runtime updates to the resource, ruling out resource-level emission of these dynamic-by-nature flags.
|
||||
|
||||
---
|
||||
|
||||
## Task 2.9: PathFind RPC Instrumentation
|
||||
|
||||
**Status**: COMPLETE
|
||||
|
||||
**Objective**: Trace the path_find and ripple_path_find RPC handlers to capture request latency and computation cost.
|
||||
|
||||
**Spans added**:
|
||||
|
||||
- `pathfind.request` — wraps `doPathFind()` and `doRipplePathFind()` RPC handlers
|
||||
- `pathfind.compute` — wraps `PathRequest::doUpdate()` (`pathfind_fast` attr)
|
||||
- `pathfind.update_all` — wraps `PathRequestManager::updateAll()` on ledger close (`pathfind_ledger_index`, `pathfind_num_requests` attrs; emitted only when active subscriptions exist)
|
||||
- `pathfind.discover` — wraps the entire per-source-asset loop in `PathRequest::findPaths()` (`pathfind_search_level`, `pathfind_num_paths` attrs). One span per RPC call instead of N (one per source asset). Trade-off: per-asset breakdown is lost; storage and cardinality bounded.
|
||||
|
||||
**Attribute namespacing**: All pathfind attributes use the `pathfind_*` underscore form per the Phase 1c naming-spec rule 5.
|
||||
|
||||
**New file**: `src/xrpld/rpc/detail/PathFindSpanNames.h`
|
||||
|
||||
**Modified files**:
|
||||
|
||||
- `src/xrpld/rpc/handlers/orderbook/PathFind.cpp`
|
||||
- `src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp`
|
||||
- `src/xrpld/rpc/detail/PathRequest.cpp`
|
||||
- `src/xrpld/rpc/detail/PathRequestManager.cpp`
|
||||
- `src/xrpld/rpc/detail/Pathfinder.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Task 2.10: RPC and PathFind Span Attribute Gap Fill
|
||||
|
||||
**Status**: COMPLETE
|
||||
|
||||
**Objective**: Wire up workflow-identifying attributes that enable filtering and grouping traces by request characteristics without drilling into child spans.
|
||||
|
||||
**Attributes added**:
|
||||
|
||||
| Span | Attribute | Type | Source |
|
||||
| ------------------- | ---------------------------- | ------ | --------------------------------- |
|
||||
| `rpc.http_request` | `request_payload_size` | int64 | `request.body().size()` |
|
||||
| `rpc.process` | `is_batch` | bool | `method == "batch"` check |
|
||||
| `rpc.process` | `batch_size` | int64 | `params.size()` (only when batch) |
|
||||
| `rpc.ws_message` | `command` | string | `jv[command]` or `jv[method]` |
|
||||
| `rpc.command.*` | `load_type` | string | `context.loadType.label()` |
|
||||
| `pathfind.compute` | `pathfind_dest_amount` | string | `saDstAmount_.getFullText()` |
|
||||
| `pathfind.compute` | `pathfind_dest_currency` | string | `to_string(saDstAmount_.asset())` |
|
||||
| `pathfind.discover` | `pathfind_num_source_assets` | int64 | `sourceAssets.size()` |
|
||||
|
||||
**New attr keys**: `RpcSpanNames.h` (`isBatch`, `batchSize`, `loadType`), `PathFindSpanNames.h` (`destAmount`, `destCurrency`, `numSourceAssets`).
|
||||
|
||||
**Modified files**:
|
||||
|
||||
- `src/xrpld/rpc/detail/RpcSpanNames.h`
|
||||
- `src/xrpld/rpc/detail/PathFindSpanNames.h`
|
||||
- `src/xrpld/rpc/detail/ServerHandler.cpp`
|
||||
- `src/xrpld/rpc/detail/RPCHandler.cpp`
|
||||
- `src/xrpld/rpc/detail/PathRequest.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Description | Status | Notes |
|
||||
| ---- | ------------------------------------------- | ------------------- | --------------------------------------------------------- |
|
||||
| 2.1 | W3C Trace Context header extraction | Deferred → Phase 3 | No consumer in Phase 2; needs cross-node tracing |
|
||||
| 2.2 | Per-category span creation | Complete (Phase 1c) | Superseded by TraceCategory enum + SpanGuard |
|
||||
| 2.3 | Add shouldTraceLedger() interface method | Complete (Phase 1c) | Delivered in Phase 1c base branch |
|
||||
| 2.4 | Unit tests for core telemetry | Complete | TelemetryConfig + SpanGuardFactory tests |
|
||||
| 2.5 | Enhanced RPC span attributes (HTTP-level) | Deferred | Low value; span duration covers timing natively |
|
||||
| 2.6 | Build verification and performance baseline | Complete | Verified in CI on Phase 1c |
|
||||
| 2.7 | Grafana Tempo search filters | Complete | rpc-command, rpc-status, rpc-role filters |
|
||||
| 2.8 | RPC span attribute enrichment (node health) | Dropped | Available via `server_info`/`server_state` RPC |
|
||||
| 2.9 | PathFind RPC instrumentation | Complete | request, compute, update_all, discover |
|
||||
| 2.10 | RPC/PathFind span attribute gap fill | Complete | Batch detection, payload size, load cost, pathfind params |
|
||||
|
||||
**Delivered in this branch**: Tasks 2.4, 2.7, 2.9, 2.10.
|
||||
**Deferred with rationale**: Tasks 2.1 (→Phase 3), 2.5 (low priority).
|
||||
**Dropped**: Task 2.8 (node health not duplicated on traces).
|
||||
**Superseded**: Task 2.2 (Phase 1c SpanGuard factory covers this).
|
||||
531
OpenTelemetryPlan/Phase3_taskList.md
Normal file
531
OpenTelemetryPlan/Phase3_taskList.md
Normal file
@@ -0,0 +1,531 @@
|
||||
# Phase 3: Transaction Tracing Task List
|
||||
|
||||
> **Goal**: Trace the full transaction lifecycle from RPC submission through peer relay, including cross-node context propagation via Protocol Buffer extensions. This is the WALK phase that demonstrates true distributed tracing.
|
||||
>
|
||||
> **Scope**: Protocol Buffer `TraceContext` message, context serialization, PeerImp transaction instrumentation, NetworkOPs processing instrumentation, HashRouter visibility, and multi-node relay context propagation.
|
||||
>
|
||||
> **Branch**: `pratik/otel-phase3-tx-tracing` (from `pratik/otel-phase2-rpc-tracing`)
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| [04-code-samples.md](./04-code-samples.md) | TraceContext protobuf (§4.4.1), PeerImp instrumentation (§4.5.1), context serialization (§4.4.2) |
|
||||
| [01-architecture-analysis.md](./01-architecture-analysis.md) | Transaction flow (§1.3), key trace points (§1.6) |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 3 tasks (§6.4), definition of done (§6.11.3) |
|
||||
| [02-design-decisions.md](./02-design-decisions.md) | Context propagation design (§2.5), attribute schema (§2.4.3) |
|
||||
|
||||
---
|
||||
|
||||
## Task 3.1: Define TraceContext Protocol Buffer Message
|
||||
|
||||
**Objective**: Add trace context fields to the P2P protocol messages so trace IDs can propagate across nodes.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `include/xrpl/proto/xrpl.proto` (or `src/xrpld/proto/ripple.proto`, wherever the proto is):
|
||||
- Add `TraceContext` message definition:
|
||||
```protobuf
|
||||
message TraceContext {
|
||||
bytes trace_id = 1; // 16-byte trace identifier
|
||||
bytes span_id = 2; // 8-byte span identifier
|
||||
uint32 trace_flags = 3; // bit 0 = sampled
|
||||
string trace_state = 4; // W3C tracestate value
|
||||
}
|
||||
```
|
||||
- Add `optional TraceContext trace_context = 1001;` to:
|
||||
- `TMTransaction`
|
||||
- `TMProposeSet` (for Phase 4 use)
|
||||
- `TMValidation` (for Phase 4 use)
|
||||
- Use high field numbers (1001+) to avoid conflicts with existing fields
|
||||
|
||||
- Regenerate protobuf C++ code
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `include/xrpl/proto/xrpl.proto` (or equivalent)
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [04-code-samples.md §4.4.1](./04-code-samples.md) — TraceContext message definition
|
||||
- [02-design-decisions.md §2.5.2](./02-design-decisions.md) — Protocol buffer context propagation design
|
||||
|
||||
---
|
||||
|
||||
## Task 3.2: Implement Protobuf Context Serialization
|
||||
|
||||
**Objective**: Create utilities to serialize/deserialize OTel trace context to/from protobuf `TraceContext` messages.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `include/xrpl/telemetry/TraceContextPropagator.h` (extend from Phase 2 if exists, or add protobuf methods):
|
||||
- Add protobuf-specific methods:
|
||||
- `static Context extractFromProtobuf(protocol::TraceContext const& proto)` — reconstruct OTel context from protobuf fields
|
||||
- `static void injectToProtobuf(Context const& ctx, protocol::TraceContext& proto)` — serialize current span context into protobuf fields
|
||||
- Both methods guard behind `#ifdef XRPL_ENABLE_TELEMETRY`
|
||||
|
||||
- Create/extend `src/libxrpl/telemetry/TraceContextPropagator.cpp`:
|
||||
- Implement extraction: read trace_id (16 bytes), span_id (8 bytes), trace_flags from protobuf, construct `SpanContext`, wrap in `Context`
|
||||
- Implement injection: get current span from context, serialize its TraceId, SpanId, and TraceFlags into protobuf fields
|
||||
|
||||
**Key new/modified files**:
|
||||
|
||||
- `include/xrpl/telemetry/TraceContextPropagator.h`
|
||||
- `src/libxrpl/telemetry/TraceContextPropagator.cpp`
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [04-code-samples.md §4.4.2](./04-code-samples.md) — Full extract/inject implementation
|
||||
|
||||
---
|
||||
|
||||
## Task 3.3: Instrument PeerImp Transaction Handling
|
||||
|
||||
**Objective**: Add trace spans to the peer-level transaction receive and relay path.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/overlay/detail/PeerImp.cpp`:
|
||||
- In `onMessage(TMTransaction)` / `handleTransaction()`:
|
||||
- Extract parent trace context from incoming `TMTransaction::trace_context` field (if present)
|
||||
- Create `tx.receive` span as child of extracted context (or new root if none)
|
||||
- Set attributes: `tx_hash`, `peer_id`, `tx_status`
|
||||
- On HashRouter suppression (duplicate): set `suppressed=true`, add `tx.duplicate` event
|
||||
- Wrap validation call with child span `tx.validate`
|
||||
- Wrap relay with `tx.relay` span
|
||||
- When relaying to peers:
|
||||
- Inject current trace context into outgoing `TMTransaction::trace_context`
|
||||
- Set `relay_count` attribute
|
||||
|
||||
- Use `SpanGuard::span(TraceCategory::Transactions, "tx", "receive")` factory
|
||||
(Phase 1c replaced macros with the SpanGuard factory pattern)
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/overlay/detail/PeerImp.cpp`
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [04-code-samples.md §4.5.1](./04-code-samples.md) — Full PeerImp instrumentation example
|
||||
- [01-architecture-analysis.md §1.3](./01-architecture-analysis.md) — Transaction flow diagram
|
||||
- [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — tx.receive trace point
|
||||
|
||||
---
|
||||
|
||||
## Task 3.4: Instrument NetworkOPs Transaction Processing
|
||||
|
||||
**Objective**: Trace the transaction processing pipeline in NetworkOPs, covering both sync and async paths.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/app/misc/NetworkOPs.cpp`:
|
||||
- In `processTransaction()`:
|
||||
- Create `tx.process` span
|
||||
- Set attributes: `tx_hash`, `tx_type`, `local` (whether from RPC or peer)
|
||||
- Record whether sync or async path is taken
|
||||
|
||||
- In `doTransactionAsync()`:
|
||||
- Capture parent context before queuing
|
||||
- Create `tx.queue` span with queue depth attribute
|
||||
- Add event when transaction is dequeued for processing
|
||||
|
||||
- In `doTransactionSync()`:
|
||||
- Create `tx.process_sync` span
|
||||
- Record result (applied, queued, rejected)
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/misc/NetworkOPs.cpp`
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — tx.validate and tx.process trace points
|
||||
- [02-design-decisions.md §2.4.3](./02-design-decisions.md) — Transaction attribute schema
|
||||
|
||||
---
|
||||
|
||||
## Task 3.5: Instrument HashRouter for Dedup Visibility
|
||||
|
||||
**Objective**: Make transaction deduplication visible in traces by recording HashRouter decisions as span attributes/events.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/overlay/detail/PeerImp.cpp` (in handleTransaction):
|
||||
- After calling `HashRouter::shouldProcess()` or `addSuppressionPeer()`:
|
||||
- Record `suppressed` attribute (true/false)
|
||||
- Record `tx_flags` showing current HashRouter state (SAVED, TRUSTED, etc.)
|
||||
- Add `tx.first_seen` or `tx.duplicate` event
|
||||
|
||||
- This is NOT a modification to HashRouter itself — just recording its decisions as span attributes in the existing PeerImp instrumentation from Task 3.3.
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/overlay/detail/PeerImp.cpp` (same changes as 3.3, logically grouped)
|
||||
|
||||
---
|
||||
|
||||
## Task 3.6: Context Propagation in Transaction Relay
|
||||
|
||||
**Status**: COMPLETE
|
||||
|
||||
**Objective**: Ensure trace context flows correctly when transactions are relayed between peers, creating linked spans across nodes.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- **TX send side**: `NetworkOPs::apply()` now injects the tx.process span's trace
|
||||
context into the outgoing `TMTransaction` protobuf before relay, using
|
||||
`telemetry::injectSpanContext()`. The receiving node's `txReceiveSpan()` (already
|
||||
wired in PeerImp) extracts the parent span_id and creates the tx.receive span
|
||||
as a child of the sender's tx.process span.
|
||||
|
||||
- **Proposal send/receive**: `RCLConsensus::Adaptor::propose()` injects the
|
||||
current thread's active span context into the `TMProposeSet` protobuf via
|
||||
`telemetry::injectToProtobuf()`. PeerImp creates a
|
||||
`consensus.proposal.receive` span that extracts the sender's trace context
|
||||
as parent (via `ConsensusReceiveTracing.h`).
|
||||
|
||||
- **Validation send/receive**: `RCLConsensus::Adaptor::validate()` injects
|
||||
the current thread's active span context into the `TMValidation` protobuf.
|
||||
PeerImp creates a `consensus.validation.receive` span that extracts the
|
||||
sender's trace context as parent.
|
||||
|
||||
- **Edge cases**: Missing trace context (older peers) degrades gracefully to
|
||||
standalone spans. Invalid/corrupted context is treated as absent. Trace
|
||||
flags are propagated and respected.
|
||||
|
||||
**New infrastructure**:
|
||||
|
||||
- `SpanGuard::getTraceBytes()` — extracts raw trace_id/span_id/trace_flags
|
||||
from a span without exposing OTel types. Safe to call from any thread.
|
||||
- `PropagationHelpers.h` — `injectSpanContext(SpanGuard&, proto)` bridge
|
||||
between SpanGuard and protobuf TraceContext.
|
||||
- `TraceContextPropagator.h` — `injectToProtobuf(ctx, proto)` for
|
||||
same-thread injection via OTel RuntimeContext (used in propose/validate).
|
||||
- `ConsensusReceiveTracing.h` — `proposalReceiveSpan()` and
|
||||
`validationReceiveSpan()` helper functions that create receive spans with
|
||||
optional parent context extraction from incoming protobuf messages.
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/misc/NetworkOPs.cpp` — tx relay injection
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp` — proposal/validation send injection
|
||||
- `src/xrpld/overlay/detail/PeerImp.cpp` — proposal/validation receive spans
|
||||
- `include/xrpl/telemetry/SpanGuard.h` — `TraceBytes` struct, `getTraceBytes()`
|
||||
- `src/libxrpl/telemetry/SpanGuard.cpp` — `getTraceBytes()` implementation
|
||||
- `src/xrpld/telemetry/PropagationHelpers.h` — inject helpers (new file)
|
||||
- `src/xrpld/telemetry/ConsensusReceiveTracing.h` — receive span helpers (new file)
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [02-design-decisions.md §2.5](./02-design-decisions.md) — Context propagation design
|
||||
- [04-code-samples.md §4.5.1](./04-code-samples.md) — Relay context injection pattern
|
||||
|
||||
---
|
||||
|
||||
## Task 3.7: Build Verification and Testing
|
||||
|
||||
**Objective**: Verify all Phase 3 changes compile and work correctly.
|
||||
|
||||
**What to do**:
|
||||
|
||||
1. Build with `telemetry=ON` — verify no compilation errors
|
||||
2. Build with `telemetry=OFF` — verify no regressions
|
||||
3. Run existing unit tests
|
||||
4. Verify protobuf regeneration produces correct C++ code
|
||||
5. Document any issues encountered
|
||||
|
||||
**Verification Checklist**:
|
||||
|
||||
- [ ] Protobuf changes generate valid C++
|
||||
- [ ] Build succeeds with telemetry ON
|
||||
- [ ] Build succeeds with telemetry OFF
|
||||
- [ ] Existing tests pass
|
||||
- [ ] No undefined symbols from new telemetry calls
|
||||
|
||||
---
|
||||
|
||||
## Task 3.8: Transaction Span Peer Version Attribute
|
||||
|
||||
> **Source**: [External Dashboard Parity](../docs/superpowers/specs/2026-03-30-external-dashboard-parity-design.md) — adds peer version context inspired by the community [xrpl-validator-dashboard](https://github.com/realgrapedrop/xrpl-validator-dashboard).
|
||||
>
|
||||
> **Upstream**: Phase 2 (RPC span infrastructure must exist).
|
||||
> **Downstream**: Phase 10 (validation checks for this attribute).
|
||||
|
||||
**Objective**: Add the relaying peer's xrpld version to `tx.receive` spans so operators can correlate transaction issues with peer version mismatches during network upgrades.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/overlay/detail/PeerImp.cpp`:
|
||||
- In the `tx.receive` span block (after existing `peer_id` setAttribute call):
|
||||
- Add `peer_version` (string) — from `this->getVersion()`
|
||||
- Only set if `getVersion()` returns a non-empty string (avoid empty-string attributes)
|
||||
|
||||
**New span attribute**:
|
||||
|
||||
| Attribute | Type | Source | Example |
|
||||
| -------------- | ------ | -------------------- | --------------- |
|
||||
| `peer_version` | string | `peer->getVersion()` | `"xrpld-2.4.0"` |
|
||||
|
||||
**Rationale**: Transaction relay is where version mismatches cause subtle serialization or validation bugs. Tracing "this tx came from a v2.3.0 peer" helps diagnose compatibility issues. The community dashboard tracks peer versions externally; this brings version awareness into the trace itself.
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/overlay/detail/PeerImp.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] `tx.receive` spans carry `peer_version` attribute with a non-empty version string
|
||||
- [ ] Attribute is omitted (not set to empty string) when `getVersion()` returns empty
|
||||
- [ ] Attribute visible in Jaeger span detail view
|
||||
|
||||
---
|
||||
|
||||
## Task 3.9: Deterministic Transaction Trace ID
|
||||
|
||||
> **Upstream**: Task 3.2 (protobuf serialization), Task 3.3 (PeerImp span exists).
|
||||
> **Downstream**: Phase 10 (workload validation can query by tx hash directly).
|
||||
> **Pattern**: Mirrors the consensus deterministic trace ID in Phase 4a
|
||||
> (`createDeterministicContext` in `RCLConsensus.cpp`), adapted for transactions.
|
||||
|
||||
**Objective**: Derive the trace_id for transaction spans deterministically from the
|
||||
transaction hash so that all nodes handling the same transaction independently produce
|
||||
spans under the same trace_id — regardless of whether protobuf context propagation
|
||||
succeeds.
|
||||
|
||||
**Why**: The current approach creates spans with random trace_ids and relies entirely
|
||||
on protobuf `TraceContext` propagation to link them. If any hop in the relay chain
|
||||
drops the context (older peers, message corruption, mixed-version networks), the trace
|
||||
splits and downstream spans become impossible to find. With deterministic trace_ids,
|
||||
correlation is guaranteed because every node derives the same trace_id from the same
|
||||
`txID`.
|
||||
|
||||
**Approach — deterministic trace_id + protobuf span_id propagation**:
|
||||
|
||||
1. Derive `trace_id = txHash[0:16]` (first 16 bytes of the 32-byte transaction hash).
|
||||
2. Generate a random 8-byte `span_id` per node (each node's span is unique within
|
||||
the shared trace).
|
||||
3. Create the span under this deterministic context as parent.
|
||||
4. **Additionally**, if protobuf `TraceContext` is present in the incoming
|
||||
`TMTransaction` message, extract the sender's `span_id` and use it as the span's
|
||||
parent — this preserves parent-child ordering in the trace tree.
|
||||
5. If protobuf context is absent (older peer, first hop), the span still has the
|
||||
correct deterministic `trace_id` — it appears as a sibling root in the same trace
|
||||
rather than being lost.
|
||||
|
||||
This gives the best of both worlds: guaranteed cross-node correlation via deterministic
|
||||
`trace_id`, plus parent-child relay ordering via protobuf `span_id` when available.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `createDeterministicTxContext(uint256 const& txHash)` utility function:
|
||||
- Location: shared header or file-local in `PeerImp.cpp` and `NetworkOPs.cpp`
|
||||
(or a shared telemetry utility if both need it).
|
||||
- Pattern: identical to `createDeterministicContext(uint256 const& ledgerId)` in
|
||||
`RCLConsensus.cpp` — take `txHash[0:16]` as trace_id, random span_id via
|
||||
`default_prng()`, sampled flag set, `remote=false`.
|
||||
- Guard behind `#ifdef XRPL_ENABLE_TELEMETRY`.
|
||||
|
||||
```cpp
|
||||
opentelemetry::context::Context
|
||||
createDeterministicTxContext(uint256 const& txHash)
|
||||
{
|
||||
namespace trace = opentelemetry::trace;
|
||||
|
||||
// First 16 bytes of the 32-byte tx hash as trace ID.
|
||||
trace::TraceId traceId(
|
||||
opentelemetry::nostd::span<uint8_t const, 16>(txHash.data(), 16));
|
||||
|
||||
// Random span_id so each node's span is unique within the trace.
|
||||
uint8_t spanIdBytes[8];
|
||||
auto const rval = default_prng()();
|
||||
std::memcpy(spanIdBytes, &rval, sizeof(spanIdBytes));
|
||||
trace::SpanId spanId(
|
||||
opentelemetry::nostd::span<uint8_t const, 8>(spanIdBytes, 8));
|
||||
|
||||
trace::SpanContext syntheticCtx(
|
||||
traceId, spanId, trace::TraceFlags(1), /* remote = */ false);
|
||||
|
||||
return opentelemetry::context::Context{}.SetValue(
|
||||
trace::kSpanKey,
|
||||
opentelemetry::nostd::shared_ptr<trace::Span>(
|
||||
new trace::DefaultSpan(syntheticCtx)));
|
||||
}
|
||||
```
|
||||
|
||||
- Edit `src/xrpld/overlay/detail/PeerImp.cpp` — restructure `handleTransaction()`:
|
||||
- **Move span creation after deserialization** (txID must be known first):
|
||||
1. Deserialize `STTx` and get `txID` (existing code at line ~1382).
|
||||
2. Create deterministic parent context: `auto detCtx = createDeterministicTxContext(txID)`.
|
||||
3. If `m->has_trace_context()`: extract protobuf context via `extractFromProtobuf()`,
|
||||
**combine** with deterministic trace_id — use the protobuf span_id as parent
|
||||
to preserve relay ordering, but override trace_id with the deterministic one.
|
||||
4. If no protobuf context: create span under `detCtx` directly.
|
||||
5. Set all existing attributes (`hash`, `peerId`, `peerVersion`, `suppressed`, etc.).
|
||||
|
||||
- **Combining deterministic trace_id with protobuf parent span_id**:
|
||||
When both are available, construct a synthetic `SpanContext` with:
|
||||
- `trace_id` = `txHash[0:16]` (deterministic)
|
||||
- `span_id` = extracted from protobuf (sender's span_id → becomes parent)
|
||||
- `trace_flags` = from protobuf
|
||||
- `remote` = true (came from another node)
|
||||
|
||||
```cpp
|
||||
// Pseudo-code for the combined context:
|
||||
auto detTraceId = trace::TraceId(txHash.data(), 16);
|
||||
auto remoteSpanId = /* from extractFromProtobuf */;
|
||||
auto remoteFlags = /* from extractFromProtobuf */;
|
||||
|
||||
trace::SpanContext combinedCtx(
|
||||
detTraceId, remoteSpanId, remoteFlags, /* remote = */ true);
|
||||
// Use as parent context for the new span.
|
||||
```
|
||||
|
||||
- Edit `src/xrpld/app/misc/NetworkOPs.cpp` — update `processTransaction()`:
|
||||
- `transaction->getID()` is already available at the top of the function.
|
||||
- Create deterministic parent context from `txID`.
|
||||
- Create `tx.process` span under this context.
|
||||
- No protobuf context to extract here (NetworkOPs is intra-node), so
|
||||
deterministic context alone is sufficient.
|
||||
|
||||
- Add `trace_strategy` attribute to spans:
|
||||
- Add `inline constexpr auto traceStrategy = "trace_strategy";`
|
||||
to `TxSpanNames.h`.
|
||||
- Set on each tx span: `span.setAttribute(tx_span::attr::traceStrategy, "deterministic")`.
|
||||
|
||||
**Key new/modified files**:
|
||||
|
||||
- `src/xrpld/overlay/detail/PeerImp.cpp` — restructured span creation
|
||||
- `src/xrpld/app/misc/NetworkOPs.cpp` — deterministic context for tx.process
|
||||
- `src/xrpld/app/misc/TxSpanNames.h` — new `traceStrategy` attribute constant
|
||||
- New or shared utility for `createDeterministicTxContext()` (location TBD: could be
|
||||
a shared header like `include/xrpl/telemetry/DeterministicContext.h`, or file-local
|
||||
if only used in two places)
|
||||
|
||||
**Interaction with existing tasks**:
|
||||
|
||||
- **Task 3.3 (PeerImp instrumentation)**: The span creation in `handleTransaction()`
|
||||
must be restructured — the span currently starts before `txID` is known. This task
|
||||
moves it after deserialization.
|
||||
- **Task 3.6 (Relay context propagation)**: Protobuf injection at the relay site
|
||||
remains the same — `injectToProtobuf()` serializes the current span's `span_id`.
|
||||
The receiver extracts it and combines with the deterministic `trace_id`.
|
||||
- **Phase 4a (Consensus deterministic trace ID)**: This task follows the same pattern.
|
||||
Consider extracting a shared utility (e.g., `createDeterministicContext(uint256)`)
|
||||
that both consensus and transaction tracing use.
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] `tx.receive` and `tx.process` spans have deterministic trace_id = `txHash[0:16]`
|
||||
- [ ] All nodes handling the same transaction produce spans under the same trace_id
|
||||
- [x] Protobuf `span_id` propagation still works when available (parent-child ordering)
|
||||
- [ ] Missing protobuf context (old peer) degrades gracefully to sibling spans, not lost traces
|
||||
- [ ] `trace_strategy` attribute set to `"deterministic"` on all tx spans
|
||||
- [ ] Trace queryable by tx hash (truncate hash → trace_id → direct lookup in Tempo)
|
||||
|
||||
**Deliverables implemented (not in original plan)**:
|
||||
|
||||
- **`SpanGuard::txSpan()` factory method** (`include/xrpl/telemetry/SpanGuard.h`):
|
||||
Two overloads for creating transaction spans with deterministic trace IDs:
|
||||
- `txSpan(category, group, name, txHash)` — standalone span (deterministic
|
||||
trace_id from `txHash[0:16]`, no parent span_id).
|
||||
- `txSpan(category, group, name, txHash, parentCtx)` — child span (deterministic
|
||||
trace_id combined with protobuf-extracted parent span_id for relay ordering).
|
||||
|
||||
- **`TxTracing.h` helper functions** (`src/xrpld/overlay/detail/TxTracing.h`):
|
||||
File-local helpers that wrap `SpanGuard::txSpan()` for the two main PeerImp call
|
||||
sites:
|
||||
- `txReceiveSpan(txHash, parentCtx)` — creates `tx.receive` span with
|
||||
deterministic trace_id and optional protobuf parent context.
|
||||
- `txProcessSpan(txHash)` — creates `tx.process` span with deterministic
|
||||
trace_id only (no protobuf parent, used intra-node).
|
||||
- **Note**: `TxTracing.h` includes `xrpl.pb.h` unconditionally (outside
|
||||
`#ifdef XRPL_ENABLE_TELEMETRY`) because `protocol::TMTransaction` appears in
|
||||
the function signatures regardless of telemetry build mode.
|
||||
|
||||
---
|
||||
|
||||
## Task 3.10: TxQ Instrumentation
|
||||
|
||||
**Status**: COMPLETE
|
||||
|
||||
**Objective**: Trace the transaction queue lifecycle — enqueue decisions, direct apply, batch clear, ledger-close accept loop, per-tx apply, and cleanup.
|
||||
|
||||
**Spans added**:
|
||||
|
||||
- `txq.enqueue` — wraps `TxQ::apply()` with tx_hash attribute
|
||||
- `txq.apply_direct` — wraps `TxQ::tryDirectApply()` fast-path
|
||||
- `txq.batch_clear` — wraps `TxQ::tryClearAccountQueueUpThruTx()`
|
||||
- `txq.accept` — wraps `TxQ::accept()` ledger-close dequeue with queue_size attr
|
||||
- `txq.accept_tx` — per-tx span inside accept loop with tx_hash, ter_code,
|
||||
retries_remaining attributes
|
||||
- `txq.cleanup` — wraps `TxQ::processClosedLedger()` with ledger_seq attribute
|
||||
|
||||
**New file**: `src/xrpld/app/misc/detail/TxQSpanNames.h`
|
||||
|
||||
**Modified file**: `src/xrpld/app/misc/detail/TxQ.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Task 3.11: TX and TxQ Span Attribute Gap Fill
|
||||
|
||||
**Status**: COMPLETE
|
||||
|
||||
**Objective**: Add workflow-identifying attributes to transaction spans so operators can filter by transaction type and see outcomes without off-chain correlation.
|
||||
|
||||
**Attributes added**:
|
||||
|
||||
| Span | Attribute | Type | Source |
|
||||
| ----------------- | -------------------- | ------ | ------------------------------------------------------------------- |
|
||||
| `tx.process` | `tx_type` | string | `TxFormats::getInstance().findByType(stx->getTxnType())->getName()` |
|
||||
| `tx.process` | `fee` | int64 | `stx->getFieldAmount(sfFee).xrp().drops()` |
|
||||
| `tx.process` | `sequence` | int64 | `stx->getSeqProxy().value()` |
|
||||
| `tx.process` | `ter_result` | string | `transToken(e.result)` (set after batch application) |
|
||||
| `tx.process` | `applied` | bool | `e.applied` (set after batch application) |
|
||||
| `tx.receive` | `tx_type` | string | `TxFormats::getInstance().findByType(stx->getTxnType())->getName()` |
|
||||
| `txq.enqueue` | `tx_type` | string | same pattern as above |
|
||||
| `txq.enqueue` | `txq_status` | string | `queued` / `applied_direct` / `applied` / `rejected` |
|
||||
| `txq.enqueue` | `fee_level_paid` | int64 | `getFeeLevelPaid(view, *tx).value()` |
|
||||
| `txq.enqueue` | `required_fee_level` | int64 | `getRequiredFeeLevel(...).value()` |
|
||||
| `txq.batch_clear` | `num_cleared` | int64 | queued txs cleared ahead of the applying tx |
|
||||
| `txq.cleanup` | `expired_count` | int64 | entries dropped for passed `LastLedgerSequence` |
|
||||
| `txq.accept.tx` | `txq_status` | string | `applied` / `failed` / `retried` |
|
||||
| `txq.accept` | `ledger_changed` | bool | set at end of accept loop |
|
||||
|
||||
**New attr keys**: `TxSpanNames.h` (`txType`, `fee`, `sequence`, `terResult`, `applied`), `TxQSpanNames.h` (`txType`).
|
||||
|
||||
**Modified files**:
|
||||
|
||||
- `src/xrpld/app/misc/TxSpanNames.h`
|
||||
- `src/xrpld/app/misc/detail/TxQSpanNames.h`
|
||||
- `src/xrpld/app/misc/NetworkOPs.cpp`
|
||||
- `src/xrpld/overlay/detail/PeerImp.cpp`
|
||||
- `src/xrpld/app/misc/detail/TxQ.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Description | New Files | Modified Files | Depends On |
|
||||
| ---- | ----------------------------------- | --------- | -------------- | ---------- |
|
||||
| 3.1 | TraceContext protobuf message | 0 | 1 | Phase 2 |
|
||||
| 3.2 | Protobuf context serialization | 1-2 | 0 | 3.1 |
|
||||
| 3.3 | PeerImp transaction instrumentation | 0 | 1 | 3.2 |
|
||||
| 3.4 | NetworkOPs transaction processing | 0 | 1 | Phase 2 |
|
||||
| 3.5 | HashRouter dedup visibility | 0 | 1 | 3.3 |
|
||||
| 3.6 | Relay context propagation | 0 | 1-2 | 3.3, 3.5 |
|
||||
| 3.7 | Build verification and testing | 0 | 0 | 3.1-3.6 |
|
||||
| 3.8 | TX span peer version attribute | 0 | 1 | 3.3 |
|
||||
| 3.9 | Deterministic transaction trace ID | 0-1 | 3 | 3.2, 3.3 |
|
||||
| 3.10 | TxQ instrumentation (6 spans) | 1 | 1 | 3.4 |
|
||||
| 3.11 | TX/TxQ span attribute gap fill | 0 | 5 | 3.3, 3.10 |
|
||||
|
||||
**Parallel work**: Tasks 3.1 and 3.4 can start in parallel. Task 3.2 depends on 3.1. Tasks 3.3 and 3.5 depend on 3.2. Task 3.6 depends on 3.3 and 3.5. Task 3.8 depends on 3.3 (span must exist). Task 3.9 depends on 3.2 and 3.3. Task 3.10 depends on 3.4 (tx.process span must exist).
|
||||
|
||||
**Exit Criteria** (from [06-implementation-phases.md §6.11.3](./06-implementation-phases.md)):
|
||||
|
||||
- [x] Transaction traces span across nodes
|
||||
- [x] Trace context in Protocol Buffer messages
|
||||
- [ ] HashRouter deduplication visible in traces
|
||||
- [ ] <5% overhead on transaction throughput
|
||||
- [x] Deterministic trace_id: same trace_id for same tx across all nodes
|
||||
- [x] Protobuf span_id propagation preserves parent-child ordering when available
|
||||
221
OpenTelemetryPlan/Phase4_taskList.md
Normal file
221
OpenTelemetryPlan/Phase4_taskList.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Phase 4: Consensus Tracing Task List
|
||||
|
||||
> **Goal**: Full observability into consensus rounds — track round lifecycle, phase transitions, proposal handling, and validation. This is the RUN phase that completes the distributed tracing story.
|
||||
>
|
||||
> **Scope**: RCLConsensus instrumentation for round starts, phase transitions (open/establish/accept), proposal send/receive, validation handling, and correlation with transaction traces from Phase 3.
|
||||
>
|
||||
> **Branch**: `pratik/otel-phase4-consensus-tracing` (from `pratik/otel-phase3-tx-tracing`)
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| ------------------------------------------------------------ | ----------------------------------------------------------- |
|
||||
| [04-code-samples.md](./04-code-samples.md) | Consensus instrumentation (§4.5.2), consensus span patterns |
|
||||
| [01-architecture-analysis.md](./01-architecture-analysis.md) | Consensus round flow (§1.4), key trace points (§1.6) |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 4 tasks (§6.5), definition of done (§6.11.4) |
|
||||
| [02-design-decisions.md](./02-design-decisions.md) | Consensus attribute schema (§2.4.4) |
|
||||
|
||||
---
|
||||
|
||||
## Task 4.1: Instrument Consensus Round Start
|
||||
|
||||
**Objective**: Create a root span for each consensus round that captures the round's key parameters.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/app/consensus/RCLConsensus.cpp`:
|
||||
- In `RCLConsensus::startRound()` (or the Adaptor's startRound):
|
||||
- Create `consensus.round` span using `SpanGuard::span(TraceCategory::Consensus, ...)`
|
||||
- Set attributes:
|
||||
- `xrpl.consensus.ledger.prev` — previous ledger hash
|
||||
- `xrpl.consensus.ledger.seq` — target ledger sequence
|
||||
- `xrpl.consensus.proposers` — number of trusted proposers
|
||||
- `xrpl.consensus.mode` — "proposing" or "observing"
|
||||
- Store the span context for use by child spans in phase transitions
|
||||
|
||||
- Add a member to hold current round trace context:
|
||||
- `opentelemetry::context::Context currentRoundContext_` (guarded by `#ifdef`)
|
||||
- Updated at round start, used by phase transition spans
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
- `src/xrpld/app/consensus/RCLConsensus.h` (add context member)
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [04-code-samples.md §4.5.2](./04-code-samples.md) — startRound instrumentation example
|
||||
- [01-architecture-analysis.md §1.4](./01-architecture-analysis.md) — Consensus round flow
|
||||
|
||||
---
|
||||
|
||||
## Task 4.2: Instrument Phase Transitions
|
||||
|
||||
**Objective**: Create child spans for each consensus phase (open, establish, accept) to show timing breakdown.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/app/consensus/RCLConsensus.cpp`:
|
||||
- Identify where phase transitions occur (the `Consensus<Adaptor>` template drives this)
|
||||
- For each phase entry:
|
||||
- Create span as child of `currentRoundContext_`: `consensus.phase.open`, `consensus.phase.establish`, `consensus.phase.accept`
|
||||
- Set `xrpl.consensus.phase` attribute
|
||||
- Add `phase.enter` event at start, `phase.exit` event at end
|
||||
- Record phase duration in milliseconds
|
||||
|
||||
- In the `onClose` adaptor method:
|
||||
- Create `consensus.ledger_close` span
|
||||
- Set attributes: close_time, mode, transaction count in initial position
|
||||
|
||||
- Note: The Consensus template class in `include/xrpl/consensus/Consensus.h` drives phase transitions — check if instrumentation goes there or in the Adaptor
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
- Possibly `include/xrpl/consensus/Consensus.h` (for template-level phase tracking)
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [04-code-samples.md §4.5.2](./04-code-samples.md) — phaseTransition instrumentation
|
||||
|
||||
---
|
||||
|
||||
## Task 4.3: Instrument Proposal Handling
|
||||
|
||||
**Objective**: Trace proposal send and receive to show validator coordination.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/app/consensus/RCLConsensus.cpp`:
|
||||
- In `Adaptor::propose()`:
|
||||
- Create `consensus.proposal.send` span
|
||||
- Set attributes: `xrpl.consensus.round` (proposal sequence), proposal hash
|
||||
- Inject trace context into outgoing `TMProposeSet::trace_context` (from Phase 3 protobuf)
|
||||
|
||||
- In `Adaptor::peerProposal()` (or wherever peer proposals are received):
|
||||
- Extract trace context from incoming `TMProposeSet::trace_context`
|
||||
- Create `consensus.proposal.receive` span as child of extracted context
|
||||
- Set attributes: `xrpl.consensus.proposer` (node ID), `xrpl.consensus.round`
|
||||
|
||||
- In `Adaptor::share(RCLCxPeerPos)`:
|
||||
- Create `consensus.proposal.relay` span for relaying peer proposals
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [04-code-samples.md §4.5.2](./04-code-samples.md) — peerProposal instrumentation
|
||||
- [02-design-decisions.md §2.4.4](./02-design-decisions.md) — Consensus attribute schema
|
||||
|
||||
---
|
||||
|
||||
## Task 4.4: Instrument Validation Handling
|
||||
|
||||
**Objective**: Trace validation send and receive to show ledger validation flow.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/app/consensus/RCLConsensus.cpp` (or the validation handler):
|
||||
- When sending our validation:
|
||||
- Create `consensus.validation.send` span
|
||||
- Set attributes: validated ledger hash, sequence, signing time
|
||||
|
||||
- When receiving a peer validation:
|
||||
- Extract trace context from `TMValidation::trace_context` (if present)
|
||||
- Create `consensus.validation.receive` span
|
||||
- Set attributes: `xrpl.consensus.validator` (node ID), ledger hash
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
- `src/xrpld/app/misc/NetworkOPs.cpp` (if validation handling is here)
|
||||
|
||||
---
|
||||
|
||||
## Task 4.5: Add Consensus-Specific Attributes
|
||||
|
||||
**Objective**: Enrich consensus spans with detailed attributes for debugging and analysis.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Review all consensus spans and ensure they include:
|
||||
- `xrpl.consensus.ledger.seq` — target ledger sequence number
|
||||
- `xrpl.consensus.round` — consensus round number
|
||||
- `xrpl.consensus.mode` — proposing/observing/wrongLedger
|
||||
- `xrpl.consensus.phase` — current phase name
|
||||
- `xrpl.consensus.phase_duration_ms` — time spent in phase
|
||||
- `xrpl.consensus.proposers` — number of trusted proposers
|
||||
- `xrpl.consensus.tx_count` — transactions in proposed set
|
||||
- `xrpl.consensus.disputes` — number of disputed transactions
|
||||
- `xrpl.consensus.converge_percent` — convergence percentage
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Task 4.6: Correlate Transaction and Consensus Traces
|
||||
|
||||
**Objective**: Link transaction traces from Phase 3 with consensus traces so you can follow a transaction from submission through consensus into the ledger.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- In `onClose()` or `onAccept()`:
|
||||
- When building the consensus position, link the round span to individual transaction spans using span links (if OTel SDK supports it) or events
|
||||
- At minimum, record the transaction hashes included in the consensus set as span events: `tx.included` with `xrpl.tx.hash` attribute
|
||||
|
||||
- In `processTransactionSet()` (NetworkOPs):
|
||||
- If the consensus round span context is available, create child spans for each transaction applied to the ledger
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
- `src/xrpld/app/misc/NetworkOPs.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Task 4.7: Build Verification and Testing
|
||||
|
||||
**Objective**: Verify all Phase 4 changes compile and don't affect consensus timing.
|
||||
|
||||
**What to do**:
|
||||
|
||||
1. Build with `telemetry=ON` — verify no compilation errors
|
||||
2. Build with `telemetry=OFF` — verify no regressions (critical for consensus code)
|
||||
3. Run existing consensus-related unit tests
|
||||
4. Verify that all macros expand to no-ops when disabled
|
||||
5. Check that no consensus-critical code paths are affected by instrumentation overhead
|
||||
|
||||
**Verification Checklist**:
|
||||
|
||||
- [ ] Build succeeds with telemetry ON
|
||||
- [ ] Build succeeds with telemetry OFF
|
||||
- [ ] Existing consensus tests pass
|
||||
- [ ] No new includes in consensus headers when telemetry is OFF
|
||||
- [ ] Phase timing instrumentation doesn't use blocking operations
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Description | New Files | Modified Files | Depends On |
|
||||
| ---- | ------------------------------------- | --------- | -------------- | ------------- |
|
||||
| 4.1 | Consensus round start instrumentation | 0 | 2 | Phase 3 |
|
||||
| 4.2 | Phase transition instrumentation | 0 | 1-2 | 4.1 |
|
||||
| 4.3 | Proposal handling instrumentation | 0 | 1 | 4.1 |
|
||||
| 4.4 | Validation handling instrumentation | 0 | 1-2 | 4.1 |
|
||||
| 4.5 | Consensus-specific attributes | 0 | 1 | 4.2, 4.3, 4.4 |
|
||||
| 4.6 | Transaction-consensus correlation | 0 | 2 | 4.2, Phase 3 |
|
||||
| 4.7 | Build verification and testing | 0 | 0 | 4.1-4.6 |
|
||||
|
||||
**Parallel work**: Tasks 4.2, 4.3, and 4.4 can run in parallel after 4.1 is complete. Task 4.5 depends on all three. Task 4.6 depends on 4.2 and Phase 3.
|
||||
|
||||
**Exit Criteria** (from [06-implementation-phases.md §6.11.4](./06-implementation-phases.md)):
|
||||
|
||||
- [ ] Complete consensus round traces
|
||||
- [ ] Phase transitions visible
|
||||
- [ ] Proposals and validations traced
|
||||
- [ ] No impact on consensus timing
|
||||
241
OpenTelemetryPlan/Phase5_taskList.md
Normal file
241
OpenTelemetryPlan/Phase5_taskList.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Phase 5: Documentation & Deployment Task List
|
||||
|
||||
> **Goal**: Production readiness — Grafana dashboards, spanmetrics pipeline, operator runbook, alert definitions, and final integration testing. This phase ensures the telemetry system is useful and maintainable in production.
|
||||
>
|
||||
> **Scope**: Grafana dashboard definitions, OTel Collector spanmetrics connector, Prometheus integration, alert rules, operator documentation, and production-ready Docker Compose stack.
|
||||
>
|
||||
> **Branch**: `pratik/otel-phase5-docs-deployment` (from `pratik/otel-phase4-consensus-tracing`)
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| ---------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| [07-observability-backends.md](./07-observability-backends.md) | Tempo setup (§7.1), Grafana dashboards (§7.6), alerts (§7.6.3) |
|
||||
| [05-configuration-reference.md](./05-configuration-reference.md) | Collector config (§5.5), production config (§5.5.2), Docker Compose (§5.6) |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 5 tasks (§6.6), definition of done (§6.11.5) |
|
||||
|
||||
---
|
||||
|
||||
## Task 5.1: Add Spanmetrics Connector to OTel Collector
|
||||
|
||||
**Objective**: Derive RED metrics (Rate, Errors, Duration) from trace spans automatically, enabling Grafana time-series dashboards.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `docker/telemetry/otel-collector-config.yaml`:
|
||||
- Add `spanmetrics` connector:
|
||||
```yaml
|
||||
connectors:
|
||||
spanmetrics:
|
||||
histogram:
|
||||
explicit:
|
||||
buckets: [1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 5s]
|
||||
dimensions:
|
||||
- name: xrpl.rpc.command
|
||||
- name: xrpl.rpc.status
|
||||
- name: xrpl.consensus.phase
|
||||
- name: xrpl.tx.type
|
||||
```
|
||||
- Add `prometheus` exporter:
|
||||
```yaml
|
||||
exporters:
|
||||
prometheus:
|
||||
endpoint: 0.0.0.0:8889
|
||||
```
|
||||
- Wire the pipeline:
|
||||
```yaml
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [debug, otlp/tempo, spanmetrics]
|
||||
metrics:
|
||||
receivers: [spanmetrics]
|
||||
exporters: [prometheus]
|
||||
```
|
||||
|
||||
- Edit `docker/telemetry/docker-compose.yml`:
|
||||
- Expose port `8889` on the collector for Prometheus scraping
|
||||
- Add Prometheus service
|
||||
- Add Prometheus as Grafana datasource
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docker/telemetry/otel-collector-config.yaml`
|
||||
- `docker/telemetry/docker-compose.yml`
|
||||
|
||||
**Key new files**:
|
||||
|
||||
- `docker/telemetry/prometheus.yml` (Prometheus scrape config)
|
||||
- `docker/telemetry/grafana/provisioning/datasources/prometheus.yaml`
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [POC_taskList.md §Next Steps](./POC_taskList.md) — Metrics pipeline for Grafana dashboards
|
||||
|
||||
---
|
||||
|
||||
## Task 5.2: Create Grafana Dashboards
|
||||
|
||||
**Objective**: Provide pre-built Grafana dashboards for RPC performance, transaction lifecycle, and consensus health.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `docker/telemetry/grafana/provisioning/dashboards/dashboards.yaml` (provisioning config)
|
||||
- Create dashboard JSON files:
|
||||
1. **RPC Performance Dashboard** (`rpc-performance.json`):
|
||||
- RPC request latency (p50/p95/p99) by command — histogram panel
|
||||
- RPC throughput (requests/sec) by command — time series
|
||||
- RPC error rate by command — bar gauge
|
||||
- Top slowest RPC commands — table
|
||||
|
||||
2. **Transaction Overview Dashboard** (`transaction-overview.json`):
|
||||
- Transaction processing rate — time series
|
||||
- Transaction latency distribution — histogram
|
||||
- Suppression rate (duplicates) — stat panel
|
||||
- Transaction processing path (sync vs async) — pie chart
|
||||
|
||||
3. **Consensus Health Dashboard** (`consensus-health.json`):
|
||||
- Consensus round duration — time series
|
||||
- Phase duration breakdown (open/establish/accept) — stacked bar
|
||||
- Proposals sent/received per round — stat panel
|
||||
- Consensus mode distribution (proposing/observing) — pie chart
|
||||
|
||||
- Store dashboards in `docker/telemetry/grafana/dashboards/`
|
||||
|
||||
**Key new files**:
|
||||
|
||||
- `docker/telemetry/grafana/provisioning/dashboards/dashboards.yaml`
|
||||
- `docker/telemetry/grafana/dashboards/rpc-performance.json`
|
||||
- `docker/telemetry/grafana/dashboards/transaction-overview.json`
|
||||
- `docker/telemetry/grafana/dashboards/consensus-health.json`
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [07-observability-backends.md §7.6](./07-observability-backends.md) — Grafana dashboard specifications
|
||||
- [01-architecture-analysis.md §1.8.3](./01-architecture-analysis.md) — Dashboard panel examples
|
||||
|
||||
---
|
||||
|
||||
## Task 5.3: Define Alert Rules
|
||||
|
||||
**Objective**: Create alert definitions for key telemetry anomalies.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `docker/telemetry/grafana/provisioning/alerting/alerts.yaml`:
|
||||
- **RPC Latency Alert**: p99 latency > 1s for any command over 5 minutes
|
||||
- **RPC Error Rate Alert**: Error rate > 5% for any command over 5 minutes
|
||||
- **Consensus Duration Alert**: Round duration > 10s (warn), > 30s (critical)
|
||||
- **Transaction Processing Alert**: Processing rate drops below threshold
|
||||
- **Telemetry Pipeline Health**: No spans received for > 2 minutes
|
||||
|
||||
**Key new files**:
|
||||
|
||||
- `docker/telemetry/grafana/provisioning/alerting/alerts.yaml`
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [07-observability-backends.md §7.6.3](./07-observability-backends.md) — Alert rule definitions
|
||||
|
||||
---
|
||||
|
||||
## Task 5.4: Production Collector Configuration
|
||||
|
||||
**Objective**: Create a production-ready OTel Collector configuration with tail-based sampling and resource limits.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `docker/telemetry/otel-collector-config-production.yaml`:
|
||||
- Tail-based sampling policy:
|
||||
- Always sample errors and slow traces
|
||||
- 10% base sampling rate for normal traces
|
||||
- Always sample first trace for each unique RPC command
|
||||
- Resource limits:
|
||||
- Memory limiter processor (80% of available memory)
|
||||
- Queued retry for export failures
|
||||
- TLS configuration for production endpoints
|
||||
- Health check endpoint
|
||||
|
||||
**Key new files**:
|
||||
|
||||
- `docker/telemetry/otel-collector-config-production.yaml`
|
||||
|
||||
**Reference**:
|
||||
|
||||
- [05-configuration-reference.md §5.5.2](./05-configuration-reference.md) — Production collector config
|
||||
|
||||
---
|
||||
|
||||
## Task 5.5: Operator Runbook
|
||||
|
||||
**Objective**: Create operator documentation for managing the telemetry system in production.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `docs/telemetry-runbook.md`:
|
||||
- **Setup**: How to enable telemetry in xrpld
|
||||
- **Configuration**: All config options with descriptions
|
||||
- **Collector Deployment**: Docker Compose vs. Kubernetes vs. bare metal
|
||||
- **Troubleshooting**: Common issues and resolutions
|
||||
- No traces appearing
|
||||
- High memory usage from telemetry
|
||||
- Collector connection failures
|
||||
- Sampling configuration tuning
|
||||
- **Performance Tuning**: Batch size, queue size, sampling ratio guidelines
|
||||
- **Upgrading**: How to upgrade OTel SDK and Collector versions
|
||||
|
||||
**Key new files**:
|
||||
|
||||
- `docs/telemetry-runbook.md`
|
||||
|
||||
---
|
||||
|
||||
## Task 5.6: Final Integration Testing
|
||||
|
||||
**Objective**: Validate the complete telemetry stack end-to-end.
|
||||
|
||||
**What to do**:
|
||||
|
||||
1. Start full Docker stack (Collector, Tempo, Grafana, Prometheus)
|
||||
2. Build xrpld with `telemetry=ON`
|
||||
3. Run in standalone mode with telemetry enabled
|
||||
4. Generate RPC traffic and verify traces in Tempo
|
||||
5. Verify dashboards populate in Grafana
|
||||
6. Verify alerts trigger correctly
|
||||
7. Test telemetry OFF path (no regressions)
|
||||
8. Run full test suite
|
||||
|
||||
**Verification Checklist**:
|
||||
|
||||
- [ ] Docker stack starts without errors
|
||||
- [ ] Traces appear in Tempo with correct hierarchy
|
||||
- [ ] Grafana dashboards show metrics derived from spans
|
||||
- [ ] Prometheus scrapes spanmetrics successfully
|
||||
- [ ] Alerts can be triggered by simulated conditions
|
||||
- [ ] Build succeeds with telemetry ON and OFF
|
||||
- [ ] Full test suite passes
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Description | New Files | Modified Files | Depends On |
|
||||
| ---- | ---------------------------------- | --------- | -------------- | ---------- |
|
||||
| 5.1 | Spanmetrics connector + Prometheus | 2 | 2 | Phase 4 |
|
||||
| 5.2 | Grafana dashboards | 4 | 0 | 5.1 |
|
||||
| 5.3 | Alert definitions | 1 | 0 | 5.1 |
|
||||
| 5.4 | Production collector config | 1 | 0 | Phase 4 |
|
||||
| 5.5 | Operator runbook | 1 | 0 | Phase 4 |
|
||||
| 5.6 | Final integration testing | 0 | 0 | 5.1-5.5 |
|
||||
|
||||
**Parallel work**: Tasks 5.1, 5.4, and 5.5 can run in parallel. Tasks 5.2 and 5.3 depend on 5.1. Task 5.6 depends on all others.
|
||||
|
||||
**Exit Criteria** (from [06-implementation-phases.md §6.11.5](./06-implementation-phases.md)):
|
||||
|
||||
- [ ] Dashboards deployed and showing data
|
||||
- [ ] Alerts configured and tested
|
||||
- [ ] Operator documentation complete
|
||||
- [ ] Production collector config ready
|
||||
- [ ] Full test suite passes
|
||||
673
OpenTelemetryPlan/presentation.md
Normal file
673
OpenTelemetryPlan/presentation.md
Normal file
@@ -0,0 +1,673 @@
|
||||
# OpenTelemetry Distributed Tracing for xrpld
|
||||
|
||||
---
|
||||
|
||||
## Slide 1: Introduction
|
||||
|
||||
> **CNCF** = Cloud Native Computing Foundation
|
||||
|
||||
### What is OpenTelemetry?
|
||||
|
||||
OpenTelemetry is an open-source, CNCF-backed observability framework for distributed tracing, metrics, and logs.
|
||||
|
||||
### Why OpenTelemetry for xrpld?
|
||||
|
||||
- **End-to-End Transaction Visibility**: Track transactions from submission → consensus → ledger inclusion
|
||||
- **Cross-Node Correlation**: Follow requests across multiple independent nodes using a unique `trace_id`
|
||||
- **Consensus Round Analysis**: Understand timing and behavior across validators
|
||||
- **Incident Debugging**: Correlate events across distributed nodes during issues
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Node A<br/>tx.receive<br/>trace_id: abc123"] --> B["Node B<br/>tx.relay<br/>trace_id: abc123"] --> C["Node C<br/>tx.validate<br/>trace_id: abc123"] --> D["Node D<br/>ledger.apply<br/>trace_id: abc123"]
|
||||
|
||||
style A fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style B fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style C fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style D fill:#e65100,stroke:#bf360c,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Node A (blue, leftmost)**: The originating node that first receives the transaction and assigns a new `trace_id: abc123`; this ID becomes the correlation key for the entire distributed trace.
|
||||
- **Node B and Node C (green, middle)**: Relay and validation nodes — each creates its own span but carries the same `trace_id`, so their work is linked to the original submission without any central coordinator.
|
||||
- **Node D (orange, rightmost)**: The final node that applies the transaction to the ledger; the trace now spans the full lifecycle from submission to ledger inclusion.
|
||||
- **Left-to-right flow**: The horizontal progression shows the real-world message path — a transaction hops from node to node, and the shared `trace_id` stitches all hops into a single queryable trace.
|
||||
|
||||
> **Trace ID: abc123** — All nodes share the same trace, enabling cross-node correlation.
|
||||
|
||||
---
|
||||
|
||||
## Slide 2: OpenTelemetry vs Open Source Alternatives
|
||||
|
||||
> **CNCF** = Cloud Native Computing Foundation
|
||||
|
||||
| Feature | OpenTelemetry | Jaeger | Zipkin | SkyWalking | Pinpoint | Prometheus |
|
||||
| ------------------- | ---------------- | ---------------- | ------------------ | ---------- | ---------- | ---------- |
|
||||
| **Tracing** | YES | YES | YES | YES | YES | NO |
|
||||
| **Metrics** | YES | NO | NO | YES | YES | YES |
|
||||
| **Logs** | YES | NO | NO | YES | NO | NO |
|
||||
| **C++ SDK** | YES Official | YES (Deprecated) | YES (Unmaintained) | NO | NO | YES |
|
||||
| **Vendor Neutral** | YES Primary goal | NO | NO | NO | NO | NO |
|
||||
| **Instrumentation** | Manual + Auto | Manual | Manual | Auto-first | Auto-first | Manual |
|
||||
| **Backend** | Any (exporters) | Self | Self | Self | Self | Self |
|
||||
| **CNCF Status** | Incubating | Graduated | NO | Incubating | NO | Graduated |
|
||||
|
||||
> **Why OpenTelemetry?** It's the only actively maintained, full-featured C++ option with vendor neutrality — allowing export to Tempo, Prometheus, Grafana, or any commercial backend without changing instrumentation.
|
||||
|
||||
---
|
||||
|
||||
## Slide 3: Adoption Scope — Traces Only (Current Plan)
|
||||
|
||||
OpenTelemetry supports three signal types: **Traces**, **Metrics**, and **Logs**. xrpld already captures metrics (StatsD via Beast Insight) and logs (Journal/PerfLog). The question is: how much of OTel do we adopt?
|
||||
|
||||
> **Scenario A**: Add distributed tracing. Keep StatsD for metrics and Journal for logs.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph xrpld["xrpld Process"]
|
||||
direction TB
|
||||
OTel["OTel SDK<br/>(Traces)"]
|
||||
Insight["Beast Insight<br/>(StatsD Metrics)"]
|
||||
Journal["Journal + PerfLog<br/>(Logging)"]
|
||||
end
|
||||
|
||||
OTel -->|"OTLP"| Collector["OTel Collector"]
|
||||
Insight -->|"UDP"| StatsD["StatsD Server"]
|
||||
Journal -->|"File I/O"| LogFile["perf.log / debug.log"]
|
||||
|
||||
Collector --> Tempo["Tempo"]
|
||||
StatsD --> Graphite["Graphite / Grafana"]
|
||||
LogFile --> Loki["Loki (optional)"]
|
||||
|
||||
style xrpld fill:#424242,stroke:#212121,color:#fff
|
||||
style OTel fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style Insight fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style Journal fill:#e65100,stroke:#bf360c,color:#fff
|
||||
style Collector fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
```
|
||||
|
||||
| Aspect | Details |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **What changes for operators** | Deploy OTel Collector + trace backend. Existing StatsD and log pipelines stay as-is. |
|
||||
| **Codebase impact** | New `Telemetry` module (~1500 LOC). Beast Insight and Journal untouched. |
|
||||
| **New capabilities** | Cross-node trace correlation, span-based debugging, request lifecycle visibility. |
|
||||
| **What we still can't do** | Correlate metrics with specific traces natively. StatsD metrics remain fire-and-forget with no trace exemplars. |
|
||||
| **Maintenance burden** | Three separate observability systems to maintain (OTel + StatsD + Journal). |
|
||||
| **Risk** | Lowest — additive change, no existing systems disturbed. |
|
||||
|
||||
---
|
||||
|
||||
## Slide 4: Future Adoption — Metrics & Logs via OTel
|
||||
|
||||
### Scenario B: + OTel Metrics (Replace StatsD)
|
||||
|
||||
> Migrate StatsD to OTel Metrics API, exposing Prometheus-compatible metrics. Remove Beast Insight.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph xrpld["xrpld Process"]
|
||||
direction TB
|
||||
OTel["OTel SDK<br/>(Traces + Metrics)"]
|
||||
Journal["Journal + PerfLog<br/>(Logging)"]
|
||||
end
|
||||
|
||||
OTel -->|"OTLP"| Collector["OTel Collector"]
|
||||
Journal -->|"File I/O"| LogFile["perf.log / debug.log"]
|
||||
|
||||
Collector --> Tempo["Tempo<br/>(Traces)"]
|
||||
Collector --> Prom["Prometheus<br/>(Metrics)"]
|
||||
LogFile --> Loki["Loki (optional)"]
|
||||
|
||||
style xrpld fill:#424242,stroke:#212121,color:#fff
|
||||
style OTel fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style Journal fill:#e65100,stroke:#bf360c,color:#fff
|
||||
style Collector fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
```
|
||||
|
||||
- **Better metrics?** Yes — Prometheus gives native histograms (p50/p95/p99), multi-dimensional labels, and exemplars linking metric spikes to traces.
|
||||
- **Codebase**: Remove `Beast::Insight` + `StatsDCollector` (~2000 LOC). Single SDK for traces and metrics.
|
||||
- **Operator effort**: Rewrite dashboards from StatsD/Graphite queries to PromQL. Run both in parallel during transition.
|
||||
- **Risk**: Medium — operators must migrate monitoring infrastructure.
|
||||
|
||||
### Scenario C: + OTel Logs (Full Stack)
|
||||
|
||||
> Also replace Journal logging with OTel Logs API. Single SDK for everything.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph xrpld["xrpld Process"]
|
||||
OTel["OTel SDK<br/>(Traces + Metrics + Logs)"]
|
||||
end
|
||||
|
||||
OTel -->|"OTLP"| Collector["OTel Collector"]
|
||||
|
||||
Collector --> Tempo["Tempo<br/>(Traces)"]
|
||||
Collector --> Prom["Prometheus<br/>(Metrics)"]
|
||||
Collector --> Loki["Loki / Elastic<br/>(Logs)"]
|
||||
|
||||
style xrpld fill:#424242,stroke:#212121,color:#fff
|
||||
style OTel fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style Collector fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
```
|
||||
|
||||
- **Structured logging**: OTel Logs API outputs structured records with `trace_id`, `span_id`, severity, and attributes by design.
|
||||
- **Full correlation**: Every log line carries `trace_id`. Click trace → see logs. Click metric spike → see trace → see logs.
|
||||
- **Codebase**: Remove Beast Insight (~2000 LOC) + simplify Journal/PerfLog (~3000 LOC). One dependency instead of three.
|
||||
- **Risk**: Highest — `beast::Journal` is deeply embedded in every component. Large refactor. OTel C++ Logs API is newer (stable since v1.11, less battle-tested).
|
||||
|
||||
### Recommendation
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Phase 1<br/><b>Traces Only</b><br/>(Current Plan)"] --> B["Phase 2<br/><b>+ Metrics</b><br/>(Replace StatsD)"] --> C["Phase 3<br/><b>+ Logs</b><br/>(Full OTel)"]
|
||||
|
||||
style A fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style B fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style C fill:#e65100,stroke:#bf360c,color:#fff
|
||||
```
|
||||
|
||||
| Phase | Signal | Strategy | Risk |
|
||||
| -------------------- | --------- | -------------------------------------------------------------- | ------ |
|
||||
| **Phase 1** (now) | Traces | Add OTel traces. Keep StatsD and Journal. Prove value. | Low |
|
||||
| **Phase 2** (future) | + Metrics | Migrate StatsD → Prometheus via OTel. Remove Beast Insight. | Medium |
|
||||
| **Phase 3** (future) | + Logs | Adopt OTel Logs API. Align with structured logging initiative. | High |
|
||||
|
||||
> **Key Takeaway**: Start with traces (unique value, lowest risk), then incrementally adopt metrics and logs as the OTel infrastructure proves itself.
|
||||
|
||||
---
|
||||
|
||||
## Slide 5: Comparison with xrpld's Existing Solutions
|
||||
|
||||
### Current Observability Stack
|
||||
|
||||
| Aspect | PerfLog (JSON) | StatsD (Metrics) | OpenTelemetry (NEW) |
|
||||
| --------------------- | --------------------- | --------------------- | --------------------------- |
|
||||
| **Type** | Logging | Metrics | Distributed Tracing |
|
||||
| **Scope** | Single node | Single node | **Cross-node** |
|
||||
| **Data** | JSON log entries | Counters, gauges | Spans with context |
|
||||
| **Correlation** | By timestamp | By metric name | By `trace_id` |
|
||||
| **Overhead** | Low (file I/O) | Low (UDP) | Low-Medium (configurable) |
|
||||
| **Question Answered** | "What happened here?" | "How many? How fast?" | **"What was the journey?"** |
|
||||
|
||||
### Use Case Matrix
|
||||
|
||||
| Scenario | PerfLog | StatsD | OpenTelemetry |
|
||||
| -------------------------------- | ------- | ------ | ------------- |
|
||||
| "How many TXs per second?" | ❌ | ✅ | ❌ |
|
||||
| "Why was this specific TX slow?" | ⚠️ | ❌ | ✅ |
|
||||
| "Which node delayed consensus?" | ❌ | ❌ | ✅ |
|
||||
| "Show TX journey across 5 nodes" | ❌ | ❌ | ✅ |
|
||||
|
||||
> **Key Insight**: In the **traces-only** approach (Phase 1), OpenTelemetry **complements** existing systems. In future phases, OTel metrics and logs could **replace** StatsD and Journal respectively — see Slides 3-4 for the full adoption roadmap.
|
||||
|
||||
---
|
||||
|
||||
## Slide 6: Architecture
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **WS** = WebSocket
|
||||
|
||||
### High-Level Integration Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Node"]
|
||||
subgraph services["Core Services"]
|
||||
direction LR
|
||||
RPC["RPC Server<br/>(HTTP/WS)"] ~~~ Overlay["Overlay<br/>(P2P Network)"] ~~~ Consensus["Consensus<br/>(RCLConsensus)"]
|
||||
end
|
||||
|
||||
Telemetry["Telemetry Module<br/>(OpenTelemetry SDK)"]
|
||||
|
||||
services --> Telemetry
|
||||
end
|
||||
|
||||
Telemetry -->|OTLP/gRPC| Collector["OTel Collector"]
|
||||
|
||||
Collector --> Tempo["Grafana Tempo"]
|
||||
Collector --> Elastic["Elastic APM"]
|
||||
|
||||
style xrpld fill:#424242,stroke:#212121,color:#fff
|
||||
style services fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style Telemetry fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style Collector fill:#e65100,stroke:#bf360c,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Core Services (blue, top)**: RPC Server, Overlay, and Consensus are the three primary components that generate trace data — they represent the entry points for client requests, peer messages, and consensus rounds respectively.
|
||||
- **Telemetry Module (green, middle)**: The OpenTelemetry SDK sits below the core services and receives span data from all three; it acts as a single collection point within the xrpld process.
|
||||
- **OTel Collector (orange, center)**: An external process that receives spans over OTLP/gRPC from the Telemetry Module; it decouples xrpld from backend choices and handles batching, sampling, and routing.
|
||||
- **Backends (bottom row)**: Tempo and Elastic APM are interchangeable — the Collector fans out to any combination, so operators can switch backends without modifying xrpld code.
|
||||
- **Top-to-bottom flow**: Data flows from instrumented code down through the SDK, out over the network to the Collector, and finally into storage/visualization backends.
|
||||
|
||||
### Context Propagation
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant NodeA as Node A
|
||||
participant NodeB as Node B
|
||||
|
||||
Client->>NodeA: Submit TX (no context)
|
||||
Note over NodeA: Creates trace_id: abc123<br/>span: tx.receive
|
||||
NodeA->>NodeB: Relay TX<br/>(traceparent: abc123)
|
||||
Note over NodeB: Links to trace_id: abc123<br/>span: tx.relay
|
||||
```
|
||||
|
||||
- **HTTP/RPC**: W3C Trace Context headers (`traceparent`)
|
||||
- **P2P Messages**: Protocol Buffer extension fields
|
||||
|
||||
---
|
||||
|
||||
## Slide 7: Implementation Plan
|
||||
|
||||
### 5-Phase Rollout (9 Weeks)
|
||||
|
||||
> **Note**: Dates shown are relative to project start, not calendar dates.
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Implementation Timeline
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat Week %W
|
||||
|
||||
section Phase 1
|
||||
Core Infrastructure :p1, 2024-01-01, 2w
|
||||
|
||||
section Phase 2
|
||||
RPC Tracing :p2, after p1, 2w
|
||||
|
||||
section Phase 3
|
||||
Transaction Tracing :p3, after p2, 2w
|
||||
|
||||
section Phase 4
|
||||
Consensus Tracing :p4, after p3, 2w
|
||||
|
||||
section Phase 5
|
||||
Documentation :p5, after p4, 1w
|
||||
```
|
||||
|
||||
### Phase Details
|
||||
|
||||
| Phase | Focus | Key Deliverables | Effort |
|
||||
| ----- | ------------------- | -------------------------------------------- | ------- |
|
||||
| 1 | Core Infrastructure | SDK integration, Telemetry interface, Config | 10 days |
|
||||
| 2 | RPC Tracing | HTTP context extraction, Handler spans | 10 days |
|
||||
| 3 | Transaction Tracing | Protobuf context, P2P relay propagation | 10 days |
|
||||
| 4 | Consensus Tracing | Round spans, Proposal/validation tracing | 10 days |
|
||||
| 5 | Documentation | Runbook, Dashboards, Training | 7 days |
|
||||
|
||||
**Total Effort**: ~47 developer-days (2 developers)
|
||||
|
||||
> **Future Phases** (not in current scope): After traces are stable, OTel metrics can replace StatsD (~3 weeks), and OTel logs can replace Journal (~4 weeks, aligned with structured logging initiative). See Slides 3-4 for the full adoption roadmap.
|
||||
|
||||
---
|
||||
|
||||
## Slide 8: Performance Overhead
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### Estimated System Impact
|
||||
|
||||
| Metric | Overhead | Notes |
|
||||
| ----------------- | ---------- | ------------------------------------------------ |
|
||||
| **CPU** | 1-3% | Span creation and attribute setting |
|
||||
| **Memory** | ~10 MB | SDK statics + batch buffer + worker thread stack |
|
||||
| **Network** | 10-50 KB/s | Compressed OTLP export to collector |
|
||||
| **Latency (p99)** | <2% | With proper sampling configuration |
|
||||
|
||||
#### How We Arrived at These Numbers
|
||||
|
||||
**Assumptions (XRPL mainnet baseline)**:
|
||||
|
||||
| Parameter | Value | Source |
|
||||
| ------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| Transaction throughput | ~25 TPS (peaks to ~50) | Mainnet average |
|
||||
| Default peers per node | 21 | `peerfinder/detail/Tuning.h` (`defaultMaxPeers`) |
|
||||
| Consensus round frequency | ~1 round / 3-4 seconds | `ConsensusParms.h` (`ledgerMIN_CONSENSUS=1950ms`) |
|
||||
| Proposers per round | ~20-35 | Mainnet UNL size |
|
||||
| P2P message rate | ~160 msgs/sec | See message breakdown below |
|
||||
| Avg TX processing time | ~200 μs | Profiled baseline |
|
||||
| Single span creation cost | 500-1000 ns | OTel C++ SDK benchmarks (see [3.5.4](./03-implementation-strategy.md#354-performance-data-sources)) |
|
||||
|
||||
**P2P message breakdown** (per node, mainnet):
|
||||
|
||||
| Message Type | Rate | Derivation |
|
||||
| ------------- | ------------ | --------------------------------------------------------------------- |
|
||||
| TMTransaction | ~100/sec | ~25 TPS × ~4 relay hops per TX, deduplicated by HashRouter |
|
||||
| TMValidation | ~50/sec | ~35 validators × ~1 validation/3s round ≈ ~12/sec, plus relay fan-out |
|
||||
| TMProposeSet | ~10/sec | ~35 proposers / 3s round ≈ ~12/round, clustered in establish phase |
|
||||
| **Total** | **~160/sec** | **Only traced message types counted** |
|
||||
|
||||
**CPU (1-3%) — Calculation**:
|
||||
|
||||
Per-transaction tracing cost breakdown:
|
||||
|
||||
| Operation | Cost | Notes |
|
||||
| ----------------------------------------------- | ----------- | ------------------------------------------ |
|
||||
| `tx.receive` span (create + end + 4 attributes) | ~1400 ns | ~1000ns create + ~200ns end + 4×50ns attrs |
|
||||
| `tx.validate` span | ~1200 ns | ~1000ns create + ~200ns for 2 attributes |
|
||||
| `tx.relay` span | ~1200 ns | ~1000ns create + ~200ns for 2 attributes |
|
||||
| Context injection into P2P message | ~200 ns | Serialize trace_id + span_id into protobuf |
|
||||
| **Total per TX** | **~4.0 μs** | |
|
||||
|
||||
> **CPU overhead**: 4.0 μs / 200 μs baseline = **~2.0% per transaction**. Under high load with consensus + RPC spans overlapping, reaches ~3%. Consensus itself adds only ~36 μs per 3-second round (~0.001%), so the TX path dominates. On production server hardware (3+ GHz Xeon), span creation drops to ~500-600 ns, bringing per-TX cost to ~2.6 μs (~1.3%). See [Section 3.5.4](./03-implementation-strategy.md#354-performance-data-sources) for benchmark sources.
|
||||
|
||||
**Memory (~10 MB) — Calculation**:
|
||||
|
||||
| Component | Size | Notes |
|
||||
| --------------------------------------------- | ------------------ | ------------------------------------- |
|
||||
| TracerProvider + Exporter (gRPC channel init) | ~320 KB | Allocated once at startup |
|
||||
| BatchSpanProcessor (circular buffer) | ~16 KB | 2049 × 8-byte AtomicUniquePtr entries |
|
||||
| BatchSpanProcessor (worker thread stack) | ~8 MB | Default Linux thread stack size |
|
||||
| Active spans (in-flight, max ~1000) | ~500-800 KB | ~500-800 bytes/span × 1000 concurrent |
|
||||
| Export queue (batch buffer, max 2048 spans) | ~1 MB | ~500 bytes/span × 2048 queue depth |
|
||||
| Thread-local context storage (~100 threads) | ~6.4 KB | ~64 bytes/thread |
|
||||
| **Total** | **~10 MB ceiling** | |
|
||||
|
||||
> Memory plateaus once the export queue fills — the `max_queue_size=2048` config bounds growth.
|
||||
> The worker thread stack (~8 MB) dominates the static footprint but is virtual memory; actual RSS
|
||||
> depends on stack usage (typically much less). Active spans are larger than originally estimated
|
||||
> (~500-800 bytes) because the OTel SDK `Span` object includes a mutex (~40 bytes), `SpanData`
|
||||
> recordable (~250 bytes base), and `std::map`-based attribute storage (~200-500 bytes for 3-5
|
||||
> string attributes). See [Section 3.5.4](./03-implementation-strategy.md#354-performance-data-sources) for source references.
|
||||
|
||||
**Network (10-50 KB/s) — Calculation**:
|
||||
|
||||
Two sources of network overhead:
|
||||
|
||||
**(A) OTLP span export to Collector:**
|
||||
|
||||
| Sampling Rate | Effective Spans/sec | Avg Span Size (compressed) | Bandwidth |
|
||||
| -------------------------- | ------------------- | -------------------------- | ------------ |
|
||||
| 100% (dev only) | ~500 | ~500 bytes | ~250 KB/s |
|
||||
| **10% (recommended prod)** | **~50** | **~500 bytes** | **~25 KB/s** |
|
||||
| 1% (minimal) | ~5 | ~500 bytes | ~2.5 KB/s |
|
||||
|
||||
> The ~500 spans/sec at 100% comes from: ~100 TX spans + ~160 P2P context spans + ~23 consensus spans/round + ~50 RPC spans = ~500/sec. OTLP protobuf with gzip compression yields ~500 bytes/span average.
|
||||
|
||||
**(B) P2P trace context overhead** (added to existing messages, always-on regardless of sampling):
|
||||
|
||||
| Message Type | Rate | Context Size | Bandwidth |
|
||||
| ------------- | -------- | ------------ | ------------- |
|
||||
| TMTransaction | ~100/sec | 29 bytes | ~2.9 KB/s |
|
||||
| TMValidation | ~50/sec | 29 bytes | ~1.5 KB/s |
|
||||
| TMProposeSet | ~10/sec | 29 bytes | ~0.3 KB/s |
|
||||
| **Total P2P** | | | **~4.7 KB/s** |
|
||||
|
||||
> **Combined**: 25 KB/s (OTLP export at 10%) + 5 KB/s (P2P context) ≈ **~30 KB/s typical**. The 10-50 KB/s range covers 10-20% sampling under normal to peak mainnet load.
|
||||
|
||||
**Latency (<2%) — Calculation**:
|
||||
|
||||
| Path | Tracing Cost | Baseline | Overhead |
|
||||
| ------------------------------ | ------------ | -------- | -------- |
|
||||
| Fast RPC (e.g., `server_info`) | 2.75 μs | ~1 ms | 0.275% |
|
||||
| Slow RPC (e.g., `path_find`) | 2.75 μs | ~100 ms | 0.003% |
|
||||
| Transaction processing | 4.0 μs | ~200 μs | 2.0% |
|
||||
| Consensus round | 36 μs | ~3 sec | 0.001% |
|
||||
|
||||
> At p99, even the worst case (TX processing at 2.0%) is within the 1-3% range. RPC and consensus overhead are negligible. On production hardware, TX overhead drops to ~1.3%.
|
||||
|
||||
### Per-Message Overhead (Context Propagation)
|
||||
|
||||
Each P2P message carries trace context with the following overhead:
|
||||
|
||||
| Field | Size | Description |
|
||||
| ------------- | ------------- | ----------------------------------------- |
|
||||
| `trace_id` | 16 bytes | Unique identifier for the entire trace |
|
||||
| `span_id` | 8 bytes | Current span (becomes parent on receiver) |
|
||||
| `trace_flags` | 1 byte | Sampling decision flags |
|
||||
| `trace_state` | 0-4 bytes | Optional vendor-specific data |
|
||||
| **Total** | **~29 bytes** | **Added per traced P2P message** |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph msg["P2P Message with Trace Context"]
|
||||
A["Original Message<br/>(variable size)"] --> B["+ TraceContext<br/>(~29 bytes)"]
|
||||
end
|
||||
|
||||
subgraph breakdown["Context Breakdown"]
|
||||
C["trace_id<br/>16 bytes"]
|
||||
D["span_id<br/>8 bytes"]
|
||||
E["flags<br/>1 byte"]
|
||||
F["state<br/>0-4 bytes"]
|
||||
end
|
||||
|
||||
B --> breakdown
|
||||
|
||||
style A fill:#424242,stroke:#212121,color:#fff
|
||||
style B fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style C fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style D fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style E fill:#e65100,stroke:#bf360c,color:#fff
|
||||
style F fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Original Message (gray, left)**: The existing P2P message payload of variable size — this is unchanged; trace context is appended, never modifying the original data.
|
||||
- **+ TraceContext (green, right of message)**: The additional 29-byte context block attached to each traced message; the arrow from the original message shows it is a pure addition.
|
||||
- **Context Breakdown (right subgraph)**: The four fields — `trace_id` (16 bytes), `span_id` (8 bytes), `flags` (1 byte), and `state` (0-4 bytes) — show exactly what is added and their individual sizes.
|
||||
- **Color coding**: Blue fields (`trace_id`, `span_id`) are the core identifiers required for trace correlation; orange (`flags`) controls sampling decisions; purple (`state`) is optional vendor data typically omitted.
|
||||
|
||||
> **Note**: 29 bytes represents ~1-6% overhead depending on message size (500B simple TX to 5KB proposal), which is acceptable for the observability benefits provided.
|
||||
|
||||
### Mitigation Strategies
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Head Sampling<br/>10% default"] --> B["Tail Sampling<br/>Keep errors/slow"] --> C["Batch Export<br/>Reduce I/O"] --> D["Conditional Compile<br/>XRPL_ENABLE_TELEMETRY"]
|
||||
|
||||
style A fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style B fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style C fill:#e65100,stroke:#bf360c,color:#fff
|
||||
style D fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
> For a detailed explanation of head vs. tail sampling, see Slide 9.
|
||||
|
||||
### Kill Switches (Rollback Options)
|
||||
|
||||
1. **Config Disable**: Set `enabled=0` in config → instant disable, no restart needed for sampling
|
||||
2. **Rebuild**: Compile with `XRPL_ENABLE_TELEMETRY=OFF` → zero overhead (no-op)
|
||||
3. **Full Revert**: Clean separation allows easy commit reversion
|
||||
|
||||
---
|
||||
|
||||
## Slide 9: Sampling Strategies — Head vs. Tail
|
||||
|
||||
> Sampling controls **which traces are recorded and exported**. Without sampling, every operation generates a trace — at 500+ spans/sec, this overwhelms storage and network. Sampling lets you keep the signal, discard the noise.
|
||||
|
||||
### Head Sampling (Decision at Start)
|
||||
|
||||
The sampling decision is made **when a trace begins**, before any work is done. A random number is generated; if it falls within the configured ratio, the entire trace is recorded. Otherwise, the trace is silently dropped.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["New Request<br/>Arrives"] --> B{"Random < 10%?"}
|
||||
B -->|"Yes (1 in 10)"| C["Record Entire Trace<br/>(all spans)"]
|
||||
B -->|"No (9 in 10)"| D["Drop Entire Trace<br/>(zero overhead)"]
|
||||
|
||||
style C fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style D fill:#c62828,stroke:#8c2809,color:#fff
|
||||
style B fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
```
|
||||
|
||||
| Aspect | Details |
|
||||
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Where it runs** | Inside xrpld (SDK-level). Configured via `sampling_ratio` in `xrpld.cfg`. |
|
||||
| **When the decision happens** | At trace creation time — before the first span is even populated. |
|
||||
| **How it works** | `sampling_ratio=0.1` means each trace has a 10% probability of being recorded. Dropped traces incur near-zero overhead (no spans created, no attributes set, no export). |
|
||||
| **Propagation** | Once a trace is sampled, the `trace_flags` field (1 byte in the context header) tells downstream nodes to also sample it. Unsampled traces propagate `trace_flags=0`, so downstream nodes skip them too. |
|
||||
| **Pros** | Lowest overhead. Simple to configure. Predictable resource usage. |
|
||||
| **Cons** | **Blind** — it doesn't know if the trace will be interesting. A rare error or slow consensus round has only a 10% chance of being captured. |
|
||||
| **Best for** | High-volume, steady-state traffic where most traces look similar (e.g., routine RPC requests). |
|
||||
|
||||
**xrpld configuration**:
|
||||
|
||||
```ini
|
||||
[telemetry]
|
||||
# Record 10% of traces (recommended for production)
|
||||
sampling_ratio=0.1
|
||||
```
|
||||
|
||||
### Tail Sampling (Decision at End)
|
||||
|
||||
The sampling decision is made **after the trace completes**, based on its actual content — was it slow? Did it error? Was it a consensus round? This requires buffering complete traces before deciding.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A["All Traces<br/>Buffered (100%)"] --> B["OTel Collector<br/>Evaluates Rules"]
|
||||
|
||||
B --> C{"Error?"}
|
||||
C -->|Yes| K["KEEP"]
|
||||
|
||||
C -->|No| D{"Slow?<br/>(>5s consensus,<br/>>1s RPC)"}
|
||||
D -->|Yes| K
|
||||
|
||||
D -->|No| E{"Random < 10%?"}
|
||||
E -->|Yes| K
|
||||
E -->|No| F["DROP"]
|
||||
|
||||
style K fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
style F fill:#c62828,stroke:#8c2809,color:#fff
|
||||
style B fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style C fill:#e65100,stroke:#bf360c,color:#fff
|
||||
style D fill:#e65100,stroke:#bf360c,color:#fff
|
||||
style E fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
| Aspect | Details |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Where it runs** | In the **OTel Collector** (external process), not inside xrpld. xrpld exports 100% of traces; the Collector decides what to keep. |
|
||||
| **When the decision happens** | After the Collector has received all spans for a trace (waits `decision_wait=10s` for stragglers). |
|
||||
| **How it works** | Policy rules evaluate the completed trace: keep all errors, keep slow operations above a threshold, keep all consensus rounds, then probabilistically sample the rest at 10%. |
|
||||
| **Pros** | **Never misses important traces**. Errors, slow requests, and consensus anomalies are always captured regardless of probability. |
|
||||
| **Cons** | Higher resource usage — xrpld must export 100% of spans to the Collector, which buffers them in memory before deciding. The Collector needs more RAM (configured via `num_traces` and `decision_wait`). |
|
||||
| **Best for** | Production troubleshooting where you can't afford to miss errors or anomalies. |
|
||||
|
||||
**Collector configuration** (tail sampling rules for xrpld):
|
||||
|
||||
```yaml
|
||||
processors:
|
||||
tail_sampling:
|
||||
decision_wait: 10s # Wait for all spans in a trace
|
||||
num_traces: 100000 # Buffer up to 100K concurrent traces
|
||||
policies:
|
||||
- name: errors # Always keep error traces
|
||||
type: status_code
|
||||
status_code: { status_codes: [ERROR] }
|
||||
|
||||
- name: slow-consensus # Keep consensus rounds >5s
|
||||
type: latency
|
||||
latency: { threshold_ms: 5000 }
|
||||
|
||||
- name: slow-rpc # Keep slow RPC requests >1s
|
||||
type: latency
|
||||
latency: { threshold_ms: 1000 }
|
||||
|
||||
- name: probabilistic # Sample 10% of everything else
|
||||
type: probabilistic
|
||||
probabilistic: { sampling_percentage: 10 }
|
||||
```
|
||||
|
||||
### Head vs. Tail — Side-by-Side
|
||||
|
||||
| | Head Sampling | Tail Sampling |
|
||||
| ----------------------------- | ---------------------------------------- | ------------------------------------------------ |
|
||||
| **Decision point** | Trace start (inside xrpld) | Trace end (in OTel Collector) |
|
||||
| **Knows trace content?** | No (random coin flip) | Yes (evaluates completed trace) |
|
||||
| **Overhead on xrpld** | Lowest (dropped traces = no-op) | Higher (must export 100% to Collector) |
|
||||
| **Collector resource usage** | Low (receives only sampled traces) | Higher (buffers all traces before deciding) |
|
||||
| **Captures all errors?** | No (only if trace was randomly selected) | **Yes** (error policy catches them) |
|
||||
| **Captures slow operations?** | No (random) | **Yes** (latency policy catches them) |
|
||||
| **Configuration** | `xrpld.cfg`: `sampling_ratio=0.1` | `otel-collector.yaml`: `tail_sampling` processor |
|
||||
| **Best for** | High-throughput steady-state | Troubleshooting & anomaly detection |
|
||||
|
||||
### Recommended Strategy for xrpld
|
||||
|
||||
Use **both** in a layered approach:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph xrpld["xrpld (Head Sampling)"]
|
||||
HS["sampling_ratio=1.0<br/>(export everything)"]
|
||||
end
|
||||
|
||||
subgraph collector["OTel Collector (Tail Sampling)"]
|
||||
TS["Keep: errors + slow + 10% random<br/>Drop: routine traces"]
|
||||
end
|
||||
|
||||
subgraph storage["Backend Storage"]
|
||||
ST["Only interesting traces<br/>stored long-term"]
|
||||
end
|
||||
|
||||
xrpld -->|"100% of spans"| collector -->|"~15-20% kept"| storage
|
||||
|
||||
style xrpld fill:#424242,stroke:#212121,color:#fff
|
||||
style collector fill:#1565c0,stroke:#0d47a1,color:#fff
|
||||
style storage fill:#2e7d32,stroke:#1b5e20,color:#fff
|
||||
```
|
||||
|
||||
> **Why this works**: xrpld exports everything (no blind drops), the Collector applies intelligent filtering (keep errors/slow/anomalies, sample the rest), and only ~15-20% of traces reach storage. If Collector resource usage becomes a concern, add head sampling at `sampling_ratio=0.5` to halve the export volume while still giving the Collector enough data for good tail-sampling decisions.
|
||||
|
||||
---
|
||||
|
||||
## Slide 10: Data Collection & Privacy
|
||||
|
||||
### 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 |
|
||||
|
||||
### What is NOT Collected (Privacy Guarantees)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph notCollected["❌ NOT Collected"]
|
||||
direction LR
|
||||
A["Private Keys"] ~~~ B["Account Balances"] ~~~ C["Transaction Amounts"]
|
||||
end
|
||||
|
||||
subgraph alsoNot["❌ Also Excluded"]
|
||||
direction LR
|
||||
D["IP Addresses<br/>(configurable)"] ~~~ E["Personal Data"] ~~~ F["Raw TX Payloads"]
|
||||
end
|
||||
|
||||
style A fill:#c62828,stroke:#8c2809,color:#fff
|
||||
style B fill:#c62828,stroke:#8c2809,color:#fff
|
||||
style C fill:#c62828,stroke:#8c2809,color:#fff
|
||||
style D fill:#c62828,stroke:#8c2809,color:#fff
|
||||
style E fill:#c62828,stroke:#8c2809,color:#fff
|
||||
style F fill:#c62828,stroke:#8c2809,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **NOT Collected (top row, red)**: Private Keys, Account Balances, and Transaction Amounts are explicitly excluded — these are financial/security-sensitive fields that telemetry never touches.
|
||||
- **Also Excluded (bottom row, red)**: IP Addresses (configurable per deployment), Personal Data, and Raw TX Payloads are also excluded — these protect operator and user privacy.
|
||||
- **All-red styling**: Every box is styled in red to visually reinforce that these are hard exclusions, not optional — the telemetry system has no code path to collect any of these fields.
|
||||
- **Two-row layout**: The split between "NOT Collected" and "Also Excluded" distinguishes between financial data (top) and operational/personal data (bottom), making the privacy boundaries clear to auditors.
|
||||
|
||||
### 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 |
|
||||
|
||||
> **Key Principle**: Telemetry collects **operational metadata** (timing, counts, hashes) — never **sensitive content** (keys, balances, amounts).
|
||||
|
||||
---
|
||||
|
||||
_End of Presentation_
|
||||
239
OpenTelemetryPlan/secure-OTel.md
Normal file
239
OpenTelemetryPlan/secure-OTel.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Securing OpenTelemetry Against Trace Context Spoofing
|
||||
|
||||
> **Part of**: [OpenTelemetry Implementation Plan](./OpenTelemetryPlan.md) — see also [Design Decisions § Privacy](./02-design-decisions.md#244-privacy--sensitive-data-policy) (what we don't collect) and [Configuration Reference § 5.5](./05-configuration-reference.md#55-opentelemetry-collector-configuration) (collector base config).
|
||||
|
||||
Trace context spoofing (or poisoning) occurs when untrusted actors inject tampered or stale trace IDs into your system. If these requests are processed, the spans are appended to historical trace buckets, stretching trace durations, ruining p99 latency metrics, and breaking Grafana dashboards.
|
||||
|
||||
This guide outlines two categories of defense: mitigating tampered contexts and locking down the OpenTelemetry (OTel) Collector to trusted clients only.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Mitigating Tampered Trace Contexts
|
||||
|
||||
### 1. Perimeter Defense: Strip Headers at the API Gateway
|
||||
|
||||
The most effective way to prevent spoofing from external sources is to treat your API Gateway (Envoy, NGINX, AWS ALB) as a hard boundary. Strip incoming W3C tracing headers (`traceparent`, `tracestate`) from public traffic so the gateway is forced to generate a fresh, legitimate `trace_id`.
|
||||
|
||||
**NGINX Example (Stripping Headers):**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location {
|
||||
# Clear out untrusted incoming trace headers
|
||||
proxy_set_header traceparent "";
|
||||
proxy_set_header tracestate "";
|
||||
|
||||
proxy_pass http://backend_service;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **2. Timestamp-Anchored Trace IDs and OTTL Filtering**
|
||||
|
||||
If you use a custom trace ID generator that embeds a timestamp in the first few bytes (like AWS X-Ray or UUIDv7), you can use the OTel Collector's OpenTelemetry Transform Language (OTTL) to detect anomalies.
|
||||
**Collector Configuration (Conceptual OTTL Filter):**
|
||||
|
||||
```yaml
|
||||
processors:
|
||||
filter/stale_traces:
|
||||
error_mode: ignore
|
||||
traces:
|
||||
span:
|
||||
# Example: Drop spans where the start time is significantly different
|
||||
# from an expected parameter or embedded timestamp logic.
|
||||
# Note: Standard W3C trace IDs do not contain timestamps by default.
|
||||
- 'Keep out-of-bounds spans: time.sub(start_time, now()) > duration("1h")'
|
||||
```
|
||||
|
||||
## **Part 2: Restricting Access to the OTel Collector**
|
||||
|
||||
Locking down the Collector ensures that only authenticated, trusted clients can submit telemetry data.
|
||||
|
||||
### **Approach A: Network Layer Security (Kubernetes Network Policies)**
|
||||
|
||||
Ensure your Collector is not exposed to the public internet. If running in Kubernetes, use a NetworkPolicy to restrict ingress traffic to specific namespaces.
|
||||
**Kubernetes NetworkPolicy Example:**
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-internal-otel
|
||||
namespace: observability
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: opentelemetry-collector
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
environment: production
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 4317 # gRPC
|
||||
- protocol: TCP
|
||||
port: 4318 # HTTP
|
||||
```
|
||||
|
||||
### **Approach B: Transport Layer Security (Mutual TLS / mTLS)**
|
||||
|
||||
Require clients to present a valid cryptographic certificate to connect to the Collector.
|
||||
**Collector Configuration (mTLS):**
|
||||
|
||||
```yaml
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
tls:
|
||||
client_ca_file: /certs/client_ca.pem # CA that signs trusted client certs
|
||||
cert_file: /certs/collector.pem
|
||||
key_file: /certs/collector.key
|
||||
auth_type: require_and_verify_client_cert # Rejects unauthorized clients
|
||||
```
|
||||
|
||||
### **Approach C: Application Layer Authentication (Basic Auth Extension)**
|
||||
|
||||
Use the Collector's extension system to require an API key or Basic Auth credentials.
|
||||
**Collector Configuration (Basic Auth):**
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
basicauth/collector:
|
||||
htpasswd:
|
||||
inline: |
|
||||
# username:trusted-client, password:SecurePassword123
|
||||
trusted-client:$apr1$4v8p76o6$DMTX5Wv6uOmrFAZp2X1N1.
|
||||
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
auth:
|
||||
authenticator: basicauth/collector
|
||||
|
||||
processors:
|
||||
batch:
|
||||
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: my-backend-storage:4317
|
||||
|
||||
service:
|
||||
extensions: [basicauth/collector]
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [otlp]
|
||||
```
|
||||
|
||||
**Client Setup (Environment Variables):**
|
||||
Developers must pass the authentication header using the standard OTel SDK environment variables:
|
||||
|
||||
```bash
|
||||
# Base64 encoded "trusted-client:SecurePassword123"
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic dHJ1c3RlZC1jbGllbnQ6U2VjdXJlUGFzc3dvcmQxMjM="
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Available routes to build on top of: https://github.com/XRPLF/rippled/pull/6425#discussion_r3234751995
|
||||
|
||||
---
|
||||
|
||||
# Analysis: Applying the Guide to xrpld
|
||||
|
||||
The guide above is written for HTTP-fronted web services. xrpld is a P2P node daemon, so the threat model and the applicable defenses differ. This section captures how each approach maps to xrpld and the chosen direction.
|
||||
|
||||
## Threat Model
|
||||
|
||||
xrpld has **two distinct attack surfaces**, not one. The original guide conflates them under "trace context spoofing"; for xrpld they need separate defenses.
|
||||
|
||||
| Surface | Attacker | Vector | Defense |
|
||||
| ----------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
|
||||
| **Collector ingress** (xrpld → collector) | Anyone who can reach `4317`/`4318` on the collector host | Forged OTLP traffic, telemetry exfiltration, DoS on collector | mTLS + network policy |
|
||||
| **Peer trace context** (peer → xrpld) | Malicious peer in the XRPL overlay | Crafted `protocol::TraceContext` field inside peer protobuf messages (TMTransaction, consensus, etc.) — used to forge `trace_id`/`span_id`, pollute p99, attach spans to historical traces | Validate + rate-limit at the receive boundary |
|
||||
|
||||
**Deployment context:** Across-network. xrpld nodes (potentially run by external operators or in different DCs) ship telemetry to a centrally-hosted collector across an untrusted network. The collector is NOT on the same host or private VPC as every node.
|
||||
|
||||
```
|
||||
┌── peer (untrusted) ── TMTransaction{trace_context} ──▶ xrpld
|
||||
│ │
|
||||
│ [validate + rate-limit]
|
||||
│ │
|
||||
│ ▼
|
||||
│ SpanGuard (clean)
|
||||
│ │
|
||||
│ │ OTLP/gRPC
|
||||
│ │ + mTLS
|
||||
│ ▼
|
||||
└───────────────────────────────────────── [require_and_verify_client_cert]
|
||||
OTel Collector
|
||||
(in private subnet, NetPol)
|
||||
```
|
||||
|
||||
## Part 1 Applicability — Peer Trace-Context Validation
|
||||
|
||||
The guide's NGINX header stripping and OTTL stale-span filtering target HTTP gateways and post-hoc cleanup. Neither fits xrpld directly:
|
||||
|
||||
- **NGINX header stripping** — N/A. There is no HTTP gateway between peers and xrpld; trace context arrives inside protobuf peer messages (`protocol::TraceContext`), not as W3C `traceparent` headers. See [src/xrpld/telemetry/PropagationHelpers.h](../src/xrpld/telemetry/PropagationHelpers.h).
|
||||
- **OTTL stale-span filtering** — Weak fit. Post-hoc cleanup at the collector loses peer identity (you can't tell _which_ peer poisoned the trace). Validation at the receive site is stronger.
|
||||
|
||||
**xrpld-specific Part 1 mitigations:**
|
||||
|
||||
1. **Validate extracted context at the boundary** in [src/xrpld/telemetry/ConsensusReceiveTracing.h](../src/xrpld/telemetry/ConsensusReceiveTracing.h) and any other peer-message receive site. Reject if `trace_id` is all-zero, wrong length, or fails W3C format checks. Treat invalid context as "no propagated context" — start a fresh span — rather than dropping the message.
|
||||
2. **Per-peer sample rate limiting** so a hostile peer cannot flood the collector with spans bearing a fabricated `trace_id`. Use probabilistic sampling on the receive path keyed by peer identity.
|
||||
|
||||
## Part 2 — Comparison of Collector Hardening Approaches
|
||||
|
||||
Evaluated for the across-network deployment shape:
|
||||
|
||||
| Approach | Across-network fit | Cost | Verdict |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------- |
|
||||
| **A. NetworkPolicy / firewall** | Necessary baseline (don't expose `4317`/`4318` to the internet), but insufficient on its own when traffic genuinely crosses networks — you cannot NetworkPolicy the public internet. | Cheap. | **Defense-in-depth, not primary.** |
|
||||
| **B. mTLS** | Strongest fit. Every xrpld node holds a client cert; collector verifies with `require_and_verify_client_cert`. Encrypts in transit (raw OTLP over the internet leaks transaction patterns and validator identity). Compromised node = revoke one cert, no shared secret to rotate everywhere. | Cert issuance + rotation pipeline. | **Primary.** |
|
||||
| **C. Basic Auth** | Worst shape for this topology. Single shared password across all xrpld nodes — one leaked node config compromises the whole fleet. Doesn't encrypt; you'd need TLS underneath anyway, at which point you're 80% of the way to mTLS. | Cheap to set up, expensive to operate (rotation across N operators). | **Skip.** |
|
||||
|
||||
## Decision
|
||||
|
||||
**Primary defense:** mTLS (Approach B) on the collector's OTLP receivers, with `auth_type: require_and_verify_client_cert`.
|
||||
|
||||
**Defense-in-depth:** NetworkPolicy / firewall rules (Approach A) so `4317`/`4318` are never reachable from outside the expected operator subnets even if mTLS were misconfigured.
|
||||
|
||||
**Skipped:** Basic Auth (Approach C) — wrong shape for an across-network, multi-operator topology.
|
||||
|
||||
**Plus xrpld-specific Part 1 work:** trace-context validation and per-peer rate limiting at peer-message receive sites.
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
| -------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Cert source for mTLS | **Reuse XRPL node identity key** | One identity per node, no separate PKI to operate. Fits XRPL's existing trust model; requires small CA tooling step to derive/sign the OTel client cert from the node key. |
|
||||
| Part 1 scope | **Include in this spec** | Collector hardening and peer trace-context validation share one threat model. Coherent design doc; can still be split into multiple PRs at implementation. |
|
||||
| Dev impact | **Production-only** | Local `docker/telemetry/docker-compose.yml` keeps `insecure: true` and no auth for fast iteration. Only production deployment manifests gain mTLS. Accepted risk: minor dev/prod drift, mitigated by integration tests against a TLS-enabled collector in CI. |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- NGINX/Envoy header stripping (no HTTP gateway in front of xrpld-to-collector traffic).
|
||||
- OTTL stale-span filtering at the collector (weaker than source validation; loses peer identity).
|
||||
- Local development docker-compose hardening.
|
||||
- Telemetry backend (Tempo) hardening — separate concern, downstream of the collector.
|
||||
|
||||
## Next Step
|
||||
|
||||
Write this up as a design doc with full sections covering:
|
||||
|
||||
1. Threat model & architecture (this section, expanded)
|
||||
2. Collector hardening — mTLS config, NetworkPolicy
|
||||
3. Cert pipeline — deriving OTel client cert from XRPL node key
|
||||
4. Peer trace-context validation — receive-site checks in `ConsensusReceiveTracing.h`
|
||||
5. Per-peer span rate limiting
|
||||
6. Testing & rollout
|
||||
@@ -1621,3 +1621,96 @@ validators.txt
|
||||
# set to ssl_verify to 0.
|
||||
[ssl_verify]
|
||||
1
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 11. Telemetry (OpenTelemetry Tracing)
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Enables distributed tracing via OpenTelemetry. Requires building with
|
||||
# -DXRPL_ENABLE_TELEMETRY=ON (telemetry Conan option).
|
||||
#
|
||||
# [telemetry]
|
||||
#
|
||||
# enabled=0
|
||||
#
|
||||
# Enable or disable telemetry at runtime. Default: 0 (disabled).
|
||||
#
|
||||
# service_name=xrpld
|
||||
#
|
||||
# OTel resource attribute `service.name`. Default: xrpld.
|
||||
# The node's network ID (from [network_id]) is automatically added
|
||||
# as the `xrpl.network.id` and `xrpl.network.type` resource attributes.
|
||||
#
|
||||
# service_instance_id=<node_public_key>
|
||||
#
|
||||
# OTel resource attribute `service.instance.id`. Uniquely identifies
|
||||
# this node. Default: the node's public key (auto-detected).
|
||||
#
|
||||
# endpoint=http://localhost:4318/v1/traces
|
||||
#
|
||||
# The OTLP/HTTP exporter endpoint. The server sends trace data as
|
||||
# protobuf-encoded HTTP POST requests to this URL.
|
||||
# Default: http://localhost:4318/v1/traces.
|
||||
#
|
||||
# --- TLS settings for the OTLP exporter connection ---
|
||||
#
|
||||
# use_tls=0
|
||||
#
|
||||
# Enable TLS for the OTLP/HTTP exporter connection. Default: 0 (off).
|
||||
#
|
||||
# tls_ca_cert=
|
||||
#
|
||||
# Path to a PEM-encoded CA certificate bundle for TLS verification.
|
||||
# Only used when use_tls=1. Default: empty (system CA store).
|
||||
#
|
||||
# Head sampling is intentionally fixed at 1.0 (sample everything) and is
|
||||
# not configurable. A per-node sampling ratio would let nodes make
|
||||
# divergent keep/drop decisions for the same distributed trace, producing
|
||||
# broken/partial traces. A ParentBasedSampler ensures spans inheriting a
|
||||
# remote parent honor the upstream decision. Reduce volume at the collector
|
||||
# via tail sampling instead; for node-local post-hoc dropping use
|
||||
# SpanGuard::discard() in code.
|
||||
#
|
||||
# trace_rpc=1
|
||||
#
|
||||
# Enable tracing for JSON-RPC and WebSocket API request handling —
|
||||
# command parsing, execution, and response serialization. Default: 1.
|
||||
#
|
||||
# trace_transactions=1
|
||||
#
|
||||
# Enable tracing for the transaction lifecycle — submission, validation,
|
||||
# application to ledgers, and final disposition. Default: 1.
|
||||
#
|
||||
# trace_consensus=1
|
||||
#
|
||||
# Enable tracing for the consensus round lifecycle — proposals,
|
||||
# validations, mode changes, and ledger acceptance. Default: 1.
|
||||
#
|
||||
# trace_peer=1
|
||||
#
|
||||
# Enable tracing for peer-to-peer protocol messages — overlay message
|
||||
# send/receive, peer handshakes, and routing. High volume; enabled
|
||||
# by default. Default: 1.
|
||||
#
|
||||
# trace_ledger=1
|
||||
#
|
||||
# Enable tracing for ledger close and accept operations — ledger
|
||||
# building, state hashing, and write-back to the node store. Default: 1.
|
||||
#
|
||||
# --- Batch processor tuning ---
|
||||
#
|
||||
# batch_size=512
|
||||
#
|
||||
# Maximum number of spans exported in a single batch. Default: 512.
|
||||
#
|
||||
# batch_delay_ms=5000
|
||||
#
|
||||
# Maximum delay (milliseconds) before a partial batch is flushed.
|
||||
# Default: 5000 (5 seconds).
|
||||
#
|
||||
# max_queue_size=2048
|
||||
#
|
||||
# Maximum number of spans queued in memory before drops occur.
|
||||
# Default: 2048.
|
||||
#
|
||||
|
||||
@@ -198,8 +198,28 @@ target_link_libraries(
|
||||
xrpl.libxrpl.conditions
|
||||
)
|
||||
|
||||
# Telemetry module — OpenTelemetry distributed tracing support.
|
||||
# Sources: include/xrpl/telemetry/ (headers), src/libxrpl/telemetry/ (impl).
|
||||
# When telemetry=ON, links the Conan-provided umbrella target
|
||||
# opentelemetry-cpp::opentelemetry-cpp (individual component targets like
|
||||
# ::api, ::sdk are not available in the Conan package).
|
||||
add_module(xrpl telemetry)
|
||||
target_link_libraries(
|
||||
xrpl.libxrpl.telemetry
|
||||
PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.beast xrpl.libxrpl.config
|
||||
)
|
||||
if(telemetry)
|
||||
target_link_libraries(
|
||||
xrpl.libxrpl.telemetry
|
||||
PUBLIC opentelemetry-cpp::opentelemetry-cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
add_module(xrpl tx)
|
||||
target_link_libraries(xrpl.libxrpl.tx PUBLIC xrpl.libxrpl.ledger)
|
||||
target_link_libraries(
|
||||
xrpl.libxrpl.tx
|
||||
PUBLIC xrpl.libxrpl.ledger xrpl.libxrpl.telemetry
|
||||
)
|
||||
|
||||
add_library(xrpl.libxrpl)
|
||||
set_target_properties(xrpl.libxrpl PROPERTIES OUTPUT_NAME xrpl)
|
||||
@@ -233,6 +253,7 @@ target_link_modules(
|
||||
resource
|
||||
server
|
||||
shamap
|
||||
telemetry
|
||||
tx
|
||||
)
|
||||
|
||||
|
||||
22
conan.lock
22
conan.lock
@@ -1,21 +1,24 @@
|
||||
{
|
||||
"version": "0.5",
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1776096494.149",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1778091117.311",
|
||||
"soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
|
||||
"secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
|
||||
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
|
||||
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1774398111.888",
|
||||
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
|
||||
"opentelemetry-cpp/1.26.0#9d81768342c78cb897345fd419b358d2%1776934712.672",
|
||||
"openssl/3.6.2#4789bbf131b77d0515d15e094c8f697f%1778071755.506",
|
||||
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1775040983.408",
|
||||
"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",
|
||||
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
|
||||
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1776147552.838",
|
||||
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1778091117.848",
|
||||
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
|
||||
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152",
|
||||
"grpc/1.78.1#b1a9e74b145cc471bed4dc64dc6eb2c1%1774467387.342",
|
||||
@@ -23,16 +26,22 @@
|
||||
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772",
|
||||
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1774439234.681",
|
||||
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1765850143.837",
|
||||
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778050991.9",
|
||||
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778091165.282",
|
||||
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
|
||||
],
|
||||
"build_requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
|
||||
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1774447376.964",
|
||||
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
|
||||
"pkgconf/2.5.1#93c2051284cba1279494a43a4fcfeae2%1757684701.089",
|
||||
"opentelemetry-proto/1.7.0#ed6d5bd761bef0afb0ba09676420b9ea%1749461220.268",
|
||||
"ninja/1.13.2#c8c5dc2a52ed6e4e42a66d75b4717ceb%1764096931.974",
|
||||
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
|
||||
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
|
||||
"meson/1.10.2#9d2d10681fe7fe61c788c58626c89b25%1775558003.754",
|
||||
"m4/1.4.19#4523e4347b55cd26ae918bd5770cab9a%1778062762.471",
|
||||
"libtool/2.4.7#14e7739cc128bc1623d2ed318008e47e%1755679003.847",
|
||||
"gnu-config/cci.20210814#466e9d4d7779e1c142443f7ea44b4284%1762363589.329",
|
||||
"cmake/4.3.0#b939a42e98f593fb34d3a8c5cc860359%1774439249.183",
|
||||
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1774439233.447",
|
||||
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
|
||||
@@ -58,6 +67,9 @@
|
||||
],
|
||||
"lz4/[>=1.9.4 <2]": [
|
||||
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504"
|
||||
],
|
||||
"protobuf/[>=4.25.3 <7]": [
|
||||
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5"
|
||||
]
|
||||
},
|
||||
"config_requires": []
|
||||
|
||||
@@ -23,3 +23,15 @@ compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}}
|
||||
{% if compiler == "gcc" and compiler_version < 13 %}
|
||||
tools.build:cxxflags+=['-Wno-restrict']
|
||||
{% endif %}
|
||||
{% if os == "Windows" %}
|
||||
# opentelemetry-cpp's recipe removes the `shared` option on Windows and never
|
||||
# sets BUILD_SHARED_LIBS, so its upstream CMake defaults the protobuf-generated
|
||||
# `opentelemetry_proto` target to a DLL (opentelemetry_proto.dll). The rest of
|
||||
# the project links statically and nothing deploys that DLL next to the
|
||||
# executables, so the telemetry unit test fails to start with
|
||||
# STATUS_DLL_NOT_FOUND (0xC0000135). Force the dependency to build fully static
|
||||
# so no runtime DLL is produced. The conf is folded into the package id so a
|
||||
# fresh static binary is built instead of reusing a previously cached one.
|
||||
opentelemetry-cpp/*:tools.cmake.cmaketoolchain:extra_variables={"BUILD_SHARED_LIBS": "OFF"}
|
||||
opentelemetry-cpp/*:tools.info.package_id:confs+=["tools.cmake.cmaketoolchain:extra_variables"]
|
||||
{% endif %}
|
||||
|
||||
@@ -21,6 +21,7 @@ class Xrpl(ConanFile):
|
||||
"rocksdb": [True, False],
|
||||
"shared": [True, False],
|
||||
"static": [True, False],
|
||||
"telemetry": [True, False],
|
||||
"tests": [True, False],
|
||||
"unity": [True, False],
|
||||
"xrpld": [True, False],
|
||||
@@ -53,6 +54,7 @@ class Xrpl(ConanFile):
|
||||
"rocksdb": True,
|
||||
"shared": False,
|
||||
"static": True,
|
||||
"telemetry": True,
|
||||
"tests": False,
|
||||
"unity": False,
|
||||
"xrpld": False,
|
||||
@@ -139,6 +141,10 @@ class Xrpl(ConanFile):
|
||||
self.requires("jemalloc/5.3.1")
|
||||
if self.options.rocksdb:
|
||||
self.requires("rocksdb/10.5.1")
|
||||
# OpenTelemetry C++ SDK for distributed tracing (optional).
|
||||
# Provides OTLP/HTTP exporter, batch span processor, and trace API.
|
||||
if self.options.telemetry:
|
||||
self.requires("opentelemetry-cpp/1.26.0")
|
||||
self.requires("xxhash/0.8.3", transitive_headers=True)
|
||||
|
||||
exports_sources = (
|
||||
@@ -167,6 +173,7 @@ class Xrpl(ConanFile):
|
||||
tc.variables["rocksdb"] = self.options.rocksdb
|
||||
tc.variables["BUILD_SHARED_LIBS"] = self.options.shared
|
||||
tc.variables["static"] = self.options.static
|
||||
tc.variables["telemetry"] = self.options.telemetry
|
||||
tc.variables["unity"] = self.options.unity
|
||||
tc.variables["xrpld"] = self.options.xrpld
|
||||
tc.generate()
|
||||
@@ -219,3 +226,5 @@ class Xrpl(ConanFile):
|
||||
]
|
||||
if self.options.rocksdb:
|
||||
libxrpl.requires.append("rocksdb::librocksdb")
|
||||
if self.options.telemetry:
|
||||
libxrpl.requires.append("opentelemetry-cpp::opentelemetry-cpp")
|
||||
|
||||
@@ -66,6 +66,7 @@ words:
|
||||
- Btrfs
|
||||
- Buildx
|
||||
- canonicality
|
||||
- CGNAT
|
||||
- changespq
|
||||
- checkme
|
||||
- choco
|
||||
@@ -110,6 +111,7 @@ words:
|
||||
- endmacro
|
||||
- exceptioned
|
||||
- EXPECT_STREQ
|
||||
- exfiltration
|
||||
- Falco
|
||||
- fcontext
|
||||
- finalizers
|
||||
@@ -117,6 +119,8 @@ words:
|
||||
- fmtdur
|
||||
- fsanitize
|
||||
- funclets
|
||||
- gantt
|
||||
- Gantt
|
||||
- gcov
|
||||
- gcovr
|
||||
- ghead
|
||||
@@ -125,6 +129,8 @@ words:
|
||||
- gpgcheck
|
||||
- gpgkey
|
||||
- hotwallet
|
||||
- hicpp
|
||||
- htpasswd
|
||||
- hwaddress
|
||||
- hwrap
|
||||
- ifndef
|
||||
@@ -163,12 +169,11 @@ words:
|
||||
- mathbunnyru
|
||||
- mcmodel
|
||||
- MEMORYSTATUSEX
|
||||
- MPTAMM
|
||||
- MPTDEX
|
||||
- Merkle
|
||||
- Metafuncton
|
||||
- misprediction
|
||||
- missingok
|
||||
- MPTAMM
|
||||
- mptbalance
|
||||
- MPTDEX
|
||||
- mptflags
|
||||
@@ -202,6 +207,7 @@ words:
|
||||
- nonxrp
|
||||
- noreplace
|
||||
- noripple
|
||||
- nostd
|
||||
- nostdinc
|
||||
- notifempty
|
||||
- nudb
|
||||
@@ -210,6 +216,7 @@ words:
|
||||
- Nyffenegger
|
||||
- onlatest
|
||||
- ostr
|
||||
- otelc
|
||||
- pargs
|
||||
- partitioner
|
||||
- paychan
|
||||
@@ -217,6 +224,7 @@ words:
|
||||
- permdex
|
||||
- perminute
|
||||
- permissioned
|
||||
- pimpl
|
||||
- pointee
|
||||
- populator
|
||||
- preauth
|
||||
@@ -287,6 +295,7 @@ words:
|
||||
- takerpays
|
||||
- ters
|
||||
- TMEndpointv2
|
||||
- traceql
|
||||
- trixie
|
||||
- tx
|
||||
- txid
|
||||
@@ -294,6 +303,7 @@ words:
|
||||
- txjson
|
||||
- txn
|
||||
- txns
|
||||
- txqueue
|
||||
- txs
|
||||
- ubsan
|
||||
- UBSAN
|
||||
@@ -341,4 +351,7 @@ words:
|
||||
- xrplf
|
||||
- xxhash
|
||||
- xxhasher
|
||||
- CGNAT
|
||||
- xychart
|
||||
- zpages
|
||||
- pratik
|
||||
- dedup
|
||||
|
||||
80
docker/telemetry/docker-compose.yml
Normal file
80
docker/telemetry/docker-compose.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
# Docker Compose stack for xrpld OpenTelemetry observability.
|
||||
#
|
||||
# Provides services for local development:
|
||||
# - otel-collector: receives OTLP traces from xrpld, batches and
|
||||
# forwards them to Tempo. Listens on ports 4317 (gRPC)
|
||||
# and 4318 (HTTP).
|
||||
# - tempo: Grafana Tempo tracing backend, queryable via Grafana Explore
|
||||
# on port 3000. Recommended for production (S3/GCS storage, TraceQL).
|
||||
# - grafana: dashboards on port 3000, pre-configured with Tempo
|
||||
# datasource.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker/telemetry/docker-compose.yml up -d
|
||||
#
|
||||
# Configure xrpld to export traces by adding to xrpld.cfg:
|
||||
# [telemetry]
|
||||
# enabled=1
|
||||
# endpoint=http://localhost:4318/v1/traces
|
||||
|
||||
services:
|
||||
# OpenTelemetry Collector: receives spans from xrpld via OTLP protocol,
|
||||
# batches them for efficiency, and forwards to Tempo for storage.
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:0.121.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
ports:
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver (xrpld sends traces here)
|
||||
- "13133:13133" # Health check endpoint
|
||||
volumes:
|
||||
# Mount collector pipeline config (receivers → processors → exporters)
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
|
||||
depends_on:
|
||||
- tempo
|
||||
networks:
|
||||
- xrpld-telemetry
|
||||
|
||||
# Grafana Tempo: distributed tracing backend that stores and indexes
|
||||
# spans. Queryable via TraceQL in Grafana Explore.
|
||||
tempo:
|
||||
image: grafana/tempo:2.7.2
|
||||
command: ["-config.file=/etc/tempo.yaml"]
|
||||
ports:
|
||||
- "3200:3200" # Tempo HTTP API (health check, query)
|
||||
volumes:
|
||||
# Mount Tempo storage and ingestion config
|
||||
- ./tempo.yaml:/etc/tempo.yaml:ro
|
||||
# Persistent volume for trace data (WAL + blocks)
|
||||
- tempo-data:/var/tempo
|
||||
networks:
|
||||
- xrpld-telemetry
|
||||
|
||||
# Grafana: visualization UI with Tempo pre-configured as a datasource.
|
||||
# Anonymous admin access enabled for local development convenience.
|
||||
grafana:
|
||||
image: grafana/grafana:11.5.2
|
||||
environment:
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=true # No login required for local dev
|
||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin # Full access without auth
|
||||
ports:
|
||||
- "3000:3000" # Grafana web UI
|
||||
volumes:
|
||||
# Auto-provision Tempo datasource and search filters on startup
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
depends_on:
|
||||
- tempo
|
||||
networks:
|
||||
- xrpld-telemetry
|
||||
|
||||
# Named volume for Tempo trace storage (WAL and compacted blocks).
|
||||
# Data persists across container restarts. Remove with:
|
||||
# docker compose -f docker/telemetry/docker-compose.yml down -v
|
||||
volumes:
|
||||
tempo-data:
|
||||
|
||||
# Isolated bridge network so services communicate by container name
|
||||
# (e.g., the collector reaches Tempo at http://tempo:4317).
|
||||
networks:
|
||||
xrpld-telemetry:
|
||||
driver: bridge
|
||||
125
docker/telemetry/grafana/provisioning/datasources/tempo.yaml
Normal file
125
docker/telemetry/grafana/provisioning/datasources/tempo.yaml
Normal file
@@ -0,0 +1,125 @@
|
||||
# Grafana datasource provisioning for Grafana Tempo.
|
||||
# Auto-configures Tempo as a trace data source on Grafana startup.
|
||||
# Access Grafana at http://localhost:3000, then use Explore -> Tempo
|
||||
# to browse xrpld traces using TraceQL.
|
||||
#
|
||||
# Search filters provide pre-configured dropdowns in the Explore UI.
|
||||
# Each phase adds filters for the span attributes it introduces.
|
||||
# Phase 1b (infra): Base filters — node identity, service, span name, status.
|
||||
# Phase 2 (RPC): RPC command, status, role filters.
|
||||
# Phase 3 (TX): Transaction hash, local/peer origin, status.
|
||||
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Tempo
|
||||
type: tempo
|
||||
access: proxy
|
||||
url: http://tempo:3200
|
||||
uid: tempo
|
||||
jsonData:
|
||||
nodeGraph:
|
||||
enabled: true
|
||||
# Service map and traces-to-metrics require a Prometheus datasource
|
||||
# (not included in this stack). These features are inactive until a
|
||||
# Prometheus service is added to docker-compose.yml.
|
||||
serviceMap:
|
||||
datasourceUid: prometheus
|
||||
tracesToMetrics:
|
||||
datasourceUid: prometheus
|
||||
spanStartTimeShift: "-1h"
|
||||
spanEndTimeShift: "1h"
|
||||
search:
|
||||
filters:
|
||||
# --- Node identification filters ---
|
||||
# service.name: logical service name (default: "xrpld").
|
||||
# Useful when running multiple service types in the same collector.
|
||||
- id: service-name
|
||||
tag: service.name
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: static
|
||||
# service.instance.id: unique node identifier — defaults to the
|
||||
# node's public key (e.g., nHB1X37...). Distinguishes individual
|
||||
# nodes in a multi-node cluster or network.
|
||||
- id: node-id
|
||||
tag: service.instance.id
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: static
|
||||
# service.version: xrpld build version (e.g., "2.4.0-b1").
|
||||
# Filter traces from specific software releases.
|
||||
- id: node-version
|
||||
tag: service.version
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# xrpl.network.id: numeric network identifier
|
||||
# (0 = mainnet, 1 = testnet, 2 = devnet, etc.).
|
||||
# Derived from the [network_id] config section.
|
||||
- id: network-id
|
||||
tag: xrpl.network.id
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# xrpl.network.type: human-readable network name derived from
|
||||
# network ID ("mainnet", "testnet", "devnet", "unknown").
|
||||
- id: network-type
|
||||
tag: xrpl.network.type
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: static
|
||||
# --- Span intrinsic filters ---
|
||||
# name: the span operation name (e.g., "rpc.command.server_info").
|
||||
# Use to find traces for a specific RPC command or subsystem.
|
||||
- id: span-name
|
||||
tag: name
|
||||
operator: "="
|
||||
scope: intrinsic
|
||||
type: static
|
||||
# status: span completion status ("ok", "error", "unset").
|
||||
# Filter for failed operations to diagnose errors.
|
||||
- id: span-status
|
||||
tag: status
|
||||
operator: "="
|
||||
scope: intrinsic
|
||||
type: static
|
||||
# duration: span wall-clock duration. Use with ">" operator
|
||||
# to find slow operations (e.g., duration > 500ms).
|
||||
- id: span-duration
|
||||
tag: duration
|
||||
operator: ">"
|
||||
scope: intrinsic
|
||||
type: static
|
||||
# Phase 2: RPC tracing filters
|
||||
- id: rpc-command
|
||||
tag: command
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: rpc-status
|
||||
tag: rpc_status
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: rpc-role
|
||||
tag: rpc_role
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
# Phase 3: Transaction tracing filters
|
||||
- id: tx-hash
|
||||
tag: tx_hash
|
||||
operator: "="
|
||||
scope: span
|
||||
type: static
|
||||
- id: tx-origin
|
||||
tag: local
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: tx-status
|
||||
tag: tx_status
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
39
docker/telemetry/otel-collector-config.yaml
Normal file
39
docker/telemetry/otel-collector-config.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
# OpenTelemetry Collector configuration for xrpld development.
|
||||
#
|
||||
# Pipeline: OTLP receiver -> batch processor -> debug + Tempo.
|
||||
# xrpld sends traces via OTLP/HTTP to port 4318. The collector batches
|
||||
# them and forwards to Tempo via OTLP/gRPC on the Docker network. Tempo
|
||||
# is queryable via Grafana Explore using TraceQL.
|
||||
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
|
||||
processors:
|
||||
batch:
|
||||
timeout: 1s
|
||||
send_batch_size: 100
|
||||
|
||||
exporters:
|
||||
debug:
|
||||
verbosity: detailed
|
||||
otlp/tempo:
|
||||
endpoint: tempo:4317
|
||||
tls:
|
||||
insecure: true
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
|
||||
service:
|
||||
extensions: [health_check]
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [debug, otlp/tempo]
|
||||
61
docker/telemetry/tempo.yaml
Normal file
61
docker/telemetry/tempo.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
# Grafana Tempo configuration for xrpld telemetry stack.
|
||||
#
|
||||
# Runs in single-binary mode for local development.
|
||||
# Receives traces via OTLP/gRPC from the OTel Collector and stores
|
||||
# them locally. Queryable via Grafana Explore using the Tempo datasource.
|
||||
#
|
||||
# Search filters are configured on the Grafana datasource side
|
||||
# (grafana/provisioning/datasources/tempo.yaml). Tempo auto-indexes
|
||||
# all span attributes for search in single-binary mode.
|
||||
#
|
||||
# For production, replace local storage with S3/GCS backend and adjust
|
||||
# retention via the compactor settings. See:
|
||||
# https://grafana.com/docs/tempo/latest/configuration/
|
||||
|
||||
stream_over_http_enabled: true
|
||||
|
||||
server:
|
||||
http_listen_port: 3200
|
||||
|
||||
distributor:
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
|
||||
ingester:
|
||||
max_block_duration: 5m
|
||||
|
||||
compactor:
|
||||
compaction:
|
||||
block_retention: 1h
|
||||
|
||||
# Enable metrics generator for service graph and span metrics.
|
||||
# Produces RED metrics (rate, errors, duration) per service/span,
|
||||
# feeding Grafana's service map visualization.
|
||||
metrics_generator:
|
||||
registry:
|
||||
external_labels:
|
||||
source: tempo
|
||||
storage:
|
||||
path: /var/tempo/generator/wal
|
||||
# Uncomment and add a Prometheus service to docker-compose.yml
|
||||
# to enable remote_write for service graph metrics:
|
||||
# remote_write:
|
||||
# - url: http://prometheus:9090/api/v1/write
|
||||
|
||||
overrides:
|
||||
defaults:
|
||||
metrics_generator:
|
||||
processors:
|
||||
- service-graphs
|
||||
- span-metrics
|
||||
|
||||
storage:
|
||||
trace:
|
||||
backend: local
|
||||
wal:
|
||||
path: /var/tempo/wal
|
||||
local:
|
||||
path: /var/tempo/blocks
|
||||
129
docs/build/telemetry.md
vendored
Normal file
129
docs/build/telemetry.md
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
# 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)
|
||||
|
||||
## 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 **off by default** at both compile time and runtime:
|
||||
|
||||
- **Compile time**: The Conan option `telemetry` and CMake option `telemetry` must be set to `True`/`ON`.
|
||||
When disabled, all `SpanGuard` calls compile to inline no-ops (defined in `SpanGuard.h`)
|
||||
with zero overhead — no OTel SDK dependency required.
|
||||
- **Runtime**: The `[telemetry]` config section must set `enabled=1`.
|
||||
When disabled at runtime, a no-op implementation is used.
|
||||
|
||||
## Building with Telemetry
|
||||
|
||||
### Summary
|
||||
|
||||
Follow the same instructions as mentioned in [BUILD.md](../../BUILD.md) but with the following changes:
|
||||
|
||||
1. Pass `-o telemetry=True` to `conan install` to pull the `opentelemetry-cpp` dependency.
|
||||
2. CMake will automatically pick up `telemetry=ON` from the Conan-generated toolchain.
|
||||
3. Build as usual.
|
||||
|
||||
---
|
||||
|
||||
### Build steps
|
||||
|
||||
```bash
|
||||
cd /path/to/xrpld
|
||||
rm -rf .build
|
||||
mkdir .build
|
||||
cd .build
|
||||
```
|
||||
|
||||
#### Install dependencies
|
||||
|
||||
The `telemetry` option adds `opentelemetry-cpp/1.26.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
|
||||
|
||||
Omit the `-o telemetry=True` option (or pass `-o telemetry=False`).
|
||||
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.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Conan lockfile error
|
||||
|
||||
If you see `ERROR: Requirement 'opentelemetry-cpp/1.26.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.
|
||||
@@ -18,6 +18,9 @@ class Manager;
|
||||
namespace perf {
|
||||
class PerfLog;
|
||||
} // namespace perf
|
||||
namespace telemetry {
|
||||
class Telemetry;
|
||||
} // namespace telemetry
|
||||
|
||||
// This is temporary until we migrate all code to use ServiceRegistry.
|
||||
class Application;
|
||||
@@ -218,6 +221,9 @@ public:
|
||||
virtual perf::PerfLog&
|
||||
getPerfLog() = 0;
|
||||
|
||||
virtual telemetry::Telemetry&
|
||||
getTelemetry() = 0;
|
||||
|
||||
// Configuration and state
|
||||
[[nodiscard]] virtual bool
|
||||
isStopping() const = 0;
|
||||
|
||||
@@ -85,6 +85,26 @@ message TMPublicKey {
|
||||
// If you want to send an amount that is greater than any single address of yours
|
||||
// you must first combine coins from one address to another.
|
||||
|
||||
// Trace context for OpenTelemetry distributed tracing across nodes.
|
||||
// Uses W3C Trace Context format internally.
|
||||
//
|
||||
// Field numbering note: this message is embedded as field 1001 on
|
||||
// TMTransaction, TMProposeSet, and TMValidation. Field numbers >= 1000
|
||||
// are reserved for optional, observability-only additions that must not
|
||||
// collide with protocol-semantic fields (which historically use 1-99).
|
||||
// Older peers that do not understand field 1001 will simply ignore it
|
||||
// per protobuf wire-format rules, preserving backwards compatibility.
|
||||
//
|
||||
// trace_state is reserved for future use (secure tracing pipeline,
|
||||
// OpenTelemetryPlan/secure-OTel.md). It is currently neither populated
|
||||
// on inject nor read on extract; consumers must not rely on it.
|
||||
message TraceContext {
|
||||
optional bytes trace_id = 1; // 16-byte trace identifier
|
||||
optional bytes span_id = 2; // 8-byte parent span identifier
|
||||
optional uint32 trace_flags = 3; // bit 0 = sampled
|
||||
optional string trace_state = 4; // RESERVED — see TraceContext header note
|
||||
}
|
||||
|
||||
enum TransactionStatus {
|
||||
tsNEW = 1; // origin node did/could not validate
|
||||
tsCURRENT = 2; // scheduled to go in this ledger
|
||||
@@ -101,6 +121,9 @@ message TMTransaction {
|
||||
required TransactionStatus status = 2;
|
||||
optional uint64 receiveTimestamp = 3;
|
||||
optional bool deferred = 4; // not applied to open ledger
|
||||
|
||||
// Optional trace context for OpenTelemetry distributed tracing
|
||||
optional TraceContext trace_context = 1001;
|
||||
}
|
||||
|
||||
message TMTransactions {
|
||||
@@ -149,6 +172,9 @@ message TMProposeSet {
|
||||
|
||||
// Number of hops traveled
|
||||
optional uint32 hops = 12 [deprecated = true];
|
||||
|
||||
// Optional trace context for OpenTelemetry distributed tracing
|
||||
optional TraceContext trace_context = 1001;
|
||||
}
|
||||
|
||||
enum TxSetStatus {
|
||||
@@ -194,6 +220,9 @@ message TMValidation {
|
||||
|
||||
// Number of hops traveled
|
||||
optional uint32 hops = 3 [deprecated = true];
|
||||
|
||||
// Optional trace context for OpenTelemetry distributed tracing
|
||||
optional TraceContext trace_context = 1001;
|
||||
}
|
||||
|
||||
// An array of Endpoint messages
|
||||
|
||||
25
include/xrpl/telemetry/DiscardFlag.h
Normal file
25
include/xrpl/telemetry/DiscardFlag.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
/** Thread-local flag for span discard signaling.
|
||||
|
||||
SpanGuard::discard() sets gTlDiscardCurrentSpan to true before calling
|
||||
Span::End(). The OTel SDK calls SpanProcessor::OnEnd() synchronously on
|
||||
the same thread, so FilteringSpanProcessor checks and clears this flag
|
||||
in OnEnd() to drop the span before it enters the batch export queue.
|
||||
|
||||
This side-channel avoids inspecting the Recordable's internals (which
|
||||
vary by exporter type — SpanData vs OtlpRecordable).
|
||||
|
||||
Kept in a separate header to avoid transitive include bloat: SpanGuard.h
|
||||
only needs this flag, not the full Telemetry.h with BasicConfig/Journal.
|
||||
|
||||
@see SpanGuard::discard(), FilteringSpanProcessor (Telemetry.cpp)
|
||||
*/
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/** When true, the FilteringSpanProcessor drops the current span in
|
||||
OnEnd(). Set by SpanGuard::discard(), cleared by OnEnd(). */
|
||||
inline thread_local bool gTlDiscardCurrentSpan = false;
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
519
include/xrpl/telemetry/SpanGuard.h
Normal file
519
include/xrpl/telemetry/SpanGuard.h
Normal file
@@ -0,0 +1,519 @@
|
||||
#pragma once
|
||||
|
||||
/** RAII guard for OpenTelemetry trace spans.
|
||||
|
||||
Wraps an OTel Span and Scope behind the pimpl idiom so that no
|
||||
opentelemetry headers are exposed in this public header. When
|
||||
XRPL_ENABLE_TELEMETRY is not defined, SpanGuard is an empty class
|
||||
with all-inline no-op methods — zero overhead, zero dependencies.
|
||||
|
||||
Dependency diagram:
|
||||
|
||||
+------------------------------------------------+
|
||||
| SpanGuard |
|
||||
+------------------------------------------------+
|
||||
| - impl_ : unique_ptr<Impl> (pimpl) |
|
||||
+------------------------------------------------+
|
||||
| + span(cat, prefix, name) [static] |
|
||||
| + childSpan(name) : SpanGuard |
|
||||
| + linkedSpan(name) : SpanGuard |
|
||||
| + hashSpan(cat, name, hash) [static] |
|
||||
| + hashSpan(cat, name, hash, parent) [static] |
|
||||
| + captureContext() : SpanContext |
|
||||
| + getTraceBytes() : TraceBytes |
|
||||
| + setAttribute(key, value) |
|
||||
| + setOk() / setError(desc) |
|
||||
| + addEvent(name) |
|
||||
| + recordException(e) |
|
||||
| + discard() |
|
||||
| + operator bool() |
|
||||
+------------------------------------------------+
|
||||
| hides (pimpl)
|
||||
+-------+-------+
|
||||
| |
|
||||
+--------+ +-------------+
|
||||
| Span | | Scope |
|
||||
| (OTel) | | (OTel, non- |
|
||||
| | | movable) |
|
||||
+--------+ +-------------+
|
||||
|
||||
Static factory methods access the global Telemetry instance
|
||||
internally (via Telemetry::getInstance()), check whether tracing
|
||||
is enabled for the requested subsystem, and return either an
|
||||
active guard or a null (no-op) guard. Callers never need a
|
||||
Telemetry reference.
|
||||
|
||||
Usage examples:
|
||||
|
||||
Span names and attribute keys come from per-module `*SpanNames.h`
|
||||
headers (e.g. RpcSpanNames.h, TxSpanNames.h) as typed compile-time
|
||||
constants — never raw string literals — so the naming spec is
|
||||
enforced at the call site and dashboards stay in sync.
|
||||
|
||||
1. Basic RPC tracing (factory method with category):
|
||||
@code
|
||||
#include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
using namespace xrpl::telemetry;
|
||||
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, rpc_span::prefix::command, "submit");
|
||||
span.setAttribute(rpc_span::attr::command, "submit");
|
||||
span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::success);
|
||||
// span ended automatically on scope exit
|
||||
@endcode
|
||||
|
||||
2. Error recording:
|
||||
@code
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, rpc_span::prefix::command, "submit");
|
||||
try {
|
||||
doWork();
|
||||
span.setOk();
|
||||
} catch (std::exception const& e) {
|
||||
span.recordException(e);
|
||||
}
|
||||
@endcode
|
||||
|
||||
3. Cross-thread context propagation:
|
||||
@code
|
||||
#include <xrpld/consensus/ConsensusSpanNames.h>
|
||||
using namespace xrpl::telemetry;
|
||||
|
||||
// Thread A: create span and capture context
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Consensus, seg::consensus, consensus::span::op::round);
|
||||
auto ctx = span.captureContext();
|
||||
|
||||
// Thread B: create child with captured context
|
||||
auto child = SpanGuard::childSpan(consensus::span::accept, ctx);
|
||||
@endcode
|
||||
|
||||
4. Conditional check (rarely needed — methods are no-ops on null):
|
||||
@code
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::httpRequest);
|
||||
if (span) {
|
||||
// expensive attribute computation only when active
|
||||
span.setAttribute(rpc_span::attr::requestPayloadSize, computeSize());
|
||||
}
|
||||
@endcode
|
||||
|
||||
5. Tail-based filtering via discard():
|
||||
@code
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Transactions, tx_span::prefix::tx, tx_span::op::process);
|
||||
auto result = preflight(tx);
|
||||
if (result != tesSUCCESS) {
|
||||
span.discard(); // drop span, never exported
|
||||
return result;
|
||||
}
|
||||
@endcode
|
||||
|
||||
@note Thread safety: A SpanGuard must only be used on the thread
|
||||
where it was constructed (the internal Scope binds to the
|
||||
thread-local context stack). Use captureContext() to propagate
|
||||
the trace to other threads.
|
||||
|
||||
@note Move semantics: Move construction transfers ownership of
|
||||
the pimpl pointer — no double-Scope issues. Move assignment is
|
||||
deleted to prevent re-scoping mid-flight.
|
||||
|
||||
@note Known limitations:
|
||||
- Attributes cannot be removed per the OTel spec; use
|
||||
setAttribute with an empty value as a convention.
|
||||
- SpanGuard::span() (raw Span access) is intentionally not
|
||||
exposed — all interaction goes through the public methods.
|
||||
*/
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/** Trace subsystem categories for conditional span creation.
|
||||
|
||||
Each value maps to a runtime config flag (e.g. `trace_rpc=1`).
|
||||
Used by SpanGuard::span(TraceCategory, prefix, name) to decide
|
||||
whether to create a real span or return a null guard.
|
||||
*/
|
||||
enum class TraceCategory { Rpc, Transactions, Consensus, Peer, Ledger };
|
||||
|
||||
/** Raw trace context bytes for cross-node propagation.
|
||||
|
||||
Holds the binary trace_id, span_id, and trace_flags extracted from
|
||||
an active span. Used by protocol-layer code to inject trace context
|
||||
into outgoing protobuf messages without depending on OTel types.
|
||||
|
||||
@see SpanGuard::getTraceBytes(), TraceContextPropagator.h
|
||||
*/
|
||||
struct TraceBytes
|
||||
{
|
||||
/// 16-byte W3C trace identifier.
|
||||
std::array<std::uint8_t, 16> traceId{};
|
||||
/// 8-byte span identifier of the current span.
|
||||
std::array<std::uint8_t, 8> spanId{};
|
||||
/// W3C trace flags (bit 0 = sampled).
|
||||
std::uint8_t traceFlags{0};
|
||||
/// True if this struct contains valid data from an active span.
|
||||
bool valid{false};
|
||||
};
|
||||
|
||||
/** Opaque wrapper for an OTel context snapshot.
|
||||
|
||||
Used to propagate trace context across threads. Created by
|
||||
SpanGuard::captureContext(), consumed by SpanGuard::childSpan()
|
||||
or SpanGuard::linkedSpan() with an explicit parent/link context.
|
||||
*/
|
||||
class SpanContext
|
||||
{
|
||||
friend class SpanGuard;
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
struct Impl;
|
||||
std::shared_ptr<Impl> impl_;
|
||||
explicit SpanContext(std::shared_ptr<Impl> impl);
|
||||
#endif
|
||||
|
||||
public:
|
||||
SpanContext() = default;
|
||||
|
||||
/** @return true if this context holds a valid trace context. */
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
[[nodiscard]] bool
|
||||
isValid() const;
|
||||
#else
|
||||
// NOLINTBEGIN(readability-convert-member-functions-to-static)
|
||||
[[nodiscard]] bool
|
||||
isValid() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||
#endif
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Real implementation (pimpl, compiled in SpanGuard.cpp)
|
||||
// ---------------------------------------------------------------------------
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
/** RAII wrapper that activates a span on construction and ends it on
|
||||
destruction. All OTel types are hidden behind the Impl pointer.
|
||||
Non-copyable, move-constructible.
|
||||
*/
|
||||
class SpanGuard
|
||||
{
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
explicit SpanGuard(std::unique_ptr<Impl> impl);
|
||||
|
||||
public:
|
||||
/** Construct a null (no-op) guard. All methods are safe to call. */
|
||||
SpanGuard();
|
||||
~SpanGuard();
|
||||
|
||||
SpanGuard(SpanGuard&& other) noexcept;
|
||||
SpanGuard&
|
||||
operator=(SpanGuard&&) = delete;
|
||||
SpanGuard(SpanGuard const&) = delete;
|
||||
SpanGuard&
|
||||
operator=(SpanGuard const&) = delete;
|
||||
|
||||
// --- Static factory methods ----------------------------------------
|
||||
|
||||
/** Create a span guarded by a TraceCategory flag.
|
||||
The span name is built as "prefix.name". Returns a null guard
|
||||
if the category is disabled in config.
|
||||
@param cat Trace subsystem category.
|
||||
@param prefix Span name prefix (e.g. "rpc.command").
|
||||
@param name Span name suffix (e.g. "submit").
|
||||
*/
|
||||
[[nodiscard]] static SpanGuard
|
||||
span(TraceCategory cat, std::string_view prefix, std::string_view name);
|
||||
|
||||
// --- Child / linked span creation ----------------------------------
|
||||
|
||||
/** Create a child span parented to this guard's active context.
|
||||
@param name Span name for the child.
|
||||
@return A new guard, or null if this guard is inactive.
|
||||
*/
|
||||
[[nodiscard]] SpanGuard
|
||||
childSpan(std::string_view name) const;
|
||||
|
||||
/** Create a child span parented to an explicit captured context.
|
||||
@param name Span name for the child.
|
||||
@param parentCtx Context captured via captureContext().
|
||||
@return A new guard, or null if parentCtx is invalid.
|
||||
*/
|
||||
[[nodiscard]] static SpanGuard
|
||||
childSpan(std::string_view name, SpanContext const& parentCtx);
|
||||
|
||||
/** Create a span linked (follows-from) to this guard's span.
|
||||
The new span is NOT a child — it starts a new sub-tree but
|
||||
carries a causal link to this span.
|
||||
@param name Span name for the linked span.
|
||||
@return A new guard, or null if this guard is inactive.
|
||||
*/
|
||||
[[nodiscard]] SpanGuard
|
||||
linkedSpan(std::string_view name) const;
|
||||
|
||||
/** Create a span linked to an explicit captured context.
|
||||
@param name Span name for the linked span.
|
||||
@param linkCtx Context to link from.
|
||||
@return A new guard, or null if linkCtx is invalid.
|
||||
*/
|
||||
[[nodiscard]] static SpanGuard
|
||||
linkedSpan(std::string_view name, SpanContext const& linkCtx);
|
||||
|
||||
// --- Hash-derived span (category-gated) -----------------------------
|
||||
|
||||
/** Create a span whose trace_id is derived from arbitrary hash data.
|
||||
trace_id = hashData[0:16], span_id = random. Gated by the given
|
||||
TraceCategory. All nodes using the same hash independently produce
|
||||
spans under the same trace_id, enabling cross-node correlation
|
||||
without context propagation.
|
||||
@param cat Trace subsystem category.
|
||||
@param name Full span name (e.g. "tx.receive").
|
||||
@param hashData Pointer to at least 16 bytes of hash data.
|
||||
@param hashSize Size of the hash buffer (must be >= 16).
|
||||
*/
|
||||
static SpanGuard
|
||||
hashSpan(
|
||||
TraceCategory const cat,
|
||||
std::string_view const name,
|
||||
std::uint8_t const* const hashData,
|
||||
std::size_t const hashSize);
|
||||
|
||||
/** Create a hash-derived span with a remote parent.
|
||||
trace_id = hashData[0:16], parent span_id from protobuf context
|
||||
propagation. Produces a child span of the sender's span while
|
||||
sharing the deterministic trace_id.
|
||||
@param cat Trace subsystem category.
|
||||
@param name Full span name.
|
||||
@param hashData Pointer to at least 16 bytes of hash data.
|
||||
@param hashSize Size of the hash buffer (must be >= 16).
|
||||
@param parentSpanId Pointer to 8 bytes of parent span ID.
|
||||
@param parentSpanSize Size of parent span ID buffer (must be 8).
|
||||
@param traceFlags Trace flags from remote context.
|
||||
*/
|
||||
static SpanGuard
|
||||
hashSpan(
|
||||
TraceCategory const cat,
|
||||
std::string_view const name,
|
||||
std::uint8_t const* const hashData,
|
||||
std::size_t const hashSize,
|
||||
std::uint8_t const* const parentSpanId,
|
||||
std::size_t const parentSpanSize,
|
||||
std::uint8_t const traceFlags);
|
||||
|
||||
// --- Context capture -----------------------------------------------
|
||||
|
||||
/** Snapshot the current thread's OTel context for cross-thread use.
|
||||
@return An opaque SpanContext, or an invalid one if null guard.
|
||||
*/
|
||||
[[nodiscard]] SpanContext
|
||||
captureContext() const;
|
||||
|
||||
/** Extract raw trace context bytes from this span for propagation.
|
||||
|
||||
Unlike captureContext() which captures the thread-local runtime
|
||||
context, this method reads the span's own SpanContext directly.
|
||||
Safe to call from any thread that holds a reference to this guard.
|
||||
|
||||
@return A TraceBytes struct with valid=true if the span is active
|
||||
and has a valid context, or valid=false otherwise.
|
||||
*/
|
||||
[[nodiscard]] TraceBytes
|
||||
getTraceBytes() const;
|
||||
|
||||
// --- Attribute setters (explicit overloads, no OTel types) ---------
|
||||
|
||||
/** Set a string attribute. No-op on a null guard. */
|
||||
void
|
||||
setAttribute(std::string_view key, std::string_view value);
|
||||
|
||||
/** Set a string attribute (C-string overload). No-op on a null guard. */
|
||||
void
|
||||
setAttribute(std::string_view key, char const* value);
|
||||
|
||||
/** Set an integer attribute. No-op on a null guard. */
|
||||
void
|
||||
setAttribute(std::string_view key, std::int64_t value);
|
||||
|
||||
/** Set a floating-point attribute. No-op on a null guard. */
|
||||
void
|
||||
setAttribute(std::string_view key, double value);
|
||||
|
||||
/** Set a boolean attribute. No-op on a null guard. */
|
||||
void
|
||||
setAttribute(std::string_view key, bool value);
|
||||
|
||||
// --- Status / events -----------------------------------------------
|
||||
|
||||
/** Mark the span status as OK. No-op on a null guard. */
|
||||
void
|
||||
setOk();
|
||||
|
||||
/** Mark the span status as error. No-op on a null guard.
|
||||
@param description Optional human-readable error description.
|
||||
*/
|
||||
void
|
||||
setError(std::string_view description = "");
|
||||
|
||||
/** Add a named event to the span's timeline. No-op on a null guard.
|
||||
@param name Event name.
|
||||
*/
|
||||
void
|
||||
addEvent(std::string_view name);
|
||||
|
||||
/** Record an exception as a span event following OTel semantic
|
||||
conventions, and mark the span status as error.
|
||||
No-op on a null guard.
|
||||
@param e The exception to record.
|
||||
*/
|
||||
void
|
||||
recordException(std::exception const& e);
|
||||
|
||||
/** Mark this span for discard and end it immediately.
|
||||
The FilteringSpanProcessor drops the span before it enters the
|
||||
batch export queue. After discard(), the guard is inert.
|
||||
*/
|
||||
void
|
||||
discard();
|
||||
|
||||
/** @return true if this guard holds an active span. */
|
||||
explicit
|
||||
operator bool() const;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// No-op stub (all inline, zero overhead, no OTel dependency)
|
||||
// ---------------------------------------------------------------------------
|
||||
#else // XRPL_ENABLE_TELEMETRY not defined
|
||||
|
||||
class SpanGuard
|
||||
{
|
||||
public:
|
||||
SpanGuard() = default;
|
||||
~SpanGuard() = default;
|
||||
SpanGuard(SpanGuard&&) noexcept = default;
|
||||
SpanGuard&
|
||||
operator=(SpanGuard&&) = delete;
|
||||
SpanGuard(SpanGuard const&) = delete;
|
||||
SpanGuard&
|
||||
operator=(SpanGuard const&) = delete;
|
||||
|
||||
[[nodiscard]] static SpanGuard
|
||||
span(TraceCategory, std::string_view, std::string_view)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(readability-convert-member-functions-to-static)
|
||||
[[nodiscard]] SpanGuard
|
||||
childSpan(std::string_view) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] static SpanGuard
|
||||
childSpan(std::string_view, SpanContext const&)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] SpanGuard
|
||||
linkedSpan(std::string_view) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] static SpanGuard
|
||||
linkedSpan(std::string_view, SpanContext const&)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] static SpanGuard
|
||||
hashSpan(TraceCategory, std::string_view, std::uint8_t const*, std::size_t)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] static SpanGuard
|
||||
hashSpan(
|
||||
TraceCategory,
|
||||
std::string_view,
|
||||
std::uint8_t const*,
|
||||
std::size_t,
|
||||
std::uint8_t const*,
|
||||
std::size_t,
|
||||
std::uint8_t)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] SpanContext
|
||||
captureContext() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] TraceBytes
|
||||
getTraceBytes() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||
|
||||
void
|
||||
setAttribute(std::string_view, std::string_view)
|
||||
{
|
||||
}
|
||||
void
|
||||
setAttribute(std::string_view, char const*)
|
||||
{
|
||||
}
|
||||
void
|
||||
setAttribute(std::string_view, std::int64_t)
|
||||
{
|
||||
}
|
||||
void
|
||||
setAttribute(std::string_view, double)
|
||||
{
|
||||
}
|
||||
void
|
||||
setAttribute(std::string_view, bool)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
setOk()
|
||||
{
|
||||
}
|
||||
void
|
||||
setError(std::string_view = "")
|
||||
{
|
||||
}
|
||||
void
|
||||
addEvent(std::string_view)
|
||||
{
|
||||
}
|
||||
void
|
||||
recordException(std::exception const&)
|
||||
{
|
||||
}
|
||||
void
|
||||
discard()
|
||||
{
|
||||
}
|
||||
|
||||
explicit
|
||||
operator bool() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
133
include/xrpl/telemetry/SpanNames.h
Normal file
133
include/xrpl/telemetry/SpanNames.h
Normal file
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
/** Compile-time string concatenation utility and shared telemetry constants.
|
||||
*
|
||||
* Provides StaticStr<N> — a compile-time string buffer that implicitly
|
||||
* converts to std::string_view — and join() for dot-separated concatenation.
|
||||
* Module-specific span names (e.g. RPC, consensus) live in their respective
|
||||
* modules and build upon these shared primitives.
|
||||
*
|
||||
* @note These constants are NOT guarded by XRPL_ENABLE_TELEMETRY because
|
||||
* call sites reference them even when SpanGuard methods are no-ops
|
||||
* (the no-op stubs still accept string_view parameters). The compiler
|
||||
* elides all inline constexpr values whose only uses are in dead code.
|
||||
*
|
||||
* @note Json::StaticString (jss.h) is a pointer wrapper without
|
||||
* concatenation support. boost::static_string is not constexpr.
|
||||
* StaticStr<N> exists specifically for compile-time dot-join composition.
|
||||
*
|
||||
* Naming conventions (see spec 2026-05-13-span-attr-naming-design):
|
||||
* - Per-span attribute keys: bare field name (span name carries the domain).
|
||||
* - Collision qualifier: <domain>_<field> when bare name collides across
|
||||
* domains or with OTel reserved `status` (e.g. rpc_status, grpc_status).
|
||||
* - Shared cross-span attributes: <domain>_<field> (underscore) form
|
||||
* (e.g. tx_hash, peer_id, ledger_seq, consensus_round).
|
||||
* - Resource attribute keys: xrpl.<subsystem>.<field> (dotted) form is
|
||||
* RESERVED for process-identity attributes set once at startup on the
|
||||
* OTel resource (e.g. xrpl.network.id, xrpl.network.type). Do not use
|
||||
* this form for span attributes — it parses awkwardly in TraceQL and
|
||||
* blurs the resource/span scope distinction.
|
||||
* - Span prefixes: <subsystem>[.<component>].
|
||||
*/
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
// ===== Compile-time string utility =========================================
|
||||
|
||||
/// Fixed-size character buffer for compile-time string operations.
|
||||
/// Implicitly converts to std::string_view at zero cost.
|
||||
template <std::size_t N>
|
||||
struct StaticStr
|
||||
{
|
||||
char data[N + 1]{};
|
||||
static constexpr std::size_t size = N;
|
||||
|
||||
constexpr StaticStr() = default;
|
||||
|
||||
constexpr explicit StaticStr(char const (&str)[N + 1])
|
||||
{
|
||||
for (std::size_t i = 0; i <= N; ++i)
|
||||
data[i] = str[i];
|
||||
}
|
||||
|
||||
constexpr
|
||||
operator std::string_view() const noexcept
|
||||
{
|
||||
return {data, N};
|
||||
}
|
||||
};
|
||||
|
||||
/// Deduction guide: StaticStr from string literal.
|
||||
template <std::size_t N>
|
||||
StaticStr(char const (&)[N]) -> StaticStr<N - 1>;
|
||||
|
||||
/// Create a StaticStr from a string literal.
|
||||
template <std::size_t N>
|
||||
constexpr auto
|
||||
makeStr(char const (&str)[N])
|
||||
{
|
||||
return StaticStr<N - 1>(str);
|
||||
}
|
||||
|
||||
/// Concatenate two StaticStr values with a dot separator.
|
||||
template <std::size_t A, std::size_t B>
|
||||
constexpr auto
|
||||
join(StaticStr<A> const& lhs, StaticStr<B> const& rhs)
|
||||
{
|
||||
constexpr std::size_t len = A + 1 + B; // lhs + '.' + rhs
|
||||
StaticStr<len> result;
|
||||
std::size_t pos = 0;
|
||||
for (std::size_t i = 0; i < A; ++i)
|
||||
result.data[pos++] = lhs.data[i];
|
||||
result.data[pos++] = '.';
|
||||
for (std::size_t i = 0; i < B; ++i)
|
||||
result.data[pos++] = rhs.data[i];
|
||||
result.data[pos] = '\0';
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== Shared root segments ================================================
|
||||
|
||||
namespace seg {
|
||||
inline constexpr auto xrpl = makeStr("xrpl");
|
||||
inline constexpr auto rpc = makeStr("rpc");
|
||||
inline constexpr auto tx = makeStr("tx");
|
||||
inline constexpr auto consensus = makeStr("consensus");
|
||||
inline constexpr auto peer = makeStr("peer");
|
||||
inline constexpr auto ledger = makeStr("ledger");
|
||||
inline constexpr auto network = makeStr("network");
|
||||
inline constexpr auto link = makeStr("link");
|
||||
} // namespace seg
|
||||
|
||||
// ===== Shared attribute keys (used across modules) =========================
|
||||
|
||||
namespace attr {
|
||||
inline constexpr auto networkId = join(join(seg::xrpl, seg::network), makeStr("id"));
|
||||
inline constexpr auto networkType = join(join(seg::xrpl, seg::network), makeStr("type"));
|
||||
inline constexpr auto linkType = makeStr("link_type");
|
||||
|
||||
/// Canonical shared attrs (rule 5 — <domain>_<field> underscore form).
|
||||
///
|
||||
/// Per the naming convention header note: shared cross-span attribute
|
||||
/// keys use the underscore form, reserving the dotted xrpl.<domain>.<field>
|
||||
/// form for resource attributes set on the OTel resource at startup.
|
||||
/// Defined once here, aliased by domain-specific headers. These are
|
||||
/// literal underscore-joined names, not dot-joined via `join()`, since
|
||||
/// `join()` always inserts `.` between its arguments.
|
||||
inline constexpr auto txHash = makeStr("tx_hash");
|
||||
inline constexpr auto peerId = makeStr("peer_id");
|
||||
inline constexpr auto ledgerSeq = makeStr("ledger_seq");
|
||||
} // namespace attr
|
||||
|
||||
// ===== Shared attribute values =============================================
|
||||
|
||||
namespace attr_val {
|
||||
inline constexpr auto success = makeStr("success");
|
||||
inline constexpr auto error = makeStr("error");
|
||||
inline constexpr auto followsFrom = makeStr("follows_from");
|
||||
} // namespace attr_val
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
337
include/xrpl/telemetry/Telemetry.h
Normal file
337
include/xrpl/telemetry/Telemetry.h
Normal file
@@ -0,0 +1,337 @@
|
||||
#pragma once
|
||||
|
||||
/** Abstract interface for OpenTelemetry distributed tracing.
|
||||
|
||||
Provides the Telemetry base class that all components use to create trace
|
||||
spans. Three concrete implementations exist, selected at construction time
|
||||
by makeTelemetry():
|
||||
|
||||
- TelemetryImpl (Telemetry.cpp): real OTel SDK integration, compiled
|
||||
only when XRPL_ENABLE_TELEMETRY is defined and enabled at runtime.
|
||||
- NullTelemetry (NullTelemetry.cpp): no-op stub used when telemetry is
|
||||
disabled at compile time or runtime.
|
||||
- NullTelemetryOtel (Telemetry.cpp): no-op stub that still depends on
|
||||
the OTel API (used during transition or for testing).
|
||||
|
||||
Inheritance / dependency diagram:
|
||||
|
||||
+--------------------+
|
||||
| Telemetry | (abstract, this file)
|
||||
| <<interface>> |
|
||||
+---------+----------+
|
||||
|
|
||||
+---------+-----------+-------------------+
|
||||
| | |
|
||||
+---+------------+ +-----+---------+ +------+----------+
|
||||
| TelemetryImpl | | NullTelemetry | | NullTelemetryOtel|
|
||||
| (Telemetry.cpp)| |(NullTelemetry | | (Telemetry.cpp) |
|
||||
| OTel SDK | | .cpp) | | noop w/ OTel API |
|
||||
+----------------+ +---------------+ +------------------+
|
||||
|
||||
The Setup struct holds all configuration parsed from the [telemetry]
|
||||
section of xrpld.cfg. See TelemetryConfig.cpp for the parser and
|
||||
cfg/xrpld-example.cfg for the available options.
|
||||
|
||||
OTel SDK headers are conditionally included behind XRPL_ENABLE_TELEMETRY
|
||||
so that builds without telemetry have zero dependency on opentelemetry-cpp.
|
||||
|
||||
Usage examples:
|
||||
|
||||
1. Root span at a subsystem entry point (typical usage):
|
||||
@code
|
||||
#include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
using namespace xrpl::telemetry;
|
||||
|
||||
// In an RPC handler dispatch:
|
||||
auto guard = SpanGuard::span(
|
||||
TraceCategory::Rpc, rpc_span::prefix::command, commandName);
|
||||
guard.setAttribute(rpc_span::attr::command, commandName);
|
||||
// ... process request
|
||||
// guard destructor automatically ends the span on scope exit
|
||||
@endcode
|
||||
|
||||
2. Child span for a sub-operation (scoped child):
|
||||
@code
|
||||
auto parent = SpanGuard::span(TraceCategory::Transactions, "tx", "process");
|
||||
{
|
||||
auto child = parent.childSpan("tx.apply");
|
||||
child.setAttribute("tx_type", txType);
|
||||
// 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");
|
||||
// both spans end on scope exit
|
||||
@endcode
|
||||
|
||||
4. Cross-thread context propagation:
|
||||
@code
|
||||
// Thread A: capture the active context while span is in scope
|
||||
auto ctx = parentGuard.captureContext();
|
||||
|
||||
// Thread B: create child span with explicit parent
|
||||
auto child = SpanGuard::childSpan("async.work", ctx);
|
||||
@endcode
|
||||
|
||||
@note Thread safety: The Telemetry interface is safe for concurrent reads
|
||||
(isEnabled, shouldTrace*, getTracer, startSpan) after start() completes.
|
||||
setServiceInstanceId() must be called before start() and is not thread-safe.
|
||||
The OTel SDK's TracerProvider and Tracer are internally thread-safe.
|
||||
*/
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/tracer.h>
|
||||
#endif
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
class Telemetry
|
||||
{
|
||||
/** Global singleton pointer, set by start()/stop() in the active
|
||||
implementation. Allows SpanGuard factory methods to access the
|
||||
Telemetry instance without callers passing it explicitly.
|
||||
|
||||
Atomic with acquire/release ordering: start()/stop() store on
|
||||
the initialization thread, factory methods load on worker threads.
|
||||
@see setInstance(), getInstance()
|
||||
*/
|
||||
inline static std::atomic<Telemetry*> instance{nullptr};
|
||||
|
||||
public:
|
||||
/** Get the global Telemetry instance.
|
||||
@return Pointer to the active instance, or nullptr if not started.
|
||||
*/
|
||||
static Telemetry*
|
||||
getInstance()
|
||||
{
|
||||
return instance.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
/** Set the global Telemetry instance.
|
||||
Called by start()/stop() in concrete implementations.
|
||||
Tests can call this with a mock to override the global instance.
|
||||
@param t Pointer to the Telemetry instance, or nullptr to clear.
|
||||
*/
|
||||
static void
|
||||
setInstance(Telemetry* t)
|
||||
{
|
||||
instance.store(t, std::memory_order_release);
|
||||
}
|
||||
|
||||
/** Configuration parsed from the [telemetry] section of xrpld.cfg.
|
||||
|
||||
All fields have sensible defaults so the section can be minimal
|
||||
or omitted entirely. See TelemetryConfig.cpp for the parser.
|
||||
*/
|
||||
struct Setup
|
||||
{
|
||||
/** Master switch: true to enable tracing at runtime. */
|
||||
bool enabled = false;
|
||||
|
||||
/** OTel resource attribute `service.name`. */
|
||||
std::string serviceName = "xrpld";
|
||||
|
||||
/** OTel resource attribute `service.version` (set from BuildInfo). */
|
||||
std::string serviceVersion;
|
||||
|
||||
/** OTel resource attribute `service.instance.id` (defaults to node
|
||||
public key). */
|
||||
std::string serviceInstanceId;
|
||||
|
||||
/** OTLP/HTTP endpoint URL where spans are sent. */
|
||||
std::string exporterEndpoint = "http://localhost:4318/v1/traces";
|
||||
|
||||
/** Whether to use TLS for the exporter connection. */
|
||||
bool useTls = false;
|
||||
|
||||
/** Path to a CA certificate bundle for TLS verification. */
|
||||
std::string tlsCertPath;
|
||||
|
||||
/** Head-based sampling ratio. Intentionally fixed at 1.0 (sample
|
||||
everything) and NOT read from config. A per-node ratio would let
|
||||
nodes make divergent keep/drop decisions for the same distributed
|
||||
trace, producing broken/partial traces. The ratio sampler is wrapped
|
||||
in a ParentBasedSampler (see Telemetry.cpp) so spans inheriting a
|
||||
remote parent honor the upstream sampled flag. Volume reduction is
|
||||
delegated to the collector's tail sampling; for node-local post-hoc
|
||||
dropping see SpanGuard::discard().
|
||||
*/
|
||||
double const samplingRatio = 1.0;
|
||||
|
||||
/** Maximum number of spans per batch export. */
|
||||
std::uint32_t batchSize = 512;
|
||||
|
||||
/** Delay between batch exports. */
|
||||
std::chrono::milliseconds batchDelay{5000};
|
||||
|
||||
/** Maximum number of spans queued before dropping. */
|
||||
std::uint32_t maxQueueSize = 2048;
|
||||
|
||||
/** Network identifier, added as an OTel resource attribute. */
|
||||
std::uint32_t networkId = 0;
|
||||
|
||||
/** Network type label (e.g. "mainnet", "testnet", "devnet"). */
|
||||
std::string networkType = "mainnet";
|
||||
|
||||
/** Enable tracing for transaction processing. */
|
||||
bool traceTransactions = true;
|
||||
|
||||
/** Enable tracing for consensus rounds. */
|
||||
bool traceConsensus = true;
|
||||
|
||||
/** Enable tracing for RPC request handling. */
|
||||
bool traceRpc = true;
|
||||
|
||||
/** Enable tracing for peer-to-peer messages (enabled by default;
|
||||
high volume). */
|
||||
bool tracePeer = true;
|
||||
|
||||
/** Enable tracing for ledger close/accept. */
|
||||
bool traceLedger = true;
|
||||
};
|
||||
|
||||
virtual ~Telemetry() = default;
|
||||
|
||||
/** Update the service instance ID (OTel resource attribute
|
||||
`service.instance.id`).
|
||||
|
||||
Must be called before start(). The node public key is not available
|
||||
when Telemetry is constructed (during the ApplicationImp member
|
||||
initializer list), so this setter allows Application::setup() to
|
||||
inject the identity once nodeIdentity_ is known.
|
||||
|
||||
@param id The node's base58-encoded public key or custom identifier.
|
||||
*/
|
||||
virtual void
|
||||
setServiceInstanceId(std::string const& id)
|
||||
{
|
||||
// Default no-op for NullTelemetry implementations.
|
||||
(void)id;
|
||||
}
|
||||
|
||||
/** Initialize the tracing pipeline (exporter, processor, provider).
|
||||
Call after construction.
|
||||
*/
|
||||
virtual void
|
||||
start() = 0;
|
||||
|
||||
/** Flush pending spans and shut down the tracing pipeline.
|
||||
Call before destruction.
|
||||
*/
|
||||
virtual void
|
||||
stop() = 0;
|
||||
|
||||
/** @return true if this instance is actively exporting spans. */
|
||||
[[nodiscard]] virtual bool
|
||||
isEnabled() const = 0;
|
||||
|
||||
/** @return true if transaction processing should be traced. */
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTraceTransactions() const = 0;
|
||||
|
||||
/** @return true if consensus rounds should be traced. */
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTraceConsensus() const = 0;
|
||||
|
||||
/** @return true if RPC request handling should be traced. */
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTraceRpc() const = 0;
|
||||
|
||||
/** @return true if peer-to-peer messages should be traced. */
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTracePeer() const = 0;
|
||||
|
||||
/** @return true if ledger close/accept should be traced. */
|
||||
[[nodiscard]] virtual bool
|
||||
shouldTraceLedger() const = 0;
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
/** Get or create a named tracer instance.
|
||||
|
||||
@param name Tracer name used to identify the instrumentation library.
|
||||
@return A shared pointer to the Tracer.
|
||||
*/
|
||||
virtual opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer>
|
||||
getTracer(std::string_view name = "xrpld") = 0;
|
||||
|
||||
/** Start a new span on the current thread's context.
|
||||
|
||||
The span becomes a child of the current active span (if any) via
|
||||
OpenTelemetry's context propagation.
|
||||
|
||||
@param name Span name (typically "rpc.command.<cmd>").
|
||||
@param kind The span kind (defaults to kInternal). Possible values:
|
||||
- kInternal: default, in-process operation
|
||||
- kServer: incoming synchronous request (e.g. RPC)
|
||||
- kClient: outgoing synchronous request
|
||||
- kProducer: async message send (e.g. peer broadcast)
|
||||
- kConsumer: async message receive
|
||||
@return A shared pointer to the new Span.
|
||||
*/
|
||||
virtual opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
startSpan(
|
||||
std::string_view name,
|
||||
opentelemetry::trace::SpanKind kind = opentelemetry::trace::SpanKind::kInternal) = 0;
|
||||
|
||||
/** Start a new span with an explicit parent context.
|
||||
|
||||
Use this overload when the parent span is not on the current
|
||||
thread's context stack (e.g. cross-thread trace propagation).
|
||||
|
||||
@param name Span name.
|
||||
@param parentContext The parent span's context.
|
||||
@param kind The span kind (defaults to kInternal).
|
||||
@return A shared pointer to the new Span.
|
||||
*/
|
||||
virtual opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
startSpan(
|
||||
std::string_view name,
|
||||
opentelemetry::context::Context const& parentContext,
|
||||
opentelemetry::trace::SpanKind kind = opentelemetry::trace::SpanKind::kInternal) = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
/** Create a Telemetry instance.
|
||||
|
||||
Returns a TelemetryImpl when setup.enabled is true, or a
|
||||
NullTelemetry no-op stub otherwise.
|
||||
|
||||
@param setup Configuration from the [telemetry] config section.
|
||||
@param journal Journal for log output during initialization.
|
||||
*/
|
||||
std::unique_ptr<Telemetry>
|
||||
makeTelemetry(Telemetry::Setup const& setup, beast::Journal journal);
|
||||
|
||||
/** Parse the [telemetry] config section into a Setup struct.
|
||||
|
||||
@param section The [telemetry] config section.
|
||||
@param nodePublicKey Node public key, used as default instance ID.
|
||||
@param version Build version string.
|
||||
@param networkId Network identifier from [network_id] config
|
||||
(0 = mainnet, 1 = testnet, 2 = devnet).
|
||||
@return A populated Setup struct with defaults for missing values.
|
||||
*/
|
||||
Telemetry::Setup
|
||||
setupTelemetry(
|
||||
Section const& section,
|
||||
std::string const& nodePublicKey,
|
||||
std::string const& version,
|
||||
std::uint32_t networkId);
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
98
include/xrpl/telemetry/TraceContextPropagator.h
Normal file
98
include/xrpl/telemetry/TraceContextPropagator.h
Normal file
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
/** Utilities for trace context propagation across nodes.
|
||||
|
||||
Provides serialization/deserialization of OTel trace context to/from
|
||||
Protocol Buffer TraceContext messages (P2P cross-node propagation).
|
||||
Wired into the P2P message flow via PropagationHelpers.h for
|
||||
TMTransaction messages.
|
||||
|
||||
Only compiled when XRPL_ENABLE_TELEMETRY is defined.
|
||||
|
||||
@see PropagationHelpers.h (high-level inject helpers),
|
||||
TxTracing.h (transaction receive-side extraction).
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/proto/xrpl.pb.h>
|
||||
#include <xrpl/telemetry/TraceContextValidation.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/trace/context.h>
|
||||
#include <opentelemetry/trace/default_span.h>
|
||||
#include <opentelemetry/trace/span_context.h>
|
||||
#include <opentelemetry/trace/trace_flags.h>
|
||||
#include <opentelemetry/trace/trace_id.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/** Extract OTel context from a protobuf TraceContext message.
|
||||
|
||||
@param proto The protobuf TraceContext received from a peer.
|
||||
@return An OTel Context with the extracted parent span, or an empty
|
||||
context if the protobuf fields are missing or invalid.
|
||||
*/
|
||||
inline opentelemetry::context::Context
|
||||
extractFromProtobuf(protocol::TraceContext const& proto)
|
||||
{
|
||||
namespace trace = opentelemetry::trace;
|
||||
|
||||
// Reject malformed or all-zero ids from the peer before trusting
|
||||
// them as a parent. See TraceContextValidation.h.
|
||||
if (!isValidTraceContext(proto))
|
||||
{
|
||||
return opentelemetry::context::Context{};
|
||||
}
|
||||
|
||||
auto const* rawTraceId = reinterpret_cast<std::uint8_t const*>(proto.trace_id().data());
|
||||
auto const* rawSpanId = reinterpret_cast<std::uint8_t const*>(proto.span_id().data());
|
||||
trace::TraceId const traceId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 16>(rawTraceId, 16));
|
||||
trace::SpanId const spanId(opentelemetry::nostd::span<std::uint8_t const, 8>(rawSpanId, 8));
|
||||
trace::TraceFlags const flags(
|
||||
proto.has_trace_flags() ? static_cast<std::uint8_t>(proto.trace_flags())
|
||||
: static_cast<std::uint8_t>(0));
|
||||
|
||||
trace::SpanContext const spanCtx(traceId, spanId, flags, /* remote = */ true);
|
||||
|
||||
return opentelemetry::context::Context{}.SetValue(
|
||||
trace::kSpanKey,
|
||||
opentelemetry::nostd::shared_ptr<trace::Span>(new trace::DefaultSpan(spanCtx)));
|
||||
}
|
||||
|
||||
/** Inject the current span's trace context into a protobuf TraceContext.
|
||||
|
||||
@param ctx The OTel context containing the span to propagate.
|
||||
@param proto The protobuf TraceContext to populate.
|
||||
*/
|
||||
inline void
|
||||
injectToProtobuf(opentelemetry::context::Context const& ctx, protocol::TraceContext& proto)
|
||||
{
|
||||
namespace trace = opentelemetry::trace;
|
||||
|
||||
auto const span = trace::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(), trace::TraceId::kSize);
|
||||
|
||||
// Serialize span_id (8 bytes)
|
||||
auto const& spanId = spanCtx.span_id();
|
||||
proto.set_span_id(spanId.Id().data(), trace::SpanId::kSize);
|
||||
|
||||
// Serialize flags
|
||||
proto.set_trace_flags(spanCtx.trace_flags().flags());
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
93
include/xrpl/telemetry/TraceContextValidation.h
Normal file
93
include/xrpl/telemetry/TraceContextValidation.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
/** Validation predicates for peer-supplied trace context.
|
||||
*
|
||||
* A protobuf TraceContext arrives inside untrusted peer messages
|
||||
* (TMTransaction, TMProposeSet, TMValidation). Before a receiving node
|
||||
* builds a span from it, the context must be checked: a peer can send a
|
||||
* malformed or all-zero trace_id/span_id, either by accident or to
|
||||
* pollute traces. This header is the single place those checks live, so
|
||||
* every receive site agrees on what "valid" means.
|
||||
*
|
||||
* Validity follows the OpenTelemetry spec: a trace_id is 16 bytes, a
|
||||
* span_id is 8 bytes, and an all-zero id is invalid and must not be
|
||||
* used as a parent.
|
||||
*
|
||||
* Dependency diagram:
|
||||
*
|
||||
* protocol::TraceContext (untrusted, from peer)
|
||||
* |
|
||||
* v
|
||||
* isValidTraceContext() / isValidSpanId() / isValidTraceId()
|
||||
* |
|
||||
* +--- valid ----> build child span from peer context
|
||||
* |
|
||||
* +--- invalid --> ignore peer context, start fresh span
|
||||
*
|
||||
* This header depends only on the generated protobuf type. It pulls in
|
||||
* no OpenTelemetry headers, so receive sites that use SpanGuard keep
|
||||
* SpanGuard's encapsulation of OTel types.
|
||||
*
|
||||
* @note Thread-safe: all functions are pure and operate only on their
|
||||
* arguments. No shared state.
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
* // Full parent context (both ids come from the peer):
|
||||
* if (isValidTraceContext(msg.trace_context()))
|
||||
* buildChildSpan(...);
|
||||
*
|
||||
* // Span-only (trace_id is derived locally, not from the peer):
|
||||
* if (tc.has_span_id() && isValidSpanId(tc.span_id()))
|
||||
* buildChildSpan(...);
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
#include <xrpl/proto/xrpl.pb.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/** True if the bytes are a valid OTel trace_id: 16 bytes, not all-zero.
|
||||
*
|
||||
* @param traceId The raw trace_id bytes from a protobuf TraceContext.
|
||||
* @return true if usable as a trace identifier, false otherwise.
|
||||
*/
|
||||
inline bool
|
||||
isValidTraceId(std::string const& traceId)
|
||||
{
|
||||
return traceId.size() == 16 && std::ranges::any_of(traceId, [](char c) { return c != 0; });
|
||||
}
|
||||
|
||||
/** True if the bytes are a valid OTel span_id: 8 bytes, not all-zero.
|
||||
*
|
||||
* @param spanId The raw span_id bytes from a protobuf TraceContext.
|
||||
* @return true if usable as a span identifier, false otherwise.
|
||||
*/
|
||||
inline bool
|
||||
isValidSpanId(std::string const& spanId)
|
||||
{
|
||||
return spanId.size() == 8 && std::ranges::any_of(spanId, [](char c) { return c != 0; });
|
||||
}
|
||||
|
||||
/** True if the context carries a usable parent: a valid trace_id and a
|
||||
* valid span_id together.
|
||||
*
|
||||
* Use this where both ids are taken from the peer (consensus receive,
|
||||
* generic extraction). The transaction path derives its trace_id
|
||||
* locally from the txID, so it checks isValidSpanId() alone instead.
|
||||
*
|
||||
* @param tc The protobuf TraceContext received from a peer.
|
||||
* @return true if both ids are present and valid, false otherwise.
|
||||
*/
|
||||
inline bool
|
||||
isValidTraceContext(protocol::TraceContext const& tc)
|
||||
{
|
||||
return tc.has_trace_id() && isValidTraceId(tc.trace_id()) && tc.has_span_id() &&
|
||||
isValidSpanId(tc.span_id());
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
109
include/xrpl/tx/detail/TxApplySpanNames.h
Normal file
109
include/xrpl/tx/detail/TxApplySpanNames.h
Normal file
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
/** Compile-time span name constants for the transaction apply pipeline.
|
||||
*
|
||||
* Defines the span names and attribute keys used by the three apply-pipeline
|
||||
* stages — preflight, preclaim, and transactor (apply) — that run inside the
|
||||
* library (`src/libxrpl/tx/`). Built on the StaticStr/join() primitives from
|
||||
* <xrpl/telemetry/SpanNames.h>.
|
||||
*
|
||||
* Why a separate header from TxSpanNames.h:
|
||||
* TxSpanNames.h lives under src/xrpld/ (daemon) and serves the overlay/app
|
||||
* lifecycle spans (tx.receive, tx.process). Library code (applySteps.cpp,
|
||||
* Transactor.cpp) must not depend on daemon headers, so the apply-pipeline
|
||||
* constants live here instead. The attribute strings ("tx_type",
|
||||
* "ter_result", "applied") intentionally match TxSpanNames.h so the collector
|
||||
* spanmetrics connector aggregates both sets under the same dimensions.
|
||||
*
|
||||
* Span hierarchy (deterministic trace_id derived from txID[0:16]):
|
||||
*
|
||||
* The three stages run sequentially and often on different threads, so they
|
||||
* do not auto-parent. Each uses a hash-derived trace_id keyed on the same
|
||||
* transaction id, placing all three under one trace without context
|
||||
* propagation. A transaction that hard-fails preflight or preclaim never
|
||||
* reaches the transactor span — the stage attribute identifies where it
|
||||
* stopped.
|
||||
*
|
||||
* +-----------------------------------------------------------+
|
||||
* | trace_id = txID[0:16] |
|
||||
* | |
|
||||
* | +-------------------+ +------------------+ +-------+ |
|
||||
* | | tx.preflight | | tx.preclaim | | tx. | |
|
||||
* | | stage=preflight |-->| stage=preclaim |-->| trans | |
|
||||
* | | tx_type | | tx_type | | actor | |
|
||||
* | | ter_result | | ter_result | | stage=| |
|
||||
* | +-------------------+ +------------------+ | apply | |
|
||||
* | stateless checks ledger-aware checks +-------+ |
|
||||
* | (signature, fields) (sequence, fee) applies |
|
||||
* +-----------------------------------------------------------+
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
* #include <xrpl/tx/detail/TxApplySpanNames.h>
|
||||
* using namespace telemetry;
|
||||
*
|
||||
* // preflight() / preclaim() use hashSpan with a full span name:
|
||||
* auto span = SpanGuard::hashSpan(
|
||||
* TraceCategory::Transactions, tx_apply_span::preflight,
|
||||
* txID.data(), txID.kBytes);
|
||||
* span.setAttribute(tx_apply_span::attr::stage, tx_apply_span::val::preflight);
|
||||
* span.setAttribute(tx_apply_span::attr::terResult, transToken(ter).c_str());
|
||||
* @endcode
|
||||
*
|
||||
* @code
|
||||
* // Transactor::operator() uses span() with prefix + suffix:
|
||||
* auto span = SpanGuard::span(
|
||||
* TraceCategory::Transactions, seg::tx, tx_apply_span::op::transactor);
|
||||
* span.setAttribute(tx_apply_span::attr::stage, tx_apply_span::val::apply);
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
|
||||
namespace xrpl::telemetry::tx_apply_span {
|
||||
|
||||
// ===== Span operation suffixes =============================================
|
||||
|
||||
namespace op {
|
||||
/// "preflight" — stateless transaction checks (suffix form).
|
||||
inline constexpr auto preflight = makeStr("preflight");
|
||||
/// "preclaim" — ledger-aware checks before fee claim (suffix form).
|
||||
inline constexpr auto preclaim = makeStr("preclaim");
|
||||
/// "transactor" — the apply stage (suffix form, used with span()).
|
||||
inline constexpr auto transactor = makeStr("transactor");
|
||||
} // namespace op
|
||||
|
||||
// ===== Full span names (tx.<op>) ===========================================
|
||||
|
||||
/// "tx.preflight" — full name for hashSpan() at the preflight stage.
|
||||
inline constexpr auto preflight = join(seg::tx, op::preflight);
|
||||
/// "tx.preclaim" — full name for hashSpan() at the preclaim stage.
|
||||
inline constexpr auto preclaim = join(seg::tx, op::preclaim);
|
||||
|
||||
// ===== Attribute keys ======================================================
|
||||
|
||||
namespace attr {
|
||||
/// "stage" — which apply-pipeline stage this span represents. Drives the
|
||||
/// collector spanmetrics `stage` dimension for per-stage RED metrics.
|
||||
inline constexpr auto stage = makeStr("stage");
|
||||
/// "tx_type" — transaction type name (e.g., "Payment", "OfferCreate").
|
||||
/// Matches tx_span::attr::txType so both share the spanmetrics dimension.
|
||||
inline constexpr auto txType = makeStr("tx_type");
|
||||
/// "ter_result" — engine result code after the stage (e.g., "tesSUCCESS").
|
||||
inline constexpr auto terResult = makeStr("ter_result");
|
||||
/// "applied" — whether the transaction was applied to the ledger (apply only).
|
||||
inline constexpr auto applied = makeStr("applied");
|
||||
} // namespace attr
|
||||
|
||||
// ===== Attribute values (stage names) ======================================
|
||||
|
||||
namespace val {
|
||||
/// "preflight" — value of the stage attribute on tx.preflight.
|
||||
inline constexpr auto preflight = makeStr("preflight");
|
||||
/// "preclaim" — value of the stage attribute on tx.preclaim.
|
||||
inline constexpr auto preclaim = makeStr("preclaim");
|
||||
/// "apply" — value of the stage attribute on tx.transactor.
|
||||
inline constexpr auto apply = makeStr("apply");
|
||||
} // namespace val
|
||||
|
||||
} // namespace xrpl::telemetry::tx_apply_span
|
||||
137
src/libxrpl/telemetry/NullTelemetry.cpp
Normal file
137
src/libxrpl/telemetry/NullTelemetry.cpp
Normal file
@@ -0,0 +1,137 @@
|
||||
/** No-op implementation of the Telemetry interface.
|
||||
|
||||
Always compiled (regardless of XRPL_ENABLE_TELEMETRY). Provides the
|
||||
makeTelemetry() factory when telemetry is compiled out (#ifndef), which
|
||||
unconditionally returns a NullTelemetry that does nothing.
|
||||
|
||||
When XRPL_ENABLE_TELEMETRY IS defined, the OTel virtual methods
|
||||
(getTracer, startSpan) return noop tracers/spans. The makeTelemetry()
|
||||
factory in this file is not used in that case -- Telemetry.cpp provides
|
||||
its own factory that can return the real TelemetryImpl.
|
||||
*/
|
||||
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/trace/noop.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/tracer.h>
|
||||
|
||||
#include <string_view>
|
||||
#endif
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
/** No-op Telemetry that returns immediately from every method.
|
||||
|
||||
Used as the sole implementation when XRPL_ENABLE_TELEMETRY is not
|
||||
defined, or as a fallback when it is defined but enabled=0.
|
||||
*/
|
||||
class NullTelemetry : public Telemetry
|
||||
{
|
||||
/** Retained configuration (unused, kept for diagnostic access). */
|
||||
Setup const setup_;
|
||||
|
||||
public:
|
||||
explicit NullTelemetry(Setup setup) : setup_(std::move(setup))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
start() override
|
||||
{
|
||||
Telemetry::setInstance(this);
|
||||
}
|
||||
|
||||
void
|
||||
stop() override
|
||||
{
|
||||
Telemetry::setInstance(nullptr);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isEnabled() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceTransactions() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceConsensus() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceRpc() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTracePeer() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceLedger() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer>
|
||||
getTracer(std::string_view) override
|
||||
{
|
||||
static auto noopTracer = opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer>(
|
||||
new opentelemetry::trace::NoopTracer());
|
||||
return noopTracer;
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
startSpan(std::string_view, opentelemetry::trace::SpanKind) override
|
||||
{
|
||||
return opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>(
|
||||
new opentelemetry::trace::NoopSpan(nullptr));
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
startSpan(
|
||||
std::string_view,
|
||||
opentelemetry::context::Context const&,
|
||||
opentelemetry::trace::SpanKind) override
|
||||
{
|
||||
return opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>(
|
||||
new opentelemetry::trace::NoopSpan(nullptr));
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/** Factory used when XRPL_ENABLE_TELEMETRY is not defined.
|
||||
Unconditionally returns a NullTelemetry instance.
|
||||
*/
|
||||
#ifndef XRPL_ENABLE_TELEMETRY
|
||||
std::unique_ptr<Telemetry>
|
||||
makeTelemetry(Telemetry::Setup const& setup, beast::Journal)
|
||||
{
|
||||
return std::make_unique<NullTelemetry>(setup);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
467
src/libxrpl/telemetry/SpanGuard.cpp
Normal file
467
src/libxrpl/telemetry/SpanGuard.cpp
Normal file
@@ -0,0 +1,467 @@
|
||||
/** Pimpl implementation for SpanGuard and SpanContext.
|
||||
|
||||
All OpenTelemetry SDK types are confined to this translation unit.
|
||||
The public SpanGuard.h header contains only standard-library types
|
||||
and forward-declares the Impl struct.
|
||||
|
||||
Static factory methods access the global Telemetry instance via
|
||||
Telemetry::getInstance(), check whether the requested TraceCategory
|
||||
is enabled, and return either an active guard with a real Span+Scope
|
||||
or a null guard whose methods are all no-ops.
|
||||
|
||||
The Impl struct holds the OTel Span (shared_ptr) and Scope.
|
||||
Scope is non-movable, but since Impl lives behind a unique_ptr,
|
||||
SpanGuard's move constructor simply transfers the pointer — no
|
||||
double-Scope issues.
|
||||
|
||||
@see SpanGuard (SpanGuard.h), Telemetry (Telemetry.h),
|
||||
FilteringSpanProcessor (Telemetry.cpp)
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/telemetry/DiscardFlag.h>
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/context/runtime_context.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/nostd/span.h>
|
||||
#include <opentelemetry/trace/context.h>
|
||||
#include <opentelemetry/trace/default_span.h>
|
||||
#include <opentelemetry/trace/scope.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_context.h>
|
||||
#include <opentelemetry/trace/span_id.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/span_startoptions.h>
|
||||
#include <opentelemetry/trace/trace_flags.h>
|
||||
#include <opentelemetry/trace/trace_id.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <typeinfo>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace otel_trace = opentelemetry::trace;
|
||||
|
||||
// ===== SpanContext::Impl ===================================================
|
||||
|
||||
struct SpanContext::Impl
|
||||
{
|
||||
opentelemetry::context::Context ctx;
|
||||
|
||||
explicit Impl(opentelemetry::context::Context c) : ctx(std::move(c))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
SpanContext::SpanContext(std::shared_ptr<Impl> impl) : impl_(std::move(impl))
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
SpanContext::isValid() const
|
||||
{
|
||||
return impl_ != nullptr;
|
||||
}
|
||||
|
||||
// ===== SpanGuard::Impl ====================================================
|
||||
|
||||
struct SpanGuard::Impl
|
||||
{
|
||||
/** The OTel span being guarded. Set to nullptr after discard(). */
|
||||
opentelemetry::nostd::shared_ptr<otel_trace::Span> span;
|
||||
|
||||
/** Scope that activates span on the current thread's context stack. */
|
||||
otel_trace::Scope scope;
|
||||
|
||||
explicit Impl(opentelemetry::nostd::shared_ptr<otel_trace::Span> s)
|
||||
: span(std::move(s)), scope(span)
|
||||
{
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
if (span)
|
||||
span->End();
|
||||
}
|
||||
|
||||
Impl(Impl const&) = delete;
|
||||
Impl&
|
||||
operator=(Impl const&) = delete;
|
||||
Impl(Impl&&) = delete;
|
||||
Impl&
|
||||
operator=(Impl&&) = delete;
|
||||
};
|
||||
|
||||
// ===== SpanGuard core lifecycle ============================================
|
||||
|
||||
SpanGuard::SpanGuard() = default;
|
||||
SpanGuard::~SpanGuard() = default;
|
||||
SpanGuard::SpanGuard(SpanGuard&&) noexcept = default;
|
||||
|
||||
SpanGuard::SpanGuard(std::unique_ptr<Impl> impl) : impl_(std::move(impl))
|
||||
{
|
||||
}
|
||||
|
||||
SpanGuard::
|
||||
operator bool() const
|
||||
{
|
||||
return impl_ != nullptr;
|
||||
}
|
||||
|
||||
// ===== Static factory methods ==============================================
|
||||
|
||||
/** Check whether the given TraceCategory is enabled on the Telemetry instance.
|
||||
@return true if the category's shouldTrace*() flag is on.
|
||||
*/
|
||||
static bool
|
||||
isCategoryEnabled(Telemetry const& tel, TraceCategory cat)
|
||||
{
|
||||
switch (cat)
|
||||
{
|
||||
case TraceCategory::Rpc:
|
||||
return tel.shouldTraceRpc();
|
||||
case TraceCategory::Transactions:
|
||||
return tel.shouldTraceTransactions();
|
||||
case TraceCategory::Consensus:
|
||||
return tel.shouldTraceConsensus();
|
||||
case TraceCategory::Peer:
|
||||
return tel.shouldTracePeer();
|
||||
case TraceCategory::Ledger:
|
||||
return tel.shouldTraceLedger();
|
||||
}
|
||||
return false; // unreachable, silences compiler warning
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Map a TraceCategory to an OTel SpanKind so Tempo's service-graph /
|
||||
// RED metrics see the correct direction. RPC spans are emitted at the
|
||||
// server entry point (handler dispatch), Peer spans at inbound-message
|
||||
// receipt. Transactions / Consensus / Ledger are internal processing
|
||||
// and keep the default kInternal.
|
||||
otel_trace::SpanKind
|
||||
categoryToSpanKind(TraceCategory cat)
|
||||
{
|
||||
switch (cat)
|
||||
{
|
||||
case TraceCategory::Rpc:
|
||||
return otel_trace::SpanKind::kServer;
|
||||
case TraceCategory::Peer:
|
||||
return otel_trace::SpanKind::kConsumer;
|
||||
case TraceCategory::Transactions:
|
||||
case TraceCategory::Consensus:
|
||||
case TraceCategory::Ledger:
|
||||
return otel_trace::SpanKind::kInternal;
|
||||
}
|
||||
return otel_trace::SpanKind::kInternal; // unreachable
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::span(TraceCategory cat, std::string_view prefix, std::string_view name)
|
||||
{
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
return {};
|
||||
auto fullName = std::string(prefix) + "." + std::string(name);
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(fullName, categoryToSpanKind(cat))));
|
||||
}
|
||||
|
||||
// ===== Child / linked span creation ========================================
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::childSpan(std::string_view name) const
|
||||
{
|
||||
if (!impl_)
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled())
|
||||
return {};
|
||||
auto ctx = opentelemetry::context::RuntimeContext::GetCurrent();
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(name, ctx)));
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::childSpan(std::string_view name, SpanContext const& parentCtx)
|
||||
{
|
||||
if (!parentCtx.isValid())
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled())
|
||||
return {};
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(name, parentCtx.impl_->ctx)));
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::linkedSpan(std::string_view name) const
|
||||
{
|
||||
if (!impl_)
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled())
|
||||
return {};
|
||||
|
||||
auto tracer = tel->getTracer("xrpld");
|
||||
auto spanCtx = impl_->span->GetContext();
|
||||
|
||||
// Mark as root span so it starts a new trace sub-tree rather than
|
||||
// inheriting the current thread's active span as parent.
|
||||
otel_trace::StartSpanOptions opts;
|
||||
opentelemetry::context::Context rootCtx;
|
||||
rootCtx = rootCtx.SetValue(otel_trace::kIsRootSpanKey, true);
|
||||
opts.parent = rootCtx;
|
||||
|
||||
// LCOV_EXCL_START
|
||||
return SpanGuard(
|
||||
std::make_unique<Impl>(tracer->StartSpan(
|
||||
std::string(name),
|
||||
{},
|
||||
{{spanCtx, {{std::string(attr::linkType), std::string(attr_val::followsFrom)}}}},
|
||||
opts)));
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::linkedSpan(std::string_view name, SpanContext const& linkCtx)
|
||||
{
|
||||
if (!linkCtx.isValid())
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled())
|
||||
return {};
|
||||
|
||||
auto tracer = tel->getTracer("xrpld");
|
||||
|
||||
// Extract the span from the captured context to get its SpanContext.
|
||||
auto linkSpan = otel_trace::GetSpan(linkCtx.impl_->ctx);
|
||||
if (!linkSpan || !linkSpan->GetContext().IsValid())
|
||||
return {};
|
||||
|
||||
// Mark as root span so it starts a new trace sub-tree rather than
|
||||
// inheriting the current thread's active span as parent.
|
||||
otel_trace::StartSpanOptions opts;
|
||||
opentelemetry::context::Context rootCtx;
|
||||
rootCtx = rootCtx.SetValue(otel_trace::kIsRootSpanKey, true);
|
||||
opts.parent = rootCtx;
|
||||
|
||||
// LCOV_EXCL_START
|
||||
return SpanGuard(
|
||||
std::make_unique<Impl>(tracer->StartSpan(
|
||||
std::string(name),
|
||||
{},
|
||||
{{linkSpan->GetContext(),
|
||||
{{std::string(attr::linkType), std::string(attr_val::followsFrom)}}}},
|
||||
opts)));
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// ===== Hash-derived span (category-gated) ==================================
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::hashSpan(
|
||||
TraceCategory const cat,
|
||||
std::string_view const name,
|
||||
std::uint8_t const* const hashData,
|
||||
std::size_t const hashSize)
|
||||
{
|
||||
if (hashSize < 16)
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
return {};
|
||||
|
||||
otel_trace::TraceId const traceId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 16>(hashData, 16));
|
||||
|
||||
auto const rval = defaultPrng()();
|
||||
std::uint8_t spanIdBytes[8];
|
||||
std::memcpy(spanIdBytes, &rval, sizeof(spanIdBytes));
|
||||
otel_trace::SpanId const spanId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 8>(spanIdBytes, 8));
|
||||
|
||||
otel_trace::SpanContext const syntheticCtx(
|
||||
traceId, spanId, otel_trace::TraceFlags(1), /* remote = */ false);
|
||||
|
||||
auto parentCtx = opentelemetry::context::Context{}.SetValue(
|
||||
otel_trace::kSpanKey,
|
||||
opentelemetry::nostd::shared_ptr<otel_trace::Span>(
|
||||
new otel_trace::DefaultSpan(syntheticCtx)));
|
||||
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(std::string(name), parentCtx)));
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::hashSpan(
|
||||
TraceCategory const cat,
|
||||
std::string_view const name,
|
||||
std::uint8_t const* const hashData,
|
||||
std::size_t const hashSize,
|
||||
std::uint8_t const* const parentSpanId,
|
||||
std::size_t const parentSpanSize,
|
||||
std::uint8_t const traceFlags)
|
||||
{
|
||||
if (hashSize < 16 || parentSpanSize != 8)
|
||||
return {};
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
return {};
|
||||
|
||||
otel_trace::TraceId const traceId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 16>(hashData, 16));
|
||||
|
||||
otel_trace::SpanId const parentSpan(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 8>(parentSpanId, 8));
|
||||
|
||||
otel_trace::SpanContext const combinedCtx(
|
||||
traceId, parentSpan, otel_trace::TraceFlags(traceFlags), /* remote = */ true);
|
||||
|
||||
auto parentCtx = opentelemetry::context::Context{}.SetValue(
|
||||
otel_trace::kSpanKey,
|
||||
opentelemetry::nostd::shared_ptr<otel_trace::Span>(
|
||||
new otel_trace::DefaultSpan(combinedCtx)));
|
||||
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(std::string(name), parentCtx)));
|
||||
}
|
||||
|
||||
// ===== Context capture =====================================================
|
||||
|
||||
SpanContext
|
||||
SpanGuard::captureContext() const
|
||||
{
|
||||
if (!impl_)
|
||||
return {};
|
||||
auto ctx = opentelemetry::context::RuntimeContext::GetCurrent();
|
||||
return SpanContext(std::make_shared<SpanContext::Impl>(ctx));
|
||||
}
|
||||
|
||||
TraceBytes
|
||||
SpanGuard::getTraceBytes() const
|
||||
{
|
||||
if (!impl_ || !impl_->span)
|
||||
return {};
|
||||
|
||||
auto const& spanCtx = impl_->span->GetContext();
|
||||
if (!spanCtx.IsValid())
|
||||
return {};
|
||||
|
||||
TraceBytes result;
|
||||
auto const& tid = spanCtx.trace_id();
|
||||
std::memcpy(result.traceId.data(), tid.Id().data(), 16);
|
||||
auto const& sid = spanCtx.span_id();
|
||||
std::memcpy(result.spanId.data(), sid.Id().data(), 8);
|
||||
result.traceFlags = spanCtx.trace_flags().flags();
|
||||
result.valid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== Attribute setters ===================================================
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, std::string_view value)
|
||||
{
|
||||
if (impl_)
|
||||
{
|
||||
impl_->span->SetAttribute(
|
||||
opentelemetry::nostd::string_view(key.data(), key.size()),
|
||||
opentelemetry::nostd::string_view(value.data(), value.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, char const* value)
|
||||
{
|
||||
setAttribute(key, std::string_view(value));
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, std::int64_t value)
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetAttribute(opentelemetry::nostd::string_view(key.data(), key.size()), value);
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, double value)
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetAttribute(opentelemetry::nostd::string_view(key.data(), key.size()), value);
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setAttribute(std::string_view key, bool value)
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetAttribute(opentelemetry::nostd::string_view(key.data(), key.size()), value);
|
||||
}
|
||||
|
||||
// ===== Status / events =====================================================
|
||||
|
||||
void
|
||||
SpanGuard::setOk()
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetStatus(otel_trace::StatusCode::kOk);
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::setError(std::string_view description)
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->SetStatus(otel_trace::StatusCode::kError, std::string(description));
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::addEvent(std::string_view name)
|
||||
{
|
||||
if (impl_)
|
||||
impl_->span->AddEvent(std::string(name));
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::recordException(std::exception const& e)
|
||||
{
|
||||
if (!impl_)
|
||||
return;
|
||||
impl_->span->AddEvent(
|
||||
"exception",
|
||||
{{"exception.type", typeid(e).name()}, {"exception.message", std::string(e.what())}});
|
||||
impl_->span->SetStatus(otel_trace::StatusCode::kError, e.what());
|
||||
}
|
||||
|
||||
void
|
||||
SpanGuard::discard()
|
||||
{
|
||||
if (impl_)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
436
src/libxrpl/telemetry/Telemetry.cpp
Normal file
436
src/libxrpl/telemetry/Telemetry.cpp
Normal file
@@ -0,0 +1,436 @@
|
||||
/** OpenTelemetry SDK implementation of the Telemetry interface.
|
||||
|
||||
Compiled only when XRPL_ENABLE_TELEMETRY is defined (via CMake
|
||||
telemetry=ON). Contains:
|
||||
|
||||
- FilteringSpanProcessor: decorator that drops spans marked with
|
||||
kDiscardedAttr before they enter the batch export queue.
|
||||
- TelemetryImpl: configures the OTel SDK with an OTLP/HTTP exporter,
|
||||
FilteringSpanProcessor wrapping a batch span processor,
|
||||
trace-ID-ratio sampler, and resource attributes.
|
||||
- NullTelemetryOtel: no-op fallback used when telemetry is compiled in
|
||||
but disabled at runtime (enabled=0 in config).
|
||||
- makeTelemetry(): factory that selects the appropriate implementation.
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/telemetry/DiscardFlag.h>
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/exporters/otlp/otlp_http_exporter_factory.h>
|
||||
#include <opentelemetry/exporters/otlp/otlp_http_exporter_options.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/sdk/resource/resource.h>
|
||||
#include <opentelemetry/sdk/trace/batch_span_processor_factory.h>
|
||||
#include <opentelemetry/sdk/trace/batch_span_processor_options.h>
|
||||
#include <opentelemetry/sdk/trace/processor.h>
|
||||
#include <opentelemetry/sdk/trace/sampler.h>
|
||||
#include <opentelemetry/sdk/trace/samplers/parent_factory.h>
|
||||
#include <opentelemetry/sdk/trace/samplers/trace_id_ratio.h>
|
||||
#include <opentelemetry/sdk/trace/tracer_provider.h>
|
||||
#include <opentelemetry/sdk/trace/tracer_provider_factory.h>
|
||||
#include <opentelemetry/semconv/incubating/service_attributes.h>
|
||||
#include <opentelemetry/trace/noop.h>
|
||||
#include <opentelemetry/trace/provider.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/span_startoptions.h>
|
||||
#include <opentelemetry/trace/tracer.h>
|
||||
#include <opentelemetry/trace/tracer_provider.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace trace_api = opentelemetry::trace;
|
||||
namespace trace_sdk = opentelemetry::sdk::trace;
|
||||
namespace otlp_http = opentelemetry::exporter::otlp;
|
||||
namespace resource = opentelemetry::sdk::resource;
|
||||
|
||||
/** SpanProcessor decorator that drops discarded spans.
|
||||
|
||||
Wraps a delegate processor (typically BatchSpanProcessor). In OnEnd(),
|
||||
checks the gTlDiscardCurrentSpan thread-local flag. If set (by
|
||||
SpanGuard::discard()), the span is silently dropped — never entering
|
||||
the batch queue, never sent over the network, never stored.
|
||||
|
||||
Uses a thread-local flag rather than inspecting Recordable attributes
|
||||
because the Recordable type varies by exporter (SpanData for simple
|
||||
exporters, OtlpRecordable for OTLP) and none expose a uniform getter.
|
||||
The flag is safe because Span::End() calls OnEnd() synchronously on
|
||||
the same thread.
|
||||
|
||||
All other methods delegate directly to the wrapped processor.
|
||||
|
||||
Dependency diagram:
|
||||
|
||||
+---------------------------+
|
||||
| FilteringSpanProcessor |
|
||||
+---------------------------+
|
||||
| - delegate_ : unique_ptr |
|
||||
| <SpanProcessor> |
|
||||
+---------------------------+
|
||||
| wraps
|
||||
+---------+-----------+
|
||||
| BatchSpanProcessor |
|
||||
+---------------------+
|
||||
|
||||
@note Thread safety: OnEnd() may be called concurrently from multiple
|
||||
threads. The gTlDiscardCurrentSpan flag is thread-local, so each
|
||||
thread's discard state is independent — no synchronization needed.
|
||||
*/
|
||||
class FilteringSpanProcessor : public trace_sdk::SpanProcessor
|
||||
{
|
||||
std::unique_ptr<trace_sdk::SpanProcessor> delegate_;
|
||||
|
||||
public:
|
||||
explicit FilteringSpanProcessor(std::unique_ptr<trace_sdk::SpanProcessor> delegate)
|
||||
: delegate_(std::move(delegate))
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<trace_sdk::Recordable>
|
||||
MakeRecordable() noexcept override
|
||||
{
|
||||
return delegate_->MakeRecordable();
|
||||
}
|
||||
|
||||
void
|
||||
OnStart(
|
||||
trace_sdk::Recordable& span,
|
||||
opentelemetry::trace::SpanContext const& parentContext) noexcept override
|
||||
{
|
||||
delegate_->OnStart(span, parentContext);
|
||||
}
|
||||
|
||||
void
|
||||
OnEnd(std::unique_ptr<trace_sdk::Recordable>&& span) noexcept override
|
||||
{
|
||||
if (gTlDiscardCurrentSpan)
|
||||
{
|
||||
// SpanGuard::discard() set the flag on this thread just before
|
||||
// calling Span::End(), which invokes OnEnd() synchronously.
|
||||
// Drop the span.
|
||||
return;
|
||||
}
|
||||
delegate_->OnEnd(std::move(span));
|
||||
}
|
||||
|
||||
bool
|
||||
ForceFlush(
|
||||
std::chrono::microseconds timeout = (std::chrono::microseconds::max)()) noexcept override
|
||||
{
|
||||
return delegate_->ForceFlush(timeout);
|
||||
}
|
||||
|
||||
bool
|
||||
Shutdown(
|
||||
std::chrono::microseconds timeout = (std::chrono::microseconds::max)()) noexcept override
|
||||
{
|
||||
return delegate_->Shutdown(timeout);
|
||||
}
|
||||
};
|
||||
|
||||
/** No-op implementation used when XRPL_ENABLE_TELEMETRY is defined but
|
||||
setup.enabled is false at runtime.
|
||||
|
||||
Lives in the anonymous namespace so there is no ODR conflict with the
|
||||
NullTelemetry in NullTelemetry.cpp.
|
||||
*/
|
||||
class NullTelemetryOtel : public Telemetry
|
||||
{
|
||||
/** Retained configuration (unused, kept for diagnostic access). */
|
||||
Setup const setup_;
|
||||
|
||||
public:
|
||||
explicit NullTelemetryOtel(Setup setup) : setup_(std::move(setup))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
start() override
|
||||
{
|
||||
Telemetry::setInstance(this);
|
||||
}
|
||||
|
||||
void
|
||||
stop() override
|
||||
{
|
||||
Telemetry::setInstance(nullptr);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isEnabled() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceTransactions() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceConsensus() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceRpc() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTracePeer() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceLedger() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Tracer>
|
||||
getTracer(std::string_view) override
|
||||
{
|
||||
static auto noopTracer =
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Tracer>(new trace_api::NoopTracer());
|
||||
return noopTracer;
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
startSpan(std::string_view, trace_api::SpanKind) override
|
||||
{
|
||||
return opentelemetry::nostd::shared_ptr<trace_api::Span>(new trace_api::NoopSpan(nullptr));
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
startSpan(std::string_view, opentelemetry::context::Context const&, trace_api::SpanKind)
|
||||
override
|
||||
{
|
||||
return opentelemetry::nostd::shared_ptr<trace_api::Span>(new trace_api::NoopSpan(nullptr));
|
||||
}
|
||||
};
|
||||
|
||||
/** Full OTel SDK implementation that exports trace spans via OTLP/HTTP.
|
||||
|
||||
Configures an OTLP/HTTP exporter, batch span processor,
|
||||
TraceIdRatioBasedSampler, and resource attributes on start().
|
||||
*/
|
||||
class TelemetryImpl : public Telemetry
|
||||
{
|
||||
/** Configuration from the [telemetry] config section.
|
||||
Non-const so setServiceInstanceId() can update the instance ID
|
||||
before start() creates the OTel resource.
|
||||
*/
|
||||
Setup setup_;
|
||||
|
||||
/** Journal used for log output during start/stop. */
|
||||
beast::Journal const journal_;
|
||||
|
||||
/** The SDK TracerProvider that owns the export pipeline.
|
||||
|
||||
Held as std::shared_ptr so we can call ForceFlush() on shutdown.
|
||||
Wrapped in a nostd::shared_ptr when registered as the global provider.
|
||||
*/
|
||||
std::shared_ptr<trace_sdk::TracerProvider> sdkProvider_;
|
||||
|
||||
public:
|
||||
TelemetryImpl(Setup setup, beast::Journal journal) : setup_(std::move(setup)), journal_(journal)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
setServiceInstanceId(std::string const& id) override
|
||||
{
|
||||
setup_.serviceInstanceId = id;
|
||||
}
|
||||
|
||||
void
|
||||
start() override
|
||||
{
|
||||
JLOG(journal_.info()) << "Telemetry starting: endpoint=" << setup_.exporterEndpoint
|
||||
<< " sampling=" << setup_.samplingRatio;
|
||||
|
||||
// Configure OTLP HTTP exporter
|
||||
otlp_http::OtlpHttpExporterOptions exporterOpts;
|
||||
exporterOpts.url = setup_.exporterEndpoint;
|
||||
if (setup_.useTls)
|
||||
exporterOpts.ssl_ca_cert_path = setup_.tlsCertPath;
|
||||
|
||||
auto exporter = otlp_http::OtlpHttpExporterFactory::Create(exporterOpts);
|
||||
|
||||
// Configure batch processor
|
||||
trace_sdk::BatchSpanProcessorOptions processorOpts;
|
||||
processorOpts.max_queue_size = setup_.maxQueueSize;
|
||||
processorOpts.schedule_delay_millis = std::chrono::milliseconds(setup_.batchDelay);
|
||||
processorOpts.max_export_batch_size = setup_.batchSize;
|
||||
|
||||
auto batchProcessor =
|
||||
trace_sdk::BatchSpanProcessorFactory::Create(std::move(exporter), processorOpts);
|
||||
|
||||
// Wrap batch processor with filtering processor that drops spans
|
||||
// marked with kDiscardedAttr (via SpanGuard::discard()).
|
||||
auto processor = std::make_unique<FilteringSpanProcessor>(std::move(batchProcessor));
|
||||
|
||||
// Configure resource attributes
|
||||
auto resourceAttrs = resource::Resource::Create({
|
||||
{opentelemetry::semconv::service::kServiceName, setup_.serviceName},
|
||||
{opentelemetry::semconv::service::kServiceVersion, setup_.serviceVersion},
|
||||
{opentelemetry::semconv::service::kServiceInstanceId, setup_.serviceInstanceId},
|
||||
{std::string(attr::networkId),
|
||||
static_cast<int64_t>(setup_.networkId)}, // LCOV_EXCL_LINE
|
||||
{std::string(attr::networkType), setup_.networkType}, // LCOV_EXCL_LINE
|
||||
});
|
||||
|
||||
// Configure sampler. Head sampling is fixed at 1.0 (sample everything);
|
||||
// setup_.samplingRatio is not config-driven. Wrap the ratio sampler in a
|
||||
// ParentBasedSampler so spans with a remote parent honor the upstream
|
||||
// sampled flag — this keeps keep/drop decisions coherent for a single
|
||||
// distributed trace spanning multiple nodes. Volume reduction is left to
|
||||
// the collector's tail sampling.
|
||||
auto rootSampler =
|
||||
std::make_shared<trace_sdk::TraceIdRatioBasedSampler>(setup_.samplingRatio);
|
||||
auto sampler = trace_sdk::ParentBasedSamplerFactory::Create(std::move(rootSampler));
|
||||
|
||||
// Create TracerProvider
|
||||
sdkProvider_ = trace_sdk::TracerProviderFactory::Create(
|
||||
std::move(processor), resourceAttrs, std::move(sampler));
|
||||
|
||||
// Set as global provider
|
||||
trace_api::Provider::SetTracerProvider(
|
||||
opentelemetry::nostd::shared_ptr<trace_api::TracerProvider>(sdkProvider_));
|
||||
|
||||
// Register as the global Telemetry instance so SpanGuard factory
|
||||
// methods can access it without callers passing a reference.
|
||||
Telemetry::setInstance(this);
|
||||
|
||||
JLOG(journal_.info()) << "Telemetry started successfully";
|
||||
}
|
||||
|
||||
void
|
||||
stop() override
|
||||
{
|
||||
JLOG(journal_.info()) << "Telemetry stopping";
|
||||
|
||||
// Unregister global instance before tearing down the pipeline.
|
||||
Telemetry::setInstance(nullptr);
|
||||
|
||||
if (sdkProvider_)
|
||||
{
|
||||
// Force flush with timeout to avoid blocking indefinitely
|
||||
// when the OTLP endpoint is unreachable.
|
||||
sdkProvider_->ForceFlush(std::chrono::milliseconds(5000));
|
||||
// TODO: sdkProvider_ is not thread-safe. This reset() races with
|
||||
// getTracer() if any thread is still calling startSpan().
|
||||
// Currently safe because Application::stop() shuts down
|
||||
// serverHandler_, overlay_, and jobQueue_ before calling
|
||||
// telemetry_->stop() — so no callers should remain. If the
|
||||
// shutdown order ever changes, add an std::atomic<bool> stopped_
|
||||
// flag checked in getTracer() to make this robust.
|
||||
sdkProvider_.reset();
|
||||
trace_api::Provider::SetTracerProvider(
|
||||
opentelemetry::nostd::shared_ptr<trace_api::TracerProvider>(
|
||||
new trace_api::NoopTracerProvider()));
|
||||
}
|
||||
JLOG(journal_.info()) << "Telemetry stopped";
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isEnabled() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceTransactions() const override
|
||||
{
|
||||
return setup_.traceTransactions;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceConsensus() const override
|
||||
{
|
||||
return setup_.traceConsensus;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceRpc() const override
|
||||
{
|
||||
return setup_.traceRpc;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTracePeer() const override
|
||||
{
|
||||
return setup_.tracePeer;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldTraceLedger() const override
|
||||
{
|
||||
return setup_.traceLedger;
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Tracer>
|
||||
getTracer(std::string_view name) override
|
||||
{
|
||||
if (!sdkProvider_)
|
||||
return trace_api::Provider::GetTracerProvider()->GetTracer(std::string(name));
|
||||
return sdkProvider_->GetTracer(std::string(name));
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
startSpan(std::string_view name, trace_api::SpanKind kind) override
|
||||
{
|
||||
auto tracer = getTracer("xrpld");
|
||||
trace_api::StartSpanOptions opts;
|
||||
opts.kind = kind;
|
||||
return tracer->StartSpan(std::string(name), opts);
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
startSpan(
|
||||
std::string_view name,
|
||||
opentelemetry::context::Context const& parentContext,
|
||||
trace_api::SpanKind kind) override
|
||||
{
|
||||
auto tracer = getTracer("xrpld");
|
||||
trace_api::StartSpanOptions opts;
|
||||
opts.kind = kind;
|
||||
opts.parent = parentContext;
|
||||
return tracer->StartSpan(std::string(name), opts);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Telemetry>
|
||||
makeTelemetry(Telemetry::Setup const& setup, beast::Journal journal)
|
||||
{
|
||||
if (setup.enabled)
|
||||
return std::make_unique<TelemetryImpl>(setup, journal);
|
||||
return std::make_unique<NullTelemetryOtel>(setup);
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
124
src/libxrpl/telemetry/TelemetryConfig.cpp
Normal file
124
src/libxrpl/telemetry/TelemetryConfig.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
/** Parser for the [telemetry] section of xrpld.cfg.
|
||||
|
||||
Reads configuration values from the config file and populates a
|
||||
Telemetry::Setup struct. All options have sensible defaults so the
|
||||
section can be minimal or omitted entirely.
|
||||
|
||||
See cfg/xrpld-example.cfg for the full list of available options.
|
||||
*/
|
||||
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
/** Config key names for the [telemetry] section.
|
||||
|
||||
Each must match the corresponding option documented in
|
||||
cfg/xrpld-example.cfg verbatim. Defined as `char const*` so they
|
||||
pass to Section::valueOr() (which takes `std::string const&`)
|
||||
without an explicit conversion, exactly as a literal would.
|
||||
*/
|
||||
namespace key {
|
||||
constexpr char const* enabled = "enabled";
|
||||
constexpr char const* serviceName = "service_name";
|
||||
constexpr char const* serviceInstanceId = "service_instance_id";
|
||||
constexpr char const* endpoint = "endpoint";
|
||||
constexpr char const* useTls = "use_tls";
|
||||
constexpr char const* tlsCaCert = "tls_ca_cert";
|
||||
constexpr char const* batchSize = "batch_size";
|
||||
constexpr char const* batchDelayMs = "batch_delay_ms";
|
||||
constexpr char const* maxQueueSize = "max_queue_size";
|
||||
constexpr char const* traceTransactions = "trace_transactions";
|
||||
constexpr char const* traceConsensus = "trace_consensus";
|
||||
constexpr char const* traceRpc = "trace_rpc";
|
||||
constexpr char const* tracePeer = "trace_peer";
|
||||
constexpr char const* traceLedger = "trace_ledger";
|
||||
} // namespace key
|
||||
|
||||
/** Default values applied when a key is absent from the config.
|
||||
|
||||
@note serviceName mirrors SystemParameters' systemName() ("xrpld") but
|
||||
is duplicated here as a literal: the telemetry module deliberately does
|
||||
not link xrpl.libxrpl.protocol, so including SystemParameters.h would
|
||||
introduce an undeclared cross-module dependency.
|
||||
*/
|
||||
namespace dflt {
|
||||
constexpr char const* serviceName = "xrpld";
|
||||
constexpr char const* endpoint = "http://localhost:4318/v1/traces";
|
||||
constexpr std::uint32_t batchSize = 512u;
|
||||
constexpr std::uint32_t batchDelayMs = 5000u;
|
||||
constexpr std::uint32_t maxQueueSize = 2048u;
|
||||
} // namespace dflt
|
||||
|
||||
/** Derive a human-readable network type label from the numeric network ID.
|
||||
@param networkId The network identifier from [network_id] config.
|
||||
@return "mainnet", "testnet", "devnet", or "unknown" for other values.
|
||||
*/
|
||||
std::string
|
||||
networkTypeFromId(std::uint32_t networkId)
|
||||
{
|
||||
switch (networkId)
|
||||
{
|
||||
case 0:
|
||||
return "mainnet";
|
||||
case 1:
|
||||
return "testnet";
|
||||
case 2:
|
||||
return "devnet";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Telemetry::Setup
|
||||
setupTelemetry(
|
||||
Section const& section,
|
||||
std::string const& nodePublicKey,
|
||||
std::string const& version,
|
||||
std::uint32_t networkId)
|
||||
{
|
||||
Telemetry::Setup setup;
|
||||
|
||||
setup.enabled = section.valueOr<int>(key::enabled, 0) != 0;
|
||||
setup.serviceName = section.valueOr<std::string>(key::serviceName, dflt::serviceName);
|
||||
setup.serviceVersion = version;
|
||||
setup.serviceInstanceId = section.valueOr<std::string>(key::serviceInstanceId, nodePublicKey);
|
||||
|
||||
setup.exporterEndpoint = section.valueOr<std::string>(key::endpoint, dflt::endpoint);
|
||||
|
||||
setup.useTls = section.valueOr<int>(key::useTls, 0) != 0;
|
||||
setup.tlsCertPath = section.valueOr<std::string>(key::tlsCaCert, "");
|
||||
|
||||
// Head sampling is intentionally fixed at 1.0 (sample everything) and is
|
||||
// not read from config. A per-node ratio would let nodes make divergent
|
||||
// keep/drop decisions for the same distributed trace, producing broken
|
||||
// traces; volume reduction is delegated to the collector's tail sampling.
|
||||
// setup.samplingRatio is a const member fixed at 1.0; nothing to parse.
|
||||
|
||||
setup.batchSize = section.valueOr<std::uint32_t>(key::batchSize, dflt::batchSize);
|
||||
setup.batchDelay = std::chrono::milliseconds{
|
||||
section.valueOr<std::uint32_t>(key::batchDelayMs, dflt::batchDelayMs)};
|
||||
setup.maxQueueSize = section.valueOr<std::uint32_t>(key::maxQueueSize, dflt::maxQueueSize);
|
||||
|
||||
setup.networkId = networkId;
|
||||
setup.networkType = networkTypeFromId(networkId);
|
||||
|
||||
setup.traceTransactions = section.valueOr<int>(key::traceTransactions, 1) != 0;
|
||||
setup.traceConsensus = section.valueOr<int>(key::traceConsensus, 1) != 0;
|
||||
setup.traceRpc = section.valueOr<int>(key::traceRpc, 1) != 0;
|
||||
setup.tracePeer = section.valueOr<int>(key::tracePeer, 1) != 0;
|
||||
setup.traceLedger = section.valueOr<int>(key::traceLedger, 1) != 0;
|
||||
|
||||
return setup;
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
@@ -34,13 +34,17 @@
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/SignerEntries.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
#include <xrpl/tx/detail/TxApplySpanNames.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -1192,6 +1196,15 @@ Transactor::checkInvariants(TER result, XRPAmount fee)
|
||||
ApplyResult
|
||||
Transactor::operator()()
|
||||
{
|
||||
auto span = telemetry::SpanGuard::span(
|
||||
telemetry::TraceCategory::Transactions,
|
||||
telemetry::seg::tx,
|
||||
telemetry::tx_apply_span::op::transactor);
|
||||
// "apply" — the third apply-pipeline stage, after preflight and preclaim.
|
||||
span.setAttribute(telemetry::tx_apply_span::attr::stage, telemetry::tx_apply_span::val::apply);
|
||||
if (auto const* fmt = TxFormats::getInstance().findByType(ctx_.tx.getTxnType()))
|
||||
span.setAttribute(telemetry::tx_apply_span::attr::txType, fmt->getName().c_str());
|
||||
|
||||
JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID();
|
||||
|
||||
// These global updates really should have been for every Transaction
|
||||
@@ -1418,6 +1431,9 @@ Transactor::operator()()
|
||||
|
||||
JLOG(j_.trace()) << (applied ? "applied " : "not applied ") << transToken(result);
|
||||
|
||||
span.setAttribute(telemetry::tx_apply_span::attr::terResult, transToken(result).c_str());
|
||||
span.setAttribute(telemetry::tx_apply_span::attr::applied, applied);
|
||||
|
||||
return {result, applied, metadata};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,16 @@
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/detail/TxApplySpanNames.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#pragma push_macro("TRANSACTION")
|
||||
#undef TRANSACTION
|
||||
@@ -51,6 +54,47 @@ struct UnknownTxnType : std::exception
|
||||
}
|
||||
};
|
||||
|
||||
/** Look up the human-readable transaction type name for span attributes.
|
||||
* Returns nullptr if the type is unknown so the caller can skip the
|
||||
* attribute rather than emit an empty value.
|
||||
*/
|
||||
char const*
|
||||
txTypeName(TxType txnType)
|
||||
{
|
||||
if (auto const* fmt = TxFormats::getInstance().findByType(txnType))
|
||||
return fmt->getName().c_str();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/** Create a deterministic-trace span for an apply-pipeline stage.
|
||||
*
|
||||
* The trace_id is derived from txID[0:16] so the preflight, preclaim, and
|
||||
* transactor spans of one transaction share a trace even though they run
|
||||
* sequentially and often on different threads. Sets the stage, tx_type, and
|
||||
* (after the stage runs) ter_result attributes that drive the collector
|
||||
* spanmetrics dimensions. A no-op when telemetry is disabled.
|
||||
*
|
||||
* @param name Full span name (tx_apply_span::preflight / ::preclaim).
|
||||
* @param stage Stage attribute value (tx_apply_span::val::*).
|
||||
* @param tx The transaction supplying the id and type.
|
||||
*/
|
||||
[[nodiscard]] telemetry::SpanGuard
|
||||
makeStageSpan(std::string_view name, std::string_view stage, STTx const& tx)
|
||||
{
|
||||
auto const txID = tx.getTransactionID();
|
||||
auto span = telemetry::SpanGuard::hashSpan(
|
||||
telemetry::TraceCategory::Transactions, name, txID.data(), txID.kBytes);
|
||||
// Guard the type lookup behind the active check: preflight runs for every
|
||||
// transaction, so findByType() must not run when tracing is off/disabled.
|
||||
if (span)
|
||||
{
|
||||
span.setAttribute(telemetry::tx_apply_span::attr::stage, stage);
|
||||
if (char const* typeName = txTypeName(tx.getTxnType()))
|
||||
span.setAttribute(telemetry::tx_apply_span::attr::txType, typeName);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
// Call a lambda with the concrete transaction type as a template parameter
|
||||
// throw an "UnknownTxnType" exception on error
|
||||
template <class F>
|
||||
@@ -133,82 +177,122 @@ consequencesHelper(PreflightContext const& ctx)
|
||||
static std::pair<NotTEC, TxConsequences>
|
||||
invokePreflight(PreflightContext const& ctx)
|
||||
{
|
||||
// Trace the preflight stage. The span shares the transaction's
|
||||
// deterministic trace_id so it correlates with preclaim and transactor.
|
||||
auto span = makeStageSpan(
|
||||
telemetry::tx_apply_span::preflight, telemetry::tx_apply_span::val::preflight, ctx.tx);
|
||||
try
|
||||
{
|
||||
return withTxnType(ctx.rules, ctx.tx.getTxnType(), [&]<typename T>() {
|
||||
auto result = withTxnType(ctx.rules, ctx.tx.getTxnType(), [&]<typename T>() {
|
||||
auto const tec = Transactor::invokePreflight<T>(ctx);
|
||||
return std::make_pair(
|
||||
tec, isTesSuccess(tec) ? consequencesHelper<T>(ctx) : TxConsequences{tec});
|
||||
});
|
||||
if (span)
|
||||
{
|
||||
span.setAttribute(
|
||||
telemetry::tx_apply_span::attr::terResult, transToken(result.first).c_str());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (UnknownTxnType const& e)
|
||||
{
|
||||
// Should never happen
|
||||
// LCOV_EXCL_START
|
||||
JLOG(ctx.j.fatal()) << "Unknown transaction type in preflight: " << e.txnType;
|
||||
span.recordException(e);
|
||||
UNREACHABLE("xrpl::invokePreflight : unknown transaction type");
|
||||
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
// The caller's preflight() maps this to tefEXCEPTION. Record it on the
|
||||
// span before unwinding so per-stage error counts include exceptions.
|
||||
span.setAttribute(
|
||||
telemetry::tx_apply_span::attr::terResult, transToken(tefEXCEPTION).c_str());
|
||||
span.recordException(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
static TER
|
||||
invokePreclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// Trace the preclaim stage under the transaction's deterministic trace_id.
|
||||
auto span = makeStageSpan(
|
||||
telemetry::tx_apply_span::preclaim, telemetry::tx_apply_span::val::preclaim, ctx.tx);
|
||||
try
|
||||
{
|
||||
// use name hiding to accomplish compile-time polymorphism of static
|
||||
// class functions for Transactor and derived classes.
|
||||
return withTxnType(ctx.view.rules(), ctx.tx.getTxnType(), [&]<typename T>() -> TER {
|
||||
// preclaim functionality is divided into two sections:
|
||||
// 1. Up to and including the signature check: returns NotTEC.
|
||||
// All transaction checks before and including checkSign
|
||||
// MUST return NotTEC, or something more restrictive.
|
||||
// Allowing tec results in these steps risks theft or
|
||||
// destruction of funds, as a fee will be charged before the
|
||||
// signature is checked.
|
||||
// 2. After the signature check: returns TER.
|
||||
TER const preclaimTer =
|
||||
withTxnType(ctx.view.rules(), ctx.tx.getTxnType(), [&]<typename T>() -> TER {
|
||||
// preclaim functionality is divided into two sections:
|
||||
// 1. Up to and including the signature check: returns NotTEC.
|
||||
// All transaction checks before and including checkSign
|
||||
// MUST return NotTEC, or something more restrictive.
|
||||
// Allowing tec results in these steps risks theft or
|
||||
// destruction of funds, as a fee will be charged before the
|
||||
// signature is checked.
|
||||
// 2. After the signature check: returns TER.
|
||||
|
||||
// If the transactor requires a valid account and the
|
||||
// transaction doesn't list one, preflight will have already
|
||||
// a flagged a failure.
|
||||
auto const id = ctx.tx.getAccountID(sfAccount);
|
||||
// If the transactor requires a valid account and the
|
||||
// transaction doesn't list one, preflight will have already
|
||||
// a flagged a failure.
|
||||
auto const id = ctx.tx.getAccountID(sfAccount);
|
||||
|
||||
if (id != beast::kZero)
|
||||
{
|
||||
if (NotTEC const preSigResult = [&]() -> NotTEC {
|
||||
if (NotTEC const result = T::checkSeqProxy(ctx.view, ctx.tx, ctx.j))
|
||||
return result;
|
||||
if (id != beast::kZero)
|
||||
{
|
||||
if (NotTEC const preSigResult = [&]() -> NotTEC {
|
||||
if (NotTEC const result = T::checkSeqProxy(ctx.view, ctx.tx, ctx.j))
|
||||
return result;
|
||||
|
||||
if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx))
|
||||
return result;
|
||||
if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx))
|
||||
return result;
|
||||
|
||||
if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx))
|
||||
return result;
|
||||
if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx))
|
||||
return result;
|
||||
|
||||
if (NotTEC const result = T::checkSign(ctx))
|
||||
return result;
|
||||
if (NotTEC const result = T::checkSign(ctx))
|
||||
return result;
|
||||
|
||||
return tesSUCCESS;
|
||||
}())
|
||||
return preSigResult;
|
||||
return tesSUCCESS;
|
||||
}())
|
||||
return preSigResult;
|
||||
|
||||
if (TER const result = T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx)))
|
||||
return result;
|
||||
}
|
||||
if (TER const result = T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx)))
|
||||
return result;
|
||||
}
|
||||
|
||||
return T::preclaim(ctx);
|
||||
});
|
||||
return T::preclaim(ctx);
|
||||
});
|
||||
if (span)
|
||||
{
|
||||
span.setAttribute(
|
||||
telemetry::tx_apply_span::attr::terResult, transToken(preclaimTer).c_str());
|
||||
}
|
||||
return preclaimTer;
|
||||
}
|
||||
catch (UnknownTxnType const& e)
|
||||
{
|
||||
// Should never happen
|
||||
// LCOV_EXCL_START
|
||||
JLOG(ctx.j.fatal()) << "Unknown transaction type in preclaim: " << e.txnType;
|
||||
span.recordException(e);
|
||||
UNREACHABLE("xrpl::invokePreclaim : unknown transaction type");
|
||||
return temUNKNOWN;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
// The caller's preclaim() maps this to tefEXCEPTION. Record it on the
|
||||
// span before unwinding so per-stage error counts include exceptions.
|
||||
span.setAttribute(
|
||||
telemetry::tx_apply_span::attr::terResult, transToken(tefEXCEPTION).c_str());
|
||||
span.recordException(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,7 @@ set(test_modules
|
||||
json
|
||||
tx
|
||||
protocol_autogen
|
||||
telemetry
|
||||
)
|
||||
if(NOT WIN32)
|
||||
list(APPEND test_modules net)
|
||||
@@ -53,4 +54,13 @@ foreach(module IN LISTS test_modules)
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# The telemetry test module exercises SpanGuard/Telemetry, which link against
|
||||
# the OpenTelemetry SDK when the telemetry build option is enabled.
|
||||
if(telemetry)
|
||||
target_link_libraries(
|
||||
xrpl_tests
|
||||
PRIVATE opentelemetry-cpp::opentelemetry-cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
gtest_discover_tests(xrpl_tests DISCOVERY_TIMEOUT 60)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PendingSaves.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
@@ -327,6 +328,12 @@ public:
|
||||
throw std::logic_error("TestServiceRegistry::getPerfLog() not implemented");
|
||||
}
|
||||
|
||||
telemetry::Telemetry&
|
||||
getTelemetry() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getTelemetry() not implemented");
|
||||
}
|
||||
|
||||
// Configuration and state
|
||||
bool
|
||||
isStopping() const override
|
||||
|
||||
82
src/tests/libxrpl/telemetry/SpanGuardFactory.cpp
Normal file
82
src/tests/libxrpl/telemetry/SpanGuardFactory.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
using namespace xrpl;
|
||||
using namespace xrpl::telemetry;
|
||||
|
||||
TEST(SpanGuardFactory, null_guard_methods_are_safe)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "nonexistent");
|
||||
EXPECT_FALSE(span);
|
||||
|
||||
span.setAttribute("key", "value");
|
||||
span.setAttribute("int_key", static_cast<int64_t>(42));
|
||||
span.setAttribute("bool_key", true);
|
||||
span.setOk();
|
||||
span.setError("test");
|
||||
span.addEvent("event");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
TEST(SpanGuardFactory, child_span_null_when_no_parent)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "parent");
|
||||
auto child = span.childSpan("child.test");
|
||||
EXPECT_FALSE(child);
|
||||
}
|
||||
|
||||
TEST(SpanGuardFactory, linked_span_null_when_no_context)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "source");
|
||||
auto linked = span.linkedSpan("linked.test");
|
||||
EXPECT_FALSE(linked);
|
||||
}
|
||||
|
||||
TEST(SpanGuardFactory, capture_context_returns_invalid_on_null)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "ctx");
|
||||
auto ctx = span.captureContext();
|
||||
EXPECT_FALSE(ctx.isValid());
|
||||
}
|
||||
|
||||
TEST(SpanGuardFactory, move_construction_transfers_ownership)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "move");
|
||||
auto moved = std::move(span);
|
||||
EXPECT_FALSE(span); // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved)
|
||||
moved.setAttribute("key", "value");
|
||||
}
|
||||
|
||||
TEST(SpanGuardFactory, record_exception_safe_on_null)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, "rpc.command", "test");
|
||||
try
|
||||
{
|
||||
throw std::runtime_error("test error");
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
span.recordException(e);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SpanGuardFactory, discard_safe_on_null)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Transactions, "tx", "process");
|
||||
span.discard();
|
||||
EXPECT_FALSE(span);
|
||||
}
|
||||
105
src/tests/libxrpl/telemetry/TelemetryConfig.cpp
Normal file
105
src/tests/libxrpl/telemetry/TelemetryConfig.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include <xrpl/basics/BasicConfig.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
TEST(TelemetryConfig, setup_defaults)
|
||||
{
|
||||
telemetry::Telemetry::Setup const s;
|
||||
EXPECT_FALSE(s.enabled);
|
||||
EXPECT_EQ(s.serviceName, "xrpld");
|
||||
EXPECT_TRUE(s.serviceVersion.empty());
|
||||
EXPECT_TRUE(s.serviceInstanceId.empty());
|
||||
EXPECT_EQ(s.exporterEndpoint, "http://localhost:4318/v1/traces");
|
||||
EXPECT_FALSE(s.useTls);
|
||||
EXPECT_TRUE(s.tlsCertPath.empty());
|
||||
EXPECT_DOUBLE_EQ(s.samplingRatio, 1.0);
|
||||
EXPECT_EQ(s.batchSize, 512u);
|
||||
EXPECT_EQ(s.batchDelay, std::chrono::milliseconds{5000});
|
||||
EXPECT_EQ(s.maxQueueSize, 2048u);
|
||||
EXPECT_EQ(s.networkId, 0u);
|
||||
EXPECT_EQ(s.networkType, "mainnet");
|
||||
EXPECT_TRUE(s.traceTransactions);
|
||||
EXPECT_TRUE(s.traceConsensus);
|
||||
EXPECT_TRUE(s.traceRpc);
|
||||
EXPECT_TRUE(s.tracePeer);
|
||||
EXPECT_TRUE(s.traceLedger);
|
||||
}
|
||||
|
||||
TEST(TelemetryConfig, parse_empty_section)
|
||||
{
|
||||
Section const section;
|
||||
auto setup = telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0);
|
||||
|
||||
EXPECT_FALSE(setup.enabled);
|
||||
EXPECT_EQ(setup.serviceName, "xrpld");
|
||||
EXPECT_EQ(setup.serviceVersion, "2.0.0");
|
||||
EXPECT_EQ(setup.serviceInstanceId, "nHUtest123");
|
||||
EXPECT_DOUBLE_EQ(setup.samplingRatio, 1.0);
|
||||
EXPECT_TRUE(setup.traceRpc);
|
||||
EXPECT_TRUE(setup.traceTransactions);
|
||||
EXPECT_TRUE(setup.traceConsensus);
|
||||
EXPECT_TRUE(setup.tracePeer);
|
||||
EXPECT_TRUE(setup.traceLedger);
|
||||
}
|
||||
|
||||
TEST(TelemetryConfig, parse_full_section)
|
||||
{
|
||||
Section section;
|
||||
section.set("enabled", "1");
|
||||
section.set("service_name", "my-rippled");
|
||||
section.set("service_instance_id", "custom-id");
|
||||
section.set("exporter", "otlp_http");
|
||||
section.set("endpoint", "http://collector:4318/v1/traces");
|
||||
section.set("use_tls", "1");
|
||||
section.set("tls_ca_cert", "/etc/ssl/ca.pem");
|
||||
section.set("batch_size", "256");
|
||||
section.set("batch_delay_ms", "3000");
|
||||
section.set("max_queue_size", "4096");
|
||||
section.set("trace_transactions", "0");
|
||||
section.set("trace_consensus", "0");
|
||||
section.set("trace_rpc", "1");
|
||||
section.set("trace_peer", "1");
|
||||
section.set("trace_ledger", "0");
|
||||
|
||||
auto setup = telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 1);
|
||||
|
||||
EXPECT_TRUE(setup.enabled);
|
||||
EXPECT_EQ(setup.serviceName, "my-rippled");
|
||||
EXPECT_EQ(setup.serviceInstanceId, "custom-id");
|
||||
EXPECT_EQ(setup.exporterEndpoint, "http://collector:4318/v1/traces");
|
||||
EXPECT_TRUE(setup.useTls);
|
||||
EXPECT_EQ(setup.tlsCertPath, "/etc/ssl/ca.pem");
|
||||
EXPECT_EQ(setup.batchSize, 256u);
|
||||
EXPECT_EQ(setup.batchDelay, std::chrono::milliseconds{3000});
|
||||
EXPECT_EQ(setup.maxQueueSize, 4096u);
|
||||
EXPECT_FALSE(setup.traceTransactions);
|
||||
EXPECT_FALSE(setup.traceConsensus);
|
||||
EXPECT_TRUE(setup.traceRpc);
|
||||
EXPECT_TRUE(setup.tracePeer);
|
||||
EXPECT_FALSE(setup.traceLedger);
|
||||
}
|
||||
|
||||
TEST(TelemetryConfig, null_telemetry_factory)
|
||||
{
|
||||
telemetry::Telemetry::Setup setup;
|
||||
setup.enabled = false;
|
||||
|
||||
beast::Journal::Sink& sink = beast::Journal::getNullSink();
|
||||
beast::Journal const j(sink);
|
||||
auto tel = telemetry::makeTelemetry(setup, j);
|
||||
EXPECT_TRUE(tel != nullptr);
|
||||
EXPECT_FALSE(tel->isEnabled());
|
||||
EXPECT_FALSE(tel->shouldTraceRpc());
|
||||
EXPECT_FALSE(tel->shouldTraceTransactions());
|
||||
EXPECT_FALSE(tel->shouldTraceConsensus());
|
||||
EXPECT_FALSE(tel->shouldTracePeer());
|
||||
EXPECT_FALSE(tel->shouldTraceLedger());
|
||||
|
||||
// start/stop should be no-ops without crashing
|
||||
tel->start();
|
||||
tel->stop();
|
||||
}
|
||||
162
src/tests/libxrpl/telemetry/TraceContextPropagator.cpp
Normal file
162
src/tests/libxrpl/telemetry/TraceContextPropagator.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/proto/xrpl.pb.h>
|
||||
#include <xrpl/telemetry/TraceContextPropagator.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/nostd/span.h>
|
||||
#include <opentelemetry/trace/context.h>
|
||||
#include <opentelemetry/trace/default_span.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_context.h>
|
||||
#include <opentelemetry/trace/span_id.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/trace_flags.h>
|
||||
#include <opentelemetry/trace/trace_id.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace trace = opentelemetry::trace;
|
||||
|
||||
TEST(TraceContextPropagator, round_trip)
|
||||
{
|
||||
std::uint8_t traceIdBuf[16] = {
|
||||
0x01,
|
||||
0x02,
|
||||
0x03,
|
||||
0x04,
|
||||
0x05,
|
||||
0x06,
|
||||
0x07,
|
||||
0x08,
|
||||
0x09,
|
||||
0x0a,
|
||||
0x0b,
|
||||
0x0c,
|
||||
0x0d,
|
||||
0x0e,
|
||||
0x0f,
|
||||
0x10};
|
||||
std::uint8_t spanIdBuf[8] = {0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22};
|
||||
|
||||
trace::TraceId const traceId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 16>(traceIdBuf, 16));
|
||||
trace::SpanId const spanId(opentelemetry::nostd::span<std::uint8_t const, 8>(spanIdBuf, 8));
|
||||
trace::TraceFlags const flags(trace::TraceFlags::kIsSampled);
|
||||
trace::SpanContext const spanCtx(traceId, spanId, flags, true);
|
||||
|
||||
auto ctx = opentelemetry::context::Context{}.SetValue(
|
||||
trace::kSpanKey,
|
||||
opentelemetry::nostd::shared_ptr<trace::Span>(new trace::DefaultSpan(spanCtx)));
|
||||
|
||||
protocol::TraceContext proto;
|
||||
xrpl::telemetry::injectToProtobuf(ctx, proto);
|
||||
|
||||
EXPECT_TRUE(proto.has_trace_id());
|
||||
EXPECT_EQ(proto.trace_id().size(), 16u);
|
||||
EXPECT_TRUE(proto.has_span_id());
|
||||
EXPECT_EQ(proto.span_id().size(), 8u);
|
||||
EXPECT_EQ(proto.trace_flags(), static_cast<std::uint32_t>(trace::TraceFlags::kIsSampled));
|
||||
EXPECT_EQ(std::memcmp(proto.trace_id().data(), traceIdBuf, 16), 0);
|
||||
EXPECT_EQ(std::memcmp(proto.span_id().data(), spanIdBuf, 8), 0);
|
||||
|
||||
auto extractedCtx = xrpl::telemetry::extractFromProtobuf(proto);
|
||||
auto extractedSpan = trace::GetSpan(extractedCtx);
|
||||
ASSERT_NE(extractedSpan, nullptr);
|
||||
|
||||
auto const& extracted = extractedSpan->GetContext();
|
||||
EXPECT_TRUE(extracted.IsValid());
|
||||
EXPECT_TRUE(extracted.IsRemote());
|
||||
EXPECT_EQ(extracted.trace_id(), traceId);
|
||||
EXPECT_EQ(extracted.span_id(), spanId);
|
||||
EXPECT_TRUE(extracted.trace_flags().IsSampled());
|
||||
}
|
||||
|
||||
TEST(TraceContextPropagator, extract_empty_protobuf)
|
||||
{
|
||||
protocol::TraceContext const proto;
|
||||
auto ctx = xrpl::telemetry::extractFromProtobuf(proto);
|
||||
auto span = trace::GetSpan(ctx);
|
||||
if (span)
|
||||
{
|
||||
EXPECT_FALSE(span->GetContext().IsValid());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TraceContextPropagator, extract_wrong_size_trace_id)
|
||||
{
|
||||
protocol::TraceContext proto;
|
||||
proto.set_trace_id(std::string(8, '\x01'));
|
||||
proto.set_span_id(std::string(8, '\xaa'));
|
||||
|
||||
auto ctx = xrpl::telemetry::extractFromProtobuf(proto);
|
||||
auto span = trace::GetSpan(ctx);
|
||||
if (span)
|
||||
{
|
||||
EXPECT_FALSE(span->GetContext().IsValid());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TraceContextPropagator, extract_wrong_size_span_id)
|
||||
{
|
||||
protocol::TraceContext proto;
|
||||
proto.set_trace_id(std::string(16, '\x01'));
|
||||
proto.set_span_id(std::string(4, '\xaa'));
|
||||
|
||||
auto ctx = xrpl::telemetry::extractFromProtobuf(proto);
|
||||
auto span = trace::GetSpan(ctx);
|
||||
if (span)
|
||||
{
|
||||
EXPECT_FALSE(span->GetContext().IsValid());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TraceContextPropagator, inject_invalid_span)
|
||||
{
|
||||
auto ctx = opentelemetry::context::Context{};
|
||||
protocol::TraceContext proto;
|
||||
xrpl::telemetry::injectToProtobuf(ctx, proto);
|
||||
|
||||
EXPECT_FALSE(proto.has_trace_id());
|
||||
EXPECT_FALSE(proto.has_span_id());
|
||||
}
|
||||
|
||||
TEST(TraceContextPropagator, flags_preservation)
|
||||
{
|
||||
std::uint8_t traceIdBuf[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
|
||||
std::uint8_t spanIdBuf[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
|
||||
// Test with flags NOT sampled (flags = 0)
|
||||
trace::TraceFlags const flags(0);
|
||||
trace::SpanContext const spanCtx(
|
||||
trace::TraceId(opentelemetry::nostd::span<std::uint8_t const, 16>(traceIdBuf, 16)),
|
||||
trace::SpanId(opentelemetry::nostd::span<std::uint8_t const, 8>(spanIdBuf, 8)),
|
||||
flags,
|
||||
true);
|
||||
|
||||
auto ctx = opentelemetry::context::Context{}.SetValue(
|
||||
trace::kSpanKey,
|
||||
opentelemetry::nostd::shared_ptr<trace::Span>(new trace::DefaultSpan(spanCtx)));
|
||||
|
||||
protocol::TraceContext proto;
|
||||
xrpl::telemetry::injectToProtobuf(ctx, proto);
|
||||
EXPECT_EQ(proto.trace_flags(), 0u);
|
||||
|
||||
auto extracted = xrpl::telemetry::extractFromProtobuf(proto);
|
||||
auto span = trace::GetSpan(extracted);
|
||||
ASSERT_NE(span, nullptr);
|
||||
EXPECT_FALSE(span->GetContext().trace_flags().IsSampled());
|
||||
}
|
||||
|
||||
#else // XRPL_ENABLE_TELEMETRY not defined
|
||||
|
||||
TEST(TraceContextPropagator, compiles_without_telemetry)
|
||||
{
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
52
src/tests/libxrpl/telemetry/TxApplySpanNames.cpp
Normal file
52
src/tests/libxrpl/telemetry/TxApplySpanNames.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include <xrpl/tx/detail/TxApplySpanNames.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
/** Contract tests for the transaction apply-pipeline span constants.
|
||||
*
|
||||
* The span names and attribute keys in TxApplySpanNames.h are a cross-component
|
||||
* contract: the collector spanmetrics connector aggregates on these exact
|
||||
* strings (dimensions tx_type, ter_result, stage) and the Grafana
|
||||
* transaction-overview dashboard queries them. A silent rename here would
|
||||
* break per-stage metrics with no compile error, so these tests pin the
|
||||
* literal values. They need no telemetry runtime and run in every build.
|
||||
*/
|
||||
|
||||
using namespace xrpl::telemetry;
|
||||
|
||||
TEST(TxApplySpanNames, span_names_are_dot_qualified)
|
||||
{
|
||||
// Full span names feed SpanGuard::hashSpan() in applySteps.cpp.
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::preflight), "tx.preflight");
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::preclaim), "tx.preclaim");
|
||||
}
|
||||
|
||||
TEST(TxApplySpanNames, operation_suffixes)
|
||||
{
|
||||
// Suffix used with SpanGuard::span(cat, seg::tx, suffix) in Transactor.cpp.
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::op::preflight), "preflight");
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::op::preclaim), "preclaim");
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::op::transactor), "transactor");
|
||||
}
|
||||
|
||||
TEST(TxApplySpanNames, attribute_keys_match_collector_dimensions)
|
||||
{
|
||||
// These keys MUST match docker/telemetry/otel-collector-config.yaml
|
||||
// spanmetrics dimensions and TxSpanNames.h (so both span sets aggregate
|
||||
// under one dimension).
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::attr::stage), "stage");
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::attr::txType), "tx_type");
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::attr::terResult), "ter_result");
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::attr::applied), "applied");
|
||||
}
|
||||
|
||||
TEST(TxApplySpanNames, stage_values_are_the_three_pipeline_stages)
|
||||
{
|
||||
// The stage attribute carries exactly these three values; they become the
|
||||
// spanmetrics `stage` dimension cardinality (3) and the dashboard filter.
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::val::preflight), "preflight");
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::val::preclaim), "preclaim");
|
||||
EXPECT_EQ(std::string_view(tx_apply_span::val::apply), "apply");
|
||||
}
|
||||
@@ -80,10 +80,12 @@
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
#include <xrpl/rdb/DatabaseCon.h>
|
||||
#include <xrpl/resource/Charge.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
@@ -97,6 +99,7 @@
|
||||
#include <xrpl/shamap/SHAMap.h>
|
||||
#include <xrpl/shamap/SHAMapMissingNode.h>
|
||||
#include <xrpl/shamap/TreeNodeCache.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
@@ -211,6 +214,7 @@ public:
|
||||
|
||||
beast::Journal journal_;
|
||||
std::unique_ptr<perf::PerfLog> perfLog_;
|
||||
std::unique_ptr<telemetry::Telemetry> telemetry_;
|
||||
Application::MutexType masterMutex_;
|
||||
|
||||
// Required by the SHAMapStore
|
||||
@@ -320,6 +324,15 @@ public:
|
||||
*this,
|
||||
logs_->journal("PerfLog"),
|
||||
[this] { signalStop("PerfLog"); }))
|
||||
, telemetry_(
|
||||
telemetry::makeTelemetry(
|
||||
telemetry::setupTelemetry(
|
||||
config_->section("telemetry"),
|
||||
"", // Updated later via setServiceInstanceId()
|
||||
BuildInfo::getVersionString(),
|
||||
config_->networkId),
|
||||
logs_->journal("Telemetry")))
|
||||
|
||||
, txMaster_(*this)
|
||||
, collectorManager_(makeCollectorManager(
|
||||
config_->section(Sections::kInsight),
|
||||
@@ -648,6 +661,12 @@ public:
|
||||
return *perfLog_;
|
||||
}
|
||||
|
||||
telemetry::Telemetry&
|
||||
getTelemetry() override
|
||||
{
|
||||
return *telemetry_;
|
||||
}
|
||||
|
||||
NodeCache&
|
||||
getTempNodeCache() override
|
||||
{
|
||||
@@ -1296,6 +1315,14 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
|
||||
nodeIdentity_ = getNodeIdentity(*this, cmdline);
|
||||
|
||||
// Now that the node identity is known, inject it into the telemetry
|
||||
// resource attributes — but only if the user didn't already set a
|
||||
// custom service_instance_id in [telemetry]. The Telemetry object
|
||||
// was constructed with an empty serviceInstanceId because
|
||||
// nodeIdentity_ is not available in the member initializer list.
|
||||
if (!config_->section("telemetry").exists("service_instance_id"))
|
||||
telemetry_->setServiceInstanceId(toBase58(TokenType::NodePublic, nodeIdentity_->first));
|
||||
|
||||
if (!cluster_->load(config().section(Sections::kClusterNodes)))
|
||||
{
|
||||
JLOG(journal_.fatal()) << "Invalid entry in cluster configuration.";
|
||||
@@ -1508,6 +1535,7 @@ ApplicationImp::start(bool withTimers)
|
||||
|
||||
ledgerCleaner_->start();
|
||||
perfLog_->start();
|
||||
telemetry_->start();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1598,6 +1626,11 @@ ApplicationImp::run()
|
||||
ledgerCleaner_->stop();
|
||||
nodeStore_->stop();
|
||||
perfLog_->stop();
|
||||
// Telemetry must stop last among trace-producing components.
|
||||
// serverHandler_, overlay_, and jobQueue_ are already stopped above,
|
||||
// so no threads should be calling startSpan() at this point.
|
||||
// See TODO in TelemetryImpl::stop() re: thread-safety of sdkProvider_.
|
||||
telemetry_->stop();
|
||||
|
||||
JLOG(journal_.info()) << "Done.";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpld/app/main/GRPCServer.h>
|
||||
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/main/GrpcSpanNames.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/GRPCHandlers.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
@@ -48,6 +50,7 @@
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -93,7 +96,8 @@ GRPCServerImpl::CallData<Request, Response>::CallData(
|
||||
Forward<Request, Response> forward,
|
||||
RPC::Condition requiredCondition,
|
||||
Resource::Charge loadType,
|
||||
std::vector<boost::asio::ip::address> const& secureGatewayIPs)
|
||||
std::vector<boost::asio::ip::address> const& secureGatewayIPs,
|
||||
std::string_view name)
|
||||
: service_(service)
|
||||
, cq_(cq)
|
||||
, finished_(false)
|
||||
@@ -105,6 +109,7 @@ GRPCServerImpl::CallData<Request, Response>::CallData(
|
||||
, requiredCondition_(requiredCondition)
|
||||
, loadType_(std::move(loadType))
|
||||
, secureGatewayIPs_(secureGatewayIPs)
|
||||
, name_(name)
|
||||
{
|
||||
// Bind a listener. When a request is received, "this" will be returned
|
||||
// from CompletionQueue::Next
|
||||
@@ -124,7 +129,8 @@ GRPCServerImpl::CallData<Request, Response>::clone()
|
||||
forward_,
|
||||
requiredCondition_,
|
||||
loadType_,
|
||||
secureGatewayIPs_);
|
||||
secureGatewayIPs_,
|
||||
name_);
|
||||
}
|
||||
|
||||
template <class Request, class Response>
|
||||
@@ -162,12 +168,19 @@ template <class Request, class Response>
|
||||
void
|
||||
GRPCServerImpl::CallData<Request, Response>::process(std::shared_ptr<JobQueue::Coro> coro)
|
||||
{
|
||||
using namespace telemetry;
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, grpc_span::prefix::grpc, name_);
|
||||
span.setAttribute(grpc_span::attr::method, name_);
|
||||
|
||||
try
|
||||
{
|
||||
auto usage = getUsage();
|
||||
bool const isUnlimited = clientIsUnlimited();
|
||||
if (!isUnlimited && usage.disconnect(app_.getJournal("gRPCServer")))
|
||||
{
|
||||
span.setAttribute(
|
||||
grpc_span::attr::grpcStatus, grpc_span::val::error); // LCOV_EXCL_LINE
|
||||
span.setError(grpc_span::val::resourceExhausted); // LCOV_EXCL_LINE
|
||||
grpc::Status const status{
|
||||
grpc::StatusCode::RESOURCE_EXHAUSTED, "usage balance exceeds threshold"};
|
||||
responder_.FinishWithError(status, this);
|
||||
@@ -178,6 +191,11 @@ GRPCServerImpl::CallData<Request, Response>::process(std::shared_ptr<JobQueue::C
|
||||
usage.charge(loadType);
|
||||
auto role = getRole(isUnlimited);
|
||||
|
||||
span.setAttribute(
|
||||
grpc_span::attr::grpcRole,
|
||||
role == Role::ADMIN ? std::string_view{grpc_span::val::admin}
|
||||
: std::string_view{grpc_span::val::user});
|
||||
|
||||
{
|
||||
std::stringstream toLog;
|
||||
toLog << "role = " << (int)role;
|
||||
@@ -213,6 +231,9 @@ GRPCServerImpl::CallData<Request, Response>::process(std::shared_ptr<JobQueue::C
|
||||
if (conditionMetRes != RpcSuccess)
|
||||
{
|
||||
RPC::ErrorInfo const errorInfo = RPC::getErrorInfo(conditionMetRes);
|
||||
span.setAttribute(
|
||||
grpc_span::attr::grpcStatus, grpc_span::val::error); // LCOV_EXCL_LINE
|
||||
span.setError(errorInfo.token.cStr()); // LCOV_EXCL_LINE
|
||||
grpc::Status const status{
|
||||
grpc::StatusCode::FAILED_PRECONDITION, errorInfo.message.cStr()};
|
||||
responder_.FinishWithError(status, this);
|
||||
@@ -221,12 +242,16 @@ GRPCServerImpl::CallData<Request, Response>::process(std::shared_ptr<JobQueue::C
|
||||
{
|
||||
std::pair<Response, grpc::Status> result = handler_(context);
|
||||
setIsUnlimited(result.first, isUnlimited);
|
||||
span.setAttribute(grpc_span::attr::grpcStatus, grpc_span::val::success);
|
||||
span.setOk();
|
||||
responder_.Finish(result.first, result.second, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
span.setAttribute(grpc_span::attr::grpcStatus, grpc_span::val::error); // LCOV_EXCL_LINE
|
||||
span.recordException(ex); // LCOV_EXCL_LINE
|
||||
grpc::Status const status{grpc::StatusCode::INTERNAL, ex.what()};
|
||||
responder_.FinishWithError(status, this);
|
||||
}
|
||||
@@ -545,7 +570,8 @@ GRPCServerImpl::setupListeners()
|
||||
&org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedger,
|
||||
Condition::NoCondition,
|
||||
Resource::kFeeMediumBurdenRpc,
|
||||
secureGatewayIPs_));
|
||||
secureGatewayIPs_,
|
||||
"GetLedger"));
|
||||
}
|
||||
{
|
||||
using cd = CallData<
|
||||
@@ -562,7 +588,8 @@ GRPCServerImpl::setupListeners()
|
||||
&org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerData,
|
||||
Condition::NoCondition,
|
||||
Resource::kFeeMediumBurdenRpc,
|
||||
secureGatewayIPs_));
|
||||
secureGatewayIPs_,
|
||||
"GetLedgerData"));
|
||||
}
|
||||
{
|
||||
using cd = CallData<
|
||||
@@ -579,7 +606,8 @@ GRPCServerImpl::setupListeners()
|
||||
&org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerDiff,
|
||||
Condition::NoCondition,
|
||||
Resource::kFeeMediumBurdenRpc,
|
||||
secureGatewayIPs_));
|
||||
secureGatewayIPs_,
|
||||
"GetLedgerDiff"));
|
||||
}
|
||||
{
|
||||
using cd = CallData<
|
||||
@@ -596,7 +624,8 @@ GRPCServerImpl::setupListeners()
|
||||
&org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerEntry,
|
||||
Condition::NoCondition,
|
||||
Resource::kFeeMediumBurdenRpc,
|
||||
secureGatewayIPs_));
|
||||
secureGatewayIPs_,
|
||||
"GetLedgerEntry"));
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#include <grpcpp/grpcpp.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Interface that CallData implements
|
||||
@@ -185,6 +187,9 @@ private:
|
||||
|
||||
std::vector<boost::asio::ip::address> const& secureGatewayIPs_;
|
||||
|
||||
/// Human-readable name for telemetry spans (e.g. "GetLedger").
|
||||
std::string_view name_;
|
||||
|
||||
public:
|
||||
~CallData() override = default;
|
||||
|
||||
@@ -200,7 +205,8 @@ private:
|
||||
Forward<Request, Response> forward,
|
||||
RPC::Condition requiredCondition,
|
||||
Resource::Charge loadType,
|
||||
std::vector<boost::asio::ip::address> const& secureGatewayIPs);
|
||||
std::vector<boost::asio::ip::address> const& secureGatewayIPs,
|
||||
std::string_view name = "");
|
||||
|
||||
CallData(CallData const&) = delete;
|
||||
|
||||
|
||||
58
src/xrpld/app/main/GrpcSpanNames.h
Normal file
58
src/xrpld/app/main/GrpcSpanNames.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
/** Compile-time span name constants for the gRPC subsystem.
|
||||
*
|
||||
* All span prefixes, operation names, and attribute keys used by gRPC
|
||||
* tracing call sites are defined here. Built on the StaticStr/join()
|
||||
* primitives from <xrpl/telemetry/SpanNames.h>.
|
||||
*
|
||||
* Span hierarchy:
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | grpc.<MethodName> (e.g. grpc.GetLedger) |
|
||||
* | CallData::process(coro) |
|
||||
* | attrs: method, grpc_role, grpc_status |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* Unlike the HTTP/WS RPC path, gRPC has a flat single-span structure
|
||||
* per request since each CallData handles exactly one RPC method.
|
||||
* The method name is embedded in the span name (rather than only as
|
||||
* an attribute) so dashboards can break out per-method latency and
|
||||
* error rates without needing TraceQL attribute filters.
|
||||
*/
|
||||
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
|
||||
namespace xrpl::telemetry::grpc_span {
|
||||
|
||||
// ===== Span prefixes =======================================================
|
||||
|
||||
namespace prefix {
|
||||
/// "grpc" — root prefix for gRPC transport spans. The full span name is
|
||||
/// formed at the call site as `grpc.<MethodName>` (see GRPCServer.cpp).
|
||||
inline constexpr auto grpc = makeStr("grpc");
|
||||
} // namespace prefix
|
||||
|
||||
// ===== Attribute keys ======================================================
|
||||
|
||||
namespace attr {
|
||||
/// "method" — gRPC method name (e.g. GetLedger).
|
||||
inline constexpr auto method = makeStr("method");
|
||||
/// "grpc_role" — Domain-qualified: collides with rpc_role.
|
||||
inline constexpr auto grpcRole = makeStr("grpc_role");
|
||||
/// "grpc_status" — Domain-qualified: avoids OTel reserved span status.
|
||||
inline constexpr auto grpcStatus = makeStr("grpc_status");
|
||||
} // namespace attr
|
||||
|
||||
// ===== Attribute values ====================================================
|
||||
|
||||
namespace val {
|
||||
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 resourceExhausted = makeStr("resource_exhausted");
|
||||
inline constexpr auto failedPrecondition = makeStr("failed_precondition");
|
||||
} // namespace val
|
||||
|
||||
} // namespace xrpl::telemetry::grpc_span
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <xrpld/app/misc/FeeVote.h>
|
||||
#include <xrpld/app/misc/Transaction.h>
|
||||
#include <xrpld/app/misc/TxQ.h>
|
||||
#include <xrpld/app/misc/TxSpanNames.h>
|
||||
#include <xrpld/app/misc/ValidatorKeys.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/app/misc/make_NetworkOPs.h>
|
||||
@@ -33,6 +34,8 @@
|
||||
#include <xrpld/rpc/DeliveredAmount.h>
|
||||
#include <xrpld/rpc/MPTokenIssuanceID.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpld/telemetry/PropagationHelpers.h>
|
||||
#include <xrpld/telemetry/TxTracing.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/ToString.h>
|
||||
@@ -114,6 +117,7 @@
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
#include <xrpl/shamap/SHAMap.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
@@ -169,9 +173,16 @@ class NetworkOPsImp final : public NetworkOPs
|
||||
FailHard const failType;
|
||||
bool applied = false;
|
||||
TER result;
|
||||
/// Keeps the tx.process span alive until the batch processes this entry.
|
||||
std::shared_ptr<telemetry::SpanGuard> span;
|
||||
|
||||
TransactionStatus(std::shared_ptr<Transaction> t, bool a, bool l, FailHard f)
|
||||
: transaction(std::move(t)), admin(a), local(l), failType(f)
|
||||
TransactionStatus(
|
||||
std::shared_ptr<Transaction> t,
|
||||
bool a,
|
||||
bool l,
|
||||
FailHard f,
|
||||
std::shared_ptr<telemetry::SpanGuard> s = nullptr)
|
||||
: transaction(std::move(t)), admin(a), local(l), failType(f), span(std::move(s))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
local || failType == FailHard::No,
|
||||
@@ -377,9 +388,15 @@ public:
|
||||
* @param transaction Transaction object.
|
||||
* @param bUnlimited Whether a privileged client connection submitted it.
|
||||
* @param failType fail_hard setting from transaction submission.
|
||||
* @param span Optional tx.process span to keep alive across the
|
||||
* batch boundary so its context propagates to peers.
|
||||
*/
|
||||
void
|
||||
doTransactionSync(std::shared_ptr<Transaction> transaction, bool bUnlimited, FailHard failType);
|
||||
doTransactionSync(
|
||||
std::shared_ptr<Transaction> transaction,
|
||||
bool bUnlimited,
|
||||
FailHard failType,
|
||||
std::shared_ptr<telemetry::SpanGuard> span = nullptr);
|
||||
|
||||
/**
|
||||
* For transactions not submitted by a locally connected client, fire and
|
||||
@@ -394,7 +411,8 @@ public:
|
||||
doTransactionAsync(
|
||||
std::shared_ptr<Transaction> transaction,
|
||||
bool bUnlimited,
|
||||
FailHard failtype);
|
||||
FailHard failtype,
|
||||
std::shared_ptr<telemetry::SpanGuard> span = nullptr);
|
||||
|
||||
private:
|
||||
bool
|
||||
@@ -1312,6 +1330,20 @@ NetworkOPsImp::processTransaction(
|
||||
bool bLocal,
|
||||
FailHard failType)
|
||||
{
|
||||
using namespace telemetry;
|
||||
auto span = std::make_shared<SpanGuard>(txProcessSpan(transaction->getID()));
|
||||
span->setAttribute(tx_span::attr::txHash, to_string(transaction->getID()).c_str());
|
||||
span->setAttribute(tx_span::attr::local, bLocal);
|
||||
if (auto const& stx = transaction->getSTransaction())
|
||||
{
|
||||
if (auto const* fmt = TxFormats::getInstance().findByType(stx->getTxnType()))
|
||||
span->setAttribute(tx_span::attr::txType, fmt->getName().c_str());
|
||||
span->setAttribute(
|
||||
tx_span::attr::fee, static_cast<int64_t>(stx->getFieldAmount(sfFee).xrp().drops()));
|
||||
span->setAttribute(
|
||||
tx_span::attr::sequence, static_cast<int64_t>(stx->getSeqProxy().value()));
|
||||
}
|
||||
|
||||
auto ev = jobQueue_.makeLoadEvent(JtTxnProc, "ProcessTXN");
|
||||
|
||||
// preProcessTransaction can change our pointer
|
||||
@@ -1320,11 +1352,13 @@ NetworkOPsImp::processTransaction(
|
||||
|
||||
if (bLocal)
|
||||
{
|
||||
doTransactionSync(transaction, bUnlimited, failType);
|
||||
span->setAttribute(tx_span::attr::path, tx_span::val::sync);
|
||||
doTransactionSync(transaction, bUnlimited, failType, std::move(span));
|
||||
}
|
||||
else
|
||||
{
|
||||
doTransactionAsync(transaction, bUnlimited, failType);
|
||||
span->setAttribute(tx_span::attr::path, tx_span::val::async);
|
||||
doTransactionAsync(transaction, bUnlimited, failType, std::move(span));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1332,14 +1366,15 @@ void
|
||||
NetworkOPsImp::doTransactionAsync(
|
||||
std::shared_ptr<Transaction> transaction,
|
||||
bool bUnlimited,
|
||||
FailHard failType)
|
||||
FailHard failType,
|
||||
std::shared_ptr<telemetry::SpanGuard> span)
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
|
||||
if (transaction->getApplying())
|
||||
return;
|
||||
|
||||
transactions_.emplace_back(transaction, bUnlimited, false, failType);
|
||||
transactions_.emplace_back(transaction, bUnlimited, false, failType, std::move(span));
|
||||
transaction->setApplying();
|
||||
|
||||
if (dispatchState_ == DispatchState::None)
|
||||
@@ -1355,13 +1390,14 @@ void
|
||||
NetworkOPsImp::doTransactionSync(
|
||||
std::shared_ptr<Transaction> transaction,
|
||||
bool bUnlimited,
|
||||
FailHard failType)
|
||||
FailHard failType,
|
||||
std::shared_ptr<telemetry::SpanGuard> span)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
|
||||
if (!transaction->getApplying())
|
||||
{
|
||||
transactions_.emplace_back(transaction, bUnlimited, true, failType);
|
||||
transactions_.emplace_back(transaction, bUnlimited, true, failType, std::move(span));
|
||||
transaction->setApplying();
|
||||
}
|
||||
|
||||
@@ -1527,6 +1563,12 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
auto newOL = registry_.get().getOpenLedger().current();
|
||||
for (TransactionStatus const& e : transactions)
|
||||
{
|
||||
if (e.span && *e.span)
|
||||
{
|
||||
e.span->setAttribute(
|
||||
telemetry::tx_span::attr::terResult, transToken(e.result).c_str());
|
||||
e.span->setAttribute(telemetry::tx_span::attr::applied, e.applied);
|
||||
}
|
||||
e.transaction->clearSubmitResult();
|
||||
|
||||
if (e.applied)
|
||||
@@ -1685,6 +1727,10 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
tx.set_receivetimestamp(
|
||||
registry_.get().getTimeKeeper().now().time_since_epoch().count());
|
||||
tx.set_deferred(e.result == terQUEUED);
|
||||
// Inject the tx.process span's trace context so the
|
||||
// receiving node can link its tx.receive span as a child.
|
||||
if (e.span && *e.span)
|
||||
telemetry::injectSpanContext(*e.span, *tx.mutable_trace_context());
|
||||
// FIXME: This should be when we received it
|
||||
registry_.get().getOverlay().relay(e.transaction->getID(), tx, *toSkip);
|
||||
e.transaction->setBroadcast();
|
||||
|
||||
89
src/xrpld/app/misc/TxSpanNames.h
Normal file
89
src/xrpld/app/misc/TxSpanNames.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
/** Compile-time span name constants for transaction tracing.
|
||||
*
|
||||
* Used by PeerImp (overlay) and NetworkOPs (app) for transaction
|
||||
* lifecycle spans. Built on StaticStr/join() from SpanNames.h.
|
||||
*
|
||||
* Span hierarchy (cross-node propagation):
|
||||
*
|
||||
* Node A (sender) Node B (receiver)
|
||||
* +---------------------+ +---------------------+
|
||||
* | tx.process | protobuf | tx.receive |
|
||||
* | injectSpanContext | ---------> | txReceiveSpan() |
|
||||
* | (PropagationHelp.) | trace_ctx | extracts parent |
|
||||
* +---------------------+ +---------------------+
|
||||
*/
|
||||
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
|
||||
namespace xrpl::telemetry::tx_span {
|
||||
|
||||
// ===== Span prefixes =======================================================
|
||||
|
||||
namespace prefix {
|
||||
/// "tx" — root prefix for transaction lifecycle spans.
|
||||
inline constexpr auto tx = seg::tx;
|
||||
} // namespace prefix
|
||||
|
||||
// ===== Span operation suffixes =============================================
|
||||
|
||||
namespace op {
|
||||
inline constexpr auto receive = makeStr("receive");
|
||||
inline constexpr auto process = makeStr("process");
|
||||
} // namespace op
|
||||
|
||||
// ===== Full span names (prefix.op) =========================================
|
||||
|
||||
inline constexpr auto receive = join(prefix::tx, op::receive);
|
||||
inline constexpr auto process = join(prefix::tx, op::process);
|
||||
|
||||
// ===== Attribute keys ======================================================
|
||||
|
||||
namespace attr {
|
||||
/// Canonical shared constants (defined in SpanNames.h).
|
||||
using ::xrpl::telemetry::attr::peerId;
|
||||
using ::xrpl::telemetry::attr::txHash;
|
||||
|
||||
/// "local" — whether tx originated locally.
|
||||
inline constexpr auto local = makeStr("local");
|
||||
/// "path" — sync or async processing path.
|
||||
inline constexpr auto path = makeStr("path");
|
||||
/// "suppressed" — whether tx was suppressed as duplicate.
|
||||
inline constexpr auto suppressed = makeStr("suppressed");
|
||||
/// "tx_status" — domain-qualified (collides with rpc_status, txq_status).
|
||||
inline constexpr auto txStatus = makeStr("tx_status");
|
||||
/// "peer_version" — version of peer that sent the tx.
|
||||
inline constexpr auto peerVersion = makeStr("peer_version");
|
||||
/// "tx_type" — transaction type name (e.g., "Payment", "OfferCreate").
|
||||
inline constexpr auto txType = makeStr("tx_type");
|
||||
/// "fee" — transaction fee in drops.
|
||||
inline constexpr auto fee = makeStr("fee");
|
||||
/// "sequence" — transaction sequence number.
|
||||
inline constexpr auto sequence = makeStr("sequence");
|
||||
/// "ter_result" — engine result code after application.
|
||||
inline constexpr auto terResult = makeStr("ter_result");
|
||||
/// "applied" — whether the transaction was applied to the ledger.
|
||||
inline constexpr auto applied = makeStr("applied");
|
||||
} // namespace attr
|
||||
|
||||
// ===== Attribute values ====================================================
|
||||
|
||||
namespace val {
|
||||
inline constexpr auto sync = makeStr("sync");
|
||||
inline constexpr auto async = makeStr("async");
|
||||
inline constexpr auto knownBad = makeStr("known_bad");
|
||||
/// Transaction was suppressed via HashRouter (duplicate, not flagged bad).
|
||||
inline constexpr auto suppressed = makeStr("suppressed");
|
||||
/// Transaction was rejected because it carried tfInnerBatchTxn, which
|
||||
/// must never appear in network-relayed traffic.
|
||||
inline constexpr auto rejectedInnerBatch = makeStr("rejected_inner_batch");
|
||||
/// Transaction was dropped because the validated ledger is too old to
|
||||
/// confidently apply new transactions (server is out of sync).
|
||||
inline constexpr auto droppedNoSync = makeStr("dropped_no_sync");
|
||||
/// Transaction was dropped because the local job queue for jtTRANSACTION
|
||||
/// is at MAX_TRANSACTIONS — backpressure on the receive side.
|
||||
inline constexpr auto droppedQueueFull = makeStr("dropped_queue_full");
|
||||
} // namespace val
|
||||
|
||||
} // namespace xrpl::telemetry::tx_span
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/detail/TxQSpanNames.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/mulDiv.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
@@ -27,9 +29,11 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
@@ -529,6 +533,10 @@ TxQ::tryClearAccountQueueUpThruTx(
|
||||
FeeMetrics::Snapshot const& metricsSnapshot,
|
||||
beast::Journal j)
|
||||
{
|
||||
using namespace telemetry;
|
||||
[[maybe_unused]] auto span = SpanGuard::span(
|
||||
TraceCategory::Transactions, txq_span::prefix::txq, txq_span::op::batchClear);
|
||||
|
||||
SeqProxy const tSeqProx{tx.getSeqProxy()};
|
||||
XRPL_ASSERT(
|
||||
beginTxIter != accountIter->second.transactions.end(),
|
||||
@@ -600,7 +608,8 @@ TxQ::tryClearAccountQueueUpThruTx(
|
||||
if (txResult.applied)
|
||||
{
|
||||
// All of the queued transactions applied, so remove them from the
|
||||
// queue.
|
||||
// queue. `dist` queued txs preceded the current one in the batch.
|
||||
span.setAttribute(txq_span::attr::numCleared, static_cast<std::int64_t>(dist));
|
||||
endTxIter = erase(accountIter->second, beginTxIter, endTxIter);
|
||||
// If `tx` is replacing a queued tx, delete that one, too.
|
||||
if (endTxIter != accountIter->second.transactions.end() && endTxIter->first == tSeqProx)
|
||||
@@ -731,6 +740,16 @@ TxQ::apply(
|
||||
ApplyFlags flags,
|
||||
beast::Journal j)
|
||||
{
|
||||
using namespace telemetry;
|
||||
auto span =
|
||||
SpanGuard::span(TraceCategory::Transactions, txq_span::prefix::txq, txq_span::op::enqueue);
|
||||
span.setAttribute(txq_span::attr::txHash, to_string(tx->getTransactionID()).c_str());
|
||||
if (auto const* fmt = TxFormats::getInstance().findByType(tx->getTxnType()))
|
||||
span.setAttribute(txq_span::attr::txType, fmt->getName().c_str());
|
||||
// Default outcome; overridden below on the direct-apply and queued paths.
|
||||
// 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,
|
||||
@@ -743,7 +762,10 @@ TxQ::apply(
|
||||
// See if the transaction paid a high enough fee that it can go straight
|
||||
// into the ledger.
|
||||
if (auto directApplied = tryDirectApply(app, view, tx, flags, j))
|
||||
{
|
||||
span.setAttribute(txq_span::attr::txqStatus, txq_span::val::appliedDirect);
|
||||
return *directApplied;
|
||||
}
|
||||
|
||||
if ((flags & TapDryRun) != 0u)
|
||||
return {telCAN_NOT_QUEUE, false};
|
||||
@@ -870,6 +892,10 @@ TxQ::apply(
|
||||
auto const metricsSnapshot = feeMetrics_.getSnapshot();
|
||||
auto const feeLevelPaid = getFeeLevelPaid(view, *tx);
|
||||
auto const requiredFeeLevel = getRequiredFeeLevel(view, flags, metricsSnapshot, lock);
|
||||
span.setAttribute(
|
||||
txq_span::attr::feeLevelPaid, static_cast<std::int64_t>(feeLevelPaid.value()));
|
||||
span.setAttribute(
|
||||
txq_span::attr::requiredFeeLevel, static_cast<std::int64_t>(requiredFeeLevel.value()));
|
||||
|
||||
// Is there a blocker already in the account's queue? If so, don't
|
||||
// allow additional transactions in the queue.
|
||||
@@ -1203,6 +1229,7 @@ TxQ::apply(
|
||||
/* Can't erase (*replacedTxIter) here because success
|
||||
implies that it has already been deleted.
|
||||
*/
|
||||
span.setAttribute(txq_span::attr::txqStatus, txq_span::val::applied);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1318,6 +1345,7 @@ TxQ::apply(
|
||||
<< " to queue."
|
||||
<< " Flags: " << flags;
|
||||
|
||||
span.setAttribute(txq_span::attr::txqStatus, txq_span::val::queued);
|
||||
return {terQUEUED, false};
|
||||
}
|
||||
|
||||
@@ -1336,6 +1364,11 @@ TxQ::apply(
|
||||
void
|
||||
TxQ::processClosedLedger(Application& app, ReadView const& view, bool timeLeap)
|
||||
{
|
||||
using namespace telemetry;
|
||||
auto span =
|
||||
SpanGuard::span(TraceCategory::Transactions, txq_span::prefix::txq, txq_span::op::cleanup);
|
||||
span.setAttribute(txq_span::attr::ledgerSeq, static_cast<int64_t>(view.header().seq));
|
||||
|
||||
std::scoped_lock const lock(mutex_);
|
||||
|
||||
feeMetrics_.update(app, view, timeLeap, setup_);
|
||||
@@ -1347,18 +1380,21 @@ TxQ::processClosedLedger(Application& app, ReadView const& view, bool timeLeap)
|
||||
maxSize_ = std::max(snapshot.txnsExpected * setup_.ledgersInQueue, setup_.queueSizeMin);
|
||||
|
||||
// Remove any queued candidates whose LastLedgerSequence has gone by.
|
||||
std::int64_t expiredCount = 0;
|
||||
for (auto candidateIter = byFee_.begin(); candidateIter != byFee_.end();)
|
||||
{
|
||||
if (candidateIter->lastValid && *candidateIter->lastValid <= ledgerSeq)
|
||||
{
|
||||
byAccount_.at(candidateIter->account).dropPenalty = true;
|
||||
candidateIter = erase(candidateIter);
|
||||
++expiredCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
++candidateIter;
|
||||
}
|
||||
}
|
||||
span.setAttribute(txq_span::attr::expiredCount, expiredCount);
|
||||
|
||||
// Remove any TxQAccounts that don't have candidates
|
||||
// under them
|
||||
@@ -1407,6 +1443,11 @@ TxQ::processClosedLedger(Application& app, ReadView const& view, bool timeLeap)
|
||||
bool
|
||||
TxQ::accept(Application& app, OpenView& view)
|
||||
{
|
||||
using namespace telemetry;
|
||||
auto span =
|
||||
SpanGuard::span(TraceCategory::Transactions, txq_span::prefix::txq, txq_span::op::accept);
|
||||
span.setAttribute(txq_span::attr::queueSize, static_cast<int64_t>(byFee_.size()));
|
||||
|
||||
/* Move transactions from the queue from largest fee level to smallest.
|
||||
As we add more transactions, the required fee level will increase.
|
||||
Stop when the transaction fee level gets lower than the required fee
|
||||
@@ -1444,10 +1485,19 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
JLOG(j_.trace()) << "Applying queued transaction " << candidateIter->txID
|
||||
<< " to open ledger.";
|
||||
|
||||
auto txSpan = SpanGuard::span(
|
||||
TraceCategory::Transactions, txq_span::prefix::txq, txq_span::op::acceptTx);
|
||||
txSpan.setAttribute(txq_span::attr::txHash, to_string(candidateIter->txID).c_str());
|
||||
txSpan.setAttribute(
|
||||
txq_span::attr::retriesRemaining,
|
||||
static_cast<int64_t>(candidateIter->retriesRemaining));
|
||||
|
||||
auto const [txnResult, didApply, _metadata] = candidateIter->apply(app, view, j_);
|
||||
txSpan.setAttribute(txq_span::attr::terCode, transToken(txnResult).c_str());
|
||||
|
||||
if (didApply)
|
||||
{
|
||||
txSpan.setAttribute(txq_span::attr::txqStatus, txq_span::val::applied);
|
||||
// Remove the candidate from the queue
|
||||
JLOG(j_.debug()) << "Queued transaction " << candidateIter->txID
|
||||
<< " applied successfully with " << transToken(txnResult)
|
||||
@@ -1468,12 +1518,14 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
{
|
||||
account.dropPenalty = true;
|
||||
}
|
||||
txSpan.setAttribute(txq_span::attr::txqStatus, txq_span::val::failed);
|
||||
JLOG(j_.debug()) << "Queued transaction " << candidateIter->txID << " failed with "
|
||||
<< transToken(txnResult) << ". Remove from queue.";
|
||||
candidateIter = eraseAndAdvance(candidateIter);
|
||||
}
|
||||
else
|
||||
{
|
||||
txSpan.setAttribute(txq_span::attr::txqStatus, txq_span::val::retried);
|
||||
JLOG(j_.debug()) << "Queued transaction " << candidateIter->txID << " failed with "
|
||||
<< transToken(txnResult) << ". Leave in queue."
|
||||
<< " Applied: " << didApply << ". Flags: " << candidateIter->flags;
|
||||
@@ -1569,6 +1621,7 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
}
|
||||
}
|
||||
XRPL_ASSERT(byFee_.size() == startingSize, "xrpl::TxQ::accept : byFee size match");
|
||||
span.setAttribute(txq_span::attr::ledgerChanged, ledgerChanged);
|
||||
|
||||
return ledgerChanged;
|
||||
}
|
||||
@@ -1652,6 +1705,10 @@ TxQ::tryDirectApply(
|
||||
ApplyFlags flags,
|
||||
beast::Journal j)
|
||||
{
|
||||
using namespace telemetry;
|
||||
[[maybe_unused]] auto span = SpanGuard::span(
|
||||
TraceCategory::Transactions, txq_span::prefix::txq, txq_span::op::applyDirect);
|
||||
|
||||
auto const account = (*tx)[sfAccount];
|
||||
auto const sleAccount = view.read(keylet::account(account));
|
||||
|
||||
|
||||
113
src/xrpld/app/misc/detail/TxQSpanNames.h
Normal file
113
src/xrpld/app/misc/detail/TxQSpanNames.h
Normal file
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
|
||||
/** Compile-time span name constants for Transaction Queue tracing.
|
||||
*
|
||||
* Covers the TxQ lifecycle: enqueue decisions, direct apply, batch
|
||||
* clear, ledger-close accept loop, per-tx apply, and cleanup.
|
||||
*
|
||||
* Span hierarchy:
|
||||
*
|
||||
* Transaction submission:
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | tx.process (existing, from TxSpanNames.h) |
|
||||
* | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* | | txq.enqueue | |
|
||||
* | | TxQ::apply() | |
|
||||
* | | attrs: tx_hash, tx_type, txq_status, | |
|
||||
* | | fee_level_paid, required_fee_level | |
|
||||
* | | | |
|
||||
* | | +-------------------+ +----------------------+ | |
|
||||
* | | | txq.apply_direct | | txq.batch_clear | | |
|
||||
* | | | tryDirectApply() | | tryClearAccount...() | | |
|
||||
* | | +-------------------+ | attrs: num_cleared | | |
|
||||
* | | +----------------------+ | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* Ledger close (consensus thread):
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | txq.accept |
|
||||
* | TxQ::accept() |
|
||||
* | attrs: queue_size, ledger_changed |
|
||||
* | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* | | txq.accept.tx (per queued transaction) | |
|
||||
* | | attrs: tx_hash, ter_code, retries_remaining | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* Post-close cleanup:
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | txq.cleanup |
|
||||
* | TxQ::processClosedLedger() |
|
||||
* | attrs: ledger_seq, expired_count |
|
||||
* +-------------------------------------------------------+
|
||||
*/
|
||||
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
|
||||
namespace xrpl::telemetry::txq_span {
|
||||
|
||||
// ===== Span prefixes =======================================================
|
||||
|
||||
namespace prefix {
|
||||
/// "txq" — root prefix for transaction queue spans.
|
||||
inline constexpr auto txq = makeStr("txq");
|
||||
} // namespace prefix
|
||||
|
||||
// ===== Span operation suffixes =============================================
|
||||
|
||||
namespace op {
|
||||
inline constexpr auto enqueue = makeStr("enqueue");
|
||||
inline constexpr auto applyDirect = makeStr("apply_direct");
|
||||
inline constexpr auto batchClear = makeStr("batch_clear");
|
||||
inline constexpr auto accept = makeStr("accept");
|
||||
inline constexpr auto acceptTx = makeStr("accept_tx");
|
||||
inline constexpr auto cleanup = makeStr("cleanup");
|
||||
} // namespace op
|
||||
|
||||
// ===== Attribute keys ======================================================
|
||||
|
||||
namespace attr {
|
||||
/// Canonical shared constants (defined in SpanNames.h).
|
||||
using ::xrpl::telemetry::attr::ledgerSeq;
|
||||
using ::xrpl::telemetry::attr::txHash;
|
||||
|
||||
/// "txq_status" — domain-qualified (collides with tx_status, rpc_status).
|
||||
inline constexpr auto txqStatus = makeStr("txq_status");
|
||||
/// "fee_level_paid" — fee level paid by queued tx.
|
||||
inline constexpr auto feeLevelPaid = makeStr("fee_level_paid");
|
||||
/// "required_fee_level" — minimum fee level for inclusion.
|
||||
inline constexpr auto requiredFeeLevel = makeStr("required_fee_level");
|
||||
/// "queue_size" — current TxQ depth.
|
||||
inline constexpr auto queueSize = makeStr("queue_size");
|
||||
/// "ledger_changed" — whether ledger changed since last attempt.
|
||||
inline constexpr auto ledgerChanged = makeStr("ledger_changed");
|
||||
/// "expired_count" — number of expired entries cleared.
|
||||
inline constexpr auto expiredCount = makeStr("expired_count");
|
||||
/// "ter_code" — transaction engine result code.
|
||||
inline constexpr auto terCode = makeStr("ter_code");
|
||||
/// "retries_remaining" — retries left before discard.
|
||||
inline constexpr auto retriesRemaining = makeStr("retries_remaining");
|
||||
/// "num_cleared" — entries cleared in batch.
|
||||
inline constexpr auto numCleared = makeStr("num_cleared");
|
||||
/// "tx_type" — transaction type name (e.g., "Payment", "OfferCreate").
|
||||
inline constexpr auto txType = makeStr("tx_type");
|
||||
} // namespace attr
|
||||
|
||||
// ===== Attribute values ====================================================
|
||||
|
||||
namespace val {
|
||||
inline constexpr auto queued = makeStr("queued");
|
||||
inline constexpr auto appliedDirect = makeStr("applied_direct");
|
||||
inline constexpr auto rejected = makeStr("rejected");
|
||||
inline constexpr auto applied = makeStr("applied");
|
||||
inline constexpr auto failed = makeStr("failed");
|
||||
inline constexpr auto retried = makeStr("retried");
|
||||
} // namespace val
|
||||
|
||||
} // namespace xrpl::telemetry::txq_span
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
#include <xrpld/app/misc/Transaction.h>
|
||||
#include <xrpld/app/misc/TxSpanNames.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/consensus/Validations.h>
|
||||
#include <xrpld/overlay/Cluster.h>
|
||||
@@ -21,6 +22,7 @@
|
||||
#include <xrpld/overlay/detail/Tuning.h>
|
||||
#include <xrpld/peerfinder/PeerfinderManager.h>
|
||||
#include <xrpld/peerfinder/Slot.h>
|
||||
#include <xrpld/telemetry/TxTracing.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
@@ -50,6 +52,7 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
@@ -62,6 +65,7 @@
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/server/NetworkOPs.h>
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
@@ -1322,6 +1326,17 @@ PeerImp::handleTransaction(
|
||||
auto stx = std::make_shared<STTx const>(sit);
|
||||
uint256 const txID = stx->getTransactionID();
|
||||
|
||||
using namespace telemetry;
|
||||
auto span = std::make_shared<SpanGuard>(txReceiveSpan(txID, *m));
|
||||
span->setAttribute(tx_span::attr::txHash, to_string(txID).c_str());
|
||||
span->setAttribute(tx_span::attr::peerId, static_cast<int64_t>(id_));
|
||||
if (auto const* fmt = TxFormats::getInstance().findByType(stx->getTxnType()))
|
||||
span->setAttribute(tx_span::attr::txType, fmt->getName().c_str());
|
||||
if (auto const version = getVersion(); !version.empty())
|
||||
span->setAttribute(tx_span::attr::peerVersion, version.c_str());
|
||||
// Note: suppressed and txStatus are set once at each exit path
|
||||
// (not as defaults here) to avoid OTel SDK attribute duplication.
|
||||
|
||||
// Charge strongly for attempting to relay a txn with tfInnerBatchTxn
|
||||
// LCOV_EXCL_START
|
||||
/*
|
||||
@@ -1342,6 +1357,7 @@ PeerImp::handleTransaction(
|
||||
*/
|
||||
if (stx->isFlag(tfInnerBatchTxn))
|
||||
{
|
||||
span->setAttribute(tx_span::attr::txStatus, tx_span::val::rejectedInnerBatch);
|
||||
JLOG(pJournal_.warn()) << "Ignoring Network relayed Tx containing "
|
||||
"tfInnerBatchTxn (handleTransaction).";
|
||||
fee_.update(Resource::kFeeModerateBurdenPeer, "inner batch txn");
|
||||
@@ -1354,18 +1370,28 @@ PeerImp::handleTransaction(
|
||||
|
||||
if (!app_.getHashRouter().shouldProcess(txID, id_, flags, kTxInterval))
|
||||
{
|
||||
span->setAttribute(tx_span::attr::suppressed, true);
|
||||
// we have seen this transaction recently
|
||||
if (any(flags & HashRouterFlags::BAD))
|
||||
{
|
||||
span->setAttribute(tx_span::attr::txStatus, tx_span::val::knownBad);
|
||||
fee_.update(Resource::kFeeUselessData, "known bad");
|
||||
JLOG(pJournal_.debug()) << "Ignoring known bad tx " << txID;
|
||||
}
|
||||
|
||||
// Erase only if the server has seen this tx. If the server has not
|
||||
// seen this tx then the tx could not has been queued for this peer.
|
||||
else if (eraseTxQueue && txReduceRelayEnabled())
|
||||
else
|
||||
{
|
||||
removeTxQueue(txID);
|
||||
// Recently-seen but not flagged bad — this is the plain
|
||||
// duplicate-suppression path. Mark it explicitly so the
|
||||
// span never exits as "new".
|
||||
span->setAttribute(tx_span::attr::txStatus, tx_span::val::suppressed);
|
||||
|
||||
// Erase only if the server has seen this tx. If the server
|
||||
// has not seen this tx then the tx could not have been
|
||||
// queued for this peer.
|
||||
if (eraseTxQueue && txReduceRelayEnabled())
|
||||
{
|
||||
removeTxQueue(txID);
|
||||
}
|
||||
}
|
||||
|
||||
overlay_.reportInboundTraffic(
|
||||
@@ -1374,6 +1400,7 @@ PeerImp::handleTransaction(
|
||||
return;
|
||||
}
|
||||
|
||||
span->setAttribute(tx_span::attr::suppressed, false);
|
||||
JLOG(pJournal_.debug()) << "Got tx " << txID;
|
||||
|
||||
bool checkSignature = true;
|
||||
@@ -1398,10 +1425,12 @@ PeerImp::handleTransaction(
|
||||
|
||||
if (app_.getLedgerMaster().getValidatedLedgerAge() > 4min)
|
||||
{
|
||||
span->setAttribute(tx_span::attr::txStatus, tx_span::val::droppedNoSync);
|
||||
JLOG(pJournal_.trace()) << "No new transactions until synchronized";
|
||||
}
|
||||
else if (app_.getJobQueue().getJobCount(JtTransaction) > app_.config().maxTransactions)
|
||||
{
|
||||
span->setAttribute(tx_span::attr::txStatus, tx_span::val::droppedQueueFull);
|
||||
overlay_.incJqTransOverflow();
|
||||
JLOG(pJournal_.info()) << "Transaction queue is full";
|
||||
}
|
||||
@@ -1414,7 +1443,8 @@ PeerImp::handleTransaction(
|
||||
flags,
|
||||
checkSignature,
|
||||
batch,
|
||||
stx]() {
|
||||
stx,
|
||||
sp = std::move(span)]() {
|
||||
if (auto peer = weak.lock())
|
||||
peer->checkTransaction(flags, checkSignature, stx, batch);
|
||||
});
|
||||
|
||||
@@ -177,7 +177,11 @@ private:
|
||||
void
|
||||
processSession(std::shared_ptr<Session> const&, std::shared_ptr<JobQueue::Coro> 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,
|
||||
|
||||
95
src/xrpld/rpc/detail/PathFindSpanNames.h
Normal file
95
src/xrpld/rpc/detail/PathFindSpanNames.h
Normal file
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
/** Compile-time span name constants for PathFind tracing.
|
||||
*
|
||||
* Covers the path_find and ripple_path_find RPC handlers, the
|
||||
* PathRequest computation engine, and the Pathfinder graph exploration.
|
||||
*
|
||||
* Span hierarchy:
|
||||
*
|
||||
* RPC entry (one-shot or subscription):
|
||||
*
|
||||
* +----------------------------------------------------------------+
|
||||
* | pathfind.request |
|
||||
* | doPathFind() / doRipplePathFind() |
|
||||
* | attrs: pathfind_source_account, pathfind_dest_account |
|
||||
* | (set when present in request params) |
|
||||
* | |
|
||||
* | +-----------------------------------------------------------+ |
|
||||
* | | pathfind.compute | |
|
||||
* | | PathRequest::doUpdate() | |
|
||||
* | | attrs: pathfind_fast | |
|
||||
* | | | |
|
||||
* | | +-----------------------------------------------------+ | |
|
||||
* | | | pathfind.discover (one per RPC call, hoisted above | |
|
||||
* | | | the per-source-asset loop in PathRequest::findPaths)| |
|
||||
* | | | attrs: pathfind_search_level, pathfind_num_paths | |
|
||||
* | | +-----------------------------------------------------+ | |
|
||||
* | +-----------------------------------------------------------+ |
|
||||
* +----------------------------------------------------------------+
|
||||
*
|
||||
* Async recomputation (ledger close):
|
||||
*
|
||||
* +----------------------------------------------------------------+
|
||||
* | pathfind.update_all |
|
||||
* | PathRequestManager::updateAll() |
|
||||
* | attrs: pathfind_ledger_index, pathfind_num_requests |
|
||||
* | |
|
||||
* | +-----------------------------------------------------------+ |
|
||||
* | | pathfind.compute (per active request) | |
|
||||
* | +-----------------------------------------------------------+ |
|
||||
* +----------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
|
||||
namespace xrpl::telemetry::pathfind_span {
|
||||
|
||||
// ===== Span prefixes =======================================================
|
||||
|
||||
namespace prefix {
|
||||
/// "pathfind" — root prefix for path finding spans.
|
||||
inline constexpr auto pathfind = makeStr("pathfind");
|
||||
} // namespace prefix
|
||||
|
||||
// ===== Span operation suffixes =============================================
|
||||
|
||||
namespace op {
|
||||
inline constexpr auto request = makeStr("request");
|
||||
inline constexpr auto compute = makeStr("compute");
|
||||
inline constexpr auto updateAll = makeStr("update_all");
|
||||
inline constexpr auto discover = makeStr("discover");
|
||||
} // namespace op
|
||||
|
||||
// ===== Attribute keys ======================================================
|
||||
//
|
||||
// All pathfind attributes are namespaced under `pathfind_*` (underscore form,
|
||||
// per Phase 1c naming spec rule 5). Avoids collisions with bare keys like
|
||||
// `fast` or `num_paths` that other subsystems may introduce.
|
||||
|
||||
namespace attr {
|
||||
/// "pathfind_source_account" — originating account for path search.
|
||||
inline constexpr auto sourceAccount = makeStr("pathfind_source_account");
|
||||
/// "pathfind_dest_account" — destination account.
|
||||
inline constexpr auto destAccount = makeStr("pathfind_dest_account");
|
||||
/// "pathfind_fast" — whether fast pathfinding mode enabled.
|
||||
inline constexpr auto fast = makeStr("pathfind_fast");
|
||||
/// "pathfind_search_level" — depth of graph exploration.
|
||||
inline constexpr auto searchLevel = makeStr("pathfind_search_level");
|
||||
/// "pathfind_num_paths" — total paths produced across the per-source-asset
|
||||
/// loop in PathRequest::findPaths (sum of getBestPaths().size() per asset).
|
||||
inline constexpr auto numPaths = makeStr("pathfind_num_paths");
|
||||
/// "pathfind_num_requests" — snapshot size of requests_ at update_all start
|
||||
/// (may include weak_ptrs that subsequently expire during processing).
|
||||
inline constexpr auto numRequests = makeStr("pathfind_num_requests");
|
||||
/// "pathfind_ledger_index" — pathfind target ledger index.
|
||||
inline constexpr auto ledgerIndex = makeStr("pathfind_ledger_index");
|
||||
/// "pathfind_dest_amount" — requested destination amount as string.
|
||||
inline constexpr auto destAmount = makeStr("pathfind_dest_amount");
|
||||
/// "pathfind_dest_currency" — destination currency code.
|
||||
inline constexpr auto destCurrency = makeStr("pathfind_dest_currency");
|
||||
/// "pathfind_num_source_assets" — candidate source assets count.
|
||||
inline constexpr auto numSourceAssets = makeStr("pathfind_num_source_assets");
|
||||
} // namespace attr
|
||||
|
||||
} // namespace xrpl::telemetry::pathfind_span
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/rpc/detail/AccountAssets.h>
|
||||
#include <xrpld/rpc/detail/PathFindSpanNames.h>
|
||||
#include <xrpld/rpc/detail/PathRequestManager.h>
|
||||
#include <xrpld/rpc/detail/Pathfinder.h>
|
||||
#include <xrpld/rpc/detail/PathfinderUtils.h>
|
||||
@@ -34,10 +35,12 @@
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/tx/paths/RippleCalc.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -579,6 +582,22 @@ PathRequest::findPaths(
|
||||
|
||||
auto const dstAmount = convertAmount(saDstAmount_, convertAll_);
|
||||
hash_map<PathAsset, std::unique_ptr<Pathfinder>> currencyMap;
|
||||
|
||||
// One `pathfind.discover` span wraps the entire per-source-asset loop so
|
||||
// that a single RPC call produces one discover span instead of N (one per
|
||||
// candidate source asset). Trade-off: per-asset discovery/ranking timing
|
||||
// is no longer split into individual spans — span count and Tempo storage
|
||||
// are bounded per RPC at the cost of per-asset visibility. If per-asset
|
||||
// breakdown is needed in the future, add child spans inside the loop body
|
||||
// (`Pathfinder::findPaths`/`computePathRanks`) parented off this span.
|
||||
using namespace telemetry;
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::discover);
|
||||
span.setAttribute(pathfind_span::attr::searchLevel, static_cast<int64_t>(level));
|
||||
span.setAttribute(
|
||||
pathfind_span::attr::numSourceAssets, static_cast<int64_t>(sourceAssets.size()));
|
||||
|
||||
std::int64_t totalPaths = 0;
|
||||
for (auto const& asset : sourceAssets)
|
||||
{
|
||||
if (continueCallback && !continueCallback())
|
||||
@@ -598,6 +617,7 @@ PathRequest::findPaths(
|
||||
auto ps = pathfinder->getBestPaths(
|
||||
kMaxPaths, fullLiquidityPath, context_[asset], asset.getIssuer(), continueCallback);
|
||||
context_[asset] = ps;
|
||||
totalPaths += static_cast<std::int64_t>(ps.size());
|
||||
|
||||
auto const& sourceAccount = [&] {
|
||||
if (!isXRP(asset.getIssuer()))
|
||||
@@ -700,6 +720,8 @@ PathRequest::findPaths(
|
||||
}
|
||||
}
|
||||
|
||||
span.setAttribute(pathfind_span::attr::numPaths, totalPaths);
|
||||
|
||||
/* The resource fee is based on the number of source currencies used.
|
||||
The minimum cost is 50 and the maximum is 400. The cost increases
|
||||
after four source currencies, 50 - (4 * 4) = 34.
|
||||
@@ -716,6 +738,13 @@ PathRequest::doUpdate(
|
||||
std::function<bool(void)> const& continueCallback)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
using namespace telemetry;
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::compute);
|
||||
span.setAttribute(pathfind_span::attr::fast, fast);
|
||||
span.setAttribute(pathfind_span::attr::destAmount, saDstAmount_.getFullText().c_str());
|
||||
span.setAttribute(pathfind_span::attr::destCurrency, to_string(saDstAmount_.asset()).c_str());
|
||||
|
||||
JLOG(journal_.debug()) << iIdentifier_ << " update " << (fast ? "fast" : "normal");
|
||||
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/rpc/detail/AssetCache.h>
|
||||
#include <xrpld/rpc/detail/PathFindSpanNames.h>
|
||||
#include <xrpld/rpc/detail/PathRequest.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -15,6 +16,7 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -71,6 +73,18 @@ PathRequestManager::updateAll(std::shared_ptr<ReadView const> const& inLedger)
|
||||
cache = getAssetCache(inLedger, true);
|
||||
}
|
||||
|
||||
// updateAll runs on every ledger close; skip span emission entirely when
|
||||
// there are no active path subscriptions to avoid a steady stream of empty
|
||||
// spans at mainnet close cadence.
|
||||
if (requests.empty())
|
||||
return;
|
||||
|
||||
using namespace telemetry;
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::updateAll);
|
||||
span.setAttribute(pathfind_span::attr::ledgerIndex, static_cast<int64_t>(inLedger->seq()));
|
||||
span.setAttribute(pathfind_span::attr::numRequests, static_cast<int64_t>(requests.size()));
|
||||
|
||||
bool newRequests = app_.getLedgerMaster().isNewPathRequest();
|
||||
bool mustBreak = false;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/Status.h>
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
#include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -16,14 +17,18 @@
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::RPC {
|
||||
namespace xrpl {
|
||||
using namespace telemetry;
|
||||
namespace RPC {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -157,6 +162,14 @@ template <class Object, class Method>
|
||||
Status
|
||||
callMethod(JsonContext& context, Method method, std::string const& name, Object& result)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::command, name);
|
||||
span.setAttribute(rpc_span::attr::command, name.c_str());
|
||||
span.setAttribute(rpc_span::attr::version, static_cast<int64_t>(context.apiVersion));
|
||||
span.setAttribute(
|
||||
rpc_span::attr::rpcRole,
|
||||
context.role == Role::ADMIN ? std::string_view(rpc_span::val::admin)
|
||||
: std::string_view(rpc_span::val::user));
|
||||
|
||||
static std::atomic<std::uint64_t> kRequestId{0};
|
||||
auto& perfLog = context.app.getPerfLog();
|
||||
std::uint64_t const curId = ++kRequestId;
|
||||
@@ -172,12 +185,32 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object&
|
||||
JLOG(context.j.debug()) << "RPC call " << name << " completed in "
|
||||
<< ((end - start).count() / 1000000000.0) << "seconds";
|
||||
perfLog.rpcFinish(name, curId);
|
||||
span.setAttribute(rpc_span::attr::loadType, context.loadType.label().c_str());
|
||||
// Status::operator bool() returns true when there IS an error
|
||||
// (code_ != OK), so the ternary correctly maps error->error, ok->success.
|
||||
span.setAttribute(
|
||||
rpc_span::attr::rpcStatus,
|
||||
ret ? std::string_view{rpc_span::val::error}
|
||||
: std::string_view{rpc_span::val::success});
|
||||
// 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)
|
||||
{
|
||||
perfLog.rpcError(name, curId);
|
||||
JLOG(context.j.info()) << "Caught throw: " << e.what();
|
||||
span.recordException(e);
|
||||
span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error);
|
||||
|
||||
if (context.loadType == Resource::kFeeReferenceRpc)
|
||||
context.loadType = Resource::kFeeExceptionRpc;
|
||||
@@ -195,6 +228,28 @@ doCommand(RPC::JsonContext& context, json::Value& result)
|
||||
Handler const* handler = nullptr;
|
||||
if (auto error = fillHandler(context, handler))
|
||||
{
|
||||
std::string cmdName;
|
||||
if (context.params.isMember(jss::command))
|
||||
{
|
||||
cmdName = context.params[jss::command].asString();
|
||||
}
|
||||
else if (context.params.isMember(jss::method))
|
||||
{
|
||||
cmdName = context.params[jss::method].asString();
|
||||
}
|
||||
else
|
||||
{
|
||||
cmdName = "unknown"; // LCOV_EXCL_LINE
|
||||
}
|
||||
// Use the resolved command name as the span suffix so dashboards
|
||||
// can break out per-command error rates (e.g. rpc.command.submit
|
||||
// for a submit that hit rpcTOO_BUSY). Falling back to a single
|
||||
// "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);
|
||||
return error;
|
||||
}
|
||||
@@ -234,4 +289,5 @@ roleRequired(unsigned int version, bool betaEnabled, std::string const& method)
|
||||
return handler->role;
|
||||
}
|
||||
|
||||
} // namespace xrpl::RPC
|
||||
} // namespace RPC
|
||||
} // namespace xrpl
|
||||
|
||||
167
src/xrpld/rpc/detail/RpcSpanNames.h
Normal file
167
src/xrpld/rpc/detail/RpcSpanNames.h
Normal file
@@ -0,0 +1,167 @@
|
||||
#pragma once
|
||||
|
||||
/** Compile-time span name constants for the RPC subsystem.
|
||||
*
|
||||
* All span prefixes, operation names, and attribute keys used by RPC
|
||||
* tracing call sites are defined here. Built on the StaticStr/join()
|
||||
* primitives from <xrpl/telemetry/SpanNames.h>.
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
* #include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
* using namespace telemetry;
|
||||
*
|
||||
* auto span = SpanGuard::span(
|
||||
* TraceCategory::Rpc, rpc_span::prefix::command, "submit");
|
||||
* span.setAttribute(rpc_span::attr::command, "submit");
|
||||
* span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::success);
|
||||
* @endcode
|
||||
*
|
||||
* Span hierarchy (automatic nesting via OTel thread-local context):
|
||||
*
|
||||
* HTTP JSON-RPC path (single request):
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | rpc.http_request |
|
||||
* | ServerHandler::processSession(Session) |
|
||||
* | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* | | rpc.process | |
|
||||
* | | ServerHandler::processRequest() | |
|
||||
* | | | |
|
||||
* | | +---------------------------------------------+ | |
|
||||
* | | | rpc.command.{name} | | |
|
||||
* | | | RPC::callMethod() | | |
|
||||
* | | | attrs: command, version, rpc_role, rpc_status | | |
|
||||
* | | +---------------------------------------------+ | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* HTTP batch path (multiple commands per request):
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | rpc.http_request |
|
||||
* | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* | | rpc.process | |
|
||||
* | | | |
|
||||
* | | +------------------+ +------------------+ | |
|
||||
* | | | rpc.command.{a} | | rpc.command.{b} | ... | |
|
||||
* | | +------------------+ +------------------+ | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* WebSocket path:
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | rpc.ws_message |
|
||||
* | ServerHandler::processSession(WSSession) |
|
||||
* | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* | | rpc.command.{name} | |
|
||||
* | | RPC::callMethod() | |
|
||||
* | | attrs: command, version, rpc_role, rpc_status | |
|
||||
* | +--------------------------------------------------+ |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* WebSocket error paths:
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | rpc.ws_message (error: invalid_json) |
|
||||
* | ServerHandler::onWSMessage() — parse failure |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | rpc.ws_upgrade |
|
||||
* | ServerHandler::onHandoff() — upgrade try/catch |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* Command dispatch error path:
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | rpc.command.{name} (error: too_busy/unknown/etc) |
|
||||
* | RPC::doCommand() — fillHandler() rejection |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* gRPC path (see GrpcSpanNames.h for constants):
|
||||
*
|
||||
* +-------------------------------------------------------+
|
||||
* | grpc.<MethodName> (e.g. grpc.GetLedger) |
|
||||
* | CallData::process(coro) |
|
||||
* | attrs: method, grpc_status |
|
||||
* +-------------------------------------------------------+
|
||||
*
|
||||
* Covered paths:
|
||||
* - HTTP JSON-RPC (single and batch requests)
|
||||
* - WebSocket RPC commands
|
||||
* - WebSocket message parse errors (invalid JSON, oversized)
|
||||
* - WebSocket upgrade failures (protocol handshake errors)
|
||||
* - Admin CLI (connects via HTTP internally)
|
||||
* - Command dispatch rejections (unknown cmd, too busy, no perm)
|
||||
* - gRPC endpoints (GetLedger, GetLedgerData, GetLedgerDiff,
|
||||
* GetLedgerEntry)
|
||||
* - Command execution: timing, success/failure, exceptions
|
||||
* - Per-command attributes: name, API version, rpc_role, rpc_status
|
||||
*
|
||||
* Known gaps (not yet instrumented):
|
||||
* - Early validation errors in processRequest() before rpc.process
|
||||
* span (malformed JSON, auth failures, oversized requests)
|
||||
* - Subscription push notifications (server-initiated, not RPC)
|
||||
*/
|
||||
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
|
||||
namespace xrpl::telemetry::rpc_span {
|
||||
|
||||
// ===== Span prefixes =======================================================
|
||||
|
||||
namespace prefix {
|
||||
/// "rpc" — root prefix for transport-level spans.
|
||||
inline constexpr auto rpc = seg::rpc;
|
||||
/// "rpc.command" — prefix for individual RPC command spans.
|
||||
inline constexpr auto command = join(seg::rpc, makeStr("command"));
|
||||
} // namespace prefix
|
||||
|
||||
// ===== Span operation suffixes =============================================
|
||||
|
||||
namespace op {
|
||||
inline constexpr auto wsMessage = makeStr("ws_message");
|
||||
inline constexpr auto wsUpgrade = makeStr("ws_upgrade");
|
||||
inline constexpr auto httpRequest = makeStr("http_request");
|
||||
inline constexpr auto process = makeStr("process");
|
||||
} // namespace op
|
||||
|
||||
// ===== Attribute keys ======================================================
|
||||
|
||||
namespace attr {
|
||||
/// "command" — RPC method name.
|
||||
inline constexpr auto command = makeStr("command");
|
||||
/// "version" — api_version per request.
|
||||
inline constexpr auto version = makeStr("version");
|
||||
/// "rpc_role" — admin|user. Domain-qualified: collides with grpc_role.
|
||||
inline constexpr auto rpcRole = makeStr("rpc_role");
|
||||
/// "rpc_status" — success|error. Domain-qualified: avoids OTel reserved span status.
|
||||
inline constexpr auto rpcStatus = makeStr("rpc_status");
|
||||
/// "request_payload_size" — bytes of inbound request payload.
|
||||
inline constexpr auto requestPayloadSize = makeStr("request_payload_size");
|
||||
/// "is_batch" — whether request is a JSON-RPC batch.
|
||||
inline constexpr auto isBatch = makeStr("is_batch");
|
||||
/// "batch_size" — number of sub-requests in a batch.
|
||||
inline constexpr auto batchSize = makeStr("batch_size");
|
||||
/// "load_type" — resource cost category after execution.
|
||||
inline constexpr auto loadType = makeStr("load_type");
|
||||
} // namespace attr
|
||||
|
||||
// ===== Attribute values ====================================================
|
||||
|
||||
namespace val {
|
||||
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");
|
||||
/// "invalid_json" — WS message parse failure or oversize.
|
||||
inline constexpr auto invalidJson = makeStr("invalid_json");
|
||||
} // namespace val
|
||||
|
||||
} // namespace xrpl::telemetry::rpc_span
|
||||
@@ -4,8 +4,10 @@
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/rpc/RPCHandler.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
#include <xrpld/rpc/detail/WSInfoSub.h>
|
||||
#include <xrpld/rpc/json_body.h> // IWYU pragma: keep
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
@@ -43,6 +45,7 @@
|
||||
#include <xrpl/server/SimpleWriter.h>
|
||||
#include <xrpl/server/WSSession.h>
|
||||
#include <xrpl/server/detail/JSONRPCUtil.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
@@ -60,6 +63,7 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
@@ -71,6 +75,7 @@
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
using namespace telemetry;
|
||||
|
||||
class Peer;
|
||||
class LedgerMaster;
|
||||
@@ -224,13 +229,17 @@ ServerHandler::onHandoff(
|
||||
if (!isWs)
|
||||
return statusRequestResponse(request, http::status::unauthorized);
|
||||
|
||||
auto span =
|
||||
SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::wsUpgrade);
|
||||
std::shared_ptr<WSSession> ws;
|
||||
try
|
||||
{
|
||||
ws = session.websocketUpgrade();
|
||||
span.setOk();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
span.recordException(e); // LCOV_EXCL_LINE
|
||||
JLOG(journal_.error()) << "Exception upgrading websocket: " << e.what() << "\n";
|
||||
return statusRequestResponse(request, http::status::internal_server_error);
|
||||
}
|
||||
@@ -339,10 +348,14 @@ ServerHandler::onWSMessage(
|
||||
auto const size = boost::asio::buffer_size(buffers);
|
||||
if (size > RPC::Tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject())
|
||||
{
|
||||
auto span =
|
||||
SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::wsMessage);
|
||||
span.setError(rpc_span::val::invalidJson);
|
||||
|
||||
json::Value jvResult(json::ValueType::Object);
|
||||
jvResult[jss::type] = jss::error;
|
||||
jvResult[jss::error] = "jsonInvalid";
|
||||
jvResult[jss::value] = buffersToString(buffers);
|
||||
jvResult[jss::value] = ::xrpl::buffersToString(buffers);
|
||||
boost::beast::multi_buffer sb;
|
||||
json::stream(jvResult, [&sb](auto const p, auto const n) {
|
||||
sb.commit(boost::asio::buffer_copy(sb.prepare(n), boost::asio::buffer(p, n)));
|
||||
@@ -415,12 +428,24 @@ ServerHandler::processSession(
|
||||
std::shared_ptr<JobQueue::Coro> const& coro,
|
||||
json::Value const& jv)
|
||||
{
|
||||
auto span = SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::wsMessage);
|
||||
if (jv.isMember(jss::command) && jv[jss::command].isString())
|
||||
{
|
||||
span.setAttribute(rpc_span::attr::command, jv[jss::command].asString().c_str());
|
||||
}
|
||||
else if (jv.isMember(jss::method) && jv[jss::method].isString())
|
||||
{
|
||||
span.setAttribute(rpc_span::attr::command, jv[jss::method].asString().c_str());
|
||||
}
|
||||
|
||||
auto is = std::static_pointer_cast<WSInfoSub>(session->appDefined);
|
||||
if (is->getConsumer().disconnect(journal_))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -452,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;
|
||||
}
|
||||
|
||||
@@ -498,6 +525,8 @@ ServerHandler::processSession(
|
||||
jr[jss::result] = RPC::makeError(RpcInternal);
|
||||
JLOG(journal_.error()) << "Exception while processing WS: " << ex.what() << "\n"
|
||||
<< "Input JSON: " << json::Compact{json::Value{jv}};
|
||||
span.recordException(ex);
|
||||
span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -530,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))
|
||||
@@ -557,9 +593,15 @@ ServerHandler::processSession(
|
||||
std::shared_ptr<Session> const& session,
|
||||
std::shared_ptr<JobQueue::Coro> coro)
|
||||
{
|
||||
processRequest(
|
||||
auto span =
|
||||
SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::httpRequest);
|
||||
|
||||
auto const requestBody = ::xrpl::buffersToString(session->request().body().data());
|
||||
span.setAttribute(rpc_span::attr::requestPayloadSize, static_cast<int64_t>(requestBody.size()));
|
||||
|
||||
bool const ok = processRequest(
|
||||
session->port(),
|
||||
buffersToString(session->request().body().data()),
|
||||
requestBody,
|
||||
session->remoteAddress().atPort(0),
|
||||
makeOutput(*session),
|
||||
coro,
|
||||
@@ -579,6 +621,15 @@ ServerHandler::processSession(
|
||||
{
|
||||
session->close(true);
|
||||
}
|
||||
// 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
|
||||
@@ -597,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,
|
||||
@@ -607,20 +658,33 @@ ServerHandler::processRequest(
|
||||
std::string_view forwardedFor,
|
||||
std::string_view user)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,11 +695,14 @@ 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();
|
||||
}
|
||||
span.setAttribute(rpc_span::attr::isBatch, batch);
|
||||
if (batch)
|
||||
span.setAttribute(rpc_span::attr::batchSize, static_cast<int64_t>(size));
|
||||
|
||||
json::Value reply(batch ? json::ValueType::Array : json::ValueType::Object);
|
||||
auto const start(std::chrono::high_resolution_clock::now());
|
||||
@@ -670,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;
|
||||
@@ -713,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");
|
||||
@@ -728,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");
|
||||
@@ -742,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");
|
||||
@@ -757,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");
|
||||
@@ -772,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");
|
||||
@@ -798,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
|
||||
{
|
||||
@@ -807,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -825,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;
|
||||
@@ -883,6 +950,9 @@ ServerHandler::processRequest(
|
||||
JLOG(journal_.error())
|
||||
<< "Internal error : " << ex.what()
|
||||
<< " 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
|
||||
}
|
||||
|
||||
@@ -899,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];
|
||||
@@ -919,6 +990,7 @@ ServerHandler::processRequest(
|
||||
// received.
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
spanHadError = true;
|
||||
auto rq = params;
|
||||
|
||||
if (rq.isObject())
|
||||
@@ -1014,7 +1086,20 @@ ServerHandler::processRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/detail/PathFindSpanNames.h>
|
||||
#include <xrpld/rpc/detail/PathRequestManager.h>
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
@@ -9,12 +10,21 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
json::Value
|
||||
doPathFind(RPC::JsonContext& context)
|
||||
{
|
||||
using namespace telemetry;
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::request);
|
||||
if (auto const& src = context.params[jss::source_account]; src.isString())
|
||||
span.setAttribute(pathfind_span::attr::sourceAccount, src.asString());
|
||||
if (auto const& dst = context.params[jss::destination_account]; dst.isString())
|
||||
span.setAttribute(pathfind_span::attr::destAccount, dst.asString());
|
||||
|
||||
if (context.app.config().pathSearchMax == 0)
|
||||
return rpcError(RpcNotSupported);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/LegacyPathFind.h>
|
||||
#include <xrpld/rpc/detail/PathFindSpanNames.h>
|
||||
#include <xrpld/rpc/detail/PathRequest.h>
|
||||
#include <xrpld/rpc/detail/PathRequestManager.h>
|
||||
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
|
||||
@@ -13,6 +14,7 @@
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
@@ -23,6 +25,14 @@ namespace xrpl {
|
||||
json::Value
|
||||
doRipplePathFind(RPC::JsonContext& context)
|
||||
{
|
||||
using namespace telemetry;
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::request);
|
||||
if (auto const& src = context.params[jss::source_account]; src.isString())
|
||||
span.setAttribute(pathfind_span::attr::sourceAccount, src.asString());
|
||||
if (auto const& dst = context.params[jss::destination_account]; dst.isString())
|
||||
span.setAttribute(pathfind_span::attr::destAccount, dst.asString());
|
||||
|
||||
if (context.app.config().pathSearchMax == 0)
|
||||
return rpcError(RpcNotSupported);
|
||||
|
||||
|
||||
60
src/xrpld/telemetry/PropagationHelpers.h
Normal file
60
src/xrpld/telemetry/PropagationHelpers.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
/** Helpers for injecting trace context into protobuf messages.
|
||||
*
|
||||
* Bridges the gap between SpanGuard (which hides OTel types) and the
|
||||
* protobuf TraceContext message used for cross-node propagation.
|
||||
*
|
||||
* Dependency diagram:
|
||||
*
|
||||
* SpanGuard::getTraceBytes() protocol::TraceContext (proto)
|
||||
* \ /
|
||||
* +--- TraceBytes -----+
|
||||
* | |
|
||||
* injectSpanContext(span, proto)
|
||||
*
|
||||
* @note When XRPL_ENABLE_TELEMETRY is disabled, getTraceBytes() returns
|
||||
* {.valid=false}, so injectSpanContext becomes a no-op with zero overhead.
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
* // Send side — inject from a SpanGuard reference:
|
||||
* protocol::TMTransaction tx;
|
||||
* // ... populate tx fields ...
|
||||
* injectSpanContext(mySpanGuard, *tx.mutable_trace_context());
|
||||
* overlay.relay(txID, tx, toSkip);
|
||||
* @endcode
|
||||
*
|
||||
* @see TxTracing.h for receive-side extraction helpers.
|
||||
* @see TraceContextPropagator.h for low-level OTel context serialization.
|
||||
*/
|
||||
|
||||
#include <xrpl/proto/xrpl.pb.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/** Inject trace context from an active SpanGuard into a protobuf
|
||||
* TraceContext message for cross-node propagation.
|
||||
*
|
||||
* Reads the span's trace_id, span_id, and trace_flags via
|
||||
* getTraceBytes() and writes them into the protobuf fields.
|
||||
* Safe to call from any thread that holds a reference to the span.
|
||||
* No-op if the span is null or inactive.
|
||||
*
|
||||
* @param span The active SpanGuard whose context to propagate.
|
||||
* @param proto The protobuf TraceContext to populate.
|
||||
*/
|
||||
inline void
|
||||
injectSpanContext(SpanGuard const& span, protocol::TraceContext& proto)
|
||||
{
|
||||
auto const bytes = span.getTraceBytes();
|
||||
if (!bytes.valid)
|
||||
return;
|
||||
|
||||
proto.set_trace_id(bytes.traceId.data(), bytes.traceId.size());
|
||||
proto.set_span_id(bytes.spanId.data(), bytes.spanId.size());
|
||||
proto.set_trace_flags(bytes.traceFlags);
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
64
src/xrpld/telemetry/TxTracing.h
Normal file
64
src/xrpld/telemetry/TxTracing.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
/** Helper functions for creating transaction trace spans.
|
||||
*
|
||||
* Encapsulates the logic for creating SpanGuard instances with
|
||||
* hash-derived trace IDs and optional protobuf parent extraction.
|
||||
* Call sites in PeerImp and NetworkOPs stay simple one-liners.
|
||||
*
|
||||
* When XRPL_ENABLE_TELEMETRY is not defined, the functions return
|
||||
* no-op SpanGuard instances (zero overhead, zero dependencies).
|
||||
*/
|
||||
|
||||
#include <xrpld/app/misc/TxSpanNames.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/proto/xrpl.pb.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/telemetry/TraceContextValidation.h>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/** Create a "tx.receive" span for a transaction received from a peer.
|
||||
* trace_id is derived from txID[0:16]. If the incoming message carries
|
||||
* a protobuf TraceContext with a valid span_id, it is used as the
|
||||
* parent to preserve relay ordering.
|
||||
*/
|
||||
inline SpanGuard
|
||||
txReceiveSpan(uint256 const& txID, [[maybe_unused]] protocol::TMTransaction const& msg)
|
||||
{
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
if (msg.has_trace_context())
|
||||
{
|
||||
auto const& tc = msg.trace_context();
|
||||
// Only the span_id is taken from the peer here; the trace_id is
|
||||
// derived locally from txID, so validate the span_id alone.
|
||||
if (tc.has_span_id() && isValidSpanId(tc.span_id()))
|
||||
{
|
||||
return SpanGuard::hashSpan(
|
||||
TraceCategory::Transactions,
|
||||
tx_span::receive,
|
||||
txID.data(),
|
||||
txID.kBytes,
|
||||
reinterpret_cast<std::uint8_t const*>(tc.span_id().data()),
|
||||
tc.span_id().size(),
|
||||
tc.has_trace_flags() ? static_cast<std::uint8_t>(tc.trace_flags())
|
||||
: std::uint8_t{0});
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return SpanGuard::hashSpan(
|
||||
TraceCategory::Transactions, tx_span::receive, txID.data(), txID.kBytes);
|
||||
}
|
||||
|
||||
/** Create a "tx.process" span for transaction processing in NetworkOPs.
|
||||
* trace_id is derived from txID[0:16].
|
||||
*/
|
||||
inline SpanGuard
|
||||
txProcessSpan(uint256 const& txID)
|
||||
{
|
||||
return SpanGuard::hashSpan(
|
||||
TraceCategory::Transactions, tx_span::process, txID.data(), txID.kBytes);
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
Reference in New Issue
Block a user