Angka mentah dari Bagian 1 belum bisa disebut laporan — 2.50 dan 84% tidak mengatakan apa pun sebelum kita punya tolok ukur. Bagian ini memberi tiap metrik dua ambang dari berkas konfigurasi (waspada dan kritis), memvalidasi nilainya, memutuskan status tiap metrik, lalu menyusun semuanya menjadi tabel yang bisa dipahami sekilas. Bagian ini juga menetapkan kontrak keluar skrip: satu exit code agregat yang mewakili kondisi terburuk, sehingga penjadwal seperti cron cukup melihat satu angka untuk tahu ada masalah atau tidak.
Ambang di Berkas Terpisah #
Ambang adalah kebijakan, dan kebijakan berubah: server database wajar lebih longgar daripada server web, dan ambang yang kemarin cocok bisa salah hari ini. Karena itu ambang tinggal di monitor.conf, bukan di dalam skrip — pengguna cukup menyunting nilai, sama seperti pola konfigurasi pada proyek backup. Isi berkasnya:
# monitor.conf — konfigurasi skrip monitor.
# Ambang load average 1 menit (angka desimal, contoh: 1.50).
LOAD_WARN="1.50"
LOAD_KRITIS="3.00"
# Ambang memori terpakai (persen, bilangan bulat 1-99).
MEM_WARN_PCT=80
MEM_KRITIS_PCT=90
# Ambang disk terpakai per mount (persen, bilangan bulat 1-99).
DISK_WARN_PCT=80
DISK_KRITIS_PCT=90
# Mount point yang dipantau, dipisah spasi.
DISK_MOUNTS="/ /home"
# Berkas log terstruktur (opsional; default: monitor.log).
LOG_FILE="monitor.log"
# Rotasi: bila log melebihi LOG_MAX_BYTES, berkas digeser menjadi
# .1, .2, ... dan maksimal LOG_KEEP berkas lama dipertahankan.
LOG_MAX_BYTES=1048576
LOG_KEEP=3
# Alert dikirim setelah sebuah metrik melewati ambang sebanyak
# ALERT_REPEAT kali berturut-turut.
ALERT_REPEAT=3Variabel log dan alert baru dipakai pada Bagian 3, tetapi validasinya kita pasang sekarang supaya berkas konfigurasi yang sama bisa dipakai dari awal. Nilai ambang load sengaja ditulis desimal karena /proc/loadavg memang desimal; ambang persen ditulis bilangan bulat karena begitu pula bentuk datanya.
Validasi Angka dengan case #
Konfigurasi yang dibaca dengan source adalah kode shell — salah ketik bisa berupa variabel kosong, huruf, atau angka setengah jadi. Skrip menolak semuanya lebih dulu dengan pola case, karena satu karakter asing sudah cukup untuk membuat perbandingan angka berperilaku aneh:
angka_desimal() {
case "$1" in
''|*[!0-9.]*) return 1 ;;
*.*.*) return 1 ;;
.*|*.) return 1 ;;
esac
return 0
}
angka_persen() {
case "$1" in
''|*[!0-9]*) return 1 ;;
esac
[ "$1" -ge 1 ] && [ "$1" -le 99 ]
}
for uji in 1.50 3 0.75 4.5.2 .5 5. abc ''; do
if angka_desimal "$uji"; then
printf 'valid : "%s"\n' "$uji"
else
printf 'tidak valid : "%s"\n' "$uji"
fi
done
for uji in 1 80 99 100 0; do
if angka_persen "$uji"; then
printf 'valid : %s\n' "$uji"
else
printf 'tidak valid : %s\n' "$uji"
fi
doneOutput yang Dihasilkan:
valid : "1.50"
valid : "3"
valid : "0.75"
tidak valid : "4.5.2"
tidak valid : ".5"
tidak valid : "5."
tidak valid : "abc"
tidak valid : ""
valid : 1
valid : 80
valid : 99
tidak valid : 100
tidak valid : 0Bedah polanya:
*[!0-9.]*menolak isi yang memuat satu karakter pun selain digit dan titik — huruf, spasi, dan tanda minus langsung gugur.*.*.*menolak dua titik atau lebih, sedangkan.*dan*.menolak titik di ujung — tiga pola ini menyisakan hanya bentuk3atau3.25yang sah.angka_persenmenambah pemeriksaan rentang dengan[ -ge 1 ]dan[ -le 99 ]: persen 0 tidak masuk akal sebagai ambang, dan 100 berarti tidak akan pernah terlampaui. Perilaku operator perbandingan ini terdokumentasi di test(1) .- Validasi ambang-atas-bawah menyusul setelah nilai terbaca:
LOAD_WARNharus lebih kecil dariLOAD_KRITIS— perbandingannya lewat awk karena keduanya desimal.
Menentukan Status: case dan Aritmetika #
Dengan nilai dan ambang yang kini sama-sama bilangan bulat (load sudah dikali 100 sejak Bagian 1), keputusan status tinggal perbandingan sederhana. Fungsi kode_status mengembalikan angka 0-2, lalu nama_status menerjemahkannya lewat case:
kode_status() {
if [ "$1" -ge "$3" ]; then
printf '2'
elif [ "$1" -ge "$2" ]; then
printf '1'
else
printf '0'
fi
}
nama_status() {
case "$1" in
1) printf 'WASPADA' ;;
2) printf 'KRITIS' ;;
*) printf 'OK' ;;
esac
}
for v in 40 149 150 299 300 410; do
printf 'load %d/100 -> %s\n' "$v" \
"$(nama_status "$(kode_status "$v" 150 300)")"
doneOutput yang Dihasilkan:
load 40/100 -> OK
load 149/100 -> OK
load 150/100 -> WASPADA
load 299/100 -> WASPADA
load 300/100 -> KRITIS
load 410/100 -> KRITISEnam nilai uji itu sengaja mengetok semua batasnya: 149 dan 150 membuktikan ambang waspada tercapai saat sama dengan (150 masuk WASPADA), 299 dan 300 membuktikan hal yang sama untuk kritis. Mengapa angka, bukan langsung teks? Karena status juga dijumlah: variabel terburuk di skrip menyimpan kode tertinggi dengan (( kode > terburuk )) — aritmetika bash — dan angka itulah yang menjadi exit code di akhir. Teks WASPADA untuk manusia, angka 1 untuk mesin.
Laporan Tabel dengan printf #
Tiga kolom nilai (metrik, nilai, ambang) plus status cukup rapikan dengan satu format printf yang dipakai berulang: %-10s merata-kiri nama metrik, %7s merata-kan nilai, %-12s merata-kiri pasangan ambang:
printf '%-10s %7s %-12s %s\n' 'Metrik' 'Nilai' 'Ambang W/K' 'Status'
printf '%.0s-' {1..39}; printf '\n'
printf '%-10s %7s %-12s %s\n' cpu-load 2.50 1.50/3.00 WASPADA
printf '%-10s %7s %-12s %s\n' memori 84% 80%/90% WASPADA
printf '%-10s %7s %-12s %s\n' disk:/ 48% 80%/90% OK
printf '%-10s %7s %-12s %s\n' disk:/home 89% 80%/90% WASPADAOutput yang Dihasilkan:
Metrik Nilai Ambang W/K Status
---------------------------------------
cpu-load 2.50 1.50/3.00 WASPADA
memori 84% 80%/90% WASPADA
disk:/ 48% 80%/90% OK
disk:/home 89% 80%/90% WASPADATrik di baris kedua layak dijelaskan: printf '%.0s-' {1..39} mencetak string kosong (presisi .0 memotong jadi nol karakter) diikuti tanda -, satu kali untuk tiap argumen dari ekspansi kurung kurawal — hasilnya 39 tanda hubung tanpa perulangan. Lebar kolom dipilih mengikuti isi terpanjang: disk:/home (10 karakter) dan WASPADA (7), sehingga tidak ada baris yang bergeser.
Warna ANSI, Hanya di Terminal #
Status berwarna membantu mata saat laporan dilihat langsung: kuning untuk WASPADA, merah untuk KRITIS. Akan tetapi kode warna ANSI (\033[33m...) adalah sampah di berkas log dan email. Solusinya satu baris: nyalakan warna hanya bila stdout adalah terminal, yang dideteksi dengan [ -t 1 ]:
if [ -t 1 ]; then
printf 'stdout adalah terminal: warna ANSI diaktifkan\n'
else
printf 'stdout bukan terminal: warna ANSI dimatikan\n'
fiOutput yang Dihasilkan:
stdout bukan terminal: warna ANSI dimatikanBlok di atas dijalankan dengan output yang dialihkan, sehingga jalur kedua yang tercetak — dan itulah yang terjadi ketika skrip dipanggil cron, dialihkan ke berkas, atau disalurkan ke program lain. Di terminal Anda, baris pertama yang muncul; laporan tetap terbaca sama baiknya tanpa warna karena status selalu juga tertulis sebagai teks.
Skrip Laporan Lengkap #
Semua bagian di atas kini dirangkai menjadi satu skrip — versi laporan, belum dengan log dan alert. Blok berikut menulis monitor.sh dan monitor.conf, menyiapkan tiga fixture keadaan (waspada, sehat, kritis) dengan satu berkas dasar plus sed, lalu menjalankan skrip pada ketiganya:
cat > monitor.conf <<'CONF'
LOAD_WARN="1.50"
LOAD_KRITIS="3.00"
MEM_WARN_PCT=80
MEM_KRITIS_PCT=90
DISK_WARN_PCT=80
DISK_KRITIS_PCT=90
DISK_MOUNTS="/ /home"
LOG_FILE="monitor.log"
LOG_MAX_BYTES=1048576
LOG_KEEP=3
ALERT_REPEAT=3
CONF
mkdir -p fixture fixture-sehat fixture-kritis
printf '2.50 2.10 1.60 4/812 34567\n' > fixture/loadavg
cat > fixture/meminfo <<'EOF'
MemTotal: 8000000 kB
MemFree: 950000 kB
MemAvailable: 1280000 kB
Buffers: 210000 kB
Cached: 700000 kB
SwapCached: 0 kB
SwapTotal: 2000000 kB
SwapFree: 2000000 kB
EOF
cat > fixture/df.txt <<'EOF'
Filesystem 1024-blocks Used Available Capacity Mounted on
/dev/sda1 82043440 36919548 40942056 48% /
tmpfs 4042268 0 4042268 0% /dev/shm
/dev/sda2 412849860 348092000 43741860 89% /home
EOF
sed 's/1280000/4640000/' fixture/meminfo > fixture-sehat/meminfo
sed 's| 89% /home| 51% /home|' fixture/df.txt > fixture-sehat/df.txt
printf '0.40 0.35 0.30 1/512 21001\n' > fixture-sehat/loadavg
sed 's/1280000/400000/' fixture/meminfo > fixture-kritis/meminfo
sed 's| 89% /home| 91% /home|' fixture/df.txt > fixture-kritis/df.txt
printf '4.10 3.20 2.10 9/640 55002\n' > fixture-kritis/loadavg
cat > monitor.sh <<'BASH'
#!/usr/bin/env bash
# monitor.sh — tahap laporan: kumpulkan data, bandingkan dengan ambang,
# cetak laporan, keluar dengan kode agregat. Logging, alert, dan rotasi
# log menyusul di Bagian 3.
#
# Kode keluar:
# 0 semua OK 1 ada WASPADA 2 ada KRITIS
# 3 konfigurasi atau sumber data tidak valid
set -u
CONF="${MONITOR_CONF:-monitor.conf}"
data_dir="${MONITOR_SOURCE:-}"
while [ $# -gt 0 ]; do
case "$1" in
--data-dir)
[ $# -ge 2 ] || {
printf 'monitor.sh: --data-dir membutuhkan argumen DIR\n' >&2
exit 3
}
data_dir=$2
shift 2
;;
--data-dir=*)
data_dir=${1#--data-dir=}
shift
;;
*)
printf 'monitor.sh: argumen tidak dikenal: %s\n' "$1" >&2
exit 3
;;
esac
done
angka_desimal() {
case "$1" in
''|*[!0-9.]*) return 1 ;;
*.*.*) return 1 ;;
.*|*.) return 1 ;;
esac
return 0
}
angka_persen() {
case "$1" in
''|*[!0-9]*) return 1 ;;
esac
[ "$1" -ge 1 ] && [ "$1" -le 99 ]
}
angka_bulat() {
case "$1" in
''|*[!0-9]*|0) return 1 ;;
esac
return 0
}
if [ ! -f "$CONF" ]; then
printf 'monitor.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
exit 3
fi
# shellcheck source=/dev/null
. "$CONF"
gagal_konfig() {
printf 'monitor.sh: %s (periksa %s)\n' "$1" "$CONF" >&2
exit 3
}
angka_desimal "${LOAD_WARN:-}" || gagal_konfig 'LOAD_WARN harus angka desimal, mis. 1.50'
angka_desimal "${LOAD_KRITIS:-}" || gagal_konfig 'LOAD_KRITIS harus angka desimal, mis. 3.00'
angka_persen "${MEM_WARN_PCT:-}" || gagal_konfig 'MEM_WARN_PCT harus bilangan bulat 1-99'
angka_persen "${MEM_KRITIS_PCT:-}" || gagal_konfig 'MEM_KRITIS_PCT harus bilangan bulat 1-99'
angka_persen "${DISK_WARN_PCT:-}" || gagal_konfig 'DISK_WARN_PCT harus bilangan bulat 1-99'
angka_persen "${DISK_KRITIS_PCT:-}" || gagal_konfig 'DISK_KRITIS_PCT harus bilangan bulat 1-99'
angka_bulat "${ALERT_REPEAT:-}" || gagal_konfig 'ALERT_REPEAT harus bilangan bulat 1 atau lebih'
angka_bulat "${LOG_KEEP:-}" || gagal_konfig 'LOG_KEEP harus bilangan bulat 1 atau lebih'
angka_bulat "${LOG_MAX_BYTES:-}" || gagal_konfig 'LOG_MAX_BYTES harus bilangan bulat 1 atau lebih'
[ -n "${DISK_MOUNTS:-}" ] || gagal_konfig 'DISK_MOUNTS belum diisi'
[ -n "${LOG_FILE:-}" ] || gagal_konfig 'LOG_FILE belum diisi'
if ! awk -v a="$LOAD_WARN" -v b="$LOAD_KRITIS" 'BEGIN { exit (a < b) ? 0 : 1 }'; then
gagal_konfig 'LOAD_WARN harus lebih kecil dari LOAD_KRITIS'
fi
[ "${MEM_WARN_PCT}" -lt "${MEM_KRITIS_PCT}" ] ||
gagal_konfig 'MEM_WARN_PCT harus lebih kecil dari MEM_KRITIS_PCT'
[ "${DISK_WARN_PCT}" -lt "${DISK_KRITIS_PCT}" ] ||
gagal_konfig 'DISK_WARN_PCT harus lebih kecil dari DISK_KRITIS_PCT'
if [ -n "$data_dir" ]; then
loadavg_file="$data_dir/loadavg"
meminfo_file="$data_dir/meminfo"
df_file="$data_dir/df.txt"
for f in "$loadavg_file" "$meminfo_file" "$df_file"; do
if [ ! -r "$f" ]; then
printf 'monitor.sh: sumber data %s tidak terbaca\n' "$f" >&2
exit 3
fi
done
df_output=$(cat "$df_file")
else
loadavg_file=/proc/loadavg
meminfo_file=/proc/meminfo
df_output=$(df -P)
fi
ambil_load100() {
awk '{ printf "%.0f", $1 * 100 }' "$loadavg_file"
}
ke_ratus() {
awk -v v="$1" 'BEGIN { printf "%.0f", v * 100 }'
}
ambil_mem100() {
awk '
$1 == "MemTotal:" { total = $2 }
$1 == "MemAvailable:" { avail = $2; ada = 1 }
END { if (total > 0 && ada) printf "%.0f", (total - avail) * 100 / total }
' "$meminfo_file"
}
ambil_disk100() {
printf '%s\n' "$df_output" |
awk -v mnt="$1" '
$6 == mnt { gsub(/%/, "", $5); print $5 + 0; exit }
'
}
kode_status() {
if [ "$1" -ge "$3" ]; then
printf '2'
elif [ "$1" -ge "$2" ]; then
printf '1'
else
printf '0'
fi
}
nama_status() {
case "$1" in
1) printf 'WASPADA' ;;
2) printf 'KRITIS' ;;
*) printf 'OK' ;;
esac
}
warna_status() {
if [ -t 1 ]; then
case "$1" in
WASPADA) printf '\033[33m%s\033[0m' "$1" ;;
KRITIS) printf '\033[31m%s\033[0m' "$1" ;;
*) printf '%s' "$1" ;;
esac
else
printf '%s' "$1"
fi
}
load_tampil=$(awk '{ print $1 }' "$loadavg_file")
load100=$(ambil_load100)
if [ -z "$load100" ]; then
printf 'monitor.sh: %s tidak berisi load average\n' "$loadavg_file" >&2
exit 3
fi
mem100=$(ambil_mem100)
if [ -z "$mem100" ]; then
printf 'monitor.sh: %s tidak berisi MemTotal/MemAvailable\n' "$meminfo_file" >&2
exit 3
fi
load_warn100=$(ke_ratus "$LOAD_WARN")
load_kritis100=$(ke_ratus "$LOAD_KRITIS")
# shellcheck disable=SC2086 # daftar mount memang dipisah spasi
for mnt in $DISK_MOUNTS; do
if [ -z "$(ambil_disk100 "$mnt")" ]; then
printf 'monitor.sh: mount %s tidak ditemukan di data df (periksa DISK_MOUNTS)\n' "$mnt" >&2
exit 3
fi
done
terburuk=0
periksa() {
local metrik=$1 tampil=$2 nilai=$3 warn=$4 kritis=$5 label=$6
local kode
kode=$(kode_status "$nilai" "$warn" "$kritis")
if (( kode > terburuk )); then
terburuk=$kode
fi
printf '%-10s %7s %-12s %s\n' "$metrik" "$tampil" "$label" \
"$(warna_status "$(nama_status "$kode")")"
}
printf '%-10s %7s %-12s %s\n' 'Metrik' 'Nilai' 'Ambang W/K' 'Status'
printf '%.0s-' {1..39}; printf '\n'
periksa cpu-load "$load_tampil" "$load100" "$load_warn100" "$load_kritis100" \
"$LOAD_WARN/$LOAD_KRITIS"
periksa memori "${mem100}%" "$mem100" "$MEM_WARN_PCT" "$MEM_KRITIS_PCT" \
"${MEM_WARN_PCT}%/${MEM_KRITIS_PCT}%"
# shellcheck disable=SC2086 # daftar mount memang dipisah spasi
for mnt in $DISK_MOUNTS; do
disk100=$(ambil_disk100 "$mnt")
periksa "disk:$mnt" "${disk100}%" "$disk100" \
"$DISK_WARN_PCT" "$DISK_KRITIS_PCT" \
"${DISK_WARN_PCT}%/${DISK_KRITIS_PCT}%"
done
printf 'kesimpulan: %s\n' "$(nama_status "$terburuk")"
exit "$terburuk"
BASH
echo '== fixture waspada =='
bash monitor.sh --data-dir fixture; echo "exit=$?"
echo '== fixture sehat =='
bash monitor.sh --data-dir fixture-sehat; echo "exit=$?"
echo '== fixture kritis =='
bash monitor.sh --data-dir fixture-kritis; echo "exit=$?"
echo '== ambang terbalik ditolak =='
sed 's/LOAD_WARN="1.50"/LOAD_WARN="3.00"/; s/LOAD_KRITIS="3.00"/LOAD_KRITIS="1.50"/' \
monitor.conf > terbalik.conf
MONITOR_CONF=terbalik.conf bash monitor.sh --data-dir fixture 2>&1; echo "exit=$?"Output yang Dihasilkan:
== fixture waspada ==
Metrik Nilai Ambang W/K Status
---------------------------------------
cpu-load 2.50 1.50/3.00 WASPADA
memori 84% 80%/90% WASPADA
disk:/ 48% 80%/90% OK
disk:/home 89% 80%/90% WASPADA
kesimpulan: WASPADA
exit=1
== fixture sehat ==
Metrik Nilai Ambang W/K Status
---------------------------------------
cpu-load 0.40 1.50/3.00 OK
memori 42% 80%/90% OK
disk:/ 48% 80%/90% OK
disk:/home 51% 80%/90% OK
kesimpulan: OK
exit=0
== fixture kritis ==
Metrik Nilai Ambang W/K Status
---------------------------------------
cpu-load 4.10 1.50/3.00 KRITIS
memori 95% 80%/90% KRITIS
disk:/ 48% 80%/90% OK
disk:/home 91% 80%/90% KRITIS
kesimpulan: KRITIS
exit=2
== ambang terbalik ditolak ==
monitor.sh: LOAD_WARN harus lebih kecil dari LOAD_KRITIS (periksa terbalik.conf)
exit=3Perhatikan tiga hal dari eksekusi itu. Pertama, periksa adalah satu-satunya tempat status dihitung — fungsi pengumpul data dari Bagian 1 di atasnya tetap murni mengambil angka, dan fungsi pelaporan di bawahnya tinggal memakai hasilnya. Kedua, mount yang tidak dikenal ditolak sebelum tabel dicetak, sehingga laporan tidak pernah tercetak setengah jalan. Ketiga, fixture sehat dan kritis dibuat dengan sed dari fixture waspada — tiga keadaan sistem berbeda dari satu berkas dasar, teknik yang nanti dipakai test suite.
Exit Code Agregat #
Hasil akhir skrip diringkas menjadi satu angka, dan angka itu adalah kontrak dengan dunia luar:
| Exit code | Arti | Dipicu oleh |
|---|---|---|
| 0 | semua metrik OK | seluruh nilai di bawah ambang waspada |
| 1 | ada WASPADA | satu metrik saja cukup, tanpa KRITIS |
| 2 | ada KRITIS | satu metrik kritis menimpa semua waspada |
| 3 | konfigurasi/sumber tidak valid | ambang rusak, berkas hilang, mount tak dikenal |
Pembedaan kode 3 penting: ia berarti skripnya sendiri bermasalah, bukan sistem yang dipantau — campur keduanya dan kita akan mengejar “server kritis” yang sebenarnya cuma konfigurasi salah ketik. Skrip yang dijadwalkan cron bisa ditindaklanjuti per kode: baris log exit=2 berarti telepon admin, exit=3 berarti perbaiki instalasi monitor.
Laporan di layar kini utuh — ambang tervalidasi, status ditentukan, tabel tercetak, exit code bermakna. Yang belum ada adalah ingatan: begitu layar bergulir, semuanya hilang. Bagian 3 melengkapi skrip dengan log terstruktur berstempel waktu, hitungan berturut-turut di berkas state sebagai dasar alert, dan rotasi log agar arsipnya tidak menelan disk yang justru dipantau.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
src/monitor.sh
Unduh berkas#!/usr/bin/env bash
# monitor.sh — pemantau resource sederhana untuk admin kecil.
#
# Setiap eksekusi mengumpulkan tiga metrik — load average 1 menit,
# persentase memori terpakai, dan persentase disk terpakai per mount
# point dari konfigurasi — membandingkannya dengan ambang waspada dan
# kritis dari berkas konfigurasi, mencetak laporan berbentuk tabel,
# menulis log terstruktur, dan mengirim alert ke stderr setelah satu
# metrik melewati ambang beberapa kali berturut-turut. Berjalan sebagai
# user biasa — tidak butuh root.
#
# Pemakaian:
# bash monitor.sh [--data-dir DIR]
#
# --data-dir DIR baca loadavg, meminfo, dan df.txt dari DIR alih-
# alih dari /proc dan df sungguhan (untuk pengujian).
#
# Variabel lingkungan (berguna untuk pengujian):
# MONITOR_CONF berkas konfigurasi yang dibaca (default: monitor.conf)
# MONITOR_SOURCE sama dengan --data-dir (opsi menang bila keduanya diisi)
# MONITOR_STAMP stempel waktu ISO untuk log (default: waktu sekarang)
#
# Kode keluar:
# 0 semua metrik OK 1 ada metrik WASPADA
# 2 ada metrik KRITIS 3 konfigurasi atau sumber data tidak valid
set -u
CONF="${MONITOR_CONF:-monitor.conf}"
STAMP="${MONITOR_STAMP:-$(date +%Y-%m-%dT%H:%M:%S%:z)}"
data_dir="${MONITOR_SOURCE:-}"
usage() {
printf 'Pemakaian: bash monitor.sh [--data-dir DIR]\n'
printf '\n'
printf 'Memeriksa load average, memori, dan disk menurut ambang di\n'
printf 'monitor.conf, mencetak laporan, menulis log, dan mengirim\n'
printf 'alert setelah satu metrik melewati ambang beberapa kali\n'
printf 'berturut-turut.\n'
printf '\n'
printf ' --data-dir DIR baca data tiruan dari DIR (untuk pengujian)\n'
printf ' -h, --help tampilkan bantuan ini\n'
printf '\n'
printf 'Kode keluar: 0 semua OK, 1 ada WASPADA, 2 ada KRITIS,\n'
printf '3 konfigurasi atau sumber data tidak valid.\n'
}
while [ $# -gt 0 ]; do
case "$1" in
--data-dir)
if [ $# -lt 2 ]; then
printf 'monitor.sh: --data-dir membutuhkan argumen DIR\n' >&2
exit 3
fi
data_dir=$2
shift 2
;;
--data-dir=*)
data_dir=${1#--data-dir=}
shift
;;
-h|--help)
usage
exit 0
;;
*)
printf 'monitor.sh: argumen tidak dikenal: %s\n' "$1" >&2
exit 3
;;
esac
done
# angka_desimal V: sukses bila V bilangan desimal sederhana seperti
# "1.50" atau "3" — tanpa tanda, tanpa titik ganda, tanpa titik di ujung.
angka_desimal() {
case "$1" in
''|*[!0-9.]*) return 1 ;;
*.*.*) return 1 ;;
.*|*.|*.) return 1 ;;
esac
return 0
}
# angka_persen V: sukses bila V bilangan bulat 1 sampai 99.
angka_persen() {
case "$1" in
''|*[!0-9]*) return 1 ;;
esac
[ "$1" -ge 1 ] && [ "$1" -le 99 ]
}
# angka_bulat V: sukses bila V bilangan bulat 1 atau lebih.
angka_bulat() {
case "$1" in
''|*[!0-9]*|0) return 1 ;;
esac
return 0
}
# --- baca dan validasi konfigurasi ---------------------------------------
if [ ! -f "$CONF" ]; then
printf 'monitor.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
exit 3
fi
# shellcheck source=/dev/null
. "$CONF"
gagal_konfig() {
printf 'monitor.sh: %s (periksa %s)\n' "$1" "$CONF" >&2
exit 3
}
angka_desimal "${LOAD_WARN:-}" || gagal_konfig 'LOAD_WARN harus angka desimal, mis. 1.50'
angka_desimal "${LOAD_KRITIS:-}" || gagal_konfig 'LOAD_KRITIS harus angka desimal, mis. 3.00'
angka_persen "${MEM_WARN_PCT:-}" || gagal_konfig 'MEM_WARN_PCT harus bilangan bulat 1-99'
angka_persen "${MEM_KRITIS_PCT:-}" || gagal_konfig 'MEM_KRITIS_PCT harus bilangan bulat 1-99'
angka_persen "${DISK_WARN_PCT:-}" || gagal_konfig 'DISK_WARN_PCT harus bilangan bulat 1-99'
angka_persen "${DISK_KRITIS_PCT:-}" || gagal_konfig 'DISK_KRITIS_PCT harus bilangan bulat 1-99'
angka_bulat "${ALERT_REPEAT:-}" || gagal_konfig 'ALERT_REPEAT harus bilangan bulat 1 atau lebih'
angka_bulat "${LOG_KEEP:-}" || gagal_konfig 'LOG_KEEP harus bilangan bulat 1 atau lebih'
angka_bulat "${LOG_MAX_BYTES:-}" || gagal_konfig 'LOG_MAX_BYTES harus bilangan bulat 1 atau lebih'
[ -n "${DISK_MOUNTS:-}" ] || gagal_konfig 'DISK_MOUNTS belum diisi'
[ -n "${LOG_FILE:-}" ] || gagal_konfig 'LOG_FILE belum diisi'
if ! awk -v a="$LOAD_WARN" -v b="$LOAD_KRITIS" 'BEGIN { exit (a < b) ? 0 : 1 }'; then
gagal_konfig 'LOAD_WARN harus lebih kecil dari LOAD_KRITIS'
fi
[ "${MEM_WARN_PCT}" -lt "${MEM_KRITIS_PCT}" ] ||
gagal_konfig 'MEM_WARN_PCT harus lebih kecil dari MEM_KRITIS_PCT'
[ "${DISK_WARN_PCT}" -lt "${DISK_KRITIS_PCT}" ] ||
gagal_konfig 'DISK_WARN_PCT harus lebih kecil dari DISK_KRITIS_PCT'
# --- sumber data: /proc sungguhan atau fixture pengujian ------------------
if [ -n "$data_dir" ]; then
loadavg_file="$data_dir/loadavg"
meminfo_file="$data_dir/meminfo"
df_file="$data_dir/df.txt"
for f in "$loadavg_file" "$meminfo_file" "$df_file"; do
if [ ! -r "$f" ]; then
printf 'monitor.sh: sumber data %s tidak terbaca\n' "$f" >&2
exit 3
fi
done
df_output=$(cat "$df_file")
else
loadavg_file=/proc/loadavg
meminfo_file=/proc/meminfo
df_output=$(df -P)
fi
# --- siapkan berkas log, state, dan rotasi --------------------------------
LOG_DIR=$(dirname "$LOG_FILE")
mkdir -p "$LOG_DIR" || exit 3
STATE_FILE="$LOG_DIR/monitor.state"
STATE_TMP="$STATE_FILE.tmp"
if [ -f "$LOG_FILE" ]; then
ukuran=$(stat -c %s "$LOG_FILE")
if [ "$ukuran" -gt "$LOG_MAX_BYTES" ]; then
i=$LOG_KEEP
while [ "$i" -gt 1 ]; do
[ -f "$LOG_FILE.$((i - 1))" ] &&
mv "$LOG_FILE.$((i - 1))" "$LOG_FILE.$i"
i=$((i - 1))
done
mv "$LOG_FILE" "$LOG_FILE.1"
fi
fi
# --- fungsi pengumpul data ------------------------------------------------
# ambil_load100: load average 1 menit dikali 100 (2.50 menjadi 250).
ambil_load100() {
awk '{ printf "%.0f", $1 * 100 }' "$loadavg_file"
}
# ke_ratus ANGKA: kalikan angka desimal dengan 100 menjadi bilangan bulat.
ke_ratus() {
awk -v v="$1" 'BEGIN { printf "%.0f", v * 100 }'
}
# ambil_mem100: persen memori terpakai. MemAvailable dipakai karena sudah
# memperhitungkan cache yang bisa direklamasi — bukan sekadar MemFree.
ambil_mem100() {
awk '
$1 == "MemTotal:" { total = $2 }
$1 == "MemAvailable:" { avail = $2; ada = 1 }
END { if (total > 0 && ada) printf "%.0f", (total - avail) * 100 / total }
' "$meminfo_file"
}
# ambil_disk100 MOUNT: persen terpakai untuk MOUNT dari output df.
ambil_disk100() {
printf '%s\n' "$df_output" |
awk -v mnt="$1" '
$6 == mnt { gsub(/%/, "", $5); print $5 + 0; exit }
'
}
# --- fungsi status ---------------------------------------------------------
# kode_status NILAI WASPADA KRITIS: 0=OK, 1=WASPADA, 2=KRITIS.
# Nilai dan kedua ambang harus bilangan bulat.
kode_status() {
if [ "$1" -ge "$3" ]; then
printf '2'
elif [ "$1" -ge "$2" ]; then
printf '1'
else
printf '0'
fi
}
# nama_status KODE: bentuk teks dari kode status.
nama_status() {
case "$1" in
1) printf 'WASPADA' ;;
2) printf 'KRITIS' ;;
*) printf 'OK' ;;
esac
}
# --- fungsi log, state, dan alert ------------------------------------------
# catat STATUS METRIK NILAI: tambah satu baris log terstruktur.
catat() {
printf '%s %-7s %-11s %s\n' "$STAMP" "$1" "$2" "$3" >> "$LOG_FILE"
}
# baca_hitung KUNCI: berapa kali berturut-turut KUNCI melewati ambang.
baca_hitung() {
[ -f "$STATE_FILE" ] || return 0
awk -v k="$1" '$1 == k { print $2; exit }' "$STATE_FILE"
}
# warna_status STATUS: teks status, diberi warna ANSI hanya bila stdout
# adalah terminal — output yang dialihkan ke berkas tetap polos.
warna_status() {
if [ -t 1 ]; then
case "$1" in
WASPADA) printf '\033[33m%s\033[0m' "$1" ;;
KRITIS) printf '\033[31m%s\033[0m' "$1" ;;
*) printf '%s' "$1" ;;
esac
else
printf '%s' "$1"
fi
}
# --- kumpulkan data --------------------------------------------------------
load_tampil=$(awk '{ print $1 }' "$loadavg_file")
load100=$(ambil_load100)
if [ -z "$load100" ]; then
printf 'monitor.sh: %s tidak berisi load average\n' "$loadavg_file" >&2
exit 3
fi
mem100=$(ambil_mem100)
if [ -z "$mem100" ]; then
printf 'monitor.sh: %s tidak berisi MemTotal/MemAvailable\n' "$meminfo_file" >&2
exit 3
fi
load_warn100=$(ke_ratus "$LOAD_WARN")
load_kritis100=$(ke_ratus "$LOAD_KRITIS")
# Mount yang tidak ada di data df ditolak di sini, sebelum laporan
# dicetak, supaya tabel tidak tercetak setengah jalan.
# shellcheck disable=SC2086 # daftar mount memang dipisah spasi
for mnt in $DISK_MOUNTS; do
if [ -z "$(ambil_disk100 "$mnt")" ]; then
printf 'monitor.sh: mount %s tidak ditemukan di data df (periksa DISK_MOUNTS)\n' "$mnt" >&2
exit 3
fi
done
# --- periksa tiap metrik: laporan + log + alert ----------------------------
terburuk=0
: > "$STATE_TMP"
# periksa METRIK TAMPIL NILAI WASPADA KRITIS LABEL_AMBANG
periksa() {
local metrik=$1 tampil=$2 nilai=$3 warn=$4 kritis=$5 label=$6
local kode status n
kode=$(kode_status "$nilai" "$warn" "$kritis")
status=$(nama_status "$kode")
if (( kode > terburuk )); then
terburuk=$kode
fi
printf '%-10s %7s %-12s %s\n' "$metrik" "$tampil" "$label" \
"$(warna_status "$status")"
catat "$status" "$metrik" "$tampil"
n=$(baca_hitung "$metrik")
n=${n:-0}
if [ "$status" != "OK" ]; then
n=$((n + 1))
printf '%s %s\n' "$metrik" "$n" >> "$STATE_TMP"
if [ "$n" -eq "$ALERT_REPEAT" ]; then
printf 'monitor.sh: ALERT %s = %s melewati ambang %sx berturut-turut\n' \
"$metrik" "$tampil" "$ALERT_REPEAT" >&2
catat ALERT "$metrik" "$tampil"
fi
fi
}
printf '%-10s %7s %-12s %s\n' 'Metrik' 'Nilai' 'Ambang W/K' 'Status'
printf '%.0s-' {1..39}; printf '\n'
periksa cpu-load "$load_tampil" "$load100" "$load_warn100" "$load_kritis100" \
"$LOAD_WARN/$LOAD_KRITIS"
periksa memori "${mem100}%" "$mem100" "$MEM_WARN_PCT" "$MEM_KRITIS_PCT" \
"${MEM_WARN_PCT}%/${MEM_KRITIS_PCT}%"
# shellcheck disable=SC2086 # daftar mount memang dipisah spasi
for mnt in $DISK_MOUNTS; do
disk100=$(ambil_disk100 "$mnt")
periksa "disk:$mnt" "${disk100}%" "$disk100" \
"$DISK_WARN_PCT" "$DISK_KRITIS_PCT" \
"${DISK_WARN_PCT}%/${DISK_KRITIS_PCT}%"
done
printf 'kesimpulan: %s\n' "$(nama_status "$terburuk")"
mv "$STATE_TMP" "$STATE_FILE"
exit "$terburuk"
tests/run-tests.sh
Unduh berkas#!/usr/bin/env bash
# run-tests.sh — test suite untuk monitor.sh.
# Suite menyiapkan sendiri folder kerja terisolasi (mktemp) berisi
# fixture /proc dan df yang dibekukan, menyalin src/monitor.sh dan
# config/monitor.conf, lalu menjalankan rangkaian pengujian dengan
# assertion sederhana. Semua eksekusi memakai MONITOR_STAMP tetap
# supaya isi log dan pesan bisa diramalkan.
# Jalankan dari mana saja: bash tests/run-tests.sh
set -u
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")
PASS=0
FAIL=0
# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
if [ "$1" = "$2" ]; then
PASS=$((PASS + 1))
printf 'OK %s\n' "$3"
else
FAIL=$((FAIL + 1))
printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
fi
}
# --- siapkan folder kerja terisolasi ------------------------------------
WORK=$(mktemp -d)
cp "$PROJ_DIR/src/monitor.sh" "$PROJ_DIR/config/monitor.conf" "$WORK/"
cd "$WORK" || exit 1
# --- fixture: tiga keadaan sistem yang dibekukan ------------------------
# waspada: load 2.50, memori 84%, / 48%, /home 89%
mkdir -p waspada
printf '2.50 2.10 1.60 4/812 34567\n' > waspada/loadavg
cat > waspada/meminfo <<'EOF'
MemTotal: 8000000 kB
MemFree: 950000 kB
MemAvailable: 1280000 kB
Buffers: 210000 kB
Cached: 700000 kB
SwapCached: 0 kB
SwapTotal: 2000000 kB
SwapFree: 2000000 kB
EOF
cat > waspada/df.txt <<'EOF'
Filesystem 1024-blocks Used Available Capacity Mounted on
/dev/sda1 82043440 36919548 40942056 48% /
tmpfs 4042268 0 4042268 0% /dev/shm
/dev/sda2 412849860 348092000 43741860 89% /home
EOF
# sehat: load 0.40, memori 42%, / 48%, /home 51%
mkdir -p sehat
printf '0.40 0.35 0.30 1/512 21001\n' > sehat/loadavg
sed 's/1280000/4640000/' waspada/meminfo > sehat/meminfo
sed 's| 89% /home| 51% /home|' waspada/df.txt > sehat/df.txt
# kritis: load 4.10, memori 95%, / 48%, /home 91%
mkdir -p kritis
printf '4.10 3.20 2.10 9/640 55002\n' > kritis/loadavg
sed 's/1280000/400000/' waspada/meminfo > kritis/meminfo
sed 's| 89% /home| 91% /home|' waspada/df.txt > kritis/df.txt
# --- uji 1: parsing nilai dari fixture ----------------------------------
MONITOR_STAMP=2026-01-15T09:00:00+07:00 bash monitor.sh --data-dir waspada \
> laporan.txt
assert_eq 1 "$?" "eksekusi waspada keluar 1"
assert_eq 2.50 "$(awk '$1 == "cpu-load" { print $2 }' laporan.txt)" "load 1 menit terbaca 2.50"
assert_eq 84% "$(awk '$1 == "memori" { print $2 }' laporan.txt)" "memori terpakai 84 persen"
assert_eq 48% "$(awk '$1 == "disk:/" { print $2 }' laporan.txt)" "disk / terpakai 48 persen"
assert_eq 89% "$(awk '$1 == "disk:/home" { print $2 }' laporan.txt)" "disk /home terpakai 89 persen"
# --- uji 2: status per metrik pada tiga keadaan --------------------------
MONITOR_STAMP=2026-01-15T09:05:00+07:00 bash monitor.sh --data-dir sehat \
> sehat.txt
assert_eq 0 "$?" "eksekusi sehat keluar 0"
for m in cpu-load memori disk:/ disk:/home; do
assert_eq OK "$(awk -v m="$m" '$1 == m { print $NF }' sehat.txt)" "status $m OK saat sehat"
done
MONITOR_STAMP=2026-01-15T09:10:00+07:00 bash monitor.sh --data-dir kritis \
> kritis.txt
assert_eq 2 "$?" "eksekusi kritis keluar 2"
assert_eq KRITIS "$(awk '$1 == "cpu-load" { print $NF }' kritis.txt)" "status cpu-load KRITIS"
assert_eq KRITIS "$(awk '$1 == "memori" { print $NF }' kritis.txt)" "status memori KRITIS"
assert_eq OK "$(awk '$1 == "disk:/" { print $NF }' kritis.txt)" "status disk / tetap OK"
assert_eq KRITIS "$(awk '$1 == "disk:/home" { print $NF }' kritis.txt)" "status disk /home KRITIS"
# --- uji 3: laporan waspada memuat campuran status -----------------------
assert_eq WASPADA "$(awk '$1 == "cpu-load" { print $NF }' laporan.txt)" "status cpu-load WASPADA"
assert_eq OK "$(awk '$1 == "disk:/" { print $NF }' laporan.txt)" "status disk / OK saat waspada"
# --- uji 4: log terstruktur ditulis --------------------------------------
jumlah=$(wc -l < monitor.log)
assert_eq 12 "$jumlah" "tiga eksekusi menulis 12 baris log"
jumlah=$(grep -c '^2026-01-15T09:00:00+07:00' monitor.log)
assert_eq 4 "$jumlah" "satu eksekusi menulis 4 baris berstempel sama"
jumlah=$(grep -c ' ALERT ' monitor.log)
assert_eq 0 "$jumlah" "belum ada baris ALERT sebelum hitungan cukup"
# --- uji 5: alert terkirim setelah N kali berturut-turut ------------------
# Hitungan dimulai dari nol; tiga bentuk pemanggilan sumber data
# diuji bergantian pada empat eksekusi berikut.
rm -f monitor.state
: > err.txt
MONITOR_STAMP=2026-01-15T10:00:00+07:00 bash monitor.sh --data-dir waspada \
> /dev/null 2>> err.txt
assert_eq 1 "$?" "run 1 (--data-dir) keluar 1"
MONITOR_STAMP=2026-01-15T10:01:00+07:00 bash monitor.sh --data-dir=waspada \
> /dev/null 2>> err.txt
assert_eq 1 "$?" "run 2 (--data-dir=DIR) keluar 1"
MONITOR_SOURCE=waspada MONITOR_STAMP=2026-01-15T10:02:00+07:00 bash monitor.sh \
> /dev/null 2>> err.txt
assert_eq 1 "$?" "run 3 (MONITOR_SOURCE) keluar 1"
MONITOR_STAMP=2026-01-15T10:03:00+07:00 bash monitor.sh --data-dir waspada \
> /dev/null 2>> err.txt
assert_eq 1 "$?" "run 4 keluar 1 tanpa alert baru"
assert_eq 4 "$(awk '$1 == "cpu-load" { print $2 }' monitor.state)" "hitungan cpu-load naik sampai 4"
jumlah=$(grep -c ALERT err.txt)
assert_eq 3 "$jumlah" "alert terkirim tepat sekali per metrik"
grep -q '^monitor.sh: ALERT cpu-load = 2.50 melewati ambang 3x' err.txt
assert_eq 0 "$?" "isi alert menyebut metrik dan nilainya"
MONITOR_STAMP=2026-01-15T10:04:00+07:00 bash monitor.sh --data-dir sehat \
> /dev/null
assert_eq 0 "$?" "eksekusi sehat setelah deret waspada keluar 0"
[ ! -s monitor.state ]
assert_eq 0 "$?" "state dikosongkan saat semua metrik kembali OK"
# --- uji 6: rotasi log ----------------------------------------------------
cat > rotasi.conf <<'CONF'
LOAD_WARN="1.50"
LOAD_KRITIS="3.00"
MEM_WARN_PCT=80
MEM_KRITIS_PCT=90
DISK_WARN_PCT=80
DISK_KRITIS_PCT=90
DISK_MOUNTS="/ /home"
LOG_FILE="log/monitor.log"
LOG_MAX_BYTES=1
LOG_KEEP=3
ALERT_REPEAT=3
CONF
for i in 1 2 3; do
MONITOR_CONF=rotasi.conf MONITOR_STAMP=2026-01-15T11:00:00+07:00 \
bash monitor.sh --data-dir sehat > /dev/null 2>&1
done
[ -f log/monitor.log.1 ]
assert_eq 0 "$?" "rotasi menghasilkan monitor.log.1"
[ -f log/monitor.log.2 ]
assert_eq 0 "$?" "rotasi kedua menghasilkan monitor.log.2"
[ ! -f log/monitor.log.3 ]
assert_eq 0 "$?" "LOG_KEEP=3 menahan log.3"
jumlah=$(wc -l < log/monitor.log)
assert_eq 4 "$jumlah" "log aktif berisi 4 baris setelah rotasi"
# --- uji 7: konfigurasi dan sumber tidak valid keluar 3 -------------------
MONITOR_CONF=halang.conf bash monitor.sh --data-dir waspada > /dev/null 2>&1
assert_eq 3 "$?" "berkas konfigurasi hilang keluar 3"
bash monitor.sh --data-dir waspada --bogus > /dev/null 2>&1
assert_eq 3 "$?" "argumen tidak dikenal keluar 3"
cat > terbalik.conf <<'CONF'
LOAD_WARN="3.00"
LOAD_KRITIS="1.50"
MEM_WARN_PCT=80
MEM_KRITIS_PCT=90
DISK_WARN_PCT=80
DISK_KRITIS_PCT=90
DISK_MOUNTS="/ /home"
LOG_FILE="monitor.log"
LOG_MAX_BYTES=1048576
LOG_KEEP=3
ALERT_REPEAT=3
CONF
MONITOR_CONF=terbalik.conf bash monitor.sh --data-dir waspada > /dev/null 2>&1
assert_eq 3 "$?" "ambang waspada >= kritis ditolak"
sed 's/MEM_WARN_PCT=80/MEM_WARN_PCT=delapan/' monitor.conf > rusak.conf
MONITOR_CONF=rusak.conf bash monitor.sh --data-dir waspada > /dev/null 2>&1
assert_eq 3 "$?" "ambang bukan angka ditolak"
cat > salah-mount.conf <<'CONF'
LOAD_WARN="1.50"
LOAD_KRITIS="3.00"
MEM_WARN_PCT=80
MEM_KRITIS_PCT=90
DISK_WARN_PCT=80
DISK_KRITIS_PCT=90
DISK_MOUNTS="/ /data"
LOG_FILE="log2/monitor.log"
LOG_MAX_BYTES=1048576
LOG_KEEP=3
ALERT_REPEAT=3
CONF
MONITOR_CONF=salah-mount.conf bash monitor.sh --data-dir waspada > /dev/null 2>&1
assert_eq 3 "$?" "mount yang tidak ada di df ditolak"
mkdir -p rusak
printf '2.50 2.10 1.60 4/812 34567\n' > rusak/loadavg
printf 'MemTotal: 8000000 kB\n' > rusak/meminfo
cp waspada/df.txt rusak/df.txt
bash monitor.sh --data-dir rusak > /dev/null 2>&1
assert_eq 3 "$?" "meminfo tanpa MemAvailable ditolak"
# --- ringkasan --------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
cd / && rm -rf "$WORK"
[ "$FAIL" -eq 0 ]