]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
journal-gatewayd: add /boots endpoint (#37574)
authorJan Čermák <sairon@users.noreply.github.com>
Wed, 28 May 2025 18:33:03 +0000 (20:33 +0200)
committerGitHub <noreply@github.com>
Wed, 28 May 2025 18:33:03 +0000 (03:33 +0900)
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
man/systemd-journal-gatewayd.service.xml
src/journal-remote/journal-gatewayd.c
src/shared/logs-show.c
src/shared/logs-show.h
test/units/TEST-04-JOURNAL.journal-gatewayd.sh

index 38adfe6b4e8a5efeb47db655c7985a626664c043..bd7a43e181e5a0092b030ff299858cc811731d38 100644 (file)
     <para>The following URLs are recognized:</para>
 
     <variablelist>
+      <varlistentry>
+        <term><uri>/boots</uri></term>
+
+        <listitem><para>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
+        <command>journalctl --list-boots --output json</command>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><uri>/browse</uri></term>
 
index ae69e17c9c73c367bdac4d43ae31b84ecc03420d..a1de0045e8e65f6cb1e74cb3711b4e43ef03c6c4 100644 (file)
@@ -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.");
 }
 
index cf2fba29854b53349b4b69d6719dfc082d01848e..9103ca9af61913c525a82b889406aca86594032c 100644 (file)
@@ -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 */
index 7d1fe67f889b732a0c573b929cc106749873084b..22b087d3e883a8832d5a9dfc4a8d2a66714808c2 100644 (file)
@@ -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,
index 9c7a3d05bb3dc197be220b152e8c0e0f3014575a..35ac91ba4029c226951f31683f0be2f98a62b125 100755 (executable)
@@ -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