ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
otel.c
Go to the documentation of this file.
1
2#define ASCIICHAT_OTEL_IMPLEMENTATION
3
4/* Storage capacities in elements; text buffers include the terminating NUL. */
5#define OTEL_NAME_BUFFER_SIZE 256
6#define OTEL_VERSION_BUFFER_SIZE 32
7#define OTEL_BODY_BUFFER_SIZE 4096
8#define OTEL_URL_BUFFER_SIZE 2048
9#define OTEL_NAME_EXTENDED_BUFFER_SIZE 1200
10
11/* Existing buffer budgets in bytes, including NUL for text storage. */
12#define OTEL_WAIT_SNAPSHOT_CAPACITY 256
13
14#include <ascii-chat/otel.h>
15#include <ascii-chat/common.h>
16#include <ascii-chat/version.h>
21#include <libplatform/thread.h>
22#include <libplatform/process.h>
23#include <libplatform/system.h>
25#include <ascii-chat/util/url.h>
26#include <ascii-chat/log/log.h>
29#include <stdatomic.h>
30#include <math.h>
31#include <stdio.h>
32#include <string.h>
33#include <stdlib.h>
34
35static _Thread_local bool in_log;
36
37// Keep library status codes inside this boundary and retain their diagnostic text.
38static asciichat_error_t otel_status(otlp_status_t status, const char *operation) {
39 if (status == OTLP_OK)
40 return ASCIICHAT_OK;
42 switch (status) {
43 case OTLP_ERR_NOMEM:
44 error = ERROR_MEMORY;
45 break;
46 case OTLP_ERR_INVALID_ARGUMENT:
47 case OTLP_ERR_NULL:
48 error = ERROR_INVALID_PARAM;
49 break;
50 case OTLP_ERR_OVERFLOW:
52 break;
53 case OTLP_ERR_UTF8:
54 error = ERROR_STRING;
55 break;
56 case OTLP_ERR_NETWORK:
57 case OTLP_ERR_DNS:
58 case OTLP_ERR_WRITE:
59 case OTLP_ERR_READ:
60 error = ERROR_NETWORK;
61 break;
62 case OTLP_ERR_CONNECT:
64 break;
65 case OTLP_ERR_TIMEOUT:
67 break;
68 case OTLP_ERR_PROTOCOL:
69 case OTLP_ERR_INVALID_RESPONSE:
70 case OTLP_ERR_HTTP_STATUS:
71 case OTLP_ERR_SERVER:
73 break;
74 case OTLP_ERR_THROTTLED:
76 break;
77 case OTLP_ERR_BUFFER_FULL:
78 error = ERROR_BUFFER_FULL;
79 break;
80 case OTLP_ERR_SHUTDOWN:
81 error = ERROR_INVALID_STATE;
82 break;
83 case OTLP_ERR_WOULDBLOCK:
85 break;
86 case OTLP_ERR_NOT_IMPLEMENTED:
87 error = ERROR_NOT_SUPPORTED;
88 break;
89 default:
90 error = ERROR_INTERNAL;
91 break;
92 }
93 // Error recording logs locally; do not export that diagnostic back into a failing exporter.
94 bool previous = in_log;
95 in_log = true;
96 asciichat_error_t result =
97 SET_ERRNO(error, "%s failed: %s (OTLP status %d)", operation, otlp_strerror(status), (int)status);
98 in_log = previous;
99 return result;
100}
101
102static const gotel_operation_t operations[] = {
103#define ASCII_OTEL_OPERATION(id, name, realtime) {name, realtime},
104#include <ascii-chat/otel_operations.def>
105#undef ASCII_OTEL_OPERATION
106};
107static const double metric_bounds[] = {.0001, .001, .005, .01, .025, .05, .1, .25, .5, 1, 2,
108 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 1000000};
109static const gotel_metric_t metrics[] = {
110#define ASCII_OTEL_METRIC(id, name, type, unit) \
111 {name, unit, OTLP_METRIC_##type, metric_bounds, sizeof(metric_bounds) / sizeof(metric_bounds[0])},
112#include <ascii-chat/otel_metrics.def>
113#undef ASCII_OTEL_METRIC
114};
115static atomic_ptr_t host;
116static atomic_uint readers;
117static atomic_bool stopping;
118static atomic_bool worker_done;
119static platform_thread_t worker;
120static bool worker_started;
121static asciichat_error_t drain_result;
122static int minimum_log_level;
123static gotel_scope_t command_scope;
124static _Thread_local ascii_otel_scope_t *current_scope;
125static uint64_t shutdown_timeout_ns;
126static atomic_uint_fast64_t exporter_retries, exporter_batches;
127static atomic_size_t exporter_queue_capacity;
128static void exporter_event(void *context, const otlp_event_t *event) {
129 (void)context;
130 if (event->code == OTLP_EVT_RETRY_ARMED)
131 atomic_fetch_add(&exporter_retries, 1);
132 if (event->code == OTLP_EVT_BATCH_SENT)
133 atomic_fetch_add(&exporter_batches, 1);
134}
135
136static uint64_t telemetry_realtime_ns(void) {
137 // time_get_realtime_ns() is normalized to Unix epoch nanoseconds by
138 // libplatform on every supported platform, including Windows.
139 return time_get_realtime_ns();
140}
141static gotel_host_t *retain(void) {
142 if (!atomic_ptr_load(&host))
143 return NULL;
144 atomic_fetch_add_explicit(&readers, 1, memory_order_seq_cst);
145 gotel_host_t *h = atomic_ptr_load(&host);
146 if (!h)
147 atomic_fetch_sub_explicit(&readers, 1, memory_order_seq_cst);
148 return h;
149}
150static void release(void) {
151 atomic_fetch_sub_explicit(&readers, 1, memory_order_seq_cst);
152}
154 return current_scope && current_scope->scope.operation < ASCII_OTEL_OPERATION_COUNT
155 ? operations[current_scope->scope.operation].name
156 : "";
157}
159 return atomic_ptr_load(&host) != NULL;
160}
161void ascii_otel_metric(ascii_otel_metric_t metric, double value) {
162 gotel_host_t *h = retain();
163 if (h) {
164 (void)otel_status(gotel_metric_record(h, metric, value), "gotel_metric_record");
165 release();
166 }
167}
168enum {
170#define ASCII_STATS_COUNTER(id, metric_id, name, unit) +1
171#define ASCII_STATS_GAUGE(id, metric_id, name, unit) +1
172#define ASCII_STATS_DURATION(id, metric_id, name, unit) +1
173#include <ascii-chat/stats/telemetry.def>
174#undef ASCII_STATS_COUNTER
175#undef ASCII_STATS_GAUGE
176#undef ASCII_STATS_DURATION
177};
179 "Every typed stat needs a telemetry metric");
180
182 if (!((kind == STATS_RECORD_COUNTER && id < STATS_COUNTER_COUNT) ||
183 (kind == STATS_RECORD_GAUGE && id < STATS_GAUGE_COUNT) ||
186 ascii_otel_metric_t metric;
187 switch (((unsigned)kind << 16) | id) {
188#define ASCII_STATS_COUNTER(id, metric_id, name, unit) \
189 case (STATS_RECORD_COUNTER << 16) | STATS_COUNTER_##id: \
190 metric = ASCII_OTEL_METRIC_##metric_id; \
191 break;
192#define ASCII_STATS_GAUGE(id, metric_id, name, unit) \
193 case (STATS_RECORD_GAUGE << 16) | STATS_GAUGE_##id: \
194 metric = ASCII_OTEL_METRIC_##metric_id; \
195 break;
196#define ASCII_STATS_DURATION(id, metric_id, name, unit) \
197 case (STATS_RECORD_DURATION << 16) | STATS_DURATION_##id: \
198 metric = ASCII_OTEL_METRIC_##metric_id; \
199 break;
200#include <ascii-chat/stats/telemetry.def>
201#undef ASCII_STATS_COUNTER
202#undef ASCII_STATS_GAUGE
203#undef ASCII_STATS_DURATION
204 default:
206 }
207 return metric;
208}
209
210static void stats_metric_record(stats_record_kind_t kind, unsigned id, uint64_t value, void *context) {
211 (void)context;
213 if (metric == ASCII_OTEL_METRIC_COUNT)
214 return;
215 ascii_otel_metric(metric, kind == STATS_RECORD_DURATION ? (double)value / 1e9 : (double)value);
216}
217
219 stats_scope_set_observer(scope, stats_metric_record, NULL);
220}
221static void exporter_diagnostic(void *context, otlp_log_level_t level, const char *message) {
222 (void)context;
223 (void)level;
224 bool previous = in_log;
225 in_log = true;
226 log_debug("OTLP exporter: %s", message);
227 in_log = previous;
228}
229#ifndef NDEBUG
230static mutex_wait_snapshot_t watchdog_detail;
231static bool watchdog_snapshot(void *context, gotel_watchdog_snapshot_t *out) {
232 (void)context;
234 size_t count = 0;
235 if (!mutex_stack_try_snapshot(waits, 256, &count, &out->limited))
236 return false;
237 uint64_t now = time_get_ns();
238 for (size_t i = 0; i < count; ++i) {
239 if (waits[i].cycle) {
240 if (!out->blocked_threads) {
241 watchdog_detail = waits[i];
242 memcpy(out->parent.trace_id, waits[i].trace_id, sizeof(out->parent.trace_id));
243 memcpy(out->parent.span_id, waits[i].span_id, sizeof(out->parent.span_id));
244 out->parent.valid = waits[i].context_valid;
245 out->parent.sampled = waits[i].context_sampled;
246 out->parent.realtime = true;
247 out->parent_snapshot.context = out->parent;
248 memcpy(out->parent_snapshot.parent_span_id, waits[i].parent_span_id, sizeof(waits[i].parent_span_id));
249 out->parent_snapshot.start_unix_ns = waits[i].span_start_unix_ns;
250 SAFE_STRNCPY(out->parent_snapshot.name, waits[i].span_name, sizeof(out->parent_snapshot.name));
251 }
252 ++out->blocked_threads;
253 uint64_t age = now >= waits[i].since_ns ? now - waits[i].since_ns : 0;
254 if (!out->wait_ns || age < out->wait_ns)
255 out->wait_ns = age;
256 }
257 }
258 return true;
259}
260static void watchdog_decorate(void *context, gotel_scope_t *scope) {
261 (void)context;
262 gotel_attr_string(scope, "thread.blocked.span.name", watchdog_detail.span_name);
263 gotel_attr_int(scope, "asciichat_errno.code", watchdog_detail.error_code);
264 gotel_attr_string(scope, "asciichat_errno.code_message", asciichat_error_string(watchdog_detail.error_code));
265 gotel_attr_string(scope, "asciichat_errno.message", watchdog_detail.error_message);
266}
267#endif
268static atomic_uint_fast64_t pipeline_capture_ns, pipeline_display_ns, pipeline_encode_ns;
269static atomic_uint pipeline_expected;
270void ascii_otel_pipeline_progress(unsigned stage, unsigned expected) {
271 if (!ascii_otel_enabled())
272 return;
273 uint64_t now = time_get_ns();
274 if (stage == 0) {
275 if (!atomic_load(&pipeline_capture_ns)) {
276 atomic_store(&pipeline_display_ns, now);
277 atomic_store(&pipeline_encode_ns, now);
278 }
279 if (!(expected & 1))
280 atomic_store(&pipeline_display_ns, now);
281 if (!(expected & 2))
282 atomic_store(&pipeline_encode_ns, now);
283 atomic_store(&pipeline_expected, expected);
284 atomic_store(&pipeline_capture_ns, now);
285 } else if (stage == 1)
286 atomic_store(&pipeline_display_ns, now);
287 else if (stage == 2)
288 atomic_store(&pipeline_encode_ns, now);
289 else {
290 atomic_store(&pipeline_expected, 0);
291 atomic_store(&pipeline_capture_ns, 0);
292 ascii_otel_metric(ASCII_OTEL_METRIC_PIPELINE_DISPLAY_STALL, 0);
293 ascii_otel_metric(ASCII_OTEL_METRIC_PIPELINE_ENCODE_STALL, 0);
294 }
295}
296double ascii_otel_pipeline_age(uint64_t now, uint64_t capture, uint64_t progress, bool expected) {
297 return expected && capture && progress && now >= capture && now >= progress && now - capture < 2 * NS_PER_SEC_INT
298 ? (double)(now - progress) / 1e9
299 : 0;
300}
301static void pipeline_health(gotel_host_t *h, uint64_t now) {
302 uint64_t capture = atomic_load(&pipeline_capture_ns);
303 unsigned expected = atomic_load(&pipeline_expected);
304 uint64_t display = atomic_load(&pipeline_display_ns), encode = atomic_load(&pipeline_encode_ns);
305 (void)gotel_metric_record(h, ASCII_OTEL_METRIC_PIPELINE_DISPLAY_STALL,
306 ascii_otel_pipeline_age(now, capture, display, (expected & 1) != 0));
307 (void)gotel_metric_record(h, ASCII_OTEL_METRIC_PIPELINE_ENCODE_STALL,
308 ascii_otel_pipeline_age(now, capture, encode, (expected & 2) != 0));
309}
310static void *export_worker(void *arg) {
311 gotel_host_t *h = arg;
312 /* Telemetry export is best-effort. DNS/collector failures must not become
313 * user-facing modal errors, especially while the application is otherwise
314 * connected and rendering normally. */
315 bool notifications = errno_notifications_suppress(true);
316 uint64_t last_health = 0;
317 uint64_t last_failure_warning = 0, observed_failures = 0, observed_sent = 0;
318 bool collector_failed = false;
319 while (!atomic_load_explicit(&stopping, memory_order_acquire)) {
320 uint64_t start = time_get_ns();
321 (void)otel_status(gotel_tick(h), "gotel_tick");
322 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_EXPORT_LATENCY, (double)(time_get_ns() - start) / 1e9),
323 "gotel_metric_record");
324 if (!last_health || start - last_health >= NS_PER_SEC_INT) {
325 last_health = start;
326 pipeline_health(h, time_get_ns());
328 if (platform_process_stats(&process) == ASCIICHAT_OK) {
329 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_CPU, process.cpu_seconds), "gotel_metric_record");
330 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_RSS, (double)process.resident_bytes),
331 "gotel_metric_record");
332 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_THREADS, (double)process.threads),
333 "gotel_metric_record");
334 }
335 otlp_exporter_stats_t stats = {0};
336 (void)otel_status(gotel_stats(h, &stats), "gotel_stats");
337 uint64_t accepted = stats.emitted + stats.emitted_logs + stats.emitted_metrics;
338 uint64_t sent = stats.sent + stats.sent_logs + stats.sent_metrics;
339 uint64_t failures = stats.network_err + stats.http_4xx + stats.http_5xx;
340 // Exporter health logs stay local to avoid feeding failures back into its queue.
341 bool previous = in_log;
342 in_log = true;
343 if (failures > observed_failures) {
344 collector_failed = true;
345 if (!last_failure_warning || start - last_failure_warning >= 30 * NS_PER_SEC_INT) {
346 log_warn("Telemetry collector export failed (%llu failures); export continues in background",
347 (unsigned long long)failures);
348 last_failure_warning = start;
349 }
350 } else if (collector_failed && sent > observed_sent) {
351 log_info("Telemetry collector export recovered");
352 collector_failed = false;
353 last_failure_warning = 0;
354 }
355 in_log = previous;
356 observed_failures = failures;
357 observed_sent = sent;
358 uint64_t failed = stats.dropped_err + stats.dropped_logs_err + stats.dropped_metrics_err;
359 uint64_t pending = accepted > sent + failed ? accepted - sent - failed : 0;
360 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_EXPORT_QUEUE,
361 (double)pending / (3.0 * atomic_load(&exporter_queue_capacity))),
362 "gotel_metric_record");
363 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_EXPORT_SENT, (double)sent), "gotel_metric_record");
364 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_EXPORT_DROP,
365 (double)(failed + stats.dropped_full + stats.dropped_logs_full +
366 stats.dropped_metrics_full)),
367 "gotel_metric_record");
368 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_EXPORT_RETRY, (double)stats.network_err),
369 "gotel_metric_record");
370 (void)otel_status(gotel_metric_record(h, ASCII_OTEL_METRIC_EXPORT_REJECT, (double)stats.rejected_spans),
371 "gotel_metric_record");
372 (void)otel_status(
373 gotel_metric_record(h, ASCII_OTEL_METRIC_EXPORT_RETRIES, (double)atomic_load(&exporter_retries)),
374 "gotel_metric_record");
375 (void)otel_status(
376 gotel_metric_record(h, ASCII_OTEL_METRIC_EXPORT_BATCHES, (double)atomic_load(&exporter_batches)),
377 "gotel_metric_record");
378 }
380 }
381 // Keep final connection setup (including synchronous DNS) off the exit thread.
382 drain_result = otel_status(gotel_drain(h), "Drain telemetry exporter");
383 errno_notifications_suppress(notifications);
384 atomic_store_explicit(&worker_done, true, memory_order_release);
385 return NULL;
386}
390void ascii_otel_scope_parent(ascii_otel_scope_t *s, ascii_otel_operation_t op, const gotel_context_t *parent) {
391 gotel_host_t *h = retain();
392 s->retained = h != NULL;
393 if (h) {
394 gotel_context_t context = gotel_current_context();
395 bool lifetime_parent = current_scope && (current_scope->scope.operation == ASCII_OTEL_SESSION_CLIENT_LIKE_RUN ||
396 current_scope->scope.operation == ASCII_OTEL_SESSION_ATTEMPT);
397 // Lifetime spans finish only at shutdown. Completed operations must remain
398 // inspectable while a server is running, including after abrupt termination.
399 if (!parent && context.valid) {
400 gotel_parent_mode_t mode =
401 lifetime_parent || memcmp(context.span_id, command_scope.context.span_id, sizeof(context.span_id)) == 0
402 ? GOTEL_PARENT_INDEPENDENT
403 : GOTEL_PARENT_BOUNDED;
404 context = gotel_operation_parent(context, mode);
405 parent = &context;
406 }
407 gotel_begin(h, &s->scope, (size_t)op, parent);
408 s->previous = current_scope;
409 current_scope = s;
410 }
411}
412static bool scope_retained(ascii_otel_scope_t *s, ascii_otel_operation_t op, const gotel_context_t *parent) {
413 gotel_host_t *h = retain();
414 if (!h)
415 return false;
416 if (!gotel_begin_retained(h, &s->scope, op, parent)) {
417 release();
418 return false;
419 }
420 s->retained = true;
421 s->previous = current_scope;
422 current_scope = s;
423 return true;
424}
426 if (!s || !s->retained)
427 return;
428 gotel_end(&s->scope);
429 if (current_scope == s)
430 current_scope = s->previous;
431 s->retained = false;
432 release();
433}
434void ascii_otel_ui_usage(const char *screen, const char *event, uint64_t duration_ns) {
435 // UI transitions must not inherit realtime media sampling decisions.
436 gotel_context_t root = {0};
437 ASCII_OTEL_SCOPE_PARENT(UI_USAGE, &root);
438 ASCII_OTEL_ATTR_STRING("ascii_chat.ui.screen", screen);
439 ASCII_OTEL_ATTR_STRING("ascii_chat.ui.event", event);
440 ASCII_OTEL_ATTR_INT("ascii_chat.ui.duration_ns", (int64_t)duration_ns);
441}
442
443void ascii_otel_call_session_used(const char *session_string) {
444 if (!session_string || !session_string[0])
445 return;
446 // Call usage must not inherit a realtime packet's sampling decision.
447 gotel_context_t root = {0};
448 ASCII_OTEL_SCOPE_PARENT(CALL_SESSION_USED, &root);
449 ASCII_OTEL_ATTR_STRING("ascii-chat.session.string", session_string);
450}
451void ascii_otel_packet(ascii_otel_scope_t *scope, uint16_t type, size_t payload_size, const char *action,
452 int transport) {
453 if (!scope->retained || !scope->scope.span)
454 return;
455 const char *packet = acip_packet_type_name(type);
457 safe_snprintf(name, sizeof(name), "%s.%s", operations[scope->scope.operation].name, packet);
458 safe_snprintf(version, sizeof(version), "%d.%d.%d", PROTOCOL_VERSION_MAJOR, PROTOCOL_VERSION_MINOR,
460 (void)otel_status(otlp_span_set_name(scope->scope.span, name), "otlp_span_set_name");
461 gotel_attr_string(&scope->scope, "network.protocol.name", "acip");
462 gotel_attr_string(&scope->scope, "network.protocol.version", version);
463 gotel_attr_string(&scope->scope, "network.packet.name", packet);
464 gotel_attr_int(&scope->scope, "network.packet.type", type);
465 gotel_attr_int(&scope->scope, "network.packet.size", (int64_t)payload_size);
466 gotel_attr_string(&scope->scope, "network.packet.operation", action);
467 const char *carrier = transport == ACIP_TRANSPORT_TCP ? "tcp"
468 : transport == ACIP_TRANSPORT_WEBSOCKET ? "websocket"
469 : transport == ACIP_TRANSPORT_WEBRTC ? "webrtc"
470 : "unknown";
471 gotel_attr_string(&scope->scope, "acip.transport", carrier);
472}
474 gotel_host_t *h = atomic_ptr_exchange(&host, NULL);
475 if (!h)
476 return;
477 gotel_end(&command_scope);
478 uint64_t started = time_get_ns();
479 // Leave time for the rest of process teardown, even if the collector stalls.
480 uint64_t budget_ns = shutdown_timeout_ns < 400 * NS_PER_MS_INT ? shutdown_timeout_ns : 400 * NS_PER_MS_INT;
481 while (atomic_load_explicit(&readers, memory_order_seq_cst) && time_get_ns() - started < budget_ns)
483 bool producers_active = atomic_load_explicit(&readers, memory_order_seq_cst) != 0;
484 atomic_store_explicit(&stopping, true, memory_order_release);
485 if (worker_started) {
486 while (!atomic_load_explicit(&worker_done, memory_order_acquire) && time_get_ns() - started < budget_ns)
488 bool done = atomic_load_explicit(&worker_done, memory_order_acquire);
489 uint64_t elapsed_ns = time_get_ns() - started;
490 int joined = -1;
491 if (done && elapsed_ns < budget_ns) {
492#if !defined(__linux__) && !defined(_WIN32)
493 // The POSIX timed join implementation blocks on these platforms. The worker has
494 // finished using the exporter, so detach its remaining wrapper cleanup.
495 joined = thread_detach(&worker);
496#else
497 joined = thread_join_timeout(&worker, NULL, budget_ns - elapsed_ns);
498#endif
499 }
500 if (joined != 0) {
501 log_warn("Telemetry worker did not stop within its deadline; exporter retained until process exit");
502 return;
503 }
504 worker_started = false;
505 }
506 asciichat_error_t result = drain_result;
507 otlp_exporter_stats_t stats = {0};
508 (void)otel_status(gotel_stats(h, &stats), "gotel_stats");
509 // exit() can bypass scope cleanup in a producer. Do not free its host while
510 // it remains borrowed; the process will reclaim it after atexit handlers.
511 if (!producers_active)
512 gotel_destroy(h);
513 else
514 log_warn("Telemetry producer still active during process shutdown; exporter retained");
515 if (result != ASCIICHAT_OK)
516 log_warn("Telemetry flush deadline reached; pending records discarded");
517 log_debug("Telemetry exporter stopped: spans=%llu/%llu logs=%llu/%llu metrics=%llu/%llu network_errors=%llu "
518 "http_4xx=%llu dropped=%llu rejected=%llu",
519 (unsigned long long)stats.sent, (unsigned long long)stats.emitted, (unsigned long long)stats.sent_logs,
520 (unsigned long long)stats.emitted_logs, (unsigned long long)stats.sent_metrics,
521 (unsigned long long)stats.emitted_metrics, (unsigned long long)stats.network_err,
522 (unsigned long long)stats.http_4xx, (unsigned long long)(stats.dropped_full + stats.dropped_err),
523 (unsigned long long)stats.rejected_spans);
524}
525static const gotel_command_rule_t command_argument_rules[] = {
526 {"--password", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
527 {"--key", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
528 {"-K", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
529 {"--server-key", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
530 {"--client-keys", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
531 {"--discovery-service-key", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
532 {"--address", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
533 {"--address6", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
534 {"--port", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
535 {"-p", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
536 {"--websocket-port", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
537 {"--tls-cert", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
538 {"--tls-key", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
539 {"--tls-key-password", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
540 {"--discovery-service", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
541 {"--discovery-service-port", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
542 {"--discovery-service-url", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
543 {"--stun-servers", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
544 {"--turn-servers", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
545 {"--turn-username", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
546 {"--turn-credential", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
547 {"--turn-secret", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
548 {"--url", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
549 {"-u", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
550 {"--yt-dlp-options", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
551 {"--otel-endpoint", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
552 {"--otel-ca-file", GOTEL_COMMAND_VALUE_REQUIRED, GOTEL_COMMAND_REDACT},
553};
554
555asciichat_error_t ascii_otel_init(const char *mode, int argc, char **argv) {
556 // Test mode is a hard export prohibition, including explicit endpoints and CLI overrides.
557 if (is_test_environment())
558 return ASCIICHAT_OK;
559 const options_t *o = options_get();
560 const char *endpoint = o->otel_endpoint;
561 if (!endpoint || endpoint[0] == '\0')
562 return ASCIICHAT_OK;
563 bool endpoint_cli = false, timeout_cli = false;
564 for (int i = 1; i < argc; ++i) {
565 endpoint_cli |= !strcmp(argv[i], "--otel-endpoint") || !strncmp(argv[i], "--otel-endpoint=", 16);
566 timeout_cli |= !strcmp(argv[i], "--otel-timeout") || !strncmp(argv[i], "--otel-timeout=", 15);
567 }
568 unsigned signals;
569 asciichat_error_t parsed = otel_status(gotel_parse_signals(o->otel_enabled_telemetry, &signals),
570 "Parse otel-enabled-telemetry (traces,logs,metrics or none)");
571 if (parsed != ASCIICHAT_OK)
572 return parsed;
574 o->otel_queue_capacity < 2 || (o->otel_queue_capacity & (o->otel_queue_capacity - 1)) || o->otel_timeout < 1 ||
576 return SET_ERRNO(ERROR_CONFIG, "Invalid telemetry sampling, queue capacity, or interval");
577 const char *log_names[] = {"dev", "debug", "info", "warn", "error", "fatal"};
578 const int log_levels[] = {LOG_DEV, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL};
579 minimum_log_level = -1;
580 for (size_t i = 0; i < sizeof(log_levels) / sizeof(log_levels[0]); ++i)
581 if (strcmp(o->otel_log_level, log_names[i]) == 0)
582 minimum_log_level = log_levels[i];
583 if (minimum_log_level < 0)
584 return SET_ERRNO(ERROR_CONFIG, "Invalid otel-log-level");
585 if (!signals)
586 return ASCIICHAT_OK;
587 gotel_options_t config = {0};
588 config.application_name = "ascii-chat";
589 config.disable_span_name_prefix = true;
590 char service_name[OPTIONS_BUFF_SIZE];
591 int n = safe_snprintf(service_name, sizeof(service_name), "ascii-chat-%s", mode);
592 if (n < 0 || (size_t)n >= sizeof(service_name))
593 return SET_ERRNO(ERROR_CONFIG, "Telemetry service name too long");
594 config.exporter.service_name = service_name;
595 // The exporter observes its batch interval during final flush as well.
596 // Keep it short enough to deliver all signals within the shutdown budget.
597 config.exporter.batch_ms = 25;
598 config.exporter.queue_capacity = (size_t)o->otel_queue_capacity;
599 config.exporter.connect_timeout_ms = (uint32_t)o->otel_timeout;
600 config.exporter.read_timeout_ms = (uint32_t)o->otel_timeout;
601 config.exporter.flush_timeout_ms = (uint32_t)o->otel_timeout;
602 otlp_resource_attr_t resources[] = {
603 {.key = "ascii_chat.mode", .value = {.type = OTLP_VALUE_STRING, .v = {.string_val = mode}}},
604 {.key = "service.version", .value = {.type = OTLP_VALUE_STRING, .v = {.string_val = ASCII_CHAT_VERSION_FULL}}},
605 };
606 config.exporter.resource_attributes = resources;
607 config.exporter.n_resource_attributes = sizeof(resources) / sizeof(resources[0]);
608 config.signals = signals;
609 config.realtime_sample_rate = o->otel_realtime_sample_rate;
610 config.realtime_max_per_second = (uint32_t)o->otel_realtime_max_per_second;
611 config.metrics_interval_ms = (uint32_t)o->otel_metrics_interval;
612 config.monotonic_ns = time_get_ns;
613 config.realtime_ns = telemetry_realtime_ns;
614 config.operations = operations;
615 config.operation_count = ASCII_OTEL_OPERATION_COUNT;
616 config.exporter_event = exporter_event;
617 config.metrics = metrics;
618 config.metric_count = ASCII_OTEL_METRIC_COUNT;
619#ifndef NDEBUG
620 config.watchdog.snapshot = watchdog_snapshot;
621 config.watchdog.decorate = watchdog_decorate;
622 config.watchdog.operation = ASCII_OTEL_THREAD_DEADLOCK;
623 config.watchdog.metric = ASCII_OTEL_METRIC_DEADLOCK_THREADS;
624#endif
625 gotel_command_args_t command_args = {0};
626 // Addresses are positional in native client/server modes, with optional ports.
627 char address_port[OPTIONS_BUFF_SIZE + 32], address6_port[OPTIONS_BUFF_SIZE + 32];
628 char address_brackets[OPTIONS_BUFF_SIZE + 32], address6_brackets[OPTIONS_BUFF_SIZE + 32];
629 char address_brackets_port[OPTIONS_BUFF_SIZE + 32], address6_brackets_port[OPTIONS_BUFF_SIZE + 32];
630 SAFE_SNPRINTF(address_port, sizeof(address_port), "%s:%d", o->address, o->port);
631 SAFE_SNPRINTF(address6_port, sizeof(address6_port), "%s:%d", o->address6, o->port);
632 SAFE_SNPRINTF(address_brackets, sizeof(address_brackets), "[%s]", o->address);
633 SAFE_SNPRINTF(address6_brackets, sizeof(address6_brackets), "[%s]", o->address6);
634 SAFE_SNPRINTF(address_brackets_port, sizeof(address_brackets_port), "[%s]:%d", o->address, o->port);
635 SAFE_SNPRINTF(address6_brackets_port, sizeof(address6_brackets_port), "[%s]:%d", o->address6, o->port);
636 const char *sensitive_values[] = {o->address, o->address6, address_port, address6_port,
637 address_brackets, address6_brackets, address_brackets_port, address6_brackets_port};
638 gotel_command_policy_t argument_policy = {
639#ifdef NDEBUG
640 .mode = GOTEL_COMMAND_REDACT_SELECTED,
641#else
642 .mode = GOTEL_COMMAND_REDACT_NONE,
643#endif
644 .rules = command_argument_rules,
645 .sensitive_values = sensitive_values,
646 .sensitive_value_count = sizeof(sensitive_values) / sizeof(sensitive_values[0]),
647 .rule_count = sizeof(command_argument_rules) / sizeof(command_argument_rules[0]),
648 };
649 asciichat_error_t arguments_status =
650 otel_status(gotel_command_args_init_policy(&command_args, "ascii-chat", mode, argc, (const char *const *)argv,
651 &argument_policy),
652 "Sanitize telemetry command arguments");
653 if (arguments_status != ASCIICHAT_OK)
654 return arguments_status;
655 config.argc = command_args.argc;
656 config.argv = command_args.argv;
657 gotel_env_storage_t env;
658 asciichat_error_t env_status =
659 otel_status(gotel_options_apply_env(&config, &env, OPT_OTEL_ENDPOINT_DEFAULT), "Configure OTEL environment");
660 if (env_status == ASCIICHAT_OK &&
661 (endpoint_cli || SAFE_GETENV("ASCII_CHAT_OTEL_ENDPOINT") || strcmp(endpoint, OPT_OTEL_ENDPOINT_DEFAULT)))
662 env_status = otel_status(gotel_options_set_endpoint(&config, &env, endpoint), "Configure telemetry endpoint");
663 if (timeout_cli || SAFE_GETENV("ASCII_CHAT_OTEL_TIMEOUT")) {
664 config.exporter.connect_timeout_ms = (uint32_t)o->otel_timeout;
665 config.exporter.read_timeout_ms = (uint32_t)o->otel_timeout;
666 config.exporter.flush_timeout_ms = (uint32_t)o->otel_timeout;
667 }
668 if (*o->otel_ca_file)
669 config.tls_ca_file = o->otel_ca_file;
670 if (env_status != ASCIICHAT_OK || !config.signals || !config.exporter.endpoint) {
671 gotel_command_args_destroy(&command_args);
672 return env_status;
673 }
674 gotel_host_t *h = NULL;
675 asciichat_error_t created = otel_status(gotel_create(&config, &h), "Initialize telemetry exporter");
676 if (created != ASCIICHAT_OK) {
677 gotel_command_args_destroy(&command_args);
678 return created;
679 }
680 gotel_set_logger(h, exporter_diagnostic, NULL);
681 shutdown_timeout_ns = (uint64_t)o->otel_timeout * NS_PER_MS_INT;
682 atomic_store(&stopping, false);
683 atomic_store(&exporter_queue_capacity, config.exporter.queue_capacity);
684 atomic_store(&worker_done, false);
685 if (platform_thread_create(&worker, "otel_export", export_worker, h) != 0) {
686 gotel_destroy(h);
687 gotel_command_args_destroy(&command_args);
688 return SET_ERRNO(ERROR_THREAD, "Cannot start telemetry exporter");
689 }
690 worker_started = true;
691 (void)atexit(ascii_otel_shutdown);
692 atomic_ptr_store(&host, h);
693 gotel_begin(h, &command_scope, ASCII_OTEL_CLI, NULL);
694 (void)otel_status(gotel_command_args_set_span(&command_scope, &command_args), "Attach telemetry command arguments");
695 gotel_command_args_destroy(&command_args);
696
697 log_info("Telemetry enabled: %s (%s)", config.exporter.endpoint, o->otel_enabled_telemetry);
698 return ASCIICHAT_OK;
699}
700void ascii_otel_log(int level, const char *file, int line, const char *function, const char *format, va_list args) {
701 (void)file;
702 (void)line;
703 (void)function;
704 if (in_log || level < minimum_log_level || !atomic_ptr_load(&host))
705 return;
706 in_log = true;
707 gotel_host_t *h = retain();
708 if (h) {
709 char body[OTEL_BODY_BUFFER_SIZE];
710 int n = safe_vsnprintf(body, sizeof(body), format, args);
711 if (n >= 0) {
712 url_redact_diagnostic(body, (size_t)n >= sizeof(body));
713 otlp_severity_t severity = level >= LOG_FATAL ? OTLP_SEVERITY_FATAL
714 : level >= LOG_ERROR ? OTLP_SEVERITY_ERROR
715 : level >= LOG_WARN ? OTLP_SEVERITY_WARN
716 : level >= LOG_INFO ? OTLP_SEVERITY_INFO
717 : level >= LOG_DEBUG ? OTLP_SEVERITY_DEBUG
718 : OTLP_SEVERITY_TRACE;
719 gotel_log(h, severity, body, file, line, function);
720 }
721 release();
722 }
723 in_log = false;
724}
725gotel_context_t ascii_otel_context(void) {
726 return gotel_current_context();
727}
728void ascii_otel_scope_int(ascii_otel_scope_t *s, const char *key, int64_t value) {
729 if (s->retained)
730 gotel_attr_int(&s->scope, key, value);
731}
732void ascii_otel_scope_string(ascii_otel_scope_t *s, const char *key, const char *value) {
733 if (s->retained)
734 gotel_attr_string(&s->scope, key, value);
735}
736void ascii_otel_exit(int code) {
737 gotel_attr_int(&command_scope, "process.exit.code", code);
738 if (code)
739 gotel_error(&command_scope, "ascii_chat.exit", "Command failed");
740 else {
741 command_scope.failed = false;
742 if (command_scope.span)
743 (void)otel_status(otlp_span_set_status(command_scope.span, OTLP_STATUS_CODE_UNSET, ""), "otlp_span_set_status");
744 }
745}
746void ascii_otel_frame_rejected(const char *validator, const char *reason, size_t bytes, uint32_t width, uint32_t height,
747 asciichat_error_t code) {
748 ascii_otel_metric(ASCII_OTEL_METRIC_FRAME_VALIDATION_REJECTED, 1);
749 gotel_context_t parent = ascii_otel_context();
750 ascii_otel_scope_t otel_scope __attribute__((cleanup(ascii_otel_scope_end))) = {0};
751 if (!scope_retained(&otel_scope, ASCII_OTEL_FRAME_VALIDATION_REJECTED, &parent))
752 return;
753 ASCII_OTEL_ATTR_STRING("frame.validation.validator", validator);
754 ASCII_OTEL_ATTR_STRING("frame.validation.reason", reason);
755 ASCII_OTEL_ATTR_INT("frame.size_bytes", (int64_t)bytes);
756 ASCII_OTEL_ATTR_INT("frame.width", width);
757 ASCII_OTEL_ATTR_INT("frame.height", height);
758 ASCII_OTEL_ATTR_INT("asciichat_errno.code", code);
759 ASCII_OTEL_ATTR_STRING("asciichat_errno.code_message", asciichat_error_string(code));
760 if (otel_scope.retained)
761 gotel_error(&otel_scope.scope, "ascii_chat.invalid_frame", reason);
762}
764 if (code == ASCIICHAT_OK || code == ERROR_SIGNAL_INTERRUPT || code == ERROR_NETWORK_INCOMPLETE)
766 if ((code >= ERROR_CRYPTO && code <= ERROR_CRYPTO_VERIFICATION) || code == ERROR_CRYPTO_INIT ||
768 return ASCII_OTEL_FAILURE_CRYPTO;
769 if (code == ERROR_NETWORK_PROTOCOL || code == ERROR_NETWORK_SIZE || code == ERROR_UNKNOWN_PACKET)
770 return ASCII_OTEL_FAILURE_PROTOCOL;
771 if ((code >= ERROR_SESSION_NOT_FOUND && code <= ERROR_SESSION_FULL) || code == ERROR_ACDS_STRING_TAKEN ||
772 code == ERROR_ACDS_STRING_INVALID || (origin && !strncmp(origin, "ascii-chat.discovery.", 21)))
773 return ASCII_OTEL_FAILURE_DISCOVERY;
775}
776void ascii_otel_error_code(asciichat_error_t code, const char *message) {
777 if (code == ASCIICHAT_OK || code == ERROR_SIGNAL_INTERRUPT || code == ERROR_NETWORK_INCOMPLETE)
778 return;
779 char redacted[OTEL_BODY_BUFFER_SIZE];
780 int written = safe_snprintf(redacted, sizeof(redacted), "%s", message ? message : "");
781 if (written < 0)
782 return;
783 url_redact_diagnostic(redacted, (size_t)written >= sizeof(redacted));
784 gotel_current_error("ascii_chat.error", redacted);
785 if (!ascii_otel_enabled())
786 return;
787 const char *origin = ascii_otel_current_operation();
789 if (operation == ASCII_OTEL_OPERATION_COUNT)
790 return;
791 const char *category = operation == ASCII_OTEL_FAILURE_CRYPTO ? "crypto"
792 : operation == ASCII_OTEL_FAILURE_PROTOCOL ? "protocol"
793 : "discovery";
794 gotel_context_t parent = ascii_otel_context();
796 if (!scope_retained(&report, operation, &parent))
797 return;
798 gotel_attr_string(&report.scope, "ascii_chat.failure.category", category);
799 gotel_attr_string(&report.scope, "ascii_chat.failure.operation", origin);
800 gotel_attr_int(&report.scope, "asciichat_errno.code", code);
801 gotel_attr_string(&report.scope, "asciichat_errno.code_message", asciichat_error_string(code));
802 gotel_error(&report.scope, category, redacted);
803}
804void ascii_otel_error(const char *message) {
805 char redacted[OTEL_BODY_BUFFER_SIZE];
806 int written = safe_snprintf(redacted, sizeof(redacted), "%s", message ? message : "");
807 if (written < 0)
808 return;
809 url_redact_diagnostic(redacted, (size_t)written >= sizeof(redacted));
810 gotel_current_error("ascii_chat.error", redacted);
811}
812void ascii_otel_http(ascii_otel_scope_t *s, const char *hostname, const char *path) {
813 if (!s->retained || !hostname || !path)
814 return;
815 gotel_error(&s->scope, "http.transport", "HTTP operation did not complete");
817 int n = safe_snprintf(url, sizeof(url), "https://%s%s", hostname, path);
818 if (n >= 0 && (size_t)n < sizeof(url) &&
819 otel_status(gotel_http_name(name, sizeof(name), "ascii-chat", false, "GET", url), "Name HTTP telemetry span") ==
820 ASCIICHAT_OK) {
821 if (s->scope.span)
822 (void)otel_status(otlp_span_set_name(s->scope.span, name), "otlp_span_set_name");
823 gotel_http_attributes(&s->scope, "GET", 0);
824 gotel_attr_string(&s->scope, "server.address", hostname);
825 gotel_attr_string(&s->scope, "url.scheme", "https");
826 }
827}
829 if (s->retained) {
830 if (status > 0 && status < 400) {
831 s->scope.failed = false;
832 if (s->scope.span) {
833 (void)otel_status(otlp_span_set_status(s->scope.span, OTLP_STATUS_CODE_UNSET, ""), "otlp_span_set_status");
834 gotel_attr_string(&s->scope, "error.type", "");
835 }
836 }
837 gotel_http_attributes(&s->scope, "GET", status);
838 }
839}
840int ascii_otel_sqlite_prepare(sqlite3 *db, const char *sql, sqlite3_stmt **statement) {
841 return gotel_sqlite_prepare_db(db, sql, statement);
842}
843
844void ascii_otel_sqlite(sqlite3 *db, const gotel_query_t *queries, size_t count) {
845 gotel_host_t *h = retain();
846 if (!h)
847 return;
848 gotel_sqlite_t *a = NULL;
849 sequel_analysis_options_t analysis = {.format = true};
850 gotel_sqlite_options_t options = {.analysis = &analysis};
851 if (otel_status(gotel_sqlite_attach_with_options(h, db, "ascii-chat.storage", queries, count, &options, &a),
852 "Attach SQLite telemetry") != ASCIICHAT_OK)
853 log_warn("Could not attach SQLite telemetry");
854 release();
855}
856
857void ascii_otel_sqlite_catalog(sqlite3 *db, const gotel_query_catalog_t *catalog) {
858 gotel_host_t *h = retain();
859 if (!h)
860 return;
861 gotel_sqlite_t *a = NULL;
862 sequel_analysis_options_t analysis = {.format = true};
863 gotel_sqlite_options_t options = {.analysis = &analysis};
864 if (otel_status(gotel_sqlite_attach_catalog_with_options(h, db, "ascii-chat.storage", catalog, &options, &a),
865 "Attach SQLite telemetry") != ASCIICHAT_OK)
866 log_warn("Could not attach SQLite telemetry");
867 release();
868}
action_args_t args
Definition actions.c:78
deferred_action_t action
Definition actions.c:77
bool errno_notifications_suppress(bool suppress)
#define SET_ERRNO(code,...)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
unsigned short uint16_t
Definition common.h:61
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_GETENV(name)
Definition common.h:449
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507
unsigned long long uint64_t
Definition common.h:63
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_NETWORK_CONNECT
Definition error_codes.h:74
@ ERROR_NETWORK_INCOMPLETE
Definition error_codes.h:90
@ ERROR_SIGNAL_INTERRUPT
@ ERROR_UNKNOWN_PACKET
Definition error_codes.h:88
@ ERROR_INVALID_PASSWORD
Definition error_codes.h:83
@ ERROR_ACDS_STRING_INVALID
Definition error_codes.h:86
@ ERROR_ACDS_STRING_TAKEN
Definition error_codes.h:85
@ ERROR_NETWORK
Definition error_codes.h:72
@ ERROR_CRYPTO_VERIFICATION
Definition error_codes.h:97
@ ERROR_RESOURCE_EXHAUSTED
@ ERROR_BUFFER_FULL
@ ERROR_NETWORK_PROTOCOL
Definition error_codes.h:76
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_STRING
@ ERROR_INVALID_SIGNATURE
Definition error_codes.h:84
@ ERROR_NETWORK_SIZE
Definition error_codes.h:77
@ ERROR_SESSION_FULL
Definition error_codes.h:82
@ ERROR_CRYPTO_INIT
Definition error_codes.h:53
@ ERROR_CONFIG
Definition error_codes.h:52
@ ERROR_CRYPTO
Definition error_codes.h:93
@ ERROR_INVALID_PARAM
@ ERROR_BUFFER_OVERFLOW
@ ERROR_INTERNAL
Definition error_codes.h:87
@ ERROR_SESSION_NOT_FOUND
Definition error_codes.h:81
@ ERROR_NETWORK_TIMEOUT
Definition error_codes.h:75
@ ERROR_THREAD
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:69
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
int32_t vec_t __attribute__((vector_size(4 *4)))
SESSION_CREATED (PACKET_TYPE_ACIP_SESSION_CREATED) - Session created response.
Definition vector.h:42
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS_INT
Definition time.h:168
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
#define OPT_OTEL_ENDPOINT_DEFAULT
Debug and Dev builds export to the local Spaniel Compose collector.
Definition options.h:354
const options_t * options_get(void)
Get pointer to current options struct (lock-free, thread-safe).
Definition rcu.c:511
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
Definition options.h:310
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Platform-safe vsnprintf wrapper.
asciichat_error_t platform_process_stats(platform_process_stats_t *out)
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
#define PROTOCOL_VERSION_MAJOR
Major protocol version number.
#define PROTOCOL_VERSION_MINOR
Minor protocol version number.
#define PROTOCOL_VERSION_PATCH
Patch protocol version number.
void url_redact_diagnostic(char *text, bool truncated)
Definition url.c:31
const char * acip_packet_type_name(uint16_t type)
Get human-readable name for packet type.
Definition handlers.c:648
Transport abstraction layer for ACIP protocol.
@ ACIP_TRANSPORT_TCP
Raw TCP socket.
@ ACIP_TRANSPORT_WEBSOCKET
WebSocket over TCP.
@ ACIP_TRANSPORT_WEBRTC
WebRTC DataChannel (P2P).
struct stats_scope stats_scope_t
void stats_scope_set_observer(stats_scope_t *scope, stats_observer_fn observer, void *context)
📝 Logging API with multiple log levels and terminal output control
🔢 Mathematical Utility Functions
ACIP protocol message structures and helpers.
bool mutex_stack_try_snapshot(mutex_wait_snapshot_t *waits, size_t capacity, size_t *count, bool *limited)
Definition mutex.c:507
Per-thread mutex lock stack for deadlock detection.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
void ascii_otel_error_code(asciichat_error_t code, const char *message)
Definition otel.c:776
#define OTEL_VERSION_BUFFER_SIZE
Definition otel.c:6
void ascii_otel_scope_parent(ascii_otel_scope_t *s, ascii_otel_operation_t op, const gotel_context_t *parent)
Definition otel.c:390
#define OTEL_NAME_EXTENDED_BUFFER_SIZE
Definition otel.c:9
bool ascii_otel_enabled(void)
Definition otel.c:158
#define OTEL_WAIT_SNAPSHOT_CAPACITY
Definition otel.c:12
void ascii_otel_http(ascii_otel_scope_t *s, const char *hostname, const char *path)
Definition otel.c:812
void ascii_otel_shutdown(void)
Definition otel.c:473
int ascii_otel_sqlite_prepare(sqlite3 *db, const char *sql, sqlite3_stmt **statement)
Definition otel.c:840
void ascii_otel_log(int level, const char *file, int line, const char *function, const char *format, va_list args)
Definition otel.c:700
gotel_context_t ascii_otel_context(void)
Definition otel.c:725
void ascii_otel_ui_usage(const char *screen, const char *event, uint64_t duration_ns)
Definition otel.c:434
void ascii_otel_exit(int code)
Definition otel.c:736
void ascii_otel_pipeline_progress(unsigned stage, unsigned expected)
Definition otel.c:270
void ascii_otel_stats_attach(stats_scope_t *scope)
Definition otel.c:218
ascii_otel_operation_t ascii_otel_failure_operation(asciichat_error_t code, const char *origin)
Definition otel.c:763
void ascii_otel_scope_begin(ascii_otel_scope_t *s, ascii_otel_operation_t op)
Definition otel.c:387
void ascii_otel_scope_string(ascii_otel_scope_t *s, const char *key, const char *value)
Definition otel.c:732
@ stats_metric_count
Definition otel.c:169
const char * ascii_otel_current_operation(void)
Definition otel.c:153
double ascii_otel_pipeline_age(uint64_t now, uint64_t capture, uint64_t progress, bool expected)
Definition otel.c:296
void ascii_otel_sqlite_catalog(sqlite3 *db, const gotel_query_catalog_t *catalog)
Definition otel.c:857
ascii_otel_metric_t ascii_otel_stats_metric(stats_record_kind_t kind, unsigned id)
Definition otel.c:181
void ascii_otel_scope_int(ascii_otel_scope_t *s, const char *key, int64_t value)
Definition otel.c:728
void ascii_otel_sqlite(sqlite3 *db, const gotel_query_t *queries, size_t count)
Definition otel.c:844
#define OTEL_URL_BUFFER_SIZE
Definition otel.c:8
void ascii_otel_error(const char *message)
Definition otel.c:804
#define OTEL_BODY_BUFFER_SIZE
Definition otel.c:7
#define OTEL_NAME_BUFFER_SIZE
Definition otel.c:5
void ascii_otel_packet(ascii_otel_scope_t *scope, uint16_t type, size_t payload_size, const char *action, int transport)
Definition otel.c:451
void ascii_otel_scope_end(ascii_otel_scope_t *s)
Definition otel.c:425
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
void ascii_otel_call_session_used(const char *session_string)
Definition otel.c:443
void ascii_otel_http_status(ascii_otel_scope_t *s, int status)
Definition otel.c:828
void ascii_otel_frame_rejected(const char *validator, const char *reason, size_t bytes, uint32_t width, uint32_t height, asciichat_error_t code)
Definition otel.c:746
asciichat_error_t ascii_otel_init(const char *mode, int argc, char **argv)
Definition otel.c:555
ascii_otel_operation_t
Definition otel.h:6
@ ASCII_OTEL_OPERATION_COUNT
Definition otel.h:10
#define ASCII_OTEL_SCOPE_PARENT(id, parent)
Definition otel.h:69
struct ascii_otel_scope ascii_otel_scope_t
ascii otel scope is intentionally a small, concrete record. Fields such as scope, retained,...
ascii_otel_metric_t
Definition otel.h:12
@ ASCII_OTEL_METRIC_COUNT
Definition otel.h:16
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
Protocol version, features, compression, and frame flag constants.
static bool size_t gotel_query_t * out
Definition queries.h:8
🔤 String Manipulation and Shell Escaping Utilities
gotel_scope_t scope
Definition otel.h:24
bool retained
Definition otel.h:25
struct ascii_otel_scope * previous
Definition otel.h:26
For mutex wait snapshot, this is the live record that ties mutex_key, thread_key, since_ns,...
Definition mutex.h:111
uint64_t since_ns
Definition mutex.h:114
uint64_t span_start_unix_ns
Definition mutex.h:121
char otel_ca_file[256]
Definition options.h:1159
int otel_realtime_max_per_second
Definition options.h:1162
double otel_realtime_sample_rate
Definition options.h:1161
int otel_metrics_interval
Definition options.h:1163
int otel_queue_capacity
Definition options.h:1165
char otel_endpoint[256]
Definition options.h:1158
char otel_log_level[256]
Definition options.h:1157
char address6[256]
IPv6 bind address (server only).
Definition options.h:1008
int port
Server port number.
Definition options.h:1009
char otel_enabled_telemetry[256]
Definition options.h:1160
char address[256]
Server address (client) or bind address (server).
Definition options.h:1007
Test environment detection utilities.
⏱️ High-precision timing utilities using sokol_time.h and uthash
#define LOG_DEV
Definition types.h:38
#define LOG_DEBUG
Definition types.h:39
#define LOG_FATAL
Definition types.h:43
#define LOG_ERROR
Definition types.h:42
#define LOG_WARN
Definition types.h:41
#define LOG_INFO
Definition types.h:40
🌐 Production-Grade URL Validation and Parsing