Debugger membantu kita mengamati satu eksekusi secara mendalam, tetapi tidak semua pertanyaan dijawab dengan menjeda program. Ada peristiwa yang hanya terlihat polanya setelah berjam-jam aplikasi berjalan, ada kejadian langka yang tidak bisa kita reproduksi kapan pun kita mau, dan ada pertanyaan bisnis seperti “berapa banyak permintaan yang gagal kemarin”. Untuk semua itu kita butuh logging: penulisan catatan terstruktur yang sengaja direncanakan sebelum aplikasi berjalan. Pada chapter ini kita akan menyusun level log, memahami ke mana console mengalirkan keluarannya, lalu membangun logger kecil bergaya JSON yang lengkap dengan correlation ID.

Level Log dan Kapan Dipakainya #

Log yang baik tidak ditulis seragam — keluhannya digolongkan menurut tingkat urgensi. Penggolongan ini memungkinkan kita memilih detail yang tampil: baris debug yang ramai hanya dinyalakan saat menyelidiki masalah, sedangkan error selalu tercatat. Empat level berikut menjadi kesepakatan umum di ekosistem Node.js:

LevelIsi catatanContoh kejadian
debugDetail internal untuk penyelidikanIsi variabel saat loop berjalan, hasil kueri lengkap
infoPeristiwa normal yang pentingPermintaan diterima, pengguna berhasil masuk
warnKondisi mencurigakan tapi tidak fatalKuota hampir habis, percobaan ulang pertama
errorKegagalan yang mengganggu fungsiGagal menulis berkas, koneksi database terputus

Aturan praktisnya sederhana: error untuk hal yang layak membangunkan penjaga sistem di tengah malam, warn untuk hal yang perlu dipantau, info untuk jejak peristiwa utama, dan debug untuk sisanya. Kesalahan paling umum adalah mencatat segala hal sebagai error — pada aplikasi besar, kebiasaan ini membuat laporan kegagalan sesungguhnya tenggelam di antara kebisingan.

console dan Arah Alirannya #

Sebelum membangun logger sendiri, kita perlu tahu fakta penting tentang console: method yang berbeda menulis ke aliran yang berbeda. console.log dan console.info menuju stdout — keluaran normal program — sedangkan console.warn dan console.error menuju stderr, aliran khusus pesan bermasalah:

js
console.log("console.log   : menuju stdout");
console.info("console.info  : menuju stdout");
console.warn("console.warn  : menuju stderr");
console.error("console.error : menuju stderr");

Output yang Dihasilkan:

text
console.log   : menuju stdout
console.info  : menuju stdout
console.warn  : menuju stderr
console.error : menuju stderr

Pemisahan ini bukan sekadar formalitas. Ketika keluaran program diarahkan ke berkas dengan node aplikasi.mjs > laporan.txt, baris warn dan error tetap tampil di terminal karena lewat stderr — kegagalan tidak ikut tenggelam ke dalam berkas laporan. Sebaliknya, alat pengumpul log justru membaca stdout secara khusus, sehingga arah aliran yang benar membuat log kita ramah terhadap keduanya. Rincian seluruh method console tersedia di dokumentasi console .

Logging Terstruktur dengan JSON #

Bentuk log yang paling mudah ditulis manusia adalah kalimat bebas: “permintaan ke /api/tugas diterima”. Sayangnya, kalimat bebas sulit dicari, difilter, atau dihitung oleh mesin. Pendekatan structured logging menyeragamkan tiap baris log menjadi satu object JSON: satu baris, satu kejadian, sekumpulan field yang bermakna. Mari kita bangun logger kecil dengan pola tersebut:

js
const URUTAN_LEVEL = { debug: 10, info: 20, warn: 30, error: 40 };

// jam sengaja dibekukan agar contoh menghasilkan keluaran yang sama
const JAM_DEMO = new Date(Date.UTC(2026, 0, 15, 8, 0, 0));

function buatLogger(levelMinimum = "info") {
  function catat(level, pesan, konteks = {}) {
    if (URUTAN_LEVEL[level] < URUTAN_LEVEL[levelMinimum]) {
      return;
    }
    const baris = JSON.stringify({
      waktu: JAM_DEMO.toISOString(),
      level,
      pesan,
      ...konteks,
    });
    if (level === "warn" || level === "error") {
      console.error(baris);
    } else {
      console.log(baris);
    }
  }
  return {
    debug: (pesan, konteks) => catat("debug", pesan, konteks),
    info: (pesan, konteks) => catat("info", pesan, konteks),
    warn: (pesan, konteks) => catat("warn", pesan, konteks),
    error: (pesan, konteks) => catat("error", pesan, konteks),
  };
}

const levelMinimum = process.argv[2] ?? "info";
const log = buatLogger(levelMinimum);
const idPermintaan = "req-001";

log.debug("memeriksa koneksi database", { idPermintaan });
log.info("permintaan diterima", { idPermintaan, rute: "/api/tugas" });
log.warn("kuota penyimpanan hampir habis", { idPermintaan, sisaMegabyte: 3 });
log.error("gagal menulis berkas unggahan", { idPermintaan, kode: "EACCES" });

Output yang Dihasilkan:

