GET /api/logs?lines=N — returns last N log lines */
| 475 | |
| 476 | /* GET /api/logs?lines=N — returns last N log lines */ |
| 477 | static void handle_logs(cbm_http_conn_t *c, const cbm_http_req_t *req) { |
| 478 | char lines_str[16] = {0}; |
| 479 | int max_lines = 100; |
| 480 | if (cbm_http_query_param(req->query, "lines", lines_str, (int)sizeof(lines_str))) { |
| 481 | int v = atoi(lines_str); |
| 482 | if (v > 0 && v <= LOG_RING_SIZE) |
| 483 | max_lines = v; |
| 484 | } |
| 485 | |
| 486 | cbm_mutex_lock(&g_log_mutex); |
| 487 | int count = g_log_count < max_lines ? g_log_count : max_lines; |
| 488 | int start = (g_log_head - count + LOG_RING_SIZE) % LOG_RING_SIZE; |
| 489 | int total = g_log_count; |
| 490 | |
| 491 | /* Copy lines under lock. |
| 492 | * |
| 493 | * JSON escaping expands '"', '\\' and '\n' to two bytes each, so an |
| 494 | * line made mostly of those serialises to roughly twice its stored length. |
| 495 | * The previous budget of LOG_LINE_MAX + 10 per line under-counted that by |
| 496 | * half. Ring contents come from indexer stderr, which is not escaped on |
| 497 | * ingest and can legitimately contain both doubling characters — a POSIX |
| 498 | * filename may. |
| 499 | * |
| 500 | * Budget the escaped worst case, and clamp the framing writes below anyway |
| 501 | * so the size calculation is not the only thing keeping pos in range. */ |
| 502 | size_t buf_size = (size_t)count * (2 * LOG_LINE_MAX + 8) + 64; |
| 503 | char *buf = malloc(buf_size); |
| 504 | if (!buf) { |
| 505 | cbm_mutex_unlock(&g_log_mutex); |
| 506 | cbm_http_replyf(c, 500, g_cors, "oom"); |
| 507 | return; |
| 508 | } |
| 509 | |
| 510 | int pos = 0; |
| 511 | http_appendf(buf, buf_size, &pos, "{\"lines\":["); |
| 512 | for (int i = 0; i < count; i++) { |
| 513 | int idx = (start + i) % LOG_RING_SIZE; |
| 514 | if (i > 0) |
| 515 | http_appendf(buf, buf_size, &pos, ","); |
| 516 | /* Escape quotes in log lines */ |
| 517 | http_appendf(buf, buf_size, &pos, "\""); |
| 518 | for (int j = 0; g_log_ring[idx][j] && (size_t)pos < buf_size - 10; j++) { |
| 519 | char ch = g_log_ring[idx][j]; |
| 520 | if (ch == '"') { |
| 521 | buf[pos++] = '\\'; |
| 522 | buf[pos++] = '"'; |
| 523 | } else if (ch == '\\') { |
| 524 | buf[pos++] = '\\'; |
| 525 | buf[pos++] = '\\'; |
| 526 | } else if (ch == '\n') { |
| 527 | buf[pos++] = '\\'; |
| 528 | buf[pos++] = 'n'; |
| 529 | } else { |
| 530 | buf[pos++] = ch; |
| 531 | } |
| 532 | } |
| 533 | http_appendf(buf, buf_size, &pos, "\""); |
| 534 | } |
no test coverage detected