public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Maximiliano Sandoval <m.sandoval@proxmox.com>
To: "Daniel Kral" <d.kral@proxmox.com>
Cc: pve-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-mini-journalreader v3 1/2] add -B parameter to lists boots
Date: Fri, 07 Aug 2026 16:01:50 +0200	[thread overview]
Message-ID: <s8oqzkajagh.fsf@toolbox> (raw)
In-Reply-To: <DKIO7EK1JHJS.217V4MX50AE32@proxmox.com> (Daniel Kral's message of "Fri, 07 Aug 2026 13:21:24 +0200")


Replies below of what was addressed in v4.

"Daniel Kral" <d.kral@proxmox.com> writes:

> Thanks for tackling this!
>
> Works as expected, some comments inline.
>
> The style-nits are from running a clang-format on it as the rest of the
> code seemed like it used that already? Might be nice to add a
> tidy/format target, but out of scope of this series.
>
> On Thu Aug 6, 2026 at 4:09 PM CEST, Maximiliano Sandoval wrote:
>> The parameter will print all boots, in practice it should be piped into tail.
>>
>> Sample output:
>>
>> 2026-07-27 08:28	2026-07-27 15:56	3e3813556d53477f9a139a7cb2f3e8f2	7.0.14-6-pve
>> 2026-07-27 15:57	2026-07-27 17:03	5a0324e1ac3f4f24907a21ab81b67384	7.0.14-7-pve
>> 2026-07-28 08:26	2026-07-28 17:07	e62bc71e85274f21a1bfb954be38c071	7.0.14-7-pve
>> 2026-07-29 08:43	2026-07-29 08:48	7f668e4499644ea1b8339d6b8c29d53d	7.0.14-7-pve
>> 2026-07-29 08:49	2026-07-29 17:09	9ff5eb6f0485466383e0b89512f937c9	7.0.14-8-pve
>>
>> 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 <m.sandoval@proxmox.com>
>> ---
>>
>> Notes:
>>     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.
>>     
>>     This seems quick enough for ~400 boots:
>>     
>>     time sudo ./proxmox-mini-journalreader -B | wc -l
>>     417
>>     
>>     real	0m0,143s
>>     user	0m0,004s
>>     sys	0m0,004s
>
> This information should be in the patch message to give more context for
> what use case this was added for future readers.
>

Moved the rationale, left `time`s in the notes.

>>     
>>     Open questions:
>>     
>>     - Should it list the latest boots first? It amounts to reversing the sign on
>>     boot_info_cmp.
>
> I guess going from oldest to newest is the default for the rest of
> proxmox-mini-journalreader (and journalctl) as well, so if we really
> need it the other way around adding an option for that might be better.
>
>>     - 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:
>>     
>>          sd_journal_add_match(j, "_TRANSPORT=kernel", strlen("_TRANSPORT=kernel"));
>>     
>>       if this is undesirable.
>
> Looking at the relevant code [0], the Linux version line might actually
> not be the first line... At least cgroup_init_early() could
> theoretically print warnings earlier than the Linux version line, even
> though that is unlikely.
>
> So if we want to be very robust we would need to check the first few
> lines of each boot until we hit the Linux version line, though I don't
> think it's worth the added complexity here.
>
> [0] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/init/main.c?h=v7.1#n1038
>

Lets leave it like this for now. I have not seen a single journal that
does not start like this.

>>     
>>     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 unused warning. We need
>>        that parameter as part of the expected signature
>>      - Remove duplicated check for regex != NULL
>>     
>>     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 added by
>>        g_strndup anyways and 32 is the actual length of a BOOT_ID
>>      - use strlen("BOOT_ID=") instead of sizeof("BOOT_ID") so it does not 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 | 152 ++++++++++++++++++++++++++++++++++++++-
>>  3 files changed, 153 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 <support@proxmox.com>
>> -Build-Depends: debhelper-compat (= 13), libsystemd-dev, pkg-config, scdoc,
>> +Build-Depends: debhelper-compat (= 13), libsystemd-dev, pkg-config, scdoc, libglib2.0-dev,
>>  Standards-Version: 4.6.2
>>  
>>  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 ?= $(DESTDIR)/usr/libexec/
>>  MAN1_DIR ?= $(DESTDIR)/usr/share/man/man1
>>  MANPAGE ?= $(PROGRAM).1
>>  
>> -LIBS := libsystemd
>> +LIBS := libsystemd glib-2.0
>>  CFLAGS += -Wall -Wextra -Wl,-z,relro -g -O2 --std=gnu11
>>  CFLAGS += -fstack-protector-strong -D_FORTIFY_SOURCE=2
>>  CFLAGS += $(shell pkg-config --cflags $(LIBS))
>> diff --git a/src/mini-journalreader.c b/src/mini-journalreader.c
>> index fc78f65..8a7d473 100644
>> --- a/src/mini-journalreader.c
>> +++ b/src/mini-journalreader.c
>> @@ -21,6 +21,7 @@
>>  
>>  #include <errno.h>
>>  #include <fnmatch.h>
>> +#include <glib.h>
>>  #include <stdbool.h>
>>  #include <stdint.h>
>>  #include <stdio.h>
>> @@ -37,6 +38,146 @@ bool json = false;
>>  bool structured = false;
>>  bool first_line = true;
>>  
>> +typedef struct {
>> +    char *boot_id;
>> +    char *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->version);
>> +    g_free(info);
>> +}
>
> nit: maybe then also add a boot_info_{create,init,...}() to put the
>      allocs visually near the frees?
>

