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:
| Level | Arti | Kapan dipakai |
|---|---|---|
TRACE | jejak paling halus | mengikuti alur method detail |
DEBUG | informasi diagnostik | variabel antara, keputusan cabang |
INFO | peristiwa bisnis normal | aplikasi menyala, pesanan dibuat |
WARN | kondisi mencurigakan | retry, fallback aktif, nilai tak terduga |
ERROR | kegagalan yang butuh perhatian | request 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:
<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:
== 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 barisMari kita bedah kerangkanya:
lolos(level): saringan ambang bekerja lewatordinal()enum —WARN.ordinal() >= INFO.ordinal()sehingga lolos,DEBUGtertinggal. Bandingkan urutan deklarasi enumLevel: urutan itu adalah skala prioritasnya.record Peristiwa(...): peristiwa log sebagai nilai immutable dengan waktu beku — pada pustaka nyata, timestamp diambil jam sistem saat peristiwa terjadi.json(...): encoder manual yang membungkus bidang-bidang ke JSON — perhatikanpesanAmanyang meng-escape kutip ganda dan backslash; barisERRORdi keluaran memperlihatkan hasilnya (\"P-1042\"). Baris yang rusak formatnya = baris yang hilang dari indeks agregator.- MDC: field
requestIdhanya terisi pada peristiwa ketiga. Pada Logback nyata, mekanisme ini di atasThreadLocal— filter servlet menaruhrequestIddi awal request, seluruh baris log di thread itu membawanya, dibersihkan di akhir. - Bagian rotasi: lima baris JSON dialirkan ke berkas dengan batas 400 byte. Penulisan berhenti berganti berkas begitu
terisi + panjangBaris > batas— tiga baris pertama masukapp.log, dua sisanya membukaapp.log.1. KebijakanmaxHistorytinggal 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 berkasimport 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]);
}
}
}