Di mesin pengembangan, kegagalan menunjukkan dirinya lewat terminal: stack trace muncul, proses berhenti, kita memperbaikinya. Di produksi tidak ada kemewahan itu — aplikasi berjalan di luar jangkauan mata, pengguna melaporkan gejala tanpa tahu penyebabnya, dan satu-satunya saksi yang setia adalah catatan yang aplikasi tinggalkan. Logging dan monitoring adalah pasangan yang memungkinkan kita menanyakan “apa yang terjadi kemarin malam” dan mendapat jawaban. Chapter ini menerapkan log terstruktur pada permintaan HTTP, membedakan level kejadian, dan membangun penghitung metrik sederhana — kelanjutan langsung dari logger terstruktur pada Chapter Teknik Logging.

Log sebagai Jejak Operasional #

Log produksi berbeda dari console.log saat debugging dalam tiga hal. Pertama, log dikumpulkan: layanan pengumpul log membacanya dari berkas atau stdout lalu menyatukannya dari banyak server menjadi satu tempat pencarian. Kedua, log dibaca mesin: pencarian dan penyaringan dilakukan per field, bukan dengan mata memindai teks. Ketiga, log berkelanjutan: volume-nya besar, sehingga kejadian kecil yang sering berbeda nilainya dari kejadian besar yang sekali. Ketiganya menuntut format yang sama disiplinnya dengan kode — satu kejadian, satu baris, field yang tetap.

Apa yang layak dicatat di server produksi? Setiap permintaan yang masuk (dengan status akhirnya), setiap galat yang tertangani (dengan pesannya), dan setiap kejadian siklus hidup — proses menyala, koneksi database terbuka, pematian teratur dimulai. Sebaliknya, hal yang tidak boleh masuk log justru lebih penting: sandi, token, dan data pribadi pengguna, seperti sudah kita bahas pada Chapter Manajemen Secret.

Satu Baris JSON per Permintaan #

Format yang kita bangun pada Section Debugging dan Code Quality — satu objek per baris — kini diterapkan pada lalu lintas HTTP. Demo berikut menjalankan server dengan tiga rute, mencatat setiap permintaan sebagai satu baris JSON, lalu mencetak ringkasannya. Waktu dibekukan pada jam contoh agar keluarannya stabil untuk dibedah:

js
// Log permintaan terstruktur + penghitung metrik sederhana.
// Waktu dibekukan agar contoh stabil untuk dipelajari.
import http from 'node:http';

const jamMulai = new Date(2026, 0, 15, 8, 0, 0);
let urutan = 0;
const penghitung = { total: 0, perStatus: {} };

function catatPermintaan(metode, jalur, status) {
  urutan += 1;
  const waktu = new Date(jamMulai.getTime() + urutan * 1000).toISOString();
  const level = status < 400 ? 'info' : 'warn';
  console.log(
    JSON.stringify({ waktu, level, metode, jalur, status })
  );
}

function hitung(status) {
  penghitung.total += 1;
  penghitung.perStatus[status] = (penghitung.perStatus[status] ?? 0) + 1;
}

const server = http.createServer((req, res) => {
  const jalur = req.url;

  if (jalur === '/statistik') {
    hitung(200);
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(penghitung));
    catatPermintaan(req.method, jalur, 200);
    return;
  }

  if (jalur === '/kesehatan') {
    hitung(200);
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'siaga' }));
  } else {
    hitung(404);
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'rute tidak dikenal' }));
  }
  catatPermintaan(req.method, jalur, res.statusCode);
});

server.listen(0, '127.0.0.1', async () => {
  const base = `http://127.0.0.1:${server.address().port}`;
  await fetch(`${base}/kesehatan`);
  await fetch(`${base}/kesehatan`);
  await fetch(`${base}/lupa`);
  const r = await fetch(`${base}/statistik`);
  console.log('isi /statistik :', JSON.stringify(await r.json()));

  console.log('ringkasan akhir:', JSON.stringify(penghitung));
  server.close();
  server.closeAllConnections();
});

Output yang Dihasilkan:

text
{"waktu":"2026-01-15T01:00:01.000Z","level":"info","metode":"GET","jalur":"/kesehatan","status":200}
{"waktu":"2026-01-15T01:00:02.000Z","level":"info","metode":"GET","jalur":"/kesehatan","status":200}
{"waktu":"2026-01-15T01:00:03.000Z","level":"warn","metode":"GET","jalur":"/lupa","status":404}
{"waktu":"2026-01-15T01:00:04.000Z","level":"info","metode":"GET","jalur":"/statistik","status":200}
isi /statistik : {"total":4,"perStatus":{"200":3,"404":1}}
ringkasan akhir: {"total":4,"perStatus":{"200":3,"404":1}}

