Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

telemetry.h

v0.1.0-build.85 Latest Commit ede38f5

Size
13 KB
Lines
271
Language
cpp
SHA-256
0f45d69ab24d…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1#ifndef CKEY_TELEMETRY_H
2#define CKEY_TELEMETRY_H
3
4/*
5 * CKey telemetry core (specification §9, docs/adoption-and-telemetry.md).
6 *
7 * This module is optional and self-contained. It is built as its own library
8 * (ckey_telemetry); the scanner library does not depend on it and it does not
9 * call any other ckey_ function, so builds for OS vendors can leave it out.
10 * This header includes ckey/ckey.h only for CKEY_API and ckey_status.
11 *
12 * WHAT IT DOES
13 * It keeps aggregate counters in process memory and serializes them into the
14 * version-1 batch JSON. The core NEVER does networking: SDK wrappers take a
15 * batch, POST it to CKEY_TELEMETRY_ENDPOINT asynchronously, off the decode
16 * path, and report the HTTP result back with ckey_telemetry_batch_result().
17 * The core never reads a clock either; every time is a caller-supplied
18 * monotonic millisecond value (the UTC day for a batch is also supplied).
19 *
20 * WHAT IS COLLECTED
21 * Session counts (started, succeeded, failed), the number of processed
22 * frames, decodes per profile, a histogram of time to first decode, a
23 * histogram of per-frame processing time, the SDK name and version, a coarse
24 * platform name and the UTC day the batch was cut.
25 *
26 * WHAT IS NEVER COLLECTED
27 * Payloads, URLs or any decoded content; user, device or installation IDs;
28 * advertising IDs or fingerprints; location; IP addresses; per-scan event
29 * histories. No API in this header accepts any of these.
30 *
31 * MODES (canonical numeric values shared by every SDK)
32 * 0 CKEY_TELEMETRY_OFF no counting, nothing sent.
33 * 1 CKEY_TELEMETRY_ON local counting and aggregate batches (DEFAULT).
34 * 2 CKEY_TELEMETRY_LOCAL local counting, never sent.
35 * Switching (ckey_telemetry_set_mode):
36 * any -> 0 purges every counter (totals, unsent delta, in-flight batch)
37 * and the open session. Recording calls become no-ops.
38 * 1 -> 2 keeps local totals; drops the unsent delta; the in-flight
39 * batch (if any) is discarded and its result is ignored.
40 * 2 -> 1 starts a new unsent delta at zero; counts made in mode 2 are
41 * never sent.
42 * 0 -> 1|2 starts counting from zero.
43 * same mode no effect.
44 * Any other value is rejected with CKEY_INVALID_ARGUMENT; the mode is left
45 * unchanged and is never silently reset to 1.
46 *
47 * THREADING
48 * All state is process-global and guarded by one internal mutex; every
49 * function may be called from any thread. Recording calls take the mutex and
50 * do a few saturating integer updates; they never allocate, block on I/O or
51 * serialize. Serialization happens only in take_batch/export_state, which
52 * wrappers call off the decode path.
53 *
54 * COUNTERS
55 * Counters saturate at UINT64_MAX instead of wrapping. A batch never carries
56 * a count above CKEY_TELEMETRY_MAX_COUNT or a sum above CKEY_TELEMETRY_MAX_SUM;
57 * any excess stays in the unsent delta and goes in a later batch.
58 */
59
60#include "ckey/ckey.h"
61
62#include <stddef.h>
63#include <stdint.h>
64
65#ifdef __cplusplus
66extern "C" {
67#endif
68
69#define CKEY_TELEMETRY_ENDPOINT "https://openckey.org/api/telemetry/v1/batch"
70#define CKEY_TELEMETRY_SCHEMA 1
71
72/* Default sdk_version reported until ckey_telemetry_set_client() is called:
73 * the core's own version (ckey.h), which matches the schema's pattern. */
74#ifndef CKEY_TELEMETRY_DEFAULT_VERSION
75#define CKEY_TELEMETRY_DEFAULT_VERSION CKEY_SOFTWARE_VERSION
76#endif
77
78/* Schema limits for one batch. */
79#define CKEY_TELEMETRY_MAX_COUNT 10000000ull
80#define CKEY_TELEMETRY_MAX_SUM 1000000000000ull
81#define CKEY_TELEMETRY_MAX_BODY_BYTES 4096u
82
83/* A session with no decode fails once now - start >= this many ms. */
84#define CKEY_TELEMETRY_SESSION_TIMEOUT_MS 30000u
85/* Minimum ms between the last reported batch result and the next batch. */
86#define CKEY_TELEMETRY_SEND_INTERVAL_MS 3600000u
87/* An unsent delta must be this old before the first batch is cut from it. */
88#define CKEY_TELEMETRY_MIN_DELTA_AGE_MS 60000u
89
90#define CKEY_TELEMETRY_BUCKETS 10u
91#define CKEY_TELEMETRY_PROFILES 5u
92
93typedef enum ckey_telemetry_mode {
94 CKEY_TELEMETRY_OFF = 0,
95 CKEY_TELEMETRY_ON = 1,
96 CKEY_TELEMETRY_LOCAL = 2
97} ckey_telemetry_mode;
98
99/* profile_code values for ckey_telemetry_session_success(); they index
100 * ckey_telemetry_counters.decoded and map to the batch keys p0..p3. */
101typedef enum ckey_telemetry_profile {
102 CKEY_TELEMETRY_PROFILE_P0 = 0, /* "p0" */
103 CKEY_TELEMETRY_PROFILE_P0_1 = 1, /* "p0_1" */
104 CKEY_TELEMETRY_PROFILE_P1 = 2, /* "p1" */
105 CKEY_TELEMETRY_PROFILE_P2 = 3, /* "p2" */
106 CKEY_TELEMETRY_PROFILE_P3 = 4 /* "p3" */
107} ckey_telemetry_profile;
108
109/* Histogram buckets use INCLUSIVE upper edges: a value v goes in the first
110 * bucket i with v <= edge[i]; the last bucket holds everything above the
111 * ninth edge. So 25 ms is in time_to_decode bucket 0 and 26 ms in bucket 1.
112 * time_to_decode_ms: 25, 50, 100, 200, 400, 800, 1600, 3200, 6400, inf
113 * frame_ms: 2, 5, 10, 20, 40, 80, 160, 320, 640, inf
114 * Invariant: the buckets add up to count. */
115typedef struct ckey_telemetry_histogram {
116 uint64_t count;
117 uint64_t sum;
118 uint64_t buckets[CKEY_TELEMETRY_BUCKETS];
119} ckey_telemetry_histogram;
120
121typedef struct ckey_telemetry_counters {
122 uint64_t sessions_started;
123 uint64_t sessions_succeeded;
124 uint64_t sessions_failed;
125 uint64_t frames;
126 uint64_t decoded[CKEY_TELEMETRY_PROFILES]; /* indexed by ckey_telemetry_profile */
127 ckey_telemetry_histogram time_to_decode_ms;
128 ckey_telemetry_histogram frame_ms;
129} ckey_telemetry_counters;
130
131/* ---- Configuration --------------------------------------------------- */
132
133/* Sets the mode (0, 1 or 2) and applies the switching table above.
134 * Returns CKEY_INVALID_ARGUMENT, mode unchanged, for any other value. */
135CKEY_API ckey_status ckey_telemetry_set_mode(int mode);
136CKEY_API int ckey_telemetry_get_mode(void);
137
138/* Sets the client identity reported in batches. All three are required and
139 * validated together; on any error nothing changes.
140 * sdk: "c", "swift", "android", "web" or "dotnet" (default "c")
141 * sdk_version: ^0\.[0-9]+\.[0-9]+(-(build|dev)\.[0-9]{1,6})?$, at most 63
142 * bytes (default CKEY_TELEMETRY_DEFAULT_VERSION)
143 * platform: "ios", "macos", "android", "web", "windows", "linux" or
144 * "other" (default "other")
145 * The identity is configuration, not a counter: mode changes keep it. */
146CKEY_API ckey_status ckey_telemetry_set_client(const char *sdk, const char *sdk_version,
147 const char *platform);
148
149/* ---- Recording (no-ops in mode 0) -------------------------------------
150 *
151 * now_ms is any monotonic millisecond clock of the caller's choice, used
152 * consistently. A value earlier than the session start counts as 0 ms elapsed.
153 *
154 * Session state machine:
155 * IDLE --begin/frame/success--> OPEN --success--> DONE
156 * OPEN --end, begin, or (now - start >= 30 s at the next call)--> failed
157 * DONE --end--> IDLE (no further count); DONE --begin--> new OPEN session
158 *
159 * - session_begin: the host started scanning. An OPEN session is first ended
160 * as failed (the host restarted without a decode). started += 1.
161 * - frame: counts one processed frame and its processing time. If the OPEN
162 * session has timed out it is ended as failed. If no session is open (IDLE,
163 * or just timed out) a new session implicitly begins at now_ms. In DONE the
164 * frame is counted as a frame only: per the normative doc, a session
165 * succeeds at its FIRST valid decode and later frames/decodes of the same
166 * session are not counted, so a new session begins only with the next
167 * session_begin (or after session_end, at the next frame).
168 * - session_success: the first valid decode. Records time to decode
169 * (now_ms - session start), succeeded += 1, decoded[profile] += 1, and moves
170 * the session to DONE. A timed-out OPEN session is first ended as failed;
171 * with no OPEN session, one implicitly begins at now_ms (time to decode 0).
172 * In DONE it is ignored. Returns CKEY_INVALID_ARGUMENT, recording nothing,
173 * for a profile_code outside 0..4.
174 * - session_end: the host stopped scanning. An OPEN session fails; a DONE
175 * session just closes.
176 * Timeouts are detected lazily, at the next recording call.
177 * The outcome of a session is counted in the delta of the mode current when
178 * the outcome happens. */
179CKEY_API void ckey_telemetry_session_begin(uint64_t now_ms);
180CKEY_API void ckey_telemetry_frame(uint64_t now_ms, uint32_t processing_ms);
181CKEY_API ckey_status ckey_telemetry_session_success(uint64_t now_ms, uint32_t profile_code);
182CKEY_API void ckey_telemetry_session_end(uint64_t now_ms);
183
184/* Local totals since counting started (modes 1 and 2; all zero in mode 0).
185 * CKEY_INVALID_ARGUMENT if out is NULL. */
186CKEY_API ckey_status ckey_telemetry_snapshot(ckey_telemetry_counters *out);
187
188/* ---- Batching (mode 1 only; the wrapper does the HTTP) ----------------
189 *
190 * Wrapper loop, e.g. on a timer and when the app moves to the background:
191 * if (ckey_telemetry_should_send(now)) {
192 * ckey_telemetry_take_batch(utc_day, buf, sizeof buf, &len, &id);
193 * ... POST buf (Content-Type: application/json) asynchronously ...
194 * ckey_telemetry_batch_result(id, http_status_or_0, now_after);
195 * }
196 * Every taken batch must be reported exactly once; until then no new batch
197 * can be taken. */
198
199/* Non-zero only when: mode is 1, the unsent delta is non-empty, no batch is
200 * in flight, at least CKEY_TELEMETRY_SEND_INTERVAL_MS have passed since the
201 * now_ms of the last ckey_telemetry_batch_result (whatever its outcome, so a
202 * failed send is retried an hour later and there is never more than one batch
203 * per hour; the first batch of a process has no such wait), and the delta has
204 * existed for
205 * at least CKEY_TELEMETRY_MIN_DELTA_AGE_MS (so the first batch of a process
206 * is not cut after a single scan; deltas restored by import or by a failed
207 * send are not subject to this age). */
208CKEY_API int ckey_telemetry_should_send(uint64_t now_ms);
209
210/* Serializes the unsent delta as the compact version-1 batch JSON (keys in
211 * the documented order, NUL-terminated) and moves it to "in flight".
212 * utc_day: "YYYY-MM-DD", a valid calendar date (supplied by the caller).
213 * out_json/cap: output buffer; out_json may be NULL only when cap is 0.
214 * out_len: receives strlen of the JSON (excluding the NUL).
215 * out_batch_id: receives a non-zero id for ckey_telemetry_batch_result.
216 * Returns CKEY_INSUFFICIENT_BUFFER with *out_len = required length (cap must
217 * be at least *out_len + 1) and nothing moved. Returns CKEY_OK with
218 * *out_len = 0 and *out_batch_id = 0 when there is nothing to send (mode is
219 * not 1, delta empty, or a batch already in flight). The cadence is NOT
220 * checked here; call ckey_telemetry_should_send first. Values above the schema
221 * maxima are clamped and the excess stays in the unsent delta. The body is
222 * always well under CKEY_TELEMETRY_MAX_BODY_BYTES. */
223CKEY_API ckey_status ckey_telemetry_take_batch(const char *utc_day, char *out_json, size_t cap,
224 size_t *out_len, uint64_t *out_batch_id);
225
226/* Reports the outcome of a taken batch.
227 * http_status 200..299: accepted; the batch is dropped.
228 * 429, 0 (network error, timeout, no connectivity), 1xx, 3xx, 5xx: kept;
229 * the batch is merged back into the unsent delta for a later attempt.
230 * 400, 413 and every other 4xx: rejected; the batch is dropped.
231 * Returns CKEY_INVALID_ARGUMENT, changing nothing, when batch_id is not the
232 * batch currently in flight (for example after a mode switch made it stale:
233 * such a batch is never merged back or resent) or http_status is outside
234 * 0..999. now_ms (same clock as the recording calls) starts the hourly
235 * cadence interval for every accepted report, success or failure. */
236CKEY_API ckey_status ckey_telemetry_batch_result(uint64_t batch_id, int http_status, uint64_t now_ms);
237
238/* ---- Optional persistence (host option, independent of the mode) -------
239 *
240 * The state holds the local totals and the unsent delta. It never holds the
241 * mode, the client identity, the in-flight batch or the open session. Format
242 * (compact JSON, keys in this exact order):
243 * {"state":1,"totals":C,"unsent":C}
244 * where C is {"sessions":{...},"frames":N,"decoded":{...},
245 * "time_to_decode_ms":{...},"frame_ms":{...}} with the batch field layout.
246 *
247 * export: NUL-terminated into out/cap; *len = strlen. CKEY_INSUFFICIENT_BUFFER
248 * with *len = required length when cap < *len + 1. In mode 0 it exports an
249 * all-zero state, so a host that stores it also forgets purged counts.
250 *
251 * import: ADDS the stored counts to the current ones (saturating); call it
252 * once, at startup. In mode 2 only the totals are imported (mode-2 counts are
253 * never sent). Rejected with CKEY_INVALID_ARGUMENT in mode 0, for NULL, and
254 * for anything that is not exactly the format above: unknown, missing or
255 * reordered keys, non-decimal or out-of-range numbers, buckets that do not add
256 * up to count, trailing data, or input over 16 KiB. Whitespace between tokens
257 * is allowed. On rejection the state is unchanged. Import never changes the
258 * mode and never re-enables sending. */
259CKEY_API ckey_status ckey_telemetry_export_state(char *out, size_t cap, size_t *len);
260CKEY_API ckey_status ckey_telemetry_import_state(const char *json);
261
262/* TEST ONLY. Restores defaults: mode 1, default client, all counters, the
263 * delta, the in-flight batch, the session and the cadence cleared. Not for
264 * production use. */
265CKEY_API void ckey_telemetry_reset_for_testing(void);
266
267#ifdef __cplusplus
268}
269#endif
270
271#endif