From: Jan Čermák Date: Wed, 28 May 2025 18:33:03 +0000 (+0200) Subject: journal-gatewayd: add /boots endpoint (#37574) X-Git-Tag: v258-rc1~459 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c9f931b737dd4736f68f4ee6b4a99852232c7a40;p=thirdparty%2Fsystemd.git journal-gatewayd: add /boots endpoint (#37574) Add endpoint for listing boots. Output format mimics `journalctl --list-boots -o json`, so it's a plain array containing index, boot ID and timestamps of the first and last entry. Initial implementation returns boots ordered starting with the current one and doesn't allow any filtering (i.e. equivalent of --lines argument). Fixes: #37573 --- diff --git a/man/systemd-journal-gatewayd.service.xml b/man/systemd-journal-gatewayd.service.xml index 38adfe6b4e8..bd7a43e181e 100644 --- a/man/systemd-journal-gatewayd.service.xml +++ b/man/systemd-journal-gatewayd.service.xml @@ -151,6 +151,17 @@ The following URLs are recognized: + + /boots + + Returns json-seq (RFC 7464) response containing JSON objects, each one containing + boot number, its ID and the timestamps of the first and last message. Boots are returned starting + with the latest boot first and use the same schema as + journalctl --list-boots --output json. + + + + /browse diff --git a/src/journal-remote/journal-gatewayd.c b/src/journal-remote/journal-gatewayd.c index ae69e17c9c7..a1de0045e8e 100644 --- a/src/journal-remote/journal-gatewayd.c +++ b/src/journal-remote/journal-gatewayd.c @@ -64,6 +64,9 @@ typedef struct RequestMeta { uint64_t n_entries; bool n_entries_set, since_set, until_set; + sd_id128_t previous_boot_id; + int32_t boot_index; + FILE *tmp; uint64_t delta, size; @@ -886,6 +889,132 @@ static int request_handler_machine( return MHD_queue_response(connection, MHD_HTTP_OK, response); } +static int output_boot(FILE *f, LogId boot, int boot_display_index) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; + int r; + + r = sd_json_build( + &json, + SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_INTEGER("index", boot_display_index), + SD_JSON_BUILD_PAIR_ID128("boot_id", boot.id), + SD_JSON_BUILD_PAIR_UNSIGNED("first_entry", boot.first_usec), + SD_JSON_BUILD_PAIR_UNSIGNED("last_entry", boot.last_usec))); + if (r < 0) + return r; + + return sd_json_variant_dump(json, SD_JSON_FORMAT_SEQ, f, /* prefix= */ NULL); +} + +static ssize_t request_reader_boots( + void *cls, + uint64_t pos, + char *buf, + size_t max) { + + RequestMeta *m = ASSERT_PTR(cls); + int r; + + assert(buf); + assert(max > 0); + assert(pos >= m->delta); + + pos -= m->delta; + + while (pos >= m->size) { + LogId boot; + off_t sz; + + /* We're seeking from tail (newest boot) so advance to older. */ + r = discover_next_id( + m->journal, + LOG_BOOT_ID, + /* boot_id */ SD_ID128_NULL, + /* unit = */ NULL, + m->previous_boot_id, + /* advance_older = */ true, + &boot); + if (r < 0) { + log_error_errno(r, "Failed to advance boot index: %m"); + return MHD_CONTENT_READER_END_WITH_ERROR; + } + if (r == 0) + return MHD_CONTENT_READER_END_OF_STREAM; + + pos -= m->size; + m->delta += m->size; + + r = request_meta_ensure_tmp(m); + if (r < 0) { + log_error_errno(r, "Failed to create temporary file: %m"); + return MHD_CONTENT_READER_END_WITH_ERROR; + } + + r = output_boot(m->tmp, boot, m->boot_index); + if (r < 0) { + log_error_errno(r, "Failed to serialize boot: %m"); + return MHD_CONTENT_READER_END_WITH_ERROR; + } + + sz = ftello(m->tmp); + if (sz < 0) { + log_error_errno(errno, "Failed to retrieve file position: %m"); + return MHD_CONTENT_READER_END_WITH_ERROR; + } + + m->size = (uint64_t) sz; + + m->previous_boot_id = boot.id; + m->boot_index -= 1; + } + + if (fseeko(m->tmp, pos, SEEK_SET) < 0) { + log_error_errno(errno, "Failed to seek to position: %m"); + return MHD_CONTENT_READER_END_WITH_ERROR; + } + + size_t n = MIN(m->size - pos, max); + + errno = 0; + size_t k = fread(buf, 1, n, m->tmp); + if (k != n) { + log_error("Failed to read from file: %s", STRERROR_OR_EOF(errno)); + return MHD_CONTENT_READER_END_WITH_ERROR; + } + + return (ssize_t) k; +} + +static int request_handler_boots( + struct MHD_Connection *connection, + void *connection_cls) { + + _cleanup_(MHD_destroy_responsep) struct MHD_Response *response = NULL; + RequestMeta *m = ASSERT_PTR(connection_cls); + int r; + + assert(connection); + + r = open_journal(m); + if (r < 0) + return mhd_respondf(connection, r, MHD_HTTP_INTERNAL_SERVER_ERROR, "Failed to open journal: %m"); + + m->previous_boot_id = SD_ID128_NULL; + m->boot_index = 0; + r = sd_journal_seek_tail(m->journal); /* seek to newest */ + if (r < 0) + return mhd_respondf(connection, r, MHD_HTTP_INTERNAL_SERVER_ERROR, "Failed to seek in journal: %m"); + + response = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN, 4*1024, request_reader_boots, m, NULL); + if (!response) + return respond_oom(connection); + + if (MHD_add_response_header(response, "Content-Type", "application/json-seq") == MHD_NO) + return respond_oom(connection); + + return MHD_queue_response(connection, MHD_HTTP_OK, response); +} + static mhd_result request_handler( void *cls, struct MHD_Connection *connection, @@ -932,6 +1061,9 @@ static mhd_result request_handler( if (streq(url, "/machine")) return request_handler_machine(connection, *connection_cls); + if (streq(url, "/boots")) + return request_handler_boots(connection, *connection_cls); + return mhd_respond(connection, MHD_HTTP_NOT_FOUND, "Not found."); } diff --git a/src/shared/logs-show.c b/src/shared/logs-show.c index cf2fba29854..9103ca9af61 100644 --- a/src/shared/logs-show.c +++ b/src/shared/logs-show.c @@ -1993,7 +1993,7 @@ static int set_matches_for_discover_id( return -EINVAL; } -static int discover_next_id( +int discover_next_id( sd_journal *j, LogIdType type, sd_id128_t boot_id, /* optional, used when type == JOURNAL_{SYSTEM,USER}_UNIT_INVOCATION_ID */ diff --git a/src/shared/logs-show.h b/src/shared/logs-show.h index 7d1fe67f889..22b087d3e88 100644 --- a/src/shared/logs-show.h +++ b/src/shared/logs-show.h @@ -81,6 +81,15 @@ void json_escape( size_t l, OutputFlags flags); +int discover_next_id( + sd_journal *j, + LogIdType type, + sd_id128_t boot_id, /* optional, used when type == JOURNAL_{SYSTEM,USER}_UNIT_INVOCATION_ID */ + const char *unit, /* mandatory when type == JOURNAL_{SYSTEM,USER}_UNIT_INVOCATION_ID */ + sd_id128_t previous_id, + bool advance_older, + LogId *ret); + int journal_find_log_id( sd_journal *j, LogIdType type, diff --git a/test/units/TEST-04-JOURNAL.journal-gatewayd.sh b/test/units/TEST-04-JOURNAL.journal-gatewayd.sh index 9c7a3d05bb3..35ac91ba402 100755 --- a/test/units/TEST-04-JOURNAL.journal-gatewayd.sh +++ b/test/units/TEST-04-JOURNAL.journal-gatewayd.sh @@ -139,6 +139,12 @@ curl -LSfs http://localhost:19531/fields/_TRANSPORT (! curl -LSfs http://localhost:19531/fields) (! curl -LSfs http://localhost:19531/fields/foo-bar-baz) +# /boots +curl -LSfs http://localhost:19531/boots >"$LOG_FILE" +jq --seq -s . "$LOG_FILE" +LAST_BOOT_ID=$(journalctl --list-boots -ojson -n1 | jq -r '.[0].boot_id') +jq --seq -se ".[0] | select(.boot_id == \"$LAST_BOOT_ID\")" "$LOG_FILE" + systemctl stop systemd-journal-gatewayd.{socket,service} if ! command -v openssl >/dev/null; then