From a109189fabe6a4c307528459f891c2d545361622 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 10 Feb 2026 16:27:58 +0100 Subject: [PATCH] journalctl: add new varlink read service to get entries We already have some varlink support for the journal to perform some actions like `Rotate`. It would be nice to be able to query the journal via varlink too so this commit adds a new varlinkctl based journal service that exposes a single GetEntries() call to retrieve journal entries. Basic filtering is supported and we can expand the API as needed. This is a separate `io.systemd.JournalControl` [1] service from the existing `io.systemd.Journald` to decouple read and write (thanks to Lennart for suggesting this). This also extracts some shared helper so that we do not duplicate code when generating the json or when adding the filters. [1] The name mirrors the bootctl->io.systemd.BootControl naming. --- meson.build | 1 + meson_options.txt | 2 + src/journal/journalctl-filter.c | 98 +-------------- src/journal/journalctl-util.c | 102 ++++++++++++++++ src/journal/journalctl-util.h | 2 + src/journal/journalctl-varlink-server.c | 114 ++++++++++++++++++ src/journal/journalctl-varlink-server.h | 6 + src/journal/journalctl.c | 40 ++++++ src/journal/journald-varlink.c | 2 - src/journal/meson.build | 1 + src/shared/logs-show.c | 34 ++++-- src/shared/logs-show.h | 2 + src/shared/meson.build | 1 + src/shared/varlink-io.systemd.JournalAccess.c | 30 +++++ src/shared/varlink-io.systemd.JournalAccess.h | 6 + sysusers.d/systemd-journal.conf.in | 3 +- .../TEST-04-JOURNAL.journalctl-varlink.sh | 59 +++++++++ units/meson.build | 5 + units/systemd-journalctl.socket | 23 ++++ units/systemd-journalctl@.service | 19 +++ units/user/meson.build | 5 + units/user/systemd-journalctl.socket | 20 +++ units/user/systemd-journalctl@.service | 15 +++ 23 files changed, 480 insertions(+), 110 deletions(-) create mode 100644 src/journal/journalctl-varlink-server.c create mode 100644 src/journal/journalctl-varlink-server.h create mode 100644 src/shared/varlink-io.systemd.JournalAccess.c create mode 100644 src/shared/varlink-io.systemd.JournalAccess.h create mode 100755 test/units/TEST-04-JOURNAL.journalctl-varlink.sh create mode 100644 units/systemd-journalctl.socket create mode 100644 units/systemd-journalctl@.service create mode 100644 units/user/systemd-journalctl.socket create mode 100644 units/user/systemd-journalctl@.service diff --git a/meson.build b/meson.build index b50466dcfd..1143181131 100644 --- a/meson.build +++ b/meson.build @@ -895,6 +895,7 @@ foreach option : ['adm-gid', 'video-gid', 'wheel-gid', 'systemd-journal-gid', + 'systemd-journal-uid', 'systemd-network-uid', 'systemd-resolve-uid', 'systemd-timesync-uid'] diff --git a/meson_options.txt b/meson_options.txt index c1af7ce237..16d7f69611 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -328,6 +328,8 @@ option('wheel-gid', type : 'integer', value : 0, description : 'soft-static allocation for the "wheel" group') option('systemd-journal-gid', type : 'integer', value : 0, description : 'soft-static allocation for the systemd-journal group') +option('systemd-journal-uid', type : 'integer', value : 0, + description : 'soft-static allocation for the systemd-journal user') option('systemd-network-uid', type : 'integer', value : 0, description : 'soft-static allocation for the systemd-network user') option('systemd-resolve-uid', type : 'integer', value : 0, diff --git a/src/journal/journalctl-filter.c b/src/journal/journalctl-filter.c index 54a54d105c..308f735477 100644 --- a/src/journal/journalctl-filter.c +++ b/src/journal/journalctl-filter.c @@ -9,7 +9,6 @@ #include "chase.h" #include "devnum-util.h" #include "fileio.h" -#include "glob-util.h" #include "journal-internal.h" #include "journalctl.h" #include "journalctl-filter.h" @@ -73,112 +72,17 @@ static int add_dmesg(sd_journal *j) { } static int add_units(sd_journal *j) { - _cleanup_strv_free_ char **patterns = NULL; - bool added = false; MatchUnitFlag flags = MATCH_UNIT_ALL; - int r; assert(j); - if (strv_isempty(arg_system_units) && strv_isempty(arg_user_units)) - return 0; - /* When --directory/-D, --root, --file/-i, or --machine/-M is specified, the opened journal file may * be external, and the uid of the systemd-coredump user that generates the coredump entries may be * different from the one in the current host. Let's relax the filter condition in such cases. */ if (arg_directory || arg_root || arg_file_stdin || arg_file || arg_machine) flags &= ~MATCH_UNIT_COREDUMP_UID; - STRV_FOREACH(i, arg_system_units) { - _cleanup_free_ char *u = NULL; - - r = unit_name_mangle(*i, UNIT_NAME_MANGLE_GLOB | (arg_quiet ? 0 : UNIT_NAME_MANGLE_WARN), &u); - if (r < 0) - return r; - - if (string_is_glob(u)) { - r = strv_consume(&patterns, TAKE_PTR(u)); - if (r < 0) - return r; - } else { - r = add_matches_for_unit_full(j, flags, u); - if (r < 0) - return r; - r = sd_journal_add_disjunction(j); - if (r < 0) - return r; - added = true; - } - } - - if (!strv_isempty(patterns)) { - _cleanup_set_free_ Set *units = NULL; - char *u; - - r = get_possible_units(j, SYSTEM_UNITS_FULL, patterns, &units); - if (r < 0) - return r; - - SET_FOREACH(u, units) { - r = add_matches_for_unit_full(j, flags, u); - if (r < 0) - return r; - r = sd_journal_add_disjunction(j); - if (r < 0) - return r; - added = true; - } - } - - patterns = strv_free(patterns); - - STRV_FOREACH(i, arg_user_units) { - _cleanup_free_ char *u = NULL; - - r = unit_name_mangle(*i, UNIT_NAME_MANGLE_GLOB | (arg_quiet ? 0 : UNIT_NAME_MANGLE_WARN), &u); - if (r < 0) - return r; - - if (string_is_glob(u)) { - r = strv_consume(&patterns, TAKE_PTR(u)); - if (r < 0) - return r; - } else { - r = add_matches_for_user_unit_full(j, flags, u); - if (r < 0) - return r; - r = sd_journal_add_disjunction(j); - if (r < 0) - return r; - added = true; - } - } - - if (!strv_isempty(patterns)) { - _cleanup_set_free_ Set *units = NULL; - char *u; - - r = get_possible_units(j, USER_UNITS_FULL, patterns, &units); - if (r < 0) - return r; - - SET_FOREACH(u, units) { - r = add_matches_for_user_unit_full(j, flags, u); - if (r < 0) - return r; - r = sd_journal_add_disjunction(j); - if (r < 0) - return r; - added = true; - } - } - - /* Complain if the user request matches but nothing whatsoever was found, since otherwise everything - * would be matched. */ - if (!added) - return -ENODATA; - - return sd_journal_add_conjunction(j); + return journal_add_unit_matches(j, flags, arg_quiet ? 0 : UNIT_NAME_MANGLE_WARN, arg_system_units, arg_user_units); } static int add_syslog_identifier(sd_journal *j) { diff --git a/src/journal/journalctl-util.c b/src/journal/journalctl-util.c index aedb2da20d..68761c83c9 100644 --- a/src/journal/journalctl-util.c +++ b/src/journal/journalctl-util.c @@ -177,6 +177,108 @@ int get_possible_units( return 0; } +int journal_add_unit_matches(sd_journal *j, MatchUnitFlag flags, UnitNameMangle mangle_flags, char **system_units, char **user_units) { + _cleanup_strv_free_ char **patterns = NULL; + bool added = false; + int r; + + assert(j); + + if (strv_isempty(system_units) && strv_isempty(user_units)) + return 0; + + STRV_FOREACH(i, system_units) { + _cleanup_free_ char *u = NULL; + + r = unit_name_mangle(*i, UNIT_NAME_MANGLE_GLOB | mangle_flags, &u); + if (r < 0) + return r; + + if (string_is_glob(u)) { + r = strv_consume(&patterns, TAKE_PTR(u)); + if (r < 0) + return r; + } else { + r = add_matches_for_unit_full(j, flags, u); + if (r < 0) + return r; + r = sd_journal_add_disjunction(j); + if (r < 0) + return r; + added = true; + } + } + + if (!strv_isempty(patterns)) { + _cleanup_set_free_ Set *units = NULL; + + r = get_possible_units(j, SYSTEM_UNITS_FULL, patterns, &units); + if (r < 0) + return r; + + char *u; + SET_FOREACH(u, units) { + r = add_matches_for_unit_full(j, flags, u); + if (r < 0) + return r; + r = sd_journal_add_disjunction(j); + if (r < 0) + return r; + added = true; + } + } + + patterns = strv_free(patterns); + + STRV_FOREACH(i, user_units) { + _cleanup_free_ char *u = NULL; + + r = unit_name_mangle(*i, UNIT_NAME_MANGLE_GLOB | mangle_flags, &u); + if (r < 0) + return r; + + if (string_is_glob(u)) { + r = strv_consume(&patterns, TAKE_PTR(u)); + if (r < 0) + return r; + } else { + r = add_matches_for_user_unit_full(j, flags, u); + if (r < 0) + return r; + r = sd_journal_add_disjunction(j); + if (r < 0) + return r; + added = true; + } + } + + if (!strv_isempty(patterns)) { + _cleanup_set_free_ Set *units = NULL; + + r = get_possible_units(j, USER_UNITS_FULL, patterns, &units); + if (r < 0) + return r; + + char *u; + SET_FOREACH(u, units) { + r = add_matches_for_user_unit_full(j, flags, u); + if (r < 0) + return r; + r = sd_journal_add_disjunction(j); + if (r < 0) + return r; + added = true; + } + } + + /* Complain if the user request matches but nothing whatsoever was found, since otherwise everything + * would be matched. */ + if (!added) + return -ENODATA; + + return sd_journal_add_conjunction(j); +} + int acquire_unit(sd_journal *j, const char *option_name, const char **ret_unit, LogIdType *ret_type) { size_t n; int r; diff --git a/src/journal/journalctl-util.h b/src/journal/journalctl-util.h index f237aab2b0..0b4993053a 100644 --- a/src/journal/journalctl-util.h +++ b/src/journal/journalctl-util.h @@ -3,6 +3,7 @@ #include "shared-forward.h" #include "logs-show.h" +#include "unit-name.h" /* The lists below are supposed to return the superset of unit names possibly matched by rules added with * add_matches_for_unit() and add_matches_for_user_unit(). */ @@ -31,5 +32,6 @@ int acquire_journal(sd_journal **ret); bool journal_boot_has_effect(sd_journal *j); int journal_acquire_boot(sd_journal *j); int get_possible_units(sd_journal *j, const char *fields, char * const *patterns, Set **ret); +int journal_add_unit_matches(sd_journal *j, MatchUnitFlag flags, UnitNameMangle mangle_flags, char **system_units, char **user_units); int acquire_unit(sd_journal *j, const char *option_name, const char **ret_unit, LogIdType *ret_type); int journal_acquire_invocation(sd_journal *j); diff --git a/src/journal/journalctl-varlink-server.c b/src/journal/journalctl-varlink-server.c new file mode 100644 index 0000000000..e78aa16be2 --- /dev/null +++ b/src/journal/journalctl-varlink-server.c @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-journal.h" +#include "sd-varlink.h" + +#include "journal-internal.h" +#include "journalctl-util.h" +#include "journalctl-varlink-server.h" +#include "json-util.h" +#include "log.h" +#include "logs-show.h" +#include "output-mode.h" +#include "strv.h" +#include "varlink-util.h" + +typedef struct GetEntriesParameters { + char **units; + char **user_units; + const char *namespace; + int priority; + uint64_t limit; +} GetEntriesParameters; + +static void get_entries_parameters_done(GetEntriesParameters *p) { + assert(p); + p->units = strv_free(p->units); + p->user_units = strv_free(p->user_units); +} + +int vl_method_get_entries(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { + { "units", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(GetEntriesParameters, units), 0 }, + { "userUnits", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(GetEntriesParameters, user_units), 0 }, + { "namespace", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(GetEntriesParameters, namespace), 0 }, + { "priority", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_log_level, offsetof(GetEntriesParameters, priority), 0 }, + { "limit", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(GetEntriesParameters, limit), 0 }, + {} + }; + + _cleanup_(get_entries_parameters_done) GetEntriesParameters p = { + .priority = -1, + }; + _cleanup_(sd_journal_closep) sd_journal *j = NULL; + int r; + + assert(link); + assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)); + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + /* systemd ships with sensible defaults for the system/user services and the socket permissions so we + * do not need to do extra sd_varlink_get_peer_uid() or policykit checks here */ + r = sd_journal_open_namespace(&j, p.namespace, SD_JOURNAL_LOCAL_ONLY | SD_JOURNAL_ASSUME_IMMUTABLE); + if (r < 0) + return log_error_errno(r, "Failed to open journal: %m"); + + r = journal_add_unit_matches(j, MATCH_UNIT_ALL, /* mangle_flags= */ 0, p.units, p.user_units); + if (r == -ENODATA) + return sd_varlink_error(link, SD_VARLINK_ERROR_INVALID_PARAMETER, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add unit matches: %m"); + + if (p.priority >= 0) { + for (int i = 0; i <= p.priority; i++) { + r = journal_add_matchf(j, "PRIORITY=%d", i); + if (r < 0) + return log_error_errno(r, "Failed to add priority match: %m"); + } + + r = sd_journal_add_conjunction(j); + if (r < 0) + return r; + } + + /* this simulates "journalctl -n $p.limit" */ + r = sd_journal_seek_tail(j); + if (r < 0) + return log_error_errno(r, "Failed to seek to tail: %m"); + + /* FIXME: this restriction should be removed eventually */ + if (p.limit > 10000) + return sd_varlink_error_invalid_parameter_name(link, "limit"); + + uint64_t n = p.limit == 0 ? 100 : p.limit; + + r = varlink_set_sentinel(link, "io.systemd.JournalAccess.NoEntries"); + if (r < 0) + return r; + + for (uint64_t i = 0; i < n; i++) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *entry = NULL; + + r = sd_journal_previous(j); + if (r < 0) + return log_error_errno(r, "Failed to iterate journal: %m"); + if (r == 0) + break; + + r = journal_entry_to_json(j, OUTPUT_SHOW_ALL, /* output_fields= */ NULL, &entry); + if (r < 0) + return r; + if (r == 0) + continue; /* skip corrupted entry */ + + r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR("entry", SD_JSON_BUILD_VARIANT(entry))); + if (r < 0) + return r; + } + + return 0; +} diff --git a/src/journal/journalctl-varlink-server.h b/src/journal/journalctl-varlink-server.h new file mode 100644 index 0000000000..45cf0e6918 --- /dev/null +++ b/src/journal/journalctl-varlink-server.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink.h" + +int vl_method_get_entries(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c index 95b6cf20da..82bf6b8894 100644 --- a/src/journal/journalctl.c +++ b/src/journal/journalctl.c @@ -4,6 +4,7 @@ #include #include "sd-journal.h" +#include "sd-varlink.h" #include "build.h" #include "dissect-image.h" @@ -17,6 +18,7 @@ #include "journalctl-misc.h" #include "journalctl-show.h" #include "journalctl-varlink.h" +#include "journalctl-varlink-server.h" #include "log.h" #include "loop-util.h" #include "main-func.h" @@ -35,6 +37,8 @@ #include "strv.h" #include "syslog-util.h" #include "time-util.h" +#include "varlink-io.systemd.JournalAccess.h" +#include "varlink-util.h" #define DEFAULT_FSS_INTERVAL_USEC (15*USEC_PER_MINUTE) @@ -107,6 +111,7 @@ pcre2_code *arg_compiled_pattern = NULL; PatternCompileCase arg_case = PATTERN_COMPILE_CASE_AUTO; ImagePolicy *arg_image_policy = NULL; bool arg_synchronize_on_exit = false; +static bool arg_varlink = false; STATIC_DESTRUCTOR_REGISTER(arg_cursor, freep); STATIC_DESTRUCTOR_REGISTER(arg_cursor_file, freep); @@ -324,6 +329,29 @@ static int help(void) { return 0; } +static int vl_server(void) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; + int r; + + r = varlink_server_new(&varlink_server, /* flags= */ 0, /* userdata= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_JournalAccess); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = sd_varlink_server_bind_method(varlink_server, "io.systemd.JournalAccess.GetEntries", vl_method_get_entries); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink method: %m"); + + r = sd_varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return 0; +} + static int parse_argv(int argc, char *argv[]) { enum { @@ -453,6 +481,15 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); + r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r > 0) { + arg_varlink = true; + arg_pager_flags |= PAGER_DISABLE; + return 1; + } + while ((c = getopt_long(argc, argv, "hefo:aln::qmb::kD:p:g:c:S:U:t:T:u:INF:xrM:i:W", options, NULL)) >= 0) switch (c) { @@ -1094,6 +1131,9 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; + if (arg_varlink) + return vl_server(); + r = strv_copy_unless_empty(strv_skip(argv, optind), &args); if (r < 0) return log_oom(); diff --git a/src/journal/journald-varlink.c b/src/journal/journald-varlink.c index 6ea293791d..674f2524eb 100644 --- a/src/journal/journald-varlink.c +++ b/src/journal/journald-varlink.c @@ -1,7 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include "sd-event.h" - #include "journald-manager.h" #include "journald-sync.h" #include "journald-varlink.h" diff --git a/src/journal/meson.build b/src/journal/meson.build index 1ad5c40dfa..5f64304219 100644 --- a/src/journal/meson.build +++ b/src/journal/meson.build @@ -41,6 +41,7 @@ journalctl_sources = files( 'journalctl-show.c', 'journalctl-util.c', 'journalctl-varlink.c', + 'journalctl-varlink-server.c', ) if get_option('link-journalctl-shared') diff --git a/src/shared/logs-show.c b/src/shared/logs-show.c index 119c48936a..7fa8935af3 100644 --- a/src/shared/logs-show.c +++ b/src/shared/logs-show.c @@ -1193,16 +1193,7 @@ static int update_json_data_split( return update_json_data(h, flags, name, eq + 1, size - fieldlen - 1); } -static int output_json( - FILE *f, - sd_journal *j, - OutputMode mode, - unsigned n_columns, - OutputFlags flags, - const Set *output_fields, - const size_t highlight[2], - dual_timestamp *previous_display_ts, /* unused */ - sd_id128_t *previous_boot_id) { /* unused */ +int journal_entry_to_json(sd_journal *j, OutputFlags flags, const Set *output_fields, sd_json_variant **ret) { char usecbuf[CONST_MAX(DECIMAL_STR_MAX(usec_t), DECIMAL_STR_MAX(uint64_t))]; _cleanup_(sd_json_variant_unrefp) sd_json_variant *object = NULL; @@ -1217,6 +1208,7 @@ static int output_json( int r; assert(j); + assert(ret); (void) sd_journal_set_data_threshold(j, flags & OUTPUT_SHOW_ALL ? 0 : JSON_THRESHOLD); @@ -1323,6 +1315,28 @@ static int output_json( if (r < 0) return log_error_errno(r, "Failed to allocate JSON object: %m"); + *ret = TAKE_PTR(object); + return 1; +} + +static int output_json( + FILE *f, + sd_journal *j, + OutputMode mode, + unsigned n_columns, + OutputFlags flags, + const Set *output_fields, + const size_t highlight[2], + dual_timestamp *previous_display_ts, /* unused */ + sd_id128_t *previous_boot_id) { /* unused */ + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *object = NULL; + int r; + + r = journal_entry_to_json(j, flags, output_fields, &object); + if (r <= 0) + return r; + return sd_json_variant_dump(object, output_mode_to_json_format_flags(mode) | (FLAGS_SET(flags, OUTPUT_COLOR) ? SD_JSON_FORMAT_COLOR : 0), diff --git a/src/shared/logs-show.h b/src/shared/logs-show.h index e9f537681c..e2886d69cd 100644 --- a/src/shared/logs-show.h +++ b/src/shared/logs-show.h @@ -81,6 +81,8 @@ void json_escape( size_t l, OutputFlags flags); +int journal_entry_to_json(sd_journal *j, OutputFlags flags, const Set *output_fields, sd_json_variant **ret); + int discover_next_id( sd_journal *j, LogIdType type, diff --git a/src/shared/meson.build b/src/shared/meson.build index 5becbf3b4f..1a07b908ca 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -205,6 +205,7 @@ shared_sources = files( 'varlink-io.systemd.Hostname.c', 'varlink-io.systemd.Import.c', 'varlink-io.systemd.Journal.c', + 'varlink-io.systemd.JournalAccess.c', 'varlink-io.systemd.Login.c', 'varlink-io.systemd.Machine.c', 'varlink-io.systemd.MachineImage.c', diff --git a/src/shared/varlink-io.systemd.JournalAccess.c b/src/shared/varlink-io.systemd.JournalAccess.c new file mode 100644 index 0000000000..c9e65c1a4f --- /dev/null +++ b/src/shared/varlink-io.systemd.JournalAccess.c @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.JournalAccess.h" + +static SD_VARLINK_DEFINE_METHOD_FULL( + GetEntries, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_FIELD_COMMENT("Show messages for the specified systemd units (e.g. ['foo.service'])."), + SD_VARLINK_DEFINE_INPUT(units, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("Show messages for the specified user units (e.g. ['foo.service'])."), + SD_VARLINK_DEFINE_INPUT(userUnits, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("If specified, shows the log data of the specified namespace, otherwise the default namespace."), + SD_VARLINK_DEFINE_INPUT(namespace, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Filter output by message priorities or priority ranges (i.e. between 0/'emerg' and 7/'debug')"), + SD_VARLINK_DEFINE_INPUT(priority, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Maximum number of entries to return. Defaults to 100, capped at 10000."), + SD_VARLINK_DEFINE_INPUT(limit, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The journal entry in flat JSON format, matching journalctl --output=json."), + SD_VARLINK_DEFINE_OUTPUT(entry, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_ERROR(NoEntries); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_JournalAccess, + "io.systemd.JournalAccess", + SD_VARLINK_INTERFACE_COMMENT("Journal log read APIs"), + SD_VARLINK_SYMBOL_COMMENT("Retrieve journal log entries, optionally filtered by unit, priority, etc."), + &vl_method_GetEntries, + SD_VARLINK_SYMBOL_COMMENT("No journal entries matched the specified filters."), + &vl_error_NoEntries); diff --git a/src/shared/varlink-io.systemd.JournalAccess.h b/src/shared/varlink-io.systemd.JournalAccess.h new file mode 100644 index 0000000000..d2d27dc600 --- /dev/null +++ b/src/shared/varlink-io.systemd.JournalAccess.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink-idl.h" + +extern const sd_varlink_interface vl_interface_io_systemd_JournalAccess; diff --git a/sysusers.d/systemd-journal.conf.in b/sysusers.d/systemd-journal.conf.in index 61768b234e..bb1277f2bb 100644 --- a/sysusers.d/systemd-journal.conf.in +++ b/sysusers.d/systemd-journal.conf.in @@ -5,4 +5,5 @@ # the Free Software Foundation; either version 2.1 of the License, or # (at your option) any later version. -g systemd-journal {{SYSTEMD_JOURNAL_GID}} - +g systemd-journal {{SYSTEMD_JOURNAL_GID}} - +u! systemd-journal {{SYSTEMD_JOURNAL_UID}} "systemd Journal" diff --git a/test/units/TEST-04-JOURNAL.journalctl-varlink.sh b/test/units/TEST-04-JOURNAL.journalctl-varlink.sh new file mode 100755 index 0000000000..2d7b19990c --- /dev/null +++ b/test/units/TEST-04-JOURNAL.journalctl-varlink.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +VARLINK_SOCKET="/run/systemd/io.systemd.JournalAccess" + +# ensure the varlink basics work +varlinkctl list-interfaces "$VARLINK_SOCKET" | grep io.systemd.JournalAccess +varlinkctl introspect "$VARLINK_SOCKET" | grep "method GetEntries(" + +# lets start with a basic log entry +TAG="$(systemd-id128 new)" +echo "varlink-test-message" | systemd-cat -t "$TAG" +systemd-cat -t "$TAG" -p warning echo "varlink-test-warning" +journalctl --sync + +# most basic call works +varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{}' | jq --seq . +# validate the JSON has some basic properties (similar to journalctls json output) +varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{}' | jq --seq '.entry | {MESSAGE, PRIORITY, _UID}' + +# check that default limit works (100), we don't know how many entries we have so we just check +# bounds +ENTRIES=$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{}' | wc -l) +test "$ENTRIES" -gt 0 +test "$ENTRIES" -le 100 + +# check explicit limit +ENTRIES=$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{"limit": 3}' | wc -l) +test "$ENTRIES" -le 3 + +# check unit filter: use transient units to get deterministic results +UNIT_NAME_1="test-journalctl-varlink-1-$RANDOM.service" +systemd-run --unit="$UNIT_NAME_1" --wait bash -c 'echo hello-from-varlink-test-1' +UNIT_NAME_2="test-journalctl-varlink-2-$RANDOM.service" +systemd-run --unit="$UNIT_NAME_2" --wait bash -c 'echo hello-from-varlink-test-2' +journalctl --sync + +# single unit filter +varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries "{\"units\": [\"$UNIT_NAME_1\"]}" | grep -q "hello-from-varlink-test-1" +(! varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries "{\"units\": [\"$UNIT_NAME_1\"]}" | grep "hello-from-varlink-test-2") +# multi unit filter +varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries "{\"units\": [\"$UNIT_NAME_1\", \"$UNIT_NAME_2\"]}" | grep -q "hello-from-varlink-test-1" +varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries "{\"units\": [\"$UNIT_NAME_1\", \"$UNIT_NAME_2\"]}" | grep -q "hello-from-varlink-test-2" + +# check priority filter: priority 4 (warning) should include our warning message +varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{"priority": 4, "limit": 1000}' | grep -q "varlink-test-warning" +# check priority filter: priority 3 (error) should NOT include our warning (priority 4) +(! varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{"priority": 3, "limit": 1000}' | grep "varlink-test-warning") + +# invalid parameter: limit over 10000 should fail +(! varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{"limit": 99999}') + +# invalid parameter: priority over 7 should fail +(! varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{"priority": 99}') + +# NoEntries error is raised if there is no result +(! varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{"units": ["nonexistent-unit-that-should-have-no-entries.service"]}' 2>&1 | grep io.systemd.JournalAccess.NoEntries ) diff --git a/units/meson.build b/units/meson.build index ebefaf87f0..b2cf9bd8f3 100644 --- a/units/meson.build +++ b/units/meson.build @@ -439,6 +439,11 @@ units = [ 'file' : 'systemd-journal-upload.service.in', 'conditions' : ['ENABLE_REMOTE', 'HAVE_LIBCURL'], }, + { 'file' : 'systemd-journalctl@.service' }, + { + 'file' : 'systemd-journalctl.socket', + 'symlinks' : ['sockets.target.wants/'], + }, { 'file' : 'systemd-journald-audit.socket' }, { 'file' : 'systemd-journald-dev-log.socket', diff --git a/units/systemd-journalctl.socket b/units/systemd-journalctl.socket new file mode 100644 index 0000000000..267b89adbb --- /dev/null +++ b/units/systemd-journalctl.socket @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Journal Log Access Socket +Documentation=man:journalctl(1) +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/io.systemd.JournalAccess +Symlinks=/run/varlink/registry/io.systemd.JournalAccess +FileDescriptorName=varlink +SocketGroup=systemd-journal +SocketMode=0660 +Accept=yes +MaxConnectionsPerSource=16 diff --git a/units/systemd-journalctl@.service b/units/systemd-journalctl@.service new file mode 100644 index 0000000000..84ac18cbbc --- /dev/null +++ b/units/systemd-journalctl@.service @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Journal Log Access Service +Documentation=man:journalctl(1) +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target + +[Service] +ExecStart=journalctl +User=systemd-journal diff --git a/units/user/meson.build b/units/user/meson.build index baa30c500f..a9c6d44281 100644 --- a/units/user/meson.build +++ b/units/user/meson.build @@ -56,6 +56,11 @@ units = [ }, { 'file' : 'systemd-ask-password@.service' }, { 'file' : 'systemd-exit.service' }, + { 'file' : 'systemd-journalctl@.service' }, + { + 'file' : 'systemd-journalctl.socket', + 'symlinks' : ['sockets.target.wants/'], + }, { 'file' : 'systemd-tmpfiles-clean.service' }, { 'file' : 'systemd-tmpfiles-clean.timer' }, { 'file' : 'systemd-tmpfiles-setup.service' }, diff --git a/units/user/systemd-journalctl.socket b/units/user/systemd-journalctl.socket new file mode 100644 index 0000000000..0ea3a25c90 --- /dev/null +++ b/units/user/systemd-journalctl.socket @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Journal Log Access Socket +Documentation=man:journalctl(1) + +[Socket] +ListenStream=%t/systemd/io.systemd.JournalAccess +Symlinks=%t/varlink/registry/io.systemd.JournalAccess +FileDescriptorName=varlink +SocketMode=0600 +Accept=yes +MaxConnectionsPerSource=16 diff --git a/units/user/systemd-journalctl@.service b/units/user/systemd-journalctl@.service new file mode 100644 index 0000000000..04749fdb58 --- /dev/null +++ b/units/user/systemd-journalctl@.service @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Journal Log Access Service +Documentation=man:journalctl(1) + +[Service] +ExecStart=journalctl