feat(telemetry): expose direct OTel metrics API via getMeter()

Give developers direct access to the full OpenTelemetry metrics API,
symmetric with getTracer(), so all seven OTel instrument types
(including UpDownCounter, sync Gauge, and the observable variants) are
reachable — not just the four beast::insight models.

- Telemetry: build and own the metrics pipeline (OTLP HTTP metric
  exporter + PeriodicExportingMetricReader + SpanMetrics histogram view)
  alongside the tracer, sharing the same resource attributes and TLS
  config. Register it globally via metrics::Provider::SetMeterProvider
  and expose Telemetry::getMeter(). Metrics enable with [telemetry];
  the metrics endpoint is derived from the trace endpoint.
- beast OTelCollector: no longer owns a pipeline. It fetches the global
  Meter, becoming a thin shim over the shared provider (legacy path
  during beast deprecation). This also resolves the review note that the
  metric exporter ignored [telemetry] use_tls — TLS now comes from the
  shared telemetry pipeline.
- Add a libxrpl unit test covering getMeter() on the enabled (global
  provider) and disabled (noop) paths, exercising an UpDownCounter.

Design: docs/superpowers/specs/2026-07-07-direct-otel-metrics-api-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pratik Mankawde
2026-07-07 16:52:45 +01:00
parent cc4457b5a2
commit ab0d8249fd
5 changed files with 316 additions and 103 deletions

View File