Mari kita bedah komponennya:

  1. catatPermintaan(metode, jalur, status): satu fungsi pencatat tunggal — semua kejadian melewati pintu yang sama sehingga format seluruh baris dijamin seragam.
  2. waktu: dihitung dari jam beku ditambah nomor urut detik, lalu disimpan sebagai ISO string lewat toISOString(). Pada produksi, sumbernya new Date() saat itu juga; pembekuan di sini murni demi contoh yang bisa dibandingkan.
  3. level: diturunkan dari status — info untuk yang di bawah 400, warn untuk sisanya. Level inilah yang membuat penyaringan menjadi murah: “tampilkan hanya warn dan error dari satu jam terakhir”.
  4. hitung(status): penghitung diperbarui pada saat yang sama dengan pencatatan — log dan metrik tidak saling bertengkar soal jumlah.
  5. Rute /statistik: mengembalikan isi penghitung pada saat permintaan itu ditangani — perhatikan pada keluaran bahwa angkanya sudah mencakup permintaan /statistik itu sendiri karena hitung(200) dipanggil sebelum res.end().
  6. Pola server: listen(0) di 127.0.0.1, semua klien memakai fetch, dan close() ditutup dengan closeAllConnections() — kebiasaan yang sama dengan seluruh demo Course ini.

Level yang Benar untuk Setiap Kejadian #

Level bukan dekorasi — ia adalah kesepakatan tentang siapa memerlukan perhatian. Kesepakatan yang umum dipakai:

LevelMaknaContoh kejadian
debugdetail internal, hanya saat menyelidikasiisi payload yang gagal validasi
infokejadian normal yang patut tercatatpermintaan 200, proses menyala
warnanomali yang pulih sendiri, tapi perlu dipantaupermintaan 404 berulang dari satu alamat
errorkegagalan yang menuntut tindakan manusiakoneksi database terputus, handler melempar

Dua kesalahan yang paling sering menggerogoti tabel di atas: mencatat semuanya sebagai error (maka galat sungguhan tenggelam di antara derau), dan mencatat hal rutin sebagai debug lalu menyalakan debug di produksi saat krisis (maka volume log melonjak tepat saat server sedang sibuk). Aturan praktisnya: kalau sebuah baris tidak akan pernah dicari siapa pun, pertimbangkan untuk tidak menuliskannya.

Penghitung Sederhana sebagai Metrik #

Log menjawab “kejadian apa saja yang terjadi”; metrik menjawab “berapa banyak dan seberapa cepat”. Keduanya melengkapi: dari log kita bisa menggali satu kejadian spesifik, dari metrik kita bisa melihat tren yang tidak terlihat pada baris demi baris. Penghitung total dan perStatus pada demo adalah bentuk metrik paling sederhana — angka yang hanya naik, bisa diambil kapan saja lewat /statistik.

Metrik di dunia nyata biasanya diekspos dalam format standar agar dikumpulkan sistem pemantauan, dan populer dengan nama-nama seperti counter (jumlah kumulatif), gauge (nilai sesaat seperti jumlah koneksi aktif), dan histogram (sebaran seperti latency per persentil — konsep yang kita pelajari pada Chapter Load Testing). Untuk volume log yang besar, package seperti pino menawarkan pencatatan berkinerja tinggi dengan output JSON — pola field-nya sama persis dengan yang kita bangun sendiri di sini.

Memantau dari Luar #

Penghitung internal punya satu kelemahan: ia tidak bisa melaporkan bahwa prosesnya sendiri telah berhenti. Karena itu pemantauan produksi selalu punya lapisan luar — pengecek kesehatan yang menghubungi /kesehatan setiap beberapa detik dari mesin berbeda, dan memberi peringatan ketika jawabannya lambat, salah, atau tidak datang. Rute /kesehatan yang sengaja kita siapkan sejak Chapter Persiapan Deployment adalah kontrak untuk lapisan ini: semurhan mungkin, tanpa menyentuh database, cukup menyatakan “proses ini hidup dan event loop-nya berputar”.

Ada satu kejadian yang belum bisa ditangani lapisan mana pun: pematian yang tiba-tiba di tengah permintaan. Ketika process manager mengirim sinyal untuk memuat versi baru, permintaan yang sedang berjalan berhak diselesaikan lebih dulu — dan itulah topik chapter berikutnya: Graceful Shutdown.