Membangun observability aplikasi: morgan untuk request log, pino untuk structured logging dengan level dan request id, serta alur debugging interaktif dengan Node inspector dan debug.

Aplikasi yang berjalan adalah aplikasi yang perlu diselidiki saat gagal. Episode 17 membangun dua kemampuan investigasi: logging (merekam apa yang terjadi) dan debugging (mencari tahu mengapa sesuatu salah).
Mengapa ini bukan prioritas belakangan? Karena di produksi, satu-satunya jendela ke dalam aplikasi adalah log. Error handler episode 7 sudah memanggil console.error — sekarang kita menaikkan itu menjadi sistem yang benar: request log otomatis, structured logging yang bisa di-query, level log yang terkelola, dan request id yang menelusuri satu request dari masuk hingga keluar.
morgan (dikenalkan episode 5) mencatat tiap request. Konfigurasi level format dan skip dapat dibuat pintar:
import morgan from "morgan"
app.use(
morgan("combined", {
skip: (req, res) =>
req.path === "/health" || res.statusCode < 400,
})
)Format "combined" menghasilkan baris log standar Apache:
::1 - - [16/Aug/2026:12:00:01 +0700] "GET /users HTTP/1.1" 200 512 "-" "Mozilla/5.0 ..."Pola skip di atas menghindari spam: request sukses dan ping /health tidak dicatat, sementara error 4xx/5xx selalu tercatat. Di produksi, lewati log body dan token — mereka hanya akan membanjiri log dengan data sensitif.
console.log mencetak teks bebas yang sulit di-query. Structured logging mencetak JSON — setiap baris berisi field yang bisa difilter (status, latency, error). Pino adalah pilihan populer karena cepat (overhead rendah) dan produksi-default:
npm install pinoimport pino from "pino"
import { config } from "../config.js"
export const logger = pino({
level: config.logging.level,
redact: {
paths: ["req.headers.authorization", "req.body.password", "res.body.token"],
censor: "[REDACTED]",
},
})Dua opsi penting:
level mengontrol verbositas — debug di development, info di produksi (dari config.js episode 16).redact menyensor field sensitif — bukti bahwa log tidak akan pernah berisi token atau password, meski ada yang salah memanggil.import { logger } from "../utils/logger.js"
export const getUser = async (req, res) => {
const started = Date.now()
try {
const user = await User.findById(req.params.id).lean()
if (!user) throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
res.json({ data: user })
} finally {
logger.info({
userId: req.user?.id,
latencyMs: Date.now() - started,
path: req.path,
}, "getUser selesai")
}
}Baris log yang dihasilkan (dalam bentuk JSON) memungkinkan pertanyaan seperti: "tunjukkan semua request getUser dengan latency di atas 500ms dari user X" — sesuatu yang mustahil dengan console.log biasa.
JSON mentah sulit dibaca manusia. Di development, cetak dengan pino-pretty:
npm install -D pino-pretty{
"scripts": {
"dev": "node --watch src/server.js | pino-pretty",
"start": "node src/server.js"
}
}pino-pretty mengubah JSON menjadi baris berwarna yang mudah dipindai — tanpa mengubah kode, karena pino sudah menghasilkan JSON sejak awal.
Di aplikasi yang sibuk, log dari banyak request tercampur. Request id mengelompokkan semua log milik satu request — trik sederhana dengan middleware custom:
import { randomUUID } from "node:crypto"
export const requestId = (req, res, next) => {
const id = req.get("X-Request-Id") || randomUUID()
req.id = id
res.setHeader("X-Request-Id", id)
next()
}Dipasang paling awal (di atas morgan) dan dipakai di logger:
import { logger } from "../utils/logger.js"
export const errorHandler = (err, req, res, next) => {
const status = err.status || 500
logger.error({
requestId: req.id,
status,
path: req.path,
err: err.stack,
})
res.status(status).json({ error: err.code || "INTERNAL_ERROR", message: err.message })
}Saat user melapor "request dengan id abc-123 gagal", kalian tinggal menggrep id itu di seluruh log dan melihat perjalanan lengkap request — termasuk error handler episode 7 yang kini terhubung dengan sistem ini.
Tip
Konsumen API juga bisa mengirim X-Request-Id sendiri; server meneruskannya ke log. Ini menciptakan korelasi ujung-ke-ujung antara client dan server — pola yang di episode 25 diperluas ke distributed tracing.
Node.js punya inspector bawaan — tanpa dependency tambahan:
node --inspect src/server.js
# tampilkan Chrome DevTools: chrome://inspectJalankan dengan breakpoint:
node --inspect-brk src/server.js--inspect-brk berhenti di baris pertama kode sebelum dieksekusi — kalian bisa menambahkan breakpoint di DevTools, lalu resume. Debugger ini menunjukkan stack, variabel lokal, dan bisa mengevaluasi ekspresi di titik berhenti.
export const createUser = async (req, res) => {
debugger // Node inspector berhenti di sini
const { name, email } = req.body
// periksa nilai name, email, dan req.headers
const user = await User.create({ name, email })
res.status(201).json({ data: user })
}Aturan praktis debugging:
req.body, req.params, dan hasil query.Untuk log debug yang bisa dihidupkan/matikan per modul tanpa mengubah kode, pakai debug:
npm install debugimport debug from "debug"
const dbg = debug("app:auth")
export const login = async (req, res) => {
dbg("login dimulai untuk %s", req.body?.email)
// ...
dbg("login berhasil untuk %s", req.body.email)
}DEBUG=app:auth npm run devNamespace (app:auth, app:users) membuat log debug granular: matikan semua, nyalakan hanya yang sedang diselidiki.
Body request berisi data pribadi, header berisi token. Selalu redact (pino redact) dan jangan pernah mencatat body login.
Log di tiap baris kode = noise. Log peristiwa bernilai: awal/akhir request, error, transisi state penting. Verbositas diturunkan di produksi lewat level.
--inspect membuka port debug yang berbahaya jika terekspos. Jangan pernah menjalankannya di produksi tanpa pembatasan akses.
Note
Urutan observasi: error handler (episode 7) → config (episode 16) → logging pino + request id (episode 17). Di episode 21 dan 25 kita tambahkan metrik dan tracing — seluruhnya berpijak pada struktur log JSON yang konsisten ini.
Episode 17 membuat aplikasi bisa diselidiki: morgan untuk request log otomatis, pino untuk structured JSON dengan level dan redaction, request id untuk korelasi, dan Node inspector + debug untuk debugging interaktif.
Inti yang harus dibawa pulang:
"combined" mencatat request; lewati /health dan status sukses agar tidak spam.redact menyensor field sensitif.pino-pretty untuk development, JSON mentah untuk produksi.X-Request-Id) mengelompokkan semua log satu request.node --inspect + debugger, dan namespace debug yang bisa dinyalakan selektif.Di episode 18 selanjutnya kita akan mengeraskan keamanan: security best practices — Helmet, konfigurasi CORS, sanitasi input, SQLi/NoSQLi, XSS, CSRF, dan secure headers. Sampai jumpa di episode 18!