LCOV - code coverage report
Current view: top level - src/stats - client.cpp (source / functions) Hit Total Coverage
Test: total_coverage.info Lines: 82 113 72.6 %
Date: 2026-06-25 07:23:43 Functions: 17 36 47.2 %

          Line data    Source code
       1             : // Copyright (c) 2014-2017 Statoshi Developers
       2             : // Copyright (c) 2017-2023 Vincent Thiery
       3             : // Copyright (c) 2020-2025 The Dash Core developers
       4             : // Distributed under the MIT software license, see the accompanying
       5             : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
       6             : 
       7             : #include <stats/client.h>
       8             : 
       9             : #include <logging.h>
      10             : #include <random.h>
      11             : #include <stats/rawsender.h>
      12             : #include <sync.h>
      13             : #include <util/check.h>
      14             : #include <util/strencodings.h>
      15             : #include <util/system.h>
      16             : #include <util/translation.h>
      17             : 
      18             : #include <algorithm>
      19             : #include <cmath>
      20             : #include <limits>
      21             : #include <random>
      22             : 
      23             : namespace {
      24             : /** Threshold below which a value is considered effectively zero */
      25             : static constexpr float EPSILON{0.0001f};
      26             : /** Delimiter segmenting scheme from the rest of the URL */
      27             : static constexpr std::string_view URL_SCHEME_DELIMITER{"://"};
      28             : 
      29             : /** Default port used to connect to a Statsd server */
      30             : static constexpr uint16_t DEFAULT_STATSD_PORT{8125};
      31             : 
      32             : /** Delimiter segmenting two fully formed Statsd messages */
      33             : static constexpr char STATSD_MSG_DELIMITER{'\n'};
      34             : /** Delimiter segmenting namespaces in a Statsd key */
      35             : static constexpr char STATSD_NS_DELIMITER{'.'};
      36             : /** Character used to denote Statsd message type as count */
      37             : static constexpr char STATSD_METRIC_COUNT[]{"c"};
      38             : /** Character used to denote Statsd message type as gauge */
      39             : static constexpr char STATSD_METRIC_GAUGE[]{"g"};
      40             : /** Characters used to denote Statsd message type as timing */
      41             : static constexpr char STATSD_METRIC_TIMING[]{"ms"};
      42             : 
      43             : class StatsdClientImpl final : public StatsdClient
      44             : {
      45             : public:
      46             :     explicit StatsdClientImpl(const std::string& host, uint16_t port, uint64_t batch_size, uint64_t interval_ms,
      47             :                               const std::string& prefix, const std::string& suffix, std::optional<bilingual_str>& error);
      48          22 :     ~StatsdClientImpl() = default;
      49             : 
      50             : public:
      51           0 :     bool dec(std::string_view key, float sample_rate) override EXCLUSIVE_LOCKS_REQUIRED(!cs)
      52             :     {
      53           0 :         return count(key, -1, sample_rate);
      54             :     }
      55           0 :     bool inc(std::string_view key, float sample_rate) override EXCLUSIVE_LOCKS_REQUIRED(!cs)
      56             :     {
      57           0 :         return count(key, 1, sample_rate);
      58             :     }
      59           0 :     bool count(std::string_view key, int64_t delta, float sample_rate) override EXCLUSIVE_LOCKS_REQUIRED(!cs)
      60             :     {
      61           0 :         return _send(key, delta, STATSD_METRIC_COUNT, sample_rate);
      62             :     }
      63           0 :     bool gauge(std::string_view key, int64_t value, float sample_rate) override EXCLUSIVE_LOCKS_REQUIRED(!cs)
      64             :     {
      65           0 :         return _send(key, value, STATSD_METRIC_GAUGE, sample_rate);
      66             :     }
      67           0 :     bool gaugeDouble(std::string_view key, double value, float sample_rate) override EXCLUSIVE_LOCKS_REQUIRED(!cs)
      68             :     {
      69           0 :         return _send(key, value, STATSD_METRIC_GAUGE, sample_rate);
      70             :     }
      71          72 :     bool timing(std::string_view key, uint64_t ms, float sample_rate) override EXCLUSIVE_LOCKS_REQUIRED(!cs)
      72             :     {
      73          72 :         return _send(key, ms, STATSD_METRIC_TIMING, sample_rate);
      74             :     }
      75             : 
      76           0 :     bool send(std::string_view key, double value, std::string_view type, float sample_rate) override
      77             :         EXCLUSIVE_LOCKS_REQUIRED(!cs)
      78             :     {
      79           0 :         return _send(key, value, type, sample_rate);
      80             :     }
      81           0 :     bool send(std::string_view key, int32_t value, std::string_view type, float sample_rate) override
      82             :         EXCLUSIVE_LOCKS_REQUIRED(!cs)
      83             :     {
      84           0 :         return _send(key, value, type, sample_rate);
      85             :     }
      86           0 :     bool send(std::string_view key, int64_t value, std::string_view type, float sample_rate) override
      87             :         EXCLUSIVE_LOCKS_REQUIRED(!cs)
      88             :     {
      89           0 :         return _send(key, value, type, sample_rate);
      90             :     }
      91           0 :     bool send(std::string_view key, uint32_t value, std::string_view type, float sample_rate) override
      92             :         EXCLUSIVE_LOCKS_REQUIRED(!cs)
      93             :     {
      94           0 :         return _send(key, value, type, sample_rate);
      95             :     }
      96           0 :     bool send(std::string_view key, uint64_t value, std::string_view type, float sample_rate) override
      97             :         EXCLUSIVE_LOCKS_REQUIRED(!cs)
      98             :     {
      99           0 :         return _send(key, value, type, sample_rate);
     100             :     }
     101             : 
     102           6 :     bool active() const override { return m_sender != nullptr; }
     103             : 
     104             : private:
     105             :     template <typename T1>
     106             :     inline bool _send(std::string_view key, T1 value, std::string_view type, float sample_rate)
     107             :         EXCLUSIVE_LOCKS_REQUIRED(!cs);
     108             : 
     109             : private:
     110             :     /* Mutex to protect PRNG */
     111             :     mutable Mutex cs;
     112             :     /* PRNG used to dice-roll messages that are 0 < f < 1 */
     113             :     mutable FastRandomContext insecure_rand GUARDED_BY(cs);
     114             : 
     115             :     /* Broadcasts messages crafted by StatsdClient */
     116           8 :     std::unique_ptr<RawSender> m_sender{nullptr};
     117             : 
     118             :     /* Phrase prepended to keys */
     119             :     const std::string m_prefix{};
     120             :     /* Phrase appended to keys */
     121             :     const std::string m_suffix{};
     122             : };
     123             : } // anonymous namespace
     124             : 
     125             : std::unique_ptr<StatsdClient> g_stats_client;
     126             : 
     127        3573 : util::Result<std::unique_ptr<StatsdClient>> StatsdClient::make(const ArgsManager& args)
     128             : {
     129        3573 :     auto host = args.GetArg("-statshost", DEFAULT_STATSD_HOST);
     130        3573 :     if (host.empty()) {
     131        3557 :         LogPrintf("Transmitting stats are disabled, will not init Statsd client\n");
     132        3557 :         return std::make_unique<StatsdClient>();
     133             :     }
     134             : 
     135          16 :     const int64_t batch_size = args.GetIntArg("-statsbatchsize", DEFAULT_STATSD_BATCH_SIZE);
     136          16 :     if (batch_size < 0) {
     137           0 :         return util::Error{_("-statsbatchsize cannot be configured with a negative value.")};
     138             :     }
     139             : 
     140          16 :     const int64_t interval_ms = args.GetIntArg("-statsduration", DEFAULT_STATSD_DURATION);
     141          16 :     if (interval_ms < 0) {
     142           0 :         return util::Error{_("-statsduration cannot be configured with a negative value.")};
     143             :     }
     144             : 
     145          16 :     auto port_arg = args.GetIntArg("-statsport", DEFAULT_STATSD_PORT);
     146          16 :     if (args.IsArgSet("-statsport")) {
     147             :         // Port range validation if -statsport is specified.
     148           4 :         if (port_arg < 1 || port_arg > std::numeric_limits<uint16_t>::max()) {
     149           2 :             return util::Error{strprintf(_("Port must be between %d and %d, supplied %d"), 1,
     150           2 :                                          std::numeric_limits<uint16_t>::max(), port_arg)};
     151             :         }
     152           2 :     }
     153          14 :     uint16_t port = static_cast<uint16_t>(port_arg);
     154             : 
     155             :     // Could be a URL, try to parse it.
     156          14 :     const size_t scheme_idx{host.find(URL_SCHEME_DELIMITER)};
     157          14 :     if (scheme_idx != std::string::npos) {
     158             :         // Parse the scheme and trim it out of the URL if we succeed
     159          14 :         if (scheme_idx == 0) {
     160           2 :             return util::Error{_("No text before the scheme delimiter, malformed URL")};
     161             :         }
     162          12 :         std::string scheme{ToLower(host.substr(/*pos=*/0, scheme_idx))};
     163          12 :         if (scheme != "udp") {
     164           2 :             return util::Error{_("Unsupported URL scheme, must begin with udp://")};
     165             :         }
     166          10 :         host = host.substr(scheme_idx + URL_SCHEME_DELIMITER.length());
     167             : 
     168             :         // Strip trailing slashes and parse the port
     169          10 :         const size_t colon_idx{host.rfind(':')};
     170          10 :         if (colon_idx != std::string::npos) {
     171             :             // Remove all forward slashes found after the port delimiter (colon)
     172           2 :             host = std::string(
     173           4 :                 host.begin(), host.end() - [&colon_idx, &host]() {
     174           2 :                     const size_t slash_idx{host.find('/', /*pos=*/colon_idx + 1)};
     175           2 :                     return slash_idx != std::string::npos ? host.length() - slash_idx : 0;
     176             :                 }());
     177           2 :             uint16_t port_url{0};
     178           2 :             SplitHostPort(host, port_url, host);
     179           2 :             if (port_url != 0) {
     180           2 :                 if (args.IsArgSet("-statsport")) {
     181           2 :                     LogPrintf("%s: Supplied URL with port, ignoring -statsport\n", __func__);
     182           2 :                 }
     183           2 :                 port = port_url;
     184           2 :             }
     185           2 :         } else {
     186             :             // There was no port specified, remove everything after the first forward slash
     187           8 :             host = host.substr(/*pos=*/0, host.find("/"));
     188             :         }
     189             : 
     190          10 :         if (host.empty()) {
     191           2 :             return util::Error{_("No host specified, malformed URL")};
     192             :         }
     193          12 :     }
     194             : 
     195          24 :     auto sanitize_string = [](std::string string) {
     196             :         // Remove key delimiters from the front and back as they're added back in
     197             :         // the constructor
     198          16 :         if (!string.empty()) {
     199           0 :             if (string.front() == STATSD_NS_DELIMITER) string.erase(string.begin());
     200           0 :             if (string.back() == STATSD_NS_DELIMITER) string.pop_back();
     201           0 :         }
     202          16 :         return string;
     203             :     };
     204             : 
     205           8 :     std::optional<bilingual_str> error_opt;
     206           8 :     auto statsd_ptr = std::make_unique<StatsdClientImpl>(
     207             :         host, port, batch_size, interval_ms,
     208           8 :         sanitize_string(args.GetArg("-statsprefix", DEFAULT_STATSD_PREFIX)),
     209           8 :         sanitize_string(args.GetArg("-statssuffix", DEFAULT_STATSD_SUFFIX)), error_opt);
     210           8 :     if (error_opt.has_value()) {
     211           2 :         statsd_ptr.reset();
     212           2 :         return util::Error{error_opt.value()};
     213             :     }
     214           6 :     return {std::move(statsd_ptr)};
     215        3573 : }
     216             : 
     217          24 : StatsdClientImpl::StatsdClientImpl(const std::string& host, uint16_t port, uint64_t batch_size, uint64_t interval_ms,
     218             :                                    const std::string& prefix, const std::string& suffix,
     219             :                                    std::optional<bilingual_str>& error) :
     220          16 :     m_prefix{[prefix]() { return !prefix.empty() ? prefix + STATSD_NS_DELIMITER : prefix; }()},
     221          16 :     m_suffix{[suffix]() { return !suffix.empty() ? STATSD_NS_DELIMITER + suffix : suffix; }()}
     222          16 : {
     223           8 :     m_sender = std::make_unique<RawSender>(host, port,
     224           8 :                                            std::make_pair(batch_size, static_cast<uint8_t>(STATSD_MSG_DELIMITER)),
     225           8 :                                            interval_ms, error);
     226           8 :     if (error.has_value()) {
     227           2 :         m_sender.reset();
     228           2 :         return;
     229             :     }
     230             : 
     231           6 :     LogPrintf("StatsdClient initialized to transmit stats to %s:%d\n", host, port);
     232          16 : }
     233             : 
     234             : template <typename T1>
     235          72 : inline bool StatsdClientImpl::_send(std::string_view key, T1 value, std::string_view type, float sample_rate)
     236             : {
     237             :     static_assert(std::is_arithmetic<T1>::value, "Must specialize to an arithmetic type");
     238             : 
     239             :     // Determine if we should send the message at all but claim that we did even if we don't
     240          72 :     sample_rate = std::clamp(sample_rate, 0.f, 1.f);
     241          72 :     bool always_send = std::fabs(sample_rate - 1.f) < EPSILON;
     242          72 :     bool never_send = std::fabs(sample_rate) < EPSILON;
     243          72 :     if (never_send || (!always_send &&
     244           0 :                        WITH_LOCK(cs, return sample_rate < std::uniform_real_distribution<float>(0.f, 1.f)(insecure_rand)))) {
     245           0 :         return true;
     246             :     }
     247             : 
     248             :     // Construct the message and if our message isn't always-send, report the sample rate
     249          72 :     RawMessage msg{strprintf("%s%s%s:%f|%s", m_prefix, key, m_suffix, value, type)};
     250          72 :     if (!always_send) {
     251           0 :         msg += strprintf("|@%.2f", sample_rate);
     252           0 :     }
     253             : 
     254             :     // Send it and report an error if we encounter one
     255          72 :     if (auto error_opt = Assert(m_sender)->Send(msg); error_opt.has_value()) {
     256           0 :         LogPrintf("ERROR: %s.\n", error_opt->original);
     257           0 :         return false;
     258             :     }
     259             : 
     260          72 :     return true;
     261          72 : }

Generated by: LCOV version 1.16