Breakpoint sangat kuat, tetapi hanya bekerja saat kita hadir di depan IDE. Produksi tidak menunggu: kegagalan terjadi pukul tiga pagi, pengguna melaporkan bug yang tidak bisa direproduksi, dan satu-satunya saksi mata adalah jejak yang ditinggalkan program sendiri. Logging adalah perekam penerbangan (flight recorder) aplikasi — catatan kejadian yang bertahan setelah layar bergulir dan proses berakhir. Chapter ini membangun logger berkas sederhana dari nol, memperkenalkan format JSON lines, membahas rotasi agar berkas tak membesar tanpa batas, lalu menutup dengan ekosistem Monolog serta daftar hal yang layak dan tidak layak dicatat.

Mengapa Aplikasi Perlu Logging #

Pesan echo mati bersama terminal yang menampilkannya. Ia tidak bisa dibaca ulang besok pagi, tidak bisa dicari, dan tidak bisa diurutkan menurut waktu. Log menyelesaikan tiga masalah itu sekaligus: ia ditulis ke media permanen (umumnya berkas), setiap baris berstempel waktu, dan seluruhnya bisa digrepl untuk menelusuri satu kejadian di antara jutaan baris lainnya.

Kebutuhan ini sesungguhnya sudah pernah kita sentuh. Chapter Logging Errors pada Section Error Handling membahas function error_log() beserta directive php.ini untuk mencatat kegagalan PHP. Rekap singkatnya: error_log($pesan, 3, $berkasLog) menambahkan satu baris ke berkas tujuan tanpa pernah menghapus isinya. Chapter kini melangkah lebih jauh — dari mencatat error saja menuju mencatat kejadian aplikasi secara keseluruhan, dengan format yang kita kendalikan sendiri.

Logger Berkas Sederhana #

Kita mulai dengan logger paling sederhana yang bisa dibayangkan: sebuah function yang menambahkan baris berstempel waktu ke sebuah berkas. Latihan berikut menulis ke /tmp dan membersihkan berkasnya di akhir agar bisa diulang tanpa sisa:

php
<?php
function catatLog(string $waktu, string $pesan, string $berkasLog): void
{
    $baris = "[$waktu] $pesan" . PHP_EOL;
    file_put_contents($berkasLog, $baris, FILE_APPEND | LOCK_EX);
}

$berkasLog = "/tmp/php-course-latihan.log";
if (file_exists($berkasLog)) {
    unlink($berkasLog); // mulai dari berkas kosong agar latihan bisa diulang
}

catatLog("2026-01-15 08:00:00", "Pengguna budi login berhasil", $berkasLog);
catatLog("2026-01-15 08:00:02", "Pesanan #1042 dibuat", $berkasLog);

echo file_get_contents($berkasLog);
unlink($berkasLog); // bereskan berkas latihan setelah selesai

Output yang Dihasilkan:

text
[2026-01-15 08:00:00] Pengguna budi login berhasil
[2026-01-15 08:00:02] Pesanan #1042 dibuat

Mari kita bedah elemen kuncinya:

  1. file_put_contents(..., FILE_APPEND | LOCK_EX): flag FILE_APPEND menambahkan baris di akhir berkas alih-alih menimpanya; LOCK_EX mengunci berkas selama penulisan agar dua proses yang mencatat bersamaan tidak saling menimpa baris.
  2. PHP_EOL: penanda akhir baris yang portabel — \n di Linux dan macOS.
  3. Parameter $waktu dibekukan menjadi string tetap agar output latihan ini konsisten antar eksekusi. Di aplikasi nyata, gunakan waktu nyata, misalnya (new DateTimeImmutable())->format("Y-m-d H:i:s").
  4. file_get_contents($berkasLog) membaca kembali seluruh isi berkas — bukti bahwa log benar-benar persisten, terpisah dari proses yang menulisnya.

Format [waktu] pesan ini mudah dibaca manusia, tetapi rapuh: begitu pesan mengandung tanda ] atau formatnya berubah, parsing menjadi tebak-tebakan. Kita butuh format yang lebih disiplin.

Format Baris JSON #

Daripada berharap pesan cocok dengan pola tertentu, kita jadikan setiap baris log sebuah dokumen JSON yang membatasi dirinya sendiri. Satu baris = satu objek JSON — konvensi ini dikenal sebagai JSON lines. Function berikut membangun baris dengan json_encode():

php
<?php
function catatJson(string $waktu, string $level, string $pesan, string $berkasLog): void
{
    $baris = [
        "waktu" => $waktu,
        "level" => $level,
        "pesan" => $pesan,
    ];
    file_put_contents(
        $berkasLog,
        json_encode($baris, JSON_UNESCAPED_UNICODE) . PHP_EOL,
        FILE_APPEND | LOCK_EX
    );
}

$berkasLog = "/tmp/php-course-latihan-json.log";
if (file_exists($berkasLog)) {
    unlink($berkasLog);
}

catatJson("2026-01-15 08:00:00", "info", "Pengguna budi login berhasil", $berkasLog);
catatJson("2026-01-15 08:00:05", "error", "Koneksi database gagal", $berkasLog);

echo file_get_contents($berkasLog);
unlink($berkasLog);

Output yang Dihasilkan:

text
{"waktu":"2026-01-15 08:00:00","level":"info","pesan":"Pengguna budi login berhasil"}
{"waktu":"2026-01-15 08:00:05","level":"error","pesan":"Koneksi database gagal"}

Setiap baris kini memiliki field bernama — waktu, level, pesan — dan nilai yang mengandung tanda kutip maupun karakter aneh otomatis di-escape oleh json_encode(). Flag JSON_UNESCAPED_UNICODE menjaga teks berbahasa Indonesia tetap terbaca sebagai karakter aslinya. Field level yang baru kita perkenalkan (info, error) adalah bibit dari sistem level yang akan dibedah tuntas pada chapter Log Levels dan Context.

Rotasi Log agar Berkas Terkendali #

Logger di atas punya kelemahan struktural: berkasnya tumbuh selamanya. Aplikasi sibuk mudah menghasilkan gigabyte log per hari sampai partisi disk penuh dan aplikasi ikut tumbang. Solusinya adalah rotasi: ketika berkas melewati batas ukuran atau usia, berkas lama diarsipkan dan penulisan dimulai dari berkas baru. Kerangka size-based yang paling sederhana:

php
<?php
function rotasiJikaBesar(string $berkasLog, int $batasByte): bool
{
    if (!file_exists($berkasLog) || filesize($berkasLog) < $batasByte) {
        return false;
    }

    rename($berkasLog, $berkasLog . ".1"); // arsipkan berkas lama, mulai berkas baru
    return true;
}

Function ini memeriksa ukuran berkas dengan filesize(); bila melewati $batasByte, rename() memindahkan berkas aktif menjadi app.log.1 sehingga penulisan berikutnya otomatis membuat app.log yang baru. Versi produksi biasanya menjaga beberapa generasi (app.log.1, app.log.2, …) dan mengompres arsipnya. Di server Linux, tool logrotate mengurus pola ini lewat konfigurasi terpusat; library Monolog juga menyediakan RotatingFileHandler yang membagi berkas per tanggal.

Monolog dan Ekosistem Logger #

Untuk aplikasi nyata, kita tidak perlu membangun semuanya sendiri. Monolog adalah library logging standar de facto di ekosistem PHP — Laravel dan Symfony sama-sama memakainya. Pasang lewat Composer:

bash
composer require monolog/monolog

Kekuatan Monolog terletak pada arsitektur tiga lapis: channel memberi nama aliran log (misalnya "toko" atau "pembayaran"), handler menentukan tujuan tulisnya (berkas, syslog, Slack, email), dan formatter membentuk tampilan tiap baris. Satu channel bisa mendorong pesan yang sama ke beberapa handler sekaligus — misalnya level error dikirim ke Slack agar tim terbangun, sementara seluruh level tetap mengalir ke berkas:

php
<?php
// Membutuhkan: composer require monolog/monolog
require "vendor/autoload.php";

use Monolog\Level;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger("toko");
$log->pushHandler(new StreamHandler("/tmp/toko-app.log", Level::Warning));

$log->info("Pengguna budi login");
$log->error("Pembayaran gagal", ["order_id" => 1042]);

Blok di atas tidak dijalankan pada lingkungan buku ini karena membutuhkan package Monolog yang diunduh Composer. Perhatikan dua hal dari contohnya: new Logger("toko") membuat channel bernama toko, dan array kedua pada $log->error() adalah context — pasangan kunci-nilai tambahan yang menempel pada pesan. Panggilan $log->info() tidak akan tertulis karena handler dibatasi level Level::Warning ke atas — sekilas tentang kekuatan level yang akan dibedah di chapter berikutnya.

Apa yang Perlu Dicatat #

Logging yang berguna selektif. Mencatat segalanya membuat berkas log menjadi lautan noise yang mustahil ditelusuri, sementara mencatat terlalu sedikit meninggalkan lubang di kronologi. Empat kategori berikut adalah fondasinya:

  • Error dan exception — lengkap dengan class, pesan, dan lokasi kejadiannya.
  • Kejadian autentikasi — login berhasil, login gagal, logout, perubahan password.
  • Transaksi bisnis — pesanan dibuat, pembayaran diterima, refund diproses.
  • Kinerja — query yang lambat, durasi pemrosesan, ukuran antrian.

Ujian sederhananya: dari log saja, bisakah kita merekonstruksi apa yang dialami satu pengguna pada satu sore tertentu? Bila ya, cakupan logging sudah memadai.

Ringkasan #

Logging mengubah debugging dari seni mengingat menjadi seni membaca kronologi. Logger berkas sederhana cukup dibangun dari file_put_contents() dengan FILE_APPEND dan LOCK_EX, lalu naik kelas ke format JSON lines yang disiplin dan tahan terhadap isi pesan yang aneh. Rotasi menjaga ukuran berkas tetap terkendali, Monolog menyediakan arsitektur channel-handler-formatter yang teruji, dan daftar hal yang layak dicatat menjaga log tetap berisi sinyal, bukan noise. Format JSON lines yang kita bangun di sini, bagaimanapun, baru memuat tiga field — chapter berikutnya memperluasnya menjadi logging terstruktur penuh dengan konteks, konvensi penamaan, dan correlation ID.

Referensi #