]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
report: report user and system CPU time per cgroup
authorYaping Li <202858510+YapingLi04@users.noreply.github.com>
Wed, 29 Apr 2026 22:17:22 +0000 (15:17 -0700)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Thu, 30 Apr 2026 18:08:39 +0000 (20:08 +0200)
Extend io.systemd.CGroup.CpuUsage from a single per-unit nanosecond
counter to three rows distinguished by a "type" field of "total",
"user", or "system". The values come from cpu.stat's usage_usec,
user_usec and system_usec keys, read in a single keyed-attribute
fetch and cached on each CGroupInfo so each scrape only opens
cpu.stat once per cgroup.

src/report/report-cgroup.c
test/units/TEST-74-AUX-UTILS.report.sh

index c3dabe41b1016d48a380c3ccd7645eddee29783f..9a52c03d1774140208fa6d47aeed9ccb64ee6421 100644 (file)
@@ -22,6 +22,10 @@ typedef struct CGroupInfo {
         uint64_t io_rbytes;
         uint64_t io_rios;
         int io_stat_cached; /* 0 = not attempted, > 0 = cached, < 0 = -errno */
+        uint64_t cpu_total_nsec;
+        uint64_t cpu_user_nsec;
+        uint64_t cpu_system_nsec;
+        int cpu_stat_cached; /* 0 = not attempted, > 0 = cached, < 0 = -errno */
 } CGroupInfo;
 
 static CGroupInfo *cgroup_info_free(CGroupInfo *info) {
@@ -154,6 +158,89 @@ static int walk_cgroups(CGroupContext *ctx, CGroupInfo ***ret, size_t *ret_n) {
         return 0;
 }
 
+/* Parse cpu.stat for a cgroup once, extracting usage_usec, user_usec and system_usec
+ * in a single read so each scrape only opens the file once per cgroup. */
+static int cpu_stat_parse(
+                const char *cgroup_path,
+                uint64_t *ret_total_nsec,
+                uint64_t *ret_user_nsec,
+                uint64_t *ret_system_nsec) {
+
+        char *values[3] = {};
+        uint64_t total_us, user_us, system_us;
+        int r;
+
+        assert(cgroup_path);
+        assert(ret_total_nsec);
+        assert(ret_user_nsec);
+        assert(ret_system_nsec);
+
+        r = cg_get_keyed_attribute(
+                        cgroup_path,
+                        "cpu.stat",
+                        STRV_MAKE("usage_usec", "user_usec", "system_usec"),
+                        values);
+        if (r < 0)
+                return r;
+
+        r = safe_atou64(values[0], &total_us);
+        if (r >= 0)
+                r = safe_atou64(values[1], &user_us);
+        if (r >= 0)
+                r = safe_atou64(values[2], &system_us);
+
+        free_many_charp(values, ELEMENTSOF(values));
+        if (r < 0)
+                return r;
+
+        *ret_total_nsec = total_us * NSEC_PER_USEC;
+        *ret_user_nsec = user_us * NSEC_PER_USEC;
+        *ret_system_nsec = system_us * NSEC_PER_USEC;
+        return 0;
+}
+
+static int ensure_cpu_stat_cached(CGroupInfo *info) {
+        int r;
+
+        assert(info);
+
+        if (info->cpu_stat_cached > 0)
+                return 0;
+        if (info->cpu_stat_cached < 0)
+                return info->cpu_stat_cached;
+
+        r = cpu_stat_parse(info->path, &info->cpu_total_nsec, &info->cpu_user_nsec, &info->cpu_system_nsec);
+        if (r < 0) {
+                if (r != -ENOENT)
+                        log_debug_errno(r, "Failed to parse cpu.stat for '%s': %m", info->path);
+                info->cpu_stat_cached = r;
+                return r;
+        }
+
+        info->cpu_stat_cached = 1;
+        return 0;
+}
+
+static int cpu_usage_send_one(
+                MetricFamilyContext *context,
+                const char *unit,
+                uint64_t value_nsec,
+                const char *type) {
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *fields = NULL;
+        int r;
+
+        assert(context);
+        assert(unit);
+        assert(type);
+
+        r = sd_json_buildo(&fields, SD_JSON_BUILD_PAIR_STRING("type", type));
+        if (r < 0)
+                return r;
+
+        return metric_build_send_unsigned(context, unit, value_nsec, fields);
+}
+
 static int cpu_usage_build_json(MetricFamilyContext *context, void *userdata) {
         CGroupContext *ctx = ASSERT_PTR(userdata);
         CGroupInfo **cgroups;
@@ -167,17 +254,18 @@ static int cpu_usage_build_json(MetricFamilyContext *context, void *userdata) {
                 return 0; /* Skip metric on failure */
 
         FOREACH_ARRAY(c, cgroups, n_cgroups) {
-                uint64_t us;
+                if (ensure_cpu_stat_cached(*c) < 0)
+                        continue;
 
-                r = cg_get_keyed_attribute_uint64((*c)->path, "cpu.stat", "usage_usec", &us);
+                r = cpu_usage_send_one(context, (*c)->unit, (*c)->cpu_total_nsec, "total");
                 if (r < 0)
-                        continue;
+                        return r;
 
-                r = metric_build_send_unsigned(
-                                context,
-                                (*c)->unit,
-                                us * NSEC_PER_USEC,
-                                /* fields= */ NULL);
+                r = cpu_usage_send_one(context, (*c)->unit, (*c)->cpu_user_nsec, "user");
+                if (r < 0)
+                        return r;
+
+                r = cpu_usage_send_one(context, (*c)->unit, (*c)->cpu_system_nsec, "system");
                 if (r < 0)
                         return r;
         }
@@ -451,7 +539,7 @@ static const MetricFamily cgroup_metric_family_table[] = {
         /* Keep metrics ordered alphabetically */
         {
                 .name = METRIC_IO_SYSTEMD_CGROUP_PREFIX "CpuUsage",
-                .description = "Per unit metric: CPU usage in nanoseconds",
+                .description = "Per unit metric: CPU usage in nanoseconds (type=total|user|system)",
                 .type = METRIC_FAMILY_TYPE_COUNTER,
                 .generate = cpu_usage_build_json,
         },
index 53b83c4dd94779f88b09d9f3aef9567ca34651bc..8bca8447f3e3f7f96a49879d418d55c80bd91265 100755 (executable)
@@ -37,6 +37,13 @@ varlinkctl list-methods /run/systemd/report/io.systemd.CGroup
 varlinkctl --more call /run/systemd/report/io.systemd.CGroup io.systemd.Metrics.List {}
 varlinkctl --more call /run/systemd/report/io.systemd.CGroup io.systemd.Metrics.Describe {}
 
+# CpuUsage emits one row per (cgroup, type) where type is total, user, or system.
+# Confirm all three are present.
+cgroup_metrics=$(varlinkctl --more --json=short call /run/systemd/report/io.systemd.CGroup io.systemd.Metrics.List {})
+echo "$cgroup_metrics" | grep '"name":"io.systemd.CGroup.CpuUsage"' | grep '"type":"total"' >/dev/null
+echo "$cgroup_metrics" | grep '"name":"io.systemd.CGroup.CpuUsage"' | grep '"type":"user"' >/dev/null
+echo "$cgroup_metrics" | grep '"name":"io.systemd.CGroup.CpuUsage"' | grep '"type":"system"' >/dev/null
+
 # test io.systemd.Network Metrics
 varlinkctl info /run/systemd/report/io.systemd.Network
 varlinkctl list-methods /run/systemd/report/io.systemd.Network