diff --git a/man/systemd-report.xml b/man/systemd-report.xml index 0974244a8f..560d940624 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 486e815e8d..fce0c0e551 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. */ + /* Upload a JSON report in text form as a single JSON object, instead of a JSON-SEQ list. */ - _cleanup_(sd_json_variant_unrefp) sd_json_variant *vl = NULL; - r = build_json_report(context, &vl); - if (r < 0) - return r; - - 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 feedcaa43a..c23417afb5 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 4adb203495..196a3daf57 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 73678fcabf..e9332806b3 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'