Log menceritakan kejadian satu per satu, tetapi pertanyaan operasional paling sering berbentuk angka: berapa request per menit, berapa persen yang gagal, berapa lama yang terlama. Menjawabnya dengan memindai log berarti menghitung baris setiap kali ditanya — mahal dan lambat. Application metrics membalik pendekatannya: angka dihitung sekali saat kejadian terjadi, lalu disimpan sebagai deret kecil yang bisa dikueri dalam hitungan milidetik. Chapter ini membahas tiga jenis metrics, implementasi registry di dalam aplikasi, perekaman durasi, ekspor format Prometheus, hingga jebakan kardinalitas label.
Metrik vs Log #
Keduanya bukan pesaing melainkan pelengkap dengan biaya dan kekuatan berbeda. Tabel berikut merangkum kapan memilih yang mana:
| Aspek | Log | Metrics |
|---|---|---|
| Unit data | satu baris per kejadian | satu angka per selang waktu |
| Pertanyaan khas | apa yang terjadi pada order ORD-123? | berapa rasio error lima menit terakhir? |
| Volume | besar, retensi terbatas | kecil, retensi panjang |
| Biaya menjawab ulang | scan ulang seluruh log | kueri deret waktu yang sudah teragregasi |
| Kedalaman konteks | kaya (pesan, context, trace) | tipis (hanya nama, label, angka) |
Aturan praktisnya: log menjelaskan kejadian, metrics mengukur laju. Ketika dashboard memperlihatkan lonjakan, kita turun ke log — dan ke tracing — untuk mencari tahu mengapa.
Tiga Jenis Metrik Dasar #
Hampir semua kebutuhan tercakup oleh tiga bentuk dasar berikut:
| Jenis | Sifat nilai | Contoh pemakaian |
|---|---|---|
| counter | hanya naik sejak proses hidup | total request, total job diproses, total byte dikirim |
| gauge | nilai terkini yang bebas naik-turun | kedalaman antrian, koneksi database aktif, memori terpakai |
| histogram | distribusi nilai dalam bucket | latensi request, durasi query, ukuran payload |
Counter cocok untuk “berapa banyak totalnya”, gauge untuk “berapa sekarang”, dan histogram untuk “bagaimana sebarannya”. Rata-rata saja sering menipu — dua kelompok request, setengah sangat cepat dan setengah sangat lambat, bisa menghasilkan rata-rata yang tampak sehat — sehingga untuk durasi kita hampir selalu memilih histogram atau ringkasan persentil.
Membuat Counter Sederhana #
Registry di bawah menyimpan setiap seri dalam satu array asosiatif. Kunci seri adalah nama metrik berikut labelnya, sehingga http_requests_total dengan kode status berbeda menjadi seri berbeda:
<?php
class RegistriMetrik
{
/** @var array<string, float> */
private array $seri = [];
public function naikkan(string $nama, array $label = [], float $tambah = 1.0): void
{
$kunci = $this->kunciSeri($nama, $label);
$this->seri[$kunci] = ($this->seri[$kunci] ?? 0.0) + $tambah;
}
public function baca(string $nama, array $label = []): float
{
return $this->seri[$this->kunciSeri($nama, $label)] ?? 0.0;
}
/** @return array<string, float> */
public function snapshot(): array
{
return $this->seri;
}
private function kunciSeri(string $nama, array $label): string
{
if ($label === []) {
return $nama;
}
$pasangan = [];
foreach ($label as $kunci => $nilai) {
$pasangan[] = "$kunci=\"$nilai\"";
}
return $nama . '{' . implode(',', $pasangan) . '}';
}
}
$metrik = new RegistriMetrik();
$metrik->naikkan('http_requests_total', ['code' => '200', 'rute' => '/api/products']);
$metrik->naikkan('http_requests_total', ['code' => '200', 'rute' => '/api/products']);
$metrik->naikkan('http_requests_total', ['code' => '500', 'rute' => '/api/orders']);
$metrik->naikkan('http_requests_total', ['code' => '200', 'rute' => '/api/products']);
foreach ($metrik->snapshot() as $seri => $nilai) {
echo $seri, ' ', $nilai, "\n";
}Output yang Dihasilkan:
http_requests_total{code="200",rute="/api/products"} 3
http_requests_total{code="500",rute="/api/orders"} 1Method kunciSeri() merangkai label menjadi bentuk nama{label="nilai"} yang kebetulan sama dengan format Prometheus — pilihan sadar agar ekspor tinggal mencetak kunci apa adanya. Empat pemanggilan naikkan() hanya menghasilkan dua seri karena tiga request pada kombinasi label yang sama tergabung menjadi satu angka; inilah inti agregasi yang membuat metrics jauh lebih ringkas daripada log. Dalam aplikasi nyata, pemanggilan ini hidup di middleware yang membungkus setiap request.
Merekam Durasi Operasi #
Untuk durasi, PHP menyediakan hrtime(): satu panggilan sebelum operasi dan satu sesudahnya, selisihnya dalam nanodetik yang bebas dari jam sistem:
<?php
$mulai = hrtime(true);
usleep(150_000);
$selesai = hrtime(true);
$durasiNs = $selesai - $mulai;
$durasiMs = $durasiNs / 1_000_000;
$durasiDetik = $durasiNs / 1_000_000_000;Kelas berikut mengumpulkan pengamatan durasi lalu merangkumnya menjadi persentil. Data durasi sengaja dimasukkan sebagai nilai tetap agar hasilnya bisa diverifikasi — di aplikasi nyata, angka-angka ini datang dari pengukuran hrtime() di atas:
<?php
class PencatatDurasi
{
/** @var array<string, list<float>> */
private array $pengamatan = [];
public function catat(string $nama, float $durasiMs): void
{
$this->pengamatan[$nama][] = $durasiMs;
}
public function ringkasan(string $nama): array
{
$nilai = $this->pengamatan[$nama];
sort($nilai);
return [
'count' => count($nilai),
'min_ms' => $nilai[0],
'p50_ms' => $nilai[(int) floor((count($nilai) - 1) * 0.50)],
'p95_ms' => $nilai[(int) floor((count($nilai) - 1) * 0.95)],
'max_ms' => $nilai[count($nilai) - 1],
];
}
}
$durasi = new PencatatDurasi();
foreach ([120.0, 95.0, 210.0, 180.0, 310.0, 88.0, 150.0] as $ms) {
$durasi->catat('db_query_duration_ms', $ms);
}
foreach ($durasi->ringkasan('db_query_duration_ms') as $kunci => $nilai) {
echo str_pad($kunci, 8), $nilai, "\n";
}Output yang Dihasilkan:
count 7
min_ms 88
p50_ms 150
p95_ms 210
max_ms 310Setelah diurutkan, persentil dihitung dengan mengambil indeks proporsional: p50 pada setengah data, p95 pada 95% data, sehingga nilai p95_ms dibaca sebagai “95% query lebih cepat dari angka ini”. Dari tujuh pengamatan contoh, median 150 ms tampak sehat, tetapi p95 210 ms dan maksimum 310 ms memperlihatkan ekor lambat — persis alasan p95 menjadi angka favorit untuk perjanjian tingkat layanan. Histogram di sistem monitoring besar bekerja dengan cara serupa, hanya saja pengelompokan ke bucket dilakukan saat pencatatan agar memori tetap konstan.
Mengekspor ke Format Prometheus #
Prometheus adalah sistem monitoring berbasis pull: servernya datang menjemput angka dari endpoint aplikasi, biasanya /metrics. Format teksnya satu baris per seri dengan pola nama{label="nilai"} angka, dan setiap metrik lazim didahului baris metadata # HELP (deskripsi singkat) serta # TYPE (jenis metrik) yang menjelaskan cara membacanya:
<?php
function renderPrometheus(array $seri): string
{
$baris = [];
foreach ($seri as $kunci => $nilai) {
$baris[] = "$kunci $nilai";
}
return implode("\n", $baris) . "\n";
}
$registri = [
'http_requests_total{code="200",rute="/api/products"}' => 1204,
'http_requests_total{code="500",rute="/api/orders"}' => 3,
'antrian_kedalaman' => 42,
'build_info{versi="2.14.0"}' => 1,
];
echo renderPrometheus($registri);Output yang Dihasilkan:
http_requests_total{code="200",rute="/api/products"} 1204
http_requests_total{code="500",rute="/api/orders"} 3
antrian_kedalaman 42
build_info{versi="2.14.0"} 1Baris pertama dan kedua adalah counter dari registry sebelumnya, baris ketiga gauge kedalaman antrian, dan baris keempat trik yang lazim: metrik bernilai konstan 1 yang membawa informasi versi aplikasi sebagai label — cara murah menandai deret mana yang berasal dari versi kode tertentu. Fungsi renderPrometheus() mencetak kunci seri apa adanya karena kunciSeri() memang sudah membentuk sintaks labelnya; tinggal menambahkan baris # HELP dan # TYPE di depan tiap kelompok bila diperlukan.
Metrik Kunci Aplikasi: Metode RED #
Untuk layanan berbasis request, metode RED merangkum tiga angka yang menjawab hampir semua pertanyaan kesehatan: Rate (laju request), Errors (rasio gagal), Duration (latensi). Aplikasi dengan antrian background menambahkan satu lagi: kedalaman antrian (queue depth). Keempatnya bisa dihitung dari counter dan ringkasan latensi dalam satu jendela waktu:
<?php
function ringkasanRed(array $jendela): array
{
$total = array_sum($jendela['http_status']);
$gagal = 0;
foreach ($jendela['http_status'] as $kode => $jumlah) {
if ($kode >= 500) {
$gagal += $jumlah;
}
}
return [
'rate_per_menit' => (int) round($total / $jendela['durasi_menit']),
'error_rate_persen' => round($gagal / $total * 100, 2),
'latensi_p95_ms' => $jendela['latensi_p95_ms'],
];
}
$jendelaLimaMenit = [
'durasi_menit' => 5,
'http_status' => ['200' => 11850, '404' => 92, '500' => 58],
'latensi_p95_ms' => 245,
];
foreach (ringkasanRed($jendelaLimaMenit) as $metrik => $nilai) {
echo str_pad($metrik, 20), $nilai, "\n";
}Output yang Dihasilkan:
rate_per_menit 2400
error_rate_persen 0.48
latensi_p95_ms 245Counter status HTTP dibedakan menurut kodenya, lalu error rate hanya menghitung kode 5xx — keluarga 4xx adalah interaksi klien yang gagal, bukan kegagalan layanan kita. Dari 12.000 request dalam lima menit, laju 2.400 per menit dengan error rate 0,48% terbaca sehat; ambang “sehat” tentu bergantung perjanjian tingkat layanan masing-masing aplikasi. Perhatikan juga bahwa rate, error rate, dan latensi adalah tiga angka independen — layanan bisa cepat tetapi bocor error, atau bersih tetapi lambat — sehingga ketiganya selalu dilihat berdampingan.
Bahaya Kardinalitas Label #
Label sangat kuat: satu counter bisa dipotong menjadi ratusan sudut pandang. Tetapi setiap kombinasi nilai label adalah seri waktu tersendiri yang harus disimpan, diindeks, dan dikueri sistem monitoring — dan jumlah kombinasi tumbuh secara multiplikatif:
<?php
$kodeStatus = ['200', '201', '204', '301', '302', '400', '401', '403', '404', '422', '429', '500', '502', '503'];
$ruteApi = ['/api/products', '/api/orders', '/api/checkout', '/api/users', '/api/reports'];
$versi = ['v1', 'v2'];
$seriSaatIni = count($kodeStatus) * count($ruteApi) * count($versi);
echo 'Label saat ini : ', count($kodeStatus), ' kode x ', count($ruteApi), ' rute x ', count($versi), ' versi', "\n";
echo 'Seri time-series: ', $seriSaatIni, "\n";
$penggunaAktif = 10000;
$seriTambahUser = $seriSaatIni * $penggunaAktif;
echo "Dengan label user_id ($penggunaAktif pengguna aktif): $seriTambahUser seri\n";Output yang Dihasilkan:
Label saat ini : 14 kode x 5 rute x 2 versi
Seri time-series: 140
Dengan label user_id (10000 pengguna aktif): 1400000 seriEmpat belas kode status dikali lima rute dikali dua versi menghasilkan 140 seri — masih wajar. Menambahkan satu label user_id dengan sepuluh ribu pengguna aktif melipatgandakannya menjadi 1.400.000 seri, sesuatu yang akan membuat server monitoring tumbang jauh sebelum bermanfaat. Angka pada contoh bahkan optimistis karena hanya menghitung kombinasi yang benar-benar terjadi untuk pengguna aktif saat itu.
Ringkasan #
Metrics mengubah laju kejadian menjadi angka yang murah dikueri: counter menghitung, gauge memotret keadaan, histogram memetakan sebaran durasi, dan format ekspor Prometheus membuat semuanya bisa dijemput sistem monitoring. Metode RED memberi kerangka angka minimum yang harus ada di setiap layanan, sementara disiplin kardinalitas menjaga agar kekuatan label tidak berubah menjadi bencana penyimpanan. Angka-angka ini akan memberi tahu kita bahwa ada yang lambat; untuk mengetahui tepatnya di langkah mana request menghabiskan waktunya, kita bergerak ke chapter Tracing .