Membangun observability di FrankenPHP: access log Caddy, structured logging dari PHP dengan frankenphp_log(), log JSON yang siap di-ingest ke Datadog/Grafana Loki, serta metrik Prometheus untuk memantau thread, worker, dan kedalaman antrean request.

Setelah episode 11 mengurus konfigurasi, kini tiba saatnya memastikan aplikasi bisa diamati ketika berjalan. Produksi tanpa observability seperti terbang tanpa instrumen: kalian tahu ada masalah hanya dari keluhan pengguna. Episode ini membangun tiga pilar observability di FrankenPHP — log, structured logging, dan metrics — dengan integrasi yang sudah disediakan langsung oleh Caddy dan FrankenPHP.
Mengapa penting? Mode worker mengubah perilaku log PHP: error_log() klasik tidak lagi muncul per-request di file yang sama. FrankenPHP merutekan log PHP ke logger Caddy, dan menyediakan frankenphp_log() untuk log terstruktur — kalian perlu memahami keduanya agar log tidak hilang atau sulit ditelusuri.
Directive log mengaktifkan access log. Format default sudah cukup informatif — mengikuti Common Log Format Caddy:
localhost {
log
root * public/
php_server
}Untuk kontrol format, tentukan output dan format via blok log di global options:
{
log {
output file /var/log/frankenphp/access.log {
roll_size 100MiB
roll_keep 10
}
format json
}
}
localhost {
root * public/
php_server
}output file ... — tulis ke file dengan rotasi (roll_size, roll_keep).format json — setiap request menjadi satu baris JSON, siap di-ingest oleh aggregator (Loki, ELK, Datadog).Tip
Default Caddy menulis log ke stderr dalam format JSON ketika stdout bukan TTY (misal di Docker). Untuk log yang "enak dibaca" di terminal development, jalankan container dengan --tty.
frankenphp_log()Fungsi frankenphp_log() adalah jalan terbaik untuk log aplikasi: mendukung level dan context, dan menghasilkan JSON terstruktur karena dibangun di atas log/slog milik Go.
<?php
frankenphp_log("Request selesai diproses");
frankenphp_log(
"Memory usage tinggi",
FRANKENPHP_LOG_LEVEL_WARN,
[
'current_usage' => memory_get_usage(),
'peak_usage' => memory_get_peak_usage(),
],
);Level yang tersedia: FRANKENPHP_LOG_LEVEL_DEBUG (-4), INFO (0), WARN (4), ERROR (8). Output di sisi server berupa satu baris JSON:
{"level":"info","ts":1770000000,"logger":"frankenphp","msg":"Request selesai diproses"}
{"level":"warn","ts":1770000000,"logger":"frankenphp","msg":"Memory usage tinggi","current_usage":10485760,"peak_usage":12582912}Karena formatnya JSON, log bisa langsung difilter berdasarkan level dan di-query berdasarkan field context di Grafana Loki atau Datadog.
error_log() yang KompatibelUntuk kode lama atau library yang memakai error_log(), FrankenPHP merutekan pesan ke logger Caddy saat $message_type = 4 (SAPI):
<?php
error_log("Koneksi database gagal", 4);Pesan ini muncul di log Caddy sebagai teks tidak terstruktur — berguna untuk kompatibilitas, tetapi untuk produksi lebih baik pindah ke frankenphp_log() yang bisa difilter per level dan punya context.
FrankenPHP mengekspos metrik Prometheus-compatible begitu Caddy metrics diaktifkan. Aktifkan lewat global options:
{
servers {
metrics
}
}Lalu scrape dari admin endpoint:
curl -s http://localhost:2019/metrics | grep frankenphpMetrik yang tersedia untuk thread dan worker:
| Metrik | Arti |
|---|---|
frankenphp_total_threads | Jumlah total thread PHP |
frankenphp_busy_threads | Thread yang sedang memproses request |
frankenphp_queue_depth | Antrean request yang menunggu thread |
frankenphp_total_workers{worker="..."} | Total worker untuk sebuah worker script |
frankenphp_busy_workers{worker="..."} | Worker yang sedang sibuk |
frankenphp_worker_request_time | Waktu pemrosesan request per worker |
frankenphp_worker_request_count | Jumlah request diproses per worker |
frankenphp_worker_crashes | Berapa kali worker mati tidak wajar |
frankenphp_worker_restarts | Berapa kali worker di-restart sengaja |
frankenphp_worker_queue_depth | Antrean request per worker |
Metrik paling penting untuk dipantau: frankenphp_queue_depth (indikasi thread menipis) dan frankenphp_worker_crashes (worker tidak stabil).
Contoh job scrape di prometheus.yml:
scrape_configs:
- job_name: frankenphp
metrics_path: /metrics
static_configs:
- targets:
- frankenphp-server:2019Catatan: endpoint metrics berada di admin port Caddy (default 2019) — jangan expose port ini ke publik. Biarkan hanya bisa diakses dari jaringan monitoring.
Untuk yang ingin dashboard tanpa menyusun Prometheus + Grafana sendiri, Ember adalah tool yang dirancang khusus untuk FrankenPHP. Ia terhubung ke admin API Caddy, menampilkan TUI real-time di development, dan bisa dijalankan sebagai daemon dengan mode --expose untuk mengekspor metrik gabungan (RPS, persentil latensi, error rate) ke Prometheus.
emberEmber sangat membantu memvisualisasikan health worker saat melakukan load test (episode 17).
Note
Metrik, log, dan Ember adalah bagian dari satu cerita observability: gunakan log untuk mengapa, metrik untuk kapan dan seberapa, dan Ember untuk memandang semuanya sekilas. Kita gabungkan ketiganya pada uji beban di episode 17.
debug membanjiri disk dan memperlambat. Gunakan info/warn untuk produksi.frankenphp_log() tidak muncul: pastikan logging Caddy aktif (directive log atau default stderr).servers { metrics } dideklarasikan — ini bukan default.Pada episode 12 ini, kalian telah membangun observability untuk FrankenPHP.
Inti yang harus dibawa pulang:
log; format json siap untuk aggregator log.frankenphp_log() untuk structured logging dari PHP dengan level + context.error_log($msg, 4) tetap didukung untuk kompatibilitas.servers { metrics } lalu scrape :2019/metrics dengan Prometheus.queue_depth dan worker_crashes; jaga port admin tetap privat.Di episode 13 selanjutnya kita membahas tantangan klasik PHP di mode worker: session, persistence & state — kenapa session file tidak cocok, cara memindahkan session ke Redis, dan batasan enable_post_data_reading. Sampai jumpa di episode 13!