Application Logging — DevOps

Mengapa Logging Penting? Logging adalah cara utama untuk memahami apa yang terjadi di aplikasi kita, terutama di production dimana kita tidak bisa attach…

Log adalah catatan yang ditulis aplikasi sambil jalan, satu baris per kejadian: kapan terjadi, seberapa penting, apa yang terjadi, dan konteksnya. Bayangin buku laporan shift di minimarket. Tiap kejadian ditulis dengan jam, tanda pentingnya (biasa, perhatian, gawat), dan detailnya. Manajer yang sibuk cuma membaca yang bertanda gawat. Waktu ada selisih kas jam tiga sore, dia membaca semua catatan di sekitar jam itu. Membaca log di server persis seperti itu: saring menurut tingkat penting, lalu menurut waktu.

catatan-api sekarang jalan sebagai container di web-1 (pelajaran Docker Compose dan CI/CD). Waktu masih jalan langsung lewat systemd, log-nya ditulis ke storage/logs/app.log dan kamu membacanya dengan tail dan grep (pelajaran Command Line Essentials). Di pelajaran ini log-nya kita rapikan: tiap baris berbentuk JSON, punya level, dan ditulis ke stdout.

🧪 Tebakan cepat: di Lab ada 16 baris log catatan-api: 3 debug, 7 info, 3 warn, 3 error. Kamu pasang filter level warn. Berapa baris yang tampil, 3 atau 6? Jawabannya ada di bagian "Level: seberapa penting".

Satu baris log, empat bagian

Ini satu baris yang ditulis catatan-api setelah request selesai:

{"time":"2026-10-09T10:41:02.114Z","level":"info","msg":"request completed","reqId":"r-101","method":"GET","path":"/catatan","status":200,"ms":38,"userId":42}

Log seperti ini disebut structured logging: tiap baris berformat terstruktur (JSON) yang bisa di-query per field, misalnya "semua baris dengan status 500 dan path /catatan". Bandingkan dengan teks biasa User 42 GET /catatan 200 38ms: untuk mengambil angka 38 kamu harus menebak posisinya. Jadi minimal tiap baris punya timestamp, level, message, dan konteks (request ID, user ID).

Level: seberapa penting

Level Dipakai untuk Contoh di catatan-api
debug detail untuk developer saat melacak bug isi query SQL, cache hit
info kejadian normal yang ingin dihitung request selesai, user login
warn belum gagal, tapi tanda bahaya query 870 ms, pool koneksi 9 dari 10
error satu operasi gagal, app masih jalan koneksi database timeout, request 500
fatal app gak bisa lanjut, proses berhenti DATABASE_URL kosong saat start

Urutannya debug < info < warn < error < fatal. Logger diberi level minimum, biasanya dari environment variable LOG_LEVEL. Baris di bawah minimum langsung dibuang, sisanya dicetak. Filter warn berarti warn dan semua yang lebih penting, jadi 3 warn + 3 error = 6 baris. Itu jawaban tebakannya. Di production biasanya info, di laptop debug.

Sebagian sistem punya delapan level, misalnya syslog dan Monolog (dipakai Laravel): debug, info, notice, warning, error, critical, alert, emergency. Tiga yang terakhir menandai kegagalan serius yang harus segera ditangani, misalnya database utama mati atau seluruh app down.

Logger kecil dengan level minimum

const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };

function createLogger(minLevel) {
  function log(level, message, data) {
    if (LEVELS[level] < LEVELS[minLevel]) return null; // di bawah minimum: dibuang
    const entry = { level, message, data, timestamp: new Date().toISOString() };
    console.log(JSON.stringify(entry)); // satu baris JSON ke stdout
    return entry;
  }

  return {
    debug: (message, data) => log("debug", message, data),
    info: (message, data) => log("info", message, data),
    warn: (message, data) => log("warn", message, data),
    error: (message, data) => log("error", message, data),
  };
}
Panggilan Perbandingan Hasil
createLogger("info").info("test", { a: 1 }) 1 < 1? gak object { level: "info", message: "test", data: { a: 1 }, timestamp }
createLogger("info").debug("query", {}) 0 < 1? ya null
createLogger("warn").info("test", {}) 1 < 2? ya null
createLogger("error").warn("warning", {}) 2 < 3? ya null

Di latihan, timestamp ditulis tetap "2024-01-01" supaya hasilnya bisa dicek, dan kamu cukup me-return object-nya.

Request log catatan-api dan cara memfilternya

