diff --git a/meson.build b/meson.build index e091631ca82da..868ecbf720e83 100644 --- a/meson.build +++ b/meson.build @@ -2397,6 +2397,7 @@ subdir('src/quotacheck') subdir('src/random-seed') subdir('src/remount-fs') subdir('src/repart') +subdir('src/report') subdir('src/reply-password') subdir('src/resolve') subdir('src/rfkill') diff --git a/src/core/manager.h b/src/core/manager.h index c764f411086ea..2df606005dbb1 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -469,6 +469,8 @@ typedef struct Manager { * systemd-oomd to report changes in ManagedOOM settings (systemd client - oomd server). */ sd_varlink *managed_oom_varlink; + sd_varlink_server *metrics_varlink_server; + /* Reference to RestrictFileSystems= BPF program */ struct restrict_fs_bpf *restrict_fs; diff --git a/src/core/meson.build b/src/core/meson.build index 4f20cae2ee303..e703cc3728970 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -69,6 +69,7 @@ libcore_sources = files( 'varlink-dynamic-user.c', 'varlink-execute.c', 'varlink-manager.c', + 'varlink-metrics.c', 'varlink-unit.c', ) diff --git a/src/core/varlink-metrics.c b/src/core/varlink-metrics.c new file mode 100644 index 0000000000000..c64d850367b1f --- /dev/null +++ b/src/core/varlink-metrics.c @@ -0,0 +1,173 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "hashmap.h" +#include "manager.h" +#include "metrics.h" +#include "service.h" +#include "unit-def.h" +#include "unit.h" +#include "varlink-metrics.h" + +static int unit_active_state_build_json(MetricFamilyContext *context, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + Unit *unit; + char *key; + int r; + + assert(context); + + HASHMAP_FOREACH_KEY(unit, key, manager->units) { + /* ignore aliases */ + if (key != unit->id) + continue; + + r = metric_build_send_string( + context, + unit->id, + unit_active_state_to_string(unit_active_state(unit)), + /* field_pairs= */ NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int unit_load_state_build_json(MetricFamilyContext *context, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + Unit *unit; + char *key; + int r; + + assert(context); + + HASHMAP_FOREACH_KEY(unit, key, manager->units) { + /* ignore aliases */ + if (key != unit->id) + continue; + + r = metric_build_send_string( + context, + unit->id, + unit_load_state_to_string(unit->load_state), + /* field_pairs= */ NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int nrestarts_build_json(MetricFamilyContext *context, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + int r; + + assert(context); + + LIST_FOREACH(units_by_type, unit, manager->units_by_type[UNIT_SERVICE]) { + r = metric_build_send_unsigned( + context, unit->id, SERVICE(unit)->n_restarts, /* field_pairs= */ NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int units_by_type_total_build_json(MetricFamilyContext *context, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + int r; + + assert(context); + + for (UnitType type = 0; type < _UNIT_TYPE_MAX; type++) { + uint64_t counter = 0; + + LIST_FOREACH(units_by_type, _u, manager->units_by_type[type]) + counter++; + + r = metric_build_send_unsigned( + context, + /* object= */ NULL, + counter, + STRV_MAKE("type", unit_type_to_string(type))); + if (r < 0) + return r; + } + + return 0; +} + +static int units_by_state_total_build_json(MetricFamilyContext *context, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + UnitActiveState counters[_UNIT_ACTIVE_STATE_MAX] = {}; + Unit *unit; + char *key; + int r; + + assert(context); + + /* TODO need a rework probably with state counter */ + HASHMAP_FOREACH_KEY(unit, key, manager->units) { + /* ignore aliases */ + if (key != unit->id) + continue; + + counters[unit_active_state(unit)]++; + } + + for (UnitActiveState state = 0; state < _UNIT_ACTIVE_STATE_MAX; state++) { + r = metric_build_send_unsigned( + context, + /* object= */ NULL, + counters[state], + STRV_MAKE("state", unit_active_state_to_string(state))); + if (r < 0) + return r; + } + + return 0; +} + +const MetricFamily metric_family_table[] = { + // Keep metrics ordered alphabetically + { + .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "nrestarts", + .description = "Per unit metric: number of restarts", + .type = METRIC_FAMILY_TYPE_COUNTER, + .generate_cb = nrestarts_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "unit_active_state", + .description = "Per unit metric: active state", + .type = METRIC_FAMILY_TYPE_STRING, + .generate_cb = unit_active_state_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "unit_load_state", + .description = "Per unit metric: load state", + .type = METRIC_FAMILY_TYPE_STRING, + .generate_cb = unit_load_state_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "units_by_state_total", + .description = "Total number of units of different state", + .type = METRIC_FAMILY_TYPE_GAUGE, + .generate_cb = units_by_state_total_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "units_by_type_total", + .description = "Total number of units of different types", + .type = METRIC_FAMILY_TYPE_GAUGE, + .generate_cb = units_by_type_total_build_json, + }, + {} +}; + +int vl_method_describe(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return metrics_method_describe(metric_family_table, link, parameters, flags, userdata); +} + +int vl_method_list(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return metrics_method_list(metric_family_table, link, parameters, flags, userdata); +} diff --git a/src/core/varlink-metrics.h b/src/core/varlink-metrics.h new file mode 100644 index 0000000000000..92e7d81cf860d --- /dev/null +++ b/src/core/varlink-metrics.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink.h" +#include "sd-json.h" + +#define METRIC_IO_SYSTEMD_MANAGER_PREFIX "io.systemd.Manager." + +int vl_method_list(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); +int vl_method_describe(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); diff --git a/src/core/varlink.c b/src/core/varlink.c index 605673ef1977d..b5b00cdf74044 100644 --- a/src/core/varlink.c +++ b/src/core/varlink.c @@ -5,6 +5,7 @@ #include "constants.h" #include "errno-util.h" #include "manager.h" +#include "metrics.h" #include "path-util.h" #include "pidref.h" #include "string-util.h" @@ -18,6 +19,7 @@ #include "varlink-io.systemd.UserDatabase.h" #include "varlink-io.systemd.service.h" #include "varlink-manager.h" +#include "varlink-metrics.h" #include "varlink-serialize.h" #include "varlink-unit.h" #include "varlink-util.h" @@ -423,8 +425,26 @@ int manager_setup_varlink_server(Manager *m) { return 1; } +static int manager_setup_varlink_metrics_server(Manager *m) { + sd_varlink_server_flags_t flags = SD_VARLINK_SERVER_INHERIT_USERDATA; + int r; + + assert(m); + + if (MANAGER_IS_SYSTEM(m)) + flags |= SD_VARLINK_SERVER_ACCOUNT_UID; + + r = metrics_setup_varlink_server( + &m->metrics_varlink_server, flags, m->event, vl_method_list, vl_method_describe, m); + if (r < 0) + return r; + + return 0; +} + static int manager_varlink_init_system(Manager *m) { int r; + _cleanup_free_ char *metrics_address = NULL; assert(m); @@ -433,16 +453,29 @@ static int manager_varlink_init_system(Manager *m) { return log_error_errno(r, "Failed to set up varlink server: %m"); bool fresh = r > 0; + r = manager_setup_varlink_metrics_server(m); + if (r < 0) + return log_error_errno(r, "Failed to set up metrics varlink server: %m"); + bool metrics_fresh = r > 0; + + r = runtime_directory_generic(m->runtime_scope, "systemd/report/io.systemd.Manager", &metrics_address); + if (r < 0) + return r; + if (!MANAGER_IS_TEST_RUN(m)) { FOREACH_STRING(address, "/run/systemd/userdb/io.systemd.DynamicUser", VARLINK_PATH_MANAGED_OOM_SYSTEM, - "/run/systemd/io.systemd.Manager") { + "/run/systemd/io.systemd.Manager", + metrics_address) { + + sd_varlink_server *server = streq(address, metrics_address) ? m->metrics_varlink_server : m->varlink_server; + fresh = streq(address, metrics_address) ? metrics_fresh : fresh; /* We might have got sockets through deserialization. Do not bind to them twice. */ - if (!fresh && varlink_server_contains_socket(m->varlink_server, address)) + if (!fresh && varlink_server_contains_socket(server, address)) continue; - r = sd_varlink_server_listen_address(m->varlink_server, address, 0666 | SD_VARLINK_SERVER_MODE_MKDIR_0755); + r = sd_varlink_server_listen_address(server, address, 0666 | SD_VARLINK_SERVER_MODE_MKDIR_0755); if (r < 0) return log_error_errno(r, "Failed to bind to varlink socket '%s': %m", address); } @@ -479,6 +512,10 @@ static int manager_varlink_init_user(Manager *m) { return log_error_errno(r, "Failed to bind to varlink socket '%s': %m", address); } + r = manager_setup_varlink_metrics_server(m); + if (r < 0) + return log_error_errno(r, "Failed to set up metrics varlink server: %m"); + return manager_varlink_managed_oom_connect(m); } @@ -497,6 +534,7 @@ void manager_varlink_done(Manager *m) { m->varlink_server = sd_varlink_server_unref(m->varlink_server); m->managed_oom_varlink = sd_varlink_close_unref(m->managed_oom_varlink); + m->metrics_varlink_server = sd_varlink_server_unref(m->metrics_varlink_server); } void manager_varlink_send_pending_reload_message(Manager *m) { diff --git a/src/report/meson.build b/src/report/meson.build new file mode 100644 index 0000000000000..ab1ac45acce86 --- /dev/null +++ b/src/report/meson.build @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + executable_template + { + 'name' : 'systemd-report', + 'public' : true, + 'sources' : files('report.c'), + }, +] diff --git a/src/report/report.c b/src/report/report.c new file mode 100644 index 0000000000000..2ae331299c265 --- /dev/null +++ b/src/report/report.c @@ -0,0 +1,345 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-event.h" +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "ansi-color.h" +#include "build.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "log.h" +#include "main-func.h" +#include "path-lookup.h" +#include "pretty-print.h" +#include "runtime-scope.h" +#include "sort-util.h" +#include "string-util.h" +#include "time-util.h" + +#define MAX_CONCURRENT_METRICS_SOCKETS 20 +#define TIMEOUT_USEC (30 * USEC_PER_SEC) /* 30 seconds */ + +static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; +static bool arg_sort = false; + +typedef struct Context { + unsigned n_open_connections; + sd_json_variant **metrics; /* Collected metrics for sorting */ + size_t n_metrics; +} Context; + +static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b) { + const char *name_a, *name_b, *object_a, *object_b; + sd_json_variant *fields_a, *fields_b; + _cleanup_free_ char *fields_str_a = NULL, *fields_str_b = NULL; + int r; + + assert(a && *a); + assert(b && *b); + + name_a = sd_json_variant_string(sd_json_variant_by_key(*a, "name")); + name_b = sd_json_variant_string(sd_json_variant_by_key(*b, "name")); + r = strcmp_ptr(name_a, name_b); + if (r != 0) + return r; + + object_a = sd_json_variant_string(sd_json_variant_by_key(*a, "object")); + object_b = sd_json_variant_string(sd_json_variant_by_key(*b, "object")); + r = strcmp_ptr(object_a, object_b); + if (r != 0) + return r; + + fields_a = sd_json_variant_by_key(*a, "fields"); + fields_b = sd_json_variant_by_key(*b, "fields"); + if (fields_a) + (void) sd_json_variant_format(fields_a, 0, &fields_str_a); + if (fields_b) + (void) sd_json_variant_format(fields_b, 0, &fields_str_b); + + return strcmp_ptr(fields_str_a, fields_str_b); +} + +static int metrics_on_query_reply( + sd_varlink *link, + sd_json_variant *parameters, + const char *error_id, + sd_varlink_reply_flags_t flags, + void *userdata) { + + assert(link); + + Context *context = ASSERT_PTR(userdata); + + if (error_id) { + if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED)) + log_info("Varlink disconnected"); + else if (streq(error_id, SD_VARLINK_ERROR_TIMEOUT)) + log_info("Varlink timed out"); + else + log_error("Varlink error: %s", error_id); + + goto finish; + } + + if (arg_sort) { + /* Collect metrics for later sorting */ + if (!GREEDY_REALLOC(context->metrics, context->n_metrics + 1)) + return log_oom(); + context->metrics[context->n_metrics++] = sd_json_variant_ref(parameters); + } else { + /* Output immediately */ + sd_json_variant_dump( + parameters, + SD_JSON_FORMAT_PRETTY_AUTO | SD_JSON_FORMAT_COLOR_AUTO | SD_JSON_FORMAT_FLUSH, + stdout, + NULL); + } + +finish: + if (!FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES)) { + assert(context->n_open_connections > 0); + context->n_open_connections--; + + if (context->n_open_connections == 0) + (void) sd_event_exit(ASSERT_PTR(sd_varlink_get_event(link)), EXIT_SUCCESS); + } + + return 0; +} + +static int metrics_call(const char *path, sd_event *event, sd_varlink **ret, Context *context) { + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + int r; + + assert(path); + assert(event); + assert(ret); + assert(context); + + r = sd_varlink_connect_address(&vl, path); + if (r < 0) + return log_debug_errno(r, "Unable to connect to %s: %m", path); + + (void) sd_varlink_set_userdata(vl, context); + + r = sd_varlink_set_relative_timeout(vl, TIMEOUT_USEC); + if (r < 0) + return log_error_errno(r, "Failed to set varlink timeout: %m"); + + r = sd_varlink_attach_event(vl, event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_debug_errno(r, "Failed to attach varlink connection to event loop: %m"); + + r = sd_varlink_bind_reply(vl, metrics_on_query_reply); + if (r < 0) + return log_debug_errno(r, "Failed to bind reply callback: %m"); + + r = sd_varlink_observe(vl, "io.systemd.Metrics.List", /* parameters= */ NULL); + if (r < 0) + return log_debug_errno(r, "Failed to issue io.systemd.Metrics.List call: %m"); + + *ret = TAKE_PTR(vl); + + return 0; +} + +static void sd_varlink_unref_many(sd_varlink **array, size_t n) { + assert(array); + + FOREACH_ARRAY(v, array, n) + sd_varlink_unref(*v); + + free(array); +} + +static void context_done(Context *context) { + assert(context); + + for (size_t i = 0; i < context->n_metrics; i++) + sd_json_variant_unref(context->metrics[i]); + free(context->metrics); +} + +static void metrics_output_sorted(Context *context) { + assert(context); + + if (context->n_metrics == 0) + return; + + typesafe_qsort(context->metrics, context->n_metrics, metric_compare); + + FOREACH_ARRAY(m, context->metrics, context->n_metrics) + sd_json_variant_dump( + *m, + SD_JSON_FORMAT_PRETTY_AUTO | SD_JSON_FORMAT_COLOR_AUTO | SD_JSON_FORMAT_FLUSH, + stdout, + NULL); +} + +static int metrics_query(void) { + _cleanup_closedir_ DIR *d = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_free_ char *metrics_path = NULL; + int r; + + r = runtime_directory_generic(arg_runtime_scope, "systemd/report", &metrics_path); + if (r < 0) + return log_error_errno(r, "Failed to determine metrics directory path: %m"); + + d = opendir(metrics_path); + if (!d) { + if (errno == ENOENT) + return 0; + + return log_error_errno(errno, "Failed to open directory %s: %m", metrics_path); + } + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to get event loop: %m"); + + r = sd_event_set_signal_exit(event, true); + if (r < 0) + return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m"); + + size_t n_varlinks = MAX_CONCURRENT_METRICS_SOCKETS; + sd_varlink **varlinks = new0(sd_varlink *, n_varlinks); + if (!varlinks) + return log_error_errno(ENOMEM, "Failed to allocate varlinks array: %m"); + + CLEANUP_ARRAY(varlinks, n_varlinks, sd_varlink_unref_many); + + Context context = {}; + + FOREACH_DIRENT(de, d, return -errno) { + _cleanup_free_ char *p = NULL; + + if (!IN_SET(de->d_type, DT_SOCK, DT_UNKNOWN)) + continue; + + p = path_join(metrics_path, de->d_name); + if (!p) + return log_oom(); + + r = metrics_call(p, event, &varlinks[context.n_open_connections], &context); + if (r < 0) { + log_error_errno(r, "Failed to connect to %s: %m", p); + continue; + } + + if (++context.n_open_connections >= MAX_CONCURRENT_METRICS_SOCKETS) { + log_warning("Too many concurrent metrics sockets, stop iterating"); + break; + } + } + + r = sd_event_loop(event); + if (r < 0) { + context_done(&context); + return log_error_errno(r, "Failed to run event loop: %m"); + } + + if (arg_sort) + metrics_output_sorted(&context); + + context_done(&context); + + return 0; +} + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-report", "1", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] \n\n" + "%sPrint metrics for all system components.%s\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --user Connect to user service manager\n" + " --system Connect to system service manager (default)\n" + " --sort Sort metrics output\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_USER, + ARG_SYSTEM, + ARG_SORT, + }; + + 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 }, + { "sort", no_argument, NULL, ARG_SORT }, + {} + }; + + int c; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hp", options, NULL)) >= 0) + switch (c) { + case 'h': + return help(); + case ARG_VERSION: + return version(); + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + case ARG_SORT: + arg_sort = true; + break; + case '?': + return -EINVAL; + default: + 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 run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + r = metrics_query(); + if (r < 0) + return r; + + return 0; +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/shared/meson.build b/src/shared/meson.build index 9fa976925a6e9..cc953b9e3adec 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -126,6 +126,7 @@ shared_sources = files( 'machine-id-setup.c', 'macvlan-util.c', 'main-func.c', + 'metrics.c', 'mkdir-label.c', 'mkfs-util.c', 'module-util.c', @@ -206,6 +207,7 @@ shared_sources = files( 'varlink-io.systemd.MachineImage.c', 'varlink-io.systemd.ManagedOOM.c', 'varlink-io.systemd.Manager.c', + 'varlink-io.systemd.Metrics.c', 'varlink-io.systemd.MountFileSystem.c', 'varlink-io.systemd.MuteConsole.c', 'varlink-io.systemd.NamespaceResource.c', diff --git a/src/shared/metrics.c b/src/shared/metrics.c new file mode 100644 index 0000000000000..72ba400e2672f --- /dev/null +++ b/src/shared/metrics.c @@ -0,0 +1,271 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "json-util.h" +#include "log.h" +#include "metrics.h" +#include "string-table.h" +#include "strv.h" +#include "varlink-io.systemd.Metrics.h" +#include "varlink-serialize.h" +#include "varlink-util.h" + +static void metric_family_context_done(MetricFamilyContext *ctx) { + assert(ctx); + + sd_json_variant_unref(ctx->previous); +} + +int metrics_setup_varlink_server( + sd_varlink_server **server, /* in and out param */ + sd_varlink_server_flags_t flags, + sd_event *event, + sd_varlink_method_t vl_method_list_cb, + sd_varlink_method_t vl_method_describe_cb, + void *userdata) { + + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL; + int r; + + assert(server); + assert(event); + + if (*server) + return 0; + + r = varlink_server_new(&s, flags, userdata); + if (r < 0) + return log_debug_errno(r, "Failed to allocate varlink metrics server object: %m"); + + r = sd_varlink_server_add_interface(s, &vl_interface_io_systemd_Metrics); + if (r < 0) + return log_debug_errno(r, "Failed to add varlink metrics interface to varlink server: %m"); + + r = sd_varlink_server_bind_method_many( + s, + "io.systemd.Metrics.List", + vl_method_list_cb, + "io.systemd.Metrics.Describe", + vl_method_describe_cb); + if (r < 0) + return log_debug_errno(r, "Failed to register varlink metrics methods: %m"); + + r = sd_varlink_server_set_description(s, "systemd varlink metrics server"); + if (r < 0) + return log_debug_errno(r, "Failed to set varlink metrics server description: %m"); + + r = sd_varlink_server_attach_event(s, event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_debug_errno(r, "Failed to attach varlink metrics connection to event loop: %m"); + + *server = TAKE_PTR(s); + + return 0; +} + +static const char * const metric_family_type_table[_METRIC_FAMILY_TYPE_MAX] = { + [METRIC_FAMILY_TYPE_COUNTER] = "counter", + [METRIC_FAMILY_TYPE_GAUGE] = "gauge", + [METRIC_FAMILY_TYPE_STRING] = "string", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(metric_family_type, MetricFamilyType); + +static int metric_family_build_send(sd_varlink *link, const MetricFamily *mf, bool more) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + assert(link); + assert(mf); + + r = sd_json_buildo( + &v, + SD_JSON_BUILD_PAIR_STRING("name", mf->name), + SD_JSON_BUILD_PAIR_STRING("description", mf->description), + SD_JSON_BUILD_PAIR_STRING("type", metric_family_type_to_string(mf->type))); + if (r < 0) + return r; + + if (more) + return sd_varlink_notify(link, v); + + return sd_varlink_reply(link, v); +} + +int metrics_method_describe( + const MetricFamily metric_family_table[], + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + int r; + + assert(metric_family_table); + assert(link); + assert(parameters); + + r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL); + if (r != 0) + return r; + + if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL); + + const MetricFamily *previous = NULL; + for (const MetricFamily *mf = metric_family_table; mf && mf->name; mf++) { + if (previous) { + r = metric_family_build_send(link, previous, /* more= */ true); + if (r < 0) + return log_debug_errno( + r, "Failed to describe metric family '%s': %m", previous->name); + } + + previous = mf; + } + + if (!previous) + return sd_varlink_error(link, "io.systemd.Metrics.NoSuchMetric", NULL); + + r = metric_family_build_send(link, previous, /* more= */ false); + if (r < 0) + return log_debug_errno(r, "Failed to describe metric family '%s': %m", previous->name); + + return 0; +} + +int metrics_method_list( + const MetricFamily metric_family_table[], + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + int r; + + assert(metric_family_table); + assert(link); + assert(parameters); + + r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL); + if (r != 0) + return r; + + if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL); + + _cleanup_(metric_family_context_done) MetricFamilyContext ctx = { .link = link }; + for (const MetricFamily *mf = metric_family_table; mf && mf->name; mf++) { + assert(mf->generate_cb); + + ctx.metric_family = mf; + r = mf->generate_cb(&ctx, userdata); + if (r < 0) + return log_debug_errno( + r, "Failed to list metrics for metric family '%s': %m", mf->name); + } + + if (!ctx.previous) + return sd_varlink_error(link, "io.systemd.Metrics.NoSuchMetric", NULL); + + /* produce the last metric */ + return sd_varlink_reply(link, ctx.previous); +} + +static int metric_set_fields(sd_json_variant **v, char **field_pairs) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *w = NULL; + size_t n; + int r; + + assert(v); + + n = strv_length(field_pairs); + if (n == 0) + return 0; + + if (n % 2 != 0) + return log_debug_errno(SYNTHETIC_ERRNO(ERANGE), "Odd number of field pairs: %zu", n); + + sd_json_variant **array = new0(sd_json_variant *, n); + if (!array) + return log_oom(); + + CLEANUP_ARRAY(array, n, sd_json_variant_unref_many); + + int i = 0; + STRV_FOREACH_PAIR(key, value, field_pairs) { + r = sd_json_variant_new_string(&array[i++], *key); + if (r < 0) + return log_debug_errno(r, "Failed to create key variant: %m"); + + r = sd_json_variant_new_string(&array[i++], *value); + if (r < 0) + return log_debug_errno(r, "Failed to create value variant: %m"); + } + + r = sd_json_variant_new_object(&w, array, n); + if (r < 0) + return log_debug_errno(r, "Failed to allocate JSON object: %m"); + + return sd_json_variant_set_field(v, "fields", w); +} + +static int metric_build_send(MetricFamilyContext *context, const char *object, sd_json_variant *value, char **field_pairs) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + assert(context); + assert(value); + assert(context->link); + assert(context->metric_family); + + r = sd_json_buildo( + &v, + SD_JSON_BUILD_PAIR_STRING("name", context->metric_family->name), + JSON_BUILD_PAIR_STRING_NON_EMPTY("object", object), + SD_JSON_BUILD_PAIR("value", SD_JSON_BUILD_VARIANT(value))); + /* TODO JSON_BUILD_PAIR_OBJECT_STRV_NOT_NULL */ + if (r < 0) + return r; + + if (field_pairs) { /* NULL => no fields object, empty strv => fields:{} */ + r = metric_set_fields(&v, field_pairs); + if (r < 0) + return r; + } + + if (context->previous) { + r = sd_varlink_notify(context->link, context->previous); + if (r < 0) + return r; + + context->previous = sd_json_variant_unref(context->previous); + } + + context->previous = TAKE_PTR(v); + + return 0; +} + +int metric_build_send_string(MetricFamilyContext *context, const char *object, const char *value, char **field_pairs) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + assert(value); + + r = sd_json_variant_new_string(&v, value); + if (r < 0) + return log_debug_errno(r, "Failed to allocate JSON string: %m"); + + return metric_build_send(context, object, v, field_pairs); +} + +int metric_build_send_unsigned(MetricFamilyContext *context, const char *object, uint64_t value, char **field_pairs) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + r = sd_json_variant_new_unsigned(&v, value); + if (r < 0) + return log_debug_errno(r, "Failed to allocate JSON unsigned: %m"); + + return metric_build_send(context, object, v, field_pairs); +} diff --git a/src/shared/metrics.h b/src/shared/metrics.h new file mode 100644 index 0000000000000..a953ae5779469 --- /dev/null +++ b/src/shared/metrics.h @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink.h" + +#include "macro-fundamental.h" + +typedef enum MetricFamilyType { + METRIC_FAMILY_TYPE_COUNTER, + METRIC_FAMILY_TYPE_GAUGE, + METRIC_FAMILY_TYPE_STRING, + _METRIC_FAMILY_TYPE_MAX, + _METRIC_FAMILY_TYPE_INVALID = -EINVAL, +} MetricFamilyType; + +typedef struct MetricFamily MetricFamily; + +typedef struct MetricFamilyContext { + const MetricFamily* metric_family; + sd_varlink *link; + sd_json_variant *previous; +} MetricFamilyContext; + +typedef int (*metric_family_generate_cb_t) (MetricFamilyContext *mfc, void *userdata); + +typedef struct MetricFamily { + const char *name; + const char *description; + MetricFamilyType type; + metric_family_generate_cb_t generate_cb; +} MetricFamily; + +int metrics_setup_varlink_server( + sd_varlink_server **server, /* in and out param */ + sd_varlink_server_flags_t flags, + sd_event *event, + sd_varlink_method_t vl_method_list_cb, + sd_varlink_method_t vl_method_describe_cb, + void *userdata); + +const char* metric_family_type_to_string(MetricFamilyType i) _const_; +int metrics_method_describe(const MetricFamily metric_family_table[], sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); +int metrics_method_list(const MetricFamily metric_family_table[], sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); + +int metric_build_send_string(MetricFamilyContext* context, const char *object, const char *value, char **field_pairs); +int metric_build_send_unsigned(MetricFamilyContext* context, const char *object, uint64_t value, char **field_pairs); diff --git a/src/shared/varlink-io.systemd.Metrics.c b/src/shared/varlink-io.systemd.Metrics.c new file mode 100644 index 0000000000000..c052266740769 --- /dev/null +++ b/src/shared/varlink-io.systemd.Metrics.c @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-varlink-idl.h" + +#include "varlink-io.systemd.Metrics.h" + +static SD_VARLINK_DEFINE_ENUM_TYPE( + MetricFamilyType, + SD_VARLINK_FIELD_COMMENT("A counter metric family type which is a monotonically increasing value"), + SD_VARLINK_DEFINE_ENUM_VALUE(counter), + SD_VARLINK_FIELD_COMMENT("A gauge metric family type which is a value that can go up and down"), + SD_VARLINK_DEFINE_ENUM_VALUE(gauge), + SD_VARLINK_FIELD_COMMENT("A string metric family type"), + SD_VARLINK_DEFINE_ENUM_VALUE(string)); + +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.units_by_type_total or io.systemd.Manager.unit_active_state"), + 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 deferentiate between different metrics in the same metric family"), + SD_VARLINK_DEFINE_OUTPUT(fields, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + Describe, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_FIELD_COMMENT("Metric family name"), + SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Metric family description"), + SD_VARLINK_DEFINE_OUTPUT(description, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Metric family type"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, MetricFamilyType, 0)); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_Metrics, + "io.systemd.Metrics", + SD_VARLINK_INTERFACE_COMMENT("Metrics APIs"), + SD_VARLINK_SYMBOL_COMMENT("An enum representing various metric family types"), + &vl_type_MetricFamilyType, + SD_VARLINK_SYMBOL_COMMENT("Method to get a list of metrics among which the collection of related metrics forms a metric family"), + &vl_method_List, + SD_VARLINK_SYMBOL_COMMENT("Method to get the metric families"), + &vl_method_Describe, + SD_VARLINK_SYMBOL_COMMENT("No such metric found"), + &vl_error_NoSuchMetric); diff --git a/src/shared/varlink-io.systemd.Metrics.h b/src/shared/varlink-io.systemd.Metrics.h new file mode 100644 index 0000000000000..d8458a500dffe --- /dev/null +++ b/src/shared/varlink-io.systemd.Metrics.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_Metrics; diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh index c7607ce7c4501..4bff7f5d7dc53 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh @@ -210,6 +210,18 @@ varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"cgroup": invocation_id="$(systemctl show -P InvocationID systemd-journald.service)" varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "{\"invocationID\": \"$invocation_id\"}" +# test io.systemd.Metrics +varlinkctl info /run/systemd/report/io.systemd.Manager + +varlinkctl list-methods /run/systemd/report/io.systemd.Manager +varlinkctl list-methods -j /run/systemd/report/io.systemd.Manager io.systemd.Metrics | jq . + +varlinkctl introspect /run/systemd/report/io.systemd.Manager +varlinkctl introspect -j /run/systemd/report/io.systemd.Manager io.systemd.Metrics | jq . + +varlinkctl --more call /run/systemd/report/io.systemd.Manager io.systemd.Metrics.List {} +varlinkctl --more call /run/systemd/report/io.systemd.Manager io.systemd.Metrics.Describe {} + # test io.systemd.Manager in user manager testuser_uid=$(id -u testuser) systemd-run --wait --pipe --user --machine testuser@ \ @@ -222,3 +234,7 @@ 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 +systemd-report --sort