Log, metrik & alarm
Yang menentukan berapa lama sebuah insiden berlangsung bukan seberapa cepat kamu memperbaiki, melainkan seberapa cepat kamu tahu apa yang rusak.
Intisari
- Log JSON satu baris, selalu. CloudWatch Logs Insights bisa mengueri field-nya langsung.
- Sertakan
idRequestdi setiap baris โ itu yang menghubungkan satu perjalanan request. - Jangan pernah mencatat sandi, tanda tangan, token, atau isi cookie.
- Alarm hanya untuk hal yang butuh tindakan manusia. Sisanya dashboard.
- Retensi log punya biaya โ 30 hari untuk aplikasi, lebih panjang untuk audit keuangan.
Log terstruktur
// src/lib/log.ts
type Tingkat = "debug" | "info" | "warn" | "error";
export function log(tingkat: Tingkat, pesan: string, data: Record<string, unknown> = {}) {
// Satu baris JSON. CloudWatch mem-parsing field-nya otomatis.
console.log(JSON.stringify({
t: new Date().toISOString(),
level: tingkat,
msg: pesan,
...data,
}));
}
// src/middleware/log.ts
export const catatAkses = defineMiddleware(async (ctx, next) => {
const t0 = performance.now();
const res = await next();
const ms = Math.round(performance.now() - t0);
// Jangan catat aset โ jumlahnya mendominasi tanpa memberi informasi.
if (!ctx.url.pathname.startsWith("/_astro/")) {
log(res.status >= 500 ? "error" : "info", "request", {
idRequest: ctx.locals.idRequest,
metode: ctx.request.method,
jalur: ctx.url.pathname,
status: res.status,
ms,
cache: res.headers.get("cache-control")?.slice(0, 40),
ua: ctx.request.headers.get("user-agent")?.slice(0, 80),
// IP di-hash: cukup untuk mengelompokkan, tidak menyimpan data pribadi
ip: hashIp(ctx.locals.ipPembaca),
});
}
return res;
});
Meng-hash IP alih-alih menyimpannya mentah adalah keputusan yang murah dan bijak. Kamu tetap bisa mengelompokkan request dari sumber yang sama, tapi log-mu tidak lagi berisi data pribadi yang tunduk pada kewajiban penyimpanan dan penghapusan. Untuk portal dengan jutaan pembaca, itu mengurangi lingkup kepatuhan dengan satu fungsi.
Menghubungkan satu perjalanan request
// Sertakan idRequest di setiap log yang terjadi selama request itu
export async function ambilArtikelPublik(slug: string, idRequest: string) {
const t0 = performance.now();
try {
return await dbBaca.selectFrom("artikel")./* โฆ */executeTakeFirst();
} finally {
const ms = performance.now() - t0;
if (ms > 100) log("warn", "kueri lambat", { idRequest, kueri: "artikel.bySlug", ms });
}
}
Di Cloudflare, header cf-ray adalah ID request yang sama yang
terlihat di log Cloudflare. Memakainya sebagai idRequest berarti
kamu bisa menelusuri satu request dari edge sampai kueri database.
Mengueri dengan Logs Insights
# Jalur paling lambat
fields jalur, ms
| filter ispresent(ms)
| stats count() as n, avg(ms) as rata, pct(ms, 95) as p95, max(ms) as maks by jalur
| sort p95 desc | limit 20
# Semua log untuk satu request
fields @timestamp, level, msg, ms, kueri
| filter idRequest = "8f2a1b3c-..."
| sort @timestamp asc
# Error per jenis
fields msg
| filter level = "error"
| stats count() as n by msg
| sort n desc
# Kueri lambat, dikelompokkan
fields kueri, ms
| filter msg = "kueri lambat"
| stats count() as n, avg(ms) as rata, max(ms) as maks by kueri
| sort n desc
# Webhook pembayaran yang gagal
fields @timestamp, order_id, status
| filter msg like /webhook/ and level = "error"
| sort @timestamp desc | limit 50
Yang tidak boleh masuk log
| Jangan | Kenapa |
|---|---|
| Sandi, bahkan yang salah | Pembaca sering salah ketik email ke kolom sandi |
signature_key Midtrans | Setara kredensial (Fase 6) |
saved_token_id | Bisa dipakai menagih |
| Isi cookie | ID sesi = akses akun |
Header Authorization | โ |
| Badan request POST secara utuh | Bisa berisi apa saja |
| Isi artikel premium | Log lebih mudah diakses daripada database |
const SENSITIF = new Set([
"sandi", "password", "signature_key", "saved_token", "token",
"authorization", "cookie", "secret", "apiKey",
]);
export function bersihkanLog(o: Record<string, unknown>): Record<string, unknown> {
const hasil: Record<string, unknown> = {};
for (const [k, v] of Object.entries(o)) {
hasil[k] = SENSITIF.has(k.toLowerCase()) ? "[disunting]" : v;
}
return hasil;
}
Metrik dari log
aws logs put-metric-filter \
--log-group-name /ecs/portal-web \
--filter-name error-5xx \
--filter-pattern '{ $.status >= 500 }' \
--metric-transformations \
metricName=Error5xx,metricNamespace=Portal,metricValue=1,defaultValue=0
aws logs put-metric-filter \
--log-group-name /ecs/portal-web \
--filter-name webhook-gagal \
--filter-pattern '{ $.msg = "webhook" && $.level = "error" }' \
--metric-transformations \
metricName=WebhookGagal,metricNamespace=Portal,metricValue=1,defaultValue=0
Alarm: hanya yang butuh tindakan
| Alarm | Ambang | Bangunkan orang? |
|---|---|---|
| Tingkat 5xx | > 1% selama 5 menit | Ya |
| Webhook pembayaran gagal | > 3 dalam 10 menit | Ya โ ini uang |
| Koneksi database | > 70% batas | Ya |
| Penyimpanan RDS | < 20% | Ya |
| Task berjalan < minimum | Selama 5 menit | Ya |
| p95 latensi | > 1 detik selama 10 menit | Jam kerja saja |
| Cache hit ratio | < 85% | Tidak โ dashboard |
| Purge cache gagal | Naik | Tidak โ dashboard |
| Lag replica | > 5 detik | Jam kerja saja |
Alarm yang membangunkan orang untuk hal yang tidak bisa mereka perbaiki jam tiga pagi akan diabaikan โ dan setelah itu alarm yang penting juga ikut diabaikan. Ini kegagalan sosial, bukan teknis, dan memperbaikinya lebih sulit daripada memasang alarmnya. Mulai dengan sedikit alarm yang benar-benar berarti, dan tambahkan hanya setelah ada insiden yang seharusnya tertangkap.
Retensi
aws logs put-retention-policy \
--log-group-name /ecs/portal-web --retention-in-days 30
aws logs put-retention-policy \
--log-group-name /ecs/portal-pembayaran --retention-in-days 365
Log CloudWatch ditagih per GB yang diserap dan per GB yang disimpan. Untuk portal dengan trafikmu,
mencatat setiap aset akan menghasilkan tagihan yang tidak sepadan โ karena itu filter _astro
di middleware. Log pembayaran dipisahkan ke grup sendiri dengan retensi panjang, karena pertanyaan tentang
transaksi bisa datang berbulan-bulan kemudian.
Dashboard
| Panel | Sumber |
|---|---|
| Request/detik ke origin | ALB RequestCount |
| Cache hit ratio | Cloudflare Analytics |
| p50 / p95 / p99 latensi | ALB TargetResponseTime |
| Tingkat error 4xx / 5xx | ALB + filter metrik |
| Task berjalan | ECS |
| CPU & memori | ECS |
| Koneksi database | RDS |
| Lag replica | RDS |
| Langganan aktif hari ini | Metrik kustom dari aplikasi |
| Webhook diproses vs gagal | Filter metrik |
Dua panel terakhir yang paling sering dilihat orang non-teknis. Menaruhnya di dashboard yang sama dengan metrik infrastruktur membuat percakapan tentang gangguan jauh lebih cepat: "apakah ini memengaruhi pendapatan" bisa dijawab dalam lima detik.
Latihan: ubah seluruh console.log di proyekmu jadi JSON satu baris dengan
idRequest. Deploy, hasilkan trafik, lalu jalankan keempat kueri Logs Insights di atas. Buat
satu alarm untuk tingkat 5xx dan uji dengan sengaja membuat endpoint yang melempar error. Terakhir,
periksa log-mu untuk kata "signature", "token", dan "password" โ hasilnya harus nol.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.