โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 10 ยท Deploy di AWS

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 idRequest di 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

JanganKenapa
Sandi, bahkan yang salahPembaca sering salah ketik email ke kolom sandi
signature_key MidtransSetara kredensial (Fase 6)
saved_token_idBisa dipakai menagih
Isi cookieID sesi = akses akun
Header Authorizationโ€”
Badan request POST secara utuhBisa berisi apa saja
Isi artikel premiumLog 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

AlarmAmbangBangunkan orang?
Tingkat 5xx> 1% selama 5 menitYa
Webhook pembayaran gagal> 3 dalam 10 menitYa โ€” ini uang
Koneksi database> 70% batasYa
Penyimpanan RDS< 20%Ya
Task berjalan < minimumSelama 5 menitYa
p95 latensi> 1 detik selama 10 menitJam kerja saja
Cache hit ratio< 85%Tidak โ€” dashboard
Purge cache gagalNaikTidak โ€” dashboard
Lag replica> 5 detikJam 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

PanelSumber
Request/detik ke originALB RequestCount
Cache hit ratioCloudflare Analytics
p50 / p95 / p99 latensiALB TargetResponseTime
Tingkat error 4xx / 5xxALB + filter metrik
Task berjalanECS
CPU & memoriECS
Koneksi databaseRDS
Lag replicaRDS
Langganan aktif hari iniMetrik kustom dari aplikasi
Webhook diproses vs gagalFilter 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.