cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
otel.c
Go to the documentation of this file.
1
15#define _POSIX_C_SOURCE 200809L
18#include <cwist/net/http/http.h>
20#include <cjson/cJSON.h>
21#include <openssl/rand.h>
22#include <pthread.h>
23#include <stdbool.h>
24#include <stdint.h>
25#include <stdio.h>
26#include <string.h>
27#include <time.h>
28
29#define OTEL_DEFAULT_ENDPOINT "http://localhost:4318/v1/traces"
30#define OTEL_DEFAULT_SERVICE "cwist"
31#define OTEL_DEFAULT_QUEUE 512U
32#define OTEL_DEFAULT_BATCH 64U
33#define OTEL_EXPORT_TIMEOUT_MS 2000
34#define OTEL_TRACE_ID_HEX 32U
35#define OTEL_SPAN_ID_HEX 16U
36
37typedef struct {
38 char trace_id[OTEL_TRACE_ID_HEX + 1];
39 char span_id[OTEL_SPAN_ID_HEX + 1];
40 char parent_span_id[OTEL_SPAN_ID_HEX + 1]; /* "" for a root span */
42 char path[256];
43 int status;
44 uint64_t start_ns;
45 uint64_t end_ns;
46} otel_span_t;
47
48/* Configuration is fixed by the first cwist_mw_otel_trace() call; the
49 * middleware is a plain function pointer, so there is one exporter per
50 * process. g_otel_lock guards the ring and the writer state. */
51static struct {
52 char *endpoint;
54 size_t batch;
55 otel_span_t *ring;
56 size_t cap;
57 size_t head;
58 size_t count;
62} g_otel;
64static pthread_cond_t g_otel_work = PTHREAD_COND_INITIALIZER;
65
66/* Trace context of the requests running on this thread, innermost first, for
67 * cwist_mw_otel_get_trace_id() (same scheme as cwist_mw_jwt_get_claims()). */
68typedef struct otel_ctx {
69 const cwist_http_request *req;
70 const char *trace_id;
71 struct otel_ctx *outer;
73static _Thread_local otel_ctx *t_otel_ctx;
74
75static uint64_t otel_unix_nanos(void) {
76 struct timespec ts;
78 return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
79}
80
81static bool otel_is_hex(const char *s, size_t n) {
82 for (size_t i = 0; i < n; i++) {
83 char c = s[i];
84 if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return false;
85 }
86 return true;
87}
88
89static bool otel_is_zero(const char *s, size_t n) {
90 for (size_t i = 0; i < n; i++) {
91 if (s[i] != '0') return false;
92 }
93 return true;
94}
95
96/* Random non-zero id of `nibbles` lowercase hex digits. */
97static void otel_random_id(char *out, size_t nibbles) {
98 static const char hex[] = "0123456789abcdef";
99 uint8_t bytes[OTEL_TRACE_ID_HEX / 2];
100 do {
101 if (RAND_bytes(bytes, (int)(nibbles / 2)) != 1) memset(bytes, 0x5a, sizeof(bytes));
102 for (size_t i = 0; i < nibbles / 2; i++) {
103 out[2 * i] = hex[bytes[i] >> 4];
104 out[2 * i + 1] = hex[bytes[i] & 0xF];
105 }
106 out[nibbles] = '\0';
107 } while (otel_is_zero(out, nibbles));
108}
109
110/* W3C trace-context: "vv-<32 hex trace-id>-<16 hex parent-id>-ff", version
111 * not "ff", ids not all zero. Later versions may append "-..." fields. */
112static bool otel_parse_traceparent(const char *h, char *trace_id, char *parent_id) {
113 if (!h || strlen(h) < 55) return false;
114 if (!otel_is_hex(h, 2) || strncmp(h, "ff", 2) == 0 || h[2] != '-') return false;
115 if (!otel_is_hex(h + 3, OTEL_TRACE_ID_HEX) || otel_is_zero(h + 3, OTEL_TRACE_ID_HEX) ||
116 h[35] != '-')
117 return false;
118 if (!otel_is_hex(h + 36, OTEL_SPAN_ID_HEX) || otel_is_zero(h + 36, OTEL_SPAN_ID_HEX) ||
119 h[52] != '-')
120 return false;
121 if (!otel_is_hex(h + 53, 2) || (h[55] != '\0' && (strncmp(h, "00", 2) == 0 || h[55] != '-')))
122 return false;
123 memcpy(trace_id, h + 3, OTEL_TRACE_ID_HEX);
124 trace_id[OTEL_TRACE_ID_HEX] = '\0';
127 return true;
128}
129
130static cJSON *otel_attr(const char *key, cJSON *value, const char *type) {
132 cJSON_AddStringToObject(a, "key", key);
133 cJSON_AddItemToObject(cJSON_AddObjectToObject(a, "value"), type, value);
134 return a;
135}
136
137/* OTLP/HTTP JSON body for `n` spans (uint64 fields are strings per the
138 * protobuf JSON mapping). */
139static char *otel_encode_spans(const otel_span_t *spans, size_t n) {
143 cJSON *rattrs = cJSON_AddArrayToObject(cJSON_AddObjectToObject(rs, "resource"), "attributes");
145 rattrs, otel_attr("service.name", cJSON_CreateString(g_otel.service_name), "stringValue"));
148 cJSON_AddStringToObject(cJSON_AddObjectToObject(ss, "scope"), "name", "cwist");
149 cJSON *out = cJSON_AddArrayToObject(ss, "spans");
150
151 for (size_t i = 0; i < n; i++) {
152 const otel_span_t *s = &spans[i];
153 const char *method = cwist_http_method_to_string(s->method);
154 char num[24];
156 cJSON_AddStringToObject(span, "traceId", s->trace_id);
157 cJSON_AddStringToObject(span, "spanId", s->span_id);
158 if (s->parent_span_id[0]) cJSON_AddStringToObject(span, "parentSpanId", s->parent_span_id);
159 cJSON_AddStringToObject(span, "name", method);
160 cJSON_AddNumberToObject(span, "kind", 2); /* SPAN_KIND_SERVER */
161 snprintf(num, sizeof(num), "%llu", (unsigned long long)s->start_ns);
162 cJSON_AddStringToObject(span, "startTimeUnixNano", num);
163 snprintf(num, sizeof(num), "%llu", (unsigned long long)s->end_ns);
164 cJSON_AddStringToObject(span, "endTimeUnixNano", num);
165 cJSON *attrs = cJSON_AddArrayToObject(span, "attributes");
167 attrs, otel_attr("http.request.method", cJSON_CreateString(method), "stringValue"));
169 otel_attr("url.path", cJSON_CreateString(s->path), "stringValue"));
170 snprintf(num, sizeof(num), "%d", s->status);
172 attrs, otel_attr("http.response.status_code", cJSON_CreateString(num), "intValue"));
173 if (s->status >= 500)
176 }
177 char *body = cJSON_PrintUnformatted(root);
179 return body;
180}
181
182static void otel_export_batch(cwist_http_client *client, const otel_span_t *spans, size_t n) {
183 char *body = otel_encode_spans(spans, n);
184 if (!body) return;
185 cwist_http_header_node *headers = NULL;
186 cwist_http_header_add(&headers, "Content-Type", "application/json");
188 cwist_http_client_request(client, g_otel.endpoint, CWIST_HTTP_POST, headers, body, strlen(body),
189 &res);
190 if (res) cwist_http_response_destroy(res);
192 cJSON_free(body);
193}
194
195static void *otel_exporter_main(void *arg) {
196 (void)arg;
198 otel_span_t *batch = cwist_alloc_array(g_otel.batch, sizeof(*batch));
200
201 pthread_mutex_lock(&g_otel_lock);
202 for (;;) {
203 while (g_otel.count == 0) pthread_cond_wait(&g_otel_work, &g_otel_lock);
204 size_t n = g_otel.count < g_otel.batch ? g_otel.count : g_otel.batch;
205 for (size_t i = 0; i < n; i++) {
206 if (batch) batch[i] = g_otel.ring[g_otel.head];
207 g_otel.head = (g_otel.head + 1) % g_otel.cap;
208 }
209 g_otel.count -= n;
210 pthread_mutex_unlock(&g_otel_lock);
211 if (client && batch) otel_export_batch(client, batch, n);
212 pthread_mutex_lock(&g_otel_lock);
213 }
214 return NULL;
215}
216
217static void otel_atfork_prepare(void) {
218 pthread_mutex_lock(&g_otel_lock);
219}
220
221static void otel_atfork_parent(void) {
222 pthread_mutex_unlock(&g_otel_lock);
223}
224
225/* The parent exports its own queued spans; the child starts empty and spawns
226 * its exporter on its first span. */
227static void otel_atfork_child(void) {
228 g_otel.head = g_otel.count = 0;
229 g_otel.exporter_alive = false;
231 g_otel_work = fresh;
232 pthread_mutex_unlock(&g_otel_lock);
233}
234
235static void otel_enqueue(const otel_span_t *span) {
236 pthread_mutex_lock(&g_otel_lock);
237 if (!g_otel.exporter_alive)
238 g_otel.exporter_alive =
239 pthread_create(&g_otel.exporter, NULL, otel_exporter_main, NULL) == 0;
240 if (g_otel.exporter_alive) {
241 if (g_otel.count == g_otel.cap) { /* full: drop the oldest */
242 g_otel.head = (g_otel.head + 1) % g_otel.cap;
243 g_otel.count--;
244 }
245 g_otel.ring[(g_otel.head + g_otel.count) % g_otel.cap] = *span;
246 g_otel.count++;
247 pthread_cond_signal(&g_otel_work);
248 }
249 pthread_mutex_unlock(&g_otel_lock);
250}
251
252static void otel_middleware(cwist_http_request *req, cwist_http_response *res,
253 cwist_handler_func next) {
254 otel_span_t span = {.method = req->method, .start_ns = otel_unix_nanos()};
255 if (!otel_parse_traceparent(cwist_http_header_get(req->headers, "traceparent"), span.trace_id,
256 span.parent_span_id))
257 otel_random_id(span.trace_id, OTEL_TRACE_ID_HEX);
258 otel_random_id(span.span_id, OTEL_SPAN_ID_HEX);
259
260 otel_ctx ctx = {.req = req, .trace_id = span.trace_id, .outer = t_otel_ctx};
261 t_otel_ctx = &ctx;
262 next(req, res);
263 t_otel_ctx = ctx.outer;
264
265 span.end_ns = otel_unix_nanos();
266 span.status = (int)res->status_code;
267 snprintf(span.path, sizeof(span.path), "%s",
268 req->path && req->path->data ? req->path->data : "/");
269 otel_enqueue(&span);
270}
271
273 pthread_mutex_lock(&g_otel_lock);
274 if (!g_otel.configured) {
275 const char *endpoint = cfg && cfg->endpoint ? cfg->endpoint : OTEL_DEFAULT_ENDPOINT;
276 const char *service = cfg && cfg->service_name ? cfg->service_name : OTEL_DEFAULT_SERVICE;
277 size_t cap = cfg && cfg->queue_cap ? cfg->queue_cap : OTEL_DEFAULT_QUEUE;
278 size_t batch = cfg && cfg->batch_size ? cfg->batch_size : OTEL_DEFAULT_BATCH;
279 g_otel.endpoint = cwist_strdup(endpoint);
280 g_otel.service_name = cwist_strdup(service);
281 g_otel.ring = cwist_alloc_array(cap, sizeof(*g_otel.ring));
282 if (g_otel.endpoint && g_otel.service_name && g_otel.ring &&
283 pthread_atfork(otel_atfork_prepare, otel_atfork_parent, otel_atfork_child) == 0) {
284 g_otel.cap = cap;
285 g_otel.batch = batch < cap ? batch : cap;
286 g_otel.configured = true;
287 } else {
288 cwist_free(g_otel.endpoint);
289 cwist_free(g_otel.service_name);
290 cwist_free(g_otel.ring);
291 g_otel.endpoint = g_otel.service_name = NULL;
292 g_otel.ring = NULL;
293 }
294 }
295 bool ok = g_otel.configured;
296 pthread_mutex_unlock(&g_otel_lock);
297 return ok ? otel_middleware : NULL;
298}
299
301 if (!req) return NULL;
302 for (const otel_ctx *ctx = t_otel_ctx; ctx; ctx = ctx->outer) {
303 if (ctx->req == req) return ctx->trace_id;
304 }
305 return NULL;
306}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
void * cwist_alloc_array(size_t count, size_t elem_size)
Allocate zeroed memory for count elements.
Definition alloc.c:21
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
Definition http.c:1880
cwist_http_method_t
Definition http.h:16
@ CWIST_HTTP_POST
Definition http.h:18
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
void cwist_http_response_destroy(cwist_http_response *res)
Destroy an HTTP response object.
Definition http.c:2315
void cwist_http_header_free_all(cwist_http_header_node *head)
Free all headers.
Definition http.c:2006
const char * cwist_http_method_to_string(cwist_http_method_t method)
Convert method enum to string.
Definition http.c:1554
Unified HTTP/1.1 and HTTP/2 client using libcurl.
cwist_http_client * cwist_http_client_create(void)
Create a new HTTP client.
struct cwist_http_client cwist_http_client
Opaque HTTP client handle.
Definition http_client.h:28
cwist_error_t cwist_http_client_request(cwist_http_client *client, const char *url, cwist_http_method_t method, cwist_http_header_node *headers, const char *body, size_t body_len, cwist_http_response **out_response)
Perform a synchronous HTTP request.
void cwist_http_client_set_timeout_ms(cwist_http_client *client, int timeout_ms)
Set request timeout in milliseconds.
HTTP Protocol Definitions and Helpers.
size_t batch
Definition otel.c:54
struct otel_ctx otel_ctx
#define OTEL_DEFAULT_SERVICE
Definition otel.c:30
const char * cwist_mw_otel_get_trace_id(const cwist_http_request *req)
Trace id of the span cwist_mw_otel_trace() opened for req.
Definition otel.c:300
#define OTEL_DEFAULT_BATCH
Definition otel.c:32
#define OTEL_DEFAULT_ENDPOINT
Definition otel.c:29
bool configured
Definition otel.c:59
#define OTEL_DEFAULT_QUEUE
Definition otel.c:31
bool exporter_alive
Definition otel.c:60
char * endpoint
Definition otel.c:52
char * service_name
Definition otel.c:53
#define OTEL_EXPORT_TIMEOUT_MS
Definition otel.c:33
size_t head
Definition otel.c:57
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
#define OTEL_TRACE_ID_HEX
Definition otel.c:34
pthread_t exporter
Definition otel.c:61
#define OTEL_SPAN_ID_HEX
Definition otel.c:35
cwist_middleware_func cwist_mw_otel_trace(const cwist_otel_cfg_t *cfg)
OpenTelemetry tracing middleware.
Definition otel.c:272
otel_span_t * ring
Definition otel.c:55
mbedTLS TLS backend: RAND_bytes over mbedTLS CTR-DRBG.
#define RAND_bytes
Definition rand.h:17
Linked list for headers to handle multiple headers easily.
Definition http.h:59
cwist_sstring * path
e.g., "/users/1"
Definition http.h:67
cwist_http_header_node * headers
Definition http.h:72
cwist_http_method_t method
Definition http.h:66
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
cwist_http_status_t status_code
Definition http.h:84
Configuration for cwist_mw_otel_trace().
Definition middleware.h:116
const char * endpoint
Definition middleware.h:117
const char * service_name
Definition middleware.h:119
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
void(* cwist_handler_func)(cwist_http_request *req, cwist_http_response *res)
Function pointer type for HTTP route handlers.
Definition app.h:32
void(* cwist_middleware_func)(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next)
Middleware type that receives req/res pair and the next stage in the chain.
Definition app.h:115
Built-in middleware factories.