mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-11 17:20:18 +00:00
Compare commits
972 Commits
bthomee/no
...
pratik/ote
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a1bbc5c2b | ||
|
|
919478fab0 | ||
|
|
8a63b31c0e | ||
|
|
905061ce80 | ||
|
|
17c17901dc | ||
|
|
906780b622 | ||
|
|
b2baefa907 | ||
|
|
868ee3eadd | ||
|
|
a512009170 | ||
|
|
e9a0b49c5b | ||
|
|
ae47ab08f0 | ||
|
|
8ca7036a8b | ||
|
|
5f8ca9d84c | ||
|
|
24542c46c5 | ||
|
|
0c9001d058 | ||
|
|
e357cf743d | ||
|
|
086c85fb2c | ||
|
|
a998b80f7b | ||
|
|
4f2b7cfb45 | ||
|
|
4aa2181b99 | ||
|
|
3143216ad1 | ||
|
|
9820ae8e38 | ||
|
|
ba9ac50559 | ||
|
|
b0f4370397 | ||
|
|
a90d51c808 | ||
|
|
9fa9a34d49 | ||
|
|
d03128e2ed | ||
|
|
982e3cf9ef | ||
|
|
86bbe8f55b | ||
|
|
34a237fab6 | ||
|
|
d174d4a7d8 | ||
|
|
6ae6e623a7 | ||
|
|
6530e7d76c | ||
|
|
dae3f6e4d1 | ||
|
|
f2d1ac5c78 | ||
|
|
50d7047abe | ||
|
|
4ed9bd595d | ||
|
|
bab0c84793 | ||
|
|
46cff27b15 | ||
|
|
91c6582bd5 | ||
|
|
cefcd88c87 | ||
|
|
7ab34d2e37 | ||
|
|
e304d65d63 | ||
|
|
48c3c7ecfc | ||
|
|
bbdb8dc19b | ||
|
|
71f725204c | ||
|
|
c2e5e0d808 | ||
|
|
9240dc2a12 | ||
|
|
baacd054c9 | ||
|
|
ebf8b6e8a9 | ||
|
|
84f2ff6b58 | ||
|
|
c7b04a043e | ||
|
|
6512ce604f | ||
|
|
f6b5b663dc | ||
|
|
1732c913a3 | ||
|
|
5f32fb79d4 | ||
|
|
d17c1c5c7f | ||
|
|
2321b4985d | ||
|
|
1d6475ac0e | ||
|
|
04f1775e10 | ||
|
|
ec7d99228a | ||
|
|
09e083a718 | ||
|
|
e7ba376839 | ||
|
|
0b76f91f02 | ||
|
|
77f35c03db | ||
|
|
11d2d9038b | ||
|
|
f0d72f54d0 | ||
|
|
12060bc219 | ||
|
|
a8ce1408a9 | ||
|
|
c0a3e113d9 | ||
|
|
ce9bdfcc55 | ||
|
|
02b32ecdd8 | ||
|
|
61ae9998e1 | ||
|
|
22f5b77e0c | ||
|
|
adeda255f0 | ||
|
|
d64608e8a7 | ||
|
|
68b1c7d39a | ||
|
|
087e6e5b63 | ||
|
|
2ac68427fc | ||
|
|
5ba27cc90f | ||
|
|
f4549b68c7 | ||
|
|
7848574a38 | ||
|
|
8800160bb5 | ||
|
|
4e9df8b5c3 | ||
|
|
c2e5c933a7 | ||
|
|
a396fe073f | ||
|
|
0bfb338666 | ||
|
|
a681eea6c4 | ||
|
|
0a373f631c | ||
|
|
b1091a482c | ||
|
|
a1a9d45bff | ||
|
|
4826038068 | ||
|
|
749535a3ca | ||
|
|
87c003230c | ||
|
|
6938614468 | ||
|
|
8108caffe3 | ||
|
|
db38ba268f | ||
|
|
01c9f6cf08 | ||
|
|
ba8785dc16 | ||
|
|
c6be7cf83a | ||
|
|
c75de2d58c | ||
|
|
a2a58a5d85 | ||
|
|
a3053a34a7 | ||
|
|
87f370a64d | ||
|
|
ff92c3727c | ||
|
|
1eb44689d0 | ||
|
|
58af1e6f18 | ||
|
|
e372c45836 | ||
|
|
2ebc96a4a6 | ||
|
|
e4a78684e1 | ||
|
|
f99a1c5310 | ||
|
|
41875b1897 | ||
|
|
9a769b9729 | ||
|
|
afe653e4b6 | ||
|
|
fca98d1003 | ||
|
|
35148b57f8 | ||
|
|
5e77968ca2 | ||
|
|
9e5f1caff2 | ||
|
|
e7bdf659fb | ||
|
|
ba987bcb39 | ||
|
|
aefc87a572 | ||
|
|
89cb633869 | ||
|
|
40b457669e | ||
|
|
b54e0e50c0 | ||
|
|
36c08fdc66 | ||
|
|
9879780dd8 | ||
|
|
b1af0a9511 | ||
|
|
c3ac033a78 | ||
|
|
a8ca3d50c3 | ||
|
|
d76e067148 | ||
|
|
bc140d9e96 | ||
|
|
f80c1c3b4e | ||
|
|
04f351f3b7 | ||
|
|
ab0d8249fd | ||
|
|
cc4457b5a2 | ||
|
|
c5a6de6ef7 | ||
|
|
4abc180778 | ||
|
|
134b419895 | ||
|
|
55052917db | ||
|
|
363c2caf94 | ||
|
|
f4981a1907 | ||
|
|
7cc963f494 | ||
|
|
1938b9e377 | ||
|
|
04d4bba780 | ||
|
|
5287893d82 | ||
|
|
b7d0b700c6 | ||
|
|
89b5210e5b | ||
|
|
ea9c639ee9 | ||
|
|
7998d01dc9 | ||
|
|
e02b7968ad | ||
|
|
38f6afbf4c | ||
|
|
092afb7aae | ||
|
|
7a6ff619e5 | ||
|
|
60a2ac7bfe | ||
|
|
5ee8ee71fd | ||
|
|
3b08c2d90a | ||
|
|
26a85c764e | ||
|
|
0505ca35aa | ||
|
|
fe88034ea9 | ||
|
|
fd518d3e7b | ||
|
|
d5b858ce9d | ||
|
|
eec92f52e9 | ||
|
|
e969b0c6ed | ||
|
|
33c17e50b3 | ||
|
|
e129b0793c | ||
|
|
45aa2f276a | ||
|
|
3789ac45e2 | ||
|
|
dec68e3673 | ||
|
|
af783b1df6 | ||
|
|
7881b707d2 | ||
|
|
424be46662 | ||
|
|
2da3e73b63 | ||
|
|
2b419338fb | ||
|
|
2ab9fb9a54 | ||
|
|
d4ee847bcc | ||
|
|
d84fdbd29f | ||
|
|
0f630d1afa | ||
|
|
22940969ff | ||
|
|
5709b7484e | ||
|
|
90b3726ed7 | ||
|
|
be50ecda06 | ||
|
|
f600660364 | ||
|
|
5cf22a9758 | ||
|
|
8c067f5fb4 | ||
|
|
4e21aefe74 | ||
|
|
bb0df16c90 | ||
|
|
5031ab2b4e | ||
|
|
90584be746 | ||
|
|
80d33662e4 | ||
|
|
8c02c1595d | ||
|
|
629514dcc3 | ||
|
|
3b84e32941 | ||
|
|
52ec7f2a11 | ||
|
|
14a689f1ed | ||
|
|
2f027551c9 | ||
|
|
85ce6467d0 | ||
|
|
1ea7bb5252 | ||
|
|
2c590a47c5 | ||
|
|
d5d9d1d8da | ||
|
|
6ff90a2892 | ||
|
|
78fb08ef38 | ||
|
|
324b58d38f | ||
|
|
9f8a4c9561 | ||
|
|
2a90b582ff | ||
|
|
6961727ba7 | ||
|
|
e86c1adc5d | ||
|
|
b0d46af0ca | ||
|
|
50377df271 | ||
|
|
7befb16705 | ||
|
|
9a4a97cdec | ||
|
|
b0197105ea | ||
|
|
400f84aafe | ||
|
|
b59b3c1bb4 | ||
|
|
1a279e3edc | ||
|
|
5ade9431ef | ||
|
|
c669c20bc0 | ||
|
|
93f776fda4 | ||
|
|
fdfa651006 | ||
|
|
f1f0189c11 | ||
|
|
1ed08d09d0 | ||
|
|
8e4ab5bd04 | ||
|
|
73edc2dd58 | ||
|
|
a2e8f3a6b2 | ||
|
|
b724dd1e94 | ||
|
|
6e9d71c5eb | ||
|
|
f81919af05 | ||
|
|
4c46d39955 | ||
|
|
d2c7a00584 | ||
|
|
20179dc0ed | ||
|
|
294b167355 | ||
|
|
f55f8c59fc | ||
|
|
3c0ac8f257 | ||
|
|
3d4119a3ba | ||
|
|
50c7d672d4 | ||
|
|
04df7fd92c | ||
|
|
d79a41dde6 | ||
|
|
78f3595bc1 | ||
|
|
55e136f1b4 | ||
|
|
389adbce62 | ||
|
|
ebc355a465 | ||
|
|
ffafa801f6 | ||
|
|
f7ddfbe3a9 | ||
|
|
0fa2cc0c63 | ||
|
|
bf808399a2 | ||
|
|
074fb0ff22 | ||
|
|
c16b71dac6 | ||
|
|
73e386eca5 | ||
|
|
b8a5361e28 | ||
|
|
5cc837289a | ||
|
|
498bc3ee96 | ||
|
|
cc37bc3775 | ||
|
|
c7d0fdfac3 | ||
|
|
58b02f3fbc | ||
|
|
3c9179e59d | ||
|
|
b9692c5536 | ||
|
|
4e60190199 | ||
|
|
3635fe0f2e | ||
|
|
508549f657 | ||
|
|
43eddb79f6 | ||
|
|
2668e9f8ca | ||
|
|
0ce13ff2e7 | ||
|
|
394a64f437 | ||
|
|
ea2e6787c6 | ||
|
|
b3c5f8860c | ||
|
|
f48b212a1c | ||
|
|
0f1bd86ba2 | ||
|
|
6c21b92d15 | ||
|
|
cf629e2a76 | ||
|
|
190b9470a4 | ||
|
|
dc3d5107fd | ||
|
|
3cfec0f1e0 | ||
|
|
ce7fe95715 | ||
|
|
3463a85943 | ||
|
|
bc7cff230c | ||
|
|
54d92ff973 | ||
|
|
9a2d682a42 | ||
|
|
9d4570f65c | ||
|
|
ed0bab7c40 | ||
|
|
49103d635c | ||
|
|
4d5a71d327 | ||
|
|
58d217599b | ||
|
|
039d6ccf4f | ||
|
|
f9092e7948 | ||
|
|
2701ca416f | ||
|
|
f0b834a913 | ||
|
|
0e9a71798c | ||
|
|
ba85aecf00 | ||
|
|
37a4c9deb8 | ||
|
|
482e136d19 | ||
|
|
0ff947454c | ||
|
|
b9f0a3ae19 | ||
|
|
34f41ea37f | ||
|
|
46dbc92b5f | ||
|
|
7b7b3e7dcd | ||
|
|
f08c098049 | ||
|
|
bef40b37e7 | ||
|
|
8eac1a0e20 | ||
|
|
f76f0c00ac | ||
|
|
c30123796e | ||
|
|
88d2ff61cf | ||
|
|
a7c7e2767e | ||
|
|
a3c629251a | ||
|
|
f29f32816b | ||
|
|
663ef989eb | ||
|
|
6baad644b7 | ||
|
|
a0b5f0ecd7 | ||
|
|
e1045a84aa | ||
|
|
25447e508e | ||
|
|
1ad67e8aa5 | ||
|
|
84089b4b4d | ||
|
|
e5b46e6cf8 | ||
|
|
c8e5670aed | ||
|
|
43a70551b9 | ||
|
|
725b4fb5cb | ||
|
|
ae114c5341 | ||
|
|
19a6c2a306 | ||
|
|
172dff77d5 | ||
|
|
c3ccde3e39 | ||
|
|
282aec4367 | ||
|
|
c13aac48c4 | ||
|
|
94a85627f2 | ||
|
|
e97878c5d7 | ||
|
|
bf7fcf58f0 | ||
|
|
a8a88410f4 | ||
|
|
5e7b71d600 | ||
|
|
68173a8354 | ||
|
|
2644179a42 | ||
|
|
6c62bfd2ad | ||
|
|
fb81d13d64 | ||
|
|
bb4f129aec | ||
|
|
5d116ff2ec | ||
|
|
4dff30b6c6 | ||
|
|
2709c7ebc5 | ||
|
|
686c8a8ffb | ||
|
|
be9cc0df5b | ||
|
|
3236daf08e | ||
|
|
8f90b6a3d0 | ||
|
|
c28e432cd7 | ||
|
|
3272f07058 | ||
|
|
b1ecb718ba | ||
|
|
d058e5ac3c | ||
|
|
ae80391da6 | ||
|
|
6ec60ff52c | ||
|
|
2efc932165 | ||
|
|
8b2695b015 | ||
|
|
65e64205ef | ||
|
|
1b6945317e | ||
|
|
8add336c1a | ||
|
|
d27d67dfe4 | ||
|
|
59030e5d61 | ||
|
|
6c4c3e1049 | ||
|
|
5705c4dc02 | ||
|
|
448aaa4718 | ||
|
|
180c905a8a | ||
|
|
85103e4550 | ||
|
|
087fcf54b1 | ||
|
|
294c276e29 | ||
|
|
7cb08307a7 | ||
|
|
b1e6d90af1 | ||
|
|
9e6c5b5778 | ||
|
|
2f02fc2c04 | ||
|
|
4086ac9518 | ||
|
|
4d044e6254 | ||
|
|
d8d6142fbe | ||
|
|
afe0818c33 | ||
|
|
ca7282479f | ||
|
|
134a24d5bc | ||
|
|
480b6cab3c | ||
|
|
d5efd657ae | ||
|
|
d6450631bf | ||
|
|
e9cb9421ef | ||
|
|
c0272f1314 | ||
|
|
ede8a53a76 | ||
|
|
fdcbf37e0b | ||
|
|
53ebd7a8c2 | ||
|
|
6c1d4d92bb | ||
|
|
ffc197b914 | ||
|
|
4a0994209e | ||
|
|
f37589b1f5 | ||
|
|
4ed409f35a | ||
|
|
d126868a25 | ||
|
|
56f2e2c4cf | ||
|
|
34d7280df4 | ||
|
|
8908036b11 | ||
|
|
e205d0ef8e | ||
|
|
38fbab1d18 | ||
|
|
c128625857 | ||
|
|
e11bf35691 | ||
|
|
7784fa29fa | ||
|
|
93aae45c77 | ||
|
|
c35874be6d | ||
|
|
c2a39bc21f | ||
|
|
0ff2bed63a | ||
|
|
331d9d55b1 | ||
|
|
4ce882965a | ||
|
|
385c3cd91c | ||
|
|
7a4baab7cb | ||
|
|
3cb9a2bf51 | ||
|
|
21c177c8f9 | ||
|
|
a7c3cd8a79 | ||
|
|
734cec319a | ||
|
|
ad48c7b3a9 | ||
|
|
9268242040 | ||
|
|
b32db4ceeb | ||
|
|
207864a98b | ||
|
|
57d382ceda | ||
|
|
848cbcbfbe | ||
|
|
4f53291fe8 | ||
|
|
55a674f042 | ||
|
|
2569cd129a | ||
|
|
bf7f705de2 | ||
|
|
f3fc85fd2b | ||
|
|
72b449c3ce | ||
|
|
ffd972f8be | ||
|
|
5e6817df61 | ||
|
|
b9de9d0abe | ||
|
|
f12c896ac7 | ||
|
|
8fe3f06999 | ||
|
|
d3c09fd3f4 | ||
|
|
142e8c5b36 | ||
|
|
e5f890e195 | ||
|
|
c712968890 | ||
|
|
fa71280795 | ||
|
|
bb8f7f0e9a | ||
|
|
c43348886f | ||
|
|
5598b0eac7 | ||
|
|
0628dc172e | ||
|
|
3e4b1eb60f | ||
|
|
3633ffc4c7 | ||
|
|
bbabe8d8a7 | ||
|
|
07599778d4 | ||
|
|
5e448e32f9 | ||
|
|
b602348e64 | ||
|
|
cf71dc0a01 | ||
|
|
9512930b0f | ||
|
|
be67ad25e7 | ||
|
|
3ee8f900ec | ||
|
|
a119efc478 | ||
|
|
a946ce7458 | ||
|
|
57a54ad0fe | ||
|
|
fe13359024 | ||
|
|
ea5991fb0a | ||
|
|
f5f13df5ff | ||
|
|
4a3ba35c1d | ||
|
|
c5b96afe07 | ||
|
|
5cedc5d44b | ||
|
|
f12fa8d3a9 | ||
|
|
8046a30e9b | ||
|
|
4a8aa9e514 | ||
|
|
b8dd848899 | ||
|
|
b321792a14 | ||
|
|
72642b5dc6 | ||
|
|
8f3974c094 | ||
|
|
283fbaa54f | ||
|
|
3167a49f41 | ||
|
|
759d3506b2 | ||
|
|
021300538a | ||
|
|
a71d6635e6 | ||
|
|
3df7e9cba6 | ||
|
|
6a16dfa823 | ||
|
|
6428c9f13c | ||
|
|
d6b314e8d5 | ||
|
|
0a800069bf | ||
|
|
ca3a78abce | ||
|
|
eef11a65fa | ||
|
|
342b9f55a1 | ||
|
|
000ad1d1f5 | ||
|
|
17ffe8b049 | ||
|
|
63c6f3b8df | ||
|
|
4174aef07b | ||
|
|
a5f80514a9 | ||
|
|
45ab508ed8 | ||
|
|
6c71aa8c2a | ||
|
|
9b46a343fc | ||
|
|
15d3e3a375 | ||
|
|
0fe09cda9b | ||
|
|
194f5b8af8 | ||
|
|
8f9fa52f93 | ||
|
|
fb7c3bc38d | ||
|
|
8e606bbaf4 | ||
|
|
811b934004 | ||
|
|
c80038fd42 | ||
|
|
7397bbcdd2 | ||
|
|
9947a52e79 | ||
|
|
ee2f1b4fbf | ||
|
|
2627ea7f65 | ||
|
|
4e422a0354 | ||
|
|
36cae13352 | ||
|
|
013252f210 | ||
|
|
970914d2ce | ||
|
|
289b049b70 | ||
|
|
dfd67b8124 | ||
|
|
f60c995fe1 | ||
|
|
fff8598a33 | ||
|
|
ac1805f0a4 | ||
|
|
365907ab22 | ||
|
|
8b5ded4324 | ||
|
|
39f3b86d17 | ||
|
|
2ef026aef5 | ||
|
|
03fffec640 | ||
|
|
a13a858112 | ||
|
|
a4bc7bd611 | ||
|
|
8adb5d03da | ||
|
|
66552e7858 | ||
|
|
2264a8427a | ||
|
|
c5bdaafc39 | ||
|
|
4b6c1c270f | ||
|
|
3eeb8b3730 | ||
|
|
93c27997b4 | ||
|
|
ac79a5123e | ||
|
|
1b227a1eff | ||
|
|
b0e9e1a24d | ||
|
|
bf0b843ce1 | ||
|
|
fce770e4f4 | ||
|
|
8dd5ac55e8 | ||
|
|
507828edde | ||
|
|
aca6623f14 | ||
|
|
765c96919c | ||
|
|
7a9215a4d3 | ||
|
|
dd9cde88f3 | ||
|
|
e52f1470b6 | ||
|
|
1a2f9a71f5 | ||
|
|
ebf107e73c | ||
|
|
d5f9242f84 | ||
|
|
84fc829be3 | ||
|
|
bdd7dea4b4 | ||
|
|
b46ee12a19 | ||
|
|
25e08b1840 | ||
|
|
994e425804 | ||
|
|
bed6770751 | ||
|
|
dfdda305ee | ||
|
|
2ac93c504e | ||
|
|
47f3480fb9 | ||
|
|
20a6274a48 | ||
|
|
7c09c38a5b | ||
|
|
c5ae16cdd9 | ||
|
|
1162b6f3bc | ||
|
|
0bcc7635ac | ||
|
|
1ab28d0cf0 | ||
|
|
967f0082c3 | ||
|
|
eeae09c645 | ||
|
|
34a2afa704 | ||
|
|
b8602b7821 | ||
|
|
7bc6c65bb2 | ||
|
|
d6fefe2468 | ||
|
|
781e08a6a6 | ||
|
|
a8d70c15f8 | ||
|
|
dc7788aab8 | ||
|
|
cd6264c02f | ||
|
|
7aebc62223 | ||
|
|
6554f04252 | ||
|
|
f51b113f4b | ||
|
|
7cf55315b5 | ||
|
|
3cd3d5e80e | ||
|
|
c76008d24c | ||
|
|
1fd0ee5999 | ||
|
|
771eb3d66c | ||
|
|
b24456abb4 | ||
|
|
ded9847eaf | ||
|
|
9918803333 | ||
|
|
bf527a41dd | ||
|
|
154d441ff2 | ||
|
|
ce6a3153a1 | ||
|
|
3115313551 | ||
|
|
2e61a1c412 | ||
|
|
046e2e2b85 | ||
|
|
e901a3604a | ||
|
|
523bfdbbe1 | ||
|
|
9f81e770eb | ||
|
|
670b6ef3d5 | ||
|
|
5d1e3f207c | ||
|
|
e321f294e5 | ||
|
|
1a0780fd3e | ||
|
|
5dd5e765ae | ||
|
|
c157253372 | ||
|
|
280217653d | ||
|
|
ba7e1f98e4 | ||
|
|
d7579b2861 | ||
|
|
088848e7ab | ||
|
|
e7dea147cd | ||
|
|
8d730b8b9a | ||
|
|
e5fae351d6 | ||
|
|
a44d91ec27 | ||
|
|
2f96c6547c | ||
|
|
c187a62353 | ||
|
|
c848e51e13 | ||
|
|
8395e69e94 | ||
|
|
8f9057729c | ||
|
|
f031befc6e | ||
|
|
4e8d37facf | ||
|
|
071ad45d31 | ||
|
|
c9901595f7 | ||
|
|
3a1f22583f | ||
|
|
e1163f7180 | ||
|
|
f66a53cfc9 | ||
|
|
68a69d9064 | ||
|
|
309d3e6449 | ||
|
|
4e0b6f5b9e | ||
|
|
a35003b123 | ||
|
|
53e8c4d54e | ||
|
|
c070177800 | ||
|
|
5700eeed1b | ||
|
|
23387f6a06 | ||
|
|
d6b101069e | ||
|
|
c9521b97fe | ||
|
|
248d85cae6 | ||
|
|
7ac5343119 | ||
|
|
954223958f | ||
|
|
c6c019ed8b | ||
|
|
43258e8dc0 | ||
|
|
9e89d74d2f | ||
|
|
8b790ebac9 | ||
|
|
4bd1176df5 | ||
|
|
4c4c6f5de2 | ||
|
|
9498b2865f | ||
|
|
64ffcffe32 | ||
|
|
bfdcd3da87 | ||
|
|
f6f0cb1a5f | ||
|
|
6aa8570d6c | ||
|
|
824f63216a | ||
|
|
a104140a51 | ||
|
|
0e25103fdb | ||
|
|
0e5e802e5e | ||
|
|
6985e1948b | ||
|
|
a844c14e49 | ||
|
|
c3c980e858 | ||
|
|
92bc0b24b8 | ||
|
|
1a36ef4b0f | ||
|
|
a789f6ccf5 | ||
|
|
44cdc8133e | ||
|
|
dfe91e071f | ||
|
|
dec8b0a9a1 | ||
|
|
df1d8aed44 | ||
|
|
41d72cb51b | ||
|
|
45e1c15d24 | ||
|
|
865ab65a07 | ||
|
|
009c63e7db | ||
|
|
5d70a5fffd | ||
|
|
f3a095ab65 | ||
|
|
577cb9b5f0 | ||
|
|
7d202127bb | ||
|
|
56090b0ead | ||
|
|
6c6d6f953f | ||
|
|
0b4b3c7bf2 | ||
|
|
3e894f8e93 | ||
|
|
cb7dc5c52e | ||
|
|
9cfb43d8d0 | ||
|
|
7ada57e2a8 | ||
|
|
8df3ea1bbe | ||
|
|
5a6882f119 | ||
|
|
b449db0434 | ||
|
|
9babfff3c8 | ||
|
|
68b32ed0f0 | ||
|
|
61ab5c6fe3 | ||
|
|
837f7e7b50 | ||
|
|
b392035544 | ||
|
|
450004ebd8 | ||
|
|
6f403fdd1b | ||
|
|
690841e934 | ||
|
|
93caaba5ca | ||
|
|
829094df5a | ||
|
|
9554e3889b | ||
|
|
fe7cb33b65 | ||
|
|
f5cf4155c2 | ||
|
|
ea30adeb47 | ||
|
|
9bc2e4abb3 | ||
|
|
7b9e0bc300 | ||
|
|
6dd43765b5 | ||
|
|
cbf389943f | ||
|
|
b05e650b6f | ||
|
|
57175ab12c | ||
|
|
d44a0aa3ff | ||
|
|
522fe562ff | ||
|
|
745102360b | ||
|
|
19d9c44cf5 | ||
|
|
5c14b57462 | ||
|
|
c875944e05 | ||
|
|
2430032e3a | ||
|
|
0f63d14999 | ||
|
|
faaec003f4 | ||
|
|
6cd910f06f | ||
|
|
5cd71ed107 | ||
|
|
9e27120a15 | ||
|
|
e60efd4d2f | ||
|
|
c48f5ed6e7 | ||
|
|
c9fe4b1a14 | ||
|
|
46d1012ad4 | ||
|
|
7eeddd3ad9 | ||
|
|
e339ba1f6b | ||
|
|
ac1b01b4c7 | ||
|
|
497dd007d9 | ||
|
|
0d845149ec | ||
|
|
7a854ccad2 | ||
|
|
580ee5ede7 | ||
|
|
937d11d7c3 | ||
|
|
fac3287912 | ||
|
|
4bbe28cb92 | ||
|
|
dd4911ef5e | ||
|
|
e49c5997b7 | ||
|
|
5ad5bacc94 | ||
|
|
98aa9c9095 | ||
|
|
92072ecca4 | ||
|
|
85330920ac | ||
|
|
a61cdf0214 | ||
|
|
fac6c3ac1d | ||
|
|
a8549a7ab2 | ||
|
|
761688383d | ||
|
|
ed31bab500 | ||
|
|
b62987bda7 | ||
|
|
f08412b3e0 | ||
|
|
43ae5c2d20 | ||
|
|
8dec56df0b | ||
|
|
beaf01ae4d | ||
|
|
5e37f71139 | ||
|
|
3f793b2098 | ||
|
|
f4555c80fe | ||
|
|
5bdb6b4eaf | ||
|
|
f44b89b99d | ||
|
|
1c0d21dba4 | ||
|
|
f183e9b57f | ||
|
|
8e7a2d6c53 | ||
|
|
9adcc49171 | ||
|
|
7ab6f4d34b | ||
|
|
81b47afde7 | ||
|
|
b65f91117f | ||
|
|
57ed0d9fd0 | ||
|
|
51918ef868 | ||
|
|
c8674d61b8 | ||
|
|
cf18032e7f | ||
|
|
f18ddd95c1 | ||
|
|
769668579a | ||
|
|
e6266e4e8d | ||
|
|
025620cc4e | ||
|
|
692ce65f3e | ||
|
|
bb3732c22f | ||
|
|
87a8780b73 | ||
|
|
78522ba18e | ||
|
|
79fbb9c303 | ||
|
|
e21e7b0d51 | ||
|
|
3dd2f34591 | ||
|
|
521e0756e1 | ||
|
|
dbcd040180 | ||
|
|
17e69e660c | ||
|
|
ef10c754b1 | ||
|
|
912890c104 | ||
|
|
ac68091bec | ||
|
|
2773de7b54 | ||
|
|
5f7de1bb48 | ||
|
|
264516c37d | ||
|
|
eb84ac57c7 | ||
|
|
021c81e978 | ||
|
|
ab6b6d215e | ||
|
|
53d0daf3b4 | ||
|
|
8fb33b0818 | ||
|
|
61cb1faf8f | ||
|
|
5cbb349efa | ||
|
|
93bed03d8d | ||
|
|
581ab8f552 | ||
|
|
6154357daa | ||
|
|
3a1e462bef | ||
|
|
30af98200f | ||
|
|
ff27e62e1f | ||
|
|
7e93e75d8e | ||
|
|
ecd02134fa | ||
|
|
3c0eec0209 | ||
|
|
7b9e2cf91f | ||
|
|
312dec2baa | ||
|
|
be812b8d21 | ||
|
|
e63a5f72be | ||
|
|
1e2287e6e1 | ||
|
|
3508917f17 | ||
|
|
3ed22580fe | ||
|
|
f434706eec | ||
|
|
8a54ef1600 | ||
|
|
612a32d047 | ||
|
|
7be06aaae0 | ||
|
|
70aa2b66dd | ||
|
|
887b35821d | ||
|
|
faf9342695 | ||
|
|
0a371dca7d | ||
|
|
6c904a5593 | ||
|
|
75191e472b | ||
|
|
1e6d55bbce | ||
|
|
86ef6ff2cf | ||
|
|
6157624103 | ||
|
|
54c97daaf1 | ||
|
|
654fe2d30f | ||
|
|
0012f52940 | ||
|
|
46af5bdc5a | ||
|
|
a05ada89ec | ||
|
|
8afe604aff | ||
|
|
417d7ec6d5 | ||
|
|
2918001602 | ||
|
|
6a8053df2d | ||
|
|
e2a7802945 | ||
|
|
7f0a8a7ed7 | ||
|
|
39f690a751 | ||
|
|
d6ee6c6bbc | ||
|
|
4d3d15eda8 | ||
|
|
8bed4bc95a | ||
|
|
92072d0304 | ||
|
|
94005ca0e4 | ||
|
|
780cc434a7 | ||
|
|
20fabbc0ec | ||
|
|
39273e3aae | ||
|
|
9f571e5d1e | ||
|
|
dc3cfc325c | ||
|
|
ac11217195 | ||
|
|
103dd605d2 | ||
|
|
12b7316f71 | ||
|
|
b933e8ae00 | ||
|
|
a1cb752745 | ||
|
|
fb04271204 | ||
|
|
35fb33438f | ||
|
|
36c4363c54 | ||
|
|
831be14fd9 | ||
|
|
019e84f0d2 | ||
|
|
0dec657c61 | ||
|
|
694abe2004 | ||
|
|
b7c9e5775e | ||
|
|
2aa8dbc2cb | ||
|
|
8daf09b3ce | ||
|
|
a3044bcef9 | ||
|
|
3433c9583d | ||
|
|
a271744d42 | ||
|
|
09c5f5c3bf | ||
|
|
b8d3c52017 | ||
|
|
21dad9a17d | ||
|
|
e07391fe78 | ||
|
|
9515177e29 | ||
|
|
1a96f75954 | ||
|
|
88e25119f0 | ||
|
|
c5a59645d9 | ||
|
|
c0a5f57cdf | ||
|
|
8e97c7329a | ||
|
|
fe058d49b4 | ||
|
|
bd6e58a20e | ||
|
|
e8c826c816 | ||
|
|
d753191d20 | ||
|
|
d4e91b462e | ||
|
|
c01f8ae99c | ||
|
|
fb25d97077 | ||
|
|
d50e0ff48e | ||
|
|
d990f7f197 | ||
|
|
1e4ce19556 | ||
|
|
bc49eb6f83 | ||
|
|
90c2321bb8 | ||
|
|
901b3e34f6 | ||
|
|
908eb841bd | ||
|
|
128de625e2 | ||
|
|
ebd84a2338 | ||
|
|
fa2736277f | ||
|
|
196c309d1d | ||
|
|
d46d015fd5 | ||
|
|
999bf83f15 | ||
|
|
96470e0c8d | ||
|
|
cb7ee2358d | ||
|
|
b54b17708f | ||
|
|
cbbd6ebee2 | ||
|
|
de7194011d | ||
|
|
ae475793d5 | ||
|
|
f6105ece98 | ||
|
|
360698d79d | ||
|
|
b136b80c13 | ||
|
|
7e47c6303f | ||
|
|
689e803cc7 | ||
|
|
34ee231d62 | ||
|
|
4f4b4dd199 | ||
|
|
d87839230a | ||
|
|
e2cb811bf7 | ||
|
|
2bb0995ff8 | ||
|
|
793fe65a96 | ||
|
|
737b0f5488 | ||
|
|
ded848075d | ||
|
|
397c66cede | ||
|
|
2fb165cd54 | ||
|
|
c585d9b66c | ||
|
|
79ed703bb2 | ||
|
|
441c88dfb1 | ||
|
|
178bc916a8 | ||
|
|
19eead6955 | ||
|
|
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 | ||
|
|
747247153b | ||
|
|
d6c8dec451 | ||
|
|
30ecb32a6f | ||
|
|
a01b274352 | ||
|
|
193f5b39cb | ||
|
|
db8111ef7c | ||
|
|
913a4b794c | ||
|
|
accea17e9d | ||
|
|
c6fa00fbe3 | ||
|
|
bfb8f4f01a | ||
|
|
4b745a86b7 | ||
|
|
ddf894dcb0 | ||
|
|
30c430aec8 | ||
|
|
fdec3ce5c4 | ||
|
|
aa062ecdbe | ||
|
|
0e15f95543 | ||
|
|
eca887c66e | ||
|
|
f51976f63e | ||
|
|
1f2a36b316 | ||
|
|
8365f7dda3 | ||
|
|
391b8f91ce | ||
|
|
2f7064ace6 | ||
|
|
1ef234de9d | ||
|
|
a37cf74868 | ||
|
|
21192e9b3f | ||
|
|
2a2c9dc5dc | ||
|
|
6723815563 | ||
|
|
7e5591318f | ||
|
|
87ed778efe | ||
|
|
d0ff82801c | ||
|
|
f940290866 | ||
|
|
014060370a | ||
|
|
8c222b9e05 | ||
|
|
95f0c8bf51 | ||
|
|
a127711b86 | ||
|
|
715c531512 | ||
|
|
e6508a5bbc | ||
|
|
88d17e4c04 | ||
|
|
9ab8570153 | ||
|
|
8f2507a945 | ||
|
|
befffc573c | ||
|
|
945faac770 | ||
|
|
c8b1686ce4 | ||
|
|
ba92ccad14 | ||
|
|
012e453997 | ||
|
|
79b95c8cc6 | ||
|
|
34d0f40ee7 | ||
|
|
8421134420 | ||
|
|
a7470615be | ||
|
|
33b09d29e1 | ||
|
|
f135842071 | ||
|
|
a9bc525f22 | ||
|
|
5c9102bd9a | ||
|
|
c556f3471b | ||
|
|
2fb6124412 | ||
|
|
e482b56f58 |
19
.clang-tidy
19
.clang-tidy
@@ -9,37 +9,24 @@ Checks: "-*,
|
||||
|
||||
cppcoreguidelines-*,
|
||||
-cppcoreguidelines-avoid-c-arrays,
|
||||
-cppcoreguidelines-avoid-capturing-lambda-coroutines,
|
||||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-avoid-goto,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-avoid-non-const-global-variables,
|
||||
-cppcoreguidelines-avoid-reference-coroutine-parameters,
|
||||
-cppcoreguidelines-c-copy-assignment-signature,
|
||||
-cppcoreguidelines-explicit-virtual-functions,
|
||||
-cppcoreguidelines-interfaces-global-init,
|
||||
-cppcoreguidelines-macro-to-enum,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-missing-std-forward,
|
||||
-cppcoreguidelines-narrowing-conversions,
|
||||
-cppcoreguidelines-no-malloc,
|
||||
-cppcoreguidelines-noexcept-destructor,
|
||||
-cppcoreguidelines-noexcept-move-operations,
|
||||
-cppcoreguidelines-noexcept-swap,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
-cppcoreguidelines-prefer-member-initializer,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
|
||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-type-const-cast,
|
||||
-cppcoreguidelines-pro-type-cstyle-cast,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-union-access,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-cppcoreguidelines-slicing,
|
||||
-cppcoreguidelines-special-member-functions,
|
||||
|
||||
llvm-namespace-comment,
|
||||
@@ -63,13 +50,7 @@ Checks: "-*,
|
||||
performance-*,
|
||||
-performance-avoid-endl,
|
||||
-performance-enum-size,
|
||||
-performance-inefficient-algorithm,
|
||||
-performance-inefficient-string-concatenation,
|
||||
-performance-no-int-to-ptr,
|
||||
-performance-noexcept-destructor,
|
||||
-performance-noexcept-move-constructor,
|
||||
-performance-noexcept-swap,
|
||||
-performance-type-promotion-in-math-fn,
|
||||
-performance-unnecessary-copy-initialization,
|
||||
-performance-unnecessary-value-param,
|
||||
|
||||
|
||||
@@ -36,3 +36,8 @@ ignore:
|
||||
- "src/tests/"
|
||||
- "include/xrpl/beast/test/"
|
||||
- "include/xrpl/beast/unit_test/"
|
||||
# Telemetry modules — conditionally compiled behind XRPL_ENABLE_TELEMETRY,
|
||||
# which is not enabled in coverage builds.
|
||||
- "src/xrpld/telemetry/"
|
||||
- "src/libxrpl/beast/insight/OTelCollector.cpp"
|
||||
- "include/xrpl/beast/insight/OTelCollector.h"
|
||||
|
||||
@@ -65,6 +65,7 @@ words:
|
||||
- Btrfs
|
||||
- Buildx
|
||||
- canonicality
|
||||
- CGNAT
|
||||
- changespq
|
||||
- checkme
|
||||
- choco
|
||||
@@ -96,6 +97,8 @@ words:
|
||||
- dearmor
|
||||
- decryptor
|
||||
- dedented
|
||||
- Dedup
|
||||
- dedup
|
||||
- deleteme
|
||||
- demultiplexer
|
||||
- deserializaton
|
||||
@@ -111,8 +114,10 @@ words:
|
||||
- enablerepo
|
||||
- endmacro
|
||||
- envrc
|
||||
- EOCFG
|
||||
- exceptioned
|
||||
- EXPECT_STREQ
|
||||
- exfiltration
|
||||
- Falco
|
||||
- fcontext
|
||||
- finalizers
|
||||
@@ -121,6 +126,8 @@ words:
|
||||
- fsanitize
|
||||
- funclets
|
||||
- Gamal
|
||||
- gantt
|
||||
- Gantt
|
||||
- gcov
|
||||
- gcovr
|
||||
- ghead
|
||||
@@ -128,7 +135,10 @@ words:
|
||||
- godexsoft
|
||||
- gpgcheck
|
||||
- gpgkey
|
||||
- havetxset
|
||||
- hotwallet
|
||||
- hicpp
|
||||
- htpasswd
|
||||
- hwaddress
|
||||
- hwrap
|
||||
- ifndef
|
||||
@@ -160,6 +170,7 @@ words:
|
||||
- libxrpl
|
||||
- llection
|
||||
- LOCALGOOD
|
||||
- logql
|
||||
- logwstream
|
||||
- lseq
|
||||
- lsmf
|
||||
@@ -167,12 +178,11 @@ words:
|
||||
- mathbunnyru
|
||||
- mcmodel
|
||||
- MEMORYSTATUSEX
|
||||
- MPTAMM
|
||||
- MPTDEX
|
||||
- Merkle
|
||||
- Metafuncton
|
||||
- misprediction
|
||||
- missingok
|
||||
- MPTAMM
|
||||
- mptbalance
|
||||
- MPTDEX
|
||||
- mptflags
|
||||
@@ -201,11 +211,13 @@ words:
|
||||
- nixfmt
|
||||
- nixos
|
||||
- nixpkgs
|
||||
- NETOP
|
||||
- NOLINT
|
||||
- NOLINTNEXTLINE
|
||||
- nonxrp
|
||||
- noreplace
|
||||
- noripple
|
||||
- nostd
|
||||
- nostdinc
|
||||
- notifempty
|
||||
- nudb
|
||||
@@ -214,6 +226,7 @@ words:
|
||||
- Nyffenegger
|
||||
- onlatest
|
||||
- ostr
|
||||
- otelc
|
||||
- pargs
|
||||
- partitioner
|
||||
- paychan
|
||||
@@ -222,8 +235,12 @@ words:
|
||||
- permdex
|
||||
- perminute
|
||||
- permissioned
|
||||
- pgrep
|
||||
- pkill
|
||||
- pimpl
|
||||
- pointee
|
||||
- populator
|
||||
- pratik
|
||||
- preauth
|
||||
- preauthorization
|
||||
- preauthorize
|
||||
@@ -241,7 +258,9 @@ words:
|
||||
- queuable
|
||||
- Raphson
|
||||
- rcflags
|
||||
- reparent
|
||||
- replayer
|
||||
- reqps
|
||||
- rerandomize
|
||||
- rerandomization
|
||||
- rerandomized
|
||||
@@ -300,6 +319,7 @@ words:
|
||||
- takerpays
|
||||
- ters
|
||||
- TMEndpointv2
|
||||
- traceql
|
||||
- trixie
|
||||
- tx
|
||||
- txid
|
||||
@@ -307,6 +327,7 @@ words:
|
||||
- txjson
|
||||
- txn
|
||||
- txns
|
||||
- txqueue
|
||||
- txs
|
||||
- ubsan
|
||||
- UBSAN
|
||||
@@ -355,4 +376,5 @@ words:
|
||||
- xrplf
|
||||
- xxhash
|
||||
- xxhasher
|
||||
- CGNAT
|
||||
- xychart
|
||||
- zpages
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
Loop: xrpl.telemetry xrpld.rpc
|
||||
xrpld.rpc > xrpl.telemetry
|
||||
|
||||
Loop: xrpld.app xrpld.overlay
|
||||
xrpld.app > xrpld.overlay
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ 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.protocol
|
||||
libxrpl.telemetry > xrpl.telemetry
|
||||
libxrpl.tx > xrpl.basics
|
||||
libxrpl.tx > xrpl.conditions
|
||||
libxrpl.tx > xrpl.core
|
||||
@@ -52,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
|
||||
@@ -104,6 +109,7 @@ test.csf > xrpl.basics
|
||||
test.csf > xrpld.consensus
|
||||
test.csf > xrpl.json
|
||||
test.csf > xrpl.ledger
|
||||
test.csf > xrpl.telemetry
|
||||
test.json > test.jtx
|
||||
test.json > xrpl.json
|
||||
test.jtx > test.unit_test
|
||||
@@ -189,6 +195,7 @@ test.unit_test > xrpl.protocol
|
||||
tests.libxrpl > xrpl.basics
|
||||
tests.libxrpl > xrpl.config
|
||||
tests.libxrpl > xrpl.core
|
||||
tests.libxrpl > xrpld.telemetry
|
||||
tests.libxrpl > xrpl.json
|
||||
tests.libxrpl > xrpl.ledger
|
||||
tests.libxrpl > xrpl.net
|
||||
@@ -198,6 +205,7 @@ tests.libxrpl > xrpl.protocol_autogen
|
||||
tests.libxrpl > xrpl.resource
|
||||
tests.libxrpl > xrpl.server
|
||||
tests.libxrpl > xrpl.shamap
|
||||
tests.libxrpl > xrpl.telemetry
|
||||
tests.libxrpl > xrpl.tx
|
||||
xrpl.conditions > xrpl.basics
|
||||
xrpl.conditions > xrpl.protocol
|
||||
@@ -235,16 +243,19 @@ xrpl.server > xrpl.resource
|
||||
xrpl.shamap > xrpl.basics
|
||||
xrpl.shamap > xrpl.nodestore
|
||||
xrpl.shamap > xrpl.protocol
|
||||
xrpl.telemetry > xrpl.config
|
||||
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
|
||||
xrpld.app > xrpl.core
|
||||
xrpld.app > xrpld.consensus
|
||||
xrpld.app > xrpld.core
|
||||
xrpld.app > xrpld.telemetry
|
||||
xrpld.app > xrpl.json
|
||||
xrpld.app > xrpl.ledger
|
||||
xrpld.app > xrpl.net
|
||||
@@ -254,11 +265,13 @@ 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
|
||||
xrpld.consensus > xrpl.ledger
|
||||
xrpld.consensus > xrpl.protocol
|
||||
xrpld.consensus > xrpl.telemetry
|
||||
xrpld.core > xrpl.basics
|
||||
xrpld.core > xrpl.config
|
||||
xrpld.core > xrpl.core
|
||||
@@ -271,12 +284,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
|
||||
@@ -310,3 +325,7 @@ xrpld.shamap > xrpld.core
|
||||
xrpld.shamap > xrpl.nodestore
|
||||
xrpld.shamap > xrpl.protocol
|
||||
xrpld.shamap > xrpl.shamap
|
||||
xrpld.telemetry > xrpl.basics
|
||||
xrpld.telemetry > xrpld.consensus
|
||||
xrpld.telemetry > xrpl.protocol
|
||||
xrpld.telemetry > xrpl.telemetry
|
||||
|
||||
70
.github/scripts/otel-naming/README.md
vendored
Normal file
70
.github/scripts/otel-naming/README.md
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
# OTel naming-consistency check
|
||||
|
||||
`check_otel_naming.py` enforces the OpenTelemetry span-attribute naming
|
||||
convention documented in
|
||||
[CONTRIBUTING.md](../../../CONTRIBUTING.md#telemetry-span-attribute-naming)
|
||||
across every layer of the telemetry pipeline. The `*SpanNames.h` constants are
|
||||
the single source of truth (L1); every other layer must agree with them.
|
||||
|
||||
## Running locally
|
||||
|
||||
```
|
||||
python .github/scripts/otel-naming/check_otel_naming.py
|
||||
```
|
||||
|
||||
It takes no arguments, can be run from any directory inside the repo, and uses
|
||||
only the Python standard library (no `pip install`, matching the levelization
|
||||
check). A non-zero exit code means a violation was found; the output lists each
|
||||
violation as `RULE | location | token | expected`.
|
||||
|
||||
## What it checks
|
||||
|
||||
The valid key set is **derived dynamically from the OTel code** — there is no
|
||||
hardcoded allowlist:
|
||||
|
||||
- **L1 keys** come from the `namespace attr { ... }` blocks of every
|
||||
`*SpanNames.h`, resolving the `makeStr("x")` / `join(seg::a, seg::b)` DSL
|
||||
(cross-file, so `join(seg::rpc, ...)` resolves `seg::rpc` from the base
|
||||
`SpanNames.h`). Each constant is resolved against **its own** header, so two
|
||||
headers that define a same-named constant (e.g. a base `attr::ledgerHash` and
|
||||
a domain `attr::ledgerHash`) each contribute their real wire key — a later
|
||||
header cannot clobber an earlier one's value in a flat table.
|
||||
- **Legitimate dotted keys** = ONLY the keys the code actually sets as resource
|
||||
attributes, i.e. the entries inside `Telemetry.cpp`'s `Resource::Create({...})`
|
||||
call: the `semconv::service::*` keys (`service.*`) plus any `attr::<name>`
|
||||
constants passed there (`xrpl.network.*`). A dotted key that is _declared_ in a
|
||||
header but never set as a resource attr is a span attribute in resource
|
||||
clothing — a Rule-A violation, even if it lives in the base `SpanNames.h`.
|
||||
|
||||
### Rules (each fails the build, when its inputs are present)
|
||||
|
||||
| Rule | Check |
|
||||
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| A | No stray dotted span-attribute key (only the derived resource keys may be dotted). |
|
||||
| G | Attribute keys are `lower_snake_case` (`^[a-z][a-z0-9_]*$` per dot-segment) — no camelCase, UPPERCASE, or spaces. |
|
||||
| F | No string literals as attribute keys or span-name arguments in `setAttribute`/`addEvent`/`span`/`childSpan`. Attribute _values_ are exempt (runtime data); `*SpanNames.h` definitions and test files are exempt. |
|
||||
| B | Every collector `spanmetrics.dimensions` name exists in the L1 key set. |
|
||||
| C | Every Tempo span-filter tag exists in the L1 key set. |
|
||||
| D | Every dashboard label resolves to an L1 span attribute, a native-metric label (L6, emitted by MetricsRegistry), or a Prometheus/Grafana builtin. TraceQL scope prefixes (`span.`/`resource.`/…) are stripped before the L1 lookup. |
|
||||
| E | No dotted `xrpl.<domain>.<field>` attribute key in the runbook (only the L1 resource attrs `xrpl.network.*` may be dotted). Span names, filenames, OTel-standard keys, and metric labels are not flagged. |
|
||||
|
||||
Rule F runs **unconditionally** (it is a purely syntactic check on the
|
||||
call-sites and needs no `*SpanNames.h`), so a code path that calls
|
||||
`SpanGuard::span`/`setAttribute` directly without ever defining a header is
|
||||
still caught.
|
||||
|
||||
### Warnings (printed, never fail the build)
|
||||
|
||||
| Rule | Check |
|
||||
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| H | A namespace-qualified constant (e.g. `foo::bar::myKey`) used at a telemetry call-site is not defined in any `*SpanNames.h`. The constant should live in the proper header; defining it in-place bypasses rules A/G/F. Warns rather than fails — the argument may be a legitimately dynamic value, and the header may live on a later branch. Bare locals and `std::` names are not warned. |
|
||||
|
||||
## Presence-gated
|
||||
|
||||
Every rule runs **only when the source files it needs are present** in the tree
|
||||
and is otherwise skipped (printed as `SKIP: <rule> — <reason>`), never failed.
|
||||
This keeps the check correct no matter how telemetry work is split across PRs —
|
||||
a stacked chain, one large PR, or independent per-stage PRs where (for example)
|
||||
the collector config lands before the dashboards. The collector/Tempo/dashboard/
|
||||
runbook layers are introduced in later phases; on a branch without them, only
|
||||
the L1-intrinsic rules (A, G, F) run.
|
||||
886
.github/scripts/otel-naming/check_otel_naming.py
vendored
Normal file
886
.github/scripts/otel-naming/check_otel_naming.py
vendored
Normal file
@@ -0,0 +1,886 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Usage: check_otel_naming.py
|
||||
This script takes no parameters and can be called from any directory inside the
|
||||
repository (it locates the repo root via `git rev-parse`).
|
||||
|
||||
Enforces the OpenTelemetry span-attribute naming convention documented in
|
||||
CONTRIBUTING.md ("Telemetry span attribute naming") across every layer of the
|
||||
telemetry pipeline. The `*SpanNames.h` constants are the single source of truth
|
||||
(L1); every other layer must agree with them.
|
||||
|
||||
Design principles
|
||||
-----------------
|
||||
1. No hardcoded allowlist. The set of valid attribute keys — including which
|
||||
dotted keys are legitimate resource attributes — is derived dynamically by
|
||||
parsing the repository's own OTel code:
|
||||
* `*SpanNames.h` `namespace attr { ... }` blocks (the underscore/bare keys
|
||||
and the `join(seg::..., ...)` dotted resource compositions), and
|
||||
* the keys the code passes to `Resource::Create({ ... })` in Telemetry.cpp
|
||||
(the standard `semconv::service::*` keys -> service.name/version/...).
|
||||
|
||||
2. Presence-gated enforcement. Every rule runs ONLY when the source files it
|
||||
needs are present in the tree, and is otherwise skipped (never failed). This
|
||||
keeps the check correct no matter how work is split across PRs: a stacked
|
||||
chain, one large PR, or independent per-stage PRs where (for example) the
|
||||
collector config lands in a different PR than the dashboards. The check never
|
||||
assumes a file from another phase/PR exists.
|
||||
|
||||
Layers
|
||||
------
|
||||
L1 code : src/**/*SpanNames.h, include/**/*SpanNames.h (ground truth)
|
||||
L1 resource : src/libxrpl/telemetry/Telemetry.cpp (dotted allowlist)
|
||||
L1 callsites : setAttribute/addEvent/span/childSpan in src/**, include/**
|
||||
L2 collector : docker/telemetry/otel-collector-config.yaml (spanmetrics dims)
|
||||
L3 tempo : docker/telemetry/tempo.yaml (span filter tags)
|
||||
L4 dashboards: docker/telemetry/grafana/dashboards/*.json (PromQL labels)
|
||||
L5 runbook : docs/telemetry-runbook.md (attr tables)
|
||||
L6 metrics : MetricsRegistry.cpp instrument labels (native-metric
|
||||
label keys, a valid dashboard-label source besides L1)
|
||||
|
||||
Rules (each FAILS the build, when its inputs are present)
|
||||
---------------------------------------------------------
|
||||
A No stray dotted span-attribute key. A dotted `<a>.<b>` used as a span
|
||||
attribute that is not in the derived resource-key set is a violation.
|
||||
G Attribute keys must be lower_snake_case (^[a-z][a-z0-9_]*$ per segment).
|
||||
Flags camelCase, UPPERCASE, spaces, and other stray characters.
|
||||
F No string literals as attribute keys or span-name arguments. The
|
||||
setAttribute/addEvent key and the span/childSpan prefix/name args must
|
||||
reference a *SpanNames.h constant, never a "literal". Attribute VALUES are
|
||||
exempt (runtime data). Definitions inside *SpanNames.h are exempt, and
|
||||
test files are exempt (they pass arbitrary literals to exercise the API).
|
||||
B Every collector spanmetrics dimension exists in the L1 key set.
|
||||
C Every tempo span-filter tag exists in the L1 key set.
|
||||
D Every dashboard label resolves to an L1 span attribute, an L6
|
||||
native-metric label, or a builtin. TraceQL `span.`/`resource.` scope
|
||||
prefixes are stripped before the L1 lookup.
|
||||
E No dotted `xrpl.<domain>.<field>` attribute key in the runbook (only the
|
||||
L1 resource attrs xrpl.network.* may be dotted). Span names, filenames,
|
||||
OTel-standard keys, and metric labels are not flagged.
|
||||
|
||||
Warnings (printed, but do NOT fail the build)
|
||||
----------------------------------------------
|
||||
H A constant referenced at a telemetry call-site is not defined in any
|
||||
*SpanNames.h. Span constants should live in the corresponding
|
||||
*SpanNames.h (single source of truth); defining one in-place bypasses the
|
||||
naming rules. A warning (not a failure) because the argument may instead
|
||||
be a legitimately dynamic local (e.g. a computed span-name leaf).
|
||||
|
||||
Exit code is non-zero if any present-and-enforced rule finds a violation.
|
||||
Warnings never change the exit code.
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repo location
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
"""Return the repository root, so the script works from any CWD.
|
||||
|
||||
Exits with a readable message (not a traceback) if git is unavailable or the
|
||||
CWD is outside a repository."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print(
|
||||
"error: check_otel_naming.py must be run inside the git repository.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
return Path(out.stdout.strip())
|
||||
|
||||
|
||||
def read_source(path: Path) -> str:
|
||||
"""Read a file as UTF-8, tolerating stray non-UTF-8 bytes rather than
|
||||
crashing the whole check on one bad byte."""
|
||||
return path.read_text(encoding="utf-8", errors="ignore")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regexes (compiled once)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# A segment/string constant definition: `inline constexpr auto NAME = <expr>;`
|
||||
CONST_DEF = re.compile(r"inline\s+constexpr\s+auto\s+(\w+)\s*=\s*(.+?);", re.DOTALL)
|
||||
MAKESTR = re.compile(r'makeStr\(\s*"([^"]*)"\s*\)')
|
||||
# A `namespace <name> {` opener, to track which namespace a constant lives in.
|
||||
NS_OPEN = re.compile(r"namespace\s+([\w:]+)\s*\{")
|
||||
# A `using ::a::b::field;` re-export inside an attr block; captures the leaf.
|
||||
USING_DECL = re.compile(r"using\s+(?:::)?[\w:]*::(\w+)\s*;")
|
||||
# Telemetry call-sites whose string arguments must be constants, not literals.
|
||||
# Require a receiver so we match real SpanGuard calls, not std::span / a math
|
||||
# `span(...)` / a bare method declaration:
|
||||
# - `SpanGuard::span(` / `SpanGuard::childSpan(` (static factory)
|
||||
# - `<obj>.span(` / `<obj>->setAttribute(` etc. (member call)
|
||||
# `span`/`childSpan` additionally require the `SpanGuard`/`.`/`->` receiver;
|
||||
# `setAttribute`/`addEvent` only ever exist on a guard, so a `.`/`->` suffices.
|
||||
CALLSITE = re.compile(
|
||||
r"(?:SpanGuard::|\.|->)\s*(setAttribute|addEvent|span|childSpan)\s*\("
|
||||
)
|
||||
# A C++ string literal (used to flag literals inside call-site argument lists).
|
||||
STRING_LITERAL = re.compile(r'"((?:[^"\\]|\\.)*)"')
|
||||
# A C++ line comment (`//` ... end of line) and a block comment (`/* ... */`).
|
||||
LINE_COMMENT = re.compile(r"//[^\n]*")
|
||||
BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
||||
# A TraceQL scope prefix on a label (`span.`, `resource.`, `event.`, etc.).
|
||||
# Dashboards reference span attributes in TraceQL as `span.<attr>`; the bare
|
||||
# attribute is what must exist in L1, so strip the scope before validating.
|
||||
TRACEQL_SCOPE = re.compile(r"^(?:span|resource|event|link|instrumentation_scope)\.")
|
||||
# An OTel metric label key as emitted in C++: `Add(.., {{"label", ...}})` /
|
||||
# `{{"label", value}}` instrument calls in MetricsRegistry.
|
||||
METRIC_LABEL = re.compile(r'\{\{\s*"([a-z_][a-z0-9_]*)"\s*,')
|
||||
|
||||
|
||||
def strip_comments(text: str) -> str:
|
||||
"""Remove C/C++ `//` line comments and `/* ... */` block comments.
|
||||
|
||||
Used only for L1 attribute-key extraction so that a commented-out or
|
||||
illustrative `makeStr("...")` inside a `namespace attr` block does not leak
|
||||
into the authoritative key set. Rule F deliberately does NOT strip comments
|
||||
— it must still see `@code` doc-comment examples so their call-site
|
||||
arguments are held to the constant-only convention.
|
||||
|
||||
String literals are not specially handled; a `//` or `/*` appearing inside a
|
||||
string is vanishingly rare in the *SpanNames.h headers and would at worst
|
||||
drop a constant from L1 (a conservative direction).
|
||||
"""
|
||||
text = BLOCK_COMMENT.sub("", text)
|
||||
text = LINE_COMMENT.sub("", text)
|
||||
return text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# L1: parse *SpanNames.h into the authoritative key set
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def find_spanname_headers(root: Path) -> List[Path]:
|
||||
return sorted(
|
||||
p
|
||||
for p in list((root / "src").rglob("*SpanNames.h"))
|
||||
+ list((root / "include").rglob("*SpanNames.h"))
|
||||
if p.is_file()
|
||||
)
|
||||
|
||||
|
||||
def resolve_constants(
|
||||
text: str, symbols: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, str]:
|
||||
"""Resolve `inline constexpr auto NAME = <makeStr/join expr>` to strings.
|
||||
|
||||
Supports the small constexpr DSL used by SpanNames.h:
|
||||
makeStr("x") -> "x"
|
||||
join(a, b) -> resolve(a) + "." + resolve(b)
|
||||
seg::xrpl / attr::foo -> looked up in the symbol table
|
||||
The optional `symbols` argument seeds (and is updated in place with) the
|
||||
table, so a global pass over ALL *SpanNames.h headers can resolve
|
||||
cross-file references such as `join(seg::rpc, ...)` where `seg::rpc` is
|
||||
defined in the base SpanNames.h. Keys are stored by their bare name
|
||||
(last `::` component), so `seg::rpc` and `rpc` both resolve.
|
||||
"""
|
||||
if symbols is None:
|
||||
symbols = {}
|
||||
|
||||
def resolve_expr(expr: str) -> Optional[str]:
|
||||
expr = expr.strip()
|
||||
m = MAKESTR.fullmatch(expr)
|
||||
if m:
|
||||
return m.group(1)
|
||||
if expr.startswith("join(") and expr.endswith(")"):
|
||||
args = split_top_level_args(expr[len("join(") : -1])
|
||||
parts = [resolve_expr(a) for a in args]
|
||||
if any(p is None for p in parts):
|
||||
return None
|
||||
return ".".join(p for p in parts if p is not None)
|
||||
# Bare or qualified symbol reference, e.g. `seg::xrpl` or `networkId`.
|
||||
key = expr.split("::")[-1]
|
||||
return symbols.get(key, symbols.get(expr))
|
||||
|
||||
# Iterate definitions in source order so earlier symbols are available.
|
||||
for m in CONST_DEF.finditer(text):
|
||||
name, expr = m.group(1), m.group(2)
|
||||
val = resolve_expr(expr)
|
||||
if val is not None:
|
||||
symbols[name] = val
|
||||
return symbols
|
||||
|
||||
|
||||
def build_global_symbols(headers: List[Path]) -> Dict[str, str]:
|
||||
"""Resolve constants across ALL headers so cross-file `seg::`/`join`
|
||||
references (e.g. `join(seg::rpc, ...)` in RpcSpanNames.h, where `seg::rpc`
|
||||
lives in the base SpanNames.h) resolve. Base SpanNames.h is processed
|
||||
first so its `seg::` segments seed the table."""
|
||||
symbols: Dict[str, str] = {}
|
||||
ordered = sorted(headers, key=lambda p: (p.name != "SpanNames.h", str(p)))
|
||||
# Two passes: the first seeds segments, the second resolves dependents.
|
||||
# Comments are stripped so a commented-out constant cannot seed the table.
|
||||
for _ in range(2):
|
||||
for h in ordered:
|
||||
resolve_constants(strip_comments(read_source(h)), symbols)
|
||||
return symbols
|
||||
|
||||
|
||||
def split_top_level_args(s: str) -> List[str]:
|
||||
"""Split a comma-separated arg list, respecting nested parentheses and
|
||||
ignoring parens/commas that appear inside a "string literal" (so a value
|
||||
like `setAttribute(k, ",")` does not get mis-split)."""
|
||||
args, depth, cur = [], 0, ""
|
||||
in_str = False
|
||||
escaped = False
|
||||
for ch in s:
|
||||
if in_str:
|
||||
cur += ch
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif ch == "\\":
|
||||
escaped = True
|
||||
elif ch == '"':
|
||||
in_str = False
|
||||
continue
|
||||
if ch == '"':
|
||||
in_str = True
|
||||
cur += ch
|
||||
elif ch == "(":
|
||||
depth += 1
|
||||
cur += ch
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
cur += ch
|
||||
elif ch == "," and depth == 0:
|
||||
args.append(cur)
|
||||
cur = ""
|
||||
else:
|
||||
cur += ch
|
||||
if cur.strip():
|
||||
args.append(cur)
|
||||
return args
|
||||
|
||||
|
||||
def attr_namespace_spans(text: str) -> List[str]:
|
||||
"""Return the source text of each `namespace attr { ... }` block in `text`.
|
||||
|
||||
Brace-matched over the whole (comment-stripped) text, so a definition that
|
||||
wraps across several physical lines is contained in one span. Nested braces
|
||||
inside the block are balanced correctly."""
|
||||
spans: List[str] = []
|
||||
for opener in NS_OPEN.finditer(text):
|
||||
if opener.group(1).split("::")[-1] != "attr":
|
||||
continue
|
||||
# Walk from the opening brace, balancing nesting to the matching close.
|
||||
i = opener.end() # one char past the namespace's `{`
|
||||
depth = 1
|
||||
start = i
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
spans.append(text[start : i - 1])
|
||||
return spans
|
||||
|
||||
|
||||
def attr_keys_from_header(path: Path, symbols: Dict[str, str]) -> Set[str]:
|
||||
"""Return the set of attribute-key strings declared in a header's
|
||||
`namespace attr { ... }` block(s). `symbols` is the global cross-file
|
||||
table, used ONLY to seed `seg::`/segment references for `join(...)`
|
||||
resolution — never to look up an attr constant's value.
|
||||
|
||||
A constant DEFINED in this header is resolved against this header's OWN
|
||||
text, so two headers that each define a same-named constant (e.g. the base
|
||||
`attr::ledgerHash = xrpl.ledger.hash` and consensus
|
||||
`attr::ledgerHash = ledger_hash`) each report their real wire key. The
|
||||
global table is keyed by bare name and would otherwise let a later header
|
||||
clobber an earlier one, erasing the real key from L1 (a Rule-A blind spot).
|
||||
A `using`-re-export, by contrast, imports a constant defined elsewhere, so
|
||||
it is resolved against the global table.
|
||||
|
||||
Comments are stripped first (a commented constant must not enter L1), and
|
||||
each attr block is brace-matched over the whole text so multi-line
|
||||
`inline constexpr auto NAME = join(\\n ...);` definitions are captured."""
|
||||
text = strip_comments(read_source(path))
|
||||
# Local table: the global segments/symbols seed cross-file `join` parts,
|
||||
# then this header's own definitions overwrite any same-named global entry
|
||||
# so a locally-defined attr resolves to ITS value, not another header's.
|
||||
local = dict(symbols)
|
||||
resolve_constants(text, local)
|
||||
keys: Set[str] = set()
|
||||
for block in attr_namespace_spans(text):
|
||||
for md in CONST_DEF.finditer(block):
|
||||
# Resolve a locally-defined constant against the LOCAL table; this
|
||||
# captures makeStr("x") and join(seg::y, ...) with the header's own
|
||||
# value, immune to cross-header bare-name collisions.
|
||||
val = local.get(md.group(1))
|
||||
if val is not None:
|
||||
keys.add(val)
|
||||
# `using ::ns::attr::field;` re-exports a constant defined in ANOTHER
|
||||
# header (e.g. PeerSpanNames imports the base ledgerHash). Resolve the
|
||||
# imported name against the global table.
|
||||
for um in USING_DECL.finditer(block):
|
||||
val = symbols.get(um.group(1))
|
||||
if val is not None:
|
||||
keys.add(val)
|
||||
return keys
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reporting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Report:
|
||||
def __init__(self) -> None:
|
||||
self.violations: List[Tuple[str, str, str, str]] = []
|
||||
self.warnings: List[Tuple[str, str, str, str]] = []
|
||||
self.skips: List[str] = []
|
||||
self.checked: List[str] = []
|
||||
|
||||
def violation(self, rule: str, loc: str, token: str, expected: str) -> None:
|
||||
self.violations.append((rule, loc, token, expected))
|
||||
|
||||
def warning(self, rule: str, loc: str, token: str, note: str) -> None:
|
||||
"""A non-fatal finding: printed, but does not fail the build. Used where
|
||||
the script cannot be certain a finding is wrong (e.g. a constant used at
|
||||
a call-site that is not defined in any *SpanNames.h — it might be a
|
||||
misplaced constant, or a legitimately dynamic value)."""
|
||||
self.warnings.append((rule, loc, token, note))
|
||||
|
||||
def skip(self, rule: str, reason: str) -> None:
|
||||
self.skips.append(f"SKIP: {rule} — {reason}")
|
||||
|
||||
def ok(self, msg: str) -> None:
|
||||
self.checked.append(f"OK: {msg}")
|
||||
|
||||
def render_and_exit(self) -> None:
|
||||
for line in self.skips:
|
||||
print(line)
|
||||
for line in self.checked:
|
||||
print(line)
|
||||
if self.warnings:
|
||||
print("\nNaming-convention warnings (non-fatal):\n")
|
||||
print(f" {'RULE':<5} {'LOCATION':<48} {'TOKEN':<28} NOTE")
|
||||
print(f" {'-' * 5} {'-' * 48} {'-' * 28} {'-' * 30}")
|
||||
for rule, loc, token, note in self.warnings:
|
||||
print(f" {rule:<5} {loc:<48} {token:<28} {note}")
|
||||
if self.violations:
|
||||
print("\nNaming-convention violations:\n")
|
||||
print(f" {'RULE':<5} {'LOCATION':<48} {'TOKEN':<28} EXPECTED")
|
||||
print(f" {'-' * 5} {'-' * 48} {'-' * 28} {'-' * 30}")
|
||||
for rule, loc, token, expected in self.violations:
|
||||
print(f" {rule:<5} {loc:<48} {token:<28} {expected}")
|
||||
print(
|
||||
"\nSee CONTRIBUTING.md -> 'Telemetry span attribute naming'. "
|
||||
"The *SpanNames.h constants are the single source of truth."
|
||||
)
|
||||
sys.exit(1)
|
||||
print("\nAll present telemetry naming layers are consistent.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = repo_root()
|
||||
report = Report()
|
||||
|
||||
# --- Build the L1 ground-truth key set (presence-gated) ----------------
|
||||
headers = find_spanname_headers(root)
|
||||
l1_keys: Set[str] = set()
|
||||
if headers:
|
||||
symbols = build_global_symbols(headers)
|
||||
# Map each key to the header(s) that declare it, so Rule A can tell a
|
||||
# legitimate resource attr (declared in the base SpanNames.h) from a
|
||||
# stray dotted key declared in a domain header.
|
||||
keys_by_header: Dict[Path, Set[str]] = {}
|
||||
for h in headers:
|
||||
hk = attr_keys_from_header(h, symbols)
|
||||
keys_by_header[h] = hk
|
||||
l1_keys |= hk
|
||||
report.ok(
|
||||
f"L1: {len(l1_keys)} attribute keys from {len(headers)} "
|
||||
f"*SpanNames.h header(s)"
|
||||
)
|
||||
else:
|
||||
report.skip("L1", "no *SpanNames.h present (not a naming-relevant tree)")
|
||||
keys_by_header = {}
|
||||
|
||||
# --- Derive the legitimate dotted (resource) keys dynamically ----------
|
||||
# ONLY the keys actually passed to Resource::Create() in Telemetry.cpp
|
||||
# (semconv service.* + the attr:: constants set there, e.g. xrpl.network.*).
|
||||
# A dotted key declared in a header but NOT set as a resource attr is a
|
||||
# Rule-A violation, not an allowlist entry.
|
||||
resource_symbols = symbols if headers else {}
|
||||
dotted_allow = derive_dotted_resource_keys(root, resource_symbols, report)
|
||||
|
||||
# --- Rule A: no stray dotted span-attribute keys -----------------------
|
||||
if l1_keys:
|
||||
run_rule_a(keys_by_header, dotted_allow, report)
|
||||
# --- Rule G: keys must be lower_snake_case -----------------------------
|
||||
if l1_keys:
|
||||
run_rule_g(keys_by_header, report)
|
||||
# --- Rule F (+ Rule H): scan telemetry call-sites ----------------------
|
||||
# Runs UNCONDITIONALLY: Rule F is a purely syntactic check (is this argument
|
||||
# a literal?) and does not need the L1 key set, so a code path that uses
|
||||
# SpanGuard::span/setAttribute directly without ever defining a *SpanNames.h
|
||||
# is still caught. Rule H (warning) additionally flags constant references
|
||||
# not defined in any *SpanNames.h.
|
||||
header_symbols = spanname_symbol_names(headers)
|
||||
run_rule_f(root, report, header_symbols)
|
||||
|
||||
# --- Cross-layer rules B/C/D/E (each presence-gated) -------------------
|
||||
# L6 native-metric labels: span attributes are not the only valid dashboard
|
||||
# labels — the MetricsRegistry emits OTel metrics whose label keys are an
|
||||
# additional source of truth. Derive them dynamically (same principle as L1)
|
||||
# so dashboards may reference them without tripping Rule D.
|
||||
metric_labels = metric_label_names(root)
|
||||
run_rule_b_collector(root, l1_keys, report)
|
||||
run_rule_c_tempo(root, l1_keys, report)
|
||||
run_rule_d_dashboards(root, l1_keys, metric_labels, report)
|
||||
run_rule_e_runbook(root, l1_keys, report)
|
||||
|
||||
report.render_and_exit()
|
||||
|
||||
|
||||
def resource_create_block(text: str) -> str:
|
||||
"""Return the text inside the first `Resource::Create({ ... })` argument
|
||||
list, brace-matched so nested `{key, value}` initializers are contained.
|
||||
Empty string if the call is absent."""
|
||||
m = re.search(r"Resource::Create\(\s*\{", text)
|
||||
if not m:
|
||||
return ""
|
||||
i = m.end() # one char past the opening `{`
|
||||
depth, start = 1, i
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
return text[start : i - 1]
|
||||
|
||||
|
||||
def derive_dotted_resource_keys(
|
||||
root: Path, symbols: Dict[str, str], report: Report
|
||||
) -> Set[str]:
|
||||
"""Legitimate dotted keys = ONLY the keys the code actually sets as RESOURCE
|
||||
attributes, i.e. the entries inside Telemetry.cpp's `Resource::Create({...})`
|
||||
call: the standard semconv keys (`service.*`) plus any `attr::<name>`
|
||||
constants passed there (resolved to their wire key via the global symbol
|
||||
table, e.g. `attr::networkId` -> `xrpl.network.id`).
|
||||
|
||||
A dotted key DECLARED in a `*SpanNames.h` header but NOT passed to
|
||||
Resource::Create() is a span attribute wearing the resource form — a Rule-A
|
||||
violation, never allowlisted. Deriving the allowlist from the actual
|
||||
resource call (not from "any dotted key in the base header") is what lets
|
||||
Rule A catch a stray dotted span attr such as `xrpl.ledger.hash`."""
|
||||
allow: Set[str] = set()
|
||||
tele = root / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp"
|
||||
if not tele.is_file():
|
||||
report.skip("resource-derive", "Telemetry.cpp not present")
|
||||
return allow
|
||||
block = resource_create_block(read_source(tele))
|
||||
# semconv::<group>::k<CamelKey> -> the dotted OTel-standard key. The
|
||||
# CamelKey already embeds the group, e.g. service::kServiceInstanceId
|
||||
# -> service.instance.id. Split the CamelCase name into dotted lowercase
|
||||
# segments; if it does not lead with the group, prepend the group.
|
||||
for m in re.finditer(r"semconv::(\w+)::k(\w+)", block):
|
||||
group, camel = m.group(1), m.group(2)
|
||||
segments = camel_to_dotsegments(camel)
|
||||
if segments and segments[0] == group:
|
||||
allow.add(".".join(segments))
|
||||
else:
|
||||
allow.add(group + "." + ".".join(segments))
|
||||
# attr::<name> constants set as resource attrs (e.g. networkId/networkType);
|
||||
# resolve each to its wire key and allowlist only the dotted ones.
|
||||
for m in re.finditer(r"attr::(\w+)", block):
|
||||
val = symbols.get(m.group(1))
|
||||
if val is not None and "." in val:
|
||||
allow.add(val)
|
||||
report.ok(f"resource dotted-key allowlist derived: {sorted(allow)}")
|
||||
return allow
|
||||
|
||||
|
||||
def camel_to_dotsegments(s: str) -> List[str]:
|
||||
"""Split a CamelCase identifier into lowercase dot-segment parts, e.g.
|
||||
`ServiceInstanceId` -> ['service', 'instance', 'id']."""
|
||||
return [w.lower() for w in re.findall(r"[A-Z][a-z0-9]*", s)]
|
||||
|
||||
|
||||
def run_rule_a(
|
||||
keys_by_header: Dict[Path, Set[str]], dotted_allow: Set[str], report: Report
|
||||
) -> None:
|
||||
"""Any dotted attribute key that is not an allowed resource key is a
|
||||
violation, reported against the header that declares it."""
|
||||
found = False
|
||||
for h in sorted(keys_by_header):
|
||||
for key in sorted(keys_by_header[h]):
|
||||
if "." in key and key not in dotted_allow:
|
||||
found = True
|
||||
report.violation("A", h.name, key, "underscore form, not dotted")
|
||||
if not found:
|
||||
report.ok("A: no stray dotted span-attribute keys")
|
||||
|
||||
|
||||
# A lower_snake_case identifier segment: starts lowercase, then lowercase /
|
||||
# digits / underscores. No uppercase, no spaces, no camelCase.
|
||||
SNAKE_SEGMENT = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||
|
||||
|
||||
def run_rule_g(keys_by_header: Dict[Path, Set[str]], report: Report) -> None:
|
||||
"""Every attribute key must be lower_snake_case. Bare/underscore keys must
|
||||
match ^[a-z][a-z0-9_]*$; dotted resource keys must be lowercase
|
||||
dot-separated segments (each segment lower_snake_case). Flags camelCase,
|
||||
UPPERCASE, spaces, and other stray characters."""
|
||||
found = False
|
||||
for h in sorted(keys_by_header):
|
||||
for key in sorted(keys_by_header[h]):
|
||||
segments = key.split(".")
|
||||
if all(SNAKE_SEGMENT.match(seg) for seg in segments):
|
||||
continue
|
||||
found = True
|
||||
report.violation("G", h.name, key, "must be lower_snake_case")
|
||||
if not found:
|
||||
report.ok("G: all attribute keys are lower_snake_case")
|
||||
|
||||
|
||||
# Which argument positions of each call must be a constant (0-based). The
|
||||
# attribute VALUE position is intentionally absent: values are runtime data
|
||||
# (command names, hashes, counts), not naming-convention surface.
|
||||
# setAttribute(key, value) -> check arg 0 (key); value (arg 1) exempt
|
||||
# addEvent(name[, attrs]) -> check arg 0 (event name)
|
||||
# span(category, prefix, name) -> check args 1,2 (prefix + span-name leaf)
|
||||
# childSpan(name[, parentCtx]) -> check arg 0 (span-name leaf)
|
||||
CONSTANT_ARG_POSITIONS: Dict[str, Set[int]] = {
|
||||
"setAttribute": {0},
|
||||
"addEvent": {0},
|
||||
"span": {1, 2},
|
||||
"childSpan": {0},
|
||||
}
|
||||
|
||||
|
||||
def is_test_path(path: Path) -> bool:
|
||||
"""True if the path is test code. Tests legitimately pass arbitrary literal
|
||||
keys/names to exercise the API mechanics, so Rule F does not apply to them.
|
||||
Matches a `test`/`tests` directory anywhere in the path (e.g. src/test/,
|
||||
src/tests/, .../detail/tests/)."""
|
||||
return any(part in ("test", "tests") for part in path.parts)
|
||||
|
||||
|
||||
# A constant reference passed at a call-site, e.g. `rpc_span::attr::command`
|
||||
# or a bare `myKey`. We capture the leaf identifier (after the last `::`).
|
||||
IDENTIFIER_ARG = re.compile(r"^[\s&*]*([A-Za-z_][\w:]*)\s*$")
|
||||
|
||||
|
||||
def spanname_symbol_names(headers: List[Path]) -> Set[str]:
|
||||
"""Every `inline constexpr auto NAME = ...;` symbol defined across the
|
||||
*SpanNames.h headers, by bare name. Used by Rule H to tell whether a
|
||||
constant referenced at a call-site actually lives in a SpanNames header."""
|
||||
names: Set[str] = set()
|
||||
for h in headers:
|
||||
for m in CONST_DEF.finditer(strip_comments(read_source(h))):
|
||||
names.add(m.group(1))
|
||||
return names
|
||||
|
||||
|
||||
def run_rule_f(root: Path, report: Report, header_symbols: Set[str]) -> None:
|
||||
"""Walk every telemetry call-site (non-test, non-*SpanNames.h) and check the
|
||||
constant-only argument positions of setAttribute/addEvent/span/childSpan:
|
||||
|
||||
Rule F (FAIL): a string literal in a key / span-name position. Attribute
|
||||
VALUES are exempt (runtime data).
|
||||
Rule H (WARN): a constant reference whose name is not defined in any
|
||||
*SpanNames.h. The constant should live in the corresponding
|
||||
*SpanNames.h (single source of truth); defining it in-place bypasses
|
||||
the naming rules. Warn rather than fail — the argument may instead be a
|
||||
legitimately dynamic local (e.g. a computed span-name leaf)."""
|
||||
found_f = False
|
||||
sources = [
|
||||
p
|
||||
for base in ("src", "include")
|
||||
for ext in ("*.h", "*.cpp")
|
||||
for p in (root / base).rglob(ext)
|
||||
if p.is_file()
|
||||
]
|
||||
for path in sorted(sources):
|
||||
if path.name.endswith("SpanNames.h") or is_test_path(path):
|
||||
continue
|
||||
text = read_source(path)
|
||||
rel = path.relative_to(root)
|
||||
for call, arglist, lineno in iter_calls(text):
|
||||
positions = CONSTANT_ARG_POSITIONS.get(call, set())
|
||||
args = split_top_level_args(arglist)
|
||||
for idx in positions:
|
||||
if idx >= len(args):
|
||||
continue
|
||||
arg = args[idx]
|
||||
lit = STRING_LITERAL.search(arg)
|
||||
if lit:
|
||||
found_f = True
|
||||
report.violation(
|
||||
"F",
|
||||
f"{rel}:{lineno}",
|
||||
f'{call} arg{idx} "{lit.group(1)}"',
|
||||
"use a *SpanNames.h constant",
|
||||
)
|
||||
continue
|
||||
# Not a literal: Rule H warns when a NAMESPACE-QUALIFIED constant
|
||||
# reference (e.g. `consensus::span::accept`) is not defined in
|
||||
# any *SpanNames.h — i.e. the constant was defined in-place
|
||||
# instead of in the proper header. We only consider qualified
|
||||
# refs (containing `::`): a bare lowercase identifier is almost
|
||||
# always a legitimately dynamic local (a computed span-name leaf
|
||||
# or attribute value), not a misplaced constant, so warning on it
|
||||
# would be noise. Standard-library types (std::...) are skipped.
|
||||
ident = IDENTIFIER_ARG.match(arg)
|
||||
if not (ident and header_symbols):
|
||||
continue
|
||||
ref = ident.group(1)
|
||||
if "::" not in ref or ref.startswith("std::"):
|
||||
continue
|
||||
leaf = ref.split("::")[-1]
|
||||
if leaf not in header_symbols:
|
||||
report.warning(
|
||||
"H",
|
||||
f"{rel}:{lineno}",
|
||||
f"{call} arg{idx} {ref}",
|
||||
"not defined in any *SpanNames.h",
|
||||
)
|
||||
if not found_f:
|
||||
report.ok("F: no string-literal keys/names at telemetry call-sites")
|
||||
|
||||
|
||||
def iter_calls(text: str):
|
||||
"""Yield (call_name, raw_arglist, lineno) for each setAttribute/addEvent/
|
||||
span/childSpan invocation, spanning multiple physical lines if needed."""
|
||||
for m in CALLSITE.finditer(text):
|
||||
name = m.group(1)
|
||||
# Walk from the opening paren, balancing nesting to find the close.
|
||||
# Parens inside a "string literal" are ignored so a value such as
|
||||
# `setAttribute(k, ")")` does not close the call early.
|
||||
i = m.end() # one char past the '('
|
||||
depth = 1
|
||||
in_str = False
|
||||
escaped = False
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if in_str:
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif c == "\\":
|
||||
escaped = True
|
||||
elif c == '"':
|
||||
in_str = False
|
||||
elif c == '"':
|
||||
in_str = True
|
||||
elif c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
i += 1
|
||||
arglist = text[m.end() : i - 1]
|
||||
lineno = text.count("\n", 0, m.start()) + 1
|
||||
yield name, arglist, lineno
|
||||
|
||||
|
||||
def run_rule_b_collector(root: Path, l1_keys: Set[str], report: Report) -> None:
|
||||
path = root / "docker" / "telemetry" / "otel-collector-config.yaml"
|
||||
if not path.is_file():
|
||||
report.skip("B", "collector config not present")
|
||||
return
|
||||
text = read_source(path)
|
||||
if "spanmetrics" not in text:
|
||||
report.skip("B", "no spanmetrics block in collector config")
|
||||
return
|
||||
dims = extract_spanmetrics_dimensions(text)
|
||||
if not l1_keys:
|
||||
report.skip("B", "no L1 key set to validate against")
|
||||
return
|
||||
miss = [d for d in dims if d not in l1_keys]
|
||||
for d in miss:
|
||||
report.violation("B", str(path.relative_to(root)), d, "must exist in L1")
|
||||
if not miss:
|
||||
report.ok(f"B: {len(dims)} collector dimension(s) all in L1")
|
||||
|
||||
|
||||
def extract_spanmetrics_dimensions(text: str) -> List[str]:
|
||||
dims: List[str] = []
|
||||
in_dims = False
|
||||
for line in text.splitlines():
|
||||
if re.search(r"\bdimensions\s*:", line):
|
||||
in_dims = True
|
||||
continue
|
||||
if in_dims:
|
||||
m = re.search(r"-\s*name\s*:\s*([A-Za-z0-9_.]+)", line)
|
||||
if m:
|
||||
dims.append(m.group(1))
|
||||
elif line.strip() and not line.lstrip().startswith("-") and ":" in line:
|
||||
in_dims = False
|
||||
return dims
|
||||
|
||||
|
||||
def run_rule_c_tempo(root: Path, l1_keys: Set[str], report: Report) -> None:
|
||||
# The trace-search filter tags live in the Grafana Tempo DATASOURCE
|
||||
# provisioning file (search.filters[].{tag,scope}); the Tempo server
|
||||
# tempo.yaml has no such tags. Prefer the datasource file; fall back to the
|
||||
# server file so the rule still does something if the layout changes.
|
||||
candidates = [
|
||||
root / "docker/telemetry/grafana/provisioning/datasources/tempo.yaml",
|
||||
root / "docker/telemetry/tempo.yaml",
|
||||
]
|
||||
path = next((p for p in candidates if p.is_file()), None)
|
||||
if path is None:
|
||||
report.skip("C", "tempo datasource provisioning not present")
|
||||
return
|
||||
if not l1_keys:
|
||||
report.skip("C", "no L1 key set to validate against")
|
||||
return
|
||||
# Pair each filter's `tag:` with its `scope:` (a few lines below it) and
|
||||
# validate only span-scope tags — resource/intrinsic tags (service.*, name,
|
||||
# status, duration) are not span attributes. Strip a TraceQL span. prefix.
|
||||
lines = read_source(path).splitlines()
|
||||
span_tags: List[str] = []
|
||||
for i, line in enumerate(lines):
|
||||
m = re.search(r"^\s*tag:\s*(\S+)", line)
|
||||
if not m:
|
||||
continue
|
||||
scope = next(
|
||||
(
|
||||
sm.group(1)
|
||||
for j in range(i, min(i + 4, len(lines)))
|
||||
for sm in [re.search(r"scope:\s*(\S+)", lines[j])]
|
||||
if sm
|
||||
),
|
||||
"",
|
||||
)
|
||||
if scope == "span":
|
||||
span_tags.append(TRACEQL_SCOPE.sub("", m.group(1)))
|
||||
if not span_tags:
|
||||
report.skip("C", "no span-scope filter tags in tempo datasource")
|
||||
return
|
||||
miss = [t for t in span_tags if t not in l1_keys]
|
||||
for t in sorted(set(miss)):
|
||||
report.violation("C", str(path.relative_to(root)), t, "must exist in L1")
|
||||
if not miss:
|
||||
report.ok(f"C: {len(span_tags)} tempo span-filter tag(s) all in L1")
|
||||
|
||||
|
||||
def metric_label_names(root: Path) -> Set[str]:
|
||||
"""L6: OTel native-metric label keys emitted by the telemetry code, e.g.
|
||||
`counter->Add(1, {{"job_type", value}})` in MetricsRegistry.cpp. These are
|
||||
a valid source of dashboard labels distinct from span attributes (L1)."""
|
||||
labels: Set[str] = set()
|
||||
for base in ("src", "include"):
|
||||
for p in (root / base).rglob("*.cpp"):
|
||||
if not p.is_file():
|
||||
continue
|
||||
text = read_source(p)
|
||||
if "MetricsRegistry" not in p.name and "metric" not in text.lower():
|
||||
continue
|
||||
labels |= set(METRIC_LABEL.findall(text))
|
||||
return labels
|
||||
|
||||
|
||||
def run_rule_d_dashboards(
|
||||
root: Path, l1_keys: Set[str], metric_labels: Set[str], report: Report
|
||||
) -> None:
|
||||
dash_dir = root / "docker" / "telemetry" / "grafana" / "dashboards"
|
||||
files = sorted(dash_dir.glob("*.json")) if dash_dir.is_dir() else []
|
||||
if not files:
|
||||
report.skip("D", "no dashboard JSON present")
|
||||
return
|
||||
if not l1_keys:
|
||||
report.skip("D", "no L1 key set to validate against")
|
||||
return
|
||||
builtins = {
|
||||
"__name__", # Prometheus reserved label for the metric name itself
|
||||
"le",
|
||||
"exported_instance",
|
||||
"span_name",
|
||||
"status_code",
|
||||
"service_name",
|
||||
"service_version",
|
||||
"service_instance_id",
|
||||
"job",
|
||||
"job_type", # standard Prometheus label for job-queue metrics
|
||||
"instance",
|
||||
}
|
||||
# A dashboard label is valid if it is a span attribute (L1), a native-metric
|
||||
# label (L6), or a Prometheus/Grafana builtin.
|
||||
valid = l1_keys | metric_labels | builtins
|
||||
found = False
|
||||
for f in files:
|
||||
try:
|
||||
text = read_source(f)
|
||||
except OSError:
|
||||
continue
|
||||
# PromQL `sum by (a, b)` and `{label="..."}` references.
|
||||
labels: Set[str] = set()
|
||||
for m in re.finditer(r"by\s*\(([^)]*)\)", text):
|
||||
labels |= {x.strip() for x in m.group(1).split(",") if x.strip()}
|
||||
for m in re.finditer(r"\b([a-z_][a-z0-9_.]*)\s*[=!]~?\s*\"", text):
|
||||
labels.add(m.group(1))
|
||||
for lbl in sorted(labels):
|
||||
# Strip a TraceQL scope prefix (span./resource./...) — the bare
|
||||
# attribute is what must resolve against L1.
|
||||
bare = TRACEQL_SCOPE.sub("", lbl)
|
||||
if bare in valid:
|
||||
continue
|
||||
found = True
|
||||
report.violation(
|
||||
"D",
|
||||
str(f.relative_to(root)),
|
||||
lbl,
|
||||
"must exist in L1, a metric label, or be a builtin",
|
||||
)
|
||||
if not found:
|
||||
report.ok(f"D: dashboard PromQL labels all resolve ({len(files)} file(s))")
|
||||
|
||||
|
||||
def run_rule_e_runbook(root: Path, l1_keys: Set[str], report: Report) -> None:
|
||||
path = root / "docs" / "telemetry-runbook.md"
|
||||
if not path.is_file():
|
||||
report.skip("E", "runbook not present")
|
||||
return
|
||||
if not l1_keys:
|
||||
report.skip("E", "no L1 key set to validate against")
|
||||
return
|
||||
text = read_source(path)
|
||||
found = False
|
||||
# Only the dotted `xrpl.<domain>.<field>` attribute form is a violation. The
|
||||
# `xrpl.`-with-trailing-dot anchor is the discriminator: it matches the old
|
||||
# dotted attribute convention being migrated away from, while everything
|
||||
# else legitimately dotted in the runbook does NOT match it —
|
||||
# * span names (`consensus.round`, `tx.process`) no `xrpl.` prefix
|
||||
# * filenames (`xrpld.cfg`, `RCLConsensus.cpp`) `xrpld.`/`.cpp`, not `xrpl.`
|
||||
# * OTel-standard (`service.name`, `http.method`) no `xrpl.` prefix
|
||||
# * metric labels (`xrpl_rpc_command`) underscore, no dot
|
||||
# Legitimate dotted resource attrs (`xrpl.network.id`/`.type`) are in L1 and
|
||||
# are skipped. A dotted `xrpl.` token absent from L1 is a genuine doc/code
|
||||
# mismatch (e.g. `xrpl.tx.hash` where the code emits `tx_hash`).
|
||||
for m in re.finditer(r"`(xrpl\.[a-z][a-z0-9_.]*)`", text):
|
||||
token = m.group(1)
|
||||
if token in l1_keys: # legitimate dotted resource attr (xrpl.network.*)
|
||||
continue
|
||||
found = True
|
||||
report.violation(
|
||||
"E", str(path.relative_to(root)), token, "underscore, not dotted"
|
||||
)
|
||||
if not found:
|
||||
report.ok("E: runbook attribute references consistent with L1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
864
.github/scripts/otel-naming/test_check_otel_naming.py
vendored
Normal file
864
.github/scripts/otel-naming/test_check_otel_naming.py
vendored
Normal file
@@ -0,0 +1,864 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Unit tests for check_otel_naming.py.
|
||||
|
||||
Stdlib-only (unittest), matching the dependency-free policy of the check itself.
|
||||
Run from anywhere:
|
||||
|
||||
python .github/scripts/otel-naming/test_check_otel_naming.py
|
||||
|
||||
Each rule is exercised in isolation against a synthetic tree / synthetic L1 key
|
||||
set, covering positive (must flag), negative (must not flag), and boundary
|
||||
cases. Rule E (runbook dotted-attribute detection) has the densest coverage
|
||||
because its discriminator — the `xrpl.<domain>.` prefix vs span names,
|
||||
filenames, OTel-standard keys, and metric labels — is the subtlest.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# Load the check module by path (it is not an importable package).
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"check_otel_naming", str(Path(__file__).with_name("check_otel_naming.py"))
|
||||
)
|
||||
chk = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(chk)
|
||||
|
||||
|
||||
# A controlled L1 set used across tests: the two legitimate dotted resource
|
||||
# attrs plus a handful of underscore span-attribute keys.
|
||||
L1 = {
|
||||
"xrpl.network.id",
|
||||
"xrpl.network.type",
|
||||
"tx_hash",
|
||||
"peer_id",
|
||||
"consensus_mode",
|
||||
"command",
|
||||
"rpc_status",
|
||||
"ledger_seq",
|
||||
}
|
||||
|
||||
|
||||
def _run_rule_e(runbook_text: str):
|
||||
"""Run Rule E against a synthetic runbook; return the flagged tokens."""
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "docs").mkdir()
|
||||
(d / "docs" / "telemetry-runbook.md").write_text(runbook_text)
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, set(L1), report)
|
||||
return sorted(v[2] for v in report.violations)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class RuleERunbook(unittest.TestCase):
|
||||
"""Rule E: only dotted `xrpl.<domain>.<field>` attribute keys are flagged."""
|
||||
|
||||
# ----- positive: genuine dotted attribute-key violations -----
|
||||
def test_single_dotted_attr(self):
|
||||
self.assertEqual(_run_rule_e("`xrpl.tx.hash`"), ["xrpl.tx.hash"])
|
||||
|
||||
def test_multiple_dotted_attrs(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.tx.hash` and `xrpl.consensus.mode`"),
|
||||
["xrpl.consensus.mode", "xrpl.tx.hash"],
|
||||
)
|
||||
|
||||
def test_deep_dotted_three_segments(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.consensus.ledger.seq`"), ["xrpl.consensus.ledger.seq"]
|
||||
)
|
||||
|
||||
def test_dotted_attr_with_underscore_field(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.consensus.round_id`"), ["xrpl.consensus.round_id"]
|
||||
)
|
||||
|
||||
def test_repeated_token_reported_each_occurrence(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.tx.hash` ... `xrpl.tx.hash`"),
|
||||
["xrpl.tx.hash", "xrpl.tx.hash"],
|
||||
)
|
||||
|
||||
def test_resource_attr_not_in_l1_is_flagged(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`xrpl.network.unknown`"), ["xrpl.network.unknown"]
|
||||
)
|
||||
|
||||
# ----- negative: legitimately-dotted tokens that must NOT be flagged -----
|
||||
def test_span_name_single(self):
|
||||
self.assertEqual(_run_rule_e("`consensus.round`"), [])
|
||||
|
||||
def test_span_name_multi_segment(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`consensus.phase.open` `rpc.command.server_info`"), []
|
||||
)
|
||||
|
||||
def test_filename_cfg(self):
|
||||
self.assertEqual(_run_rule_e("`xrpld.cfg`"), [])
|
||||
|
||||
def test_filename_cpp(self):
|
||||
self.assertEqual(_run_rule_e("`RCLConsensus.cpp`"), [])
|
||||
|
||||
def test_otel_standard_service_name(self):
|
||||
self.assertEqual(_run_rule_e("`service.name`"), [])
|
||||
|
||||
def test_otel_standard_http_method(self):
|
||||
self.assertEqual(_run_rule_e("`http.method`"), [])
|
||||
|
||||
def test_metric_label_underscore(self):
|
||||
self.assertEqual(_run_rule_e("`xrpl_rpc_command`"), [])
|
||||
|
||||
def test_bare_underscore_attrs(self):
|
||||
self.assertEqual(_run_rule_e("`tx_hash` `consensus_mode`"), [])
|
||||
|
||||
def test_legit_dotted_resource_attrs_in_l1(self):
|
||||
self.assertEqual(_run_rule_e("`xrpl.network.id` `xrpl.network.type`"), [])
|
||||
|
||||
def test_prose_word(self):
|
||||
self.assertEqual(_run_rule_e("the `command` attribute"), [])
|
||||
|
||||
def test_plain_prose_no_backticks(self):
|
||||
self.assertEqual(_run_rule_e("xrpl.tx.hash without backticks is prose"), [])
|
||||
|
||||
# ----- boundary -----
|
||||
def test_empty_runbook(self):
|
||||
self.assertEqual(_run_rule_e(""), [])
|
||||
|
||||
def test_lookalike_prefix_xrpld(self):
|
||||
# `xrpld.` is NOT `xrpl.` — must not match.
|
||||
self.assertEqual(_run_rule_e("`xrpld.foo`"), [])
|
||||
|
||||
def test_lookalike_prefix_underscore(self):
|
||||
# `xrpl_rpc.command` starts with `xrpl_`, not `xrpl.`.
|
||||
self.assertEqual(_run_rule_e("`xrpl_rpc.command`"), [])
|
||||
|
||||
def test_uppercase_segment_not_matched(self):
|
||||
# The pattern requires a lowercase char after `xrpl.`; uppercase keys are
|
||||
# caught by Rule G at the L1 layer, not by the runbook text scan.
|
||||
self.assertEqual(_run_rule_e("`xrpl.TX.hash`"), [])
|
||||
|
||||
def test_token_touching_table_pipes(self):
|
||||
self.assertEqual(_run_rule_e("| `xrpl.tx.hash` | desc |"), ["xrpl.tx.hash"])
|
||||
|
||||
def test_mixed_line_only_xrpl_dotted_flagged(self):
|
||||
self.assertEqual(
|
||||
_run_rule_e("`consensus.round` uses `xrpl.tx.hash` and `service.name`"),
|
||||
["xrpl.tx.hash"],
|
||||
)
|
||||
|
||||
def test_skips_when_runbook_absent(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, set(L1), report)
|
||||
self.assertEqual(report.violations, [])
|
||||
self.assertTrue(any("SKIP: E" in s for s in report.skips))
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_skips_when_l1_empty(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "docs").mkdir()
|
||||
(d / "docs" / "telemetry-runbook.md").write_text("`xrpl.tx.hash`")
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, set(), report)
|
||||
self.assertEqual(report.violations, [])
|
||||
self.assertTrue(any("SKIP: E" in s for s in report.skips))
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class DslParser(unittest.TestCase):
|
||||
"""The makeStr/join/seg:: constexpr DSL resolver — the foundation of the
|
||||
L1 key set. Covers flat, nested, cross-file, alias, and multi-line forms."""
|
||||
|
||||
def test_flat_join(self):
|
||||
syms = chk.resolve_constants(
|
||||
'inline constexpr auto a = makeStr("xrpl");\n'
|
||||
'inline constexpr auto b = makeStr("network");\n'
|
||||
"inline constexpr auto c = join(a, b);\n"
|
||||
)
|
||||
self.assertEqual(syms["c"], "xrpl.network")
|
||||
|
||||
def test_nested_join_three_segments(self):
|
||||
syms = chk.resolve_constants(
|
||||
'inline constexpr auto xrpl = makeStr("xrpl");\n'
|
||||
'inline constexpr auto network = makeStr("network");\n'
|
||||
"inline constexpr auto networkId = "
|
||||
'join(join(xrpl, network), makeStr("id"));\n'
|
||||
)
|
||||
self.assertEqual(syms["networkId"], "xrpl.network.id")
|
||||
|
||||
def test_qualified_seg_reference(self):
|
||||
# `seg::rpc` resolves by its bare leaf `rpc`.
|
||||
syms = chk.resolve_constants('inline constexpr auto rpc = makeStr("rpc");\n')
|
||||
syms2 = chk.resolve_constants(
|
||||
'inline constexpr auto command = join(seg::rpc, makeStr("command"));\n',
|
||||
syms,
|
||||
)
|
||||
self.assertEqual(syms2["command"], "rpc.command")
|
||||
|
||||
def test_alias_reference(self):
|
||||
syms = chk.resolve_constants('inline constexpr auto rpc = makeStr("rpc");\n')
|
||||
chk.resolve_constants("inline constexpr auto alias = seg::rpc;\n", syms)
|
||||
self.assertEqual(syms["alias"], "rpc")
|
||||
|
||||
def test_unresolvable_expr_omitted(self):
|
||||
syms = chk.resolve_constants("inline constexpr auto x = join(unknown, y);\n")
|
||||
self.assertNotIn("x", syms)
|
||||
|
||||
def test_split_top_level_args_respects_nesting(self):
|
||||
self.assertEqual(
|
||||
chk.split_top_level_args("join(seg::a, b), c"),
|
||||
["join(seg::a, b)", " c"],
|
||||
)
|
||||
|
||||
def test_split_top_level_args_ignores_comma_in_string(self):
|
||||
self.assertEqual(
|
||||
chk.split_top_level_args('key, ","'),
|
||||
["key", ' ","'],
|
||||
)
|
||||
|
||||
def test_strip_comments_removes_line_and_block(self):
|
||||
self.assertEqual(
|
||||
chk.strip_comments("a // line\nb /* blk */ c").split(),
|
||||
["a", "b", "c"],
|
||||
)
|
||||
|
||||
|
||||
def _write(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
|
||||
|
||||
def _header(ns_attr_body: str, prefix_seg: str = "") -> str:
|
||||
"""A minimal *SpanNames.h body: optional seg defs + a namespace attr block."""
|
||||
return (
|
||||
"#pragma once\n"
|
||||
+ prefix_seg
|
||||
+ "namespace xrpl::telemetry::demo::span {\n"
|
||||
+ "namespace attr {\n"
|
||||
+ ns_attr_body
|
||||
+ "} // namespace attr\n"
|
||||
+ "}\n"
|
||||
)
|
||||
|
||||
|
||||
class AttrKeyExtraction(unittest.TestCase):
|
||||
"""attr_keys_from_header: comment-stripping + multi-line + using re-export."""
|
||||
|
||||
def _l1(self, header_text):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
h = d / "src" / "DemoSpanNames.h"
|
||||
_write(h, header_text)
|
||||
syms = chk.build_global_symbols([h])
|
||||
return chk.attr_keys_from_header(h, syms)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_single_line_makestr(self):
|
||||
keys = self._l1(_header('inline constexpr auto k = makeStr("tx_hash");\n'))
|
||||
self.assertIn("tx_hash", keys)
|
||||
|
||||
def test_multiline_constexpr_captured(self):
|
||||
keys = self._l1(
|
||||
_header("inline constexpr auto k =\n" ' makeStr("round_time_ms");\n')
|
||||
)
|
||||
self.assertIn("round_time_ms", keys)
|
||||
|
||||
def test_commented_makestr_not_leaked(self):
|
||||
keys = self._l1(
|
||||
_header(
|
||||
'inline constexpr auto k = makeStr("good");\n'
|
||||
'// inline constexpr auto bad = makeStr("old.dotted");\n'
|
||||
)
|
||||
)
|
||||
self.assertIn("good", keys)
|
||||
self.assertNotIn("old.dotted", keys)
|
||||
|
||||
def test_block_commented_makestr_not_leaked(self):
|
||||
keys = self._l1(
|
||||
_header(
|
||||
'inline constexpr auto k = makeStr("good");\n'
|
||||
'/* makeStr("blockbad") */\n'
|
||||
)
|
||||
)
|
||||
self.assertNotIn("blockbad", keys)
|
||||
|
||||
|
||||
class CamelToDotSegments(unittest.TestCase):
|
||||
"""semconv CamelCase -> dotted OTel-standard key derivation."""
|
||||
|
||||
def test_service_instance_id(self):
|
||||
self.assertEqual(
|
||||
chk.camel_to_dotsegments("ServiceInstanceId"),
|
||||
["service", "instance", "id"],
|
||||
)
|
||||
|
||||
def test_service_name(self):
|
||||
self.assertEqual(chk.camel_to_dotsegments("ServiceName"), ["service", "name"])
|
||||
|
||||
def test_derive_keys_from_telemetry_cpp(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
tele = d / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp"
|
||||
_write(
|
||||
tele,
|
||||
"resource::Resource::Create({\n"
|
||||
" {semconv::service::kServiceName, x},\n"
|
||||
" {semconv::service::kServiceInstanceId, y},\n"
|
||||
"});\n",
|
||||
)
|
||||
report = chk.Report()
|
||||
allow = chk.derive_dotted_resource_keys(d, {}, report)
|
||||
self.assertIn("service.name", allow)
|
||||
self.assertIn("service.instance.id", allow)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class SymbolCollision(unittest.TestCase):
|
||||
"""attr_keys_from_header must resolve a constant against ITS OWN header, so
|
||||
two headers defining a same-named constant each report their real wire key.
|
||||
Regression for the flat-symbol-table collision that let a later header
|
||||
clobber an earlier one and erased a dotted key from L1 (a Rule-A blind
|
||||
spot)."""
|
||||
|
||||
def _build(self, files):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
paths = {}
|
||||
for rel, text in files.items():
|
||||
p = d / rel
|
||||
_write(p, text)
|
||||
paths[rel] = p
|
||||
return d, paths
|
||||
|
||||
def test_same_named_const_not_clobbered_across_headers(self):
|
||||
base = (
|
||||
"#pragma once\n"
|
||||
"namespace xrpl::telemetry {\n"
|
||||
'namespace seg { inline constexpr auto xrpl = makeStr("xrpl");\n'
|
||||
'inline constexpr auto ledger = makeStr("ledger"); }\n'
|
||||
"namespace attr {\n"
|
||||
"inline constexpr auto ledgerHash = "
|
||||
'join(join(seg::xrpl, seg::ledger), makeStr("hash"));\n'
|
||||
"}\n}\n"
|
||||
)
|
||||
cons = (
|
||||
"#pragma once\n"
|
||||
"namespace xrpl::telemetry::consensus::span {\n"
|
||||
"namespace attr { inline constexpr auto ledgerHash = "
|
||||
'makeStr("ledger_hash"); }\n}\n'
|
||||
)
|
||||
d, paths = self._build(
|
||||
{
|
||||
"include/xrpl/telemetry/SpanNames.h": base,
|
||||
"src/xrpld/consensus/ConsensusSpanNames.h": cons,
|
||||
}
|
||||
)
|
||||
try:
|
||||
headers = chk.find_spanname_headers(d)
|
||||
syms = chk.build_global_symbols(headers)
|
||||
by_name = {p.name: chk.attr_keys_from_header(p, syms) for p in headers}
|
||||
# The base header keeps its dotted key; consensus keeps the bare one.
|
||||
self.assertIn("xrpl.ledger.hash", by_name["SpanNames.h"])
|
||||
self.assertEqual(by_name["ConsensusSpanNames.h"], {"ledger_hash"})
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_using_reexport_still_resolves_globally(self):
|
||||
# A `using`-re-export imports a constant defined elsewhere; it must
|
||||
# resolve against the global table, not the local header.
|
||||
base = (
|
||||
"#pragma once\n"
|
||||
"namespace xrpl::telemetry {\n"
|
||||
"namespace attr { inline constexpr auto txHash = "
|
||||
'makeStr("tx_hash"); }\n}\n'
|
||||
)
|
||||
dom = (
|
||||
"#pragma once\n"
|
||||
"namespace xrpl::telemetry::tx::span {\n"
|
||||
"namespace attr { using ::xrpl::telemetry::attr::txHash; }\n}\n"
|
||||
)
|
||||
d, paths = self._build(
|
||||
{
|
||||
"include/xrpl/telemetry/SpanNames.h": base,
|
||||
"src/xrpld/app/misc/TxSpanNames.h": dom,
|
||||
}
|
||||
)
|
||||
try:
|
||||
headers = chk.find_spanname_headers(d)
|
||||
syms = chk.build_global_symbols(headers)
|
||||
keys = chk.attr_keys_from_header(
|
||||
paths["src/xrpld/app/misc/TxSpanNames.h"], syms
|
||||
)
|
||||
self.assertEqual(keys, {"tx_hash"})
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class ResourceAllowlistScope(unittest.TestCase):
|
||||
"""derive_dotted_resource_keys must allowlist ONLY the dotted keys actually
|
||||
passed to Resource::Create() — not every dotted key in the base header. A
|
||||
dotted attr declared in a header but not set as a resource attr is a Rule-A
|
||||
violation."""
|
||||
|
||||
def _derive(self, tele_text, span_text):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(d / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp", tele_text)
|
||||
_write(d / "include" / "xrpl" / "telemetry" / "SpanNames.h", span_text)
|
||||
headers = chk.find_spanname_headers(d)
|
||||
syms = chk.build_global_symbols(headers)
|
||||
allow = chk.derive_dotted_resource_keys(d, syms, chk.Report())
|
||||
return allow, syms, headers, d
|
||||
except Exception:
|
||||
shutil.rmtree(d)
|
||||
raise
|
||||
|
||||
def test_dotted_span_attr_not_allowlisted_and_flagged(self):
|
||||
span = (
|
||||
"#pragma once\n"
|
||||
"namespace xrpl::telemetry {\n"
|
||||
'namespace seg { inline constexpr auto xrpl = makeStr("xrpl");\n'
|
||||
'inline constexpr auto ledger = makeStr("ledger");\n'
|
||||
'inline constexpr auto network = makeStr("network"); }\n'
|
||||
"namespace attr {\n"
|
||||
"inline constexpr auto networkId = "
|
||||
'join(join(seg::xrpl, seg::network), makeStr("id"));\n'
|
||||
"inline constexpr auto ledgerHash = "
|
||||
'join(join(seg::xrpl, seg::ledger), makeStr("hash"));\n'
|
||||
"}\n}\n"
|
||||
)
|
||||
tele = (
|
||||
"auto r = resource::Resource::Create({\n"
|
||||
" {semconv::service::kServiceName, x},\n"
|
||||
" {std::string(attr::networkId), n},\n"
|
||||
"});\n"
|
||||
)
|
||||
allow, syms, headers, d = self._derive(tele, span)
|
||||
try:
|
||||
# networkId IS a resource attr; ledgerHash is NOT, despite living in
|
||||
# the base header.
|
||||
self.assertIn("xrpl.network.id", allow)
|
||||
self.assertNotIn("xrpl.ledger.hash", allow)
|
||||
kbh = {h: chk.attr_keys_from_header(h, syms) for h in headers}
|
||||
report = chk.Report()
|
||||
chk.run_rule_a(kbh, allow, report)
|
||||
self.assertEqual([v[2] for v in report.violations], ["xrpl.ledger.hash"])
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_resource_block_brace_matched(self):
|
||||
# A nested {key,value} initializer must not truncate the block scan.
|
||||
tele = (
|
||||
"auto r = resource::Resource::Create({\n"
|
||||
" {semconv::service::kServiceName, x},\n"
|
||||
" {std::string(attr::networkType), t},\n"
|
||||
"});\n"
|
||||
)
|
||||
span = (
|
||||
"#pragma once\n"
|
||||
"namespace xrpl::telemetry {\n"
|
||||
'namespace seg { inline constexpr auto xrpl = makeStr("xrpl");\n'
|
||||
'inline constexpr auto network = makeStr("network"); }\n'
|
||||
"namespace attr { inline constexpr auto networkType = "
|
||||
'join(join(seg::xrpl, seg::network), makeStr("type")); }\n}\n'
|
||||
)
|
||||
allow, _syms, _headers, d = self._derive(tele, span)
|
||||
try:
|
||||
self.assertIn("xrpl.network.type", allow)
|
||||
self.assertIn("service.name", allow)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
def _run_rule_a(keys_by_header, allow):
|
||||
report = chk.Report()
|
||||
chk.run_rule_a(keys_by_header, allow, report)
|
||||
return sorted(v[2] for v in report.violations)
|
||||
|
||||
|
||||
class RuleADotted(unittest.TestCase):
|
||||
def test_dotted_attr_not_in_allow_flagged(self):
|
||||
kbh = {Path("src/RpcSpanNames.h"): {"xrpl.tx.hash", "command"}}
|
||||
self.assertEqual(_run_rule_a(kbh, {"xrpl.network.id"}), ["xrpl.tx.hash"])
|
||||
|
||||
def test_resource_attr_in_allow_passes(self):
|
||||
kbh = {Path("src/SpanNames.h"): {"xrpl.network.id"}}
|
||||
self.assertEqual(_run_rule_a(kbh, {"xrpl.network.id"}), [])
|
||||
|
||||
def test_bare_key_never_flagged(self):
|
||||
kbh = {Path("src/TxSpanNames.h"): {"tx_hash", "command"}}
|
||||
self.assertEqual(_run_rule_a(kbh, set()), [])
|
||||
|
||||
|
||||
def _run_rule_g(keys_by_header):
|
||||
report = chk.Report()
|
||||
chk.run_rule_g(keys_by_header, report)
|
||||
return sorted(v[2] for v in report.violations)
|
||||
|
||||
|
||||
class RuleGSnakeCase(unittest.TestCase):
|
||||
def test_camelcase_flagged(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"txHash"}}), ["txHash"])
|
||||
|
||||
def test_uppercase_flagged(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"TX_HASH"}}), ["TX_HASH"])
|
||||
|
||||
def test_space_flagged(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"bad key"}}), ["bad key"])
|
||||
|
||||
def test_snake_case_passes(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"tx_hash", "rpc_status"}}), [])
|
||||
|
||||
def test_dotted_resource_segments_pass(self):
|
||||
self.assertEqual(_run_rule_g({Path("h"): {"xrpl.network.id"}}), [])
|
||||
|
||||
def test_dotted_with_bad_segment_flagged(self):
|
||||
self.assertEqual(
|
||||
_run_rule_g({Path("h"): {"xrpl.Network.id"}}), ["xrpl.Network.id"]
|
||||
)
|
||||
|
||||
|
||||
class RuleFAndH(unittest.TestCase):
|
||||
"""run_rule_f: literal keys/span-names flagged; values & tests exempt.
|
||||
Rule H: qualified constant not in any header warns (non-fatal)."""
|
||||
|
||||
def _run(self, rel_path, source, header_symbols=frozenset()):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(d / rel_path, source)
|
||||
report = chk.Report()
|
||||
chk.run_rule_f(d, report, set(header_symbols))
|
||||
return (
|
||||
sorted(v[2] for v in report.violations),
|
||||
sorted(w[2] for w in report.warnings),
|
||||
)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_literal_key_flagged(self):
|
||||
v, _ = self._run("src/Foo.cpp", 'g.setAttribute("lit_key", v);\n')
|
||||
self.assertEqual(v, ['setAttribute arg0 "lit_key"'])
|
||||
|
||||
def test_literal_value_exempt(self):
|
||||
v, _ = self._run("src/Foo.cpp", 'g.setAttribute(attr::command, "submit");\n')
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_span_name_args_flagged(self):
|
||||
v, _ = self._run("src/Foo.cpp", 'SpanGuard::span(cat, "rpc", "command");\n')
|
||||
self.assertEqual(v, ['span arg1 "rpc"', 'span arg2 "command"'])
|
||||
|
||||
def test_test_path_exempt(self):
|
||||
v, _ = self._run("src/test/Foo.cpp", 'g.setAttribute("lit_key", v);\n')
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_spannames_header_exempt(self):
|
||||
v, _ = self._run("src/DemoSpanNames.h", 'g.setAttribute("lit_key", v);\n')
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_bare_span_call_not_matched(self):
|
||||
# No SpanGuard/./-> receiver -> not a telemetry call-site.
|
||||
v, _ = self._run("src/Foo.cpp", 'auto s = span("not", "telemetry");\n')
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_multiline_call_reports_first_line(self):
|
||||
v, _ = self._run("src/Foo.cpp", 'g.setAttribute(\n "k",\n v);\n')
|
||||
self.assertEqual(v, ['setAttribute arg0 "k"'])
|
||||
|
||||
def test_paren_in_string_value_does_not_break_parsing(self):
|
||||
# The ")" inside the value must not end the call early; key still seen.
|
||||
v, _ = self._run("src/Foo.cpp", 'g.setAttribute("k", ")");\n')
|
||||
self.assertEqual(v, ['setAttribute arg0 "k"'])
|
||||
|
||||
def test_rule_h_qualified_constant_warns(self):
|
||||
v, w = self._run(
|
||||
"src/Foo.cpp",
|
||||
"g.setAttribute(consensus::span::accept, v);\n",
|
||||
header_symbols={"command"},
|
||||
)
|
||||
self.assertEqual(v, [])
|
||||
self.assertEqual(w, ["setAttribute arg0 consensus::span::accept"])
|
||||
|
||||
def test_rule_h_known_constant_no_warning(self):
|
||||
_, w = self._run(
|
||||
"src/Foo.cpp",
|
||||
"g.setAttribute(rpc_span::attr::command, v);\n",
|
||||
header_symbols={"command"},
|
||||
)
|
||||
self.assertEqual(w, [])
|
||||
|
||||
def test_rule_h_bare_local_no_warning(self):
|
||||
_, w = self._run(
|
||||
"src/Foo.cpp", "g.setAttribute(myLeaf, v);\n", header_symbols={"command"}
|
||||
)
|
||||
self.assertEqual(w, [])
|
||||
|
||||
|
||||
class RuleBCollector(unittest.TestCase):
|
||||
def _run(self, yaml_text, l1):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(d / "docker" / "telemetry" / "otel-collector-config.yaml", yaml_text)
|
||||
report = chk.Report()
|
||||
chk.run_rule_b_collector(d, set(l1), report)
|
||||
return sorted(v[2] for v in report.violations), report.skips
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_dimension_not_in_l1_flagged(self):
|
||||
y = "spanmetrics:\n dimensions:\n - name: bogus_dim\n - name: command\n"
|
||||
v, _ = self._run(y, {"command"})
|
||||
self.assertEqual(v, ["bogus_dim"])
|
||||
|
||||
def test_all_dimensions_in_l1_pass(self):
|
||||
y = "spanmetrics:\n dimensions:\n - name: command\n - name: rpc_status\n"
|
||||
v, _ = self._run(y, {"command", "rpc_status"})
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_skip_when_no_spanmetrics_block(self):
|
||||
v, skips = self._run("receivers:\n otlp:\n", {"command"})
|
||||
self.assertEqual(v, [])
|
||||
self.assertTrue(any("SKIP: B" in s for s in skips))
|
||||
|
||||
|
||||
class RuleCTempo(unittest.TestCase):
|
||||
"""Rule C reads the Grafana Tempo DATASOURCE file's search.filters and
|
||||
validates only span-scope tags against L1."""
|
||||
|
||||
DS = "docker/telemetry/grafana/provisioning/datasources/tempo.yaml"
|
||||
|
||||
def _run(self, yaml_text, l1):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(d / self.DS, yaml_text)
|
||||
report = chk.Report()
|
||||
chk.run_rule_c_tempo(d, set(l1), report)
|
||||
return sorted(v[2] for v in report.violations), report.skips
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def _filter(self, fid, tag, scope):
|
||||
return (
|
||||
f" - id: {fid}\n"
|
||||
f" tag: {tag}\n"
|
||||
f' operator: "="\n'
|
||||
f" scope: {scope}\n"
|
||||
f" type: static\n"
|
||||
)
|
||||
|
||||
def test_span_tag_not_in_l1_flagged(self):
|
||||
y = "search:\n filters:\n" + self._filter("f1", "bogus_tag", "span")
|
||||
v, _ = self._run(y, {"command"})
|
||||
self.assertEqual(v, ["bogus_tag"])
|
||||
|
||||
def test_span_tags_in_l1_pass(self):
|
||||
y = (
|
||||
"search:\n filters:\n"
|
||||
+ self._filter("f1", "command", "span")
|
||||
+ self._filter("f2", "tx_hash", "span")
|
||||
)
|
||||
v, _ = self._run(y, {"command", "tx_hash"})
|
||||
self.assertEqual(v, [])
|
||||
|
||||
def test_resource_and_intrinsic_tags_ignored(self):
|
||||
# service.* (resource) and name/status/duration (intrinsic) are not
|
||||
# span attributes — they must not be validated against L1.
|
||||
y = (
|
||||
"search:\n filters:\n"
|
||||
+ self._filter("f1", "service.instance.id", "resource")
|
||||
+ self._filter("f2", "name", "intrinsic")
|
||||
+ self._filter("f3", "duration", "intrinsic")
|
||||
)
|
||||
v, skips = self._run(y, {"command"})
|
||||
self.assertEqual(v, [])
|
||||
self.assertTrue(any("SKIP: C" in s for s in skips))
|
||||
|
||||
def test_skip_when_datasource_absent(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
report = chk.Report()
|
||||
chk.run_rule_c_tempo(d, {"command"}, report)
|
||||
self.assertEqual(report.violations, [])
|
||||
self.assertTrue(any("SKIP: C" in s for s in report.skips))
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class RuleDDashboards(unittest.TestCase):
|
||||
def _run(self, json_text, l1, metric_labels=frozenset()):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(
|
||||
d / "docker" / "telemetry" / "grafana" / "dashboards" / "x.json",
|
||||
json_text,
|
||||
)
|
||||
report = chk.Report()
|
||||
chk.run_rule_d_dashboards(d, set(l1), set(metric_labels), report)
|
||||
return sorted(v[2] for v in report.violations)
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_unknown_promql_label_flagged(self):
|
||||
self.assertEqual(
|
||||
self._run('"expr": "sum by (bogus_label) (x)"', {"command"}),
|
||||
["bogus_label"],
|
||||
)
|
||||
|
||||
def test_builtin_labels_not_flagged(self):
|
||||
self.assertEqual(
|
||||
self._run('"expr": "sum by (le, span_name, exported_instance) (x)"', set()),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_prometheus_name_label_not_flagged(self):
|
||||
# `__name__` is the Prometheus reserved metric-name label; the renamed
|
||||
# system-*.json dashboards use `sum by (le, __name__)`.
|
||||
self.assertEqual(
|
||||
self._run('"expr": "sum by (le, __name__) (rate(x[5m]))"', set()),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_l1_label_passes(self):
|
||||
self.assertEqual(self._run('"q": "{command=\\"x\\"}"', {"command"}), [])
|
||||
|
||||
def test_traceql_span_prefix_stripped(self):
|
||||
# `span.establish_count` must validate against the bare L1 key.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "count_over_time(x) by (span.establish_count)"',
|
||||
{"establish_count"},
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_traceql_resource_prefix_stripped(self):
|
||||
self.assertEqual(self._run('"q": "{resource.service_name=\\"x\\"}"', set()), [])
|
||||
|
||||
def test_native_metric_label_passes(self):
|
||||
# `job_type` / `reason` are emitted by MetricsRegistry, not span attrs.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "sum by (job_type, reason) (x)"',
|
||||
{"command"},
|
||||
metric_labels={"job_type", "reason"},
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_unknown_label_still_flagged_with_metric_labels(self):
|
||||
# A label that is neither L1, metric label, nor builtin still fails.
|
||||
self.assertEqual(
|
||||
self._run(
|
||||
'"expr": "sum by (bogus) (x)"',
|
||||
{"command"},
|
||||
metric_labels={"job_type"},
|
||||
),
|
||||
["bogus"],
|
||||
)
|
||||
|
||||
def test_span_prefixed_unknown_still_flagged(self):
|
||||
# `span.not_a_key` whose bare form is unknown is still a violation.
|
||||
self.assertEqual(
|
||||
self._run('"expr": "x by (span.not_a_key)"', {"command"}),
|
||||
["span.not_a_key"],
|
||||
)
|
||||
|
||||
|
||||
class MetricLabelExtraction(unittest.TestCase):
|
||||
"""L6: native-metric label keys parsed from C++ instrument calls."""
|
||||
|
||||
def test_extracts_add_label(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
_write(
|
||||
d / "src" / "xrpld" / "telemetry" / "MetricsRegistry.cpp",
|
||||
'counter->Add(1, {{"job_type", std::string(jobType)}});\n'
|
||||
'c2->Add(1, {{"reason", std::string(r)}});\n',
|
||||
)
|
||||
self.assertEqual(chk.metric_label_names(d), {"job_type", "reason"})
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_no_metrics_file_empty(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "src").mkdir()
|
||||
self.assertEqual(chk.metric_label_names(d), set())
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
class ReportExitContract(unittest.TestCase):
|
||||
@staticmethod
|
||||
def _exit_code(report):
|
||||
"""Call render_and_exit (which prints + raises SystemExit), swallowing
|
||||
its stdout, and return the exit code."""
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
try:
|
||||
report.render_and_exit()
|
||||
except SystemExit as e:
|
||||
return e.code
|
||||
return None # pragma: no cover - render_and_exit always exits
|
||||
|
||||
def test_violation_exits_nonzero(self):
|
||||
r = chk.Report()
|
||||
r.violation("A", "f", "tok", "exp")
|
||||
self.assertEqual(self._exit_code(r), 1)
|
||||
|
||||
def test_clean_exits_zero(self):
|
||||
r = chk.Report()
|
||||
r.ok("all good")
|
||||
self.assertEqual(self._exit_code(r), 0)
|
||||
|
||||
def test_warning_only_exits_zero(self):
|
||||
r = chk.Report()
|
||||
r.warning("H", "f", "tok", "note")
|
||||
self.assertEqual(self._exit_code(r), 0)
|
||||
|
||||
|
||||
class RuleEReportTuple(unittest.TestCase):
|
||||
"""Assert Rule E records the full (rule, expected) tuple, not just token."""
|
||||
|
||||
def test_violation_tuple_fields(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "docs").mkdir()
|
||||
(d / "docs" / "telemetry-runbook.md").write_text("`xrpl.tx.hash`")
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, {"xrpl.network.id"}, report)
|
||||
self.assertEqual(len(report.violations), 1)
|
||||
rule, _loc, token, expected = report.violations[0]
|
||||
self.assertEqual(rule, "E")
|
||||
self.assertEqual(token, "xrpl.tx.hash")
|
||||
self.assertEqual(expected, "underscore, not dotted")
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
def test_clean_runbook_records_ok(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
(d / "docs").mkdir()
|
||||
(d / "docs" / "telemetry-runbook.md").write_text(
|
||||
"`tx_hash` `consensus.round`"
|
||||
)
|
||||
report = chk.Report()
|
||||
chk.run_rule_e_runbook(d, {"tx_hash"}, report)
|
||||
self.assertEqual(report.violations, [])
|
||||
self.assertTrue(any("E:" in c for c in report.checked))
|
||||
finally:
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
8
.github/workflows/on-pr.yml
vendored
8
.github/workflows/on-pr.yml
vendored
@@ -51,8 +51,10 @@ jobs:
|
||||
files: |
|
||||
# These paths are unique to `on-pr.yml`.
|
||||
.github/scripts/levelization/**
|
||||
.github/scripts/otel-naming/**
|
||||
.github/scripts/rename/**
|
||||
.github/workflows/reusable-check-levelization.yml
|
||||
.github/workflows/reusable-check-otel-naming.yml
|
||||
.github/workflows/reusable-check-rename.yml
|
||||
.github/workflows/on-pr.yml
|
||||
|
||||
@@ -109,6 +111,11 @@ jobs:
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-levelization.yml
|
||||
|
||||
check-otel-naming:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-otel-naming.yml
|
||||
|
||||
check-rename:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
@@ -176,6 +183,7 @@ jobs:
|
||||
if: failure() || cancelled()
|
||||
needs:
|
||||
- check-levelization
|
||||
- check-otel-naming
|
||||
- check-rename
|
||||
- clang-tidy
|
||||
- build-test
|
||||
|
||||
28
.github/workflows/reusable-check-otel-naming.yml
vendored
Normal file
28
.github/workflows/reusable-check-otel-naming.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# This workflow checks that OpenTelemetry span-attribute names stay consistent
|
||||
# across the code (*SpanNames.h), collector, Tempo, dashboards, and docs.
|
||||
# See .github/scripts/otel-naming/check_otel_naming.py and the
|
||||
# "Telemetry span attribute naming" section in CONTRIBUTING.md.
|
||||
name: Check OTel naming
|
||||
|
||||
# This workflow can only be triggered by other workflows.
|
||||
on: workflow_call
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-otel-naming
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
otel-naming:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Check OTel naming
|
||||
# The script is stdlib-only and reads only files already in the tree;
|
||||
# it enforces each rule only when the layer it needs is present, so it
|
||||
# works whether telemetry changes land in one PR or several.
|
||||
run: python .github/scripts/otel-naming/check_otel_naming.py
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -86,3 +86,6 @@ __pycache__
|
||||
|
||||
# clangd cache
|
||||
/.cache
|
||||
|
||||
# Env. file carrying environmental setup data for local or cloud runs.
|
||||
.env.*
|
||||
|
||||
@@ -121,6 +121,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)
|
||||
|
||||
@@ -298,6 +298,66 @@ If you wish to automatically fix whatever clang-tidy finds _and_ is capable of f
|
||||
run-clang-tidy -p build -quiet -fix -allow-no-checks src tests
|
||||
```
|
||||
|
||||
## Telemetry span attribute naming
|
||||
|
||||
OpenTelemetry span attribute keys follow these rules so they stay consistent
|
||||
across the code, the OTel collector, Tempo, Grafana dashboards, and docs. The
|
||||
constants in the `*SpanNames.h` headers are the single source of truth; every
|
||||
other layer must match them. A CI check enforces this end to end.
|
||||
|
||||
1. Per-span unique attribute: bare field name — allowed when the field is
|
||||
recorded by a single span/workflow, so the span name already supplies the
|
||||
domain (e.g. `command`, `local`, `version` on `rpc.command` / `tx.process`).
|
||||
2. Shared attribute (same concept on more than one span): ONE key, reused
|
||||
verbatim on every span that records it — the span name tells the occurrences
|
||||
apart, so no per-emitter prefix is added. Pick the name by the field's
|
||||
meaning: a property of a domain object keeps that object's bare field name
|
||||
(`ledger_hash`, `ledger_seq`, `tx_hash`, `peer_id`, `full_validation`); a
|
||||
field already qualified by a sub-kind keeps that qualifier on every emitter
|
||||
(`proposal_trusted` on both `consensus.proposal.receive` and
|
||||
`peer.proposal.receive`; `validation_trusted` likewise). Define it once in
|
||||
the base `SpanNames.h` `namespace attr` block and re-export (`using`) it from
|
||||
each domain header, so all emitters share the exact string.
|
||||
3. Collision qualifier: `<domain>_<field>` — only when a bare name would collide
|
||||
with a DIFFERENT concept in the shared spanmetrics label space, or with the
|
||||
OTel-reserved `status` key (e.g. `rpc_status`, `grpc_status`,
|
||||
`consensus_phase`, `consensus_round`). This disambiguates distinct concepts
|
||||
that share a word; it is NOT used to tag the same concept with the workflow
|
||||
that emitted it — that is rule 2 (one shared name).
|
||||
4. Resource attribute: dotted `xrpl.<subsystem>.<field>` — reserved ONLY for
|
||||
process/network identity set once at startup (`xrpl.network.id`,
|
||||
`xrpl.network.type`). Never use the dotted `xrpl.` form for span attributes.
|
||||
5. Span names use `<subsystem>[.<component>]` (dotted). Only attribute _keys_
|
||||
follow rules 1–4.
|
||||
|
||||
All attribute keys are `lower_snake_case` (lowercase letters, digits, and
|
||||
underscores; each dot-separated segment of a resource key likewise). No
|
||||
camelCase, uppercase, or spaces.
|
||||
|
||||
Standard OpenTelemetry semantic-convention keys keep their canonical dotted
|
||||
form (e.g. `service.*` resource attributes, `http.*` span attributes); the
|
||||
"no dotted form" rule above applies to xrpl-custom keys, not to OTel-standard
|
||||
conventions.
|
||||
|
||||
Always reference the `*SpanNames.h` constants for attribute keys and span
|
||||
names — never pass a string literal as a key or as a `span`/`childSpan` name
|
||||
argument. (Attribute _values_ may be runtime data.)
|
||||
|
||||
These rules are enforced by `.github/scripts/otel-naming/check_otel_naming.py`,
|
||||
run in CI on every pull request. The check derives the set of valid keys
|
||||
directly from the `*SpanNames.h` constants and the resource attributes the code
|
||||
registers, so there is no separate list to keep in sync. It cross-validates the
|
||||
collector, Tempo, dashboards, and docs against those keys, and each rule runs
|
||||
only when the file it needs is present — so it works whether telemetry changes
|
||||
land in one pull request or several. Run it locally with:
|
||||
|
||||
```
|
||||
python .github/scripts/otel-naming/check_otel_naming.py
|
||||
```
|
||||
|
||||
See [.github/scripts/otel-naming/README.md](.github/scripts/otel-naming/README.md)
|
||||
for the full rule list.
|
||||
|
||||
## Contracts and instrumentation
|
||||
|
||||
We are using [Antithesis](https://antithesis.com/) for continuous fuzzing,
|
||||
|
||||
565
OpenTelemetryPlan/00-tracing-fundamentals.md
Normal file
565
OpenTelemetryPlan/00-tracing-fundamentals.md
Normal file
@@ -0,0 +1,565 @@
|
||||
# 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)
|
||||
|
||||
xrpld P2P messages such as `TMTransaction` carry the trace context in two added byte fields alongside the existing payload: `trace_parent` holds the W3C traceparent (`trace_id`, `span_id`, and `trace_flags`), and `trace_state` holds the optional W3C tracestate. Together they propagate the trace across the P2P boundary so a receiving node can attach its spans to the sender's span.
|
||||
|
||||
---
|
||||
|
||||
## Sampling
|
||||
|
||||
Not every trace needs to be recorded. **Sampling** reduces overhead:
|
||||
|
||||
### Head Sampling (at trace start)
|
||||
|
||||
```
|
||||
Request arrives → Random N% chance → Record or skip entire trace
|
||||
```
|
||||
|
||||
- ✅ Low overhead
|
||||
- ❌ May miss interesting traces
|
||||
|
||||
> **xrpld note**: xrpld intentionally fixes head sampling at 100% (sample
|
||||
> everything) and does not expose a configurable ratio. A per-node ratio
|
||||
> would let different nodes make divergent keep/drop decisions for the same
|
||||
> distributed trace, producing broken/partial traces. 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/>ledger_seq = 12345678<br/>consensus_mode = proposing<br/>proposers = 35"]
|
||||
|
||||
subgraph open["consensus.phase.open"]
|
||||
open_desc["Duration: ~3s<br/>Waiting for transactions"]
|
||||
end
|
||||
|
||||
subgraph establish["consensus.phase.establish"]
|
||||
est_attrs["proposals_received = 28<br/>disputes_resolved = 3"]
|
||||
est_children["├── consensus.proposal.receive (×28)<br/>├── consensus.proposal.send (×1)<br/>└── consensus.dispute.resolve (×3)"]
|
||||
end
|
||||
|
||||
subgraph accept["consensus.phase.accept"]
|
||||
acc_attrs["transactions_applied = 150<br/>ledger_hash = DEF456..."]
|
||||
acc_children["├── ledger.build<br/>└── ledger.validate"]
|
||||
end
|
||||
|
||||
attrs --> open
|
||||
open --> establish
|
||||
establish --> accept
|
||||
end
|
||||
|
||||
style round fill:#f57f17,stroke:#e65100,color:#ffffff
|
||||
style open fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style establish fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style accept fill:#c2185b,stroke:#880e4f,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **consensus.round (orange, root span)**: The top-level span encompassing the entire consensus round, with attributes like ledger sequence, mode, and proposer count.
|
||||
- **consensus.phase.open (blue)**: The first phase where the node waits (~3s) to collect incoming transactions before proposing.
|
||||
- **consensus.phase.establish (green)**: The negotiation phase where validators exchange proposals, resolve disputes, and converge on a transaction set. Child spans track each proposal received/sent and each dispute resolved.
|
||||
- **consensus.phase.accept (pink)**: The final phase where the agreed transaction set is applied, a new ledger is built, and the ledger is validated. Child spans cover `ledger.build` and `ledger.validate`.
|
||||
- **Arrows (open to establish to accept)**: The sequential flow through the three consensus phases. Each phase must complete before the next begins.
|
||||
|
||||
---
|
||||
|
||||
## 1.5 RPC Request Flow
|
||||
|
||||
> **WS** = WebSocket
|
||||
|
||||
RPC requests support W3C Trace Context headers for distributed tracing across services:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph request["rpc.request (root span)"]
|
||||
http["HTTP Request — POST /<br/>traceparent:<br/>00-abc123...-def456...-01"]
|
||||
|
||||
attrs["Attributes:<br/>http.method = POST<br/>net.peer.ip = 192.168.1.100<br/>command = submit"]
|
||||
|
||||
subgraph enqueue["jobqueue.enqueue"]
|
||||
job_attr["job_type = jtCLIENT_RPC"]
|
||||
end
|
||||
|
||||
subgraph command["rpc.command.submit"]
|
||||
cmd_attrs["version = 2<br/>rpc_role = user"]
|
||||
cmd_children["├── tx.deserialize<br/>├── tx.validate_local<br/>└── tx.submit_to_network"]
|
||||
end
|
||||
|
||||
response["Response: 200 OK<br/>Duration: 45ms"]
|
||||
|
||||
http --> attrs
|
||||
attrs --> enqueue
|
||||
enqueue --> command
|
||||
command --> response
|
||||
end
|
||||
|
||||
style request fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style enqueue fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style command fill:#e65100,stroke:#bf360c,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **rpc.request (green, root span)**: The outermost span representing the full RPC request lifecycle, from HTTP receipt to response. Carries the W3C `traceparent` header for distributed tracing.
|
||||
- **HTTP Request node**: Shows the incoming POST request with its `traceparent` header and extracted attributes (method, peer IP, command name).
|
||||
- **jobqueue.enqueue (blue)**: The span covering the asynchronous handoff from the RPC thread to the JobQueue worker thread. The trace context is preserved across this async boundary.
|
||||
- **rpc.command.submit (orange)**: The span for the actual command execution, with child spans for deserialization, local validation, and network submission.
|
||||
- **Response node**: The final output with HTTP status and total duration, marking the end of the root span.
|
||||
- **Arrows (top to bottom)**: The sequential processing pipeline -- receive request, extract attributes, enqueue job, execute command, return response.
|
||||
|
||||
---
|
||||
|
||||
## 1.6 Key Trace Points
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
The following table identifies priority instrumentation points across the codebase:
|
||||
|
||||
| Category | Span Name | File | Method | Priority |
|
||||
| --------------- | ---------------------- | ---------------------- | ----------------------- | -------- |
|
||||
| **Transaction** | `tx.receive` | `PeerImp.cpp` | `handleTransaction()` | High |
|
||||
| **Transaction** | `tx.validate` | `NetworkOPs.cpp` | `processTransaction()` | High |
|
||||
| **Transaction** | `tx.process` | `NetworkOPs.cpp` | `doTransactionSync()` | High |
|
||||
| **Transaction** | `tx.relay` | `OverlayImpl.cpp` | `relay()` | Medium |
|
||||
| **Consensus** | `consensus.round` | `RCLConsensus.cpp` | `startRound()` | High |
|
||||
| **Consensus** | `consensus.phase.*` | `Consensus.h` | `timerEntry()` | High |
|
||||
| **Consensus** | `consensus.proposal.*` | `RCLConsensus.cpp` | `peerProposal()` | Medium |
|
||||
| **RPC** | `rpc.request` | `ServerHandler.cpp` | `onRequest()` | High |
|
||||
| **RPC** | `rpc.command.*` | `RPCHandler.cpp` | `doCommand()` | High |
|
||||
| **Peer** | `peer.connect` | `OverlayImpl.cpp` | `onHandoff()` | Low |
|
||||
| **Peer** | `peer.message.*` | `PeerImp.cpp` | `onMessage()` | Low |
|
||||
| **Ledger** | `ledger.acquire` | `InboundLedgers.cpp` | `acquire()` | Medium |
|
||||
| **Ledger** | `ledger.build` | `RCLConsensus.cpp` | `buildLCL()` | High |
|
||||
| **PathFinding** | `pathfind.request` | `PathRequest.cpp` | `doUpdate()` | High |
|
||||
| **PathFinding** | `pathfind.compute` | `Pathfinder.cpp` | `findPaths()` | High |
|
||||
| **TxQ** | `txq.enqueue` | `TxQ.cpp` | `apply()` | High |
|
||||
| **TxQ** | `txq.apply` | `TxQ.cpp` | `processClosedLedger()` | High |
|
||||
| **Fee** | `fee.escalate` | `LoadManager.cpp` | `raiseLocalFee()` | Medium |
|
||||
| **Ledger** | `ledger.replay` | `LedgerReplayer.h` | `replay()` | Medium |
|
||||
| **Ledger** | `ledger.delta` | `LedgerDeltaAcquire.h` | `processData()` | Medium |
|
||||
| **Validator** | `validator.list.fetch` | `ValidatorList.cpp` | `verify()` | Medium |
|
||||
| **Validator** | `validator.manifest` | `Manifest.cpp` | `applyManifest()` | Low |
|
||||
| **Amendment** | `amendment.vote` | `AmendmentTable.cpp` | `doVoting()` | Low |
|
||||
| **SHAMap** | `shamap.sync` | `SHAMap.cpp` | `fetchRoot()` | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 1.7 Instrumentation Priority
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Instrumentation Priority Matrix
|
||||
x-axis Low Complexity --> High Complexity
|
||||
y-axis Low Value --> High Value
|
||||
quadrant-1 Implement First
|
||||
quadrant-2 Plan Carefully
|
||||
quadrant-3 Quick Wins
|
||||
quadrant-4 Consider Later
|
||||
|
||||
RPC Tracing: [0.2, 0.92]
|
||||
Transaction Tracing: [0.55, 0.88]
|
||||
Consensus Tracing: [0.78, 0.82]
|
||||
PathFinding: [0.38, 0.75]
|
||||
TxQ and Fees: [0.25, 0.65]
|
||||
Ledger Sync: [0.62, 0.58]
|
||||
Peer Message Tracing: [0.35, 0.25]
|
||||
JobQueue Tracing: [0.2, 0.48]
|
||||
Validator Mgmt: [0.48, 0.42]
|
||||
Amendment Tracking: [0.15, 0.32]
|
||||
SHAMap Operations: [0.72, 0.45]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.8 Observable Outcomes
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List
|
||||
|
||||
After implementing OpenTelemetry, operators and developers will gain visibility into the following:
|
||||
|
||||
### 1.8.1 What You Will See: Traces
|
||||
|
||||
| Trace Type | Description | Example Query in Grafana/Tempo |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| **Transaction Lifecycle** | Full journey from RPC submission through validation, relay, consensus, and ledger inclusion | `{service.name="xrpld" && tx_hash="ABC123..."}` |
|
||||
| **Cross-Node Propagation** | Transaction path across multiple xrpld nodes with timing | `{relay_count > 0}` |
|
||||
| **Consensus Rounds** | Complete round with all phases (open, establish, accept) | `{span.name=~"consensus.round.*"}` |
|
||||
| **RPC Request Processing** | Individual command execution with timing breakdown | `{command="account_info"}` |
|
||||
| **Ledger Acquisition** | Peer-to-peer ledger data requests and responses | `{span.name="ledger.acquire"}` |
|
||||
| **PathFinding Latency** | Path computation time and cache effectiveness for payment RPCs | `{span.name="pathfind.compute"}` |
|
||||
| **TxQ Behavior** | Queue depth, eviction patterns, fee escalation during congestion | `{span.name=~"txq.*"}` |
|
||||
| **Ledger Sync** | Full acquisition timeline including delta and transaction fetches | `{span.name=~"ledger.acquire.*"}` |
|
||||
| **Validator Health** | UNL fetch success, manifest updates, stale list detection | `{span.name=~"validator.*"}` |
|
||||
|
||||
### 1.8.2 What You Will See: Metrics (Derived from Traces)
|
||||
|
||||
| Metric | Description | Dashboard Panel |
|
||||
| ----------------------------- | --------------------------------------- | --------------------------- |
|
||||
| **RPC Latency (p50/p95/p99)** | Response time distribution per command | Heatmap by command |
|
||||
| **Transaction Throughput** | Transactions processed per second | Time series graph |
|
||||
| **Consensus Round Duration** | Time to complete consensus phases | Histogram |
|
||||
| **Cross-Node Latency** | Time for transaction to reach N nodes | Line chart with percentiles |
|
||||
| **Error Rate** | Failed transactions/RPC calls by type | Stacked bar chart |
|
||||
| **PathFinding Latency** | Path computation time per currency pair | Heatmap by currency |
|
||||
| **TxQ Depth** | Queued transactions over time | Time series with thresholds |
|
||||
| **Fee Escalation Level** | Current fee multiplier | Gauge with alert thresholds |
|
||||
| **Ledger Sync Duration** | Time to acquire missing ledgers | Histogram |
|
||||
|
||||
### 1.8.3 Concrete Dashboard Examples
|
||||
|
||||
**Transaction Trace View (Tempo):**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Trace: abc123... (Transaction Submission) Duration: 847ms │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ├── rpc.request [ServerHandler] ████░░░░░░ 45ms │
|
||||
│ │ └── rpc.command.submit [RPCHandler] ████░░░░░░ 42ms │
|
||||
│ │ └── tx.receive [NetworkOPs] ███░░░░░░░ 35ms │
|
||||
│ │ ├── tx.validate [TxQ] █░░░░░░░░░ 8ms │
|
||||
│ │ └── tx.relay [Overlay] ██░░░░░░░░ 15ms │
|
||||
│ │ ├── tx.receive [Node-B] █████░░░░░ 52ms │
|
||||
│ │ │ └── tx.relay [Node-B] ██░░░░░░░░ 18ms │
|
||||
│ │ └── tx.receive [Node-C] ██████░░░░ 65ms │
|
||||
│ └── consensus.round [RCLConsensus] ████████░░ 720ms │
|
||||
│ ├── consensus.phase.open ██░░░░░░░░ 180ms │
|
||||
│ ├── consensus.phase.establish █████░░░░░ 480ms │
|
||||
│ └── consensus.phase.accept █░░░░░░░░░ 60ms │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**RPC Performance Dashboard Panel:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ RPC Command Latency (Last 1 Hour) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Command │ p50 │ p95 │ p99 │ Errors │ Rate │
|
||||
│──────────────────┼────────┼────────┼────────┼────────┼──────│
|
||||
│ account_info │ 12ms │ 45ms │ 89ms │ 0.1% │ 150/s│
|
||||
│ submit │ 35ms │ 120ms │ 250ms │ 2.3% │ 45/s│
|
||||
│ ledger │ 8ms │ 25ms │ 55ms │ 0.0% │ 80/s│
|
||||
│ tx │ 15ms │ 50ms │ 100ms │ 0.5% │ 60/s│
|
||||
│ server_info │ 5ms │ 12ms │ 20ms │ 0.0% │ 200/s│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Consensus Health Dashboard Panel:**
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 1200
|
||||
height: 400
|
||||
plotReservedSpacePercent: 50
|
||||
chartOrientation: vertical
|
||||
themeVariables:
|
||||
xyChart:
|
||||
plotColorPalette: "#3498db"
|
||||
---
|
||||
xychart-beta
|
||||
title "Consensus Round Duration (Last 24 Hours)"
|
||||
x-axis "Time of Day (Hours)" [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]
|
||||
y-axis "Duration (seconds)" 1 --> 5
|
||||
line [2.1, 2.4, 2.8, 3.2, 3.8, 4.3, 4.5, 5.0, 4.7, 4.0, 3.2, 2.6, 2.0]
|
||||
```
|
||||
|
||||
### 1.8.4 Operator Actionable Insights
|
||||
|
||||
| Scenario | What You'll See | Action |
|
||||
| ------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| **Slow RPC** | Span showing which phase is slow (parsing, execution, serialization) | Optimize specific code path |
|
||||
| **Transaction Stuck** | Trace stops at validation; error attribute shows reason | Fix transaction parameters |
|
||||
| **Consensus Delay** | Phase.establish taking too long; proposer attribute shows missing validators | Investigate network connectivity |
|
||||
| **Memory Spike** | Large batch of spans correlating with memory increase | Tune batch_size or sampling |
|
||||
| **Network Partition** | Traces missing cross-node links for specific peer | Check peer connectivity |
|
||||
| **Path Computation Slow** | pathfind.compute span shows high latency; cache miss rate in attributes | Warm the RippleLineCache, check order book depth |
|
||||
| **TxQ Full** | txq.enqueue spans show evictions; fee.escalate spans increasing | Monitor fee levels, alert operators |
|
||||
| **Ledger Sync Stalled** | ledger.acquire spans timing out; peer reliability attributes show issues | Check peer connectivity, add trusted peers |
|
||||
| **UNL Stale** | validator.list.fetch spans failing; last_update attribute aging | Verify validator site URLs, check DNS |
|
||||
|
||||
### 1.8.5 Developer Debugging Workflow
|
||||
|
||||
1. **Find Transaction**: Query by `tx_hash` to get full trace
|
||||
2. **Identify Bottleneck**: Look at span durations to find slowest component
|
||||
3. **Check Attributes**: Review `validity`, `rpc_status` for errors
|
||||
4. **Correlate Logs**: Use `trace_id` to find related PerfLog entries
|
||||
5. **Compare Nodes**: Filter by `service.instance.id` to compare behavior across nodes
|
||||
|
||||
---
|
||||
|
||||
_Next: [Design Decisions](./02-design-decisions.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
686
OpenTelemetryPlan/02-design-decisions.md
Normal file
686
OpenTelemetryPlan/02-design-decisions.md
Normal file
@@ -0,0 +1,686 @@
|
||||
# Design Decisions
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Architecture Analysis](./01-architecture-analysis.md)
|
||||
|
||||
---
|
||||
|
||||
## 2.1 OpenTelemetry Components
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 2.1.1 SDK Selection
|
||||
|
||||
**Primary Choice**: OpenTelemetry C++ SDK (`opentelemetry-cpp`)
|
||||
|
||||
| Component | Purpose | Required |
|
||||
| --------------------------------------- | ---------------------- | ------------------------- |
|
||||
| `opentelemetry-cpp::api` | Tracing API headers | Yes |
|
||||
| `opentelemetry-cpp::sdk` | SDK implementation | Yes |
|
||||
| `opentelemetry-cpp::ext` | Extensions (exporters) | Yes |
|
||||
| `opentelemetry-cpp::otlp_http_exporter` | OTLP/HTTP export | Yes (shipped in Phase 1b) |
|
||||
| `opentelemetry-cpp::otlp_grpc_exporter` | OTLP/gRPC export | Future (not yet wired up) |
|
||||
|
||||
### 2.1.2 Instrumentation Strategy
|
||||
|
||||
**Manual Instrumentation** (recommended):
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
| ---------- | --------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **Manual** | Precise control, optimized placement, xrpld-specific attributes | More development effort |
|
||||
| **Auto** | Less code, automatic coverage | Less control, potential overhead, limited customization |
|
||||
|
||||
---
|
||||
|
||||
## 2.2 Exporter Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph nodes["xrpld Nodes"]
|
||||
node1["xrpld<br/>Node 1"]
|
||||
node2["xrpld<br/>Node 2"]
|
||||
node3["xrpld<br/>Node 3"]
|
||||
end
|
||||
|
||||
collector["OpenTelemetry<br/>Collector<br/>(sidecar or standalone)"]
|
||||
|
||||
subgraph backends["Observability Backends"]
|
||||
tempo["Tempo"]
|
||||
elastic["Elastic<br/>APM"]
|
||||
end
|
||||
|
||||
node1 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
node2 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
node3 -->|"OTLP/HTTP<br/>:4318"| collector
|
||||
|
||||
collector --> tempo
|
||||
collector --> elastic
|
||||
|
||||
style nodes fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style backends fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style collector fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Nodes (blue)**: The source of telemetry data. Each xrpld node exports spans via OTLP/HTTP on port 4318 (the only exporter shipped in Phase 1b).
|
||||
- **OpenTelemetry Collector (red)**: The central aggregation point that receives spans from all nodes. Can run as a sidecar (per-node) or standalone (shared). Handles batching, filtering, and routing.
|
||||
- **Observability Backends (green)**: The storage and visualization destinations. Tempo is the recommended backend for both development and production, and Elastic APM is an alternative. The Collector routes to one or more backends.
|
||||
- **Arrows (nodes to collector to backends)**: The data pipeline -- spans flow from nodes to the Collector over HTTP, then the Collector fans out to the configured backends.
|
||||
|
||||
### 2.2.1 OTLP/HTTP (Shipped in Phase 1b)
|
||||
|
||||
OTLP/HTTP is the only exporter wired up in Phase 1b. It is configured via
|
||||
`OtlpHttpExporterOptions` with the collector traces endpoint
|
||||
(`http://localhost:4318/v1/traces` by default) and a JSON content type
|
||||
(binary protobuf is also available).
|
||||
|
||||
### 2.2.2 OTLP/gRPC (Future Work — Planned Upgrade)
|
||||
|
||||
OTLP/gRPC is planned as a future upgrade from the HTTP exporter. The gRPC
|
||||
transport offers lower per-span overhead and tighter back-pressure semantics
|
||||
than HTTP/JSON, making it attractive for production deployments once the HTTP
|
||||
path is validated in earlier phases.
|
||||
|
||||
Required to land this upgrade:
|
||||
|
||||
1. Add `opentelemetry-cpp::otlp_grpc_exporter` to the Conan recipe (the
|
||||
dependency already exists but is not linked in Phase 1b builds).
|
||||
2. Extend `TelemetryConfig.cpp` to parse an `exporter` key (`otlp_http`
|
||||
default, `otlp_grpc` opt-in) and a gRPC endpoint override.
|
||||
3. In `Telemetry::start()` branch on the parsed exporter type and construct
|
||||
either `OtlpHttpExporterFactory::Create(httpOpts)` or
|
||||
`OtlpGrpcExporterFactory::Create(grpcOpts)` accordingly.
|
||||
4. Update the runbook and dashboards to document the alternate port and TLS
|
||||
settings.
|
||||
|
||||
When wired up, the gRPC path will use `OtlpGrpcExporterOptions` configured with
|
||||
the collector endpoint (host on port 4317), TLS credentials enabled, and a CA
|
||||
certificate path.
|
||||
|
||||
Until that work lands, `OtlpGrpcExporterOptions` is **not** used by any code
|
||||
path in Phase 1b through Phase 5.
|
||||
|
||||
---
|
||||
|
||||
## 2.3 Span Naming Conventions
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List | **WS** = WebSocket
|
||||
|
||||
### 2.3.1 Naming Schema
|
||||
|
||||
```
|
||||
<component>.<operation>[.<sub-operation>]
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
- `tx.receive` - Transaction received from peer
|
||||
- `consensus.phase.establish` - Consensus establish phase
|
||||
- `rpc.command.server_info` - server_info RPC command
|
||||
|
||||
### 2.3.2 Complete Span Catalog
|
||||
|
||||
| Span name | Description |
|
||||
| ------------------------------ | --------------------------------------- |
|
||||
| `tx.receive` | Transaction received from network |
|
||||
| `tx.validate` | Transaction signature/format validation |
|
||||
| `tx.process` | Full transaction processing |
|
||||
| `tx.relay` | Transaction relay to peers |
|
||||
| `tx.apply` | Apply transaction to ledger |
|
||||
| `consensus.round` | Complete consensus round |
|
||||
| `consensus.phase.open` | Open phase - collecting transactions |
|
||||
| `consensus.phase.establish` | Establish phase - reaching agreement |
|
||||
| `consensus.phase.accept` | Accept phase - applying consensus |
|
||||
| `consensus.proposal.receive` | Receive peer proposal |
|
||||
| `consensus.proposal.send` | Send our proposal |
|
||||
| `consensus.validation.receive` | Receive peer validation |
|
||||
| `consensus.validation.send` | Send our validation |
|
||||
| `rpc.request` | HTTP/WebSocket request handling |
|
||||
| `rpc.command.*` | Specific RPC command (dynamic) |
|
||||
| `peer.connect` | Peer connection establishment |
|
||||
| `peer.disconnect` | Peer disconnection |
|
||||
| `peer.message.send` | Send protocol message |
|
||||
| `peer.message.receive` | Receive protocol message |
|
||||
| `ledger.acquire` | Ledger acquisition from network |
|
||||
| `ledger.build` | Build new ledger |
|
||||
| `ledger.validate` | Ledger validation |
|
||||
| `ledger.close` | Close ledger |
|
||||
| `ledger.replay` | Ledger replay executed |
|
||||
| `ledger.delta` | Delta-based ledger acquired |
|
||||
| `pathfind.request` | Path request initiated |
|
||||
| `pathfind.compute` | Path computation executed |
|
||||
| `txq.enqueue` | Transaction queued |
|
||||
| `txq.apply` | Queued transaction applied |
|
||||
| `fee.escalate` | Fee escalation triggered |
|
||||
| `validator.list.fetch` | UNL list fetched |
|
||||
| `validator.manifest` | Manifest update processed |
|
||||
| `amendment.vote` | Amendment voting executed |
|
||||
| `shamap.sync` | State tree synchronization |
|
||||
| `job.enqueue` | Job added to queue |
|
||||
| `job.execute` | Job execution |
|
||||
|
||||
### 2.3.3 Attribute Naming Conventions
|
||||
|
||||
Span **names** follow §2.3.1 (dotted `<component>.<operation>`). Span
|
||||
**attribute keys** follow the rules below. The constants in the `*SpanNames.h`
|
||||
headers are the single source of truth; the collector, Tempo, the Grafana
|
||||
dashboards, and the runbook all consume these exact keys, so every layer must
|
||||
agree with the code. A CI check enforces this end to end.
|
||||
|
||||
1. **Per-span unique attribute** → bare field name, allowed when the field is
|
||||
recorded by a single span/workflow so the span name already supplies the
|
||||
domain (e.g. `command`, `version`, `local` on `rpc.command`).
|
||||
2. **Shared attribute (same concept on more than one span)** → ONE key, reused
|
||||
verbatim on every span that records it; the span name tells the occurrences
|
||||
apart, so no per-emitter prefix is added. Name it by the field's meaning: a
|
||||
property of a domain object keeps that object's bare field name (`ledger_hash`,
|
||||
`ledger_seq`, `tx_hash`, `peer_id`, `full_validation`); a field already
|
||||
qualified by a sub-kind keeps that qualifier on every emitter (`proposal_trusted`
|
||||
on both `consensus.proposal.receive` and `peer.proposal.receive`;
|
||||
`validation_trusted` likewise). Defined once in the base `SpanNames.h`
|
||||
`namespace attr` block and re-exported (`using`) by each domain header.
|
||||
3. **Collision qualifier** → `<domain>_<field>`, only when a bare name would
|
||||
collide with a DIFFERENT concept in the shared spanmetrics label space or with
|
||||
the OTel-reserved `status` key (e.g. `rpc_status`, `grpc_status`,
|
||||
`consensus_phase`, `consensus_round`, `consensus_mode`). This disambiguates
|
||||
distinct concepts that share a word; it is NOT used to tag the same concept
|
||||
with its emitting workflow — that is rule 2 (one shared name).
|
||||
4. **Resource attribute** → dotted `xrpl.<subsystem>.<field>`, reserved ONLY
|
||||
for process/network identity set once at startup (`xrpl.network.id`,
|
||||
`xrpl.network.type`). Span attributes are never dotted in the `xrpl.` form —
|
||||
it blurs the resource/span scope boundary and parses awkwardly in TraceQL.
|
||||
5. **Span names** use `<subsystem>[.<component>]` (dotted, per §2.3.1). Only
|
||||
attribute _keys_ follow rules 1–4.
|
||||
|
||||
Standard OpenTelemetry semantic-convention keys keep their canonical dotted
|
||||
form (e.g. `service.*` resource attributes, `http.*` span attributes); the
|
||||
"no dotted form" rule applies to xrpl-custom keys only.
|
||||
|
||||
The same rules are recorded in `CONTRIBUTING.md` (the permanent home, since
|
||||
`OpenTelemetryPlan/` is removed once the rollout completes). The attribute
|
||||
examples in §2.4 below follow these rules.
|
||||
|
||||
---
|
||||
|
||||
## 2.4 Attribute Schema
|
||||
|
||||
> **TxQ** = Transaction Queue | **UNL** = Unique Node List | **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 2.4.1 Resource Attributes (Set Once at Startup)
|
||||
|
||||
Resource attributes identify the process and are set once at startup. They use
|
||||
the standard OpenTelemetry semantic conventions plus custom dotted `xrpl.*`
|
||||
keys (the dotted form is reserved for resource scope per §2.3.3).
|
||||
|
||||
| Key | Type / value | Description |
|
||||
| --------------------- | ------------------------------------------------------- | ------------------------------ |
|
||||
| `service.name` | `"xrpld"` | Standard `SERVICE_NAME` |
|
||||
| `service.version` | `BuildInfo::getVersionString()` | Standard `SERVICE_VERSION` |
|
||||
| `service.instance.id` | node public key (base58) | Standard `SERVICE_INSTANCE_ID` |
|
||||
| `xrpl.network.id` | network id (e.g. 0 for mainnet) | Network identifier |
|
||||
| `xrpl.network.type` | `"mainnet"` \| `"testnet"` \| `"devnet"` \| `"unknown"` | Network kind |
|
||||
| `xrpl.node.type` | `"validator"` \| `"stock"` \| `"reporting"` | Node role |
|
||||
| `xrpl.node.cluster` | cluster name | Cluster name, if clustered |
|
||||
|
||||
### 2.4.2 Span Attributes by Category
|
||||
|
||||
> Span attribute keys use the underscore form from §2.3.3 (shared/qualified
|
||||
> keys are `<domain>_<field>`; per-span unique keys are bare). The dotted form
|
||||
> is reserved for the resource attributes in §2.4.1 above. This catalog lists
|
||||
> the planned attribute set by category; the exact emitted key for each
|
||||
> implemented span is defined by the `*SpanNames.h` constants, which are the
|
||||
> single source of truth where the two differ.
|
||||
|
||||
#### Transaction Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------- | ------ | ------------------------------------- |
|
||||
| `tx_hash` | string | Transaction hash (hex) |
|
||||
| `tx_type` | string | `"Payment"`, `"OfferCreate"`, etc. |
|
||||
| `tx_account` | string | Source account (redacted in prod) |
|
||||
| `tx_sequence` | int64 | Account sequence number |
|
||||
| `tx_fee` | int64 | Fee in drops |
|
||||
| `tx_result` | string | `"tesSUCCESS"`, `"tecPATH_DRY"`, etc. |
|
||||
| `ledger_index` | int64 | Ledger containing transaction |
|
||||
| `relay_count` | int64 | Peers the transaction was relayed to |
|
||||
| `suppressed` | bool | `true` when HashRouter dropped a dup |
|
||||
|
||||
#### Consensus Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------------- | ------- | ----------------------------------- |
|
||||
| `consensus_round` | int64 | Round number |
|
||||
| `consensus_phase` | string | `"open"`, `"establish"`, `"accept"` |
|
||||
| `consensus_mode` | string | `"proposing"`, `"observing"`, etc. |
|
||||
| `proposers` | int64 | Number of proposers |
|
||||
| `prev_ledger_prefix` | string | Previous ledger hash prefix |
|
||||
| `ledger_seq` | int64 | Ledger sequence |
|
||||
| `tx_count` | int64 | Transactions in consensus set |
|
||||
| `round_time_ms` | float64 | Round duration |
|
||||
|
||||
Establish-phase gap fill and cross-node correlation attributes (Phase 4a):
|
||||
|
||||
| Key | Type | Description |
|
||||
| --------------------- | ------ | --------------------------------------------------------- |
|
||||
| `consensus_round_id` | int64 | Consensus round number |
|
||||
| `consensus_ledger_id` | string | `previousLedger.id()` — shared across nodes |
|
||||
| `trace_strategy` | string | `"deterministic"` or `"attribute"` |
|
||||
| `converge_percent` | int64 | Convergence % (0-100+) |
|
||||
| `establish_count` | int64 | Number of establish iterations |
|
||||
| `disputes_count` | int64 | Active disputed transactions |
|
||||
| `agree_count` | int64 | Peers that agree (haveConsensus) |
|
||||
| `disagree_count` | int64 | Peers that disagree |
|
||||
| `threshold_percent` | int64 | Close-time consensus threshold (`avCT_CONSENSUS_PCT`=75%) |
|
||||
| `consensus_result` | string | `"yes"`, `"no"`, `"moved_on"`, `"expired"` |
|
||||
| `mode_old` | string | Previous consensus mode |
|
||||
| `mode_new` | string | New consensus mode |
|
||||
|
||||
#### RPC Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------- | ------- | ----------------------------------------------------------------------------- |
|
||||
| `command` | string | Command name (per-span unique on `rpc.command`) |
|
||||
| `version` | int64 | API version |
|
||||
| `rpc_role` | string | `"admin"` or `"user"` (qualified — `role` is generic) |
|
||||
| `params` | string | Sanitized parameters (optional) |
|
||||
| `rpc_status` | string | Response status: `success` \| `error` (qualified — `status` is OTel-reserved) |
|
||||
| `duration_ms` | float64 | Request duration in milliseconds |
|
||||
|
||||
#### Peer & Message Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------------- | ------- | -------------------------- |
|
||||
| `peer_id` | string | Peer public key (base58) |
|
||||
| `peer_address` | string | IP:port |
|
||||
| `peer_latency_ms` | float64 | Measured latency |
|
||||
| `peer_cluster` | string | Cluster name if clustered |
|
||||
| `message_type` | string | Protocol message type name |
|
||||
| `message_size_bytes` | int64 | Message size |
|
||||
| `message_compressed` | bool | Whether compressed |
|
||||
|
||||
#### Ledger & Job Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ----------------- | ------- | --------------------- |
|
||||
| `ledger_hash` | string | Ledger hash |
|
||||
| `ledger_index` | int64 | Ledger sequence/index |
|
||||
| `close_time` | int64 | Close time (epoch) |
|
||||
| `ledger_tx_count` | int64 | Transaction count |
|
||||
| `job_type` | string | Job type name |
|
||||
| `job_queue_ms` | float64 | Time spent in queue |
|
||||
| `job_worker` | int64 | Worker thread ID |
|
||||
|
||||
#### PathFinding Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------------------- | ------ | ------------------------- |
|
||||
| `pathfind_source_currency` | string | Source currency code |
|
||||
| `pathfind_dest_currency` | string | Destination currency code |
|
||||
| `pathfind_path_count` | int64 | Number of paths found |
|
||||
| `pathfind_cache_hit` | bool | RippleLineCache hit |
|
||||
|
||||
#### TxQ Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| --------------------- | ------ | --------------------------- |
|
||||
| `txq_queue_depth` | int64 | Current queue depth |
|
||||
| `txq_fee_level` | int64 | Fee level of transaction |
|
||||
| `txq_eviction_reason` | string | Why transaction was evicted |
|
||||
|
||||
#### Fee Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ---------------------- | ----- | ------------------------- |
|
||||
| `fee_load_factor` | int64 | Current load factor |
|
||||
| `fee_escalation_level` | int64 | Fee escalation multiplier |
|
||||
|
||||
#### Validator Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------------------ | ----- | ------------------------- |
|
||||
| `validator_list_size` | int64 | UNL size |
|
||||
| `validator_list_age_sec` | int64 | Seconds since last update |
|
||||
|
||||
#### Amendment Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------------ | ------ | -------------------------------------- |
|
||||
| `amendment_name` | string | Amendment name |
|
||||
| `amendment_status` | string | `"enabled"`, `"vetoed"`, `"supported"` |
|
||||
|
||||
#### SHAMap Attributes
|
||||
|
||||
| Key | Type | Description |
|
||||
| ---------------------- | ------- | --------------------------------------------- |
|
||||
| `shamap_type` | string | `"transaction"`, `"state"`, `"account_state"` |
|
||||
| `shamap_missing_nodes` | int64 | Number of missing nodes during sync |
|
||||
| `shamap_duration_ms` | float64 | Sync duration |
|
||||
|
||||
### 2.4.3 Data Collection Summary
|
||||
|
||||
The following table summarizes what data is collected by category:
|
||||
|
||||
| Category | Attributes Collected | Purpose |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------- |
|
||||
| **Transaction** | `tx_hash`, `tx_type`, `tx_result`, `tx_fee`, `ledger_index` | Trace transaction lifecycle |
|
||||
| **Consensus** | `consensus_round`, `consensus_phase`, `consensus_mode`, `proposers`, `round_time_ms` | Analyze consensus timing |
|
||||
| **RPC** | `command`, `version`, `rpc_status`, `duration_ms` | Monitor RPC performance |
|
||||
| **Peer** | `peer_id` (public key), `peer_latency_ms`, `message_type`, `message_size_bytes` | Network topology analysis |
|
||||
| **Ledger** | `ledger_hash`, `ledger_index`, `close_time`, `ledger_tx_count` | Ledger progression tracking |
|
||||
| **Job** | `job_type`, `job_queue_ms`, `job_worker` | JobQueue performance |
|
||||
| **PathFinding** | `pathfind_fast`, `pathfind_search_level`, `pathfind_num_paths`, `pathfind_ledger_index`, `pathfind_num_requests` | Payment path analysis |
|
||||
| **TxQ** | `txq_queue_depth`, `txq_fee_level`, `txq_eviction_reason` | Queue depth and fee tracking |
|
||||
| **Fee** | `fee_load_factor`, `fee_escalation_level` | Fee escalation monitoring |
|
||||
| **Validator** | `validator_list_size`, `validator_list_age_sec` | UNL health monitoring |
|
||||
| **Amendment** | `amendment_name`, `amendment_status` | Protocol upgrade tracking |
|
||||
| **SHAMap** | `shamap_type`, `shamap_missing_nodes`, `shamap_duration_ms` | State tree sync performance |
|
||||
|
||||
### 2.4.4 Privacy & Sensitive Data Policy
|
||||
|
||||
> **PII** = Personally Identifiable Information
|
||||
|
||||
OpenTelemetry instrumentation is designed to collect **operational metadata only**, never sensitive content.
|
||||
|
||||
#### Data NOT Collected
|
||||
|
||||
The following data is explicitly **excluded** from telemetry collection:
|
||||
|
||||
| Excluded Data | Reason |
|
||||
| ----------------------- | ----------------------------------------- |
|
||||
| **Private Keys** | Never exposed; not relevant to tracing |
|
||||
| **Account Balances** | Financial data; privacy sensitive |
|
||||
| **Transaction Amounts** | Financial data; privacy sensitive |
|
||||
| **Raw TX Payloads** | May contain sensitive memo/data fields |
|
||||
| **Personal Data** | No PII collected |
|
||||
| **IP Addresses** | Configurable; excluded by default in prod |
|
||||
|
||||
#### Privacy Protection Mechanisms
|
||||
|
||||
| Mechanism | Description |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Account Hashing** | Account addresses are hashed both SDK-side (`pathfind_source_account`, `pathfind_dest_account` — always hashed before emission) and again at the collector level, so raw addresses never reach storage |
|
||||
| **Configurable Redaction** | Sensitive fields can be excluded via `[telemetry]` config section |
|
||||
| **Collector Tail Sampling** | xrpld head sampling is fixed at 1.0 (every span emitted); the collector retains ~10% of non-error traces, reducing stored data exposure |
|
||||
| **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 |
|
||||
|
||||
#### Account Address Hashing
|
||||
|
||||
Account addresses are **always** hashed before they reach the telemetry
|
||||
backend — there is no opt-out flag and therefore no insecure-by-default
|
||||
failure mode. Protection is applied in two independent layers:
|
||||
|
||||
1. **SDK-side** (this node): the path-finding RPC handlers call
|
||||
`redactAccount()` (`xrpl::telemetry`, `Redaction.h`) before setting the
|
||||
`pathfind_source_account` / `pathfind_dest_account` span attributes. The
|
||||
helper emits the first 16 characters of `sha512Half(address)` as
|
||||
lowercase hex — deterministic (spans for one account still correlate)
|
||||
but non-reversible.
|
||||
2. **Collector-side** (defense-in-depth): an `attributes/hash` processor in
|
||||
the OpenTelemetry Collector re-hashes those same attributes, so any node
|
||||
that emitted a raw value is still redacted before storage.
|
||||
|
||||
#### Collector-Level Data Protection
|
||||
|
||||
The OpenTelemetry Collector can be configured (via an `attributes` processor)
|
||||
to hash or redact sensitive attributes before export — for example, hashing
|
||||
`pathfind_source_account` / `pathfind_dest_account`, deleting `peer_address`
|
||||
to drop IP addresses, and deleting `params` to redact request parameters.
|
||||
|
||||
#### Configuration Options for Privacy
|
||||
|
||||
In `xrpld.cfg`, operators control data collection granularity through the
|
||||
`[telemetry]` section. Besides `enabled`, per-component toggles
|
||||
(`trace_transactions`, `trace_consensus`, `trace_rpc`, `trace_peer` — the last
|
||||
often disabled due to high volume) select which spans are emitted. Account
|
||||
address hashing is not configurable: addresses are hashed unconditionally by
|
||||
the SDK helper described above, with collector-level hashing as a second
|
||||
layer.
|
||||
|
||||
> **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
|
||||
|
||||
A PerfLog entry is a JSON object with fields such as `time`, `method`,
|
||||
`duration_us`, and `result`.
|
||||
|
||||
#### Beast Insight (StatsD)
|
||||
|
||||
- **Purpose**: Real-time metrics for monitoring dashboards
|
||||
- **Strengths**:
|
||||
- Aggregated metrics (counters, gauges, histograms)
|
||||
- Low overhead (UDP, fire-and-forget)
|
||||
- Good for alerting thresholds
|
||||
- **Limitations**:
|
||||
- No request-level detail
|
||||
- No causal relationships
|
||||
- Single-node perspective
|
||||
|
||||
In xrpld, Beast Insight is used through `increment` (counters), `gauge`
|
||||
(point-in-time values), and `timing` (durations) calls.
|
||||
|
||||
#### 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
|
||||
|
||||
A span is created via `startSpan` (e.g. `"tx.relay"`), annotated with
|
||||
attributes such as `tx_hash` and `peer_id`, and is automatically linked to its
|
||||
parent through the active context.
|
||||
|
||||
### 2.6.3 When to Use Each
|
||||
|
||||
| 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
|
||||
|
||||
> **Note**: Phase 7 replaces the StatsD bridge with native OTel Metrics SDK export. The diagram below shows the Phase 6 intermediate state. See [Phase7_taskList.md](./Phase7_taskList.md) for the migration design where Beast Insight emits via OTLP instead of StatsD.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Process"]
|
||||
perflog["PerfLog<br/>(JSON to file)"]
|
||||
insight["Beast Insight<br/>(StatsD)"]
|
||||
otel["OpenTelemetry<br/>(Tracing)"]
|
||||
end
|
||||
|
||||
perflog --> perffile["perf.log"]
|
||||
insight --> statsd["StatsD Server"]
|
||||
otel --> collector["OTLP Collector"]
|
||||
|
||||
perffile --> grafana["Grafana<br/>(Unified UI)"]
|
||||
statsd --> grafana
|
||||
collector --> grafana
|
||||
|
||||
style xrpld fill:#212121,stroke:#0a0a0a,color:#ffffff
|
||||
style grafana fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Process (dark gray)**: The single xrpld node running all three observability frameworks side by side. Each framework operates independently with no interference.
|
||||
- **PerfLog to perf.log**: PerfLog writes JSON-formatted event logs to a local file. Grafana can ingest these via Loki or a file-based datasource.
|
||||
- **Beast Insight to StatsD Server**: Insight sends aggregated metrics (counters, gauges) over UDP to a StatsD server. Grafana reads from StatsD-compatible backends like Graphite or Prometheus (via StatsD exporter).
|
||||
- **OpenTelemetry to OTLP Collector**: OTel exports spans over OTLP/HTTP to a Collector, which then forwards to a trace backend (Tempo). (OTLP/gRPC is future work — §2.2.2.)
|
||||
- **Grafana (red, unified UI)**: All three data streams converge in Grafana, enabling operators to correlate logs, metrics, and traces in a single dashboard.
|
||||
|
||||
**Phase 7 target state**: Beast Insight routes to `OTelCollector` (new `Collector` implementation) which exports via OTLP/HTTP to the same collector endpoint as traces. StatsD UDP path becomes a deprecated fallback (`[insight] server=statsd`). See [06-implementation-phases.md §6.8](./06-implementation-phases.md) and [Phase7_taskList.md](./Phase7_taskList.md) for details.
|
||||
|
||||
### 2.6.5 Correlation with PerfLog
|
||||
|
||||
Trace IDs can be correlated with existing PerfLog entries for comprehensive
|
||||
debugging. The design is for `RPCHandler.cpp` to start an `rpc.command.<method>`
|
||||
span alongside the existing PerfLog `rpcStart`/`rpcFinish`/`rpcError` calls,
|
||||
extract the span's `trace_id` (when valid), and eventually stamp it onto the
|
||||
PerfLog entry (a planned `setTraceId` hook) so logs and traces share a key. The
|
||||
span status is set to OK on success or to error (recording the exception) on
|
||||
failure.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Architecture Analysis](./01-architecture-analysis.md)_ | _Next: [Implementation Strategy](./03-implementation-strategy.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
483
OpenTelemetryPlan/03-implementation-strategy.md
Normal file
483
OpenTelemetryPlan/03-implementation-strategy.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# Implementation Strategy
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Configuration Reference](./05-configuration-reference.md)
|
||||
|
||||
---
|
||||
|
||||
## 3.1 Directory Structure
|
||||
|
||||
The telemetry implementation follows xrpld's existing code organization pattern:
|
||||
|
||||
```
|
||||
include/xrpl/
|
||||
├── telemetry/
|
||||
│ ├── Telemetry.h # Main telemetry interface (global singleton)
|
||||
│ ├── TelemetryConfig.h # Configuration structures
|
||||
│ ├── TraceContext.h # Context propagation utilities
|
||||
│ ├── SpanGuard.h # RAII span management with factory methods + discard()
|
||||
│ ├── DiscardFlag.h # Thread-local discard flag
|
||||
│ └── SpanAttributes.h # Attribute helper functions
|
||||
|
||||
src/libxrpl/
|
||||
├── telemetry/
|
||||
│ ├── Telemetry.cpp # Implementation + FilteringSpanProcessor
|
||||
│ ├── TelemetryConfig.cpp # Config parsing
|
||||
│ ├── TraceContext.cpp # Context serialization
|
||||
│ └── NullTelemetry.cpp # No-op implementation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3.2 Implementation Approach
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 30}}}%%
|
||||
flowchart TB
|
||||
subgraph phase1["Phase 1: Core"]
|
||||
direction LR
|
||||
sdk["SDK Integration"] ~~~ interface["Telemetry Interface"] ~~~ config["Configuration"]
|
||||
end
|
||||
|
||||
subgraph phase2["Phase 2: RPC"]
|
||||
direction LR
|
||||
http["HTTP Context"] ~~~ rpc["RPC Handlers"]
|
||||
end
|
||||
|
||||
subgraph phase3["Phase 3: P2P"]
|
||||
direction LR
|
||||
proto["Protobuf Context"] ~~~ tx["Transaction Relay"]
|
||||
end
|
||||
|
||||
subgraph phase4["Phase 4: Consensus"]
|
||||
direction LR
|
||||
consensus["Consensus Rounds"] ~~~ proposals["Proposals"]
|
||||
end
|
||||
|
||||
phase1 --> phase2 --> phase3 --> phase4
|
||||
|
||||
style phase1 fill:#1565c0,stroke:#0d47a1,color:#ffffff
|
||||
style phase2 fill:#2e7d32,stroke:#1b5e20,color:#ffffff
|
||||
style phase3 fill:#e65100,stroke:#bf360c,color:#ffffff
|
||||
style phase4 fill:#c2185b,stroke:#880e4f,color:#ffffff
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Minimal Intrusion**: Instrumentation should not alter existing control flow
|
||||
2. **Zero-Cost When Disabled**: Use compile-time flags and no-op implementations
|
||||
3. **Backward Compatibility**: Protocol Buffer extensions use high field numbers
|
||||
4. **Graceful Degradation**: Tracing failures must not affect node operation
|
||||
|
||||
---
|
||||
|
||||
## 3.3 Performance Overhead Summary
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
| Metric | Overhead | Notes |
|
||||
| ------------- | ---------- | ------------------------------------------------ |
|
||||
| CPU | 1-3% | Of per-transaction CPU cost (~200μs baseline) |
|
||||
| Memory | ~10 MB | SDK statics + batch buffer + worker thread stack |
|
||||
| Network | 10-50 KB/s | Compressed OTLP export to collector |
|
||||
| Latency (p99) | <2% | With proper sampling configuration |
|
||||
|
||||
---
|
||||
|
||||
## 3.4 Detailed CPU Overhead Analysis
|
||||
|
||||
### 3.4.1 Per-Operation Costs
|
||||
|
||||
> **Note on hardware assumptions**: The costs below are based on the official OTel C++ SDK CI benchmarks
|
||||
> (969 runs on GitHub Actions 2-core shared runners). On production server hardware (3+ GHz Xeon),
|
||||
> expect costs at the **lower end** of each range (~30-50% improvement over CI hardware).
|
||||
|
||||
| Operation | Time (ns) | Frequency | Impact |
|
||||
| --------------------- | --------- | ---------------------- | ---------- |
|
||||
| Span creation | 500-1000 | Every traced operation | Low |
|
||||
| Span end | 100-200 | Every traced operation | Low |
|
||||
| SetAttribute (string) | 80-120 | 3-5 per span | Low |
|
||||
| SetAttribute (int) | 40-60 | 2-3 per span | Negligible |
|
||||
| AddEvent | 100-200 | 0-2 per span | Low |
|
||||
| Context injection | 150-250 | Per outgoing message | Low |
|
||||
| Context extraction | 100-180 | Per incoming message | Low |
|
||||
| GetCurrent context | 10-20 | Thread-local access | Negligible |
|
||||
|
||||
**Source**: Span creation based on OTel C++ SDK `BM_SpanCreation` benchmark (AlwaysOnSampler +
|
||||
SimpleSpanProcessor + InMemoryExporter), median ~1,000 ns on CI hardware. AddEvent includes
|
||||
timestamp read + string copy + vector push + mutex acquisition. Context injection/extraction
|
||||
confirmed by `BM_SpanCreationWithScope` benchmark delta (~160 ns).
|
||||
|
||||
### 3.4.2 Transaction Processing Overhead
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
%%{init: {'pie': {'textPosition': 0.75}}}%%
|
||||
pie showData
|
||||
"tx.receive (1400ns)" : 1400
|
||||
"tx.validate (1200ns)" : 1200
|
||||
"tx.relay (1200ns)" : 1200
|
||||
"Context inject (200ns)" : 200
|
||||
```
|
||||
|
||||
**Transaction Tracing Overhead (~4.0μs total)**
|
||||
|
||||
</div>
|
||||
|
||||
**Overhead percentage**: 4.0 μs / 200 μs (avg tx processing) = **~2.0%**
|
||||
|
||||
> **Breakdown**: Each span (tx.receive, tx.validate, tx.relay) costs ~1,000 ns for creation plus
|
||||
> ~200-400 ns for 3-5 attribute sets. Context injection is ~200 ns (confirmed by benchmarks).
|
||||
> On production hardware, expect ~2.6 μs total (~1.3% overhead) due to faster span creation (~500-600 ns).
|
||||
|
||||
### 3.4.3 Consensus Round Overhead
|
||||
|
||||
| Operation | Count | Cost (ns) | Total |
|
||||
| ---------------------- | ----- | --------- | ---------- |
|
||||
| consensus.round span | 1 | ~1200 | ~1.2 μs |
|
||||
| consensus.phase spans | 3 | ~1100 | ~3.3 μs |
|
||||
| proposal.receive spans | ~20 | ~1100 | ~22 μs |
|
||||
| proposal.send spans | ~3 | ~1100 | ~3.3 μs |
|
||||
| Context operations | ~30 | ~200 | ~6 μs |
|
||||
| **TOTAL** | | | **~36 μs** |
|
||||
|
||||
> **Why higher**: Each span costs ~1,000 ns creation + ~100-200 ns for 1-2 attributes, totaling ~1,100-1,200 ns.
|
||||
> Context operations remain ~200 ns (confirmed by benchmarks). On production hardware, expect ~24 μs total.
|
||||
|
||||
**Overhead percentage**: 36 μs / 3s (typical round) = **~0.001%** (negligible)
|
||||
|
||||
### 3.4.4 RPC Request Overhead
|
||||
|
||||
| Operation | Cost (ns) |
|
||||
| ---------------- | ------------ |
|
||||
| rpc.request span | ~1200 |
|
||||
| rpc.command span | ~1100 |
|
||||
| Context extract | ~250 |
|
||||
| Context inject | ~200 |
|
||||
| **TOTAL** | **~2.75 μs** |
|
||||
|
||||
> **Why higher**: Each span costs ~1,000 ns creation + ~100-200 ns for attributes (command name,
|
||||
> version, role). Context extract/inject costs are confirmed by OTel C++ benchmarks.
|
||||
|
||||
- Fast RPC (1ms): 2.75 μs / 1ms = **~0.275%**
|
||||
- Slow RPC (100ms): 2.75 μs / 100ms = **~0.003%**
|
||||
|
||||
---
|
||||
|
||||
## 3.5 Memory Overhead Analysis
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 3.5.1 Static Memory
|
||||
|
||||
| Component | Size | Allocated |
|
||||
| ------------------------------------ | ----------- | ---------- |
|
||||
| TracerProvider singleton | ~64 KB | At startup |
|
||||
| BatchSpanProcessor (circular buffer) | ~16 KB | At startup |
|
||||
| BatchSpanProcessor (worker thread) | ~8 MB | At startup |
|
||||
| OTLP/HTTP exporter (client init) | ~64 KB | At startup |
|
||||
| Propagator registry | ~8 KB | At startup |
|
||||
| **Total static** | **~8.1 MB** | |
|
||||
|
||||
> **Why higher than earlier estimate**: The BatchSpanProcessor's circular buffer itself is only ~16 KB
|
||||
> (2049 x 8-byte `AtomicUniquePtr` entries), but it spawns a dedicated worker thread whose default
|
||||
> stack size on Linux is ~8 MB. The OTLP/HTTP exporter allocates a small client and TLS
|
||||
> initialization buffer. The worker thread stack dominates the static footprint.
|
||||
|
||||
### 3.5.2 Dynamic Memory
|
||||
|
||||
| Component | Size per unit | Max units | Peak |
|
||||
| -------------------- | -------------- | ---------- | --------------- |
|
||||
| Active span | ~500-800 bytes | 1000 | ~500-800 KB |
|
||||
| Queued span (export) | ~500 bytes | 2048 | ~1 MB |
|
||||
| Attribute storage | ~80 bytes | 5 per span | Included |
|
||||
| Context storage | ~64 bytes | Per thread | ~6.4 KB |
|
||||
| **Total dynamic** | | | **~1.5-1.8 MB** |
|
||||
|
||||
> **Why active spans are larger**: An active `Span` object includes the wrapper (~88 bytes: shared_ptr,
|
||||
> mutex, unique_ptr to Recordable) plus `SpanData` (~250 bytes: SpanContext, timestamps, name, status,
|
||||
> empty containers) plus attribute storage (~200-500 bytes for 3-5 string attributes in a `std::map`).
|
||||
> Source: `sdk/src/trace/span.h` and `sdk/include/opentelemetry/sdk/trace/span_data.h`.
|
||||
> Queued spans release the wrapper, keeping only `SpanData` + attributes (~500 bytes).
|
||||
|
||||
### 3.5.3 Memory Growth Characteristics
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 700
|
||||
height: 400
|
||||
---
|
||||
xychart-beta
|
||||
title "Memory Usage vs Span Rate (bounded by queue limit)"
|
||||
x-axis "Spans/second" [0, 200, 400, 600, 800, 1000]
|
||||
y-axis "Memory (MB)" 0 --> 12
|
||||
line [8.5, 9.2, 9.6, 9.9, 10.0, 10.0]
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
|
||||
- Memory increases with span rate but **plateaus at queue capacity** (default 2048 spans)
|
||||
- Batch export prevents unbounded growth
|
||||
- At queue limit, oldest spans are dropped (not blocked)
|
||||
- Maximum memory is bounded: ~8.3 MB static (dominated by worker thread stack) + 2048 queued spans x ~500 bytes (~1 MB) + active spans (~0.8 MB) ≈ **~10 MB ceiling**
|
||||
- The worker thread stack (~8 MB) is virtual memory; actual RSS depends on stack usage (typically much less)
|
||||
|
||||
> **Measured outcome**: A perf-iac comparison (telemetry compiled-in + enabled vs compiled-out,
|
||||
> 9 nodes — validators and client-handlers — under sustained payment load) recorded **no measurable
|
||||
> RSS increase over the telemetry-off baseline** (~15 GiB mean / ~18–19 GiB peak on both sides),
|
||||
> with no OOM, no swap, and no leak across the run. The ~10 MB ceiling above is therefore a
|
||||
> provisioning safety margin (dominated by virtual thread-stack address space), not an expected
|
||||
> resident-memory increase. Steady-state cost shows up as throughput (~3–4% at head sampling 1.0),
|
||||
> not memory.
|
||||
|
||||
### 3.5.4 Performance Data Sources
|
||||
|
||||
The overhead estimates in Sections 3.3-3.5 are derived from the following sources:
|
||||
|
||||
| Source | What it covers | URL |
|
||||
| ------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| OTel C++ SDK CI benchmarks (969 runs) | Span creation, context activation, sampler overhead | [Benchmark Dashboard](https://open-telemetry.github.io/opentelemetry-cpp/benchmarks/) |
|
||||
| `api/test/trace/span_benchmark.cc` | API-level span creation (~22 ns no-op) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/api/test/trace/span_benchmark.cc) |
|
||||
| `sdk/test/trace/sampler_benchmark.cc` | SDK span creation with samplers (~1,000 ns AlwaysOn) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/test/trace/sampler_benchmark.cc) |
|
||||
| `sdk/include/.../span_data.h` | SpanData memory layout (~250 bytes base) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/trace/span_data.h) |
|
||||
| `sdk/src/trace/span.h` | Span wrapper memory layout (~88 bytes) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/src/trace/span.h) |
|
||||
| `sdk/include/.../batch_span_processor_options.h` | Default queue size (2048), batch size (512) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h) |
|
||||
| `sdk/include/.../circular_buffer.h` | CircularBuffer implementation (AtomicUniquePtr array) | [Source](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/sdk/include/opentelemetry/sdk/common/circular_buffer.h) |
|
||||
| OTLP proto definition | Serialized span size estimation | [Proto](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto) |
|
||||
|
||||
---
|
||||
|
||||
## 3.6 Network Overhead Analysis
|
||||
|
||||
### 3.6.1 Export Bandwidth
|
||||
|
||||
> **Bytes per span**: Estimates use ~500 bytes/span (conservative upper bound). OTLP protobuf analysis
|
||||
> shows a typical span with 3-5 string attributes serializes to ~200-300 bytes raw; with gzip
|
||||
> compression (~60-70% of raw) and batching (amortized headers), ~350 bytes/span is more realistic.
|
||||
> The table uses the conservative estimate for capacity planning.
|
||||
|
||||
| Sampling Rate | Spans/sec | Bandwidth | Notes |
|
||||
| ------------- | --------- | --------- | ---------------- |
|
||||
| 100% | ~500 | ~250 KB/s | Development only |
|
||||
| 10% | ~50 | ~25 KB/s | Staging |
|
||||
| 1% | ~5 | ~2.5 KB/s | Production |
|
||||
| Error-only | ~1 | ~0.5 KB/s | Minimal overhead |
|
||||
|
||||
### 3.6.2 Trace Context Propagation
|
||||
|
||||
| Message Type | Context Size | Messages/sec | Overhead |
|
||||
| ---------------------- | ------------ | ------------ | ----------- |
|
||||
| TMTransaction | 25 bytes | ~100 | ~2.5 KB/s |
|
||||
| TMProposeSet | 25 bytes | ~10 | ~250 B/s |
|
||||
| TMValidation | 25 bytes | ~50 | ~1.25 KB/s |
|
||||
| **Total P2P overhead** | | | **~4 KB/s** |
|
||||
|
||||
---
|
||||
|
||||
## 3.7 Optimization Strategies
|
||||
|
||||
### 3.7.1 Sampling Strategies
|
||||
|
||||
#### Tail Sampling
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
trace["New Trace"]
|
||||
|
||||
trace --> errors{"Is Error?"}
|
||||
errors -->|Yes| sample["SAMPLE"]
|
||||
errors -->|No| consensus{"Is Consensus?"}
|
||||
|
||||
consensus -->|Yes| sample
|
||||
consensus -->|No| slow{"Is Slow?"}
|
||||
|
||||
slow -->|Yes| sample
|
||||
slow -->|No| prob{"Random < 10%?"}
|
||||
|
||||
prob -->|Yes| sample
|
||||
prob -->|No| drop["DROP"]
|
||||
|
||||
style sample fill:#4caf50,stroke:#388e3c,color:#fff
|
||||
style drop fill:#f44336,stroke:#c62828,color:#fff
|
||||
```
|
||||
|
||||
### 3.7.2 Batch Tuning Recommendations
|
||||
|
||||
| Environment | Batch Size | Batch Delay | Max Queue |
|
||||
| ------------------ | ---------- | ----------- | --------- |
|
||||
| Low-latency | 128 | 1000ms | 512 |
|
||||
| High-throughput | 1024 | 10000ms | 8192 |
|
||||
| Memory-constrained | 256 | 2000ms | 512 |
|
||||
|
||||
### 3.7.3 Conditional Instrumentation
|
||||
|
||||
Instrumentation is gated on two levels. A compile-time feature flag (`XRPL_ENABLE_TELEMETRY`) reduces the trace macros to no-ops when telemetry is built out, so disabled builds carry zero cost. At runtime, per-component guards (e.g. `shouldTracePeer()`) skip span creation for components whose tracing is turned off, incurring no overhead beyond a single boolean check.
|
||||
|
||||
---
|
||||
|
||||
## 3.8 Links to Detailed Documentation
|
||||
|
||||
- **[Configuration Reference](./05-configuration-reference.md)**: Configuration options and collector setup
|
||||
- **[Implementation Phases](./06-implementation-phases.md)**: Detailed timeline and milestones
|
||||
|
||||
---
|
||||
|
||||
## 3.9 Code Intrusiveness Assessment
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
This section provides a detailed assessment of how intrusive the OpenTelemetry integration is to the existing xrpld codebase.
|
||||
|
||||
### 3.9.1 Files Modified Summary
|
||||
|
||||
| Component | Files Modified | Architectural Impact |
|
||||
| --------------------- | -------------- | -------------------- |
|
||||
| **Core Telemetry** | 10 new files | None (new module) |
|
||||
| **Application Init** | 2 files | Minimal |
|
||||
| **RPC Layer** | 3 files | Minimal |
|
||||
| **Transaction Relay** | 4 files | Low |
|
||||
| **Consensus** | 3 files | Low-Medium |
|
||||
| **Protocol Buffers** | 1 file | Low |
|
||||
| **CMake/Build** | 3 files | Minimal |
|
||||
| **PathFinding** | 2 | Minimal |
|
||||
| **TxQ/Fee** | 2 | Minimal |
|
||||
| **Validator/Amend** | 3 | Minimal |
|
||||
| **Total** | **~33 files** | **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 | Purpose |
|
||||
| ------------------------------------------- | ------------------------- |
|
||||
| `include/xrpl/telemetry/Telemetry.h` | Main interface |
|
||||
| `include/xrpl/telemetry/TelemetryConfig.h` | Configuration structures |
|
||||
| `include/xrpl/telemetry/TraceContext.h` | Context propagation |
|
||||
| `include/xrpl/telemetry/SpanGuard.h` | RAII wrapper |
|
||||
| `include/xrpl/telemetry/DiscardFlag.h` | Thread-local discard flag |
|
||||
| `include/xrpl/telemetry/SpanAttributes.h` | Attribute helpers |
|
||||
| `src/libxrpl/telemetry/Telemetry.cpp` | Implementation |
|
||||
| `src/libxrpl/telemetry/TelemetryConfig.cpp` | Config parsing |
|
||||
| `src/libxrpl/telemetry/TraceContext.cpp` | Context serialization |
|
||||
| `src/libxrpl/telemetry/NullTelemetry.cpp` | No-op implementation |
|
||||
|
||||
#### Modified Files (Existing Xrpld Code)
|
||||
|
||||
| File | Risk Level |
|
||||
| ------------------------------------------------- | ---------- |
|
||||
| `src/xrpld/app/main/Application.cpp` | Low |
|
||||
| `include/xrpl/core/ServiceRegistry.h` | Low |
|
||||
| `src/xrpld/rpc/detail/ServerHandler.cpp` | Low |
|
||||
| `src/xrpld/rpc/handlers/*.cpp` | Low |
|
||||
| `src/xrpld/overlay/detail/PeerImp.cpp` | Medium |
|
||||
| `src/xrpld/overlay/detail/OverlayImpl.cpp` | Medium |
|
||||
| `src/xrpld/app/consensus/RCLConsensus.cpp` | Medium |
|
||||
| `src/xrpld/app/consensus/RCLConsensusAdaptor.cpp` | Medium |
|
||||
| `src/xrpld/core/JobQueue.cpp` | Low |
|
||||
| `src/xrpld/app/paths/PathRequest.cpp` | Low |
|
||||
| `src/xrpld/app/paths/Pathfinder.cpp` | Low |
|
||||
| `src/xrpld/app/misc/TxQ.cpp` | Low |
|
||||
| `src/xrpld/app/main/LoadManager.cpp` | Low |
|
||||
| `src/xrpld/app/misc/ValidatorList.cpp` | Low |
|
||||
| `src/xrpld/app/misc/AmendmentTable.cpp` | Low |
|
||||
| `src/xrpld/app/misc/Manifest.cpp` | Low |
|
||||
| `src/xrpld/shamap/SHAMap.cpp` | Low |
|
||||
| `src/xrpld/overlay/detail/ripple.proto` | Low |
|
||||
| `CMakeLists.txt` | Low |
|
||||
| `cmake/FindOpenTelemetry.cmake` | 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):** Instrumenting an RPC handler adds roughly 3-4 lines: one macro to start the span and one or two `setAttribute` calls (command name, status). The span ends automatically via RAII, so the existing control flow — process the request, send the result — is untouched.
|
||||
|
||||
**Consensus Instrumentation (Medium Intrusiveness):** Consensus is slightly more intrusive because child spans in later phase transitions need the round's context. Beyond the span-start and attribute macros, this requires storing the active context in a new member variable (`currentRoundContext_`) at round start. The existing round logic itself remains unchanged.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Design Decisions](./02-design-decisions.md)_ | _Next: [Configuration Reference](./05-configuration-reference.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
275
OpenTelemetryPlan/05-configuration-reference.md
Normal file
275
OpenTelemetryPlan/05-configuration-reference.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Configuration Reference
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Implementation Phases](./06-implementation-phases.md)
|
||||
|
||||
---
|
||||
|
||||
## 5.1 xrpld Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **TxQ** = Transaction Queue
|
||||
|
||||
### 5.1.1 Configuration File Section
|
||||
|
||||
The authoritative `[telemetry]` example lives in `cfg/xrpld-example.cfg`. Telemetry is disabled by default (`enabled=0`); enabling it turns on distributed tracing for transaction flow, consensus, and RPC calls, with traces exported to an OpenTelemetry Collector over OTLP. Head sampling is intentionally fixed at 1.0 (sample everything) and is not configurable — per-node head-sampling would produce broken/partial distributed traces, so volume reduction is delegated to the collector's tail sampling (see Section 7.4.2). The full option reference follows.
|
||||
|
||||
### 5.1.2 Configuration Options Summary
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| -------------------------- | ------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | bool | `false` | Enable/disable telemetry |
|
||||
| `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 |
|
||||
| `tls_client_cert` | string | `""` | Path to node's client certificate (PEM) for mutual TLS; requires `use_tls=1`; empty = one-way TLS |
|
||||
| `tls_client_key` | string | `""` | Path to private key (PEM) for `tls_client_cert`; requires `use_tls=1`; required when the cert is set |
|
||||
| `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 (`service.name`) for traces and metrics |
|
||||
| `service_instance_id` | string | `<node_pubkey>` | Instance identifier |
|
||||
|
||||
**Planned (not yet implemented)**: the following options appear in the design
|
||||
documents but are not parsed by `TelemetryConfig.cpp` in Phase 1b and later
|
||||
phases. They will be added as the corresponding subsystems are instrumented:
|
||||
|
||||
| Option | Planned Phase | Purpose |
|
||||
| -------------------------- | ------------- | ----------------------------------------------------------------------- |
|
||||
| `exporter` | Future | Select between OTLP/HTTP and OTLP/gRPC |
|
||||
| `trace_pathfind` | Phase 2 | Path computation tracing toggle |
|
||||
| `trace_txq` | Phase 3 | Transaction queue tracing toggle |
|
||||
| `trace_validator` | Future | Validator list / manifest update tracing |
|
||||
| `trace_amendment` | Future | Amendment voting tracing |
|
||||
| `consensus_trace_strategy` | Phase 4 | Trace ID strategy for consensus rounds (`deterministic` \| `attribute`) |
|
||||
|
||||
---
|
||||
|
||||
## 5.2 Configuration Parser
|
||||
|
||||
> **TxQ** = Transaction Queue
|
||||
|
||||
The parser `setupTelemetry()` in `src/libxrpl/telemetry/TelemetryConfig.cpp` reads the `[telemetry]` `Section` and populates a `Telemetry::Setup` struct, applying the defaults listed in Section 5.1.2 via `section.value_or(...)`. It derives `serviceInstanceId` from the node public key when not overridden, selects the exporter endpoint default by exporter type, and leaves the sampling ratio at its fixed 1.0 default (not read from config — see Section 7.4.2).
|
||||
|
||||
---
|
||||
|
||||
## 5.3 Application Integration
|
||||
|
||||
### 5.3.1 ApplicationImp Changes
|
||||
|
||||
> **Deferred identity**: The node public key (`nodeIdentity_`) is not
|
||||
> available during `ApplicationImp`'s member initializer list — it is
|
||||
> resolved later in `setup()`. The `Telemetry` object is therefore
|
||||
> constructed with an empty `serviceInstanceId` and patched via
|
||||
> `setServiceInstanceId()` once `setup()` has called `getNodeIdentity()`.
|
||||
|
||||
`ApplicationImp` (in `src/xrpld/app/main/Application.cpp`) owns a `std::unique_ptr<telemetry::Telemetry> telemetry_`. It is built in the member initializer list via `makeTelemetry(setupTelemetry(...))` with an empty `serviceInstanceId`, then patched in `setup()` by calling `setServiceInstanceId()` with the Base58 node public key (unless the user supplied a custom `service_instance_id`). `start()` and `run()` forward to `telemetry_->start()` / `telemetry_->stop()`, and `getTelemetry()` returns the owned instance.
|
||||
|
||||
### 5.3.2 ServiceRegistry Interface Addition
|
||||
|
||||
`include/xrpl/core/ServiceRegistry.h` gains a pure-virtual `telemetry::Telemetry& getTelemetry()` (with a forward declaration of `telemetry::Telemetry`), giving every component a uniform accessor for the tracing subsystem.
|
||||
|
||||
> **Note:** `Application` extends `ServiceRegistry`, so `getTelemetry()` is
|
||||
> available on both. Components that hold a `ServiceRegistry&` (e.g.
|
||||
> `NetworkOPsImp`) call `registry_.get().getTelemetry()`. Components that
|
||||
> still hold an `Application&` (e.g. `ServerHandler`, `PeerImp`,
|
||||
> `RCLConsensusAdaptor`) call `app_.getTelemetry()` directly.
|
||||
|
||||
---
|
||||
|
||||
## 5.4 CMake Integration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
### 5.4.1 Find OpenTelemetry Module
|
||||
|
||||
A `cmake/FindOpenTelemetry.cmake` module locates the OpenTelemetry C++ SDK. It first tries `find_package(opentelemetry-cpp CONFIG)`, aliasing the imported targets `OpenTelemetry::api`, `OpenTelemetry::sdk`, and `OpenTelemetry::otlp_grpc_exporter`, and falls back to `pkg-config` when no CMake config package is present.
|
||||
|
||||
### 5.4.2 CMakeLists.txt Changes
|
||||
|
||||
The top-level `CMakeLists.txt` adds an `XRPL_ENABLE_TELEMETRY` option (default `OFF`). When enabled, it runs `find_package(OpenTelemetry REQUIRED)`, defines the `XRPL_ENABLE_TELEMETRY` compile flag, and builds the `xrpl_telemetry` library from the real telemetry sources linked against the OpenTelemetry targets; when disabled, it builds the same target from a no-op `NullTelemetry.cpp` so call sites compile unchanged.
|
||||
|
||||
---
|
||||
|
||||
## 5.5 OpenTelemetry Collector Configuration
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring
|
||||
|
||||
> **Production hardening**: The configurations in this section are starting points. For production deployments where xrpld ships telemetry across a network to a centrally-hosted collector, see [Securing the OTel Pipeline](./secure-OTel.md) for the required mTLS receiver config, NetworkPolicy, and peer trace-context validation.
|
||||
|
||||
The authoritative collector config lives in the repo at `docker/telemetry/otel-collector-config.yaml` (with Tempo backend config in `docker/telemetry/tempo.yaml`). The sections below summarize the development and production shapes of that pipeline.
|
||||
|
||||
### 5.5.1 Development Configuration
|
||||
|
||||
The development collector enables an OTLP receiver on both gRPC (`0.0.0.0:4317`) and HTTP (`0.0.0.0:4318`), a single `batch` processor (1s timeout, batch size 100), and two exporters: a `logging` exporter for console debugging and `otlp/tempo` (insecure) for trace visualization. The single `traces` pipeline wires receiver → batch → both exporters.
|
||||
|
||||
### 5.5.2 Production Configuration
|
||||
|
||||
The production collector adds TLS on the OTLP gRPC receiver and a richer processor chain: a `memory_limiter` (OOM guard), `batch` (5s timeout, size 512), `tail_sampling`, and an `attributes` processor that hashes sensitive fields (e.g. `tx_account`) and stamps `deployment.environment`. Tail sampling keeps all `ERROR` traces, slow consensus rounds (>5s) and slow RPC requests (>1s), and probabilistically samples the remainder at 10%. Exporters target Grafana Tempo (TLS) and Elastic APM; `health_check` and `zpages` extensions are enabled for operability.
|
||||
|
||||
---
|
||||
|
||||
## 5.6 Docker Compose Development Environment
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
The authoritative development stack lives in the repo at `docker/telemetry/docker-compose.yml`. It brings up four services on a shared `xrpld-telemetry` network: an `otel-collector` (otel/opentelemetry-collector-contrib) exposing OTLP gRPC `4317`, OTLP HTTP `4318`, and health check `13133`; `tempo` for trace storage/visualization; `grafana` with provisioned datasources and dashboards (anonymous admin enabled); and an optional `prometheus` for metric correlation.
|
||||
|
||||
---
|
||||
|
||||
## 5.7 Configuration Architecture
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph config["Configuration Sources"]
|
||||
cfgFile["xrpld.cfg<br/>[telemetry] section"]
|
||||
cmake["CMake<br/>XRPL_ENABLE_TELEMETRY"]
|
||||
end
|
||||
|
||||
subgraph init["Initialization"]
|
||||
parse["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, per-component trace toggles) while the CMake flag controls whether telemetry is compiled in at all. Head sampling is fixed at 1.0 and is not a config option; volume reduction happens via tail sampling in the collector.
|
||||
- **Initialization**: `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/HTTP and enters the external Collector pipeline. (OTLP/gRPC is future work — see design decisions §2.2.2.)
|
||||
- **Collector Pipeline**: `Receivers` ingest OTLP data, `Processors` apply sampling/filtering/enrichment, and `Exporters` forward traces to storage backends (Tempo, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 5.8 Grafana Integration
|
||||
|
||||
> **APM** = Application Performance Monitoring
|
||||
|
||||
Step-by-step instructions for integrating xrpld traces with Grafana.
|
||||
|
||||
### 5.8.1 Data Source Configuration
|
||||
|
||||
#### Tempo (Recommended)
|
||||
|
||||
A Tempo datasource (`grafana/provisioning/datasources/tempo.yaml`, provisioned from `docker/telemetry/grafana/`) points at `http://tempo:3200` and enables `tracesToLogs` (linking to Loki on `service.name`/`tx_hash` and mapping `trace_id` → `traceID`), `serviceMap` against Prometheus, the node graph, and Loki search.
|
||||
|
||||
#### Elastic APM
|
||||
|
||||
Alternatively, an Elasticsearch datasource (`grafana/provisioning/datasources/elastic-apm.yaml`) of type `elasticsearch` points at `http://elasticsearch:9200` against the `apm-*` index, using `@timestamp` as the time field and mapping the log message/level fields.
|
||||
|
||||
### 5.8.2 Dashboard Provisioning
|
||||
|
||||
A dashboard provider (`grafana/provisioning/dashboards/dashboards.yaml`) loads the `xrpld` dashboard folder from disk (`/var/lib/grafana/dashboards/rippled`), polling for changes every 30s with deletion disabled.
|
||||
|
||||
### 5.8.3 Example Dashboard: RPC Performance
|
||||
|
||||
An example `xrpld RPC Performance` dashboard (uid `xrpld-rpc-performance`) sourced from Tempo via TraceQL provides four panels: RPC latency by command (heatmap), RPC error rate by command (timeseries), the top 10 slowest RPC commands by average duration (table), and a recent-traces table.
|
||||
|
||||
### 5.8.4 Example Dashboard: Transaction Tracing
|
||||
|
||||
An example `xrpld Transaction Tracing` dashboard (uid `xrpld-tx-tracing`) over Tempo provides three panels: transaction throughput (`tx.receive` rate, stat), cross-node relay count (average `span.relay_count` on `tx.relay`, timeseries), and a table of transaction validation errors (`tx.validate` with `status.code=error`).
|
||||
|
||||
### 5.8.5 TraceQL Query Examples
|
||||
|
||||
Common queries for xrpld traces:
|
||||
|
||||
```
|
||||
# Find all traces for a specific transaction hash
|
||||
{resource.service.name="xrpld" && span.tx_hash="ABC123..."}
|
||||
|
||||
# Find slow RPC commands (>100ms)
|
||||
{resource.service.name="xrpld" && name=~"rpc.command.*"} | duration > 100ms
|
||||
|
||||
# Find consensus rounds taking >5 seconds
|
||||
{resource.service.name="xrpld" && name="consensus.round"} | duration > 5s
|
||||
|
||||
# Find failed transactions with error details
|
||||
{resource.service.name="xrpld" && name="tx.validate" && status.code=error}
|
||||
|
||||
# Find transactions relayed to many peers
|
||||
{resource.service.name="xrpld" && name="tx.relay"} | span.relay_count > 10
|
||||
|
||||
# Compare latency across nodes
|
||||
{resource.service.name="xrpld" && name="rpc.command.account_info"} | avg(duration) by (resource.service.instance.id)
|
||||
```
|
||||
|
||||
### 5.8.6 Correlation with PerfLog
|
||||
|
||||
To correlate OpenTelemetry traces with existing PerfLog data:
|
||||
|
||||
**Step 1: Configure Loki to ingest PerfLog**
|
||||
|
||||
Configure a Promtail scrape job (`promtail-config.yaml`) that tails `/var/log/rippled/perf*.log`, parses each JSON line, and promotes `trace_id`, `ledger_seq`, and `tx_hash` to Loki labels.
|
||||
|
||||
**Step 2: Add trace_id to PerfLog entries**
|
||||
|
||||
Modify PerfLog so its JSON output includes a `trace_id` field whenever a valid span is active: fetch the current span from the OpenTelemetry runtime context, and if its context is valid, render the trace ID as a 32-character lowercase hex string into the log entry.
|
||||
|
||||
**Step 3: Configure Grafana trace-to-logs link**
|
||||
|
||||
In the Tempo datasource, set the `tracesToLogs` derived field to link to Loki on the `trace_id` and `tx_hash` tags, with `filterByTraceID: true`.
|
||||
|
||||
### 5.8.7 Correlation with Insight/OTel System Metrics
|
||||
|
||||
To correlate traces with Beast Insight system metrics:
|
||||
|
||||
**Step 1: Export Insight metrics to Prometheus**
|
||||
|
||||
Beast Insight metrics are exported natively via OTLP to the OTel Collector,
|
||||
which exposes them on the Prometheus endpoint alongside spanmetrics. Configure
|
||||
the `[insight]` section of `xrpld.cfg` with `server=otel`,
|
||||
`endpoint=http://localhost:4318/v1/metrics`, and `prefix=xrpld`; no separate
|
||||
StatsD exporter or Prometheus scrape job is needed when using `server=otel`.
|
||||
|
||||
**Step 2: Add exemplars to metrics**
|
||||
|
||||
The OpenTelemetry SDK automatically adds exemplars (trace IDs) to metrics when using the Prometheus exporter, linking metric spikes to specific traces.
|
||||
|
||||
**Step 3: Configure Grafana metric-to-trace link**
|
||||
|
||||
In the Prometheus datasource, set `exemplarTraceIdDestinations` to map the `trace_id` exemplar to the Tempo datasource.
|
||||
|
||||
**Step 4: Dashboard panel with exemplars**
|
||||
|
||||
Add a timeseries panel over Prometheus (e.g. `histogram_quantile(0.99, rate(xrpld_rpc_duration_seconds_bucket[5m]))`) with `exemplar: true` enabled.
|
||||
|
||||
This allows clicking on metric data points to jump directly to the related trace.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Implementation Strategy](./03-implementation-strategy.md)_ | _Next: [Implementation Phases](./06-implementation-phases.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
1703
OpenTelemetryPlan/06-implementation-phases.md
Normal file
1703
OpenTelemetryPlan/06-implementation-phases.md
Normal file
File diff suppressed because it is too large
Load Diff
407
OpenTelemetryPlan/07-observability-backends.md
Normal file
407
OpenTelemetryPlan/07-observability-backends.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# Observability Backend Recommendations
|
||||
|
||||
> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md)
|
||||
> **Related**: [Implementation Phases](./06-implementation-phases.md) | [Appendix](./08-appendix.md)
|
||||
|
||||
---
|
||||
|
||||
## 7.1 Development/Testing Backends
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
| Backend | Pros | Cons | Use Case |
|
||||
| ---------- | ----------------------------------- | ---------------------- | ------------------- |
|
||||
| **Tempo** | Cost-effective, Grafana integration | Requires Grafana stack | Local dev, CI, Prod |
|
||||
| **Zipkin** | Simple, lightweight | Basic features | Quick prototyping |
|
||||
|
||||
### Quick Start with Tempo
|
||||
|
||||
```bash
|
||||
# Start Tempo with OTLP support
|
||||
docker run -d --name tempo \
|
||||
-p 3200:3200 \
|
||||
-p 4317:4317 \
|
||||
-p 4318:4318 \
|
||||
grafana/tempo:2.6.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7.2 Production Backends
|
||||
|
||||
> **APM** = Application Performance Monitoring
|
||||
|
||||
| Backend | Pros | Cons | Use Case |
|
||||
| ----------------- | ----------------------------------------- | ---------------------- | --------------------------- |
|
||||
| **Grafana Tempo** | Cost-effective, Grafana integration | Requires Grafana stack | Most production deployments |
|
||||
| **Elastic APM** | Full observability stack, log correlation | Resource intensive | Existing Elastic users |
|
||||
| **Honeycomb** | Excellent query, high cardinality | SaaS cost | Deep debugging needs |
|
||||
| **Datadog APM** | Full platform, easy setup | SaaS cost | Enterprise with budget |
|
||||
|
||||
### Backend Selection Flowchart
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
start[Select Backend] --> budget{Budget<br/>Constraints?}
|
||||
|
||||
budget -->|Yes| oss[Open Source]
|
||||
budget -->|No| saas{Prefer<br/>SaaS?}
|
||||
|
||||
oss --> existing{Existing<br/>Stack?}
|
||||
existing -->|Grafana| tempo[Grafana Tempo]
|
||||
existing -->|Elastic| elastic[Elastic APM]
|
||||
existing -->|None| tempo
|
||||
|
||||
saas -->|Yes| enterprise{Enterprise<br/>Support?}
|
||||
saas -->|No| oss
|
||||
|
||||
enterprise -->|Yes| datadog[Datadog APM]
|
||||
enterprise -->|No| honeycomb[Honeycomb]
|
||||
|
||||
tempo --> final[Configure Collector]
|
||||
elastic --> final
|
||||
honeycomb --> final
|
||||
datadog --> final
|
||||
|
||||
style start fill:#0f172a,stroke:#020617,color:#fff
|
||||
style budget fill:#334155,stroke:#1e293b,color:#fff
|
||||
style oss fill:#1e293b,stroke:#0f172a,color:#fff
|
||||
style existing fill:#334155,stroke:#1e293b,color:#fff
|
||||
style saas fill:#334155,stroke:#1e293b,color:#fff
|
||||
style enterprise fill:#334155,stroke:#1e293b,color:#fff
|
||||
style final fill:#0f172a,stroke:#020617,color:#fff
|
||||
style tempo fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style elastic fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style honeycomb fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style datadog fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Budget Constraints? (Yes)**: Leads to open-source options. If you already run Grafana or Elastic, pick the matching backend; otherwise default to Grafana Tempo.
|
||||
- **Budget Constraints? (No) → Prefer SaaS?**: If you want a managed service, choose between Datadog (enterprise support) and Honeycomb (developer-focused). If not, fall back to open-source.
|
||||
- **Terminal nodes (Tempo / Elastic / Honeycomb / Datadog)**: Each represents a concrete backend choice, all of which feed into the same final step.
|
||||
- **Configure Collector**: Regardless of backend, you always finish by configuring the OTel Collector to export to your chosen destination.
|
||||
|
||||
---
|
||||
|
||||
## 7.3 Recommended Production Architecture
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring | **HA** = High Availability
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph validators["Validator Nodes"]
|
||||
v1[xrpld<br/>Validator 1]
|
||||
v2[xrpld<br/>Validator 2]
|
||||
end
|
||||
|
||||
subgraph stock["Stock Nodes"]
|
||||
s1[xrpld<br/>Stock 1]
|
||||
s2[xrpld<br/>Stock 2]
|
||||
end
|
||||
|
||||
subgraph collector["OTel Collector Cluster"]
|
||||
c1[Collector<br/>DC1]
|
||||
c2[Collector<br/>DC2]
|
||||
end
|
||||
|
||||
subgraph backends["Storage Backends"]
|
||||
tempo[(Grafana<br/>Tempo)]
|
||||
elastic[(Elastic<br/>APM)]
|
||||
archive[(S3/GCS<br/>Archive)]
|
||||
end
|
||||
|
||||
subgraph ui["Visualization"]
|
||||
grafana[Grafana<br/>Dashboards]
|
||||
end
|
||||
|
||||
v1 -->|OTLP| c1
|
||||
v2 -->|OTLP| c1
|
||||
s1 -->|OTLP| c2
|
||||
s2 -->|OTLP| c2
|
||||
|
||||
c1 --> tempo
|
||||
c1 --> elastic
|
||||
c2 --> tempo
|
||||
c2 --> archive
|
||||
|
||||
tempo --> grafana
|
||||
elastic --> grafana
|
||||
|
||||
%% Note: simplified single-collector-per-DC topology shown for clarity
|
||||
|
||||
style validators fill:#b71c1c,stroke:#7f1d1d,color:#ffffff
|
||||
style stock fill:#0d47a1,stroke:#082f6a,color:#ffffff
|
||||
style collector fill:#bf360c,stroke:#8c2809,color:#ffffff
|
||||
style backends fill:#1b5e20,stroke:#0d3d14,color:#ffffff
|
||||
style ui fill:#4a148c,stroke:#2e0d57,color:#ffffff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Validator / Stock Nodes**: All xrpld nodes emit trace data via OTLP. Validators and stock nodes are grouped separately because they may reside in different network zones.
|
||||
- **Collector Cluster (DC1, DC2)**: Regional collectors receive OTLP from nodes in their datacenter, apply processing (sampling, enrichment), and fan out to multiple backends. Enrichment includes deployment-tier tagging: each collector stamps `deployment.environment` and (as a fallback) `xrpl.network.type` so one Grafana stack can filter data from many collectors by tier.
|
||||
- **Storage Backends**: Tempo and Elastic provide queryable trace storage; S3/GCS Archive provides long-term cold storage for compliance or post-incident analysis.
|
||||
- **Grafana Dashboards**: The single visualization layer that queries both Tempo and Elastic, giving operators a unified view of all traces.
|
||||
- **Data flow direction**: Nodes → Collectors → Storage → Grafana. Each arrow represents a network hop; minimizing collector-to-backend hops reduces latency.
|
||||
|
||||
> **Note**: Production deployments should use multiple collector instances behind a load balancer for high availability. The diagram shows a simplified single-collector topology for clarity.
|
||||
|
||||
---
|
||||
|
||||
## 7.4 Architecture Considerations
|
||||
|
||||
### 7.4.1 Collector Placement
|
||||
|
||||
| Strategy | Description | Pros | Cons |
|
||||
| ------------- | -------------------- | ------------------------ | ----------------------- |
|
||||
| **Sidecar** | Collector per node | Isolation, simple config | Resource overhead |
|
||||
| **DaemonSet** | Collector per host | Shared resources | Complexity |
|
||||
| **Gateway** | Central collector(s) | Centralized processing | Single point of failure |
|
||||
|
||||
**Recommendation**: Use **Gateway** pattern with regional collectors for xrpld networks:
|
||||
|
||||
- One collector cluster per datacenter/region
|
||||
- Tail-based sampling at collector level
|
||||
- Multiple export destinations for redundancy
|
||||
|
||||
### 7.4.2 Sampling Strategy
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph head["Head Sampling (Node)"]
|
||||
hs[Node-level head sampling<br/>fixed at 100%<br/>not configurable]
|
||||
end
|
||||
|
||||
subgraph tail["Tail Sampling (Collector)"]
|
||||
ts1[Keep all errors]
|
||||
ts2[Keep slow >5s]
|
||||
ts3[Keep 10% rest]
|
||||
end
|
||||
|
||||
head --> tail
|
||||
|
||||
ts1 --> final[Final Traces]
|
||||
ts2 --> final
|
||||
ts3 --> final
|
||||
|
||||
style head fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style tail fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style hs fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style ts1 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style ts2 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style ts3 fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style final fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **Head Sampling (Node)**: xrpld pins head sampling at 100% (sample everything) and does not expose a configurable ratio. This is intentional: a per-node ratio would let different nodes make divergent keep/drop decisions for the same distributed trace, producing broken/partial traces. 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
|
||||
|
||||
A Tempo-backed dashboard (uid `xrpld-consensus-health`) with four panels, all driven by TraceQL:
|
||||
|
||||
- **Consensus Round Duration** (timeseries, ms): average `consensus.round` span duration per node instance, with yellow/red thresholds at 4s/5s.
|
||||
- **Phase Duration Breakdown** (barchart): average duration of `consensus.phase.*` spans grouped by span name.
|
||||
- **Proposers per Round** (stat): average of the `span.proposers` attribute on `consensus.round` spans.
|
||||
- **Recent Slow Rounds (>5s)** (table): `consensus.round` spans filtered to `duration > 5s`.
|
||||
|
||||
Each panel's TraceQL query is described inline in its bullet above.
|
||||
|
||||
### 7.6.2 Node Overview Dashboard
|
||||
|
||||
A Tempo-backed dashboard (uid `xrpld-node-overview`) with four panels:
|
||||
|
||||
- **Active Nodes** (stat): count of distinct `resource.service.instance.id` values seen for the `xrpld` service.
|
||||
- **Total Transactions (1h)** (stat): count of `tx.receive` spans.
|
||||
- **Error Rate** (gauge, percent): ratio of `status.code=error` spans to all spans, with yellow/red thresholds at 1%/5%.
|
||||
- **Service Map** (nodeGraph): Tempo-generated service dependency graph.
|
||||
|
||||
### 7.6.3 Alert Rules
|
||||
|
||||
Grafana provisions three TraceQL-based alert rules (group `xrpld-tracing-alerts`, evaluated every 1m) against the Tempo datasource:
|
||||
|
||||
- **Consensus Round Slow** (warning, `for: 5m`): fires when average `consensus.round` duration exceeds 5s.
|
||||
|
||||
```
|
||||
{resource.service.name="xrpld" && name="consensus.round"} | avg(duration) > 5s
|
||||
```
|
||||
|
||||
- **RPC Error Rate Spike** (critical, `for: 2m`): fires when the error rate across `rpc.command.*` spans exceeds 5%. Error _rate_ is a ratio, so it must divide the error-span rate by the total-span rate — a single TraceQL `rate()` returns spans/second, not a percentage, and would fire on traffic volume alone. This uses span metrics emitted by the collector's `spanmetrics` connector (Prometheus datasource), not a TraceQL query:
|
||||
|
||||
```
|
||||
sum(rate(traces_spanmetrics_calls_total{service_name="xrpld", span_name=~"rpc.command.*", status_code="STATUS_CODE_ERROR"}[5m]))
|
||||
/
|
||||
sum(rate(traces_spanmetrics_calls_total{service_name="xrpld", span_name=~"rpc.command.*"}[5m]))
|
||||
> 0.05
|
||||
```
|
||||
|
||||
- **Transaction Throughput Drop** (warning, `for: 10m`): fires when the `tx.receive` span rate falls below 10/s.
|
||||
|
||||
```
|
||||
{resource.service.name="xrpld" && name="tx.receive"} | rate() < 10
|
||||
```
|
||||
|
||||
> **Note**: The Consensus Round Slow and Transaction Throughput Drop rules use TraceQL aggregates (`avg(duration)`, `rate()`), which require Tempo 2.3+ with TraceQL metrics enabled. Verify aggregate query support in your Tempo version before provisioning. The RPC Error Rate Spike rule instead queries Prometheus span metrics (collector `spanmetrics` connector), so it needs that connector enabled in the collector pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 7.7 PerfLog and Insight Correlation
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
How to correlate OpenTelemetry traces with existing xrpld observability.
|
||||
|
||||
### 7.7.1 Correlation Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph xrpld["xrpld Node"]
|
||||
otel[OpenTelemetry<br/>Spans]
|
||||
perflog[PerfLog<br/>JSON Logs]
|
||||
insight[Beast Insight<br/>StatsD Metrics]
|
||||
end
|
||||
|
||||
subgraph collectors["Data Collection"]
|
||||
otelc[OTel Collector]
|
||||
promtail[Promtail/Fluentd]
|
||||
statsd[StatsD Exporter]
|
||||
end
|
||||
|
||||
subgraph storage["Storage"]
|
||||
tempo[(Tempo)]
|
||||
loki[(Loki)]
|
||||
prom[(Prometheus)]
|
||||
end
|
||||
|
||||
subgraph grafana["Grafana"]
|
||||
traces[Trace View]
|
||||
logs[Log View]
|
||||
metrics[Metrics View]
|
||||
corr[Correlation<br/>Panel]
|
||||
end
|
||||
|
||||
otel -->|OTLP| otelc --> tempo
|
||||
perflog -->|JSON| promtail --> loki
|
||||
insight -->|StatsD| statsd --> prom
|
||||
|
||||
tempo --> traces
|
||||
loki --> logs
|
||||
prom --> metrics
|
||||
|
||||
traces --> corr
|
||||
logs --> corr
|
||||
metrics --> corr
|
||||
|
||||
style xrpld fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style collectors fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style storage fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style grafana fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style otel fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style perflog fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style insight fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style otelc fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style promtail fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style statsd fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style tempo fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style loki fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style prom fill:#1b5e20,stroke:#0d3d14,color:#fff
|
||||
style traces fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style logs fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style metrics fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style corr fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
**Reading the diagram:**
|
||||
|
||||
- **xrpld Node (three sources)**: A single node emits three independent data streams -- OpenTelemetry spans, PerfLog JSON logs, and Beast Insight StatsD metrics.
|
||||
- **Data Collection layer**: Each stream has its own collector -- OTel Collector for spans, Promtail/Fluentd for logs, and a StatsD exporter for metrics. They operate independently.
|
||||
- **Storage layer (Tempo, Loki, Prometheus)**: Each data type lands in a purpose-built store optimized for its query patterns (trace search, log grep, metric aggregation).
|
||||
- **Grafana Correlation Panel**: The key integration point -- Grafana queries all three stores and links them via shared fields (`trace_id`, `tx_hash`, `ledger_seq`), enabling a single-pane debugging experience.
|
||||
|
||||
### 7.7.2 Correlation Fields
|
||||
|
||||
| Source | Field | Link To | Purpose |
|
||||
| ----------- | ------------------- | ------------- | -------------------------- |
|
||||
| **Trace** | `trace_id` | Logs | Find log entries for trace |
|
||||
| **Trace** | `tx_hash` | Logs, Metrics | Find TX-related data |
|
||||
| **Trace** | `ledger_seq` | Logs | Find ledger-related logs |
|
||||
| **PerfLog** | `trace_id` (new) | Traces | Jump to trace from log |
|
||||
| **PerfLog** | `ledger_seq` | Traces | Find consensus trace |
|
||||
| **Insight** | `exemplar.trace_id` | Traces | Jump from metric spike |
|
||||
|
||||
### 7.7.3 Example: Debugging a Slow Transaction
|
||||
|
||||
**Step 1: Find the trace**
|
||||
|
||||
```
|
||||
# In Grafana Explore with Tempo
|
||||
{resource.service.name="xrpld" && span.tx_hash="ABC123..."}
|
||||
```
|
||||
|
||||
**Step 2: Get the trace_id from the trace view**
|
||||
|
||||
```
|
||||
Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736
|
||||
```
|
||||
|
||||
**Step 3: Find related PerfLog entries**
|
||||
|
||||
```
|
||||
# In Grafana Explore with Loki
|
||||
{job="xrpld"} |= "4bf92f3577b34da6a3ce929d0e0e4736"
|
||||
```
|
||||
|
||||
**Step 4: Check Insight metrics for the time window**
|
||||
|
||||
```
|
||||
# In Grafana with Prometheus
|
||||
rate(xrpld_tx_applied_total[1m])
|
||||
@ timestamp_from_trace
|
||||
```
|
||||
|
||||
### 7.7.4 Unified Dashboard Example
|
||||
|
||||
A single dashboard (uid `xrpld-unified`) that ties traces, metrics, and logs together across the Tempo, Prometheus, and Loki datasources:
|
||||
|
||||
- **Transaction Latency (Traces)** (timeseries, Tempo): `histogram_over_time(duration)` of `tx.receive` spans.
|
||||
- **Transaction Rate (Metrics)** (timeseries, Prometheus): `rate(xrpld_tx_received_total[5m])` per instance, with a data link that opens the matching `tx.receive` traces in Tempo.
|
||||
- **Recent Logs** (logs, Loki): `{job="xrpld"} | json`.
|
||||
- **Trace Search** (table, Tempo): all `xrpld` traces, with per-row data links on `traceID` that jump to the trace in Tempo and to the correlated logs in Loki (`{job="xrpld"} |= "<traceID>"`).
|
||||
|
||||
The cross-datasource data links are what make this a single-pane debugging view; the correlation fields they rely on are listed in section 7.7.2.
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Implementation Phases](./06-implementation-phases.md)_ | _Next: [Appendix](./08-appendix.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
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 |
|
||||
| [05-configuration-reference.md](./05-configuration-reference.md) | xrpld config, CMake, Collector configs |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Timeline, tasks, risks, success metrics |
|
||||
| [07-observability-backends.md](./07-observability-backends.md) | Backend selection and architecture |
|
||||
| [08-appendix.md](./08-appendix.md) | Glossary, references, version history |
|
||||
| [secure-OTel.md](./secure-OTel.md) | Threat model and hardening (mTLS, peer validation) |
|
||||
| [09-data-collection-reference.md](./09-data-collection-reference.md) | Span/metric/dashboard inventory |
|
||||
|
||||
### Task Lists
|
||||
|
||||
| Document | Description |
|
||||
| -------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| [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 |
|
||||
| [Phase5_IntegrationTest_taskList.md](./Phase5_IntegrationTest_taskList.md) | Observability stack integration tests |
|
||||
| [Phase7_taskList.md](./Phase7_taskList.md) | Native OTel metrics migration |
|
||||
| [Phase8_taskList.md](./Phase8_taskList.md) | Log-trace correlation |
|
||||
|
||||
---
|
||||
|
||||
_Previous: [Observability Backends](./07-observability-backends.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_
|
||||
872
OpenTelemetryPlan/09-data-collection-reference.md
Normal file
872
OpenTelemetryPlan/09-data-collection-reference.md
Normal file
@@ -0,0 +1,872 @@
|
||||
# Observability Data Collection Reference
|
||||
|
||||
> **Audience**: Developers and operators. This is the single source of truth for all telemetry data collected by xrpld's observability stack.
|
||||
>
|
||||
> **Related docs**: [docs/telemetry-runbook.md](../docs/telemetry-runbook.md) (operator runbook with alerting and troubleshooting) | [03-implementation-strategy.md](./03-implementation-strategy.md) (code structure and performance optimization) | [04-code-samples.md](./04-code-samples.md) (C++ instrumentation examples)
|
||||
|
||||
## Data Flow Overview
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph xrpldNode["xrpld Node"]
|
||||
A["Trace Macros<br/>XRPL_TRACE_SPAN<br/>(OTLP/HTTP exporter)"]
|
||||
B["beast::insight<br/>OTel native metrics<br/>(OTLP/HTTP exporter)"]
|
||||
end
|
||||
|
||||
subgraph collector["OTel Collector :4317 / :4318"]
|
||||
direction TB
|
||||
R1["OTLP Receiver<br/>:4317 gRPC | :4318 HTTP<br/>(traces + metrics)"]
|
||||
BP["Batch Processor<br/>timeout 1s, batch 100"]
|
||||
SM["SpanMetrics Connector<br/>derives RED metrics<br/>from trace spans"]
|
||||
|
||||
R1 --> BP
|
||||
BP --> SM
|
||||
end
|
||||
|
||||
subgraph backends["Trace Backend"]
|
||||
D["Grafana Tempo :3200<br/>TraceQL search &<br/>S3/GCS long-term storage"]
|
||||
end
|
||||
|
||||
subgraph metrics["Metrics Stack"]
|
||||
E["Prometheus :9090<br/>scrapes :8889<br/>span-derived + system metrics"]
|
||||
end
|
||||
|
||||
subgraph viz["Visualization"]
|
||||
F["Grafana :3000<br/>10 dashboards"]
|
||||
end
|
||||
|
||||
A -->|"OTLP/HTTP :4318<br/>(traces + attributes)"| R1
|
||||
B -->|"OTLP/HTTP :4318<br/>(gauges, counters, histograms)"| R1
|
||||
|
||||
BP -->|"OTLP/gRPC :4317"| D
|
||||
|
||||
SM -->|"span_calls_total<br/>span_duration_ms<br/>(6 dimension labels)"| E
|
||||
R1 -->|"* gauges<br/>* counters<br/>* histograms"| E
|
||||
|
||||
E -->|"Prometheus<br/>data source"| F
|
||||
D -->|"Tempo<br/>data source"| F
|
||||
|
||||
style A fill:#4a90d9,color:#fff,stroke:#2a6db5
|
||||
style B fill:#4a90d9,color:#fff,stroke:#2a6db5
|
||||
style R1 fill:#5cb85c,color:#fff,stroke:#3d8b3d
|
||||
style BP fill:#449d44,color:#fff,stroke:#2d6e2d
|
||||
style SM fill:#449d44,color:#fff,stroke:#2d6e2d
|
||||
style D fill:#f0ad4e,color:#000,stroke:#c78c2e
|
||||
style E fill:#f0ad4e,color:#000,stroke:#c78c2e
|
||||
style F fill:#5bc0de,color:#000,stroke:#3aa8c1
|
||||
style xrpldNode fill:#1a2633,color:#ccc,stroke:#4a90d9
|
||||
style collector fill:#1a3320,color:#ccc,stroke:#5cb85c
|
||||
style backends fill:#332a1a,color:#ccc,stroke:#f0ad4e
|
||||
style metrics fill:#332a1a,color:#ccc,stroke:#f0ad4e
|
||||
style viz fill:#1a2d33,color:#ccc,stroke:#5bc0de
|
||||
```
|
||||
|
||||
There are two independent telemetry pipelines entering a single **OTel Collector** via the same OTLP receiver:
|
||||
|
||||
1. **OpenTelemetry Traces** — Distributed spans with attributes, exported via OTLP/HTTP (:4318) to the collector's **OTLP Receiver**. The **Batch Processor** groups spans (1s timeout, batch size 100) before forwarding to trace backends. The **SpanMetrics Connector** derives RED metrics (rate, errors, duration) from every span and feeds them into the metrics pipeline.
|
||||
2. **beast::insight OTel Metrics** — System-level gauges, counters, and histograms exported natively via OTLP/HTTP (:4318) to the same **OTLP Receiver**. These are batched and exported to Prometheus alongside span-derived metrics. The StatsD UDP transport has been replaced by native OTLP; `server=statsd` remains available as a fallback.
|
||||
|
||||
**Trace backend** — The collector exports traces via OTLP/gRPC to:
|
||||
|
||||
- **Grafana Tempo** — Preferred trace backend. Supports TraceQL queries at `:3200`, S3/GCS object storage for cost-effective long-term trace retention, and integrates natively with Grafana.
|
||||
|
||||
> **Further reading**: [00-tracing-fundamentals.md](./00-tracing-fundamentals.md) for core OpenTelemetry concepts (traces, spans, context propagation, sampling). [07-observability-backends.md](./07-observability-backends.md) for production backend selection, collector placement, and sampling strategies.
|
||||
|
||||
---
|
||||
|
||||
## 1. OpenTelemetry Spans
|
||||
|
||||
### 1.1 Complete Span Inventory (~36 spans)
|
||||
|
||||
> **See also**: [02-design-decisions.md §2.3](./02-design-decisions.md#23-span-naming-conventions) for naming conventions and the full span catalog with rationale. [04-code-samples.md §4.6](./04-code-samples.md#46-span-flow-visualization) for span flow diagrams.
|
||||
|
||||
> **Span names vs. attribute keys**: span names use dotted `subsystem.operation`
|
||||
> form (e.g. `rpc.http_request`). Span _attribute_ keys use the bare/underscore
|
||||
> form from the 2026-05-13 naming redesign (e.g. `tx_hash`, not `xrpl.tx.hash`).
|
||||
> The dotted `xrpl.*` form is reserved for OTel **resource** attributes set once
|
||||
> at startup. See §1.2 for the full attribute inventory.
|
||||
|
||||
#### RPC Spans
|
||||
|
||||
Controlled by `trace_rpc=1` in `[telemetry]` config.
|
||||
|
||||
| Span Name | Parent | Source File | Description |
|
||||
| -------------------- | ------------------ | ----------------- | ------------------------------------------------------------------------ |
|
||||
| `rpc.http_request` | — | ServerHandler.cpp | Top-level HTTP JSON-RPC request entry point |
|
||||
| `rpc.ws_message` | — | ServerHandler.cpp | WebSocket message handling (one per inbound frame) |
|
||||
| `rpc.ws_upgrade` | — | ServerHandler.cpp | WebSocket upgrade handshake (records handshake failures) |
|
||||
| `rpc.process` | `rpc.http_request` | ServerHandler.cpp | RPC processing pipeline (single or batch request) |
|
||||
| `rpc.command.<name>` | `rpc.process` | RPCHandler.cpp | Per-command span (e.g., `rpc.command.server_info`, `rpc.command.ledger`) |
|
||||
|
||||
**Where to find**: Tempo → TraceQL: `{resource.service.name="xrpld" && name=~"rpc.http_request|rpc.command.*"}`
|
||||
|
||||
**Grafana dashboard**: _RPC Performance_ (`rpc-performance`)
|
||||
|
||||
#### gRPC Spans
|
||||
|
||||
Controlled by `trace_rpc=1` in `[telemetry]` config.
|
||||
|
||||
| Span Name | Parent | Source File | Description |
|
||||
| ------------------- | ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `grpc.<MethodName>` | — | GRPCServer.cpp | One flat span per gRPC method (e.g., `grpc.GetLedger`, `grpc.GetLedgerData`, `grpc.GetLedgerDiff`, `grpc.GetLedgerEntry`) |
|
||||
|
||||
The method name is embedded in the span name (formed at the call site as
|
||||
`grpc.<MethodName>`), so dashboards break out per-method latency and error
|
||||
rates without TraceQL attribute filters.
|
||||
|
||||
**Where to find**: Tempo → TraceQL: `{resource.service.name="xrpld" && name=~"grpc.*"}`
|
||||
|
||||
**Grafana dashboard**: _RPC Performance_ (`rpc-performance`)
|
||||
|
||||
#### Transaction Spans
|
||||
|
||||
Controlled by `trace_transactions=1` in `[telemetry]` config.
|
||||
|
||||
| Span Name | Parent | Source File | Description |
|
||||
| --------------- | -------------- | --------------- | ----------------------------------------------------------------- |
|
||||
| `tx.process` | — | NetworkOPs.cpp | Transaction submission entry point (local or peer-relayed) |
|
||||
| `tx.receive` | — | PeerImp.cpp | Raw transaction received from peer overlay (before deduplication) |
|
||||
| `tx.apply` | `ledger.build` | BuildLedger.cpp | Transaction set applied to new ledger during consensus |
|
||||
| `tx.preflight` | — | applySteps.cpp | Stateless checks stage (`stage=preflight`) |
|
||||
| `tx.preclaim` | — | applySteps.cpp | Ledger-aware checks stage before fee claim (`stage=preclaim`) |
|
||||
| `tx.transactor` | — | Transactor.cpp | Apply stage — the transactor runs (`stage=apply`) |
|
||||
|
||||
The three apply-pipeline spans share a deterministic `trace_id` derived from
|
||||
`txID[0:16]`, so preflight, preclaim, and transactor for one transaction group
|
||||
under a single trace even though they run sequentially and often on different
|
||||
threads. A transaction that hard-fails preflight or preclaim never reaches the
|
||||
later spans — the `stage` attribute identifies where it stopped.
|
||||
|
||||
**Where to find**: Tempo → TraceQL: `{resource.service.name="xrpld" && name=~"tx.process|tx.receive"}`
|
||||
or, for the apply pipeline: `{resource.service.name="xrpld" && name=~"tx.preflight|tx.preclaim|tx.transactor"}`
|
||||
|
||||
**Grafana dashboard**: _Transaction Overview_ (`transaction-overview`)
|
||||
|
||||
#### Transaction Queue (TxQ) Spans
|
||||
|
||||
Controlled by `trace_transactions=1` in `[telemetry]` config.
|
||||
|
||||
| Span Name | Parent | Source File | Description |
|
||||
| ------------------ | ------------- | ----------- | --------------------------------------------------- |
|
||||
| `txq.enqueue` | `tx.process` | TxQ.cpp | Enqueue decision when a tx is submitted |
|
||||
| `txq.apply_direct` | `txq.enqueue` | TxQ.cpp | Direct apply attempt that bypasses the queue |
|
||||
| `txq.batch_clear` | `txq.enqueue` | TxQ.cpp | Batch clear of an account's queued txs |
|
||||
| `txq.accept` | — | TxQ.cpp | Ledger-close accept loop (drains the queue) |
|
||||
| `txq.accept_tx` | `txq.accept` | TxQ.cpp | Per-queued-transaction apply inside the accept loop |
|
||||
| `txq.cleanup` | — | TxQ.cpp | Post-close cleanup of expired queue entries |
|
||||
|
||||
**Where to find**: Tempo → TraceQL: `{resource.service.name="xrpld" && name=~"txq.*"}`
|
||||
|
||||
**Grafana dashboard**: _Transaction Overview_ (`transaction-overview`)
|
||||
|
||||
#### Consensus Spans
|
||||
|
||||
Controlled by `trace_consensus=1` in `[telemetry]` config.
|
||||
|
||||
| Span Name | Parent | Source File | Description |
|
||||
| ------------------------------ | ------------------ | ---------------- | ------------------------------------------------------------------- |
|
||||
| `consensus.round` | — (root) | RCLConsensus.cpp | Root span for one consensus round (deterministic trace per round) |
|
||||
| `consensus.phase.open` | `consensus.round` | Consensus.h | Open phase — collecting transactions before close |
|
||||
| `consensus.proposal.send` | `consensus.round` | RCLConsensus.cpp | Node broadcasts its transaction set proposal |
|
||||
| `consensus.ledger_close` | `consensus.round` | RCLConsensus.cpp | Ledger close event triggered by consensus |
|
||||
| `consensus.establish` | `consensus.round` | Consensus.h | Establish phase — converging on the transaction set |
|
||||
| `consensus.update_positions` | `consensus.round` | Consensus.h | Position update with per-dispute vote details |
|
||||
| `consensus.check` | `consensus.round` | Consensus.h | Consensus threshold check (agree/disagree tally) |
|
||||
| `consensus.accept` | `consensus.round` | RCLConsensus.cpp | Consensus accepts a ledger (round complete) |
|
||||
| `consensus.accept.apply` | `consensus.accept` | RCLConsensus.cpp | Ledger application with close-time details (jtACCEPT thread) |
|
||||
| `consensus.validation.send` | `consensus.round` | RCLConsensus.cpp | Validation message sent after ledger accepted (follows-from link) |
|
||||
| `consensus.mode_change` | `consensus.round` | RCLConsensus.cpp | Operating-mode transition during the round |
|
||||
| `consensus.proposal.receive` | (context) | PeerImp.cpp | Proposal received from a peer (context-propagated into the round) |
|
||||
| `consensus.validation.receive` | (context) | PeerImp.cpp | Validation received from a peer (context-propagated into the round) |
|
||||
|
||||
The `.receive` spans are created per-message in the overlay and joined to the
|
||||
round trace via context propagation rather than direct parenting. The
|
||||
`consensus.validation.send` span uses a follows-from link off the round.
|
||||
|
||||
**Where to find**: Tempo → TraceQL: `{resource.service.name="xrpld" && name=~"consensus.*"}`
|
||||
|
||||
**Grafana dashboard**: _Consensus Health_ (`consensus-health`)
|
||||
|
||||
#### Ledger Spans
|
||||
|
||||
Controlled by `trace_ledger=1` in `[telemetry]` config.
|
||||
|
||||
| Span Name | Parent | Source File | Description |
|
||||
| ----------------- | ------ | ---------------- | ---------------------------------------------- |
|
||||
| `ledger.build` | — | BuildLedger.cpp | Build new ledger from accepted transaction set |
|
||||
| `ledger.validate` | — | LedgerMaster.cpp | Ledger promoted to validated status |
|
||||
| `ledger.store` | — | LedgerMaster.cpp | Ledger stored to database/history |
|
||||
|
||||
**Where to find**: Tempo → TraceQL: `{resource.service.name="xrpld" && name=~"ledger.*"}`
|
||||
|
||||
**Grafana dashboard**: _Ledger Operations_ (`ledger-operations`)
|
||||
|
||||
#### Peer Spans
|
||||
|
||||
Controlled by `trace_peer` in `[telemetry]` config. **Enabled by default** (high volume).
|
||||
|
||||
| Span Name | Parent | Source File | Description |
|
||||
| ------------------------- | ------ | ----------- | ------------------------------------- |
|
||||
| `peer.proposal.receive` | — | PeerImp.cpp | Consensus proposal received from peer |
|
||||
| `peer.validation.receive` | — | PeerImp.cpp | Validation message received from peer |
|
||||
|
||||
**Where to find**: Tempo → TraceQL: `{resource.service.name="xrpld" && name=~"peer.*"}`
|
||||
|
||||
**Grafana dashboard**: _Peer Network_ (`peer-network`)
|
||||
|
||||
#### PathFind Spans
|
||||
|
||||
Controlled by `trace_rpc=1` in `[telemetry]` config.
|
||||
|
||||
| Span Name | Parent | Source File | Description |
|
||||
| --------------------- | ------------------ | --------------- | ---------------------------------------------------------- |
|
||||
| `pathfind.request` | `rpc.command.*` | PathRequest.cpp | `path_find` / `ripple_path_find` RPC entry |
|
||||
| `pathfind.compute` | `pathfind.request` | PathRequest.cpp | Path computation for one request (`PathRequest::doUpdate`) |
|
||||
| `pathfind.discover` | `pathfind.compute` | Pathfinder.cpp | Graph exploration (one per RPC call) |
|
||||
| `pathfind.update_all` | — | PathRequest.cpp | Async recomputation of all active requests at ledger close |
|
||||
|
||||
**Where to find**: Tempo → TraceQL: `{resource.service.name="xrpld" && name=~"pathfind.*"}`
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Complete Attribute Inventory (bare/underscore keys)
|
||||
|
||||
> **See also**: [02-design-decisions.md §2.4.2](./02-design-decisions.md#242-span-attributes-by-category) for attribute design rationale and privacy considerations.
|
||||
|
||||
Every span can carry key-value attributes that provide context for filtering and
|
||||
aggregation. Per the 2026-05-13 naming redesign, span-attribute keys use the
|
||||
**bare** field name (the span name already carries the domain), or the
|
||||
`<domain>_<field>` underscore form where a bare name would collide (e.g.
|
||||
`rpc_status`, `grpc_status`, `tx_status`, `txq_status`).
|
||||
|
||||
> **Dotted keys are resource attributes, never span attributes:**
|
||||
>
|
||||
> - `xrpl.network.id` and `xrpl.network.type` are **resource** attributes set
|
||||
> once at startup on the OTel resource — not span attributes. They appear on
|
||||
> every span's resource scope, queried as `{resource.xrpl.network.id=...}`.
|
||||
> - The ledger hash uses the bare `ledger_hash` key on every span that records
|
||||
> it (both `consensus.validation.send` and `peer.validation.receive`) — there
|
||||
> is no dotted span attribute.
|
||||
|
||||
#### RPC Attributes
|
||||
|
||||
| Attribute | Type | Set On | Description |
|
||||
| ---------------------- | ------- | --------------------------------- | ------------------------------------------------ |
|
||||
| `command` | string | `rpc.command.*`, `rpc.ws_message` | RPC command name (e.g., `server_info`, `ledger`) |
|
||||
| `version` | int64 | `rpc.command.*` | API version number |
|
||||
| `rpc_role` | string | `rpc.command.*` | Caller role: `"admin"` or `"user"` |
|
||||
| `rpc_status` | string | `rpc.command.*` | Result: `"success"` or `"error"` |
|
||||
| `request_payload_size` | int64 | `rpc.http_request` | Bytes of inbound request payload |
|
||||
| `is_batch` | boolean | `rpc.process` | `true` if the request is a JSON-RPC batch |
|
||||
| `batch_size` | int64 | `rpc.process` | Number of sub-requests in a batch |
|
||||
| `load_type` | string | `rpc.command.*` | Resource cost category after execution |
|
||||
|
||||
**Tempo query**: `{span.command="server_info"}` to find all `server_info` calls.
|
||||
|
||||
**Prometheus label**: `command` (used as a SpanMetrics dimension).
|
||||
|
||||
#### gRPC Attributes
|
||||
|
||||
| Attribute | Type | Set On | Description |
|
||||
| ------------- | ------ | ------------------- | ------------------------------------ |
|
||||
| `method` | string | `grpc.<MethodName>` | gRPC method name (e.g., `GetLedger`) |
|
||||
| `grpc_role` | string | `grpc.<MethodName>` | Caller role: `"admin"` or `"user"` |
|
||||
| `grpc_status` | string | `grpc.<MethodName>` | Result: `"success"` or `"error"` |
|
||||
|
||||
**Tempo query**: `{span.method="GetLedger"}` or `{name="grpc.GetLedger"}`.
|
||||
|
||||
**Prometheus labels**: `method`, `grpc_role`, `grpc_status` (SpanMetrics dimensions).
|
||||
|
||||
#### Transaction Attributes
|
||||
|
||||
| Attribute | Type | Set On | Description |
|
||||
| -------------- | ------- | ------------------------------------------------------------ | --------------------------------------------------------------------- |
|
||||
| `tx_hash` | string | `tx.process`, `tx.receive` | Transaction hash (hex-encoded) |
|
||||
| `local` | boolean | `tx.process` | `true` if locally submitted, `false` if peer-relayed |
|
||||
| `path` | string | `tx.process` | Submission path: `"sync"` or `"async"` |
|
||||
| `tx_type` | string | `tx.process`, `tx.preflight`, `tx.preclaim`, `tx.transactor` | Transaction type name (e.g., `Payment`) |
|
||||
| `fee` | int64 | `tx.process` | Transaction fee in drops |
|
||||
| `sequence` | int64 | `tx.process` | Transaction sequence number |
|
||||
| `suppressed` | boolean | `tx.receive` | `true` if transaction was suppressed (duplicate) |
|
||||
| `tx_status` | string | `tx.receive` | Transaction status (e.g., `"known_bad"`) |
|
||||
| `peer_id` | int64 | `tx.receive` | Peer identifier (also set on peer spans) |
|
||||
| `peer_version` | string | `tx.receive` | Peer protocol version string |
|
||||
| `stage` | string | `tx.preflight`, `tx.preclaim`, `tx.transactor` | Apply-pipeline stage: `preflight`, `preclaim`, or `apply` |
|
||||
| `ter_result` | string | `tx.preflight`, `tx.preclaim`, `tx.transactor` | Engine result token for that stage (e.g., `tesSUCCESS`, `terPRE_SEQ`) |
|
||||
| `applied` | boolean | `tx.transactor` | `true` if the transaction was applied to the ledger |
|
||||
|
||||
**Tempo query**: `{span.tx_hash="<hash>"}` to trace a specific transaction across nodes.
|
||||
|
||||
**Prometheus labels**: `local`, `suppressed`, `tx_type`, `ter_result`, `stage` (SpanMetrics dimensions).
|
||||
|
||||
#### Transaction Queue (TxQ) Attributes
|
||||
|
||||
| Attribute | Type | Set On | Description |
|
||||
| -------------------- | ------- | ------------------------------ | ----------------------------------------------------------- |
|
||||
| `tx_hash` | string | `txq.enqueue`, `txq.accept_tx` | Transaction hash |
|
||||
| `tx_type` | string | `txq.enqueue` | Transaction type name |
|
||||
| `txq_status` | string | `txq.enqueue`, `txq.accept_tx` | Queue outcome (e.g. `queued`, `applied_direct`, `rejected`) |
|
||||
| `fee_level_paid` | int64 | `txq.enqueue` | Fee level paid by the queued tx |
|
||||
| `required_fee_level` | int64 | `txq.enqueue` | Minimum fee level for inclusion |
|
||||
| `num_cleared` | int64 | `txq.batch_clear` | Entries cleared in a batch |
|
||||
| `queue_size` | int64 | `txq.accept` | Current TxQ depth |
|
||||
| `ledger_changed` | boolean | `txq.accept` | Whether the ledger changed since last attempt |
|
||||
| `ter_code` | int64 | `txq.accept_tx` | Transaction engine result code |
|
||||
| `retries_remaining` | int64 | `txq.accept_tx` | Retries left before discard |
|
||||
| `ledger_seq` | int64 | `txq.cleanup` | Ledger sequence number |
|
||||
| `expired_count` | int64 | `txq.cleanup` | Number of expired entries cleared |
|
||||
|
||||
**Prometheus label**: `txq_status` (SpanMetrics dimension).
|
||||
|
||||
#### Consensus Attributes
|
||||
|
||||
| Attribute | Type | Set On | Description |
|
||||
| -------------------------- | ------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| `consensus_ledger_id` | string | `consensus.round` | Previous-ledger id anchoring the round |
|
||||
| `ledger_seq` | int64 | `consensus.round`, `consensus.ledger_close`, `consensus.accept.apply`, `consensus.validation.send` | Ledger sequence number |
|
||||
| `consensus_mode` | string | `consensus.round`, `consensus.ledger_close` | Node mode: `"Proposing"`, `"Observing"`, `"Wrong"`, etc. |
|
||||
| `consensus_round_id` | int64 | `consensus.round` | Round identifier |
|
||||
| `consensus_phase` | string | `consensus.round` | Current phase name (updated on each transition) |
|
||||
| `trace_strategy` | string | `consensus.round` | Trace-id strategy (`deterministic` / `random`) |
|
||||
| `previous_ledger_seq` | int64 | `consensus.round` | Sequence of the previous ledger |
|
||||
| `previous_proposers` | int64 | `consensus.round` | Proposer count in the previous round |
|
||||
| `previous_round_time_ms` | int64 | `consensus.round` | Duration of the previous round |
|
||||
| `consensus_round` | int64 | `consensus.proposal.send` | Proposal sequence number for the broadcast proposal |
|
||||
| `is_bow_out` | boolean | `consensus.proposal.send` | Whether the proposal is a bow-out (resigning the round) |
|
||||
| `tx_count_open` | int64 | `consensus.ledger_close` | Transactions in the open ledger at close |
|
||||
| `close_time_resolution_ms` | int64 | `consensus.ledger_close` | Close-time rounding granularity |
|
||||
| `converge_percent` | int64 | `consensus.establish`, `consensus.update_positions` | Convergence percentage |
|
||||
| `establish_count` | int64 | `consensus.establish` | Establish-phase iteration count |
|
||||
| `proposers` | int64 | `consensus.establish`, `consensus.update_positions`, `consensus.accept` | Number of proposers |
|
||||
| `disputes_count` | int64 | `consensus.establish`, `consensus.update_positions` | Number of disputed transactions |
|
||||
| `tx_id` | string | `consensus.update_positions` | Disputed transaction id (per-dispute event) |
|
||||
| `dispute_our_vote` | boolean | `consensus.update_positions` | Our vote on the disputed tx |
|
||||
| `dispute_yays` | int64 | `consensus.update_positions` | Yes votes on the disputed tx |
|
||||
| `dispute_nays` | int64 | `consensus.update_positions` | No votes on the disputed tx |
|
||||
| `agree_count` | int64 | `consensus.check` | Agreeing proposer count |
|
||||
| `disagree_count` | int64 | `consensus.check` | Disagreeing proposer count |
|
||||
| `threshold_percent` | int64 | `consensus.check` | Agreement threshold percentage |
|
||||
| `consensus_result` | string | `consensus.check` | Check outcome |
|
||||
| `quorum` | int64 | `consensus.check`, `consensus.accept` | Quorum required |
|
||||
| `round_time_ms` | int64 | `consensus.accept`, `consensus.accept.apply` | Total consensus round duration in milliseconds |
|
||||
| `consensus_state` | string | `consensus.accept.apply` | Consensus outcome: `"finished"` or `"moved_on"` |
|
||||
| `close_time` | int64 | `consensus.accept.apply` | Agreed-upon ledger close time (epoch seconds) |
|
||||
| `close_time_correct` | boolean | `consensus.accept.apply` | Whether validators agreed on close time |
|
||||
| `close_resolution_ms` | int64 | `consensus.accept.apply` | Close-time rounding granularity in milliseconds |
|
||||
| `proposing` | boolean | `consensus.accept.apply`, `consensus.validation.send` | Whether this node was a proposer |
|
||||
| `parent_close_time` | int64 | `consensus.accept.apply` | Parent ledger close time |
|
||||
| `close_time_self` | int64 | `consensus.accept.apply` | This node's close-time vote |
|
||||
| `close_time_vote_bins` | string | `consensus.accept.apply` | Distribution of close-time votes |
|
||||
| `resolution_direction` | string | `consensus.accept.apply` | Whether close resolution increased/decreased/unchanged |
|
||||
| `tx_count` | int64 | `consensus.accept.apply` | Transactions in the accepted set |
|
||||
| `ledger_hash` | string | `consensus.validation.send` | Full hash of the validated ledger (shared with peer) |
|
||||
| `full_validation` | boolean | `consensus.validation.send` | Whether this is a full validation |
|
||||
| `validation_sign_time` | int64 | `consensus.validation.send` | Validation signing time |
|
||||
| `mode_old` | string | `consensus.mode_change` | Operating mode before the transition |
|
||||
| `mode_new` | string | `consensus.mode_change` | Operating mode after the transition |
|
||||
|
||||
**Tempo query**: `{span.consensus_mode="Proposing"}` to find rounds where the node was proposing.
|
||||
|
||||
**Prometheus labels**: `consensus_mode`, `consensus_state`, `consensus_phase`, `consensus_result`, `consensus_stalled`, `mode_new`, `close_time_correct` (SpanMetrics dimensions).
|
||||
|
||||
#### Ledger Attributes
|
||||
|
||||
| Attribute | Type | Set On | Description |
|
||||
| --------------------- | ------- | ------------------------------------------------- | ------------------------------------------------ |
|
||||
| `ledger_seq` | int64 | `ledger.build`, `ledger.validate`, `ledger.store` | Ledger sequence number |
|
||||
| `close_time` | int64 | `ledger.build` | Ledger close time (epoch seconds) |
|
||||
| `close_time_correct` | boolean | `ledger.build` | Whether close time was agreed upon by validators |
|
||||
| `close_resolution_ms` | int64 | `ledger.build` | Close time rounding granularity in milliseconds |
|
||||
| `tx_count` | int64 | `tx.apply` | Transactions applied to the ledger |
|
||||
| `tx_failed` | int64 | `tx.apply` | Failed transactions in the apply set |
|
||||
| `validations` | int64 | `ledger.validate` | Number of validations received for this ledger |
|
||||
|
||||
The apply-step span `tx.apply` (child of `ledger.build`) carries `tx_count`/`tx_failed`;
|
||||
the parent `ledger.build` carries `ledger_seq` and the close-time attributes.
|
||||
|
||||
**Tempo query**: `{span.ledger_seq=12345}` to find all spans for a specific ledger.
|
||||
|
||||
#### Peer Attributes
|
||||
|
||||
| Attribute | Type | Set On | Description |
|
||||
| -------------------- | ------- | ---------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `peer_id` | int64 | `tx.receive`, `peer.proposal.receive`, `peer.validation.receive` | Peer identifier |
|
||||
| `proposal_trusted` | boolean | `peer.proposal.receive` | Whether the proposal came from a trusted validator |
|
||||
| `validation_trusted` | boolean | `peer.validation.receive` | Whether the validation came from a trusted validator |
|
||||
| `full_validation` | boolean | `peer.validation.receive` | Whether the validation is a full validation |
|
||||
| `ledger_hash` | string | `peer.validation.receive` | Validated ledger hash (shared with consensus spans) |
|
||||
|
||||
**Prometheus labels**: `proposal_trusted`, `validation_trusted` (SpanMetrics dimensions).
|
||||
|
||||
#### PathFind Attributes
|
||||
|
||||
| Attribute | Type | Set On | Description |
|
||||
| ------------------------- | ------- | --------------------- | ---------------------------------------- |
|
||||
| `pathfind_source_account` | string | `pathfind.request` | Originating account for the path search |
|
||||
| `pathfind_dest_account` | string | `pathfind.request` | Destination account |
|
||||
| `pathfind_fast` | boolean | `pathfind.compute` | Whether fast pathfinding mode is enabled |
|
||||
| `pathfind_search_level` | int64 | `pathfind.discover` | Depth of graph exploration |
|
||||
| `pathfind_num_paths` | int64 | `pathfind.discover` | Total paths produced |
|
||||
| `pathfind_ledger_index` | int64 | `pathfind.update_all` | Target ledger index |
|
||||
| `pathfind_num_requests` | int64 | `pathfind.update_all` | Active requests recomputed |
|
||||
|
||||
---
|
||||
|
||||
### 1.3 SpanMetrics — Derived Prometheus Metrics
|
||||
|
||||
> **See also**: [01-architecture-analysis.md](./01-architecture-analysis.md) §1.8.2 for how span-derived metrics map to operational insights.
|
||||
|
||||
The OTel Collector's SpanMetrics connector automatically generates RED (Rate, Errors, Duration) metrics from every span. No custom metrics code in xrpld is needed.
|
||||
|
||||
| Prometheus Metric | Type | Description |
|
||||
| ----------------------------------- | --------- | ------------------------------------------------------------------------------ |
|
||||
| `span_calls_total` | Counter | Total span invocations |
|
||||
| `span_duration_milliseconds_bucket` | Histogram | Latency distribution (buckets: 1, 5, 10, 25, 50, 100, 250, 500, 1000, 5000 ms) |
|
||||
| `span_duration_milliseconds_count` | Histogram | Observation count |
|
||||
| `span_duration_milliseconds_sum` | Histogram | Cumulative latency |
|
||||
|
||||
**Standard labels on every metric**: `span_name`, `status_code`, `service_name`, `span_kind`
|
||||
|
||||
**Additional dimension labels** (configured in `otel-collector-config.yaml`).
|
||||
The Prometheus label is the **bare span-attribute key verbatim** — the
|
||||
SpanMetrics connector does not rewrite or prefix it:
|
||||
|
||||
| Prometheus Label / Span Attribute | Type | Applies To |
|
||||
| --------------------------------- | ------- | ---------------------------------------------- |
|
||||
| `command` | string | `rpc.command.*` |
|
||||
| `rpc_status` | string | `rpc.command.*` |
|
||||
| `consensus_mode` | string | `consensus.round`, `consensus.ledger_close` |
|
||||
| `close_time_correct` | boolean | `consensus.accept.apply` |
|
||||
| `local` | boolean | `tx.process` |
|
||||
| `suppressed` | boolean | `tx.receive` |
|
||||
| `proposal_trusted` | boolean | `peer.proposal.receive` |
|
||||
| `validation_trusted` | boolean | `peer.validation.receive` |
|
||||
| `tx_type` | string | `tx.*`, `txq.enqueue` |
|
||||
| `ter_result` | string | `tx.preflight`, `tx.preclaim`, `tx.transactor` |
|
||||
| `stage` | string | `tx.preflight`, `tx.preclaim`, `tx.transactor` |
|
||||
| `txq_status` | string | `txq.enqueue`, `txq.accept_tx` |
|
||||
| `consensus_state` | string | `consensus.accept.apply` |
|
||||
| `load_type` | string | `rpc.command.*` |
|
||||
| `is_batch` | boolean | `rpc.process` |
|
||||
| `mode_new` | string | `consensus.mode_change` |
|
||||
| `consensus_stalled` | boolean | `consensus.check` |
|
||||
| `consensus_phase` | string | `consensus.round` |
|
||||
| `consensus_result` | string | `consensus.check` |
|
||||
| `method` | string | `grpc.<MethodName>` |
|
||||
| `grpc_role` | string | `grpc.<MethodName>` |
|
||||
| `grpc_status` | string | `grpc.<MethodName>` |
|
||||
|
||||
The `stage` dimension (3 values: `preflight`, `preclaim`, `apply`) turns the
|
||||
apply-pipeline spans into per-stage RED metrics with no native instruments — the
|
||||
_Transaction Overview_ dashboard charts rate, p95 latency, and failure rate by stage.
|
||||
|
||||
> **Sampling caveat**: xrpld head sampling is fixed at 1.0 (every trace is
|
||||
> recorded), so span-derived metrics are not undercounted at the node. If the
|
||||
> collector is configured with tail sampling, span-derived metrics reflect only
|
||||
> the retained traces, whereas native StatsD/meter metrics do not sample.
|
||||
> Account for any collector-side tail sampling when reading absolute stage rates.
|
||||
|
||||
**Where to query**: Prometheus → `span_calls_total{span_name="rpc.command.server_info"}`
|
||||
|
||||
---
|
||||
|
||||
## 2. System Metrics (beast::insight — OTel native)
|
||||
|
||||
> **See also**: [02-design-decisions.md](./02-design-decisions.md) for the beast::insight coexistence design. [06-implementation-phases.md](./06-implementation-phases.md) for the Phase 6/7 metric inventory.
|
||||
>
|
||||
> **Migration complete**: Phase 7 replaced the StatsD UDP transport with native OTel Metrics SDK export via OTLP/HTTP. The `beast::insight::Collector` interface and all metric names are preserved — only the wire protocol changed. `[insight] server=statsd` remains as a fallback.
|
||||
|
||||
These are system-level metrics emitted by xrpld's `beast::insight` framework via OTel OTLP/HTTP. They cover operational data that doesn't map to individual trace spans.
|
||||
|
||||
### Configuration
|
||||
|
||||
```ini
|
||||
# Recommended: native OTel metrics via OTLP/HTTP
|
||||
[insight]
|
||||
server=otel
|
||||
endpoint=http://localhost:4318/v1/metrics
|
||||
prefix=xrpld
|
||||
```
|
||||
|
||||
Fallback (StatsD):
|
||||
|
||||
```ini
|
||||
[insight]
|
||||
server=statsd
|
||||
address=127.0.0.1:8125
|
||||
prefix=xrpld
|
||||
```
|
||||
|
||||
### 2.1 Gauges
|
||||
|
||||
| Prometheus Metric | Source File | Description | Typical Range |
|
||||
| ------------------------------------------- | --------------------- | ----------------------------------------- | ------------------------------- |
|
||||
| `ledgermaster_validated_ledger_age` | LedgerMaster.h | Seconds since last validated ledger | 0–10 (healthy), >30 (stale) |
|
||||
| `ledgermaster_published_ledger_age` | LedgerMaster.h | Seconds since last published ledger | 0–10 (healthy) |
|
||||
| `state_accounting_disconnected_duration` | NetworkOPs.cpp | Cumulative seconds in Disconnected state | Monotonic |
|
||||
| `state_accounting_connected_duration` | NetworkOPs.cpp | Cumulative seconds in Connected state | Monotonic |
|
||||
| `state_accounting_syncing_duration` | NetworkOPs.cpp | Cumulative seconds in Syncing state | Monotonic |
|
||||
| `state_accounting_tracking_duration` | NetworkOPs.cpp | Cumulative seconds in Tracking state | Monotonic |
|
||||
| `state_accounting_full_duration` | NetworkOPs.cpp | Cumulative seconds in Full state | Monotonic (should dominate) |
|
||||
| `state_accounting_disconnected_transitions` | NetworkOPs.cpp | Count of transitions to Disconnected | Low |
|
||||
| `state_accounting_connected_transitions` | NetworkOPs.cpp | Count of transitions to Connected | Low |
|
||||
| `state_accounting_syncing_transitions` | NetworkOPs.cpp | Count of transitions to Syncing | Low |
|
||||
| `state_accounting_tracking_transitions` | NetworkOPs.cpp | Count of transitions to Tracking | Low |
|
||||
| `state_accounting_full_transitions` | NetworkOPs.cpp | Count of transitions to Full | Low (should be 1 after startup) |
|
||||
| `peer_finder_active_inbound_peers` | PeerfinderManager.cpp | Active inbound peer connections | 0–85 |
|
||||
| `peer_finder_active_outbound_peers` | PeerfinderManager.cpp | Active outbound peer connections | 10–21 |
|
||||
| `overlay_peer_disconnects` | OverlayImpl.cpp | Cumulative peer disconnection count | Low growth |
|
||||
| `overlay_peer_disconnects_charges` | OverlayImpl.cpp | Disconnects due to resource limit charges | Low growth (subset of above) |
|
||||
| `job_count` | JobQueue.cpp | Current job queue depth | 0–100 (healthy) |
|
||||
|
||||
**Grafana dashboard**: _Node Health_ (`node-health`)
|
||||
|
||||
### 2.2 Counters
|
||||
|
||||
| Prometheus Metric | Source File | Description |
|
||||
| ------------------------- | ------------------ | --------------------------------------------- |
|
||||
| `rpc_requests` | ServerHandler.cpp | Total RPC requests received |
|
||||
| `ledger_fetches` | InboundLedgers.cpp | Inbound ledger fetch attempts |
|
||||
| `ledger_history_mismatch` | LedgerHistory.cpp | Ledger hash mismatches detected |
|
||||
| `warn` | Logic.h | Resource manager warnings issued |
|
||||
| `drop` | Logic.h | Resource manager drops (connections rejected) |
|
||||
|
||||
**Note**: With `server=otel`, `warn` and `drop` are properly exported as OTel Counter instruments. The previous StatsD `|m` type limitation no longer applies.
|
||||
|
||||
**Grafana dashboard**: _RPC & Pathfinding_ (`rpc-pathfinding`)
|
||||
|
||||
### 2.3 Histograms (Event timers)
|
||||
|
||||
| Prometheus Metric | Source File | Unit | Description |
|
||||
| ----------------- | ----------------- | ---- | ------------------------------ |
|
||||
| `rpc_time` | ServerHandler.cpp | ms | RPC response time distribution |
|
||||
| `rpc_size` | ServerHandler.cpp | ms\* | RPC response size (see note) |
|
||||
| `ios_latency` | Application.cpp | ms | I/O service loop latency |
|
||||
| `pathfind_fast` | PathRequests.h | ms | Fast pathfinding duration |
|
||||
| `pathfind_full` | PathRequests.h | ms | Full pathfinding duration |
|
||||
|
||||
Quantiles collected: 0th, 50th, 90th, 95th, 99th, 100th percentile.
|
||||
|
||||
\* **`rpc_size` instrument mismatch (known issue):** response size in bytes is
|
||||
recorded through the millisecond-scaled event histogram (`makeEvent`), so it is
|
||||
exported as `rpc_size_milliseconds_bucket` with time-scaled boundaries that top
|
||||
out at 5000. Byte values above ~5 KB saturate in the last bucket, so the
|
||||
percentiles are not true byte sizes. The _RPC & Pathfinding_ panel is flagged
|
||||
accordingly. A dedicated byte-unit histogram is needed to fix this; tracked
|
||||
separately.
|
||||
|
||||
**Grafana dashboards**: _Node Health_ (`ios_latency`), _RPC & Pathfinding_ (`rpc_time`, `rpc_size`, `pathfind_*`)
|
||||
|
||||
### 2.4 Overlay Traffic Metrics
|
||||
|
||||
For each of the 45+ overlay traffic categories (defined in `TrafficCount.h`), four gauges are emitted:
|
||||
|
||||
- `{category}_bytes_in`
|
||||
- `{category}_bytes_out`
|
||||
- `{category}_messages_in`
|
||||
- `{category}_messages_out`
|
||||
|
||||
**Key categories**:
|
||||
|
||||
| Category | Description |
|
||||
| ----------------------------------------------------------------- | -------------------------- |
|
||||
| `total` | All traffic aggregated |
|
||||
| `overhead` / `overhead_overlay` | Protocol overhead |
|
||||
| `transactions` / `transactions_duplicate` | Transaction relay |
|
||||
| `proposals` / `proposals_untrusted` / `proposals_duplicate` | Consensus proposals |
|
||||
| `validations` / `validations_untrusted` / `validations_duplicate` | Consensus validations |
|
||||
| `ledger_data_get` / `ledger_data_share` | Ledger data exchange |
|
||||
| `ledger_data_Transaction_Node_get/share` | Transaction node data |
|
||||
| `ledger_data_Account_State_Node_get/share` | Account state node data |
|
||||
| `ledger_data_Transaction_Set_candidate_get/share` | Transaction set candidates |
|
||||
| `getObject` / `haveTxSet` / `ledgerData` | Object requests |
|
||||
| `ping` / `status` | Keepalive and status |
|
||||
| `set_get` | Set requests |
|
||||
|
||||
**Grafana dashboards**: _Network Traffic_ (`network-traffic`), _Overlay Traffic Detail_ (`overlay-traffic-detail`), _Ledger Data & Sync_ (`ledger-data-sync`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Grafana Dashboard Reference
|
||||
|
||||
> **See also**: [05-configuration-reference.md](./05-configuration-reference.md) §5.8 for Grafana data source provisioning (Tempo, Prometheus) and TraceQL query examples.
|
||||
|
||||
### 3.1 Span-Derived Dashboards (5)
|
||||
|
||||
| Dashboard | UID | Data Source | Key Panels |
|
||||
| -------------------- | ---------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| RPC Performance | `rpc-performance` | Prometheus (SpanMetrics) | Request rate by command, p95 latency by command, error rate, heatmap, top commands |
|
||||
| Transaction Overview | `transaction-overview` | Prometheus (SpanMetrics) | Processing rate, latency p95/p50, local vs relay split, apply duration, heatmap |
|
||||
| Consensus Health | `consensus-health` | Prometheus (SpanMetrics) | Round duration p95/p50, proposals rate, close duration, mode timeline, heatmap |
|
||||
| Ledger Operations | `ledger-operations` | Prometheus (SpanMetrics) | Build rate, build duration, validation rate, store rate, build vs close comparison |
|
||||
| Peer Network | `peer-network` | Prometheus (SpanMetrics) | Proposal receive rate, validation receive rate, trusted vs untrusted breakdown |
|
||||
|
||||
### 3.2 System Metrics Dashboards (5)
|
||||
|
||||
| Dashboard | UID | Data Source | Key Panels |
|
||||
| ---------------------- | ------------------------ | ----------------- | --------------------------------------------------------------------------------- |
|
||||
| Node Health | `node-health` | Prometheus (OTLP) | Ledger age, operating mode, I/O latency, job queue, fetch rate |
|
||||
| Network Traffic | `network-traffic` | Prometheus (OTLP) | Active peers, disconnects, bytes in/out, messages in/out, traffic by category |
|
||||
| RPC & Pathfinding | `rpc-pathfinding` | Prometheus (OTLP) | RPC rate, response time/size, pathfinding duration, resource warnings/drops |
|
||||
| Overlay Traffic Detail | `overlay-traffic-detail` | Prometheus (OTLP) | Squelch, overhead, validator lists, set get/share, have/requested tx, proof paths |
|
||||
| Ledger Data & Sync | `ledger-data-sync` | Prometheus (OTLP) | Ledger data exchange, legacy ledger share/get, getobject by type, traffic heatmap |
|
||||
|
||||
### 3.3 Deployment-Tier Template Variables
|
||||
|
||||
Every dashboard carries four filtering template variables (each variable name
|
||||
matches its Prometheus label), letting one Grafana stack be sliced by tier:
|
||||
|
||||
| Variable | Source label | Description |
|
||||
| ------------------------- | ------------------------ | ------------------------------------------------------------ |
|
||||
| `$node` | `exported_instance` | Filter by xrpld node instance |
|
||||
| `$service_name` | `service_name` | Filter by service (`service.name`, e.g. `xrpld`) |
|
||||
| `$deployment_environment` | `deployment_environment` | Filter by deployment tier (`local` / `test` / `ci` / `prod`) |
|
||||
| `$xrpl_network_type` | `xrpl_network_type` | Filter by network (`mainnet` / `testnet` / `devnet`) |
|
||||
|
||||
See [telemetry-runbook.md](../docs/telemetry-runbook.md) "Deployment Tiers"
|
||||
for how the tier attributes are set and reach metrics.
|
||||
|
||||
### 3.4 Accessing the Dashboards
|
||||
|
||||
1. Open Grafana at **http://localhost:3000**
|
||||
2. Navigate to **Dashboards → xrpld** folder
|
||||
3. All 10 dashboards are auto-provisioned from `docker/telemetry/grafana/dashboards/`
|
||||
|
||||
---
|
||||
|
||||
## 4. Tempo Trace Search Guide
|
||||
|
||||
> **See also**: [08-appendix.md](./08-appendix.md) §8.2 for span hierarchy visualizations. [05-configuration-reference.md](./05-configuration-reference.md) §5.8.5 for TraceQL query examples.
|
||||
|
||||
### Finding Traces by Type
|
||||
|
||||
| What to Find | Tempo TraceQL Query |
|
||||
| ------------------------ | ------------------------------------------------------------------------------ |
|
||||
| All RPC calls | `{resource.service.name="xrpld" && name="rpc.http_request"}` |
|
||||
| Specific RPC command | `{resource.service.name="xrpld" && name="rpc.command.server_info"}` |
|
||||
| Slow RPC calls | `{resource.service.name="xrpld" && name=~"rpc.command.*"} \| duration > 100ms` |
|
||||
| Failed RPC calls | `{span.rpc_status="error"}` |
|
||||
| gRPC method calls | `{resource.service.name="xrpld" && name="grpc.GetLedger"}` |
|
||||
| Specific transaction | `{span.tx_hash="<hex_hash>"}` |
|
||||
| Local transactions only | `{span.local=true}` |
|
||||
| Consensus rounds | `{resource.service.name="xrpld" && name="consensus.round"}` |
|
||||
| Rounds by mode | `{span.consensus_mode="Proposing"}` |
|
||||
| Specific ledger | `{span.ledger_seq=12345}` |
|
||||
| Peer proposals (trusted) | `{span.proposal_trusted=true}` |
|
||||
|
||||
### Trace Structure
|
||||
|
||||
A typical RPC trace shows the span hierarchy:
|
||||
|
||||
```
|
||||
rpc.http_request (ServerHandler)
|
||||
└── rpc.process (ServerHandler)
|
||||
└── rpc.command.server_info (RPCHandler)
|
||||
```
|
||||
|
||||
A consensus round groups its lifecycle spans under a single root
|
||||
(`consensus.round`); the build/ledger spans run as their own trees:
|
||||
|
||||
```
|
||||
consensus.round (root — one per round)
|
||||
├── consensus.phase.open (open phase)
|
||||
├── consensus.proposal.send (broadcast proposal)
|
||||
├── consensus.ledger_close (close event)
|
||||
├── consensus.establish (establish phase)
|
||||
├── consensus.update_positions (position updates)
|
||||
├── consensus.check (threshold check)
|
||||
├── consensus.accept (accept result)
|
||||
│ └── consensus.accept.apply (apply, jtACCEPT thread)
|
||||
└── consensus.validation.send (send validation, follows-from link)
|
||||
|
||||
ledger.build (build new ledger)
|
||||
└── tx.apply (apply transaction set)
|
||||
ledger.validate (promote to validated)
|
||||
ledger.store (persist to DB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Prometheus Query Examples
|
||||
|
||||
> **See also**: [05-configuration-reference.md](./05-configuration-reference.md) §5.8.7 for correlating Prometheus system metrics with trace-derived metrics.
|
||||
|
||||
### Span-Derived Metrics
|
||||
|
||||
```promql
|
||||
# RPC request rate by command (last 5 minutes)
|
||||
sum by (command) (rate(span_calls_total{span_name=~"rpc.command.*"}[5m]))
|
||||
|
||||
# RPC p95 latency by command
|
||||
histogram_quantile(0.95, sum by (le, command) (rate(span_duration_milliseconds_bucket{span_name=~"rpc.command.*"}[5m])))
|
||||
|
||||
# Consensus round duration p95
|
||||
histogram_quantile(0.95, sum by (le) (rate(span_duration_milliseconds_bucket{span_name="consensus.round"}[5m])))
|
||||
|
||||
# Transaction processing rate (local vs relay)
|
||||
sum by (local) (rate(span_calls_total{span_name="tx.process"}[5m]))
|
||||
|
||||
# Trusted vs untrusted proposal rate
|
||||
sum by (proposal_trusted) (rate(span_calls_total{span_name="peer.proposal.receive"}[5m]))
|
||||
```
|
||||
|
||||
### StatsD Metrics
|
||||
|
||||
```promql
|
||||
# Validated ledger age (should be < 10s)
|
||||
ledgermaster_validated_ledger_age
|
||||
|
||||
# Active peer count
|
||||
peer_finder_active_inbound_peers + peer_finder_active_outbound_peers
|
||||
|
||||
# RPC response time p95
|
||||
histogram_quantile(0.95, rpc_time_bucket)
|
||||
|
||||
# Total network bytes in (rate)
|
||||
rate(total_bytes_in[5m])
|
||||
|
||||
# Operating mode (should be "Full" after startup)
|
||||
state_accounting_full_duration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5a. Log-Trace Correlation (Phase 8)
|
||||
|
||||
> **Plan details**: [06-implementation-phases.md §6.8.1](./06-implementation-phases.md) — motivation, architecture, Mermaid diagrams
|
||||
> **Task breakdown**: [Phase8_taskList.md](./Phase8_taskList.md) — per-task implementation details
|
||||
|
||||
Phase 8 injects OTel trace context into xrpld's `Logs::format()` output, enabling log-trace correlation. When a log line is emitted within an active OTel span, the trace and span identifiers are automatically appended after the severity field:
|
||||
|
||||
### Log Format
|
||||
|
||||
```
|
||||
<timestamp> <partition>:<severity> trace_id=<32hex> span_id=<16hex> <message>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
2024-Jan-15 10:30:45.123456 UTC LedgerMaster:NFO trace_id=abc123def456789012345678abcdef01 span_id=0123456789abcdef Validated ledger 42
|
||||
```
|
||||
|
||||
- **`trace_id=<hex32>`** — 32-character lowercase hex trace identifier. Links to the distributed trace in Tempo/Jaeger.
|
||||
- **`span_id=<hex16>`** — 16-character lowercase hex span identifier. Identifies the specific span within the trace.
|
||||
- **Only present** when the log is emitted within an active OTel span. Log lines outside of traced code paths have no trace context fields.
|
||||
|
||||
### Implementation
|
||||
|
||||
The trace context injection is implemented in `Logs::format()` (`src/libxrpl/basics/Log.cpp`), guarded by `#ifdef XRPL_ENABLE_TELEMETRY`. It checks the thread-local runtime context value directly (via `RuntimeContext::GetCurrent().GetValue(kSpanKey)`) to avoid the heap allocation that `GetSpan()` performs on the no-span path. On threads without an active span, the cost is a thread-local read + variant type check (~15-20ns). On the active-span path, total cost is ~50ns per log call.
|
||||
|
||||
### Log Ingestion Pipeline
|
||||
|
||||
```
|
||||
xrpld debug.log -> OTel Collector filelog receiver -> regex_parser -> Loki exporter -> Grafana Loki
|
||||
```
|
||||
|
||||
The OTel Collector's `filelog` receiver tails `debug.log` files and uses a `regex_parser` operator to extract structured fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----------- | -------- | -------------------------------------------------------- |
|
||||
| `timestamp` | datetime | Log timestamp |
|
||||
| `partition` | string | Log partition (e.g., `LedgerMaster`, `PeerImp`) |
|
||||
| `severity` | string | Severity code (`TRC`, `DBG`, `NFO`, `WRN`, `ERR`, `FTL`) |
|
||||
| `trace_id` | string | 32-hex trace identifier (optional) |
|
||||
| `span_id` | string | 16-hex span identifier (optional) |
|
||||
| `message` | string | Log message body |
|
||||
|
||||
### Grafana Correlation
|
||||
|
||||
Bidirectional linking between logs and traces is configured via Grafana datasource provisioning:
|
||||
|
||||
- **Tempo -> Loki** (`tracesToLogs`): Clicking "Logs for this trace" on a Tempo trace view filters Loki logs by `trace_id`, showing all log lines from that trace.
|
||||
- **Loki -> Tempo** (`derivedFields`): A regex-based derived field on the Loki datasource extracts `trace_id` from log lines and renders it as a clickable link to the corresponding trace in Tempo.
|
||||
|
||||
### Loki Backend
|
||||
|
||||
Grafana Loki (v3.4.2) serves as the log storage backend. It receives log entries from the OTel Collector's `otlphttp/loki` exporter via the native OTLP endpoint at `http://loki:3100/otlp`.
|
||||
|
||||
### LogQL Query Examples
|
||||
|
||||
```logql
|
||||
# Find all logs for a specific trace
|
||||
{job="xrpld"} |= "trace_id=abc123def456789012345678abcdef01"
|
||||
|
||||
# Error logs with trace context
|
||||
{job="xrpld"} |= "ERR" |= "trace_id="
|
||||
|
||||
# Logs from a specific partition with trace context
|
||||
{job="xrpld"} |= "LedgerMaster" | regexp `trace_id=(?P<trace_id>[a-f0-9]+)` | trace_id != ""
|
||||
|
||||
# Count traced log lines over time
|
||||
count_over_time({job="xrpld"} |= "trace_id=" [5m])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Known Issues
|
||||
|
||||
| Issue | Impact | Status |
|
||||
| ------------------------------------------------------------------ | ------------------------------------------------ | -------------------------------------------------------------------- |
|
||||
| `warn` and `drop` metrics use non-standard StatsD `\|m` meter type | Metrics silently dropped by OTel StatsD receiver | Phase 6 Task 6.1 — needs `\|m` → `\|c` change in StatsDCollector.cpp |
|
||||
| `job_count` may not emit in standalone mode | Missing from Prometheus in some test configs | Requires active job queue activity |
|
||||
| `rpc_requests` depends on `[insight]` config | Zero series if StatsD not configured | Requires `[insight] server=statsd` in xrpld.cfg |
|
||||
| Peer tracing enabled by default | `peer.*` spans emit unless `trace_peer=0` | High volume — set `trace_peer=0` to opt out on busy mainnet nodes |
|
||||
|
||||
---
|
||||
|
||||
## 7. Privacy and Data Collection
|
||||
|
||||
The telemetry system is designed with privacy in mind:
|
||||
|
||||
- **No private keys** are ever included in spans or metrics
|
||||
- **No account balances** or financial data is traced
|
||||
- **Transaction hashes** are included (public on-ledger data) but not transaction contents
|
||||
- **Peer IDs** are internal identifiers, not IP addresses
|
||||
- **All telemetry is opt-in** — disabled by default at build time (`-Dtelemetry=OFF`)
|
||||
- **Sampling** — head sampling is fixed at 1.0 (sample everything); reduce data volume with collector-side tail sampling
|
||||
- **Data stays local** — the default stack sends data to `localhost` only
|
||||
|
||||
---
|
||||
|
||||
## 8. Configuration Quick Reference
|
||||
|
||||
> **Full reference**: [05-configuration-reference.md](./05-configuration-reference.md) §5.1 for all `[telemetry]` options with defaults, the config parser implementation, and collector YAML configurations (dev and production).
|
||||
|
||||
### Minimal Setup (development)
|
||||
|
||||
```ini
|
||||
[telemetry]
|
||||
enabled=1
|
||||
|
||||
[insight]
|
||||
server=statsd
|
||||
address=127.0.0.1:8125
|
||||
prefix=xrpld
|
||||
```
|
||||
|
||||
### Production Setup
|
||||
|
||||
```ini
|
||||
[telemetry]
|
||||
enabled=1
|
||||
endpoint=http://otel-collector:4318/v1/traces
|
||||
trace_peer=0
|
||||
batch_size=1024
|
||||
max_queue_size=4096
|
||||
|
||||
[insight]
|
||||
server=statsd
|
||||
address=otel-collector:8125
|
||||
prefix=xrpld
|
||||
```
|
||||
|
||||
### Trace Category Toggle
|
||||
|
||||
| Config Key | Default | Controls |
|
||||
| -------------------- | ------- | ---------------------------- |
|
||||
| `trace_rpc` | `1` | `rpc.*` spans |
|
||||
| `trace_transactions` | `1` | `tx.*` spans |
|
||||
| `trace_consensus` | `1` | `consensus.*` spans |
|
||||
| `trace_ledger` | `1` | `ledger.*` spans |
|
||||
| `trace_peer` | `1` | `peer.*` spans (high volume) |
|
||||
226
OpenTelemetryPlan/OpenTelemetryPlan.md
Normal file
226
OpenTelemetryPlan/OpenTelemetryPlan.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# [OpenTelemetry](00-tracing-fundamentals.md) Distributed Tracing Implementation Plan for xrpld
|
||||
|
||||
## Executive Summary
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol
|
||||
|
||||
This document provides a comprehensive implementation plan for integrating OpenTelemetry distributed tracing into the xrpld XRP Ledger node software. The plan addresses the unique challenges of a decentralized peer-to-peer system where trace context must propagate across network boundaries between independent nodes.
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- **End-to-end transaction visibility**: Track transactions from submission through consensus to ledger inclusion
|
||||
- **Consensus round analysis**: Understand timing and behavior of consensus phases across validators
|
||||
- **RPC performance insights**: Identify slow handlers and optimize response times
|
||||
- **Network topology understanding**: Visualize message propagation patterns between peers
|
||||
- **Incident debugging**: Correlate events across distributed nodes during issues
|
||||
|
||||
### Estimated Performance Overhead
|
||||
|
||||
| Metric | Overhead | Notes |
|
||||
| ------------- | ---------- | ------------------------------------------------ |
|
||||
| CPU | 1-3% | Span creation and attribute setting |
|
||||
| Memory | <10 MB | SDK statics + batch buffer + worker thread stack |
|
||||
| Network | 10-50 KB/s | Compressed OTLP export to collector |
|
||||
| Latency (p99) | <2% | With proper sampling configuration |
|
||||
|
||||
---
|
||||
|
||||
## Document Structure
|
||||
|
||||
This implementation plan is organized into modular documents for easier navigation:
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
overview["📋 OpenTelemetryPlan.md<br/>(This Document)"]
|
||||
|
||||
subgraph fundamentals["Fundamentals"]
|
||||
fund["00-tracing-fundamentals.md"]
|
||||
end
|
||||
|
||||
subgraph analysis["Analysis & Design"]
|
||||
arch["01-architecture-analysis.md"]
|
||||
design["02-design-decisions.md"]
|
||||
end
|
||||
|
||||
subgraph impl["Implementation"]
|
||||
strategy["03-implementation-strategy.md"]
|
||||
config["05-configuration-reference.md"]
|
||||
end
|
||||
|
||||
subgraph deploy["Deployment & Planning"]
|
||||
phases["06-implementation-phases.md"]
|
||||
backends["07-observability-backends.md"]
|
||||
appendix["08-appendix.md"]
|
||||
secure["secure-OTel.md"]
|
||||
dataref["09-data-collection-reference.md"]
|
||||
end
|
||||
|
||||
overview --> fundamentals
|
||||
overview --> analysis
|
||||
overview --> impl
|
||||
overview --> deploy
|
||||
|
||||
fund --> arch
|
||||
arch --> design
|
||||
design --> strategy
|
||||
strategy --> config
|
||||
config --> phases
|
||||
phases --> backends
|
||||
backends --> appendix
|
||||
backends --> secure
|
||||
appendix --> dataref
|
||||
|
||||
style overview fill:#1b5e20,stroke:#0d3d14,color:#fff,stroke-width:2px
|
||||
style fundamentals fill:#00695c,stroke:#004d40,color:#fff
|
||||
style fund fill:#00695c,stroke:#004d40,color:#fff
|
||||
style analysis fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style impl fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style deploy fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style arch fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style design fill:#0d47a1,stroke:#082f6a,color:#fff
|
||||
style strategy fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style config fill:#bf360c,stroke:#8c2809,color:#fff
|
||||
style phases fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style backends fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style appendix fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style secure fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
style dataref fill:#4a148c,stroke:#2e0d57,color:#fff
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
| Section | Document | Description |
|
||||
| ------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| **0** | [Tracing Fundamentals](./00-tracing-fundamentals.md) | Distributed tracing concepts, span relationships, context propagation |
|
||||
| **1** | [Architecture Analysis](./01-architecture-analysis.md) | xrpld component analysis, trace points, instrumentation priorities |
|
||||
| **2** | [Design Decisions](./02-design-decisions.md) | SDK selection, exporters, span naming, attributes, context propagation |
|
||||
| **3** | [Implementation Strategy](./03-implementation-strategy.md) | Directory structure, key principles, performance optimization |
|
||||
| **5** | [Configuration Reference](./05-configuration-reference.md) | xrpld config, CMake integration, Collector configurations |
|
||||
| **6** | [Implementation Phases](./06-implementation-phases.md) | 5-phase timeline, tasks, risks, success metrics |
|
||||
| **7** | [Observability Backends](./07-observability-backends.md) | Backend selection guide and production architecture |
|
||||
| **8** | [Appendix](./08-appendix.md) | Glossary, references, version history |
|
||||
| **9** | [Data Collection Reference](./09-data-collection-reference.md) | Complete inventory of spans, attributes, metrics, and dashboards |
|
||||
| **Sec** | [Securing the OTel Pipeline](./secure-OTel.md) | Threat model and hardening (mTLS, peer trace-context validation) |
|
||||
|
||||
---
|
||||
|
||||
## 0. Tracing Fundamentals
|
||||
|
||||
This document introduces distributed tracing concepts for readers unfamiliar with the domain. It covers what traces and spans are, how parent-child and follows-from relationships model causality, how context propagates across service boundaries, and how sampling controls data volume. It also maps these concepts to xrpld-specific scenarios like transaction relay and consensus.
|
||||
|
||||
➡️ **[Read Tracing Fundamentals](./00-tracing-fundamentals.md)**
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Analysis
|
||||
|
||||
> **WS** = WebSocket | **TxQ** = Transaction Queue
|
||||
|
||||
The xrpld node consists of several key components that require instrumentation for comprehensive distributed tracing. The main areas include the RPC server (HTTP/WebSocket), Overlay P2P network, Consensus mechanism (RCLConsensus), JobQueue for async task execution, PathFinding, Transaction Queue (TxQ), fee escalation (LoadManager), ledger acquisition, validator management, and existing observability infrastructure (PerfLog, Insight/StatsD, Journal logging).
|
||||
|
||||
Key trace points span across transaction submission via RPC, peer-to-peer message propagation, consensus round execution, ledger building, path computation, transaction queue behavior, fee escalation, and validator health. The implementation prioritizes high-value, low-risk components first: RPC handlers provide immediate value with minimal risk, while consensus tracing requires careful implementation to avoid timing impacts.
|
||||
|
||||
➡️ **[Read full Architecture Analysis](./01-architecture-analysis.md)**
|
||||
|
||||
---
|
||||
|
||||
## 2. Design Decisions
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **CNCF** = Cloud Native Computing Foundation
|
||||
|
||||
The OpenTelemetry C++ SDK is selected for its CNCF backing, active development, and native performance characteristics. Traces are exported via OTLP/HTTP to an OpenTelemetry Collector, which provides flexible routing and sampling. OTLP/gRPC is planned future work (see design decisions §2.2.2).
|
||||
|
||||
Span naming follows a hierarchical `<component>.<operation>` convention (e.g., `rpc.submit`, `tx.relay`, `consensus.round`). Context propagation uses W3C Trace Context headers for HTTP and embedded Protocol Buffer fields for P2P messages. The implementation coexists with existing PerfLog and Insight observability systems through correlation IDs.
|
||||
|
||||
**Data Collection & Privacy**: Telemetry collects only operational metadata (timing, counts, hashes) — never sensitive content (private keys, balances, amounts, raw payloads). Privacy protection includes account hashing, configurable redaction, sampling, and collector-level filtering. Node operators retain full control over telemetry configuration.
|
||||
|
||||
➡️ **[Read full Design Decisions](./02-design-decisions.md)**
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Strategy
|
||||
|
||||
The telemetry code is organized under `include/xrpl/telemetry/` for headers and `src/libxrpl/telemetry/` for implementation. Key principles include RAII-based span management via `SpanGuard` (with `discard()` for dropping unwanted spans), a `FilteringSpanProcessor` that intercepts `OnEnd()` to prevent discarded spans from entering the export pipeline, conditional compilation with `XRPL_ENABLE_TELEMETRY`, and minimal runtime overhead through batch processing and efficient sampling.
|
||||
|
||||
Performance optimization strategies include head sampling fixed at 100% (intentionally not configurable, so trace keep/drop decisions stay coherent across nodes), tail-based sampling at the collector for errors and slow traces to reduce volume, batch export to reduce network overhead, and conditional instrumentation that compiles to no-ops when disabled.
|
||||
|
||||
➡️ **[Read full Implementation Strategy](./03-implementation-strategy.md)**
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuration Reference
|
||||
|
||||
> **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring
|
||||
|
||||
Configuration is handled through the `[telemetry]` section in `xrpld.cfg` with options for enabling/disabling, exporter selection, endpoint configuration, and component-level filtering. Head sampling is fixed at 1.0 (not operator-configurable); volume reduction is done by tail sampling in the collector. CMake integration includes a `XRPL_ENABLE_TELEMETRY` option for compile-time control.
|
||||
|
||||
OpenTelemetry Collector configurations are provided for development and production (with tail-based sampling, Tempo, and Elastic APM). Docker Compose examples enable quick local development environment setup.
|
||||
|
||||
➡️ **[View full Configuration Reference](./05-configuration-reference.md)**
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Phases
|
||||
|
||||
The implementation spans 13 weeks across 8 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 |
|
||||
| 6 | Week 10 | StatsD Metrics Bridge | OTel Collector StatsD receiver, 3 Grafana dashboards |
|
||||
| 7 | Weeks 11-12 | Native OTel Metrics | OTelCollector impl, OTLP metrics export, StatsD deprecation |
|
||||
| 8 | Week 13 | Log-Trace Correlation | trace_id in logs, Loki ingestion, Tempo↔Loki linking |
|
||||
|
||||
**Total Effort**: 65.1 developer-days with 2 developers
|
||||
|
||||
➡️ **[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)**
|
||||
|
||||
---
|
||||
|
||||
## 9. Data Collection Reference
|
||||
|
||||
A single-source-of-truth reference documenting every piece of telemetry data collected by xrpld. Covers all 16 OpenTelemetry spans with their 22 attributes, all StatsD metrics (gauges, counters, histograms, overlay traffic), SpanMetrics-derived Prometheus metrics, and all 10 Grafana dashboards. Includes Jaeger search guides and Prometheus query examples.
|
||||
|
||||
➡️ **[View Data Collection Reference](./09-data-collection-reference.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)**
|
||||
|
||||
---
|
||||
|
||||
_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._
|
||||
240
OpenTelemetryPlan/Phase2_taskList.md
Normal file
240
OpenTelemetryPlan/Phase2_taskList.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# 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_currency` | string | `to_string(saDstAmount_.asset())` |
|
||||
| `pathfind.discover` | `pathfind_num_source_assets` | int64 | `sourceAssets.size()` |
|
||||
|
||||
_Note: `pathfind_dest_amount` was removed — the destination amount is a financial value excluded by the privacy policy (design §2.4.4)._
|
||||
|
||||
**New attr keys**: `RpcSpanNames.h` (`isBatch`, `batchSize`, `loadType`), `PathFindSpanNames.h` (`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).
|
||||
|
||||
---
|
||||
|
||||
## Known Issues / Future Work
|
||||
|
||||
### Thread safety of TelemetryImpl::stop() vs startSpan()
|
||||
|
||||
`TelemetryImpl::stop()` resets `sdkProvider_` (a `std::shared_ptr`) without
|
||||
synchronization. `getTracer()` reads the same member from RPC handler threads.
|
||||
This is a data race if any thread calls `startSpan()` concurrently with `stop()`.
|
||||
|
||||
**Current mitigation**: `Application::stop()` shuts down `serverHandler_`,
|
||||
`overlay_`, and `jobQueue_` before calling `telemetry_->stop()`, so no callers
|
||||
remain. See comments in `Telemetry.cpp:stop()` and `Application.cpp`.
|
||||
|
||||
**TODO**: Add an `std::atomic<bool> stopped_` flag checked in `getTracer()` to
|
||||
make this robust against future shutdown order changes.
|
||||
|
||||
### Macro incompatibility: XRPL_TRACE_SPAN vs XRPL_TRACE_SET_ATTR
|
||||
|
||||
`XRPL_TRACE_SPAN` and `XRPL_TRACE_SPAN_KIND` declare `_xrpl_guard_` as a bare
|
||||
`SpanGuard`, but `XRPL_TRACE_SET_ATTR` and `XRPL_TRACE_EXCEPTION` call
|
||||
`_xrpl_guard_.has_value()` which requires `std::optional<SpanGuard>`. Using
|
||||
`XRPL_TRACE_SPAN` followed by `XRPL_TRACE_SET_ATTR` in the same scope would
|
||||
fail to compile.
|
||||
|
||||
**Current mitigation**: No call site currently uses `XRPL_TRACE_SPAN` — all
|
||||
production code uses the conditional macros (`XRPL_TRACE_RPC`, `XRPL_TRACE_TX`,
|
||||
etc.) which correctly wrap the guard in `std::optional`.
|
||||
|
||||
**TODO**: Either make `XRPL_TRACE_SPAN`/`XRPL_TRACE_SPAN_KIND` also wrap in
|
||||
`std::optional`, or document that `XRPL_TRACE_SET_ATTR` is only compatible with
|
||||
the conditional macros.
|
||||
548
OpenTelemetryPlan/Phase3_taskList.md
Normal file
548
OpenTelemetryPlan/Phase3_taskList.md
Normal file
@@ -0,0 +1,548 @@
|
||||
# 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 (transaction relay). Consensus proposal/validation
|
||||
propagation is deferred to Phase 4 — see "Planned (Phase 4)" below.
|
||||
|
||||
**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.
|
||||
|
||||
- **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.
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/misc/NetworkOPs.cpp` — tx relay injection
|
||||
- `include/xrpl/telemetry/SpanGuard.h` — `TraceBytes` struct, `getTraceBytes()`
|
||||
- `src/libxrpl/telemetry/SpanGuard.cpp` — `getTraceBytes()` implementation
|
||||
- `src/xrpld/telemetry/PropagationHelpers.h` — inject helpers (new file)
|
||||
|
||||
**Planned (Phase 4 — not in this PR)**:
|
||||
|
||||
The consensus proposal/validation propagation below is Phase 4 scope and is
|
||||
not implemented on this branch. It is listed here only to record the intended
|
||||
design.
|
||||
|
||||
- **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.
|
||||
|
||||
- Planned files: `src/xrpld/app/consensus/RCLConsensus.cpp` (send injection),
|
||||
`src/xrpld/overlay/detail/PeerImp.cpp` (receive spans),
|
||||
`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](./06-implementation-phases.md#appendix-external-dashboard-parity) — 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/telemetry/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/telemetry/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/telemetry/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
|
||||
|
||||
---
|
||||
|
||||
## Known Issues / Future Work
|
||||
|
||||
### Unused trace_state proto field
|
||||
|
||||
The `TraceContext.trace_state` field (field 4) in `xrpl.proto` is reserved for
|
||||
W3C `tracestate` vendor-specific key-value pairs but is not read or written by
|
||||
`TraceContextPropagator`. Wire it when cross-vendor trace propagation is needed.
|
||||
No wire cost since proto `optional` fields are zero-cost when absent.
|
||||
946
OpenTelemetryPlan/Phase4_taskList.md
Normal file
946
OpenTelemetryPlan/Phase4_taskList.md
Normal file
@@ -0,0 +1,946 @@
|
||||
# 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`)
|
||||
|
||||
> **Note on attribute names**: the `xrpl.<domain>.<field>` keys shown below are
|
||||
> written in the older dotted form for readability — it mirrors how the fully
|
||||
> qualified attribute reads in a Tempo trace view. The implemented keys follow
|
||||
> the convention in [CONTRIBUTING.md](../CONTRIBUTING.md#telemetry-span-attribute-naming)
|
||||
> (underscore form, e.g. `consensus_round`, `consensus_mode`); the
|
||||
> `*SpanNames.h` constants are the single source of truth.
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| ------------------------------------------------------------ | ----------------------------------------------------------- |
|
||||
| [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.
|
||||
|
||||
**Status**: DONE (implemented via Task 4a.2 `startRoundTracing()` helper).
|
||||
|
||||
**What was done**:
|
||||
|
||||
- `RCLConsensus::Adaptor::startRoundTracing()` creates `consensus.round` span
|
||||
via `SpanGuard::hashSpan()` (deterministic) or `SpanGuard::span()` (attribute strategy)
|
||||
- Attributes set: `xrpl.consensus.ledger_id`, `xrpl.ledger.seq`,
|
||||
`xrpl.consensus.mode`, `trace_strategy`, `xrpl.consensus.round_id`
|
||||
- Round span stored as `roundSpan_` member in `RCLConsensus::Adaptor`
|
||||
- `roundSpanContext_` snapshot captured for cross-thread span linking
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
- `src/xrpld/app/consensus/RCLConsensus.h` (span and context members)
|
||||
|
||||
**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.
|
||||
|
||||
**Status**: DONE. All consensus phases are now instrumented:
|
||||
|
||||
- `consensus.establish` — created in `Consensus.h::startEstablishTracing()`
|
||||
- `consensus.ledger_close` — created in `RCLConsensus.cpp::onClose()`
|
||||
- `consensus.accept` / `consensus.accept.apply` — created in `onAccept()` / `doAccept()`
|
||||
- `consensus.phase.open` — `openSpan_` member in `Consensus.h`, created in `startRoundInternal()`, ended in `closeLedger()`
|
||||
|
||||
**Design notes**:
|
||||
|
||||
- `phase` attribute — phases are distinguished by span names instead
|
||||
- `phase.enter` / `phase.exit` events — not added (span start/end serves this purpose)
|
||||
- `phase_duration_ms` attribute — not set (span duration captures this)
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
- `src/xrpld/consensus/Consensus.h` (template-level establish 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.
|
||||
|
||||
**Status**: DONE. Both send and receive paths are instrumented.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- In `Adaptor::propose()`:
|
||||
- Creates `consensus.proposal.send` span via `SpanGuard::span()`
|
||||
- Sets `xrpl.consensus.round` attribute
|
||||
|
||||
- In `PeerImp::onMessage(TMProposeSet)`:
|
||||
- Creates `consensus.proposal.receive` span
|
||||
- Sets `trusted` attribute (bool)
|
||||
|
||||
**Done here** (cross-node propagation, send + receive):
|
||||
|
||||
- Trace context injection for `TMProposeSet::trace_context` in `propose()`
|
||||
- Receive-side extraction in `PeerImp::onMessage(TMProposeSet)` via
|
||||
`telemetry::proposalReceiveSpan()` (parents the receive span on the
|
||||
sender's context when a valid `trace_context` is present)
|
||||
|
||||
**Not implemented** (deferred to Phase 4b):
|
||||
|
||||
- `consensus.proposal.relay` span in `share(RCLCxPeerPos)`
|
||||
|
||||
**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.
|
||||
|
||||
**Status**: DONE. Both send and receive paths are instrumented.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- In `Adaptor::validate()` (called from `doAccept()`):
|
||||
- Creates `consensus.validation.send` span via `Adaptor::createValidationSpan()`
|
||||
- Uses `SpanGuard::linkedSpan()` to create a follows-from link to the round span
|
||||
- Thread-safe: uses `roundSpanContext_` snapshot (captured on consensus thread,
|
||||
read on jtACCEPT thread)
|
||||
- Sets `xrpl.ledger.seq` and `proposing` attributes
|
||||
|
||||
- In `PeerImp::onMessage(TMValidation)`:
|
||||
- Creates `consensus.validation.receive` span
|
||||
- Sets `trusted` attribute (bool)
|
||||
- Sets `xrpl.ledger.seq` attribute
|
||||
|
||||
**Not implemented** (deferred to Phase 4b — cross-node propagation):
|
||||
|
||||
- Validated ledger hash, signing time attributes on send span (see Task 4.8)
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Task 4.5: Add Consensus-Specific Attributes ✅
|
||||
|
||||
**Objective**: Enrich consensus spans with detailed attributes for debugging and analysis.
|
||||
|
||||
**Status**: DONE. All core attributes are set across various spans, including the previously missing `tx_count` and `disputes_count`.
|
||||
|
||||
**Implemented attributes** (across various spans):
|
||||
|
||||
- `xrpl.ledger.seq` — on `consensus.round`, `consensus.accept.apply`
|
||||
- `xrpl.consensus.round` — on `consensus.proposal.send`
|
||||
- `xrpl.consensus.mode` — on `consensus.round`, `consensus.ledger_close`
|
||||
- `proposers` — on `consensus.accept`, `consensus.establish`, `consensus.update_positions`
|
||||
- `converge_percent` — on `consensus.establish`, `consensus.update_positions`, `consensus.check`
|
||||
- `tx_count` — on `consensus.accept.apply` span (in `doAccept()`)
|
||||
- `disputes_count` — on `consensus.update_positions` span (in `updateOurPositions()`)
|
||||
|
||||
**Design notes**:
|
||||
|
||||
- `phase` — phases distinguished by span names instead
|
||||
- `phase_duration_ms` — span duration captures this
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
- `src/xrpld/consensus/Consensus.h`
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
**Status**: DONE. Transaction-consensus correlation implemented via `tx.included` events in `doAccept()`.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- In `doAccept()` (RCLConsensus.cpp):
|
||||
- Records `tx.included` events on the `consensus.accept.apply` span for each transaction in the accepted set
|
||||
- Each event includes `xrpl.tx.id` attribute with the transaction hash
|
||||
- This links consensus traces to individual transactions
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.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 `SpanGuard` factory methods compile to no-ops when disabled
|
||||
5. Check that no consensus-critical code paths are affected by instrumentation overhead
|
||||
|
||||
**Verification Checklist**:
|
||||
|
||||
- [x] Build succeeds with telemetry ON
|
||||
- [x] Build succeeds with telemetry OFF
|
||||
- [x] Existing consensus tests pass
|
||||
- [x] `SpanGuard` no-op implementation prevents overhead when telemetry is OFF
|
||||
- [x] Phase timing instrumentation doesn't use blocking operations
|
||||
|
||||
---
|
||||
|
||||
## Task 4.8: Consensus Validation Span Enrichment — NOT DONE
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity) — adds validation agreement context inspired by the community [xrpl-validator-dashboard](https://github.com/realgrapedrop/xrpl-validator-dashboard).
|
||||
>
|
||||
> **Upstream**: Phase 4 tasks 4.1-4.4 (span creation must exist).
|
||||
> **Downstream**: Phase 7 (ValidationTracker reads these attributes), Phase 10 (validation checks).
|
||||
|
||||
**Objective**: Add ledger hash, validation type, and quorum data to consensus validation spans on both send and receive paths. This enables trace-level validation agreement analysis — filter by ledger hash to see which validators agreed for a given ledger.
|
||||
|
||||
**Status**: Not implemented. None of the enrichment attributes are set. The `consensus.validation.send` span only has `ledger.seq` and `proposing`. The `consensus.accept` span has `quorum` set to `result.proposers` (not the actual validator quorum from `app_.validators().quorum()`). No `PeerImp.cpp` changes were made.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/app/consensus/RCLConsensus.cpp`:
|
||||
- On the `consensus.validation.send` span (in `validate()` / `doAccept()`):
|
||||
- Add `xrpl.validation.ledger_hash` (string) — the ledger hash being validated
|
||||
- Add `xrpl.validation.full` (bool) — whether this is a full validation (not partial)
|
||||
- On the `consensus.accept` span (in `onAccept()`):
|
||||
- Add `validation_quorum` (int64) — from `app_.validators().quorum()`
|
||||
- Add `proposers_validated` (int64) — from `result.proposers`
|
||||
|
||||
- Edit `src/xrpld/overlay/detail/PeerImp.cpp`:
|
||||
- On the `peer.validation.receive` span:
|
||||
- Add `xrpl.peer.validation.ledger_hash` (string) — from deserialized `STValidation` object
|
||||
- Add `xrpl.peer.validation.full` (bool) — from `STValidation` flags
|
||||
|
||||
**New span attributes**:
|
||||
|
||||
| Span | Attribute | Type | Source |
|
||||
| --------------------------- | ---------------------------------- | ------ | --------------------------------- |
|
||||
| `consensus.validation.send` | `xrpl.validation.ledger_hash` | string | Ledger hash from validate() args |
|
||||
| `consensus.validation.send` | `xrpl.validation.full` | bool | Full vs partial validation |
|
||||
| `peer.validation.receive` | `xrpl.peer.validation.ledger_hash` | string | From STValidation deserialization |
|
||||
| `peer.validation.receive` | `xrpl.peer.validation.full` | bool | From STValidation flags |
|
||||
| `consensus.accept` | `validation_quorum` | int64 | `app_.validators().quorum()` |
|
||||
| `consensus.accept` | `proposers_validated` | int64 | `result.proposers` |
|
||||
|
||||
**Rationale**: The external dashboard's most valuable feature is validation agreement tracking. By recording the ledger hash on both outgoing and incoming validation spans, we create the raw data for agreement analysis at the trace level. Example Tempo query:
|
||||
|
||||
```
|
||||
{name="consensus.validation.send"} | xrpl.validation.ledger_hash = "A1B2C3..."
|
||||
```
|
||||
|
||||
Phase 7's `ValidationTracker` builds metric-level aggregation (1h/24h agreement %) on top of this data.
|
||||
|
||||
**Key modified files (not yet modified)**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
- `src/xrpld/overlay/detail/PeerImp.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [x] `consensus.validation.send` spans carry `ledger_hash` and `full_validation`
|
||||
- [ ] `peer.validation.receive` spans carry `xrpl.peer.validation.ledger_hash` and `xrpl.peer.validation.full`
|
||||
- [ ] `consensus.accept` spans carry `validation_quorum` and `proposers_validated`
|
||||
- [x] Ledger hash attributes match between send and receive for the same ledger
|
||||
- [ ] No impact on consensus performance
|
||||
|
||||
---
|
||||
|
||||
## Task 4.9: Consensus Span Attribute Gap Fill
|
||||
|
||||
**Status**: COMPLETE
|
||||
|
||||
**Objective**: Add workflow-critical attributes to consensus spans that enable operators to understand consensus outcomes, identify bow-out proposals, and correlate validations to specific ledgers.
|
||||
|
||||
**Attributes added**:
|
||||
|
||||
| Span | Attribute | Type | Source |
|
||||
| --------------------------- | ----------------- | ------ | ------------------------------------- |
|
||||
| `consensus.proposal.send` | `is_bow_out` | bool | `proposal.isBowOut()` |
|
||||
| `consensus.accept` | `consensus_state` | string | `result.state` (yes/moved_on/expired) |
|
||||
| `consensus.accept` | `disputes_count` | int64 | `result.disputes.size()` |
|
||||
| `consensus.validation.send` | `ledger_hash` | string | `ledger.ledger->header().hash` |
|
||||
|
||||
**New attr keys**: `ConsensusSpanNames.h` (`isBowOut`, `ledgerHash`).
|
||||
|
||||
**Modified files**:
|
||||
|
||||
- `src/xrpld/consensus/ConsensusSpanNames.h`
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Description | Status | New Files | Modified Files | Depends On |
|
||||
| ---- | ------------------------------------------- | ----------- | --------- | -------------- | ------------- |
|
||||
| 4.1 | Consensus round start instrumentation | ✅ Done | 0 | 2 | Phase 3 |
|
||||
| 4.2 | Phase transition instrumentation | ✅ Done | 0 | 1-2 | 4.1 |
|
||||
| 4.3 | Proposal handling instrumentation | ✅ Done | 0 | 2 | 4.1 |
|
||||
| 4.4 | Validation handling instrumentation | ✅ Done | 0 | 2 | 4.1 |
|
||||
| 4.5 | Consensus-specific attributes | ✅ Done | 0 | 2 | 4.2, 4.3, 4.4 |
|
||||
| 4.6 | Transaction-consensus correlation | ✅ Done | 0 | 1 | 4.2, Phase 3 |
|
||||
| 4.7 | Build verification and testing | ✅ Done | 0 | 0 | 4.1-4.6 |
|
||||
| 4.8 | Validation span enrichment (ext. dashboard) | ❌ Not done | 0 | 2 | 4.4 |
|
||||
| 4.9 | Consensus span attribute gap fill | ✅ Done | 0 | 2 | 4.1-4.5 |
|
||||
|
||||
**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. Task 4.8 depends on 4.4 (validation spans must exist).
|
||||
|
||||
### Implemented Spans
|
||||
|
||||
| Span Name | Method | Key Attributes |
|
||||
| --------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `consensus.proposal.send` | `Adaptor::propose` | `xrpl.consensus.round`, `is_bow_out` |
|
||||
| `consensus.ledger_close` | `Adaptor::onClose` | `xrpl.ledger.seq`, `xrpl.consensus.mode` |
|
||||
| `consensus.accept` | `Adaptor::onAccept` | `proposers`, `round_time_ms`, `quorum`, `disputes_count`, `consensus_state` |
|
||||
| `consensus.accept.apply` | `Adaptor::doAccept` | `close_time`, `close_time_correct`, `close_resolution_ms`, `consensus_state`, `proposing`, `round_time_ms`, `xrpl.ledger.seq`, `parent_close_time`, `close_time_self`, `close_time_vote_bins`, `resolution_direction` |
|
||||
| `consensus.validation.send` | `Adaptor::onAccept` (via validate) | `proposing`, `ledger_hash`, `ledger_seq`, `full_validation`, `validation_sign_time` |
|
||||
|
||||
#### Close Time Attributes (consensus.accept.apply)
|
||||
|
||||
The `consensus.accept.apply` span captures ledger close time agreement details
|
||||
driven by `avCT_CONSENSUS_PCT` (75% validator agreement threshold):
|
||||
|
||||
- **`close_time`** — Agreed-upon ledger close time (epoch seconds). When validators disagree (`consensusCloseTime == epoch`), this is synthetically set to `prevCloseTime + 1s`.
|
||||
- **`close_time_correct`** — `true` if validators reached agreement, `false` if they "agreed to disagree" (close time forced to prev+1s).
|
||||
- **`close_resolution_ms`** — Rounding granularity for close time (starts at 30s, decreases as ledger interval stabilizes).
|
||||
- **`consensus_state`** — `"finished"` (normal) or `"moved_on"` (consensus failed, adopted best available).
|
||||
- **`proposing`** — Whether this node was proposing.
|
||||
- **`round_time_ms`** — Total consensus round duration.
|
||||
- **`parent_close_time`** — Previous ledger's close time (epoch seconds). Enables computing close-time deltas across consecutive rounds without correlating separate spans.
|
||||
- **`close_time_self`** — This node's own proposed close time before consensus voting.
|
||||
- **`close_time_vote_bins`** — Number of distinct close-time vote bins from peer proposals. Higher values indicate less agreement among validators.
|
||||
- **`resolution_direction`** — Whether close-time resolution `"increased"` (coarser), `"decreased"` (finer), or stayed `"unchanged"` relative to the previous ledger.
|
||||
|
||||
**Exit Criteria** (from [06-implementation-phases.md §6.11.4](./06-implementation-phases.md)):
|
||||
|
||||
- [x] Complete consensus round traces
|
||||
- [x] Phase transitions visible (open, establish, close, accept)
|
||||
- [x] Proposals and validations traced — send and receive; relay deferred to Phase 4b
|
||||
- [x] Close time agreement tracked (per `avCT_CONSENSUS_PCT`)
|
||||
- [x] No impact on consensus timing
|
||||
- [x] Transaction-consensus correlation (Task 4.6) — `tx.included` events in doAccept
|
||||
- [ ] Validation span enrichment (Task 4.8) — not implemented
|
||||
|
||||
---
|
||||
|
||||
# Phase 4a: Establish-Phase Gap Fill & Cross-Node Correlation
|
||||
|
||||
> **Goal**: Fill tracing gaps in the consensus establish phase (disputes, convergence,
|
||||
> threshold escalation, mode changes) and establish cross-node correlation using a
|
||||
> deterministic shared trace ID derived from `previousLedger.id()`.
|
||||
>
|
||||
> **Approach**: Direct instrumentation in `Consensus.h` and `RCLConsensus.cpp`.
|
||||
> All spans use `SpanGuard` factory methods (`span()`, `hashSpan()`, `linkedSpan()`)
|
||||
> with `TraceCategory::Consensus` gating. Long-lived spans (round, establish) are
|
||||
> stored as `std::optional<SpanGuard>` class members. Short-lived scoped spans
|
||||
> (update_positions, check) are local variables. No macros are used — all tracing
|
||||
> is via direct `SpanGuard` API calls. `SpanGuard` compiles to no-ops when
|
||||
> telemetry is disabled.
|
||||
>
|
||||
> **Branch**: `pratik/otel-phase4-consensus-tracing`
|
||||
|
||||
## Design: Switchable Correlation Strategy
|
||||
|
||||
Two strategies for cross-node trace correlation, switchable via config:
|
||||
|
||||
### Strategy A — Deterministic Trace ID (Default)
|
||||
|
||||
Derive `trace_id = SHA256(previousLedger.id())[0:16]` so all nodes in the same
|
||||
consensus round share the same trace_id without P2P context propagation.
|
||||
|
||||
- **Pros**: All nodes appear in the same trace in Tempo/Jaeger automatically.
|
||||
No collector-side post-processing needed.
|
||||
- **Cons**: Overrides OTel's random trace_id generation; requires custom
|
||||
`IdGenerator` or manual span context construction.
|
||||
|
||||
### Strategy B — Attribute-Based Correlation
|
||||
|
||||
Use normal random trace_id but attach `xrpl.consensus.ledger_id` as an attribute
|
||||
on every consensus span. Correlation happens at query time via Tempo/Grafana
|
||||
`by attribute` queries.
|
||||
|
||||
- **Pros**: Standard OTel trace_id semantics; no SDK customization.
|
||||
- **Cons**: Cross-node correlation requires query-time joins, not automatic.
|
||||
|
||||
### Config
|
||||
|
||||
```ini
|
||||
[telemetry]
|
||||
# "deterministic" (default) or "attribute"
|
||||
consensus_trace_strategy=deterministic
|
||||
```
|
||||
|
||||
The C++ API to query this at runtime is `Telemetry::getConsensusTraceStrategy()`,
|
||||
which returns a `std::string const&` (`"deterministic"` or `"attribute"`).
|
||||
|
||||
### Implementation
|
||||
|
||||
In `RCLConsensus::Adaptor::startRound()`:
|
||||
|
||||
- If `deterministic`:
|
||||
1. Compute `trace_id_bytes = SHA256(prevLedgerID)[0:16]`
|
||||
2. Construct `opentelemetry::trace::TraceId(trace_id_bytes)`
|
||||
3. Create a synthetic `SpanContext` with this trace_id and a random span_id:
|
||||
```cpp
|
||||
auto traceId = opentelemetry::trace::TraceId(trace_id_bytes);
|
||||
auto spanId = opentelemetry::trace::SpanId(random_8_bytes);
|
||||
auto syntheticCtx = opentelemetry::trace::SpanContext(
|
||||
traceId, spanId, opentelemetry::trace::TraceFlags(1), false);
|
||||
```
|
||||
4. Wrap in `opentelemetry::context::Context` via
|
||||
`opentelemetry::trace::SetSpan(context, syntheticSpan)`
|
||||
5. Call `startSpan("consensus.round", parentContext)` so the new span
|
||||
inherits the deterministic trace_id.
|
||||
- If `attribute`: start a normal `consensus.round` span, set
|
||||
`xrpl.consensus.ledger_id = previousLedger.id()` as attribute.
|
||||
|
||||
Both strategies always set `xrpl.consensus.round_id` (round number) and
|
||||
`xrpl.consensus.ledger_id` (previous ledger hash) as attributes.
|
||||
|
||||
---
|
||||
|
||||
## Design: Span Hierarchy
|
||||
|
||||
```
|
||||
consensus.round (root — created in RCLConsensus::startRound, closed at accept)
|
||||
│ link → previous round's SpanContext (follows-from)
|
||||
│
|
||||
├── consensus.establish (phaseEstablish → acceptance, in Consensus.h)
|
||||
│ ├── consensus.update_positions (each updateOurPositions call)
|
||||
│ │ └── consensus.dispute.resolve (per-tx dispute resolution event)
|
||||
│ ├── consensus.check (each haveConsensus call)
|
||||
│ └── consensus.mode_change (short-lived span in adaptor on mode transition)
|
||||
│
|
||||
├── consensus.accept (existing onAccept span — reparented under round)
|
||||
│
|
||||
└── consensus.validation.send (existing — reparented, follows-from link to round)
|
||||
```
|
||||
|
||||
### Span Links (follows-from relationships)
|
||||
|
||||
| Link Source | Link Target | Rationale |
|
||||
| ----------------------------------------- | -------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `consensus.round` (N+1) | `consensus.round` (N) | Causal chain: round N+1 exists because round N accepted |
|
||||
| `consensus.validation.send` | `consensus.round` | Validation follows from the round that produced it; may outlive the round span |
|
||||
| _(Phase 4b)_ Received proposal processing | Sender's `consensus.round` | Cross-node causal link via P2P context propagation |
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.0: Prerequisites — Extend SpanGuard and Telemetry APIs ✅
|
||||
|
||||
**Objective**: Add missing API surface needed by later tasks.
|
||||
|
||||
**Status**: Done, but implemented differently than originally planned. The macro-based
|
||||
approach (`XRPL_TRACE_CONSENSUS`, `XRPL_TRACE_ADD_EVENT`, `XRPL_TRACE_SET_ATTR`) was
|
||||
**not used**. Instead, all consensus tracing uses `SpanGuard` factory methods and
|
||||
direct method calls, which is cleaner and avoids macro control-flow issues.
|
||||
|
||||
**What was done**:
|
||||
|
||||
1. **`SpanGuard::addEvent()` with attributes** — implemented as planned:
|
||||
|
||||
```cpp
|
||||
using EventAttribute = std::pair<std::string_view, std::string_view>;
|
||||
|
||||
void addEvent(std::string_view name,
|
||||
std::initializer_list<EventAttribute> attrs);
|
||||
```
|
||||
|
||||
Callers pass plain `string_view` pairs; the implementation converts internally.
|
||||
|
||||
```cpp
|
||||
// Actual usage in Consensus.h::updateOurPositions():
|
||||
span.addEvent(
|
||||
"dispute.resolve",
|
||||
{{consensus::span::attr::txId, to_string(txId)},
|
||||
{consensus::span::attr::disputeOurVote, dispute.getOurVote() ? "yes" : "no"}});
|
||||
```
|
||||
|
||||
2. **Span link support** — implemented via `SpanGuard::linkedSpan()` static factory
|
||||
instead of a `Telemetry::startSpan()` overload:
|
||||
|
||||
```cpp
|
||||
static SpanGuard linkedSpan(
|
||||
std::string_view name, SpanContext const& linkTarget);
|
||||
```
|
||||
|
||||
3. **No macros added** — `TracingInstrumentation.h` was not created. The `XRPL_TRACE_CONSENSUS`,
|
||||
`XRPL_TRACE_ADD_EVENT`, and `XRPL_TRACE_SET_ATTR` macros from the original plan were
|
||||
not implemented. All consensus tracing uses direct `SpanGuard` API:
|
||||
- `SpanGuard::span()` — create scoped spans
|
||||
- `SpanGuard::hashSpan()` — create spans with deterministic trace IDs
|
||||
- `SpanGuard::linkedSpan()` — create spans with follows-from links
|
||||
- `span.setAttribute()` — set attributes directly
|
||||
- `span.addEvent()` — add events directly
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `include/xrpl/telemetry/SpanGuard.h` — `addEvent()` overload, `EventAttribute` type alias
|
||||
- `src/libxrpl/telemetry/SpanGuard.cpp` — `addEvent()` implementation
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.1: Adaptor `getTelemetry()` Method — NOT DONE (Not Needed)
|
||||
|
||||
**Objective**: Give `Consensus.h` access to the telemetry subsystem without
|
||||
coupling the generic template to OTel headers.
|
||||
|
||||
**Status**: Not implemented as specified. The `getTelemetry()` adaptor method was
|
||||
not needed because `SpanGuard::span()` is a static factory method that internally
|
||||
checks telemetry state via the global `Telemetry` singleton. `Consensus.h` creates
|
||||
spans by calling `SpanGuard::span(TraceCategory::Consensus, ...)` directly, without
|
||||
needing adaptor access. Only `RCLConsensus::Adaptor` uses `app_.getTelemetry()`
|
||||
directly (for `getConsensusTraceStrategy()` in `startRoundTracing()`).
|
||||
|
||||
**Key insight**: The `XRPL_TRACE_*` macro approach would have required
|
||||
`adaptor_.getTelemetry()`. Since macros were not used, this task became unnecessary.
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.2: Switchable Round Span with Deterministic Trace ID ✅
|
||||
|
||||
**Objective**: Create a `consensus.round` root span in `startRound()` that uses
|
||||
the switchable correlation strategy. Store span context as a member for child
|
||||
spans in `Consensus.h`.
|
||||
|
||||
**Status**: Done. Implemented in `Adaptor::startRoundTracing()`.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- `RCLConsensus::Adaptor::startRoundTracing()` helper:
|
||||
- Reads `consensus_trace_strategy` via `app_.getTelemetry().getConsensusTraceStrategy()`
|
||||
- **Deterministic**: uses `SpanGuard::hashSpan()` with `prevLgr.id()` data
|
||||
- **Attribute**: uses `SpanGuard::span(TraceCategory::Consensus, seg::consensus, "round")`
|
||||
- Sets attributes: `xrpl.consensus.ledger_id`, `xrpl.ledger.seq`, `xrpl.consensus.mode`, `trace_strategy`, `xrpl.consensus.round_id`
|
||||
- Captures `roundSpanContext_` snapshot for cross-thread span linking
|
||||
- Saves `prevRoundContext_` from previous round for follows-from links
|
||||
|
||||
- **`SpanGuard::hashSpan()` factory**: encapsulates deterministic trace ID logic:
|
||||
|
||||
```cpp
|
||||
static SpanGuard hashSpan(
|
||||
TraceCategory cat, std::string_view name,
|
||||
std::uint8_t const* hashData, std::size_t hashSize);
|
||||
```
|
||||
|
||||
Derives `trace_id = hashData[0:16]` so all nodes in the same round share
|
||||
the same trace_id. Compiles to no-op when telemetry is disabled.
|
||||
|
||||
- `consensus_trace_strategy` config parsed in `TelemetryConfig.cpp`,
|
||||
stored in `Telemetry::Setup`, accessible via `Telemetry::getConsensusTraceStrategy()`
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp` — `startRoundTracing()` implementation
|
||||
- `src/xrpld/app/consensus/ConsensusSpanNames.h` — **(new)** compile-time span name and attribute key constants
|
||||
- `include/xrpl/telemetry/Telemetry.h` — `consensusTraceStrategy` in Setup, `getConsensusTraceStrategy()`
|
||||
- `src/libxrpl/telemetry/TelemetryConfig.cpp` — parse new config option
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.3: Span Members in `Consensus.h` ✅
|
||||
|
||||
**Objective**: Add span storage to the `Consensus` class so that spans created
|
||||
in `startRound()` (adaptor) are accessible from `phaseEstablish()`,
|
||||
`updateOurPositions()`, and `haveConsensus()` (template methods).
|
||||
|
||||
**Status**: Done with documented plan deviation.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- `establishSpan_` added to `Consensus` private members (as planned):
|
||||
|
||||
```cpp
|
||||
std::optional<xrpl::telemetry::SpanGuard> establishSpan_;
|
||||
```
|
||||
|
||||
- **Plan deviation**: `roundSpan_`, `prevRoundContext_`, and `roundSpanContext_`
|
||||
are stored in `RCLConsensus::Adaptor` (not `Consensus.h`) because the adaptor
|
||||
has access to telemetry config for the deterministic trace ID strategy.
|
||||
|
||||
- **No `#ifdef XRPL_ENABLE_TELEMETRY` guards**: Members use `std::optional<SpanGuard>`
|
||||
and `SpanContext` which have no-op implementations when telemetry is disabled,
|
||||
so `#ifdef` guards are unnecessary. The members are always present in the class
|
||||
layout but incur negligible overhead.
|
||||
|
||||
- Includes added unconditionally to `Consensus.h`:
|
||||
```cpp
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpld/app/consensus/ConsensusSpanNames.h>
|
||||
```
|
||||
No `TracingInstrumentation.h` include (file doesn't exist; macros not used).
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/consensus/Consensus.h`
|
||||
- `src/xrpld/app/consensus/RCLConsensus.h` (round span and context members)
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.4: Instrument `phaseEstablish()` ✅
|
||||
|
||||
**Objective**: Create `consensus.establish` span wrapping the establish phase,
|
||||
with attributes for convergence progress.
|
||||
|
||||
**Status**: Done. Implemented via three private helpers in `Consensus.h`.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- `startEstablishTracing()` — creates `consensus.establish` span via
|
||||
`SpanGuard::span(TraceCategory::Consensus, seg::consensus, "establish")`.
|
||||
Called once at start of establish phase. No `#ifdef` guards needed —
|
||||
`SpanGuard::span()` returns a no-op guard when telemetry is disabled.
|
||||
|
||||
- `updateEstablishTracing()` — sets attributes on each `phaseEstablish()` call:
|
||||
- `converge_percent` — `convergePercent_`
|
||||
- `establish_count` — `establishCounter_`
|
||||
- `proposers` — `currPeerPositions_.size()`
|
||||
|
||||
- `endEstablishTracing()` — calls `establishSpan_.reset()` on phase exit.
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/consensus/Consensus.h` — `phaseEstablish()` method + 3 helper methods
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.5: Instrument `updateOurPositions()` ✅
|
||||
|
||||
**Objective**: Trace each position update cycle including dispute resolution
|
||||
details.
|
||||
|
||||
**Status**: DONE. Span, dispute events with yays/nays, and disputes_count attribute are all implemented.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- Creates `consensus.update_positions` scoped span via
|
||||
`SpanGuard::span(TraceCategory::Consensus, seg::consensus, "update_positions")`:
|
||||
|
||||
```cpp
|
||||
auto span = SpanGuard::span(TraceCategory::Consensus, seg::consensus, "update_positions");
|
||||
```
|
||||
|
||||
- Attributes set:
|
||||
- `converge_percent` — current convergence
|
||||
- `proposers` — `currPeerPositions_.size()`
|
||||
- `have_close_time_consensus` — close time consensus state
|
||||
- `close_time_threshold` — `avCT_CONSENSUS_PCT`
|
||||
- `disputes_count` — number of active disputes
|
||||
|
||||
- Dispute events recorded via direct `span.addEvent()` call with yays/nays:
|
||||
```cpp
|
||||
span.addEvent(
|
||||
"dispute.resolve",
|
||||
{{consensus::span::attr::txId, to_string(txId)},
|
||||
{consensus::span::attr::disputeOurVote, dispute.getOurVote() ? "yes" : "no"},
|
||||
{consensus::span::attr::disputeYays, std::to_string(dispute.getYays())},
|
||||
{consensus::span::attr::disputeNays, std::to_string(dispute.getNays())}});
|
||||
```
|
||||
|
||||
**Not implemented**:
|
||||
|
||||
- `proposers_agreed` / `proposers_total` attributes — not set
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/consensus/Consensus.h` — `updateOurPositions()` method
|
||||
- `src/xrpld/consensus/DisputedTx.h` — added `getYays()` / `getNays()` (currently unused)
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.6: Instrument `haveConsensus()` (Threshold & Convergence) ✅
|
||||
|
||||
**Objective**: Trace consensus checking including threshold escalation.
|
||||
|
||||
**Status**: DONE. The `consensus.check` span is created with all planned attributes
|
||||
including the avalanche threshold.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- Creates `consensus.check` scoped span via
|
||||
`SpanGuard::span(TraceCategory::Consensus, seg::consensus, "check")`:
|
||||
|
||||
```cpp
|
||||
auto span = SpanGuard::span(TraceCategory::Consensus, seg::consensus, "check");
|
||||
```
|
||||
|
||||
- Attributes set:
|
||||
- `agree_count` — peers that agree with our position
|
||||
- `disagree_count` — peers that disagree
|
||||
- `converge_percent` — convergence percentage
|
||||
- `have_close_time_consensus` — close time consensus state
|
||||
- `threshold_percent` — set to `avCT_CONSENSUS_PCT` (75%)
|
||||
- `consensus_result` — "yes", "no", or "moved_on"
|
||||
- `avalanche_threshold` — the escalated weight from `getNeededWeight()` on the `consensus.update_positions` span
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/consensus/Consensus.h` — `haveConsensus()` method
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.7: Instrument Mode Changes ✅
|
||||
|
||||
**Objective**: Trace consensus mode transitions (proposing ↔ observing,
|
||||
wrongLedger, switchedLedger).
|
||||
|
||||
**Status**: Done.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- In `RCLConsensus::Adaptor::onModeChange()`, creates a scoped span via direct
|
||||
`SpanGuard::span()` call:
|
||||
|
||||
```cpp
|
||||
auto span = telemetry::SpanGuard::span(
|
||||
telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "mode_change");
|
||||
span.setAttribute(consensus::span::attr::modeOld, to_string(before).c_str()); // "mode_old"
|
||||
span.setAttribute(consensus::span::attr::modeNew, to_string(after).c_str()); // "mode_new"
|
||||
```
|
||||
|
||||
- `MonitoredMode::set()` in `Consensus.h` calls `adaptor_.onModeChange(before, after)`.
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp` — `onModeChange()`
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.8: Reparent Existing Spans Under Round ✅
|
||||
|
||||
**Objective**: Make existing consensus spans (`consensus.accept`,
|
||||
`consensus.accept.apply`, `consensus.validation.send`) children of the
|
||||
`consensus.round` root span instead of being standalone.
|
||||
|
||||
**Status**: DONE. All three spans are now parented under the round span.
|
||||
|
||||
**What was done**:
|
||||
|
||||
- `consensus.validation.send` uses `SpanGuard::linkedSpan()` to create a
|
||||
follows-from link to `roundSpanContext_`. This is thread-safe because
|
||||
`roundSpanContext_` is a lightweight `SpanContext` snapshot captured on the
|
||||
consensus thread and read on the jtACCEPT worker thread.
|
||||
|
||||
- `consensus.accept` and `consensus.accept.apply` now use
|
||||
`SpanGuard::childSpan(name, roundSpanContext_)` instead of `SpanGuard::span()`
|
||||
to explicitly parent under the round span context. This solves the cross-thread
|
||||
parenting problem:
|
||||
- `doAccept()` runs on the jtACCEPT worker thread (not the consensus thread)
|
||||
- `childSpan()` explicitly passes the parent context, bypassing OTel's
|
||||
thread-local context propagation
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Task 4a.9: Build Verification and Testing ✅
|
||||
|
||||
**Objective**: Verify all Phase 4a changes compile cleanly with telemetry ON
|
||||
and OFF, and don't affect consensus timing.
|
||||
|
||||
**What to do**:
|
||||
|
||||
1. Build with `telemetry=ON` — verify no compilation errors
|
||||
2. Build with `telemetry=OFF` — verify `SpanGuard` compiles to no-ops
|
||||
3. Run existing consensus unit tests
|
||||
4. Verify `SpanGuard` / `SpanContext` members have negligible overhead when disabled
|
||||
5. Run `pccl` pre-commit checks
|
||||
|
||||
**Verification Checklist**:
|
||||
|
||||
- [x] Build succeeds with telemetry ON
|
||||
- [x] Build succeeds with telemetry OFF
|
||||
- [x] Existing consensus tests pass
|
||||
- [x] `SpanGuard` no-op path verified (no `#ifdef` needed — disabled at runtime)
|
||||
- [x] No new virtual calls in hot consensus paths
|
||||
- [x] `pccl` passes
|
||||
|
||||
---
|
||||
|
||||
## Phase 4a Summary
|
||||
|
||||
| Task | Description | Status | New Files | Modified Files | Depends On |
|
||||
| ---- | ------------------------------------------------ | ------------------------ | --------- | -------------- | ---------- |
|
||||
| 4a.0 | Prerequisites: extend SpanGuard & Telemetry APIs | ✅ Done (no macros) | 0 | 2 | Phase 4 |
|
||||
| 4a.1 | Adaptor `getTelemetry()` method | ⏭️ Skipped (not needed) | 0 | 0 | Phase 4 |
|
||||
| 4a.2 | Switchable round span with deterministic traceID | ✅ Done | 1 | 3 | 4a.0 |
|
||||
| 4a.3 | Span members in `Consensus.h` | ✅ Done (with deviation) | 0 | 2 | — |
|
||||
| 4a.4 | Instrument `phaseEstablish()` | ✅ Done | 0 | 1 | 4a.3 |
|
||||
| 4a.5 | Instrument `updateOurPositions()` | ✅ Done | 0 | 2 | 4a.0, 4a.3 |
|
||||
| 4a.6 | Instrument `haveConsensus()` (thresholds) | ✅ Done | 0 | 1 | 4a.3 |
|
||||
| 4a.7 | Instrument mode changes | ✅ Done | 0 | 1 | — |
|
||||
| 4a.8 | Reparent existing spans under round | ✅ Done | 0 | 1 | 4a.0, 4a.2 |
|
||||
| 4a.9 | Build verification and testing | ✅ Done | 0 | 0 | 4a.0-4a.8 |
|
||||
|
||||
**Parallel work**: Tasks 4a.0 and 4a.1 can run in parallel. Tasks 4a.4, 4a.5, 4a.6, and 4a.7 can run in parallel after 4a.3 (and 4a.0 for 4a.5).
|
||||
|
||||
### New Spans (Phase 4a)
|
||||
|
||||
| Span Name | Location | Key Attributes (actually set) |
|
||||
| ---------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `consensus.round` | `RCLConsensus.cpp` | `xrpl.consensus.round_id`, `xrpl.consensus.ledger_id`, `xrpl.ledger.seq`, `xrpl.consensus.mode`, `trace_strategy` |
|
||||
| `consensus.establish` | `Consensus.h` | `converge_percent`, `establish_count`, `proposers` |
|
||||
| `consensus.update_positions` | `Consensus.h` | `converge_percent`, `proposers`, `have_close_time_consensus`, `close_time_threshold`, `disputes_count`, `avalanche_threshold` |
|
||||
| `consensus.check` | `Consensus.h` | `agree_count`, `disagree_count`, `converge_percent`, `have_close_time_consensus`, `threshold_percent`, `consensus_result` |
|
||||
| `consensus.mode_change` | `RCLConsensus.cpp` | `mode_old`, `mode_new` |
|
||||
|
||||
### New Events (Phase 4a)
|
||||
|
||||
| Event Name | Parent Span | Attributes (actually set) |
|
||||
| ----------------- | ---------------------------- | ---------------------------------------------------------------- |
|
||||
| `dispute.resolve` | `consensus.update_positions` | `xrpl.tx.id`, `dispute_our_vote`, `dispute_yays`, `dispute_nays` |
|
||||
| `tx.included` | `consensus.accept.apply` | `xrpl.tx.id` |
|
||||
|
||||
### New Attributes (Phase 4a)
|
||||
|
||||
```cpp
|
||||
// Round-level (on consensus.round) — ALL IMPLEMENTED
|
||||
"xrpl.consensus.round_id" = int64 // Consensus round number
|
||||
"xrpl.consensus.ledger_id" = string // previousLedger.id() hash
|
||||
"trace_strategy" = string // "deterministic" or "attribute"
|
||||
|
||||
// Establish-level — IMPLEMENTED
|
||||
"converge_percent" = int64 // Convergence % (0-100+)
|
||||
"establish_count" = int64 // Number of establish iterations
|
||||
"agree_count" = int64 // Peers that agree (haveConsensus)
|
||||
"disagree_count" = int64 // Peers that disagree
|
||||
"threshold_percent" = int64 // Current threshold (avCT_CONSENSUS_PCT = 75%)
|
||||
"consensus_result" = string // "yes", "no", "moved_on"
|
||||
"have_close_time_consensus" = bool // Close time consensus reached
|
||||
"close_time_threshold" = int64 // Close time voting threshold
|
||||
|
||||
// Establish-level — IMPLEMENTED
|
||||
"disputes_count" = int64 // Active disputes (on update_positions)
|
||||
"avalanche_threshold" = int64 // Escalated weight (on update_positions)
|
||||
|
||||
// Establish-level — NOT IMPLEMENTED
|
||||
// "proposers_agreed" = int64 // Peers agreeing with us — not set
|
||||
// "proposers_total" = int64 // Total peer positions — not set (not defined)
|
||||
|
||||
// Mode change — ALL IMPLEMENTED
|
||||
"mode_old" = string // Previous mode
|
||||
"mode_new" = string // New mode
|
||||
```
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- **No macros**: The planned `XRPL_TRACE_CONSENSUS`, `XRPL_TRACE_ADD_EVENT`, and
|
||||
`XRPL_TRACE_SET_ATTR` macros were not implemented. All consensus tracing uses
|
||||
`SpanGuard` factory methods (`span()`, `hashSpan()`, `linkedSpan()`) and direct
|
||||
method calls (`setAttribute()`, `addEvent()`). This avoids macro control-flow
|
||||
issues and is cleaner than the planned approach.
|
||||
- **Separation of concerns**: All non-trivial telemetry code extracted to private
|
||||
helpers (`startRoundTracing`, `createValidationSpan`, `startEstablishTracing`,
|
||||
`updateEstablishTracing`, `endEstablishTracing`). Business logic methods contain
|
||||
single-line calls to these helpers.
|
||||
- **Thread safety**: `createValidationSpan()` runs on the jtACCEPT worker thread.
|
||||
Instead of accessing `roundSpan_` across threads, a `roundSpanContext_` snapshot
|
||||
(lightweight `SpanContext` value type) is captured on the consensus thread in
|
||||
`startRoundTracing()` and read by `createValidationSpan()`. The job queue
|
||||
provides the happens-before guarantee.
|
||||
- **No `#ifdef` guards**: Span members use `std::optional<SpanGuard>` and `SpanContext`
|
||||
which have no-op implementations when telemetry is disabled. No `#ifdef XRPL_ENABLE_TELEMETRY`
|
||||
guards needed around members or includes.
|
||||
- **No `getTelemetry()` adaptor method**: `SpanGuard::span()` is a static factory that
|
||||
internally checks telemetry state, so `Consensus.h` doesn't need adaptor access
|
||||
for span creation. Only `RCLConsensus::Adaptor` accesses `app_.getTelemetry()` directly.
|
||||
- **Config validation**: `consensus_trace_strategy` is validated to be either
|
||||
`"deterministic"` or `"attribute"`, falling back to `"deterministic"` for
|
||||
unrecognised values.
|
||||
- **Plan deviation**: `roundSpan_` is stored in `RCLConsensus::Adaptor` (not
|
||||
`Consensus.h`) because the adaptor has access to telemetry config and can
|
||||
implement the deterministic trace ID strategy. `establishSpan_` is correctly
|
||||
in `Consensus.h` as planned.
|
||||
|
||||
---
|
||||
|
||||
# Phase 4b: Cross-Node Propagation (Future — Documentation Only)
|
||||
|
||||
> **Goal**: Wire `TraceContextPropagator` for P2P messages so that proposals
|
||||
> and validations carry trace context between nodes. This enables true
|
||||
> distributed tracing where a proposal sent by Node A creates a child span
|
||||
> on Node B.
|
||||
>
|
||||
> **Status**: NOT IMPLEMENTED. The protobuf fields and propagator class exist
|
||||
> but are not wired. This section documents the design for future work.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Node A (proposing) Node B (receiving)
|
||||
───────────────── ──────────────────
|
||||
consensus.round consensus.round
|
||||
├── propose() ├── peerProposal()
|
||||
│ └── TraceContextPropagator │ └── TraceContextPropagator
|
||||
│ ::injectToProtobuf( │ ::extractFromProtobuf(
|
||||
│ TMProposeSet.trace_context) │ TMProposeSet.trace_context)
|
||||
│ │ └── span link → Node A's context
|
||||
└── validate() └── onValidation()
|
||||
└── inject into TMValidation └── extract from TMValidation
|
||||
```
|
||||
|
||||
## Wiring Points
|
||||
|
||||
| Message | Inject Location | Extract Location | Protobuf Field |
|
||||
| --------------- | ---------------------------------- | ----------------------------------- | -------------------------- |
|
||||
| `TMProposeSet` | `Adaptor::propose()` | `PeerImp::onMessage(TMProposeSet)` | field 1001: `TraceContext` |
|
||||
| `TMValidation` | `Adaptor::validate()` | `PeerImp::onMessage(TMValidation)` | field 1001: `TraceContext` |
|
||||
| `TMTransaction` | `NetworkOPs::processTransaction()` | `PeerImp::onMessage(TMTransaction)` | field 1001: `TraceContext` |
|
||||
|
||||
## Span Link Semantics
|
||||
|
||||
Received messages use **span links** (follows-from), NOT parent-child:
|
||||
|
||||
- The receiver's processing span links to the sender's context
|
||||
- This preserves each node's independent trace tree
|
||||
- Cross-node correlation visible via linked traces in Tempo/Jaeger
|
||||
|
||||
## Interaction with Deterministic Trace ID (Strategy A)
|
||||
|
||||
When using deterministic trace_id (Phase 4a default), cross-node spans already
|
||||
share the same trace_id. P2P propagation adds **span-level** linking:
|
||||
|
||||
- Without propagation: spans from different nodes appear in the same trace
|
||||
(same trace_id) but without parent-child or follows-from relationships.
|
||||
- With propagation: spans have explicit links showing which proposal/validation
|
||||
from Node A caused processing on Node B.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Phase 4a (this task list) — establish phase tracing must be in place
|
||||
- `TraceContextPropagator` free functions (already exist in
|
||||
`include/xrpl/telemetry/TraceContextPropagator.h`)
|
||||
- Protobuf `TraceContext` message (already exists, field 1001)
|
||||
221
OpenTelemetryPlan/Phase5_IntegrationTest_taskList.md
Normal file
221
OpenTelemetryPlan/Phase5_IntegrationTest_taskList.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Phase 5: Integration Test Task List
|
||||
|
||||
> **Goal**: End-to-end verification of the complete telemetry pipeline using a
|
||||
> 6-node consensus network. Proves that RPC, transaction, and consensus spans
|
||||
> flow through the observability stack (otel-collector, Tempo, Prometheus,
|
||||
> Grafana) under realistic conditions.
|
||||
>
|
||||
> **Scope**: Integration test script, manual testing plan, 6-node local network
|
||||
> setup, Tempo/Prometheus/Grafana verification.
|
||||
>
|
||||
> **Branch**: `pratik/otel-phase5-docs-deployment`
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------ |
|
||||
| [07-observability-backends.md](./07-observability-backends.md) | Tempo, Grafana, Prometheus setup |
|
||||
| [05-configuration-reference.md](./05-configuration-reference.md) | Collector config, Docker Compose |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 5 tasks, definition of done |
|
||||
| [Phase5_taskList.md](./Phase5_taskList.md) | Phase 5 main task list (5.6 = integration) |
|
||||
|
||||
---
|
||||
|
||||
## Task IT.1: Create Integration Test Script
|
||||
|
||||
**Objective**: Automated bash script that stands up a 6-node xrpld network
|
||||
with telemetry, exercises all span categories, and verifies data in
|
||||
Tempo/Prometheus.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `docker/telemetry/integration-test.sh`:
|
||||
- Prerequisites check (docker, xrpld binary, curl, jq)
|
||||
- Start observability stack via `docker compose`
|
||||
- Generate 6 validator key pairs via temp standalone xrpld
|
||||
- Generate 6 node configs + shared `validators.txt`
|
||||
- Start 6 xrpld nodes in consensus mode (`--start`, no `-a`)
|
||||
- Wait for all nodes to reach `"proposing"` state (120s timeout)
|
||||
|
||||
**Key new file**: `docker/telemetry/integration-test.sh`
|
||||
|
||||
**Verification**:
|
||||
|
||||
- [ ] Script starts without errors
|
||||
- [ ] All 6 nodes reach "proposing" state
|
||||
- [ ] Observability stack is healthy (otel-collector, Tempo, Prometheus, Grafana)
|
||||
|
||||
---
|
||||
|
||||
## Task IT.2: RPC Span Verification (Phase 2)
|
||||
|
||||
**Objective**: Verify RPC spans flow through the telemetry pipeline.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Send `server_info`, `server_state`, `ledger` RPCs to node1 (port 5005)
|
||||
- Wait for batch export (5s)
|
||||
- Query Tempo API for:
|
||||
- `rpc.request` spans (ServerHandler::onRequest)
|
||||
- `rpc.process` spans (ServerHandler::processRequest)
|
||||
- `rpc.command.server_info` spans (callMethod)
|
||||
- `rpc.command.server_state` spans (callMethod)
|
||||
- `rpc.command.ledger` spans (callMethod)
|
||||
- Verify `command` attribute present on `rpc.command.*` spans
|
||||
|
||||
**Verification**:
|
||||
|
||||
- [ ] Tempo shows `rpc.request` traces
|
||||
- [ ] Tempo shows `rpc.process` traces
|
||||
- [ ] Tempo shows `rpc.command.*` traces with correct attributes
|
||||
|
||||
---
|
||||
|
||||
## Task IT.3: Transaction Span Verification (Phase 3)
|
||||
|
||||
**Objective**: Verify transaction spans flow through the telemetry pipeline.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Get genesis account sequence via `account_info` RPC
|
||||
- Submit Payment transaction using genesis seed (`snoPBrXtMeMyMHUVTgbuqAfg1SUTb`)
|
||||
- Wait for consensus inclusion (10s)
|
||||
- Query Tempo API for:
|
||||
- `tx.process` spans (NetworkOPsImp::processTransaction) on submitting node
|
||||
- `tx.receive` spans (PeerImp::handleTransaction) on peer nodes
|
||||
- Verify `xrpl.tx.hash` attribute on `tx.process` spans
|
||||
- Verify `xrpl.peer.id` attribute on `tx.receive` spans
|
||||
|
||||
**Verification**:
|
||||
|
||||
- [ ] Tempo shows `tx.process` traces with `xrpl.tx.hash`
|
||||
- [ ] Tempo shows `tx.receive` traces with `xrpl.peer.id`
|
||||
|
||||
---
|
||||
|
||||
## Task IT.4: Consensus Span Verification (Phase 4)
|
||||
|
||||
**Objective**: Verify consensus spans flow through the telemetry pipeline.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Consensus runs automatically in 6-node network
|
||||
- Query Tempo API for:
|
||||
- `consensus.proposal.send` (Adaptor::propose)
|
||||
- `consensus.ledger_close` (Adaptor::onClose)
|
||||
- `consensus.accept` (Adaptor::onAccept)
|
||||
- `consensus.validation.send` (Adaptor::validate)
|
||||
- Verify attributes:
|
||||
- `xrpl.consensus.mode` on `consensus.ledger_close`
|
||||
- `proposers` on `consensus.accept`
|
||||
- `xrpl.consensus.ledger.seq` on `consensus.validation.send`
|
||||
|
||||
**Verification**:
|
||||
|
||||
- [ ] Tempo shows `consensus.ledger_close` traces with `xrpl.consensus.mode`
|
||||
- [ ] Tempo shows `consensus.accept` traces with `proposers`
|
||||
- [ ] Tempo shows `consensus.proposal.send` traces
|
||||
- [ ] Tempo shows `consensus.validation.send` traces
|
||||
|
||||
---
|
||||
|
||||
## Task IT.5: Spanmetrics Verification (Phase 5)
|
||||
|
||||
**Objective**: Verify spanmetrics connector derives RED metrics from spans.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Query Prometheus for `traces_span_metrics_calls_total`
|
||||
- Query Prometheus for `traces_span_metrics_duration_milliseconds_count`
|
||||
- Verify Grafana loads at `http://localhost:3000`
|
||||
|
||||
**Verification**:
|
||||
|
||||
- [ ] Prometheus returns non-empty results for `traces_span_metrics_calls_total`
|
||||
- [ ] Prometheus returns non-empty results for duration histogram
|
||||
- [ ] Grafana UI accessible with dashboards visible
|
||||
|
||||
---
|
||||
|
||||
## Task IT.6: Manual Testing Plan
|
||||
|
||||
**Objective**: Document how to run tests manually for future reference.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `docker/telemetry/TESTING.md` with:
|
||||
- Prerequisites section
|
||||
- Single-node standalone test (quick verification)
|
||||
- 6-node consensus test (full verification)
|
||||
- Expected span catalog (all 11 span names with attributes)
|
||||
- Verification queries (Tempo API, Prometheus API)
|
||||
- Troubleshooting guide
|
||||
|
||||
**Key new file**: `docker/telemetry/TESTING.md`
|
||||
|
||||
**Verification**:
|
||||
|
||||
- [ ] Document covers both single-node and multi-node testing
|
||||
- [ ] All 11 span names documented with source file and attributes
|
||||
- [ ] Troubleshooting section covers common failure modes
|
||||
|
||||
---
|
||||
|
||||
## Task IT.7: Run and Verify
|
||||
|
||||
**Objective**: Execute the integration test and validate results.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Run `docker/telemetry/integration-test.sh` locally
|
||||
- Debug any failures
|
||||
- Leave stack running for manual verification
|
||||
- Share URLs:
|
||||
- Tempo: `http://localhost:3200`
|
||||
- Grafana: `http://localhost:3000`
|
||||
- Prometheus: `http://localhost:9090`
|
||||
|
||||
**Verification**:
|
||||
|
||||
- [ ] Script completes with all checks passing
|
||||
- [ ] Tempo UI shows xrpld service with all expected span names
|
||||
- [ ] Grafana dashboards load and show data
|
||||
|
||||
---
|
||||
|
||||
## Task IT.8: Commit
|
||||
|
||||
**Objective**: Commit all new files to Phase 5 branch.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Run `pcc` (pre-commit checks)
|
||||
- Commit 3 new files to `pratik/otel-phase5-docs-deployment`
|
||||
|
||||
**Verification**:
|
||||
|
||||
- [ ] `pcc` passes
|
||||
- [ ] Commit created on Phase 5 branch
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Description | New Files | Depends On |
|
||||
| ---- | ----------------------------- | --------- | ---------- |
|
||||
| IT.1 | Integration test script | 1 | Phase 5 |
|
||||
| IT.2 | RPC span verification | 0 | IT.1 |
|
||||
| IT.3 | Transaction span verification | 0 | IT.1 |
|
||||
| IT.4 | Consensus span verification | 0 | IT.1 |
|
||||
| IT.5 | Spanmetrics verification | 0 | IT.1 |
|
||||
| IT.6 | Manual testing plan | 1 | -- |
|
||||
| IT.7 | Run and verify | 0 | IT.1-IT.6 |
|
||||
| IT.8 | Commit | 0 | IT.7 |
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] All 6 xrpld nodes reach "proposing" state
|
||||
- [ ] All 11 expected span names visible in Tempo
|
||||
- [ ] Spanmetrics available in Prometheus
|
||||
- [ ] Grafana dashboards show data
|
||||
- [ ] Manual testing plan document complete
|
||||
250
OpenTelemetryPlan/Phase5_taskList.md
Normal file
250
OpenTelemetryPlan/Phase5_taskList.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# 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`)
|
||||
|
||||
> **Note on attribute names**: the `xrpl.<domain>.<field>` keys shown below
|
||||
> (including the collector spanmetrics dimension examples) are written in the
|
||||
> older dotted form for readability — it mirrors how the fully qualified
|
||||
> attribute reads in a Tempo trace view. The implemented keys follow the
|
||||
> convention in [CONTRIBUTING.md](../CONTRIBUTING.md#telemetry-span-attribute-naming)
|
||||
> (underscore form, e.g. `command`, `rpc_status`); the `*SpanNames.h` constants
|
||||
> are the single source of truth, and the real collector dimensions must use
|
||||
> those exact underscore keys (the CI naming check enforces this).
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| ---------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| [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: command
|
||||
- name: rpc_status
|
||||
- name: consensus_phase
|
||||
- name: 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
|
||||
587
OpenTelemetryPlan/Phase7_taskList.md
Normal file
587
OpenTelemetryPlan/Phase7_taskList.md
Normal file
@@ -0,0 +1,587 @@
|
||||
# Phase 7: Native OTel Metrics Migration — Task List
|
||||
|
||||
> **Goal**: Replace `StatsDCollector` with a native OpenTelemetry Metrics SDK implementation behind the existing `beast::insight::Collector` interface, eliminating the StatsD UDP dependency.
|
||||
>
|
||||
> **Scope**: New `OTelCollectorImpl` class, `CollectorManager` config change, OTel Collector pipeline update, Grafana dashboard metric name migration, integration tests.
|
||||
>
|
||||
> **Branch**: `pratik/otel-phase7-native-metrics` (from `pratik/otel-phase6-statsd`)
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| -------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 7 plan: motivation, architecture, exit criteria (§6.8) |
|
||||
| [02-design-decisions.md](./02-design-decisions.md) | Collector interface design, beast::insight coexistence strategy |
|
||||
| [05-configuration-reference.md](./05-configuration-reference.md) | `[insight]` and `[telemetry]` config sections |
|
||||
| [09-data-collection-reference.md](./09-data-collection-reference.md) | Complete metric inventory that must be preserved |
|
||||
|
||||
---
|
||||
|
||||
## Task 7.1: Add OTel Metrics SDK to Build Dependencies
|
||||
|
||||
**Objective**: Enable the OTel C++ Metrics SDK components in the build system.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `conanfile.py`:
|
||||
- Add OTel metrics SDK components to the dependency list when `telemetry=True`
|
||||
- Components needed: `opentelemetry-cpp::metrics`, `opentelemetry-cpp::otlp_http_metric_exporter`
|
||||
|
||||
- Edit `CMakeLists.txt` (telemetry section):
|
||||
- Link `opentelemetry::metrics` and `opentelemetry::otlp_http_metric_exporter` targets
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `conanfile.py`
|
||||
- `CMakeLists.txt` (or the relevant telemetry cmake target)
|
||||
|
||||
**Reference**: [05-configuration-reference.md §5.3](./05-configuration-reference.md) — CMake integration
|
||||
|
||||
---
|
||||
|
||||
## Task 7.2: Implement OTelCollector Class
|
||||
|
||||
**Objective**: Create the core `OTelCollector` implementation that maps beast::insight instruments to OTel Metrics SDK instruments.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `include/xrpl/beast/insight/OTelCollector.h`:
|
||||
- Public factory: `static std::shared_ptr<OTelCollector> New(std::string const& endpoint, std::string const& prefix, beast::Journal journal)`
|
||||
- Derives from `StatsDCollector` (or directly from `Collector` — TBD based on shared code)
|
||||
|
||||
- Create `src/libxrpl/beast/insight/OTelCollector.cpp` (~400-500 lines):
|
||||
- **OTelCounterImpl**: Wraps `opentelemetry::metrics::Counter<int64_t>`. `increment(amount)` calls `counter->Add(amount)`.
|
||||
- **OTelGaugeImpl**: Uses `opentelemetry::metrics::ObservableGauge<uint64_t>` with an async callback. `set(value)` stores value atomically; callback reads it during collection.
|
||||
- **OTelMeterImpl**: Wraps `opentelemetry::metrics::Counter<uint64_t>`. `increment(amount)` calls `counter->Add(amount)`. Semantically identical to Counter but unsigned.
|
||||
- **OTelEventImpl**: Wraps `opentelemetry::metrics::Histogram<double>`. `notify(duration)` calls `histogram->Record(duration.count())`. Uses explicit bucket boundaries matching SpanMetrics: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 5000] ms.
|
||||
- **OTelHookImpl**: Stores handler function. Called during periodic metric collection (same 1s pattern via PeriodicMetricReader).
|
||||
- **OTelCollectorImp**: Main class.
|
||||
- Creates `MeterProvider` with `PeriodicMetricReader` (1s export interval)
|
||||
- Creates `OtlpHttpMetricExporter` pointing to `[telemetry]` endpoint
|
||||
- Sets resource attributes (service.name, service.instance.id) matching trace exporter
|
||||
- Implements all `make_*()` factory methods
|
||||
- Prefixes metric names with `[insight] prefix=` value
|
||||
|
||||
- Guard all OTel SDK includes with `#ifdef XRPL_ENABLE_TELEMETRY` to compile to `NullCollector` equivalents when telemetry disabled.
|
||||
|
||||
**Key new files**:
|
||||
|
||||
- `include/xrpl/beast/insight/OTelCollector.h`
|
||||
- `src/libxrpl/beast/insight/OTelCollector.cpp`
|
||||
|
||||
**Key patterns to follow**:
|
||||
|
||||
- Match `StatsDCollector.cpp` structure: private impl classes, intrusive list for metrics, strand-based thread safety
|
||||
- Match existing telemetry code style from `src/libxrpl/telemetry/Telemetry.cpp`
|
||||
- Use RAII for MeterProvider lifecycle (shutdown on destructor)
|
||||
|
||||
**Reference**: [04-code-samples.md](./04-code-samples.md) — code style and patterns
|
||||
|
||||
---
|
||||
|
||||
## Task 7.3: Update CollectorManager
|
||||
|
||||
**Objective**: Add `server=otel` config option to route metric creation to the new OTel backend.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/xrpld/app/main/CollectorManager.cpp`:
|
||||
- In the constructor, add a third branch after `server == "statsd"`:
|
||||
```cpp
|
||||
else if (server == "otel")
|
||||
{
|
||||
// Read endpoint from [telemetry] section
|
||||
auto const endpoint = get(telemetryParams, "endpoint",
|
||||
"http://localhost:4318/v1/metrics");
|
||||
std::string const& prefix(get(params, "prefix"));
|
||||
collector_ = beast::insight::OTelCollector::New(
|
||||
endpoint, prefix, journal);
|
||||
}
|
||||
```
|
||||
- This requires access to the `[telemetry]` config section — may need to pass it as a parameter or read from Application config.
|
||||
|
||||
- Edit `src/xrpld/app/main/CollectorManager.h`:
|
||||
- Add `#include <xrpl/beast/insight/OTelCollector.h>`
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/app/main/CollectorManager.cpp`
|
||||
- `src/xrpld/app/main/CollectorManager.h`
|
||||
|
||||
---
|
||||
|
||||
## Task 7.4: Update OTel Collector Configuration
|
||||
|
||||
**Objective**: Add a metrics pipeline to the OTLP receiver and remove the StatsD receiver dependency.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `docker/telemetry/otel-collector-config.yaml`:
|
||||
- Remove `statsd` receiver (no longer needed when `server=otel`)
|
||||
- Add metrics pipeline under `service.pipelines`:
|
||||
```yaml
|
||||
metrics:
|
||||
receivers: [otlp, spanmetrics]
|
||||
processors: [batch]
|
||||
exporters: [prometheus]
|
||||
```
|
||||
- The OTLP receiver already listens on :4318 — it just needs to be added to the metrics pipeline receivers.
|
||||
- Keep `spanmetrics` connector in the metrics pipeline so span-derived RED metrics continue working.
|
||||
|
||||
- Edit `docker/telemetry/docker-compose.yml`:
|
||||
- Remove UDP :8125 port mapping from otel-collector service
|
||||
- Update xrpld service config: change `[insight] server=statsd` to `server=otel`
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docker/telemetry/otel-collector-config.yaml`
|
||||
- `docker/telemetry/docker-compose.yml`
|
||||
|
||||
**Note**: Keep a commented-out `statsd` receiver block for operators who need backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
## Task 7.5: Preserve Metric Names in Prometheus
|
||||
|
||||
**Objective**: Ensure existing Grafana dashboards continue working with identical metric names.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- In `OTelCollector.cpp`, construct OTel instrument names to match existing Prometheus metric names:
|
||||
- beast::insight `make_gauge("LedgerMaster", "Validated_Ledger_Age")` → OTel instrument name: `xrpld_LedgerMaster_Validated_Ledger_Age`
|
||||
- The prefix + group + name concatenation must produce the same string as `StatsDCollector`'s format
|
||||
- Use underscores as separators (matching StatsD convention)
|
||||
|
||||
- Verify in integration test that key Prometheus queries still return data:
|
||||
- `xrpld_LedgerMaster_Validated_Ledger_Age`
|
||||
- `xrpld_Peer_Finder_Active_Inbound_Peers`
|
||||
- `xrpld_rpc_requests`
|
||||
|
||||
**Key consideration**: OTel Prometheus exporter may normalize metric names differently than StatsD receiver. Test this early (Task 7.2) and adjust naming strategy if needed. The OTel SDK's Prometheus exporter adds `_total` suffix to counters and converts dots to underscores — match existing conventions.
|
||||
|
||||
---
|
||||
|
||||
## Task 7.6: Update Grafana Dashboards
|
||||
|
||||
**Objective**: Update the 3 StatsD dashboards if any metric names change due to OTLP export format differences.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- If Task 7.5 confirms metric names are preserved exactly, no dashboard changes needed.
|
||||
- If OTLP export produces different names (e.g., `_total` suffix on counters), update:
|
||||
- `docker/telemetry/grafana/dashboards/statsd-node-health.json`
|
||||
- `docker/telemetry/grafana/dashboards/statsd-network-traffic.json`
|
||||
- `docker/telemetry/grafana/dashboards/statsd-rpc-pathfinding.json`
|
||||
- Rename dashboard titles from "StatsD" to "System Metrics" or similar (since they're no longer StatsD-sourced).
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docker/telemetry/grafana/dashboards/statsd-*.json` (3 files, conditionally)
|
||||
|
||||
---
|
||||
|
||||
## Task 7.7: Update Integration Tests
|
||||
|
||||
**Objective**: Verify the full OTLP metrics pipeline end-to-end.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `docker/telemetry/integration-test.sh`:
|
||||
- Update test config to use `[insight] server=otel`
|
||||
- Verify metrics arrive in Prometheus via OTLP (not StatsD)
|
||||
- Add check that StatsD receiver is no longer required
|
||||
- Preserve all existing metric presence checks
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docker/telemetry/integration-test.sh`
|
||||
|
||||
---
|
||||
|
||||
## Task 7.8: Update Documentation
|
||||
|
||||
**Objective**: Update all plan docs, runbook, and reference docs to reflect the migration.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `docs/telemetry-runbook.md`:
|
||||
- Update `[insight]` config examples to show `server=otel`
|
||||
- Update troubleshooting section (no more StatsD UDP debugging)
|
||||
|
||||
- Edit `OpenTelemetryPlan/09-data-collection-reference.md`:
|
||||
- Update Data Flow Overview diagram (remove StatsD receiver)
|
||||
- Update Section 2 header from "StatsD Metrics" to "System Metrics (OTel native)"
|
||||
- Update config examples
|
||||
|
||||
- Edit `OpenTelemetryPlan/05-configuration-reference.md`:
|
||||
- Add `server=otel` option to `[insight]` section docs
|
||||
|
||||
- Edit `docker/telemetry/TESTING.md`:
|
||||
- Update setup instructions to use `server=otel`
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docs/telemetry-runbook.md`
|
||||
- `OpenTelemetryPlan/09-data-collection-reference.md`
|
||||
- `OpenTelemetryPlan/05-configuration-reference.md`
|
||||
- `docker/telemetry/TESTING.md`
|
||||
|
||||
---
|
||||
|
||||
## Task 7.9: ValidationTracker — Validation Agreement Computation
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity) — the most valuable metric from the community [xrpl-validator-dashboard](https://github.com/realgrapedrop/xrpl-validator-dashboard).
|
||||
>
|
||||
> **Upstream**: Phase 4 Task 4.8 (validation span attributes provide ledger hash context).
|
||||
> **Downstream**: Phase 9 (Validator Health dashboard), Phase 10 (validation checks), Phase 11 (agreement alert rules).
|
||||
|
||||
**Objective**: Implement a stateful class that tracks whether our validator's validations agree with network consensus, maintaining rolling 1h and 24h windows with an 8-second grace period and 5-minute late repair window.
|
||||
|
||||
**Architecture**:
|
||||
|
||||
```
|
||||
consensus.validation.send ────> ValidationTracker ────> MetricsRegistry
|
||||
(records our validation (reconciles after (exports agreement
|
||||
for ledger X) 8s grace period) gauges every 10s)
|
||||
|
||||
ledger.validate ──────────────> ValidationTracker
|
||||
(records which ledger (marks ledger X as
|
||||
network validated) agreed or missed)
|
||||
```
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Create `src/xrpld/telemetry/ValidationTracker.h`:
|
||||
- `recordOurValidation(ledgerHash, ledgerSeq)` — called when we send a validation
|
||||
- `recordNetworkValidation(ledgerHash, seq)` — called when a ledger is fully validated
|
||||
- `reconcile()` — called periodically; reconciles pending ledger events after 8s grace period
|
||||
- Getters: `agreementPct1h()`, `agreementPct24h()`, `agreements1h()`, `missed1h()`, `agreements24h()`, `missed24h()`, `totalAgreements()`, `totalMissed()`, `totalValidationsSent()`, `totalValidationsChecked()`
|
||||
- Thread-safety: atomics for counters, mutex for window deques
|
||||
|
||||
- Create `src/xrpld/telemetry/detail/ValidationTracker.cpp`:
|
||||
- Reconciliation logic: after 8s grace period, check if `weValidated && networkValidated && sameHash` → agreement; else missed
|
||||
- Late repair: if a late validation arrives within 5 minutes, correct a false-positive miss
|
||||
- Sliding window: `std::deque<WindowEvent>` evicts entries older than 1h/24h on each reconciliation pass
|
||||
- Ring buffer of 1000 `LedgerEvent` structs for pending reconciliation
|
||||
|
||||
- Add recording hooks (modifying Phase 4 code from Phase 7 branch):
|
||||
- `RCLConsensus.cpp` `validate()`: call `tracker.recordOurValidation()`
|
||||
- `LedgerMaster.cpp` fully-validated path: call `tracker.recordNetworkValidation()`
|
||||
|
||||
**Key data structures**:
|
||||
|
||||
```cpp
|
||||
struct LedgerEvent {
|
||||
uint256 ledgerHash;
|
||||
LedgerIndex seq;
|
||||
TimePoint closeTime;
|
||||
bool weValidated = false;
|
||||
bool networkValidated = false;
|
||||
bool reconciled = false;
|
||||
bool agreed = false;
|
||||
};
|
||||
|
||||
struct WindowEvent {
|
||||
TimePoint time;
|
||||
bool agreed;
|
||||
};
|
||||
```
|
||||
|
||||
**Key new files**:
|
||||
|
||||
- `src/xrpld/telemetry/ValidationTracker.h`
|
||||
- `src/xrpld/telemetry/detail/ValidationTracker.cpp`
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/xrpld/telemetry/MetricsRegistry.h` (add ValidationTracker member)
|
||||
- `src/xrpld/telemetry/MetricsRegistry.cpp` (add gauge callback reading from tracker)
|
||||
- `src/xrpld/app/consensus/RCLConsensus.cpp` (add recording hooks)
|
||||
- `src/xrpld/app/ledger/detail/LedgerMaster.cpp` (add recording hook)
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] ValidationTracker correctly tracks agreement with 8s grace period
|
||||
- [ ] 5-minute late repair corrects false-positive misses
|
||||
- [ ] Thread-safe (atomics + mutex for window deques)
|
||||
- [ ] Rolling windows correctly evict stale entries
|
||||
- [ ] Unit tests: normal agreement, missed validation, late repair, window eviction
|
||||
|
||||
---
|
||||
|
||||
## Task 7.10: Validator Health Observable Gauges
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity)
|
||||
|
||||
**Objective**: Export amendment blocked, UNL health, and quorum data as a native OTel observable gauge.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- In `MetricsRegistry.cpp` `registerAsyncGauges()`, add:
|
||||
|
||||
```cpp
|
||||
validatorHealthGauge_ = meter_->CreateDoubleObservableGauge(
|
||||
"xrpld_validator_health", "Validator health indicators");
|
||||
```
|
||||
|
||||
**Gauge label values**:
|
||||
|
||||
| Label `metric=` | Type | Source |
|
||||
| ------------------- | ------ | ------------------------------------------------- |
|
||||
| `amendment_blocked` | int64 | `app_.getOPs().isAmendmentBlocked()` → 0/1 |
|
||||
| `unl_blocked` | int64 | `app_.getOPs().isUNLBlocked()` → 0/1 |
|
||||
| `unl_expiry_days` | double | `app_.validators().expires()` → days until expiry |
|
||||
| `validation_quorum` | int64 | `app_.validators().quorum()` |
|
||||
|
||||
### Sub-task 7.10a: Per-Validator Validation Count (Flag Ledger Window)
|
||||
|
||||
**Objective**: Track how many ledgers each UNL validator has validated over
|
||||
the last 256 consecutive ledgers (one flag ledger window). This is the key
|
||||
UNL participation metric — validators consistently below threshold may be
|
||||
candidates for removal from the UNL.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Add a new observable gauge:
|
||||
|
||||
```cpp
|
||||
validatorParticipationGauge_ = meter_->CreateInt64ObservableGauge(
|
||||
"xrpld_validator_participation",
|
||||
"Per-validator validation count over the last 256 ledgers");
|
||||
```
|
||||
|
||||
- The callback queries `app_.getValidations()` to get the trusted
|
||||
validation set for each of the last 256 ledger hashes (from
|
||||
`LedgerMaster::getValidatedLedger()` walking backwards). For each
|
||||
validator public key in the UNL, count how many of those 256 ledgers
|
||||
have a matching validation.
|
||||
|
||||
- **Label dimensions**:
|
||||
- `validator` — base58-encoded validator master public key
|
||||
- `service_instance_id` — this node's identity (standard)
|
||||
|
||||
- **Emission**: every flag ledger (256 ledgers, ~15 minutes) or on a
|
||||
10-second async gauge callback with cached results (recompute only
|
||||
at flag ledger boundaries).
|
||||
|
||||
- **Data source**: `RCLValidations::getTrustedForLedger(hash, seq)` returns
|
||||
`std::vector<std::shared_ptr<STValidation>>` with `getSignerPublic()`
|
||||
for each. The UNL list is from `app_.getValidators().getTrustedMasterKeys()`.
|
||||
|
||||
- **Dashboard panel**: Add a table panel to the Validator Health dashboard
|
||||
showing `xrpld_validator_participation` grouped by `validator` label,
|
||||
with a threshold color (green >= 240, yellow >= 200, red < 200).
|
||||
|
||||
**Key modified files**: `src/xrpld/telemetry/MetricsRegistry.h/.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] Gauge emits one time series per UNL validator
|
||||
- [ ] Values range 0-256 and update at flag ledger boundaries
|
||||
- [ ] Grafana table panel shows per-validator participation
|
||||
- [ ] Validators below 75% participation are highlighted in red
|
||||
|
||||
---
|
||||
|
||||
**Key modified files**: `src/xrpld/telemetry/MetricsRegistry.h/.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] All 4 base label values emitted every 10s
|
||||
- [ ] `unl_expiry_days` is negative when expired, positive when active
|
||||
- [ ] Per-validator participation gauge emits at flag ledger boundaries
|
||||
- [ ] Values visible in Prometheus
|
||||
|
||||
---
|
||||
|
||||
## Task 7.11: Peer Quality Observable Gauges
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity)
|
||||
|
||||
**Objective**: Export peer health aggregates (latency P90, insane peers, version awareness) as a native OTel observable gauge.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- In `MetricsRegistry.cpp` `registerAsyncGauges()`, add a callback that iterates `app_.overlay().foreach(...)` to:
|
||||
- Collect per-peer latency values, sort, compute P90
|
||||
- Count peers with `tracking_ == diverged` (insane)
|
||||
- Compare peer `getVersion()` to own version for upgrade awareness
|
||||
|
||||
**Gauge label values**:
|
||||
|
||||
| Label `metric=` | Type | Source |
|
||||
| -------------------------- | ------ | ------------------------------------- |
|
||||
| `peer_latency_p90_ms` | double | P90 from sorted peer latencies |
|
||||
| `peers_insane_count` | int64 | Peers with diverged tracking status |
|
||||
| `peers_higher_version_pct` | double | % of peers on newer xrpld version |
|
||||
| `upgrade_recommended` | int64 | 1 if `peers_higher_version_pct > 60%` |
|
||||
|
||||
**Implementation note**: The callback runs every 10s on the metrics reader thread. Iterating ~50-200 peers is acceptable overhead.
|
||||
|
||||
**Key modified files**: `src/xrpld/telemetry/MetricsRegistry.h/.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] P90 latency computed correctly
|
||||
- [ ] Insane count matches `peers` RPC output
|
||||
- [ ] Version comparison handles format variations (e.g., "xrpld-2.4.0-rc1")
|
||||
|
||||
---
|
||||
|
||||
## Task 7.12: Ledger Economy Observable Gauges
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity)
|
||||
|
||||
**Objective**: Export fee, reserve, ledger age, and transaction rate as a native OTel observable gauge.
|
||||
|
||||
**Gauge label values**:
|
||||
|
||||
| Label `metric=` | Type | Source |
|
||||
| -------------------- | ------ | --------------------------------------------------- |
|
||||
| `base_fee_xrp` | double | Base fee from validated ledger fee settings (drops) |
|
||||
| `reserve_base_xrp` | double | Account reserve from validated ledger (drops) |
|
||||
| `reserve_inc_xrp` | double | Owner reserve increment (drops) |
|
||||
| `ledger_age_seconds` | double | `now - lastValidatedCloseTime` |
|
||||
| `transaction_rate` | double | Derived: tx count delta / time delta (smoothed) |
|
||||
|
||||
**Key modified files**: `src/xrpld/telemetry/MetricsRegistry.h/.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] Fee values match `server_info` RPC output
|
||||
- [ ] `ledger_age_seconds` increases monotonically between ledger closes
|
||||
- [ ] `transaction_rate` is smoothed (rolling average)
|
||||
|
||||
---
|
||||
|
||||
## Task 7.13: State Tracking Observable Gauges
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity)
|
||||
|
||||
**Objective**: Export extended state value (0-6 encoding combining OperatingMode + ConsensusMode) and time-in-current-state.
|
||||
|
||||
**Gauge label values**:
|
||||
|
||||
| Label `metric=` | Type | Source |
|
||||
| ------------------------------- | ------ | ----------------------------------------------- |
|
||||
| `state_value` | int64 | 0-6 encoding (see spec for mapping) |
|
||||
| `time_in_current_state_seconds` | double | `now - lastModeChangeTime` from StateAccounting |
|
||||
|
||||
**State value encoding**: 0=disconnected, 1=connected, 2=syncing, 3=tracking, 4=full, 5=validating (full + validating), 6=proposing (full + proposing).
|
||||
|
||||
**Key modified files**: `src/xrpld/telemetry/MetricsRegistry.h/.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] `state_value` correctly combines OperatingMode and ConsensusMode
|
||||
- [ ] `time_in_current_state_seconds` resets on mode change
|
||||
|
||||
---
|
||||
|
||||
## Task 7.14: Storage Detail and Sync Info Gauges
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity)
|
||||
|
||||
**Objective**: Export NuDB-specific storage size and initial sync duration.
|
||||
|
||||
**Gauge label values**:
|
||||
|
||||
| Gauge Name | Label `metric=` | Type | Source |
|
||||
| ---------------------- | ------------------------------- | ------ | ----------------------------- |
|
||||
| `xrpld_storage_detail` | `nudb_bytes` | int64 | NuDB backend file size |
|
||||
| `xrpld_sync_info` | `initial_sync_duration_seconds` | double | Time from start to first FULL |
|
||||
|
||||
**Key modified files**: `src/xrpld/telemetry/MetricsRegistry.h/.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] NuDB file size reported in bytes (0 if NuDB not configured)
|
||||
- [ ] Sync duration captured once and remains stable after reaching FULL
|
||||
|
||||
---
|
||||
|
||||
## Task 7.15: New Synchronous Counters
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity)
|
||||
|
||||
**Objective**: Add 7 new event counters incremented at their respective instrumentation sites.
|
||||
|
||||
| Counter Name | Increment Site | Source File |
|
||||
| ----------------------------------- | -------------------------------- | --------------------- |
|
||||
| `xrpld_ledgers_closed_total` | `onAccept()` in consensus | RCLConsensus.cpp |
|
||||
| `xrpld_validations_sent_total` | `validate()` in consensus | RCLConsensus.cpp |
|
||||
| `xrpld_validations_checked_total` | Network validation received | LedgerMaster.cpp |
|
||||
| `xrpld_validation_agreements_total` | ValidationTracker reconciliation | ValidationTracker.cpp |
|
||||
| `xrpld_validation_missed_total` | ValidationTracker reconciliation | ValidationTracker.cpp |
|
||||
| `xrpld_state_changes_total` | `setMode()` in NetworkOPs | NetworkOPs.cpp |
|
||||
| `xrpld_jq_trans_overflow_total` | Job queue overflow path | JobQueue.cpp |
|
||||
|
||||
**Key modified files**: `src/xrpld/telemetry/MetricsRegistry.h/.cpp` (declarations), plus recording sites in RCLConsensus.cpp, LedgerMaster.cpp, NetworkOPs.cpp, JobQueue.cpp
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] All 7 counters monotonically increase during normal operation
|
||||
- [ ] Counter values match expected rates (e.g., ledgers_closed ≈ 1 per 3-5s)
|
||||
|
||||
---
|
||||
|
||||
## Task 7.16: Validation Agreement Observable Gauge
|
||||
|
||||
> **Source**: [External Dashboard Parity](./06-implementation-phases.md#appendix-external-dashboard-parity)
|
||||
|
||||
**Objective**: Export rolling window agreement stats from `ValidationTracker` (Task 7.9).
|
||||
|
||||
**Gauge label values**:
|
||||
|
||||
| Gauge Name | Label `metric=` | Type | Source |
|
||||
| ---------------------------- | ------------------- | ------ | --------------------------- |
|
||||
| `xrpld_validation_agreement` | `agreement_pct_1h` | double | `tracker.agreementPct1h()` |
|
||||
| | `agreements_1h` | int64 | `tracker.agreements1h()` |
|
||||
| | `missed_1h` | int64 | `tracker.missed1h()` |
|
||||
| | `agreement_pct_24h` | double | `tracker.agreementPct24h()` |
|
||||
| | `agreements_24h` | int64 | `tracker.agreements24h()` |
|
||||
| | `missed_24h` | int64 | `tracker.missed24h()` |
|
||||
|
||||
**Key modified files**: `src/xrpld/telemetry/MetricsRegistry.cpp`
|
||||
|
||||
**Exit Criteria**:
|
||||
|
||||
- [ ] Agreement percentages in range [0.0, 100.0]
|
||||
- [ ] Window stats stabilize after 1h/24h of operation
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | Description | New Files | Modified Files | Depends On |
|
||||
| ---- | -------------------------------------- | --------- | -------------- | ---------- |
|
||||
| 7.1 | Add OTel Metrics SDK to build deps | 0 | 2 | — |
|
||||
| 7.2 | Implement OTelCollector class | 2 | 0 | 7.1 |
|
||||
| 7.3 | Update CollectorManager config routing | 0 | 2 | 7.2 |
|
||||
| 7.4 | Update OTel Collector YAML and Docker | 0 | 2 | 7.3 |
|
||||
| 7.5 | Preserve metric names in Prometheus | 0 | 1 | 7.2 |
|
||||
| 7.6 | Update Grafana dashboards (if needed) | 0 | 3 | 7.5 |
|
||||
| 7.7 | Update integration tests | 0 | 1 | 7.4 |
|
||||
| 7.8 | Update documentation | 0 | 4 | 7.6 |
|
||||
| 7.9 | ValidationTracker (agreement tracking) | 2 | 4 | 7.2, P4.8 |
|
||||
| 7.10 | Validator health observable gauges | 0 | 2 | 7.2 |
|
||||
| 7.11 | Peer quality observable gauges | 0 | 2 | 7.2 |
|
||||
| 7.12 | Ledger economy observable gauges | 0 | 2 | 7.2 |
|
||||
| 7.13 | State tracking observable gauges | 0 | 2 | 7.2 |
|
||||
| 7.14 | Storage detail and sync info gauges | 0 | 2 | 7.2 |
|
||||
| 7.15 | New synchronous counters | 0 | 6 | 7.2 |
|
||||
| 7.16 | Validation agreement observable gauge | 0 | 1 | 7.9 |
|
||||
|
||||
**Parallel work**: Tasks 7.4 and 7.5 can run in parallel after 7.2/7.3 complete. Task 7.6 depends on 7.5's findings. Tasks 7.7 and 7.8 can run in parallel after 7.6. Tasks 7.10-7.14 can all run in parallel after 7.2. Task 7.15 depends on 7.2. Task 7.16 depends on 7.9. Task 7.9 depends on 7.2 and Phase 4 Task 4.8.
|
||||
|
||||
**Exit Criteria** (from [06-implementation-phases.md §6.8](./06-implementation-phases.md)):
|
||||
|
||||
- [ ] All 255+ metrics visible in Prometheus via OTLP pipeline (no StatsD receiver)
|
||||
- [ ] `server=otel` is the default in development docker-compose
|
||||
- [ ] `server=statsd` still works as a fallback
|
||||
- [ ] Existing Grafana dashboards display data correctly
|
||||
- [ ] Integration test passes with OTLP-only metrics pipeline
|
||||
- [ ] No performance regression vs StatsD baseline (< 1% CPU overhead)
|
||||
- [ ] Deferred Task 6.1 (`|m` wire format) no longer relevant — Meter mapped to OTel Counter
|
||||
- [ ] ValidationTracker agreement % stabilizes after 1h under normal consensus
|
||||
- [ ] All new gauges and counters visible in Prometheus with non-zero values
|
||||
239
OpenTelemetryPlan/Phase8_taskList.md
Normal file
239
OpenTelemetryPlan/Phase8_taskList.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Phase 8: Log-Trace Correlation and Centralized Log Ingestion — Task List
|
||||
|
||||
> **Goal**: Inject trace context (trace_id, span_id) into xrpld's Journal log output for log-trace correlation, and add OTel Collector filelog receiver to ingest logs into Grafana Loki for unified observability.
|
||||
>
|
||||
> **Scope**: Two independent sub-phases — 8a (code change: trace_id in logs) and 8b (infra only: filelog receiver to Loki). No changes to the `beast::Journal` public API.
|
||||
>
|
||||
> **Branch**: `pratik/otel-phase8-log-correlation` (from `pratik/otel-phase7-native-metrics`)
|
||||
|
||||
### Related Plan Documents
|
||||
|
||||
| Document | Relevance |
|
||||
| ---------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 8 plan: motivation, architecture, exit criteria (§6.8.1) |
|
||||
| [07-observability-backends.md](./07-observability-backends.md) | Loki backend recommendation, Grafana data source provisioning |
|
||||
| [Phase7_taskList.md](./Phase7_taskList.md) | Prerequisite — native OTel metrics pipeline must be working |
|
||||
| [05-configuration-reference.md](./05-configuration-reference.md) | `[telemetry]` config (trace_id injection toggle) |
|
||||
|
||||
---
|
||||
|
||||
## Task 8.1: Inject trace_id into Logs::format()
|
||||
|
||||
**Objective**: Add OTel trace context to every log line that is emitted within an active span.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `src/libxrpl/basics/Log.cpp`:
|
||||
- In `Logs::format()` (around line 346), after severity is appended, check for active OTel span. The implementation checks the context value directly to avoid the heap allocation that `GetSpan()` performs on the no-span path:
|
||||
```cpp
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
{
|
||||
auto context = opentelemetry::context::RuntimeContext::GetCurrent();
|
||||
auto spanValue = context.GetValue(opentelemetry::trace::kSpanKey);
|
||||
if (opentelemetry::nostd::holds_alternative<
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>>(spanValue))
|
||||
{
|
||||
auto span = opentelemetry::nostd::get<
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>>(spanValue);
|
||||
auto spanCtx = span->GetContext();
|
||||
if (spanCtx.IsValid())
|
||||
{
|
||||
char traceId[32], spanId[16];
|
||||
spanCtx.trace_id().ToLowerBase16(
|
||||
opentelemetry::nostd::span<char, 32>{traceId});
|
||||
spanCtx.span_id().ToLowerBase16(
|
||||
opentelemetry::nostd::span<char, 16>{spanId});
|
||||
output += "trace_id=";
|
||||
output.append(traceId, 32);
|
||||
output += " span_id=";
|
||||
output.append(spanId, 16);
|
||||
output += ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
```
|
||||
- Add `#include` for OTel context headers, guarded by `#ifdef XRPL_ENABLE_TELEMETRY`
|
||||
|
||||
- Edit `include/xrpl/basics/Log.h`:
|
||||
- No changes needed — format() signature unchanged
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `src/libxrpl/basics/Log.cpp`
|
||||
|
||||
**Performance note**: The implementation checks the thread-local context value directly (avoiding the heap allocation that `GetSpan()` performs on the no-span path). On threads without an active span (~99% of log lines), the cost is a thread-local read + variant type check (~15-20ns). On the active-span path, an additional shared_ptr copy + `GetContext()` + `IsValid()` adds ~50ns total. Overhead is negligible at typical logging rates.
|
||||
|
||||
---
|
||||
|
||||
## Task 8.2: Add Loki to Docker Compose Stack
|
||||
|
||||
**Objective**: Add Grafana Loki as a log storage backend in the development observability stack.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `docker/telemetry/docker-compose.yml`:
|
||||
- Add Loki service:
|
||||
```yaml
|
||||
loki:
|
||||
image: grafana/loki:3.4.2
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
```
|
||||
- Add Loki as a Grafana data source in provisioning
|
||||
|
||||
- Create `docker/telemetry/grafana/provisioning/datasources/loki.yaml`:
|
||||
- Configure Loki data source with derived fields linking `trace_id` to Tempo
|
||||
|
||||
**Key new files**:
|
||||
|
||||
- `docker/telemetry/grafana/provisioning/datasources/loki.yaml`
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docker/telemetry/docker-compose.yml`
|
||||
|
||||
---
|
||||
|
||||
## Task 8.3: Add Filelog Receiver to OTel Collector
|
||||
|
||||
**Objective**: Configure the OTel Collector to tail xrpld's log file and export to Loki.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `docker/telemetry/otel-collector-config.yaml`:
|
||||
- Add `filelog` receiver:
|
||||
```yaml
|
||||
receivers:
|
||||
filelog:
|
||||
include: [/var/log/xrpld/*/debug.log]
|
||||
operators:
|
||||
- type: regex_parser
|
||||
regex: '^(?P<timestamp>\S+)\s+(?P<partition>\S+):(?P<severity>\S+)\s+(?:trace_id=(?P<trace_id>[a-f0-9]+)\s+span_id=(?P<span_id>[a-f0-9]+)\s+)?(?P<message>.*)$'
|
||||
timestamp:
|
||||
parse_from: attributes.timestamp
|
||||
layout: "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
```
|
||||
- Add logs pipeline:
|
||||
```yaml
|
||||
service:
|
||||
pipelines:
|
||||
logs:
|
||||
receivers: [filelog]
|
||||
processors: [batch]
|
||||
exporters: [otlp/loki]
|
||||
```
|
||||
- Add Loki exporter:
|
||||
```yaml
|
||||
exporters:
|
||||
otlphttp/loki:
|
||||
endpoint: http://loki:3100/otlp
|
||||
```
|
||||
|
||||
- Mount xrpld's log directory into the collector container via docker-compose volume
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docker/telemetry/otel-collector-config.yaml`
|
||||
- `docker/telemetry/docker-compose.yml`
|
||||
|
||||
---
|
||||
|
||||
## Task 8.4: Configure Grafana Trace-to-Log Correlation
|
||||
|
||||
**Objective**: Enable one-click navigation from Tempo traces to Loki logs in Grafana.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit Grafana Tempo data source provisioning to add `tracesToLogs` configuration:
|
||||
|
||||
```yaml
|
||||
tracesToLogs:
|
||||
datasourceUid: loki
|
||||
filterByTraceID: true
|
||||
filterBySpanID: false
|
||||
tags: ["partition", "severity"]
|
||||
```
|
||||
|
||||
- Edit Grafana Loki data source provisioning to add `derivedFields` linking trace_id back to Tempo:
|
||||
```yaml
|
||||
derivedFields:
|
||||
- datasourceUid: tempo
|
||||
matcherRegex: "trace_id=(\\w+)"
|
||||
name: TraceID
|
||||
url: "$${__value.raw}"
|
||||
```
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docker/telemetry/grafana/provisioning/datasources/loki.yaml`
|
||||
- `docker/telemetry/grafana/provisioning/datasources/` (Tempo data source file)
|
||||
|
||||
---
|
||||
|
||||
## Task 8.5: Update Integration Tests
|
||||
|
||||
**Objective**: Verify trace_id appears in logs and Loki correlation works.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `docker/telemetry/integration-test.sh`:
|
||||
- After sending RPC requests (which create spans), grep xrpld's log output for `trace_id=`
|
||||
- Verify trace_id matches a trace visible in Tempo
|
||||
- Optionally: query Loki via API to confirm log ingestion
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docker/telemetry/integration-test.sh`
|
||||
|
||||
---
|
||||
|
||||
## Task 8.6: Update Documentation
|
||||
|
||||
**Objective**: Document the log correlation feature in runbook and reference docs.
|
||||
|
||||
**What to do**:
|
||||
|
||||
- Edit `docs/telemetry-runbook.md`:
|
||||
- Add "Log-Trace Correlation" section explaining how to use Grafana Tempo -> Loki linking
|
||||
- Add LogQL query examples for filtering by trace_id
|
||||
|
||||
- Edit `OpenTelemetryPlan/09-data-collection-reference.md`:
|
||||
- Add new section "3. Log Correlation" between SpanMetrics and StatsD sections
|
||||
- Document the log format with trace_id injection
|
||||
- Document Loki as a new backend
|
||||
|
||||
- Edit `docker/telemetry/TESTING.md`:
|
||||
- Add log correlation verification steps
|
||||
|
||||
**Key modified files**:
|
||||
|
||||
- `docs/telemetry-runbook.md`
|
||||
- `OpenTelemetryPlan/09-data-collection-reference.md`
|
||||
- `docker/telemetry/TESTING.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | Description | Sub-Phase | New Files | Modified Files | Depends On |
|
||||
| ---- | ------------------------------------------ | --------- | --------- | -------------- | ---------- |
|
||||
| 8.1 | Inject trace_id into Logs::format() | 8a | 0 | 1 | Phase 7 |
|
||||
| 8.2 | Add Loki to Docker Compose stack | 8b | 1 | 1 | -- |
|
||||
| 8.3 | Add filelog receiver to OTel Collector | 8b | 0 | 2 | 8.1, 8.2 |
|
||||
| 8.4 | Configure Grafana trace-to-log correlation | 8b | 0 | 2 | 8.3 |
|
||||
| 8.5 | Update integration tests | 8a + 8b | 0 | 1 | 8.4 |
|
||||
| 8.6 | Update documentation | 8a + 8b | 0 | 3 | 8.5 |
|
||||
|
||||
**Parallel work**: Task 8.2 (Loki infra) can run in parallel with Task 8.1 (code change). Tasks 8.3-8.6 are sequential.
|
||||
|
||||
**Exit Criteria** (from [06-implementation-phases.md §6.8.1](./06-implementation-phases.md)):
|
||||
|
||||
- [ ] Log lines within active spans contain `trace_id=<hex> span_id=<hex>`
|
||||
- [ ] Log lines outside spans have no trace context (no empty fields)
|
||||
- [ ] Loki ingests xrpld logs via OTel Collector filelog receiver
|
||||
- [ ] Grafana Tempo -> Loki one-click correlation works
|
||||
- [ ] Grafana Loki -> Tempo reverse lookup works via derived field
|
||||
- [ ] Integration test verifies trace_id presence in logs
|
||||
- [ ] No performance regression from trace_id injection (< 0.1% overhead)
|
||||
240
OpenTelemetryPlan/secure-OTel.md
Normal file
240
OpenTelemetryPlan/secure-OTel.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# 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:
|
||||
# Setting client_ca_file makes the collector require and verify a
|
||||
# client cert, rejecting connections without a trusted one.
|
||||
client_ca_file: /certs/client_ca.pem # CA that signs trusted client certs
|
||||
cert_file: /certs/collector.pem
|
||||
key_file: /certs/collector.key
|
||||
```
|
||||
|
||||
### **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
|
||||
│ ▼
|
||||
└───────────────────────────────────────── [client_ca_file: 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; the collector verifies it via `client_ca_file` in the receiver's `tls` block. 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. The collector requires and verifies each client certificate when `client_ca_file` is set in the receiver's `tls` block (there is no `auth_type` field — setting `client_ca_file` is what enforces client-cert verification).
|
||||
|
||||
**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,108 @@ 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).
|
||||
#
|
||||
# tls_client_cert=
|
||||
#
|
||||
# Path to this node's PEM-encoded client certificate, presented to the
|
||||
# collector for mutual TLS (mTLS). Only used when use_tls=1. Leave empty
|
||||
# for one-way (server-only) TLS. Default: empty.
|
||||
#
|
||||
# tls_client_key=
|
||||
#
|
||||
# Path to the PEM-encoded private key for tls_client_cert. Required
|
||||
# whenever tls_client_cert is set. Only used when use_tls=1.
|
||||
# Default: empty.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
@@ -78,6 +78,19 @@ include(target_link_modules)
|
||||
# Level 01
|
||||
add_module(xrpl beast)
|
||||
target_link_libraries(xrpl.libxrpl.beast PUBLIC xrpl.imports.main)
|
||||
# OTelCollector in beast/insight uses the OTel Metrics SDK when telemetry is
|
||||
# enabled. Link the Conan-provided umbrella target rather than individual
|
||||
# component targets: the OTel package's per-component dependency graph is
|
||||
# under-declared (e.g. the OTLP client references sdk::common symbols without
|
||||
# declaring the edge), so naming components directly reorders the static-link
|
||||
# line into an unresolvable state. The umbrella carries the full, internally
|
||||
# consistent graph the package authors validated.
|
||||
if(telemetry)
|
||||
target_link_libraries(
|
||||
xrpl.libxrpl.beast
|
||||
PUBLIC opentelemetry-cpp::opentelemetry-cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
include(GitInfo)
|
||||
add_module(xrpl git)
|
||||
@@ -198,8 +211,36 @@ 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).
|
||||
#
|
||||
# Links xrpl.libxrpl.protocol PRIVATELY for sha512Half (digest.h)
|
||||
add_module(xrpl telemetry)
|
||||
target_link_libraries(
|
||||
xrpl.libxrpl.telemetry
|
||||
PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.beast xrpl.libxrpl.config
|
||||
PRIVATE xrpl.libxrpl.protocol
|
||||
)
|
||||
if(telemetry)
|
||||
# Telemetry owns both the trace and (as of the direct-metrics API) the
|
||||
# metrics pipeline. Link the umbrella target: it supplies the trace and
|
||||
# metrics SDK components with the correct static-link ordering, which
|
||||
# naming components individually does not (the package under-declares
|
||||
# inter-component dependencies).
|
||||
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 +274,7 @@ target_link_modules(
|
||||
resource
|
||||
server
|
||||
shamap
|
||||
telemetry
|
||||
tx
|
||||
)
|
||||
|
||||
|
||||
12
conan.lock
12
conan.lock
@@ -10,11 +10,14 @@
|
||||
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713",
|
||||
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
|
||||
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
|
||||
"opentelemetry-cpp/1.26.0#9d81768342c78cb897345fd419b358d2%1776934712.672",
|
||||
"openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
|
||||
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
|
||||
"nlohmann_json/3.11.3#45828be26eb619a2e04ca517bb7b828d%1701220705.259",
|
||||
"mpt-crypto/0.4.0-rc2#a580f2f9ad0e795de696aa62d54fb9af%1782425834.488828",
|
||||
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
|
||||
"libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744",
|
||||
"libcurl/8.20.0#c90b0c91a33d9a79b519c1c70bafc823%1780907438.587",
|
||||
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
|
||||
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
|
||||
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
|
||||
@@ -31,9 +34,15 @@
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1782395692.540639",
|
||||
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
|
||||
"pkgconf/2.5.1#93c2051284cba1279494a43a4fcfeae2%1757684701.089",
|
||||
"opentelemetry-proto/1.7.0#ed6d5bd761bef0afb0ba09676420b9ea%1749461220.268",
|
||||
"ninja/1.13.2#c8c5dc2a52ed6e4e42a66d75b4717ceb%1764096931.974",
|
||||
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1782395690.33162",
|
||||
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
|
||||
"meson/1.10.2#9d2d10681fe7fe61c788c58626c89b25%1775558003.754",
|
||||
"m4/1.4.19#34c4bbc3eeebe98ca6edf2f52d602e7d%1777282960.259",
|
||||
"libtool/2.4.7#14e7739cc128bc1623d2ed318008e47e%1755679003.847",
|
||||
"gnu-config/cci.20210814#466e9d4d7779e1c142443f7ea44b4284%1762363589.329",
|
||||
"cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091",
|
||||
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
|
||||
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
|
||||
@@ -59,6 +68,9 @@
|
||||
],
|
||||
"lz4/[>=1.9.4 <2]": [
|
||||
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab"
|
||||
],
|
||||
"protobuf/[>=4.25.3 <7]": [
|
||||
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81"
|
||||
]
|
||||
},
|
||||
"config_requires": []
|
||||
|
||||
@@ -25,3 +25,18 @@ compiler.libcxx={{ detect_api.detect_libcxx(compiler, version, compiler_exe) }}
|
||||
{# More info: https://docs.conan.io/2/reference/extensions/binary_compatibility.html #}
|
||||
user.package:cppstd_version=23
|
||||
tools.info.package_id:confs+=["user.package:cppstd_version"]
|
||||
{% 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],
|
||||
@@ -52,6 +53,7 @@ class Xrpl(ConanFile):
|
||||
"rocksdb": True,
|
||||
"shared": False,
|
||||
"static": True,
|
||||
"telemetry": True,
|
||||
"tests": False,
|
||||
"unity": False,
|
||||
"xrpld": False,
|
||||
@@ -140,6 +142,10 @@ class Xrpl(ConanFile):
|
||||
self.requires("rocksdb/10.5.1")
|
||||
self.requires("secp256k1/0.7.1", transitive_headers=True)
|
||||
self.requires("sqlite3/3.53.0", force=True)
|
||||
# OpenTelemetry C++ SDK for distributed tracing (optional).
|
||||
# Provides OTLP/HTTP exporter, batch span processor, and trace API.
|
||||
if self.options.telemetry:
|
||||
self.requires("opentelemetry-cpp/1.26.0")
|
||||
self.requires("xxhash/0.8.3", transitive_headers=True)
|
||||
|
||||
exports_sources = (
|
||||
@@ -168,6 +174,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()
|
||||
@@ -221,3 +228,5 @@ class Xrpl(ConanFile):
|
||||
]
|
||||
if self.options.rocksdb:
|
||||
libxrpl.requires.append("rocksdb::librocksdb")
|
||||
if self.options.telemetry:
|
||||
libxrpl.requires.append("opentelemetry-cpp::opentelemetry-cpp")
|
||||
|
||||
2
docker/telemetry/.gitignore
vendored
Normal file
2
docker/telemetry/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Runtime data generated by xrpld and telemetry stack
|
||||
data/
|
||||
649
docker/telemetry/TESTING.md
Normal file
649
docker/telemetry/TESTING.md
Normal file
@@ -0,0 +1,649 @@
|
||||
# OpenTelemetry Integration Testing Guide
|
||||
|
||||
This document describes how to verify the xrpld OpenTelemetry telemetry
|
||||
pipeline end-to-end, from span generation through the observability stack
|
||||
(otel-collector, Tempo, Prometheus, Grafana).
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Build xrpld with telemetry
|
||||
|
||||
```bash
|
||||
conan install . --build=missing -o telemetry=True
|
||||
cmake --preset default -Dtelemetry=ON
|
||||
cmake --build --preset default --target xrpld
|
||||
```
|
||||
|
||||
The binary is at `.build/xrpld`.
|
||||
|
||||
### Required tools
|
||||
|
||||
- **Docker** with `docker compose` (v2)
|
||||
- **curl**
|
||||
- **jq** (JSON processor)
|
||||
|
||||
### Verify binary
|
||||
|
||||
```bash
|
||||
.build/xrpld --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test 1: Single-Node Standalone (Quick Verification)
|
||||
|
||||
This test verifies RPC and transaction spans in standalone mode. Consensus
|
||||
spans will not fire because standalone mode does not run consensus.
|
||||
|
||||
### Step 1: Start the observability stack
|
||||
|
||||
```bash
|
||||
docker compose -f docker/telemetry/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
Wait for services to be ready:
|
||||
|
||||
```bash
|
||||
# otel-collector health
|
||||
curl -sf http://localhost:13133/ && echo "collector ready"
|
||||
|
||||
# Tempo readiness
|
||||
curl -sf http://localhost:3200/ready >/dev/null && echo "tempo ready"
|
||||
```
|
||||
|
||||
### Step 2: Start xrpld in standalone mode
|
||||
|
||||
```bash
|
||||
.build/xrpld --conf docker/telemetry/xrpld-telemetry.cfg -a --start
|
||||
```
|
||||
|
||||
Wait a few seconds for the node to initialize.
|
||||
|
||||
### Step 3: Exercise RPC spans
|
||||
|
||||
```bash
|
||||
# server_info
|
||||
curl -s http://localhost:5005 \
|
||||
-d '{"method":"server_info"}' | jq .result.info.server_state
|
||||
|
||||
# server_state
|
||||
curl -s http://localhost:5005 \
|
||||
-d '{"method":"server_state"}' | jq .result.state.server_state
|
||||
|
||||
# ledger
|
||||
curl -s http://localhost:5005 \
|
||||
-d '{"method":"ledger","params":[{"ledger_index":"current"}]}' |
|
||||
jq .result.ledger_current_index
|
||||
```
|
||||
|
||||
### Step 4: Submit a transaction
|
||||
|
||||
Close the ledger first (required in standalone mode):
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:5005 -d '{"method":"ledger_accept"}'
|
||||
```
|
||||
|
||||
Submit a Payment from the genesis account:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:5005 -d '{
|
||||
"method": "submit",
|
||||
"params": [{
|
||||
"secret": "snoPBrXtMeMyMHUVTgbuqAfg1SUTb",
|
||||
"tx_json": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Destination": "rPMh7Pi9ct699iZUTWzJaUMR1o42VEfGqF",
|
||||
"Amount": "10000000"
|
||||
}
|
||||
}]
|
||||
}' | jq .result.engine_result
|
||||
```
|
||||
|
||||
Expected result: `"tesSUCCESS"`.
|
||||
|
||||
Close the ledger again to finalize:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:5005 -d '{"method":"ledger_accept"}'
|
||||
```
|
||||
|
||||
### Step 5: Verify traces in Tempo
|
||||
|
||||
Wait 5 seconds for the batch export, then:
|
||||
|
||||
```bash
|
||||
TEMPO="http://localhost:3200"
|
||||
|
||||
# Check xrpld service is registered
|
||||
curl -s "$TEMPO/api/v2/search/tag/resource.service.name/values" | jq '.tagValues[].value'
|
||||
|
||||
# Check RPC spans
|
||||
curl -s "$TEMPO/api/search" \
|
||||
--data-urlencode 'q={resource.service.name="xrpld" && name="rpc.http_request"}' \
|
||||
--data-urlencode 'limit=5' | jq '.traces | length'
|
||||
|
||||
curl -s "$TEMPO/api/search" \
|
||||
--data-urlencode 'q={resource.service.name="xrpld" && name="rpc.process"}' \
|
||||
--data-urlencode 'limit=5' | jq '.traces | length'
|
||||
|
||||
curl -s "$TEMPO/api/search" \
|
||||
--data-urlencode 'q={resource.service.name="xrpld" && name="rpc.command.server_info"}' \
|
||||
--data-urlencode 'limit=5' | jq '.traces | length'
|
||||
|
||||
# Check transaction spans
|
||||
curl -s "$TEMPO/api/search" \
|
||||
--data-urlencode 'q={resource.service.name="xrpld" && name="tx.process"}' \
|
||||
--data-urlencode 'limit=5' | jq '.traces | length'
|
||||
```
|
||||
|
||||
Or open Grafana Explore with Tempo datasource: http://localhost:3000
|
||||
|
||||
### Step 6: Teardown
|
||||
|
||||
```bash
|
||||
# Kill xrpld (Ctrl+C or)
|
||||
kill $(pgrep -f 'xrpld.*xrpld-telemetry')
|
||||
|
||||
# Stop observability stack
|
||||
docker compose -f docker/telemetry/docker-compose.yml down
|
||||
|
||||
# Clean xrpld data
|
||||
rm -rf data/
|
||||
```
|
||||
|
||||
### Expected spans (standalone mode)
|
||||
|
||||
| Span Name | Expected | Notes |
|
||||
| --------------------------- | -------- | ----------------------------- |
|
||||
| `rpc.http_request` | Yes | Every HTTP RPC call |
|
||||
| `rpc.process` | Yes | Every RPC processing |
|
||||
| `rpc.command.server_info` | Yes | server_info RPC |
|
||||
| `rpc.command.server_state` | Yes | server_state RPC |
|
||||
| `rpc.command.ledger` | Yes | ledger RPC |
|
||||
| `rpc.command.submit` | Yes | submit RPC |
|
||||
| `rpc.command.ledger_accept` | Yes | ledger_accept RPC |
|
||||
| `tx.process` | Yes | Transaction submission |
|
||||
| `tx.receive` | No | No peers in standalone |
|
||||
| `consensus.*` | No | Consensus disabled standalone |
|
||||
|
||||
---
|
||||
|
||||
## Test 2: 6-Node Consensus Network (Full Verification)
|
||||
|
||||
This test verifies ALL span categories including consensus and peer
|
||||
transaction relay, using a 6-node validator network.
|
||||
|
||||
### Automated
|
||||
|
||||
Run the integration test script:
|
||||
|
||||
```bash
|
||||
bash docker/telemetry/integration-test.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
1. Start the observability stack
|
||||
2. Generate 6 validator key pairs
|
||||
3. Create config files for each node
|
||||
4. Start all 6 nodes
|
||||
5. Wait for consensus ("proposing" state)
|
||||
6. Exercise RPC, submit transactions
|
||||
7. Verify all span categories in Tempo
|
||||
8. Verify spanmetrics in Prometheus
|
||||
9. Print results and leave the stack running
|
||||
|
||||
### Manual
|
||||
|
||||
If you prefer to run the steps manually:
|
||||
|
||||
#### Step 1: Start observability stack
|
||||
|
||||
```bash
|
||||
docker compose -f docker/telemetry/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
#### Step 2: Generate validator keys
|
||||
|
||||
Start a temporary standalone xrpld:
|
||||
|
||||
```bash
|
||||
.build/xrpld --conf docker/telemetry/xrpld-telemetry.cfg -a --start &
|
||||
TEMP_PID=$!
|
||||
sleep 5
|
||||
```
|
||||
|
||||
Generate 6 key pairs:
|
||||
|
||||
```bash
|
||||
for i in $(seq 1 6); do
|
||||
curl -s http://localhost:5005 \
|
||||
-d '{"method":"validation_create"}' | jq '.result'
|
||||
done
|
||||
```
|
||||
|
||||
Record the `validation_seed` and `validation_public_key` for each.
|
||||
Kill the temporary node:
|
||||
|
||||
```bash
|
||||
kill $TEMP_PID
|
||||
rm -rf data/
|
||||
```
|
||||
|
||||
#### Step 3: Create node configs
|
||||
|
||||
For each node (1-6), create a config file. Template:
|
||||
|
||||
```ini
|
||||
[server]
|
||||
port_rpc
|
||||
port_peer
|
||||
|
||||
[port_rpc]
|
||||
port = {5004 + node_number}
|
||||
ip = 127.0.0.1
|
||||
admin = 127.0.0.1
|
||||
protocol = http
|
||||
|
||||
[port_peer]
|
||||
port = {51234 + node_number}
|
||||
ip = 0.0.0.0
|
||||
protocol = peer
|
||||
|
||||
[node_db]
|
||||
type=NuDB
|
||||
path=/tmp/xrpld-integration/node{N}/nudb
|
||||
online_delete=256
|
||||
|
||||
[database_path]
|
||||
/tmp/xrpld-integration/node{N}/db
|
||||
|
||||
[debug_logfile]
|
||||
/tmp/xrpld-integration/node{N}/debug.log
|
||||
|
||||
[validation_seed]
|
||||
{seed from step 2}
|
||||
|
||||
[validators_file]
|
||||
/tmp/xrpld-integration/validators.txt
|
||||
|
||||
[ips_fixed]
|
||||
127.0.0.1 51235
|
||||
127.0.0.1 51236
|
||||
127.0.0.1 51237
|
||||
127.0.0.1 51238
|
||||
127.0.0.1 51239
|
||||
127.0.0.1 51240
|
||||
|
||||
[peer_private]
|
||||
1
|
||||
|
||||
[telemetry]
|
||||
enabled=1
|
||||
endpoint=http://localhost:4318/v1/traces
|
||||
batch_size=512
|
||||
batch_delay_ms=2000
|
||||
max_queue_size=2048
|
||||
trace_rpc=1
|
||||
trace_transactions=1
|
||||
trace_consensus=1
|
||||
trace_peer=1
|
||||
trace_ledger=1
|
||||
|
||||
[rpc_startup]
|
||||
{ "command": "log_level", "severity": "warning" }
|
||||
|
||||
[ssl_verify]
|
||||
0
|
||||
```
|
||||
|
||||
#### Step 4: Create validators.txt
|
||||
|
||||
```ini
|
||||
[validators]
|
||||
{public_key_1}
|
||||
{public_key_2}
|
||||
{public_key_3}
|
||||
{public_key_4}
|
||||
{public_key_5}
|
||||
{public_key_6}
|
||||
```
|
||||
|
||||
#### Step 5: Start all 6 nodes
|
||||
|
||||
```bash
|
||||
for i in $(seq 1 6); do
|
||||
.build/xrpld --conf /tmp/xrpld-integration/node$i/xrpld.cfg --start &
|
||||
echo $! >/tmp/xrpld-integration/node$i/xrpld.pid
|
||||
done
|
||||
```
|
||||
|
||||
#### Step 6: Wait for consensus
|
||||
|
||||
Poll each node until `server_state` = `"proposing"`:
|
||||
|
||||
```bash
|
||||
for port in 5005 5006 5007 5008 5009 5010; do
|
||||
while true; do
|
||||
state=$(curl -s http://localhost:$port \
|
||||
-d '{"method":"server_info"}' |
|
||||
jq -r '.result.info.server_state')
|
||||
echo "Port $port: $state"
|
||||
[ "$state" = "proposing" ] && break
|
||||
sleep 5
|
||||
done
|
||||
done
|
||||
```
|
||||
|
||||
#### Step 7: Exercise RPC and submit transaction
|
||||
|
||||
```bash
|
||||
# RPC calls
|
||||
curl -s http://localhost:5005 -d '{"method":"server_info"}'
|
||||
curl -s http://localhost:5005 -d '{"method":"server_state"}'
|
||||
curl -s http://localhost:5005 -d '{"method":"ledger","params":[{"ledger_index":"current"}]}'
|
||||
|
||||
# Submit transaction
|
||||
curl -s http://localhost:5005 -d '{
|
||||
"method": "submit",
|
||||
"params": [{
|
||||
"secret": "snoPBrXtMeMyMHUVTgbuqAfg1SUTb",
|
||||
"tx_json": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Destination": "rPMh7Pi9ct699iZUTWzJaUMR1o42VEfGqF",
|
||||
"Amount": "10000000"
|
||||
}
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
Wait 15 seconds for consensus and batch export.
|
||||
|
||||
#### Step 8: Verify in Tempo
|
||||
|
||||
See the "Verification Queries" section below.
|
||||
|
||||
---
|
||||
|
||||
## Expected Span Catalog
|
||||
|
||||
All 16 production span names instrumented across Phases 2-5:
|
||||
|
||||
| Span Name | Source File | Phase | Key Attributes | How to Trigger |
|
||||
| --------------------------- | ----------------- | ----- | ---------------------------------------------------------------------------------------- | ------------------------- |
|
||||
| `rpc.http_request` | ServerHandler.cpp | 2 | -- | Any HTTP RPC call |
|
||||
| `rpc.ws_upgrade` | ServerHandler.cpp | 2 | -- | WebSocket upgrade |
|
||||
| `rpc.ws_message` | ServerHandler.cpp | 2 | -- | WebSocket RPC message |
|
||||
| `rpc.process` | ServerHandler.cpp | 2 | -- | RPC processing |
|
||||
| `rpc.command.<name>` | RPCHandler.cpp | 2 | `xrpl.rpc.command`, `xrpl.rpc.version`, `xrpl.rpc.role` | Any RPC command |
|
||||
| `tx.process` | NetworkOPs.cpp | 3 | `xrpl.tx.hash`, `xrpl.tx.local`, `xrpl.tx.path` | Submit transaction |
|
||||
| `tx.receive` | PeerImp.cpp | 3 | `xrpl.peer.id` | Peer relays transaction |
|
||||
| `consensus.proposal.send` | RCLConsensus.cpp | 4 | `xrpl.consensus.round` | Consensus proposing phase |
|
||||
| `consensus.ledger_close` | RCLConsensus.cpp | 4 | `xrpl.consensus.ledger.seq`, `xrpl.consensus.mode` | Ledger close event |
|
||||
| `consensus.accept` | RCLConsensus.cpp | 4 | `xrpl.consensus.proposers`, `xrpl.consensus.round_time_ms` | Ledger accepted |
|
||||
| `consensus.validation.send` | RCLConsensus.cpp | 4 | `xrpl.consensus.ledger.seq`, `xrpl.consensus.proposing` | Validation sent |
|
||||
| `consensus.accept.apply` | RCLConsensus.cpp | 4 | `xrpl.consensus.close_time`, `close_time_correct`, `close_resolution_ms`, `state` | Ledger apply + close time |
|
||||
| `tx.apply` | BuildLedger.cpp | 5 | `xrpl.ledger.tx_count`, `xrpl.ledger.tx_failed` | Ledger close (tx set) |
|
||||
| `ledger.build` | BuildLedger.cpp | 5 | `xrpl.ledger.seq`, `xrpl.ledger.close_time`, `close_time_correct`, `close_resolution_ms` | Ledger build |
|
||||
| `ledger.validate` | LedgerMaster.cpp | 5 | `xrpl.ledger.seq`, `xrpl.ledger.validations` | Ledger validated |
|
||||
| `ledger.store` | LedgerMaster.cpp | 5 | `xrpl.ledger.seq` | Ledger stored |
|
||||
| `peer.proposal.receive` | PeerImp.cpp | 5 | `xrpl.peer.id`, `xrpl.peer.proposal.trusted` | Peer sends proposal |
|
||||
| `peer.validation.receive` | PeerImp.cpp | 5 | `xrpl.peer.id`, `xrpl.peer.validation.trusted` | Peer sends validation |
|
||||
|
||||
---
|
||||
|
||||
## Verification Queries
|
||||
|
||||
### Tempo API
|
||||
|
||||
Base URL: `http://localhost:3200`
|
||||
|
||||
```bash
|
||||
TEMPO="http://localhost:3200"
|
||||
|
||||
# List all services
|
||||
curl -s "$TEMPO/api/v2/search/tag/resource.service.name/values" | jq '.tagValues[].value'
|
||||
|
||||
# Query traces by operation
|
||||
for op in "rpc.http_request" "rpc.ws_upgrade" "rpc.ws_message" "rpc.process" \
|
||||
"rpc.command.server_info" "rpc.command.server_state" "rpc.command.ledger" \
|
||||
"tx.process" "tx.receive" "tx.apply" \
|
||||
"consensus.proposal.send" "consensus.ledger_close" \
|
||||
"consensus.accept" "consensus.accept.apply" \
|
||||
"consensus.validation.send" \
|
||||
"ledger.build" "ledger.validate" "ledger.store" \
|
||||
"peer.proposal.receive" "peer.validation.receive"; do
|
||||
count=$(curl -s "$TEMPO/api/search" \
|
||||
--data-urlencode "q={resource.service.name=\"xrpld\" && name=\"$op\"}" \
|
||||
--data-urlencode "limit=5" |
|
||||
jq '.traces | length')
|
||||
printf "%-35s %s traces\n" "$op" "$count"
|
||||
done
|
||||
```
|
||||
|
||||
### Prometheus API
|
||||
|
||||
Base URL: `http://localhost:9090`
|
||||
|
||||
```bash
|
||||
PROM="http://localhost:9090"
|
||||
|
||||
# Span call counts (from spanmetrics connector)
|
||||
curl -s "$PROM/api/v1/query?query=traces_span_metrics_calls_total" |
|
||||
jq '.data.result[] | {span: .metric.span_name, count: .value[1]}'
|
||||
|
||||
# Latency histogram
|
||||
curl -s "$PROM/api/v1/query?query=traces_span_metrics_duration_milliseconds_count" |
|
||||
jq '.data.result[] | {span: .metric.span_name, count: .value[1]}'
|
||||
|
||||
# RPC calls by command
|
||||
curl -s "$PROM/api/v1/query?query=traces_span_metrics_calls_total{span_name=~\"rpc.command.*\"}" |
|
||||
jq '.data.result[] | {command: .metric["xrpl.rpc.command"], count: .value[1]}'
|
||||
|
||||
# Deployment-tier labels present on metrics (set by the collector's
|
||||
# resource/tier processor and promoted via resource_to_telemetry_conversion).
|
||||
# Expect deployment_environment and xrpl_network_type on each series.
|
||||
curl -s "$PROM/api/v1/query?query=traces_span_metrics_calls_total" |
|
||||
jq '.data.result[0].metric | {deployment_environment, xrpl_network_type, service_name}'
|
||||
```
|
||||
|
||||
### Grafana
|
||||
|
||||
Open http://localhost:3000 (anonymous admin access enabled).
|
||||
|
||||
Pre-configured dashboards:
|
||||
|
||||
- **RPC Performance**: Request rates, latency percentiles by command, top commands, WebSocket rate
|
||||
- **Transaction Overview**: Transaction processing rates, apply duration, peer relay, failed tx rate
|
||||
- **Consensus Health**: Consensus round duration, proposer counts, mode tracking, accept heatmap
|
||||
- **Ledger Operations**: Build/validate/store rates and durations, TX apply metrics
|
||||
- **Peer Network**: Proposal/validation receive rates, trusted vs untrusted breakdown (requires `trace_peer=1`)
|
||||
|
||||
Pre-configured datasources:
|
||||
|
||||
- **Tempo**: Trace data at `http://tempo:3200`
|
||||
- **Prometheus**: Metrics at `http://prometheus:9090`
|
||||
- **Loki**: Log data at `http://loki:3100` (via Grafana Explore)
|
||||
|
||||
---
|
||||
|
||||
## Test 3: Log-Trace Correlation (Phase 8)
|
||||
|
||||
Phase 8 injects `trace_id` and `span_id` into xrpld's log output when
|
||||
a log line is emitted within an active OTel span. This test verifies the
|
||||
end-to-end log-trace correlation pipeline.
|
||||
|
||||
### Step 1: Verify trace_id in log output
|
||||
|
||||
After running Test 1 or Test 2 (which generate RPC spans), check the
|
||||
xrpld debug.log for trace context:
|
||||
|
||||
```bash
|
||||
grep 'trace_id=[a-f0-9]\{32\} span_id=[a-f0-9]\{16\}' /path/to/debug.log
|
||||
```
|
||||
|
||||
Expected: log lines with `trace_id=<32hex> span_id=<16hex>` between the
|
||||
severity code and the message. Example:
|
||||
|
||||
```
|
||||
2024-Jan-15 10:30:45.123456 UTC RPCHandler:NFO trace_id=abc123def456789012345678abcdef01 span_id=0123456789abcdef Calling server_info
|
||||
```
|
||||
|
||||
Lines emitted outside of an active span (background tasks, startup) will
|
||||
NOT have trace context — this is expected.
|
||||
|
||||
### Step 2: Cross-check trace_id in Tempo
|
||||
|
||||
Extract a `trace_id` from the log and verify it exists in Tempo:
|
||||
|
||||
```bash
|
||||
TRACE_ID=$(grep -o 'trace_id=[a-f0-9]\{32\}' /path/to/debug.log | head -1 | cut -d= -f2)
|
||||
echo "Checking trace: $TRACE_ID"
|
||||
curl -s "http://localhost:3200/api/traces/$TRACE_ID" | jq '.data | length'
|
||||
```
|
||||
|
||||
Expected result: `1` (the trace exists in Tempo).
|
||||
|
||||
### Step 3: Verify Loki log ingestion
|
||||
|
||||
The OTel Collector's filelog receiver tails xrpld's debug.log and
|
||||
exports parsed entries to Loki. Verify Loki has received entries:
|
||||
|
||||
```bash
|
||||
# Query Loki for any xrpld logs
|
||||
curl -sG "http://localhost:3100/loki/api/v1/query" \
|
||||
--data-urlencode 'query={job="xrpld"}' \
|
||||
--data-urlencode 'limit=5' | jq '.data.result | length'
|
||||
```
|
||||
|
||||
Expected: > 0 results.
|
||||
|
||||
### Step 4: Verify Grafana Tempo-to-Loki correlation
|
||||
|
||||
1. Open Grafana at http://localhost:3000
|
||||
2. Navigate to **Explore** -> select **Tempo** datasource
|
||||
3. Search for a trace (e.g., operation `rpc.command.server_info`)
|
||||
4. Click **"Logs for this trace"** in the trace detail view
|
||||
5. Verify that Loki log lines appear, filtered by the trace's `trace_id`
|
||||
|
||||
### Step 5: Verify Grafana Loki-to-Tempo correlation
|
||||
|
||||
1. In Grafana **Explore**, select **Loki** datasource
|
||||
2. Query: `{job="xrpld"} |= "trace_id="`
|
||||
3. In the log results, click the **TraceID** derived field link
|
||||
4. Verify it navigates to the full trace in Tempo
|
||||
|
||||
### Expected results
|
||||
|
||||
| Check | Expected |
|
||||
| ------------------------------ | ---------------------------------------- |
|
||||
| `trace_id=` in debug.log | Present in log lines within active spans |
|
||||
| `span_id=` in debug.log | Present alongside trace_id |
|
||||
| Logs without active span | No trace_id/span_id fields |
|
||||
| trace_id in Tempo | Matches a valid trace |
|
||||
| Loki log ingestion | Logs visible via LogQL |
|
||||
| Tempo -> Loki "Logs for trace" | Shows correlated log lines |
|
||||
| Loki -> Tempo TraceID link | Navigates to correct trace |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No traces in Tempo
|
||||
|
||||
1. Check otel-collector logs:
|
||||
```bash
|
||||
docker compose -f docker/telemetry/docker-compose.yml logs otel-collector
|
||||
```
|
||||
2. Verify xrpld telemetry config has `enabled=1` and correct endpoint
|
||||
3. Check that otel-collector port 4318 is accessible:
|
||||
```bash
|
||||
curl -sf http://localhost:4318 && echo "reachable"
|
||||
```
|
||||
4. Increase `batch_delay_ms` or decrease `batch_size` in xrpld config
|
||||
|
||||
### Nodes not reaching "proposing" state
|
||||
|
||||
1. Check that all peer ports (51235-51240) are not in use:
|
||||
```bash
|
||||
for p in 51235 51236 51237 51238 51239 51240; do
|
||||
ss -tlnp | grep ":$p " && echo "port $p in use"
|
||||
done
|
||||
```
|
||||
2. Verify `[ips_fixed]` lists all 6 peer ports
|
||||
3. Verify `validators.txt` has all 6 public keys
|
||||
4. Check node debug logs: `tail -50 /tmp/xrpld-integration/node1/debug.log`
|
||||
5. Ensure `[peer_private]` is set to `1` (prevents reaching out to public network)
|
||||
|
||||
### Transaction not processing
|
||||
|
||||
1. Verify genesis account exists:
|
||||
```bash
|
||||
curl -s http://localhost:5005 \
|
||||
-d '{"method":"account_info","params":[{"account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"}]}' |
|
||||
jq .result.account_data.Balance
|
||||
```
|
||||
2. Check submit response for error codes
|
||||
3. In standalone mode, remember to call `ledger_accept` after submitting
|
||||
|
||||
### No trace_id in log output (Phase 8)
|
||||
|
||||
1. Verify xrpld was built with `telemetry=ON` (`-Dtelemetry=ON` in CMake)
|
||||
2. Verify `enabled=1` in the `[telemetry]` config section
|
||||
3. Log lines only contain trace context when emitted inside an active span.
|
||||
Background logs (startup, periodic tasks outside spans) will not have
|
||||
`trace_id`/`span_id`.
|
||||
4. Ensure the trace category is enabled (e.g., `trace_rpc=1` for RPC logs)
|
||||
|
||||
### No logs in Loki (Phase 8)
|
||||
|
||||
1. Verify the log file mount in docker-compose.yml:
|
||||
```yaml
|
||||
volumes:
|
||||
- ${XRPLD_LOG_DIR:-./data/logs}:/var/log/xrpld:ro
|
||||
```
|
||||
The mount source defaults to the repo-relative `docker/telemetry/data/logs`
|
||||
(where the telemetry configs write). Override `XRPLD_LOG_DIR` to tail logs
|
||||
from another root.
|
||||
2. Check OTel Collector logs for filelog receiver errors:
|
||||
```bash
|
||||
docker compose -f docker/telemetry/docker-compose.yml logs otel-collector | grep -i "filelog\|loki\|error"
|
||||
```
|
||||
3. Verify Loki is running:
|
||||
```bash
|
||||
curl -s http://localhost:3100/ready
|
||||
```
|
||||
4. Verify the filelog receiver glob pattern matches your log files:
|
||||
The default pattern is `/var/log/xrpld/*/debug.log`
|
||||
|
||||
### Grafana trace-log links not working (Phase 8)
|
||||
|
||||
1. Verify `tracesToLogs` is configured in the Tempo datasource provisioning
|
||||
(`docker/telemetry/grafana/provisioning/datasources/tempo.yaml`)
|
||||
2. Verify `derivedFields` is configured in the Loki datasource provisioning
|
||||
(`docker/telemetry/grafana/provisioning/datasources/loki.yaml`)
|
||||
3. Restart Grafana after changing provisioning files:
|
||||
```bash
|
||||
docker compose -f docker/telemetry/docker-compose.yml restart grafana
|
||||
```
|
||||
|
||||
### Spanmetrics not appearing in Prometheus
|
||||
|
||||
1. Verify otel-collector config has `spanmetrics` connector
|
||||
2. Check that the metrics pipeline is configured:
|
||||
```yaml
|
||||
service:
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [spanmetrics]
|
||||
exporters: [prometheus]
|
||||
```
|
||||
3. Verify Prometheus can reach collector:
|
||||
```bash
|
||||
curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets'
|
||||
```
|
||||
126
docker/telemetry/docker-compose.yml
Normal file
126
docker/telemetry/docker-compose.yml
Normal file
@@ -0,0 +1,126 @@
|
||||
# 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. Also tails xrpld log files
|
||||
# via filelog receiver and exports to Loki. 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).
|
||||
# - loki: Grafana Loki log aggregation backend for centralized log
|
||||
# ingestion and log-trace correlation (Phase 8).
|
||||
# - grafana: dashboards on port 3000, pre-configured with Tempo,
|
||||
# Prometheus, and Loki datasources.
|
||||
#
|
||||
# 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
|
||||
- "4318:4318" # OTLP HTTP (traces + native OTel metrics)
|
||||
- "8889:8889" # Prometheus metrics (spanmetrics + OTLP)
|
||||
# StatsD UDP port removed — beast::insight now uses native OTLP.
|
||||
# Uncomment if using server=statsd fallback:
|
||||
# - "8125:8125/udp"
|
||||
volumes:
|
||||
# Mount collector pipeline config (receivers → processors → exporters)
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
|
||||
# Mount the xrpld log root for the filelog receiver. The telemetry
|
||||
# configs write to docker/telemetry/data/logs/<network>/debug.log, so
|
||||
# the default source is the repo-relative ./data/logs — user-owned and
|
||||
# needing no root, so `docker compose up` works with no setup. Override
|
||||
# XRPLD_LOG_DIR to point at another root (e.g. the integration test sets
|
||||
# it to its own workdir). Mounted read-only so the collector only tails.
|
||||
- ${XRPLD_LOG_DIR:-./data/logs}:/var/log/xrpld:ro
|
||||
depends_on:
|
||||
- tempo
|
||||
- loki
|
||||
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 Loki for centralized log ingestion and log-trace
|
||||
# correlation. Loki 3.x supports native OTLP ingestion, so the OTel
|
||||
# Collector exports via otlphttp to Loki's /otlp endpoint.
|
||||
# Query logs via Grafana Explore -> Loki at http://localhost:3000.
|
||||
loki:
|
||||
image: grafana/loki:3.4.2
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
volumes:
|
||||
- loki-data:/loki
|
||||
networks:
|
||||
- xrpld-telemetry
|
||||
|
||||
prometheus:
|
||||
# Pinned to the v2.53 LTS line for reproducible, config-stable runs.
|
||||
image: prom/prometheus:v2.53.3
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
depends_on:
|
||||
- otel-collector
|
||||
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
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
depends_on:
|
||||
- tempo
|
||||
- prometheus
|
||||
- loki
|
||||
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:
|
||||
prometheus-data:
|
||||
loki-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
|
||||
1174
docker/telemetry/grafana/dashboards/consensus-health.json
Normal file
1174
docker/telemetry/grafana/dashboards/consensus-health.json
Normal file
File diff suppressed because it is too large
Load Diff
587
docker/telemetry/grafana/dashboards/ledger-data-sync.json
Normal file
587
docker/telemetry/grafana/dashboards/ledger-data-sync.json
Normal file
@@ -0,0 +1,587 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"description": "What this shows: Ledger data exchange and object-fetch traffic between this node and its peers: ledger sync, tree-node retrieval, and transaction-set exchange.\nUse it to: See how much ledger data the node is pulling or serving and spot catch-up activity.",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "Ledger Data Exchange (Bytes In)",
|
||||
"description": "###### What this is:\n*Inbound bytes for ledger-data message categories, split into aggregate get/share plus the transaction-set, transaction-node, and account-state-node sub-types the node receives from peers.*\n\n###### How it's computed:\n*Per-category inbound byte rate per node.*\n\n###### Reading it:\n*Normally low and flat once synced. Account-state-node traffic dominates during state sync; transaction-set-candidate traffic dominates during consensus catch-up.*\n\n###### Healthy range:\n*workload-dependent; low and steady on a synced node.*\n\n###### Watch for:\n*Sustained high account-state or tx-node inbound bytes on a node that should be caught up (repeated re-sync, missing history), or a single peer driving all traffic.*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_data_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ledger Data Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_data_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ledger Data Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_data_transaction_set_candidate_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Set Candidate Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_data_transaction_set_candidate_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Set Candidate Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_data_transaction_node_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Node Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_data_transaction_node_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Node Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_data_account_state_node_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Account State Node Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_data_account_state_node_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Account State Node Share [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes In",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Ledger Share/Get Traffic (Bytes)",
|
||||
"description": "###### What this is:\n*Inbound bytes for the older ledger share/get message categories and their tx-set, tx-node, and account-state sub-types.*\n\n###### How it's computed:\n*Per-category inbound byte rate per node.*\n\n###### Reading it:\n*Usually small; these legacy categories carry ledger-fetch traffic for peers using the older protocol.*\n\n###### Healthy range:\n*workload-dependent; low on a synced node.*\n\n###### Watch for:\n*Large sustained volumes indicating heavy fetch load or a peer repeatedly requesting the same data.*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ledger Share In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ledger Get In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_transaction_set_candidate_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Set Candidate Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_transaction_set_candidate_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Set Candidate Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_transaction_node_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Node Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_transaction_node_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Node Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_account_state_node_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Account State Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(ledger_account_state_node_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Account State Get [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes In",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "GetObject Traffic by Type (Bytes In)",
|
||||
"description": "###### What this is:\n*Inbound bytes for object-fetch traffic broken down by object type: ledger headers, individual transactions, transaction-tree nodes, and state-tree nodes.*\n\n###### How it's computed:\n*Per-type inbound byte rate per node.*\n\n###### Reading it:\n*Small during steady state; grows when the node fetches missing tree nodes.*\n\n###### Healthy range:\n*workload-dependent; low when synced.*\n\n###### Watch for:\n*A large share on state/tx nodes for long periods (persistent gap-filling), meaning the node keeps catching up.*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_ledger_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ledger Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_ledger_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ledger Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_transaction_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Transaction Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_transaction_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Transaction Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_transaction_node_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Node Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_transaction_node_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Node Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_account_state_node_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Account State Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_account_state_node_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Account State Share [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes In",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "GetObject Aggregate & Special Types (Bytes In)",
|
||||
"description": "###### What this is:\n*Aggregate object-fetch inbound bytes plus special buckets: content-addressed storage fetches, bulk fetch-pack downloads used during catch-up, and bulk transaction fetches.*\n\n###### How it's computed:\n*Per-category inbound byte rate per node.*\n\n###### Reading it:\n*Fetch-pack rises sharply while catching up a range of ledgers; near zero when fully synced.*\n\n###### Healthy range:\n*workload-dependent; low when synced.*\n\n###### Watch for:\n*Continuous fetch-pack traffic (node never fully catches up) or unexpectedly high content-store volume.*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_cas_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "CAS Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_cas_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "CAS Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_fetch_pack_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Fetch Pack Share [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_fetch_pack_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Fetch Pack Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_transactions_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Transactions Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Aggregate Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Aggregate Share [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes In",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "GetObject Messages by Type",
|
||||
"description": "###### What this is:\n*Count of individual object-fetch request/response messages per object type.*\n\n###### How it's computed:\n*Per-type inbound message rate per node.*\n\n###### Reading it:\n*Many messages with few bytes means small piecemeal fetches; few messages with many bytes means large batch transfers.*\n\n###### Healthy range:\n*workload-dependent.*\n\n###### Watch for:\n*High message counts with tiny payloads sustained over time (inefficient per-node fetching).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_ledger_get_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ledger Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_transaction_get_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Transaction Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_transaction_node_get_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Node Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_account_state_node_get_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Account State Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_cas_get_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "CAS Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_fetch_pack_get_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Fetch Pack Get [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(getobject_transactions_get_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Transactions Get [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "cps",
|
||||
"custom": {
|
||||
"axisLabel": "Messages In",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Overlay Traffic Heatmap (All Categories, Bytes In)",
|
||||
"description": "###### What this is:\n*All overlay traffic categories ranked by inbound bytes, giving an at-a-glance view of which message types consume the most receive bandwidth.*\n\n###### How it's computed:\n*Top categories by latest inbound byte value across all traffic categories.*\n\n###### Reading it:\n*The longest bars are the biggest bandwidth consumers; on a synced node transactions, proposals, and validations usually lead.*\n\n###### Healthy range:\n*workload-dependent.*\n\n###### Watch for:\n*A single ledger-data or fetch category dominating (ongoing sync) or an unexpected category topping the list.*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "bargauge",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
},
|
||||
"displayMode": "gradient",
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "topk(20, {service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", __name__=~\".*_bytes_in\", __name__!~\"total_.*\"})",
|
||||
"legendFormat": "{{__name__}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "decbytes",
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 1048576
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 104857600
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": ["ledger", "sync"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"label": "Service Name",
|
||||
"description": "Filter by service.name (e.g. xrpld, xrpld-validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(service_name)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "xrpld",
|
||||
"value": "xrpld"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "deployment_environment",
|
||||
"label": "Environment",
|
||||
"description": "Filter by deployment tier (local/ci/test/prod)",
|
||||
"type": "query",
|
||||
"query": "label_values(deployment_environment)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "xrpl_network_type",
|
||||
"label": "Network",
|
||||
"description": "Filter by XRPL network (mainnet/testnet/devnet)",
|
||||
"type": "query",
|
||||
"query": "label_values(xrpl_network_type)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"label": "Node",
|
||||
"description": "Filter by rippled node (service.instance.id)",
|
||||
"type": "query",
|
||||
"query": "label_values(target_info, service_instance_id)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"title": "Ledger Data & Sync",
|
||||
"uid": "ledger-data-sync"
|
||||
}
|
||||
425
docker/telemetry/grafana/dashboards/ledger-operations.json
Normal file
425
docker/telemetry/grafana/dashboards/ledger-operations.json
Normal file
@@ -0,0 +1,425 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "Ledger Build Rate",
|
||||
"description": "**What:** How often the node constructs a new ledger from its parent, applying transactions and writing state.\n**How it's computed:** Per-second rate over a 5-minute window, per node.\n**Reading it:** Should match the consensus close cadence.\n**Healthy range:** About 0.25/s on mainnet (~4s rounds).\n**Watch for:** A drop below the close rate means the node is not keeping up with ledger construction.\n**Source:** src/xrpld/app/ledger/detail/BuildLedger.cpp:buildLedgerImpl",
|
||||
"type": "stat",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"ledger.build\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Builds / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Ledger Build Duration",
|
||||
"description": "**What:** Time to build a full ledger, including applying transactions, writing state, and finalising acceptance.\n**How it's computed:** 95th and 50th percentile of per-build durations over a 5-minute window, per node.\n**Reading it:** Scales with transaction volume and disk I/O.\n**Healthy range:** Tens to a few hundred milliseconds under typical load.\n**Watch for:** Long build times indicate expensive transaction sets or I/O pressure while flushing state.\n**Source:** src/xrpld/app/ledger/detail/BuildLedger.cpp:buildLedgerImpl",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"ledger.build\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 Build Duration [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.50, sum by (le, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"ledger.build\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P50 Build Duration [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Duration (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Ledger Validation Rate",
|
||||
"description": "**What:** How often ledgers reach the trusted-validation quorum and become fully validated.\n**How it's computed:** Per-second rate over a 5-minute window, per node.\n**Reading it:** Should match the build and close rates on a healthy node.\n**Healthy range:** About 0.25/s on mainnet.\n**Watch for:** A shortfall means ledgers are not accumulating enough trusted validations to be accepted.\n**Source:** src/xrpld/app/ledger/detail/LedgerMaster.cpp:LedgerMaster::checkAccept",
|
||||
"type": "stat",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"ledger.validate\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Validations / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Ledger Build Duration Heatmap",
|
||||
"description": "**What:** Distribution of ledger-build durations over time, revealing spread beyond a single percentile.\n**How it's computed:** Counts of builds by duration band in each 5-minute window, per node.\n**Reading it:** A tight band at low durations is healthy; brighter cells mark the common build time.\n**Healthy range:** Concentrated in the low tens of milliseconds.\n**Watch for:** An occasional high-duration band flags slow builds that percentile charts may miss.\n**Source:** src/xrpld/app/ledger/detail/BuildLedger.cpp:buildLedgerImpl",
|
||||
"type": "heatmap",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
},
|
||||
"yAxis": {
|
||||
"axisLabel": "Duration (ms)"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum(increase(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"ledger.build\"}[5m])) by (le)",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{le}}",
|
||||
"format": "heatmap"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Transaction Apply Duration",
|
||||
"description": "**What:** Time to apply the agreed transaction set into the new ledger, including retry passes.\n**How it's computed:** 95th and 50th percentile of per-ledger apply durations over a 5-minute window, per node.\n**Reading it:** Grows with the number and complexity of transactions in the set.\n**Healthy range:** Tens to a few hundred milliseconds under normal load.\n**Watch for:** Sustained high durations indicate heavy or expensive transaction sets.\n**Source:** src/xrpld/app/ledger/detail/BuildLedger.cpp:applyTransactions",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"tx.apply\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 tx.apply [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.50, sum by (le, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"tx.apply\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P50 tx.apply [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Duration (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Transaction Apply Rate",
|
||||
"description": "**What:** How often the transaction-application phase runs during ledger building.\n**How it's computed:** Per-second rate over a 5-minute window, per node.\n**Reading it:** One apply pass per ledger build, so it should track the build rate.\n**Healthy range:** About 0.25/s on mainnet.\n**Watch for:** Divergence from the build rate signals an accounting or pipeline anomaly.\n**Source:** src/xrpld/app/ledger/detail/BuildLedger.cpp:applyTransactions",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"tx.apply\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "tx.apply / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Operations / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Ledger Store Rate",
|
||||
"description": "**What:** How often completed ledgers are written into the local ledger history.\n**How it's computed:** Per-second rate over a 5-minute window, per node.\n**Reading it:** Should match the build rate under normal operation.\n**Healthy range:** About 0.25/s on mainnet.\n**Watch for:** A store rate below the build rate means ledgers are being built but not persisted.\n**Source:** src/xrpld/app/ledger/detail/LedgerMaster.cpp:LedgerMaster::storeLedger",
|
||||
"type": "stat",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"ledger.store\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Stores / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Build vs Close Duration",
|
||||
"description": "**What:** Ledger construction time versus total ledger-close time; construction is a subset of close.\n**How it's computed:** 95th percentile of each duration over a 5-minute window, per node.\n**Reading it:** Build should sit at or just below close.\n**Healthy range:** Both a few hundred milliseconds or less, tracking each other.\n**Watch for:** A large gap means significant overhead in the consensus pipeline outside ledger construction.\n**Source:** src/xrpld/app/ledger/detail/BuildLedger.cpp:buildLedgerImpl, src/xrpld/app/consensus/RCLConsensus.cpp:RCLConsensus::Adaptor::onClose",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"ledger.build\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 ledger.build [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"consensus.ledger_close\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 consensus.ledger_close [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Duration (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": ["ledger"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"label": "Service Name",
|
||||
"description": "Filter by service.name (e.g. xrpld, xrpld-validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(service_name)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "xrpld",
|
||||
"value": "xrpld"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "deployment_environment",
|
||||
"label": "Environment",
|
||||
"description": "Filter by deployment tier (local/ci/test/prod)",
|
||||
"type": "query",
|
||||
"query": "label_values(deployment_environment)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "xrpl_network_type",
|
||||
"label": "Network",
|
||||
"description": "Filter by XRPL network (mainnet/testnet/devnet)",
|
||||
"type": "query",
|
||||
"query": "label_values(xrpl_network_type)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"label": "Node",
|
||||
"description": "Filter by rippled node (service.instance.id — e.g. Node-1)",
|
||||
"type": "query",
|
||||
"query": "label_values(target_info, service_instance_id)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"description": "Ledger lifecycle: build rate and duration, transaction apply and store rates, validation rate, and close timing.",
|
||||
"title": "Ledger Operations",
|
||||
"uid": "ledger-operations"
|
||||
}
|
||||
865
docker/telemetry/grafana/dashboards/network-traffic.json
Normal file
865
docker/telemetry/grafana/dashboards/network-traffic.json
Normal file
@@ -0,0 +1,865 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"description": "What this shows: Peer connectivity and overlay bandwidth for this node: peer counts, disconnects, total bytes and messages exchanged, and the transaction, proposal, and validation traffic that flows across the peer-to-peer network.\nUse it to: Gauge peer health and overall overlay bandwidth and see which message types dominate network traffic.",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active Peers",
|
||||
"description": "###### What this is:\n*Number of active inbound and outbound peer connections the node currently holds.*\n\n###### How it's computed:\n*Current value of the inbound and outbound active-peer counts per node.*\n\n###### Reading it:\n*Outbound is what the node dials out; inbound is what others open to it. Both should be stable.*\n\n###### Healthy range:\n*roughly 10-21 outbound and 0-85 inbound on mainnet, depending on config.*\n\n###### Watch for:\n*Outbound dropping toward zero (isolation) or inbound pinned at the limit with churn (connection pressure).*\n\n###### Source:\n[PeerfinderManager.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/peerfinder/detail/PeerfinderManager.cpp)\n\n###### Function:\n`Logic Stats ctor`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "peer_finder_active_inbound_peers{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}",
|
||||
"legendFormat": "Inbound Peers [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "peer_finder_active_outbound_peers{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}",
|
||||
"legendFormat": "Outbound Peers [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "none",
|
||||
"custom": {
|
||||
"axisLabel": "Peers",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Peer Disconnects",
|
||||
"description": "###### What this is:\n*Peer connections that dropped, as a per-interval increase.*\n\n###### How it's computed:\n*Disconnect events per rate-interval, per node.*\n\n###### Reading it:\n*A flat or slowly rising line is normal; the slope matters more than the absolute value.*\n\n###### Healthy range:\n*workload-dependent; slow, steady growth.*\n\n###### Watch for:\n*Sharp step-ups in the slope (network instability, resource exhaustion, or many peers dropping the node at once).*\n\n###### Source:\n[OverlayImpl.h](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.h)\n\n###### Function:\n`OverlayImpl::Stats ctor`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "increase(overlay_peer_disconnects{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Disconnects [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "none",
|
||||
"custom": {
|
||||
"axisLabel": "Disconnects per interval",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Total Network Bytes",
|
||||
"description": "###### What this is:\n*Total bytes received and sent across all peer connections.*\n\n###### How it's computed:\n*Per-second in/out byte rate per node.*\n\n###### Reading it:\n*Overall bandwidth footprint; in and out usually track network activity together.*\n\n###### Healthy range:\n*workload-dependent.*\n\n###### Watch for:\n*Sudden sustained jumps not matched by ledger or transaction activity (relay storms or a noisy peer).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(total_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Bytes In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(total_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Bytes Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Total Network Messages",
|
||||
"description": "###### What this is:\n*Total messages received and sent across all peer connections.*\n\n###### How it's computed:\n*Per-second in/out message rate per node.*\n\n###### Reading it:\n*Overall message throughput of the overlay; complements the byte totals.*\n\n###### Healthy range:\n*workload-dependent.*\n\n###### Watch for:\n*Message count climbing far faster than bytes (many tiny messages, possible flooding).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(total_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Messages In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(total_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Messages Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "cps",
|
||||
"custom": {
|
||||
"axisLabel": "Messages",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Transaction Traffic",
|
||||
"description": "###### What this is:\n*Transaction relay messages in and out, plus duplicate transaction messages received.*\n\n###### How it's computed:\n*Per-second message rate for the transaction and transaction-duplicate categories.*\n\n###### Reading it:\n*In/out rise with network transaction volume; duplicates are transactions the node already had.*\n\n###### Healthy range:\n*workload-dependent; duplicates a modest fraction of inbound.*\n\n###### Watch for:\n*Duplicate inbound approaching or exceeding unique inbound (redundant relay), or a sharp spike suggesting transaction flooding.*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(transactions_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Messages In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(transactions_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Messages Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(transactions_duplicate_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Duplicate In [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "cps",
|
||||
"custom": {
|
||||
"axisLabel": "Messages",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Proposal Traffic",
|
||||
"description": "###### What this is:\n*Consensus proposal messages in/out, plus untrusted and duplicate proposal messages received.*\n\n###### How it's computed:\n*Per-second message rate for the proposal, proposal-untrusted, and proposal-duplicate categories.*\n\n###### Reading it:\n*Trusted in/out track consensus rounds; untrusted come from validators not on this node's trusted list.*\n\n###### Healthy range:\n*workload-dependent; untrusted and duplicates low relative to trusted.*\n\n###### Watch for:\n*High untrusted (trusted-list misconfiguration) or high duplicates (inefficient relay or proposal spam).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proposals_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Proposals In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proposals_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Proposals Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proposals_untrusted_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Untrusted In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proposals_duplicate_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Duplicate In [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "cps",
|
||||
"custom": {
|
||||
"axisLabel": "Messages",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Validation Traffic",
|
||||
"description": "###### What this is:\n*Validation messages in/out, plus untrusted and duplicate validation messages received.*\n\n###### How it's computed:\n*Per-second message rate for the validation, validation-untrusted, and validation-duplicate categories.*\n\n###### Reading it:\n*Trusted validations should arrive steadily each ledger; untrusted come from non-trusted validators.*\n\n###### Healthy range:\n*workload-dependent; untrusted and duplicates low relative to trusted.*\n\n###### Watch for:\n*Rising untrusted or duplicate validations (trusted-list health issues or validation spam).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validations_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Validations In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validations_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Validations Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validations_untrusted_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Untrusted In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validations_duplicate_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Duplicate In [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "cps",
|
||||
"custom": {
|
||||
"axisLabel": "Messages",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Overlay Traffic by Category (Bytes In)",
|
||||
"description": "###### What this is:\n*Top overlay traffic categories ranked by inbound bytes, excluding the all-traffic total.*\n\n###### How it's computed:\n*Top categories by inbound byte rate per node.*\n\n###### Reading it:\n*Shows which message types dominate receive bandwidth right now.*\n\n###### Healthy range:\n*workload-dependent; transactions, proposals, and validations typically lead on a synced node.*\n\n###### Watch for:\n*A fetch or ledger-data category topping the list (sync activity) or an unexpected category dominating.*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "bargauge",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "topk(10, rate({service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", __name__=~\".*_bytes_in\", __name__!~\"total_.*\"}[$__rate_interval]))",
|
||||
"legendFormat": "{{__name__}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "transactions_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Transactions"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "proposals_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Proposals"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "validations_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Validations"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "overhead_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Overhead"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "overhead_overlay_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Overhead Overlay"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ping_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Ping"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "status_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Status"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "getobject_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Get Object"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "havetxset_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Have Tx Set"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledgerdata_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Ledger Data"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_share_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Ledger Share"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_data_get_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Ledger Data Get"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_data_share_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Ledger Data Share"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_data_account_state_node_get_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Account State Node Get"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_data_account_state_node_share_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Account State Node Share"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_data_transaction_node_get_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Transaction Node Get"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_data_transaction_node_share_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Transaction Node Share"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_data_transaction_set_candidate_get_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Tx Set Candidate Get"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_account_state_node_share_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Account State Node Share (Legacy)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_transaction_set_candidate_share_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Tx Set Candidate Share"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "ledger_transaction_node_share_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Transaction Node Share (Legacy)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "set_get_bytes_in"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Set Get"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Duplicate Traffic (Wasted Bandwidth)",
|
||||
"description": "###### What this is:\n*Throughput of duplicate transaction, proposal, and validation traffic: messages the node had already seen and discarded.*\n\n###### How it's computed:\n*Per-second rate of the duplicate byte counters for each category, in and out.*\n\n###### Reading it:\n*Lower is better; this is bandwidth spent on redundant relays.*\n\n###### Healthy range:\n*workload-dependent; a small fraction of total traffic.*\n\n###### Watch for:\n*Duplicate rate climbing toward the same order as useful traffic (poor relay topology or redundant flooding).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 32
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(transactions_duplicate_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Duplicate In"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(transactions_duplicate_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "TX Duplicate Out"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proposals_duplicate_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Proposals Duplicate In"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proposals_duplicate_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Proposals Duplicate Out"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validations_duplicate_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Validations Duplicate In"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validations_duplicate_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Validations Duplicate Out"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Throughput",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "All Traffic Categories (Detail)",
|
||||
"description": "###### What this is:\n*The busiest overlay categories by inbound byte rate over time, excluding the all-traffic total.*\n\n###### How it's computed:\n*Per-second inbound byte rate for the busiest categories, ranked, excluding the all-traffic total.*\n\n###### Reading it:\n*Time-series companion to the category bar view; shows how the traffic mix shifts over the window.*\n\n###### Healthy range:\n*workload-dependent.*\n\n###### Watch for:\n*A category ramping up and staying high, or the mix suddenly changing (sync, spam, or a misbehaving peer).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 32
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "topk(15, rate({__name__=~\".*_bytes_in\", __name__!~\"total_.*\", service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval]))",
|
||||
"legendFormat": "{{__name__}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Throughput",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": ["network"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"label": "Service Name",
|
||||
"description": "Filter by service.name (e.g. xrpld, xrpld-validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(service_name)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "xrpld",
|
||||
"value": "xrpld"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "deployment_environment",
|
||||
"label": "Environment",
|
||||
"description": "Filter by deployment tier (local/ci/test/prod)",
|
||||
"type": "query",
|
||||
"query": "label_values(deployment_environment)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "xrpl_network_type",
|
||||
"label": "Network",
|
||||
"description": "Filter by XRPL network (mainnet/testnet/devnet)",
|
||||
"type": "query",
|
||||
"query": "label_values(xrpl_network_type)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"label": "Node",
|
||||
"description": "Filter by rippled node (service.instance.id)",
|
||||
"type": "query",
|
||||
"query": "label_values(target_info, service_instance_id)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"title": "Network Traffic",
|
||||
"uid": "network-traffic"
|
||||
}
|
||||
1024
docker/telemetry/grafana/dashboards/node-health.json
Normal file
1024
docker/telemetry/grafana/dashboards/node-health.json
Normal file
File diff suppressed because it is too large
Load Diff
647
docker/telemetry/grafana/dashboards/overlay-traffic-detail.json
Normal file
647
docker/telemetry/grafana/dashboards/overlay-traffic-detail.json
Normal file
@@ -0,0 +1,647 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"description": "What this shows: Fine-grained breakdown of peer-to-peer overlay traffic beyond the main network view: squelch relay control, protocol overhead, validator-list distribution, transaction-set exchange, transaction availability, ledger-proof and replay traffic, and unclassified messages.\nUse it to: Drill into individual overlay message categories to diagnose relay efficiency, overhead, and catch-up traffic.",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "Squelch Traffic (Messages)",
|
||||
"description": "###### What this is:\n*Squelch relay-control messages in/out, plus messages suppressed by squelch and squelch directives that were ignored. Squelch reduces redundant message forwarding between peers.*\n\n###### How it's computed:\n*Per-second message rate for the squelch, squelch-suppressed, and squelch-ignored categories, in and out.*\n\n###### Reading it:\n*High suppressed counts mean squelch is saving bandwidth; ignored should stay low.*\n\n###### Healthy range:\n*workload-dependent; suppressed far above ignored.*\n\n###### Watch for:\n*High ignored counts (peers not honoring squelch) or squelch traffic itself dominating.*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(squelch_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Squelch In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(squelch_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Squelch Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(squelch_suppressed_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Suppressed In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(squelch_suppressed_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Suppressed Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(squelch_ignored_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ignored In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(squelch_ignored_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Ignored Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "cps",
|
||||
"custom": {
|
||||
"axisLabel": "Messages",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Overhead Traffic Breakdown (Bytes)",
|
||||
"description": "###### What this is:\n*Overlay protocol overhead bytes split into base overhead, intra-cluster overhead, and validator-manifest distribution overhead.*\n\n###### How it's computed:\n*Per-second in/out byte rate for the overhead, overhead-cluster, and overhead-manifest categories.*\n\n###### Reading it:\n*Base overhead is routine; cluster and manifest rise around cluster syncs and manifest changes.*\n\n###### Healthy range:\n*workload-dependent; low and stable.*\n\n###### Watch for:\n*Sustained high cluster or manifest overhead (frequent cluster state churn or manifest reissue).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(overhead_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Base Overhead In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(overhead_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Base Overhead Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(overhead_cluster_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Cluster In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(overhead_cluster_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Cluster Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(overhead_manifest_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Manifest In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(overhead_manifest_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Manifest Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Validator List Traffic",
|
||||
"description": "###### What this is:\n*Bytes and messages exchanged distributing validator lists (trusted-list configuration) between peers.*\n\n###### How it's computed:\n*Per-second in/out byte and message rate for the validator-lists category.*\n\n###### Reading it:\n*Bursts when lists update or new peers connect; quiet otherwise.*\n\n###### Healthy range:\n*workload-dependent; occasional bursts.*\n\n###### Watch for:\n*Continuous high volume (repeated list re-fetching or churn).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validator_lists_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Bytes In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validator_lists_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Bytes Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validator_lists_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Messages In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(validator_lists_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Messages Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "cps",
|
||||
"custom": {
|
||||
"axisLabel": "Count",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byRegexp",
|
||||
"options": "/Bytes/"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.axisPlacement",
|
||||
"value": "right"
|
||||
},
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "binBps"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Set Get/Share Traffic (Bytes)",
|
||||
"description": "###### What this is:\n*Transaction-set fetch (get) and share bytes exchanged during ledger close.*\n\n###### How it's computed:\n*Per-second in/out byte rate for the set-get and set-share categories.*\n\n###### Reading it:\n*Some exchange each ledger is normal as peers reconcile transaction sets.*\n\n###### Healthy range:\n*workload-dependent.*\n\n###### Watch for:\n*High set-get (peers frequently missing transaction sets: possible sync delays).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(set_get_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Set Get In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(set_get_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Set Get Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(set_share_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Set Share In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(set_share_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Set Share Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Have/Requested Transactions (Messages)",
|
||||
"description": "###### What this is:\n*Transaction-availability messages: advertisements that a peer has certain transactions, and explicit requests for transaction data.*\n\n###### How it's computed:\n*Per-second in/out message rate for the have-transactions and requested-transactions categories.*\n\n###### Reading it:\n*Compare requested versus have to gauge how well transactions are propagating.*\n\n###### Healthy range:\n*workload-dependent.*\n\n###### Watch for:\n*Requested far exceeding have (peers behind on transaction propagation).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(have_transactions_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Have TX In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(have_transactions_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Have TX Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(requested_transactions_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Requested TX In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(requested_transactions_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Requested TX Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "cps",
|
||||
"custom": {
|
||||
"axisLabel": "Messages",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Unknown / Unclassified Traffic",
|
||||
"description": "###### What this is:\n*Overlay traffic that matches no known message category, in bytes and messages.*\n\n###### How it's computed:\n*Current in/out byte and message counts for the unknown category.*\n\n###### Reading it:\n*Should be at or near zero.*\n\n###### Healthy range:\n*zero.*\n\n###### Watch for:\n*Any sustained non-zero value (protocol version mismatch, corrupted messages, or an unclassified new message type).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "unknown_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}",
|
||||
"legendFormat": "Unknown Bytes In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "unknown_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}",
|
||||
"legendFormat": "Unknown Bytes Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "unknown_messages_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}",
|
||||
"legendFormat": "Unknown Messages In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "unknown_messages_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}",
|
||||
"legendFormat": "Unknown Messages Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short",
|
||||
"custom": {
|
||||
"axisLabel": "Count",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byRegexp",
|
||||
"options": "/Bytes/"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.axisPlacement",
|
||||
"value": "right"
|
||||
},
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "decbytes"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Proof Path Traffic",
|
||||
"description": "###### What this is:\n*Proof-path request/response bytes used to verify individual ledger entries without downloading the whole ledger.*\n\n###### How it's computed:\n*Per-second in/out byte rate for the proof-path request and response categories.*\n\n###### Reading it:\n*Rises when peers verify specific state, often during catch-up.*\n\n###### Healthy range:\n*workload-dependent.*\n\n###### Watch for:\n*High sustained request volume (heavy state-verification load).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proof_path_request_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Request Bytes In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proof_path_request_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Request Bytes Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proof_path_response_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Response Bytes In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(proof_path_response_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Response Bytes Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Replay Delta Traffic",
|
||||
"description": "###### What this is:\n*Replay-delta request/response bytes used to efficiently replay ledger state changes during catch-up.*\n\n###### How it's computed:\n*Per-second in/out byte rate for the replay-delta request and response categories.*\n\n###### Reading it:\n*Active during catch-up and replay; quiet when synced.*\n\n###### Healthy range:\n*workload-dependent; low when synced.*\n\n###### Watch for:\n*Continuous replay traffic (node repeatedly replaying rather than staying current).*\n\n###### Source:\n[OverlayImpl.cpp](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/overlay/detail/OverlayImpl.cpp)\n\n###### Function:\n`OverlayImpl ctor (TrafficGauges)`",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(replay_delta_request_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Request Bytes In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(replay_delta_request_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Request Bytes Out [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(replay_delta_response_bytes_in{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Response Bytes In [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(replay_delta_response_bytes_out{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"legendFormat": "Response Bytes Out [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "Bps",
|
||||
"custom": {
|
||||
"axisLabel": "Bytes",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": ["network", "peer"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"label": "Service Name",
|
||||
"description": "Filter by service.name (e.g. xrpld, xrpld-validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(service_name)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "xrpld",
|
||||
"value": "xrpld"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "deployment_environment",
|
||||
"label": "Environment",
|
||||
"description": "Filter by deployment tier (local/ci/test/prod)",
|
||||
"type": "query",
|
||||
"query": "label_values(deployment_environment)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "xrpl_network_type",
|
||||
"label": "Network",
|
||||
"description": "Filter by XRPL network (mainnet/testnet/devnet)",
|
||||
"type": "query",
|
||||
"query": "label_values(xrpl_network_type)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"label": "Node",
|
||||
"description": "Filter by rippled node (service.instance.id)",
|
||||
"type": "query",
|
||||
"query": "label_values(target_info, service_instance_id)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"title": "Overlay Traffic Detail",
|
||||
"uid": "overlay-traffic-detail"
|
||||
}
|
||||
291
docker/telemetry/grafana/dashboards/peer-network.json
Normal file
291
docker/telemetry/grafana/dashboards/peer-network.json
Normal file
@@ -0,0 +1,291 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"description": "Peer-to-peer consensus traffic: rates of proposals and validations received from peers and their split between trusted and untrusted sources.",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "Peer Proposal Receive Rate",
|
||||
"description": "**What:** Rate of consensus proposals arriving from connected network peers.\n**How it's computed:** Per-second rate over a 5-minute window, per node.\n**Reading it:** Reflects how many validators' proposals reach this node each round.\n**Healthy range:** Workload-dependent; scales with peer count and network activity.\n**Watch for:** A sudden drop means loss of proposal flow; a sharp sustained spike from a single peer can indicate flooding.\n**Source:** src/xrpld/overlay/detail/PeerImp.cpp:PeerImp::onMessage(TMProposeSet)",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"peer.proposal.receive\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Proposals Received / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Proposals / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Peer Validation Receive Rate",
|
||||
"description": "**What:** Rate of ledger validations arriving from connected network peers.\n**How it's computed:** Per-second rate over a 5-minute window, per node.\n**Reading it:** Reflects the volume of validation traffic reaching this node.\n**Healthy range:** Workload-dependent; scales with the number of validators and peers.\n**Watch for:** A drop starves the validation quorum; an abnormal spike from one peer can indicate abuse.\n**Source:** src/xrpld/overlay/detail/PeerImp.cpp:PeerImp::onMessage(TMValidation)",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"peer.validation.receive\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Validations Received / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Validations / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Proposals Trusted vs Untrusted",
|
||||
"description": "**What:** Share of received proposals from trusted validators (in this node's UNL) versus untrusted sources.\n**How it's computed:** Total counts split by trust status over a 5-minute window, shown as proportions.\n**Reading it:** A well-connected node with a good UNL sees a healthy trusted share.\n**Healthy range:** A substantial trusted proportion; exact mix is workload-dependent.\n**Watch for:** A collapse of the trusted share means poor connectivity to UNL validators; a flood of untrusted proposals can indicate abuse.\n**Source:** src/xrpld/overlay/detail/PeerImp.cpp:PeerImp::onMessage(TMProposeSet)",
|
||||
"type": "piechart",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (proposal_trusted, service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", proposal_trusted=~\"$proposal_trusted\", span_name=\"peer.proposal.receive\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Trusted = {{proposal_trusted}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Validations Trusted vs Untrusted",
|
||||
"description": "**What:** Share of received validations from trusted validators (in this node's UNL) versus untrusted sources.\n**How it's computed:** Total counts split by trust status over a 5-minute window, shown as proportions.\n**Reading it:** Confirms the node is hearing from its expected trusted validator set.\n**Healthy range:** A substantial trusted proportion; exact mix is workload-dependent.\n**Watch for:** A shrinking trusted share risks the validation quorum; a surge of untrusted validations can indicate abuse.\n**Source:** src/xrpld/overlay/detail/PeerImp.cpp:PeerImp::onMessage(TMValidation)",
|
||||
"type": "piechart",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (validation_trusted, service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", validation_trusted=~\"$validation_trusted\", span_name=\"peer.validation.receive\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Trusted = {{validation_trusted}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": ["network", "peer"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"label": "Service Name",
|
||||
"description": "Filter by service.name (e.g. xrpld, xrpld-validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(service_name)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "xrpld",
|
||||
"value": "xrpld"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "deployment_environment",
|
||||
"label": "Environment",
|
||||
"description": "Filter by deployment tier (local/ci/test/prod)",
|
||||
"type": "query",
|
||||
"query": "label_values(deployment_environment)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "xrpl_network_type",
|
||||
"label": "Network",
|
||||
"description": "Filter by XRPL network (mainnet/testnet/devnet)",
|
||||
"type": "query",
|
||||
"query": "label_values(xrpl_network_type)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"label": "Node",
|
||||
"description": "Filter by rippled node (service.instance.id — e.g. Node-1)",
|
||||
"type": "query",
|
||||
"query": "label_values(target_info, service_instance_id)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "proposal_trusted",
|
||||
"label": "Proposal Trusted",
|
||||
"description": "Filter by proposal trust status (true = from trusted validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(span_calls_total{span_name=\"peer.proposal.receive\"}, proposal_trusted)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "validation_trusted",
|
||||
"label": "Validation Trusted",
|
||||
"description": "Filter by validation trust status (true = from trusted validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(span_calls_total{span_name=\"peer.validation.receive\"}, validation_trusted)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"title": "Peer Network",
|
||||
"uid": "peer-network"
|
||||
}
|
||||
728
docker/telemetry/grafana/dashboards/rpc-pathfinding.json
Normal file
728
docker/telemetry/grafana/dashboards/rpc-pathfinding.json
Normal file
@@ -0,0 +1,728 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"description": "Client-facing RPC behavior and payment pathfinding cost on this node: request rates, response times and sizes, resource-limit warnings and drops, gRPC read-path load, and pathfinding request, compute, and discovery activity.",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "RPC Request Rate",
|
||||
"description": "**What:** Rate of RPC requests handled, counting every HTTP and WebSocket call.\n**How it's computed:** Per-second rate of the RPC-requests counter over 5 minutes.\n**Reading it:** Tracks client demand on the node.\n**Healthy range:** workload-dependent.\n**Watch for:** Sudden sustained spikes (client surge or abusive polling) or a drop to zero (endpoint unavailable).\n**Source:** src/xrpld/rpc/detail/ServerHandler.cpp ServerHandler ctor",
|
||||
"type": "stat",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(rpc_requests_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Requests / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "reqps"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RPC Response Time",
|
||||
"description": "**What:** P95 and P50 of end-to-end RPC handler time (full HTTP handling, not just command execution).\n**How it's computed:** 95th and 50th percentiles over a 5-minute window.\n**Reading it:** Lower is better; P95 shows the slow tail.\n**Healthy range:** workload-dependent; most methods well under a second.\n**Watch for:** Rising P95 while request rate is flat (expensive queries or contention).\n**Source:** src/xrpld/rpc/detail/ServerHandler.cpp ServerHandler ctor",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(rpc_time_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 Response Time [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.5, sum by (le, service_instance_id) (rate(rpc_time_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P50 Response Time [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Latency (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RPC Response Size",
|
||||
"description": "**⚠ Instrument mismatch — values unreliable.** Response size is recorded through the millisecond-scaled event histogram (rpc_size_milliseconds_bucket), so byte values saturate at the top time bucket (5000) and the percentiles are not true byte sizes. A dedicated byte-unit histogram is needed to fix this; tracked separately. Treat this panel as indicative only until then.\n\n**What:** P95 and P50 of RPC response payload size in bytes.\n**How it's computed:** 95th and 50th percentiles over the dashboard rate interval.\n**Reading it:** Larger responses cost more bandwidth and CPU to build.\n**Healthy range:** workload-dependent.\n**Watch for:** Large P95 (result-heavy queries such as broad account_tx, or API misuse).\n**Source:** src/xrpld/rpc/detail/ServerHandler.cpp ServerHandler ctor",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(rpc_size_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 Response Size [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.5, sum by (le, service_instance_id) (rate(rpc_size_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P50 Response Size [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "decbytes",
|
||||
"custom": {
|
||||
"axisLabel": "Size (Bytes)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RPC Response Time Distribution",
|
||||
"description": "**What:** RPC response-time spread shown as P50, P90, P95, and P99.\n**How it's computed:** Four quantiles of response time over a 5-minute window.\n**Reading it:** A wide gap between P50 and P99 signals a long latency tail.\n**Healthy range:** workload-dependent; quantiles clustered together.\n**Watch for:** P99 pulling far above P50 (bimodal latency or occasional very slow requests).\n**Source:** src/xrpld/rpc/detail/ServerHandler.cpp ServerHandler ctor",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.5, sum by (le, service_instance_id) (rate(rpc_time_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P50 [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.9, sum by (le, service_instance_id) (rate(rpc_time_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P90 [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(rpc_time_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.99, sum by (le, service_instance_id) (rate(rpc_time_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P99 [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Latency (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Pathfinding Fast Duration",
|
||||
"description": "**What:** P95 and P50 execution time of the fast pathfinding search (simplified, favoring speed over completeness).\n**How it's computed:** 95th and 50th percentiles over a 5-minute window.\n**Reading it:** Lower is better; fast pathfinding should stay quick.\n**Healthy range:** workload-dependent; typically well below full pathfinding.\n**Watch for:** Rising fast-path latency (pathfinding load or complex order books).\n**Source:** src/xrpld/rpc/detail/PathRequestManager.h ctor",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(pathfind_fast_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 Fast Pathfind [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.5, sum by (le, service_instance_id) (rate(pathfind_fast_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P50 Fast Pathfind [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Duration (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Pathfinding Full Duration",
|
||||
"description": "**What:** P95 and P50 execution time of the full, exhaustive pathfinding search.\n**How it's computed:** 95th and 50th percentiles over a 5-minute window.\n**Reading it:** Full pathfinding is heavier and slower than fast mode.\n**Healthy range:** workload-dependent.\n**Watch for:** High or rising full-path latency (expensive path computation under subscription load).\n**Source:** src/xrpld/rpc/detail/PathRequestManager.h ctor",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(pathfind_full_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 Full Pathfind [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.5, sum by (le, service_instance_id) (rate(pathfind_full_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P50 Full Pathfind [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Duration (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Resource Warnings Rate",
|
||||
"description": "**What:** Rate of resource-limit warnings raised when a peer or client exceeds its usage warning threshold.\n**How it's computed:** Per-second rate of the warn counter over 5 minutes.\n**Reading it:** Occasional warnings are normal under load; a rising rate flags aggressive clients.\n**Healthy range:** workload-dependent; low.\n**Watch for:** A climbing warning rate (clients approaching limits, a precursor to drops).\n**Source:** include/xrpl/resource/detail/Logic.h Logic::Stats ctor",
|
||||
"type": "stat",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(warn_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Warnings / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 0.1
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Resource Drops Rate",
|
||||
"description": "**What:** Rate of resource drops: consumers disconnected or blocked for excessive usage.\n**How it's computed:** Per-second rate of the drop counter over 5 minutes.\n**Reading it:** Non-zero means the node is actively rejecting abusive connections.\n**Healthy range:** at or near zero.\n**Watch for:** Sustained non-zero drops (ongoing abuse or a misbehaving client/peer being throttled).\n**Source:** include/xrpl/resource/detail/Logic.h Logic::Stats ctor",
|
||||
"type": "stat",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "rate(drop_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\"}[$__rate_interval])",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Drops / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 0.01
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0.1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "gRPC Request Rate by Method (Spans)",
|
||||
"description": "**What:** Per-method gRPC call rate for the gRPC read API used by reporting and Clio.\n**How it's computed:** Per-second count of gRPC calls grouped by method.\n**Reading it:** Shows which gRPC methods are called and how often; only populated when the node serves gRPC.\n**Healthy range:** workload-dependent.\n**Watch for:** A single method spiking (heavy reporting load) or unexpected methods appearing.\n**Source:** src/xrpld/rpc/GRPCServer.cpp (gRPC method handlers)",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 32
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (method, service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", method=~\"$grpc_method\", span_name=~\"grpc\\\\..*\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{method}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Calls / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "gRPC Latency P95 by Method (Spans)",
|
||||
"description": "**What:** P95 latency per gRPC method.\n**How it's computed:** 95th percentile of gRPC call duration per method over 5 minutes.\n**Reading it:** Identifies slow gRPC read paths.\n**Healthy range:** workload-dependent.\n**Watch for:** One method's P95 rising (expensive ledger reads or backend pressure).\n**Source:** src/xrpld/rpc/GRPCServer.cpp (gRPC method handlers)",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 32
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, method, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", method=~\"$grpc_method\", span_name=~\"grpc\\\\..*\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{method}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Duration (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "gRPC Error Rate by Status (Spans)",
|
||||
"description": "**What:** Rate of gRPC calls broken down by result status: success, error, resource-exhausted, failed-precondition.\n**How it's computed:** Per-second count of gRPC calls grouped by status.\n**Reading it:** Errors and resource-exhausted should be a small share.\n**Healthy range:** workload-dependent; mostly success.\n**Watch for:** Rising error or resource-exhausted rate (clients hitting limits or failing reads).\n**Source:** src/xrpld/rpc/GRPCServer.cpp (gRPC method handlers)",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 40
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (grpc_status, service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=~\"grpc\\\\..*\", grpc_status!=\"\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{grpc_status}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Calls / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Pathfinding Compute Duration (Spans)",
|
||||
"description": "**What:** P95 and P50 of per-request path computation time.\n**How it's computed:** 95th and 50th percentiles of path-computation duration over 5 minutes.\n**Reading it:** Complements the fast/full timers with per-request visibility; populated under book/path RPC load.\n**Healthy range:** workload-dependent.\n**Watch for:** Rising compute time (complex paths or heavy pathfinding demand).\n**Source:** src/xrpld/rpc/detail/PathRequest.cpp PathRequest::doUpdate",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 40
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"pathfind.compute\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 Compute [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.50, sum by (le, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"pathfind.compute\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P50 Compute [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Duration (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Pathfinding Request & Discovery Rate (Spans)",
|
||||
"description": "**What:** Rate of client path requests and of path-discovery passes.\n**How it's computed:** Per-second count of path-request and path-discovery operations.\n**Reading it:** Shows pathfinding demand and the discovery cost driver for subscription-heavy nodes.\n**Healthy range:** workload-dependent.\n**Watch for:** Discovery rate climbing (subscription load driving repeated path discovery).\n**Source:** src/xrpld/rpc/handlers/orderbook/PathFind.cpp doPathFind; src/xrpld/rpc/detail/PathRequest.cpp PathRequest::findPaths",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 48
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"pathfind.request\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Requests / Sec [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"pathfind.discover\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Discoveries / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Operations / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": ["rpc", "pathfinding"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"label": "Service Name",
|
||||
"description": "Filter by service.name (e.g. xrpld, xrpld-validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(service_name)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "xrpld",
|
||||
"value": "xrpld"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "deployment_environment",
|
||||
"label": "Environment",
|
||||
"description": "Filter by deployment tier (local/ci/test/prod)",
|
||||
"type": "query",
|
||||
"query": "label_values(deployment_environment)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "xrpl_network_type",
|
||||
"label": "Network",
|
||||
"description": "Filter by XRPL network (mainnet/testnet/devnet)",
|
||||
"type": "query",
|
||||
"query": "label_values(xrpl_network_type)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"label": "Node",
|
||||
"description": "Filter by rippled node (service.instance.id)",
|
||||
"type": "query",
|
||||
"query": "label_values(target_info, service_instance_id)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "grpc_method",
|
||||
"label": "gRPC Method",
|
||||
"description": "Filter by gRPC method (GetLedger, GetLedgerData, GetLedgerDiff, GetLedgerEntry)",
|
||||
"type": "query",
|
||||
"query": "label_values(span_calls_total{span_name=~\"grpc\\\\..*\"}, method)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"title": "RPC & Pathfinding",
|
||||
"uid": "rpc-pathfinding"
|
||||
}
|
||||
546
docker/telemetry/grafana/dashboards/rpc-performance.json
Normal file
546
docker/telemetry/grafana/dashboards/rpc-performance.json
Normal file
@@ -0,0 +1,546 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "RPC Request Rate by Command",
|
||||
"description": "**What:** Per-command throughput of RPC calls the node serves (e.g. server_info, submit, account_info).\n**How it's computed:** Per-second rate split by command name over a 5-minute window, per node.\n**Reading it:** Shows which API endpoints drive load and how demand shifts over time.\n**Healthy range:** Workload-dependent; varies with client mix.\n**Watch for:** A sudden spike on one command can indicate a runaway client or scripted abuse.\n**Source:** src/xrpld/rpc/detail/RPCHandler.cpp:callMethod",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (command, service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=~\"rpc.command.*\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{command}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "reqps",
|
||||
"custom": {
|
||||
"axisLabel": "Requests / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RPC Latency P95 by Command",
|
||||
"description": "**What:** 95th-percentile response time for each RPC command.\n**How it's computed:** 95th percentile of per-command durations over a 5-minute window, per node.\n**Reading it:** Read-only lookups should be fast; heavy commands like path finding are naturally slower.\n**Healthy range:** Sub-second for most commands; command-dependent.\n**Watch for:** Rising p95 on a normally fast command signals contention or an expensive query pattern.\n**Source:** src/xrpld/rpc/detail/RPCHandler.cpp:callMethod",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, sum by (le, command, service_instance_id) (rate(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=~\"rpc.command.*\"}[5m])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "P95 {{command}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms",
|
||||
"custom": {
|
||||
"axisLabel": "Latency (ms)",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RPC Error Rate",
|
||||
"description": "**What:** Percentage of each RPC command's calls that ended in an error.\n**How it's computed:** Error calls divided by total calls per command over a 5-minute window, per node.\n**Reading it:** Green below 1%, yellow 1-5%, red above 5%.\n**Healthy range:** Below 1% for healthy commands.\n**Watch for:** A sustained error spike on one command is consistent with a client probing or misusing that endpoint.\n**Source:** src/xrpld/rpc/detail/RPCHandler.cpp:callMethod",
|
||||
"type": "bargauge",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (command, service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=~\"rpc.command.*\", status_code=\"STATUS_CODE_ERROR\"}[$__rate_interval])) / sum by (command, service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=~\"rpc.command.*\"}[$__rate_interval])) * 100",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{command}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RPC Latency Heatmap",
|
||||
"description": "**What:** Distribution of RPC response times over time across all commands.\n**How it's computed:** Counts of requests by duration band in each 5-minute window, per node.\n**Reading it:** A single tight band is healthy; brighter cells mark the common latency.\n**Healthy range:** Concentrated at low latency; command-dependent.\n**Watch for:** Two separate bands (bimodal latency) reveal a slow path affecting some requests.\n**Source:** src/xrpld/rpc/detail/RPCHandler.cpp:callMethod",
|
||||
"type": "heatmap",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
},
|
||||
"yAxis": {
|
||||
"axisLabel": "Duration (ms)",
|
||||
"unit": "ms"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum(increase(span_duration_milliseconds_bucket{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=~\"rpc.command.*\"}[5m])) by (le)",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{le}}",
|
||||
"format": "heatmap"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Overall RPC Throughput",
|
||||
"description": "**What:** Aggregate request flow through two pipeline layers: the outer HTTP handler that accepts connections and the inner layer that parses and dispatches.\n**How it's computed:** Per-second rate of each layer over a 5-minute window, per node.\n**Reading it:** The two lines should overlap when requests flow cleanly.\n**Healthy range:** Workload-dependent; both layers tracking each other.\n**Watch for:** A gap means requests are being queued or rejected before dispatch.\n**Source:** src/xrpld/rpc/detail/ServerHandler.cpp:ServerHandler::processSession, ServerHandler::processRequest",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=\"rpc.http_request\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "rpc.http_request / Sec [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=\"rpc.process\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "rpc.process / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "reqps",
|
||||
"custom": {
|
||||
"axisLabel": "Requests / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RPC Success vs Error",
|
||||
"description": "**What:** Aggregate rate of successful versus failed RPC commands across all types.\n**How it's computed:** Per-second rate of each outcome over a 5-minute window, per node.\n**Reading it:** Success should dominate; the error line should stay low.\n**Healthy range:** Error rate near zero; success rate is workload-dependent.\n**Watch for:** A sustained rise in the error line warrants drilling into the per-command breakdown.\n**Source:** src/xrpld/rpc/detail/RPCHandler.cpp:callMethod",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 16
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=~\"rpc.command.*\", status_code=\"STATUS_CODE_UNSET\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Success [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=~\"rpc.command.*\", status_code=\"STATUS_CODE_ERROR\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Error [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Commands / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Top Commands by Volume",
|
||||
"description": "**What:** The ten most-called RPC commands by total invocations recently.\n**How it's computed:** Ranked total counts per command over a 5-minute window, per node.\n**Reading it:** Identifies the hottest API endpoints driving node load.\n**Healthy range:** Workload-dependent.\n**Watch for:** An unexpected command dominating the ranking can indicate scripted abuse or a misbehaving client.\n**Source:** src/xrpld/rpc/detail/RPCHandler.cpp:callMethod",
|
||||
"type": "bargauge",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "topk(10, sum by (command, service_instance_id) (increase(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=~\"rpc.command.*\"}[$__rate_interval])))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{command}} [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "WebSocket Message Rate",
|
||||
"description": "**What:** Rate of incoming WebSocket RPC messages the server processes.\n**How it's computed:** Per-second rate over a 5-minute window, per node.\n**Reading it:** Non-zero only when clients connect over WebSocket rather than HTTP.\n**Healthy range:** Zero is normal for HTTP-only deployments; otherwise workload-dependent.\n**Watch for:** An unexpected surge points to a chatty or abusive WebSocket client.\n**Source:** src/xrpld/rpc/detail/ServerHandler.cpp:ServerHandler::processSession",
|
||||
"type": "stat",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 24
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", command=~\"$command\", span_name=\"rpc.ws_message\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "WS Messages / Sec [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops"
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RPC Resource Cost by Command",
|
||||
"description": "**What:** RPC traffic grouped by its resource-cost category, distinguishing cheap lookups from expensive or malformed requests.\n**How it's computed:** Per-second rate per cost category over a 5-minute window.\n**Reading it:** Low-cost categories should dominate.\n**Healthy range:** Mostly low-cost traffic; workload-dependent.\n**Watch for:** A rising share of high-cost or malformed categories points to problematic or abusive clients.\n**Source:** src/xrpld/rpc/detail/RPCHandler.cpp:callMethod",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 32
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "table",
|
||||
"placement": "right",
|
||||
"calcs": ["mean", "lastNotNull"]
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (load_type) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=~\"rpc.command.*\", load_type!=\"\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "{{load_type}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Requests / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Batch vs Single RPC Requests",
|
||||
"description": "**What:** Rate of batched RPC requests versus single requests.\n**How it's computed:** Per-second rate of each over a 5-minute window, per node.\n**Reading it:** Most traffic is typically single requests.\n**Healthy range:** Workload-dependent; batch share usually small.\n**Watch for:** A high batch rate may indicate bulk-automation clients or an attempt to amplify load per connection.\n**Source:** src/xrpld/rpc/detail/ServerHandler.cpp:ServerHandler::processRequest",
|
||||
"type": "timeseries",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 32
|
||||
},
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"rpc.process\", is_batch=\"true\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Batch [{{service_instance_id}}]"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus"
|
||||
},
|
||||
"expr": "sum by (service_instance_id) (rate(span_calls_total{service_instance_id=~\"$node\", deployment_environment=~\"$deployment_environment\", xrpl_network_type=~\"$xrpl_network_type\", service_name=~\"$service_name\", span_name=\"rpc.process\", is_batch=\"false\"}[$__rate_interval]))",
|
||||
"interval": "15s",
|
||||
"legendFormat": "Single [{{service_instance_id}}]"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {
|
||||
"axisLabel": "Requests / Sec",
|
||||
"spanNulls": true,
|
||||
"insertNulls": false,
|
||||
"showPoints": "auto",
|
||||
"pointSize": 3
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 39,
|
||||
"tags": ["rpc"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "service_name",
|
||||
"label": "Service Name",
|
||||
"description": "Filter by service.name (e.g. xrpld, xrpld-validator)",
|
||||
"type": "query",
|
||||
"query": "label_values(service_name)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "xrpld",
|
||||
"value": "xrpld"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "deployment_environment",
|
||||
"label": "Environment",
|
||||
"description": "Filter by deployment tier (local/ci/test/prod)",
|
||||
"type": "query",
|
||||
"query": "label_values(deployment_environment)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "xrpl_network_type",
|
||||
"label": "Network",
|
||||
"description": "Filter by XRPL network (mainnet/testnet/devnet)",
|
||||
"type": "query",
|
||||
"query": "label_values(xrpl_network_type)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"label": "Node",
|
||||
"description": "Filter by rippled node (service.instance.id — e.g. Node-1)",
|
||||
"type": "query",
|
||||
"query": "label_values(target_info, service_instance_id)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "command",
|
||||
"label": "RPC Command",
|
||||
"description": "Filter by RPC command name (e.g., server_info, submit)",
|
||||
"type": "query",
|
||||
"query": "label_values(span_calls_total{span_name=~\"rpc.command.*\"}, command)",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"multi": true,
|
||||
"refresh": 2,
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"title": "RPC Performance",
|
||||
"uid": "rpc-performance",
|
||||
"description": "Client RPC service health: per-command request rates, latency, error rates, throughput across pipeline layers, and resource cost."
|
||||
}
|
||||
1022
docker/telemetry/grafana/dashboards/transaction-overview.json
Normal file
1022
docker/telemetry/grafana/dashboards/transaction-overview.json
Normal file
File diff suppressed because it is too large
Load Diff
127
docker/telemetry/grafana/dashboards/validate_dashboards.py
Executable file
127
docker/telemetry/grafana/dashboards/validate_dashboards.py
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dashboard lint: cumulative metrics must be rate()-wrapped; tier filters present."""
|
||||
|
||||
import json, re, sys
|
||||
|
||||
# Prometheus gauges that hold a CUMULATIVE total -> must be rate()/increase()-wrapped.
|
||||
CUMULATIVE_PREFIXES = (
|
||||
"total_bytes_",
|
||||
"total_messages_",
|
||||
"transactions_messages_",
|
||||
"transactions_duplicate_messages_",
|
||||
"proposals_",
|
||||
"validations_",
|
||||
"overlay_peer_disconnects",
|
||||
"squelch_",
|
||||
"overhead_",
|
||||
"validator_lists_",
|
||||
"set_get_",
|
||||
"set_share_",
|
||||
"have_transactions_",
|
||||
"requested_transactions_",
|
||||
"proof_path_",
|
||||
"replay_delta_",
|
||||
"ledger_data_",
|
||||
"ledger_get_",
|
||||
"ledger_share_",
|
||||
"ledger_transaction_",
|
||||
"ledger_account_state_",
|
||||
"getobject_",
|
||||
"jq_trans_overflow_total",
|
||||
)
|
||||
# nodestore_state{metric=...} cumulative sub-series (raw only inside these metric= labels).
|
||||
NODESTORE_CUMULATIVE = (
|
||||
"node_reads_total",
|
||||
"node_reads_hit",
|
||||
"node_writes",
|
||||
"node_read_bytes",
|
||||
"node_written_bytes",
|
||||
"node_reads_duration_us",
|
||||
)
|
||||
# state_accounting_*_duration are cumulative µs.
|
||||
STATE_DURATION = re.compile(r"state_accounting_\w+_duration")
|
||||
REQUIRED_FILTERS = (
|
||||
"$node",
|
||||
"$deployment_environment",
|
||||
"$xrpl_network_type",
|
||||
"$service_name",
|
||||
)
|
||||
|
||||
|
||||
def iter_panels(dash):
|
||||
for p in dash.get("panels", []):
|
||||
yield p
|
||||
for sub in p.get("panels", []) or []:
|
||||
yield sub
|
||||
|
||||
|
||||
def expr_is_wrapped(expr):
|
||||
return (
|
||||
"rate(" in expr
|
||||
or "increase(" in expr
|
||||
or "irate(" in expr
|
||||
or "histogram_quantile(" in expr
|
||||
)
|
||||
|
||||
|
||||
def check(path, forbid_5m):
|
||||
errs = []
|
||||
try:
|
||||
dash = json.load(open(path))
|
||||
except Exception as e:
|
||||
return [f"{path}: INVALID JSON: {e}"]
|
||||
for p in iter_panels(dash):
|
||||
title = p.get("title", "<untitled>")
|
||||
for tg in p.get("targets", []) or []:
|
||||
expr = (tg.get("expr") or "").strip()
|
||||
if not expr:
|
||||
continue
|
||||
# tier filters
|
||||
if "{" in expr:
|
||||
for f in REQUIRED_FILTERS:
|
||||
if f not in expr:
|
||||
errs.append(f"{path} [{title}]: missing {f} in expr")
|
||||
# cumulative metric plotted raw?
|
||||
hits = [m for m in CUMULATIVE_PREFIXES if m in expr]
|
||||
nod = [m for m in NODESTORE_CUMULATIVE if f'"{m}"' in expr]
|
||||
statedur = STATE_DURATION.search(expr)
|
||||
if (hits or nod or statedur) and not expr_is_wrapped(expr):
|
||||
who = hits or nod or [statedur.group(0)]
|
||||
errs.append(
|
||||
f"{path} [{title}]: cumulative metric {who} plotted RAW (needs rate/increase)"
|
||||
)
|
||||
if "deriv(" in expr or "idelta(" in expr:
|
||||
errs.append(
|
||||
f"{path} [{title}]: uses deriv()/idelta() on a cumulative series"
|
||||
)
|
||||
if forbid_5m and ("[5m]" in expr or "[1h]" in expr):
|
||||
# Only a fixed [5m]/[1h] inside a top-level rate()/increase()/irate() on a
|
||||
# plain counter is a violation. Legitimate fixed-window exprs -- histogram
|
||||
# buckets (rate(..._bucket[5m])), avg_over_time windows, and subqueries
|
||||
# ([5m:]) -- are intentional and must not be flagged.
|
||||
if not ("_bucket" in expr or "avg_over_time" in expr or ":]" in expr):
|
||||
errs.append(
|
||||
f"{path} [{title}]: hardcoded range window; use [$__rate_interval]"
|
||||
)
|
||||
# unit check
|
||||
unit = p.get("fieldConfig", {}).get("defaults", {}).get("unit", "")
|
||||
if unit == "mps":
|
||||
errs.append(f"{path} [{title}]: invalid Grafana unit 'mps' (use 'cps')")
|
||||
return errs
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
forbid_5m = "--no-5m" in sys.argv
|
||||
all_errs = []
|
||||
for path in args:
|
||||
all_errs += check(path, forbid_5m)
|
||||
if all_errs:
|
||||
print("\n".join(all_errs))
|
||||
print(f"\nFAIL: {len(all_errs)} violation(s)")
|
||||
sys.exit(1)
|
||||
print(f"OK: {len(args)} dashboard(s) passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: xrpld-telemetry
|
||||
orgId: 1
|
||||
folder: xrpld
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
foldersFromFilesStructure: false
|
||||
20
docker/telemetry/grafana/provisioning/datasources/loki.yaml
Normal file
20
docker/telemetry/grafana/provisioning/datasources/loki.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# Grafana Loki data source provisioning for rippled log-trace correlation.
|
||||
#
|
||||
# Loki ingests rippled logs via OTel Collector's filelog receiver.
|
||||
# The derivedFields config links trace_id values in log lines back to
|
||||
# Tempo traces, enabling one-click log-to-trace navigation in Grafana.
|
||||
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Loki
|
||||
type: loki
|
||||
access: proxy
|
||||
url: http://loki:3100
|
||||
uid: loki
|
||||
jsonData:
|
||||
derivedFields:
|
||||
- datasourceUid: tempo
|
||||
matcherRegex: "trace_id=(\\w+)"
|
||||
name: TraceID
|
||||
url: "$${__value.raw}"
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
uid: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
228
docker/telemetry/grafana/provisioning/datasources/tempo.yaml
Normal file
228
docker/telemetry/grafana/provisioning/datasources/tempo.yaml
Normal file
@@ -0,0 +1,228 @@
|
||||
# Grafana datasource provisioning for Grafana Tempo.
|
||||
# Auto-configures Tempo as a trace data source on Grafana startup.
|
||||
# Access Grafana at http://localhost:3000, then use Explore -> Tempo
|
||||
# to browse xrpld traces using TraceQL.
|
||||
#
|
||||
# Search filters provide pre-configured dropdowns in the Explore UI.
|
||||
# Each phase adds filters for the span attributes it introduces.
|
||||
# Base filters — node identity, service, span name, status.
|
||||
# RPC command, status, role filters.
|
||||
# Transaction hash, local/peer origin, status.
|
||||
# Consensus mode, round, ledger sequence, close time.
|
||||
|
||||
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
|
||||
# Trace-to-log correlation — enables one-click navigation
|
||||
# from a Tempo trace to the corresponding Loki log lines. Filters
|
||||
# by trace_id so only logs from the same trace are shown.
|
||||
tracesToLogs:
|
||||
datasourceUid: loki
|
||||
filterByTraceID: true
|
||||
filterBySpanID: false
|
||||
tags: []
|
||||
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 — configurable via
|
||||
# the service_instance_id setting in [telemetry], defaults to the
|
||||
# node's public key. E.g. "Node-1" or "nHB1X37...".
|
||||
- id: node-id
|
||||
tag: service.instance.id
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# service.version: xrpld build version (e.g., "2.4.0-b1").
|
||||
# Filter traces from specific software releases.
|
||||
- id: node-version
|
||||
tag: service.version
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# xrpl.network.id: numeric network identifier
|
||||
# (0 = mainnet, 1 = testnet, 2 = devnet, etc.).
|
||||
# Derived from the [network_id] config section.
|
||||
- id: network-id
|
||||
tag: xrpl.network.id
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# xrpl.network.type: human-readable network name derived from
|
||||
# network ID ("mainnet", "testnet", "devnet", "unknown").
|
||||
- id: network-type
|
||||
tag: xrpl.network.type
|
||||
operator: "="
|
||||
scope: resource
|
||||
type: dynamic
|
||||
# --- Span intrinsic filters ---
|
||||
# name: the span operation name (e.g., "rpc.command.server_info").
|
||||
# Use to find traces for a specific RPC command or subsystem.
|
||||
- id: span-name
|
||||
tag: name
|
||||
operator: "="
|
||||
scope: intrinsic
|
||||
type: dynamic
|
||||
# status: span completion status ("ok", "error", "unset").
|
||||
# Filter for failed operations to diagnose errors.
|
||||
- id: span-status
|
||||
tag: status
|
||||
operator: "="
|
||||
scope: intrinsic
|
||||
type: dynamic
|
||||
# duration: span wall-clock duration. Use with ">" operator
|
||||
# to find slow operations (e.g., duration > 500ms).
|
||||
- id: span-duration
|
||||
tag: duration
|
||||
operator: ">"
|
||||
scope: intrinsic
|
||||
type: dynamic
|
||||
# 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
|
||||
# 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
|
||||
# tx_type: transaction type (e.g., "Payment", "OfferCreate").
|
||||
- id: tx-type
|
||||
tag: tx_type
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
# Consensus tracing filters
|
||||
- id: consensus-mode
|
||||
tag: consensus_mode
|
||||
operator: "="
|
||||
scope: span
|
||||
type: static
|
||||
- id: consensus-round
|
||||
tag: consensus_round
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: consensus-ledger-seq
|
||||
tag: ledger_seq
|
||||
operator: "="
|
||||
scope: span
|
||||
type: static
|
||||
# ledger_hash: ledger hash — scope all spans to a specific closed ledger.
|
||||
- id: ledger-hash
|
||||
tag: ledger_hash
|
||||
operator: "="
|
||||
scope: span
|
||||
type: static
|
||||
- id: consensus-close-time-correct
|
||||
tag: close_time_correct
|
||||
operator: "="
|
||||
scope: span
|
||||
type: static
|
||||
- id: consensus-state
|
||||
tag: consensus_state
|
||||
operator: "="
|
||||
scope: span
|
||||
type: static
|
||||
- id: consensus-close-resolution
|
||||
tag: close_resolution_ms
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: consensus-proposers
|
||||
tag: proposers
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: consensus-result
|
||||
tag: consensus_result
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: consensus-mode-old
|
||||
tag: mode_old
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: consensus-mode-new
|
||||
tag: mode_new
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: consensus-ledger-id
|
||||
tag: consensus_ledger_id
|
||||
operator: "="
|
||||
scope: span
|
||||
type: static
|
||||
# Additional transaction and queue filters
|
||||
- id: tx-path
|
||||
tag: path
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: tx-suppressed
|
||||
tag: suppressed
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: peer-version
|
||||
tag: peer_version
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: txq-status
|
||||
tag: txq_status
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
- id: txq-ter-code
|
||||
tag: ter_code
|
||||
operator: "="
|
||||
scope: span
|
||||
type: dynamic
|
||||
686
docker/telemetry/integration-test.sh
Executable file
686
docker/telemetry/integration-test.sh
Executable file
@@ -0,0 +1,686 @@
|
||||
#!/usr/bin/env bash
|
||||
# Integration test for rippled OpenTelemetry instrumentation.
|
||||
#
|
||||
# Launches a 6-node xrpld consensus network with telemetry enabled,
|
||||
# exercises RPC / transaction / consensus code paths, then verifies
|
||||
# that the expected spans and metrics appear in Tempo and Prometheus.
|
||||
#
|
||||
# Usage:
|
||||
# bash docker/telemetry/integration-test.sh
|
||||
#
|
||||
# Prerequisites:
|
||||
# - .build/xrpld built with telemetry=ON
|
||||
# - docker compose (v2)
|
||||
# - curl, jq
|
||||
#
|
||||
# The script leaves the observability stack and xrpld nodes running
|
||||
# so you can manually inspect Tempo (localhost:3200) and Grafana
|
||||
# (localhost:3000). Run with --cleanup to tear down instead.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
XRPLD="$REPO_ROOT/.build/xrpld"
|
||||
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
|
||||
STANDALONE_CFG="$SCRIPT_DIR/xrpld-telemetry.cfg"
|
||||
WORKDIR="/tmp/xrpld-integration"
|
||||
NUM_NODES=6
|
||||
PEER_PORT_BASE=51235
|
||||
RPC_PORT_BASE=5005
|
||||
CONSENSUS_TIMEOUT=120
|
||||
GENESIS_ACCOUNT="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"
|
||||
GENESIS_SEED="snoPBrXtMeMyMHUVTgbuqAfg1SUTb"
|
||||
DEST_ACCOUNT="" # Generated dynamically via wallet_propose
|
||||
TEMPO="http://localhost:3200"
|
||||
PROM="http://localhost:9090"
|
||||
|
||||
# Counters for pass/fail
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
log() { printf "\033[1;34m[INFO]\033[0m %s\n" "$*"; }
|
||||
ok() {
|
||||
printf "\033[1;32m[PASS]\033[0m %s\n" "$*"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
fail() {
|
||||
printf "\033[1;31m[FAIL]\033[0m %s\n" "$*"
|
||||
FAIL=$((FAIL + 1))
|
||||
}
|
||||
die() {
|
||||
printf "\033[1;31m[ERROR]\033[0m %s\n" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_span() {
|
||||
local op="$1"
|
||||
local count
|
||||
count=$(curl -sf "$TEMPO/api/search" \
|
||||
--data-urlencode "q={resource.service.name=\"xrpld\" && name=\"$op\"}" \
|
||||
--data-urlencode "limit=5" |
|
||||
jq '.traces | length' 2>/dev/null || echo 0)
|
||||
if [ "$count" -gt 0 ]; then
|
||||
ok "$op ($count traces)"
|
||||
else
|
||||
fail "$op (0 traces)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify trace_id injection in xrpld log output.
|
||||
# Greps all node debug.log files for the "trace_id=<hex> span_id=<hex>"
|
||||
# pattern that Logs::format() injects when an active OTel span exists.
|
||||
# Also cross-checks that a trace_id found in logs matches a trace in Tempo.
|
||||
check_log_correlation() {
|
||||
log "Checking log-trace correlation..."
|
||||
|
||||
local total_matches=0
|
||||
local files_scanned=0
|
||||
local sample_trace_id=""
|
||||
|
||||
for i in $(seq 1 "$NUM_NODES"); do
|
||||
local logfile="$WORKDIR/node$i/debug.log"
|
||||
if [ ! -f "$logfile" ]; then
|
||||
continue
|
||||
fi
|
||||
files_scanned=$((files_scanned + 1))
|
||||
local matches
|
||||
matches=$(grep -c 'trace_id=[a-f0-9]\{32\} span_id=[a-f0-9]\{16\}' "$logfile") || matches=0
|
||||
total_matches=$((total_matches + matches))
|
||||
if [ -z "$sample_trace_id" ] && [ "$matches" -gt 0 ]; then
|
||||
sample_trace_id=$(grep -o 'trace_id=[a-f0-9]\{32\}' "$logfile" | head -1 | cut -d= -f2)
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$files_scanned" -eq 0 ]; then
|
||||
fail "Log correlation: no debug.log files found in $WORKDIR/node*/"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$total_matches" -gt 0 ]; then
|
||||
ok "Log correlation: found $total_matches log lines with trace_id ($files_scanned nodes scanned)"
|
||||
else
|
||||
fail "Log correlation: no trace_id found in any node debug.log ($files_scanned nodes scanned)"
|
||||
fi
|
||||
|
||||
# Cross-check: verify the sample trace_id exists in Tempo
|
||||
if [ -n "$sample_trace_id" ]; then
|
||||
local trace_found
|
||||
# Tempo /api/traces/{id} returns OTLP shape: {"batches":[...]}
|
||||
trace_found=$(curl -sf "$TEMPO/api/traces/$sample_trace_id" |
|
||||
jq '.batches | length' 2>/dev/null) || trace_found=0
|
||||
if [ "$trace_found" -gt 0 ]; then
|
||||
ok "Log-Tempo cross-check: trace_id=$sample_trace_id found in Tempo"
|
||||
else
|
||||
fail "Log-Tempo cross-check: trace_id=$sample_trace_id NOT found in Tempo"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
log "Cleaning up..."
|
||||
# Kill xrpld nodes
|
||||
for i in $(seq 1 "$NUM_NODES"); do
|
||||
local pidfile="$WORKDIR/node$i/xrpld.pid"
|
||||
if [ -f "$pidfile" ]; then
|
||||
kill "$(cat "$pidfile")" 2>/dev/null || true
|
||||
rm -f "$pidfile"
|
||||
fi
|
||||
done
|
||||
# Also kill any straggling xrpld processes from our workdir
|
||||
pkill -f "$WORKDIR" 2>/dev/null || true
|
||||
# Stop docker stack
|
||||
docker compose -f "$COMPOSE_FILE" down 2>/dev/null || true
|
||||
# Remove workdir
|
||||
rm -rf "$WORKDIR"
|
||||
log "Cleanup complete."
|
||||
}
|
||||
|
||||
# Handle --cleanup flag
|
||||
if [ "${1:-}" = "--cleanup" ]; then
|
||||
cleanup
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 0: Prerequisites
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Checking prerequisites..."
|
||||
|
||||
command -v docker >/dev/null 2>&1 || die "docker not found"
|
||||
docker compose version >/dev/null 2>&1 || die "docker compose (v2) not found"
|
||||
command -v curl >/dev/null 2>&1 || die "curl not found"
|
||||
command -v jq >/dev/null 2>&1 || die "jq not found"
|
||||
[ -x "$XRPLD" ] || die "xrpld binary not found at $XRPLD (build with telemetry=ON)"
|
||||
[ -f "$COMPOSE_FILE" ] || die "docker-compose.yml not found at $COMPOSE_FILE"
|
||||
[ -f "$STANDALONE_CFG" ] || die "xrpld-telemetry.cfg not found at $STANDALONE_CFG"
|
||||
|
||||
log "All prerequisites met."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1: Clean previous run
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Cleaning previous run data..."
|
||||
for i in $(seq 1 "$NUM_NODES"); do
|
||||
pidfile="$WORKDIR/node$i/xrpld.pid"
|
||||
if [ -f "$pidfile" ]; then
|
||||
kill "$(cat "$pidfile")" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
pkill -f "$WORKDIR" 2>/dev/null || true
|
||||
# Kill any xrpld using the standalone config (from key generation)
|
||||
pkill -f "xrpld-telemetry.cfg" 2>/dev/null || true
|
||||
sleep 2
|
||||
rm -rf "$WORKDIR"
|
||||
mkdir -p "$WORKDIR"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 2: Start observability stack
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Starting observability stack..."
|
||||
# Point the collector's log mount at this test's workdir so it tails the
|
||||
# per-node debug.log files this script generates. The compose default
|
||||
# (./data/logs) is for user-run xrpld; the test owns its own log root.
|
||||
XRPLD_LOG_DIR="$WORKDIR" docker compose -f "$COMPOSE_FILE" up -d
|
||||
|
||||
log "Waiting for otel-collector to be ready..."
|
||||
for attempt in $(seq 1 30); do
|
||||
# The OTLP HTTP endpoint returns 405 for GET (expects POST), which
|
||||
# means it is listening. curl -sf would fail on 405, so we check
|
||||
# the HTTP status code explicitly.
|
||||
status=$(curl -so /dev/null -w '%{http_code}' http://localhost:4318/ 2>/dev/null || echo 000)
|
||||
if [ "$status" != "000" ]; then
|
||||
log "otel-collector ready (attempt $attempt, HTTP $status)."
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 30 ]; then
|
||||
die "otel-collector not ready after 30s"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log "Waiting for Tempo to be ready..."
|
||||
for attempt in $(seq 1 30); do
|
||||
if curl -sf "$TEMPO/ready" >/dev/null 2>&1; then
|
||||
log "Tempo ready (attempt $attempt)."
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 30 ]; then
|
||||
die "Tempo not ready after 30s"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 3: Generate validator keys
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Generating $NUM_NODES validator key pairs..."
|
||||
|
||||
# Start a temporary standalone xrpld for key generation
|
||||
TEMP_DATA="$WORKDIR/temp-keygen"
|
||||
mkdir -p "$TEMP_DATA"
|
||||
|
||||
# Create a minimal temp config for key generation
|
||||
TEMP_CFG="$TEMP_DATA/xrpld.cfg"
|
||||
cat >"$TEMP_CFG" <<EOCFG
|
||||
[server]
|
||||
port_rpc_temp
|
||||
|
||||
[port_rpc_temp]
|
||||
port = 5099
|
||||
ip = 127.0.0.1
|
||||
admin = 127.0.0.1
|
||||
protocol = http
|
||||
|
||||
[node_db]
|
||||
type=NuDB
|
||||
path=$TEMP_DATA/nudb
|
||||
online_delete=256
|
||||
|
||||
[database_path]
|
||||
$TEMP_DATA/db
|
||||
|
||||
[debug_logfile]
|
||||
$TEMP_DATA/debug.log
|
||||
|
||||
[ssl_verify]
|
||||
0
|
||||
EOCFG
|
||||
|
||||
"$XRPLD" --conf "$TEMP_CFG" -a --start >"$TEMP_DATA/stdout.log" 2>&1 &
|
||||
TEMP_PID=$!
|
||||
log "Temporary xrpld started (PID $TEMP_PID), waiting for RPC..."
|
||||
|
||||
for attempt in $(seq 1 30); do
|
||||
if curl -sf http://localhost:5099 -d '{"method":"server_info"}' >/dev/null 2>&1; then
|
||||
log "Temporary xrpld RPC ready (attempt $attempt)."
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 30 ]; then
|
||||
kill "$TEMP_PID" 2>/dev/null || true
|
||||
die "Temporary xrpld RPC not ready after 30s"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
declare -a SEEDS
|
||||
declare -a PUBKEYS
|
||||
|
||||
for i in $(seq 1 "$NUM_NODES"); do
|
||||
result=$(curl -sf http://localhost:5099 -d '{"method":"validation_create"}')
|
||||
seed=$(echo "$result" | jq -r '.result.validation_seed')
|
||||
pubkey=$(echo "$result" | jq -r '.result.validation_public_key')
|
||||
if [ -z "$seed" ] || [ "$seed" = "null" ]; then
|
||||
kill "$TEMP_PID" 2>/dev/null || true
|
||||
die "Failed to generate key pair $i"
|
||||
fi
|
||||
SEEDS+=("$seed")
|
||||
PUBKEYS+=("$pubkey")
|
||||
log " Node $i: $pubkey"
|
||||
done
|
||||
|
||||
kill "$TEMP_PID" 2>/dev/null || true
|
||||
wait "$TEMP_PID" 2>/dev/null || true
|
||||
rm -rf "$TEMP_DATA"
|
||||
log "Key generation complete."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 4: Generate node configs and validators.txt
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Generating node configs..."
|
||||
|
||||
# Create shared validators.txt
|
||||
VALIDATORS_FILE="$WORKDIR/validators.txt"
|
||||
{
|
||||
echo "[validators]"
|
||||
for i in $(seq 0 $((NUM_NODES - 1))); do
|
||||
echo "${PUBKEYS[$i]}"
|
||||
done
|
||||
} >"$VALIDATORS_FILE"
|
||||
|
||||
# Create per-node configs
|
||||
for i in $(seq 1 "$NUM_NODES"); do
|
||||
NODE_DIR="$WORKDIR/node$i"
|
||||
mkdir -p "$NODE_DIR/nudb" "$NODE_DIR/db"
|
||||
|
||||
RPC_PORT=$((RPC_PORT_BASE + i - 1))
|
||||
PEER_PORT=$((PEER_PORT_BASE + i - 1))
|
||||
SEED="${SEEDS[$((i - 1))]}"
|
||||
|
||||
# Build ips_fixed list (all peers except self)
|
||||
IPS_FIXED=""
|
||||
for j in $(seq 1 "$NUM_NODES"); do
|
||||
if [ "$j" -ne "$i" ]; then
|
||||
IPS_FIXED="${IPS_FIXED}127.0.0.1 $((PEER_PORT_BASE + j - 1))
|
||||
"
|
||||
fi
|
||||
done
|
||||
|
||||
cat >"$NODE_DIR/xrpld.cfg" <<EOCFG
|
||||
[server]
|
||||
port_rpc
|
||||
port_peer
|
||||
|
||||
[port_rpc]
|
||||
port = $RPC_PORT
|
||||
ip = 127.0.0.1
|
||||
admin = 127.0.0.1
|
||||
protocol = http
|
||||
|
||||
[port_peer]
|
||||
port = $PEER_PORT
|
||||
ip = 0.0.0.0
|
||||
protocol = peer
|
||||
|
||||
[node_db]
|
||||
type=NuDB
|
||||
path=$NODE_DIR/nudb
|
||||
online_delete=256
|
||||
|
||||
[database_path]
|
||||
$NODE_DIR/db
|
||||
|
||||
[debug_logfile]
|
||||
$NODE_DIR/debug.log
|
||||
|
||||
[validation_seed]
|
||||
$SEED
|
||||
|
||||
[validators_file]
|
||||
$VALIDATORS_FILE
|
||||
|
||||
[ips_fixed]
|
||||
${IPS_FIXED}
|
||||
[peer_private]
|
||||
1
|
||||
|
||||
[telemetry]
|
||||
enabled=1
|
||||
service_instance_id=Node-${i}
|
||||
endpoint=http://localhost:4318/v1/traces
|
||||
exporter=otlp_http
|
||||
batch_size=512
|
||||
batch_delay_ms=2000
|
||||
max_queue_size=2048
|
||||
trace_rpc=1
|
||||
trace_transactions=1
|
||||
trace_consensus=1
|
||||
trace_peer=1
|
||||
trace_ledger=1
|
||||
|
||||
[insight]
|
||||
server=otel
|
||||
endpoint=http://localhost:4318/v1/metrics
|
||||
prefix=rippled
|
||||
|
||||
[rpc_startup]
|
||||
{ "command": "log_level", "severity": "warning" }
|
||||
|
||||
[ssl_verify]
|
||||
0
|
||||
EOCFG
|
||||
|
||||
log " Node $i config: RPC=$RPC_PORT, Peer=$PEER_PORT"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 5: Start all 6 nodes
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Starting $NUM_NODES xrpld nodes..."
|
||||
|
||||
for i in $(seq 1 "$NUM_NODES"); do
|
||||
NODE_DIR="$WORKDIR/node$i"
|
||||
"$XRPLD" --conf "$NODE_DIR/xrpld.cfg" --start >"$NODE_DIR/stdout.log" 2>&1 &
|
||||
echo $! >"$NODE_DIR/xrpld.pid"
|
||||
log " Node $i started (PID $(cat "$NODE_DIR/xrpld.pid"))"
|
||||
done
|
||||
|
||||
# Give nodes a moment to initialize
|
||||
sleep 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 6: Wait for consensus
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Waiting for nodes to reach 'proposing' state (timeout: ${CONSENSUS_TIMEOUT}s)..."
|
||||
|
||||
start_time=$(date +%s)
|
||||
nodes_ready=0
|
||||
|
||||
while [ "$nodes_ready" -lt "$NUM_NODES" ]; do
|
||||
elapsed=$(($(date +%s) - start_time))
|
||||
if [ "$elapsed" -ge "$CONSENSUS_TIMEOUT" ]; then
|
||||
fail "Consensus timeout after ${CONSENSUS_TIMEOUT}s ($nodes_ready/$NUM_NODES nodes ready)"
|
||||
log "Continuing with partial consensus..."
|
||||
break
|
||||
fi
|
||||
|
||||
nodes_ready=0
|
||||
for i in $(seq 1 "$NUM_NODES"); do
|
||||
RPC_PORT=$((RPC_PORT_BASE + i - 1))
|
||||
state=$(curl -sf "http://localhost:$RPC_PORT" \
|
||||
-d '{"method":"server_info"}' 2>/dev/null |
|
||||
jq -r '.result.info.server_state' 2>/dev/null || echo "unreachable")
|
||||
if [ "$state" = "proposing" ]; then
|
||||
nodes_ready=$((nodes_ready + 1))
|
||||
fi
|
||||
done
|
||||
printf "\r %d/%d nodes proposing (%ds elapsed)..." "$nodes_ready" "$NUM_NODES" "$elapsed"
|
||||
if [ "$nodes_ready" -lt "$NUM_NODES" ]; then
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
if [ "$nodes_ready" -eq "$NUM_NODES" ]; then
|
||||
ok "All $NUM_NODES nodes reached 'proposing' state"
|
||||
else
|
||||
fail "Only $nodes_ready/$NUM_NODES nodes reached 'proposing' state"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 6b: Wait for validated ledger
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Waiting for first validated ledger..."
|
||||
for attempt in $(seq 1 60); do
|
||||
val_seq=$(curl -sf "http://localhost:$RPC_PORT_BASE" \
|
||||
-d '{"method":"server_info"}' 2>/dev/null |
|
||||
jq -r '.result.info.validated_ledger.seq // 0' 2>/dev/null || echo 0)
|
||||
if [ "$val_seq" -gt 2 ] 2>/dev/null; then
|
||||
ok "First validated ledger: seq $val_seq"
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 60 ]; then
|
||||
fail "No validated ledger after 60s"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 7: Exercise RPC spans (Phase 2)
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Exercising RPC spans..."
|
||||
|
||||
curl -sf "http://localhost:$RPC_PORT_BASE" \
|
||||
-d '{"method":"server_info"}' >/dev/null
|
||||
curl -sf "http://localhost:$RPC_PORT_BASE" \
|
||||
-d '{"method":"server_state"}' >/dev/null
|
||||
curl -sf "http://localhost:$RPC_PORT_BASE" \
|
||||
-d '{"method":"ledger","params":[{"ledger_index":"current"}]}' >/dev/null
|
||||
|
||||
log "RPC commands sent. Waiting 5s for batch export..."
|
||||
sleep 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 8: Submit transaction (Phase 3)
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Submitting Payment transaction..."
|
||||
|
||||
# Generate a destination wallet
|
||||
log " Generating destination wallet..."
|
||||
wallet_result=$(curl -sf "http://localhost:$RPC_PORT_BASE" \
|
||||
-d '{"method":"wallet_propose"}')
|
||||
DEST_ACCOUNT=$(echo "$wallet_result" | jq -r '.result.account_id' 2>/dev/null)
|
||||
if [ -z "$DEST_ACCOUNT" ] || [ "$DEST_ACCOUNT" = "null" ]; then
|
||||
fail "Could not generate destination wallet"
|
||||
DEST_ACCOUNT="rrrrrrrrrrrrrrrrrrrrrhoLvTp" # ACCOUNT_ZERO fallback
|
||||
fi
|
||||
log " Destination: $DEST_ACCOUNT"
|
||||
|
||||
# Get genesis account info
|
||||
acct_result=$(curl -sf "http://localhost:$RPC_PORT_BASE" \
|
||||
-d "{\"method\":\"account_info\",\"params\":[{\"account\":\"$GENESIS_ACCOUNT\"}]}")
|
||||
seq_num=$(echo "$acct_result" | jq -r '.result.account_data.Sequence' 2>/dev/null || echo "unknown")
|
||||
log " Genesis account sequence: $seq_num"
|
||||
|
||||
# Submit payment
|
||||
submit_result=$(curl -sf "http://localhost:$RPC_PORT_BASE" \
|
||||
-d "{\"method\":\"submit\",\"params\":[{\"secret\":\"$GENESIS_SEED\",\"tx_json\":{\"TransactionType\":\"Payment\",\"Account\":\"$GENESIS_ACCOUNT\",\"Destination\":\"$DEST_ACCOUNT\",\"Amount\":\"10000000\"}}]}")
|
||||
|
||||
engine_result=$(echo "$submit_result" | jq -r '.result.engine_result' 2>/dev/null || echo "unknown")
|
||||
tx_hash=$(echo "$submit_result" | jq -r '.result.tx_json.hash' 2>/dev/null || echo "unknown")
|
||||
|
||||
if [ "$engine_result" = "tesSUCCESS" ] || [ "$engine_result" = "terQUEUED" ]; then
|
||||
ok "Transaction submitted: $engine_result (hash: ${tx_hash:0:16}...)"
|
||||
else
|
||||
fail "Transaction submission: $engine_result"
|
||||
log " Full response: $(echo "$submit_result" | jq -c .result 2>/dev/null)"
|
||||
fi
|
||||
|
||||
log "Waiting 15s for consensus round + batch export..."
|
||||
sleep 15
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 9: Verify Tempo traces
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Verifying spans in Tempo..."
|
||||
|
||||
# Check service registration
|
||||
services=$(curl -sf "$TEMPO/api/v2/search/tag/resource.service.name/values" |
|
||||
jq -r '.tagValues[].value' 2>/dev/null || echo "")
|
||||
if echo "$services" | grep -q "xrpld"; then
|
||||
ok "Service 'xrpld' registered in Tempo"
|
||||
else
|
||||
fail "Service 'xrpld' NOT found in Tempo (found: $services)"
|
||||
fi
|
||||
|
||||
log ""
|
||||
log "--- Phase 2: RPC Spans ---"
|
||||
check_span "rpc.request"
|
||||
check_span "rpc.process"
|
||||
check_span "rpc.command.server_info"
|
||||
check_span "rpc.command.server_state"
|
||||
check_span "rpc.command.ledger"
|
||||
|
||||
log ""
|
||||
log "--- Phase 3: Transaction Spans ---"
|
||||
check_span "tx.process"
|
||||
check_span "tx.receive"
|
||||
check_span "tx.apply"
|
||||
|
||||
log ""
|
||||
log "--- Phase 4: Consensus Spans ---"
|
||||
check_span "consensus.proposal.send"
|
||||
check_span "consensus.ledger_close"
|
||||
check_span "consensus.accept"
|
||||
check_span "consensus.validation.send"
|
||||
|
||||
log ""
|
||||
log "--- Phase 5: Ledger Spans ---"
|
||||
check_span "ledger.build"
|
||||
check_span "ledger.validate"
|
||||
check_span "ledger.store"
|
||||
|
||||
log ""
|
||||
log "--- Phase 5: Peer Spans (trace_peer=1) ---"
|
||||
check_span "peer.proposal.receive"
|
||||
check_span "peer.validation.receive"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 9b: Verify log-trace correlation (Phase 8)
|
||||
# ---------------------------------------------------------------------------
|
||||
log ""
|
||||
log "--- Log-Trace Correlation ---"
|
||||
check_log_correlation
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 10: Verify Prometheus spanmetrics
|
||||
# ---------------------------------------------------------------------------
|
||||
log ""
|
||||
log "--- Phase 5: Spanmetrics ---"
|
||||
log "Waiting 20s for Prometheus scrape cycle..."
|
||||
sleep 20
|
||||
|
||||
calls_count=$(curl -sf "$PROM/api/v1/query?query=traces_span_metrics_calls_total" |
|
||||
jq '.data.result | length' 2>/dev/null || echo 0)
|
||||
if [ "$calls_count" -gt 0 ]; then
|
||||
ok "Prometheus: traces_span_metrics_calls_total ($calls_count series)"
|
||||
else
|
||||
fail "Prometheus: traces_span_metrics_calls_total (0 series)"
|
||||
fi
|
||||
|
||||
duration_count=$(curl -sf "$PROM/api/v1/query?query=traces_span_metrics_duration_milliseconds_count" |
|
||||
jq '.data.result | length' 2>/dev/null || echo 0)
|
||||
if [ "$duration_count" -gt 0 ]; then
|
||||
ok "Prometheus: duration histogram ($duration_count series)"
|
||||
else
|
||||
fail "Prometheus: duration histogram (0 series)"
|
||||
fi
|
||||
|
||||
# Check Grafana
|
||||
if curl -sf http://localhost:3000/api/health >/dev/null 2>&1; then
|
||||
ok "Grafana: healthy at localhost:3000"
|
||||
else
|
||||
fail "Grafana: not reachable at localhost:3000"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 10b: Verify native OTel metrics in Prometheus (beast::insight)
|
||||
# ---------------------------------------------------------------------------
|
||||
log ""
|
||||
log "--- Phase 7: Native OTel Metrics (beast::insight via OTLP) ---"
|
||||
log "Waiting 20s for OTLP metric export + Prometheus scrape..."
|
||||
sleep 20
|
||||
|
||||
check_otel_metric() {
|
||||
local metric_name="$1"
|
||||
local result
|
||||
result=$(curl -sf "$PROM/api/v1/query?query=$metric_name" |
|
||||
jq '.data.result | length' 2>/dev/null || echo 0)
|
||||
if [ "$result" -gt 0 ]; then
|
||||
ok "OTel: $metric_name ($result series)"
|
||||
else
|
||||
fail "OTel: $metric_name (0 series)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Node health gauges (ObservableGauge — no _total suffix)
|
||||
check_otel_metric "rippled_LedgerMaster_Validated_Ledger_Age"
|
||||
check_otel_metric "rippled_LedgerMaster_Published_Ledger_Age"
|
||||
check_otel_metric "rippled_job_count"
|
||||
|
||||
# State accounting
|
||||
check_otel_metric "rippled_State_Accounting_Full_duration"
|
||||
|
||||
# Peer finder
|
||||
check_otel_metric "rippled_Peer_Finder_Active_Inbound_Peers"
|
||||
check_otel_metric "rippled_Peer_Finder_Active_Outbound_Peers"
|
||||
|
||||
# RPC counters (Counter — Prometheus adds _total suffix automatically)
|
||||
check_otel_metric "rippled_rpc_requests_total"
|
||||
|
||||
# Overlay traffic
|
||||
check_otel_metric "rippled_total_Bytes_In"
|
||||
|
||||
# Verify StatsD receiver is NOT required (no statsd receiver in pipeline)
|
||||
log ""
|
||||
log "--- Verify StatsD receiver is not required ---"
|
||||
# StatsD listens on UDP 8125, so probe with a UDP-aware tool, not curl (TCP).
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
if ss -ulnp 2>/dev/null | grep -q ":8125"; then
|
||||
fail "StatsD port 8125 appears to be listening (should not be needed)"
|
||||
else
|
||||
ok "StatsD port 8125 is not listening (not required)"
|
||||
fi
|
||||
else
|
||||
log "ss not found -- skipping StatsD UDP port check"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 11: Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "==========================================================="
|
||||
echo " INTEGRATION TEST RESULTS"
|
||||
echo "==========================================================="
|
||||
printf " \033[1;32mPASSED: %d\033[0m\n" "$PASS"
|
||||
printf " \033[1;31mFAILED: %d\033[0m\n" "$FAIL"
|
||||
echo "==========================================================="
|
||||
echo ""
|
||||
echo " Observability stack is running:"
|
||||
echo ""
|
||||
echo " Tempo: http://localhost:3200"
|
||||
echo " Grafana: http://localhost:3000"
|
||||
echo " Prometheus: http://localhost:9090"
|
||||
echo " Loki: http://localhost:3100"
|
||||
echo ""
|
||||
echo " xrpld nodes (6) are running:"
|
||||
for i in $(seq 1 "$NUM_NODES"); do
|
||||
RPC_PORT=$((RPC_PORT_BASE + i - 1))
|
||||
PEER_PORT=$((PEER_PORT_BASE + i - 1))
|
||||
echo " Node $i: RPC=localhost:$RPC_PORT Peer=:$PEER_PORT PID=$(cat "$WORKDIR/node$i/xrpld.pid" 2>/dev/null || echo 'unknown')"
|
||||
done
|
||||
echo ""
|
||||
echo " To tear down:"
|
||||
echo " bash docker/telemetry/integration-test.sh --cleanup"
|
||||
echo ""
|
||||
echo "==========================================================="
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
193
docker/telemetry/otel-collector-config.yaml
Normal file
193
docker/telemetry/otel-collector-config.yaml
Normal file
@@ -0,0 +1,193 @@
|
||||
# OpenTelemetry Collector configuration for xrpld development.
|
||||
#
|
||||
# Pipelines:
|
||||
# traces: OTLP receiver -> batch processor -> debug + Tempo + spanmetrics
|
||||
# metrics: OTLP receiver + spanmetrics connector -> Prometheus exporter
|
||||
# logs: filelog receiver -> batch processor -> otlphttp/Loki (Phase 8)
|
||||
#
|
||||
# xrpld sends traces via OTLP/HTTP to port 4318. The collector batches
|
||||
# them, forwards to Tempo, and derives RED metrics via the spanmetrics
|
||||
# connector, which Prometheus scrapes on port 8889.
|
||||
#
|
||||
# xrpld sends beast::insight metrics natively via OTLP/HTTP to port 4318
|
||||
# (same endpoint as traces). The OTLP receiver feeds both the traces and
|
||||
# metrics pipelines. Metrics are exported to Prometheus alongside
|
||||
# span-derived metrics.
|
||||
#
|
||||
# The filelog receiver tails xrpld's debug.log files under
|
||||
# /var/log/xrpld/ (mounted from the host). A regex_parser operator
|
||||
# extracts timestamp, partition, severity, and optional trace_id/span_id
|
||||
# fields injected by Logs::format(). Parsed logs are exported to Grafana
|
||||
# Loki for log-trace correlation.
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
# Filelog receiver tails xrpld debug.log files for log-trace
|
||||
# correlation. Extracts structured fields (timestamp, partition, severity,
|
||||
# trace_id, span_id, message) via regex. The trace_id and span_id are
|
||||
# optional — only present when the log was emitted within an active span.
|
||||
filelog:
|
||||
include: [/var/log/xrpld/*/debug.log]
|
||||
operators:
|
||||
# Log format emitted by Logs::format() is:
|
||||
# YYYY-Mmm-DD HH:MM:SS.ffffff UTC <partition>:<severity> [trace_id=... span_id=...] <message>
|
||||
# The `partition:` prefix is omitted when partition is empty, so the
|
||||
# capture group is non-capturing optional. Fractional seconds up to 6
|
||||
# digits are parsed via the `%f` strptime directive.
|
||||
- type: regex_parser
|
||||
regex: '^(?P<timestamp>\S+\s+\S+)\s+\S+\s+(?:(?P<partition>\S+):)?(?P<severity>\S+)\s+(?:trace_id=(?P<trace_id>[a-f0-9]+)\s+span_id=(?P<span_id>[a-f0-9]+)\s+)?(?P<message>.*)$'
|
||||
timestamp:
|
||||
parse_from: attributes.timestamp
|
||||
layout: "%Y-%b-%d %H:%M:%S.%f"
|
||||
location: UTC
|
||||
|
||||
processors:
|
||||
batch:
|
||||
timeout: 1s
|
||||
send_batch_size: 100
|
||||
resource/logs:
|
||||
attributes:
|
||||
- key: service.name
|
||||
value: xrpld
|
||||
action: upsert
|
||||
# Loki 3.x OTLP ingestion converts `service.name` to the label
|
||||
# `service_name`. The runbook and integration-test queries use the
|
||||
# canonical Loki label `job` so operators can paste `{job="xrpld"}`
|
||||
# without guessing the otel-to-loki naming convention. Upsert the
|
||||
# `job` resource attribute here so it round-trips through OTLP
|
||||
# into Loki as the `job` label.
|
||||
- key: job
|
||||
value: xrpld
|
||||
action: upsert
|
||||
# Deployment-tier tagging. Each collector serves ONE environment and ONE
|
||||
# network, so it stamps both onto every signal it forwards. This lets a
|
||||
# single Grafana stack hold data from many collectors and filter by tier.
|
||||
# - deployment.environment: the collector IS the environment (local, ci,
|
||||
# test, prod), so it is authoritative -> upsert (overwrite).
|
||||
# - xrpl.network.type: the xrpld node knows its own chain and already
|
||||
# stamps this, so the collector only fills it when absent -> insert.
|
||||
# This keeps a node's real network (e.g. a local node on mainnet)
|
||||
# from being overwritten by a collector's default.
|
||||
# Replace the placeholder values per collector; see docker/telemetry
|
||||
# tier examples.
|
||||
resource/tier:
|
||||
attributes:
|
||||
- key: deployment.environment
|
||||
value: local
|
||||
action: upsert
|
||||
- key: xrpl.network.type
|
||||
value: mainnet
|
||||
action: insert
|
||||
# Strip SDK-injected resource attributes (telemetry.sdk.language/name/version).
|
||||
# The OpenTelemetry SDK auto-adds these to every Resource; they carry no
|
||||
# operational value and clutter the attribute set on every backend, so drop
|
||||
# them here for all signals.
|
||||
resource/stripsdk:
|
||||
attributes:
|
||||
- key: telemetry.sdk.language
|
||||
action: delete
|
||||
- key: telemetry.sdk.name
|
||||
action: delete
|
||||
- key: telemetry.sdk.version
|
||||
action: delete
|
||||
# Defense-in-depth: hash path-finding account attributes. The xrpld SDK
|
||||
# already hashes these before export, but a node that emitted raw values
|
||||
# is caught here so raw addresses never reach the backend.
|
||||
attributes/hash:
|
||||
actions:
|
||||
- key: pathfind_source_account
|
||||
action: hash
|
||||
- key: pathfind_dest_account
|
||||
action: hash
|
||||
|
||||
connectors:
|
||||
spanmetrics:
|
||||
namespace: "span"
|
||||
# Resource attributes that define the resource-metrics grouping key.
|
||||
# All resource attributes are propagated onto the output metrics
|
||||
# regardless; promotion to Prometheus labels is handled by the
|
||||
# prometheus exporter's resource_to_telemetry_conversion. Listing the
|
||||
# per-node id and the deployment-tier keys keeps series from distinct
|
||||
# nodes/tiers grouped separately.
|
||||
resource_metrics_key_attributes:
|
||||
- service.instance.id
|
||||
- deployment.environment
|
||||
- xrpl.network.type
|
||||
histogram:
|
||||
explicit:
|
||||
buckets: [1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 5s]
|
||||
dimensions:
|
||||
- name: command
|
||||
- name: rpc_status
|
||||
- name: consensus_mode
|
||||
- name: close_time_correct
|
||||
- name: local
|
||||
- name: suppressed
|
||||
- name: proposal_trusted
|
||||
- name: validation_trusted
|
||||
- name: tx_type
|
||||
- name: ter_result
|
||||
# Apply-pipeline stage (preflight|preclaim|apply) — splits the
|
||||
# tx.preflight/tx.preclaim/tx.transactor span RED metrics per stage.
|
||||
- name: stage
|
||||
- name: txq_status
|
||||
- name: consensus_state
|
||||
- name: load_type
|
||||
- name: is_batch
|
||||
# Consensus lifecycle dimensions (low cardinality, bounded value sets).
|
||||
- name: mode_new
|
||||
- name: consensus_stalled
|
||||
- name: consensus_phase
|
||||
- name: consensus_result
|
||||
# gRPC surface dimensions (bounded: method names, role, status).
|
||||
- name: method
|
||||
- name: grpc_role
|
||||
- name: grpc_status
|
||||
|
||||
exporters:
|
||||
debug:
|
||||
verbosity: detailed
|
||||
otlp/tempo:
|
||||
endpoint: tempo:4317
|
||||
tls:
|
||||
insecure: true
|
||||
# Export logs to Grafana Loki via OTLP/HTTP. Loki 3.x supports
|
||||
# native OTLP ingestion on its /otlp endpoint, replacing the removed
|
||||
# loki exporter (dropped in otel-collector-contrib v0.147.0).
|
||||
otlphttp/loki:
|
||||
endpoint: http://loki:3100/otlp
|
||||
prometheus:
|
||||
endpoint: 0.0.0.0:8889
|
||||
# Promote resource attributes (deployment.environment, xrpl.network.type,
|
||||
# service.name, service.instance.id) to Prometheus labels. Without this
|
||||
# the tier attributes stamped by resource/tier stay on the resource and
|
||||
# never reach the metric series, so dashboards could not filter by them.
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
|
||||
service:
|
||||
extensions: [health_check]
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [resource/tier, resource/stripsdk, attributes/hash, batch]
|
||||
exporters: [debug, otlp/tempo, spanmetrics]
|
||||
metrics:
|
||||
receivers: [otlp, spanmetrics]
|
||||
processors: [resource/tier, resource/stripsdk, batch]
|
||||
exporters: [prometheus]
|
||||
# Log pipeline ingests xrpld debug.log via filelog receiver,
|
||||
# batches entries, and exports to Loki for log-trace correlation.
|
||||
logs:
|
||||
receivers: [filelog]
|
||||
processors: [resource/logs, resource/tier, resource/stripsdk, batch]
|
||||
exporters: [otlphttp/loki]
|
||||
9
docker/telemetry/prometheus.yml
Normal file
9
docker/telemetry/prometheus.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
# Prometheus configuration for scraping spanmetrics from OTel Collector.
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: otel-collector
|
||||
static_configs:
|
||||
- targets: ["otel-collector:8889"]
|
||||
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
|
||||
70
docker/telemetry/xrpld-telemetry.cfg
Normal file
70
docker/telemetry/xrpld-telemetry.cfg
Normal file
@@ -0,0 +1,70 @@
|
||||
# Standalone xrpld configuration with OpenTelemetry enabled.
|
||||
#
|
||||
# Usage:
|
||||
# 1. Start the observability stack:
|
||||
# docker compose -f docker/telemetry/docker-compose.yml up -d
|
||||
# 2. Run xrpld in standalone mode:
|
||||
# ./xrpld --conf docker/telemetry/xrpld-telemetry.cfg -a --start
|
||||
# 3. Send RPC commands to exercise tracing:
|
||||
# curl -s http://localhost:5005 -d '{"method":"server_info"}'
|
||||
# 4. View traces in Grafana Explore: http://localhost:3000/explore
|
||||
|
||||
[server]
|
||||
port_rpc_admin_local
|
||||
port_ws_admin_local
|
||||
|
||||
[port_rpc_admin_local]
|
||||
port = 5005
|
||||
ip = 127.0.0.1
|
||||
admin = 127.0.0.1
|
||||
protocol = http
|
||||
|
||||
[port_ws_admin_local]
|
||||
port = 6006
|
||||
ip = 127.0.0.1
|
||||
admin = 127.0.0.1
|
||||
protocol = ws
|
||||
|
||||
[node_db]
|
||||
type=NuDB
|
||||
path=docker/telemetry/data/nudb
|
||||
online_delete=256
|
||||
advisory_delete=0
|
||||
|
||||
[database_path]
|
||||
docker/telemetry/data
|
||||
|
||||
# Path is resolved relative to this config file's directory (docker/telemetry),
|
||||
# so this writes to docker/telemetry/data/logs/devnet/debug.log — the same
|
||||
# dir the compose stack bind-mounts into the collector as /var/log/xrpld.
|
||||
[debug_logfile]
|
||||
data/logs/devnet/debug.log
|
||||
|
||||
[rpc_startup]
|
||||
{ "command": "log_level", "severity": "debug" }
|
||||
|
||||
[ssl_verify]
|
||||
0
|
||||
|
||||
# --- OpenTelemetry tracing ---
|
||||
[telemetry]
|
||||
enabled=1
|
||||
service_instance_id=xrpld-standalone
|
||||
endpoint=http://localhost:4318/v1/traces
|
||||
exporter=otlp_http
|
||||
batch_size=512
|
||||
batch_delay_ms=5000
|
||||
max_queue_size=2048
|
||||
trace_rpc=1
|
||||
trace_transactions=1
|
||||
trace_consensus=1
|
||||
trace_peer=1
|
||||
trace_ledger=1
|
||||
|
||||
# --- StatsD metrics (beast::insight) ---
|
||||
# Sends UDP metrics to the collector's StatsD receiver so the StatsD
|
||||
# dashboards have data. The collector binds this port on the host loopback.
|
||||
[insight]
|
||||
server=statsd
|
||||
address=127.0.0.1:8125
|
||||
prefix=xrpld
|
||||
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.
|
||||
881
docs/telemetry-runbook.md
Normal file
881
docs/telemetry-runbook.md
Normal file
@@ -0,0 +1,881 @@
|
||||
# xrpld Telemetry Operator Runbook
|
||||
|
||||
## Overview
|
||||
|
||||
xrpld supports OpenTelemetry distributed tracing to provide visibility into RPC requests, transaction processing, and consensus rounds.
|
||||
|
||||
This runbook covers operating a running node and querying its traces. For
|
||||
building xrpld with telemetry support and the internal architecture, see
|
||||
[build/telemetry.md](build/telemetry.md).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start the observability stack
|
||||
|
||||
```bash
|
||||
docker compose -f docker/telemetry/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
This starts:
|
||||
|
||||
- **OTel Collector** on ports 4317 (gRPC) and 4318 (HTTP), and 13133 (health)
|
||||
- **Tempo** on http://localhost:3200 (trace backend)
|
||||
- **Prometheus** on http://localhost:9090
|
||||
- **Loki** on http://localhost:3100 (log aggregation)
|
||||
- **Grafana** on http://localhost:3000 (Tempo pre-configured as datasource)
|
||||
|
||||
### 2. Enable telemetry in xrpld
|
||||
|
||||
Add to your `xrpld.cfg`:
|
||||
|
||||
```ini
|
||||
[telemetry]
|
||||
enabled=1
|
||||
endpoint=http://localhost:4318/v1/traces
|
||||
```
|
||||
|
||||
### 3. Build with telemetry support
|
||||
|
||||
```bash
|
||||
conan install . --build=missing -o telemetry=True
|
||||
cmake --preset default -Dtelemetry=ON
|
||||
cmake --build --preset default
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------------------- | --------------------------------- | --------------------------------------------------------- |
|
||||
| `enabled` | `0` | Master switch for telemetry |
|
||||
| `endpoint` | `http://localhost:4318/v1/traces` | OTLP/HTTP endpoint |
|
||||
| `service_name` | `xrpld` | OpenTelemetry service name resource attribute |
|
||||
| `service_instance_id` | node public key | OpenTelemetry service instance ID resource attribute |
|
||||
| `trace_rpc` | `1` | Enable RPC request tracing |
|
||||
| `trace_transactions` | `1` | Enable transaction tracing |
|
||||
| `trace_consensus` | `1` | Enable consensus tracing |
|
||||
| `trace_peer` | `1` | Enable peer message tracing (high volume) |
|
||||
| `trace_ledger` | `1` | Enable ledger tracing |
|
||||
| `consensus_trace_strategy` | `deterministic` | Consensus trace ID strategy (`deterministic` or `random`) |
|
||||
| `batch_size` | `512` | Max spans per batch export |
|
||||
| `batch_delay_ms` | `5000` | Delay between batch exports |
|
||||
| `max_queue_size` | `2048` | Max spans queued before dropping |
|
||||
| `use_tls` | `0` | Use TLS for exporter connection |
|
||||
| `tls_ca_cert` | (empty) | Path to CA certificate bundle |
|
||||
| `tls_client_cert` | (empty) | Client cert (PEM) for mutual TLS; empty = one-way TLS |
|
||||
| `tls_client_key` | (empty) | Private key (PEM) for `tls_client_cert` |
|
||||
|
||||
## Span Reference
|
||||
|
||||
All spans instrumented in xrpld, grouped by subsystem:
|
||||
|
||||
### RPC Spans
|
||||
|
||||
| Span Name | Source File | Attributes | Description |
|
||||
| -------------------- | ----------------- | ----------------------------------------------------------- | ----------------------------------------------------- |
|
||||
| `rpc.http_request` | ServerHandler.cpp | `request_payload_size` | Top-level HTTP RPC request |
|
||||
| `rpc.ws_upgrade` | ServerHandler.cpp | — | WebSocket upgrade handshake |
|
||||
| `rpc.ws_message` | ServerHandler.cpp | `command` | WebSocket RPC message |
|
||||
| `rpc.process` | ServerHandler.cpp | `is_batch`, `batch_size` | RPC processing (child of rpc.http_request/ws_message) |
|
||||
| `rpc.command.<name>` | RPCHandler.cpp | `command`, `version`, `rpc_role`, `rpc_status`, `load_type` | Per-command span (e.g., `rpc.command.server_info`) |
|
||||
|
||||
### Transaction Spans
|
||||
|
||||
| Span Name | Source File | Attributes | Description |
|
||||
| --------------- | --------------- | --------------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| `tx.process` | NetworkOPs.cpp | `tx_hash`, `local`, `path`, `tx_type`, `fee`, `sequence`, `ter_result`, `applied` | Transaction submission and processing |
|
||||
| `tx.receive` | PeerImp.cpp | `peer_id`, `tx_hash`, `tx_type`, `peer_version`, `suppressed`, `tx_status` | Transaction received from peer relay |
|
||||
| `tx.apply` | BuildLedger.cpp | `ledger_seq`, `tx_count`, `tx_failed` | Transaction set applied per ledger |
|
||||
| `tx.preflight` | applySteps.cpp | `stage`, `tx_type`, `ter_result` | Stateless checks stage |
|
||||
| `tx.preclaim` | applySteps.cpp | `stage`, `tx_type`, `ter_result` | Ledger-aware checks stage |
|
||||
| `tx.transactor` | Transactor.cpp | `stage`, `tx_type`, `ter_result`, `applied` | Apply stage (transactor runs) |
|
||||
|
||||
The three apply-pipeline spans (`tx.preflight`, `tx.preclaim`, `tx.transactor`)
|
||||
share a deterministic `trace_id` from `txID[0:16]`, so they group under one
|
||||
trace per transaction. The `stage` attribute (`preflight` / `preclaim` /
|
||||
`apply`) drives the collector spanmetrics `stage` dimension, giving per-stage
|
||||
RED metrics on the _Transaction Overview_ dashboard.
|
||||
|
||||
### Transaction Queue Spans
|
||||
|
||||
| Span Name | Source File | Attributes | Description |
|
||||
| ------------------ | ----------- | -------------------------------------------------------- | -------------------------------------------------- |
|
||||
| `txq.enqueue` | TxQ.cpp | `tx_hash`, `tx_type` | Transaction enqueue decision (child of tx.process) |
|
||||
| `txq.apply_direct` | TxQ.cpp | -- | Direct apply attempt (bypassing queue) |
|
||||
| `txq.batch_clear` | TxQ.cpp | -- | Batch clear of queued transactions for an account |
|
||||
| `txq.accept` | TxQ.cpp | `queue_size`, `ledger_changed` | Ledger-close accept loop over queued transactions |
|
||||
| `txq.accept_tx` | TxQ.cpp | `tx_hash`, `retries_remaining`, `ter_code`, `txq_status` | Per-transaction apply during accept |
|
||||
| `txq.cleanup` | TxQ.cpp | `ledger_seq` | Post-close cleanup of expired queue entries |
|
||||
|
||||
### PathFinding Spans
|
||||
|
||||
| Span Name | Source File | Attributes | Description |
|
||||
| --------------------- | --------------------------------- | -------------------------------------------------- | ------------------------------------------------------- |
|
||||
| `pathfind.request` | PathFind.cpp / RipplePathFind.cpp | `pathfind_source_account`, `pathfind_dest_account` | Path-find RPC entry (accounts hashed; set when present) |
|
||||
| `pathfind.compute` | PathRequest.cpp | `pathfind_fast`, `pathfind_dest_currency` | Path computation for one request (`doUpdate`) |
|
||||
| `pathfind.discover` | PathRequest.cpp | `pathfind_search_level`, `pathfind_num_paths` | Graph exploration (one per RPC call in `findPaths`) |
|
||||
| `pathfind.update_all` | PathRequestManager.cpp | `pathfind_ledger_index`, `pathfind_num_requests` | Async recomputation of active requests on ledger close |
|
||||
|
||||
### Consensus Spans
|
||||
|
||||
| Span Name | Source File | Attributes | Description |
|
||||
| ------------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `consensus.round` | RCLConsensus.cpp | `consensus_ledger_id`, `ledger_seq`, `consensus_mode`, `trace_strategy`, `consensus_round_id` | Root span for a consensus round (deterministic or random trace ID) |
|
||||
| `consensus.phase.open` | Consensus.h | -- | Open phase duration (child of round) |
|
||||
| `consensus.proposal.send` | RCLConsensus.cpp | `consensus_round`, `is_bow_out` | Consensus proposal broadcast |
|
||||
| `consensus.ledger_close` | RCLConsensus.cpp | `ledger_seq`, `consensus_mode` | Ledger close event |
|
||||
| `consensus.establish` | Consensus.h | `converge_percent`, `establish_count`, `proposers` | Establish phase duration (child of round) |
|
||||
| `consensus.update_positions` | Consensus.h | `converge_percent`, `proposers`, `disputes_count` | Position update and dispute resolution (see Events below) |
|
||||
| `consensus.check` | Consensus.h | `agree_count`, `disagree_count`, `converge_percent`, `have_close_time_consensus`, `threshold_percent`, `proposers_finished`, `consensus_stalled`, `establish_count`, `consensus_result` | Consensus threshold check |
|
||||
| `consensus.accept` | RCLConsensus.cpp | `proposers`, `round_time_ms`, `quorum`, `disputes_count`, `consensus_state` | Ledger accepted by consensus |
|
||||
| `consensus.accept.apply` | RCLConsensus.cpp | `ledger_seq`, `close_time`, `close_time_correct`, `close_resolution_ms`, `consensus_state`, `proposing`, `round_time_ms`, `parent_close_time`, `close_time_self`, `close_time_vote_bins`, `resolution_direction`, `tx_count` | Ledger application with close time details (see Events below) |
|
||||
| `consensus.validation.send` | RCLConsensus.cpp | `ledger_seq`, `proposing`, `ledger_hash`, `full_validation`, `validation_sign_time` | Validation sent after accept (follows-from link) |
|
||||
| `consensus.mode_change` | RCLConsensus.cpp | `mode_old`, `mode_new` | Consensus mode transition |
|
||||
| `consensus.proposal.receive` | PeerImp.cpp | `proposal_trusted`, `consensus_round` | Proposal received from peer (extracts parent context from TraceContext when present; falls back to standalone span for older peers) |
|
||||
| `consensus.validation.receive` | PeerImp.cpp | `validation_trusted`, `ledger_seq` | Validation received from peer (extracts parent context from TraceContext when present; falls back to standalone span for older peers) |
|
||||
|
||||
#### Consensus Span Events
|
||||
|
||||
| Parent Span | Event Name | Event Attributes | Description |
|
||||
| ---------------------------- | ----------------- | ----------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| `consensus.update_positions` | `dispute.resolve` | `tx_id`, `dispute_our_vote`, `dispute_yays`, `dispute_nays` | Emitted per dispute when votes are tallied |
|
||||
| `consensus.accept.apply` | `tx.included` | `tx_id` | Emitted per transaction included in the accepted ledger |
|
||||
|
||||
#### Close Time Queries (Tempo TraceQL)
|
||||
|
||||
```
|
||||
# Find rounds where validators disagreed on close time
|
||||
{name="consensus.accept.apply"} | close_time_correct = false
|
||||
|
||||
# Find consensus failures (moved_on)
|
||||
{name="consensus.accept.apply"} | consensus_state = "moved_on"
|
||||
|
||||
# Find slow ledger applications (>5s)
|
||||
{name="consensus.accept.apply"} | duration > 5s
|
||||
|
||||
# Find specific ledger's consensus details
|
||||
{name="consensus.accept.apply"} | ledger_seq = 92345678
|
||||
|
||||
# Find all spans in a consensus round (deterministic trace strategy)
|
||||
{name="consensus.round"} | consensus_round_id = <round_id>
|
||||
|
||||
# Find dispute resolutions
|
||||
{name="consensus.update_positions"} >> {event:name="dispute.resolve"}
|
||||
```
|
||||
|
||||
### Ledger Spans
|
||||
|
||||
| Span Name | Source File | Attributes | Description |
|
||||
| ----------------- | -------------------- | ------------------------------------- | ----------------------------- |
|
||||
| `ledger.build` | BuildLedger.cpp:31 | `ledger_seq`, `tx_count`, `tx_failed` | Ledger build during consensus |
|
||||
| `ledger.validate` | LedgerMaster.cpp:915 | `ledger_seq`, `validations` | Ledger promoted to validated |
|
||||
| `ledger.store` | LedgerMaster.cpp:409 | `ledger_seq` | Ledger stored in history |
|
||||
|
||||
### Peer Spans
|
||||
|
||||
| Span Name | Source File | Attributes | Description |
|
||||
| ------------------------- | ---------------- | ------------------------------- | ----------------------------- |
|
||||
| `peer.proposal.receive` | PeerImp.cpp:1667 | `peer_id`, `proposal_trusted` | Proposal received from peer |
|
||||
| `peer.validation.receive` | PeerImp.cpp:2264 | `peer_id`, `validation_trusted` | Validation received from peer |
|
||||
|
||||
---
|
||||
|
||||
## Insights and Sample Queries
|
||||
|
||||
This section shows what questions you can answer using the span attributes, with example Tempo TraceQL queries.
|
||||
|
||||
### Transaction Workflow Analysis
|
||||
|
||||
```
|
||||
# Find all AMM transactions (AMMDeposit, AMMWithdraw, AMMCreate, etc.)
|
||||
{name="tx.process"} | tx_type =~ "AMM.*"
|
||||
|
||||
# Find Payment transactions that failed
|
||||
{name="tx.process"} | tx_type = "Payment" && ter_result != "tesSUCCESS"
|
||||
|
||||
# Compare latency of different transaction types
|
||||
{name="tx.process"} | tx_type = "OfferCreate"
|
||||
{name="tx.process"} | tx_type = "Payment"
|
||||
|
||||
# Find high-fee transactions (fee > 1 XRP = 1000000 drops)
|
||||
{name="tx.process"} | fee > 1000000
|
||||
|
||||
# Find transactions that were not applied
|
||||
{name="tx.process"} | applied = false
|
||||
|
||||
# Trace a specific transaction by type across the network
|
||||
{name=~"tx\\..*"} | tx_type = "NFTokenMint"
|
||||
```
|
||||
|
||||
### Apply Pipeline by Stage
|
||||
|
||||
```
|
||||
# All three stages of one transaction (preflight -> preclaim -> apply)
|
||||
{name=~"tx.preflight|tx.preclaim|tx.transactor"}
|
||||
|
||||
# Transactions that failed at the preclaim stage
|
||||
{name="tx.preclaim"} | ter_result != "tesSUCCESS"
|
||||
|
||||
# Transactions that hard-failed preflight (never reached preclaim/apply)
|
||||
{name="tx.preflight"} | ter_result != "tesSUCCESS"
|
||||
```
|
||||
|
||||
PromQL on the span-derived metrics (dashboard: _Transaction Overview_):
|
||||
|
||||
```
|
||||
# Per-stage throughput — the funnel preflight >= preclaim >= apply
|
||||
sum by (stage) (rate(span_calls_total{span_name=~"tx.preflight|tx.preclaim|tx.transactor"}[5m]))
|
||||
|
||||
# Per-stage p95 latency
|
||||
histogram_quantile(0.95, sum by (le, stage) (rate(span_duration_milliseconds_bucket{span_name=~"tx.preflight|tx.preclaim|tx.transactor"}[5m])))
|
||||
|
||||
# Per-stage failure rate (ter_result != tesSUCCESS; a failing ter completes the
|
||||
# span normally, so filter on the attribute, not status_code which only flags exceptions)
|
||||
sum by (stage) (rate(span_calls_total{span_name=~"tx.preflight|tx.preclaim|tx.transactor", ter_result!~"tesSUCCESS|"}[5m]))
|
||||
```
|
||||
|
||||
> **Alerting**: a rising `tx.preflight` / `tx.preclaim` failure rate points to
|
||||
> malformed or stale-sequence submissions (often spam or a misbehaving client);
|
||||
> a rising `tx.transactor` failure rate points to apply-time problems. Alert per
|
||||
> stage rather than on a single aggregate so the failing stage is obvious.
|
||||
|
||||
> **Sampling caveat**: these stage metrics are span-derived and inherit the
|
||||
> **tracer head-sampling** ratio (`sampling_ratio`). At `sampling_ratio < 1.0`
|
||||
> they undercount proportionally — treat them as relative trends, not absolute
|
||||
> transaction counts. Native StatsD metrics are unsampled.
|
||||
|
||||
### Transaction Queue Health
|
||||
|
||||
```
|
||||
# Find transactions rejected from the queue
|
||||
{name="txq.accept_tx"} | txq_status = "failed"
|
||||
|
||||
# Which transaction types get queued most often?
|
||||
{name="txq.enqueue"} | tx_type = "Payment"
|
||||
{name="txq.enqueue"} | tx_type = "OfferCreate"
|
||||
|
||||
# Find ledger closes that applied queued transactions
|
||||
{name="txq.accept"} | ledger_changed = true
|
||||
|
||||
# Find transactions that exhausted retries
|
||||
{name="txq.accept_tx"} | txq_status = "retried" && retries_remaining = 0
|
||||
```
|
||||
|
||||
### RPC Debugging
|
||||
|
||||
```
|
||||
# Find batch RPC requests
|
||||
{name="rpc.process"} | is_batch = true
|
||||
|
||||
# Find large RPC payloads (>100KB)
|
||||
{name="rpc.http_request"} | request_payload_size > 100000
|
||||
|
||||
# Find resource-heavy RPC commands (by load_type)
|
||||
{name=~"rpc.command.*"} | load_type = "exceptioned RPC"
|
||||
|
||||
# Find a specific WebSocket command
|
||||
{name="rpc.ws_message"} | command = "subscribe"
|
||||
|
||||
# Find slow pathfinding with many source assets
|
||||
{name="pathfind.discover"} | pathfind_num_source_assets > 10
|
||||
```
|
||||
|
||||
### PathFinding Performance
|
||||
|
||||
```
|
||||
# Find pathfinding for specific currencies
|
||||
{name="pathfind.compute"} | pathfind_dest_currency = "USD"
|
||||
|
||||
# Find expensive pathfinding (many source assets to explore)
|
||||
{name="pathfind.discover"} | pathfind_num_source_assets > 20
|
||||
|
||||
# Find large pathfinding requests
|
||||
{name="pathfind.compute"} | duration > 1s
|
||||
```
|
||||
|
||||
### Consensus Health
|
||||
|
||||
```
|
||||
# Find rounds where consensus timed out (expired)
|
||||
{name="consensus.accept"} | consensus_state = "expired"
|
||||
|
||||
# Find rounds where we moved on without full agreement
|
||||
{name="consensus.accept"} | consensus_state = "moved_on"
|
||||
|
||||
# Find rounds with many disputes
|
||||
{name="consensus.accept"} | disputes_count > 5
|
||||
|
||||
# Find bow-out proposals (node resigned from round)
|
||||
{name="consensus.proposal.send"} | is_bow_out = true
|
||||
|
||||
# Correlate validation with its ledger
|
||||
{name="consensus.validation.send"} | ledger_hash = "<hash>"
|
||||
|
||||
# Find rounds where validators disagreed on close time
|
||||
{name="consensus.accept.apply"} | close_time_correct = false
|
||||
```
|
||||
|
||||
### Cross-Subsystem Correlation
|
||||
|
||||
```
|
||||
# Follow a transaction from receive through queue to ledger
|
||||
{name=~"tx\\..*|txq\\..*"} | tx_type = "Payment" && duration > 500ms
|
||||
|
||||
# Find all NFT-related activity
|
||||
{name=~"tx\\..*|txq\\..*"} | tx_type =~ "NFToken.*"
|
||||
|
||||
# Find consensus rounds with slow transactions
|
||||
{name="consensus.accept"} | round_time_ms > 5000
|
||||
```
|
||||
|
||||
### Where to Look (Quick Reference)
|
||||
|
||||
| Question | Span | Key Attributes |
|
||||
| ----------------------------------- | --------------------------- | ------------------------------ |
|
||||
| "Which tx type is slowest?" | `tx.process` | `tx_type` + duration |
|
||||
| "Why was my tx rejected?" | `tx.process` | `ter_result`, `applied` |
|
||||
| "Is the TxQ backing up?" | `txq.accept` | `queue_size`, `ledger_changed` |
|
||||
| "Why was my tx dropped from queue?" | `txq.accept_tx` | `txq_status`, `ter_code` |
|
||||
| "Are batch requests a problem?" | `rpc.process` | `is_batch`, `batch_size` |
|
||||
| "Which RPC is expensive?" | `rpc.command.*` | `load_type`, duration |
|
||||
| "Did consensus stall?" | `consensus.check` | `consensus_stalled` |
|
||||
| "Was consensus outcome normal?" | `consensus.accept` | `consensus_state` |
|
||||
| "Did a validator bow out?" | `consensus.proposal.send` | `is_bow_out` |
|
||||
| "Which ledger was validated?" | `consensus.validation.send` | `ledger_hash` |
|
||||
|
||||
---
|
||||
|
||||
## Cross-Node Trace Propagation
|
||||
|
||||
xrpld propagates trace context across nodes via protobuf `TraceContext` fields
|
||||
embedded in peer-to-peer messages. When Node A sends a transaction, proposal,
|
||||
or validation, it injects its active span's trace/span IDs into the protobuf
|
||||
message. Node B extracts that context on receipt and creates a child span,
|
||||
linking the two nodes into a single distributed trace.
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
Node A (sender) Node B (receiver)
|
||||
+-----------------------------+ +-------------------------------+
|
||||
| tx.process / consensus.* | | PeerImp::onMessage() |
|
||||
| | | | | |
|
||||
| v | | v |
|
||||
| SpanGuard::getTraceBytes() | | extract TraceContext from |
|
||||
| | | | protobuf message |
|
||||
| v | send | | |
|
||||
| injectSpanContext() --------|--------->| v |
|
||||
| sets TraceContext fields | proto | txReceiveSpan() |
|
||||
| (trace_id, span_id, flags) | msg | proposalReceiveSpan() |
|
||||
+-----------------------------+ | validationReceiveSpan() |
|
||||
| | |
|
||||
| v |
|
||||
| child span with parent link |
|
||||
+-------------------------------+
|
||||
```
|
||||
|
||||
### Send-Side Injection
|
||||
|
||||
| Message Type | Injection Point | Mechanism |
|
||||
| ------------- | -------------------------- | ------------------------------------------ |
|
||||
| TMTransaction | `NetworkOPs::apply()` | Injects `tx.process` span into relay msg |
|
||||
| TMProposeSet | `RCLConsensus::propose()` | Injects active context into proposal msg |
|
||||
| TMValidation | `RCLConsensus::validate()` | Injects active context into validation msg |
|
||||
|
||||
### Receive-Side Extraction
|
||||
|
||||
| Message Type | Extraction Point | Helper Function |
|
||||
| ------------- | ----------------------------------- | -------------------------------------------------- |
|
||||
| TMTransaction | `PeerImp::onMessage(TMTransaction)` | `TxTracing::txReceiveSpan()` |
|
||||
| TMProposeSet | `PeerImp::onMessage(TMProposeSet)` | `ConsensusReceiveTracing::proposalReceiveSpan()` |
|
||||
| TMValidation | `PeerImp::onMessage(TMValidation)` | `ConsensusReceiveTracing::validationReceiveSpan()` |
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Role |
|
||||
| ------------------------------------------------- | ----------------------------------------------- |
|
||||
| `src/xrpld/telemetry/PropagationHelpers.h` | `injectSpanContext()` — SpanGuard to protobuf |
|
||||
| `include/xrpl/telemetry/TraceContextPropagator.h` | OTel context <-> protobuf conversion primitives |
|
||||
| `src/xrpld/telemetry/ConsensusReceiveTracing.h` | Proposal/validation receive span factories |
|
||||
| `src/xrpld/telemetry/TxTracing.h` | Transaction receive span factory |
|
||||
|
||||
### Backwards Compatibility
|
||||
|
||||
Older peers that do not populate `TraceContext` fields in their messages will
|
||||
simply produce empty trace bytes on the receive side. The extraction helpers
|
||||
detect this and create standalone (root) spans instead of child spans. No
|
||||
errors are logged and no data is lost — the receive span is still created with
|
||||
all its normal attributes, it just lacks a cross-node parent link.
|
||||
|
||||
### Example Tempo Queries
|
||||
|
||||
```
|
||||
# Find cross-node transaction traces (tx.process -> tx.receive across nodes)
|
||||
{name="tx.receive"} && status != error
|
||||
|
||||
# Find proposals received with cross-node parent context
|
||||
{} >> {name="consensus.proposal.receive"}
|
||||
|
||||
# Trace a transaction across the network by its hash
|
||||
{name=~"tx\\..*"} | tx_hash = "<hash>"
|
||||
|
||||
# Find all spans in a cross-node consensus trace
|
||||
{rootServiceName="xrpld"} | consensus_round_id = <round_id>
|
||||
|
||||
# Compare latency between sender and receiver for validations
|
||||
{name="consensus.validation.send" || name="consensus.validation.receive"}
|
||||
```
|
||||
|
||||
## Prometheus Metrics (Spanmetrics)
|
||||
|
||||
The OTel Collector's spanmetrics connector automatically derives RED (Rate, Errors, Duration) metrics from every span. No custom metrics code is needed in xrpld.
|
||||
|
||||
### Generated Metric Names
|
||||
|
||||
| Prometheus Metric | Type | Description |
|
||||
| ----------------------------------- | --------- | ---------------------------- |
|
||||
| `span_calls_total` | Counter | Total span invocations |
|
||||
| `span_duration_milliseconds_bucket` | Histogram | Latency distribution buckets |
|
||||
| `span_duration_milliseconds_count` | Histogram | Latency observation count |
|
||||
| `span_duration_milliseconds_sum` | Histogram | Cumulative latency |
|
||||
|
||||
### Metric Labels
|
||||
|
||||
Every metric carries these standard labels:
|
||||
|
||||
| Label | Source | Example |
|
||||
| -------------- | ------------------ | ---------------------------------------- |
|
||||
| `span_name` | Span name | `rpc.command.server_info` |
|
||||
| `status_code` | Span status | `STATUS_CODE_UNSET`, `STATUS_CODE_ERROR` |
|
||||
| `service_name` | Resource attribute | `xrpld` |
|
||||
| `span_kind` | Span kind | `SPAN_KIND_INTERNAL` |
|
||||
|
||||
Additionally, span attributes configured as dimensions in the collector
|
||||
become metric labels. The span attribute keys are already underscore form
|
||||
(the naming convention forbids dots), so the label name matches the attribute
|
||||
name verbatim. Prometheus' dots → underscores sanitization only fires for
|
||||
dotted attribute names (e.g. resource attributes like `service.name`), which
|
||||
does not apply to these dimensions.
|
||||
|
||||
| Span Attribute | Metric Label | Applies To |
|
||||
| -------------------- | -------------------- | ------------------------------- |
|
||||
| `command` | `command` | `rpc.command.*` spans |
|
||||
| `rpc_status` | `rpc_status` | `rpc.command.*` spans |
|
||||
| `consensus_mode` | `consensus_mode` | `consensus.ledger_close` spans |
|
||||
| `local` | `local` | `tx.process` spans |
|
||||
| `proposal_trusted` | `proposal_trusted` | `peer.proposal.receive` spans |
|
||||
| `validation_trusted` | `validation_trusted` | `peer.validation.receive` spans |
|
||||
|
||||
### Histogram Buckets
|
||||
|
||||
Configured in `otel-collector-config.yaml`:
|
||||
|
||||
```
|
||||
1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 5s
|
||||
```
|
||||
|
||||
## System Metrics (OTel native -- beast::insight)
|
||||
|
||||
xrpld has a built-in metrics framework (`beast::insight`) that exports metrics natively via OTLP to the OTel Collector. These complement the span-derived RED metrics by providing system-level gauges, counters, and timers that don't map to individual trace spans.
|
||||
|
||||
### Configuration
|
||||
|
||||
Add to `xrpld.cfg`:
|
||||
|
||||
```ini
|
||||
[insight]
|
||||
server=otel
|
||||
endpoint=http://localhost:4318/v1/metrics
|
||||
prefix=xrpld
|
||||
```
|
||||
|
||||
The `OTelCollector` implementation exports metrics via OTLP/HTTP to the same OTel Collector that receives traces. No separate StatsD receiver is needed.
|
||||
|
||||
> **Fallback**: Set `server=statsd` and `address=127.0.0.1:8125` to use the legacy StatsD UDP path. This requires re-enabling the `statsd` receiver in `otel-collector-config.yaml` and uncommenting port 8125 in `docker-compose.yml`.
|
||||
|
||||
### Metric Reference
|
||||
|
||||
#### Gauges
|
||||
|
||||
| Prometheus Metric | Source | Description |
|
||||
| ------------------------------------- | ------------------------- | -------------------------------------------------------------------------- |
|
||||
| `ledgermaster_validated_ledger_age` | LedgerMaster.h:373 | Age of validated ledger (seconds) |
|
||||
| `ledgermaster_published_ledger_age` | LedgerMaster.h:374 | Age of published ledger (seconds) |
|
||||
| `state_accounting_{Mode}_duration` | NetworkOPs.cpp:774 | Time in each operating mode (Disconnected/Connected/Syncing/Tracking/Full) |
|
||||
| `state_accounting_{Mode}_transitions` | NetworkOPs.cpp:780 | Transition count per mode |
|
||||
| `peer_finder_active_inbound_peers` | PeerfinderManager.cpp:214 | Active inbound peer connections |
|
||||
| `peer_finder_active_outbound_peers` | PeerfinderManager.cpp:215 | Active outbound peer connections |
|
||||
| `overlay_peer_disconnects` | OverlayImpl.h:557 | Peer disconnect count |
|
||||
| `job_count` | JobQueue.cpp:26 | Current job queue depth |
|
||||
| `{category}_bytes_in/out` | OverlayImpl.h:535 | Overlay traffic bytes per category (57 categories) |
|
||||
| `{category}_messages_in/out` | OverlayImpl.h:535 | Overlay traffic messages per category |
|
||||
|
||||
#### Counters
|
||||
|
||||
| Prometheus Metric | Source | Description |
|
||||
| ------------------------- | --------------------- | ------------------------------ |
|
||||
| `rpc_requests` | ServerHandler.cpp:108 | Total RPC request count |
|
||||
| `ledger_fetches` | InboundLedgers.cpp:44 | Ledger fetch request count |
|
||||
| `ledger_history_mismatch` | LedgerHistory.cpp:16 | Ledger hash mismatch count |
|
||||
| `warn` | Logic.h:33 | Resource manager warning count |
|
||||
| `drop` | Logic.h:34 | Resource manager drop count |
|
||||
|
||||
#### Histograms
|
||||
|
||||
| Prometheus Metric | Source | Description |
|
||||
| ----------------- | --------------------- | ------------------------------ |
|
||||
| `rpc_time` | ServerHandler.cpp:110 | RPC response time (ms) |
|
||||
| `rpc_size` | ServerHandler.cpp:109 | RPC response size (bytes) |
|
||||
| `ios_latency` | Application.cpp:438 | I/O service loop latency (ms) |
|
||||
| `pathfind_fast` | PathRequests.h:23 | Fast pathfinding duration (ms) |
|
||||
| `pathfind_full` | PathRequests.h:24 | Full pathfinding duration (ms) |
|
||||
|
||||
## Deployment Tiers
|
||||
|
||||
Multiple xrpld instances can send telemetry to per-tier collectors that all
|
||||
forward to one Grafana stack. Four resource attributes segregate the data so
|
||||
one dashboard set serves every deployment:
|
||||
|
||||
| Dimension | Attribute | Set by | Example values |
|
||||
| ----------- | ------------------------ | ---------- | ------------------------------ |
|
||||
| Node | `service.instance.id` | xrpld cfg | `alice-laptop`, `ci-runner-7` |
|
||||
| Service | `service.name` | xrpld cfg | `xrpld`, `xrpld-validator` |
|
||||
| Network | `xrpl.network.type` | xrpld node | `mainnet`, `testnet`, `devnet` |
|
||||
| Environment | `deployment.environment` | collector | `local`, `test`, `ci`, `prod` |
|
||||
|
||||
Dashboards expose these as the template variables `$node`, `$service_name`,
|
||||
`$xrpl_network_type`, and `$deployment_environment` (each variable name
|
||||
matches its Prometheus label). Select them top-down — environment → network
|
||||
→ service → node. Selecting **All** matches every value, including series
|
||||
lacking the label, so mixed old/new data never disappears.
|
||||
|
||||
### Who owns which attribute
|
||||
|
||||
- **Node and service** come from xrpld config (`service_instance_id`,
|
||||
`service_name`). Unique per process.
|
||||
- **Network** is a property of the chain the node joined; the node derives it
|
||||
from `[network_id]` and stamps `xrpl.network.type` on all three signals.
|
||||
- **Environment** is a property of where the collector runs; each collector
|
||||
serves one environment and stamps it.
|
||||
|
||||
### The upsert vs insert rule
|
||||
|
||||
The collector's `resource/tier` processor uses two actions on purpose:
|
||||
|
||||
- `deployment.environment` → **`upsert`** (overwrite). The collector _is_ the
|
||||
environment, so it is authoritative.
|
||||
- `xrpl.network.type` → **`insert`** (fill only if absent). The node knows
|
||||
its real network, so the collector must not overwrite it — `insert` only
|
||||
supplies a value when the source did not (e.g. an older xrpld build). This
|
||||
is what lets a local node connected to mainnet report `network=mainnet`,
|
||||
not the collector's default.
|
||||
|
||||
### Configuring a collector for a tier
|
||||
|
||||
Each tier runs its own collector. Set the two values in the `resource/tier`
|
||||
processor of the collector config (`otel-collector-config.yaml` for local
|
||||
backends, `otel-collector-config.grafanacloud.yaml` for Grafana Cloud):
|
||||
|
||||
```yaml
|
||||
processors:
|
||||
resource/tier:
|
||||
attributes:
|
||||
- key: deployment.environment
|
||||
value: <tier> # local | test | ci | prod
|
||||
action: upsert
|
||||
- key: xrpl.network.type
|
||||
value: <network> # mainnet | testnet | devnet (fallback only)
|
||||
action: insert
|
||||
```
|
||||
|
||||
Suggested per-tier values:
|
||||
|
||||
| Collector | `deployment.environment` | `xrpl.network.type` (fallback) |
|
||||
| ------------------- | ------------------------ | ------------------------------ |
|
||||
| Developer laptop | `local` | `devnet` |
|
||||
| Test machines | `test` | `testnet` |
|
||||
| CI runs | `ci` | `testnet` |
|
||||
| Production observer | `prod` | `mainnet` |
|
||||
|
||||
The `xrpl.network.type` value is only a fallback: when the node stamps its
|
||||
own network (all current builds do), the node's value wins. Set it to the
|
||||
network the collector most commonly serves.
|
||||
|
||||
### How the tier labels reach metrics
|
||||
|
||||
Resource attributes do not become Prometheus labels automatically. Two
|
||||
collector settings make it work, both already enabled:
|
||||
|
||||
- `prometheus.resource_to_telemetry_conversion: enabled: true` promotes
|
||||
resource attributes to metric labels on the local scrape surface.
|
||||
- `spanmetrics.resource_metrics_key_attributes` lists the tier attributes so
|
||||
span-derived series stay grouped per node and tier.
|
||||
|
||||
Traces and logs carry resource attributes natively; Grafana Cloud ingests all
|
||||
three signals' attributes over OTLP directly.
|
||||
|
||||
## Grafana Dashboards
|
||||
|
||||
Ten dashboards are pre-provisioned in `docker/telemetry/grafana/dashboards/`:
|
||||
|
||||
### RPC Performance (`rpc-performance`)
|
||||
|
||||
| Panel | Type | PromQL | Labels Used |
|
||||
| --------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
|
||||
| RPC Request Rate by Command | timeseries | `sum by (command) (rate(span_calls_total{span_name=~"rpc.command.*"}[5m]))` | `command` |
|
||||
| RPC Latency p95 by Command | timeseries | `histogram_quantile(0.95, sum by (le, command) (rate(span_duration_milliseconds_bucket{span_name=~"rpc.command.*"}[5m])))` | `command` |
|
||||
| RPC Error Rate | bargauge | Error spans / total spans × 100, grouped by `command` | `command`, `status_code` |
|
||||
| RPC Latency Heatmap | heatmap | `sum(increase(span_duration_milliseconds_bucket{span_name=~"rpc.command.*"}[5m])) by (le)` | `le` (bucket boundaries) |
|
||||
| Overall RPC Throughput | timeseries | `rpc.request` + `rpc.process` rate | — |
|
||||
| RPC Success vs Error | timeseries | by `status_code` (UNSET vs ERROR) | `status_code` |
|
||||
| Top Commands by Volume | bargauge | `topk(10, ...)` by `command` | `command` |
|
||||
| WebSocket Message Rate | stat | `rpc.ws_message` rate | — |
|
||||
|
||||
### Transaction Overview (`transaction-overview`)
|
||||
|
||||
| Panel | Type | PromQL | Labels Used |
|
||||
| ---------------------------------- | -------------- | -------------------------------------------------------------------------------------------- | ----------------------------------- |
|
||||
| Transaction Processing Rate | timeseries | `rate(span_calls_total{span_name="tx.process"}[5m])` and `tx.receive` | `span_name` |
|
||||
| Transaction Processing Latency | timeseries | `histogram_quantile(0.95 / 0.50, ... {span_name="tx.process"})` | — |
|
||||
| Transaction Path Distribution | piechart | `sum by (local) (rate(span_calls_total{span_name="tx.process"}[5m]))` | `local` |
|
||||
| Transaction Receive vs Suppressed | timeseries | `rate(span_calls_total{span_name="tx.receive"}[5m])` | — |
|
||||
| TX Processing Duration Heatmap | heatmap | `tx.process` histogram buckets | `le` |
|
||||
| TX Apply Duration per Ledger | timeseries | p95/p50 of `tx.apply` | — |
|
||||
| Peer TX Receive Rate | timeseries | `tx.receive` rate | — |
|
||||
| TX Apply Failed Rate | stat | `rate(span_calls_total{span_name="tx.transactor",stage="apply",ter_result!~"tesSUCCESS\|"})` | `stage`, `ter_result` |
|
||||
| TxQ Accept: Applied Ratio per Node | state-timeline | applied / (applied+failed) of `span_calls_total{span_name="txq.accept_tx"}` per node | `txq_status`, `service_instance_id` |
|
||||
|
||||
### Consensus Health (`consensus-health`)
|
||||
|
||||
| Panel | Type | PromQL | Labels Used |
|
||||
| ----------------------------- | ---------- | --------------------------------------------------------------------------- | ---------------- |
|
||||
| Consensus Round Duration | timeseries | `histogram_quantile(0.95 / 0.50, ... {span_name="consensus.accept"})` | — |
|
||||
| Consensus Proposals Sent Rate | timeseries | `rate(span_calls_total{span_name="consensus.proposal.send"}[5m])` | — |
|
||||
| Ledger Close Duration | timeseries | `histogram_quantile(0.95, ... {span_name="consensus.ledger_close"})` | — |
|
||||
| Validation Send Rate | stat | `rate(span_calls_total{span_name="consensus.validation.send"}[5m])` | — |
|
||||
| Ledger Apply Duration | timeseries | `histogram_quantile(0.95 / 0.50, ... {span_name="consensus.accept.apply"})` | — |
|
||||
| Close Time Agreement | timeseries | `rate(span_calls_total{span_name="consensus.accept.apply"}[5m])` | — |
|
||||
| Consensus Mode Over Time | timeseries | `consensus.ledger_close` by `consensus_mode` | `consensus_mode` |
|
||||
| Accept vs Close Rate | timeseries | `consensus.accept` vs `consensus.ledger_close` rate | — |
|
||||
| Validation vs Close Rate | timeseries | `consensus.validation.send` vs `consensus.ledger_close` | — |
|
||||
| Accept Duration Heatmap | heatmap | `consensus.accept` histogram buckets | `le` |
|
||||
|
||||
### Ledger Operations (`ledger-operations`)
|
||||
|
||||
| Panel | Type | PromQL | Labels Used |
|
||||
| ----------------------- | ---------- | ---------------------------------------------- | ----------- |
|
||||
| Ledger Build Rate | stat | `ledger.build` call rate | — |
|
||||
| Ledger Build Duration | timeseries | p95/p50 of `ledger.build` | — |
|
||||
| Ledger Validation Rate | stat | `ledger.validate` call rate | — |
|
||||
| Build Duration Heatmap | heatmap | `ledger.build` histogram buckets | `le` |
|
||||
| TX Apply Duration | timeseries | p95/p50 of `tx.apply` | — |
|
||||
| TX Apply Rate | timeseries | `tx.apply` call rate | — |
|
||||
| Ledger Store Rate | stat | `ledger.store` call rate | — |
|
||||
| Build vs Close Duration | timeseries | p95 `ledger.build` vs `consensus.ledger_close` | — |
|
||||
|
||||
### Peer Network (`peer-network`)
|
||||
|
||||
Requires `trace_peer=1` in the `[telemetry]` config section.
|
||||
|
||||
| Panel | Type | PromQL | Labels Used |
|
||||
| -------------------------------- | ---------- | ------------------------------ | -------------------- |
|
||||
| Proposal Receive Rate | timeseries | `peer.proposal.receive` rate | — |
|
||||
| Validation Receive Rate | timeseries | `peer.validation.receive` rate | — |
|
||||
| Proposals Trusted vs Untrusted | piechart | by `proposal_trusted` | `proposal_trusted` |
|
||||
| Validations Trusted vs Untrusted | piechart | by `validation_trusted` | `validation_trusted` |
|
||||
|
||||
### Node Health -- System Metrics (`node-health`)
|
||||
|
||||
| Panel | Type | PromQL | Labels Used |
|
||||
| -------------------------------------- | ---------- | ---------------------------------------------------------- | ----------- |
|
||||
| Validated Ledger Age | stat | `ledgermaster_validated_ledger_age` | — |
|
||||
| Published Ledger Age | stat | `ledgermaster_published_ledger_age` | — |
|
||||
| Operating Mode (Time Share) | timeseries | `rate(state_accounting_X_duration) / sum(rate(all modes))` | — |
|
||||
| Operating Mode Transitions | timeseries | `state_accounting_*_transitions` | — |
|
||||
| I/O Latency | timeseries | `histogram_quantile(0.95, ios_latency_bucket)` | — |
|
||||
| Job Queue Depth | timeseries | `job_count` | — |
|
||||
| Ledger Fetch Rate | stat | `rate(ledger_fetches[5m])` | — |
|
||||
| Ledger History Mismatches | stat | `rate(ledger_history_mismatch[5m])` | — |
|
||||
| Key Jobs Execution Time | timeseries | `acceptledger{quantile="$quantile"}` (+ 10 more key jobs) | `quantile` |
|
||||
| Key Jobs Dequeue Wait Time | timeseries | `acceptledger_q{quantile="$quantile"}` (+ 10 more) | `quantile` |
|
||||
| FullBelowCache Size | timeseries | `node_family_full_below_cache_size` | — |
|
||||
| FullBelowCache Hit Rate | gauge | `node_family_full_below_cache_hit_rate` | — |
|
||||
| Ledger Publish Gap | stat | `Published_Ledger_Age - Validated_Ledger_Age` | — |
|
||||
| State Duration Rate (Full vs Tracking) | timeseries | `rate(state_accounting_full_duration[5m]) / 1000000` | — |
|
||||
| All Jobs Execution Time (Detail) | timeseries | `{__name__=~"<all_jobs>", quantile="$quantile"}` | `quantile` |
|
||||
| All Jobs Dequeue Wait (Detail) | timeseries | `{__name__=~"<all_jobs>_q", quantile="$quantile"}` | `quantile` |
|
||||
|
||||
### Network Traffic -- System Metrics (`network-traffic`)
|
||||
|
||||
| Panel | Type | PromQL | Labels Used |
|
||||
| ------------------------------------ | ---------- | ------------------------------------------------------ | ----------- |
|
||||
| Active Peers | timeseries | `peer_finder_active_*_peers` | — |
|
||||
| Peer Disconnects | timeseries | `increase(overlay_peer_disconnects[$__rate_interval])` | — |
|
||||
| Total Network Bytes | timeseries | `rate(total_bytes_in/out[$__rate_interval])` | — |
|
||||
| Total Network Messages | timeseries | `rate(total_messages_in/out[$__rate_interval])` | — |
|
||||
| Transaction Traffic | timeseries | `rate(transactions_messages_in/out[$__rate_interval])` | — |
|
||||
| Proposal Traffic | timeseries | `rate(proposals_messages_in/out[$__rate_interval])` | — |
|
||||
| Validation Traffic | timeseries | `rate(validations_messages_in/out[$__rate_interval])` | — |
|
||||
| Traffic by Category | bargauge | `topk(10, rate(*_bytes_in[$__rate_interval]))` | — |
|
||||
| Duplicate Traffic (Wasted Bandwidth) | timeseries | `rate(*_duplicate_bytes_in/out[$__rate_interval])` | — |
|
||||
| All Traffic Categories (Detail) | timeseries | `topk(15, rate(*_bytes_in[5m]))` | — |
|
||||
|
||||
### RPC & Pathfinding -- System Metrics (`rpc-pathfinding`)
|
||||
|
||||
| Panel | Type | PromQL | Labels Used |
|
||||
| ------------------------- | ---------- | ------------------------------------------------ | ----------- |
|
||||
| RPC Request Rate | stat | `rate(rpc_requests[5m])` | — |
|
||||
| RPC Response Time | timeseries | `histogram_quantile(0.95, rpc_time_bucket)` | — |
|
||||
| RPC Response Size | timeseries | `histogram_quantile(0.95, rpc_size_bucket)` | — |
|
||||
| RPC Response Time Heatmap | heatmap | `rpc_time_bucket` | — |
|
||||
| Pathfinding Fast Duration | timeseries | `histogram_quantile(0.95, pathfind_fast_bucket)` | — |
|
||||
| Pathfinding Full Duration | timeseries | `histogram_quantile(0.95, pathfind_full_bucket)` | — |
|
||||
| Resource Warnings Rate | stat | `rate(warn[5m])` | — |
|
||||
| Resource Drops Rate | stat | `rate(drop[5m])` | — |
|
||||
|
||||
### Span → Metric → Dashboard Summary
|
||||
|
||||
| Span Name | Prometheus Metric Filter | Grafana Dashboard |
|
||||
| ------------------------------ | -------------------------------------------- | --------------------------------------------- |
|
||||
| `rpc.http_request` | `{span_name="rpc.http_request"}` | RPC Performance (Overall Throughput) |
|
||||
| `rpc.ws_upgrade` | `{span_name="rpc.ws_upgrade"}` | -- (available but not paneled) |
|
||||
| `rpc.ws_message` | `{span_name="rpc.ws_message"}` | RPC Performance (WebSocket Rate) |
|
||||
| `rpc.process` | `{span_name="rpc.process"}` | RPC Performance (Overall Throughput) |
|
||||
| `rpc.command.*` | `{span_name=~"rpc.command.*"}` | RPC Performance (Rate, Latency, Error, Top) |
|
||||
| `tx.process` | `{span_name="tx.process"}` | Transaction Overview (Rate, Latency, Heatmap) |
|
||||
| `tx.receive` | `{span_name="tx.receive"}` | Transaction Overview (Rate, Receive) |
|
||||
| `tx.apply` | `{span_name="tx.apply"}` | Transaction Overview + Ledger Ops (Apply) |
|
||||
| `txq.enqueue` | `{span_name="txq.enqueue"}` | -- (available but not paneled) |
|
||||
| `txq.apply_direct` | `{span_name="txq.apply_direct"}` | -- (available but not paneled) |
|
||||
| `txq.batch_clear` | `{span_name="txq.batch_clear"}` | -- (available but not paneled) |
|
||||
| `txq.accept` | `{span_name="txq.accept"}` | -- (available but not paneled) |
|
||||
| `txq.accept_tx` | `{span_name="txq.accept_tx"}` | -- (available but not paneled) |
|
||||
| `txq.cleanup` | `{span_name="txq.cleanup"}` | -- (available but not paneled) |
|
||||
| `consensus.round` | `{span_name="consensus.round"}` | -- (available but not paneled) |
|
||||
| `consensus.phase.open` | `{span_name="consensus.phase.open"}` | -- (available but not paneled) |
|
||||
| `consensus.establish` | `{span_name="consensus.establish"}` | -- (available but not paneled) |
|
||||
| `consensus.update_positions` | `{span_name="consensus.update_positions"}` | -- (available but not paneled) |
|
||||
| `consensus.check` | `{span_name="consensus.check"}` | -- (available but not paneled) |
|
||||
| `consensus.accept` | `{span_name="consensus.accept"}` | Consensus Health (Duration, Rate, Heatmap) |
|
||||
| `consensus.proposal.send` | `{span_name="consensus.proposal.send"}` | Consensus Health (Proposals Rate) |
|
||||
| `consensus.ledger_close` | `{span_name="consensus.ledger_close"}` | Consensus Health (Close, Mode) |
|
||||
| `consensus.validation.send` | `{span_name="consensus.validation.send"}` | Consensus Health (Validation Rate) |
|
||||
| `consensus.accept.apply` | `{span_name="consensus.accept.apply"}` | Consensus Health (Apply Duration, Close Time) |
|
||||
| `consensus.mode_change` | `{span_name="consensus.mode_change"}` | -- (available but not paneled) |
|
||||
| `consensus.proposal.receive` | `{span_name="consensus.proposal.receive"}` | -- (available but not paneled) |
|
||||
| `consensus.validation.receive` | `{span_name="consensus.validation.receive"}` | -- (available but not paneled) |
|
||||
| `ledger.build` | `{span_name="ledger.build"}` | Ledger Ops (Build Rate, Duration, Heatmap) |
|
||||
| `ledger.validate` | `{span_name="ledger.validate"}` | Ledger Ops (Validation Rate) |
|
||||
| `ledger.store` | `{span_name="ledger.store"}` | Ledger Ops (Store Rate) |
|
||||
| `peer.proposal.receive` | `{span_name="peer.proposal.receive"}` | Peer Network (Rate, Trusted/Untrusted) |
|
||||
| `peer.validation.receive` | `{span_name="peer.validation.receive"}` | Peer Network (Rate, Trusted/Untrusted) |
|
||||
|
||||
## Log-Trace Correlation
|
||||
|
||||
When xrpld is built with `telemetry=ON`, log lines emitted within an active OpenTelemetry span automatically include `trace_id` and `span_id` fields:
|
||||
|
||||
```
|
||||
2024-Jan-15 10:30:45.123456 UTC LedgerMaster:NFO trace_id=abc123def456789012345678abcdef01 span_id=0123456789abcdef Validated ledger 42
|
||||
```
|
||||
|
||||
This enables bidirectional navigation between logs and traces in Grafana:
|
||||
|
||||
- **Tempo -> Loki**: Click "Logs for this trace" on any trace in Grafana Tempo to see all log lines from that trace.
|
||||
- **Loki -> Tempo**: Click the `TraceID` derived field link on any log line containing `trace_id=` to jump to the full trace in Tempo.
|
||||
|
||||
### Log Ingestion Pipeline
|
||||
|
||||
Log files are ingested by the OTel Collector's `filelog` receiver, which tails `debug.log` files and parses them with a regex that extracts `timestamp`, `partition`, `severity`, `trace_id`, `span_id`, and `message` fields. Parsed entries are exported to Grafana Loki.
|
||||
|
||||
The receiver tails `/var/log/xrpld/*/debug.log` inside the collector container. docker-compose bind-mounts the host log root there; the source defaults to the repo-relative `docker/telemetry/data/logs`, which the telemetry configs write to (`data/logs/<network>/debug.log`) and which needs no root. To tail logs from elsewhere, set `XRPLD_LOG_DIR` before `docker compose up` (the integration test does this to point at its own workdir). The single trailing `*` matches one per-network or per-node subdirectory.
|
||||
|
||||
### LogQL Query Examples
|
||||
|
||||
```logql
|
||||
# Find all logs for a specific trace
|
||||
{job="xrpld"} |= "trace_id=abc123def456789012345678abcdef01"
|
||||
|
||||
# Error logs with trace context (log lines with ERR severity that have a trace_id)
|
||||
{job="xrpld"} |= "ERR" |= "trace_id="
|
||||
|
||||
# All logs from a specific partition that were emitted during a span
|
||||
{job="xrpld"} |= "LedgerMaster" | regexp `trace_id=(?P<trace_id>[a-f0-9]+)` | trace_id != ""
|
||||
|
||||
# Logs from the last hour containing trace context
|
||||
{job="xrpld"} |= "trace_id=" | regexp `(?P<partition>\S+):(?P<sev>\S+)\s+trace_id=(?P<tid>[a-f0-9]+)`
|
||||
|
||||
# Count of traced vs untraced log lines
|
||||
count_over_time({job="xrpld"} |= "trace_id=" [5m])
|
||||
```
|
||||
|
||||
### Verifying Log Correlation
|
||||
|
||||
1. Start the observability stack and xrpld with telemetry enabled.
|
||||
2. Send an RPC request: `curl http://localhost:5005 -d '{"method":"server_info"}'`
|
||||
3. Check the debug.log for `trace_id=` entries: `grep trace_id= /path/to/debug.log`
|
||||
4. Open Grafana at http://localhost:3000 -> Explore -> Loki and search for `{job="xrpld"} |= "trace_id="`.
|
||||
5. Click the TraceID link to navigate to the corresponding trace in Tempo.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No traces appearing in Tempo
|
||||
|
||||
1. Check xrpld logs for `Telemetry starting` message
|
||||
2. Verify `enabled=1` in the `[telemetry]` config section
|
||||
3. Test collector connectivity: `curl -v http://localhost:4318/v1/traces`
|
||||
4. Check collector logs: `docker compose -f docker/telemetry/docker-compose.yml logs otel-collector`
|
||||
5. Verify Tempo is receiving data: open Grafana → Explore → select Tempo datasource → search by `service.name = xrpld`
|
||||
6. Check Tempo logs: `docker compose -f docker/telemetry/docker-compose.yml logs tempo`
|
||||
|
||||
### No system metrics in Prometheus
|
||||
|
||||
1. Check xrpld logs for `OTelCollector starting` message
|
||||
2. Verify `server=otel` in the `[insight]` config section
|
||||
3. Verify the endpoint in `[insight]` points to the OTLP/HTTP port (default: `http://localhost:4318/v1/metrics`)
|
||||
4. Check that the `otlp` receiver is in the metrics pipeline receivers in `otel-collector-config.yaml`
|
||||
5. Query Prometheus directly: `curl 'http://localhost:9090/api/v1/query?query=job_count'`
|
||||
|
||||
### High memory usage
|
||||
|
||||
- Reduce trace volume with collector-side tail sampling (xrpld head sampling is
|
||||
fixed at 1.0 and is not configurable)
|
||||
- Reduce `max_queue_size` and `batch_size`
|
||||
- Disable high-volume trace categories: `trace_peer=0`
|
||||
|
||||
### Collector connection failures
|
||||
|
||||
- Verify endpoint URL matches collector address
|
||||
- Check firewall rules for ports 4317/4318
|
||||
- If using TLS, verify certificate path with `tls_ca_cert`
|
||||
|
||||
### No trace_id in log output
|
||||
|
||||
- Verify xrpld was built with `telemetry=ON` (the `XRPL_ENABLE_TELEMETRY` preprocessor flag)
|
||||
- Verify `enabled=1` in the `[telemetry]` config section
|
||||
- Log lines only contain `trace_id`/`span_id` when emitted inside an active span — background logs outside of RPC/consensus/transaction processing will not have trace context
|
||||
- Check that the specific trace category is enabled (e.g., `trace_rpc=1`)
|
||||
|
||||
### No logs in Loki
|
||||
|
||||
- Verify the log file mount in docker-compose.yml points to the correct xrpld log directory (default source `docker/telemetry/data/logs`, or the `XRPLD_LOG_DIR` override) and that xrpld actually writes `debug.log` there
|
||||
- Check OTel Collector logs for filelog receiver errors: `docker compose logs otel-collector`
|
||||
- Verify Loki is running: `curl http://localhost:3100/ready`
|
||||
- Check the filelog receiver glob `/var/log/xrpld/*/debug.log` matches your log layout — the log file must sit one subdirectory below the mount root
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
| Scenario | Recommendation |
|
||||
| ------------------------ | --------------------------------------------------------- |
|
||||
| Production mainnet | `trace_peer=0`; reduce volume via collector tail sampling |
|
||||
| Testnet/devnet | Full tracing (head sampling fixed at 1.0) |
|
||||
| Debugging specific issue | Full tracing (head sampling fixed at 1.0) |
|
||||
| High-throughput node | Increase `batch_size=1024`, `max_queue_size=4096` |
|
||||
|
||||
## Disabling Telemetry
|
||||
|
||||
Set `enabled=0` in config (runtime disable) or build without the flag:
|
||||
|
||||
```bash
|
||||
cmake --preset default -Dtelemetry=OFF
|
||||
```
|
||||
|
||||
When telemetry is compiled out, all trace macros expand to no-ops with zero overhead.
|
||||
@@ -641,6 +641,9 @@ template <class T>
|
||||
T*
|
||||
SharedWeakUnion<T>::unsafeGetRawPtr() const
|
||||
{
|
||||
// tp_ packs a raw pointer together with a strength bit; recovering the
|
||||
// pointer inherently requires an integer-to-pointer cast.
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr)
|
||||
return reinterpret_cast<T*>(tp_ & kPtrMask);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
@@ -234,11 +233,4 @@ makeSlice(std::basic_string<char, Traits, Alloc> const& s)
|
||||
return Slice(s.data(), s.size());
|
||||
}
|
||||
|
||||
template <class Traits>
|
||||
Slice
|
||||
makeSlice(std::basic_string_view<char, Traits> s)
|
||||
{
|
||||
return Slice(s.data(), s.size());
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -138,11 +138,8 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
ConstIterator(Iterator const& orig)
|
||||
ConstIterator(Iterator const& orig) : map(orig.map), ait(orig.ait), mit(orig.mit)
|
||||
{
|
||||
map = orig.map;
|
||||
ait = orig.ait;
|
||||
mit = orig.mit;
|
||||
}
|
||||
|
||||
const_reference
|
||||
@@ -231,11 +228,11 @@ private:
|
||||
|
||||
public:
|
||||
PartitionedUnorderedMap(std::optional<std::size_t> partitions = std::nullopt)
|
||||
{
|
||||
// Set partitions to the number of hardware threads if the parameter
|
||||
// is either empty or set to 0.
|
||||
partitions_ =
|
||||
partitions && (*partitions != 0u) ? *partitions : std::thread::hardware_concurrency();
|
||||
: partitions_(
|
||||
partitions && (*partitions != 0u) ? *partitions : std::thread::hardware_concurrency())
|
||||
{
|
||||
map_.resize(partitions_);
|
||||
XRPL_ASSERT(
|
||||
partitions_,
|
||||
|
||||
@@ -359,6 +359,7 @@ private:
|
||||
deleteElement(Element const* p)
|
||||
{
|
||||
ElementAllocatorTraits::destroy(config_.alloc(), p);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
|
||||
ElementAllocatorTraits::deallocate(config_.alloc(), const_cast<Element*>(p), 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -528,6 +528,7 @@ private:
|
||||
deleteElement(Element const* p)
|
||||
{
|
||||
ElementAllocatorTraits::destroy(config_.alloc(), p);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
|
||||
ElementAllocatorTraits::deallocate(config_.alloc(), const_cast<Element*>(p), 1);
|
||||
}
|
||||
|
||||
|
||||
135
include/xrpl/beast/insight/OTelCollector.h
Normal file
135
include/xrpl/beast/insight/OTelCollector.h
Normal file
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file OTelCollector.h
|
||||
* @brief OpenTelemetry-based implementation of the beast::insight::Collector
|
||||
* interface for native OTLP metric export.
|
||||
*
|
||||
* When XRPL_ENABLE_TELEMETRY is defined, OTelCollector maps each
|
||||
* beast::insight instrument type (Counter, Gauge, Event, Meter, Hook) to
|
||||
* the corresponding OpenTelemetry Metrics SDK instrument and exports
|
||||
* them via OTLP/HTTP to an OpenTelemetry Collector.
|
||||
*
|
||||
* When XRPL_ENABLE_TELEMETRY is NOT defined, OTelCollector::New() returns
|
||||
* a NullCollector so the binary compiles without OTel dependencies.
|
||||
*
|
||||
* Dependency diagram:
|
||||
*
|
||||
* +-----------------+ +-------------------+
|
||||
* | Collector (ABC) |<----| OTelCollector |
|
||||
* +-----------------+ | (public header) |
|
||||
* ^ +-------------------+
|
||||
* | |
|
||||
* +-----------------+ +-------------------+
|
||||
* | NullCollector | | OTelCollectorImp |
|
||||
* | (fallback when | | (impl in .cpp, |
|
||||
* | no telemetry) | | uses OTel SDK) |
|
||||
* +-----------------+ +-------------------+
|
||||
* |
|
||||
* +-------------------+
|
||||
* | OTel Metrics SDK |
|
||||
* | MeterProvider |
|
||||
* | OTLP HTTP Metric |
|
||||
* | Exporter |
|
||||
* +-------------------+
|
||||
*/
|
||||
|
||||
#include <xrpl/beast/insight/Collector.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace beast::insight {
|
||||
|
||||
/**
|
||||
* @brief A Collector that exports metrics via OpenTelemetry OTLP/HTTP.
|
||||
*
|
||||
* Replaces StatsD-based metric collection with native OTel Metrics SDK
|
||||
* instruments. Each beast::insight instrument maps to an OTel equivalent:
|
||||
*
|
||||
* - Counter -> OTel Counter<uint64_t>
|
||||
* - Gauge -> OTel ObservableGauge<int64_t> (async callback)
|
||||
* - Event -> OTel Histogram<double> (duration in milliseconds)
|
||||
* - Meter -> OTel Counter<uint64_t> (monotonic, unsigned)
|
||||
* - Hook -> Called by PeriodicMetricReader at collection time
|
||||
*
|
||||
* Example — primary use (create the collector and record a metric):
|
||||
* @code
|
||||
* auto collector = beast::insight::OTelCollector::New(
|
||||
* "http://localhost:4318/v1/metrics", // OTLP/HTTP endpoint
|
||||
* "xrpld", // metric name prefix
|
||||
* "node-1", // service.instance.id
|
||||
* "xrpld", // service.name
|
||||
* "mainnet", // xrpl.network.type
|
||||
* journal);
|
||||
*
|
||||
* auto counter = collector->makeCounter("ledgers", "closed");
|
||||
* ++counter; // exported on the next PeriodicMetricReader tick
|
||||
* @endcode
|
||||
*
|
||||
* Example — edge case (telemetry disabled at compile time): New()
|
||||
* returns a NullCollector, so callers need no #ifdef guard. The same
|
||||
* instruments compile and run, but recording is a no-op.
|
||||
* @code
|
||||
* auto collector = beast::insight::OTelCollector::New(
|
||||
* endpoint, prefix, instanceId, serviceName, networkType, journal);
|
||||
* auto gauge = collector->makeGauge("peers", "count");
|
||||
* gauge = 42; // silently discarded when XRPL_ENABLE_TELEMETRY is off
|
||||
* @endcode
|
||||
*
|
||||
* @note Thread safety: instrument recording (Counter::Add,
|
||||
* Histogram::Record, and atomic gauge writes) is thread-safe and
|
||||
* may be called concurrently. Instrument *creation* (make_counter,
|
||||
* make_gauge, etc.) is serialized by an internal mutex. Hook and
|
||||
* observable-gauge callbacks run on the SDK's collection thread, so
|
||||
* any state they read must itself be thread-safe.
|
||||
* @note Limitations: metrics export over OTLP/HTTP only (no gRPC); the
|
||||
* PeriodicMetricReader interval is fixed at 1s; and gauge values
|
||||
* are stored as int64_t, so fractional gauges are truncated.
|
||||
*
|
||||
* @see StatsDCollector for the StatsD-based alternative.
|
||||
* @see NullCollector for the no-op fallback.
|
||||
*/
|
||||
class OTelCollector : public Collector
|
||||
{
|
||||
public:
|
||||
explicit OTelCollector() = default;
|
||||
|
||||
/**
|
||||
* @brief Factory method to create an OTelCollector instance.
|
||||
*
|
||||
* When XRPL_ENABLE_TELEMETRY is defined, creates a real OTel-backed
|
||||
* collector that exports metrics via OTLP/HTTP. When telemetry is
|
||||
* disabled at compile time, returns a NullCollector.
|
||||
*
|
||||
* @param endpoint OTLP/HTTP metrics endpoint URL
|
||||
* (e.g. "http://localhost:4318/v1/metrics").
|
||||
* @param prefix Prefix prepended to all metric names
|
||||
* (e.g. "xrpld").
|
||||
* @param instanceId Unique identifier for this node instance,
|
||||
* emitted as the `service.instance.id` OTel
|
||||
* resource attribute. Defaults to empty string
|
||||
* (attribute omitted when empty).
|
||||
* @param serviceName Value for the `service.name` OTel resource
|
||||
* attribute. When empty, defaults to "xrpld".
|
||||
* Matches the trace exporter's service.name so
|
||||
* metrics and traces share one service identity.
|
||||
* @param networkType Value for the `xrpl.network.type` OTel resource
|
||||
* attribute (e.g. "mainnet"). When empty, the
|
||||
* attribute is omitted. Supplied by the node so
|
||||
* metrics carry the same network label as traces.
|
||||
* @param journal Journal for logging.
|
||||
* @return Shared pointer to the created Collector.
|
||||
*/
|
||||
static std::shared_ptr<Collector>
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
New(std::string const& endpoint,
|
||||
std::string const& prefix,
|
||||
std::string const& instanceId,
|
||||
std::string const& serviceName,
|
||||
std::string const& networkType,
|
||||
Journal journal);
|
||||
};
|
||||
|
||||
} // namespace beast::insight
|
||||
@@ -23,6 +23,7 @@ typeName()
|
||||
if (auto s = abi::__cxa_demangle(name.c_str(), nullptr, nullptr, nullptr))
|
||||
{
|
||||
name = s;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-no-malloc)
|
||||
std::free(s);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -24,6 +24,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;
|
||||
@@ -224,6 +227,9 @@ public:
|
||||
virtual perf::PerfLog&
|
||||
getPerfLog() = 0;
|
||||
|
||||
virtual telemetry::Telemetry&
|
||||
getTelemetry() = 0;
|
||||
|
||||
// Configuration and state
|
||||
[[nodiscard]] virtual bool
|
||||
isStopping() const = 0;
|
||||
|
||||
@@ -34,11 +34,11 @@ public:
|
||||
createObject();
|
||||
|
||||
private:
|
||||
bool success_;
|
||||
bool success_{false};
|
||||
|
||||
void const* key_;
|
||||
NodeObjectType objectType_;
|
||||
unsigned char const* objectData_;
|
||||
NodeObjectType objectType_{NodeObjectType::Unknown};
|
||||
unsigned char const* objectData_{nullptr};
|
||||
int dataBytes_;
|
||||
};
|
||||
|
||||
|
||||
@@ -85,6 +85,30 @@ 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
|
||||
// TODO: trace_state is reserved for W3C tracestate vendor-specific
|
||||
// key-value pairs but is not yet read or written by
|
||||
// TraceContextPropagator. Wire it when cross-vendor trace
|
||||
// propagation is needed.
|
||||
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 +125,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 +176,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 +224,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
|
||||
@@ -246,15 +279,7 @@ message TMGetObjectByHash {
|
||||
|
||||
message TMLedgerNode {
|
||||
required bytes nodedata = 1;
|
||||
|
||||
// Used when protocol version <2.3. Not set for ledger base data.
|
||||
optional bytes nodeid = 2;
|
||||
|
||||
// Used when protocol version >=2.3. Neither value is set for ledger base data.
|
||||
oneof reference {
|
||||
bytes id = 3; // Set for inner nodes.
|
||||
uint32 depth = 4; // Set for leaf nodes.
|
||||
}
|
||||
optional bytes nodeid = 2; // missing for ledger base data
|
||||
}
|
||||
|
||||
enum TMLedgerInfoType {
|
||||
|
||||
@@ -240,6 +240,9 @@ private:
|
||||
|
||||
inline STPathElement::STPathElement() : type_(TypeNone), isOffer_(true)
|
||||
{
|
||||
// hashValue_ is derived from the whole object, so it is computed in the body
|
||||
// once every other member is initialized (as in the other constructors).
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
|
||||
hashValue_ = getHash(*this);
|
||||
}
|
||||
|
||||
@@ -315,6 +318,9 @@ inline STPathElement::STPathElement(
|
||||
assetID_.visit(
|
||||
[&](Currency const&) { type_ = type_ & (~Type::TypeMpt); },
|
||||
[&](MPTID const&) { type_ = type_ & (~Type::TypeCurrency); });
|
||||
// hashValue_ must be computed after type_ is adjusted above, so this cannot
|
||||
// be a member initializer.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
|
||||
hashValue_ = getHash(*this);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,14 +50,13 @@ public:
|
||||
STVar&
|
||||
operator=(STVar&& rhs);
|
||||
|
||||
STVar(STBase&& t) // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
|
||||
STVar(STBase&& t) : p_(t.move(kMaxSize, &d_))
|
||||
{
|
||||
p_ = t.move(kMaxSize, &d_);
|
||||
}
|
||||
|
||||
STVar(STBase const& t)
|
||||
STVar(STBase const& t) : p_(t.copy(kMaxSize, &d_))
|
||||
{
|
||||
p_ = t.copy(kMaxSize, &d_);
|
||||
}
|
||||
|
||||
STVar(DefaultObjectT, SField const& name);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/IntrusivePointer.h>
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -87,17 +88,6 @@ enum class SHAMapState {
|
||||
|
||||
See https://en.wikipedia.org/wiki/Merkle_tree
|
||||
*/
|
||||
|
||||
/** Holds a SHAMap node's identity, serialized data, and leaf status.
|
||||
Used by getNodeFat to return node data for peer synchronization.
|
||||
*/
|
||||
struct SHAMapNodeData
|
||||
{
|
||||
SHAMapNodeID nodeID;
|
||||
bool isLeaf;
|
||||
Blob data; // Placed last, so `isLeaf` can fit into the alignment padding of `nodeID`.
|
||||
};
|
||||
|
||||
class SHAMap
|
||||
{
|
||||
private:
|
||||
@@ -275,10 +265,10 @@ public:
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter);
|
||||
|
||||
[[nodiscard]] bool
|
||||
bool
|
||||
getNodeFat(
|
||||
SHAMapNodeID const& wanted,
|
||||
std::vector<SHAMapNodeData>& data,
|
||||
std::vector<std::pair<SHAMapNodeID, Blob>>& data,
|
||||
bool fatLeaves,
|
||||
std::uint32_t depth) const;
|
||||
|
||||
@@ -305,43 +295,10 @@ public:
|
||||
void
|
||||
serializeRoot(Serializer& s) const;
|
||||
|
||||
/** Add a root node to the SHAMap during synchronization.
|
||||
*
|
||||
* This function is used when receiving the root node of a SHAMap from a peer during ledger
|
||||
* synchronization. The node must already have been deserialized.
|
||||
*
|
||||
* @param hash The expected hash of the root node.
|
||||
* @param rootNode A deserialized root node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
* @return Status indicating whether the node was useful, duplicate, or invalid.
|
||||
*
|
||||
* @note This function expects the rootNode to be a valid, deserialized SHAMapTreeNode. The
|
||||
* caller is responsible for deserialization and basic validation before calling this
|
||||
* function.
|
||||
*/
|
||||
SHAMapAddNode
|
||||
addRootNode(SHAMapHash const& hash, SHAMapTreeNodePtr rootNode, SHAMapSyncFilter const* filter);
|
||||
|
||||
/** Add a known node at a specific position in the SHAMap during synchronization.
|
||||
*
|
||||
* This function is used when receiving nodes from peers during ledger synchronization. The node
|
||||
* is inserted at the position specified by nodeID. The node must already have been
|
||||
* deserialized.
|
||||
*
|
||||
* @param nodeID The position in the tree where this node belongs.
|
||||
* @param treeNode A deserialized tree node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
* @return Status indicating whether the node was useful, duplicate, or invalid.
|
||||
*
|
||||
* @note This function expects the treeNode to be a valid, deserialized SHAMapTreeNode. The
|
||||
* caller is responsible for deserialization and basic validation before calling this
|
||||
* function. This also means that the nodeID must be consistent with the node's content.
|
||||
*/
|
||||
addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFilter const* filter);
|
||||
SHAMapAddNode
|
||||
addKnownNode(
|
||||
SHAMapNodeID const& nodeID,
|
||||
SHAMapTreeNodePtr treeNode,
|
||||
SHAMapSyncFilter const* filter);
|
||||
addKnownNode(SHAMapNodeID const& nodeID, Slice const& rawNode, SHAMapSyncFilter const* filter);
|
||||
|
||||
// status functions
|
||||
void
|
||||
|
||||
@@ -138,6 +138,7 @@ intrusive_ptr_release(SHAMapItem const* x)
|
||||
|
||||
// If the slabber doesn't claim this pointer, it was allocated
|
||||
// manually, so we free it manually.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
|
||||
if (!detail::gSlabber.deallocate(const_cast<std::uint8_t*>(p)))
|
||||
delete[] p;
|
||||
}
|
||||
|
||||
@@ -507,6 +507,9 @@ TaggedPointer::operator=(TaggedPointer&& other)
|
||||
[[nodiscard]] inline std::pair<std::uint8_t, void*>
|
||||
TaggedPointer::decode() const
|
||||
{
|
||||
// tp_ packs a raw pointer together with the tag bits; recovering the
|
||||
// pointer inherently requires an integer-to-pointer cast.
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr)
|
||||
return {tp_ & kTagMask, reinterpret_cast<void*>(tp_ & kPtrMask)};
|
||||
}
|
||||
|
||||
@@ -535,6 +538,9 @@ TaggedPointer::getHashesAndChildren() const
|
||||
[[nodiscard]] inline SHAMapHash*
|
||||
TaggedPointer::getHashes() const
|
||||
{
|
||||
// tp_ packs a raw pointer together with the tag bits; recovering the
|
||||
// pointer inherently requires an integer-to-pointer cast.
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr)
|
||||
return reinterpret_cast<SHAMapHash*>(tp_ & kPtrMask);
|
||||
};
|
||||
|
||||
|
||||
89
include/xrpl/telemetry/DiscardFlag.h
Normal file
89
include/xrpl/telemetry/DiscardFlag.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
/** Thread-local discard signaling between SpanGuard and the span processor.
|
||||
|
||||
SpanGuard::discard() wants to drop a span without sending it to the
|
||||
exporter. The OTel SDK calls SpanProcessor::OnEnd() synchronously on the
|
||||
same thread that calls Span::End(), so a thread-local flag set just before
|
||||
End() and read inside OnEnd() lets FilteringSpanProcessor drop the span
|
||||
before it enters the batch export queue.
|
||||
|
||||
This side-channel avoids inspecting the Recordable's internals (which vary
|
||||
by exporter type — SpanData vs OtlpRecordable).
|
||||
|
||||
The flag is a *private* thread-local member of DiscardScope, mutated only by
|
||||
its constructor and destructor. This gives real access control rather than a
|
||||
naming convention: no code that includes this header can flip the flag
|
||||
directly — it can only enter a DiscardScope (which sets and clears the flag
|
||||
over its own lifetime) and observe the state via DiscardScope::isActive().
|
||||
Binding set/clear to a scope also means the flag cannot leak onto the next
|
||||
span even if End() were to throw.
|
||||
|
||||
Kept in a separate header to avoid transitive include bloat: SpanGuard.h
|
||||
only needs this signaling, not the full Telemetry.h with BasicConfig/Journal.
|
||||
|
||||
Usage:
|
||||
@code
|
||||
// In SpanGuard::discard():
|
||||
{
|
||||
DiscardScope discardScope; // flag set for this scope only
|
||||
span->End(); // OnEnd() runs synchronously, sees flag
|
||||
} // flag cleared here, unconditionally
|
||||
|
||||
// In FilteringSpanProcessor::OnEnd():
|
||||
if (DiscardScope::isActive())
|
||||
return; // drop the span
|
||||
@endcode
|
||||
|
||||
@note Thread safety: the flag is thread-local, so each thread observes only
|
||||
its own discard signal — no synchronization is required.
|
||||
|
||||
@see SpanGuard::discard(), FilteringSpanProcessor (Telemetry.cpp)
|
||||
*/
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/** RAII guard that marks the current thread's span for discard.
|
||||
|
||||
Sets the thread-local discard flag on construction and clears it on
|
||||
destruction, so a span ended within the guard's scope is dropped by
|
||||
FilteringSpanProcessor::OnEnd() while the flag stays confined to that scope.
|
||||
The flag is private and mutated only here, so no other code can set it.
|
||||
Non-copyable and non-movable — its sole purpose is the scoped flag lifetime.
|
||||
*/
|
||||
class DiscardScope
|
||||
{
|
||||
public:
|
||||
DiscardScope() noexcept
|
||||
{
|
||||
discarding = true;
|
||||
}
|
||||
|
||||
~DiscardScope()
|
||||
{
|
||||
discarding = false;
|
||||
}
|
||||
|
||||
DiscardScope(DiscardScope const&) = delete;
|
||||
DiscardScope&
|
||||
operator=(DiscardScope const&) = delete;
|
||||
DiscardScope(DiscardScope&&) = delete;
|
||||
DiscardScope&
|
||||
operator=(DiscardScope&&) = delete;
|
||||
|
||||
/** @return true if the current thread is inside a DiscardScope, i.e. the
|
||||
span ending now should be dropped rather than exported. Read by
|
||||
FilteringSpanProcessor::OnEnd(). */
|
||||
[[nodiscard]] static bool
|
||||
isActive() noexcept
|
||||
{
|
||||
return discarding;
|
||||
}
|
||||
|
||||
private:
|
||||
/** Thread-local discard flag. Private, so only this class's ctor/dtor can
|
||||
mutate it; observers use isActive(). One instance per thread. */
|
||||
inline static thread_local bool discarding = false;
|
||||
};
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
64
include/xrpl/telemetry/Redaction.h
Normal file
64
include/xrpl/telemetry/Redaction.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
/** Account-address redaction for telemetry span attributes.
|
||||
|
||||
Path-finding RPC handlers would otherwise emit the caller's raw
|
||||
account addresses as span attributes. To keep plaintext addresses out
|
||||
of the telemetry backend, they are hashed at the point of emission.
|
||||
This header exposes a single pure helper that turns an address into a
|
||||
short, stable, obfuscated token.
|
||||
|
||||
Data flow:
|
||||
|
||||
handler -> redactAccount(addr) -> span attribute -> OTLP export
|
||||
|
||||
The returned token is the first 16 hex characters (lowercase) of the
|
||||
SHA-512Half digest of the address. It is deterministic (same address
|
||||
always maps to the same token) so operators can still correlate spans
|
||||
for a given account across nodes and restarts.
|
||||
|
||||
The hash is unsalted, so it is obfuscation, not a secrecy guarantee:
|
||||
XRP account addresses are a public, enumerable set, so a determined
|
||||
observer with the telemetry stream could rebuild the address->token
|
||||
mapping. The goal here is to keep plaintext addresses out of traces
|
||||
and dashboards, not to defend against a precomputation attack. A salt
|
||||
is intentionally omitted because it would break cross-node/restart
|
||||
correlation, which is the reason for hashing rather than dropping.
|
||||
|
||||
A second, independent hashing layer runs in the OpenTelemetry
|
||||
Collector (an `attributes/hash` processor) as defense-in-depth for
|
||||
any node that emits a raw value.
|
||||
|
||||
@note This function is pure and reentrant: it holds no global state,
|
||||
performs no I/O, and is safe to call concurrently from any thread.
|
||||
|
||||
Usage example:
|
||||
@code
|
||||
#include <xrpl/telemetry/Redaction.h>
|
||||
using namespace xrpl::telemetry;
|
||||
|
||||
span.setAttribute(
|
||||
pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
|
||||
@endcode
|
||||
|
||||
Edge case (empty input yields empty output):
|
||||
@code
|
||||
assert(redactAccount("") == "");
|
||||
@endcode
|
||||
*/
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/** Hash an account address into a short, stable, obfuscated token.
|
||||
|
||||
@param addr The account address to redact (e.g. an r-address).
|
||||
@return The first 16 lowercase hex characters of sha512Half(addr),
|
||||
or an empty string when @p addr is empty.
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
redactAccount(std::string_view addr);
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
571
include/xrpl/telemetry/SpanGuard.h
Normal file
571
include/xrpl/telemetry/SpanGuard.h
Normal file
@@ -0,0 +1,571 @@
|
||||
#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, commandName);
|
||||
span.setAttribute(rpc_span::attr::command, commandName);
|
||||
span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::success);
|
||||
// span ended automatically on scope exit
|
||||
@endcode
|
||||
|
||||
2. Error recording:
|
||||
@code
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, rpc_span::prefix::command, commandName);
|
||||
try {
|
||||
doWork();
|
||||
span.setOk();
|
||||
} catch (std::exception const& e) {
|
||||
span.recordException(e);
|
||||
}
|
||||
@endcode
|
||||
|
||||
3. Cross-thread context propagation:
|
||||
@code
|
||||
#include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
using namespace xrpl::telemetry;
|
||||
|
||||
// Thread A: create span and capture context
|
||||
auto span = SpanGuard::span(
|
||||
TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process);
|
||||
auto ctx = span.captureContext();
|
||||
|
||||
// Thread B: create child with captured context
|
||||
auto child = SpanGuard::childSpan(rpc_span::op::process, 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 <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace protocol {
|
||||
class TraceContext;
|
||||
} // namespace protocol
|
||||
|
||||
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};
|
||||
};
|
||||
|
||||
/** Key-value pair for span event attributes.
|
||||
Used by addEvent(name, attrs) to attach structured metadata to events.
|
||||
*/
|
||||
using EventAttribute = std::pair<std::string_view, std::string_view>;
|
||||
|
||||
/** 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).
|
||||
@param followsFrom Optional captured context to attach as a
|
||||
follows-from link. Use to stitch sequential
|
||||
top-level spans (e.g. consecutive consensus
|
||||
rounds). Ignored if nullptr or invalid.
|
||||
*/
|
||||
static SpanGuard
|
||||
hashSpan(
|
||||
TraceCategory const cat,
|
||||
std::string_view const name,
|
||||
std::uint8_t const* const hashData,
|
||||
std::size_t const hashSize,
|
||||
SpanContext const* followsFrom = nullptr);
|
||||
|
||||
/** 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;
|
||||
|
||||
/** Inject the calling thread's currently-active OTel context into a
|
||||
protobuf TraceContext message for cross-node propagation.
|
||||
|
||||
Encapsulates `RuntimeContext::GetCurrent()` + `injectToProtobuf`
|
||||
so callers in app-layer code (e.g. RCLConsensus broadcasting
|
||||
TMProposeSet / TMValidation) don't depend on any OTel headers.
|
||||
No-op if telemetry is disabled or no span is active.
|
||||
|
||||
@param proto The protobuf TraceContext to populate.
|
||||
*/
|
||||
static void
|
||||
injectCurrentContextToProtobuf(protocol::TraceContext& proto);
|
||||
|
||||
// --- 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);
|
||||
|
||||
/** Add a named event with key-value attributes to the span's timeline.
|
||||
No-op on a null guard.
|
||||
@param name Event name.
|
||||
@param attrs Attribute pairs (all string_view for simplicity).
|
||||
*/
|
||||
void
|
||||
addEvent(std::string_view name, std::initializer_list<EventAttribute> attrs);
|
||||
|
||||
/** 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,
|
||||
SpanContext const* = nullptr)
|
||||
{
|
||||
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 {};
|
||||
}
|
||||
|
||||
static void
|
||||
injectCurrentContextToProtobuf(protocol::TraceContext&)
|
||||
{
|
||||
}
|
||||
// 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
|
||||
addEvent(std::string_view, std::initializer_list<EventAttribute>)
|
||||
{
|
||||
}
|
||||
void
|
||||
recordException(std::exception const&)
|
||||
{
|
||||
}
|
||||
void
|
||||
discard()
|
||||
{
|
||||
}
|
||||
|
||||
explicit
|
||||
operator bool() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
143
include/xrpl/telemetry/SpanNames.h
Normal file
143
include/xrpl/telemetry/SpanNames.h
Normal file
@@ -0,0 +1,143 @@
|
||||
#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"));
|
||||
|
||||
/// 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");
|
||||
|
||||
/// Shared close-time attrs — bare names, reused by consensus and ledger.
|
||||
inline constexpr auto closeTime = makeStr("close_time");
|
||||
inline constexpr auto closeTimeCorrect = makeStr("close_time_correct");
|
||||
inline constexpr auto closeResolutionMs = makeStr("close_resolution_ms");
|
||||
/// Shared validation attrs — reused by the consensus and peer validation
|
||||
/// spans. Same concept, same key on every span; the span name tells them
|
||||
/// apart, so neither is emitter-prefixed. `ledgerHash` is a ledger-object
|
||||
/// property (bare, like ledgerSeq); `fullValidation` is the is-full-validation
|
||||
/// flag. Never the dotted xrpl. form (reserved for resource attrs).
|
||||
inline constexpr auto ledgerHash = makeStr("ledger_hash");
|
||||
inline constexpr auto fullValidation = makeStr("full_validation");
|
||||
} // namespace attr
|
||||
|
||||
// ===== Shared attribute values =============================================
|
||||
|
||||
namespace attr_val {
|
||||
inline constexpr auto success = makeStr("success");
|
||||
inline constexpr auto error = makeStr("error");
|
||||
} // namespace attr_val
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
402
include/xrpl/telemetry/Telemetry.h
Normal file
402
include/xrpl/telemetry/Telemetry.h
Normal file
@@ -0,0 +1,402 @@
|
||||
#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::Rpc, rpc_span::prefix::rpc, rpc_span::op::process);
|
||||
{
|
||||
auto child = parent.childSpan(rpc_span::op::process);
|
||||
child.setAttribute(rpc_span::attr::version, apiVersion);
|
||||
// child ends here
|
||||
}
|
||||
@endcode
|
||||
|
||||
3. Unrelated span (cross-scope, same thread):
|
||||
@code
|
||||
// gRPC and RPC handlers can be active simultaneously
|
||||
auto grpcSpan = SpanGuard::span(
|
||||
TraceCategory::Rpc, grpc_span::prefix::grpc, grpc_span::attr::method);
|
||||
auto rpcSpan = SpanGuard::span(
|
||||
TraceCategory::Rpc, rpc_span::prefix::command, commandName);
|
||||
// both spans end on scope exit
|
||||
@endcode
|
||||
|
||||
4. Cross-thread context propagation:
|
||||
@code
|
||||
// Thread A: 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(rpc_span::op::process, ctx);
|
||||
@endcode
|
||||
|
||||
@note Thread safety: The Telemetry interface is safe for concurrent reads
|
||||
(isEnabled, shouldTrace*, getTracer, startSpan) after start() completes.
|
||||
setServiceInstanceId() must be called before start() and is not thread-safe.
|
||||
The OTel SDK's TracerProvider and Tracer are internally thread-safe.
|
||||
*/
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/metrics/meter.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#include <opentelemetry/trace/tracer.h>
|
||||
#endif
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
/** OTel instrumentation scope (tracer) name. Identifies this library as the
|
||||
source of spans; distinct from the `service.name` resource attribute
|
||||
(Setup::serviceName), which is config-overridable. */
|
||||
inline constexpr std::string_view kTracerName{"xrpld"};
|
||||
|
||||
/** OTel instrumentation scope (meter) name. Identifies this library as the
|
||||
source of metrics; symmetric with kTracerName for the tracing side. */
|
||||
inline constexpr std::string_view kMeterName{"xrpld"};
|
||||
|
||||
/** OTel instrumentation scope version reported for the meter. */
|
||||
inline constexpr std::string_view kMeterVersion{"1.0.0"};
|
||||
#endif
|
||||
|
||||
class Telemetry
|
||||
{
|
||||
/** Global singleton pointer, set by start()/stop() in the active
|
||||
implementation. Allows SpanGuard factory methods to access the
|
||||
Telemetry instance without callers passing it explicitly.
|
||||
|
||||
Atomic with acquire/release ordering: start()/stop() store on
|
||||
the initialization thread, factory methods load on worker threads.
|
||||
@see setInstance(), getInstance()
|
||||
*/
|
||||
inline static std::atomic<Telemetry*> instance{nullptr};
|
||||
|
||||
public:
|
||||
/** Get the global Telemetry instance.
|
||||
@return Pointer to the active instance, or nullptr if not started.
|
||||
*/
|
||||
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;
|
||||
|
||||
/** Path to this node's client certificate (PEM), presented to the
|
||||
collector for mutual TLS. Empty disables client-side auth, in
|
||||
which case only server (one-way) TLS is used. */
|
||||
std::string tlsClientCertPath;
|
||||
|
||||
/** Path to the private key (PEM) for tlsClientCertPath. Required
|
||||
whenever tlsClientCertPath is set. */
|
||||
std::string tlsClientKeyPath;
|
||||
|
||||
/** 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().
|
||||
*/
|
||||
static constexpr double samplingRatio = 1.0;
|
||||
|
||||
/** Maximum number of spans per batch export. */
|
||||
std::uint32_t batchSize = 512;
|
||||
|
||||
/** Delay between batch exports. */
|
||||
std::chrono::milliseconds batchDelay = std::chrono::milliseconds{5000};
|
||||
|
||||
/** Maximum number of spans queued before dropping. */
|
||||
std::uint32_t maxQueueSize = 2048;
|
||||
|
||||
/** Network identifier, added as an OTel resource attribute. */
|
||||
std::uint32_t networkId = 0;
|
||||
|
||||
/** Network type label (e.g. "mainnet", "testnet", "devnet"). */
|
||||
std::string networkType = "mainnet";
|
||||
|
||||
/** Enable tracing for transaction processing. */
|
||||
bool traceTransactions = true;
|
||||
|
||||
/** Enable tracing for consensus rounds. */
|
||||
bool traceConsensus = true;
|
||||
|
||||
/** Enable tracing for RPC request handling. */
|
||||
bool traceRpc = true;
|
||||
|
||||
/** Enable tracing for peer-to-peer messages (enabled by default;
|
||||
high volume). */
|
||||
bool tracePeer = true;
|
||||
|
||||
/** Enable tracing for ledger close/accept. */
|
||||
bool traceLedger = true;
|
||||
|
||||
/** Strategy for cross-node consensus trace correlation.
|
||||
"deterministic" — derive trace_id from ledger hash so all
|
||||
validators in the same round share the same trace_id.
|
||||
"attribute" — random trace_id, correlate via ledger_id attribute.
|
||||
*/
|
||||
std::string consensusTraceStrategy = "deterministic";
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
/** @return The configured consensus trace correlation strategy. */
|
||||
[[nodiscard]] virtual std::string const&
|
||||
getConsensusTraceStrategy() 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 = kTracerName) = 0;
|
||||
|
||||
/** Get or create a named meter instance.
|
||||
|
||||
Returns the raw OTel Meter, giving developers direct access to the
|
||||
full metrics API. From the returned Meter any instrument type can be
|
||||
created: Counter, UpDownCounter, Gauge, and Histogram (synchronous),
|
||||
plus their observable/async variants (ObservableCounter,
|
||||
ObservableUpDownCounter, ObservableGauge).
|
||||
|
||||
@param name Meter name used to identify the instrumentation scope.
|
||||
@return A shared pointer to the Meter.
|
||||
*/
|
||||
virtual opentelemetry::nostd::shared_ptr<opentelemetry::metrics::Meter>
|
||||
getMeter(std::string_view name = kMeterName) = 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
|
||||
makeTelemetrySetup(
|
||||
Section const& section,
|
||||
std::string const& nodePublicKey,
|
||||
std::string const& version,
|
||||
std::uint32_t networkId);
|
||||
|
||||
/** Derive a human-readable network type label from the numeric network ID.
|
||||
|
||||
Shared by the trace and metric export paths so both stamp the same
|
||||
`xrpl.network.type` resource attribute value.
|
||||
|
||||
@param networkId The network identifier from [network_id] config.
|
||||
@return "mainnet" (0), "testnet" (1), "devnet" (2), or "unknown".
|
||||
*/
|
||||
std::string
|
||||
networkTypeFromId(std::uint32_t networkId);
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
112
include/xrpl/telemetry/TraceContextPropagator.h
Normal file
112
include/xrpl/telemetry/TraceContextPropagator.h
Normal file
@@ -0,0 +1,112 @@
|
||||
#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, TMProposeSet, and TMValidation messages.
|
||||
|
||||
Only compiled when XRPL_ENABLE_TELEMETRY is defined.
|
||||
|
||||
@see PropagationHelpers.h (high-level inject helpers),
|
||||
TxTracing.h (transaction receive-side extraction),
|
||||
ConsensusReceiveTracing.h (proposal/validation receive-side).
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/proto/xrpl.pb.h>
|
||||
#include <xrpl/telemetry/TraceContextValidation.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>
|
||||
|
||||
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());
|
||||
|
||||
// TODO(observability/secure-OTel): the protobuf TraceContext message
|
||||
// also carries `trace_state` (field 4), which is currently neither
|
||||
// populated here nor read by extractFromProtobuf above. The field is
|
||||
// reserved for the secure tracing pipeline outlined in
|
||||
// OpenTelemetryPlan/secure-OTel.md, where an authenticated token in
|
||||
// tracestate will let receivers reject spoofed/poisoned trace context.
|
||||
// Wire trace_state through inject/extract once the consumer lands.
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
92
include/xrpl/telemetry/TraceContextValidation.h
Normal file
92
include/xrpl/telemetry/TraceContextValidation.h
Normal file
@@ -0,0 +1,92 @@
|
||||
#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 <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
|
||||
114
include/xrpl/tx/detail/TxApplySpanNames.h
Normal file
114
include/xrpl/tx/detail/TxApplySpanNames.h
Normal file
@@ -0,0 +1,114 @@
|
||||
#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() also uses hashSpan on the same txID so it
|
||||
* // co-traces with preflight and preclaim under one trace_id:
|
||||
* auto span = SpanGuard::hashSpan(
|
||||
* TraceCategory::Transactions, tx_apply_span::transactor,
|
||||
* txID.data(), txID.kBytes);
|
||||
* 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);
|
||||
/// "tx.transactor" — full name for hashSpan() at the apply stage. Shares the
|
||||
/// txID-derived trace_id so it co-traces with tx.preflight and tx.preclaim.
|
||||
inline constexpr auto transactor = join(seg::tx, op::transactor);
|
||||
|
||||
// ===== 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
|
||||
@@ -92,6 +92,7 @@ public:
|
||||
[[nodiscard]] TOffer<TIn, TOut>&
|
||||
tip() const
|
||||
{
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
|
||||
return const_cast<TOfferStreamBase*>(this)->offer_;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ undefined:nudb
|
||||
# Snappy compression library intentional overflows
|
||||
unsigned-integer-overflow:snappy.cc
|
||||
|
||||
# OpenTelemetry SDK intentional overflows in hash functions
|
||||
unsigned-integer-overflow:opentelemetry/sdk/common/attributemap_hash.h
|
||||
unsigned-integer-overflow:opentelemetry
|
||||
|
||||
# Abseil intentional overflows in hashing, RNG and time arithmetic.
|
||||
# Matched at library scope (like boost above): the wraparound is by design
|
||||
# across many absl files (hash mixing, raw_hash_set probing, duration math,
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
#include <opentelemetry/context/runtime_context.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/nostd/span.h>
|
||||
#include <opentelemetry/nostd/variant.h>
|
||||
#include <opentelemetry/trace/span.h>
|
||||
#include <opentelemetry/trace/span_metadata.h>
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
@@ -291,6 +300,34 @@ Logs::format(
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
// Inject OTel trace context when an active span exists on this thread.
|
||||
// Checks the thread-local context value directly to avoid the heap
|
||||
// allocation that GetSpan() performs on the no-span path.
|
||||
{
|
||||
auto context = opentelemetry::context::RuntimeContext::GetCurrent();
|
||||
auto spanValue = context.GetValue(opentelemetry::trace::kSpanKey);
|
||||
if (opentelemetry::nostd::holds_alternative<
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>>(spanValue))
|
||||
{
|
||||
auto span = opentelemetry::nostd::get<
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>>(spanValue);
|
||||
auto spanCtx = span->GetContext();
|
||||
if (spanCtx.IsValid())
|
||||
{
|
||||
char traceId[32], spanId[16];
|
||||
spanCtx.trace_id().ToLowerBase16(opentelemetry::nostd::span<char, 32>{traceId});
|
||||
spanCtx.span_id().ToLowerBase16(opentelemetry::nostd::span<char, 16>{spanId});
|
||||
output += "trace_id=";
|
||||
output.append(traceId, 32);
|
||||
output += " span_id=";
|
||||
output.append(spanId, 16);
|
||||
output += ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
|
||||
output += message;
|
||||
|
||||
// Limit the maximum length of the output
|
||||
|
||||
891
src/libxrpl/beast/insight/OTelCollector.cpp
Normal file
891
src/libxrpl/beast/insight/OTelCollector.cpp
Normal file
@@ -0,0 +1,891 @@
|
||||
/**
|
||||
* @file OTelCollector.cpp
|
||||
* @brief OpenTelemetry Metrics SDK implementation of beast::insight::Collector.
|
||||
*
|
||||
* Compiled only when XRPL_ENABLE_TELEMETRY is defined (via CMake
|
||||
* telemetry=ON). Maps beast::insight instruments to OTel SDK instruments
|
||||
* created on the GLOBAL Meter published by the telemetry module. This class
|
||||
* is a legacy shim: it no longer owns an export pipeline. The MeterProvider,
|
||||
* PeriodicExportingMetricReader, OTLP exporter and histogram view all live in
|
||||
* xrpl::telemetry::Telemetry.
|
||||
*
|
||||
* When XRPL_ENABLE_TELEMETRY is not defined, OTelCollector::New() returns
|
||||
* a NullCollector so the build succeeds without OTel dependencies.
|
||||
*
|
||||
* Data flow:
|
||||
*
|
||||
* beast::insight callers
|
||||
* |
|
||||
* v
|
||||
* OTelCounterImpl / OTelGaugeImpl / OTelEventImpl / OTelMeterImpl
|
||||
* | | | |
|
||||
* v v v v
|
||||
* Counter<uint64_t> ObservableGauge Histogram<double> Counter<uint64_t>
|
||||
* | | | |
|
||||
* +--------------------+----------------+--------------+
|
||||
* |
|
||||
* v
|
||||
* GLOBAL Meter (from metrics::Provider::GetMeterProvider())
|
||||
* |
|
||||
* v
|
||||
* telemetry-owned PeriodicMetricReader (1s) -> OTLP exporter -> Prometheus
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/beast/insight/OTelCollector.h>
|
||||
|
||||
#include <xrpl/beast/insight/Collector.h>
|
||||
#include <xrpl/beast/insight/CounterImpl.h>
|
||||
#include <xrpl/beast/insight/EventImpl.h>
|
||||
#include <xrpl/beast/insight/GaugeImpl.h>
|
||||
#include <xrpl/beast/insight/Hook.h>
|
||||
#include <xrpl/beast/insight/HookImpl.h>
|
||||
#include <xrpl/beast/insight/MeterImpl.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
|
||||
#include <opentelemetry/metrics/async_instruments.h>
|
||||
#include <opentelemetry/metrics/meter.h>
|
||||
#include <opentelemetry/metrics/meter_provider.h>
|
||||
#include <opentelemetry/metrics/observer_result.h>
|
||||
#include <opentelemetry/metrics/provider.h>
|
||||
#include <opentelemetry/metrics/sync_instruments.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/nostd/unique_ptr.h>
|
||||
#include <opentelemetry/nostd/variant.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace beast::insight {
|
||||
|
||||
namespace detail {
|
||||
|
||||
namespace metrics_api = opentelemetry::metrics;
|
||||
|
||||
class OTelCollectorImp;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief OTel-backed implementation of beast::insight::HookImpl.
|
||||
*
|
||||
* Stores a handler function that is invoked during each periodic
|
||||
* metric collection cycle. This mirrors the StatsDHookImpl pattern
|
||||
* where hooks are called at each 1-second timer tick, but here the
|
||||
* invocation is triggered by the OTel PeriodicMetricReader's
|
||||
* observable callback mechanism.
|
||||
*/
|
||||
class OTelHookImpl : public HookImpl
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @param handler Callback invoked at each collection interval.
|
||||
* @param impl Owning collector (prevents premature destruction).
|
||||
*/
|
||||
OTelHookImpl(HandlerType handler, std::shared_ptr<OTelCollectorImp> impl);
|
||||
|
||||
~OTelHookImpl() override;
|
||||
|
||||
OTelHookImpl&
|
||||
operator=(OTelHookImpl const&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Invoke the stored handler.
|
||||
*
|
||||
* Called by the collector during observable gauge callbacks to give
|
||||
* metric producers a chance to update gauge values before export.
|
||||
*/
|
||||
void
|
||||
callHandler();
|
||||
|
||||
private:
|
||||
/** Owning collector. Prevents collector destruction while hook alive. */
|
||||
std::shared_ptr<OTelCollectorImp> impl_;
|
||||
|
||||
/** User-supplied handler called at each collection interval. */
|
||||
HandlerType handler_;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief OTel-backed implementation of beast::insight::CounterImpl.
|
||||
*
|
||||
* Wraps an OTel Counter<uint64_t> instrument. Each increment() call
|
||||
* is forwarded directly to the OTel counter's Add() method. The
|
||||
* PeriodicMetricReader collects and exports the accumulated delta.
|
||||
*
|
||||
* Thread safety: OTel Counter::Add() is thread-safe by specification.
|
||||
*/
|
||||
class OTelCounterImpl : public CounterImpl
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @param name Export-ready metric name, already run through
|
||||
* formatName() by the collector: prefix prepended and
|
||||
* dots replaced with underscores (e.g. "xrpld_rpc_size").
|
||||
* @param meter OTel Meter used to create the counter instrument.
|
||||
*/
|
||||
OTelCounterImpl(
|
||||
std::string const& name,
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const& meter);
|
||||
|
||||
~OTelCounterImpl() override = default;
|
||||
|
||||
OTelCounterImpl&
|
||||
operator=(OTelCounterImpl const&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Add amount to the counter.
|
||||
* @param amount Value to add (must be non-negative for OTel counters).
|
||||
*/
|
||||
void
|
||||
increment(value_type amount) override;
|
||||
|
||||
private:
|
||||
/** OTel synchronous counter instrument. */
|
||||
opentelemetry::nostd::unique_ptr<metrics_api::Counter<uint64_t>> counter_;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief OTel-backed implementation of beast::insight::EventImpl.
|
||||
*
|
||||
* Wraps an OTel Histogram<double> instrument. Each notify() call
|
||||
* records the duration in milliseconds. Uses explicit bucket boundaries
|
||||
* matching the SpanMetrics connector configuration:
|
||||
* [1, 5, 10, 25, 50, 100, 250, 500, 1000, 5000] ms
|
||||
*
|
||||
* Thread safety: OTel Histogram::Record() is thread-safe by specification.
|
||||
*/
|
||||
class OTelEventImpl : public EventImpl
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @param name Export-ready metric name, already run through
|
||||
* formatName() by the collector: prefix prepended and
|
||||
* dots replaced with underscores (e.g. "xrpld_rpc_size").
|
||||
* @param meter OTel Meter used to create the histogram instrument.
|
||||
*/
|
||||
OTelEventImpl(
|
||||
std::string const& name,
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const& meter);
|
||||
|
||||
~OTelEventImpl() override = default;
|
||||
|
||||
OTelEventImpl&
|
||||
operator=(OTelEventImpl const&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Record a duration measurement.
|
||||
* @param value Duration in milliseconds.
|
||||
*/
|
||||
void
|
||||
notify(value_type const& value) override;
|
||||
|
||||
private:
|
||||
/** OTel histogram instrument for recording durations. */
|
||||
opentelemetry::nostd::unique_ptr<metrics_api::Histogram<double>> histogram_;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief OTel-backed implementation of beast::insight::GaugeImpl.
|
||||
*
|
||||
* Uses an atomic int64_t to store the current gauge value. The OTel SDK
|
||||
* reads this value via an ObservableGauge async callback during each
|
||||
* collection cycle. The set() and increment() methods update the
|
||||
* atomic value without blocking the collection thread.
|
||||
*
|
||||
* Design note: OTel gauges are asynchronous (observable) instruments.
|
||||
* The SDK calls a registered callback to read the value rather than
|
||||
* accepting push-style updates. We bridge the beast::insight push-style
|
||||
* API to OTel's pull-style API via the atomic variable.
|
||||
*
|
||||
* Thread safety: std::atomic operations are lock-free on all platforms.
|
||||
*/
|
||||
class OTelGaugeImpl : public GaugeImpl
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @param name Export-ready metric name, already run through
|
||||
* formatName() by the collector: prefix prepended
|
||||
* and dots replaced with underscores.
|
||||
* @param meter OTel Meter used to create the observable gauge.
|
||||
* @param collector Owning collector, used to invoke hooks before reads.
|
||||
*/
|
||||
OTelGaugeImpl(
|
||||
std::string const& name,
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const& meter,
|
||||
std::shared_ptr<OTelCollectorImp> const& collector);
|
||||
|
||||
~OTelGaugeImpl() override;
|
||||
|
||||
/**
|
||||
* @brief Set the gauge to an absolute value.
|
||||
* @param value New gauge value.
|
||||
*/
|
||||
void
|
||||
set(value_type value) override;
|
||||
|
||||
/**
|
||||
* @brief Increment (or decrement) the gauge by a signed amount.
|
||||
*
|
||||
* Clamps the result to [0, INT64_MAX] to match StatsDGaugeImpl
|
||||
* behavior.
|
||||
*
|
||||
* @param amount Signed amount to add to the current value.
|
||||
*/
|
||||
void
|
||||
increment(difference_type amount) override;
|
||||
|
||||
/**
|
||||
* @brief Return the current gauge value for the OTel callback.
|
||||
* @return The most recently set/incremented value.
|
||||
*/
|
||||
int64_t
|
||||
currentValue() const;
|
||||
|
||||
OTelGaugeImpl&
|
||||
operator=(OTelGaugeImpl const&) = delete;
|
||||
|
||||
/** Static callback registered with the OTel SDK observable gauge. */
|
||||
static void
|
||||
gaugeCallback(opentelemetry::metrics::ObserverResult result, void* state);
|
||||
|
||||
private:
|
||||
/** Current gauge value, updated atomically by set()/increment(). */
|
||||
std::atomic<int64_t> value_{0};
|
||||
|
||||
/** OTel observable gauge handle (prevents deregistration). */
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::ObservableInstrument> gauge_;
|
||||
|
||||
/** Owning collector, used to invoke hooks before reading gauge values. */
|
||||
std::shared_ptr<OTelCollectorImp> collector_;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief OTel-backed implementation of beast::insight::MeterImpl.
|
||||
*
|
||||
* Wraps an OTel Counter<uint64_t> instrument. Semantically identical
|
||||
* to Counter but uses unsigned values. The OTel SDK accumulates deltas
|
||||
* and exports them via the PeriodicMetricReader.
|
||||
*
|
||||
* Note: In StatsD, Meter used the non-standard "|m" type which was
|
||||
* silently dropped by the OTel StatsD receiver. With native OTel,
|
||||
* Meter values are properly captured as counter deltas.
|
||||
*
|
||||
* Thread safety: OTel Counter::Add() is thread-safe by specification.
|
||||
*/
|
||||
class OTelMeterImpl : public MeterImpl
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @param name Export-ready metric name, already run through
|
||||
* formatName() by the collector: prefix prepended and
|
||||
* dots replaced with underscores (e.g. "xrpld_rpc_size").
|
||||
* @param meter OTel Meter used to create the counter instrument.
|
||||
*/
|
||||
OTelMeterImpl(
|
||||
std::string const& name,
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const& meter);
|
||||
|
||||
~OTelMeterImpl() override = default;
|
||||
|
||||
OTelMeterImpl&
|
||||
operator=(OTelMeterImpl const&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Add amount to the meter.
|
||||
* @param amount Value to add (unsigned).
|
||||
*/
|
||||
void
|
||||
increment(value_type amount) override;
|
||||
|
||||
private:
|
||||
/** OTel synchronous counter instrument (unsigned). */
|
||||
opentelemetry::nostd::unique_ptr<metrics_api::Counter<uint64_t>> counter_;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Main OTel Collector implementation (legacy shim).
|
||||
*
|
||||
* Obtains its Meter from the GLOBAL MeterProvider owned and published by the
|
||||
* telemetry module (xrpl::telemetry::Telemetry), rather than building its own
|
||||
* export pipeline. Implements all Collector::make_*() factory methods to
|
||||
* create OTel-backed instrument wrappers on that shared Meter.
|
||||
*
|
||||
* The metrics pipeline (MeterProvider + PeriodicExportingMetricReader + OTLP
|
||||
* HTTP exporter + histogram view) lives in the telemetry module. This class is
|
||||
* a thin adapter kept for beast::insight callers during deprecation.
|
||||
*
|
||||
* Class diagram:
|
||||
*
|
||||
* +------------------+ +------------------+
|
||||
* | Collector (ABC) |<-----| OTelCollector |
|
||||
* +------------------+ | (public header) |
|
||||
* ^ +------------------+
|
||||
* | ^
|
||||
* +------------------+ |
|
||||
* | OTelCollectorImp |-------------+
|
||||
* +------------------+
|
||||
* | - journal_ |
|
||||
* | - prefix_ | +--------------------------+
|
||||
* | - otelMeter_ |---->| GLOBAL MeterProvider |
|
||||
* | - hooks_[] | | (owned by Telemetry) |
|
||||
* | - gauges_[] | | + PeriodicReader |
|
||||
* +------------------+ | + OtlpHttpExporter |
|
||||
* +--------------------------+
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. Constructor fetches the Meter from the global MeterProvider.
|
||||
* 2. make_*() methods create instruments on that shared Meter.
|
||||
* 3. The telemetry-owned PeriodicMetricReader collects every 1s, calling
|
||||
* observable callbacks.
|
||||
* 4. Observable callbacks invoke hooks, read gauge atomics.
|
||||
* 5. Destructor only logs; the telemetry module owns pipeline teardown.
|
||||
*
|
||||
* Caveats:
|
||||
* - Observable gauge callbacks run on the SDK's internal thread. Hook
|
||||
* handlers must be thread-safe.
|
||||
* - Metric names are formed as "prefix_name" with dots replaced by
|
||||
* underscores to match StatsD->Prometheus naming conventions.
|
||||
* - The OTel Prometheus exporter appends "_total" to counters. The
|
||||
* metric names we register do NOT include this suffix — Prometheus
|
||||
* adds it automatically.
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* auto collector = OTelCollector::New(
|
||||
* "http://localhost:4318/v1/metrics", "xrpld",
|
||||
* "node-1", "xrpld", "mainnet", journal);
|
||||
* auto counter = collector->makeCounter("rpc.requests");
|
||||
* counter.increment(1);
|
||||
* // Metric "xrpld_rpc_requests" exported via OTLP every 1s.
|
||||
* @endcode
|
||||
*/
|
||||
class OTelCollectorImp : public OTelCollector, public std::enable_shared_from_this<OTelCollectorImp>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the OTel collector over the global MeterProvider.
|
||||
*
|
||||
* @param endpoint OTLP/HTTP metrics endpoint URL. Informational only:
|
||||
* the global telemetry pipeline is authoritative for
|
||||
* the actual export endpoint. Retained for logging and
|
||||
* back-compat with the New() signature.
|
||||
* @param prefix Prefix for all metric names.
|
||||
* @param instanceId Value for the service.instance.id resource attribute.
|
||||
* When empty, the attribute is omitted.
|
||||
* @param serviceName Value for the service.name resource attribute.
|
||||
* When empty, defaults to "xrpld".
|
||||
* @param networkType Value for the xrpl.network.type resource attribute.
|
||||
* When empty, the attribute is omitted.
|
||||
* @param journal Journal for logging.
|
||||
*/
|
||||
OTelCollectorImp(
|
||||
std::string const& endpoint,
|
||||
std::string prefix,
|
||||
std::string const& instanceId,
|
||||
std::string const& serviceName,
|
||||
std::string const& networkType,
|
||||
Journal journal);
|
||||
|
||||
/**
|
||||
* @brief Shut down the MeterProvider, flushing any pending exports.
|
||||
*/
|
||||
~OTelCollectorImp() override;
|
||||
|
||||
/** @name Collector interface implementation */
|
||||
/** @{ */
|
||||
Hook
|
||||
makeHook(HookImpl::HandlerType const& handler) override;
|
||||
|
||||
Counter
|
||||
makeCounter(std::string const& name) override;
|
||||
|
||||
Event
|
||||
makeEvent(std::string const& name) override;
|
||||
|
||||
Gauge
|
||||
makeGauge(std::string const& name) override;
|
||||
|
||||
Meter
|
||||
makeMeter(std::string const& name) override;
|
||||
/** @} */
|
||||
|
||||
/** @name Hook management for observable callbacks */
|
||||
/** @{ */
|
||||
|
||||
/**
|
||||
* @brief Register a hook for periodic invocation.
|
||||
* @param hook Pointer to the hook to register.
|
||||
*/
|
||||
void
|
||||
addHook(OTelHookImpl* hook);
|
||||
|
||||
/**
|
||||
* @brief Unregister a hook.
|
||||
* @param hook Pointer to the hook to unregister.
|
||||
*/
|
||||
void
|
||||
removeHook(OTelHookImpl* hook);
|
||||
|
||||
/**
|
||||
* @brief Invoke all registered hooks.
|
||||
*
|
||||
* Called from observable gauge callbacks before reading gauge values,
|
||||
* so that hook handlers have a chance to update metrics.
|
||||
*/
|
||||
void
|
||||
callHooks();
|
||||
/** @} */
|
||||
|
||||
/** @name Gauge registration for observable callbacks */
|
||||
/** @{ */
|
||||
|
||||
/**
|
||||
* @brief Register a gauge for observable callback reading.
|
||||
* @param gauge Pointer to the gauge to register.
|
||||
*/
|
||||
void
|
||||
addGauge(OTelGaugeImpl* gauge);
|
||||
|
||||
/**
|
||||
* @brief Unregister a gauge.
|
||||
* @param gauge Pointer to the gauge to unregister.
|
||||
*/
|
||||
void
|
||||
removeGauge(OTelGaugeImpl* gauge);
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @brief Get the OTel Meter instance for creating instruments.
|
||||
* @return Shared pointer to the OTel Meter.
|
||||
*/
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const&
|
||||
otelMeter() const;
|
||||
|
||||
/**
|
||||
* @brief Format a metric name with the configured prefix.
|
||||
*
|
||||
* Replaces dots with underscores to match StatsD->Prometheus naming.
|
||||
* Example: prefix="xrpld", name="LedgerMaster.Validated_Ledger_Age"
|
||||
* -> "xrpld_LedgerMaster_Validated_Ledger_Age"
|
||||
*
|
||||
* @param name Raw metric name from beast::insight callers.
|
||||
* @return Fully-qualified metric name.
|
||||
*/
|
||||
static std::string
|
||||
formatName(std::string const& name);
|
||||
|
||||
private:
|
||||
/** Journal for log output. */
|
||||
Journal journal_;
|
||||
|
||||
/** Prefix for all metric names (e.g., "xrpld"). */
|
||||
std::string prefix_;
|
||||
|
||||
/** OTel Meter used to create all instruments. */
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> otelMeter_;
|
||||
|
||||
/** Mutex protecting hook and gauge registration lists. */
|
||||
std::mutex mutex_;
|
||||
|
||||
/** Registered hooks called during observable callbacks. */
|
||||
std::vector<OTelHookImpl*> hooks_;
|
||||
|
||||
/** Registered gauges read during observable callbacks. */
|
||||
std::vector<OTelGaugeImpl*> gauges_;
|
||||
|
||||
/**
|
||||
* @brief Debounce timestamp for callHooks().
|
||||
*
|
||||
* Multiple gauge callbacks fire during the same collection cycle.
|
||||
* This atomic tracks the last time hooks were invoked (ms since epoch).
|
||||
* Hooks are called at most once per 500ms window to avoid redundant
|
||||
* invocations while still ensuring fresh values each collection cycle.
|
||||
*/
|
||||
std::atomic<int64_t> lastHookCallMs_{0};
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// Implementation
|
||||
//==============================================================================
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// OTelHookImpl
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
OTelHookImpl::OTelHookImpl(HandlerType handler, std::shared_ptr<OTelCollectorImp> impl)
|
||||
: impl_(std::move(impl)), handler_(std::move(handler))
|
||||
{
|
||||
impl_->addHook(this);
|
||||
}
|
||||
|
||||
OTelHookImpl::~OTelHookImpl()
|
||||
{
|
||||
impl_->removeHook(this);
|
||||
}
|
||||
|
||||
void
|
||||
OTelHookImpl::callHandler()
|
||||
{
|
||||
handler_();
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// OTelCounterImpl
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
OTelCounterImpl::OTelCounterImpl(
|
||||
std::string const& name,
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const& meter)
|
||||
: counter_(meter->CreateUInt64Counter(name))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
OTelCounterImpl::increment(value_type amount)
|
||||
{
|
||||
// OTel counters require non-negative values. beast::insight CounterImpl
|
||||
// uses int64_t, so clamp negative values to 0 and cast to uint64_t.
|
||||
if (amount > 0)
|
||||
counter_->Add(static_cast<uint64_t>(amount));
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// OTelEventImpl
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
OTelEventImpl::OTelEventImpl(
|
||||
std::string const& name,
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const& meter)
|
||||
: histogram_(meter->CreateDoubleHistogram(name, "Duration in ms", "ms"))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
OTelEventImpl::notify(value_type const& value)
|
||||
{
|
||||
histogram_->Record(static_cast<double>(value.count()), opentelemetry::context::Context{});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// OTelGaugeImpl
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
OTelGaugeImpl::OTelGaugeImpl(
|
||||
std::string const& name,
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const& meter,
|
||||
std::shared_ptr<OTelCollectorImp> const& collector)
|
||||
: gauge_(meter->CreateInt64ObservableGauge(name)), collector_(collector)
|
||||
{
|
||||
collector_->addGauge(this);
|
||||
gauge_->AddCallback(gaugeCallback, this);
|
||||
}
|
||||
|
||||
void
|
||||
OTelGaugeImpl::gaugeCallback(opentelemetry::metrics::ObserverResult result, void* state)
|
||||
{
|
||||
auto* self = static_cast<OTelGaugeImpl*>(state);
|
||||
self->collector_->callHooks();
|
||||
if (auto intResult = opentelemetry::nostd::get_if<
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::metrics::ObserverResultT<int64_t>>>(
|
||||
&result))
|
||||
{
|
||||
(*intResult)->Observe(self->currentValue());
|
||||
}
|
||||
}
|
||||
|
||||
OTelGaugeImpl::~OTelGaugeImpl()
|
||||
{
|
||||
// RemoveCallback must run before this object is destroyed so the SDK
|
||||
// collection thread cannot invoke gaugeCallback on a dangling `this`.
|
||||
// The SDK's ObservableRegistry guards its callback list and the Observe()
|
||||
// pass with the same mutex, so RemoveCallback cannot return while a
|
||||
// callback for this instrument is in flight — removal is synchronous.
|
||||
gauge_->RemoveCallback(gaugeCallback, this);
|
||||
collector_->removeGauge(this);
|
||||
}
|
||||
|
||||
void
|
||||
OTelGaugeImpl::set(value_type value)
|
||||
{
|
||||
value_.store(static_cast<int64_t>(value), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void
|
||||
OTelGaugeImpl::increment(difference_type amount)
|
||||
{
|
||||
// Use compare-exchange loop to safely clamp to [0, MAX].
|
||||
int64_t current = value_.load(std::memory_order_relaxed);
|
||||
int64_t desired = 0;
|
||||
do
|
||||
{
|
||||
desired = current + amount;
|
||||
// Clamp to 0 on underflow.
|
||||
desired = std::max(desired, int64_t{0});
|
||||
} while (!value_.compare_exchange_weak(current, desired, std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
int64_t
|
||||
OTelGaugeImpl::currentValue() const
|
||||
{
|
||||
return value_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// OTelMeterImpl
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
OTelMeterImpl::OTelMeterImpl(
|
||||
std::string const& name,
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const& meter)
|
||||
: counter_(meter->CreateUInt64Counter(name))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
OTelMeterImpl::increment(value_type amount)
|
||||
{
|
||||
counter_->Add(amount);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// OTelCollectorImp
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
OTelCollectorImp::OTelCollectorImp(
|
||||
std::string const& endpoint,
|
||||
std::string prefix,
|
||||
std::string const& instanceId,
|
||||
std::string const& serviceName,
|
||||
std::string const& networkType,
|
||||
Journal journal)
|
||||
: journal_(journal), prefix_(std::move(prefix))
|
||||
{
|
||||
// instanceId/serviceName/networkType are retained on the New() signature
|
||||
// for back-compat but no longer used here: the telemetry module owns the
|
||||
// resource attributes for the shared metrics pipeline.
|
||||
(void)instanceId;
|
||||
(void)serviceName;
|
||||
(void)networkType;
|
||||
|
||||
if (journal_.info())
|
||||
{
|
||||
// endpoint is informational: the global telemetry pipeline owns the
|
||||
// real exporter. It is logged here for back-compat and diagnostics.
|
||||
journal_.info() << "OTelCollector starting: endpoint=" << endpoint << " prefix=" << prefix_;
|
||||
}
|
||||
|
||||
// Fetch the Meter from the GLOBAL MeterProvider. The telemetry module
|
||||
// (xrpl::telemetry::Telemetry) builds the metrics pipeline (exporter,
|
||||
// periodic reader, histogram view, resource attributes) and registers it
|
||||
// via metrics::Provider::SetMeterProvider() during start(). beast metrics
|
||||
// ride that shared pipeline, so both direct-API and beast-sourced metrics
|
||||
// export under one resource identity.
|
||||
//
|
||||
// The name/version literals MUST match the telemetry module's kMeterName
|
||||
// ("xrpld") and kMeterVersion ("1.0.0"). They are written as literals (not
|
||||
// referenced from Telemetry.h) because beast/insight sits below the
|
||||
// telemetry module in the layering and cannot include its header.
|
||||
otelMeter_ = metrics_api::Provider::GetMeterProvider()->GetMeter(
|
||||
std::string{"xrpld"}, std::string{"1.0.0"});
|
||||
|
||||
if (journal_.info())
|
||||
{
|
||||
journal_.info() << "OTelCollector started successfully";
|
||||
}
|
||||
}
|
||||
|
||||
OTelCollectorImp::~OTelCollectorImp()
|
||||
{
|
||||
if (journal_.info())
|
||||
{
|
||||
journal_.info() << "OTelCollector shutting down";
|
||||
}
|
||||
// No pipeline teardown here: the telemetry module owns the global
|
||||
// MeterProvider lifecycle (ForceFlush/Shutdown happen in Telemetry::stop()).
|
||||
if (journal_.info())
|
||||
{
|
||||
journal_.info() << "OTelCollector stopped";
|
||||
}
|
||||
}
|
||||
|
||||
Hook
|
||||
OTelCollectorImp::makeHook(HookImpl::HandlerType const& handler)
|
||||
{
|
||||
return Hook(std::make_shared<OTelHookImpl>(handler, shared_from_this()));
|
||||
}
|
||||
|
||||
Counter
|
||||
OTelCollectorImp::makeCounter(std::string const& name)
|
||||
{
|
||||
return Counter(std::make_shared<OTelCounterImpl>(formatName(name), otelMeter_));
|
||||
}
|
||||
|
||||
Event
|
||||
OTelCollectorImp::makeEvent(std::string const& name)
|
||||
{
|
||||
return Event(std::make_shared<OTelEventImpl>(formatName(name), otelMeter_));
|
||||
}
|
||||
|
||||
Gauge
|
||||
OTelCollectorImp::makeGauge(std::string const& name)
|
||||
{
|
||||
return Gauge(std::make_shared<OTelGaugeImpl>(formatName(name), otelMeter_, shared_from_this()));
|
||||
}
|
||||
|
||||
Meter
|
||||
OTelCollectorImp::makeMeter(std::string const& name)
|
||||
{
|
||||
return Meter(std::make_shared<OTelMeterImpl>(formatName(name), otelMeter_));
|
||||
}
|
||||
|
||||
void
|
||||
OTelCollectorImp::addHook(OTelHookImpl* hook)
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
hooks_.push_back(hook);
|
||||
}
|
||||
|
||||
void
|
||||
OTelCollectorImp::removeHook(OTelHookImpl* hook)
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
std::erase(hooks_, hook);
|
||||
}
|
||||
|
||||
void
|
||||
OTelCollectorImp::callHooks()
|
||||
{
|
||||
// Debounce: hooks run at most once per 500ms. Multiple gauge callbacks
|
||||
// fire during the same collection cycle — only the first one triggers
|
||||
// hooks. Subsequent callbacks within the window read already-updated
|
||||
// gauge values.
|
||||
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
auto last = lastHookCallMs_.load(std::memory_order_acquire);
|
||||
if (now - last < 500)
|
||||
return;
|
||||
if (!lastHookCallMs_.compare_exchange_strong(last, now, std::memory_order_acq_rel))
|
||||
return; // Another thread won the race.
|
||||
|
||||
// Copy the hook list under the lock, then invoke handlers outside it.
|
||||
// A handler may drop the last reference to an OTelHookImpl, whose
|
||||
// destructor calls removeHook() and re-acquires mutex_; invoking
|
||||
// handlers while holding the (non-recursive) lock would deadlock.
|
||||
std::vector<OTelHookImpl*> hooks;
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
hooks = hooks_;
|
||||
}
|
||||
for (auto* hook : hooks)
|
||||
hook->callHandler();
|
||||
}
|
||||
|
||||
void
|
||||
OTelCollectorImp::addGauge(OTelGaugeImpl* gauge)
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
gauges_.push_back(gauge);
|
||||
}
|
||||
|
||||
void
|
||||
OTelCollectorImp::removeGauge(OTelGaugeImpl* gauge)
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
std::erase(gauges_, gauge);
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter> const&
|
||||
OTelCollectorImp::otelMeter() const
|
||||
{
|
||||
return otelMeter_;
|
||||
}
|
||||
|
||||
std::string
|
||||
OTelCollectorImp::formatName(std::string const& name)
|
||||
{
|
||||
// Produce a clean, lowercase, Prometheus-compatible metric name.
|
||||
// No prefix — the OTel resource (service.name) identifies the service.
|
||||
// Dots and spaces become underscores; everything lowercased.
|
||||
std::string result;
|
||||
result.reserve(name.size());
|
||||
for (char const c : name)
|
||||
{
|
||||
if (c == '.' || c == ' ')
|
||||
{
|
||||
result += '_';
|
||||
}
|
||||
else
|
||||
{
|
||||
result += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
std::shared_ptr<Collector>
|
||||
OTelCollector::New(
|
||||
std::string const& endpoint,
|
||||
std::string const& prefix,
|
||||
std::string const& instanceId,
|
||||
std::string const& serviceName,
|
||||
std::string const& networkType,
|
||||
Journal journal)
|
||||
{
|
||||
return std::make_shared<detail::OTelCollectorImp>(
|
||||
endpoint, prefix, instanceId, serviceName, networkType, journal);
|
||||
}
|
||||
|
||||
} // namespace beast::insight
|
||||
|
||||
#else // !XRPL_ENABLE_TELEMETRY
|
||||
|
||||
// When telemetry is disabled at compile time, OTelCollector::New()
|
||||
// returns a NullCollector so callers do not need conditional logic.
|
||||
|
||||
#include <xrpl/beast/insight/Collector.h>
|
||||
#include <xrpl/beast/insight/NullCollector.h>
|
||||
#include <xrpl/beast/insight/OTelCollector.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
|
||||
namespace beast::insight {
|
||||
|
||||
std::shared_ptr<Collector>
|
||||
OTelCollector::New(
|
||||
std::string const& /* endpoint */,
|
||||
std::string const& /* prefix */,
|
||||
std::string const& /* instanceId */,
|
||||
std::string const& /* serviceName */,
|
||||
std::string const& /* networkType */,
|
||||
Journal /* journal */)
|
||||
{
|
||||
return NullCollector::make();
|
||||
}
|
||||
|
||||
} // namespace beast::insight
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -166,7 +166,7 @@ private:
|
||||
std::string name_;
|
||||
GaugeImpl::value_type lastValue_{0};
|
||||
GaugeImpl::value_type value_{0};
|
||||
bool dirty_{false};
|
||||
bool dirty_{true};
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -572,6 +572,9 @@ StatsDEventImpl::doNotify(EventImpl::value_type const& value)
|
||||
StatsDGaugeImpl::StatsDGaugeImpl(std::string name, std::shared_ptr<StatsDCollectorImp> impl)
|
||||
: impl_(std::move(impl)), name_(std::move(name))
|
||||
{
|
||||
// Start dirty so the initial value (0) is emitted on the first flush.
|
||||
// Without this, gauges whose value never changes from 0 would never
|
||||
// appear in downstream metric stores (e.g. Prometheus via StatsD).
|
||||
impl_->add(*this);
|
||||
}
|
||||
|
||||
|
||||
@@ -230,9 +230,8 @@ Writer::~Writer()
|
||||
impl_->finishAll();
|
||||
}
|
||||
|
||||
Writer::Writer(Writer&& w) noexcept
|
||||
Writer::Writer(Writer&& w) noexcept : impl_(std::move(w.impl_))
|
||||
{
|
||||
impl_ = std::move(w.impl_);
|
||||
}
|
||||
|
||||
Writer&
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <istream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
namespace json {
|
||||
// Implementation of class Reader
|
||||
@@ -605,34 +606,17 @@ bool
|
||||
Reader::decodeDouble(Token& token)
|
||||
{
|
||||
double value = 0;
|
||||
int const bufferSize = 32;
|
||||
int count = 0;
|
||||
int const length = int(token.end - token.start);
|
||||
// Sanity check to avoid buffer overflow exploits.
|
||||
if (length < 0)
|
||||
{
|
||||
return addError("Unable to parse token length", token);
|
||||
}
|
||||
// Avoid using a string constant for the format control string given to
|
||||
// sscanf, as this can cause hard to debug crashes on OS X. See here for
|
||||
// more info:
|
||||
//
|
||||
// http://developer.apple.com/library/mac/#DOCUMENTATION/DeveloperTools/gcc-4.0.1/gcc/Incompatibilities.html
|
||||
char format[] = "%lf";
|
||||
if (length <= bufferSize)
|
||||
{
|
||||
Char buffer[bufferSize + 1];
|
||||
memcpy(buffer, token.start, length);
|
||||
buffer[length] = 0;
|
||||
count = sscanf(buffer, format, &value);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string const buffer(token.start, token.end);
|
||||
count = sscanf(buffer.c_str(), format, &value);
|
||||
}
|
||||
if (count != 1)
|
||||
auto const [ptr, ec] = std::from_chars(token.start, token.end, value);
|
||||
|
||||
// Reject anything from_chars could not turn into a finite double:
|
||||
// - ec != std::errc{}: no valid conversion, or an out-of-range magnitude
|
||||
// (e.g. 1e400).
|
||||
// - ptr != token.end: readNumber() is permissive about which characters
|
||||
// it collects into a token (it will, for example, keep a '+' mid-token),
|
||||
// but from_chars() will stop at the first character it cannot parse.
|
||||
if (ec != std::errc{} || ptr != token.end)
|
||||
return addError("'" + std::string(token.start, token.end) + "' is not a number.", token);
|
||||
|
||||
currentValue() = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public:
|
||||
if (length == kUnknown)
|
||||
length = (value != nullptr) ? (unsigned int)strlen(value) : 0;
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-no-malloc)
|
||||
char* newString = static_cast<char*>(malloc(length + 1));
|
||||
if (value != nullptr)
|
||||
memcpy(newString, value, length);
|
||||
@@ -59,7 +60,10 @@ public:
|
||||
releaseStringValue(char* value) override
|
||||
{
|
||||
if (value != nullptr)
|
||||
{
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-no-malloc)
|
||||
free(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,7 +124,10 @@ Value::CZString::CZString(CZString const& other)
|
||||
Value::CZString::~CZString()
|
||||
{
|
||||
if ((cstr_ != nullptr) && index_ == static_cast<int>(DuplicationPolicy::Duplicate))
|
||||
{
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
|
||||
valueAllocator()->releaseMemberName(const_cast<char*>(cstr_));
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -241,6 +248,7 @@ Value::Value(std::string const& value) : type_(ValueType::String), allocated_(tr
|
||||
|
||||
Value::Value(StaticString const& value) : type_(ValueType::String)
|
||||
{
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
|
||||
value_.stringVal = const_cast<char*>(value.cStr());
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
namespace json {
|
||||
@@ -76,19 +77,18 @@ valueToString(UInt value)
|
||||
std::string
|
||||
valueToString(double value)
|
||||
{
|
||||
// Allocate a buffer that is more than large enough to store the 16 digits
|
||||
// of precision requested below.
|
||||
// Format with 16 significant digits.
|
||||
// We need not request the alternative representation that always has a
|
||||
// decimal point because JSON doesn't distinguish the concepts of reals and integers.
|
||||
// A double never needs more than 32 characters in this form,
|
||||
// so to_chars cannot actually run out of room here.
|
||||
char buffer[32];
|
||||
// Print into the buffer. We need not request the alternative representation
|
||||
// that always has a decimal point because JSON doesn't distinguish the
|
||||
// concepts of reals and integers.
|
||||
#if defined(_MSC_VER) && defined(__STDC_SECURE_LIB__) // Use secure version with visual studio 2005
|
||||
// to avoid warning.
|
||||
sprintf_s(buffer, sizeof(buffer), "%.16g", value);
|
||||
#else
|
||||
snprintf(buffer, sizeof(buffer), "%.16g", value);
|
||||
#endif
|
||||
return buffer;
|
||||
auto const [ptr, ec] =
|
||||
std::to_chars(buffer, buffer + sizeof(buffer), value, std::chars_format::general, 16);
|
||||
XRPL_ASSERT(ec == std::errc{}, "json::valueToString(double) : conversion fits buffer");
|
||||
if (ec != std::errc{})
|
||||
return {};
|
||||
return std::string(buffer, ptr);
|
||||
}
|
||||
|
||||
std::string
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
namespace xrpl::NodeStore {
|
||||
|
||||
DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes)
|
||||
DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes) : key_(key)
|
||||
{
|
||||
/* Data format:
|
||||
|
||||
@@ -23,10 +23,6 @@ DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes)
|
||||
9...end The body of the object data
|
||||
*/
|
||||
|
||||
success_ = false;
|
||||
key_ = key;
|
||||
objectType_ = NodeObjectType::Unknown;
|
||||
objectData_ = nullptr;
|
||||
dataBytes_ = std::max(0, valueBytes - 9);
|
||||
|
||||
// VFALCO NOTE What about bytes 4 through 7 inclusive?
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.3.0-b0"
|
||||
char const* const versionString = "3.3.0-b1"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
|
||||
@@ -152,7 +152,12 @@ std::uint64_t
|
||||
getQuality(uint256 const& uBase)
|
||||
{
|
||||
// VFALCO [base_uint] This assumes a certain storage format
|
||||
return boost::endian::big_to_native(((std::uint64_t*)uBase.end())[-1]);
|
||||
//
|
||||
// Load the final 8 bytes as a big-endian integer. load_big_u64 reads
|
||||
// through unaligned byte storage (via memcpy) and applies the endian
|
||||
// conversion, avoiding the alignment/strict-aliasing UB of casting the
|
||||
// unsigned char* returned by end() to a std::uint64_t*.
|
||||
return boost::endian::load_big_u64(uBase.end() - 8);
|
||||
}
|
||||
|
||||
uint256
|
||||
@@ -278,8 +283,11 @@ quality(Keylet const& k, std::uint64_t q) noexcept
|
||||
// for indexes.
|
||||
uint256 x = k.key;
|
||||
|
||||
// FIXME This is ugly and we can and should do better...
|
||||
((std::uint64_t*)x.end())[-1] = boost::endian::native_to_big(q);
|
||||
// Store the quality as a big-endian integer in the final 8 bytes.
|
||||
// store_big_u64 writes through unaligned byte storage (via memcpy) and
|
||||
// applies the endian conversion, avoiding the alignment/strict-aliasing UB
|
||||
// of casting the unsigned char* returned by end() to a std::uint64_t*.
|
||||
boost::endian::store_big_u64(x.end() - 8, q);
|
||||
|
||||
return {ltDIR_NODE, x};
|
||||
}
|
||||
|
||||
@@ -21,11 +21,12 @@ SOTemplate::SOTemplate(
|
||||
}
|
||||
|
||||
SOTemplate::SOTemplate(std::vector<SOElement> uniqueFields, std::vector<SOElement> commonFields)
|
||||
: indices_(SField::getNumFields() + 1, -1) // Unmapped indices == -1
|
||||
: elements_(std::move(uniqueFields))
|
||||
, indices_(SField::getNumFields() + 1, -1) // Unmapped indices == -1
|
||||
{
|
||||
// Add all SOElements.
|
||||
//
|
||||
elements_ = std::move(uniqueFields);
|
||||
|
||||
std::ranges::move(commonFields, std::back_inserter(elements_));
|
||||
|
||||
// Validate and index elements_.
|
||||
|
||||
@@ -69,6 +69,17 @@ toUnsigned(U2 value)
|
||||
return static_cast<U1>(value);
|
||||
}
|
||||
|
||||
static std::string
|
||||
joinName(std::string const& jsonName, std::string const& fieldName)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(jsonName.size() + 1 + fieldName.size());
|
||||
result += jsonName;
|
||||
result += '.';
|
||||
result += fieldName;
|
||||
return result;
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
static inline std::string
|
||||
makeName(std::string const& object, std::string const& field)
|
||||
@@ -76,7 +87,7 @@ makeName(std::string const& object, std::string const& field)
|
||||
if (field.empty())
|
||||
return object;
|
||||
|
||||
return object + "." + field;
|
||||
return joinName(object, field);
|
||||
}
|
||||
|
||||
static inline json::Value
|
||||
@@ -1036,8 +1047,8 @@ parseObject(
|
||||
|
||||
try
|
||||
{
|
||||
auto ret =
|
||||
parseObject(jsonName + "." + fieldName, value, field, depth + 1, error);
|
||||
auto ret = parseObject(
|
||||
joinName(jsonName, fieldName), value, field, depth + 1, error);
|
||||
if (!ret)
|
||||
return std::nullopt;
|
||||
data.emplaceBack(std::move(*ret));
|
||||
@@ -1054,8 +1065,8 @@ parseObject(
|
||||
case STI_ARRAY:
|
||||
try
|
||||
{
|
||||
auto array =
|
||||
parseArray(jsonName + "." + fieldName, value, field, depth + 1, error);
|
||||
auto array = parseArray(
|
||||
joinName(jsonName, fieldName), value, field, depth + 1, error);
|
||||
if (!array.has_value())
|
||||
return std::nullopt;
|
||||
data.emplaceBack(std::move(*array));
|
||||
|
||||
@@ -66,9 +66,9 @@ getTxFormat(TxType type)
|
||||
return format;
|
||||
}
|
||||
|
||||
STTx::STTx(STObject&& object) : STObject(std::move(object))
|
||||
STTx::STTx(STObject&& object)
|
||||
: STObject(std::move(object)), txType_(safeCast<TxType>(getFieldU16(sfTransactionType)))
|
||||
{
|
||||
txType_ = safeCast<TxType>(getFieldU16(sfTransactionType));
|
||||
applyTemplate(getTxFormat(txType_)->getSOTemplate()); // may throw
|
||||
tid_ = getHash(HashPrefix::TransactionId);
|
||||
buildBatchTxnIds();
|
||||
@@ -100,6 +100,9 @@ STTx::STTx(TxType type, std::function<void(STObject&)> assembler) : STObject(sfT
|
||||
|
||||
assembler(*this);
|
||||
|
||||
// txType_ must be read after the object is assembled, so this cannot be a
|
||||
// member initializer.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
|
||||
txType_ = safeCast<TxType>(getFieldU16(sfTransactionType));
|
||||
|
||||
if (txType_ != type)
|
||||
|
||||
@@ -99,7 +99,7 @@ int
|
||||
Serializer::addRaw(void const* ptr, int len)
|
||||
{
|
||||
int const ret = data_.size();
|
||||
data_.insert(data_.end(), (char const*)ptr, ((char const*)ptr) + len);
|
||||
data_.insert(data_.end(), static_cast<char const*>(ptr), static_cast<char const*>(ptr) + len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user