From: Zbigniew Jędrzejewski-Szmek Date: Tue, 28 Apr 2026 21:55:48 +0000 (+0200) Subject: report: upload reports using a "varlink socket directory" X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a2186070b79b2dcce332465696ad31241f27cf11;p=thirdparty%2Fsystemd.git report: upload reports using a "varlink socket directory" Two new verbs are added: "generate" and "upload". The first one just creates a "report", i.e. puts the metrics into a structured JSON object that in the future is intended to carry additional data like a signature: $ build/systemd-report generate io.systemd.Manager.UnitsTotal { "mediaType" : "application/vnd.io.systemd.report", "timestamp" : "Tue 2026-04-28 22:30:09 UTC", "metrics" : [ { "name" : "io.systemd.Manager.UnitsTotal", "value" : 520 } ] } The second verb can be used to upload or otherwise process the report. It builds on the code added in 0a8560eed873a5f89487630a19db550fdbee3c15. In /run/systemd/metrics-upload/ we expect a set of sockets. We'll call out to each one of them. This allows the data to be processed in custom ways, incl. writing to storage or sending over the network. Each socket must provide a single interface: io.systemd.Metrics.Upload {"report":$data} --- diff --git a/man/systemd-report.xml b/man/systemd-report.xml index 0974244a8f5..560d9406240 100644 --- a/man/systemd-report.xml +++ b/man/systemd-report.xml @@ -71,6 +71,31 @@ + + generate MATCH + + Acquire a list of metrics and build a JSON report. + + Match expressions supported by metrics are supported here too. + + + + + + upload MATCH + + This command can be used to send the report built by generate + to an external server. Two upload mechanisms are supported. If an http:// or + https:// URL is specified with , an HTTP upload will be + performed to the specified location. Otherwise, any sockets under + /run/systemd/metrics-upload/ will be used to call + io.systemd.Report.Upload(). + + Match expressions supported by metrics are supported here too. + + + + list-sources diff --git a/src/report/report-upload.c b/src/report/report-upload.c index 486e815e8d8..fce0c0e5513 100644 --- a/src/report/report-upload.c +++ b/src/report/report-upload.c @@ -2,12 +2,15 @@ #include "sd-json.h" +#include "alloc-util.h" +#include "errno-util.h" #include "log.h" #include "report.h" #include "string-util.h" #include "strv.h" #include "time-util.h" #include "utf8.h" +#include "varlink-util.h" #include "version.h" #if HAVE_LIBCURL @@ -49,6 +52,7 @@ static size_t output_callback(char *buf, return nmemb; } +#endif static int build_json_report(Context *context, sd_json_variant **ret) { /* Convert the variant array to a JSON report. */ @@ -60,6 +64,7 @@ static int build_json_report(Context *context, sd_json_variant **ret) { int r; r = sd_json_buildo(ret, + SD_JSON_BUILD_PAIR_STRING("mediaType", "application/vnd.io.systemd.report"), SD_JSON_BUILD_PAIR("timestamp", SD_JSON_BUILD_STRING(FORMAT_TIMESTAMP_STYLE(ts, TIMESTAMP_UTC))), SD_JSON_BUILD_PAIR("metrics", @@ -68,9 +73,8 @@ static int build_json_report(Context *context, sd_json_variant **ret) { return log_error_errno(r, "Failed to build JSON data: %m"); return 0; } -#endif -int upload_collected(Context *context) { +static int http_upload_collected(Context *context, sd_json_variant *report) { #if HAVE_LIBCURL _cleanup_(curl_slist_free_allp) struct curl_slist *header = NULL; char error[CURL_ERROR_SIZE] = {}; @@ -81,19 +85,11 @@ int upload_collected(Context *context) { if (r < 0) return r; - { - /* Convert our variant array to a JSON report. - * We won't need the JSON structure again, so free it quickly. */ - - _cleanup_(sd_json_variant_unrefp) sd_json_variant *vl = NULL; - r = build_json_report(context, &vl); - if (r < 0) - return r; + /* Upload a JSON report in text form as a single JSON object, instead of a JSON-SEQ list. */ - r = sd_json_variant_format(vl, /* flags= */ 0, &json); - if (r < 0) - return log_error_errno(r, "Failed to format JSON data: %m"); - } + r = sd_json_variant_format(report, /* flags= */ 0, &json); + if (r < 0) + return log_error_errno(r, "Failed to format JSON data: %m"); r = curl_append_to_header(&header, STRV_MAKE("Content-Type: application/json", @@ -206,3 +202,84 @@ int upload_collected(Context *context) { "Compiled without libcurl."); #endif } + +static int execute_dir_reply( + sd_varlink *link, + sd_json_variant *reply, + const char *error_id, + sd_varlink_reply_flags_t flags, + void *userdata) { + + assert(link); + + Context *context = ASSERT_PTR(userdata); + int r; + + if (error_id) { + r = sd_varlink_error_to_errno(error_id, reply); + RET_GATHER(context->upload_result, r); + return log_error_errno(r, "Upload via Varlink failed: %s", error_id); + } + + printf("Upload via Varlink was successful; reply: "); + // TODO: once we know what we want to put in the reply, replace the JSON dump by + // some formatted output. + r = sd_json_variant_dump(reply, arg_json_format_flags, stderr, /* prefix= */ ">>> "); + if (r < 0) + return log_error_errno(r, "Failed to dump json object: %m"); + + return 0; +} + +static int upload_collected(Context *context, sd_json_variant *report) { + int r; + + if (arg_url) + return http_upload_collected(context, report); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; + r = sd_json_buildo(¶ms, + SD_JSON_BUILD_PAIR_VARIANT("report", report)); + if (r < 0) + return log_error_errno(r, "Failed to build JSON data: %m"); + + ssize_t jobs = varlink_execute_directory( + REPORT_UPLOAD_DIR, + "io.systemd.Report.Upload", + params, + /* more= */ false, + arg_network_timeout_usec, + execute_dir_reply, + /* userdata= */ context); + if (jobs < 0) + return log_error_errno(jobs, "Failed to execute upload via %s: %m", REPORT_UPLOAD_DIR); + if (jobs == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), + "No upload mechanism found via %s.", REPORT_UPLOAD_DIR); + if (context->upload_result < 0) + /* The details were printed at error level by execute_dir_reply above. */ + return log_debug_errno(context->upload_result, "Upload via %s failed: %m", REPORT_UPLOAD_DIR); + + log_debug("Upload via %s finished successfully.", REPORT_UPLOAD_DIR); + return 0; +} + +/* Make a structured report and either print it or upload it. */ +int report_collected(Context *context) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL; + int r; + + r = build_json_report(context, &report); + if (r < 0) + return r; + + if (context->action == ACTION_UPLOAD) + return upload_collected(context, report); + + /* Just print the report for now. */ + assert(context->action == ACTION_GENERATE); + r = sd_json_variant_dump(report, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to dump json object: %m"); + return 0; +} diff --git a/src/report/report.c b/src/report/report.c index feedcaa43a5..c23417afb5b 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -20,7 +20,6 @@ #include "runtime-scope.h" #include "set.h" #include "sort-util.h" -#include "string-table.h" #include "string-util.h" #include "strv.h" #include "time-util.h" @@ -35,8 +34,8 @@ 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_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; static char **arg_matches = NULL; +sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; char *arg_url = NULL; char *arg_key = NULL; char *arg_cert = NULL; @@ -84,13 +83,6 @@ DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( void, trivial_hash_func, trivial_compare_func, LinkInfo, link_info_free); -static const char* const action_method_table[] = { - [ACTION_LIST_METRICS] = "io.systemd.Metrics.List", - [ACTION_DESCRIBE_METRICS] = "io.systemd.Metrics.Describe", -}; - -DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(action_method, Action); - 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; @@ -300,7 +292,9 @@ static int call_collect(Context *context, const char *name, const char *path) { if (r < 0) return log_error_errno(r, "Failed to bind reply callback: %m"); - const char *method = ASSERT_PTR(action_method_to_string(context->action)); + const char *method = context->action == ACTION_DESCRIBE_METRICS ? + "io.systemd.Metrics.Describe" : + "io.systemd.Metrics.List"; /* This is the method for all other actions. */ r = sd_varlink_observe(vl, method, /* parameters= */ NULL); if (r < 0) @@ -591,16 +585,22 @@ VERB_FULL(verb_metrics, "metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_L "Acquire list of metrics and their values"); VERB_FULL(verb_metrics, "describe", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS, "Describe available metrics"); +VERB_FULL(verb_metrics, "generate", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_GENERATE, + "Build a report with metrics"); +VERB_FULL(verb_metrics, "upload", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_UPLOAD, + "Upload a report with metrics"); static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) { Action action = data; int r; assert(argc >= 1); assert(argv); - assert(IN_SET(action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS)); + assert(IN_SET(action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS, ACTION_GENERATE, ACTION_UPLOAD)); - /* Enable JSON-SEQ mode here, since we'll dump a large series of JSON objects */ - arg_json_format_flags |= SD_JSON_FORMAT_SEQ; + if (IN_SET(action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS)) + /* Enable JSON-SEQ mode for the first two verbs, since we'll dump a large series of JSON + * objects. In the report format, we return a single JSON object, so don't do this. */ + arg_json_format_flags |= SD_JSON_FORMAT_SEQ; r = parse_metrics_matches(argv + 1); if (r < 0) @@ -651,8 +651,8 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) if (r < 0) return log_error_errno(r, "Failed to run event loop: %m"); - if (arg_url) - r = upload_collected(&context); + if (IN_SET(action, ACTION_GENERATE, ACTION_UPLOAD)) + r = report_collected(&context); else r = output_collected(&context); if (r < 0) diff --git a/src/report/report.h b/src/report/report.h index 4adb2034951..196a3daf577 100644 --- a/src/report/report.h +++ b/src/report/report.h @@ -9,6 +9,9 @@ #define REPORT_CERT_FILE CERTIFICATE_ROOT "/certs/systemd-report.pem" #define REPORT_TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem" +#define REPORT_UPLOAD_DIR "/run/systemd/metrics-upload" + +extern sd_json_format_flags_t arg_json_format_flags; extern char *arg_url, *arg_key, *arg_cert, *arg_trust; extern char **arg_extra_headers; extern usec_t arg_network_timeout_usec; @@ -16,6 +19,8 @@ extern usec_t arg_network_timeout_usec; typedef enum Action { ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS, + ACTION_GENERATE, + ACTION_UPLOAD, _ACTION_MAX, _ACTION_INVALID = -EINVAL, } Action; @@ -27,7 +32,9 @@ typedef struct Context { Set *link_infos; sd_json_variant **metrics; /* Collected metrics for sorting */ size_t n_metrics, n_skipped_metrics, n_invalid_metrics; + + int upload_result; struct iovec_wrapper upload_answer; } Context; -int upload_collected(Context *context); +int report_collected(Context *context); diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh index 73678fcabf1..e9332806b37 100755 --- a/test/units/TEST-74-AUX-UTILS.report.sh +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -60,7 +60,11 @@ trap at_exit EXIT systemd-run -p Type=notify --unit=fake-report-server "$FAKE_SERVER" systemctl status fake-report-server -"$REPORT" metrics --url=http://localhost:8089/ +"$REPORT" generate io.systemd.Manager.UnitsTotal + +"$REPORT" generate io.systemd.Manager.UnitsTotal | jq . + +"$REPORT" upload --url=http://localhost:8089/ # Test HTTPS upload with generated TLS certificates openssl req -x509 -newkey rsa:2048 -keyout "$CERTDIR/server.key" -out "$CERTDIR/server.crt" \ @@ -70,5 +74,5 @@ systemd-run -p Type=notify --unit=fake-report-server-tls \ "$FAKE_SERVER" --cert="$CERTDIR/server.crt" --key="$CERTDIR/server.key" --port=8090 systemctl status fake-report-server-tls -"$REPORT" metrics --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \ +"$REPORT" upload --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \ --extra-header='Authorization: Bearer magic string'