Error yang terjadi di server produksi hanya dapat diperbaiki apabila jejaknya sampai ke pengembang. Topik ini membahas siklus pelaporan exception pada Laravel, pelaporan bawaan ke berkas log, kustomisasi exception handler pada bootstrap/app.php, serta penerusan exception ke layanan pelaporan eksternal. Setelah topik ini dibaca, setiap error aplikasi catatan memiliki jalur yang pasti dari penyebabnya di kode hingga entri yang dapat dicari di log atau di layanan eksternal.
Siklus report dan render#
Setiap exception yang tidak ditangkap kode aplikasi melewati dua tahap sebelum permintaan selesai. Tahap pertama adalah pelaporan (report), yaitu pencatatan kejadian ke log atau layanan terkait, sedangkan tahap kedua adalah perenderan (render), yaitu penyusunan respons HTTP yang dikirim ke peminta. Kedua tahap itu dijalankan exception handler, yaitu objek yang dikonfigurasi melalui callback withExceptions() pada berkas bootstrap/app.php. Jalur yang sama digunakan permintaan web maupun perintah artisan, sehingga error pada job dan tugas terjadwal tercatat di tempat yang sama. Diagram berikut merangkum alurnya.
try-catch juga dapat dilaporkan secara manual melalui helper report($e), yang meneruskan exception tersebut ke handler yang sama tanpa menghentikan alur program.
Pelaporan bawaan ke berkas log#
Pelaporan bawaan menulis setiap exception pada kanal log aktif dengan level error, lengkap dengan konteks berupa jejak berkas dan baris sumbernya. Pencatatan itu berlaku untuk exception yang naik sampai ke handler maupun yang diteruskan manual melalui report(), sehingga satu jalur pencatatan mencakup keduanya. Jenis exception yang lazim digunakan sebagai alur kendali tidak dicatat, karena daftar bawaannya mencakup ValidationException, AuthenticationException, HttpException, dan TokenMismatchException yang memang diterjemahkan menjadi respons tertentu. Perilaku tersebut dapat dibuktikan dengan melempar exception pada tinker lalu melaporkannya melalui report().
# melaporkan exception dan membaca jejaknya di laravel.log
cd ~/aplikasi-catatan
php artisan tinker --execute='
try {
throw new RuntimeException("Uji error: tautan arsip tidak ditemukan");
} catch (RuntimeException $e) {
report($e);
echo "report() selesai dipanggil." . PHP_EOL;
}
'
# menampilkan bagian pesan dari entri uji terakhir
grep "Uji error: tautan arsip" storage/logs/laravel.log | tail -n 1 | sed -E "s/^\[[^]]*\] //" | cut -d"{" -f1 | sed -E "s/[[:space:]]+$//"report() selesai dipanggil.
local.ERROR: Uji error: tautan arsip tidak ditemukanEntri uji tersebut muncul dengan level ERROR, dan pesannya persis seperti argumen exception yang dilempar. Baris yang ditampilkan hanya berisi bagian pesan, sedangkan entri lengkap pada berkas masih memuat konteks "exception" yang menyebut kelas, berkas, dan baris tempat exception dilempar. Karena laporan ditulis melalui kanal log aktif, pengaturan kanal pada konfigurasi logging berlaku pula untuk laporan error, termasuk rotasi harian. Dari entri lengkap itulah nomor baris menjadi titik awal penyelidikan ketika pesan error dilaporkan oleh pengguna.
Kustomisasi pelaporan pada bootstrap/app.php#
Kustomisasi pelaporan dilakukan pada callback withExceptions() di bootstrap/app.php. Method $exceptions->reportable() menambahkan callback yang dijalankan sebelum pelaporan bawaan, dengan tipe parameter callback menentukan jenis exception yang ditanganinya. Callback yang mengembalikan false menghentikan pelaporan bawaan, sedangkan callback tanpa nilai kembalian membiarkannya berjalan.
// tidak-dijalankan
use Illuminate\Support\Facades\Log;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->reportable(function (RuntimeException $e): bool {
Log::warning("Ekspor catatan gagal", ["pesan" => $e->getMessage()]);
return false;
});
})Mekanisme callback tersebut dapat dibuktikan tanpa menyunting berkas apa pun, karena reportable() juga dapat dipanggil pada handler yang sudah aktif, dan report() menggunakan handler tersebut. Pada blok berikut, callback kustom mencatat pelaporannya dengan level warning lalu mengembalikan false.
# menambahkan callback pelaporan kustom lalu melaporkan exception
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Support\Facades\Log;
$handler = app(ExceptionHandler::class);
$handler->reportable(function (RuntimeException $e): bool {
Log::warning("Uji error: laporan kustom", ["pesan" => $e->getMessage()]);
return false;
});
try {
throw new RuntimeException("Uji error: tautan arsip rusak");
} catch (RuntimeException $e) {
report($e);
echo "report() selesai dipanggil." . PHP_EOL;
}
'
# menampilkan entri kustom tanpa stempel waktunya
grep "Uji error: laporan kustom" storage/logs/laravel.log | tail -n 1 | sed -E "s/^\[[^]]*\] //" | sed -E "s/[[:space:]]+$//"report() selesai dipanggil.
local.WARNING: Uji error: laporan kustom {"pesan":"Uji error: tautan arsip rusak"}Entri yang muncul adalah entri kustom dengan level WARNING, dan tidak ada baris local.ERROR untuk pesan yang sama karena pelaporan bawaannya sudah dihentikan oleh return false. Dua pengaturan lain melengkapi method tersebut, yaitu $exceptions->level(RuntimeException::class, "warning") yang menurunkan level pencatatan suatu jenis exception, dan $exceptions->dontReport([RuntimeException::class]) yang menghapus jenis tersebut dari pelaporan sama sekali. Pengabaian semacam itu sudah berlaku pada daftar bawaan, sebagaimana ValidationException pada blok berikut yang dilaporkan tanpa meninggalkan jejak apa pun di log.
# melaporkan ValidationException yang tidak masuk log
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Validation\ValidationException;
report(ValidationException::withMessages(["judul" => ["Judul wajib diisi"]]));
echo "report() selesai dipanggil." . PHP_EOL;
'
# menghitung jejak pesannya pada log, hasilnya nol
grep -c "Judul wajib diisi" storage/logs/laravel.log || truereport() selesai dipanggil.
0Pelaporan ke layanan eksternal#
Berkas log menyimpan kejadian di satu server, sedangkan layanan pelaporan error seperti Sentry dan Flare mengumpulkan kejadian dari banyak server, mengelompokkannya menurut jejak yang sama, dan mengirim peringatan ketika error baru muncul. Kelompok yang terbentuk umumnya memuat hitungan kejadian, jejak lengkapnya, dan waktu kemunculan pertama, sehingga error yang berulang ribuan kali tetap terbaca sebagai satu masalah. Penerusan ke layanan tersebut dilakukan melalui SDK resminya, yang mendaftarkan integrasi pelaporannya sendiri saat dipasang. Layanan lain dapat dihubungkan dengan pola yang sama, karena antarmukanya berupa callback pelaporan biasa.
# tidak-dijalankan
# menginstal SDK Sentry untuk Laravel
composer require sentry/sentry-laravel
php artisan sentry:publish --dsn="https://[email protected]/0"Apabila integrasi disusun sendiri, penerusannya menggunakan callback reportable() yang memanggil pengirim layanan tersebut. Callback tanpa nilai kembalian dibiarkan berlanjut ke pelaporan bawaan, sehingga entri log lokal tetap tercatat sementara salinannya dikirim keluar.
// tidak-dijalankan
use App\Support\PengirimError;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->reportable(function (Throwable $e): void {
PengirimError::kirim($e);
});
})Kelas PengirimError pada contoh adalah pembungkus untuk SDK pilihan, dan callback sejenis lazimnya diberi syarat lingkungan, misalnya hanya berjalan ketika app()->environment("production") bernilai true, agar data uji pengembangan tidak memenuhi kuota layanannya. Callback yang sama juga menjadi tempat menyaring data sebelum dikirim, misalnya memangkas isi variabel yang bersifat rahasia dari jejak exception.
Kesalahan umum#
Kesalahan pertama adalah menangkap exception dengan try-catch yang bloknya kosong. Alurnya berjalan kembali seolah tidak terjadi apa pun, tetapi jejak penyebabnya hilang dan masalah baru muncul di tempat lain tanpa petunjuk. Apabila exception memang harus ditangkap agar alur berlanjut, panggil report($e) di dalam blok catch sehingga kejadian itu tetap tercatat.
Kesalahan kedua adalah mendaftarkan terlalu banyak jenis exception pada dontReport(). Setiap jenis yang diabaikan membuat error nyata menjadi senyap, dan log yang bersih bukan tujuan akhir, melainkan efek samping yang berisiko. Abaikan hanya jenis yang memang diterjemahkan menjadi respons kendali, misalnya jenis yang tidak pernah sampai ke pengguna sebagai kegagalan, dan biarkan sisanya tercatat.
Kesalahan ketiga adalah meneruskan setiap exception ke layanan eksternal tanpa memandang lingkungannya. Uji otomatis dan percobaan pengembangan menghasilkan banyak exception yang sengaja dilempar, sehingga kuota layanan terkuras dan error yang benar-benar penting sulit ditemukan di antaranya. Syaratkan penerusannya pada lingkungan produksi, atau gunakan reportable() yang memeriksa lingkungan sebelum mengirim.
Ringkasan#
- Exception melewati tahap pelaporan terlebih dahulu lalu perenderan, dan keduanya dikustomisasi melalui
withExceptions()padabootstrap/app.php. - Pelaporan bawaan menulis exception dengan level
errorbeserta jejaknya ke kanal log aktif, kecuali jenis yang diabaikan bawaan sepertiValidationException. reportable()menambahkan callback sebelum pelaporan bawaan denganreturn falsesebagai penghentinya, sedangkanlevel()menurunkan level dandontReport()menghapus jenis exception dari pelaporan.- Layanan eksternal seperti Sentry dan Flare menerima exception melalui SDK resminya atau callback
reportable(), sementara pencatatan log lokal tetap berjalan.
Referensi#
- Errors — Laravel
— Dokumentasi siklus pelaporan exception,
reportable(),level(), dandontReport(). - Logging — Laravel — Dokumentasi kanal log tempat laporan exception bawaan ditulis.
- Sentry for Laravel — Sentry
— Panduan pemasangan SDK Sentry, termasuk perintah
sentry:publish.