text
{"waktu":"2026-01-15T08:00:00.000Z","level":"info","pesan":"permintaan diterima","idPermintaan":"req-001","rute":"/api/tugas"}
{"waktu":"2026-01-15T08:00:00.000Z","level":"warn","pesan":"kuota penyimpanan hampir habis","idPermintaan":"req-001","sisaMegabyte":3}
{"waktu":"2026-01-15T08:00:00.000Z","level":"error","pesan":"gagal menulis berkas unggahan","idPermintaan":"req-001","kode":"EACCES"}

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 1 (URUTAN_LEVEL): tiap level diberi angka sehingga membandingkan urutannya cukup dengan operator <.
  2. Baris 4 (JAM_DEMO): waktu dicetak dari tanggal beku, bukan new Date() tanpa argumen — pada aplikasi nyata kita memakai waktu sesungguhnya, tetapi contoh Course ini membekukan waktunya agar keluarannya selalu sama di setiap eksekusi.
  3. Baris 7-10 (gerbang level): baris dengan level di bawah levelMinimum langsung diabaikan; inilah mekanisme penyaring keramaian log.
  4. Baris 11-16 (JSON.stringify): object berisi waktu, level, pesan, lalu ...konteks meratakan field tambahan — apa pun yang kita kirim di parameter ketiga menjadi field sejajar.
  5. Baris 17-21 (pemilihan aliran): warn dan error lewat console.error agar tetap terlihat saat stdout dialihkan, mengikuti pembagian aliran pada bagian sebelumnya.
  6. Baris 23-28 (pengembalian object): logger mengembalikan empat function pendek, satu per level, sehingga pemanggilan di baris 35-38 terbaca seperti kalimat.

Level minimum bisa diatur dari argumen command line — coba jalankan dengan debug:

bash
node logger-terstruktur.mjs debug

Output yang Dihasilkan:

text
{"waktu":"2026-01-15T08:00:00.000Z","level":"debug","pesan":"memeriksa koneksi database","idPermintaan":"req-001"}
{"waktu":"2026-01-15T08:00:00.000Z","level":"info","pesan":"permintaan diterima","idPermintaan":"req-001","rute":"/api/tugas"}
{"waktu":"2026-01-15T08:00:00.000Z","level":"warn","pesan":"kuota penyimpanan hampir habis","idPermintaan":"req-001","sisaMegabyte":3}
{"waktu":"2026-01-15T08:00:00.000Z","level":"error","pesan":"gagal menulis berkas unggahan","idPermintaan":"req-001","kode":"EACCES"}

Dengan argumen debug, baris pemeriksaan koneksi database ikut tampil; tanpa argumen, baris tersebut tersaring. Pola serupa dipakai aplikasi nyata lewat environment variable seperti LOG_LEVEL.

Menyusun Konteks dengan Correlation ID #

Server melayani banyak permintaan hampir bersamaan, sehingga baris log dari permintaan yang berbeda saling berselang-seling. Tanpa penanda, urutan baris seperti permintaan diterima, kuota hampir habis, permintaan diterima, gagal menulis berkas tidak bisa dipilah milik siapa. Solusinya adalah correlation ID: penanda unik yang dibuat di awal permintaan lalu dibawa pada setiap baris log sepanjang hidupnya — pada contoh kita field idPermintaan bernilai req-001. Saat dua permintaan berjalan bergantian, keluarannya akan terlihat seperti ini:

text
{"waktu":"2026-01-15T08:00:01.000Z","level":"info","pesan":"permintaan diterima","idPermintaan":"req-001","rute":"/api/tugas"}
{"waktu":"2026-01-15T08:00:01.000Z","level":"info","pesan":"permintaan diterima","idPermintaan":"req-002","rute":"/api/pengguna"}
{"waktu":"2026-01-15T08:00:02.000Z","level":"warn","pesan":"kuota penyimpanan hampir habis","idPermintaan":"req-001","sisaMegabyte":3}
{"waktu":"2026-01-15T08:00:02.000Z","level":"error","pesan":"gagal menulis berkas unggahan","idPermintaan":"req-002","kode":"EACCES"}

Mencari seluruh kisah satu permintaan kini cukup menyaring field tersebut — grep req-002 langsung memisahkan empat baris di atas menjadi dua kisah yang utuh. Penanda yang sama sebaiknya ikut dikembalikan pada respons ke klien, sehingga laporan pengguna bisa dicocokkan dengan log.

Logger Khusus dan Ekosistem #

Logger buatan sendiri seperti di atas baik untuk memahami mekanismenya, tetapi kebutuhan produksi biasanya lebih besar: waktu dengan resolusi tinggi, penyaringan per modul, penulisan ke berkas, dan format yang bisa diganti tanpa mengubah kode pemanggil. Paket pino adalah pilihan populer di ekosistem Node.js karena cepat dan memang dirancang untuk JSON lines sejak awal; keluarganya menyediakan transport ke berbagai tujuan. Untuk aplikasi kecil, console pun tetap sah asal dipakai dengan sadar — dokumentasi MDN merinci seluruh method yang tersedia.

Log menjawab “apa yang terjadi” selama aplikasi berjalan. Pertanyaan berikutnya bersifat pencegahan: bagaimana menemukan pola kode bermasalah sebelum program dijalankan. Untuk itu kita membutuhkan linter — topik Chapter Linting dengan ESLint.