]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
report: upload reports using a "varlink socket directory"
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Tue, 28 Apr 2026 21:55:48 +0000 (23:55 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Tue, 5 May 2026 16:12:35 +0000 (18:12 +0200)
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}

man/systemd-report.xml
src/report/report-upload.c
src/report/report.c
src/report/report.h
test/units/TEST-74-AUX-UTILS.report.sh

index 0974244a8f5636871a54886679d2a1ef4ba81744..560d9406240f0aaca58f63e2a10a4ed7c0c1f9cb 100644 (file)
         <xi:include href="version-info.xml" xpointer="v260"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><command>generate</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
+
+        <listitem><para>Acquire a list of metrics and build a JSON report.</para>
+
+        <para>Match expressions supported by <command>metrics</command> are supported here too.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>upload</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
+
+        <listitem><para>This command can be used to send the report built by <command>generate</command>
+        to an external server. Two upload mechanisms are supported. If an <literal>http://</literal> or
+        <literal>https://</literal> URL is specified with <option>--url=</option>, an HTTP upload will be
+        performed to the specified location. Otherwise, any sockets under
+        <filename>/run/systemd/metrics-upload/</filename> will be used to call
+        <function>io.systemd.Report.Upload()</function>.</para>
+
+        <para>Match expressions supported by <command>metrics</command> are supported here too.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><command>list-sources</command></term>
 
index 486e815e8d857a9d78b23e75ba01119ad26f876e..fce0c0e551396cb43fa03d1323b8961c9962691f 100644 (file)
@@ -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(&params,
+                           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;
+}
index feedcaa43a5b8c7c609d783e27692a597b2e9460..c23417afb5b291a5338599a889d5d31dfc2bf076 100644 (file)
@@ -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)
index 4adb20349514ad7613d0fb82d857bd33e9403ea6..196a3daf577d540bba97d2a598032fee3375611e 100644 (file)
@@ -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);
index 73678fcabf1f86873228610f386fe53d74631558..e9332806b379158b464630bcda533948745aa321 100755 (executable)
@@ -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'