Sebelum memutuskan apa itu “sehat” dan apa itu “bahaya”, skrip monitor harus lebih dulu bisa menjawab pertanyaan yang jauh lebih sederhana: berapa nilainya? Bagian ini fokus pada tiga fungsi pengumpul data — load average, memori, dan disk — masing-masing dibangun dari sumber mentahnya dengan awk. Ada satu tantangan khusus yang kita selesaikan sejak awal: /proc sungguhan berubah setiap milidetik sehingga hampir mustahil diuji, jadi kita membekukan ketiga sumber itu menjadi fixture dan merancang skrip agar sumbernya bisa diganti. Keputusan kecil ini yang membuat seluruh proyek bisa diverifikasi otomatis pada Bagian 3.

Tiga Sumber, Tiga Bentuk Data #

Ketiga metrik yang dipantau datang dari tiga sumber dengan bentuk data yang berbeda, dan perbedaan bentuk inilah yang menentukan cara mem-parsenya:

MetrikSumberBentuk data
Load average 1 menit/proc/loadavgsatu baris, lima kolom dipisah spasi
Memori terpakai/proc/meminfobanyak baris Nama: nilai kB
Disk terpakaikeluaran df -Ptabel: satu baris per filesystem

/proc adalah filesystem semu yang isinya disediakan langsung oleh kernel — membacanya tidak menimbulkan akses disk sama sekali, dan bentuknya terdokumentasi stabil di proc(5) . Adapun df -P dipilih, bukan df polos, karena format POSIX-nya menjamin satu baris per filesystem dengan kolom tetap; opsi lengkapnya ada di df(1) .

Membekukan Data: Fixture #

Blok berikut menyiapkan folder fixture berisi tiruan ketiga sumber — keadaan satu server pada satu titik waktu, dibekukan. Angka-angkanya dipilih sengaja: load 2.50 yang mulai tidak sehat, memori terpakai 84%, dan partisi /home yang mulai penuh:

bash
mkdir -p fixture
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
cat fixture/loadavg
head -n 3 fixture/meminfo
cat fixture/df.txt

Output yang Dihasilkan:

text
2.50 2.10 1.60 4/812 34567
MemTotal:        8000000 kB
MemFree:          950000 kB
MemAvailable:    1280000 kB
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

Blok-blok pada halaman ini bersifat mandiri: bila sebuah blok membutuhkan berkas fixture/, blok itu membuatnya sendiri di awal. Anda dengan demikian bisa mengulang blok mana pun tanpa menjalankan blok sebelumnya — dan test suite di Bagian 3 memakai berkas-berkas yang sama persis.

Load Average dari /proc/loadavg #

Lima kolom /proc/loadavg berisi load 1 menit, 5 menit, dan 15 menit, lalu jumlah proses running/total, dan PID terakhir. Kita mengambil ketiga beban itu sekali jalan, tetapi yang dipantau skrip adalah kolom pertama — load 1 menit bereaksi paling cepat terhadap masalah yang baru mulai:

bash
mkdir -p fixture
printf '2.50 2.10 1.60 4/812 34567\n' > fixture/loadavg
awk '{ print $1, $2, $3 }' fixture/loadavg
awk '{ printf "%.0f\n", $1 * 100 }' fixture/loadavg

Output yang Dihasilkan:

text
2.50 2.10 1.60
250

Perintah kedua menyimpan satu keputusan penting: load dikalikan 100 menjadi bilangan bulat (2.50 menjadi 250). Bash hanya mengenal aritmetika bilangan bulat, sedangkan load adalah desimal — daripada memaksakan perbandingan desimal di bash, kita menormalkan satuan sekali di awal. Format %.0f membulatkan ke bilangan bulat terdekat, bukan memotong, sehingga hasil kali titik-mengambang seperti 4.10 × 100 tetap jatuh ke 410 dan bukan 409.

Memori dari /proc/meminfo #

/proc/meminfo adalah daftar baris Nama: nilai kB, jadi parsenya memakai awk dengan pencocokan kolom pertama. Yang dihitung adalah (MemTotal − MemAvailable) / MemTotal, bukan MemFree — MemAvailable sudah memperhitungkan cache dan buffer yang bisa direklamasi kapan pun aplikasi membutuhkannya, sehingga jauh lebih jujur menggambarkan memori yang benar-benar tertekan. Perbedaan keduanya dibahas tuntas di Chapter Memory Monitoring :

