ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 pc_statsd_format assembles the line and the transport
10 * UDP service sends it.
11 */
12
14#include "protocore_config.h"
15
16#if PC_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 {
40 b[i] = tmp[n - 1 - i];
41 }
42 return n;
43}
44
45// Signed -> decimal (handles INT64_MIN via -(v+1)+1); returns length.
46size_t i64_str(char *b, int64_t v)
47{
48 if (v < 0)
49 {
50 b[0] = '-';
51 return 1 + u64_str(b + 1, (uint64_t)(-(v + 1)) + 1);
52 }
53 return u64_str(b, (uint64_t)v);
54}
55
56// Signed -> "+N" / "-N" for a StatsD gauge delta.
57size_t i64_delta_str(char *b, int64_t v)
58{
59 if (v >= 0)
60 {
61 b[0] = '+';
62 return 1 + u64_str(b + 1, (uint64_t)v);
63 }
64 return i64_str(b, v); // already carries the '-'
65}
66
67// Sample rate in (0,1) -> "0.xxx" (<= 3 decimals, trailing zeros trimmed). >= 1 -> no text.
68size_t rate_str(char *b, float r)
69{
70 if (r >= 1.0f || r <= 0.0f)
71 {
72 return 0;
73 }
74 int m = (int)(r * 1000.0f + 0.5f); // thousandths
75 if (m <= 0)
76 {
77 m = 1;
78 }
79 if (m > 999)
80 {
81 m = 999;
82 }
83 b[0] = '0';
84 b[1] = '.';
85 b[2] = (char)('0' + (m / 100) % 10);
86 b[3] = (char)('0' + (m / 10) % 10);
87 b[4] = (char)('0' + m % 10);
88 size_t len = 5;
89 while (len > 3 && b[len - 1] == '0')
90 {
91 len--; // trim trailing zeros ("0.100" -> "0.1")
92 }
93 return len;
94}
95
96// Bounded append into out[*pos]; false (and leaves *pos) if it would not fit with room for NUL.
97bool app(char *out, size_t cap, size_t *pos, const char *s, size_t n)
98{
99 if (*pos + n >= cap)
100 {
101 return false;
102 }
103 memcpy(out + *pos, s, n);
104 *pos += n;
105 return true;
106}
107} // namespace
108
109size_t pc_statsd_format(char *out, size_t cap, const char *name, const char *value, StatsdType type, float rate,
110 const char *tags)
111{
112 if (!out || cap == 0 || !name || !name[0] || !value)
113 {
114 return 0;
115 }
118 {
119 return 0;
120 }
121
122 size_t pos = 0;
123 char t = (char)type;
124 if (!app(out, cap, &pos, name, strnlen(name, cap)) || !app(out, cap, &pos, ":", 1) ||
125 !app(out, cap, &pos, value, strnlen(value, cap)) || !app(out, cap, &pos, "|", 1))
126 {
127 return 0;
128 }
129 if (type == StatsdType::STATSD_TIMING)
130 {
131 if (!app(out, cap, &pos, "ms", 2))
132 {
133 return 0;
134 }
135 }
136 else if (!app(out, cap, &pos, &t, 1))
137 {
138 return 0;
139 }
140
141 char rbuf[8];
142 size_t rn = rate_str(rbuf, rate);
143 if (rn && (!app(out, cap, &pos, "|@", 2) || !app(out, cap, &pos, rbuf, rn)))
144 {
145 return 0;
146 }
147 if (tags && tags[0] && (!app(out, cap, &pos, "|#", 2) || !app(out, cap, &pos, tags, strnlen(tags, cap))))
148 {
149 return 0;
150 }
151
152 out[pos] = '\0';
153 return pos;
154}
155
156// ---------------------------------------------------------------------------
157// Emit helpers: render the value, then send via the transport UDP service. Host builds
158// send through pc_udp_sendto's stub + capture seam, so these are host-testable too.
159// ---------------------------------------------------------------------------
160
161namespace
162{
163// All StatsD client state, owned by one instance (internal linkage): destination host/port,
164// global tags, and the ready flag, grouped so it is one named owner, unreachable cross-TU.
165struct StatsdCtx
166{
167 char host[64] = {};
168 uint16_t port = PC_STATSD_PORT;
169 char tags[96] = {};
170 bool ready = false;
171};
172StatsdCtx s_statsd;
173
174void emit(const StatsdCtx &c, const char *name, const char *value, StatsdType type, float rate)
175{
176 if (!c.ready)
177 {
178 return;
179 }
180 char line[PC_STATSD_LINE_MAX];
181 size_t n = pc_statsd_format(line, sizeof(line), name, value, type, rate, c.tags[0] ? c.tags : nullptr);
182 if (n)
183 {
184 pc_udp_sendto(c.host, c.port, (const uint8_t *)line, n);
185 }
186}
187} // namespace
188
189void pc_statsd_begin(const char *host, uint16_t port, const char *global_tags)
190{
191 if (!host)
192 {
193 s_statsd.ready = false;
194 return;
195 }
196 strncpy(s_statsd.host, host, sizeof(s_statsd.host) - 1);
197 s_statsd.host[sizeof(s_statsd.host) - 1] = '\0';
198 s_statsd.port = port ? port : PC_STATSD_PORT;
199 if (global_tags)
200 {
201 strncpy(s_statsd.tags, global_tags, sizeof(s_statsd.tags) - 1);
202 s_statsd.tags[sizeof(s_statsd.tags) - 1] = '\0';
203 }
204 else
205 {
206 s_statsd.tags[0] = '\0';
207 }
208 s_statsd.ready = true;
209}
210
211void pc_statsd_count(const char *name, int64_t delta)
212{
213 char v[24];
214 v[i64_str(v, delta)] = '\0';
215 emit(s_statsd, name, v, StatsdType::STATSD_COUNTER, 1.0f);
216}
217
218void pc_statsd_count_sampled(const char *name, int64_t delta, float rate)
219{
220 char v[24];
221 v[i64_str(v, delta)] = '\0';
222 emit(s_statsd, name, v, StatsdType::STATSD_COUNTER, rate);
223}
224
225void pc_statsd_gauge(const char *name, int64_t value)
226{
227 char v[24];
228 v[i64_str(v, value)] = '\0';
229 emit(s_statsd, name, v, StatsdType::STATSD_GAUGE, 1.0f);
230}
231
232void pc_statsd_gauge_delta(const char *name, int64_t delta)
233{
234 char v[24];
235 v[i64_delta_str(v, delta)] = '\0';
236 emit(s_statsd, name, v, StatsdType::STATSD_GAUGE, 1.0f);
237}
238
239void pc_statsd_timing(const char *name, uint32_t ms)
240{
241 char v[16];
242 v[u64_str(v, ms)] = '\0';
243 emit(s_statsd, name, v, StatsdType::STATSD_TIMING, 1.0f);
244}
245
246void pc_statsd_set(const char *name, const char *member)
247{
248 emit(s_statsd, name, member ? member : "", StatsdType::STATSD_SET, 1.0f);
249}
250
251#endif // PC_ENABLE_STATSD
User-facing configuration for ProtoCore.
#define PC_STATSD_LINE_MAX
Stack buffer for one StatsD line (bytes; caps metric name + value + tags).
#define PC_STATSD_PORT
Default StatsD collector UDP port (StatsD/Graphite standard).
StatsD metrics client - push counters/gauges/timings/sets to a StatsD collector.
void pc_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 pc_statsd_count(const char *name, int64_t delta)
Increment a counter by delta (may be negative).
void pc_statsd_gauge_delta(const char *name, int64_t delta)
Adjust a gauge by a signed delta (sent as a +/- gauge update).
void pc_statsd_set(const char *name, const char *member)
Add a unique member to a set (counts distinct values server-side).
void pc_statsd_timing(const char *name, uint32_t ms)
Record a timing/duration of ms milliseconds.
void pc_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.
StatsdType
StatsD metric type codes (the token after the |).
Definition statsd.h:33
@ STATSD_TIMING
emitted as "ms"
void pc_statsd_gauge(const char *name, int64_t value)
Set a gauge to an absolute value.
size_t pc_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.
bool pc_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:358
Layer 4 (Transport) - connectionless UDP datagram service.