emme coverage


Directory: src/
File: src/metrics.c
Date: 2026-05-14 14:35:13
Exec Total Coverage
Lines: 303 371 81.7%
Functions: 22 28 78.6%
Branches: 57 114 50.0%

Line Branch Exec Source
1 #include "metrics.h"
2 #include "log.h"
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <sys/socket.h>
8 #include <netinet/in.h>
9 #include <arpa/inet.h>
10
11 #define METRICS_BUFFER_SIZE 65536
12 #define METRICS_REQUEST_BUFFER_SIZE 4096
13 #define METRICS_RESPONSE_BUFFER_SIZE 8192
14 #define METRICS_SERVER_BACKLOG 10
15
16 static MetricsRegistry g_metrics;
17 static int g_metrics_server_fd = -1;
18 static pthread_t g_metrics_server_thread;
19 static _Atomic int g_server_running = 0;
20
21 static const double HISTOGRAM_BUCKET_BOUNDS[HISTOGRAM_BUCKETS] = {
22 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 25.0, 50.0
23 };
24
25 36 static void initialize_histogram(MetricHistogram *histogram, const char *name,
26 const char *help)
27 {
28 36 histogram->metadata.name = name;
29 36 histogram->metadata.help = help;
30 36 histogram->metadata.type = "histogram";
31
2/2
✓ Branch 0 taken 504 times.
✓ Branch 1 taken 36 times.
540 for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
32 504 histogram->data.buckets[i] = HISTOGRAM_BUCKET_BOUNDS[i];
33 }
34 36 pthread_mutex_init(&histogram->data.lock, NULL);
35 36 }
36
37 18 void metrics_init(void)
38 {
39 18 memset(&g_metrics, 0, sizeof(g_metrics));
40
41 18 g_metrics.requests_total.metadata.name = "emme_requests_total";
42 18 g_metrics.requests_total.metadata.help = "Total number of HTTP requests";
43 18 g_metrics.requests_total.metadata.type = "counter";
44
45 18 g_metrics.tls_handshakes_total.metadata.name = "emme_tls_handshakes_total";
46 18 g_metrics.tls_handshakes_total.metadata.help = "Total number of TLS handshakes";
47 18 g_metrics.tls_handshakes_total.metadata.type = "counter";
48
49 18 g_metrics.request_timeouts_total.metadata.name = "emme_request_timeouts_total";
50 18 g_metrics.request_timeouts_total.metadata.help = "Total number of request timeouts";
51 18 g_metrics.request_timeouts_total.metadata.type = "counter";
52
53 18 g_metrics.security_headers_sent_total.metadata.name = "emme_security_headers_sent_total";
54 18 g_metrics.security_headers_sent_total.metadata.help = "Total number of security headers sent";
55 18 g_metrics.security_headers_sent_total.metadata.type = "counter";
56
57 18 g_metrics.cors_headers_sent_total.metadata.name = "emme_cors_headers_sent_total";
58 18 g_metrics.cors_headers_sent_total.metadata.help = "Total number of CORS headers sent";
59 18 g_metrics.cors_headers_sent_total.metadata.type = "counter";
60
61 18 g_metrics.per_ip_limit_rejected_total.metadata.name = "emme_per_ip_limit_rejected_total";
62 18 g_metrics.per_ip_limit_rejected_total.metadata.help = "Total number of connections rejected due to per-IP limit";
63 18 g_metrics.per_ip_limit_rejected_total.metadata.type = "counter";
64
65 18 g_metrics.ip_limiter_entries_total.metadata.name = "emme_ip_limiter_entries_total";
66 18 g_metrics.ip_limiter_entries_total.metadata.help = "Number of unique IPs currently tracked by the IP limiter";
67 18 g_metrics.ip_limiter_entries_total.metadata.type = "gauge";
68
69 18 g_metrics.active_connections.metadata.name = "emme_active_connections";
70 18 g_metrics.active_connections.metadata.help = "Number of active connections";
71 18 g_metrics.active_connections.metadata.type = "gauge";
72
73 18 g_metrics.thread_pool_active_threads.metadata.name = "emme_thread_pool_active_threads";
74 18 g_metrics.thread_pool_active_threads.metadata.help = "Number of active threads in the pool";
75 18 g_metrics.thread_pool_active_threads.metadata.type = "gauge";
76
77 18 g_metrics.thread_pool_queue_depth.metadata.name = "emme_thread_pool_queue_depth";
78 18 g_metrics.thread_pool_queue_depth.metadata.help = "Depth of the thread pool queue";
79 18 g_metrics.thread_pool_queue_depth.metadata.type = "gauge";
80
81 18 g_metrics.shutdown_drain_active.metadata.name = "emme_shutdown_drain_active";
82 18 g_metrics.shutdown_drain_active.metadata.help = "1 if shutdown drain is active, 0 otherwise";
83 18 g_metrics.shutdown_drain_active.metadata.type = "gauge";
84
85 18 initialize_histogram(&g_metrics.request_duration_seconds,
86 "emme_request_duration_seconds",
87 "HTTP request duration in seconds");
88
89 18 initialize_histogram(&g_metrics.tls_handshake_duration_seconds,
90 "emme_tls_handshake_duration_seconds",
91 "TLS handshake duration in seconds");
92
93 18 g_metrics.io_uring_sqe_depth.metadata.name = "emme_io_uring_sqe_depth";
94 18 g_metrics.io_uring_sqe_depth.metadata.help = "io_uring submission queue depth";
95 18 g_metrics.io_uring_sqe_depth.metadata.type = "gauge";
96
97 18 g_metrics.io_uring_cqe_depth.metadata.name = "emme_io_uring_cqe_depth";
98 18 g_metrics.io_uring_cqe_depth.metadata.help = "io_uring completion queue depth";
99 18 g_metrics.io_uring_cqe_depth.metadata.type = "gauge";
100
101 18 g_metrics.backend_pool_active.metadata.name = "emme_backend_pool_active_connections";
102 18 g_metrics.backend_pool_active.metadata.help = "Number of active backend connections";
103 18 g_metrics.backend_pool_active.metadata.type = "gauge";
104
105 18 g_metrics.backend_pool_idle.metadata.name = "emme_backend_pool_idle_connections";
106 18 g_metrics.backend_pool_idle.metadata.help = "Number of idle backend connections";
107 18 g_metrics.backend_pool_idle.metadata.type = "gauge";
108
109 18 g_metrics.backend_pool_healthy.metadata.name = "emme_backend_pool_healthy_connections";
110 18 g_metrics.backend_pool_healthy.metadata.help = "Number of healthy backend connections";
111 18 g_metrics.backend_pool_healthy.metadata.type = "gauge";
112
113 18 g_metrics.health_checker_total_checks.metadata.name = "emme_health_checker_total_checks";
114 18 g_metrics.health_checker_total_checks.metadata.help = "Total health checks performed";
115 18 g_metrics.health_checker_total_checks.metadata.type = "gauge";
116
117 18 g_metrics.health_checker_failed_checks.metadata.name = "emme_health_checker_failed_checks";
118 18 g_metrics.health_checker_failed_checks.metadata.help = "Total failed health checks";
119 18 g_metrics.health_checker_failed_checks.metadata.type = "gauge";
120
121 18 g_metrics.health_checker_health_status.metadata.name = "emme_health_checker_health_status";
122 18 g_metrics.health_checker_health_status.metadata.help = "Backend health status (0=unknown, 1=healthy, 2=unhealthy)";
123 18 g_metrics.health_checker_health_status.metadata.type = "gauge";
124
125 18 g_metrics.health_checker_last_check_time.metadata.name = "emme_health_checker_last_check_time_seconds";
126 18 g_metrics.health_checker_last_check_time.metadata.help = "Unix timestamp of last health check";
127 18 g_metrics.health_checker_last_check_time.metadata.type = "gauge";
128
129 18 g_metrics.circuit_breaker_state.metadata.name = "emme_circuit_breaker_state";
130 18 g_metrics.circuit_breaker_state.metadata.help = "Circuit breaker state (0=closed, 1=open, 2=half-open)";
131 18 g_metrics.circuit_breaker_state.metadata.type = "gauge";
132
133 18 g_metrics.circuit_breaker_failure_count.metadata.name = "emme_circuit_breaker_failure_count";
134 18 g_metrics.circuit_breaker_failure_count.metadata.help = "Current failure count";
135 18 g_metrics.circuit_breaker_failure_count.metadata.type = "gauge";
136
137 18 g_metrics.circuit_breaker_total_opens.metadata.name = "emme_circuit_breaker_total_opens";
138 18 g_metrics.circuit_breaker_total_opens.metadata.help = "Total times circuit breaker opened";
139 18 g_metrics.circuit_breaker_total_opens.metadata.type = "gauge";
140
141 18 g_metrics.circuit_breaker_total_closes.metadata.name = "emme_circuit_breaker_total_closes";
142 18 g_metrics.circuit_breaker_total_closes.metadata.help = "Total times circuit breaker closed";
143 18 g_metrics.circuit_breaker_total_closes.metadata.type = "gauge";
144
145 18 log_message(LOG_LEVEL_INFO, "Metrics registry initialized");
146 18 }
147
148 18 void metrics_shutdown(void)
149 {
150 18 metrics_stop_server();
151 18 pthread_mutex_destroy(&g_metrics.request_duration_seconds.data.lock);
152 18 pthread_mutex_destroy(&g_metrics.tls_handshake_duration_seconds.data.lock);
153 18 log_message(LOG_LEVEL_INFO, "Metrics registry shutdown complete");
154 18 }
155
156 122 static void histogram_record(HistogramData *histogram, double value)
157 {
158 122 pthread_mutex_lock(&histogram->lock);
159
160
1/2
✓ Branch 0 taken 587 times.
✗ Branch 1 not taken.
587 for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
161
2/2
✓ Branch 0 taken 122 times.
✓ Branch 1 taken 465 times.
587 if (value <= histogram->buckets[i]) {
162 122 atomic_fetch_add(&histogram->counts[i], 1);
163 122 break;
164 }
165 }
166
167 122 atomic_fetch_add(&histogram->sum_count, 1);
168 122 histogram->sum_total += value;
169
170 122 pthread_mutex_unlock(&histogram->lock);
171 122 }
172
173 109 void metrics_increment_request(const char *method, const char *path, int status)
174 {
175 (void)method;
176 (void)path;
177 (void)status;
178 109 atomic_fetch_add(&g_metrics.requests_total.value, 1);
179 109 }
180
181 110 void metrics_record_request_duration(double duration_seconds)
182 {
183 110 histogram_record(&g_metrics.request_duration_seconds.data, duration_seconds);
184 110 }
185
186 void metrics_increment_request_timeouts(void)
187 {
188 atomic_fetch_add(&g_metrics.request_timeouts_total.value, 1);
189 }
190
191 3 void metrics_increment_security_headers_sent(void)
192 {
193 3 atomic_fetch_add(&g_metrics.security_headers_sent_total.value, 1);
194 3 }
195
196 1 void metrics_increment_cors_headers_sent(void)
197 {
198 1 atomic_fetch_add(&g_metrics.cors_headers_sent_total.value, 1);
199 1 }
200
201 122 void metrics_set_active_connections(long count)
202 {
203 122 atomic_store(&g_metrics.active_connections.value, count);
204 122 }
205
206 void metrics_increment_per_ip_limit_rejected(void)
207 {
208 atomic_fetch_add(&g_metrics.per_ip_limit_rejected_total.value, 1);
209 }
210
211 void metrics_set_ip_limiter_entries(long count)
212 {
213 atomic_store(&g_metrics.ip_limiter_entries_total.value, count);
214 }
215
216 1 void metrics_set_thread_pool_stats(int active_threads, int queue_depth)
217 {
218 1 atomic_store(&g_metrics.thread_pool_active_threads.value, active_threads);
219 1 atomic_store(&g_metrics.thread_pool_queue_depth.value, queue_depth);
220 1 }
221
222 13 void metrics_increment_tls_handshake(int success)
223 {
224 (void)success;
225 13 atomic_fetch_add(&g_metrics.tls_handshakes_total.value, 1);
226 13 }
227
228 12 void metrics_record_tls_handshake_duration(double duration_seconds)
229 {
230 12 histogram_record(&g_metrics.tls_handshake_duration_seconds.data, duration_seconds);
231 12 }
232
233 void metrics_set_io_uring_depth(int sqe_depth, int cqe_depth)
234 {
235 atomic_store(&g_metrics.io_uring_sqe_depth.value, sqe_depth);
236 atomic_store(&g_metrics.io_uring_cqe_depth.value, cqe_depth);
237 }
238
239 2 void metrics_set_shutdown_drain(int active)
240 {
241 2 atomic_store(&g_metrics.shutdown_drain_active.value, active ? 1 : 0);
242 2 }
243
244 1 void metrics_set_backend_pool_stats(const char *backend, int active, int idle, int healthy)
245 {
246 (void)backend;
247 1 atomic_store(&g_metrics.backend_pool_active.value, active);
248 1 atomic_store(&g_metrics.backend_pool_idle.value, idle);
249 1 atomic_store(&g_metrics.backend_pool_healthy.value, healthy);
250 1 }
251
252 void metrics_set_health_checker_stats(const char *backend, int total_checks, int failed_checks,
253 int health_status, long last_check_time)
254 {
255 (void)backend;
256 atomic_store(&g_metrics.health_checker_total_checks.value, total_checks);
257 atomic_store(&g_metrics.health_checker_failed_checks.value, failed_checks);
258 atomic_store(&g_metrics.health_checker_health_status.value, health_status);
259 atomic_store(&g_metrics.health_checker_last_check_time.value, last_check_time);
260 }
261
262 void metrics_set_circuit_breaker_stats(const char *backend, int state, int failure_count,
263 long total_opens, long total_closes)
264 {
265 (void)backend;
266 atomic_store(&g_metrics.circuit_breaker_state.value, state);
267 atomic_store(&g_metrics.circuit_breaker_failure_count.value, failure_count);
268 atomic_store(&g_metrics.circuit_breaker_total_opens.value, total_opens);
269 atomic_store(&g_metrics.circuit_breaker_total_closes.value, total_closes);
270 }
271
272 54 static int format_counter(char *buffer, size_t remaining, const MetricCounter *counter)
273 {
274 108 return snprintf(buffer, remaining,
275 "# HELP %s %s\n# TYPE %s counter\n%ld\n",
276 54 counter->metadata.name,
277 54 counter->metadata.help,
278 54 counter->metadata.type,
279 54 atomic_load(&counter->value));
280 }
281
282 162 static int format_gauge(char *buffer, size_t remaining, const MetricGauge *gauge)
283 {
284 324 return snprintf(buffer, remaining,
285 "# HELP %s %s\n# TYPE %s gauge\n%ld\n",
286 162 gauge->metadata.name,
287 162 gauge->metadata.help,
288 162 gauge->metadata.type,
289 162 atomic_load(&gauge->value));
290 }
291
292 18 static int format_histogram(char *buffer, size_t remaining, const MetricHistogram *histogram)
293 {
294 18 int written = 0;
295 18 size_t offset = 0;
296
297 18 written = snprintf(buffer, remaining,
298 "# HELP %s %s\n# TYPE histogram\n",
299 18 histogram->metadata.name,
300 18 histogram->metadata.help);
301
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 18 times.
18 if (written < 0 || (size_t)written >= remaining) {
302 return -1;
303 }
304 18 offset = (size_t)written;
305
306 18 pthread_mutex_lock((pthread_mutex_t *)&histogram->data.lock);
307 18 long cumulative = 0;
308
309
2/2
✓ Branch 0 taken 252 times.
✓ Branch 1 taken 18 times.
270 for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
310 252 cumulative += atomic_load(&histogram->data.counts[i]);
311 252 int n = snprintf(buffer + offset, remaining - offset,
312 "%s_bucket{le=\"%.3f\"} %ld\n",
313 252 histogram->metadata.name,
314 252 histogram->data.buckets[i],
315 cumulative);
316
2/4
✓ Branch 0 taken 252 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 252 times.
252 if (n < 0 || (size_t)n >= remaining - offset) {
317 pthread_mutex_unlock((pthread_mutex_t *)&histogram->data.lock);
318 return -1;
319 }
320 252 offset += (size_t)n;
321 }
322
323 18 int n = snprintf(buffer + offset, remaining - offset,
324 "%s_bucket{le=\"+Inf\"} %ld\n",
325 18 histogram->metadata.name,
326 18 atomic_load(&histogram->data.sum_count));
327
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 18 times.
18 if (n < 0 || (size_t)n >= remaining - offset) {
328 pthread_mutex_unlock((pthread_mutex_t *)&histogram->data.lock);
329 return -1;
330 }
331 18 offset += (size_t)n;
332
333 18 n = snprintf(buffer + offset, remaining - offset,
334 "%s_sum %f\n",
335 18 histogram->metadata.name,
336 18 histogram->data.sum_total);
337
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 18 times.
18 if (n < 0 || (size_t)n >= remaining - offset) {
338 pthread_mutex_unlock((pthread_mutex_t *)&histogram->data.lock);
339 return -1;
340 }
341 18 offset += (size_t)n;
342
343 18 n = snprintf(buffer + offset, remaining - offset,
344 "%s_count %ld\n",
345 18 histogram->metadata.name,
346 18 atomic_load(&histogram->data.sum_count));
347
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 18 times.
18 if (n < 0 || (size_t)n >= remaining - offset) {
348 pthread_mutex_unlock((pthread_mutex_t *)&histogram->data.lock);
349 return -1;
350 }
351 18 offset += (size_t)n;
352
353 18 pthread_mutex_unlock((pthread_mutex_t *)&histogram->data.lock);
354 18 return (int)offset;
355 }
356
357 9 char *metrics_format_prometheus(void)
358 {
359 9 char *buffer = malloc(METRICS_BUFFER_SIZE);
360
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (!buffer) {
361 return NULL;
362 }
363
364 9 size_t offset = 0;
365 9 int written = 0;
366
367 9 written = format_counter(buffer + offset, METRICS_BUFFER_SIZE - offset,
368 &g_metrics.requests_total);
369
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
370 9 offset += (size_t)written;
371
372 9 written = format_counter(buffer + offset, METRICS_BUFFER_SIZE - offset,
373 &g_metrics.tls_handshakes_total);
374
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
375 9 offset += (size_t)written;
376
377 9 written = format_counter(buffer + offset, METRICS_BUFFER_SIZE - offset,
378 &g_metrics.request_timeouts_total);
379
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
380 9 offset += (size_t)written;
381
382 9 written = format_counter(buffer + offset, METRICS_BUFFER_SIZE - offset,
383 &g_metrics.security_headers_sent_total);
384
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
385 9 offset += (size_t)written;
386
387 9 written = format_counter(buffer + offset, METRICS_BUFFER_SIZE - offset,
388 &g_metrics.cors_headers_sent_total);
389
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
390 9 offset += (size_t)written;
391
392 9 written = format_counter(buffer + offset, METRICS_BUFFER_SIZE - offset,
393 &g_metrics.per_ip_limit_rejected_total);
394
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
395 9 offset += (size_t)written;
396
397 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
398 &g_metrics.active_connections);
399
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
400 9 offset += (size_t)written;
401
402 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
403 &g_metrics.thread_pool_active_threads);
404
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
405 9 offset += (size_t)written;
406
407 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
408 &g_metrics.thread_pool_queue_depth);
409
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
410 9 offset += (size_t)written;
411
412 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
413 &g_metrics.shutdown_drain_active);
414
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
415 9 offset += (size_t)written;
416
417 9 written = format_histogram(buffer + offset, METRICS_BUFFER_SIZE - offset,
418 &g_metrics.request_duration_seconds);
419
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
420 9 offset += (size_t)written;
421
422 9 written = format_histogram(buffer + offset, METRICS_BUFFER_SIZE - offset,
423 &g_metrics.tls_handshake_duration_seconds);
424
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
425 9 offset += (size_t)written;
426
427 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
428 &g_metrics.io_uring_sqe_depth);
429
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
430 9 offset += (size_t)written;
431
432 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
433 &g_metrics.io_uring_cqe_depth);
434
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
435 9 offset += (size_t)written;
436
437 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
438 &g_metrics.backend_pool_active);
439
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
440 9 offset += (size_t)written;
441
442 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
443 &g_metrics.backend_pool_idle);
444
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
445 9 offset += (size_t)written;
446
447 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
448 &g_metrics.backend_pool_healthy);
449
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
450 9 offset += (size_t)written;
451
452 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
453 &g_metrics.health_checker_total_checks);
454
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
455 9 offset += (size_t)written;
456
457 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
458 &g_metrics.ip_limiter_entries_total);
459
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
460 9 offset += (size_t)written;
461
462 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
463 &g_metrics.health_checker_failed_checks);
464
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
465 9 offset += (size_t)written;
466
467 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
468 &g_metrics.health_checker_health_status);
469
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
470 9 offset += (size_t)written;
471
472 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
473 &g_metrics.health_checker_last_check_time);
474
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
475 9 offset += (size_t)written;
476
477 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
478 &g_metrics.circuit_breaker_state);
479
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
480 9 offset += (size_t)written;
481
482 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
483 &g_metrics.circuit_breaker_failure_count);
484
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
485 9 offset += (size_t)written;
486
487 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
488 &g_metrics.circuit_breaker_total_opens);
489
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
490 9 offset += (size_t)written;
491
492 9 written = format_gauge(buffer + offset, METRICS_BUFFER_SIZE - offset,
493 &g_metrics.circuit_breaker_total_closes);
494
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (written < 0) goto truncation_error;
495 9 offset += (size_t)written;
496
497 9 return buffer;
498
499 truncation_error:
500 free(buffer);
501 return NULL;
502 }
503
504 9 void metrics_free_format(char *formatted)
505 {
506
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (formatted) {
507 9 free(formatted);
508 }
509 9 }
510
511 10 static void *metrics_server_thread(void *arg)
512 {
513 (void)arg;
514 10 int server_fd = g_metrics_server_fd;
515
516
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 10 times.
20 while (atomic_load(&g_server_running)) {
517 struct sockaddr_in client_addr;
518 10 socklen_t client_len = sizeof(client_addr);
519
520 10 int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
521
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (client_fd < 0) {
522
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (atomic_load(&g_server_running)) {
523 log_message(LOG_LEVEL_ERROR, "Metrics server accept failed");
524 }
525 10 continue;
526 }
527
528 char request_buffer[METRICS_REQUEST_BUFFER_SIZE];
529 ssize_t bytes_read = read(client_fd, request_buffer, sizeof(request_buffer) - 1);
530 if (bytes_read > 0) {
531 request_buffer[bytes_read] = '\0';
532
533 if (strstr(request_buffer, "GET /metrics") != NULL) {
534 char *metrics_data = metrics_format_prometheus();
535 if (metrics_data) {
536 char response[METRICS_RESPONSE_BUFFER_SIZE];
537 size_t metrics_len = strlen(metrics_data);
538 int response_len = snprintf(response, sizeof(response),
539 "HTTP/1.1 200 OK\r\n"
540 "Content-Type: text/plain; version=0.0.4\r\n"
541 "Content-Length: %zu\r\n"
542 "Connection: close\r\n"
543 "\r\n"
544 "%s",
545 metrics_len, metrics_data);
546
547 if (response_len > 0 && response_len < (int)sizeof(response)) {
548 write(client_fd, response, (size_t)response_len);
549 }
550 metrics_free_format(metrics_data);
551 }
552 } else {
553 const char *response = "HTTP/1.1 404 Not Found\r\n\r\n";
554 write(client_fd, response, strlen(response));
555 }
556 }
557
558 close(client_fd);
559 }
560
561 10 return NULL;
562 }
563
564 10 int metrics_start_server(int port)
565 {
566 10 g_metrics_server_fd = socket(AF_INET, SOCK_STREAM, 0);
567
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (g_metrics_server_fd < 0) {
568 log_message(LOG_LEVEL_ERROR, "Failed to create metrics socket");
569 return -1;
570 }
571
572 10 int opt = 1;
573
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (setsockopt(g_metrics_server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
574 log_message(LOG_LEVEL_ERROR, "Failed to set SO_REUSEADDR on metrics socket");
575 close(g_metrics_server_fd);
576 return -1;
577 }
578
579 10 struct sockaddr_in addr = {0};
580 10 addr.sin_family = AF_INET;
581 10 addr.sin_addr.s_addr = inet_addr("127.0.0.1");
582 10 addr.sin_port = htons(port);
583
584
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (bind(g_metrics_server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
585 log_message(LOG_LEVEL_ERROR, "Failed to bind metrics server to port %d", port);
586 close(g_metrics_server_fd);
587 return -1;
588 }
589
590
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (listen(g_metrics_server_fd, METRICS_SERVER_BACKLOG) < 0) {
591 log_message(LOG_LEVEL_ERROR, "Failed to listen on metrics socket");
592 close(g_metrics_server_fd);
593 return -1;
594 }
595
596 10 atomic_store(&g_server_running, 1);
597
598
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (pthread_create(&g_metrics_server_thread, NULL, metrics_server_thread, NULL) != 0) {
599 log_message(LOG_LEVEL_ERROR, "Failed to create metrics server thread");
600 atomic_store(&g_server_running, 0);
601 close(g_metrics_server_fd);
602 return -1;
603 }
604
605 10 log_message(LOG_LEVEL_INFO, "Metrics server started on http://127.0.0.1:%d/metrics", port);
606 10 return 0;
607 }
608
609 18 void metrics_stop_server(void)
610 {
611
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 8 times.
18 if (atomic_load(&g_server_running)) {
612 10 atomic_store(&g_server_running, 0);
613
614
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (g_metrics_server_fd >= 0) {
615 10 shutdown(g_metrics_server_fd, SHUT_RDWR);
616 10 close(g_metrics_server_fd);
617 10 g_metrics_server_fd = -1;
618 }
619
620 10 pthread_join(g_metrics_server_thread, NULL);
621 10 log_message(LOG_LEVEL_INFO, "Metrics server stopped");
622 }
623 18 }
624