bash
mkdir -p fixture
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
awk '$1 == "MemTotal:" || $1 == "MemAvailable:" { print $1, $2 }' fixture/meminfo
awk '
    $1 == "MemTotal:"     { total = $2 }
    $1 == "MemAvailable:" { avail = $2 }
    END { printf "%.0f\n", (total - avail) * 100 / total }
' fixture/meminfo

Output yang Dihasilkan:

text
MemTotal: 8000000
MemAvailable: 1280000
84

Bedah skrip kedua:

  1. Aksi pertama berjalan untuk setiap baris yang kolom pertamanya MemTotal: — nilai 8000000 disimpan ke variabel total. Aksi kedua serupa untuk MemAvailable.
  2. Blok END baru berjalan setelah seluruh berkas dibaca, saat kedua variabel sudah pasti terisi — inilah pola dua-fase awk: kumpulkan dulu, rangkum kemudian.
  3. printf "%.0f" membulatkan 84 persen hasil pembagian; pembulatan penting karena 84.6% ditampilkan 85%, bukan dipotong menjadi 84% oleh %d.
  4. Tidak ada if di akhir: bila MemAvailable tidak ditemukan, avail kosong dan hasilnya 100. Versi final di src/monitor.sh karenanya menandai baris itu sebagai wajib dan menolak data yang tidak lengkap.

Disk dari df -P #

Dengan -P, kolom keenam selalu menjadi mount point dan kolom kelima berisi persen dengan tanda %. Pencocokan dilakukan persis pada kolom enam, bukan grep pada seluruh baris, supaya /home tidak ikut mencocokkan /home2:

bash
mkdir -p fixture
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
awk '$6 == "/" { print $5 }' fixture/df.txt
awk -v mnt="/home" '$6 == mnt { print $5 }' fixture/df.txt
awk -v mnt="/home" '$6 == mnt { gsub(/%/, "", $5); print $5 + 0; exit }' fixture/df.txt

Output yang Dihasilkan:

text
48%
89%
89

Dua baris pertama hanya memperlihatkan bentuk mentahnya (48%, 89%). Baris ketiga adalah versi yang dipakai skrip: gsub menghapus tanda persen, + 0 memaksa awk mengubah sisanya menjadi angka, dan exit berhenti di baris pertama yang cocok — pada df sungguhan ada puluhan baris, dan mencocokkan lebih dari satu tidak memberi apa-apa. Mount mana yang dipantau ditentukan konfigurasi, bukan ditebak skrip; itu pekerjaan Bagian 2.

Kenapa Sumber Bisa Dioverride #

Sekarang bagian yang paling sering diabaikan pembuat skrip: /proc/loadavg sungguhan berbeda isinya setiap kali dibaca. Itu bukan masalah untuk laporan sekali jalan, tetapi fatal untuk pengujian — assertion seperti “status harus WASPADA” mustahil ditulis bila nilainya berganti tiap eksekusi. Solusinya bukan menyerah pada pengujian, melainkan membalik arah ketergantungan: skrip tidak menulis jalur /proc/loadavg secara kaku, melainkan membacanya dari variabel yang bisa diganti.

Bukti bahwa pendekatan ini bekerja cukup satu baris awk yang sama pada dua fixture berbeda:

bash
mkdir -p fixture kritis
printf '2.50 2.10 1.60 4/812 34567\n' > fixture/loadavg
printf '4.10 3.20 2.10 9/640 55002\n' > kritis/loadavg
awk '{ printf "%.0f\n", $1 * 100 }' fixture/loadavg kritis/loadavg

Output yang Dihasilkan:

text
250
410

Parser yang sama menghasilkan 250 untuk fixture pertama dan 410 untuk fixture kedua — keadaan kritis bisa disimulasikan tanpa benar-benar membebani CPU. Pada skrip final, mekanisme penggantian sumber ini muncul sebagai opsi --data-dir DIR (atau variabel MONITOR_SOURCE): bila diisi, skrip membaca loadavg, meminfo, dan df.txt dari folder itu; bila kosong, ia memakai /proc dan df -P sungguhan. Test suite di Bagian 3 sepenuhnya berjalan pada fixture, sehingga lulusnya suite adalah bukti perilaku skrip — bukan kebetulan mesin sedang sehat.

Tiga pengumpul data kini siap: load dari /proc/loadavg, memori dari /proc/meminfo, dan disk dari df -P. Angka mentah saja belum berarti apa-apa tanpa tolok ukur — pada Bagian 2 kita memberi tiap metrik ambang waspada dan kritis dari berkas konfigurasi, lalu menyusunnya menjadi laporan yang bisa dibaca sekilas.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

src/monitor.sh

Unduh berkas
bash
#!/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
bash
#!/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 ]