Imo it does not improve much the situation, since it is only allocated
in one place.

>> +
>> +static int boot_info_cmp(gconstpointer a, gconstpointer b) {
>> +    const BootInfo *entry1 = a;
>> +    const BootInfo *entry2 = b;
>> +
>> +    if (entry1->first_timestamp < entry2->first_timestamp) return -1;
>> +    if (entry1->first_timestamp > entry2->first_timestamp) return 1;
>> +    return 0;
>
> style-nit: if chain without {}s
>
>> +}
>> +
>> +static char *regex_extract_match(GRegex *regex, const char *message) {
>> +    g_autoptr(GMatchInfo) match_info = NULL;
>> +    int start_pos = 0, end_pos = 0;
>> +
>> +    g_return_val_if_fail(message != 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");
>
> style-nit: if without {}
>
>> +
>> +    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 = NULL;
>> +
>> +    if (timestamp == 0) {
>> +        return g_strdup ("-");
>
> style nit: GLib-style function invocation
>
>> +    }
>> +
>> +    time = 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_GNUC_UNUSED gpointer user_data) {
>> +    g_return_if_fail(info != NULL);
>> +
>> +    g_autofree char *start = format_timestamp(info->first_timestamp);
>> +    g_autofree char *end = format_timestamp(info->last_timestamp);
>> +    g_print("%s\t%s\t%s\t%s\n", start, end, info->boot_id, info->version);
>> +}
>> +
>> +static int list_boots_and_kernels(sd_journal *j) {
>> +    const void *data;
>> +    const size_t prefix_len = strlen("_BOOT_ID=");
>> +    size_t len;
>> +    int r;
>> +    g_autoptr(GPtrArray) boot_ids = NULL;
>> +    g_autoptr(GPtrArray) boot_array = NULL;
>> +    g_autoptr(GRegex) regex = NULL;
>> +    g_autoptr(GError) error = NULL;
>> +
>> +    boot_ids = g_ptr_array_new_with_free_func(g_free);
>> +
>> +    r = sd_journal_query_unique(j, "_BOOT_ID");
>> +    if (r < 0) {
>> +        g_printerr("Failed to query _BOOT_ID identifier: %s\n", strerror(-r));
>> +        return 1;
>> +    }
>> +    SD_JOURNAL_FOREACH_UNIQUE(j, data, len) {
>> +        g_ptr_array_add(boot_ids, g_strndup((const char *)data, len));
>> +    }
>> +
>> +    boot_array = g_ptr_array_new_full(boot_ids->len, (GDestroyNotify)boot_info_free);
>> +
>> +    sd_journal_flush_matches(j);
>> +
>> +    regex = g_regex_new("Linux version ([a-zA-Z0-9.-]+)", G_REGEX_OPTIMIZE, G_REGEX_MATCH_DEFAULT, &error);
>> +    if (error) {
>> +        g_critical("regex compilation failed: %s", error->message);
>> +        return 1;
>> +    }
>> +
>> +    for (size_t i = 0; i < boot_ids->len; i++) {
>> +        uint64_t first_ts = 0, last_ts = 0;
>> +        size_t msg_len;
>> +        const char *message = NULL;
>> +        const char *match = g_ptr_array_index(boot_ids, i);
>
> nit: maybe use a more telling name like `boot_id`?
>

We use boot_id just later. Will leave as is since we use it in add_match
bellow.

>> +        g_autofree char* version = NULL;
>
> style nit: placement of the * type specifier
>
>> +        g_autofree char *boot_id = NULL;
>> +
>> +        // Strip _BOOT_ID= from the message
>> +        boot_id = g_strndup(match + prefix_len, 32);
>> +
>> +        sd_journal_add_match(j, match, strlen(match));
>> +
>> +        r = sd_journal_seek_head(j);
>> +        if (r >= 0) {
>> +            r = sd_journal_next(j);
>> +            if (r > 0) {
>> +                sd_journal_get_realtime_usec(j, &first_ts);
>> +            }
>> +        }
>> +
>> +        r = sd_journal_get_data(j, "MESSAGE", (const void **)&message, &msg_len);
>> +        if (r >= 0 && message != NULL && msg_len > 0)
>> +            version = regex_extract_match(regex, message);
>> +        else
>> +            version = g_strdup("unknown");
>
> style-nit: if chain without {}s
>
>> +
>> +        r = sd_journal_seek_tail(j);
>> +        if (r >= 0) {
>> +            r = sd_journal_previous(j);
>> +            if (r > 0) {
>> +                sd_journal_get_realtime_usec(j, &last_ts);
>> +            }
>> +        }
>> +
>> +        sd_journal_flush_matches(j);
>> +
>> +        BootInfo *boot_info = g_new0 (BootInfo, 1);
>
> style nit: GLib-style function invocation
>
>> +        boot_info->boot_id = g_steal_pointer(&boot_id);
>> +        boot_info->version = g_steal_pointer(&version);
>> +        boot_info->first_timestamp = first_ts;
>> +        boot_info->last_timestamp = last_ts;
>> +
>> +        g_ptr_array_add(boot_array, boot_info);
>> +    }
>> +
>> +    g_ptr_array_sort_values(boot_array, (GCompareFunc)boot_info_cmp);
>> +    g_ptr_array_foreach(boot_array, (GFunc)print_boot, NULL);
>
> misses a sd_journal_close(j) at the end, though see comment at the end

Done, but on the outer scope since the outerscope opened the journal.

>> +
>> +    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 +612,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 syslog identifiers\n"
>>          "  -U\t\t\twith -J, also emit a record listing the distinct systemd units\n"
>> +        "  -B\t\t\tList boots. The columns are: the date of the first entry in the boot and last entry, the boot id, and the kernel version\n"
>
> This should at least state that it does ignore all the other parameters,
> so it essentially has a different functionality than the rest of the
> parameters.
>
>>          "  -h\t\t\tthis help\n"
>>          "\n"
>>          "Passing no range option will dump all the available journal\n"
>> @@ -711,11 +853,12 @@ int main(int argc, char *argv[]) {
>>      bool kernel = false;
>>      bool list_identifiers = false;
>>      bool list_units = false;
>> +    bool list_boots = false;
>>      int c;
>>  
>>      progname = argv[0];
>>  
>> -    while ((c = getopt(argc, argv, "b:e:d:n:f:t:p:i:u:jJIUkh")) != -1) {
>> +    while ((c = getopt(argc, argv, "b:e:d:n:f:t:p:i:u:jJIUkh:B")) != -1) {
>>          switch (c) {
>>          case 'b':
>>              begin = arg_to_timestamp_usec(optarg);
>> @@ -762,6 +905,9 @@ int main(int argc, char *argv[]) {
>>          case 'U':
>>              list_units = true;
>>              break;
>> +        case 'B':
>> +            list_boots = true;
>> +            break;
>>          case 'h':
>>              usage(NULL);
>>          case '?':
>> @@ -816,6 +962,10 @@ int main(int argc, char *argv[]) {
>>          return 1;
>>      }
>>  
>> +    if (list_boots) {
>> +        return list_boots_and_kernels(j);
>> +    }
>> +
>
> As this new flag does ignore the rest of the parameters (except -d), it
> might be nicer to factor out the rest of the functionality into a
> separate function to clearly show that there are essentially two
> different 'modes' that proxmox-mini-journalreader can operate in.
> Also we get the missing sd_journal_close() for free then by not
> returning straight away.
>
> On another note, a few of the parameters might be nice to actually
> acknowledge for listing the boots and kernel versions, e.g.
>
>    -n: limit to last number of boots
>    -{b,e}: begin and end where to print boots for
>    -{j,J}: print the output as JSON
>
> Though for the current usage -n would be the nicest so we don't have to
> use tail, but no hard feelings about this.
>

Implemented -n, -b, -e, -j.

>>      // 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 explicit disjunction for the
>>      // unit), so the groups must be split by a conjunction

-- 
Maximiliano




  reply	other threads:[~2026-08-07 14:01 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-06 14:09 [PATCH manager/proxmox-mini-journalreader v3 0/2] report: list recent reboot events and their kernel version Maximiliano Sandoval
2026-08-06 14:09 ` [PATCH proxmox-mini-journalreader v3 1/2] add -B parameter to lists boots Maximiliano Sandoval
2026-08-07 11:21   ` Daniel Kral
2026-08-07 14:01     ` Maximiliano Sandoval [this message]
2026-08-06 14:09 ` [PATCH manager v3 2/2] report: list recent reboot events and their kernel version Maximiliano Sandoval
2026-08-07 13:58 ` superseded: [PATCH manager/proxmox-mini-journalreader v3 0/2] " Maximiliano Sandoval

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=s8oqzkajagh.fsf@toolbox \
    --to=m.sandoval@proxmox.com \
    --cc=d.kral@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal