From a4db27de48c40367c8d926d612f862e7acd8ce54 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:09:52 +0100 Subject: [PATCH] feat(telemetry): add RPC trace filters and SpanGuard unit tests - Grafana Tempo datasource: add rpc-command, rpc-status, rpc-role search filters for the Explore UI - Unit tests: TelemetryConfig (config parsing defaults and sections), SpanGuardFactory (null guard safety, move semantics, discard, all factory methods) - Test CMake registration with optional OTel linking Co-Authored-By: Claude Opus 4.6 (1M context) --- .../provisioning/datasources/tempo.yaml | 17 +++ src/tests/libxrpl/CMakeLists.txt | 11 ++ .../libxrpl/telemetry/SpanGuardFactory.cpp | 77 ++++++++++++ .../libxrpl/telemetry/TelemetryConfig.cpp | 111 ++++++++++++++++++ src/tests/libxrpl/telemetry/main.cpp | 8 ++ 5 files changed, 224 insertions(+) create mode 100644 src/tests/libxrpl/telemetry/SpanGuardFactory.cpp create mode 100644 src/tests/libxrpl/telemetry/TelemetryConfig.cpp create mode 100644 src/tests/libxrpl/telemetry/main.cpp diff --git a/docker/telemetry/grafana/provisioning/datasources/tempo.yaml b/docker/telemetry/grafana/provisioning/datasources/tempo.yaml index 825d55453c..576819660c 100644 --- a/docker/telemetry/grafana/provisioning/datasources/tempo.yaml +++ b/docker/telemetry/grafana/provisioning/datasources/tempo.yaml @@ -6,6 +6,7 @@ # Search filters provide pre-configured dropdowns in the Explore UI. # Each phase adds filters for the span attributes it introduces. # Phase 1b (infra): Base filters — node identity, service, span name, status. +# Phase 2 (RPC): RPC command, status, role filters. apiVersion: 1 @@ -89,3 +90,19 @@ datasources: operator: ">" scope: intrinsic type: static + # Phase 2: RPC tracing filters + - id: rpc-command + tag: xrpl.rpc.command + operator: "=" + scope: span + type: static + - id: rpc-status + tag: xrpl.rpc.status + operator: "=" + scope: span + type: dynamic + - id: rpc-role + tag: xrpl.rpc.role + operator: "=" + scope: span + type: dynamic diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 0b666441d1..b74ef02771 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -42,3 +42,14 @@ if(NOT WIN32) target_link_libraries(xrpl.test.net PRIVATE xrpl.imports.test) add_dependencies(xrpl.tests xrpl.test.net) endif() + +xrpl_add_test(telemetry) +target_link_libraries(xrpl.test.telemetry PRIVATE xrpl.imports.test) +target_include_directories(xrpl.test.telemetry PRIVATE ${CMAKE_SOURCE_DIR}/src) +if(telemetry) + target_link_libraries( + xrpl.test.telemetry + PRIVATE opentelemetry-cpp::opentelemetry-cpp + ) +endif() +add_dependencies(xrpl.tests xrpl.test.telemetry) diff --git a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp new file mode 100644 index 0000000000..89f6283bca --- /dev/null +++ b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp @@ -0,0 +1,77 @@ +#include + +#include + +using namespace xrpl; +using namespace xrpl::telemetry; + +TEST(SpanGuardFactory, null_guard_methods_are_safe) +{ + auto span = SpanGuard::span("nonexistent.span"); + EXPECT_FALSE(span); + + span.setAttribute("key", "value"); + span.setAttribute("int_key", static_cast(42)); + span.setAttribute("bool_key", true); + span.setOk(); + span.setError("test"); + span.addEvent("event"); +} + +TEST(SpanGuardFactory, category_span_returns_null_when_disabled) +{ + auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "test"); + EXPECT_FALSE(span); + + span.setAttribute("xrpl.rpc.command", "test"); + span.setAttribute("xrpl.rpc.status", "success"); +} + +TEST(SpanGuardFactory, child_span_null_when_no_parent) +{ + auto span = SpanGuard::span("parent.test"); + auto child = span.childSpan("child.test"); + EXPECT_FALSE(child); +} + +TEST(SpanGuardFactory, linked_span_null_when_no_context) +{ + auto span = SpanGuard::span("source.test"); + auto linked = span.linkedSpan("linked.test"); + EXPECT_FALSE(linked); +} + +TEST(SpanGuardFactory, capture_context_returns_invalid_on_null) +{ + auto span = SpanGuard::span("ctx.test"); + auto ctx = span.captureContext(); + EXPECT_FALSE(ctx.isValid()); +} + +TEST(SpanGuardFactory, move_construction_transfers_ownership) +{ + auto span = SpanGuard::span("move.test"); + auto moved = std::move(span); + EXPECT_FALSE(span); + moved.setAttribute("key", "value"); +} + +TEST(SpanGuardFactory, record_exception_safe_on_null) +{ + auto span = SpanGuard::span(TraceCategory::Rpc, "rpc.command", "test"); + try + { + throw std::runtime_error("test error"); + } + catch (std::exception const& e) + { + span.recordException(e); + } +} + +TEST(SpanGuardFactory, discard_safe_on_null) +{ + auto span = SpanGuard::span(TraceCategory::Transactions, "tx", "process"); + span.discard(); + EXPECT_FALSE(span); +} diff --git a/src/tests/libxrpl/telemetry/TelemetryConfig.cpp b/src/tests/libxrpl/telemetry/TelemetryConfig.cpp new file mode 100644 index 0000000000..de58a3827f --- /dev/null +++ b/src/tests/libxrpl/telemetry/TelemetryConfig.cpp @@ -0,0 +1,111 @@ +#include +#include + +#include + +#include + +using namespace xrpl; + +TEST(TelemetryConfig, setup_defaults) +{ + telemetry::Telemetry::Setup s; + EXPECT_FALSE(s.enabled); + EXPECT_EQ(s.serviceName, "rippled"); + EXPECT_TRUE(s.serviceVersion.empty()); + EXPECT_TRUE(s.serviceInstanceId.empty()); + EXPECT_EQ(s.exporterType, "otlp_http"); + EXPECT_EQ(s.exporterEndpoint, "http://localhost:4318/v1/traces"); + EXPECT_FALSE(s.useTls); + EXPECT_TRUE(s.tlsCertPath.empty()); + EXPECT_DOUBLE_EQ(s.samplingRatio, 1.0); + EXPECT_EQ(s.batchSize, 512u); + EXPECT_EQ(s.batchDelay, std::chrono::milliseconds{5000}); + EXPECT_EQ(s.maxQueueSize, 2048u); + EXPECT_EQ(s.networkId, 0u); + EXPECT_EQ(s.networkType, "mainnet"); + EXPECT_TRUE(s.traceTransactions); + EXPECT_TRUE(s.traceConsensus); + EXPECT_TRUE(s.traceRpc); + EXPECT_FALSE(s.tracePeer); + EXPECT_TRUE(s.traceLedger); +} + +TEST(TelemetryConfig, parse_empty_section) +{ + Section section; + auto setup = telemetry::setup_Telemetry(section, "nHUtest123", "2.0.0"); + + EXPECT_FALSE(setup.enabled); + EXPECT_EQ(setup.serviceName, "rippled"); + EXPECT_EQ(setup.serviceVersion, "2.0.0"); + EXPECT_EQ(setup.serviceInstanceId, "nHUtest123"); + EXPECT_EQ(setup.exporterType, "otlp_http"); + EXPECT_DOUBLE_EQ(setup.samplingRatio, 1.0); + EXPECT_TRUE(setup.traceRpc); + EXPECT_TRUE(setup.traceTransactions); + EXPECT_TRUE(setup.traceConsensus); + EXPECT_FALSE(setup.tracePeer); + EXPECT_TRUE(setup.traceLedger); +} + +TEST(TelemetryConfig, parse_full_section) +{ + Section section; + section.set("enabled", "1"); + section.set("service_name", "my-rippled"); + section.set("service_instance_id", "custom-id"); + section.set("exporter", "otlp_http"); + section.set("endpoint", "http://collector:4318/v1/traces"); + section.set("use_tls", "1"); + section.set("tls_ca_cert", "/etc/ssl/ca.pem"); + section.set("sampling_ratio", "0.5"); + section.set("batch_size", "256"); + section.set("batch_delay_ms", "3000"); + section.set("max_queue_size", "4096"); + section.set("trace_transactions", "0"); + section.set("trace_consensus", "0"); + section.set("trace_rpc", "1"); + section.set("trace_peer", "1"); + section.set("trace_ledger", "0"); + + auto setup = telemetry::setup_Telemetry(section, "nHUtest123", "2.0.0"); + + EXPECT_TRUE(setup.enabled); + EXPECT_EQ(setup.serviceName, "my-rippled"); + EXPECT_EQ(setup.serviceInstanceId, "custom-id"); + EXPECT_EQ(setup.exporterType, "otlp_http"); + EXPECT_EQ(setup.exporterEndpoint, "http://collector:4318/v1/traces"); + EXPECT_TRUE(setup.useTls); + EXPECT_EQ(setup.tlsCertPath, "/etc/ssl/ca.pem"); + EXPECT_DOUBLE_EQ(setup.samplingRatio, 0.5); + EXPECT_EQ(setup.batchSize, 256u); + EXPECT_EQ(setup.batchDelay, std::chrono::milliseconds{3000}); + EXPECT_EQ(setup.maxQueueSize, 4096u); + EXPECT_FALSE(setup.traceTransactions); + EXPECT_FALSE(setup.traceConsensus); + EXPECT_TRUE(setup.traceRpc); + EXPECT_TRUE(setup.tracePeer); + EXPECT_FALSE(setup.traceLedger); +} + +TEST(TelemetryConfig, null_telemetry_factory) +{ + telemetry::Telemetry::Setup setup; + setup.enabled = false; + + beast::Journal::Sink& sink = beast::Journal::getNullSink(); + beast::Journal j(sink); + auto tel = telemetry::make_Telemetry(setup, j); + EXPECT_TRUE(tel != nullptr); + EXPECT_FALSE(tel->isEnabled()); + EXPECT_FALSE(tel->shouldTraceRpc()); + EXPECT_FALSE(tel->shouldTraceTransactions()); + EXPECT_FALSE(tel->shouldTraceConsensus()); + EXPECT_FALSE(tel->shouldTracePeer()); + EXPECT_FALSE(tel->shouldTraceLedger()); + + // start/stop should be no-ops without crashing + tel->start(); + tel->stop(); +} diff --git a/src/tests/libxrpl/telemetry/main.cpp b/src/tests/libxrpl/telemetry/main.cpp new file mode 100644 index 0000000000..5142bbe08a --- /dev/null +++ b/src/tests/libxrpl/telemetry/main.cpp @@ -0,0 +1,8 @@ +#include + +int +main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}