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) <noreply@anthropic.com>
This commit is contained in:
Pratik Mankawde
2026-04-20 15:09:52 +01:00
parent a06e96ea8a
commit a4db27de48
5 changed files with 224 additions and 0 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,77 @@
#include <xrpl/telemetry/SpanGuard.h>
#include <gtest/gtest.h>
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<int64_t>(42));
span.setAttribute("bool_key", true);
span.setOk();
span.setError("test");
span.addEvent("event");
}
TEST(SpanGuardFactory, category_span_returns_null_when_disabled)
{
auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "test");
EXPECT_FALSE(span);
span.setAttribute("xrpl.rpc.command", "test");
span.setAttribute("xrpl.rpc.status", "success");
}
TEST(SpanGuardFactory, child_span_null_when_no_parent)
{
auto span = SpanGuard::span("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);
}

View File

@@ -0,0 +1,111 @@
#include <xrpl/basics/BasicConfig.h>
#include <xrpl/telemetry/Telemetry.h>
#include <gtest/gtest.h>
#include <chrono>
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();
}

View File

@@ -0,0 +1,8 @@
#include <gtest/gtest.h>
int
main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}