Sejauh ini kita memantau aplikasi dengan System.out.println — sah untuk belajar, tetapi gagal di tiga ujian produksi: tidak ada level (semua baris sama pentingnya), tidak ada struktur (grep satu-satunya alat baca), dan tidak ada siklus hidup (berkas log membesar tanpa batas sampai mengisi disk). Log adalah data diagnosis utama saat insiden terjadi tengah malam, dan kualitasnya ditentukan jauh sebelum insiden itu lahir. Chapter ini membahas tiga komponen yang menjadikan log produksi layak pakai: saringan level, format terstruktur dengan konteks korelasi, dan kebijakan rotasi-retensi.

Level dan Saringan #

Setiap peristiwa log diberi level yang menunjukkan kadar pentingnya:

LevelArtiKapan dipakai
TRACEjejak paling halusmengikuti alur method detail
DEBUGinformasi diagnostikvariabel antara, keputusan cabang
INFOperistiwa bisnis normalaplikasi menyala, pesanan dibuat
WARNkondisi mencurigakanretry, fallback aktif, nilai tak terduga
ERRORkegagalan yang butuh perhatianrequest gagal, koneksi hilang

Saringan bekerja dengan ambang: aplikasi diatur INFO, maka seluruh peristiwa TRACE/DEBUG tidak pernah di-format apalagi ditulis — bukan sekadar disembunyikan dari tampilan. Di lingkungan pengembangan ambang diturunkan ke DEBUG lewat konfigurasi eksternal, tanpa mengubah satu baris kode.

Log Terstruktur untuk Agregator #

Log produksi tidak dibaca baris demi baris oleh manusia — dikumpulkan ke satu pusat (Elasticsearch, Loki) lalu diinterogasi. Agar bisa diinterogasi, baris log harus punya bidang: format JSON dengan ts, level, logger, msg, plus MDC (Mapped Diagnostic Context) — konteks per request yang menempel otomatis pada setiap baris, seperti requestId di bawah. Ekosistem Javanya standar: SLF4J sebagai antarmuka, Logback sebagai implementasi:

xml logback.xml — JSON per baris dengan kebijakan rotasi
<configuration>
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <maxFileSize>50MB</maxFileSize>
      <maxHistory>14</maxHistory>
      <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
    </rollingPolicy>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
  </appender>
  <root level="INFO">
    <appender-ref ref="FILE"/>
  </root>
</configuration>

Kebijakan rotasinya membaca: berkas maksimum 50 MB, arsip disimpan 14 hari, lalu dikompresi .gz — tiga angka itu adalah seluruh kebijakan retensi.

Kerangka Logging Mini #

Bagaimana level, struktur, dan rotasi bekerja di balik logger.info(...)? Program berikut merakit ketiganya dalam satu kerangka kecil:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

/** Logging mini: filter level, log terstruktur JSON dengan MDC, dan rotasi berkas. */
public class LogTerstruktur {

    enum Level { TRACE, DEBUG, INFO, WARN, ERROR }

    /** Satu peristiwa log: waktu beku, level, logger, pesan, dan konteks MDC. */
    record Peristiwa(LocalDateTime waktu, Level level, String logger,
                     String requestId, String pesan) {}

    static final Level AMBANG = Level.INFO;
    static final DateTimeFormatter FORMAT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

    /** MDC ala SLF4J: konteks per thread yang ikut setiap baris log. */
    static final ThreadLocal<String> MDC_REQUEST_ID = new ThreadLocal<>();

    /** Lolos saringan ambang: level >= AMBANG. */
    static boolean lolos(Level level) {
        return level.ordinal() >= AMBANG.ordinal();
    }

    /** Bentuk baris JSON — pengganti sederhana encoder JSON milik Logback. */
    static String json(Peristiwa p) {
        String pesanAman = p.pesan().replace("\\", "\\\\").replace("\"", "\\\"");
        String mdc = (p.requestId() == null) ? "{}" : "{\"requestId\":\"" + p.requestId() + "\"}";
        return "{\"ts\":\"" + FORMAT.format(p.waktu()) + "\",\"level\":\"" + p.level()
                + "\",\"logger\":\"" + p.logger() + "\",\"mdc\":" + mdc
                + ",\"msg\":\"" + pesanAman + "\"}";
    }

