Logger buatan sendiri pada Chapter sebelumnya memenuhi dua kebutuhan dasar — saklar runtime dan aliran terpisah — tetapi juga memperlihatkan betapa banyak detail yang harus kita bangun sendiri: level kepentingan, penanda waktu, nama module, sampai hemat biaya format. Ekosistem Rust menyerahkan bagian yang seragam dari pekerjaan tersebut kepada satu crate kecil bernama log. Crate log tidak pernah mencetak apa pun; log hanya menyediakan bahasa bersama berupa macro dan trait Log, lalu membiarkan pemilik program memilih implementasi pencetakan yang diinginkan. Pembagian kerja semacam ini disebut pola facade, dan memahami pola tersebut adalah kunci untuk memakai env_logger pada Chapter berikutnya.
Pola Facade pada Crate log #
Dalam arsitektur perangkat lunak, facade adalah lapisan antarmuka sederhana yang berdiri di depan banyak implementasi berbeda. Crate log menerapkan pola tersebut untuk logging dengan pembagian tugas yang tegas: macro error!, warn!, info!, debug!, dan trace! tidak memanggil fungsi cetak apa pun. Setiap macro membangkitkan sebuah Record berisi tiga informasi — level pesan, target berupa module path tempat macro ditulis, dan args berisi teks yang sudah diformat — lalu Record tersebut diserahkan kepada satu implementasi trait Log yang terpasang secara global. Ketika belum ada implementasi yang terpasang, seluruh macro menjadi no-op: program tetap berjalan normal tanpa mencetak satu baris pun.
Pembagian tersebut menyelesaikan masalah library pada Chapter sebelumnya. Penulis library cukup bergantung pada crate log dan memanggil macro-nya; pemilik binary-lah yang memilih implementasi — logger buatan sendiri, env_logger, atau jembatan menuju tracing. Library tidak lagi memaksakan destinasi output kepada pemakainya, karena hak memilih destinasi berpindah ke tangan pemilik binary.
Diagram di atas menegaskan posisi facade: banyak sumber pesan di atas, banyak kandidat implementasi di bawah, dan crate log menjadi satu titik temu yang seragam. Sebuah program hanya boleh memasang satu implementasi global, sehingga seluruh pesan dari library maupun binary mengalir ke destinasi yang dipilih sekali di satu tempat.
Lima Level Pesan #
Crate log mendefinisikan lima level dengan urutan kepentingan yang ketat. Level menentukan dua hal: seberapa penting sebuah pesan, dan ambang batas yang dipakai implementasi untuk menyaring pesan.
| Level | Peruntukan | Contoh |
|---|---|---|
error | Kegagalan yang menghentikan operasi | koneksi ke database gagal |
warn | Kondisi mencurigakan, program masih lanjut | sisa stok tinggal 2 item |
info | Kejadian operasional yang penting | pesanan 42 telah dikirim |
debug | Detail internal untuk pengembang | durasi query 17 ms |
trace | Denyut paling halus dari eksekusi | buffer 128 byte dialokasikan |
Urutan pada tabel bukan hiasan: error menempati posisi tertinggi dan trace terendah, sehingga menyaring dengan level info berarti menampilkan info, warn, dan error sekaligus. Perjanjian makna inilah yang membuat saklar RUST_LOG=info pada Chapter berikutnya punya arti yang sama untuk semua crate di ekosistem.
Program Tanpa Logger: Senyap #
Kita buktikan dulu klaim utama facade: tanpa implementasi terpasang, macro log tidak mencetak apa pun. Buat proyek baru dengan satu dependency:
[package]
name = "myapp"
version = "0.1.0"
edition = "2024"
[dependencies]
log = "0.4"Lalu tulis program yang memanggil kelima macro:
use log::{debug, error, info, trace, warn};
fn main() {
error!("koneksi ke database gagal");
warn!("sisa stok tinggal 2 item");
info!("pesanan 42 telah dikirim");
debug!("durasi query: 17 ms");
trace!("buffer 128 byte dialokasikan");
}Jalankan program tersebut:
cargo runOutput yang Dihasilkan:
Blok output di atas memang kosong: program berjalan sampai selesai dengan kode keluar 0 tanpa mencetak satu baris pun. Lima macro di dalam main semuanya dijalankan, tetapi karena tidak ada implementasi Log yang terpasang, setiap macro berhenti sebelum menyentuh terminal. Perilaku senyap ini sering mengejutkan pemula yang baru menyalin log::info! dari dokumentasi — pesan tidak hilang, pesan tersebut hanya menunggu implementasi yang mau menerimanya.
Memasang Logger Buatan Sendiri #
Sekarang kita isi sisi kosong facade dengan implementasi buatan sendiri. Trait Log hanya menuntut tiga method: enabled untuk pemeriksaan murah, log untuk perekaman, dan flush untuk menguras penyangga. Implementasi berikut mencetak setiap Record ke stderr dengan format [LEVEL target] pesan — format yang kita rancang sendiri, karena keputusan destinasi dan format memang milik implementasi.
use log::{debug, error, info, set_logger, set_max_level, trace, warn, Level, LevelFilter, Log, Metadata, Record};
struct SimpleLogger;
impl Log for SimpleLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Debug
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
eprintln!("[{} {}] {}", record.level(), record.target(), record.args());
}
}
fn flush(&self) {}
}
static LOGGER: SimpleLogger = SimpleLogger;
fn main() {
set_logger(&LOGGER).expect("logger sudah terpasang sebelumnya");
set_max_level(LevelFilter::Debug);
error!("koneksi ke database gagal");
warn!("sisa stok tinggal 2 item");
info!("pesanan 42 telah dikirim");
debug!("durasi query: 17 ms");
trace!("buffer 128 byte dialokasikan");
}Output yang Dihasilkan:
[ERROR myapp] koneksi ke database gagal
[WARN myapp] sisa stok tinggal 2 item
[INFO myapp] pesanan 42 telah dikirim
[DEBUG myapp] durasi query: 17 msMari kita bedah program di atas baris demi baris:
- Baris 1 (
use log::{debug, error, info, ...}): kita mengimpor lima macro besertaLevel,LevelFilter,Log,Metadata,Record, dan dua fungsi pemasanganset_loggersertaset_max_level. - Baris 3 (
struct SimpleLogger;): unit struct kosong menjadi wadah implementasi; struct tersebut tidak butuh data apa pun karena format dan ambang level kita tulis langsung di dalam method. - Baris 5-8 (
impl Log for SimpleLoggerdanenabled): methodenabledmenerimaMetadatadan membandingkan level pesan denganLevel::Debug— pesanTracetidak lolos karenaTraceberada di bawahDebugdalam urutan kepentingan. - Baris 10-14 (
fn log(&self, record: &Record)): methodlogmenerimaRecordlengkap, memanggilenabledsekali lagi, lalu mencetak tiga bagianRecord—record.level()untuk level,record.target()untuk module path, danrecord.args()untuk teks pesan. - Baris 16 (
fn flush(&self) {}):flushkita kosongkan karenaeprintln!langsung menulis tanpa penyangga. - Baris 19 (
static LOGGER: SimpleLogger = SimpleLogger;):set_loggermenuntut reference berumur'static, sehingga kita menaruhSimpleLoggerdi dalamstaticagar alamatnya berlaku sepanjang umur program. - Baris 22 (
set_logger(&LOGGER).expect(...)): fungsi ini memasangSimpleLoggersebagai implementasi global; pemanggilan kedua atas fungsi tersebut akan gagal, dan.expectmengubah kegagalan itu menjadipanic!dengan pesan jelas. - Baris 23 (
set_max_level(LevelFilter::Debug)): gerbang pertama — level maksimum global kita kunci diDebug, sehingga pesantrace!tertolak bahkan sebelumenabledsempat dipanggil.
Pesan trace! sengaja tidak muncul pada output, dan absennya baris tersebut adalah bukti dua gerbang bekerja. Gerbang pertama adalah LevelFilter::Debug dari set_max_level: pesan trace! dicekal di titik macro dengan satu perbandingan bilangan yang murah. Gerbang kedua adalah method enabled pada implementasi, yang menyaring ulang dengan aturan milik kita. Perhatikan juga bahwa target tercetak sebagai myapp tanpa kita menuliskannya — module path direkam otomatis oleh macro dari lokasi kode.
Pembagian Peran Library dan Binary #
Praktik nyata dari pola facade terlihat pada dua Cargo.toml yang berbeda. Penulis library cukup mendeklarasikan crate log tanpa implementasi apa pun, karena library hanya berbicara lewat macro.
# Cargo.toml milik library
[dependencies]
log = "0.4"Pemilik binary menambahkan satu baris implementasi pilihan — di sini env_logger — dan memasangnya sekali di awal main:
# Cargo.toml milik binary yang memakai library tersebut
[dependencies]
log = "0.4"
env_logger = "0.11"Ketika binary memanggil env_logger::init(), seluruh pesan dari library ikut mengalir ke destinasi yang sama karena keduanya bertemu di facade yang identik. Kita tidak perlu lagi menghafal saklar seperti TOKO_DEBUG atau HTTP_DEBUG milik tiap library — satu mekanisme mengatur semuanya.
Ringkasan #
Crate log adalah facade logging standar ekosistem Rust: macro error!, warn!, info!, debug!, dan trace! membangkitkan Record berisi level, target, dan teks, lalu menyerahkannya kepada satu implementasi trait Log yang dipasang global. Tanpa implementasi terpasang, seluruh macro menjadi no-op yang senyap. Implementasi buatan sendiri menunjukkan tiga method enabled, log, dan flush, serta dua gerbang penyaringan lewat set_max_level dan enabled. Menulis implementasi sendiri memberi kendali penuh, tetapi sebagian besar program lebih baik memakai implementasi siap pakai yang dikendalikan variabel lingkungan — env_logger, bintang Chapter berikutnya.