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).
This commit is contained in:
@@ -1,162 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Outputs per-pod CPU/MEM metrics and namespace quota as JSON.
|
||||
# Usage: k8s-metrics.sh <namespace>
|
||||
#
|
||||
# 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)"
|
||||
}
|
||||
}
|
||||
'
|
||||
|
||||
Reference in New Issue
Block a user