DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
statsd.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file statsd.cpp
6 * @brief StatsD metrics client - implementation. See statsd.h.
7 *
8 * The value and sample-rate are rendered by hand (no printf %lld/%f, which need extra
9 * newlib support on some targets), then statsd_format assembles the line and the transport
10 * UDP service sends it.
11 */
12
14#include "ServerConfig.h"
15
16#if DETWS_ENABLE_STATSD
17
19#include <string.h>
20
21namespace
22{
23// Unsigned -> decimal (not NUL-terminated); returns the digit count.
24size_t u64_str(char *b, uint64_t v)
25{
26 if (v == 0)
27 {
28 b[0] = '0';
29 return 1;
30 }
31 char tmp[20];
32 size_t n = 0;
33 while (v)
34 {
35 tmp[n++] = (char)('0' + v % 10);
36 v /= 10;
37 }
38 for (size_t i = 0; i < n; i++)
39 b[i] = tmp[n - 1 - i];
40 return n;
41}
42
43// Signed -> decimal (handles INT64_MIN via -(v+1)+1); returns length.
44size_t i64_str(char *b, int64_t v)
45{
46 if (v < 0)
47 {
48 b[0] = '-';
49 return 1 + u64_str(b + 1, (uint64_t)(-(v + 1)) + 1);
50 }
51 return u64_str(b, (uint64_t)v);
52}
53
54// Signed -> "+N" / "-N" for a StatsD gauge delta.
55size_t i64_delta_str(char *b, int64_t v)
56{
57 if (v >= 0)
58 {
59 b[0] = '+';
60 return 1 + u64_str(b + 1, (uint64_t)v);
61 }
62 return i64_str(b, v); // already carries the '-'
63}
64
65// Sample rate in (0,1) -> "0.xxx" (<= 3 decimals, trailing zeros trimmed). >= 1 -> no text.
66size_t rate_str(char *b, float r)
67{
68 if (r >= 1.0f || r <= 0.0f)
69 return 0;
70 int m = (int)(r * 1000.0f + 0.5f); // thousandths
71 if (m <= 0)
72 m = 1;
73 if (m > 999)
74 m = 999;
75 b[0] = '0';
76 b[1] = '.';
77 b[2] = (char)('0' + (m / 100) % 10);
78 b[3] = (char)('0' + (m / 10) % 10);
79 b[4] = (char)('0' + m % 10);
80 size_t len = 5;
81 while (len > 3 && b[len - 1] == '0')
82 len--; // trim trailing zeros ("0.100" -> "0.1")
83 return len;
84}
85
86// Bounded append into out[*pos]; false (and leaves *pos) if it would not fit with room for NUL.
87bool app(char *out, size_t cap, size_t *pos, const char *s, size_t n)
88{
89 if (*pos + n >= cap)
90 return false;
91 memcpy(out + *pos, s, n);
92 *pos += n;
93 return true;
94}
95} // namespace
96
97size_t statsd_format(char *out, size_t cap, const char *name, const char *value, StatsdType type, float rate,
98 const char *tags)
99{
100 if (!out || cap == 0 || !name || !name[0] || !value)
101 return 0;
104 return 0;
105
106 size_t pos = 0;
107 char t = (char)type;
108 if (!app(out, cap, &pos, name, strnlen(name, cap)) || !app(out, cap, &pos, ":", 1) ||
109 !app(out, cap, &pos, value, strnlen(value, cap)) || !app(out, cap, &pos, "|", 1))
110 return 0;
111 if (type == StatsdType::STATSD_TIMING)
112 {
113 if (!app(out, cap, &pos, "ms", 2))
114 return 0;
115 }
116 else if (!app(out, cap, &pos, &t, 1))
117 return 0;
118
119 char rbuf[8];
120 size_t rn = rate_str(rbuf, rate);
121 if (rn && (!app(out, cap, &pos, "|@", 2) || !app(out, cap, &pos, rbuf, rn)))
122 return 0;
123 if (tags && tags[0] && (!app(out, cap, &pos, "|#", 2) || !app(out, cap, &pos, tags, strnlen(tags, cap))))
124 return 0;
125
126 out[pos] = '\0';
127 return pos;
128}
129
130// ---------------------------------------------------------------------------
131// Emit helpers: render the value, then send via the transport UDP service. Host builds
132// send through det_udp_sendto's stub + capture seam, so these are host-testable too.
133// ---------------------------------------------------------------------------
134
135namespace
136{
137// All StatsD client state, owned by one instance (internal linkage): destination host/port,
138// global tags, and the ready flag, grouped so it is one named owner, unreachable cross-TU.
139struct StatsdCtx
140{
141 char host[64] = {};
142 uint16_t port = DETWS_STATSD_PORT;
143 char tags[96] = {};
144 bool ready = false;
145};
146StatsdCtx s_statsd;
147
148void emit(const StatsdCtx &c, const char *name, const char *value, StatsdType type, float rate)
149{
150 if (!c.ready)
151 return;
152 char line[DETWS_STATSD_LINE_MAX];
153 size_t n = statsd_format(line, sizeof(line), name, value, type, rate, c.tags[0] ? c.tags : nullptr);
154 if (n)
155 det_udp_sendto(c.host, c.port, (const uint8_t *)line, n);
156}
157} // namespace
158
159void statsd_begin(const char *host, uint16_t port, const char *global_tags)
160{
161 if (!host)
162 {
163 s_statsd.ready = false;
164 return;
165 }
166 strncpy(s_statsd.host, host, sizeof(s_statsd.host) - 1);
167 s_statsd.host[sizeof(s_statsd.host) - 1] = '\0';
168 s_statsd.port = port ? port : DETWS_STATSD_PORT;
169 if (global_tags)
170 {
171 strncpy(s_statsd.tags, global_tags, sizeof(s_statsd.tags) - 1);
172 s_statsd.tags[sizeof(s_statsd.tags) - 1] = '\0';
173 }
174 else
175 s_statsd.tags[0] = '\0';
176 s_statsd.ready = true;
177}
178
179void statsd_count(const char *name, int64_t delta)
180{
181 char v[24];
182 v[i64_str(v, delta)] = '\0';
183 emit(s_statsd, name, v, StatsdType::STATSD_COUNTER, 1.0f);
184}
185
186void statsd_count_sampled(const char *name, int64_t delta, float rate)
187{
188 char v[24];
189 v[i64_str(v, delta)] = '\0';
190 emit(s_statsd, name, v, StatsdType::STATSD_COUNTER, rate);
191}
192
193void statsd_gauge(const char *name, int64_t value)
194{
195 char v[24];
196 v[i64_str(v, value)] = '\0';
197 emit(s_statsd, name, v, StatsdType::STATSD_GAUGE, 1.0f);
198}
199
200void statsd_gauge_delta(const char *name, int64_t delta)
201{
202 char v[24];
203 v[i64_delta_str(v, delta)] = '\0';
204 emit(s_statsd, name, v, StatsdType::STATSD_GAUGE, 1.0f);
205}
206
207void statsd_timing(const char *name, uint32_t ms)
208{
209 char v[16];
210 v[u64_str(v, ms)] = '\0';
211 emit(s_statsd, name, v, StatsdType::STATSD_TIMING, 1.0f);
212}
213
214void statsd_set(const char *name, const char *member)
215{
216 emit(s_statsd, name, member ? member : "", StatsdType::STATSD_SET, 1.0f);
217}
218
219#endif // DETWS_ENABLE_STATSD
User-facing configuration for DeterministicESPAsyncWebServer.
#define DETWS_STATSD_PORT
Default StatsD collector UDP port (StatsD/Graphite standard).
#define DETWS_STATSD_LINE_MAX
Stack buffer for one StatsD line (bytes; caps metric name + value + tags).
StatsD metrics client - push counters/gauges/timings/sets to a StatsD collector.
void statsd_gauge_delta(const char *name, int64_t delta)
Adjust a gauge by a signed delta (sent as a +/- gauge update).
void statsd_begin(const char *host, uint16_t port, const char *global_tags)
Point the client at a collector and set optional global tags (added to every metric).
void statsd_count(const char *name, int64_t delta)
Increment a counter by delta (may be negative).
void statsd_timing(const char *name, uint32_t ms)
Record a timing/duration of ms milliseconds.
StatsdType
StatsD metric type codes (the token after the |).
Definition statsd.h:33
@ STATSD_TIMING
emitted as "ms"
void statsd_set(const char *name, const char *member)
Add a unique member to a set (counts distinct values server-side).
size_t statsd_format(char *out, size_t cap, const char *name, const char *value, StatsdType type, float rate, const char *tags)
Format one StatsD line: name:value|type[|@rate][|#tags]. Pure - no I/O.
void statsd_gauge(const char *name, int64_t value)
Set a gauge to an absolute value.
void statsd_count_sampled(const char *name, int64_t delta, float rate)
Increment a counter, annotated with sample rate (0,1] so the server scales up.
bool det_udp_sendto(const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a UDP datagram to an arbitrary destination (outbound client).
Definition udp.cpp:196
Layer 4 (Transport) - connectionless UDP datagram service.