diff --git a/man/systemd-journal-gatewayd.service.xml b/man/systemd-journal-gatewayd.service.xml index 38adfe6b4e..bd7a43e181 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 ae69e17c9c..a1de0045e8 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 cf2fba2985..9103ca9af6 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 7d1fe67f88..22b087d3e8 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 9c7a3d05bb..35ac91ba40 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