From 9e9ee3554b341fab7e19a859524a443b8bc5aac8 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:56:01 +0000 Subject: [PATCH 1/9] stats: move implementation to source file, define interface Also add some basic docs for RawSender and privatize functions --- src/bench/checkblock.cpp | 6 ++- src/init.cpp | 2 +- src/stats/client.cpp | 99 +++++++++++++++++++--------------- src/stats/client.h | 49 ++++++----------- src/stats/rawsender.h | 12 +++++ src/test/util/setup_common.cpp | 2 +- 6 files changed, 90 insertions(+), 80 deletions(-) diff --git a/src/bench/checkblock.cpp b/src/bench/checkblock.cpp index b4ff0ee3373f..5e987bcefc66 100644 --- a/src/bench/checkblock.cpp +++ b/src/bench/checkblock.cpp @@ -11,6 +11,8 @@ #include #include +#include + // These are the two major time-sinks which happen after we have fully received // a block off the wire, but before we can relay the block on to peers using // compact block relay. @@ -38,8 +40,8 @@ static void DeserializeAndCheckBlockTest(benchmark::Bench& bench) ArgsManager bench_args; const auto chainParams = CreateChainParams(bench_args, CBaseChainParams::MAIN); // CheckBlock calls g_stats_client internally, we aren't using a testing setup - // so we need to do this manually. - ::g_stats_client = InitStatsClient(bench_args); + // so we need to do this manually. We can use the stub interface for this. + ::g_stats_client = std::make_unique(); bench.unit("block").run([&] { CBlock block; // Note that CBlock caches its checked state, so we need to recreate it here diff --git a/src/init.cpp b/src/init.cpp index d9d2cbde48ec..2fc3606de8d7 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1587,7 +1587,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // We need to initialize g_stats_client early as currently, g_stats_client is called // regardless of whether transmitting stats are desirable or not and if // g_stats_client isn't present when that attempt is made, the client will crash. - ::g_stats_client = InitStatsClient(args); + ::g_stats_client = StatsdClient::make(args); { diff --git a/src/stats/client.cpp b/src/stats/client.cpp index 7eb3c962c7dd..b473e17100dc 100644 --- a/src/stats/client.cpp +++ b/src/stats/client.cpp @@ -7,9 +7,12 @@ #include #include +#include #include +#include #include +#include #include #include @@ -27,11 +30,53 @@ static constexpr char STATSD_METRIC_COUNT[]{"c"}; static constexpr char STATSD_METRIC_GAUGE[]{"g"}; /** Characters used to denote Statsd message type as timing */ static constexpr char STATSD_METRIC_TIMING[]{"ms"}; + +class StatsdClientImpl final : public StatsdClient +{ +public: + explicit StatsdClientImpl(const std::string& host, uint16_t port, uint64_t batch_size, uint64_t interval_ms, + const std::string& prefix, const std::string& suffix); + ~StatsdClientImpl() = default; + +public: + bool dec(const std::string& key, float sample_rate) override { return count(key, -1, sample_rate); } + bool inc(const std::string& key, float sample_rate) override { return count(key, 1, sample_rate); } + bool count(const std::string& key, int64_t delta, float sample_rate) override { return _send(key, delta, STATSD_METRIC_COUNT, sample_rate); } + bool gauge(const std::string& key, int64_t value, float sample_rate) override { return _send(key, value, STATSD_METRIC_GAUGE, sample_rate); } + bool gaugeDouble(const std::string& key, double value, float sample_rate) override { return _send(key, value, STATSD_METRIC_GAUGE, sample_rate); } + bool timing(const std::string& key, uint64_t ms, float sample_rate) override { return _send(key, ms, STATSD_METRIC_TIMING, sample_rate); } + + bool send(const std::string& key, double value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool send(const std::string& key, int32_t value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool send(const std::string& key, int64_t value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool send(const std::string& key, uint32_t value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool send(const std::string& key, uint64_t value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } + + bool active() const override { return m_sender != nullptr; } + +private: + template + inline bool _send(const std::string& key, T1 value, const std::string& type, float sample_rate); + +private: + /* Mutex to protect PRNG */ + mutable Mutex cs; + /* PRNG used to dice-roll messages that are 0 < f < 1 */ + mutable FastRandomContext insecure_rand GUARDED_BY(cs); + + /* Broadcasts messages crafted by StatsdClient */ + std::unique_ptr m_sender{nullptr}; + + /* Phrase prepended to keys */ + const std::string m_prefix{}; + /* Phrase appended to keys */ + const std::string m_suffix{}; +}; } // anonymous namespace std::unique_ptr g_stats_client; -std::unique_ptr InitStatsClient(const ArgsManager& args) +std::unique_ptr StatsdClient::make(const ArgsManager& args) { auto sanitize_string = [](std::string string) { // Remove key delimiters from the front and back as they're added back in @@ -43,16 +88,18 @@ std::unique_ptr InitStatsClient(const ArgsManager& args) return string; }; - return std::make_unique(args.GetArg("-statshost", DEFAULT_STATSD_HOST), - args.GetIntArg("-statsport", DEFAULT_STATSD_PORT), - args.GetIntArg("-statsbatchsize", DEFAULT_STATSD_BATCH_SIZE), - args.GetIntArg("-statsduration", DEFAULT_STATSD_DURATION), - sanitize_string(args.GetArg("-statsprefix", DEFAULT_STATSD_PREFIX)), - sanitize_string(args.GetArg("-statssuffix", DEFAULT_STATSD_SUFFIX))); + return std::make_unique( + args.GetArg("-statshost", DEFAULT_STATSD_HOST), + args.GetIntArg("-statsport", DEFAULT_STATSD_PORT), + args.GetIntArg("-statsbatchsize", DEFAULT_STATSD_BATCH_SIZE), + args.GetIntArg("-statsduration", DEFAULT_STATSD_DURATION), + sanitize_string(args.GetArg("-statsprefix", DEFAULT_STATSD_PREFIX)), + sanitize_string(args.GetArg("-statssuffix", DEFAULT_STATSD_SUFFIX)) + ); } -StatsdClient::StatsdClient(const std::string& host, uint16_t port, uint64_t batch_size, uint64_t interval_ms, - const std::string& prefix, const std::string& suffix) : +StatsdClientImpl::StatsdClientImpl(const std::string& host, uint16_t port, uint64_t batch_size, uint64_t interval_ms, + const std::string& prefix, const std::string& suffix) : m_prefix{[prefix]() { return !prefix.empty() ? prefix + STATSD_NS_DELIMITER : prefix; }()}, m_suffix{[suffix]() { return !suffix.empty() ? STATSD_NS_DELIMITER + suffix : suffix; }()} { @@ -74,34 +121,8 @@ StatsdClient::StatsdClient(const std::string& host, uint16_t port, uint64_t batc LogPrintf("StatsdClient initialized to transmit stats to %s:%d\n", host, port); } -StatsdClient::~StatsdClient() {} - -bool StatsdClient::dec(const std::string& key, float sample_rate) { return count(key, -1, sample_rate); } - -bool StatsdClient::inc(const std::string& key, float sample_rate) { return count(key, 1, sample_rate); } - -bool StatsdClient::count(const std::string& key, int64_t delta, float sample_rate) -{ - return send(key, delta, STATSD_METRIC_COUNT, sample_rate); -} - -bool StatsdClient::gauge(const std::string& key, int64_t value, float sample_rate) -{ - return send(key, value, STATSD_METRIC_GAUGE, sample_rate); -} - -bool StatsdClient::gaugeDouble(const std::string& key, double value, float sample_rate) -{ - return send(key, value, STATSD_METRIC_GAUGE, sample_rate); -} - -bool StatsdClient::timing(const std::string& key, uint64_t ms, float sample_rate) -{ - return send(key, ms, STATSD_METRIC_TIMING, sample_rate); -} - template -bool StatsdClient::send(const std::string& key, T1 value, const std::string& type, float sample_rate) +inline bool StatsdClientImpl::_send(const std::string& key, T1 value, const std::string& type, float sample_rate) { static_assert(std::is_arithmetic::value, "Must specialize to an arithmetic type"); @@ -132,9 +153,3 @@ bool StatsdClient::send(const std::string& key, T1 value, const std::string& typ return true; } - -template bool StatsdClient::send(const std::string& key, double value, const std::string& type, float sample_rate); -template bool StatsdClient::send(const std::string& key, int32_t value, const std::string& type, float sample_rate); -template bool StatsdClient::send(const std::string& key, int64_t value, const std::string& type, float sample_rate); -template bool StatsdClient::send(const std::string& key, uint32_t value, const std::string& type, float sample_rate); -template bool StatsdClient::send(const std::string& key, uint64_t value, const std::string& type, float sample_rate); diff --git a/src/stats/client.h b/src/stats/client.h index e5e8fe7c9a98..7cd819c4dfcd 100644 --- a/src/stats/client.h +++ b/src/stats/client.h @@ -7,14 +7,11 @@ #ifndef BITCOIN_STATS_CLIENT_H #define BITCOIN_STATS_CLIENT_H -#include -#include - +#include #include #include class ArgsManager; -class RawSender; /** Default port used to connect to a Statsd server */ static constexpr uint16_t DEFAULT_STATSD_PORT{8125}; @@ -39,44 +36,28 @@ static constexpr int MAX_STATSD_PERIOD{60 * 60}; class StatsdClient { public: - explicit StatsdClient(const std::string& host, uint16_t port, uint64_t batch_size, uint64_t interval_ms, - const std::string& prefix, const std::string& suffix); - ~StatsdClient(); + static std::unique_ptr make(const ArgsManager& args); + virtual ~StatsdClient() = default; -public: /* Statsd-defined APIs */ - bool dec(const std::string& key, float sample_rate = 1.f); - bool inc(const std::string& key, float sample_rate = 1.f); - bool count(const std::string& key, int64_t delta, float sample_rate = 1.f); - bool gauge(const std::string& key, int64_t value, float sample_rate = 1.f); - bool gaugeDouble(const std::string& key, double value, float sample_rate = 1.f); - bool timing(const std::string& key, uint64_t ms, float sample_rate = 1.f); + virtual bool dec(const std::string& key, float sample_rate = 1.f) { return false; } + virtual bool inc(const std::string& key, float sample_rate = 1.f) { return false; } + virtual bool count(const std::string& key, int64_t delta, float sample_rate = 1.f) { return false; } + virtual bool gauge(const std::string& key, int64_t value, float sample_rate = 1.f) { return false; } + virtual bool gaugeDouble(const std::string& key, double value, float sample_rate = 1.f) { return false; } + virtual bool timing(const std::string& key, uint64_t ms, float sample_rate = 1.f) { return false; } /* Statsd-compatible APIs */ - template - bool send(const std::string& key, T1 value, const std::string& type, float sample_rate = 1.f); + virtual bool send(const std::string& key, double value, const std::string& type, float sample_rate = 1.f) { return false; } + virtual bool send(const std::string& key, int32_t value, const std::string& type, float sample_rate = 1.f) { return false; } + virtual bool send(const std::string& key, int64_t value, const std::string& type, float sample_rate = 1.f) { return false; } + virtual bool send(const std::string& key, uint32_t value, const std::string& type, float sample_rate = 1.f) { return false; } + virtual bool send(const std::string& key, uint64_t value, const std::string& type, float sample_rate = 1.f) { return false; } /* Check if a StatsdClient instance is ready to send messages */ - bool active() const { return m_sender != nullptr; } - -private: - /* Mutex to protect PRNG */ - mutable Mutex cs; - /* PRNG used to dice-roll messages that are 0 < f < 1 */ - mutable FastRandomContext insecure_rand GUARDED_BY(cs); - - /* Broadcasts messages crafted by StatsdClient */ - std::unique_ptr m_sender{nullptr}; - - /* Phrase prepended to keys */ - const std::string m_prefix{""}; - /* Phrase appended to keys */ - const std::string m_suffix{""}; + virtual bool active() const { return false; } }; -/** Parses arguments and constructs a StatsdClient instance */ -std::unique_ptr InitStatsClient(const ArgsManager& args); - /** Global smart pointer containing StatsdClient instance */ extern std::unique_ptr g_stats_client; diff --git a/src/stats/rawsender.h b/src/stats/rawsender.h index 65941f80dc5a..c6cdc0882018 100644 --- a/src/stats/rawsender.h +++ b/src/stats/rawsender.h @@ -63,14 +63,26 @@ class RawSender RawSender& operator=(const RawSender&) = delete; RawSender(RawSender&&) = delete; + //! Request a message to be sent based on configuration (queueing, batching) std::optional Send(const RawMessage& msg) EXCLUSIVE_LOCKS_REQUIRED(!cs); + +private: + //! Send a message directly using ::send{,to}() std::optional SendDirectly(const RawMessage& msg); + //! Get target server address as string std::string ToStringHostPort() const; + //! Add message to queue void QueueAdd(const RawMessage& msg) EXCLUSIVE_LOCKS_REQUIRED(!cs); + + //! Send all messages in queue of RawSender entity and flush it void QueueFlush() EXCLUSIVE_LOCKS_REQUIRED(!cs); + + //! Send all messages in given queue and flush it void QueueFlush(std::deque& queue); + + //! Worker thread function if queueing is requested void QueueThreadMain() EXCLUSIVE_LOCKS_REQUIRED(!cs); private: diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 423abe4860d3..9f99263e82a8 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -188,7 +188,7 @@ BasicTestingSetup::BasicTestingSetup(const std::string& chainName, const std::ve SetupNetworking(); InitSignatureCache(); InitScriptExecutionCache(); - ::g_stats_client = InitStatsClient(*m_node.args); + ::g_stats_client = StatsdClient::make(*m_node.args); m_node.chain = interfaces::MakeChain(m_node); m_node.netgroupman = std::make_unique(/*asmap=*/std::vector()); From 1508f4a82810d4dcb267e74f4e7abba7f1a99265 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 1 Sep 2025 03:30:04 +0000 Subject: [PATCH 2/9] stats: use string_view for fixed values --- src/stats/client.cpp | 28 ++++++++++++++-------------- src/stats/client.h | 23 ++++++++++++----------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/stats/client.cpp b/src/stats/client.cpp index b473e17100dc..c5664606ea1d 100644 --- a/src/stats/client.cpp +++ b/src/stats/client.cpp @@ -39,24 +39,24 @@ class StatsdClientImpl final : public StatsdClient ~StatsdClientImpl() = default; public: - bool dec(const std::string& key, float sample_rate) override { return count(key, -1, sample_rate); } - bool inc(const std::string& key, float sample_rate) override { return count(key, 1, sample_rate); } - bool count(const std::string& key, int64_t delta, float sample_rate) override { return _send(key, delta, STATSD_METRIC_COUNT, sample_rate); } - bool gauge(const std::string& key, int64_t value, float sample_rate) override { return _send(key, value, STATSD_METRIC_GAUGE, sample_rate); } - bool gaugeDouble(const std::string& key, double value, float sample_rate) override { return _send(key, value, STATSD_METRIC_GAUGE, sample_rate); } - bool timing(const std::string& key, uint64_t ms, float sample_rate) override { return _send(key, ms, STATSD_METRIC_TIMING, sample_rate); } - - bool send(const std::string& key, double value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } - bool send(const std::string& key, int32_t value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } - bool send(const std::string& key, int64_t value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } - bool send(const std::string& key, uint32_t value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } - bool send(const std::string& key, uint64_t value, const std::string& type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool dec(std::string_view key, float sample_rate) override { return count(key, -1, sample_rate); } + bool inc(std::string_view key, float sample_rate) override { return count(key, 1, sample_rate); } + bool count(std::string_view key, int64_t delta, float sample_rate) override { return _send(key, delta, STATSD_METRIC_COUNT, sample_rate); } + bool gauge(std::string_view key, int64_t value, float sample_rate) override { return _send(key, value, STATSD_METRIC_GAUGE, sample_rate); } + bool gaugeDouble(std::string_view key, double value, float sample_rate) override { return _send(key, value, STATSD_METRIC_GAUGE, sample_rate); } + bool timing(std::string_view key, uint64_t ms, float sample_rate) override { return _send(key, ms, STATSD_METRIC_TIMING, sample_rate); } + + bool send(std::string_view key, double value, std::string_view type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool send(std::string_view key, int32_t value, std::string_view type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool send(std::string_view key, int64_t value, std::string_view type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool send(std::string_view key, uint32_t value, std::string_view type, float sample_rate) override { return _send(key, value, type, sample_rate); } + bool send(std::string_view key, uint64_t value, std::string_view type, float sample_rate) override { return _send(key, value, type, sample_rate); } bool active() const override { return m_sender != nullptr; } private: template - inline bool _send(const std::string& key, T1 value, const std::string& type, float sample_rate); + inline bool _send(std::string_view key, T1 value, std::string_view type, float sample_rate); private: /* Mutex to protect PRNG */ @@ -122,7 +122,7 @@ StatsdClientImpl::StatsdClientImpl(const std::string& host, uint16_t port, uint6 } template -inline bool StatsdClientImpl::_send(const std::string& key, T1 value, const std::string& type, float sample_rate) +inline bool StatsdClientImpl::_send(std::string_view key, T1 value, std::string_view type, float sample_rate) { static_assert(std::is_arithmetic::value, "Must specialize to an arithmetic type"); diff --git a/src/stats/client.h b/src/stats/client.h index 7cd819c4dfcd..b7b4440b6baf 100644 --- a/src/stats/client.h +++ b/src/stats/client.h @@ -10,6 +10,7 @@ #include #include #include +#include class ArgsManager; @@ -40,19 +41,19 @@ class StatsdClient virtual ~StatsdClient() = default; /* Statsd-defined APIs */ - virtual bool dec(const std::string& key, float sample_rate = 1.f) { return false; } - virtual bool inc(const std::string& key, float sample_rate = 1.f) { return false; } - virtual bool count(const std::string& key, int64_t delta, float sample_rate = 1.f) { return false; } - virtual bool gauge(const std::string& key, int64_t value, float sample_rate = 1.f) { return false; } - virtual bool gaugeDouble(const std::string& key, double value, float sample_rate = 1.f) { return false; } - virtual bool timing(const std::string& key, uint64_t ms, float sample_rate = 1.f) { return false; } + virtual bool dec(std::string_view key, float sample_rate = 1.f) { return false; } + virtual bool inc(std::string_view key, float sample_rate = 1.f) { return false; } + virtual bool count(std::string_view key, int64_t delta, float sample_rate = 1.f) { return false; } + virtual bool gauge(std::string_view key, int64_t value, float sample_rate = 1.f) { return false; } + virtual bool gaugeDouble(std::string_view key, double value, float sample_rate = 1.f) { return false; } + virtual bool timing(std::string_view key, uint64_t ms, float sample_rate = 1.f) { return false; } /* Statsd-compatible APIs */ - virtual bool send(const std::string& key, double value, const std::string& type, float sample_rate = 1.f) { return false; } - virtual bool send(const std::string& key, int32_t value, const std::string& type, float sample_rate = 1.f) { return false; } - virtual bool send(const std::string& key, int64_t value, const std::string& type, float sample_rate = 1.f) { return false; } - virtual bool send(const std::string& key, uint32_t value, const std::string& type, float sample_rate = 1.f) { return false; } - virtual bool send(const std::string& key, uint64_t value, const std::string& type, float sample_rate = 1.f) { return false; } + virtual bool send(std::string_view key, double value, std::string_view type, float sample_rate = 1.f) { return false; } + virtual bool send(std::string_view key, int32_t value, std::string_view type, float sample_rate = 1.f) { return false; } + virtual bool send(std::string_view key, int64_t value, std::string_view type, float sample_rate = 1.f) { return false; } + virtual bool send(std::string_view key, uint32_t value, std::string_view type, float sample_rate = 1.f) { return false; } + virtual bool send(std::string_view key, uint64_t value, std::string_view type, float sample_rate = 1.f) { return false; } /* Check if a StatsdClient instance is ready to send messages */ virtual bool active() const { return false; } From 8d7e74fb8f1e86b568e91ce5f7a3780e7b2b61dc Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:38:25 +0000 Subject: [PATCH 3/9] stats: bubble errors up, halt init if stats client reports errors Also we should initialize the Statsd client *after* setting g_socket_events_mode or risk a crash when we try to establish a connection later. --- src/init.cpp | 16 ++++++++---- src/stats/client.cpp | 46 ++++++++++++++++++---------------- src/stats/client.h | 4 ++- src/stats/rawsender.cpp | 12 ++++----- src/stats/rawsender.h | 5 ++-- src/test/util/setup_common.cpp | 8 +++++- 6 files changed, 54 insertions(+), 37 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 2fc3606de8d7..7411cd5b4168 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1584,11 +1584,6 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) fDiscover = args.GetBoolArg("-discover", true); const bool ignores_incoming_txs{args.GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY)}; - // We need to initialize g_stats_client early as currently, g_stats_client is called - // regardless of whether transmitting stats are desirable or not and if - // g_stats_client isn't present when that attempt is made, the client will crash. - ::g_stats_client = StatsdClient::make(args); - { // Read asmap file if configured @@ -1631,6 +1626,17 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) return InitError(strprintf(_("Invalid -socketevents ('%s') specified. Only these modes are supported: %s"), sem_str, GetSupportedSocketEventsStr())); } + // We need to initialize g_stats_client early as currently, g_stats_client is called + // regardless of whether transmitting stats are desirable or not and if + // g_stats_client isn't present when that attempt is made, the client will crash. + { + auto stats_client = StatsdClient::make(args); + if (!stats_client) { + return InitError(_("Cannot init Statsd client") + Untranslated(" (") + util::ErrorString(stats_client) + Untranslated(")")); + } + ::g_stats_client = std::move(*stats_client); + } + assert(!node.banman); node.banman = std::make_unique(gArgs.GetDataDirNet() / "banlist", &uiInterface, args.GetIntArg("-bantime", DEFAULT_MISBEHAVING_BANTIME)); assert(!node.connman); diff --git a/src/stats/client.cpp b/src/stats/client.cpp index c5664606ea1d..d46f24064a47 100644 --- a/src/stats/client.cpp +++ b/src/stats/client.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include @@ -35,7 +37,7 @@ class StatsdClientImpl final : public StatsdClient { public: explicit StatsdClientImpl(const std::string& host, uint16_t port, uint64_t batch_size, uint64_t interval_ms, - const std::string& prefix, const std::string& suffix); + const std::string& prefix, const std::string& suffix, std::optional& error); ~StatsdClientImpl() = default; public: @@ -76,8 +78,14 @@ class StatsdClientImpl final : public StatsdClient std::unique_ptr g_stats_client; -std::unique_ptr StatsdClient::make(const ArgsManager& args) +util::Result> StatsdClient::make(const ArgsManager& args) { + const auto host = args.GetArg("-statshost", DEFAULT_STATSD_HOST); + if (host.empty()) { + LogPrintf("Transmitting stats are disabled, will not init Statsd client\n"); + return std::make_unique(); + } + auto sanitize_string = [](std::string string) { // Remove key delimiters from the front and back as they're added back in // the constructor @@ -88,32 +96,30 @@ std::unique_ptr StatsdClient::make(const ArgsManager& args) return string; }; - return std::make_unique( - args.GetArg("-statshost", DEFAULT_STATSD_HOST), - args.GetIntArg("-statsport", DEFAULT_STATSD_PORT), + std::optional error_opt; + auto statsd_ptr = std::make_unique( + host, args.GetIntArg("-statsport", DEFAULT_STATSD_PORT), args.GetIntArg("-statsbatchsize", DEFAULT_STATSD_BATCH_SIZE), args.GetIntArg("-statsduration", DEFAULT_STATSD_DURATION), sanitize_string(args.GetArg("-statsprefix", DEFAULT_STATSD_PREFIX)), - sanitize_string(args.GetArg("-statssuffix", DEFAULT_STATSD_SUFFIX)) - ); + sanitize_string(args.GetArg("-statssuffix", DEFAULT_STATSD_SUFFIX)), error_opt); + if (error_opt.has_value()) { + statsd_ptr.reset(); + return util::Error{error_opt.value()}; + } + return {std::move(statsd_ptr)}; } StatsdClientImpl::StatsdClientImpl(const std::string& host, uint16_t port, uint64_t batch_size, uint64_t interval_ms, - const std::string& prefix, const std::string& suffix) : + const std::string& prefix, const std::string& suffix, + std::optional& error) : m_prefix{[prefix]() { return !prefix.empty() ? prefix + STATSD_NS_DELIMITER : prefix; }()}, m_suffix{[suffix]() { return !suffix.empty() ? STATSD_NS_DELIMITER + suffix : suffix; }()} { - if (host.empty()) { - LogPrintf("Transmitting stats are disabled, will not init StatsdClient\n"); - return; - } - - std::optional error_opt; m_sender = std::make_unique(host, port, std::make_pair(batch_size, static_cast(STATSD_MSG_DELIMITER)), - interval_ms, error_opt); - if (error_opt.has_value()) { - LogPrintf("ERROR: %s, cannot initialize StatsdClient.\n", error_opt.value()); + interval_ms, error); + if (error.has_value()) { m_sender.reset(); return; } @@ -126,10 +132,6 @@ inline bool StatsdClientImpl::_send(std::string_view key, T1 value, std::string_ { static_assert(std::is_arithmetic::value, "Must specialize to an arithmetic type"); - if (!m_sender) { - return false; - } - // Determine if we should send the message at all but claim that we did even if we don't sample_rate = std::clamp(sample_rate, 0.f, 1.f); bool always_send = std::fabs(sample_rate - 1.f) < EPSILON; @@ -146,7 +148,7 @@ inline bool StatsdClientImpl::_send(std::string_view key, T1 value, std::string_ } // Send it and report an error if we encounter one - if (auto error_opt = m_sender->Send(msg); error_opt.has_value()) { + if (auto error_opt = Assert(m_sender)->Send(msg); error_opt.has_value()) { LogPrintf("ERROR: %s.\n", error_opt.value()); return false; } diff --git a/src/stats/client.h b/src/stats/client.h index b7b4440b6baf..b9484f8f5cb9 100644 --- a/src/stats/client.h +++ b/src/stats/client.h @@ -7,6 +7,8 @@ #ifndef BITCOIN_STATS_CLIENT_H #define BITCOIN_STATS_CLIENT_H +#include + #include #include #include @@ -37,7 +39,7 @@ static constexpr int MAX_STATSD_PERIOD{60 * 60}; class StatsdClient { public: - static std::unique_ptr make(const ArgsManager& args); + static util::Result> make(const ArgsManager& args); virtual ~StatsdClient() = default; /* Statsd-defined APIs */ diff --git a/src/stats/rawsender.cpp b/src/stats/rawsender.cpp index 8b56875f2fab..3f72e2207d10 100644 --- a/src/stats/rawsender.cpp +++ b/src/stats/rawsender.cpp @@ -12,34 +12,34 @@ #include RawSender::RawSender(const std::string& host, uint16_t port, std::pair batching_opts, - uint64_t interval_ms, std::optional& error) : + uint64_t interval_ms, std::optional& error) : m_host{host}, m_port{port}, m_batching_opts{batching_opts}, m_interval_ms{interval_ms} { if (host.empty()) { - error = "No host specified"; + error = _("No host specified"); return; } if (auto netaddr = LookupHost(m_host, /*fAllowLookup=*/true); netaddr.has_value()) { if (!netaddr->IsIPv4()) { - error = strprintf("Host %s on unsupported network", m_host); + error = strprintf(_("Host %s on unsupported network"), m_host); return; } if (!CService(*netaddr, port).GetSockAddr(reinterpret_cast(&m_server.first), &m_server.second)) { - error = strprintf("Cannot get socket address for %s", m_host); + error = strprintf(_("Cannot get socket address for %s"), m_host); return; } } else { - error = strprintf("Unable to lookup host %s", m_host); + error = strprintf(_("Unable to lookup host %s"), m_host); return; } SOCKET hSocket = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (hSocket == INVALID_SOCKET) { - error = strprintf("Cannot create socket (socket() returned error %s)", NetworkErrorString(WSAGetLastError())); + error = strprintf(_("Cannot create socket (socket() returned error %s)"), NetworkErrorString(WSAGetLastError())); return; } m_sock = std::make_unique(hSocket); diff --git a/src/stats/rawsender.h b/src/stats/rawsender.h index c6cdc0882018..911e5d9a05a3 100644 --- a/src/stats/rawsender.h +++ b/src/stats/rawsender.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -55,8 +56,8 @@ struct RawMessage : public std::vector class RawSender { public: - RawSender(const std::string& host, uint16_t port, std::pair batching_opts, - uint64_t interval_ms, std::optional& error); + RawSender(const std::string& host, uint16_t port, std::pair batching_opts, uint64_t interval_ms, + std::optional& error); ~RawSender(); RawSender(const RawSender&) = delete; diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 9f99263e82a8..7942e3c2a57f 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -188,7 +188,6 @@ BasicTestingSetup::BasicTestingSetup(const std::string& chainName, const std::ve SetupNetworking(); InitSignatureCache(); InitScriptExecutionCache(); - ::g_stats_client = StatsdClient::make(*m_node.args); m_node.chain = interfaces::MakeChain(m_node); m_node.netgroupman = std::make_unique(/*asmap=*/std::vector()); @@ -203,6 +202,13 @@ BasicTestingSetup::BasicTestingSetup(const std::string& chainName, const std::ve strprintf("Invalid -socketevents ('%s') specified. Only these modes are supported: %s", sem_str, GetSupportedSocketEventsStr())); } + { + auto stats_client = StatsdClient::make(*m_node.args); + if (!stats_client) { + throw std::runtime_error{strprintf("Cannot init Statsd client (%s)", util::ErrorString(stats_client).original)}; + } + ::g_stats_client = std::move(*stats_client); + } m_node.connman = std::make_unique(0x1337, 0x1337, *m_node.addrman, *m_node.netgroupman); // Deterministic randomness for tests. From fced9f66c7d37adfc85e75057cff1ae67aa0752c Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:56:24 +0000 Subject: [PATCH 4/9] stats: add stricter validation for arguments --- src/stats/client.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/stats/client.cpp b/src/stats/client.cpp index d46f24064a47..2af9b5111f76 100644 --- a/src/stats/client.cpp +++ b/src/stats/client.cpp @@ -16,6 +16,7 @@ #include #include +#include #include namespace { @@ -86,6 +87,22 @@ util::Result> StatsdClient::make(const ArgsManager return std::make_unique(); } + const int64_t batch_size = args.GetIntArg("-statsbatchsize", DEFAULT_STATSD_BATCH_SIZE); + if (batch_size < 0) { + return util::Error{_("-statsbatchsize cannot be configured with a negative value.")}; + } + + const int64_t interval_ms = args.GetIntArg("-statsduration", DEFAULT_STATSD_DURATION); + if (interval_ms < 0) { + return util::Error{_("-statsduration cannot be configured with a negative value.")}; + } + + const int64_t port = args.GetIntArg("-statsport", DEFAULT_STATSD_PORT); + if (port < 1 || port > std::numeric_limits::max()) { + return util::Error{strprintf(_("Port must be between %d and %d, supplied %d"), 1, + std::numeric_limits::max(), port)}; + } + auto sanitize_string = [](std::string string) { // Remove key delimiters from the front and back as they're added back in // the constructor @@ -98,9 +115,7 @@ util::Result> StatsdClient::make(const ArgsManager std::optional error_opt; auto statsd_ptr = std::make_unique( - host, args.GetIntArg("-statsport", DEFAULT_STATSD_PORT), - args.GetIntArg("-statsbatchsize", DEFAULT_STATSD_BATCH_SIZE), - args.GetIntArg("-statsduration", DEFAULT_STATSD_DURATION), + host, port, batch_size, interval_ms, sanitize_string(args.GetArg("-statsprefix", DEFAULT_STATSD_PREFIX)), sanitize_string(args.GetArg("-statssuffix", DEFAULT_STATSD_SUFFIX)), error_opt); if (error_opt.has_value()) { From 131d536f27997a3218c60b007301516f76a800e4 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:28:18 +0000 Subject: [PATCH 5/9] stats: use `bilingual_str` for RawSender error messages --- src/stats/client.cpp | 2 +- src/stats/rawsender.cpp | 8 ++++---- src/stats/rawsender.h | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/stats/client.cpp b/src/stats/client.cpp index 2af9b5111f76..5e2f61ae5c28 100644 --- a/src/stats/client.cpp +++ b/src/stats/client.cpp @@ -164,7 +164,7 @@ inline bool StatsdClientImpl::_send(std::string_view key, T1 value, std::string_ // Send it and report an error if we encounter one if (auto error_opt = Assert(m_sender)->Send(msg); error_opt.has_value()) { - LogPrintf("ERROR: %s.\n", error_opt.value()); + LogPrintf("ERROR: %s.\n", error_opt->original); return false; } diff --git a/src/stats/rawsender.cpp b/src/stats/rawsender.cpp index 3f72e2207d10..f849682d70c0 100644 --- a/src/stats/rawsender.cpp +++ b/src/stats/rawsender.cpp @@ -68,7 +68,7 @@ RawSender::~RawSender() m_host, m_port, m_successes, m_failures); } -std::optional RawSender::Send(const RawMessage& msg) +std::optional RawSender::Send(const RawMessage& msg) { // If there is a thread, append to queue if (m_thread.joinable()) { @@ -79,11 +79,11 @@ std::optional RawSender::Send(const RawMessage& msg) return SendDirectly(msg); } -std::optional RawSender::SendDirectly(const RawMessage& msg) +std::optional RawSender::SendDirectly(const RawMessage& msg) { if (!m_sock) { m_failures++; - return "Socket not initialized, cannot send message"; + return _("Socket not initialized, cannot send message"); } if (::sendto(m_sock->Get(), reinterpret_cast(msg.data()), @@ -94,7 +94,7 @@ std::optional RawSender::SendDirectly(const RawMessage& msg) #endif // WIN32 /*flags=*/0, reinterpret_cast(&m_server.first), m_server.second) == SOCKET_ERROR) { m_failures++; - return strprintf("Unable to send message to %s (sendto() returned error %s)", this->ToStringHostPort(), + return strprintf(_("Unable to send message to %s (::sendto() returned error %s)"), this->ToStringHostPort(), NetworkErrorString(WSAGetLastError())); } diff --git a/src/stats/rawsender.h b/src/stats/rawsender.h index 911e5d9a05a3..fc78ff650772 100644 --- a/src/stats/rawsender.h +++ b/src/stats/rawsender.h @@ -65,11 +65,11 @@ class RawSender RawSender(RawSender&&) = delete; //! Request a message to be sent based on configuration (queueing, batching) - std::optional Send(const RawMessage& msg) EXCLUSIVE_LOCKS_REQUIRED(!cs); + std::optional Send(const RawMessage& msg) EXCLUSIVE_LOCKS_REQUIRED(!cs); private: //! Send a message directly using ::send{,to}() - std::optional SendDirectly(const RawMessage& msg); + std::optional SendDirectly(const RawMessage& msg); //! Get target server address as string std::string ToStringHostPort() const; From 3640071485312397cd36326ce8c4a5b9734b08fe Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 16 Oct 2024 20:10:37 +0000 Subject: [PATCH 6/9] stats: extend connection support to IPv6 --- src/stats/rawsender.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stats/rawsender.cpp b/src/stats/rawsender.cpp index f849682d70c0..74647eed131e 100644 --- a/src/stats/rawsender.cpp +++ b/src/stats/rawsender.cpp @@ -24,7 +24,7 @@ RawSender::RawSender(const std::string& host, uint16_t port, std::pairIsIPv4()) { + if (!netaddr->IsIPv4() && !netaddr->IsIPv6()) { error = strprintf(_("Host %s on unsupported network"), m_host); return; } @@ -37,7 +37,7 @@ RawSender::RawSender(const std::string& host, uint16_t port, std::pair(&m_server.first)->sa_family, SOCK_DGRAM, IPPROTO_UDP); if (hSocket == INVALID_SOCKET) { error = strprintf(_("Cannot create socket (socket() returned error %s)"), NetworkErrorString(WSAGetLastError())); return; From 592aa0408327ae4050f768750225d5922223e3af Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:58:51 +0000 Subject: [PATCH 7/9] stats: add support for URLs in `statshost`, deprecate `statsport` --- src/init.cpp | 2 +- src/stats/client.cpp | 60 ++++++++++++++++++++++++++++++++++++++++---- src/stats/client.h | 2 -- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 7411cd5b4168..25b2ca72090c 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -783,7 +783,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-statsbatchsize=", strprintf("Specify the size of each batch of stats messages (default: %d)", DEFAULT_STATSD_BATCH_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::STATSD); argsman.AddArg("-statsduration=", strprintf("Specify the number of milliseconds between stats messages (default: %d)", DEFAULT_STATSD_DURATION), ArgsManager::ALLOW_ANY, OptionsCategory::STATSD); argsman.AddArg("-statshost=", strprintf("Specify statsd host (default: %s)", DEFAULT_STATSD_HOST), ArgsManager::ALLOW_ANY, OptionsCategory::STATSD); - argsman.AddArg("-statsport=", strprintf("Specify statsd port (default: %u)", DEFAULT_STATSD_PORT), ArgsManager::ALLOW_ANY, OptionsCategory::STATSD); + hidden_args.emplace_back("-statsport"); argsman.AddArg("-statsperiod=", strprintf("Specify the number of seconds between periodic measurements (default: %d)", DEFAULT_STATSD_PERIOD), ArgsManager::ALLOW_ANY, OptionsCategory::STATSD); argsman.AddArg("-statsprefix=", strprintf("Specify an optional string prepended to every stats key (default: %s)", DEFAULT_STATSD_PREFIX), ArgsManager::ALLOW_ANY, OptionsCategory::STATSD); argsman.AddArg("-statssuffix=", strprintf("Specify an optional string appended to every stats key (default: %s)", DEFAULT_STATSD_SUFFIX), ArgsManager::ALLOW_ANY, OptionsCategory::STATSD); diff --git a/src/stats/client.cpp b/src/stats/client.cpp index 5e2f61ae5c28..d0e3a938e1e1 100644 --- a/src/stats/client.cpp +++ b/src/stats/client.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,11 @@ namespace { /** Threshold below which a value is considered effectively zero */ static constexpr float EPSILON{0.0001f}; +/** Delimiter segmenting scheme from the rest of the URL */ +static constexpr std::string_view URL_SCHEME_DELIMITER{"://"}; + +/** Default port used to connect to a Statsd server */ +static constexpr uint16_t DEFAULT_STATSD_PORT{8125}; /** Delimiter segmenting two fully formed Statsd messages */ static constexpr char STATSD_MSG_DELIMITER{'\n'}; @@ -81,7 +87,7 @@ std::unique_ptr g_stats_client; util::Result> StatsdClient::make(const ArgsManager& args) { - const auto host = args.GetArg("-statshost", DEFAULT_STATSD_HOST); + auto host = args.GetArg("-statshost", DEFAULT_STATSD_HOST); if (host.empty()) { LogPrintf("Transmitting stats are disabled, will not init Statsd client\n"); return std::make_unique(); @@ -97,10 +103,54 @@ util::Result> StatsdClient::make(const ArgsManager return util::Error{_("-statsduration cannot be configured with a negative value.")}; } - const int64_t port = args.GetIntArg("-statsport", DEFAULT_STATSD_PORT); - if (port < 1 || port > std::numeric_limits::max()) { - return util::Error{strprintf(_("Port must be between %d and %d, supplied %d"), 1, - std::numeric_limits::max(), port)}; + auto port_arg = args.GetIntArg("-statsport", DEFAULT_STATSD_PORT); + if (args.IsArgSet("-statsport")) { + // Port range validation if -statsport is specified. + if (port_arg < 1 || port_arg > std::numeric_limits::max()) { + return util::Error{strprintf(_("Port must be between %d and %d, supplied %d"), 1, + std::numeric_limits::max(), port_arg)}; + } + } + uint16_t port = static_cast(port_arg); + + // Could be a URL, try to parse it. + const size_t scheme_idx{host.find(URL_SCHEME_DELIMITER)}; + if (scheme_idx != std::string::npos) { + // Parse the scheme and trim it out of the URL if we succeed + if (scheme_idx == 0) { + return util::Error{_("No text before the scheme delimiter, malformed URL")}; + } + std::string scheme{ToLower(host.substr(/*pos=*/0, scheme_idx))}; + if (scheme != "udp") { + return util::Error{_("Unsupported URL scheme, must begin with udp://")}; + } + host = host.substr(scheme_idx + URL_SCHEME_DELIMITER.length()); + + // Strip trailing slashes and parse the port + const size_t colon_idx{host.rfind(':')}; + if (colon_idx != std::string::npos) { + // Remove all forward slashes found after the port delimiter (colon) + host = std::string( + host.begin(), host.end() - [&colon_idx, &host]() { + const size_t slash_idx{host.find('/', /*pos=*/colon_idx + 1)}; + return slash_idx != std::string::npos ? host.length() - slash_idx : 0; + }()); + uint16_t port_url{0}; + SplitHostPort(host, port_url, host); + if (port_url != 0) { + if (args.IsArgSet("-statsport")) { + LogPrintf("%s: Supplied URL with port, ignoring -statsport\n", __func__); + } + port = port_url; + } + } else { + // There was no port specified, remove everything after the first forward slash + host = host.substr(/*pos=*/0, host.find("/")); + } + + if (host.empty()) { + return util::Error{_("No host specified, malformed URL")}; + } } auto sanitize_string = [](std::string string) { diff --git a/src/stats/client.h b/src/stats/client.h index b9484f8f5cb9..dad20111d9c7 100644 --- a/src/stats/client.h +++ b/src/stats/client.h @@ -16,8 +16,6 @@ class ArgsManager; -/** Default port used to connect to a Statsd server */ -static constexpr uint16_t DEFAULT_STATSD_PORT{8125}; /** Default host assumed to be running a Statsd server */ static const std::string DEFAULT_STATSD_HOST{""}; /** Default prefix prepended to Statsd message keys */ From b50c7f211e16778169fedc35cfe8b97906fd061a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:37:03 +0000 Subject: [PATCH 8/9] stats: add functional test for statsd reporting --- test/functional/feature_stats.py | 141 +++++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 142 insertions(+) create mode 100755 test/functional/feature_stats.py diff --git a/test/functional/feature_stats.py b/test/functional/feature_stats.py new file mode 100755 index 000000000000..64b4674b102c --- /dev/null +++ b/test/functional/feature_stats.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test stats reporting""" + +import queue +import socket +import time +import threading + +from test_framework.netutil import test_ipv6_local +from test_framework.test_framework import BitcoinTestFramework +from queue import Queue + +ONION_ADDR = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion" + +class StatsServer: + def __init__(self, host: str, port: int): + self.running = False + self.thread = None + self.queue: Queue[str] = Queue() + + addr_info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_DGRAM) + self.af = addr_info[0][0] + self.addr = (host, port) + + self.s = socket.socket(self.af, socket.SOCK_DGRAM) + self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.s.bind(self.addr) + self.s.settimeout(0.1) + + def run(self): + while self.running: + try: + data, _ = self.s.recvfrom(4096) + messages = data.decode('utf-8').strip().split('\n') + for msg in messages: + if msg: + self.queue.put(msg) + except socket.timeout: + continue + except Exception as e: + if self.running: + raise AssertionError("Unexpected exception raised: " + type(e).__name__) + + def start(self): + assert not self.running + self.running = True + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True + self.thread.start() + + def stop(self): + self.running = False + if self.thread: + self.thread.join(timeout=2) + self.s.close() + + def assert_msg_received(self, expected_msg: str, timeout: int = 30): + deadline = time.time() + timeout + while time.time() < deadline: + try: + msg = self.queue.get(timeout=5) + if expected_msg in msg: + return + except queue.Empty: + continue + raise AssertionError(f"Did not receive message containing '{expected_msg}' within {timeout} seconds") + + +class StatsTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + self.log.info("Test invalid command line options") + self.test_invalid_command_line_options() + + self.log.info("Test command line behavior") + self.test_command_behavior() + + self.log.info("Check that server can receive stats client messages") + self.have_ipv6 = test_ipv6_local() + self.test_conn('127.0.0.1') + if self.have_ipv6: + self.test_conn('::1') + else: + self.log.warning("Testing without local IPv6 support") + + def test_invalid_command_line_options(self): + self.stop_node(0) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Cannot init Statsd client (Port must be between 1 and 65535, supplied 65536)', + extra_args=['-statshost=127.0.0.1', '-statsport=65536'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Cannot init Statsd client (No text before the scheme delimiter, malformed URL)', + extra_args=['-statshost=://127.0.0.1'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Cannot init Statsd client (Unsupported URL scheme, must begin with udp://)', + extra_args=['-statshost=http://127.0.0.1'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Cannot init Statsd client (No host specified, malformed URL)', + extra_args=['-statshost=udp://'], + ) + self.nodes[0].assert_start_raises_init_error( + expected_msg=f'Error: Cannot init Statsd client (Host {ONION_ADDR} on unsupported network)', + extra_args=[f'-statshost=udp://{ONION_ADDR}'], + ) + + def test_command_behavior(self): + with self.nodes[0].assert_debug_log(expected_msgs=['Transmitting stats are disabled, will not init Statsd client']): + self.restart_node(0, extra_args=[]) + # The port specified in the URL supercedes -statsport + with self.nodes[0].assert_debug_log(expected_msgs=[ + 'Supplied URL with port, ignoring -statsport', + 'StatsdClient initialized to transmit stats to 127.0.0.1:8126', + 'Started threaded RawSender sending messages to 127.0.0.1:8126' + ]): + self.restart_node(0, extra_args=['-debug=net', '-statshost=udp://127.0.0.1:8126', '-statsport=8125']) + # Not specifying the port in the URL or -statsport will select the default port. Also, validate -statsduration behavior. + with self.nodes[0].assert_debug_log(expected_msgs=[ + 'Send interval is zero, not starting RawSender queueing thread', + 'StatsdClient initialized to transmit stats to 127.0.0.1:8125', + 'Started RawSender sending messages to 127.0.0.1:8125' + ]): + self.restart_node(0, extra_args=['-debug=net', '-statshost=udp://127.0.0.1', '-statsduration=0']) + + def test_conn(self, host: str): + server = StatsServer(host, 8125) + server.start() + self.restart_node(0, extra_args=[f'-statshost=udp://{host}', '-statsbatchsize=0', '-statsduration=0']) + server.assert_msg_received("CheckBlock_us") + server.stop() + +if __name__ == '__main__': + StatsTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 2c1a8597f535..02f1fde0f6b6 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -308,6 +308,7 @@ 'mining_basic.py', 'rpc_named_arguments.py', 'feature_startupnotify.py', + 'feature_stats.py', 'wallet_simulaterawtx.py --legacy-wallet', 'wallet_simulaterawtx.py --descriptors', 'wallet_listsinceblock.py --legacy-wallet', From e607836a2058fc6a5944578c3e73fbd7fd7e161a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 30 Sep 2025 07:54:10 +0000 Subject: [PATCH 9/9] docs: add release notes --- doc/release-notes-6837.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 doc/release-notes-6837.md diff --git a/doc/release-notes-6837.md b/doc/release-notes-6837.md new file mode 100644 index 000000000000..757baeeb86d0 --- /dev/null +++ b/doc/release-notes-6837.md @@ -0,0 +1,17 @@ +Statistics +---------- + +- IPv6 hosts are now supported by the StatsD client. + +- `-statshost` now accepts URLs to allow specifying the protocol, host and port in one argument. + +- Specifying invalid values will no longer result in silent disablement of the StatsD client and will now cause errors + at startup. + +### Deprecations + +- `-statsport` has been deprecated and ports are now specified using the new URL syntax supported by `-statshost`. + `-statsport` will be removed in a future release. + + - If both `-statsport` and `-statshost` with a URL specifying a port is supplied, the `-statsport` value will be + ignored.