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:
// 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:
{"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:
catatPermintaan(metode, jalur, status): satu fungsi pencatat tunggal — semua kejadian melewati pintu yang sama sehingga format seluruh baris dijamin seragam.waktu: dihitung dari jam beku ditambah nomor urut detik, lalu disimpan sebagai ISO string lewattoISOString(). Pada produksi, sumbernyanew Date()saat itu juga; pembekuan di sini murni demi contoh yang bisa dibandingkan.level: diturunkan dari status —infountuk yang di bawah 400,warnuntuk sisanya. Level inilah yang membuat penyaringan menjadi murah: “tampilkan hanya warn dan error dari satu jam terakhir”.hitung(status): penghitung diperbarui pada saat yang sama dengan pencatatan — log dan metrik tidak saling bertengkar soal jumlah.- Rute
/statistik: mengembalikan isipenghitungpada saat permintaan itu ditangani — perhatikan pada keluaran bahwa angkanya sudah mencakup permintaan/statistikitu sendiri karenahitung(200)dipanggil sebelumres.end(). - Pola server:
listen(0)di127.0.0.1, semua klien memakaifetch, danclose()ditutup dengancloseAllConnections()— 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:
| Level | Makna | Contoh kejadian |
|---|---|---|
debug | detail internal, hanya saat menyelidikasi | isi payload yang gagal validasi |
info | kejadian normal yang patut tercatat | permintaan 200, proses menyala |
warn | anomali yang pulih sendiri, tapi perlu dipantau | permintaan 404 berulang dari satu alamat |
error | kegagalan yang menuntut tindakan manusia | koneksi 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.