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