Menjadikan produksi tidak buta: structured logging yang bisa dicari dan dianalisis, taksonomi error yang konsisten, error handler terpusat, dan tracing dasar untuk melacak satu request melintasi layanan

Setelah di episode 10 alur kalian menjadi asinkron dan tersebar di worker, muncul masalah baru: bagaimana tahu apa yang terjadi di produksi? Di tahap awal, kalian mungkin men-debug dengan console.log — tetapi di produksi, log itu tidak terstruktur, tersebar di banyak proses, dan tidak bisa dicari. Inilah saat observability masuk.
Mengapa logging & error handling penting? Karena bug produksi hampir selalu menyerang saat kita tidak melihat langsung. Log yang baik mengubah "entah kenapa rusak" menjadi "di endpoint ini, untuk user ini, terjadi error ini pada langkah itu". Episode ini membangun structured logging, taksonomi error, error handler terpusat, dan tracing dasar.
console.log("order created", orderId, userId, "total:", total)Log ini sulit dicari, sulit di-filter, dan tidak konsisten formatnya. Di produksi dengan ribuan request, log seperti ini menjadi dinding teks yang tidak berguna. Solusinya: structured logging — log sebagai data (JSON), bukan sebagai kalimat.
Structured logging mencatat field berpasangan sehingga bisa di-query: filter per orderId, hitung per endpoint, alarm per level.
import pino from "pino"
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
base: { service: "shop-api" },
})
logger.info({ orderId: 87, userId: 5 }, "order_created")
logger.error(
{ err, orderId: 87, step: "payment" },
"payment_failed",
)Output-nya adalah JSON yang bisa dianalisis:
{
"level": 30,
"time": 1784149200000,
"service": "shop-api",
"msg": "order_created",
"orderId": 87,
"userId": 5
}Aturan praktis:
order_created, payment_failed) — stabil dan bisa dipakai untuk alerting, bukan kalimat yang berubah-ubah.userId, orderId, requestId agar satu kejadian bisa dilacak.Semua error sebaiknya dikelompokkan agar tahu mana yang perlu alarm dan mana yang normal:
| Kelas | Contoh | Tindakan |
|---|---|---|
| Client error | 400, 401, 404, 409 | Log warn, bukan error |
| Expected failure | Payment ditolak | Log warn + metric |
| Unexpected error | Exception tak dikenal | Log error + alert + stack trace |
| Fatal | Out of memory | Log fatal + alert segera |
Kuncinya: jangan beri error untuk sesuatu yang sebenarnya normal. Alarm yang terlalu banyak membuat tim mati rasa dan mengabaikan yang benar-benar penting.
Kode kalian harus melempar error ke satu titik yang mencatat dan merespons dengan konsisten — bukan menangani error di tiap handler secara berbeda.
export class HttpError extends Error {
constructor(
public status: number,
public code: string,
public message: string,
public details?: unknown,
) {
super(message)
}
}import type { NextFunction, Request, Response } from "express"
import { HttpError } from "../classes/http-error.js"
import { logger } from "./logger.js"
export function errorHandler(
err: unknown,
req: Request,
res: Response,
_next: NextFunction,
) {
if (err instanceof HttpError) {
logger.warn(
{ code: err.code, status: err.status, path: req.path },
"client_error",
)
return res.status(err.status).json({
error: err.code,
message: err.message,
details: err.details,
})
}
logger.error({ err, path: req.path }, "unhandled_error")
res.status(500).json({
error: "INTERNAL_ERROR",
message: "Terjadi kesalahan di server",
})
}Handler ini memisahkan dua kelas: error yang diketahui (HttpError → respons bersih + log warn) dan yang tidak diketahui (log error + 500 generik). Jangan pernah membocorkan stack trace ke client — itu intel untuk penyerang.
Note
Pastikan error handler dipasang setelah semua route dan menangkap error async. Framework modern (Express 5, Fastify) meneruskan rejection Promise otomatis ke error handler; framework lama wajib memakai wrapper. Uji dengan sengaja melempar error di satu endpoint untuk memastikan alur ini bekerja.
Log terstruktur mencatat banyak hal, tapi saat satu request melewati banyak layanan (API → queue → worker → database), bagaimana menyatukan semua log itu? Jawabannya tracing dengan traceId yang dibawa dari awal hingga akhir.
import { randomUUID } from "node:crypto"
app.use((req, res, next) => {
const traceId = req.headers["x-trace-id"] ?? randomUUID()
req.traceId = traceId
res.setHeader("x-trace-id", traceId)
next()
})Setiap log di dalam satu request membawa traceId yang sama, sehingga saat ada error bisa dilakukan:
curl -s http://localhost:3000/v1/orders/87 | grep -o '"traceId":"[^"]*"' | head -1
# lalu filter log dengan traceId tersebutUntuk produksi yang lebih serius, pakai OpenTelemetry — standar terbuka yang mengirim trace ke Jaeger/Tempo, menggabungkan log, metrik, dan trace dalam satu dasbor. Kita sentuh ini di episode 25.
Jangan menulis log ke file manual di dalam aplikasi — biarkan log mengalir ke stdout, dan prosesor log eksternal (systemd, Docker, atau agent logging) yang menangani file dan rotasi.
services:
api:
image: shop-api:latest
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"Log terstruktur dikirim ke sistem agregasi (ELK, Loki, CloudWatch) dan dijadikan alarm:
payment_failed berulang dalam 5 menit.Tip
Mulailah sesederhana mungkin: pino untuk structured log ke stdout, x-trace-id untuk menghubungkan log, dan sebuah dasbor sederhana. Jangan langsung pasang platform observability penuh — log terstruktur yang konsisten lebih berharga daripada tooling yang canggih.
Mencatat email, token, atau body penuh request = kebocoran data diam-diam. Redact field sensitif di serializer log (episode 19).
errorClient error yang normal di-log sebagai error membuat alarm banjir. warn untuk yang diketahui, error untuk yang tidak terduga.
logger.error("gagal") tanpa orderId/traceId tidak bisa diinvestigasi. Selalu lampirkan konteks yang cukup untuk reproduksi.
Response 500 harus generik; detail error hanya di log server.
Episode 11 menjadikan sistem terlihat: structured logging dengan pino, taksonomi error yang mengelompokkan berdasarkan urgensi, error handler terpusat, dan tracing dengan traceId untuk menyatukan perjalanan satu request.
Inti yang harus dibawa pulang:
console.log dengan structured logging (JSON) yang bisa di-query.warn; tak terduga → error + alarm.traceId menyatukan log satu request lintas layanan.Di episode 12 selanjutnya kita akan membedah arsitektur: microservices & modular monolith — perbandingan jujur kedua pendekatan dan tren 2026 yang pragmatis. Sampai jumpa di episode 12!