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.
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:
[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: Rp165000Mari kita bedah program di atas dan lihat di mana masalahnya mulai menumpuk:
- Baris 2 (
println!("[DEBUG] hitung_total: ...")): macroprintln!mencetak label debug beserta isi dua parameter ke stdout. Baris tersebut berjalan setiap kali fungsi dipanggil, tanpa syarat apa pun. - Baris 7 (
println!("[DEBUG] proses_pesanan mulai: ...")): penanda awal eksekusi fungsi juga dicetak ke stdout — alur yang sama dengan hasil program. - Baris 9 (
println!("[DEBUG] proses_pesanan selesai: ...")): penanda akhir menambah tiga baris pembantu per pesanan; dua pesanan berarti enam baris[DEBUG]. - Baris 14-15 (
let total_a = ...; let total_b = ...;): kedua pemanggilan memicu seluruh rantaiprintln!debug di atas sebelummainsempat mencetak apa pun. - 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.
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:
cargo runOutput yang Dihasilkan:
Total akhir: Rp165000Lalu nyalakan saklar debug dengan mengisi variabel lingkungan pada perintah yang sama:
TOKO_DEBUG=1 cargo runOutput yang Dihasilkan:
[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: Rp165000Mari kita bedah perubahan penting pada program kedua:
- Baris 1-6 (
fn catat(pesan: &str) ...): fungsicatatmenjadi satu-satunya pintu keluar catatan debug.std::env::var("TOKO_DEBUG")mengembalikanResult<String, VarError>; pemanggilan.is_ok()bernilaitruehanya ketika variabel terisi, dan pada kasus tersebuteprintln!mencetak pesan ke stderr. - Baris 9 (
catat(&format!("hitung_total: ..."))): pesan dibangun lebih dulu olehformat!menjadiString, lalu reference menuju string tersebut dikirim kecatat. Pola yang sama berlaku pada baris 14 dan 16. - 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:
| Kebutuhan | Kondisi pada logger buatan sendiri |
|---|---|
| Tingkat kepentingan (log level) | Hanya nyala atau mati — pesan gagal koneksi dan catatan internal mendapat derajat yang sama |
| Penanda waktu | Tidak 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 mati | format! tetap membangun String walaupun hasilnya tidak dicetak |
| Keseragaman antar crate | Setiap 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.