Merge pull request #25790 from joshua-zivkovic/JZ/plotjson-main

systemd-analyze: Add JSON and table output to systemd-analyze's plot
This commit is contained in:
Lennart Poettering
2023-01-18 18:11:12 +01:00
committed by GitHub
7 changed files with 210 additions and 58 deletions

View File

@@ -274,8 +274,8 @@ Timestamp units-load-finish: Thu 2019-03-14 23:28:07 CET
<refsect2>
<title><command>systemd-analyze plot</command></title>
<para>This command prints an SVG graphic detailing which system services have been started at what
time, highlighting the time they spent on initialization.</para>
<para>This command prints either an SVG graphic, detailing which system services have been started at what
time, highlighting the time they spent on initialization, or the raw time data in JSON or table format.</para>
<example>
<title><command>Plot a bootchart</command></title>
@@ -1211,7 +1211,17 @@ $ systemd-analyze verify /tmp/source:alias.service
corresponds to a higher security threat. The JSON version of the table is printed to standard
output. The <replaceable>MODE</replaceable> passed to the option can be one of three:
<option>off</option> which is the default, <option>pretty</option> and <option>short</option>
which respectively output a prettified or shorted JSON version of the security table.</para></listitem>
which respectively output a prettified or shorted JSON version of the security table.
With the <command>plot</command> command, genereate a JSON formatted output of the raw time data.
The format is a JSON array with objects containing the following fields: <varname>name</varname>
which is the unit name, <varname>activated</varname> which is the time after startup the
service was activated, <varname>activating</varname> which is how long after startup the service
was initially started, <varname>time</varname> which is how long the service took to activate
from when it was initially started, <varname>deactivated</varname> which is the time after startup
that the service was deactivated, <varname>deactivating</varname> whcih is the time after startup
that the service was initially told to deactivate.
</para></listitem>
</varlistentry>
<varlistentry>
@@ -1242,6 +1252,21 @@ $ systemd-analyze verify /tmp/source:alias.service
other paths.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--table</option></term>
<listitem><para>When used with the <command>plot</command> command, the raw time data is output in a table.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--no-legend</option></term>
<listitem><para>When used with the <command>plot</command> command in combination with either
<option>--table</option> or <option>--json=</option>, no legends or hints are included in the output.
</para></listitem>
</varlistentry>
<xi:include href="user-system-options.xml" xpointer="host" />
<xi:include href="user-system-options.xml" xpointer="machine" />

View File

