ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
statsd.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file statsd.h
6 * @brief The StatsD metrics client: one metric line per UDP datagram.
7 *
8 * **No formal specification governs StatsD.** It is a line protocol that originated at Etsy, and the
9 * only description of it is the reference implementation's own documentation, github.com/statsd/statsd
10 * `README.md` and `docs/metric_types.md`. There is no IETF RFC for it, and no OASIS, OMG, W3C, CNCF
11 * or Eclipse document defines it. The names below are that documentation's own.
12 *
13 * `docs/metric_types.md` gives the line as `<metricname>:<value>|<type>` and names four types:
14 *
15 * Counting gorets:1|c "Add 1 to the 'gorets' bucket."
16 * Sampling gorets:1|c|@0.1 "this counter is being sent sampled every 1/10th of the time"
17 * Timing glork:320|ms "The glork took 320ms to complete this time."
18 * Gauges gaugor:333|g "will maintain its value until it is next set"
19 * Sets uniques:765|s "counting unique occurrences of events between flushes"
20 *
21 * The sample rate is the counter's and the timer's: "The field is optional and defaults to 1."
22 * A signed gauge value adjusts instead of assigning, `gaugor:-10|g` then `gaugor:+4|g` taking 333 to
23 * 327. `README.md`: "Each stat is in its own 'bucket'. They are not predefined anywhere", and its
24 * example names 8125 as the port the reference daemon listens on.
25 *
26 * The `|#k:v,k2:v2` tail is not Etsy's. It is DogStatsD, Datadog's extension, whose datagram format
27 * is `<METRIC_NAME>:<VALUE>|<TYPE>|@<SAMPLE_RATE>|#<TAG_KEY_1>:<TAG_VALUE_1>,<TAG_2>` and whose tag
28 * field is "A comma separated list of strings. Use colons for key/value tags (`env:prod`)". A daemon
29 * that does not speak it ignores the field.
30 *
31 * The daemon address and the tag list every metric carries are copied into fixed storage by an init,
32 * so nothing a caller passes has to outlive the call. Values are rendered by hand and the line is
33 * assembled into storage, so a metric costs no heap and nothing lands on a task stack.
34 *
35 * The module exports one symbol, @ref Statsd. Everything in statsd.c has internal linkage.
36 *
37 * @author Douglas Quigg (dstroy0)
38 * @date 2026
39 */
40
41#ifndef PROTOCORE_STATSD_H
42#define PROTOCORE_STATSD_H
43
44#include "protocore_config.h" // the entry point: protocore_types.h for the widths
45
46#if PROTOCORE_ENABLE_STATSD
47
49
50/**
51 * @brief The four metric types of github.com/statsd/statsd `docs/metric_types.md`.
52 *
53 * The value is the selector a call sets, not the token the line carries: a timing selects with 'm'
54 * and writes `ms`.
55 */
56typedef enum PROTO_ENUM_PACKED
57{
58 STATSD_COUNTER = 'c', ///< Counting: `gorets:1|c`
59 STATSD_GAUGE = 'g', ///< Gauges: `gaugor:333|g`, or a signed value to adjust
60 STATSD_TIMING = 'm', ///< Timing: written as `ms`, `glork:320|ms`
61 STATSD_SET = 's', ///< Sets: `uniques:765|s`
62} StatsdType;
63
64/** @brief The StatsD daemon every datagram is sent to. */
65typedef struct
66{
67 const char *addr; ///< its address as text, v4 or v6, parsed once by an init
68 uint16_t port; ///< its UDP port; 0 selects ::PROTOCORE_STATSD_PORT, the reference daemon's 8125
69} StatsdServerArgs;
70
71/** @brief The DogStatsD tag lists, "k:v,k2:v2" with no leading `#`. */
72typedef struct
73{
74 const char *global; ///< the list an init stores and every metric carries; NULL or "" stores none
75 const char *metric; ///< the list one format writes; a metric call stamps the stored one here
76} StatsdTagArgs;
77
78/** @brief What one line names, in the grammar `<metricname>:<value>|<type>[|@<rate>]`. */
79typedef struct
80{
81 const char *name; ///< the bucket, "api.requests"; empty formats nothing
82 StatsdType type; ///< which of the four types; a metric call stamps its own
83 float rate; ///< the sample rate in (0,1); 0 or >= 1 writes no `|@` field
84} StatsdMetricArgs;
85
86/** @brief The value a line carries, in the form the call taking it reads. */
87typedef struct
88{
89 const char *text; ///< the value already rendered, what a format reads
90 int64_t i64; ///< a counter delta, a gauge's absolute value, or a signed gauge adjustment
91 uint32_t ms; ///< a timing in milliseconds
92 const char *member; ///< the set member counted as one unique occurrence; NULL writes empty
93} StatsdValueArgs;
94
95/** @brief Where a formatted metric line lands. */
96typedef struct
97{
98 char *out; ///< the buffer a format writes the line into
99 size_t cap; ///< how much room it has, the NUL included
100} StatsdLineArgs;
101
102/**
103 * @brief The StatsD metrics client.
104 *
105 * A caller sets the members a call takes, invokes it through ::Statsd, and reads the outcome off the
106 * same handle.
107 *
108 * No slot member: one client sends to one daemon, so no call names a row.
109 *
110 * @var StatsdNs::server the daemon an init parses and every metric is sent to
111 * @var StatsdNs::tags the DogStatsD tag lists, the stored one and the per-line one
112 * @var StatsdNs::metric the bucket, the type and the sample rate one line carries
113 * @var StatsdNs::value the value one line carries, in each form a call reads
114 * @var StatsdNs::line the buffer a format writes into
115 * @var StatsdNs::ok a call's true/false outcome
116 * @var StatsdNs::n the line length a format wrote, excluding the NUL, 0 if it did not fit
117 * @var StatsdNs::init parse the daemon address and copy its port and @c tags.global into storage
118 * @var StatsdNs::format build one line from @c metric, @c value.text and @c tags.metric into @c line
119 * @var StatsdNs::count add @c value.i64 to the bucket as `|c`, annotated with @c metric.rate
120 * @var StatsdNs::gauge assign @c value.i64 to the bucket as `|g`
121 * @var StatsdNs::gauge_delta adjust the bucket by @c value.i64 as a signed `|g`
122 * @var StatsdNs::timing record @c value.ms as `|ms`
123 * @var StatsdNs::set count @c value.member as one unique occurrence, `|s`
124 *
125 * @c format touches no socket. Every other metric call renders its value into the client's scratch,
126 * stamps its own @c metric.type and the stored @c tags.global onto @c tags.metric, formats into the
127 * client's line storage, and sends those octets as one datagram. @c gauge, @c gauge_delta, @c timing
128 * and @c set stamp @c metric.rate to 1, the unsampled rate, so no `|@` field is written.
129 */
130typedef struct
131{
132 StatsdServerArgs server; ///< where the datagrams go
133 StatsdTagArgs tags; ///< what every line, and this line, is tagged with
134 StatsdMetricArgs metric; ///< what one line names
135 StatsdValueArgs value; ///< what one line carries
136 StatsdLineArgs line; ///< where the formatted line lands
137 proto_bool ok;
138 size_t n;
139} StatsdVars;
140
141/** @brief The operands and the outcome. */
142extern StatsdVars StatsdV;
143
144/** @brief The entries. */
145typedef struct
146{
147 void (*const init)(uint8_t *work);
148 void (*const format)(uint8_t *work);
149 void (*const count)(uint8_t *work);
150 void (*const gauge)(uint8_t *work);
151 void (*const gauge_delta)(uint8_t *work);
152 void (*const timing)(uint8_t *work);
153 void (*const set)(uint8_t *work);
154} StatsdNs;
155
156// What the table binds, defined once in the .c and taking one parameter each: everything
157// else an entry needs is an operand in StatsdV or a region of the borrow at a fixed offset.
158void protocore_statsd_init(uint8_t *work);
159void protocore_statsd_format(uint8_t *work);
160void protocore_statsd_count(uint8_t *work);
161void protocore_statsd_gauge(uint8_t *work);
162void protocore_statsd_gauge_delta(uint8_t *work);
163void protocore_statsd_timing(uint8_t *work);
164void protocore_statsd_set(uint8_t *work);
165
166// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
167// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
168// `Statsd.init(work)` resolves to a named function and becomes a DIRECT call. An extern table
169// leaves the call indirect and the symbol live at every level, -O2 -flto included.
170static const StatsdNs Statsd __attribute__((unused)) = {
171 .init = protocore_statsd_init,
172 .format = protocore_statsd_format,
173 .count = protocore_statsd_count,
174 .gauge = protocore_statsd_gauge,
175 .gauge_delta = protocore_statsd_gauge_delta,
176 .timing = protocore_statsd_timing,
177 .set = protocore_statsd_set,
178};
179
180/**
181 * @brief The PROTOCORE_STATSD_BORROW bytes this module's state lives in.
182 *
183 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
184 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
185 * walks, so the state lasts the life of the program.
186 *
187 * @return the span.
188 */
189uint8_t *protocore_statsd_span(void);
190
192
193#endif // PROTOCORE_ENABLE_STATSD
194
195#endif // PROTOCORE_STATSD_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97