diff --git a/TODO b/TODO
index b413dc39e8..1edeaee48b 100644
--- a/TODO
+++ b/TODO
@@ -134,17 +134,12 @@ Features:
* oci: support "data" in any OCI descriptor, not just manifest config.
* report:
- - should the list of metrics use JSON-SEQ? or maybe be wrapped in a json
- array (the latter might be necessary, once we sign the combination)
+ - plug "facts" into systemd-report too, i.e. stuff that is more static, such as hostnames, ssh keys and so on.
+ - pass filtering hints to services, so that they can also be applied server-side, not just client side
- metrics from pid1: suppress metrics form units that are inactive and have nothing to report
- - how to plug facts into this? i.e. hostname, ssh keys, and so on
- - switch to daan's suggested hierarchy?
- - enforce naming rules: a backend can only report metrics with the prefix of its service name
- - use that to optimize sorted report generation
- - figure out report vs. metrics
- - systemd-report: add prefix matching of metrics
- add "hint-suppress-zero" flag (which suppresses all metrics which are zero)
- add "hint-object" parameter (which only queries info about certain object)
+ - make systemd-report a varlink service
* implement a varlink registry service, similar to the one of the reference
implementation, backed by /run/varlink/registry/. Then, also implement
diff --git a/man/systemd-report.xml b/man/systemd-report.xml
index f786f0eb72..b8ca0a1a24 100644
--- a/man/systemd-report.xml
+++ b/man/systemd-report.xml
@@ -37,6 +37,47 @@
standard output.
+
+ Commands
+
+ The following commands are understood:
+
+
+
+ metrics MATCH
+
+ Acquire a list of metrics values from all local services providing them, and write
+ them to standard output. Optionally takes one or more match expressions for filtering the metrics to
+ show. The expression either may be a literal metric family name to search for, or a prefix of one
+ (which will be matched only at dot boundaries). If multiple matches are specified as multiple
+ paramaters, any metric matching any of the specified matches are shown.
+
+
+
+
+
+ describe-metrics MATCH
+
+ Acquire a list of metric families from all local services providing them, and write
+ them to standard output. This returns primarily static information about metrics, their data types
+ and human readable description, without values.
+
+ Match expressions similar to those supported by metrics are supported for
+ describe-metrics, too.
+
+
+
+
+
+ list-sources
+
+ Show list of known metrics sources.
+
+
+
+
+
+
Options
@@ -54,7 +95,10 @@
+
+
+
diff --git a/src/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c
index 41ac8c4097..7f90b7fc79 100644
--- a/src/libsystemd/sd-json/json-util.c
+++ b/src/libsystemd/sd-json/json-util.c
@@ -743,3 +743,105 @@ int json_dispatch_access_mode(const char *name, sd_json_variant *variant, sd_jso
return 0;
}
+
+int json_variant_compare(sd_json_variant *a, sd_json_variant *b) {
+ int r;
+
+ r = CMP(!!a, !!b);
+ if (r != 0)
+ return r;
+
+ if (sd_json_variant_equal(a, b))
+ return 0;
+
+ if (sd_json_variant_is_null(a))
+ return -1;
+ if (sd_json_variant_is_null(b))
+ return 1;
+
+ if (sd_json_variant_is_string(a) &&
+ sd_json_variant_is_string(b))
+ return strcmp(sd_json_variant_string(a), sd_json_variant_string(b));
+
+ if (sd_json_variant_is_integer(a) &&
+ sd_json_variant_is_integer(b))
+ return CMP(sd_json_variant_integer(a), sd_json_variant_integer(b));
+
+ if (sd_json_variant_is_unsigned(a) &&
+ sd_json_variant_is_unsigned(b))
+ return CMP(sd_json_variant_unsigned(a), sd_json_variant_unsigned(b));
+
+ /* We cannot necessarily compare 64bit signed with unsigned, hence we go via sign checking instead */
+ if (sd_json_variant_is_number(a) && sd_json_variant_is_number(b)) {
+ if (sd_json_variant_is_negative(a) &&
+ !sd_json_variant_is_negative(b))
+ return -1;
+
+ if (!sd_json_variant_is_negative(a) &&
+ sd_json_variant_is_negative(b))
+ return 1;
+ }
+
+ if (sd_json_variant_is_real(a) &&
+ sd_json_variant_is_real(b))
+ return CMP(sd_json_variant_real(a), sd_json_variant_real(b));
+
+ if (sd_json_variant_is_boolean(a) &&
+ sd_json_variant_is_boolean(b))
+ return CMP(sd_json_variant_boolean(a), sd_json_variant_boolean(b));
+
+ if (sd_json_variant_is_array(a) &&
+ sd_json_variant_is_array(b)) {
+
+ size_t n = sd_json_variant_elements(a),
+ m = sd_json_variant_elements(b);
+ for (size_t i = 0; i < n || i < m; i++) {
+
+ if (i >= n)
+ return -1;
+ if (i >= m)
+ return 1;
+
+ r = json_variant_compare(
+ sd_json_variant_by_index(a, i),
+ sd_json_variant_by_index(b, i));
+ if (r != 0)
+ return r;
+ }
+
+ return 0;
+ }
+
+ if (sd_json_variant_is_object(a) &&
+ sd_json_variant_is_object(b)) {
+ const char *k, *lowest = NULL;
+ sd_json_variant *v;
+ int result = 0;
+
+ JSON_VARIANT_OBJECT_FOREACH(k, v, a) {
+ if (lowest && strcmp(k, lowest) >= 0)
+ continue;
+
+ r = json_variant_compare(v, sd_json_variant_by_key(b, k));
+ if (r != 0) {
+ lowest = k;
+ result = r;
+ }
+ }
+
+ JSON_VARIANT_OBJECT_FOREACH(k, v, b) {
+ if (lowest && strcmp(k, lowest) >= 0)
+ continue;
+
+ r = json_variant_compare(v, sd_json_variant_by_key(a, k));
+ if (r != 0) {
+ lowest = k;
+ result = -r;
+ }
+ }
+
+ return result;
+ }
+
+ return CMP(sd_json_variant_type(a), sd_json_variant_type(b));
+}
diff --git a/src/libsystemd/sd-json/json-util.h b/src/libsystemd/sd-json/json-util.h
index d06a72aef9..847725a41e 100644
--- a/src/libsystemd/sd-json/json-util.h
+++ b/src/libsystemd/sd-json/json-util.h
@@ -273,3 +273,5 @@ int json_variant_new_fd_info(sd_json_variant **ret, int fd);
char *json_underscorify(char *p);
char *json_dashify(char *p);
+
+int json_variant_compare(sd_json_variant *a, sd_json_variant *b);
diff --git a/src/report/meson.build b/src/report/meson.build
index ab1ac45acc..2813f9d033 100644
--- a/src/report/meson.build
+++ b/src/report/meson.build
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
executables += [
- executable_template + {
+ libexec_template + {
'name' : 'systemd-report',
'public' : true,
'sources' : files('report.c'),
diff --git a/src/report/report.c b/src/report/report.c
index e038d4fa5b..3c2f89e002 100644
--- a/src/report/report.c
+++ b/src/report/report.c
@@ -8,27 +8,45 @@
#include "alloc-util.h"
#include "ansi-color.h"
#include "build.h"
+#include "chase.h"
#include "dirent-util.h"
-#include "fd-util.h"
+#include "format-table.h"
#include "log.h"
#include "main-func.h"
#include "parse-argument.h"
#include "path-lookup.h"
#include "pretty-print.h"
+#include "recurse-dir.h"
#include "runtime-scope.h"
#include "set.h"
#include "sort-util.h"
#include "string-util.h"
+#include "strv.h"
#include "time-util.h"
+#include "varlink-idl-util.h"
+#include "verbs.h"
#define METRICS_MAX 1024U
#define METRICS_LINKS_MAX 128U
#define TIMEOUT_USEC (30 * USEC_PER_SEC) /* 30 seconds */
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
-static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
+static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
+static char **arg_matches = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep);
+
+typedef enum Action {
+ ACTION_LIST,
+ ACTION_DESCRIBE,
+ _ACTION_MAX,
+ _ACTION_INVALID = -EINVAL,
+} Action;
typedef struct Context {
+ Action action;
sd_event *event;
Set *link_infos;
sd_json_variant **metrics; /* Collected metrics for sorting */
@@ -38,7 +56,7 @@ typedef struct Context {
typedef struct LinkInfo {
Context *context;
sd_varlink *link;
- char *metric_prefix;
+ char *name;
} LinkInfo;
static LinkInfo* link_info_free(LinkInfo *li) {
@@ -46,7 +64,7 @@ static LinkInfo* link_info_free(LinkInfo *li) {
return NULL;
sd_varlink_close_unref(li->link);
- free(li->metric_prefix);
+ free(li->name);
return mfree(li);
}
@@ -61,12 +79,12 @@ static void context_done(Context *context) {
DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_free);
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
- link_info_hash_ops,
- void,
- trivial_hash_func,
- trivial_compare_func,
- LinkInfo,
- link_info_free);
+ link_info_hash_ops,
+ void,
+ trivial_hash_func,
+ trivial_compare_func,
+ LinkInfo,
+ link_info_free);
static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b) {
const char *name_a, *name_b, *object_a, *object_b;
@@ -99,15 +117,47 @@ static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b)
return strcmp_ptr(fields_str_a, fields_str_b);
}
-static inline bool metric_startswith_prefix(const char *metric_name, const char *prefix) {
+static int metrics_name_valid(const char *metric_name) {
+
+ /* Validates a metrics family name. Since the prefix shall match the Varlink service name, we'll
+ * enforce Varlink interface naming rules on it. Given how close we are to Varlink let's also enforce
+ * rules on metrics names similar to those of Varlink field names. */
+
+ const char *e = strrchr(metric_name, '.');
+ if (!e)
+ return false;
+
+ _cleanup_free_ char *j = strndup(metric_name, e - metric_name);
+ if (!j)
+ return -ENOMEM;
+
+ if (!varlink_idl_interface_name_is_valid(j))
+ return false;
+
+ if (!varlink_idl_field_name_is_valid(e+1))
+ return false;
+
+ return true;
+}
+
+static bool metric_startswith_prefix(const char *metric_name, const char *prefix) {
if (isempty(metric_name) || isempty(prefix))
return false;
+ /* NB: this checks for a *true* prefix, i.e. insists on the dot separator after the prefix. Or in
+ * other words, "foo" is not going to be considered a prefix of "foo", but of "foo.bar" it will. */
+
const char *m = startswith(metric_name, prefix);
return !isempty(m) && m[0] == '.';
}
-static bool metrics_validate_one(LinkInfo *li, sd_json_variant *metric) {
+typedef enum {
+ VERDICT_INVALID,
+ VERDICT_MATCH,
+ VERDICT_MISMATCH,
+} Verdict;
+
+static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) {
int r;
assert(li);
@@ -122,10 +172,48 @@ static bool metrics_validate_one(LinkInfo *li, sd_json_variant *metric) {
r = sd_json_dispatch(metric, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &metric_name);
if (r < 0) {
log_debug_errno(r, "Failed to get metric name, assuming name is not valid: %m");
- return false;
+ return VERDICT_INVALID;
}
- return metric_startswith_prefix(metric_name, li->metric_prefix);
+ /* Validate metric name is generally valid */
+ r = metrics_name_valid(metric_name);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to determine if '%s' is a valid metric name: %m", metric_name);
+ return VERDICT_INVALID;
+ }
+ if (!r) {
+ log_debug("Metric name '%s' is not valid, skipping.", metric_name);
+ return VERDICT_INVALID;
+ }
+
+ /* Validate metric name matches the Varlink service it was found on */
+ if (!metric_startswith_prefix(metric_name, li->name)) {
+ log_debug("Metric name '%s' does not match service name '%s', skipping.", metric_name, li->name);
+ return VERDICT_INVALID;
+ }
+
+ /* Check it against any specified matches */
+ bool matches;
+ if (strv_isempty(arg_matches))
+ matches = true;
+ else {
+ matches = false;
+
+ /* Allow exact matches or prefix matches */
+ STRV_FOREACH(i, arg_matches)
+ if (streq(metric_name, *i) ||
+ metric_startswith_prefix(metric_name, *i)) {
+ matches = true;
+ break;
+ }
+ }
+
+ if (!matches) {
+ log_debug("Metric '%s' does not match search, ignoring.", metric_name);
+ return VERDICT_MISMATCH;
+ }
+
+ return VERDICT_MATCH;
}
static int metrics_on_query_reply(
@@ -142,11 +230,11 @@ static int metrics_on_query_reply(
if (error_id) {
if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED))
- log_info("Varlink disconnected");
+ log_warning("Varlink connection to '%s' disconnected prematurely, ignoring.", li->name);
else if (streq(error_id, SD_VARLINK_ERROR_TIMEOUT))
- log_info("Varlink timed out");
+ log_warning("Varlink connection to '%s' timed out, ignoring.", li->name);
else
- log_error("Varlink error: %s", error_id);
+ log_warning("Varlink error from '%s', ignoring: %s", li->name, error_id);
goto finish;
}
@@ -156,10 +244,13 @@ static int metrics_on_query_reply(
goto finish;
}
- if (!metrics_validate_one(li, parameters)) {
+ Verdict v = metrics_verdict(li, parameters);
+ if (v == VERDICT_INVALID) {
context->n_invalid_metrics++;
goto finish;
}
+ if (v == VERDICT_MISMATCH)
+ goto finish;
/* Collect metrics for later sorting */
if (!GREEDY_REALLOC(context->metrics, context->n_metrics + 1))
@@ -178,7 +269,7 @@ finish:
return 0;
}
-static int metrics_call(Context *context, const char *path) {
+static int metrics_call(Context *context, const char *name, const char *path) {
_cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
int r;
@@ -201,9 +292,12 @@ static int metrics_call(Context *context, const char *path) {
if (r < 0)
return log_error_errno(r, "Failed to bind reply callback: %m");
- r = sd_varlink_observe(vl, "io.systemd.Metrics.List", /* parameters= */ NULL);
+ const char *method = context->action == ACTION_LIST ? "io.systemd.Metrics.List" : "io.systemd.Metrics.Describe";
+ r = sd_varlink_observe(vl,
+ method,
+ /* parameters= */ NULL);
if (r < 0)
- return log_error_errno(r, "Failed to issue io.systemd.Metrics.List call: %m");
+ return log_error_errno(r, "Failed to issue %s() call: %m", method);
_cleanup_(link_info_freep) LinkInfo *li = new(LinkInfo, 1);
if (!li)
@@ -212,95 +306,346 @@ static int metrics_call(Context *context, const char *path) {
*li = (LinkInfo) {
.context = context,
.link = sd_varlink_ref(vl),
+ .name = strdup(name),
};
- r = path_extract_filename(path, &li->metric_prefix);
- if (r < 0)
- return log_error_errno(r, "Failed to extract metric name from path %s: %m", path);
+ if (!li->name)
+ return log_oom();
- (void) sd_varlink_set_userdata(vl, li);
if (set_ensure_put(&context->link_infos, &link_info_hash_ops, li) < 0)
return log_oom();
+ (void) sd_varlink_set_userdata(vl, li);
+
TAKE_PTR(li);
return 0;
}
-static int metrics_output_sorted(Context *context) {
+static int metrics_output_list(Context *context, Table **ret) {
+ int r;
+
+ assert(context);
+
+ _cleanup_(table_unrefp) Table *table = table_new("family", "object", "fields", "value");
+ if (!table)
+ return log_oom();
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+ table_set_sort(table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3);
+
+ FOREACH_ARRAY(m, context->metrics, context->n_metrics) {
+ struct {
+ const char *name;
+ const char *object;
+ sd_json_variant *fields, *value;
+ } d = {};
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, name), SD_JSON_MANDATORY },
+ { "object", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, object), 0 },
+ { "fields", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_variant_noref, voffsetof(d, fields), 0 },
+ { "value", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_variant_noref, voffsetof(d, value), SD_JSON_MANDATORY },
+ {}
+ };
+
+ r = sd_json_dispatch(*m, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &d);
+ if (r < 0) {
+ _cleanup_free_ char *t = NULL;
+ int k = sd_json_variant_format(*m, /* flags= */ 0, &t);
+ if (k < 0)
+ return log_error_errno(k, "Failed to format JSON: %m");
+
+ log_warning_errno(r, "Cannot parse metric, skipping: %s", t);
+ continue;
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_STRING, d.name,
+ TABLE_STRING, d.object,
+ TABLE_JSON, d.fields,
+ TABLE_SET_WEIGHT, 50U,
+ TABLE_JSON, d.value,
+ TABLE_SET_WEIGHT, 50U);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ *ret = TAKE_PTR(table);
+ return 0;
+}
+
+static int metrics_output_describe(Context *context, Table **ret) {
+ int r;
+
+ assert(context);
+
+ _cleanup_(table_unrefp) Table *table = table_new("family", "type", "description");
+ if (!table)
+ return log_oom();
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+ table_set_sort(table, (size_t) 0, (size_t) 1, (size_t) 2);
+
+ FOREACH_ARRAY(m, context->metrics, context->n_metrics) {
+ struct {
+ const char *name;
+ const char *type;
+ const char *description;
+ } d = {};
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, name), SD_JSON_MANDATORY },
+ { "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, type), SD_JSON_MANDATORY },
+ { "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, description), 0 },
+ {}
+ };
+
+ r = sd_json_dispatch(*m, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &d);
+ if (r < 0) {
+ _cleanup_free_ char *t = NULL;
+ int k = sd_json_variant_format(*m, /* flags= */ 0, &t);
+ if (k < 0)
+ return log_error_errno(k, "Failed to format JSON: %m");
+
+ log_warning_errno(r, "Cannot parse metric description, skipping: %s", t);
+ continue;
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_STRING, d.name,
+ TABLE_STRING, d.type,
+ TABLE_STRING, d.description,
+ TABLE_SET_WEIGHT, 50U);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ *ret = TAKE_PTR(table);
+ return 0;
+}
+
+static int metrics_output(Context *context) {
int r;
assert(context);
typesafe_qsort(context->metrics, context->n_metrics, metric_compare);
- FOREACH_ARRAY(m, context->metrics, context->n_metrics) {
- r = sd_json_variant_dump(
- *m,
- arg_json_format_flags | SD_JSON_FORMAT_FLUSH,
- stdout,
- /* prefix= */ NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to write JSON: %m");
+ if (sd_json_format_enabled(arg_json_format_flags)) {
+ FOREACH_ARRAY(m, context->metrics, context->n_metrics) {
+ r = sd_json_variant_dump(
+ *m,
+ arg_json_format_flags | SD_JSON_FORMAT_FLUSH,
+ stdout,
+ /* prefix= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write JSON: %m");
+ }
+
+ if (context->n_metrics == 0 && arg_legend)
+ log_info("No metrics collected.");
+
+ return 0;
}
- if (context->n_metrics == 0)
- log_info("No metrics collected.");
+ _cleanup_(table_unrefp) Table *table = NULL;
+ switch(context->action) {
+
+ case ACTION_LIST:
+ r = metrics_output_list(context, &table);
+ break;
+
+ case ACTION_DESCRIBE:
+ r = metrics_output_describe(context, &table);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+ if (r < 0)
+ return r;
+
+ if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) {
+ r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) {
+ if (table_isempty(table))
+ printf("No metrics available.\n");
+ else
+ printf("\n%zu metrics listed.\n", table_get_rows(table) - 1);
+ }
return 0;
}
-static int metrics_query(void) {
+static int parse_metrics_matches(char **matches) {
int r;
- _cleanup_free_ char *metrics_path = NULL;
- r = runtime_directory_generic(arg_runtime_scope, "systemd/report", &metrics_path);
+ STRV_FOREACH(i, matches) {
+ r = metrics_name_valid(*i);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine if '%s' is a valid metric name: %m", *i);
+ if (!r && !varlink_idl_interface_name_is_valid(*i))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Match is not a valid family name or prefix: %s", *i);
+
+ if (strv_extend(&arg_matches, *i) < 0)
+ return log_oom();
+ }
+
+ strv_sort_uniq(arg_matches);
+ return 0;
+}
+
+static bool test_service_matches(const char *service) {
+ assert(service);
+
+ if (strv_isempty(arg_matches))
+ return true;
+
+ /* Only contact services whose name is either a prefix of any of the specified metrics families, or
+ * if the specified metric families are a prefix of the service.
+ *
+ * Example: if user specifies "foo" we want to match sockets "foo" and "foo.bar".
+ * if user specifies "foo.waldo" we want to match sockets "foo" and "foo.waldo" as well as "foo.waldo.quux".
+ *
+ * in other words: it should be fine to specify any prefix of a service name to get all metrics from all matching services.
+ * it should also be fine to specify a full metric name, and then go directly to the relevant services, and ask for matching metrics.
+ */
+
+ STRV_FOREACH(i, arg_matches) {
+ if (streq(service, *i))
+ return true;
+
+ if (metric_startswith_prefix(*i, service) ||
+ metric_startswith_prefix(service, *i))
+ return true;
+ }
+
+ return false;
+}
+
+static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
+ int r;
+
+ assert(ret_directory);
+ assert(ret);
+
+ _cleanup_free_ char *sources_path = NULL;
+ r = runtime_directory_generic(arg_runtime_scope, "systemd/report", &sources_path);
if (r < 0)
- return log_error_errno(r, "Failed to determine metrics directory path: %m");
+ return log_error_errno(r, "Failed to determine sources directory path: %m");
- log_debug("Looking for reports in %s/", metrics_path);
+ log_debug("Looking for metrics in '%s'.", sources_path);
- _cleanup_(context_done) Context context = {};
+ size_t m = 0;
- r = sd_event_default(&context.event);
- if (r < 0)
- return log_error_errno(r, "Failed to get event loop: %m");
+ _cleanup_free_ DirectoryEntries *de = NULL;
+ r = readdir_all_at(
+ AT_FDCWD,
+ sources_path,
+ RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE,
+ &de);
+ if (r == -ENOENT)
+ *ret = NULL;
+ else if (r < 0)
+ return log_error_errno(r, "Failed to enumerate '%s': %m", sources_path);
+ else {
+ /* Filter out non-sockets/non-symlinks and badly named entries */
+ FOREACH_ARRAY(i, de->entries, de->n_entries) {
+ struct dirent *d = *i;
- r = sd_event_set_signal_exit(context.event, true);
- if (r < 0)
- return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m");
-
- size_t n_skipped_sources = 0;
- _cleanup_closedir_ DIR *d = opendir(metrics_path);
- if (!d) {
- if (errno != ENOENT)
- return log_error_errno(errno, "Failed to open metrics directory %s: %m", metrics_path);
- } else
- FOREACH_DIRENT(de, d,
- return log_warning_errno(errno, "Failed to read %s: %m", metrics_path)) {
-
- if (!IN_SET(de->d_type, DT_SOCK, DT_UNKNOWN))
+ if (!IN_SET(d->d_type, DT_SOCK, DT_LNK))
continue;
+ if (!varlink_idl_interface_name_is_valid(d->d_name))
+ continue;
+
+ if (!test_service_matches(d->d_name))
+ continue;
+
+ de->entries[m++] = *i;
+ }
+
+ de->n_entries = m;
+ *ret = TAKE_PTR(de);
+ }
+
+ *ret_directory = TAKE_PTR(sources_path);
+ return m > 0;
+}
+
+static int verb_metrics(int argc, char *argv[], void *userdata) {
+ Action action;
+ int r;
+
+ assert(argc >= 1);
+ assert(argv);
+
+ /* Enable JSON-SEQ mode here, since we'll dump a large series of JSON objects */
+ arg_json_format_flags |= SD_JSON_FORMAT_SEQ;
+
+ if (streq_ptr(argv[0], "metrics"))
+ action = ACTION_LIST;
+ else {
+ assert(streq_ptr(argv[0], "describe-metrics"));
+ action = ACTION_DESCRIBE;
+ }
+
+ r = parse_metrics_matches(argv + 1);
+ if (r < 0)
+ return r;
+
+ _cleanup_(context_done) Context context = {
+ .action = action,
+ };
+ size_t n_skipped_sources = 0;
+
+ _cleanup_free_ DirectoryEntries *de = NULL;
+ _cleanup_free_ char *sources_path = NULL;
+ r = readdir_sources(&sources_path, &de);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ r = sd_event_default(&context.event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get event loop: %m");
+
+ r = sd_event_set_signal_exit(context.event, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m");
+
+ FOREACH_ARRAY(i, de->entries, de->n_entries) {
+ struct dirent *d = *i;
+
if (set_size(context.link_infos) >= METRICS_LINKS_MAX) {
n_skipped_sources++;
break;
}
- _cleanup_free_ char *p = path_join(metrics_path, de->d_name);
+ _cleanup_free_ char *p = path_join(sources_path, d->d_name);
if (!p)
return log_oom();
- (void) metrics_call(&context, p);
+ (void) metrics_call(&context, d->d_name, p);
}
+ }
+
+ if (set_isempty(context.link_infos)) {
+ if (arg_legend)
+ log_info("No metrics sources found.");
+ } else {
+ assert(context.event);
- if (set_isempty(context.link_infos))
- log_info("No metrics sources found.");
- else {
r = sd_event_loop(context.event);
if (r < 0)
return log_error_errno(r, "Failed to run event loop: %m");
- r = metrics_output_sorted(&context);
+ r = metrics_output(&context);
if (r < 0)
return r;
}
@@ -320,7 +665,62 @@ static int metrics_query(void) {
return 0;
}
-static int help(void) {
+static int verb_list_sources(int argc, char *argv[], void *userdata) {
+ int r;
+
+ _cleanup_(table_unrefp) Table *table = table_new("source", "address");
+ if (!table)
+ return log_oom();
+
+ _cleanup_free_ char *sources_path = NULL;
+ _cleanup_free_ DirectoryEntries *de = NULL;
+ r = readdir_sources(&sources_path, &de);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ FOREACH_ARRAY(i, de->entries, de->n_entries) {
+ struct dirent *d = *i;
+
+ _cleanup_free_ char *k = path_join(sources_path, d->d_name);
+ if (!k)
+ return log_oom();
+
+ _cleanup_free_ char *resolved = NULL;
+ r = chase(k, /* root= */ NULL, CHASE_MUST_BE_SOCKET, &resolved, /* ret_fd= */ NULL);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to resolve '%s', skipping: %m", k);
+ continue;
+ }
+
+ _cleanup_free_ char *j = strjoin("unix:", resolved);
+ if (!j)
+ return log_oom();
+
+ r = table_add_many(
+ table,
+ TABLE_STRING, d->d_name,
+ TABLE_STRING, j);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) {
+ r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) {
+ if (table_isempty(table))
+ printf("No sources available.\n");
+ else
+ printf("\n%zu sources listed.\n", table_get_rows(table) - 1);
+ }
+
+ return 0;
+}
+
+static int verb_help(int argc, char *argv[], void *userdata) {
_cleanup_free_ char *link = NULL;
int r;
@@ -328,19 +728,31 @@ static int help(void) {
if (r < 0)
return log_oom();
- printf("%s [OPTIONS...] \n\n"
- "%sPrint metrics for all system components.%s\n\n"
+ printf("%1$s [OPTIONS...] COMMAND ...\n"
+ "\n%5$sAcquire metrics from local sources.%6$s\n"
+ "\n%3$sCommands:%4$s\n"
+ " metrics [MATCH...] Acquire list of metrics and their values\n"
+ " describe-metrics [MATCH...]\n"
+ " Describe available metrics\n"
+ " list-sources Show list of known metrics sources\n"
+ "\n%3$sOptions:%4$s\n"
" -h --help Show this help\n"
" --version Show package version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
" --user Connect to user service manager\n"
" --system Connect to system service manager (default)\n"
" --json=pretty|short\n"
" Configure JSON output\n"
- "\nSee the %s for details.\n",
+ " -j Equivalent to --json=pretty (on TTY) or --json=short\n"
+ " (otherwise)\n"
+ "\nSee the %2$s for details.\n",
program_invocation_short_name,
- ansi_highlight(),
+ link,
+ ansi_underline(),
ansi_normal(),
- link);
+ ansi_highlight(),
+ ansi_normal());
return 0;
}
@@ -348,17 +760,21 @@ static int help(void) {
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
ARG_USER,
ARG_SYSTEM,
ARG_JSON,
};
static const struct option options[] = {
- { "help", no_argument, NULL, 'h' },
- { "version", no_argument, NULL, ARG_VERSION },
- { "user", no_argument, NULL, ARG_USER },
- { "system", no_argument, NULL, ARG_SYSTEM },
- { "json", required_argument, NULL, ARG_JSON },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "user", no_argument, NULL, ARG_USER },
+ { "system", no_argument, NULL, ARG_SYSTEM },
+ { "json", required_argument, NULL, ARG_JSON },
{}
};
@@ -367,14 +783,22 @@ static int parse_argv(int argc, char *argv[]) {
assert(argc >= 0);
assert(argv);
- while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ while ((c = getopt_long(argc, argv, "hj", options, NULL)) >= 0)
switch (c) {
case 'h':
- return help();
+ return verb_help(/* argc= */ 0, /* argv= */ NULL, /* userdata= */ NULL);
case ARG_VERSION:
return version();
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
case ARG_USER:
arg_runtime_scope = RUNTIME_SCOPE_USER;
break;
@@ -390,6 +814,10 @@ static int parse_argv(int argc, char *argv[]) {
break;
+ case 'j':
+ arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
+ break;
+
case '?':
return -EINVAL;
@@ -397,15 +825,22 @@ static int parse_argv(int argc, char *argv[]) {
assert_not_reached();
}
- if (optind < argc)
- return log_error_errno(
- SYNTHETIC_ERRNO(EINVAL),
- "%s takes no arguments.",
- program_invocation_short_name);
-
return 1;
}
+static int report_main(int argc, char *argv[]) {
+
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, 1, 0, verb_help },
+ { "metrics", VERB_ANY, VERB_ANY, 0, verb_metrics },
+ { "describe-metrics", VERB_ANY, VERB_ANY, 0, verb_metrics },
+ { "list-sources", VERB_ANY, 1, 0, verb_list_sources },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
static int run(int argc, char *argv[]) {
int r;
@@ -415,7 +850,7 @@ static int run(int argc, char *argv[]) {
if (r <= 0)
return r;
- return metrics_query();
+ return report_main(argc, argv);
}
DEFINE_MAIN_FUNCTION(run);
diff --git a/src/shared/format-table.c b/src/shared/format-table.c
index 9f6c46afb7..8b5b88370f 100644
--- a/src/shared/format-table.c
+++ b/src/shared/format-table.c
@@ -14,6 +14,7 @@
#include "glyph-util.h"
#include "gunicode.h"
#include "in-addr-util.h"
+#include "json-util.h"
#include "memory-util.h"
#include "memstream-util.h"
#include "pager.h"
@@ -110,6 +111,7 @@ typedef struct TableData {
pid_t pid;
mode_t mode;
dev_t devnum;
+ sd_json_variant *json;
/* … add more here as we start supporting more cell data types … */
};
} TableData;
@@ -249,6 +251,9 @@ static TableData *table_data_free(TableData *d) {
if (IN_SET(d->type, TABLE_STRV, TABLE_STRV_WRAPPED))
strv_free(d->strv);
+ if (d->type == TABLE_JSON)
+ sd_json_variant_unref(d->json);
+
return mfree(d);
}
@@ -363,6 +368,9 @@ static size_t table_data_size(TableDataType type, const void *data) {
case TABLE_DEVNUM:
return sizeof(dev_t);
+ case TABLE_JSON:
+ return sizeof(sd_json_variant*);
+
default:
assert_not_reached();
}
@@ -445,12 +453,22 @@ static TableData *table_data_new(
d->ellipsize_percent = ellipsize_percent;
d->uppercase = uppercase;
- if (IN_SET(type, TABLE_STRV, TABLE_STRV_WRAPPED)) {
+ switch (type) {
+
+ case TABLE_STRV:
+ case TABLE_STRV_WRAPPED:
d->strv = strv_copy(data);
if (!d->strv)
return NULL;
- } else
+ break;
+
+ case TABLE_JSON:
+ d->json = sd_json_variant_ref((sd_json_variant*) data);
+ break;
+
+ default:
memcpy_safe(d->data, data, data_size);
+ }
return TAKE_PTR(d);
}
@@ -1096,6 +1114,10 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) {
data = &buffer.devnum;
break;
+ case TABLE_JSON:
+ data = va_arg(ap, sd_json_variant*);
+ break;
+
case TABLE_SET_MINIMUM_WIDTH: {
size_t w = va_arg(ap, size_t);
@@ -1506,6 +1528,9 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t
return CMP(minor(a->devnum), minor(b->devnum));
+ case TABLE_JSON:
+ return json_variant_compare(a->json, b->json);
+
default:
;
}
@@ -2071,6 +2096,18 @@ static const char *table_data_format(
break;
+ case TABLE_JSON: {
+ if (!d->json)
+ return table_ersatz_string(t);
+
+ char *p;
+ if (sd_json_variant_format(d->json, /* flags= */ 0, &p) < 0)
+ return NULL;
+
+ d->formatted = p;
+ break;
+ }
+
default:
assert_not_reached();
}
@@ -2284,6 +2321,9 @@ static bool table_data_isempty(const TableData *d) {
if (IN_SET(d->type, TABLE_STRV, TABLE_STRV_WRAPPED))
return strv_isempty(d->strv);
+ if (d->type == TABLE_JSON)
+ return sd_json_variant_is_null(d->json);
+
/* Note that an empty string we do not consider empty here! */
return false;
}
@@ -2991,6 +3031,15 @@ static int table_data_to_json(TableData *d, sd_json_variant **ret) {
SD_JSON_BUILD_UNSIGNED(major(d->devnum)),
SD_JSON_BUILD_UNSIGNED(minor(d->devnum))));
+ case TABLE_JSON:
+ if (!d->json)
+ return sd_json_variant_new_null(ret);
+
+ if (ret)
+ *ret = sd_json_variant_ref(d->json);
+
+ return 0;
+
case TABLE_STRING_WITH_ANSI: {
_cleanup_free_ char *s = strdup(d->string);
if (!s)
diff --git a/src/shared/format-table.h b/src/shared/format-table.h
index a6a06a009a..ec9989c54c 100644
--- a/src/shared/format-table.h
+++ b/src/shared/format-table.h
@@ -58,6 +58,7 @@ typedef enum TableDataType {
TABLE_MODE, /* as in UNIX file mode (mode_t), in typical octal output */
TABLE_MODE_INODE_TYPE, /* also mode_t, but displays only the inode type as string */
TABLE_DEVNUM, /* a dev_t, displayed in the usual major:minor way */
+ TABLE_JSON, /* an sd_json_variant, for output formatted into text */
_TABLE_DATA_TYPE_MAX,
/* The following are not really data types, but commands for table_add_cell_many() to make changes to
diff --git a/src/shared/varlink-io.systemd.Metrics.c b/src/shared/varlink-io.systemd.Metrics.c
index e9d463e9fd..f4623f1f15 100644
--- a/src/shared/varlink-io.systemd.Metrics.c
+++ b/src/shared/varlink-io.systemd.Metrics.c
@@ -18,15 +18,15 @@ static SD_VARLINK_DEFINE_ERROR(NoSuchMetric);
static SD_VARLINK_DEFINE_METHOD_FULL(
List,
SD_VARLINK_REQUIRES_MORE,
- SD_VARLINK_FIELD_COMMENT("Metric name, e.g. io.systemd.Manager.unitsByTypeTotal or io.systemd.Manager.unitActiveState"),
+ SD_VARLINK_FIELD_COMMENT("Metric family name, e.g. io.systemd.Manager.unitsByTypeTotal or io.systemd.Manager.unitActiveState"),
SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0),
/* metric value has various types depending on MetricFamilyType and actual data double/int/uint */
- SD_VARLINK_FIELD_COMMENT("Metric value"),
- SD_VARLINK_DEFINE_OUTPUT(value, SD_VARLINK_ANY, 0),
SD_VARLINK_FIELD_COMMENT("Metric object name can be unit name, process name, etc, e.g. dev-hvc0.device"),
SD_VARLINK_DEFINE_OUTPUT(object, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Metric fields are values to differentiate between different metrics in the same metric family"),
- SD_VARLINK_DEFINE_OUTPUT(fields, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE));
+ SD_VARLINK_DEFINE_OUTPUT(fields, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Metric value"),
+ SD_VARLINK_DEFINE_OUTPUT(value, SD_VARLINK_ANY, 0));
static SD_VARLINK_DEFINE_METHOD_FULL(
Describe,
diff --git a/src/test/test-json.c b/src/test/test-json.c
index 1f0e07d7d4..679152fd95 100644
--- a/src/test/test-json.c
+++ b/src/test/test-json.c
@@ -1523,4 +1523,69 @@ TEST(access_mode) {
&mm), ERANGE);
}
+static void test_json_variant_compare_one(const char *a, const char *b, int expected) {
+ int r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *aa = NULL;
+ if (!isempty(a))
+ ASSERT_OK(sd_json_parse(a, /* flags= */ 0, &aa, /* reterr_line= */ NULL, /* reterr_column= */ NULL));
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *bb = NULL;
+ if (!isempty(b))
+ ASSERT_OK(sd_json_parse(b, /* flags= */ 0, &bb, /* reterr_line= */ NULL, /* reterr_column= */ NULL));
+
+ r = json_variant_compare(aa, bb);
+
+ log_debug("%s vs %s → %i (expected %i)", a, b, r, expected);
+
+ if (expected < 0)
+ ASSERT_LT(r, 0);
+ else if (expected > 0)
+ ASSERT_GT(r, 0);
+ else
+ ASSERT_EQ(r, 0);
+
+ r = json_variant_compare(bb, aa);
+
+ if (expected < 0)
+ ASSERT_GT(r, 0);
+ else if (expected > 0)
+ ASSERT_LT(r, 0);
+ else
+ ASSERT_EQ(r, 0);
+}
+
+TEST(json_variant_compare) {
+ test_json_variant_compare_one("null", "\"a\"", -1);
+ test_json_variant_compare_one(NULL, "\"a\"", -1);
+ test_json_variant_compare_one("0", "1", -1);
+ test_json_variant_compare_one("1", "0", 1);
+ test_json_variant_compare_one("0", "0", 0);
+ test_json_variant_compare_one("1", "1", 0);
+ test_json_variant_compare_one("1", "null", 1);
+ test_json_variant_compare_one("null", "1", -1);
+ test_json_variant_compare_one("null", "null", 0);
+ test_json_variant_compare_one("false", "true", -1);
+ test_json_variant_compare_one("true", "false", 1);
+ test_json_variant_compare_one("true", "true", 0);
+ test_json_variant_compare_one("false", "false", 0);
+ test_json_variant_compare_one("\"a\"", "\"b\"", -1);
+ test_json_variant_compare_one("\"b\"", "\"a\"", 1);
+ test_json_variant_compare_one("18446744073709551615", "0", 1);
+ test_json_variant_compare_one("0", "18446744073709551615", -1);
+ test_json_variant_compare_one("18446744073709551615", "18446744073709551615", 0);
+ test_json_variant_compare_one("-9223372036854775808", "18446744073709551615", -1);
+ test_json_variant_compare_one("18446744073709551615", "-9223372036854775808", 1);
+ test_json_variant_compare_one("1.1", "3.4", -1);
+ test_json_variant_compare_one("1", "3.4", -1);
+ test_json_variant_compare_one("[1,2]", "[1,2]", 0);
+ test_json_variant_compare_one("[1,2]", "[2,1]", -1);
+ test_json_variant_compare_one("[1,2]", "[1,2,3]", -1);
+ test_json_variant_compare_one("{}", "{\"a\":\"b\"}", -1);
+ test_json_variant_compare_one("{\"a\":\"b\"}", "{\"a\":\"b\"}", 0);
+ test_json_variant_compare_one("{\"a\":\"b\"}", "{\"b\":\"c\"}", 1);
+ test_json_variant_compare_one("{\"a\":\"b\",\"b\":\"c\"}", "{\"b\":\"c\",\"a\":\"b\"}", 0);
+ test_json_variant_compare_one("{\"a\":\"b\",\"b\":\"c\"}", "{\"a\":\"b\"}", 1);
+}
+
DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh
new file mode 100755
index 0000000000..876c5b2cad
--- /dev/null
+++ b/test/units/TEST-74-AUX-UTILS.report.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+# Unset $PAGER so we don't have to use --no-pager everywhere
+export PAGER=
+
+REPORT=/usr/lib/systemd/systemd-report
+
+"$REPORT" --help
+"$REPORT" help
+"$REPORT" --version
+"$REPORT" --json=help
+
+"$REPORT" metrics
+"$REPORT" metrics -j
+"$REPORT" metrics --no-legend
+"$REPORT" describe-metrics
+"$REPORT" describe-metrics -j
+"$REPORT" describe-metrics --no-legend
+"$REPORT" list-sources
+"$REPORT" list-sources -j
+"$REPORT" list-sources --no-legend
+
+"$REPORT" metrics io
+"$REPORT" metrics io.systemd piff
+"$REPORT" metrics piff
+"$REPORT" describe-metrics io
+"$REPORT" describe-metrics io.systemd piff
+"$REPORT" describe-metrics piff
+
+# test io.systemd.Network Metrics
+varlinkctl info /run/systemd/report/io.systemd.Network
+varlinkctl list-methods /run/systemd/report/io.systemd.Network
+varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.List {}
+varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.Describe {}
diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh
index 377c4734e0..4308092240 100755
--- a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh
+++ b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh
@@ -242,12 +242,3 @@ systemd-run --wait --pipe --user --machine testuser@ \
# test io.systemd.Unit in user manager
systemd-run --wait --pipe --user --machine testuser@ \
varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}'
-
-# test report
-systemd-report
-
-# test io.systemd.Network Metrics
-varlinkctl info /run/systemd/report/io.systemd.Network
-varlinkctl list-methods /run/systemd/report/io.systemd.Network
-varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.List {}
-varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.Describe {}