@@ -62,7 +62,7 @@ _systemd_analyze() {
)
local -A VERBS=(
[STANDALONE]='time blame plot unit-paths exit-status calendar timestamp timespan'
[STANDALONE]='time blame unit-paths exit-status calendar timestamp timespan'
[CRITICAL_CHAIN]='critical-chain'
[DOT]='dot'
[DUMP]='dump'
@@ -72,6 +72,7 @@ _systemd_analyze() {
[SECURITY]='security'
[CONDITION]='condition'
[INSPECT_ELF]='inspect-elf'
[PLOT]='plot'
)
local CONFIGS='systemd/bootchart.conf systemd/coredump.conf systemd/journald.conf
@@ -195,6 +196,11 @@ _systemd_analyze() {
comps=$( compgen -A file -- "$cur" )
compopt -o filenames
fi
elif __contains_word "$verb" ${VERBS[PLOT]}; then
if [[ $cur = -* ]]; then
comps='--help --version --system --user --global --no-pager --json=off --json=pretty --json=short --table --no-legend'
fi
fi
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )

View File

@@ -40,6 +40,13 @@
_describe -t groups 'file system groups' _groups || compadd "$@"
}
(( $+functions[_systemd-analyze_plot] )) ||
_systemd-analyze_plot() {
local -a _options
_options=( '--json=off' '--json=pretty' '--json=short' '--table' '--no-legend' )
_describe 'plot options' _options
}
(( $+functions[_systemd-analyze_commands] )) ||
_systemd-analyze_commands(){
local -a _systemd_analyze_cmds
@@ -48,7 +55,8 @@
'time:Print time spent in the kernel before reaching userspace'
'blame:Print list of running units ordered by time to init'
'critical-chain:Print a tree of the time critical chain of units'
'plot:Output SVG graphic showing service initialization'
'plot:Output SVG graphic showing service initialization, or raw time data in
JSON or table format'
'dot:Dump dependency graph (in dot(1) format)'
'dump:Dump server status'
'cat-config:Cat systemd config files'
@@ -97,9 +105,11 @@ _arguments \
'--offline=[Perform a security review of the specified unit files]:BOOL:(yes no)' \
'--threshold=[Set a value to compare the overall security exposure level with]: NUMBER' \
'--security-policy=[Use customized requirements to compare unit files against]: PATH' \
'--json=[Generate a JSON output of the security analysis table]:MODE:(pretty short off)' \
"--json=[Generate a JSON output of the security analysis table or plot's raw time data]:MODE:(pretty short off)" \
"--table=[Generate a table of plot's raw time data]" \
'--profile=[Include the specified profile in the security review of units]: PATH' \
'--no-pager[Do not pipe output into a pager]' \
"--no-legend[Do not show the headers and footers for plot's raw time data formats]" \
'--man=[Do (not) check for existence of man pages]:BOOL:(yes no)' \
'--generators=[Do (not) run unit generators]:BOOL:(yes no)' \
'--order[When generating graph for dot, show only order]' \

View File

@@ -5,6 +5,7 @@
#include "analyze-time-data.h"
#include "bus-error.h"
#include "bus-map-properties.h"
#include "format-table.h"
#include "sort-util.h"
#include "version.h"
@@ -37,7 +38,7 @@ typedef struct HostInfo {
char *architecture;
} HostInfo;
static HostInfo* free_host_info(HostInfo *hi) {
static HostInfo *free_host_info(HostInfo *hi) {
if (!hi)
return NULL;
@@ -87,7 +88,7 @@ static int acquire_host_info(sd_bus *bus, HostInfo **hi) {
}
r = bus_map_all_properties(
system_bus ?: bus,
system_bus ? : bus,
"org.freedesktop.hostname1",
"/org/freedesktop/hostname1",
hostname_map,
@@ -156,15 +157,14 @@ static void svg_graph_box(double height, double begin, double end) {
SCALE_Y * height);
}
}
static int plot_unit_times(UnitTimes *u, double width, int y) {
bool b;
if (!u->name)
return 0;
svg_bar("activating", u->activating, u->activated, y);
svg_bar("active", u->activated, u->deactivating, y);
svg_bar("activating", u->activating, u->activated, y);
svg_bar("active", u->activated, u->deactivating, y);
svg_bar("deactivating", u->deactivating, u->deactivated, y);
/* place the text on the left if we have passed the half of the svg width */
@@ -178,41 +178,27 @@ static int plot_unit_times(UnitTimes *u, double width, int y) {
return 1;
}
int verb_plot(int argc, char *argv[], void *userdata) {
_cleanup_(free_host_infop) HostInfo *host = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL;
_cleanup_free_ char *pretty_times = NULL;
bool use_full_bus = arg_scope == LOOKUP_SCOPE_SYSTEM;
BootTimes *boot;
static void limit_times_to_boot(const BootTimes *boot, UnitTimes *u) {
if (u->deactivated > u->activating && u->deactivated <= boot->finish_time && u->activated == 0
&& u->deactivating == 0)
u->activated = u->deactivating = u->deactivated;
if (u->activated < u->activating || u->activated > boot->finish_time)
u->activated = boot->finish_time;
if (u->deactivating < u->activated || u->deactivating > boot->finish_time)
u->deactivating = boot->finish_time;
if (u->deactivated < u->deactivating || u->deactivated > boot->finish_time)
u->deactivated = boot->finish_time;
}
static int produce_plot_as_svg(
UnitTimes *times,
const HostInfo *host,
const BootTimes *boot,
const char *pretty_times) {
int m = 1, y = 0;
UnitTimes *u;
int n, m = 1, y = 0, r;
double width;
r = acquire_bus(&bus, &use_full_bus);
if (r < 0)
return bus_log_connect_error(r, arg_transport);
n = acquire_boot_times(bus, &boot);
if (n < 0)
return n;
n = pretty_boot_time(bus, &pretty_times);
if (n < 0)
return n;
if (use_full_bus || arg_scope != LOOKUP_SCOPE_SYSTEM) {
n = acquire_host_info(bus, &host);
if (n < 0)
return n;
}
n = acquire_time_data(bus, &times);
if (n <= 0)
return n;
typesafe_qsort(times, n, compare_unit_start);
width = SCALE_X * (boot->firmware_time + boot->finish_time);
if (width < 800.0)
width = 800.0;
@@ -245,16 +231,8 @@ int verb_plot(int argc, char *argv[], void *userdata) {
if (text_width > text_start && text_width + text_start > width)
width = text_width + text_start;
if (u->deactivated > u->activating &&
u->deactivated <= boot->finish_time &&
u->activated == 0 && u->deactivating == 0)
u->activated = u->deactivating = u->deactivated;
if (u->activated < u->activating || u->activated > boot->finish_time)
u->activated = boot->finish_time;
if (u->deactivating < u->activated || u->deactivating > boot->finish_time)
u->deactivating = boot->finish_time;
if (u->deactivated < u->deactivating || u->deactivated > boot->finish_time)
u->deactivated = boot->finish_time;
limit_times_to_boot(boot, u);
m++;
}
@@ -391,5 +369,101 @@ int verb_plot(int argc, char *argv[], void *userdata) {
svg("</svg>\n");
return 0;
}
static int show_table(Table *table, const char *word) {
int r;
assert(table);
assert(word);
if (table_get_rows(table) > 1) {
table_set_header(table, arg_legend);
if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF))
r = table_print_json(table, NULL, arg_json_format_flags | JSON_FORMAT_COLOR_AUTO);
else
r = table_print(table, NULL);
if (r < 0)
return table_log_print_error(r);
}
if (arg_legend) {
if (table_get_rows(table) > 1)
printf("\n%zu %s listed.\n", table_get_rows(table) - 1, word);
else
printf("No %s.\n", word);
}
return 0;
}
static int produce_plot_as_text(UnitTimes *times, const BootTimes *boot) {
_cleanup_(table_unrefp) Table *table = NULL;
int r;
table = table_new("name", "activated", "activating", "time", "deactivated", "deactivating");
if (!table)
return log_oom();
for (; times->has_data; times++) {
limit_times_to_boot(boot, times);
r = table_add_many(
table,
TABLE_STRING, times->name,
TABLE_TIMESPAN_MSEC, times->activated,
TABLE_TIMESPAN_MSEC, times->activating,
TABLE_TIMESPAN_MSEC, times->time,
TABLE_TIMESPAN_MSEC, times->deactivated,
TABLE_TIMESPAN_MSEC, times->deactivating);
if (r < 0)
return table_log_add_error(r);
}
return show_table(table, "Units");
}
int verb_plot(int argc, char *argv[], void *userdata) {
_cleanup_(free_host_infop) HostInfo *host = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL;
_cleanup_free_ char *pretty_times = NULL;
bool use_full_bus = arg_scope == LOOKUP_SCOPE_SYSTEM;
BootTimes *boot;
int n, r;
r = acquire_bus(&bus, &use_full_bus);
if (r < 0)
return bus_log_connect_error(r, arg_transport);
n = acquire_boot_times(bus, &boot);
if (n < 0)
return n;
n = pretty_boot_time(bus, &pretty_times);
if (n < 0)
return n;
if (use_full_bus || arg_scope != LOOKUP_SCOPE_SYSTEM) {
n = acquire_host_info(bus, &host);
if (n < 0)
return n;
}
n = acquire_time_data(bus, &times);
if (n <= 0)
return n;
typesafe_qsort(times, n, compare_unit_start);
if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) || arg_table)
r = produce_plot_as_text(times, boot);
else
r = produce_plot_as_svg(times, host, boot, pretty_times);
if (r < 0)
return r;
return EXIT_SUCCESS;
}

View File

@@ -105,6 +105,8 @@ char *arg_unit = NULL;
JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
bool arg_quiet = false;
char *arg_profile = NULL;
bool arg_legend = true;
bool arg_table = false;
STATIC_DESTRUCTOR_REGISTER(arg_dot_from_patterns, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_dot_to_patterns, strv_freep);
@@ -217,8 +219,10 @@ static int help(int argc, char *argv[], void *userdata) {
" --security-policy=PATH Use custom JSON security policy instead\n"
" of built-in one\n"
" --json=pretty|short|off Generate JSON output of the security\n"
" analysis table\n"
" analysis table, or plot's raw time data\n"
" --no-pager Do not pipe output into a pager\n"
" --no-legend Disable column headers and hints in plot\n"
" with either --table or --json=\n"
" --system Operate on system systemd instance\n"
" --user Operate on user systemd instance\n"
" --global Operate on global user configuration\n"
@@ -238,6 +242,7 @@ static int help(int argc, char *argv[], void *userdata) {
" specified time\n"
" --profile=name|PATH Include the specified profile in the\n"
" security review of the unit(s)\n"
" --table Output plot's raw time data as a table\n"
" -h --help Show this help\n"
" --version Show package version\n"
" -q --quiet Do not emit hints\n"
@@ -280,6 +285,8 @@ static int parse_argv(int argc, char *argv[]) {
ARG_SECURITY_POLICY,
ARG_JSON,
ARG_PROFILE,
ARG_TABLE,
ARG_NO_LEGEND,
};
static const struct option options[] = {
@@ -310,6 +317,8 @@ static int parse_argv(int argc, char *argv[]) {
{ "unit", required_argument, NULL, 'U' },
{ "json", required_argument, NULL, ARG_JSON },
{ "profile", required_argument, NULL, ARG_PROFILE },
{ "table", optional_argument, NULL, ARG_TABLE },
{ "no-legend", optional_argument, NULL, ARG_NO_LEGEND },
{}
};
@@ -448,14 +457,12 @@ static int parse_argv(int argc, char *argv[]) {
r = safe_atou(optarg, &arg_iterations);
if (r < 0)
return log_error_errno(r, "Failed to parse iterations: %s", optarg);
break;
case ARG_BASE_TIME:
r = parse_timestamp(optarg, &arg_base_time);
if (r < 0)
return log_error_errno(r, "Failed to parse --base-time= parameter: %s", optarg);
break;
case ARG_PROFILE:
@@ -486,6 +493,15 @@ static int parse_argv(int argc, char *argv[]) {
free_and_replace(arg_unit, mangled);
break;
}
case ARG_TABLE:
arg_table = true;
break;
case ARG_NO_LEGEND:
arg_legend = false;
break;
case '?':
return -EINVAL;
@@ -497,9 +513,9 @@ static int parse_argv(int argc, char *argv[]) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Option --offline= is only supported for security right now.");
if (arg_json_format_flags != JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf"))
if (arg_json_format_flags != JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf", "plot"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Option --json= is only supported for security and inspect-elf right now.");
"Option --json= is only supported for security, inspect-elf, and plot right now.");
if (arg_threshold != 100 && !streq_ptr(argv[optind], "security"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
@@ -536,6 +552,16 @@ static int parse_argv(int argc, char *argv[]) {
if (streq_ptr(argv[optind], "condition") && arg_unit && optind < argc - 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No conditions can be passed if --unit= is used.");
if ((!arg_legend && !streq_ptr(argv[optind], "plot")) ||
(streq_ptr(argv[optind], "plot") && !arg_legend && !arg_table && FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --no-legend is only supported for plot with either --table or --json=.");
if (arg_table && !streq_ptr(argv[optind], "plot"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --table is only supported for plot right now.");
if (arg_table && !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--table and --json= are mutually exclusive.");
return 1; /* work to do */
}

View File

@@ -36,6 +36,8 @@ extern char *arg_unit;
extern JsonFormatFlags arg_json_format_flags;
extern bool arg_quiet;
extern char *arg_profile;
extern bool arg_legend;
extern bool arg_table;
int acquire_bus(sd_bus **bus, bool *use_full_bus);

View File

@@ -18,7 +18,16 @@ systemd-analyze || :
systemd-analyze time || :
systemd-analyze blame || :
systemd-analyze critical-chain || :
# plot
systemd-analyze plot >/dev/null || :
systemd-analyze plot --json=pretty >/dev/null || :
systemd-analyze plot --json=short >/dev/null || :
systemd-analyze plot --json=off >/dev/null || :
systemd-analyze plot --json=pretty --no-legend >/dev/null || :
systemd-analyze plot --json=short --no-legend >/dev/null || :
systemd-analyze plot --json=off --no-legend >/dev/null || :
systemd-analyze plot --table >/dev/null || :
systemd-analyze plot --table --no-legend >/dev/null || :
# legacy/deprecated options (moved to systemctl, but still usable from analyze)
systemd-analyze log-level
systemd-analyze log-level "$(systemctl log-level)"