Pressure metrics (#43377)
This adds a slightly extended and improved version of the patch we had internally.
This commit is contained in:
+48
-26
@@ -6,6 +6,7 @@
|
||||
#include "extract-word.h"
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
#include "log.h"
|
||||
#include "parse-util.h"
|
||||
#include "process-util.h"
|
||||
#include "procfs-util.h"
|
||||
@@ -127,12 +128,36 @@ static uint64_t calc_gcd64(uint64_t a, uint64_t b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
nsec_t procfs_ticks_to_nsec(uint64_t ticks) {
|
||||
uint64_t ticks_per_second = sysconf_clock_ticks_cached(),
|
||||
gcd = calc_gcd64(NSEC_PER_SEC, ticks_per_second);
|
||||
|
||||
/* Reduce NSEC_PER_SEC/ticks_per_second first to avoid overflow of ticks * NSEC_PER_SEC, and split
|
||||
* the multiplication into whole and remainder parts, so that the result stays exact.
|
||||
*
|
||||
* Note that in most cases b == 1 and the second term is trivial. */
|
||||
uint64_t a = NSEC_PER_SEC / gcd,
|
||||
b = ticks_per_second / gcd;
|
||||
|
||||
return ticks / b * a + DIV_ROUND_UP(ticks % b * a, b);
|
||||
}
|
||||
|
||||
int procfs_cpu_get_usage(nsec_t *ret) {
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
|
||||
ProcfsCpuTicks ticks;
|
||||
r = procfs_cpu_get_ticks(&ticks);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret = procfs_ticks_to_nsec(ticks.user + ticks.nice + ticks.system + ticks.irq + ticks.softirq);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int procfs_cpu_get_ticks(ProcfsCpuTicks *ret) {
|
||||
_cleanup_free_ char *first_line = NULL;
|
||||
unsigned long user_ticks, nice_ticks, system_ticks, irq_ticks, softirq_ticks,
|
||||
guest_ticks = 0, guest_nice_ticks = 0;
|
||||
uint64_t sum, gcd, a, b;
|
||||
const char *p;
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
@@ -141,33 +166,30 @@ int procfs_cpu_get_usage(nsec_t *ret) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
p = first_word(first_line, "cpu");
|
||||
const char *p = first_word(first_line, "cpu");
|
||||
if (!p)
|
||||
return -EINVAL;
|
||||
|
||||
if (sscanf(p, "%lu %lu %lu %*u %*u %lu %lu %*u %lu %lu",
|
||||
&user_ticks,
|
||||
&nice_ticks,
|
||||
&system_ticks,
|
||||
&irq_ticks,
|
||||
&softirq_ticks,
|
||||
&guest_ticks,
|
||||
&guest_nice_ticks) < 5) /* we only insist on the first five fields */
|
||||
/* All eight fields exist on any kernel we support: iowait was added in 2.5.41, steal in 2.6.11.
|
||||
* "guest" and "guest_nice" are not returned, the kernel already includes them in "user" and "nice".
|
||||
* Nevertheless, this file might be synthesized slopilly, so let's not require the last three fields.
|
||||
* We don't care about them in most cases anyway. */
|
||||
ProcfsCpuTicks t = {};
|
||||
r = sscanf(p, "%"SCNu64" %"SCNu64" %"SCNu64" %"SCNu64" %"SCNu64" %"SCNu64" %"SCNu64" %"SCNu64,
|
||||
&t.user,
|
||||
&t.nice,
|
||||
&t.system,
|
||||
&t.idle,
|
||||
&t.iowait,
|
||||
&t.irq,
|
||||
&t.softirq,
|
||||
&t.steal);
|
||||
if (r < 5)
|
||||
return -EINVAL;
|
||||
if (r < 8)
|
||||
log_once(LOG_INFO, "Short read (%d fields) from /proc/stat, continuing.", r);
|
||||
|
||||
uint64_t ticks_per_second = sysconf_clock_ticks_cached();
|
||||
|
||||
sum = (uint64_t) user_ticks + (uint64_t) nice_ticks + (uint64_t) system_ticks +
|
||||
(uint64_t) irq_ticks + (uint64_t) softirq_ticks +
|
||||
(uint64_t) guest_ticks + (uint64_t) guest_nice_ticks;
|
||||
|
||||
/* Let's reduce this fraction before we apply it to avoid overflows when converting this to μsec */
|
||||
gcd = calc_gcd64(NSEC_PER_SEC, ticks_per_second);
|
||||
|
||||
a = NSEC_PER_SEC / gcd;
|
||||
b = ticks_per_second / gcd;
|
||||
|
||||
*ret = DIV_ROUND_UP((nsec_t) sum * (nsec_t) a, (nsec_t) b);
|
||||
*ret = t;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,22 @@ int procfs_get_threads_max(uint64_t *ret);
|
||||
int procfs_tasks_set_limit(uint64_t limit);
|
||||
int procfs_tasks_get_current(uint64_t *ret);
|
||||
|
||||
nsec_t procfs_ticks_to_nsec(uint64_t ticks);
|
||||
int procfs_cpu_get_usage(nsec_t *ret);
|
||||
|
||||
typedef struct ProcfsCpuTicks {
|
||||
uint64_t user;
|
||||
uint64_t nice;
|
||||
uint64_t system;
|
||||
uint64_t idle;
|
||||
uint64_t iowait;
|
||||
uint64_t irq;
|
||||
uint64_t softirq;
|
||||
uint64_t steal;
|
||||
} ProcfsCpuTicks;
|
||||
|
||||
int procfs_cpu_get_ticks(ProcfsCpuTicks *ret);
|
||||
|
||||
int procfs_memory_get(uint64_t *ret_total, uint64_t *ret_used);
|
||||
static inline int procfs_memory_get_used(uint64_t *ret) {
|
||||
return procfs_memory_get(NULL, ret);
|
||||
|
||||
@@ -19,7 +19,7 @@ int read_resource_pressure(const char *path, PressureType type, ResourcePressure
|
||||
_cleanup_fclose_ FILE *f = NULL;
|
||||
unsigned field_filled = 0;
|
||||
ResourcePressure rp = {};
|
||||
const char *t, *cline;
|
||||
const char *cline;
|
||||
char *word;
|
||||
int r;
|
||||
|
||||
@@ -27,12 +27,7 @@ int read_resource_pressure(const char *path, PressureType type, ResourcePressure
|
||||
assert(IN_SET(type, PRESSURE_TYPE_SOME, PRESSURE_TYPE_FULL));
|
||||
assert(ret);
|
||||
|
||||
if (type == PRESSURE_TYPE_SOME)
|
||||
t = "some";
|
||||
else if (type == PRESSURE_TYPE_FULL)
|
||||
t = "full";
|
||||
else
|
||||
return -EINVAL;
|
||||
const char *t = ASSERT_PTR(pressure_type_to_string(type));
|
||||
|
||||
r = fopen_unlocked(path, "re", &f);
|
||||
if (r < 0)
|
||||
@@ -131,6 +126,13 @@ static const char* const pressure_resource_table[_PRESSURE_RESOURCE_MAX] = {
|
||||
|
||||
DEFINE_STRING_TABLE_LOOKUP(pressure_resource, PressureResource);
|
||||
|
||||
static const char* const pressure_type_table[_PRESSURE_TYPE_MAX] = {
|
||||
[PRESSURE_TYPE_SOME] = "some",
|
||||
[PRESSURE_TYPE_FULL] = "full",
|
||||
};
|
||||
|
||||
DEFINE_STRING_TABLE_LOOKUP(pressure_type, PressureType);
|
||||
|
||||
int is_pressure_supported(void) {
|
||||
static thread_local int cached = -1;
|
||||
int r;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
typedef enum PressureType {
|
||||
PRESSURE_TYPE_SOME,
|
||||
PRESSURE_TYPE_FULL,
|
||||
_PRESSURE_TYPE_MAX,
|
||||
_PRESSURE_TYPE_INVALID = -EINVAL,
|
||||
} PressureType;
|
||||
|
||||
typedef enum PressureResource {
|
||||
@@ -50,6 +52,7 @@ static inline const PressureResourceInfo* pressure_resource_get_info(PressureRes
|
||||
}
|
||||
|
||||
DECLARE_STRING_TABLE_LOOKUP(pressure_resource, PressureResource);
|
||||
DECLARE_STRING_TABLE_LOOKUP(pressure_type, PressureType);
|
||||
|
||||
/* Default parameters for pressure watch logic in sd-event and PID 1 */
|
||||
#define PRESSURE_DEFAULT_TYPE "some"
|
||||
|
||||
@@ -161,8 +161,9 @@ int vl_method_list_candidate_devices(
|
||||
BLOCKDEV_LIST_SHOW_SYMLINKS|
|
||||
BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING|
|
||||
BLOCKDEV_LIST_IGNORE_ZRAM|
|
||||
BLOCKDEV_LIST_METADATA|
|
||||
BLOCKDEV_LIST_IGNORE_READ_ONLY|
|
||||
BLOCKDEV_LIST_METADATA|
|
||||
BLOCKDEV_LIST_DISKSEQ|
|
||||
(p.ignore_empty ? BLOCKDEV_LIST_IGNORE_EMPTY : 0)|
|
||||
(p.ignore_root ? BLOCKDEV_LIST_IGNORE_ROOT : 0);
|
||||
|
||||
|
||||
+274
-12
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "architecture.h"
|
||||
#include "blockdev-list.h"
|
||||
#include "confidential-virt.h"
|
||||
#include "cpu-set-util.h"
|
||||
#include "env-file.h"
|
||||
@@ -23,8 +24,13 @@
|
||||
#include "log.h"
|
||||
#include "metrics.h"
|
||||
#include "os-util.h"
|
||||
#include "path-util.h"
|
||||
#include "procfs-util.h"
|
||||
#include "psi-util.h"
|
||||
#include "report-basic.h"
|
||||
#include "string-util.h"
|
||||
#include "time-util.h"
|
||||
#include "utf8.h"
|
||||
#include "virt.h"
|
||||
|
||||
static int architecture_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) {
|
||||
@@ -124,6 +130,94 @@ static int physical_memory_generate(const MetricFamily *mf, sd_varlink *link, vo
|
||||
/* fields= */ NULL);
|
||||
}
|
||||
|
||||
static int memory_used_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) {
|
||||
int r;
|
||||
|
||||
assert(mf && mf->name);
|
||||
assert(link);
|
||||
|
||||
/* Memory currently in use, i.e. MemTotal − MemAvailable from /proc/meminfo. Note that
|
||||
* PhysicalMemoryBytes is not necessarily the matching total: it is capped by the root cgroup's
|
||||
* memory limit, while /proc/meminfo always shows the whole system. */
|
||||
|
||||
uint64_t used;
|
||||
r = procfs_memory_get_used(&used);
|
||||
if (r < 0) {
|
||||
log_warning_errno(r, "Failed to read memory usage from /proc/meminfo, ignoring: %m");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return metric_build_send_unsigned(
|
||||
mf,
|
||||
link,
|
||||
/* object= */ NULL,
|
||||
used,
|
||||
/* fields= */ NULL);
|
||||
}
|
||||
|
||||
static int pressure_generate(const MetricFamily mf[static 2], sd_varlink *link, void *userdata) {
|
||||
int r;
|
||||
|
||||
assert(mf && mf[0].name && mf[1].name);
|
||||
assert(!mf[1].generate);
|
||||
assert(link);
|
||||
|
||||
/* The kernel's smoothed pressure stall percentages over the last 10s, plus the cumulative stall
|
||||
* time, as documented in https://docs.kernel.org/accounting/psi.html */
|
||||
|
||||
for (PressureResource resource = 0; resource < _PRESSURE_RESOURCE_MAX; resource++) {
|
||||
const char *name = pressure_resource_to_string(resource);
|
||||
|
||||
_cleanup_free_ char *path = path_join("/proc/pressure", name);
|
||||
if (!path)
|
||||
return -ENOMEM;
|
||||
|
||||
PressureType type;
|
||||
FOREACH_ARGUMENT(type, PRESSURE_TYPE_SOME, PRESSURE_TYPE_FULL) {
|
||||
/* The "full" number for cpu pressure at the system level is always reported as 0,
|
||||
* so skip it. */
|
||||
if (resource == PRESSURE_CPU && type == PRESSURE_TYPE_FULL)
|
||||
continue;
|
||||
|
||||
ResourcePressure rp;
|
||||
r = read_resource_pressure(path, type, &rp);
|
||||
if (r < 0) {
|
||||
bool absent = IN_SET(r, -ENOENT, -ENODATA) || ERRNO_IS_NEG_NOT_SUPPORTED(r);
|
||||
log_full_errno(absent ? LOG_DEBUG : LOG_WARNING, r,
|
||||
"Failed to read %s pressure, ignoring: %m", name);
|
||||
break; /* If "some" can't be read, "full" won't work either */
|
||||
}
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *fields = NULL;
|
||||
r = sd_json_buildo(&fields,
|
||||
SD_JSON_BUILD_PAIR_STRING("resource", name),
|
||||
SD_JSON_BUILD_PAIR_STRING("type", pressure_type_to_string(type)));
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = metric_build_send_double(
|
||||
mf,
|
||||
link,
|
||||
/* object= */ NULL,
|
||||
(double) rp.avg10 / LOADAVG_FIXED_POINT_1_0,
|
||||
fields);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = metric_build_send_double(
|
||||
mf + 1,
|
||||
link,
|
||||
/* object= */ NULL,
|
||||
(double) rp.total / USEC_PER_SEC,
|
||||
fields);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cpus_online_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) {
|
||||
int r;
|
||||
|
||||
@@ -143,6 +237,65 @@ static int cpus_online_generate(const MetricFamily *mf, sd_varlink *link, void *
|
||||
/* fields= */ NULL);
|
||||
}
|
||||
|
||||
static int cpu_usage_generate(const MetricFamily mf[static 2], sd_varlink *link, void *userdata) {
|
||||
int r;
|
||||
|
||||
assert(mf && mf[0].name && mf[1].name);
|
||||
assert(!mf[1].generate);
|
||||
assert(link);
|
||||
|
||||
/* The aggregate "cpu" line of /proc/stat, converted from USER_HZ ticks to nanoseconds, matching
|
||||
* the unit of io.systemd.CGroup.CpuUsage. "nice" time is folded into "user" and "irq"/"softirq"
|
||||
* into "system", so that the published types together account for all CPU time. */
|
||||
|
||||
ProcfsCpuTicks ticks;
|
||||
r = procfs_cpu_get_ticks(&ticks);
|
||||
if (r < 0) {
|
||||
log_warning_errno(r, "Failed to read CPU time from /proc/stat, ignoring: %m");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct {
|
||||
const char *name;
|
||||
size_t family_offset;
|
||||
} attributes[] = {
|
||||
{ "user", 0 },
|
||||
{ "system", 0 },
|
||||
{ "idle", 1 },
|
||||
{ "iowait", 1 },
|
||||
{ "steal", 1 },
|
||||
};
|
||||
uint64_t values[] = {
|
||||
ticks.user + ticks.nice,
|
||||
ticks.system + ticks.irq + ticks.softirq,
|
||||
ticks.idle,
|
||||
ticks.iowait,
|
||||
ticks.steal,
|
||||
};
|
||||
assert_cc(ELEMENTSOF(attributes) == ELEMENTSOF(values));
|
||||
|
||||
for (size_t i = 0; i < ELEMENTSOF(attributes); i++) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *fields = NULL;
|
||||
|
||||
r = sd_json_buildo(&fields, SD_JSON_BUILD_PAIR_STRING("type", attributes[i].name));
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
uint64_t nsec = procfs_ticks_to_nsec(values[i]);
|
||||
|
||||
r = metric_build_send_unsigned(
|
||||
&mf[attributes[i].family_offset],
|
||||
link,
|
||||
/* object= */ NULL,
|
||||
nsec,
|
||||
fields);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_average_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) {
|
||||
enum {
|
||||
LOAD_AVERAGE_FIELD_1MIN,
|
||||
@@ -181,11 +334,14 @@ static int load_average_generate(const MetricFamily *mf, sd_varlink *link, void
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int swap_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) {
|
||||
assert(mf && mf->name);
|
||||
static int swap_generate(const MetricFamily mf[static 2], sd_varlink *link, void *userdata) {
|
||||
int r;
|
||||
|
||||
assert(mf && mf[0].name && mf[1].name);
|
||||
assert(!mf[1].generate);
|
||||
assert(link);
|
||||
|
||||
/* The total amount of configured swap space, in bytes. */
|
||||
/* The total amount of configured swap space and the amount currently in use, in bytes. */
|
||||
|
||||
struct sysinfo info;
|
||||
if (sysinfo(&info) < 0)
|
||||
@@ -193,15 +349,69 @@ static int swap_generate(const MetricFamily *mf, sd_varlink *link, void *userdat
|
||||
|
||||
/* Overflow is unrealistic (would need >16 EiB of swap), but use MUL_SAFE to make this obvious to
|
||||
* static analyzers. */
|
||||
uint64_t swap;
|
||||
assert_se(MUL_SAFE(&swap, (uint64_t) info.totalswap, (uint64_t) info.mem_unit));
|
||||
uint64_t total, used;
|
||||
assert_se(MUL_SAFE(&total, (uint64_t) info.totalswap, (uint64_t) info.mem_unit));
|
||||
assert_se(MUL_SAFE(&used, (uint64_t) LESS_BY(info.totalswap, info.freeswap), (uint64_t) info.mem_unit));
|
||||
|
||||
return metric_build_send_unsigned(
|
||||
mf,
|
||||
link,
|
||||
/* object= */ NULL,
|
||||
swap,
|
||||
/* fields= */ NULL);
|
||||
const uint64_t values[] = { total, used };
|
||||
for (size_t i = 0; i < ELEMENTSOF(values); i++) {
|
||||
r = metric_build_send_unsigned(
|
||||
mf + i,
|
||||
link,
|
||||
/* object= */ NULL,
|
||||
values[i],
|
||||
/* fields= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int disk_io_generate(const MetricFamily mf[static 2], sd_varlink *link, void *userdata) {
|
||||
int r;
|
||||
|
||||
assert(mf && mf[0].name && mf[1].name);
|
||||
assert(!mf[1].generate);
|
||||
assert(link);
|
||||
|
||||
/* Cumulative read/write byte counters of all whole physical block devices. Partitions are skipped,
|
||||
* their traffic is already accounted for in the parent device. Stacked devices (dm, md, …) are
|
||||
* skipped for the same reason: their traffic is accounted for again in the devices they are built
|
||||
* on. */
|
||||
|
||||
BlockDevice *devices = NULL;
|
||||
size_t n_devices = 0;
|
||||
CLEANUP_ARRAY(devices, n_devices, block_device_array_free);
|
||||
|
||||
r = blockdev_list(BLOCKDEV_LIST_IGNORE_VIRTUAL | BLOCKDEV_LIST_IGNORE_PARTITIONS | BLOCKDEV_LIST_RW_STATS,
|
||||
&devices, &n_devices);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
FOREACH_ARRAY(d, devices, n_devices) {
|
||||
/* A non-UTF-8 name would make the JSON reply builder fail, taking down the whole method
|
||||
* call rather than just this device. */
|
||||
if (!utf8_is_valid(d->node)) {
|
||||
log_warning("%s: block device name is not valid UTF-8, ignoring device.", d->node);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t values[] = { d->read_bytes, d->write_bytes };
|
||||
for (size_t i = 0; i < ELEMENTSOF(values); i++)
|
||||
if (values[i] != UINT64_MAX) {
|
||||
r = metric_build_send_unsigned(
|
||||
mf + i,
|
||||
link,
|
||||
/* object= */ d->node,
|
||||
values[i],
|
||||
/* fields= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
enum {
|
||||
@@ -522,7 +732,7 @@ static int virtualization_generate(const MetricFamily *mf, sd_varlink *link, voi
|
||||
}
|
||||
|
||||
static const MetricFamily metric_family_table[] = {
|
||||
/* Keep entries ordered alphabetically */
|
||||
/* Keep entries (with .generate) ordered alphabetically */
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "Architecture",
|
||||
"CPU architecture",
|
||||
@@ -547,6 +757,34 @@ static const MetricFamily metric_family_table[] = {
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
.generate = cpus_online_generate,
|
||||
},
|
||||
/* METRIC_FAMILY_TYPE_COUNTER must be monotonic, but steal and iowait can go down,
|
||||
* so CPUUsage is split into two families of different types. */
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "CPUUsage",
|
||||
"Aggregate CPU usage across all CPUs in nanoseconds (type=user|system)",
|
||||
METRIC_FAMILY_TYPE_COUNTER,
|
||||
.generate = cpu_usage_generate,
|
||||
},
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "CPUWait",
|
||||
"Aggregate CPU wait across all CPUs in nanoseconds (type=idle|iowait|steal)",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
},
|
||||
/* Keep those ↑ in sync with cpu_usage_generate(). */
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "DiskReadBytes",
|
||||
"Per block device metric: cumulative number of bytes read "
|
||||
"(partitions, stacked and virtual devices are excluded)",
|
||||
METRIC_FAMILY_TYPE_COUNTER,
|
||||
.generate = disk_io_generate,
|
||||
},
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "DiskWriteBytes",
|
||||
"Per block device metric: cumulative number of bytes written "
|
||||
"(partitions, stacked and virtual devices are excluded)",
|
||||
METRIC_FAMILY_TYPE_COUNTER,
|
||||
},
|
||||
/* Keep those ↑ in sync with disk_io_generate(). */
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "Hostname",
|
||||
"System hostname",
|
||||
@@ -592,6 +830,12 @@ static const MetricFamily metric_family_table[] = {
|
||||
MACHINE_INFO_STANDARD_FIELD("LOCATION"),
|
||||
MACHINE_INFO_STANDARD_FIELD("TAGS"),
|
||||
/* Keep those ↑ in sync with machine_info_generate(). */
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "MemoryUsedBytes",
|
||||
"Memory currently in use (MemTotal minus MemAvailable) in bytes",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
.generate = memory_used_generate,
|
||||
},
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "OSRelease.NAME",
|
||||
"Operating system human-readable name (PRETTY_NAME= or NAME= field from os-release)",
|
||||
@@ -616,6 +860,18 @@ static const MetricFamily metric_family_table[] = {
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
.generate = physical_memory_generate,
|
||||
},
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "PressureAvg10",
|
||||
"Pressure stall percentage over the last 10s (resource=cpu|memory|io, type=some|full)",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
.generate = pressure_generate,
|
||||
},
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "PressureStallSeconds",
|
||||
"Cumulative time stalled on a resource, in seconds (resource=cpu|memory|io, type=some|full)",
|
||||
METRIC_FAMILY_TYPE_COUNTER,
|
||||
},
|
||||
/* Keep those ↑ in sync with pressure_generate(). */
|
||||
{
|
||||
/* NB: Here we use the naming of the field as per SMBIOS specification, i.e. undo the weird
|
||||
* renaming that Linux did on the fields. When new fields are added here, please make sure to
|
||||
@@ -650,6 +906,12 @@ static const MetricFamily metric_family_table[] = {
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
.generate = swap_generate,
|
||||
},
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "SwapUsedBytes",
|
||||
"Swap space currently in use in bytes",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
},
|
||||
/* Keep those ↑ in sync with swap_generate(). */
|
||||
{
|
||||
METRIC_IO_SYSTEMD_BASIC_PREFIX "TPM2.Manufacturer",
|
||||
"TPM2 device manufacturer (ID_TPM2_MANUFACTURER property of the tpmrm0 device)",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "device-util.h"
|
||||
#include "devnum-util.h"
|
||||
#include "errno-util.h"
|
||||
#include "path-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
@@ -166,6 +167,29 @@ int blockdev_list_one(
|
||||
goto no_match;
|
||||
}
|
||||
|
||||
if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_VIRTUAL)) {
|
||||
const char *devpath;
|
||||
|
||||
r = sd_device_get_devpath(dev, &devpath);
|
||||
if (r < 0) {
|
||||
log_device_warning_errno(dev, r, "Failed to acquire device path of discovered block device '%s', ignoring: %m", node);
|
||||
goto skipped;
|
||||
}
|
||||
|
||||
if (path_startswith(devpath, "/devices/virtual/"))
|
||||
goto no_match;
|
||||
}
|
||||
|
||||
if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_PARTITIONS)) {
|
||||
r = device_is_devtype(dev, "partition");
|
||||
if (r < 0) {
|
||||
log_device_warning_errno(dev, r, "Failed to check if device is a partition '%s', ignoring: %m", node);
|
||||
goto skipped;
|
||||
}
|
||||
if (r > 0)
|
||||
goto no_match;
|
||||
}
|
||||
|
||||
if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_ZRAM)) {
|
||||
r = device_sysname_startswith(dev, "zram");
|
||||
if (r < 0) {
|
||||
@@ -255,9 +279,35 @@ int blockdev_list_one(
|
||||
}
|
||||
|
||||
uint64_t diskseq = UINT64_MAX;
|
||||
r = sd_device_get_diskseq(dev, &diskseq);
|
||||
if (r < 0)
|
||||
log_device_debug_errno(dev, r, "Failed to acquire diskseq of device '%s', ignoring: %m", node);
|
||||
if (FLAGS_SET(flags, BLOCKDEV_LIST_DISKSEQ)) {
|
||||
r = sd_device_get_diskseq(dev, &diskseq);
|
||||
if (r < 0)
|
||||
log_device_debug_errno(dev, r, "Failed to acquire diskseq of device '%s', ignoring: %m", node);
|
||||
}
|
||||
|
||||
uint64_t read_bytes, write_bytes;
|
||||
bool bytes_good = false;
|
||||
if (FLAGS_SET(flags, BLOCKDEV_LIST_RW_STATS)) {
|
||||
const char *v;
|
||||
|
||||
r = sd_device_get_sysattr_value(dev, "stat", &v);
|
||||
if (r < 0)
|
||||
log_device_full_errno(dev, ERRNO_IS_NEG_DEVICE_ABSENT(r) ? LOG_DEBUG : LOG_WARNING,
|
||||
r, "%s: failed to read 'stat' attribute, ignoring: %m", node);
|
||||
else {
|
||||
/* Fields 3 and 7: the number of sectors read and written. The "stat" attribute
|
||||
* always counts in 512 byte sectors, regardless of the device's logical block size.
|
||||
* The skipped fields are consumed with %*s, since their values may exceed any fixed
|
||||
* integer width. */
|
||||
if (sscanf(v, "%*s %*s %"SCNu64" %*s %*s %*s %"SCNu64, &read_bytes, &write_bytes) < 2)
|
||||
log_device_warning(dev, "%s: failed to parse 'stat' attribute.", node);
|
||||
else if (!MUL_SAFE(&read_bytes, read_bytes, UINT64_C(512)) ||
|
||||
!MUL_SAFE(&write_bytes, write_bytes, UINT64_C(512)))
|
||||
log_device_debug(dev, "%s: sector counters out of range, ignoring.", node);
|
||||
else
|
||||
bytes_good = true;
|
||||
}
|
||||
}
|
||||
|
||||
_cleanup_free_ char *m = strdup(node);
|
||||
if (!m)
|
||||
@@ -266,11 +316,13 @@ int blockdev_list_one(
|
||||
*ret = (BlockDevice) {
|
||||
.node = TAKE_PTR(m),
|
||||
.symlinks = TAKE_PTR(list),
|
||||
.diskseq = diskseq,
|
||||
.size = size,
|
||||
.model = TAKE_PTR(model),
|
||||
.vendor = TAKE_PTR(vendor),
|
||||
.subsystem = TAKE_PTR(subsystem),
|
||||
.diskseq = diskseq,
|
||||
.size = size,
|
||||
.read_bytes = bytes_good ? read_bytes : UINT64_MAX,
|
||||
.write_bytes = bytes_good ? write_bytes : UINT64_MAX,
|
||||
.read_only = ro,
|
||||
};
|
||||
|
||||
@@ -311,6 +363,13 @@ int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *re
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to add subsystem match: %m");
|
||||
|
||||
if (!(flags & (BLOCKDEV_LIST_REQUIRE_LUKS | BLOCKDEV_LIST_SHOW_SYMLINKS | BLOCKDEV_LIST_METADATA))) {
|
||||
/* If everything we need comes from the device name or sysfs directly, no udev db entry is needed. */
|
||||
r = sd_device_enumerator_allow_uninitialized(e);
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to allow uninitialized devices, ignoring: %m");
|
||||
}
|
||||
|
||||
if (FLAGS_SET(flags, BLOCKDEV_LIST_REQUIRE_LUKS)) {
|
||||
/* blockdev_list_one() enforces this filter authoritatively; the enumerator match is just
|
||||
* an optimization so we don't iterate non-LUKS devices here. */
|
||||
|
||||
+20
-12
@@ -4,14 +4,18 @@
|
||||
#include "forward.h"
|
||||
|
||||
typedef enum BlockDevListFlags {
|
||||
BLOCKDEV_LIST_SHOW_SYMLINKS = 1 << 0, /* Pick up symlinks to block devices too */
|
||||
BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING = 1 << 1, /* Only consider block devices with partition scanning */
|
||||
BLOCKDEV_LIST_IGNORE_ZRAM = 1 << 2, /* Ignore ZRAM */
|
||||
BLOCKDEV_LIST_REQUIRE_LUKS = 1 << 3, /* Only consider block devices with LUKS superblocks */
|
||||
BLOCKDEV_LIST_IGNORE_ROOT = 1 << 4, /* Ignore the block device we are currently booted from */
|
||||
BLOCKDEV_LIST_IGNORE_EMPTY = 1 << 5, /* Ignore disks of zero size (usually drives without a medium) */
|
||||
BLOCKDEV_LIST_METADATA = 1 << 6, /* Fill in model, vendor, subsystem, read_only */
|
||||
BLOCKDEV_LIST_IGNORE_READ_ONLY = 1 << 7, /* Ignore read-only block devices */
|
||||
BLOCKDEV_LIST_SHOW_SYMLINKS = 1 << 0, /* Pick up symlinks to block devices too */
|
||||
BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING = 1 << 1, /* Only consider block devices with partition scanning */
|
||||
BLOCKDEV_LIST_IGNORE_VIRTUAL = 1 << 2, /* Ignore virtual devices (devices under /sys/devices/virtual/) */
|
||||
BLOCKDEV_LIST_IGNORE_PARTITIONS = 1 << 3, /* Ignore partitions */
|
||||
BLOCKDEV_LIST_IGNORE_ZRAM = 1 << 4, /* Ignore ZRAM */
|
||||
BLOCKDEV_LIST_IGNORE_ROOT = 1 << 5, /* Ignore the block device we are currently booted from */
|
||||
BLOCKDEV_LIST_IGNORE_EMPTY = 1 << 6, /* Ignore disks of zero size (usually drives without a medium) */
|
||||
BLOCKDEV_LIST_IGNORE_READ_ONLY = 1 << 7, /* Ignore read-only block devices */
|
||||
BLOCKDEV_LIST_REQUIRE_LUKS = 1 << 8, /* Only consider block devices with LUKS superblocks */
|
||||
BLOCKDEV_LIST_METADATA = 1 << 9, /* Fill in model, vendor, subsystem, read_only */
|
||||
BLOCKDEV_LIST_DISKSEQ = 1 << 10, /* Fill in the diskseq field */
|
||||
BLOCKDEV_LIST_RW_STATS = 1 << 11, /* Fill in read and write measurements */
|
||||
} BlockDevListFlags;
|
||||
|
||||
/* The "dynamic" filters – ones the kernel can flip at runtime via sysattrs on a live device.
|
||||
@@ -28,13 +32,17 @@ typedef struct BlockDevice {
|
||||
char *subsystem;
|
||||
uint64_t diskseq;
|
||||
uint64_t size; /* in bytes */
|
||||
uint64_t read_bytes;
|
||||
uint64_t write_bytes;
|
||||
int read_only;
|
||||
} BlockDevice;
|
||||
|
||||
#define BLOCK_DEVICE_NULL (BlockDevice) { \
|
||||
.diskseq = UINT64_MAX, \
|
||||
.size = UINT64_MAX, \
|
||||
.read_only = -1, \
|
||||
#define BLOCK_DEVICE_NULL (BlockDevice) { \
|
||||
.diskseq = UINT64_MAX, \
|
||||
.size = UINT64_MAX, \
|
||||
.read_bytes = UINT64_MAX, \
|
||||
.write_bytes = UINT64_MAX, \
|
||||
.read_only = -1, \
|
||||
}
|
||||
|
||||
/* Return values of blockdev_list_one(). Not its own type – returned as int. */
|
||||
|
||||
+25
-19
@@ -15,37 +15,43 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
test_setup_logging(LOG_DEBUG);
|
||||
|
||||
assert_se(procfs_cpu_get_usage(&nsec) >= 0);
|
||||
ASSERT_OK(procfs_cpu_get_usage(&nsec));
|
||||
log_info("Current system CPU time: %s", FORMAT_TIMESPAN(nsec/NSEC_PER_USEC, 1));
|
||||
|
||||
assert_se(procfs_memory_get_used(&v) >= 0);
|
||||
ProcfsCpuTicks ticks;
|
||||
ASSERT_OK(procfs_cpu_get_ticks(&ticks));
|
||||
log_info("Current CPU ticks: user=%" PRIu64 " nice=%" PRIu64 " system=%" PRIu64 " idle=%" PRIu64
|
||||
" iowait=%" PRIu64 " irq=%" PRIu64 " softirq=%" PRIu64 " steal=%" PRIu64,
|
||||
ticks.user, ticks.nice, ticks.system, ticks.idle,
|
||||
ticks.iowait, ticks.irq, ticks.softirq, ticks.steal);
|
||||
ASSERT_GT(ticks.idle, 0U);
|
||||
|
||||
ASSERT_OK(procfs_memory_get_used(&v));
|
||||
log_info("Current memory usage: %s", FORMAT_BYTES(v));
|
||||
|
||||
assert_se(procfs_tasks_get_current(&v) >= 0);
|
||||
ASSERT_OK(procfs_tasks_get_current(&v));
|
||||
log_info("Current number of tasks: %" PRIu64, v);
|
||||
|
||||
pid_max = TASKS_MAX;
|
||||
r = procfs_get_pid_max(&pid_max);
|
||||
if (r == -ENOENT || ERRNO_IS_NEG_PRIVILEGE(r))
|
||||
return log_tests_skipped_errno(r, "can't get pid max");
|
||||
assert(r >= 0);
|
||||
ASSERT_OK(r);
|
||||
log_info("kernel.pid_max: %"PRIu64, pid_max);
|
||||
|
||||
threads_max = TASKS_MAX;
|
||||
r = procfs_get_threads_max(&threads_max);
|
||||
if (r == -ENOENT || ERRNO_IS_NEG_PRIVILEGE(r))
|
||||
return log_tests_skipped_errno(r, "can't get threads max");
|
||||
assert(r >= 0);
|
||||
ASSERT_OK(r);
|
||||
log_info("kernel.threads-max: %"PRIu64, threads_max);
|
||||
|
||||
limit = MIN(pid_max - (pid_max > 0), threads_max);
|
||||
|
||||
assert_se(r >= 0);
|
||||
log_info("Limit of tasks: %" PRIu64, limit);
|
||||
assert_se(limit > 0);
|
||||
ASSERT_GT(limit, 0U);
|
||||
|
||||
/* This call should never fail, as we're trying to set it to the same limit */
|
||||
assert(procfs_tasks_set_limit(limit) >= 0);
|
||||
ASSERT_OK(procfs_tasks_set_limit(limit));
|
||||
|
||||
if (limit > 100) {
|
||||
log_info("Reducing limit by one to %"PRIu64"…", limit-1);
|
||||
@@ -53,22 +59,22 @@ int main(int argc, char *argv[]) {
|
||||
r = procfs_tasks_set_limit(limit-1);
|
||||
if (r == -ENOENT || ERRNO_IS_NEG_FS_WRITE_REFUSED(r))
|
||||
return log_tests_skipped_errno(r, "can't set tasks limit");
|
||||
assert_se(r >= 0);
|
||||
ASSERT_OK(r);
|
||||
|
||||
assert_se(procfs_get_pid_max(&v) >= 0);
|
||||
ASSERT_OK(procfs_get_pid_max(&v));
|
||||
/* We never decrease the pid_max, so it shouldn't have changed */
|
||||
assert_se(v == pid_max);
|
||||
ASSERT_EQ(v, pid_max);
|
||||
|
||||
assert_se(procfs_get_threads_max(&v) >= 0);
|
||||
assert_se(v == limit-1);
|
||||
ASSERT_OK(procfs_get_threads_max(&v));
|
||||
ASSERT_EQ(v, limit - 1);
|
||||
|
||||
assert_se(procfs_tasks_set_limit(limit) >= 0);
|
||||
ASSERT_OK(procfs_tasks_set_limit(limit));
|
||||
|
||||
assert_se(procfs_get_pid_max(&v) >= 0);
|
||||
assert_se(v == pid_max);
|
||||
ASSERT_OK(procfs_get_pid_max(&v));
|
||||
ASSERT_EQ(v, pid_max);
|
||||
|
||||
assert_se(procfs_get_threads_max(&v) >= 0);
|
||||
assert_se(v == limit);
|
||||
ASSERT_OK(procfs_get_threads_max(&v));
|
||||
ASSERT_EQ(v, limit);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
set -eux
|
||||
set -o pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# Unset $PAGER so we don't have to use --no-pager everywhere
|
||||
export PAGER=
|
||||
@@ -77,20 +78,99 @@ id2="$(. /etc/os-release; echo "$ID")"
|
||||
basic_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.Basic io.systemd.Metrics.List {})"
|
||||
# NB: '| tostring' turns the numeric value into a raw string, so 'jq -r' prints it cleanly
|
||||
# (numbers, unlike strings, are otherwise still emitted with the json-seq record separator).
|
||||
basic_value() { echo "$basic_metrics" | jq --seq -r "select(.name == \"$1\") | .value | tostring"; }
|
||||
basic_value() { echo "$basic_metrics" | jq --seq -r "select(.name == \"io.systemd.Basic.$1\") | .value | tostring"; }
|
||||
# Like basic_value, but the value must be a JSON number (jq's 'numbers' filter emits the value
|
||||
# only if it is one, so a non-empty result confirms both presence and type), and an optional
|
||||
# second argument gives a jq condition further restricting the match, e.g. on .object or .fields.
|
||||
basic_number() { echo "$basic_metrics" | jq --seq -r "select(.name == \"io.systemd.Basic.$1\" and ${2:-true}) | .value | numbers | tostring"; }
|
||||
|
||||
# The three classic load average fields must be present and numeric (jq's 'numbers' filter
|
||||
# emits the value only if it is a JSON number, so a non-empty result confirms both).
|
||||
# The three classic load average fields must be present and numeric.
|
||||
for field in LoadAverage1Min LoadAverage5Min LoadAverage15Min; do
|
||||
loadavg="$(echo "$basic_metrics" | jq --seq -r "select(.name == \"io.systemd.Basic.$field\") | .value | numbers | tostring")"
|
||||
test -n "$loadavg"
|
||||
test -n "$(basic_number "$field")"
|
||||
done
|
||||
|
||||
# SwapBytes must match the total the kernel reports in /proc/meminfo (which is in kB).
|
||||
swap_reported="$(basic_value io.systemd.Basic.SwapBytes)"
|
||||
swap_reported="$(basic_value SwapBytes)"
|
||||
swap_expected=$(( $(awk '/^SwapTotal:/ { print $2; found=1 } END { if (!found) print 0 }' /proc/meminfo) * 1024 ))
|
||||
[ "$swap_reported" = "$swap_expected" ]
|
||||
|
||||
# SwapUsedBytes must be numeric and cannot exceed the configured total.
|
||||
swap_used="$(basic_number SwapUsedBytes)"
|
||||
test -n "$swap_used"
|
||||
[ "$swap_used" -le "$swap_reported" ]
|
||||
|
||||
# Those fields must be reported with a numeric nanoseconds value.
|
||||
for type in user system; do
|
||||
test -n "$(basic_number CPUUsage ".fields.type == \"$type\"")"
|
||||
done
|
||||
|
||||
# The values are USER_HZ ticks converted to nanoseconds. The metrics were sampled before the
|
||||
# current /proc/stat state, and the idle counter mostly grows, so the reported idle time
|
||||
# should be positive and no larger than what /proc/stat shows now with some margin.
|
||||
cpu_idle_reported="$(basic_number CPUWait '.fields.type == "idle"')"
|
||||
awk -v hz="$(getconf CLK_TCK)" -v sampled="$cpu_idle_reported" \
|
||||
'/^cpu / { exit !(sampled > 0 && sampled <= ($5 + 1) / hz * 1e9 ) }' /proc/stat
|
||||
|
||||
# iowait/idle/steal must be valid nanoseconds values, but can go up and down.
|
||||
for type in idle iowait steal; do
|
||||
test -n "$(basic_number CPUWait ".fields.type == \"$type\"")"
|
||||
done
|
||||
|
||||
# MemoryUsedBytes is MemTotal − MemAvailable, so it cannot exceed the MemTotal reported by the
|
||||
# same file. (PhysicalMemoryBytes is not a valid bound here: it is capped by the root cgroup's
|
||||
# memory limit, so it can legitimately be smaller than the value computed from /proc/meminfo.)
|
||||
mem_used="$(basic_number MemoryUsedBytes)"
|
||||
mem_total=$(( $(awk '/^MemTotal:/ { print $2 }' /proc/meminfo) * 1024 ))
|
||||
test -n "$mem_used"
|
||||
[ "$mem_used" -le "$mem_total" ]
|
||||
|
||||
# Pressure metrics are only reported if the kernel supports PSI.
|
||||
if cat /proc/pressure/cpu >/dev/null 2>&1; then
|
||||
for family in PressureAvg10 PressureStallSeconds; do
|
||||
for resource in cpu memory io; do
|
||||
test -n "$(basic_number "$family" ".fields.resource == \"$resource\" and .fields.type == \"some\"")"
|
||||
# "full" is deliberately not emitted for cpu, where the kernel always reports 0.
|
||||
full="$(basic_number "$family" ".fields.resource == \"$resource\" and .fields.type == \"full\"")"
|
||||
if [ "$resource" = cpu ]; then
|
||||
test -z "$full"
|
||||
else
|
||||
test -n "$full"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# The avg10 values are percentages, so they must lie in the 0…100 range. This would catch a
|
||||
# wrong fixed-point conversion, which would inflate the value 2048x.
|
||||
echo "$basic_metrics" | jq --seq -r 'select(.name == "io.systemd.Basic.PressureAvg10") | .value | tostring' |
|
||||
awk '{ if ($1 < 0 || $1 > 100) exit 1 }'
|
||||
fi
|
||||
|
||||
# Every whole physical block device (not partitions, not virtual or stacked devices) must be
|
||||
# reported, with byte values derived from the 512-byte sector counters in sysfs. Those counters
|
||||
# only grow, so the values sampled above cannot exceed what sysfs shows now.
|
||||
for dev in /sys/block/*; do
|
||||
devpath="$(udevadm info --query=property --value --property=DEVPATH "$dev" || :)"
|
||||
devname="$(udevadm info --query=property --value --property=DEVNAME "$dev" || :)"
|
||||
[[ -n "$devpath" && -n "$devname" ]] || continue
|
||||
|
||||
[[ "$devpath" =~ "/devices/virtual/" ]] && continue
|
||||
|
||||
read_bytes="$(basic_number DiskReadBytes ".object == \"${devname}\"")"
|
||||
write_bytes="$(basic_number DiskWriteBytes ".object == \"${devname}\"")"
|
||||
test -n "$read_bytes"
|
||||
test -n "$write_bytes"
|
||||
|
||||
read -r _ _ read_sectors _ _ _ write_sectors _ <"$dev/stat"
|
||||
[ "$read_bytes" -le "$(( read_sectors * 512 ))" ]
|
||||
[ "$write_bytes" -le "$(( write_sectors * 512 ))" ]
|
||||
done
|
||||
|
||||
# Every new metric family must be described, even those whose values depend on the environment.
|
||||
basic_describe="$(varlinkctl call --more /run/systemd/report/io.systemd.Basic io.systemd.Metrics.Describe {})"
|
||||
for name in CPUUsage CPUWait DiskReadBytes DiskWriteBytes MemoryUsedBytes PressureAvg10 PressureStallSeconds SwapUsedBytes; do
|
||||
echo "$basic_describe" | grep -F "io.systemd.Basic.$name" >/dev/null
|
||||
done
|
||||
|
||||
# io.systemd.Manager.Version should be non-empty and match what `systemctl --version` reports
|
||||
metrics_version="$(varlinkctl call --more /run/systemd/report/io.systemd.Manager io.systemd.Metrics.List {} | jq --seq -r 'select(.name == "io.systemd.Manager.Version") | .value')"
|
||||
[ -n "$metrics_version" ]
|
||||
|
||||
Reference in New Issue
Block a user