    public static void main(String[] args) {
        LocalDateTime dasar = LocalDateTime.of(2026, 1, 15, 8, 0, 0);
        List<Peristiwa> semuanya = List.of(
                new Peristiwa(dasar.plusSeconds(1), Level.DEBUG, "db.pool", null,
                        "koneksi dibuka"),
                new Peristiwa(dasar.plusSeconds(1), Level.INFO, "server", null,
                        "aplikasi menyala"),
                new Peristiwa(dasar.plusSeconds(2), Level.WARN, "db.pool", null,
                        "koneksi ulang setelah timeout"),
                new Peristiwa(dasar.plusSeconds(3), Level.ERROR, "api", "req-77",
                        "gagal memproses pesanan \"P-1042\""));

        System.out.println("== Saringan level (ambang " + AMBANG + ") ==");
        for (Peristiwa p : semuanya) {
            if (lolos(p.level())) {
                System.out.printf("[%s] %s - %s%n", String.format("%-5s", p.level()),
                        p.logger(), p.pesan());
            } else {
                System.out.printf("[%s] %s - %s  (DILEWATI: di bawah ambang)%n",
                        String.format("%-5s", p.level()), p.logger(), p.pesan());
            }
        }
        System.out.println();

        System.out.println("== Bentuk terstruktur (JSON) untuk agregator ==");
        for (Peristiwa p : semuanya) {
            if (lolos(p.level())) {
                System.out.println(json(p));
            }
        }
        System.out.println();

        System.out.println("== Rotasi berkas: batas 400 byte ==");
        List<String> baris = new ArrayList<>();
        for (Peristiwa p : semuanya) {
            if (lolos(p.level())) {
                baris.add(json(p));
            }
        }
        baris.addAll(List.of(
                "{\"ts\":\"2026-01-15T08:05:00\",\"level\":\"INFO\",\"logger\":\"api\",\"mdc\":{},\"msg\":\"GET /api/pesanan 200\"}",
                "{\"ts\":\"2026-01-15T08:05:01\",\"level\":\"INFO\",\"logger\":\"api\",\"mdc\":{},\"msg\":\"GET /api/pesanan 200\"}"));
        int batas = 400;
        int nomorBerkas = 0;
        long terisi = 0;
        int[] perBerkas = new int[3];
        for (String b : baris) {
            if (terisi + b.getBytes().length > batas && nomorBerkas < 2) {
                nomorBerkas++;
                terisi = 0;
            }
            terisi += b.getBytes().length + 1; // +1 untuk baris baru
            perBerkas[nomorBerkas]++;
        }
        String[] nama = {"app.log", "app.log.1", "app.log.2"};
        for (int i = 0; i <= nomorBerkas; i++) {
            System.out.printf("  %-12s %d baris%n", nama[i], perBerkas[i]);
        }
    }
}

Output yang Dihasilkan:

text
== Saringan level (ambang INFO) ==
[DEBUG] db.pool - koneksi dibuka  (DILEWATI: di bawah ambang)
[INFO ] server - aplikasi menyala
[WARN ] db.pool - koneksi ulang setelah timeout
[ERROR] api - gagal memproses pesanan "P-1042"

== Bentuk terstruktur (JSON) untuk agregator ==
{"ts":"2026-01-15T08:00:01","level":"INFO","logger":"server","mdc":{},"msg":"aplikasi menyala"}
{"ts":"2026-01-15T08:00:02","level":"WARN","logger":"db.pool","mdc":{},"msg":"koneksi ulang setelah timeout"}
{"ts":"2026-01-15T08:00:03","level":"ERROR","logger":"api","mdc":{"requestId":"req-77"},"msg":"gagal memproses pesanan \"P-1042\""}

== Rotasi berkas: batas 400 byte ==
  app.log      3 baris
  app.log.1    2 baris

