Chapter Error Reporting
pada section sebelumnya telah membahas directive dasar seperti error_reporting() dan display_errors dari sisi perilaku runtime. Bab ini melihat konfigurasi yang sama dari sudut pandang observabilitas: bagaimana tiga lingkungan kerja mendapat nilai yang berbeda, bagaimana error yang tidak tertangani tetap meninggalkan catatan terstruktur di berkas log, bagaimana kegagalan fungsional dipisahkan dari masalah performa, serta bagaimana log yang terlalu berisik dijinakkan agar aturan peringatan tetap bisa dipercaya.
Matriks Konfigurasi per Lingkungan #
Tujuan tiap lingkungan berbeda, jadi konfigurasinya juga tidak boleh seragam. Development membutuhkan error tampil langsung di layar agar cepat diperbaiki; staging dan production mematikan tampilan namun mengaktifkan pencatatan ke berkas. Matriks di bawah adalah titik awal yang bisa Anda sesuaikan:
| Directive | development | staging | production |
|---|---|---|---|
display_errors | On | Off | Off |
log_errors | On | On | On |
error_reporting | E_ALL | E_ALL | E_ALL & ~E_DEPRECATED |
error_log | php://stderr | /var/log/app/staging-error.log | /var/log/app/prod-error.log |
Matriks seperti ini paling praktik disimpan sebagai satu fungsi agar bisa diuji, dibandingkan, dan menghasilkan berkas konfigurasi secara otomatis:
<?php
function konfigurasiError(string $lingkungan): array
{
return match ($lingkungan) {
'development' => [
'display_errors' => 'On',
'log_errors' => 'On',
'error_reporting' => 'E_ALL',
'error_log' => 'php://stderr',
],
'staging' => [
'display_errors' => 'Off',
'log_errors' => 'On',
'error_reporting' => 'E_ALL',
'error_log' => '/var/log/app/staging-error.log',
],
'production' => [
'display_errors' => 'Off',
'log_errors' => 'On',
'error_reporting' => 'E_ALL & ~E_DEPRECATED',
'error_log' => '/var/log/app/prod-error.log',
],
default => throw new InvalidArgumentException(
"Lingkungan tidak dikenal: $lingkungan"
),
};
}
foreach (['development', 'staging', 'production'] as $lingkungan) {
$konfigurasi = konfigurasiError($lingkungan);
echo strtoupper($lingkungan), "\n";
foreach ($konfigurasi as $kunci => $nilai) {
echo ' ', str_pad($kunci, 16), '= ', $nilai, "\n";
}
}Output yang Dihasilkan:
DEVELOPMENT
display_errors = On
log_errors = On
error_reporting = E_ALL
error_log = php://stderr
STAGING
display_errors = Off
log_errors = On
error_reporting = E_ALL
error_log = /var/log/app/staging-error.log
PRODUCTION
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED
error_log = /var/log/app/prod-error.logPerhatikan tiga keputusan pada matriks tersebut. Pertama, E_ALL di staging sengaja sama verbose dengan development karena staging adalah tempat terakhir menangkap peringatan sebelum rilis. Kedua, production menyingkirkan E_DEPRECATED untuk fokus pada error aktual — banyak tim justru mempertahankannya sebagai data perencanaan upgrade, jadi pahami bahwa ini pilihan, bukan kewajiban. Ketiga, cabang default yang melempar exception membuat lingkungan baru yang belum terdaftar langsung gagal keras alih-alih diam-diam memakai konfigurasi kosong.
Mencatat Error Fatal yang Tidak Tertangani #
Dengan display_errors dimatikan, exception yang lolos dari semua blok catch hanya meninggalkan respons 500 kosong bagi pengguna — dan tidak ada apa-apanya bagi kita, kecuali mendaftarkan handler khusus. Fungsi set_exception_handler() memberi kita satu titik akhir untuk mengubah error fatal menjadi catatan terstruktur:
<?php
$jalurLog = sys_get_temp_dir() . '/kursus-php-error-demo.log';
@unlink($jalurLog);
function catatErrorTerstruktur(Throwable $e, string $waktu, string $jalurLog): void
{
$catatan = [
'waktu' => $waktu,
'jenis' => $e::class,
'pesan' => $e->getMessage(),
'lokasi' => basename($e->getFile()) . ':' . $e->getLine(),
'trace_id' => 'belum-ada',
];
file_put_contents($jalurLog, json_encode($catatan, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);
}
$handlerException = function (Throwable $e) use ($jalurLog): void {
catatErrorTerstruktur($e, '2026-01-15T08:05:00+07:00', $jalurLog);
};
set_exception_handler($handlerException);
$handlerException(new RuntimeException('Gateway pembayaran tidak merespons'));
echo file_get_contents($jalurLog);
unlink($jalurLog);Output yang Dihasilkan:
{"waktu":"2026-01-15T08:05:00+07:00","jenis":"RuntimeException","pesan":"Gateway pembayaran tidak merespons","lokasi":".verify_tmp.php:23","trace_id":"belum-ada"}Handler di atas merakit catatan JSON berisi jenis exception, pesan, dan lokasi pelemparan, lalu menambahkannya ke berkas log dengan FILE_APPEND. Pada demo, handler dipanggil secara langsung agar hasilnya deterministik; pada aplikasi nyata, baris throw new RuntimeException(...) yang tidak tertangkap apa pun yang memicu handler secara otomatis, dan handler sungguhan biasanya diakhiri exit(1) setelah mencatat agar eksekusi berhenti bersih. Kolom trace_id kita isi sementara belum-ada — nilainya akan disuntik dari context tracing pada chapter Tracing
. Satu catatan kecil: kolom lokasi pada output menampilkan .verify_tmp.php karena contoh ini dieksekusi oleh perangkat verifikasi materi sebagai berkas sementara; di aplikasi Anda, nama berkas dan nomor baris mengacu pada kode Anda sendiri.
Menangkap Warning dan Notice menjadi Log Terstruktur #
Warning, notice, dan deprecation tidak menghentikan request, tetapi justru karena itu sering tidak terlihat siapa pun. set_error_handler() mengalihkan error tersebut ke callback kita sehingga semuanya bisa berubah menjadi catatan berlevel jelas:
<?php
$jalurLog = sys_get_temp_dir() . '/kursus-php-warning-demo.log';
@unlink($jalurLog);
$handlerError = function (int $nomor, string $pesan, ?string $berkas, ?int $baris) use ($jalurLog): bool {
$level = match ($nomor) {
E_WARNING, E_CORE_WARNING, E_USER_WARNING => 'warning',
E_NOTICE, E_USER_NOTICE, E_DEPRECATED => 'notice',
default => 'error',
};
$catatan = [
'waktu' => '2026-01-15T08:06:30+07:00',
'level' => $level,
'pesan' => $pesan,
'sumber' => basename($berkas ?? 'tidak-diketahui') . ':' . ($baris ?? 0),
];
file_put_contents($jalurLog, json_encode($catatan, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);
return true;
};
set_error_handler($handlerError);
echo "Mulai demo\n";
echo $skorBelumDiatur;
echo "Selesai demo\n";
restore_error_handler();
echo file_get_contents($jalurLog);
unlink($jalurLog);Output yang Dihasilkan:
Mulai demo
Selesai demo
{"waktu":"2026-01-15T08:06:30+07:00","level":"warning","pesan":"Undefined variable $skorBelumDiatur","sumber":".verify_tmp.php:25"}Callback menerima nomor error, pesan, berkas, dan baris, lalu menerjemahkan nomor tersebut menjadi level log melalui match — warning keluarga E_WARNING, sementara E_NOTICE dan E_DEPRECATED dianggap notice. Nilai kembali true memberi tahu PHP bahwa error sudah sepenuhnya tertangani, sehingga PHP tidak mencetak atau mencatatnya untuk kedua kalinya; itulah mengapa baris echo $skorBelumDiatur; tidak menampilkan peringatan apa pun di layar dan hanya muncul sebagai satu baris JSON di berkas log. Fungsi restore_error_handler() di akhir mengembalikan handler bawaan, kebiasaan yang penting bila kode ini berjalan sebagai bagian dari program yang lebih besar.
Membedakan Kegagalan Fungsional dan Masalah Performa #
Lonjakan error 5xx dan halaman yang makin lambat adalah dua keluhan berbeda yang butuh kanal diagnosis berbeda: yang pertama soal kegagalan fungsional, yang kedua soal performa. Mencampur keduanya dalam satu aliran log membuat tim sulit memilih siapa yang harus bereaksi. Klasifikasi sederhana berikut memisahkan keduanya sejak kejadian tercatat:
<?php
function klasifikasiKejadian(bool $berhasil, float $durasiDetik, float $batasDetik = 1.0): array
{
if (! $berhasil) {
return ['kanal' => 'errors', 'level' => 'error', 'alasan' => 'gagal secara fungsional'];
}
if ($durasiDetik > $batasDetik) {
return ['kanal' => 'performance', 'level' => 'warning', 'alasan' => 'berhasil tetapi lambat'];
}
return ['kanal' => '-', 'level' => 'debug', 'alasan' => 'sehat'];
}
$kejadian = [
['GET /api/products', true, 0.18],
['GET /api/orders', true, 2.40],
['POST /api/checkout', false, 0.02],
];
foreach ($kejadian as [$rute, $berhasil, $durasi]) {
$hasil = klasifikasiKejadian($berhasil, $durasi);
printf("%-20s kanal=%-12s level=%-8s %s\n", $rute, $hasil['kanal'], $hasil['level'], $hasil['alasan']);
}Output yang Dihasilkan:
GET /api/products kanal=- level=debug sehat
GET /api/orders kanal=performance level=warning berhasil tetapi lambat
POST /api/checkout kanal=errors level=error gagal secara fungsionalRequest yang gagal fungsional masuk kanal errors dengan level error berapa pun durasinya, sedangkan request yang berhasil namun melewati ambang satu detik masuk kanal performance dengan level warning. Request yang sehat tidak mencuat — kanalnya kosong dan levelnya debug — supaya kedua kanal diagnosis tetap bersinyal tinggi. Untuk sisi database, aktifkan slow query log dengan directive long_query_time sehingga query lambat juga punya kanalnya sendiri; cara membacanya kita bahas tuntas di chapter Production Diagnostics
.
Menjinakkan Log Berisik dengan Sampling #
Beberapa kejadian terjadi ribuan kali per menit: request rutin, health check, cache miss yang normal. Mencatat semuanya penuh membuat berkas log bengkak dan biaya penyimpanan membengkak, padahal nilai diagnostiknya rendah. Solusinya adalah sampling deterministik “satu dari N” — setiap N kejadian, satu dicatat:
<?php
class SamplerBatas
{
private int $hitung = 0;
public function __construct(private int $setiap)
{
}
public function bolehCatat(): bool
{
$this->hitung++;
return $this->hitung % $this->setiap === 0;
}
}
$sampler = new SamplerBatas(10);
$tercatat = 0;
for ($i = 1; $i <= 100; $i++) {
if ($sampler->bolehCatat()) {
$tercatat++;
}
}
echo "Total kejadian: 100, yang tercatat: $tercatat\n";
$samplerPeringatan = new SamplerBatas(4);
$jejak = [];
for ($i = 1; $i <= 8; $i++) {
$jejak[] = $samplerPeringatan->bolehCatat() ? 'catat' : 'lewati';
}
echo 'Pola 8 kejadian (satu per 4): ', implode(' - ', $jejak), "\n";Output yang Dihasilkan:
Total kejadian: 100, yang tercatat: 10
Pola 8 kejadian (satu per 4): lewati - lewati - lewati - catat - lewati - lewati - lewati - catatProperti $hitung menaik pada setiap kejadian, dan operator modulo memilih hanya iterasi yang habis dibagi $setiap — dari 100 kejadian, tepat 10 baris log yang ditulis. Pendamping yang lazim: naikkan sebuah counter penuh setiap kali kejadian terjadi, lalu tulis log ringkas hanya pada kejadian yang terpilih, sehingga angka totalnya tetap akurat meski detailnya disampel.
Aturan Peringatan Berbasis Log #
Log yang tidak pernah dibaca siapa pun tidak menyelesaikan apa-apa; aturan peringatan (alert rule) adalah jembatan antara catatan dan tindakan. Konsepnya tigalapis: ekspresi yang dihitung dari log dalam jendela waktu, ambang yang harus terlampaui, dan durasi for yang menuntut kondisi bertahan sebelum alarm berbunyi agar lonjakan sesaat tidak membangunkan siapa pun:
groups:
- name: aturan-aplikasi
rules:
- alert: ErrorRateTinggi
expr: sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.01
for: 5m
labels:
severity: criticalLogika yang sama bisa dievaluasi di sisi PHP, misalnya untuk memeriksa statistik jendela terakhir sebelum memutuskan mengirim notifikasi:
<?php
function evaluasiAturan(array $statistik, array $aturan): array
{
$terpicu = [];
foreach ($aturan as $nama => [$metrik, $operator, $ambang]) {
$nilai = $statistik[$metrik];
$terpenuhi = match ($operator) {
'>' => $nilai > $ambang,
'>=' => $nilai >= $ambang,
default => false,
};
if ($terpenuhi) {
$terpicu[] = sprintf('%s (%s=%s)', $nama, $metrik, $nilai);
}
}
return $terpicu;
}
$statistikJendela = [
'error_rate_persen' => 4.2,
'latensi_p95_detik' => 2.8,
'antrian_menumpuk' => 1500,
];
$aturan = [
'ErrorRateTinggi' => ['error_rate_persen', '>', 1.0],
'LatensiP95Tinggi' => ['latensi_p95_detik', '>', 2.0],
'AntrianMenumpuk' => ['antrian_menumpuk', '>=', 1000],
];
$terpicu = evaluasiAturan($statistikJendela, $aturan);
if ($terpicu === []) {
echo "Semua aturan sehat\n";
} else {
echo "Aturan terpicu:\n";
foreach ($terpicu as $nama) {
echo " - $nama\n";
}
}Output yang Dihasilkan:
Aturan terpicu:
- ErrorRateTinggi (error_rate_persen=4.2)
- LatensiP95Tinggi (latensi_p95_detik=2.8)
- AntrianMenumpuk (antrian_menumpuk=1500)Setiap aturan didefinisikan sebagai pasangan metrik, operator, dan ambang, lalu evaluasiAturan() mengembalikan daftar aturan yang terpenuhi berikut nilai yang memicunya. Pada contoh, ketiga aturan terpicu sekaligus — pola yang khas saat satu akar masalah menjalar: gateway lambat menaikkan latensi, sebagian request gagal menaikkan error rate, dan job yang gagal menumpuk di antrian. Aturan yang baik saling melengkapi seperti ini, bukan berteriak serentak untuk dua puluh hal yang tidak berhubungan.
Ringkasan #
Matriks konfigurasi memastikan error tampil saat development dan tercatat saat production, dua handler global menjaring semua yang lolos dari catch, pemisahan kanal fungsional dan performa menjaga sinyal tetap jernih, sampling menjinakkan kebisingan, dan aturan berbasis log mengubah catatan menjadi alarm. Log telah menjawab pertanyaan “apa yang terjadi”; untuk pertanyaan “berapa banyak dan seberapa cepat”, kita berpindah ke chapter Application Metrics
.