From 7d3ae6b21308ca8959d7c70b1f9fa9012db6ec15 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 25 Apr 2024 13:38:24 +0900 Subject: [PATCH] journalctl: add --list-invocations command and -I/--invocation options The --list-invocations command is similar to --list-boots, but shows invocation IDs of specified unit. This should be useful when showing a specific invocation of a unit. The --invocation option is similar to --boot, but takes a invocation ID or an offset. The -I option is equivalent to --invocation=0. --- man/journalctl.xml | 53 ++++++++++++++++++++++ shell-completion/bash/journalctl | 4 +- src/journal/journalctl-filter.c | 44 ++++++++++++++---- src/journal/journalctl-misc.c | 37 ++++++++++++++++ src/journal/journalctl-misc.h | 1 + src/journal/journalctl-util.c | 76 ++++++++++++++++++++++++++++++++ src/journal/journalctl-util.h | 3 ++ src/journal/journalctl.c | 33 +++++++++++++- src/journal/journalctl.h | 4 ++ 9 files changed, 244 insertions(+), 11 deletions(-) diff --git a/man/journalctl.xml b/man/journalctl.xml index 5fd9263116..0cc2b72acc 100644 --- a/man/journalctl.xml +++ b/man/journalctl.xml @@ -350,6 +350,42 @@ + + + + + + Show messages from a specific invocation of unit. This will add a match for + _SYSTEMD_INVOCATION_ID=, OBJECT_SYSTEMD_INVOCATION_ID=, + INVOCATION_ID=, USER_INVOCATION_ID=. + + A positive offset will look up the invocations of a systemd unit + from the beginning of the journal, and zero or a negative offset will look up invocations starting + from the end of the journal. Thus, 1 means the first invocation found in the + journal in chronological order, 2 the second and so on; while + 0 is the latest invocation, -1 the invocation before the + latest, and so on. + + If the 32-character ID is specified, it may optionally be followed + by ±offset which identifies the invocation relative to the one given by + invocation ID. Negative values mean earlier invocations and positive + values mean later invocations. If ±offset is not specified, a value of + zero is assumed, and the logs for the invocation given by ID will be + shown. + + is equivalent to , and logs for the latest + invocation will be shown. + + When an offset is specified, a unit name must be specified with + or option. + + When specified with , then invocations are searched within the + specified boot. + + + + + @@ -862,6 +898,23 @@ + + + + + List invocation IDs of a unit. Requires a unit name with or + . Show a tabular list of invocation numbers (relative to the current + or latest invocation), their IDs, and the timestamps of the first and last message pertaining to + the invocation. When is specified, invocations in the boot will be shown. + When specified with + option, only the first (when the number prefixed with +) or the last (without + prefix) N entries will be shown. When specified with + , the list will be shown in the reverse order. + + + + + diff --git a/shell-completion/bash/journalctl b/shell-completion/bash/journalctl index c79a38c352..5d1cc174c9 100644 --- a/shell-completion/bash/journalctl +++ b/shell-completion/bash/journalctl @@ -47,11 +47,11 @@ _journalctl() { --show-cursor --dmesg -k --pager-end -e -r --reverse --utc -x --catalog --no-full --force --dump-catalog --flush --rotate --sync --no-hostname -N --fields - --list-namespaces' + --list-namespaces --list-invocations -I' [ARG]='-b --boot -D --directory --file -F --field -t --identifier -T --exclude-identifier --facility -M --machine -o --output -u --unit --user-unit -p --priority --root --case-sensitive - --namespace' + --namespace --invocation' [ARGUNKNOWN]='-c --cursor --interval -n --lines -S --since -U --until --after-cursor --cursor-file --verify-key -g --grep --vacuum-size --vacuum-time --vacuum-files --output-fields' diff --git a/src/journal/journalctl-filter.c b/src/journal/journalctl-filter.c index f9eb9f8c58..ab65a51451 100644 --- a/src/journal/journalctl-filter.c +++ b/src/journal/journalctl-filter.c @@ -16,6 +16,23 @@ #include "path-util.h" #include "unit-name.h" +static int add_invocation(sd_journal *j) { + int r; + + assert(j); + + if (!arg_invocation) + return 0; + + assert(!sd_id128_is_null(arg_invocation_id)); + + r = add_matches_for_invocation_id(j, arg_invocation_id); + if (r < 0) + return r; + + return sd_journal_add_conjunction(j); +} + static int add_boot(sd_journal *j) { int r; @@ -429,27 +446,38 @@ int add_filters(sd_journal *j, char **matches) { assert(j); - /* First, search boot ID, as that may set and flush matches and seek journal. */ + /* First, search boot or invocation ID, as that may set and flush matches and seek journal. */ r = journal_acquire_boot(j); if (r < 0) return r; + r = journal_acquire_invocation(j); + if (r < 0) + return r; + /* Clear unexpected matches for safety. */ sd_journal_flush_matches(j); /* Then, add filters in the below. */ - r = add_boot(j); - if (r < 0) - return log_error_errno(r, "Failed to add filter for boot: %m"); + if (arg_invocation) { + /* If an invocation ID is found, then it is not necessary to add matches for boot and units. */ + r = add_invocation(j); + if (r < 0) + return log_error_errno(r, "Failed to add filter for invocation: %m"); + } else { + r = add_boot(j); + if (r < 0) + return log_error_errno(r, "Failed to add filter for boot: %m"); + + r = add_units(j); + if (r < 0) + return log_error_errno(r, "Failed to add filter for units: %m"); + } r = add_dmesg(j); if (r < 0) return log_error_errno(r, "Failed to add filter for dmesg: %m"); - r = add_units(j); - if (r < 0) - return log_error_errno(r, "Failed to add filter for units: %m"); - r = add_syslog_identifier(j); if (r < 0) return log_error_errno(r, "Failed to add filter for syslog identifiers: %m"); diff --git a/src/journal/journalctl-misc.c b/src/journal/journalctl-misc.c index ca0c9ec924..01fb83807a 100644 --- a/src/journal/journalctl-misc.c +++ b/src/journal/journalctl-misc.c @@ -236,6 +236,43 @@ int action_list_field_names(void) { return 0; } +int action_list_invocations(void) { + _cleanup_(sd_journal_closep) sd_journal *j = NULL; + _cleanup_free_ LogId *ids = NULL; + size_t n_ids; + LogIdType type; + const char *unit; + int r; + + assert(arg_action == ACTION_LIST_INVOCATIONS); + + r = acquire_unit("--list-invocations", &unit, &type); + if (r < 0) + return r; + + r = acquire_journal(&j); + if (r < 0) + return r; + + r = journal_acquire_boot(j); + if (r < 0) + return r; + + r = journal_get_log_ids( + j, type, + /* boot_id = */ arg_boot_id, /* unit = */ unit, + /* advance_older = */ arg_lines_needs_seek_end(), + /* max_ids = */ arg_lines >= 0 ? (size_t) arg_lines : SIZE_MAX, + &ids, &n_ids); + if (r < 0) + return log_error_errno(r, "Failed to list invocation id for %s: %m", unit); + if (r == 0) + return log_full_errno(arg_quiet ? LOG_DEBUG : LOG_ERR, SYNTHETIC_ERRNO(ENODATA), + "No invocation ID for %s found.", unit); + + return show_log_ids(ids, n_ids, "invocation id"); +} + int action_list_namespaces(void) { _cleanup_(table_unrefp) Table *table = NULL; sd_id128_t machine; diff --git a/src/journal/journalctl-misc.h b/src/journal/journalctl-misc.h index 70f851bc14..72036890a8 100644 --- a/src/journal/journalctl-misc.h +++ b/src/journal/journalctl-misc.h @@ -9,4 +9,5 @@ int action_disk_usage(void); int action_list_boots(void); int action_list_fields(void); int action_list_field_names(void); +int action_list_invocations(void); int action_list_namespaces(void); diff --git a/src/journal/journalctl-util.c b/src/journal/journalctl-util.c index 5ff02baeb9..b554f4d78a 100644 --- a/src/journal/journalctl-util.c +++ b/src/journal/journalctl-util.c @@ -9,6 +9,7 @@ #include "logs-show.h" #include "rlimit-util.h" #include "sigbus.h" +#include "strv.h" #include "terminal-util.h" char* format_timestamp_maybe_utc(char *buf, size_t l, usec_t t) { @@ -118,3 +119,78 @@ int journal_acquire_boot(sd_journal *j) { return 1; } + +int acquire_unit(const char *option_name, const char **ret_unit, LogIdType *ret_type) { + size_t n; + + assert(option_name); + assert(ret_unit); + assert(ret_type); + + n = strv_length(arg_system_units) + strv_length(arg_user_units); + if (n <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Using %s requires a unit. Please specify a unit name with -u/--unit=/--user-unit=.", + option_name); + if (n > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Using %s with multiple units is not supported.", + option_name); + + if (!strv_isempty(arg_system_units)) { + *ret_type = LOG_SYSTEM_UNIT_INVOCATION_ID; + *ret_unit = arg_system_units[0]; + } else { + assert(!strv_isempty(arg_user_units)); + *ret_type = LOG_USER_UNIT_INVOCATION_ID; + *ret_unit = arg_user_units[0]; + } + + return 0; +} + +int journal_acquire_invocation(sd_journal *j) { + LogIdType type = LOG_SYSTEM_UNIT_INVOCATION_ID; + const char *unit = NULL; + sd_id128_t id; + int r; + + assert(j); + + /* journal_acquire_boot() must be called before this. */ + + if (!arg_invocation) { + /* Clear relevant field for safety. */ + arg_invocation_id = SD_ID128_NULL; + arg_invocation_offset = 0; + return 0; + } + + /* When an invocation ID is explicitly specified without an offset, we do not care the ID is about + * system unit or user unit, and calling without unit name is allowed. Otherwise, a unit name must + * be specified. */ + if (arg_invocation_offset != 0 || sd_id128_is_null(arg_invocation_id)) { + r = acquire_unit("-I/--invocation= with an offset", &unit, &type); + if (r < 0) + return r; + } + + r = journal_find_log_id(j, type, arg_boot_id, unit, arg_invocation_id, arg_invocation_offset, &id); + if (r < 0) + return log_error_errno(r, "Failed to find journal entry for the invocation (%s%+i): %m", + sd_id128_is_null(arg_invocation_id) ? "" : SD_ID128_TO_STRING(arg_invocation_id), + arg_invocation_offset); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), + "No journal entry found for the invocation (%s%+i).", + sd_id128_is_null(arg_invocation_id) ? "" : SD_ID128_TO_STRING(arg_invocation_id), + arg_invocation_offset); + + log_debug("Found invocation ID %s for %s%+i", + SD_ID128_TO_STRING(id), + sd_id128_is_null(arg_invocation_id) ? "" : SD_ID128_TO_STRING(arg_invocation_id), + arg_invocation_offset); + + arg_invocation_id = id; + return 1; +} diff --git a/src/journal/journalctl-util.h b/src/journal/journalctl-util.h index ea3e56838d..14e3d569a6 100644 --- a/src/journal/journalctl-util.h +++ b/src/journal/journalctl-util.h @@ -3,9 +3,12 @@ #include "sd-journal.h" +#include "logs-show.h" #include "time-util.h" char* format_timestamp_maybe_utc(char *buf, size_t l, usec_t t); int acquire_journal(sd_journal **ret); bool journal_boot_has_effect(sd_journal *j); int journal_acquire_boot(sd_journal *j); +int acquire_unit(const char *option_name, const char **ret_unit, LogIdType *ret_type); +int journal_acquire_invocation(sd_journal *j); diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c index 369280a009..8ed5d98675 100644 --- a/src/journal/journalctl.c +++ b/src/journal/journalctl.c @@ -72,6 +72,9 @@ char **arg_syslog_identifier = NULL; char **arg_exclude_identifier = NULL; char **arg_system_units = NULL; char **arg_user_units = NULL; +bool arg_invocation = false; +sd_id128_t arg_invocation_id = SD_ID128_NULL; +int arg_invocation_offset = 0; const char *arg_field = NULL; bool arg_catalog = false; bool arg_reverse = false; @@ -227,6 +230,8 @@ static int help(void) { " -b --boot[=ID] Show current boot or the specified boot\n" " -u --unit=UNIT Show logs from the specified unit\n" " --user-unit=UNIT Show logs from the specified user unit\n" + " --invocation=ID Show logs from the matching invocation ID\n" + " -I Show logs from the latest invocation of unit\n" " -t --identifier=STRING Show entries with the specified syslog identifier\n" " -T --exclude-identifier=STRING\n" " Hide entries with the specified syslog identifier\n" @@ -267,6 +272,7 @@ static int help(void) { " -N --fields List all field names currently used\n" " -F --field=FIELD List all values that a specified field takes\n" " --list-boots Show terse information about recorded boots\n" + " --list-invocations Show invocation IDs of specified unit\n" " --list-namespaces Show list of journal namespaces\n" " --disk-usage Show total disk usage of all journal files\n" " --vacuum-size=BYTES Reduce disk usage below specified size\n" @@ -304,6 +310,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_NEW_ID128, ARG_THIS_BOOT, ARG_LIST_BOOTS, + ARG_LIST_INVOCATIONS, ARG_USER, ARG_SYSTEM, ARG_ROOT, @@ -320,6 +327,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_CURSOR_FILE, ARG_SHOW_CURSOR, ARG_USER_UNIT, + ARG_INVOCATION, ARG_LIST_CATALOG, ARG_DUMP_CATALOG, ARG_UPDATE_CATALOG, @@ -361,6 +369,7 @@ static int parse_argv(int argc, char *argv[]) { { "this-boot", no_argument, NULL, ARG_THIS_BOOT }, /* deprecated */ { "boot", optional_argument, NULL, 'b' }, { "list-boots", no_argument, NULL, ARG_LIST_BOOTS }, + { "list-invocations", no_argument, NULL, ARG_LIST_INVOCATIONS }, { "dmesg", no_argument, NULL, 'k' }, { "system", no_argument, NULL, ARG_SYSTEM }, { "user", no_argument, NULL, ARG_USER }, @@ -389,6 +398,7 @@ static int parse_argv(int argc, char *argv[]) { { "until", required_argument, NULL, 'U' }, { "unit", required_argument, NULL, 'u' }, { "user-unit", required_argument, NULL, ARG_USER_UNIT }, + { "invocation", required_argument, NULL, ARG_INVOCATION }, { "field", required_argument, NULL, 'F' }, { "fields", no_argument, NULL, 'N' }, { "catalog", no_argument, NULL, 'x' }, @@ -418,7 +428,7 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hefo:aln::qmb::kD:p:g:c:S:U:t:T:u:NF:xrM:i:", options, NULL)) >= 0) + while ((c = getopt_long(argc, argv, "hefo:aln::qmb::kD:p:g:c:S:U:t:T:u:INF:xrM:i:", options, NULL)) >= 0) switch (c) { @@ -540,6 +550,10 @@ static int parse_argv(int argc, char *argv[]) { arg_action = ACTION_LIST_BOOTS; break; + case ARG_LIST_INVOCATIONS: + arg_action = ACTION_LIST_INVOCATIONS; + break; + case 'k': arg_boot = arg_dmesg = true; break; @@ -831,6 +845,20 @@ static int parse_argv(int argc, char *argv[]) { return log_oom(); break; + case ARG_INVOCATION: + r = parse_id_descriptor(optarg, &arg_invocation_id, &arg_invocation_offset); + if (r < 0) + return log_error_errno(r, "Failed to parse invocation descriptor: %s", optarg); + arg_invocation = r; + break; + + case 'I': + /* Equivalent to --invocation=0 */ + arg_invocation = true; + arg_invocation_id = SD_ID128_NULL; + arg_invocation_offset = 0; + break; + case 'F': arg_action = ACTION_LIST_FIELDS; arg_field = optarg; @@ -1074,6 +1102,9 @@ static int run(int argc, char *argv[]) { case ACTION_LIST_FIELD_NAMES: return action_list_field_names(); + case ACTION_LIST_INVOCATIONS: + return action_list_invocations(); + case ACTION_LIST_NAMESPACES: return action_list_namespaces(); diff --git a/src/journal/journalctl.h b/src/journal/journalctl.h index 2d86c16917..409cfe2bca 100644 --- a/src/journal/journalctl.h +++ b/src/journal/journalctl.h @@ -26,6 +26,7 @@ typedef enum JournalctlAction { ACTION_LIST_BOOTS, ACTION_LIST_FIELDS, ACTION_LIST_FIELD_NAMES, + ACTION_LIST_INVOCATIONS, ACTION_LIST_NAMESPACES, ACTION_FLUSH, ACTION_RELINQUISH_VAR, @@ -76,6 +77,9 @@ extern char **arg_syslog_identifier; extern char **arg_exclude_identifier; extern char **arg_system_units; extern char **arg_user_units; +extern bool arg_invocation; +extern sd_id128_t arg_invocation_id; +extern int arg_invocation_offset; extern const char *arg_field; extern bool arg_catalog; extern bool arg_reverse; -- 2.25.1