Log yang ditulis tanpa disiplin cepat berubah menjadi lautan teks yang mustahil ditelusuri ketika insiden terjadi. Dua keputusan kecil pada saat menulis setiap baris menentukan mutu log tersebut: level yang menyatakan seberapa parah sebuah kejadian, dan context yang menyertakan data pendukung secara terstruktur. Chapter ini melanjutkan pembahasan structured logging dengan membahas taksonomi delapan level, pemfilteran level per lingkungan, context dinamis untuk identitas pengguna, prinsip satu kejadian satu baris log, hingga pemetaan exception ke level yang konsisten.
Taksonomi Level Log #
Delapan level pada tabel di bawah diadopsi dari standar syslog yang dirangkum RFC 5424, lalu dipopulerkan di ekosistem PHP oleh antarmuka PSR-3 dan library Monolog. Kedelapan level tersebut kini menjadi kosakata baku hampir semua library logging, sehingga tim operasional tidak perlu mempelajari konvensi baru setiap berganti proyek.
| Level | Makna | Contoh nyata |
|---|---|---|
emergency | sistem tidak dapat digunakan sama sekali | database utama tidak bisa dihubungi, seluruh layanan mati |
alert | butuh tindakan segera | disk server hampir penuh, antrian job berhenti diproses |
critical | kondisi kritis yang merusak fitur inti | bug logika terdeteksi di produksi, data pesanan korup |
error | satu operasi gagal, aplikasi tetap berjalan | email gagal terkirim, panggilan API gateway timeout |
warning | kejadian mencurigakan yang berpotensi memburuk | query 2,4 detik, penggunaan memori melewati ambang |
notice | kejadian normal namun penting untuk diaudit | pengguna mengganti kata sandi, deploy selesai |
info | jejak operasional rutin | pengguna login, halaman kategori dibuka |
debug | detail internal untuk pemeriksaan mendalam | isi parameter request, hasil parsing konfigurasi |
Aturan praktisnya sederhana: jika sebuah kejadian layak membangunkan manusia tengah malam, gunakan alert ke atas; jika kejadian itu baru berguna saat kita sedang menyelidiki sesuatu, itu wilayahnya debug. Level yang salah kaprah membuat alert berbunyi untuk hal sepele, atau insiden serius luput karena ditulis sebagai info.
Memfilter Level per Lingkungan #
Volume log yang wajar berbeda antar lingkungan: mesin development milik kita sendiri boleh penuh debug, sedangkan server production yang melayani jutaan request harus hemat. Kuncinya, level dapat dibandingkan sebagai angka severity: sebuah baris layak ditulis jika severity-nya minimal sama parah dengan batas lingkungan tersebut.
<?php
const BOBOT_LEVEL = [
'emergency' => 700,
'alert' => 600,
'critical' => 500,
'error' => 400,
'warning' => 300,
'notice' => 200,
'info' => 100,
'debug' => 0,
];
function bolehTulis(string $level, string $batasMinimum): bool
{
return BOBOT_LEVEL[strtolower($level)] >= BOBOT_LEVEL[strtolower($batasMinimum)];
}
$batasPerLingkungan = [
'development' => 'debug',
'staging' => 'info',
'production' => 'warning',
];
foreach ($batasPerLingkungan as $lingkungan => $batas) {
echo "$lingkungan (batas: $batas)\n";
foreach (['debug', 'info', 'warning', 'error'] as $level) {
$tanda = bolehTulis($level, $batas) ? 'ya' : 'tidak';
echo " $level: $tanda\n";
}
}Output yang Dihasilkan:
development (batas: debug)
debug: ya
info: ya
warning: ya
error: ya
staging (batas: info)
debug: tidak
info: ya
warning: ya
error: ya
production (batas: warning)
debug: tidak
info: tidak
warning: ya
error: yaMari kita telaah kode di atas:
- Konstanta
BOBOT_LEVELmemberi setiap level angka severity pada skala sederhana: makin parah kejadian, makin besar angkanya. - Fungsi
bolehTulis()mengembalikantruehanya jika bobot level minimal sama dengan bobot batas;strtolowermembuat pembandingan toleran terhadap kapitalisasi. - Perulangan terluar mencetak matriks tiga lingkungan, dan output memperlihatkan pola yang benar:
warningdanerrorselalu lolos di production,infobaru lolos mulai staging, sedangkandebughanya hidup di development.
Menyertakan Context Terstruktur #
Pesan log yang dibangun dengan interpolasi manual seperti "Pembayaran ORD-123 gagal" membuang struktur: order id menempel pada teks dan tidak bisa difilter tanpa regex. Pola yang lebih baik adalah menjaga pesan tetap stabil, lalu menaruh data pendukung sebagai kolom context dalam bentuk array yang diserialisasi menjadi JSON.
<?php
function tulisLog(string $level, string $pesan, array $context = []): string
{
$catatan = [
'waktu' => '2026-01-15T08:00:00+07:00',
'level' => $level,
'pesan' => $pesan,
] + $context;
return json_encode($catatan, JSON_UNESCAPED_UNICODE) . "\n";
}
echo tulisLog('info', 'Pengguna masuk');
echo tulisLog('error', 'Pembayaran gagal', [
'order_id' => 'ORD-2026-0115',
'gateway' => 'midtrans',
'amount' => 250000,
]);Output yang Dihasilkan:
{"waktu":"2026-01-15T08:00:00+07:00","level":"info","pesan":"Pengguna masuk"}
{"waktu":"2026-01-15T08:00:00+07:00","level":"error","pesan":"Pembayaran gagal","order_id":"ORD-2026-0115","gateway":"midtrans","amount":250000}Dua hal patut diperhatikan pada kode di atas. Pertama, operator union array (+) menjadikan kunci khusus waktu, level, dan pesan selalu menang, sehingga context dari pemanggil tidak bisa menimpa kolom dasar meski tidak sengaja mengirim kunci bernama sama. Kedua, baris kedua kini bisa dicari dengan select(.order_id == "ORD-2026-0115") di jq atau difilter di sistem log apa pun, karena order_id, gateway, dan amount berdiri sebagai kolom, bukan bagian dari kalimat.
Context Dinamis: Menyuntik Identitas Pengguna #
Data seperti user_id dan request_id diminta oleh hampir semua baris log dalam satu request. Menuliskannya manual pada setiap pemanggilan merepotkan dan mudah lupa, sehingga praktik umum adalah menyuntikkannya lewat context global yang digabungkan otomatis.
<?php
class ContextGlobal
{
/** @var array<string, mixed> */
private static array $context = [];
public static function atur(array $context): void
{
self::$context = $context;
}
public static function gabung(array $contextLokal = []): array
{
return $contextLokal + self::$context;
}
public static function reset(): void
{
self::$context = [];
}
}
ContextGlobal::atur(['user_id' => 42, 'plan' => 'pro']);
echo json_encode(ContextGlobal::gabung(['aksi' => 'checkout']), JSON_UNESCAPED_SLASHES), "\n";
ContextGlobal::atur(['user_id' => 7, 'plan' => 'gratis']);
echo json_encode(ContextGlobal::gabung(['aksi' => 'login']), JSON_UNESCAPED_SLASHES), "\n";
ContextGlobal::reset();
echo json_encode(ContextGlobal::gabung(['aksi' => 'logout']), JSON_UNESCAPED_SLASHES), "\n";Output yang Dihasilkan:
{"aksi":"checkout","user_id":42,"plan":"pro"}
{"aksi":"login","user_id":7,"plan":"gratis"}
{"aksi":"logout"}Method gabung() memakai union + lagi, sehingga context lokal di pemanggil selalu lebih prioritas daripada context global. Dalam PHP-FPM, state statis otomatis bersih pada akhir request karena seluruh request lifecycle dimulai ulang. Namun pada queue worker yang berumur panjang, state statis bertahan antar job, jadi panggil reset() di awal setiap job agar user_id job sebelumnya tidak bocor ke log job berikutnya.
Satu Kejadian, Satu Baris Log #
Anti-pattern yang sering muncul: sebuah exception ditangkap di layer bawah, dicatat sebagai error, lalu di-rethrow dan dicatat lagi di layer atas sebagai warning. Satu kejadian kini menghasilkan dua baris dengan level berbeda — alert rule terpicu ganda dan laporan insiden menghitung kejadian dua kali. Aturannya: catat sekali, di titik penanganan akhir; layer lain cukup melempar ulang.
<?php
class PencatatSekali
{
/** @var array<string, bool> */
private array $sudahDilaporkan = [];
public function __construct(private Closure $penulisLog)
{
}
public function laporkanSekali(string $kunciKejadian, string $pesan): void
{
if (isset($this->sudahDilaporkan[$kunciKejadian])) {
return;
}
$this->sudahDilaporkan[$kunciKejadian] = true;
($this->penulisLog)($pesan);
}
}
$log = [];
$pencatat = new PencatatSekali(
function (string $pesan) use (&$log): void {
$log[] = $pesan;
}
);
$pencatat->laporkanSekali('disk_hampir_penuh', 'Disk /var terpakai 92%');
$pencatat->laporkanSekali('disk_hampir_penuh', 'Disk /var terpakai 92%');
$pencatat->laporkanSekali('disk_hampir_penuh', 'Disk /var terpakai 92%');
$pencatat->laporkanSekali('antrian_email_penuh', 'Antrian email melebihi 1000 job');
echo implode("\n", $log), "\n";
echo 'Total baris log: ', count($log), "\n";Output yang Dihasilkan:
Disk /var terpakai 92%
Antrian email melebihi 1000 job
Total baris log: 2Kelas di atas menerima dependency Closure sebagai penulis log, sehingga demo memakai closure yang mengumpulkan baris ke array $log (perhatikan use (&$log) untuk melewati semantik salinan closure). Tiga panggilan dengan kunci disk_hampir_penuh hanya menghasilkan satu baris, sedangkan kunci kejadian yang berbeda tetap dilaporkan terpisah — total dua baris dari empat panggilan. Dalam aplikasi nyata, kunci seperti ini biasanya dipasang di middleware atau event listener untuk mencegah log banjir saat sebuah masalah berulang cepat.
Memetakan Exception ke Level #
Ketika exception mengalir ke handler global, level log yang dipilih sebaiknya tidak ditentukan oleh selera penulis kode saat itu. Pemetaan yang eksplisit menjaga alert rule tetap stabil: bug pemrograman mendapat perhatian berbeda dari gangguan lingkungan.
<?php
function levelUntukThrowable(Throwable $e): string
{
return match (true) {
$e instanceof LogicException => 'critical',
$e instanceof TypeError => 'critical',
$e instanceof RuntimeException => 'error',
default => 'warning',
};
}
$contoh = [
'invalid-argument' => new InvalidArgumentException('Parameter $limit harus integer positif'),
'runtime-db' => new RuntimeException('Koneksi database timeout setelah 5 detik'),
'type-error' => new TypeError('Argument #1 ($id) must be of type int, string given'),
'exception-umum' => new Exception('Metode pembayaran tidak didukung'),
];
foreach ($contoh as $nama => $e) {
echo str_pad($nama, 18), ' => ', levelUntukThrowable($e), "\n";
}Output yang Dihasilkan:
invalid-argument => critical
runtime-db => error
type-error => critical
exception-umum => warningPemetaan pada contoh ini mengikuti tiga asumsi yang perlu Anda sadari. LogicException dan TypeError menandakan bug kode — hal yang seharusnya tidak pernah terjadi di produksi — sehingga dinaikkan ke critical. RuntimeException dan turunannya (termasuk PDOException) berarti gangguan lingkungan eksternal yang layak error namun sering pulih sendiri. Sisa exception dianggap kejadian domain yang diharapkan dan cukup dicatat warning.
Ringkasan #
Level memberi log sistem prioritas yang bisa difilter per lingkungan, context memberi log kolom data yang bisa dicari dan diagregasi, dan disiplin satu-kejadian-satu-baris menjaga laporan tetap jujur. Pemetaan exception ke level menutup lingkaran dengan alert rule yang akan kita bangun di atasnya. Setelah log tertata, pertanyaan berikutnya adalah bagaimana konfigurasi error PHP sendiri diatur berbeda untuk development, staging, dan production — fokus chapter Error Reporting Configuration .