From 1d85012b538ab191e1560936acc00bcfd44e4c57 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Mon, 1 Jun 2026 14:05:35 +0200 Subject: [PATCH] quickshell: collapse k8s-metrics into a single jq pass Feed the three kubectl outputs into one jq program that does app aggregation, CPU/memory unit normalization, and quota math, replacing the per-pod jq fork loops and eight quota jq reads (~40 forks down to ~4). --- .../scripts/executable_k8s-metrics.sh | 228 ++++++------------ 1 file changed, 76 insertions(+), 152 deletions(-) diff --git a/dot_config/quickshell/scripts/executable_k8s-metrics.sh b/dot_config/quickshell/scripts/executable_k8s-metrics.sh index 7c7fbea..17044be 100644 --- a/dot_config/quickshell/scripts/executable_k8s-metrics.sh +++ b/dot_config/quickshell/scripts/executable_k8s-metrics.sh @@ -1,162 +1,86 @@ #!/usr/bin/env bash # Outputs per-pod CPU/MEM metrics and namespace quota as JSON. # Usage: k8s-metrics.sh +# +# All aggregation and unit normalization happen in a single jq pass — the three +# kubectl calls below are the only subprocesses besides jq itself. set -euo pipefail NS="${1:-tenant-5}" -# ── Unit normalization ─────────────────────────────────────────────────────── - -# Normalize CPU string to millicores integer -normalize_cpu() { - local val="$1" - if [[ "$val" =~ ^([0-9]+)m$ ]]; then - echo "${BASH_REMATCH[1]}" - elif [[ "$val" =~ ^([0-9]+(\.[0-9]+)?)$ ]]; then - awk "BEGIN { printf \"%d\", ${BASH_REMATCH[1]} * 1000 }" - else - echo 0 - fi -} - -# Normalize memory string to MiB integer -normalize_mem() { - local val="$1" - if [[ "$val" =~ ^([0-9]+)Gi$ ]]; then - echo $(( ${BASH_REMATCH[1]} * 1024 )) - elif [[ "$val" =~ ^([0-9]+)Mi$ ]]; then - echo "${BASH_REMATCH[1]}" - elif [[ "$val" =~ ^([0-9]+)Ki$ ]]; then - echo $(( ${BASH_REMATCH[1]} / 1024 )) - elif [[ "$val" =~ ^([0-9]+)$ ]]; then - echo $(( ${BASH_REMATCH[1]} / 1048576 )) - else - echo 0 - fi -} - -# Format millicores for display (always "Xm") -fmt_cpu() { - local m="$1" - echo "${m}m" -} - -# Format MiB for display ("343Mi" or "1.50Gi") -fmt_mem() { - local mib="$1" - if [[ $mib -lt 1024 ]]; then - echo "${mib}Mi" - else - awk "BEGIN { printf \"%.2fGi\", $mib / 1024 }" - fi -} - -# ── Fetch data ─────────────────────────────────────────────────────────────── - -pods_json=$(kubectl get pods -n "$NS" -o json 2>/dev/null) || exit 1 -top_output=$(kubectl top pods -n "$NS" --no-headers 2>/dev/null) || top_output="" +pods_json=$(kubectl get pods -n "$NS" -o json 2>/dev/null) || exit 1 +top_output=$(kubectl top pods -n "$NS" --no-headers 2>/dev/null) || top_output="" quota_json=$(kubectl get resourcequota -n "$NS" -o json 2>/dev/null) || quota_json='{"items":[]}' -# ── Build podName → app map ────────────────────────────────────────────────── - -declare -A name_to_app -while IFS= read -r line; do - pod_name=$(echo "$line" | jq -r '.name') - app=$(echo "$line" | jq -r '.app') - name_to_app["$pod_name"]="$app" -done < <(echo "$pods_json" | jq -c '.items[] | {name: .metadata.name, app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)}') - -# ── Parse kubectl top ──────────────────────────────────────────────────────── - -declare -A top_cpu # app → cpuM -declare -A top_mem # app → memMi -cpu_actual=0 -mem_actual=0 - -while IFS= read -r line; do - [[ -z "$line" ]] && continue - read -r pod_name cpu_str mem_str <<< "$line" - app="${name_to_app[$pod_name]:-}" - [[ -z "$app" ]] && continue - cpu_m=$(normalize_cpu "$cpu_str") - mem_mib=$(normalize_mem "$mem_str") - top_cpu["$app"]=$(( ${top_cpu[$app]:-0} + cpu_m )) - top_mem["$app"]=$(( ${top_mem[$app]:-0} + mem_mib )) - cpu_actual=$(( cpu_actual + cpu_m )) - mem_actual=$(( mem_actual + mem_mib )) -done <<< "$top_output" - -# ── Build podMetrics JSON array ────────────────────────────────────────────── - -pod_metrics="[" -first=1 -while IFS= read -r line; do - app=$(echo "$line" | jq -r '.app') - cpu_m="${top_cpu[$app]:-"-1"}" - mem_mib="${top_mem[$app]:-"-1"}" - [[ $first -eq 0 ]] && pod_metrics+="," - pod_metrics+=$(jq -nc --arg app "$app" --argjson cpu "$cpu_m" --argjson mem "$mem_mib" \ - '{app: $app, cpuM: $cpu, memMi: $mem}') - first=0 -done < <(echo "$pods_json" | jq -c '[.items[] | {app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)}] | unique_by(.app)[]') -pod_metrics+="]" - -# ── Parse resourcequota ────────────────────────────────────────────────────── - -q_cpu_req_used=$(echo "$quota_json" | jq -r '.items[0].status.used["requests.cpu"] // "0"') -q_cpu_req_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["requests.cpu"] // "0"') -q_cpu_lim_used=$(echo "$quota_json" | jq -r '.items[0].status.used["limits.cpu"] // "0"') -q_cpu_lim_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["limits.cpu"] // "0"') -q_mem_req_used=$(echo "$quota_json" | jq -r '.items[0].status.used["requests.memory"] // "0"') -q_mem_req_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["requests.memory"] // "0"') -q_mem_lim_used=$(echo "$quota_json" | jq -r '.items[0].status.used["limits.memory"] // "0"') -q_mem_lim_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["limits.memory"] // "0"') - -cpu_req_used_m=$(normalize_cpu "$q_cpu_req_used") -cpu_req_hard_m=$(normalize_cpu "$q_cpu_req_hard") -cpu_lim_used_m=$(normalize_cpu "$q_cpu_lim_used") -cpu_lim_hard_m=$(normalize_cpu "$q_cpu_lim_hard") -mem_req_used_mib=$(normalize_mem "$q_mem_req_used") -mem_req_hard_mib=$(normalize_mem "$q_mem_req_hard") -mem_lim_used_mib=$(normalize_mem "$q_mem_lim_used") -mem_lim_hard_mib=$(normalize_mem "$q_mem_lim_hard") - -cpu_req_pct=$(awk "BEGIN { printf \"%.4f\", $cpu_req_used_m / ($cpu_req_hard_m > 0 ? $cpu_req_hard_m : 1) }") -cpu_lim_pct=$(awk "BEGIN { printf \"%.4f\", $cpu_lim_used_m / ($cpu_lim_hard_m > 0 ? $cpu_lim_hard_m : 1) }") -mem_req_pct=$(awk "BEGIN { printf \"%.4f\", $mem_req_used_mib / ($mem_req_hard_mib > 0 ? $mem_req_hard_mib : 1) }") -mem_lim_pct=$(awk "BEGIN { printf \"%.4f\", $mem_lim_used_mib / ($mem_lim_hard_mib > 0 ? $mem_lim_hard_mib : 1) }") - -cpu_req_label="$(fmt_cpu $cpu_req_used_m) / $(fmt_cpu $cpu_req_hard_m)" -cpu_lim_label="$(fmt_cpu $cpu_lim_used_m) / $(fmt_cpu $cpu_lim_hard_m)" -mem_req_label="$(fmt_mem $mem_req_used_mib) / $(fmt_mem $mem_req_hard_mib)" -mem_lim_label="$(fmt_mem $mem_lim_used_mib) / $(fmt_mem $mem_lim_hard_mib)" - -# ── Output ─────────────────────────────────────────────────────────────────── - jq -nc \ - --argjson podMetrics "$pod_metrics" \ - --argjson cpuActualM "$cpu_actual" \ - --argjson memActualMi "$mem_actual" \ - --argjson cpuReqPct "$cpu_req_pct" \ - --argjson cpuLimPct "$cpu_lim_pct" \ - --argjson memReqPct "$mem_req_pct" \ - --argjson memLimPct "$mem_lim_pct" \ - --arg cpuReqLabel "$cpu_req_label" \ - --arg cpuLimLabel "$cpu_lim_label" \ - --arg memReqLabel "$mem_req_label" \ - --arg memLimLabel "$mem_lim_label" \ - '{ - podMetrics: $podMetrics, - quota: { - cpuActualM: $cpuActualM, - memActualMi: $memActualMi, - cpuReqPct: $cpuReqPct, - cpuLimPct: $cpuLimPct, - memReqPct: $memReqPct, - memLimPct: $memLimPct, - cpuReqLabel: $cpuReqLabel, - cpuLimLabel: $cpuLimLabel, - memReqLabel: $memReqLabel, - memLimLabel: $memLimLabel - } - }' + --argjson pods "$pods_json" \ + --argjson quota "$quota_json" \ + --arg top "$top_output" ' + # ── unit normalizers ──────────────────────────────────────────────────────── + def ncpu: # CPU string → integer millicores + if . == null or . == "" then 0 + elif test("^[0-9]+m$") then (rtrimstr("m")|tonumber) + elif test("^[0-9]+n$") then ((rtrimstr("n")|tonumber)/1000000|floor) + elif test("^[0-9]+u$") then ((rtrimstr("u")|tonumber)/1000|floor) + elif test("^[0-9.]+$") then (tonumber*1000|floor) + else 0 end; + def nmem: # memory string → integer MiB + if . == null or . == "" then 0 + elif test("Gi$") then (rtrimstr("Gi")|tonumber*1024|floor) + elif test("Mi$") then (rtrimstr("Mi")|tonumber|floor) + elif test("Ki$") then (rtrimstr("Ki")|tonumber/1024|floor) + elif test("Ti$") then (rtrimstr("Ti")|tonumber*1048576|floor) + elif test("^[0-9]+$") then (tonumber/1048576|floor) + else 0 end; + def fmtcpu: "\(.)m"; + def fmtmem: if . < 1024 then "\(.)Mi" else "\(((./1024)*100|round)/100)Gi" end; + + # ── podName → app map ─────────────────────────────────────────────────────── + ($pods.items // []) as $items + | ($items + | map({ (.metadata.name): (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name) }) + | add // {}) as $name2app + + # ── parse `kubectl top` text (pod / cpu / mem columns) ────────────────────── + | ($top | split("\n") + | map(select(test("\\S")) | [match("\\S+";"g").string] + | { pod: .[0], cpuM: (.[1]|ncpu), memMi: (.[2]|nmem) })) as $tops + + # ── aggregate per app (sum pods sharing an app label) ─────────────────────── + | (reduce $tops[] as $t ({}; + ($name2app[$t.pod]) as $a + | if $a == null then . + else .[$a] = { cpuM: ((.[$a].cpuM // 0) + $t.cpuM), + memMi: ((.[$a].memMi // 0) + $t.memMi) } end)) as $byApp + + | ([$items[] | (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)] | unique) as $apps + | ([ $apps[] | { app: ., cpuM: ($byApp[.].cpuM // -1), memMi: ($byApp[.].memMi // -1) } ]) as $podMetrics + + | (reduce ($byApp|to_entries[]) as $e (0; . + $e.value.cpuM)) as $cpuActual + | (reduce ($byApp|to_entries[]) as $e (0; . + $e.value.memMi)) as $memActual + + # ── resource quota ────────────────────────────────────────────────────────── + | (($quota.items // [])[0].status // {}) as $qs + | ($qs.used // {}) as $u + | ($qs.hard // {}) as $h + | ($u["requests.cpu"] | ncpu) as $crqu | ($h["requests.cpu"] | ncpu) as $crqh + | ($u["limits.cpu"] | ncpu) as $clu | ($h["limits.cpu"] | ncpu) as $clh + | ($u["requests.memory"] | nmem) as $mrqu | ($h["requests.memory"] | nmem) as $mrqh + | ($u["limits.memory"] | nmem) as $mlu | ($h["limits.memory"] | nmem) as $mlh + + | { + podMetrics: $podMetrics, + quota: { + cpuActualM: $cpuActual, + memActualMi: $memActual, + cpuReqPct: (if $crqh>0 then $crqu/$crqh else 0 end), + cpuLimPct: (if $clh>0 then $clu/$clh else 0 end), + memReqPct: (if $mrqh>0 then $mrqu/$mrqh else 0 end), + memLimPct: (if $mlh>0 then $mlu/$mlh else 0 end), + cpuReqLabel: "\($crqu|fmtcpu) / \($crqh|fmtcpu)", + cpuLimLabel: "\($clu|fmtcpu) / \($clh|fmtcpu)", + memReqLabel: "\($mrqu|fmtmem) / \($mrqh|fmtmem)", + memLimLabel: "\($mlu|fmtmem) / \($mlh|fmtmem)" + } + } +'