From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id A5FFF1FF0A7 for ; Mon, 17 Aug 2026 11:43:06 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id A164323A5C; Mon, 17 Aug 2026 11:43:05 +0200 (CEST) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Mon, 17 Aug 2026 11:43:00 +0200 Message-Id: Subject: Re: [PATCH proxmox-mini-journalreader v4 1/2] add -B parameter to lists boots From: "Daniel Kral" To: "Maximiliano Sandoval" , X-Mailer: aerc 0.22.0-10-g6373ac9d2179-dirty References: <20260807135757.459145-1-m.sandoval@proxmox.com> <20260807135757.459145-2-m.sandoval@proxmox.com> In-Reply-To: <20260807135757.459145-2-m.sandoval@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1786959759670 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.374 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust RDNS_NONE 1.274 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: D3XWFE45NDUHHDDVTOF5QYVB7HMWWGGT X-Message-ID-Hash: D3XWFE45NDUHHDDVTOF5QYVB7HMWWGGT X-MailFrom: d.kral@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Looks good to me and works as expected with the added -n, -b, -e, -j and -J functionality for me, thanks for working on this! -j and -J aren't separate functionalities for -B, but it seems fine to me as there were different semantics to handle for the full journal output and it's different here. Some small comments left inline, with those addressed consider this as Reviewed-by: Daniel Kral Tested-by: Daniel Kral On Fri Aug 7, 2026 at 3:57 PM CEST, Maximiliano Sandoval wrote: > We usually require the output from `last` or similar in support to query = the > previously booted kernel versions. We add this helper to avoid adding a > dependency on either last or wtmpdb on the ISO images. > > The program, when executed with -B, will print all boots unless it is res= tricted > with -n or -b, -e. > > glib is added as a dependency for simplicity of memory management. > > As per SD_JOURNAL_QUERY_UNIQUE(3), sd_journal_query_unique ignore matches= hence > why we need to iterate twice: > > Note that these functions currently are not influenced by matches > set with sd_journal_add_match() but this might change in a later > version of this software. > > Signed-off-by: Maximiliano Sandoval > --- > > Notes: > Sample output > =20 > $ proxmox-mini-journalreader -B -n 3 -j |jq > { > "data": [ > { > "first_timestamp": "1785911171", > "last_timestamp": "1785942175", > "boot_id": "912937846ddd40e2b1acb57fb18c9cc2", > "kernel_version": "7.0.14-8-pve" > }, > { > "first_timestamp": "1785998528", > "last_timestamp": "1786029768", > "boot_id": "09b4f7cd09f14086af52fff5e31931bd", > "kernel_version": "7.0.14-8-pve" > }, > { > "first_timestamp": "1786084575", > "last_timestamp": "1786109309", > "boot_id": "f4fade4c1ce648be98886e8aabdf28db", > "kernel_version": "7.0.14-9-pve" > } > ], > "success": 1 > } > =20 > $ proxmox-mini-journalreader -B -n 3 > 2026-08-05 08:26 2026-08-05 17:02 912937846ddd40e2b1acb57fb18c9cc2 7.= 0.14-8-pve > 2026-08-06 08:42 2026-08-06 17:22 09b4f7cd09f14086af52fff5e31931bd 7.= 0.14-8-pve > 2026-08-07 08:36 2026-08-07 14:50 f4fade4c1ce648be98886e8aabdf28db 7.= 0.14-9-pve > =20 > This seems quick enough for ~400 boots: > =20 > time ./proxmox-mini-journalreader -B | wc -l > 419 > =20 > real 0m0,133s > user 0m0,004s > sys 0m0,003s > =20 > It might be even quicker than systemd itself: > =20 > $ time journalctl --list-boots | wc -l > 420 > =20 > real 0m0,216s > user 0m0,003s > sys 0m0,004s > =20 > although it does not compare with `last` since they use their own dat= abase: > =20 > $ time last > [...] > real 0m0,005s > user 0m0,000s > sys 0m0,003s > =20 > Open questions: > =20 > - We assume the first line in the journal is the "Linux version" line= . This is > true as of now in practice. One could add a match like the following: > =20 > sd_journal_add_match(j, "_TRANSPORT=3Dkernel", strlen("_TRANSPOR= T=3Dkernel")); > =20 > if this is undesirable. > =20 > Differences from v3: > - Close journal after listing boots > - Style fixes > - Support -j, we print timestamps in seconds since epoch > - Support -n, -b and -e > - Rename BootInfo->version to BootInfo->kernel_version > =20 > Differences from v2: > - Mark regex_extract_match and format_timestamp functions as static = char * > - Move the regex out of the for loop so it is only compiled once > - Add G_GNUC_UNUSED hint so that user_data does not produce an unuse= d warning. We need > that parameter as part of the expected signature > - Remove duplicated check for regex !=3D NULL > =20 > Differences from v1: > - Replace uint with size_t > - Remove the use of a second journal > - Copy only 32 chars instead of 33. We were copying \0 which is adde= d by > g_strndup anyways and 32 is the actual length of a BOOT_ID > - use strlen("BOOT_ID=3D") instead of sizeof("BOOT_ID") so it does n= ot work by accident > - Use proper signature for GCompareFunc > - Add a couple of guards for empty messages > > debian/control | 2 +- > src/Makefile | 2 +- > src/mini-journalreader.c | 190 ++++++++++++++++++++++++++++++++++++++- > 3 files changed, 191 insertions(+), 3 deletions(-) > > diff --git a/debian/control b/debian/control > index 8280f6d..d7d8cc0 100644 > --- a/debian/control > +++ b/debian/control > @@ -2,7 +2,7 @@ Source: proxmox-mini-journalreader > Section: admin > Priority: optional > Maintainer: Proxmox Support Team > -Build-Depends: debhelper-compat (=3D 13), libsystemd-dev, pkg-config, sc= doc, > +Build-Depends: debhelper-compat (=3D 13), libsystemd-dev, pkg-config, sc= doc, libglib2.0-dev, > Standards-Version: 4.6.2 > =20 > Package: proxmox-mini-journalreader > diff --git a/src/Makefile b/src/Makefile > index e64e066..c89e74a 100644 > --- a/src/Makefile > +++ b/src/Makefile > @@ -5,7 +5,7 @@ LIBEXEC_DIR ?=3D $(DESTDIR)/usr/libexec/ > MAN1_DIR ?=3D $(DESTDIR)/usr/share/man/man1 > MANPAGE ?=3D $(PROGRAM).1 > =20 > -LIBS :=3D libsystemd > +LIBS :=3D libsystemd glib-2.0 > CFLAGS +=3D -Wall -Wextra -Wl,-z,relro -g -O2 --std=3Dgnu11 > CFLAGS +=3D -fstack-protector-strong -D_FORTIFY_SOURCE=3D2 > CFLAGS +=3D $(shell pkg-config --cflags $(LIBS)) > diff --git a/src/mini-journalreader.c b/src/mini-journalreader.c > index fc78f65..ff1a413 100644 > --- a/src/mini-journalreader.c > +++ b/src/mini-journalreader.c > @@ -21,6 +21,7 @@ > =20 > #include > #include > +#include > #include > #include > #include > @@ -37,6 +38,182 @@ bool json =3D false; > bool structured =3D false; > bool first_line =3D true; > =20 > +typedef struct { > + char *boot_id; > + char *kernel_version; > + uint64_t first_timestamp; > + uint64_t last_timestamp; > +} BootInfo; > + > +static void boot_info_free(BootInfo *info) { > + g_free(info->boot_id); > + g_free(info->kernel_version); > + g_free(info); > +} > + > +static int boot_info_cmp(gconstpointer a, gconstpointer b) { > + const BootInfo *entry1 =3D a; > + const BootInfo *entry2 =3D b; > + > + if (entry1->first_timestamp < entry2->first_timestamp) { > + return -1; > + } > + if (entry1->first_timestamp > entry2->first_timestamp) { > + return 1; > + } > + return 0; > +} > + > +static char *regex_extract_match(GRegex *regex, const char *message) { > + g_autoptr(GMatchInfo) match_info =3D NULL; > + int start_pos =3D 0, end_pos =3D 0; > + > + g_return_val_if_fail(message !=3D NULL, g_strdup("unknown")); > + > + g_regex_match(regex, message, G_REGEX_MATCH_DEFAULT, &match_info); > + > + if (!g_match_info_matches(match_info)) { > + return g_strdup("unknown"); > + } > + > + g_match_info_fetch_pos(match_info, 1, &start_pos, &end_pos); > + > + return g_strndup(message + start_pos, end_pos - start_pos); > +} > + > +static char *format_timestamp(uint64_t timestamp) { > + g_autoptr(GDateTime) time =3D NULL; > + > + if (timestamp =3D=3D 0) { > + return g_strdup("-"); > + } > + > + time =3D g_date_time_new_from_unix_local_usec(timestamp); > + return g_date_time_format(time, "%Y-%m-%d %H:%M"); > +} > + > +static void print_boot(BootInfo *info) { > + g_return_if_fail(info !=3D NULL); > + > + if (json) { > + uint64_t start =3D info->first_timestamp / (1000 * 1000); // use= c to sec > + uint64_t end =3D info->last_timestamp / (1000 * 1000); > + g_print("{ \"first_timestamp\": \"%ld\", \"last_timestamp\": \"%= ld\", \"boot_id\": \"%s\", \"kernel_version\": \"%s\"}", > + start, end, info->boot_id, info->kernel_version); nit: first_timestamp and last_timestamp could be JSON numbers > + } else { > + g_autofree char *start =3D format_timestamp(info->first_timestam= p); > + g_autofree char *end =3D format_timestamp(info->last_timestamp); > + > + g_print("%s\t%s\t%s\t%s\n", start, end, info->boot_id, info->ker= nel_version); > + } > +} > + > +static int list_boots_and_kernels(sd_journal *j, bool have_number, uint6= 4_t number, uint64_t begin, uint64_t end) { > + const void *data; > + const size_t prefix_len =3D strlen("_BOOT_ID=3D"); > + size_t len; > + int r; > + g_autoptr(GPtrArray) boot_ids =3D NULL; > + g_autoptr(GPtrArray) boot_array =3D NULL; > + g_autoptr(GRegex) regex =3D NULL; > + g_autoptr(GError) error =3D NULL; > + end =3D (end =3D=3D 0) ? G_MAXUINT64 : end; > + > + boot_ids =3D g_ptr_array_new_with_free_func(g_free); > + > + r =3D sd_journal_query_unique(j, "_BOOT_ID"); > + if (r < 0) { > + g_printerr("Failed to query _BOOT_ID identifier: %s\n", strerror= (-r)); > + return 1; > + } This can use print_error_and_exit() so it also handles the JSON error output correctly > + SD_JOURNAL_FOREACH_UNIQUE(j, data, len) { > + g_ptr_array_add(boot_ids, g_strndup((const char *)data, len)); > + } > + > + boot_array =3D g_ptr_array_new_full(boot_ids->len, (GDestroyNotify)b= oot_info_free); > + > + sd_journal_flush_matches(j); > + > + regex =3D g_regex_new("Linux version ([a-zA-Z0-9.-]+)", G_REGEX_OPTI= MIZE, G_REGEX_MATCH_DEFAULT, &error); > + if (error) { > + g_critical("regex compilation failed: %s", error->message); > + return 1; > + } Same here, even though it's g_critical() here > + > + for (size_t i =3D 0; i < boot_ids->len; i++) { > + uint64_t first_ts =3D 0, last_ts =3D 0; > + size_t msg_len; > + const char *message =3D NULL; > + const char *match =3D g_ptr_array_index(boot_ids, i); > + g_autofree char *version =3D NULL; > + g_autofree char *boot_id =3D NULL; > + > + // Strip _BOOT_ID=3D from the message > + boot_id =3D g_strndup(match + prefix_len, 32); > + > + sd_journal_add_match(j, match, strlen(match)); > + > + r =3D sd_journal_seek_head(j); > + if (r >=3D 0) { > + r =3D sd_journal_next(j); > + if (r > 0) { > + sd_journal_get_realtime_usec(j, &first_ts); > + } > + } > + > + if (first_ts < begin || end < first_ts) { > + sd_journal_flush_matches(j); > + continue; > + } > + > + r =3D sd_journal_get_data(j, "MESSAGE", (const void **)&message,= &msg_len); > + if (r >=3D 0 && message !=3D NULL && msg_len > 0) { > + version =3D regex_extract_match(regex, message); > + } else { > + version =3D g_strdup("unknown"); > + } > + > + r =3D sd_journal_seek_tail(j); > + if (r >=3D 0) { > + r =3D sd_journal_previous(j); > + if (r > 0) { > + sd_journal_get_realtime_usec(j, &last_ts); > + } > + } > + > + sd_journal_flush_matches(j); > + > + BootInfo *boot_info =3D g_new0(BootInfo, 1); > + boot_info->boot_id =3D g_steal_pointer(&boot_id); > + boot_info->kernel_version =3D g_steal_pointer(&version); > + boot_info->first_timestamp =3D first_ts; > + boot_info->last_timestamp =3D last_ts; > + > + g_ptr_array_add(boot_array, boot_info); > + } > + > + g_ptr_array_sort_values(boot_array, (GCompareFunc)boot_info_cmp); > + > + if (json) { > + g_print("{\"data\":["); > + } > + > + uint64_t starting_idx =3D (have_number && boot_array->len > number) = ? boot_array->len - number : 0; > + for (uint64_t i =3D starting_idx; i < boot_array->len; i++) { > + BootInfo *boot_info =3D g_ptr_array_index(boot_array, i); > + print_boot(boot_info); > + if (json && i + 1 < boot_array->len) { > + g_print(","); > + } > + } > + > + if (json) { > + g_print("],\"success\":1}"); > + } > + > + return 0; > +} > + > // helper to print errors on stderr > // if we're in json mode, print closing json body if possible > static void print_error_and_exit(const char *fmt, ...) { > @@ -471,6 +648,7 @@ _Noreturn static void usage(char *error) { > " -J\t\t\tprint as json with one object of separate fields per = entry\n" > " -I\t\t\twith -J, also emit a record listing the distinct sysl= og identifiers\n" > " -U\t\t\twith -J, also emit a record listing the distinct syst= emd units\n" > + " -B\t\t\tlist boots. The columns are: the dates of the first a= nd last entry in the boot, the boot id, and the kernel version\n" > " -h\t\t\tthis help\n" > "\n" > "Passing no range option will dump all the available journal\n" > @@ -711,11 +889,12 @@ int main(int argc, char *argv[]) { > bool kernel =3D false; > bool list_identifiers =3D false; > bool list_units =3D false; > + bool list_boots =3D false; > int c; > =20 > progname =3D argv[0]; > =20 > - while ((c =3D getopt(argc, argv, "b:e:d:n:f:t:p:i:u:jJIUkh")) !=3D -= 1) { > + while ((c =3D getopt(argc, argv, "b:e:d:n:f:t:p:i:u:jJIUkhB")) !=3D = -1) { > switch (c) { > case 'b': > begin =3D arg_to_timestamp_usec(optarg); > @@ -762,6 +941,9 @@ int main(int argc, char *argv[]) { > case 'U': > list_units =3D true; > break; > + case 'B': > + list_boots =3D true; > + break; > case 'h': > usage(NULL); > case '?': > @@ -816,6 +998,12 @@ int main(int argc, char *argv[]) { > return 1; > } > =20 > + if (list_boots) { > + int ret =3D list_boots_and_kernels(j, have_number, number, begin= , end); > + sd_journal_close(j); > + return ret; > + } > + > // restrict the traversal before seeking. Each filter is a separate = group, conjoined (AND) with > // the others; within a group matches are OR'd (same field, or an ex= plicit disjunction for the > // unit), so the groups must be split by a conjunction