Pukul 10:42 UTC beberapa user melapor catatannya gagal dimuat. Lab memuat 16 baris log dari 10:41 sampai 10:44. Untuk mencari penyebabnya, ambil baris warn ke atas di menit 10:42:

function filterLogs(lines, minLevel, from, to) {
  return lines
    .map((line) => JSON.parse(line))
    .filter((log) => LEVELS[log.level] >= LEVELS[minLevel])
    .filter((log) => {
      const t = Date.parse(log.time);
      return t >= Date.parse(from) && t < Date.parse(to);
    });
}

filterLogs(lines, "warn", "2026-10-09T10:42:00.000Z", "2026-10-09T10:43:00.000Z");
time level msg konteks
10:42:03.215 warn db pool almost full used 9, max 10
10:42:07.402 error db connection timeout reqId r-105, timeoutMs 5000
10:42:07.405 error request failed reqId r-105, GET /catatan, 500, 5.003 ms
10:42:19.044 error request failed reqId r-107, POST /catatan, 500, 5.001 ms

Dari 16 baris tersisa 4, dan urutannya bercerita: pool koneksi hampir penuh, satu koneksi timeout setelah 5 detik, lalu dua request jadi 500. reqId r-105 menyambungkan error database dengan request yang gagal. Di menit 10:41 ada warn query 870 ms: query lambat itu yang mulai menahan koneksi.

Yang gak boleh masuk log

Log dibaca banyak orang dan disalin ke banyak tempat: disk server, layanan log, backup. Jangan pernah menulis:

Data pribadi seperti email dan nomor HP ditulis seperlunya saja, atau disamarkan. Kesalahan klasiknya me-log seluruh request body. Baris login gagal di Lab membawa password Sari sebagai teks biasa. Hapus di sumbernya, sebelum baris dicetak. Di pino caranya opsi redact:

const logger = pino({ level: process.env.LOG_LEVEL || "info", redact: ["body.password", "req.headers.authorization"] });

Field yang disebut ditulis sebagai "[Redacted]". Pelajaran Security Logging & Monitoring di track Security membahas sisi sebaliknya: kejadian apa yang wajib dicatat untuk keamanan, seperti login gagal dan perubahan hak akses.

Di container: tulis ke stdout

App di container sebaiknya gak menulis file log sendiri. Tulis ke stdout (yang dicetak console.log), lalu biarkan platform yang menyimpan dan mengirimnya. Docker menyimpan stdout tiap container, dan kamu membacanya dengan:

deploy@web-1:/srv/catatan-api$ docker compose logs --since 10m app
app-1  | {"time":"2026-10-09T10:41:02.114Z","level":"info","msg":"request completed","reqId":"r-101", ...}

-f mengikuti log secara live, --no-log-prefix membuang awalan app-1 | . Supaya disk gak penuh, batasi ukurannya di docker-compose.yml:

services:
  app:
    image: ghcr.io/budi/catatan-api:e4b2d90
    environment:
      LOG_LEVEL: info
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Log yang disimpan Docker ikut hilang saat container dihapus, dan tiap server punya log sendiri. Begitu app jalan di banyak container atau server, log dikirim oleh shipper (Vector, Fluent Bit, Grafana Alloy) ke satu tempat terpusat seperti Loki, Elasticsearch, atau CloudWatch Logs. Gunanya: request yang lewat beberapa service bisa disambungkan lewat request ID, log tetap ada walau container-nya sudah hilang, dan semuanya di-query dari satu tempat. Polanya dibahas lebih dalam di pelajaran Structured Logging Patterns dan OpenTelemetry Fundamentals.

⚠️ Jebakan yang sering ketemu

TL;DR: Log yang baik satu baris JSON per kejadian dengan time (ISO 8601, UTC), level, msg yang tetap, dan konteks seperti reqId dan userId. Level urut debug < info < warn < error < fatal. Level minimum dari LOG_LEVEL membuang yang di bawahnya, dan filter warn berarti warn ke atas. Di syslog dan Monolog, critical, alert, dan emergency menandai kegagalan serius yang harus segera ditangani. Untuk mencari penyebab masalah, saring per level lalu per jendela waktu, dan sambungkan baris lewat request ID. Jangan pernah me-log password, token, header Authorization, atau cookie: redact di logger sebelum dicetak. Di container, tulis ke stdout, baca dengan docker compose logs, batasi ukurannya, dan kirim ke log terpusat (Loki, Elasticsearch, CloudWatch) begitu server-nya lebih dari satu.

Yang akan kamu pelajari