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 0 : ~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 0 : bool timing(std::string_view key, uint64_t ms, float sample_rate) override EXCLUSIVE_LOCKS_REQUIRED(!cs)
72 : {
73 0 : 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 0 : 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 0 : 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 627 : util::Result<std::unique_ptr<StatsdClient>> StatsdClient::make(const ArgsManager& args)
128 : {
129 627 : auto host = args.GetArg("-statshost", DEFAULT_STATSD_HOST);
130 627 : if (host.empty()) {
131 627 : LogPrintf("Transmitting stats are disabled, will not init Statsd client\n");
132 627 : return std::make_unique<StatsdClient>();
133 : }
134 :
135 0 : const int64_t batch_size = args.GetIntArg("-statsbatchsize", DEFAULT_STATSD_BATCH_SIZE);
136 0 : if (batch_size < 0) {
137 0 : return util::Error{_("-statsbatchsize cannot be configured with a negative value.")};
138 : }
139 :
140 0 : const int64_t interval_ms = args.GetIntArg("-statsduration", DEFAULT_STATSD_DURATION);
141 0 : if (interval_ms < 0) {
142 0 : return util::Error{_("-statsduration cannot be configured with a negative value.")};
143 : }
144 :
145 0 : auto port_arg = args.GetIntArg("-statsport", DEFAULT_STATSD_PORT);
146 0 : if (args.IsArgSet("-statsport")) {
147 : // Port range validation if -statsport is specified.
148 0 : if (port_arg < 1 || port_arg > std::numeric_limits<uint16_t>::max()) {
149 0 : return util::Error{strprintf(_("Port must be between %d and %d, supplied %d"), 1,
150 0 : std::numeric_limits<uint16_t>::max(), port_arg)};
151 : }
152 0 : }
153 0 : uint16_t port = static_cast<uint16_t>(port_arg);
154 :
155 : // Could be a URL, try to parse it.
156 0 : const size_t scheme_idx{host.find(URL_SCHEME_DELIMITER)};
157 0 : if (scheme_idx != std::string::npos) {
158 : // Parse the scheme and trim it out of the URL if we succeed
159 0 : if (scheme_idx == 0) {
160 0 : return util::Error{_("No text before the scheme delimiter, malformed URL")};
161 : }
162 0 : std::string scheme{ToLower(host.substr(/*pos=*/0, scheme_idx))};
163 0 : if (scheme != "udp") {
164 0 : return util::Error{_("Unsupported URL scheme, must begin with udp://")};
165 : }
166 0 : host = host.substr(scheme_idx + URL_SCHEME_DELIMITER.length());
167 :
168 : // Strip trailing slashes and parse the port
169 0 : const size_t colon_idx{host.rfind(':')};
170 0 : if (colon_idx != std::string::npos) {
171 : // Remove all forward slashes found after the port delimiter (colon)
172 0 : host = std::string(
173 0 : host.begin(), host.end() - [&colon_idx, &host]() {
174 0 : const size_t slash_idx{host.find('/', /*pos=*/colon_idx + 1)};
175 0 : return slash_idx != std::string::npos ? host.length() - slash_idx : 0;
176 : }());
177 0 : uint16_t port_url{0};
178 0 : SplitHostPort(host, port_url, host);
179 0 : if (port_url != 0) {
180 0 : if (args.IsArgSet("-statsport")) {
181 0 : LogPrintf("%s: Supplied URL with port, ignoring -statsport\n", __func__);
182 0 : }
183 0 : port = port_url;
184 0 : }
185 0 : } else {
186 : // There was no port specified, remove everything after the first forward slash
187 0 : host = host.substr(/*pos=*/0, host.find("/"));
188 : }
189 :
190 0 : if (host.empty()) {
191 0 : return util::Error{_("No host specified, malformed URL")};
192 : }
193 0 : }
194 :
195 0 : 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 0 : 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 0 : return string;
203 : };
204 :
205 0 : std::optional<bilingual_str> error_opt;
206 0 : auto statsd_ptr = std::make_unique<StatsdClientImpl>(
207 : host, port, batch_size, interval_ms,
208 0 : sanitize_string(args.GetArg("-statsprefix", DEFAULT_STATSD_PREFIX)),
209 0 : sanitize_string(args.GetArg("-statssuffix", DEFAULT_STATSD_SUFFIX)), error_opt);
210 0 : if (error_opt.has_value()) {
211 0 : statsd_ptr.reset();
212 0 : return util::Error{error_opt.value()};
213 : }
214 0 : return {std::move(statsd_ptr)};
215 627 : }
216 :
217 0 : 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 0 : m_prefix{[prefix]() { return !prefix.empty() ? prefix + STATSD_NS_DELIMITER : prefix; }()},
221 0 : m_suffix{[suffix]() { return !suffix.empty() ? STATSD_NS_DELIMITER + suffix : suffix; }()}
222 0 : {
223 0 : m_sender = std::make_unique<RawSender>(host, port,
224 0 : std::make_pair(batch_size, static_cast<uint8_t>(STATSD_MSG_DELIMITER)),
225 0 : interval_ms, error);
226 0 : if (error.has_value()) {
227 0 : m_sender.reset();
228 0 : return;
229 : }
230 :
231 0 : LogPrintf("StatsdClient initialized to transmit stats to %s:%d\n", host, port);
232 0 : }
233 :
234 : template <typename T1>
235 0 : 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 0 : sample_rate = std::clamp(sample_rate, 0.f, 1.f);
241 0 : bool always_send = std::fabs(sample_rate - 1.f) < EPSILON;
242 0 : bool never_send = std::fabs(sample_rate) < EPSILON;
243 0 : 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 0 : RawMessage msg{strprintf("%s%s%s:%f|%s", m_prefix, key, m_suffix, value, type)};
250 0 : 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 0 : 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 0 : return true;
261 0 : }
|