Sepanjang Course ini kita menelusuri perilaku program dengan println!: mencetak isi variabel, menandai titik eksekusi yang dicurigai, lalu membaca hasilnya di terminal. Kebiasaan tersebut sah untuk program latihan, tetapi kebiasaan yang sama runtuh ketika program bertambah besar — baris debug berhamburan di antara output asli program, tidak ada saklar untuk mematikannya tanpa mengompilasi ulang, dan tidak ada petunjuk kapan sebuah baris dicetak. Chapter ini membedah masalah tersebut lewat program kasir kecil, membangun saklar logging pertama kita dengan variabel lingkungan, lalu menghitung sisa kebutuhan yang tidak mampu dipenuhi logger buatan sendiri. Peta kebutuhan inilah yang membawa kita ke crate log, env_logger, dan tracing pada empat Chapter berikutnya.

Masalah dengan println! untuk Debugging #

Kita mulai dari skenario yang paling sering terjadi. Bayangkan sebuah program kasir sederhana yang memproses pesanan pelanggan: setiap pesanan punya pengenal, nama pelanggan, harga satuan, dan jumlah item. Selama mengembangkan fitur perhitungan, kita menaruh println! bertanda [DEBUG] di setiap fungsi supaya alur perhitungan terlihat. Program berikut adalah hasil akhir kebiasaan tersebut.

rust
fn hitung_total(harga_satuan: u64, jumlah: u64) -> u64 {
    println!("[DEBUG] hitung_total: harga={} jumlah={}", harga_satuan, jumlah);
    harga_satuan * jumlah
}

fn proses_pesanan(pesanan_id: u64, pelanggan: &str, harga_satuan: u64, jumlah: u64) -> u64 {
    println!("[DEBUG] proses_pesanan mulai: pesanan={} pelanggan={}", pesanan_id, pelanggan);
    let total = hitung_total(harga_satuan, jumlah);
    println!("[DEBUG] proses_pesanan selesai: total={}", total);
    total
}

fn main() {
    let total_a = proses_pesanan(42, "Budi", 25_000, 3);
    let total_b = proses_pesanan(43, "Siti", 90_000, 1);

    println!("Total akhir: Rp{}", total_a + total_b);
}

Output yang Dihasilkan:

text
[DEBUG] proses_pesanan mulai: pesanan=42 pelanggan=Budi
[DEBUG] hitung_total: harga=25000 jumlah=3
[DEBUG] proses_pesanan selesai: total=75000
[DEBUG] proses_pesanan mulai: pesanan=43 pelanggan=Siti
[DEBUG] hitung_total: harga=90000 jumlah=1
[DEBUG] proses_pesanan selesai: total=90000
Total akhir: Rp165000

Mari kita bedah program di atas dan lihat di mana masalahnya mulai menumpuk:

  1. Baris 2 (println!("[DEBUG] hitung_total: ...")): macro println! mencetak label debug beserta isi dua parameter ke stdout. Baris tersebut berjalan setiap kali fungsi dipanggil, tanpa syarat apa pun.
  2. Baris 7 (println!("[DEBUG] proses_pesanan mulai: ...")): penanda awal eksekusi fungsi juga dicetak ke stdout — alur yang sama dengan hasil program.
  3. Baris 9 (println!("[DEBUG] proses_pesanan selesai: ...")): penanda akhir menambah tiga baris pembantu per pesanan; dua pesanan berarti enam baris [DEBUG].
  4. Baris 14-15 (let total_a = ...; let total_b = ...;): kedua pemanggilan memicu seluruh rantai println! debug di atas sebelum main sempat mencetak apa pun.
  5. Baris 17 (println!("Total akhir: Rp{}", total_a + total_b);): satu-satunya baris yang benar-benar dimaksudkan sebagai output program nyaris tenggelam di antara baris pembantu.

Output di atas memperlihatkan masalahnya secara kasat mata. Baris [DEBUG] bercampur dengan hasil program yang sesungguhnya karena println! menulis semuanya ke stdout, sehingga konsumen output — misalnya skrip yang membaca angka total — harus menyaring sampah debug sendiri. Kita juga tidak punya saklar: mematikan catatan debug berarti menghapus enam baris lalu mengompilasi ulang, dan menyalakannya kembali berarti mengulang pekerjaan yang sama. Tidak ada penanda waktu pada tiap baris, tidak ada nama fungsi atau module yang mencetak, dan semua catatan mendapat perlakuan sama — kita tidak bisa memilih hanya menampilkan kejadian penting.

Saklar Pertama: Variabel Lingkungan dan stderr #

Perbaikan pertama menambahkan dua mekanisme kecil. Pertama, program membaca variabel lingkungan TOKO_DEBUG saat berjalan lewat std::env::var: fungsi tersebut mengembalikan Result, dan ketika variabel tidak terisi, semua catatan debug dilewati. Kedua, catatan debug dipindahkan dari stdout ke stderr dengan eprintln!, sehingga hasil program dan catatan debug mengalir di dua stream yang terpisah. Kombinasi keduanya memberi kita saklar yang bisa diubah tanpa kompilasi ulang.