@@ -225,6 +225,11 @@ target_link_libraries(
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

View File

@@ -97,6 +97,7 @@
#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/tracer.h>
@@ -109,6 +110,13 @@ namespace xrpl::telemetry {
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
@@ -300,6 +308,20 @@ public:
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

View File

@@ -4,7 +4,10 @@
*
* Compiled only when XRPL_ENABLE_TELEMETRY is defined (via CMake
* telemetry=ON). Maps beast::insight instruments to OTel SDK instruments
* and exports them via OTLP/HTTP using a PeriodicMetricReader.
* 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.
@@ -22,10 +25,10 @@
* +--------------------+----------------+--------------+
* |
* v
* PeriodicMetricReader (1s interval)
* GLOBAL Meter (from metrics::Provider::GetMeterProvider())
* |
* v
* OtlpHttpMetricExporter -> OTel Collector -> Prometheus
* telemetry-owned PeriodicMetricReader (1s) -> OTLP exporter -> Prometheus
*/
#ifdef XRPL_ENABLE_TELEMETRY
@@ -41,28 +44,15 @@
#include <xrpl/beast/insight/MeterImpl.h>
#include <xrpl/beast/utility/Journal.h>
#include <opentelemetry/exporters/otlp/otlp_http_metric_exporter_factory.h>
#include <opentelemetry/exporters/otlp/otlp_http_metric_exporter_options.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 <opentelemetry/sdk/metrics/aggregation/aggregation_config.h>
#include <opentelemetry/sdk/metrics/export/periodic_exporting_metric_reader_factory.h>
#include <opentelemetry/sdk/metrics/export/periodic_exporting_metric_reader_options.h>
#include <opentelemetry/sdk/metrics/instruments.h>
#include <opentelemetry/sdk/metrics/meter_provider.h>
#include <opentelemetry/sdk/metrics/meter_provider_factory.h>
#include <opentelemetry/sdk/metrics/view/instrument_selector_factory.h>
#include <opentelemetry/sdk/metrics/view/meter_selector_factory.h>
#include <opentelemetry/sdk/metrics/view/view_factory.h>
#include <opentelemetry/sdk/metrics/view/view_registry.h>
#include <opentelemetry/sdk/resource/resource.h>
#include <opentelemetry/semconv/incubating/service_attributes.h>
#include <algorithm>
#include <atomic>
@@ -79,9 +69,6 @@ namespace beast::insight {
namespace detail {
namespace metrics_api = opentelemetry::metrics;
namespace metrics_sdk = opentelemetry::sdk::metrics;
namespace otlp_http = opentelemetry::exporter::otlp;
namespace resource = opentelemetry::sdk::resource;
class OTelCollectorImp;
@@ -335,12 +322,16 @@ private:
//------------------------------------------------------------------------------
/**
* @brief Main OTel Collector implementation.
* @brief Main OTel Collector implementation (legacy shim).
*
* Creates an OTel MeterProvider with a PeriodicMetricReader that
* exports metrics via OTLP/HTTP at 1-second intervals. Implements
* all Collector::make_*() factory methods to create OTel-backed
* instrument wrappers.
* 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:
*
@@ -353,19 +344,20 @@ private:
* | OTelCollectorImp |-------------+
* +------------------+
* | - journal_ |
* | - prefix_ |
* | - provider_ | +---------------------+
* | - otelMeter_ |---->| OTel MeterProvider |
* | - hooks_[] | | + PeriodicReader |
* | - gauges_[] | | + OtlpHttpExporter |
* +------------------+ +---------------------+
* | - prefix_ | +--------------------------+
* | - otelMeter_ |---->| GLOBAL MeterProvider |
* | - hooks_[] | | (owned by Telemetry) |
* | - gauges_[] | | + PeriodicReader |
* +------------------+ | + OtlpHttpExporter |
* +--------------------------+
*
* Lifecycle:
* 1. Constructor creates MeterProvider + exporter pipeline.
* 2. make_*() methods create instruments registered with the provider.
* 3. PeriodicMetricReader collects every 1s, calling observable callbacks.
* 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 shuts down MeterProvider (flushes pending exports).
* 5. Destructor only logs; the telemetry module owns pipeline teardown.
*
* Caveats:
* - Observable gauge callbacks run on the SDK's internal thread. Hook
@@ -390,9 +382,12 @@ class OTelCollectorImp : public OTelCollector, public std::enable_shared_from_th
{
public:
/**
* @brief Construct the OTel collector and initialize the export pipeline.
* @brief Construct the OTel collector over the global MeterProvider.
*
* @param endpoint OTLP/HTTP metrics endpoint URL.
* @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.
@@ -505,9 +500,6 @@ private:
/** Prefix for all metric names (e.g., "xrpld"). */
std::string prefix_;
/** OTel SDK MeterProvider owning the export pipeline. RAII lifecycle. */
std::shared_ptr<metrics_sdk::MeterProvider> provider_;
/** OTel Meter used to create all instruments. */
opentelemetry::nostd::shared_ptr<metrics_api::Meter> otelMeter_;
@@ -682,69 +674,33 @@ OTelCollectorImp::OTelCollectorImp(
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_;
}
// Configure OTLP HTTP metric exporter.
otlp_http::OtlpHttpMetricExporterOptions exporterOpts;
exporterOpts.url = endpoint;
auto exporter = otlp_http::OtlpHttpMetricExporterFactory::Create(exporterOpts);
// Configure periodic metric reader (1-second export interval).
metrics_sdk::PeriodicExportingMetricReaderOptions readerOpts;
readerOpts.export_interval_millis = std::chrono::milliseconds(1000);
readerOpts.export_timeout_millis = std::chrono::milliseconds(500);
auto reader =
metrics_sdk::PeriodicExportingMetricReaderFactory::Create(std::move(exporter), readerOpts);
// Configure resource attributes matching the trace exporter so metrics
// and traces share one identity. service.name defaults to "xrpld" when
// unset. service.instance.id and xrpl.network.type are added when
// provided so Prometheus labels distinguish node and network.
// "xrpl.network.type" is a string literal (not the telemetry SpanNames
// const) because beast/insight sits below the telemetry module and
// cannot include it; the value must match the trace exporter's key.
resource::ResourceAttributes attrs;
attrs[opentelemetry::semconv::service::kServiceName] =
serviceName.empty() ? "xrpld" : serviceName;
if (!instanceId.empty())
{
attrs[opentelemetry::semconv::service::kServiceInstanceId] = instanceId;
}
if (!networkType.empty())
{
attrs["xrpl.network.type"] = networkType;
}
auto resourceAttrs = resource::Resource::Create(attrs);
// Create MeterProvider with resource, then attach the metric reader.
provider_ = metrics_sdk::MeterProviderFactory::Create(
std::make_unique<metrics_sdk::ViewRegistry>(), resourceAttrs);
provider_->AddMetricReader(std::move(reader));
// Configure histogram bucket boundaries for Event instruments.
// These match the SpanMetrics connector buckets for consistency.
auto histogramSelector = metrics_sdk::InstrumentSelectorFactory::Create(
metrics_sdk::InstrumentType::kHistogram, "*", "ms");
auto meterSelector = metrics_sdk::MeterSelectorFactory::Create("xrpld_metrics", "", "");
auto histogramConfig = std::make_shared<metrics_sdk::HistogramAggregationConfig>();
histogramConfig->boundaries_ =
std::vector<double>{1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 5000.0};
auto histogramView = metrics_sdk::ViewFactory::Create(
"default_histogram",
"Default histogram view with SpanMetrics-compatible buckets",
metrics_sdk::AggregationType::kHistogram,
std::move(histogramConfig));
provider_->AddView(
std::move(histogramSelector), std::move(meterSelector), std::move(histogramView));
// Create the OTel Meter for creating instruments.
otelMeter_ = provider_->GetMeter("xrpld_metrics", "1.0.0");
// 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())
{
@@ -758,11 +714,8 @@ OTelCollectorImp::~OTelCollectorImp()
{
journal_.info() << "OTelCollector shutting down";
}
if (provider_)
{
provider_->ForceFlush(std::chrono::milliseconds(2000));
provider_->Shutdown();
}
// 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";

View File

@@ -25,7 +25,23 @@
#include <opentelemetry/context/context.h>
#include <opentelemetry/exporters/otlp/otlp_http_exporter_factory.h>
#include <opentelemetry/exporters/otlp/otlp_http_exporter_options.h>
#include <opentelemetry/exporters/otlp/otlp_http_metric_exporter_factory.h>
#include <opentelemetry/exporters/otlp/otlp_http_metric_exporter_options.h>
#include <opentelemetry/metrics/meter.h>
#include <opentelemetry/metrics/meter_provider.h>
#include <opentelemetry/metrics/noop.h>
#include <opentelemetry/metrics/provider.h>
#include <opentelemetry/nostd/shared_ptr.h>
#include <opentelemetry/sdk/metrics/aggregation/aggregation_config.h>
#include <opentelemetry/sdk/metrics/export/periodic_exporting_metric_reader_factory.h>
#include <opentelemetry/sdk/metrics/export/periodic_exporting_metric_reader_options.h>
#include <opentelemetry/sdk/metrics/instruments.h>
#include <opentelemetry/sdk/metrics/meter_provider.h>
#include <opentelemetry/sdk/metrics/meter_provider_factory.h>
#include <opentelemetry/sdk/metrics/view/instrument_selector_factory.h>
#include <opentelemetry/sdk/metrics/view/meter_selector_factory.h>
#include <opentelemetry/sdk/metrics/view/view_factory.h>
#include <opentelemetry/sdk/metrics/view/view_registry.h>
#include <opentelemetry/sdk/resource/resource.h>
#include <opentelemetry/sdk/trace/batch_span_processor_factory.h>
#include <opentelemetry/sdk/trace/batch_span_processor_options.h>
@@ -50,6 +66,7 @@
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::telemetry {
@@ -57,6 +74,8 @@ namespace {
namespace trace_api = opentelemetry::trace;
namespace trace_sdk = opentelemetry::sdk::trace;
namespace metrics_api = opentelemetry::metrics;
namespace metrics_sdk = opentelemetry::sdk::metrics;
namespace otlp_http = opentelemetry::exporter::otlp;
namespace resource = opentelemetry::sdk::resource;
@@ -222,6 +241,16 @@ public:
return noopTracer;
}
opentelemetry::nostd::shared_ptr<metrics_api::Meter>
getMeter(std::string_view name) override
{
// Serve a meter from a process-wide noop provider, mirroring the
// noop tracer above. Instruments created from it are inert.
static auto noopProvider = opentelemetry::nostd::shared_ptr<metrics_api::MeterProvider>(
new metrics_api::NoopMeterProvider());
return noopProvider->GetMeter(std::string(name), std::string(kMeterVersion));
}
opentelemetry::nostd::shared_ptr<trace_api::Span>
startSpan(std::string_view, trace_api::SpanKind) override
{
@@ -259,6 +288,14 @@ class TelemetryImpl : public Telemetry
*/
std::shared_ptr<trace_sdk::TracerProvider> sdkProvider_;
/** The SDK MeterProvider that owns the metric export pipeline.
Symmetric with sdkProvider_ on the tracing side. Held as
std::shared_ptr so we can ForceFlush() on shutdown; wrapped in a
nostd::shared_ptr when registered as the global meter provider.
*/
std::shared_ptr<metrics_sdk::MeterProvider> meterProvider_;
public:
TelemetryImpl(Setup setup, beast::Journal journal) : setup_(std::move(setup)), journal_(journal)
{
@@ -331,6 +368,70 @@ public:
trace_api::Provider::SetTracerProvider(
opentelemetry::nostd::shared_ptr<trace_api::TracerProvider>(sdkProvider_));
// Build the metrics pipeline, parallel to the tracer above and
// reusing the same resourceAttrs so metrics and traces share one
// resource identity.
// Derive the metrics endpoint from the trace endpoint by swapping
// the trailing "/v1/traces" path for "/v1/metrics". Any other URL
// shape is used as-is.
std::string metricsEndpoint = setup_.exporterEndpoint;
constexpr std::string_view tracesPath{"/v1/traces"};
if (metricsEndpoint.ends_with(tracesPath))
{
metricsEndpoint.replace(
metricsEndpoint.size() - tracesPath.size(), tracesPath.size(), "/v1/metrics");
}
// Configure OTLP HTTP metric exporter, honoring the same TLS
// options as the trace exporter.
otlp_http::OtlpHttpMetricExporterOptions metricExporterOpts;
metricExporterOpts.url = metricsEndpoint;
if (setup_.useTls)
{
metricExporterOpts.ssl_ca_cert_path = setup_.tlsCertPath;
metricExporterOpts.ssl_client_cert_path = setup_.tlsClientCertPath;
metricExporterOpts.ssl_client_key_path = setup_.tlsClientKeyPath;
}
auto metricExporter = otlp_http::OtlpHttpMetricExporterFactory::Create(metricExporterOpts);
// Configure periodic metric reader (1-second export interval,
// matching the beast OTelCollector path).
metrics_sdk::PeriodicExportingMetricReaderOptions readerOpts;
readerOpts.export_interval_millis = std::chrono::milliseconds(1000);
readerOpts.export_timeout_millis = std::chrono::milliseconds(500);
auto reader = metrics_sdk::PeriodicExportingMetricReaderFactory::Create(
std::move(metricExporter), readerOpts);
// Create MeterProvider with the shared resource, then attach reader.
meterProvider_ = metrics_sdk::MeterProviderFactory::Create(
std::make_unique<metrics_sdk::ViewRegistry>(), resourceAttrs);
meterProvider_->AddMetricReader(std::move(reader));
// Histogram view: SpanMetrics-compatible bucket boundaries (ms) so
// histogram instruments align with the collector's SpanMetrics.
auto histogramSelector = metrics_sdk::InstrumentSelectorFactory::Create(
metrics_sdk::InstrumentType::kHistogram, "*", "ms");
auto meterSelector = metrics_sdk::MeterSelectorFactory::Create("xrpld_metrics", "", "");
auto histogramConfig = std::make_shared<metrics_sdk::HistogramAggregationConfig>();
histogramConfig->boundaries_ =
std::vector<double>{1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 5000.0};
auto histogramView = metrics_sdk::ViewFactory::Create(
"default_histogram",
"Default histogram view with SpanMetrics-compatible buckets",
metrics_sdk::AggregationType::kHistogram,
std::move(histogramConfig));
meterProvider_->AddView(
std::move(histogramSelector), std::move(meterSelector), std::move(histogramView));
// Publish as the global meter provider so developers (and the beast
// OTelCollector shim) reach the same pipeline.
metrics_api::Provider::SetMeterProvider(
opentelemetry::nostd::shared_ptr<metrics_api::MeterProvider>(meterProvider_));
// Register as the global Telemetry instance so SpanGuard factory
// methods can access it without callers passing a reference.
Telemetry::setInstance(this);
@@ -363,6 +464,18 @@ public:
opentelemetry::nostd::shared_ptr<trace_api::TracerProvider>(
new trace_api::NoopTracerProvider()));
}
if (meterProvider_)
{
// Mirror the tracer teardown: bounded flush, drop our provider,
// then restore a noop global provider. Same shutdown-order
// caveat as sdkProvider_ above applies to getMeter() callers.
meterProvider_->ForceFlush(std::chrono::milliseconds(5000));
meterProvider_.reset();
metrics_api::Provider::SetMeterProvider(
opentelemetry::nostd::shared_ptr<metrics_api::MeterProvider>(
new metrics_api::NoopMeterProvider()));
}
JLOG(journal_.info()) << "Telemetry stopped";
}
@@ -416,6 +529,15 @@ public:
return sdkProvider_->GetTracer(std::string(name));
}
opentelemetry::nostd::shared_ptr<metrics_api::Meter>
getMeter(std::string_view name = kMeterName) override
{
if (!meterProvider_)
return metrics_api::Provider::GetMeterProvider()->GetMeter(
std::string(name), std::string(kMeterVersion));
return meterProvider_->GetMeter(std::string(name), std::string(kMeterVersion));
}
opentelemetry::nostd::shared_ptr<trace_api::Span>
startSpan(std::string_view name, trace_api::SpanKind kind) override
{

View File

@@ -0,0 +1,111 @@
/** @file GetMeter.cpp
Unit tests for the direct OTel metrics API surface:
- xrpl::telemetry::Telemetry::getMeter() on the disabled (noop) path.
- The global MeterProvider contract that beast's OTelCollector shim
relies on (GetMeterProvider()->GetMeter(kMeterName, kMeterVersion)).
These tests deliberately exercise the global-provider level rather than a
fully started TelemetryImpl: TelemetryImpl::start() spins up an OTLP HTTP
exporter with background export threads and network connect attempts, which
is unsuitable for a hermetic unit test. The design spec permits testing the
metrics unlock at the provider level, which is the exact path the beast shim
and getMeter() delegate to.
Compiled only when XRPL_ENABLE_TELEMETRY is defined; the metrics API and SDK
headers exist only in that build.
*/
#ifdef XRPL_ENABLE_TELEMETRY
#include <xrpl/telemetry/Telemetry.h>
#include <gtest/gtest.h>
#include <opentelemetry/metrics/meter.h>
#include <opentelemetry/metrics/meter_provider.h>
#include <opentelemetry/metrics/provider.h>
#include <opentelemetry/metrics/sync_instruments.h>
#include <opentelemetry/nostd/shared_ptr.h>
#include <opentelemetry/sdk/metrics/meter_provider.h>
#include <opentelemetry/sdk/metrics/meter_provider_factory.h>
#include <cstdint>
#include <string>
using namespace xrpl;
using namespace xrpl::telemetry;
namespace metrics_api = opentelemetry::metrics;
namespace metrics_sdk = opentelemetry::sdk::metrics;
// ---------------------------------------------------------------
// 1. Disabled path: Telemetry::getMeter() returns a usable noop meter.
// makeTelemetry() with enabled=false yields NullTelemetryOtel, whose
// getMeter() serves a meter from a process-wide NoopMeterProvider. The
// meter and any instrument built from it must be non-null, and Add()
// (positive and negative) must be inert but safe.
// ---------------------------------------------------------------
TEST(GetMeter, disabled_path_returns_usable_noop_meter)
{
Telemetry::Setup setup;
setup.enabled = false;
beast::Journal::Sink& sink = beast::Journal::getNullSink();
beast::Journal const journal(sink);
auto telemetry = makeTelemetry(setup, journal);
ASSERT_NE(telemetry, nullptr);
// getMeter() must return a concrete (non-null) meter, never a raw nullptr.
auto meter = telemetry->getMeter();
ASSERT_TRUE(static_cast<bool>(meter));
// A synchronous UpDownCounter — one of the three instrument kinds the
// direct API unlocks — must be creatable and non-null.
auto upDown = meter->CreateInt64UpDownCounter("test_open_ledgers");
ASSERT_TRUE(static_cast<bool>(upDown));
// Both increment and decrement paths must execute without error (inert on
// the noop instrument, but exercised here to prove both directions work).
upDown->Add(static_cast<int64_t>(1));
upDown->Add(static_cast<int64_t>(-1));
}
// ---------------------------------------------------------------
// 2. Global-provider contract (the beast OTelCollector shim path).
// After a real SDK MeterProvider is registered via SetMeterProvider(),
// GetMeterProvider()->GetMeter(kMeterName, kMeterVersion) — the exact call
// the shim makes — must return a non-null meter, and an UpDownCounter built
// from it must be non-null and accept positive and negative Add().
// ---------------------------------------------------------------
TEST(GetMeter, global_provider_meter_accepts_updown_counter)
{
// Preserve and later restore the process-wide provider so this test does
// not leak state into other telemetry tests in the same binary.
auto const previous = metrics_api::Provider::GetMeterProvider();
// A views-less SDK MeterProvider with no reader is sufficient to prove the
// API contract: it hands out a real (non-noop) Meter that creates working
// instruments. No exporter/reader means no background threads or network.
std::shared_ptr<metrics_sdk::MeterProvider> sdkProvider =
metrics_sdk::MeterProviderFactory::Create();
metrics_api::Provider::SetMeterProvider(
opentelemetry::nostd::shared_ptr<metrics_api::MeterProvider>(sdkProvider));
// Fetch the meter exactly as the beast shim does: kMeterName + kMeterVersion
// from the global provider.
auto meter = metrics_api::Provider::GetMeterProvider()->GetMeter(
std::string(kMeterName), std::string(kMeterVersion));
ASSERT_TRUE(static_cast<bool>(meter));
auto upDown = meter->CreateInt64UpDownCounter("test_in_flight_requests");
ASSERT_TRUE(static_cast<bool>(upDown));
// Positive path: value goes up.
upDown->Add(static_cast<int64_t>(1));
// Negative path: UpDownCounter permits decrement (unlike a plain Counter).
upDown->Add(static_cast<int64_t>(-1));
// Restore the previous global provider.
metrics_api::Provider::SetMeterProvider(previous);
}
#endif // XRPL_ENABLE_TELEMETRY