]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
journalctl: add new varlink read service to get entries
authorMichael Vogt <michael@amutable.com>
Tue, 10 Feb 2026 15:27:58 +0000 (16:27 +0100)
committerMichael Vogt <michael@amutable.com>
Sat, 21 Feb 2026 12:02:38 +0000 (13:02 +0100)
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.

23 files changed:
meson.build
meson_options.txt
src/journal/journalctl-filter.c
src/journal/journalctl-util.c
src/journal/journalctl-util.h
src/journal/journalctl-varlink-server.c [new file with mode: 0644]
src/journal/journalctl-varlink-server.h [new file with mode: 0644]
src/journal/journalctl.c
src/journal/journald-varlink.c
src/journal/meson.build
src/shared/logs-show.c
src/shared/logs-show.h
src/shared/meson.build
src/shared/varlink-io.systemd.JournalAccess.c [new file with mode: 0644]
src/shared/varlink-io.systemd.JournalAccess.h [new file with mode: 0644]
sysusers.d/systemd-journal.conf.in
test/units/TEST-04-JOURNAL.journalctl-varlink.sh [new file with mode: 0755]
units/meson.build
units/systemd-journalctl.socket [new file with mode: 0644]
units/systemd-journalctl@.service [new file with mode: 0644]
units/user/meson.build
units/user/systemd-journalctl.socket [new file with mode: 0644]
units/user/systemd-journalctl@.service [new file with mode: 0644]

index b50466dcfd0ea99fe7a302f9b135eb168bbf3297..114318113124ce482afedba589fba6f8bafb90e2 100644 (file)
@@ -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']
index c1af7ce2374928010ea673f0b805316fb8491007..16d7f69611185dec542d71db63a0d67eb03f7f23 100644 (file)
@@ -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,
index 54a54d105c59a18bb896e1dc0a5cf9e596828e5c..308f7354770bd69876f4f2bb71f4a4e2c9637fbe 100644 (file)
@@ -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) {
index aedb2da20dc4465f61521f7ca2de234ea55b3704..68761c83c9cb652d4a4a5891585c4f129e233d86 100644 (file)
@@ -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;
index f237aab2b03751b16662031c474cdac732378fcd..0b4993053a569b0414d07d5a323f7f93e899a506 100644 (file)
@@ -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 (file)
index 0000000..e78aa16
--- /dev/null
@@ -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 (file)
index 0000000..45cf0e6
--- /dev/null
@@ -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);
index 95b6cf20dae1c7aa8ff0f65527fa5b6ef9e6c19f..82bf6b88944ab4b60a12bd0795e927cd210f0752 100644 (file)
@@ -4,6 +4,7 @@
 #include <locale.h>
 
 #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();
index 6ea293791ddcca5114cf2365ebffa93ea44d7c15..674f2524eb82872b694aa82bf9474ba250ffb55c 100644 (file)
@@ -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"
index 1ad5c40dfa8bdb5ece96630675ecdba2aacfd66d..5f643042194478208bda73813b2cc3308c9fc36e 100644 (file)
@@ -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')
index 119c48936abb284cac02d9a64147571b18482b00..7fa8935af3f53be1d4d20498d6775f6bceae1622 100644 (file)
@@ -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),
index e9f537681c80ca87b3779815805891fd7dcaec50..e2886d69cd1d664f660e661ee80544138bda4011 100644 (file)
@@ -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,
index 5becbf3b4fd2d22208432baa8b8bc0ae9c7468d4..1a07b908cafe6a1c0500300d12136f76c197adcd 100644 (file)
@@ -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 (file)
index 0000000..c9e65c1
--- /dev/null
@@ -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 (file)
index 0000000..d2d27dc
--- /dev/null
@@ -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;
index 61768b234ea72c75f85beddaea6b44703de9c56d..bb1277f2bb9bd80aea49d8265882c2780f45e787 100644 (file)
@@ -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 (executable)
index 0000000..2d7b199
--- /dev/null
@@ -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 )
index ebefaf87f06a1cde3b597b9853522f4375da500b..b2cf9bd8f39ce514726a6b04f2bdc34fb9057f9f 100644 (file)
@@ -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 (file)
index 0000000..267b89a
--- /dev/null
@@ -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 (file)
index 0000000..84ac18c
--- /dev/null
@@ -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
index baa30c500fe143030e1f258f20e3f8f79dc9daed..a9c6d44281c28c7af81e414d480b2264e3177d91 100644 (file)
@@ -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 (file)
index 0000000..0ea3a25
--- /dev/null
@@ -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 (file)
index 0000000..04749fd
--- /dev/null
@@ -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