rust
fn catat(pesan: &str) {
    // Hanya cetak bila variabel lingkungan TOKO_DEBUG terisi
    if std::env::var("TOKO_DEBUG").is_ok() {
        eprintln!("[DEBUG] {}", pesan);
    }
}

fn hitung_total(harga_satuan: u64, jumlah: u64) -> u64 {
    catat(&format!("hitung_total: harga={} jumlah={}", harga_satuan, jumlah));
    harga_satuan * jumlah
}

fn proses_pesanan(pesanan_id: u64, pelanggan: &str, harga_satuan: u64, jumlah: u64) -> u64 {
    catat(&format!("proses_pesanan mulai: pesanan={} pelanggan={}", pesanan_id, pelanggan));
    let total = hitung_total(harga_satuan, jumlah);
    catat(&format!("proses_pesanan selesai: total={}", total));
    total
}

fn main() {
    let total_a = proses_pesanan(42, "Budi", 25_000, 3);
    let total_b = proses_pesanan(43, "Siti", 90_000, 1);

    println!("Total akhir: Rp{}", total_a + total_b);
}

Jalankan program tanpa variabel apa pun:

bash
cargo run

Output yang Dihasilkan:

text
Total akhir: Rp165000

Lalu nyalakan saklar debug dengan mengisi variabel lingkungan pada perintah yang sama:

bash
TOKO_DEBUG=1 cargo run

Output yang Dihasilkan:

text
[DEBUG] proses_pesanan mulai: pesanan=42 pelanggan=Budi
[DEBUG] hitung_total: harga=25000 jumlah=3
[DEBUG] proses_pesanan selesai: total=75000
[DEBUG] proses_pesanan mulai: pesanan=43 pelanggan=Siti
[DEBUG] hitung_total: harga=90000 jumlah=1
[DEBUG] proses_pesanan selesai: total=90000
Total akhir: Rp165000

Mari kita bedah perubahan penting pada program kedua:

  1. Baris 1-6 (fn catat(pesan: &str) ...): fungsi catat menjadi satu-satunya pintu keluar catatan debug. std::env::var("TOKO_DEBUG") mengembalikan Result<String, VarError>; pemanggilan .is_ok() bernilai true hanya ketika variabel terisi, dan pada kasus tersebut eprintln! mencetak pesan ke stderr.
  2. Baris 9 (catat(&format!("hitung_total: ..."))): pesan dibangun lebih dulu oleh format! menjadi String, lalu reference menuju string tersebut dikirim ke catat. Pola yang sama berlaku pada baris 14 dan 16.
  3. Baris 24 (println!("Total akhir: Rp{}", ...)): hasil program tetap ditulis ke stdout — satu-satunya penghuni stream tersebut setelah catatan debug pindah ke stderr.

Pemisahan dua stream tersebut tergambar sebagai berikut:

Diagram di atas menegaskan pembagian peran: stdout mengalirkan data hasil program untuk konsumen mesin, sedangkan stderr mengalirkan catatan untuk mata manusia. Keduanya bertemu di terminal secara bawaan, tetapi skrip dapat memilih membaca salah satu saja — misalnya cargo run 2>/dev/null untuk membuang catatan debug, atau cargo run 2> debug.log untuk menyimpannya ke berkas.

Yang Masih Kurang dari Logger Buatan Sendiri #

Saklar dan pemisahan stream menyelesaikan dua masalah dari daftar panjang kita. Tabel berikut merangkum kebutuhan yang masih terbuka setelah logger buatan sendiri dipasang:

KebutuhanKondisi pada logger buatan sendiri
Tingkat kepentingan (log level)Hanya nyala atau mati — pesan gagal koneksi dan catatan internal mendapat derajat yang sama
Penanda waktuTidak ada — urutan baris menjadi satu-satunya petunjuk kejadian
Asal pesan (module path)Tidak ada — nama fungsi harus ditulis manual di dalam teks pesan
Biaya saat saklar matiformat! tetap membangun String walaupun hasilnya tidak dicetak
Keseragaman antar crateSetiap library akan menciptakan variabel dan format catatan versinya sendiri

Baris terakhir pada tabel adalah masalah paling besar. Ketika program kita memakai sepuluh library dan setiap library memakai saklar berbeda — TOKO_DEBUG, HTTP_DEBUG, DB_TRACE — operator produksi harus menghafal semuanya. Daftar kebutuhan ini sebenarnya adalah definisi kerja dari logging sebagai fondasi observability: kemampuan kita untuk melihat keadaan internal program yang sedang berjalan tanpa menghentikan program tersebut. Ekosistem Rust menjawabnya dengan satu standar bersama berupa crate log, yang akan kita bahas pada Chapter berikutnya.

Ringkasan #

println! debug memboroskan perhatian pembaca, tidak bisa dimatikan tanpa kompilasi ulang, dan tidak membawa informasi waktu maupun asal kejadian. Logger buatan sendiri dengan variabel lingkungan dan eprintln! menambahkan saklar runtime serta memisahkan stdout dari stderr, tetapi tetap tidak menyediakan log level, timestamp, module path, hemat biaya, dan standar lintas crate. Seluruh kebutuhan itulah yang dipenuhi crate log melalui pola facade — materi Chapter selanjutnya.