Mari kita bedah kerangkanya:

  1. lolos(level): saringan ambang bekerja lewat ordinal() enum — WARN.ordinal() >= INFO.ordinal() sehingga lolos, DEBUG tertinggal. Bandingkan urutan deklarasi enum Level: urutan itu adalah skala prioritasnya.
  2. record Peristiwa(...): peristiwa log sebagai nilai immutable dengan waktu beku — pada pustaka nyata, timestamp diambil jam sistem saat peristiwa terjadi.
  3. json(...): encoder manual yang membungkus bidang-bidang ke JSON — perhatikan pesanAman yang meng-escape kutip ganda dan backslash; baris ERROR di keluaran memperlihatkan hasilnya (\"P-1042\"). Baris yang rusak formatnya = baris yang hilang dari indeks agregator.
  4. MDC: field requestId hanya terisi pada peristiwa ketiga. Pada Logback nyata, mekanisme ini di atas ThreadLocal — filter servlet menaruh requestId di awal request, seluruh baris log di thread itu membawanya, dibersihkan di akhir.
  5. Bagian rotasi: lima baris JSON dialirkan ke berkas dengan batas 400 byte. Penulisan berhenti berganti berkas begitu terisi + panjangBaris > batas — tiga baris pertama masuk app.log, dua sisanya membuka app.log.1. Kebijakan maxHistory tinggal menghapus arsip tertua.

Dengan log yang terstruktur, kita punya jejak apa yang terjadi. Tetapi ada pertanyaan yang lebih mendesak bagi orkestrator dan load balancer: apakah aplikasi ini masih sehat saat ini? Pertanyaan itu butuh jawaban mesin yang bisa dibaca — health check, topik chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

LogTerstruktur.java

Unduh berkas
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

/** Logging mini: filter level, log terstruktur JSON dengan MDC, dan rotasi berkas. */
public class LogTerstruktur {

    enum Level { TRACE, DEBUG, INFO, WARN, ERROR }

    /** Satu peristiwa log: waktu beku, level, logger, pesan, dan konteks MDC. */
    record Peristiwa(LocalDateTime waktu, Level level, String logger,
                     String requestId, String pesan) {}

    static final Level AMBANG = Level.INFO;
    static final DateTimeFormatter FORMAT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

    /** MDC ala SLF4J: konteks per thread yang ikut setiap baris log. */
    static final ThreadLocal<String> MDC_REQUEST_ID = new ThreadLocal<>();

    /** Lolos saringan ambang: level >= AMBANG. */
    static boolean lolos(Level level) {
        return level.ordinal() >= AMBANG.ordinal();
    }

    /** Bentuk baris JSON — pengganti sederhana encoder JSON milik Logback. */
    static String json(Peristiwa p) {
        String pesanAman = p.pesan().replace("\\", "\\\\").replace("\"", "\\\"");
        String mdc = (p.requestId() == null) ? "{}" : "{\"requestId\":\"" + p.requestId() + "\"}";
        return "{\"ts\":\"" + FORMAT.format(p.waktu()) + "\",\"level\":\"" + p.level()
                + "\",\"logger\":\"" + p.logger() + "\",\"mdc\":" + mdc
                + ",\"msg\":\"" + pesanAman + "\"}";
    }

    public static void main(String[] args) {
        LocalDateTime dasar = LocalDateTime.of(2026, 1, 15, 8, 0, 0);
        List<Peristiwa> semuanya = List.of(
                new Peristiwa(dasar.plusSeconds(1), Level.DEBUG, "db.pool", null,
                        "koneksi dibuka"),
                new Peristiwa(dasar.plusSeconds(1), Level.INFO, "server", null,
                        "aplikasi menyala"),
                new Peristiwa(dasar.plusSeconds(2), Level.WARN, "db.pool", null,
                        "koneksi ulang setelah timeout"),
                new Peristiwa(dasar.plusSeconds(3), Level.ERROR, "api", "req-77",
                        "gagal memproses pesanan \"P-1042\""));

        System.out.println("== Saringan level (ambang " + AMBANG + ") ==");
        for (Peristiwa p : semuanya) {
            if (lolos(p.level())) {
                System.out.printf("[%s] %s - %s%n", String.format("%-5s", p.level()),
                        p.logger(), p.pesan());
            } else {
                System.out.printf("[%s] %s - %s  (DILEWATI: di bawah ambang)%n",
                        String.format("%-5s", p.level()), p.logger(), p.pesan());
            }
        }
        System.out.println();

        System.out.println("== Bentuk terstruktur (JSON) untuk agregator ==");
        for (Peristiwa p : semuanya) {
            if (lolos(p.level())) {
                System.out.println(json(p));
            }
        }
        System.out.println();

        System.out.println("== Rotasi berkas: batas 400 byte ==");
        List<String> baris = new ArrayList<>();
        for (Peristiwa p : semuanya) {
            if (lolos(p.level())) {
                baris.add(json(p));
            }
        }
        baris.addAll(List.of(
                "{\"ts\":\"2026-01-15T08:05:00\",\"level\":\"INFO\",\"logger\":\"api\",\"mdc\":{},\"msg\":\"GET /api/pesanan 200\"}",
                "{\"ts\":\"2026-01-15T08:05:01\",\"level\":\"INFO\",\"logger\":\"api\",\"mdc\":{},\"msg\":\"GET /api/pesanan 200\"}"));
        int batas = 400;
        int nomorBerkas = 0;
        long terisi = 0;
        int[] perBerkas = new int[3];
        for (String b : baris) {
            if (terisi + b.getBytes().length > batas && nomorBerkas < 2) {
                nomorBerkas++;
                terisi = 0;
            }
            terisi += b.getBytes().length + 1; // +1 untuk baris baru
            perBerkas[nomorBerkas]++;
        }
        String[] nama = {"app.log", "app.log.1", "app.log.2"};
        for (int i = 0; i <= nomorBerkas; i++) {
            System.out.printf("  %-12s %d baris%n", nama[i], perBerkas[i]);
        }
    }
}